32 KiB
CLAUDE.md — dreport
Proje Özeti
dreport, kullanıcıların fatura, irsaliye, rapor gibi belge şablonlarını görsel bir drag & drop editör ile tasarlayıp, JSON veri ile birleştirerek PDF çıktı almasını sağlayan bir belge tasarım aracıdır.
Temel fark: Editördeki önizleme doğrudan Typst render çıktısıdır. Canvas üzerinde ayrı bir render engine çalışmaz — kullanıcı her zaman gerçek Typst çıktısını görür. Bu sayede "editörde gördüğüm ile PDF'te aldığım farklı" sorunu ortadan kalkar.
Teknoloji Kararları
| Katman | Teknoloji | Gerekçe |
|---|---|---|
| Frontend | Vue 3 (Composition API) + TypeScript | Kullanıcı tercihi |
| Editör Render | Typst WASM → SVG | Editör çıktısı = PDF çıktısı tutarlılığı |
| Typst WASM | @myriaddreamin/typst.ts |
Tarayıcıda Typst derleme; SVG çıktı üretimi |
| Etkileşim Katmanı | SVG overlay (Vue bileşenleri) | Typst SVG üzerine seçim, sürükleme, yeniden boyutlandırma |
| Backend | Rust + Axum | Typst crate'lerini doğrudan kullanabilme; performans |
| PDF Render | typst Rust crate (server-side) |
Nihai PDF üretimi sunucuda; font tutarlılığı garantisi |
| Veri Formatı | JSON (şablon tanımı + veri) | Evrensel, kolay serialize/deserialize |
| Paket Yönetimi | bun (frontend), cargo (backend) | — |
Mimari Genel Bakış
┌─────────────────────────────────────────────────────┐
│ VUE FRONTEND │
│ │
│ ┌───────────────┐ ┌──────────────────────────┐ │
│ │ Sol Panel │ │ Editör Canvas │ │
│ │ - Bileşenler │ │ │ │
│ │ - Schema Tree │ │ ┌────────────────────┐ │ │
│ │ - Özellikler │ │ │ Typst WASM → SVG │ │ │
│ │ │ │ │ (gerçek render) │ │ │
│ │ │ │ └────────────────────┘ │ │
│ │ │ │ ┌────────────────────┐ │ │
│ │ │ │ │ SVG Overlay (Vue) │ │ │
│ │ │ │ │ seçim / drag / resize│ │ │
│ │ │ │ └────────────────────┘ │ │
│ └───────────────┘ └──────────────────────────┘ │
│ │
│ Template JSON ←→ Typst Markup ←→ SVG Render │
└──────────────────────────┬──────────────────────────┘
│ POST /api/render
▼
┌─────────────────────────────────────────────────────┐
│ RUST BACKEND (Axum) │
│ │
│ Template JSON + Data JSON → Typst Markup → PDF │
│ (typst crate ile doğrudan derleme) │
└─────────────────────────────────────────────────────┘
Editör Render Stratejisi: Typst WASM Full Render
Temel Prensip
Editörde ayrı bir canvas render engine (fabric.js, konva.js vb.) KULLANILMAZ. Bunun yerine:
- Kullanıcının tasarımı bir Template JSON olarak tutulur.
- Template JSON'dan Typst markup üretilir (frontend'de, saf fonksiyon).
- Typst markup, typst.ts WASM modülü ile tarayıcıda derlenir → SVG çıktı üretilir.
- SVG, editör alanında gösterilir.
- SVG üzerine Vue bileşenleriyle bir interaction overlay yerleştirilir (seçim kutuları, drag handle'lar, resize köşeleri).
- Kullanıcı bir elemanı sürüklediğinde → Template JSON güncellenir → Typst yeniden derlenir → SVG güncellenir.
Performans Stratejisi
Typst incremental compilation destekler, ancak her fare hareketi için full render döngüsü ağır olabilir. Bu yüzden:
- Drag sırasında: Overlay katmanında sadece CSS transform ile görsel geri bildirim ver (hafif, anlık). Typst derleme YAPMA.
- Drag bittiğinde (mouseup/pointerup): Template JSON'ı güncelle, Typst derle, SVG'yi yenile.
- Debounce: Özellik panelinden yapılan değişikliklerde (font boyutu, renk vs.) 150-300ms debounce ile derle.
- Web Worker: Typst WASM derlemeyi bir Web Worker içinde çalıştır. Ana thread'i ASLA bloklamayacak.
typst.ts Entegrasyonu
// Temel kullanım şeması
import { $typst } from "@myriaddreamin/typst.ts/dist/esm/contrib/snippet.mjs";
// Worker içinde:
async function compile(typstMarkup: string): Promise<string> {
const svg = await $typst.svg({ mainContent: typstMarkup });
return svg;
}
- WASM modülleri:
typst-ts-web-compiler(~7.6 MB) +typst-ts-renderer(~350 KB) - Fontlar: Projeye gömülü font seti gerekecek (~4.4 MB). Başlangıçta Noto Sans / Inter gibi bir set yeterli.
- İlk yükleme ağır olabilir — lazy loading ve cache stratejisi gerekli (Service Worker ile WASM cache'leme).
Veri Modeli
Layout Sistemi: Container-Based
Eski model (her eleman absolute place()) yerine, CSS Flexbox mantığına benzeyen container-based layout kullanılır:
- Sayfa = kök container.
page.margins→ kök container'ınpadding'i olur. - Container'lar iç içe geçebilir.
direction: "row" | "column"ile yatay/dikey dizilim. - Elemanlar varsayılan olarak flow içindedir — otomatik pozisyonlanır.
- Opsiyonel absolute positioning: Kullanıcı isterse bir elemanı
position: "absolute"yapabilir. Bu durumda eleman parent container içinde absolute konumlanır (place()ile).
Bu sayede:
- Tablo satırları artarsa alttaki elemanlar otomatik kayar.
- Aynı satıra iki kolon koymak için iç içe container yeterlidir.
- Absolute mod ile serbest pozisyonlama da mümkündür.
Boyut Sistemi (SizeValue)
Her eleman ve container için width ve height şu tiplerden biri olabilir:
| Tip | Açıklama | Typst karşılığı |
|---|---|---|
fixed |
Sabit boyut (mm) | 80mm |
auto |
İçeriğe göre otomatik | auto |
fr |
Kalan alanı oransal doldur | 1fr, 2fr |
Ek olarak minWidth, maxWidth, minHeight, maxHeight (mm) desteklenir.
Template JSON (Şablon Tanımı)
{
"id": "tpl_fatura_001",
"name": "Standart Fatura",
"page": { "width": 210, "height": 297 },
"fonts": ["Noto Sans", "Noto Sans Mono"],
"root": {
"id": "root",
"type": "container",
"position": { "type": "flow" },
"size": { "width": { "type": "auto" }, "height": { "type": "auto" } },
"direction": "column",
"gap": 5,
"padding": { "top": 15, "right": 15, "bottom": 15, "left": 15 },
"align": "stretch",
"justify": "start",
"style": {},
"children": [
{
"id": "c_header",
"type": "container",
"position": { "type": "flow" },
"size": { "width": { "type": "fr", "value": 1 }, "height": { "type": "auto" } },
"direction": "row",
"gap": 5,
"padding": { "top": 0, "right": 0, "bottom": 0, "left": 0 },
"align": "start",
"justify": "start",
"style": {},
"children": [
{
"id": "el_firma",
"type": "text",
"position": { "type": "flow" },
"size": { "width": { "type": "fr", "value": 1 }, "height": { "type": "auto" } },
"style": { "fontSize": 14, "fontWeight": "bold" },
"binding": { "type": "scalar", "path": "firma.unvan" }
},
{
"id": "el_fatura_baslik",
"type": "static_text",
"position": { "type": "flow" },
"size": { "width": { "type": "auto" }, "height": { "type": "auto" } },
"style": { "fontSize": 12, "fontWeight": "bold", "align": "right" },
"content": "FATURA"
}
]
},
{
"id": "el_cizgi",
"type": "line",
"position": { "type": "flow" },
"size": { "width": { "type": "fr", "value": 1 }, "height": { "type": "auto" } },
"style": { "strokeColor": "#000000", "strokeWidth": 0.5 }
}
]
}
}
Eleman Tipleri
| Tip | Açıklama | Binding |
|---|---|---|
container |
Düzen kutusu, çocuk elemanları barındırır | Yok |
static_text |
Sabit metin, veri bağlantısı yok | Yok |
text |
Dinamik metin, schema'dan veri çeker | Scalar |
repeating_table |
Array verisinden tekrarlayan tablo | Array |
line |
Yatay/dikey çizgi | Yok |
image |
Statik veya dinamik görsel | Opsiyonel scalar |
page_number |
Sayfa numarası (çok sayfalı belgeler) | Otomatik |
Container Özellikleri
| Özellik | Tip | Açıklama |
|---|---|---|
direction |
"row" | "column" |
Çocukları yatay mı dikey mi diz |
gap |
number (mm) | Çocuklar arası boşluk |
padding |
{ top, right, bottom, left } (mm) |
İç boşluk |
align |
"start" | "center" | "end" | "stretch" |
Cross-axis hizalama |
justify |
"start" | "center" | "end" | "space-between" |
Main-axis dağılım |
style |
{ backgroundColor, borderColor, borderWidth, borderRadius } |
Görsel stil |
Positioning Modları
| Mod | Açıklama | Typst karşılığı |
|---|---|---|
flow |
Parent container'ın flow'una katıl (default) | stack / box içinde |
absolute |
Parent container içinde sabit konum | place(dx, dy) |
Fatura Örneği — Container Ağacı
Sayfa (kök container, column, padding: 15mm)
├── Header (container, row, gap: 5mm)
│ ├── Logo alanı (container, column, width: 60mm)
│ │ ├── image (logo)
│ │ └── text (firma ünvanı)
│ └── Fatura bilgi (container, column, width: fill, align: end)
│ ├── static_text ("FATURA")
│ ├── text (fatura no)
│ └── text (tarih)
├── line (ayırıcı çizgi)
├── repeating_table (kalemler)
└── Footer (container, row)
├── boş alan (width: fill)
└── Toplamlar (container, column, width: 80mm)
├── text (ara toplam)
├── text (KDV)
└── text (genel toplam)
Data JSON (Gerçek Veri)
Render zamanında şablonla birleştirilen veri:
{
"firma": {
"unvan": "Acme Teknoloji A.Ş.",
"vergiNo": "1234567890",
"logo": "data:image/png;base64,...",
},
"fatura": {
"no": "FTR-2026-001",
"tarih": "2026-03-29",
},
"kalemler": [
{
"siraNo": 1,
"adi": "Web Geliştirme Hizmeti",
"miktar": 1,
"birim": "Adet",
"birimFiyat": 15000,
"tutar": 15000,
},
{
"siraNo": 2,
"adi": "SSL Sertifikası",
"miktar": 2,
"birim": "Adet",
"birimFiyat": 500,
"tutar": 1000,
},
],
"toplamlar": {
"araToplam": 16000,
"kdv": 2880,
"genelToplam": 18880,
},
}
JSON Schema (Veri Yapısı Tanımı)
Editörün sol panelinde kullanıcıya sunulan, bağlanabilir alanların ağaç yapısı. Kullanıcı bu ağaçtan sürükleyerek elemanları bağlar.
{
"$id": "fatura-schema",
"type": "object",
"properties": {
"firma": {
"type": "object",
"properties": {
"unvan": { "type": "string", "title": "Firma Ünvanı" },
"vergiNo": { "type": "string", "title": "Vergi No" },
"logo": { "type": "string", "title": "Logo", "format": "image" },
},
},
"fatura": {
"type": "object",
"properties": {
"no": { "type": "string", "title": "Fatura No" },
"tarih": { "type": "string", "title": "Tarih", "format": "date" },
},
},
"kalemler": {
"type": "array",
"title": "Fatura Kalemleri",
"items": {
"type": "object",
"properties": {
"siraNo": { "type": "integer", "title": "Sıra No" },
"adi": { "type": "string", "title": "Ürün / Hizmet Adı" },
"miktar": { "type": "number", "title": "Miktar" },
"birim": { "type": "string", "title": "Birim" },
"birimFiyat": {
"type": "number",
"title": "Birim Fiyat",
"format": "currency",
},
"tutar": { "type": "number", "title": "Tutar", "format": "currency" },
},
},
},
"toplamlar": {
"type": "object",
"properties": {
"araToplam": {
"type": "number",
"title": "Ara Toplam",
"format": "currency",
},
"kdv": { "type": "number", "title": "KDV", "format": "currency" },
"genelToplam": {
"type": "number",
"title": "Genel Toplam",
"format": "currency",
},
},
},
},
}
Binding Mekanizması
Scalar Binding
Basit alan bağlama — bir eleman, JSON'daki tek bir değere bağlanır.
- Editörde: Kullanıcı schema ağacından bir alanı sürükleyip text elemanına bırakır.
- Template JSON'da:
"binding": { "type": "scalar", "path": "firma.vergiNo" } - Typst çıktısında:
#data.firma.vergiNo
Array Binding (Tekrarlayan Tablo)
Array verisi için özel tablo bileşeni. Kullanıcı:
- Araç çubuğundan "Tekrarlayan Tablo" bileşenini sürükler.
dataSourceolarak schema'daki bir array alanı seçer (ör:kalemler).- Sütun tanımlarında array'in alt alanlarını seçer (ör:
kalemler[].adi). - Tablo stili (header rengi, zebra satırlar vs.) ayarlar.
Typst çıktısında:
#let kalemler = data.kalemler
#table(
columns: (8%, 40%, 12%, 10%, 15%, 15%),
align: (center, left, right, center, right, right),
fill: (_, row) => if row == 0 { rgb("#f0f0f0") } else if calc.odd(row) { rgb("#fafafa") } else { none },
[*\#*], [*Ürün / Hizmet*], [*Miktar*], [*Birim*], [*Birim Fiyat*], [*Tutar*],
..kalemler.map(k => (
[#k.siraNo],
[#k.adi],
[#k.miktar],
[#k.birim],
[#format-currency(k.birimFiyat)],
[#format-currency(k.tutar)],
)).flatten()
)
Format Fonksiyonları
Schema'daki format alanına göre Typst helper fonksiyonları üretilir:
currency→ para birimi formatlama (binlik ayracı, kuruş, ₺ sembolü)date→ tarih formatlama (gün.ay.yıl)percentage→ yüzde formatlama
Template JSON → Typst Markup Dönüşümü
Bu dönüşüm hem frontend'de (WASM önizleme için) hem backend'de (PDF üretimi için) çalışır. Aynı saf fonksiyon her iki tarafta da kullanılmalıdır:
- Frontend: TypeScript'te yazılır (
core/template-to-typst.ts). - Backend: Aynı mantık Rust'ta implemente edilir.
Her iki implementasyon da birebir aynı Typst çıktısını üretmelidir. Tutarlılık testleri yazılmalıdır.
Dönüşüm Kuralları (Container-Based)
-
Sayfa ayarları — kök container'ın padding'i = sayfa margin:
#set page(width: 210mm, height: 297mm, margin: (top: 15mm, right: 15mm, bottom: 15mm, left: 15mm)) -
Veri enjeksiyonu:
#let data = ( firma: ( unvan: "Acme A.Ş.", ... ), ... ) -
Container (column) →
stack(dir: ttb):#stack(dir: ttb, spacing: 5mm, [#text(size: 18pt, weight: "bold")[dreport]], [#text(size: 11pt, fill: rgb("#666666"))[Alt başlık]], ) -
Container (row) →
stack(dir: ltr):#stack(dir: ltr, spacing: 5mm, [#box(width: 1fr)[Sol kolon]], [#box(width: 1fr)[Sağ kolon]], ) -
İç içe container →
box+stack:#box(width: 1fr, inset: (top: 5mm, bottom: 5mm))[ #stack(dir: ttb, spacing: 3mm, [#text[Eleman 1]], [#text[Eleman 2]], ) ] -
Absolute eleman →
place()(sadece absolute positioning seçilmişse):#place(top + left, dx: 130mm, dy: 30mm)[ #text(size: 12pt, weight: "bold")[FATURA] ] -
Çizgi →
line():#line(length: 1fr, stroke: 0.5pt + rgb("#000000")) -
Önizleme vs. nihai render:
- Önizleme:
datamock veri ile doldurulur. - Nihai render:
datagerçek JSON verisi ile doldurulur.
- Önizleme:
SVG Overlay — Etkileşim Katmanı
Typst SVG'si read-only bir render çıktısıdır — tıklama veya sürükleme algılamaz. SVG'nin üstüne Vue bileşenleriyle bir etkileşim katmanı (overlay) koyulur.
Overlay Nasıl Çalışır (Container Layout)
- Overlay, template JSON'un ağaç yapısını yansıtır (recursive
ElementHandlebileşenleri). - Kök overlay, sayfa padding'ini CSS padding olarak uygular.
- Flow elemanlar
position: relativeile doğal akışta durur. - Absolute elemanlar
position: absoluteile parent container içinde konumlanır. - Tıklama ile seçim → mavi kenarlık (container ise mor kenarlık) + resize handle'lar.
- Absolute elemanlar sürüklenebilir — drag sırasında CSS transform, bırakınca Typst re-render.
- Flow elemanlar sürüklenemez — sıra değişikliği drag-to-reorder ile yapılır (ilerideki fazda).
Overlay ↔ SVG Koordinat Eşleştirme
- Overlay container'ı, sayfa CSS variable'ları ile margin'leri eşler.
- Zoom yapıldığında hem SVG hem overlay aynı oranda scale edilir.
- Koordinat dönüşümü:
px = mm * (containerWidthPx / pageWidthMm) * zoomLevel
Proje Yapısı (Monorepo)
dreport/
├── CLAUDE.md
├── README.md
├── frontend/ # Vue 3 + TypeScript
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── public/
│ │ └── fonts/ # Typst WASM için gömülü fontlar
│ ├── src/
│ │ ├── main.ts
│ │ ├── App.vue
│ │ ├── stores/ # Pinia
│ │ │ ├── template.ts # Template JSON state
│ │ │ ├── schema.ts # JSON Schema state
│ │ │ └── editor.ts # Editör UI state (seçili eleman, zoom vs.)
│ │ ├── components/
│ │ │ ├── editor/
│ │ │ │ ├── EditorCanvas.vue # Ana editör alanı (SVG + overlay container)
│ │ │ │ ├── TypstSvgLayer.vue # Typst SVG render katmanı
│ │ │ │ ├── InteractionOverlay.vue # Etkileşim katmanı (tüm handle'ların parent'ı)
│ │ │ │ ├── ElementHandle.vue # Tekil eleman seçim/drag/resize handle
│ │ │ │ ├── SnapGuides.vue # Hizalama çizgileri
│ │ │ │ └── RulerBar.vue # Cetvel
│ │ │ ├── panels/
│ │ │ │ ├── ToolboxPanel.vue # Sol: bileşen araç kutusu
│ │ │ │ ├── SchemaTreePanel.vue # Sol: JSON schema ağacı (drag source)
│ │ │ │ └── PropertiesPanel.vue # Sağ: seçili elemanın özellikleri
│ │ │ ├── properties/
│ │ │ │ ├── TextProperties.vue
│ │ │ │ ├── TableProperties.vue
│ │ │ │ ├── ImageProperties.vue
│ │ │ │ └── StyleProperties.vue
│ │ │ └── common/
│ │ │ ├── ColorPicker.vue
│ │ │ ├── FontSelector.vue
│ │ │ └── UnitInput.vue # mm/pt girişi
│ │ ├── composables/
│ │ │ ├── useTypstCompiler.ts # Typst WASM yönetimi (Web Worker iletişimi)
│ │ │ ├── useDragDrop.ts # Sürükle-bırak mantığı
│ │ │ ├── useElementSelection.ts # Eleman seçimi
│ │ │ ├── useSnapGuides.ts # Mıknatıslı hizalama
│ │ │ ├── useUndoRedo.ts # Geri al / yinele
│ │ │ └── useZoomPan.ts # Zoom ve kaydırma
│ │ ├── workers/
│ │ │ └── typst.worker.ts # Typst WASM Web Worker
│ │ ├── core/
│ │ │ ├── template-to-typst.ts # Template JSON → Typst markup dönüşümü
│ │ │ ├── schema-parser.ts # JSON Schema → ağaç yapısı (panel için)
│ │ │ ├── mock-data-generator.ts # Schema'dan örnek veri üretme
│ │ │ └── types.ts # Ortak TypeScript tip tanımları
│ │ └── styles/
│ │ └── editor.css
│ └── tests/
│ ├── template-to-typst.test.ts
│ └── schema-parser.test.ts
│
├── backend/ # Rust + Axum
│ ├── Cargo.toml
│ ├── src/
│ │ ├── main.rs
│ │ ├── routes/
│ │ │ ├── mod.rs
│ │ │ ├── render.rs # POST /api/render → PDF
│ │ │ └── health.rs # GET /api/health
│ │ ├── typst_engine/
│ │ │ ├── mod.rs
│ │ │ ├── compiler.rs # typst crate wrapper (World impl)
│ │ │ ├── template_to_typst.rs # Template JSON → Typst markup (Rust)
│ │ │ └── fonts.rs # Font yönetimi ve yükleme
│ │ └── models/
│ │ ├── mod.rs
│ │ ├── template.rs # Template JSON serde modelleri
│ │ └── schema.rs # JSON Schema modelleri
│ ├── fonts/ # Gömülü font dosyaları
│ │ ├── NotoSans-Regular.ttf
│ │ ├── NotoSans-Bold.ttf
│ │ ├── NotoSans-Italic.ttf
│ │ └── NotoSansMono-Regular.ttf
│ └── tests/
│ ├── render_test.rs
│ └── template_to_typst_test.rs
│
└── shared/ # Ortak şema tanımları
└── schemas/
├── fatura.schema.json
└── irsaliye.schema.json
API Endpoints
İlk aşamada minimal API:
POST /api/render
Template JSON + Data JSON alır, PDF döner.
Request:
{
"template": {},
"data": {}
}
Response: Content-Type: application/pdf — binary PDF
GET /api/health
Sunucu sağlık kontrolü.
Editör UI/UX Davranışları
Eleman Ekleme
- Araç kutusundan sürükle-bırak: Container, statik metin, çizgi, görsel, tekrarlayan tablo.
- Container'a bırakma: Eleman hedef container'ın flow'una eklenir. Drop pozisyonuna göre sıra belirlenir.
- Schema ağacından sürükle-bırak: Scalar alan container'a bırakılınca
textelemanı oluşur, binding ayarlanır. - Schema'dan array alanı sürükle-bırak:
repeating_tableoluşur, sütunları ayarlama diyaloğu açılır.
Eleman Seçimi ve Manipülasyon
- Tıklama ile seçim → mavi kenarlık (container ise mor kenarlık) + resize handle'lar.
- Flow elemanlar: Sürüklenemez (pozisyon otomatik). Sıra değişikliği drag-to-reorder ile.
- Absolute elemanlar: Drag ile taşınır. Sürükleme sırasında CSS transform, bırakınca Typst re-render.
- Container seçimi: Tıklayınca sağ panelde direction, gap, align, padding ayarları.
- Positioning modu değişikliği: Sağ panelde flow ↔ absolute geçişi.
- Delete/Backspace ile silme.
- Shift+tıklama ile çoklu seçim.
Undo/Redo
- Template JSON üzerinde immutable snapshot stack.
- Ctrl+Z / Ctrl+Shift+Z.
Zoom ve Pan
- Ctrl+scroll ile zoom.
- Space+drag veya orta fare tuşu ile pan.
- Zoom aralığı: %25 – %400.
Geliştirme Öncelikleri (Roadmap)
Faz 1: Temel Altyapı ✓
- Proje iskeleti kurulumu (Vue + Vite + Pinia, Axum boilerplate)
- Typst WASM entegrasyonu — Web Worker'da Typst markup → SVG
- Template JSON → Typst markup dönüşümü (static_text, text, line)
- Container-based layout sistemi (tree yapı, flow + absolute positioning)
- EditorCanvas: Typst SVG + recursive overlay + seçim
- Absolute elemanlar için drag ile taşıma
- Resize handle'lar
- Backend iskeleti (Axum, health endpoint, render placeholder)
- Font dosyaları (Noto Sans ailesi)
Faz 2: Editör Temelleri
text(dinamik binding'li) eleman tipi (Typst dönüşümü var, UI eksik)- Schema tree paneli — JSON schema'dan ağaç oluşturma
- Schema'dan drag ile binding oluşturma
- Properties paneli — seçili elemanın stillerini düzenleme (font, renk, boyut, hizalama)
- Container properties paneli — direction, gap, padding, align ayarları
- Mock data generator — schema'dan örnek veri üretip önizlemede kullanma
- Undo/redo
- Toolbox paneli — eleman/container ekleme
Faz 3: Tablo ve Array Binding
repeating_tablebileşeni ve Typst markup üretimi- Sütun tanımlama UI'ı (alan seçimi, genişlik, hizalama)
- Array field'larına binding
- Tablo stili ayarları (header, zebra, border)
- Format fonksiyonları (currency, date)
Faz 4: PDF Render Backend
- Axum server setup +
POST /api/render - Rust'ta template-to-typst dönüşümü (TypeScript versiyonuyla tutarlı)
typstcrate ileWorldtrait implementasyonu- PDF derleme ve response olarak dönme
- Font embed (frontend ile aynı font seti)
- Frontend'den "PDF İndir" butonu
Faz 5: Polish
- Snap guides ve hizalama
- Zoom / pan
line,recteleman tipleriimageeleman tipi (statik + dinamik)- Sayfa numarası
- Çoklu sayfa desteği
- Template kaydetme / yükleme (JSON dosyası export/import)
Önemli Teknik Notlar
Typst WASM Font Stratejisi
Typst tarayıcıda çalışırken sistem fontlarına erişemez. Font dosyaları (*.ttf / *.otf) projeye dahil edilmeli ve WASM'a yüklenmelidir. Başlangıçta minimal bir set:
- Noto Sans (Regular, Bold, Italic, Bold Italic) — genel metin
- Noto Sans Mono (Regular) — tablo sayıları, monospace ihtiyaçları
- Toplam ~4-5 MB
Kritik: Backend'de (Rust) ve frontend'de (WASM) birebir aynı font dosyaları kullanılmalıdır. Farklı font = farklı metrik = render uyumsuzluğu.
Koordinat Sistemi
- Tüm pozisyonlar milimetre (mm) cinsindendir.
- Template JSON'daki değerler mm, Typst'e
Xmmolarak yazılır. - Editör canvas'ta mm → px dönüşümü:
px = mm * (containerWidthPx / pageWidthMm) * zoomLevel - Referans: A4 = 210mm × 297mm.
Typst Özel Karakter Escape
Template JSON → Typst dönüşümünde kullanıcı verisindeki özel karakterler escape edilmelidir:
#,$,@,*,_,<,>,\Typst'te özel anlam taşır.- Kullanıcı verisi
[...]content block'a sarılarak büyük ölçüde güvenli hale gelir. - İçerideki
[,]karakterleri ise\[,\]olarak escape edilmelidir.
Hata Yönetimi
- Typst derleme hatası olursa → editörde kırmızı banner ile hata mesajı göster.
- Derleme başarısız olduğunda son başarılı SVG'yi koru, kullanıcının çalışmasını bozma.
- Web Worker crash olursa → yeniden başlat, state'i koru.
Eleman Sırası (Z-Order)
- Template JSON'daki
elementsdizisinin sırası = çizim sırası (sonraki üstte). - Kullanıcı "Öne Getir" / "Arkaya Gönder" yapabilmeli → dizi sırası değişir.
Rust Backend — Typst World Implementasyonu
Typst crate ile PDF üretmek için World trait'i implement etmek gerekir. Bu trait, Typst'e dosya sistemi, fontlar ve zaman bilgisi sağlar.
use typst::World;
struct DreportWorld {
/// Ana .typ dosyasının içeriği (dinamik üretilen markup)
main_source: String,
/// Yüklenmiş font dosyaları
fonts: Vec<typst::text::Font>,
/// Data JSON (json dosyası olarak erişilebilir)
data_json: String,
}
impl World for DreportWorld {
// file(), font(), main(), source(), ... implementasyonları
}
Derleme akışı:
- HTTP request gelir (template + data JSON).
- Template JSON → Typst markup string üretilir.
- Data JSON, sanal dosya sistemi üzerinden
data.jsonolarak erişilebilir yapılır. DreportWorldoluşturulur.typst::compile(&world)→Documentelde edilir.typst_pdf::pdf(&document, ...)→ PDF bytes.- Response olarak döndürülür.
Kod Stili ve Konvansiyonlar
Frontend (TypeScript / Vue)
- Composition API +
<script setup>kullan, Options API KULLANMA. - Pinia store'lar
defineStoreile. - Tip güvenliği:
strict: truetsconfig'de.anykullanma, gerekirseunknown+ type guard. - Composable isimlendirme:
useXxxpattern. - Bileşen isimleri: PascalCase, en az iki kelime (ör:
EditorCanvas,SchemaTreePanel). - CSS: Scoped styles veya CSS modules. Global CSS minimum.
Backend (Rust)
- Axum handler'lar async.
- Serde ile JSON serialize/deserialize (
#[derive(Serialize, Deserialize)]). - Hata yönetimi:
thiserrorile typed errors, handler'lardaanyhowkabul edilebilir. - Typst crate dependency:
typst,typst-pdf. - Clippy uyarıları temiz tutulacak.
Genel
- Commit mesajları: conventional commits (
feat:,fix:,refactor:,docs:vs.). - Türkçe yorum yazılabilir, kod ve değişken isimleri İngilizce.
- Template JSON field isimleri İngilizce (ör:
position,size,binding). - UI etiketleri ve kullanıcıya gösterilen metinler Türkçe.
Kısıtlamalar ve Bilinçli Tercihler
- Veritabanı yok (ilk aşama). Template'ler JSON dosyası olarak import/export edilir.
- Kullanıcı auth yok. Tek kullanıcılı yerel kullanım senaryosu.
- Sadece PDF çıktı. Typst'in SVG/PNG/HTML çıktıları ileride eklenebilir.
- Tekrarlayan bölge (repeating region) yok — sadece tekrarlayan tablo. Array binding yalnızca tablo bileşeni ile yapılır. Serbest form repeating region ilerideki fazlarda değerlendirilir.
- WYSIWYG garantisi Typst üzerinden. Editörde kendi render engine'imiz yok — Typst ne üretiyorsa kullanıcı onu görür.
- Canvas kütüphanesi (fabric.js / konva.js) kullanılmıyor. Etkileşim katmanı saf Vue bileşenleri + pointer event'ler ile yapılır. Render zaten Typst SVG'sidir.