mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
Compare commits
3 Commits
e95606d18b
...
672f3297f6
| Author | SHA1 | Date | |
|---|---|---|---|
| 672f3297f6 | |||
| bc02bdc82d | |||
| 9f658f5615 |
97
.gitea/workflows/ci.yml
Normal file
97
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rust:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- name: Format check
|
||||||
|
run: cargo fmt --workspace --check
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy --workspace -- -D warnings
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --workspace
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd frontend && bun install
|
||||||
|
- name: Type check
|
||||||
|
run: cd frontend && bun run type-check
|
||||||
|
- name: Lint
|
||||||
|
run: cd frontend && bun run lint
|
||||||
|
- name: Format check
|
||||||
|
run: cd frontend && bun run format:check
|
||||||
|
- name: Unit tests
|
||||||
|
run: cd frontend && bun run test:run
|
||||||
|
|
||||||
|
wasm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
- name: Install wasm-pack
|
||||||
|
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
- name: Build WASM
|
||||||
|
run: wasm-pack build layout-engine --target web --release
|
||||||
|
|
||||||
|
publish-crates:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs: [rust, wasm]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Configure Gitea cargo registry
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.cargo
|
||||||
|
cat >> ~/.cargo/config.toml <<EOF
|
||||||
|
[registries.gitea]
|
||||||
|
index = "sparse+${{ github.server_url }}/api/packages/${{ github.repository_owner }}/cargo/"
|
||||||
|
EOF
|
||||||
|
cat >> ~/.cargo/credentials.toml <<EOF
|
||||||
|
[registries.gitea]
|
||||||
|
token = "Bearer ${{ secrets.GITEA_TOKEN }}"
|
||||||
|
EOF
|
||||||
|
- name: Publish dreport-core
|
||||||
|
run: cargo publish -p dreport-core --registry gitea --allow-dirty || true
|
||||||
|
- name: Publish dreport-layout
|
||||||
|
run: cargo publish -p dreport-layout --registry gitea --allow-dirty || true
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs: [frontend, wasm]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
- name: Install wasm-pack
|
||||||
|
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
- name: Build WASM
|
||||||
|
run: wasm-pack build layout-engine --target web --release --out-dir ../frontend/src/core/wasm-pkg-layout
|
||||||
|
- name: Configure npm registry
|
||||||
|
run: |
|
||||||
|
echo "@duhanbalci:registry=${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/" >> ~/.npmrc
|
||||||
|
echo "//${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.GITEA_TOKEN }}" >> ~/.npmrc
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: cd frontend && bun install
|
||||||
|
- name: Build frontend
|
||||||
|
run: cd frontend && bun run build
|
||||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -417,7 +417,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dreport-backend"
|
name = "dreport-backend"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -432,7 +432,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dreport-core"
|
name = "dreport-core"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -441,7 +441,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dreport-layout"
|
name = "dreport-layout"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"cosmic-text",
|
"cosmic-text",
|
||||||
@@ -452,6 +452,7 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"krilla",
|
"krilla",
|
||||||
|
"rust_decimal",
|
||||||
"rxing",
|
"rxing",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
271
ELEMENTS.md
271
ELEMENTS.md
@@ -1,271 +0,0 @@
|
|||||||
# Eleman Tipleri — dreport
|
|
||||||
|
|
||||||
Bu belge, dreport toolbar'inda bulunan ve planlanmis tum eleman tiplerini aciklar.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mevcut Elemanlar
|
|
||||||
|
|
||||||
### `container` — Duzen Kutusu
|
|
||||||
|
|
||||||
CSS Flexbox mantiginda calisan layout container'i. Cocuk elemanlari `direction` (row/column) dogrultusunda dizer. Ic ice gecebilir. Tum diger elemanlar bir container icinde yer alir.
|
|
||||||
|
|
||||||
- **Binding:** Yok
|
|
||||||
- **Ozellikler:** `direction`, `gap`, `padding`, `align`, `justify`, `style`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `static_text` — Sabit Metin
|
|
||||||
|
|
||||||
Veri baglantisi olmayan, kullanicinin dogrudan yazdigi metin. Fatura basliklari, etiketler, aciklama satirlari icin kullanilir.
|
|
||||||
|
|
||||||
- **Binding:** Yok
|
|
||||||
- **Ozellikler:** `content`, `style` (fontSize, fontWeight, color, align)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `text` — Dinamik Metin
|
|
||||||
|
|
||||||
JSON schema'dan veri ceken metin elemani. Kullanici schema agacindan bir alani surukleyip bu elemana baglar.
|
|
||||||
|
|
||||||
- **Binding:** Scalar (`"binding": { "type": "scalar", "path": "firma.unvan" }`)
|
|
||||||
- **Ozellikler:** `binding`, `style`, `format` (currency, date, percentage)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `repeating_table` — Tekrarlayan Tablo
|
|
||||||
|
|
||||||
Array verisinden tekrarlayan satirlar ureten tablo bileseni. Fatura kalemleri, stok listeleri gibi tekrarlayan veri icin kullanilir.
|
|
||||||
|
|
||||||
- **Binding:** Array (`"dataSource": "kalemler"`)
|
|
||||||
- **Ozellikler:** `columns` (alan, genislik, hizalama), `headerStyle`, `rowStyle`, `zebraStyle`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `line` — Cizgi
|
|
||||||
|
|
||||||
Yatay veya dikey ayirici cizgi. Bolum ayirma, dekoratif amaclarla kullanilir.
|
|
||||||
|
|
||||||
- **Binding:** Yok
|
|
||||||
- **Ozellikler:** `style` (strokeColor, strokeWidth)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `image` — Gorsel
|
|
||||||
|
|
||||||
Statik (base64/URL) veya dinamik (schema'dan) gorsel. Logo, imza, urun gorseli gibi kullanim alanlari.
|
|
||||||
|
|
||||||
- **Binding:** Opsiyonel scalar (dinamik gorsel icin)
|
|
||||||
- **Ozellikler:** `src` (statik), `binding`, `style` (objectFit)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `page_number` — Sayfa Numarasi
|
|
||||||
|
|
||||||
Cok sayfali belgelerde otomatik sayfa numarasi. Format sablonu destekler (or: "Sayfa {current} / {total}").
|
|
||||||
|
|
||||||
- **Binding:** Otomatik
|
|
||||||
- **Ozellikler:** `format`, `style`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `barcode` — Barkod / QR Kod
|
|
||||||
|
|
||||||
1D ve 2D barkod ureteci. e-Fatura, e-Arsiv, urun etiketleri icin kullanilir.
|
|
||||||
|
|
||||||
- **Binding:** Scalar (barkod verisi icin)
|
|
||||||
- **Desteklenen formatlar:** QR, EAN-13, EAN-8, CODE128, CODE39
|
|
||||||
- **Ozellikler:** `barcodeType`, `binding`, `style`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Planlanmis Elemanlar
|
|
||||||
|
|
||||||
### `rich_text` — Zengin Metin [Yapildi]
|
|
||||||
|
|
||||||
Tek bir metin blogu icinde karisik formatlama destekleyen eleman. Kalin, italik, farkli font boyutu, renk gibi stilleri ayni paragraf icinde kullanmayi saglar.
|
|
||||||
|
|
||||||
- **Kullanim alanlari:** Fatura aciklama alanlari, sozlesme maddeleri, rapor notlari, uzun formlu metin icerikleri
|
|
||||||
- **Binding:** Opsiyonel scalar (dinamik icerik icin)
|
|
||||||
- **Yaklasim:** Inline span'lar ile zengin metin. cosmic-text attributed text destekledigi icin layout engine tarafinda uyumlu.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"type": "rich_text",
|
|
||||||
"content": [
|
|
||||||
{ "text": "Odeme vadesi: ", "style": {} },
|
|
||||||
{ "text": "30 gun", "style": { "fontWeight": "bold", "color": "#e00" } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Referans:** Telerik (HtmlTextBox), DevExpress (Rich Text), Stimulsoft, FastReport, CraftMyPDF — hepsinde mevcut. Belge tasarim araclarinda standart bir beklenti.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `shape` — Sekil (Dikdortgen / Elips) [Yapildi]
|
|
||||||
|
|
||||||
Cocuk eleman barindirmayan sade gorsel element. Vurgu kutulari, dekoratif cerceveler, arka plan alanlari icin kullanilir. Container'dan farki: layout'a katilmaz, sadece gorsel amaclidir.
|
|
||||||
|
|
||||||
- **Kullanim alanlari:** Toplam kutusunun arka plani, raporlarda highlight alanlari, dekoratif cerceveler
|
|
||||||
- **Binding:** Yok
|
|
||||||
- **Sekil tipleri:** `rectangle`, `ellipse`, `rounded_rectangle`
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"type": "shape",
|
|
||||||
"shapeType": "rectangle",
|
|
||||||
"style": {
|
|
||||||
"backgroundColor": "#f0f0f0",
|
|
||||||
"borderColor": "#333",
|
|
||||||
"borderWidth": 0.5,
|
|
||||||
"borderRadius": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Referans:** JasperReports, Telerik, DevExpress, Stimulsoft, FastReport, CraftMyPDF — neredeyse tum araclarda var.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `checkbox` — Onay Kutusu [Yapildi]
|
|
||||||
|
|
||||||
Boolean deger gosteren isaret kutusu. Isaretsiz kare veya isaretli (checkmark) kare olarak render edilir. Veri baglantisi ile dinamik calisan veya statik olarak kullanilabilen basit bir element.
|
|
||||||
|
|
||||||
- **Kullanim alanlari:** Irsaliyelerde "teslim edildi / edilmedi", faturalarda odeme durumu, raporlarda checklist, form benzeri belgeler
|
|
||||||
- **Binding:** Scalar (boolean alan)
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"type": "checkbox",
|
|
||||||
"binding": { "type": "scalar", "path": "fatura.odpiendi" },
|
|
||||||
"style": { "size": 4, "checkColor": "#000", "borderColor": "#333" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Referans:** DevExpress, Telerik, Stimulsoft, FastReport, CraftMyPDF.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `calculated_text` — Hesaplanmis Alan [Yapildi]
|
|
||||||
|
|
||||||
Basit ifadeler (expression) ile hesaplanmis deger gosteren metin elemani. Aritmetik islemler, string birlestirme ve kosullu metin destekler.
|
|
||||||
|
|
||||||
- **Kullanim alanlari:** Ara toplam hesaplari (`araToplam * 0.20`), string birlestirme (`"Fatura No: " + fatura.no`), kosullu metin, rapor ozetleri
|
|
||||||
- **Binding:** Expression-based (birden fazla alana referans verebilir)
|
|
||||||
- **Format:** currency, date, percentage, number destegi
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"type": "calculated_text",
|
|
||||||
"expression": "toplamlar.araToplam * 0.20",
|
|
||||||
"format": "currency",
|
|
||||||
"style": { "fontSize": 10 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Referans:** Crystal Reports (Formula Field), JasperReports (Variable), Stimulsoft (Expression).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `current_date` — Tarih / Zaman [Yapildi]
|
|
||||||
|
|
||||||
Belgenin basilma/render anindaki tarihi otomatik gosteren element. `page_number` gibi otomatik deger uretir, veri baglantisi gerektirmez.
|
|
||||||
|
|
||||||
- **Kullanim alanlari:** Fatura basim tarihi, rapor olusturma zamani, belge altbilgisi
|
|
||||||
- **Binding:** Otomatik
|
|
||||||
- **Format:** Konfigurasyon ile (or: `DD.MM.YYYY`, `DD MMMM YYYY`, `DD.MM.YYYY HH:mm`)
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"type": "current_date",
|
|
||||||
"format": "DD.MM.YYYY",
|
|
||||||
"style": { "fontSize": 8, "color": "#666" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Referans:** Crystal Reports (Print Date), JasperReports (Current Date), BIRT (AutoText).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `page_break` — Sayfa Sonu [Yapildi]
|
|
||||||
|
|
||||||
Kullanicinin belirli bir noktada yeni sayfaya gecmesini saglayan kontrol elemani. Otomatik sayfa sonu (page_break.rs) zaten mevcut, bu element manuel kontrol saglar.
|
|
||||||
|
|
||||||
- **Kullanim alanlari:** Rapor ozet sayfasi + detay sayfasi ayrimi, faturada ek bilgi sayfasi, belirli bolumlerin ayri sayfada baslamasi
|
|
||||||
- **Binding:** Yok
|
|
||||||
- **Gorsel:** Editorde kesikli cizgi olarak gosterilir, PDF'te sayfa gecisi uretir.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"type": "page_break"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Referans:** DevExpress (Page Break kontrol), Stimulsoft.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `chart` — Grafik [Henuz implemente edilmedi]
|
|
||||||
|
|
||||||
Veri gorselIestirme icin basit grafik elemani. Rapor ciktilari icin degerli, fatura/irsaliye icin genellikle gereksiz.
|
|
||||||
|
|
||||||
- **Kullanim alanlari:** Satis raporlari, performans ozetleri, karsilastirmali veriler
|
|
||||||
- **Binding:** Array veya multiple scalar
|
|
||||||
- **Grafik tipleri:** `bar`, `pie`, `line` (baslangic seti)
|
|
||||||
- **Yaklasim:** Backend'de SVG olarak render edilip PDF'e image olarak gomulur.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"type": "chart",
|
|
||||||
"chartType": "bar",
|
|
||||||
"dataSource": "aylik_satislar",
|
|
||||||
"labelField": "ay",
|
|
||||||
"valueField": "tutar",
|
|
||||||
"style": { "width": 120, "height": 80 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Referans:** JasperReports, Crystal Reports, Telerik, DevExpress, Stimulsoft, CraftMyPDF — enterprise araclarin tamami destekler.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Toolbar Organizasyonu
|
|
||||||
|
|
||||||
```
|
|
||||||
Toolbar
|
|
||||||
├── Duzen
|
|
||||||
│ ├── Container (mevcut)
|
|
||||||
│ └── Page Break (mevcut)
|
|
||||||
├── Metin
|
|
||||||
│ ├── Statik Metin (mevcut)
|
|
||||||
│ ├── Rich Text (mevcut)
|
|
||||||
│ └── Hesaplanmis Alan (mevcut)
|
|
||||||
├── Veri
|
|
||||||
│ ├── Tekrarlayan Tablo (mevcut)
|
|
||||||
│ └── Checkbox (mevcut)
|
|
||||||
├── Gorsel
|
|
||||||
│ ├── Gorsel (mevcut)
|
|
||||||
│ ├── Cizgi (mevcut)
|
|
||||||
│ ├── Sekil (mevcut)
|
|
||||||
│ └── Barkod / QR (mevcut)
|
|
||||||
├── Otomatik
|
|
||||||
│ ├── Sayfa No (mevcut)
|
|
||||||
│ └── Tarih (mevcut)
|
|
||||||
└── Rapor
|
|
||||||
└── Grafik (planlanmis)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Oncelik Sirasi
|
|
||||||
|
|
||||||
| Oncelik | Element | Gerekce | Durum |
|
|
||||||
|---------|---------|---------|-------|
|
|
||||||
| 1 | `rich_text` | Karisik formatlama en cok talep edilen ozellik, cosmic-text uyumlu | Yapildi |
|
|
||||||
| 2 | `shape` | Basit implementasyon, gorsel zenginlik katiyor | Yapildi |
|
|
||||||
| 3 | `checkbox` | Boolean gosterim, form/irsaliye icin onemli | Yapildi |
|
|
||||||
| 4 | `calculated_text` | Hesaplama ihtiyaci fatura/rapor icin kritik | Yapildi |
|
|
||||||
| 5 | `current_date` | Kucuk ama kullanisli, hizli implemente edilir | Yapildi |
|
|
||||||
| 6 | `page_break` | Manuel sayfa kontrolu, rapor senaryolari icin | Yapildi |
|
|
||||||
| 7 | `chart` | En karmasik, rapor fazinda ele alinabilir | |
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
## 1. Kritik Buglar
|
## 1. Kritik Buglar
|
||||||
|
|
||||||
### 1.1 Undo/Redo `Object.assign` Hatasi `[IMPLEMENTE EDILMEDI]`
|
### 1.1 Undo/Redo `Object.assign` Hatasi `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Dosya:** `frontend/src/composables/useUndoRedo.ts` (satir 52)
|
**Dosya:** `frontend/src/composables/useUndoRedo.ts` (satir 52)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Undo/redo watcher'da 300ms debounce var. Kullanici hizli bir edit yapip 300ms ic
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.2 PDF'te Text Wrapping Yok `[IMPLEMENTE EDILMEDI]`
|
### 1.2 PDF'te Text Wrapping Yok `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Dosya:** `layout-engine/src/pdf_render.rs` (satir ~487)
|
**Dosya:** `layout-engine/src/pdf_render.rs` (satir ~487)
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ Bu, projenin temel vaadi olan "editorde gordugum = PDF'te aldigim" WYSIWYG garan
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.3 Image objectFit Hardcoded `[IMPLEMENTE EDILMEDI]`
|
### 1.3 Image objectFit Hardcoded `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Dosya:** `frontend/src/components/editor/LayoutRenderer.vue` (satir ~229)
|
**Dosya:** `frontend/src/components/editor/LayoutRenderer.vue` (satir ~229)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ objectFit: el.style.objectFit || 'fill',
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.4 PDF'te Italic Font Secilmiyor `[IMPLEMENTE EDILMEDI]`
|
### 1.4 PDF'te Italic Font Secilmiyor `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Dosya:** `layout-engine/src/pdf_render.rs` (satir ~104)
|
**Dosya:** `layout-engine/src/pdf_render.rs` (satir ~104)
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za
|
|||||||
|
|
||||||
## 3. Eksik Ozellikler (CLAUDE.md'de Tanimli)
|
## 3. Eksik Ozellikler (CLAUDE.md'de Tanimli)
|
||||||
|
|
||||||
### 3.1 Coklu Secim (Multi-Selection) `[IMPLEMENTE EDILMEDI]`
|
### 3.1 Coklu Secim (Multi-Selection) `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Referans:** CLAUDE.md — "Shift+tiklama ile coklu secim"
|
**Referans:** CLAUDE.md — "Shift+tiklama ile coklu secim"
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.2 Z-Order Kontrolleri `[IMPLEMENTE EDILMEDI]`
|
### 3.2 Z-Order Kontrolleri `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Referans:** CLAUDE.md — "One Getir / Arkaya Gonder"
|
**Referans:** CLAUDE.md — "One Getir / Arkaya Gonder"
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.3 Dinamik Image Binding UI `[IMPLEMENTE EDILMEDI]`
|
### 3.3 Dinamik Image Binding UI `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Referans:** CLAUDE.md — "image: Statik veya dinamik gorsel, Opsiyonel scalar binding"
|
**Referans:** CLAUDE.md — "image: Statik veya dinamik gorsel, Opsiyonel scalar binding"
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.4 RulerBar (Cetvel) `[IMPLEMENTE EDILMEDI]`
|
### 3.4 RulerBar (Cetvel) `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Referans:** CLAUDE.md proje yapisi — `components/editor/RulerBar.vue`
|
**Referans:** CLAUDE.md proje yapisi — `components/editor/RulerBar.vue`
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ Component dosyasi olusturulmamis, hicbir yerde import edilmiyor.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.5 Format Fonksiyonlari (Tablo Sutunlari) `[IMPLEMENTE EDILMEDI]`
|
### 3.5 Format Fonksiyonlari (Tablo Sutunlari) `[IMPLEMENTE EDILDI]`
|
||||||
|
|
||||||
**Referans:** CLAUDE.md roadmap — "Format fonksiyonlari (currency, date)"
|
**Referans:** CLAUDE.md roadmap — "Format fonksiyonlari (currency, date)"
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ Component dosyasi olusturulmamis, hicbir yerde import edilmiyor.
|
|||||||
|
|
||||||
## 4. Mimari Iyilestirmeler
|
## 4. Mimari Iyilestirmeler
|
||||||
|
|
||||||
### 4.1 Worker Message Type Safety `[IMPLEMENTE EDILMEDI]`
|
### 4.1 Worker Message Type Safety `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `frontend/src/composables/useLayoutEngine.ts` (satir 27)
|
**Dosya:** `frontend/src/composables/useLayoutEngine.ts` (satir 27)
|
||||||
|
|
||||||
@@ -388,7 +388,7 @@ worker.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.2 Image Re-Encoding Optimizasyonu `[IMPLEMENTE EDILMEDI]`
|
### 4.2 Image Re-Encoding Optimizasyonu `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `layout-engine/src/pdf_render.rs` (satir ~712)
|
**Dosya:** `layout-engine/src/pdf_render.rs` (satir ~712)
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ worker.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.3 Tablo Genisletme Cache `[IMPLEMENTE EDILMEDI]`
|
### 4.3 Tablo Genisletme Cache `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `layout-engine/src/table_layout.rs`
|
**Dosya:** `layout-engine/src/table_layout.rs`
|
||||||
|
|
||||||
@@ -421,7 +421,7 @@ Buyuk tablolarda layout hesaplama suresi ve bellek kullanimi artar. Editorde her
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.4 Font Loader Iyilestirmesi (Backend) `[IMPLEMENTE EDILMEDI]`
|
### 4.4 Font Loader Iyilestirmesi (Backend) `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `backend/src/main.rs` (satirlar 44-53)
|
**Dosya:** `backend/src/main.rs` (satirlar 44-53)
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ TTF/OTF `name` tablosunu okuyarak font ailesini (family name) metadata'dan almak
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.5 Floating-Point Currency Formatlama Hatasi `[IMPLEMENTE EDILMEDI]`
|
### 4.5 Floating-Point Currency Formatlama Hatasi `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `layout-engine/src/expr_eval.rs` (satir ~82)
|
**Dosya:** `layout-engine/src/expr_eval.rs` (satir ~82)
|
||||||
|
|
||||||
@@ -444,13 +444,13 @@ TTF/OTF `name` tablosunu okuyarak font ailesini (family name) metadata'dan almak
|
|||||||
`1.005` gibi degerler icin floating-point representation kaybi nedeniyle kusurat 0 veya 1 olarak yanlis yuvarlanabilir.
|
`1.005` gibi degerler icin floating-point representation kaybi nedeniyle kusurat 0 veya 1 olarak yanlis yuvarlanabilir.
|
||||||
|
|
||||||
**Cozum:**
|
**Cozum:**
|
||||||
`Decimal` arithmetic kullanmak veya en azindan `format!("{:.2}", value)` ile string uzerinden islem yapmak.
|
`Decimal` arithmetic kullanmak.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Altyapi ve Developer Experience
|
## 5. Altyapi ve Developer Experience
|
||||||
|
|
||||||
### 5.1 CI/CD Pipeline `[IMPLEMENTE EDILMEDI]`
|
### 5.1 CI/CD Pipeline `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Mevcut Durum:**
|
**Mevcut Durum:**
|
||||||
Hicbir CI/CD konfigurasyonu yok (`.github/`, `.gitea/`, vb.).
|
Hicbir CI/CD konfigurasyonu yok (`.github/`, `.gitea/`, vb.).
|
||||||
@@ -476,7 +476,7 @@ jobs:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.2 justfile Test/Lint/Fmt Recipe'leri `[IMPLEMENTE EDILMEDI]`
|
### 5.2 justfile Test/Lint/Fmt Recipe'leri `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `justfile`
|
**Dosya:** `justfile`
|
||||||
|
|
||||||
@@ -508,7 +508,7 @@ check:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.3 rust-toolchain.toml `[IMPLEMENTE EDILMEDI]`
|
### 5.3 rust-toolchain.toml `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Sorun:**
|
**Sorun:**
|
||||||
Proje Rust edition 2024 kullaniyor (Rust 1.85+) ama toolchain pinlenmemis. Farkli gelistirici ortamlarinda farkli Rust versiyonlari derleme hatalarina yol acabilir.
|
Proje Rust edition 2024 kullaniyor (Rust 1.85+) ama toolchain pinlenmemis. Farkli gelistirici ortamlarinda farkli Rust versiyonlari derleme hatalarina yol acabilir.
|
||||||
@@ -524,7 +524,7 @@ targets = ["wasm32-unknown-unknown"]
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.4 WASM Binary Git'te Tracked `[IMPLEMENTE EDILMEDI]`
|
### 5.4 WASM Binary Git'te Tracked `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `frontend/public/wasm/dreport_layout_bg.wasm`
|
**Dosya:** `frontend/public/wasm/dreport_layout_bg.wasm`
|
||||||
|
|
||||||
@@ -539,7 +539,7 @@ WASM dosyasini build artifact olarak ele almak. CI/CD veya README'de build adimi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.5 codemirror-lang-dexpr Dis Bagimlilik `[IMPLEMENTE EDILMEDI]`
|
### 5.5 codemirror-lang-dexpr Dis Bagimlilik `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `frontend/package.json`
|
**Dosya:** `frontend/package.json`
|
||||||
|
|
||||||
@@ -556,11 +556,14 @@ Repo disinda, ust dizinde `rust-expr` projesinin checkout edilmis olmasini gerek
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.6 ESLint / Prettier Kurulumu `[IMPLEMENTE EDILMEDI]`
|
### 5.6 ESLint / oxfmt Kurulumu `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Mevcut Durum:**
|
**Mevcut Durum:**
|
||||||
Frontend'de hicbir linter veya formatter konfigurasyonu yok. TypeScript strict mode tip hatalarini yakalasa da, AST-level linting (unused imports, Vue-specific patterns, tutarli stil kurallari) bulunmuyor.
|
Frontend'de hicbir linter veya formatter konfigurasyonu yok. TypeScript strict mode tip hatalarini yakalasa da, AST-level linting (unused imports, Vue-specific patterns, tutarli stil kurallari) bulunmuyor.
|
||||||
|
|
||||||
|
**Master Thougs**
|
||||||
|
oxfmt kullanalım prettier yerine, eslint kullanmaya devam edebiliriz oxlint vue için yeterince olgun değil.
|
||||||
|
|
||||||
**Onerilen Yaklasim:**
|
**Onerilen Yaklasim:**
|
||||||
- `eslint` + `@vue/eslint-config-typescript` + `eslint-plugin-vue`
|
- `eslint` + `@vue/eslint-config-typescript` + `eslint-plugin-vue`
|
||||||
- `prettier` + `.prettierrc`
|
- `prettier` + `.prettierrc`
|
||||||
@@ -568,7 +571,7 @@ Frontend'de hicbir linter veya formatter konfigurasyonu yok. TypeScript strict m
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.7 Test Helper Duplikasyonu `[IMPLEMENTE EDILMEDI]`
|
### 5.7 Test Helper Duplikasyonu `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosyalar:**
|
**Dosyalar:**
|
||||||
- `layout-engine/tests/layout_integration.rs`
|
- `layout-engine/tests/layout_integration.rs`
|
||||||
@@ -587,7 +590,7 @@ pub fn load_test_fonts() -> Vec<FontData> { ... }
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.8 Test Artifact Temizligi `[IMPLEMENTE EDILMEDI]`
|
### 5.8 Test Artifact Temizligi `[TAMAMLANDI]`
|
||||||
|
|
||||||
**Dosya:** `layout-engine/tests/pdf_render_test.rs`
|
**Dosya:** `layout-engine/tests/pdf_render_test.rs`
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dreport-backend"
|
name = "dreport-backend"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
164
backend/src/font_registry.rs
Normal file
164
backend/src/font_registry.rs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use dreport_layout::FontData;
|
||||||
|
use dreport_layout::font_meta::{self, FontFamilyInfo, FontVariantKey};
|
||||||
|
use dreport_layout::font_provider::FontProvider;
|
||||||
|
|
||||||
|
/// Font registry — manages all available fonts from embedded defaults + external directory.
|
||||||
|
pub struct FontRegistry {
|
||||||
|
/// family_lower -> variant_key -> FontData
|
||||||
|
families: HashMap<String, HashMap<FontVariantKey, FontData>>,
|
||||||
|
/// Original-case family names
|
||||||
|
family_names: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontRegistry {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut registry = Self {
|
||||||
|
families: HashMap::new(),
|
||||||
|
family_names: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load embedded default fonts
|
||||||
|
registry.load_embedded_defaults();
|
||||||
|
|
||||||
|
// Load fonts from DREPORT_FONTS_DIR if set
|
||||||
|
if let Ok(dir) = std::env::var("DREPORT_FONTS_DIR") {
|
||||||
|
registry.load_from_directory(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
registry
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_embedded_defaults(&mut self) {
|
||||||
|
let embedded: &[(&str, &[u8])] = &[
|
||||||
|
("NotoSans-Regular", include_bytes!("../fonts/NotoSans-Regular.ttf")),
|
||||||
|
("NotoSans-Bold", include_bytes!("../fonts/NotoSans-Bold.ttf")),
|
||||||
|
("NotoSans-Italic", include_bytes!("../fonts/NotoSans-Italic.ttf")),
|
||||||
|
("NotoSans-BoldItalic", include_bytes!("../fonts/NotoSans-BoldItalic.ttf")),
|
||||||
|
("NotoSansMono-Regular", include_bytes!("../fonts/NotoSansMono-Regular.ttf")),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (_name, data) in embedded {
|
||||||
|
self.register_font(data.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_from_directory(&mut self, dir: &str) {
|
||||||
|
let path = std::path::Path::new(dir);
|
||||||
|
if !path.is_dir() {
|
||||||
|
eprintln!("DREPORT_FONTS_DIR dizini bulunamadı: {}", dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = match std::fs::read_dir(path) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("DREPORT_FONTS_DIR okunamadı: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let p = entry.path();
|
||||||
|
if p.extension().is_some_and(|e| e == "ttf" || e == "otf") {
|
||||||
|
if let Ok(data) = std::fs::read(&p) {
|
||||||
|
if self.register_font(data) {
|
||||||
|
println!(" Font yüklendi: {}", p.display());
|
||||||
|
} else {
|
||||||
|
eprintln!(" Font parse edilemedi: {}", p.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a font from raw bytes. Returns true if successful.
|
||||||
|
fn register_font(&mut self, data: Vec<u8>) -> bool {
|
||||||
|
let Some(meta) = font_meta::parse_font_meta(&data) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let family_lower = meta.family.to_lowercase();
|
||||||
|
let variant_key = meta.variant_key();
|
||||||
|
|
||||||
|
self.family_names
|
||||||
|
.entry(family_lower.clone())
|
||||||
|
.or_insert_with(|| meta.family.clone());
|
||||||
|
|
||||||
|
let font_data = FontData::new(meta.family, meta.weight, meta.italic, data);
|
||||||
|
|
||||||
|
self.families
|
||||||
|
.entry(family_lower)
|
||||||
|
.or_default()
|
||||||
|
.insert(variant_key, font_data);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific font's raw bytes
|
||||||
|
pub fn get_font_bytes(&self, family: &str, weight: u16, italic: bool) -> Option<&[u8]> {
|
||||||
|
let family_lower = family.to_lowercase();
|
||||||
|
let key = FontVariantKey { weight, italic };
|
||||||
|
self.families
|
||||||
|
.get(&family_lower)
|
||||||
|
.and_then(|variants| variants.get(&key))
|
||||||
|
.map(|fd| fd.data.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all FontData for given family names (for passing to layout engine)
|
||||||
|
pub fn fonts_for_families(&self, families: &[String]) -> Vec<FontData> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut loaded = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
// Always include default family
|
||||||
|
let default_lower = "noto sans".to_string();
|
||||||
|
let mut to_load: Vec<String> = vec![default_lower.clone()];
|
||||||
|
for f in families {
|
||||||
|
let fl = f.to_lowercase();
|
||||||
|
if !to_load.contains(&fl) {
|
||||||
|
to_load.push(fl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for family_lower in &to_load {
|
||||||
|
if loaded.contains(family_lower) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(variants) = self.families.get(family_lower) {
|
||||||
|
for fd in variants.values() {
|
||||||
|
result.push(fd.clone());
|
||||||
|
}
|
||||||
|
loaded.insert(family_lower.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontProvider for FontRegistry {
|
||||||
|
fn list_families(&self) -> Vec<FontFamilyInfo> {
|
||||||
|
self.families
|
||||||
|
.iter()
|
||||||
|
.map(|(family_lower, variants)| {
|
||||||
|
let family = self.family_names
|
||||||
|
.get(family_lower)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| family_lower.clone());
|
||||||
|
FontFamilyInfo {
|
||||||
|
family,
|
||||||
|
variants: variants.keys().cloned().collect(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_font(&self, family: &str, weight: u16, italic: bool) -> Option<FontData> {
|
||||||
|
let family_lower = family.to_lowercase();
|
||||||
|
let key = FontVariantKey { weight, italic };
|
||||||
|
self.families
|
||||||
|
.get(&family_lower)
|
||||||
|
.and_then(|variants| variants.get(&key))
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
use axum::{Router, serve};
|
use axum::{Router, serve};
|
||||||
use dreport_layout::FontData;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
|
||||||
|
mod font_registry;
|
||||||
mod models;
|
mod models;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
|
use font_registry::FontRegistry;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
println!("Fontlar yukleniyor...");
|
println!("Font registry başlatılıyor...");
|
||||||
let fonts = Arc::new(load_fonts());
|
let registry = Arc::new(FontRegistry::new());
|
||||||
println!("Fontlar yuklendi ({} font dosyasi)", fonts.len());
|
|
||||||
|
let family_count = dreport_layout::font_provider::FontProvider::list_families(registry.as_ref()).len();
|
||||||
|
println!("Font registry hazır ({} font ailesi)", family_count);
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
@@ -21,7 +25,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.merge(routes::router())
|
.merge(routes::router())
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.with_state(fonts);
|
.with_state(registry);
|
||||||
|
|
||||||
let listener = TcpListener::bind("0.0.0.0:3001").await?;
|
let listener = TcpListener::bind("0.0.0.0:3001").await?;
|
||||||
println!("dreport backend listening on http://localhost:3001");
|
println!("dreport backend listening on http://localhost:3001");
|
||||||
@@ -29,31 +33,3 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proje fontlarını yükler (backend/fonts/ dizininden).
|
|
||||||
fn load_fonts() -> Vec<FontData> {
|
|
||||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("fonts");
|
|
||||||
let mut fonts = Vec::new();
|
|
||||||
|
|
||||||
let entries = std::fs::read_dir(&font_dir).expect("backend/fonts dizini bulunamadi");
|
|
||||||
for entry in entries {
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().is_some_and(|e| e == "ttf" || e == "otf") {
|
|
||||||
let data = std::fs::read(&path).unwrap();
|
|
||||||
let family = if path
|
|
||||||
.file_name()
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.contains("Mono")
|
|
||||||
{
|
|
||||||
"Noto Sans Mono".to_string()
|
|
||||||
} else {
|
|
||||||
"Noto Sans".to_string()
|
|
||||||
};
|
|
||||||
fonts.push(FontData { family, data });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fonts
|
|
||||||
}
|
|
||||||
|
|||||||
77
backend/src/routes/fonts.rs
Normal file
77
backend/src/routes/fonts.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
extract::{Path, State},
|
||||||
|
http::{StatusCode, header},
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::get,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use dreport_layout::font_provider::FontProvider;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::font_registry::FontRegistry;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct FontFamilyResponse {
|
||||||
|
family: String,
|
||||||
|
variants: Vec<FontVariantResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct FontVariantResponse {
|
||||||
|
weight: u16,
|
||||||
|
italic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/fonts — list all available font families
|
||||||
|
async fn list_fonts(
|
||||||
|
State(registry): State<Arc<FontRegistry>>,
|
||||||
|
) -> Json<Vec<FontFamilyResponse>> {
|
||||||
|
let families = registry.list_families();
|
||||||
|
let response: Vec<FontFamilyResponse> = families
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| FontFamilyResponse {
|
||||||
|
family: f.family,
|
||||||
|
variants: f.variants
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| FontVariantResponse {
|
||||||
|
weight: v.weight,
|
||||||
|
italic: v.italic,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Json(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/fonts/:family/:weight/:italic — serve font binary
|
||||||
|
async fn get_font(
|
||||||
|
State(registry): State<Arc<FontRegistry>>,
|
||||||
|
Path((family, weight, italic)): Path<(String, u16, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let is_italic = italic == "true" || italic == "1";
|
||||||
|
|
||||||
|
match registry.get_font_bytes(&family, weight, is_italic) {
|
||||||
|
Some(data) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
[(header::CONTENT_TYPE, "font/ttf")],
|
||||||
|
data.to_vec(),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
None => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
format!(
|
||||||
|
"Font bulunamadı: {} weight={} italic={}",
|
||||||
|
family, weight, is_italic
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/fonts", get(list_fonts))
|
||||||
|
.route("/api/fonts/{family}/{weight}/{italic}", get(get_font))
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
use axum::{Router, routing::get, Json};
|
use axum::{Router, routing::get, Json};
|
||||||
use dreport_layout::FontData;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::font_registry::FontRegistry;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct HealthResponse {
|
struct HealthResponse {
|
||||||
status: &'static str,
|
status: &'static str,
|
||||||
@@ -16,6 +17,6 @@ async fn health() -> Json<HealthResponse> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<Vec<FontData>>> {
|
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||||
Router::new().route("/api/health", get(health))
|
Router::new().route("/api/health", get(health))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
mod fonts;
|
||||||
mod health;
|
mod health;
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use dreport_layout::FontData;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<Vec<FontData>>> {
|
use crate::font_registry::FontRegistry;
|
||||||
|
|
||||||
|
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(health::router())
|
.merge(health::router())
|
||||||
.merge(render::router())
|
.merge(render::router())
|
||||||
|
.merge(fonts::router())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ use axum::{
|
|||||||
routing::post,
|
routing::post,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use dreport_layout::FontData;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::font_registry::FontRegistry;
|
||||||
use crate::models::Template;
|
use crate::models::Template;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -20,28 +20,39 @@ pub struct RenderRequest {
|
|||||||
|
|
||||||
/// POST /api/render — Template + Data → PDF
|
/// POST /api/render — Template + Data → PDF
|
||||||
pub async fn render(
|
pub async fn render(
|
||||||
State(fonts): State<Arc<Vec<FontData>>>,
|
State(registry): State<Arc<FontRegistry>>,
|
||||||
Json(payload): Json<RenderRequest>,
|
Json(payload): Json<RenderRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// 1. Layout hesapla
|
// CPU-intensive layout + PDF render'ı blocking thread'de çalıştır
|
||||||
let layout = dreport_layout::compute_layout(&payload.template, &payload.data, &fonts);
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
// Template'in fonts alanına göre sadece gerekli fontları yükle
|
||||||
|
let fonts = registry.fonts_for_families(&payload.template.fonts);
|
||||||
|
let layout = dreport_layout::compute_layout(&payload.template, &payload.data, &fonts)
|
||||||
|
.map_err(|e| format!("Layout error: {}", e))?;
|
||||||
|
dreport_layout::pdf_render::render_pdf(&layout, &fonts)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
// 2. PDF render
|
match result {
|
||||||
match dreport_layout::pdf_render::render_pdf(&layout, &fonts) {
|
Ok(Ok(pdf_bytes)) => (
|
||||||
Ok(pdf_bytes) => (
|
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[(header::CONTENT_TYPE, "application/pdf")],
|
[(header::CONTENT_TYPE, "application/pdf")],
|
||||||
pdf_bytes,
|
pdf_bytes,
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
|
Ok(Err(err)) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("PDF render hatası: {}", err),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("PDF render hatasi: {}", err),
|
format!("Task hatası: {}", err),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<Vec<FontData>>> {
|
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||||
Router::new().route("/api/render", post(render))
|
Router::new().route("/api/render", post(render))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dreport-core"
|
name = "dreport-core"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Core models and types for dreport document design tool"
|
description = "Core models and types for dreport document design tool"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ pub enum PositionMode {
|
|||||||
pub struct TextStyle {
|
pub struct TextStyle {
|
||||||
pub font_size: Option<f64>,
|
pub font_size: Option<f64>,
|
||||||
pub font_weight: Option<String>,
|
pub font_weight: Option<String>,
|
||||||
|
pub font_style: Option<String>,
|
||||||
pub font_family: Option<String>,
|
pub font_family: Option<String>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
pub align: Option<String>,
|
pub align: Option<String>,
|
||||||
@@ -559,4 +560,42 @@ pub struct Template {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub footer: Option<ContainerElement>,
|
pub footer: Option<ContainerElement>,
|
||||||
pub root: ContainerElement,
|
pub root: ContainerElement,
|
||||||
|
#[serde(default)]
|
||||||
|
pub format_config: Option<FormatConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sayı/para birimi formatlama ayarları.
|
||||||
|
/// Belirtilmezse Türk Lirası varsayılan (. binlik, , ondalık, ₺ sembol).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct FormatConfig {
|
||||||
|
/// Binlik ayırıcı (varsayılan ".")
|
||||||
|
#[serde(default = "FormatConfig::default_thousands_sep")]
|
||||||
|
pub thousands_separator: String,
|
||||||
|
/// Ondalık ayırıcı (varsayılan ",")
|
||||||
|
#[serde(default = "FormatConfig::default_decimal_sep")]
|
||||||
|
pub decimal_separator: String,
|
||||||
|
/// Para birimi sembolü (varsayılan "₺")
|
||||||
|
#[serde(default = "FormatConfig::default_currency_symbol")]
|
||||||
|
pub currency_symbol: String,
|
||||||
|
/// Para birimi sembolü pozisyonu: "suffix" (varsayılan) veya "prefix"
|
||||||
|
#[serde(default = "FormatConfig::default_currency_position")]
|
||||||
|
pub currency_position: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatConfig {
|
||||||
|
fn default_thousands_sep() -> String { ".".to_string() }
|
||||||
|
fn default_decimal_sep() -> String { ",".to_string() }
|
||||||
|
fn default_currency_symbol() -> String { "₺".to_string() }
|
||||||
|
fn default_currency_position() -> String { "suffix".to_string() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FormatConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
thousands_separator: Self::default_thousands_sep(),
|
||||||
|
decimal_separator: Self::default_decimal_sep(),
|
||||||
|
currency_symbol: Self::default_currency_symbol(),
|
||||||
|
currency_position: Self::default_currency_position(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
frontend/.oxfmtrc.json
Normal file
12
frontend/.oxfmtrc.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"ignorePatterns": [
|
||||||
|
"src/core/wasm-layout/",
|
||||||
|
"src/core/wasm-pkg-layout/",
|
||||||
|
"src/core/wasm-pkg/",
|
||||||
|
"src/core/wasm/"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -9,26 +9,32 @@
|
|||||||
"@codemirror/language": "^6.12.3",
|
"@codemirror/language": "^6.12.3",
|
||||||
"@codemirror/state": "^6.6.0",
|
"@codemirror/state": "^6.6.0",
|
||||||
"@codemirror/view": "^6.41.0",
|
"@codemirror/view": "^6.41.0",
|
||||||
|
"@duhanbalci/codemirror-lang-dexpr": "0.1.0",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.8",
|
"@lezer/lr": "^1.4.8",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"codemirror-lang-dexpr": "file:../../rust-expr/editor",
|
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.31",
|
"vue": "^3.5.31",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "^6.0.5",
|
||||||
"@vitejs/plugin-vue": "^6.0.5",
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
|
"eslint": "^10.2.0",
|
||||||
|
"eslint-plugin-vue": "^10.8.0",
|
||||||
"happy-dom": "^20.8.9",
|
"happy-dom": "^20.8.9",
|
||||||
|
"oxfmt": "^0.43.0",
|
||||||
"pixelmatch": "^7.1.0",
|
"pixelmatch": "^7.1.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.1",
|
"vite": "^8.0.1",
|
||||||
"vitest": "^4.1.2",
|
"vitest": "^4.1.2",
|
||||||
|
"vue-eslint-parser": "^10.4.0",
|
||||||
"vue-tsc": "^3.2.5",
|
"vue-tsc": "^3.2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -56,6 +62,8 @@
|
|||||||
|
|
||||||
"@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
|
"@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
|
||||||
|
|
||||||
|
"@duhanbalci/codemirror-lang-dexpr": ["@duhanbalci/codemirror-lang-dexpr@0.1.0", "https://gitea.duhanbalci.com/api/packages/duhanbalci/npm/%40duhanbalci%2Fcodemirror-lang-dexpr/-/0.1.0/codemirror-lang-dexpr-0.1.0.tgz", { "peerDependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-cR8SGbtW3Dq1/w7RyWG0yJeBfl3sqQGtJtPZE/d7hziIy75tvt1zEtYxODyuzlDkteX9XzUnaLrS3/TK9dZj8Q=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||||
@@ -114,20 +122,36 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
||||||
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
|
"@eslint/config-array": ["@eslint/config-array@0.23.4", "", { "dependencies": { "@eslint/object-schema": "^3.0.4", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow=="],
|
||||||
|
|
||||||
|
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.4", "", { "dependencies": { "@eslint/core": "^1.2.0" } }, "sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg=="],
|
||||||
|
|
||||||
|
"@eslint/core": ["@eslint/core@1.2.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A=="],
|
||||||
|
|
||||||
|
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
|
||||||
|
|
||||||
|
"@eslint/object-schema": ["@eslint/object-schema@3.0.4", "", {}, "sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw=="],
|
||||||
|
|
||||||
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.0", "levn": "^0.4.1" } }, "sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg=="],
|
||||||
|
|
||||||
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
|
|
||||||
|
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
|
||||||
|
|
||||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
|
||||||
|
|
||||||
"@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
|
"@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
|
||||||
|
|
||||||
"@lezer/generator": ["@lezer/generator@1.8.0", "", { "dependencies": { "@lezer/common": "^1.1.0", "@lezer/lr": "^1.3.0" }, "bin": { "lezer-generator": "src/lezer-generator.cjs" } }, "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg=="],
|
|
||||||
|
|
||||||
"@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
|
"@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
|
||||||
|
|
||||||
"@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
|
"@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
|
||||||
@@ -140,6 +164,44 @@
|
|||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.43.0", "", { "os": "android", "cpu": "arm" }, "sha512-CgU2s+/9hHZgo0IxVxrbMPrMj+tJ6VM3mD7Mr/4oiz4FNTISLoCvRmB5nk4wAAle045RtRjd86m673jwPyb1OQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.43.0", "", { "os": "android", "cpu": "arm64" }, "sha512-T9OfRwjA/EdYxAqbvR7TtqLv5nIrwPXuCtTwOHtS7aR9uXyn74ZYgzgTo6/ZwvTq9DY4W+DsV09hB2EXgn9EbA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.43.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-o3i49ZUSJWANzXMAAVY1wnqb65hn4JVzwlRQ5qfcwhRzIA8lGVaud31Q3by5ALHPrksp5QEaKCQF9aAS3TXpZA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.43.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vWECzzCFkb0kK6jaHjbtC5sC3adiNWtqawFCxhpvsWlzVeKmv5bNvkB4nux+o4JKWTpHCM57NDK/MeXt44txmA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.43.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rgz8JpkKiI/umOf7fl9gwKyQasC8bs5SYHy6g7e4SunfLBY3+8ATcD5caIg8KLGEtKFm5ujKaH8EfjcmnhzTLg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-nWYnF3vIFzT4OM1qL/HSf1Yuj96aBuKWSaObXHSWliwAk2rcj7AWd6Lf7jowEBQMo4wCZVnueIGw/7C4u0KTBQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-sFg+NWJbLfupYTF4WELHAPSnLPOn1jiDZ33Z1jfDnTaA+cC3iB35x0FMMZTFdFOz3icRIArncwCcemJFGXu6TQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MelWqv68tX6wZEILDrTc9yewiGXe7im62+5x0bNXlCYFOZdA+VnYiJfAihbROsZ5fm90p9C3haFrqjj43XnlAA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ROaWfYh+6BSJ1Arwy5ujijTlwnZetxDxzBpDc1oBR4d7rfrPBqzeyjd5WOudowzQUgyavl2wEpzn1hw3jWcqLA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.43.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PJRs/uNxmFipJJ8+SyKHh7Y7VZIKQicqrrBzvfyM5CtKi8D7yZKTwUOZV3ffxmiC2e7l1SDJpkBEOyue5NAFsg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-j6biGAgzIhj+EtHXlbNumvwG7XqOIdiU4KgIWRXAEj/iUbHKukKW8eXa4MIwpQwW1YkxovduKtzEAPnjlnAhVQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-RYWxAcslKxvy7yri24Xm9cmD0RiANaiEPs007EFG6l9h1ChM69Q5SOzACaCoz4Z9dEplnhhneeBaTWMEdpgIbA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.43.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-DT6Q8zfQQy3jxpezAsBACEHNUUixKSYTwdXeXojNHe4DQOoxjPdjr3Szu6BRNjxLykZM/xMNmp9ElOIyDppwtw=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-R8Yk7iYcuZORXmCfFZClqbDxRZgZ9/HEidUuBNdoX8Ptx07cMePnMVJ/woB84lFIDjh2ROHVaOP40Ds3rBXFqg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-F2YYqyvnQNvi320RWZNAvsaWEHwmW3k4OwNJ1hZxRKXupY63expbBaNp6jAgvYs7y/g546vuQnGHQuCBhslhLQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.43.0", "", { "os": "none", "cpu": "arm64" }, "sha512-OE6TdietLXV3F6c7pNIhx/9YC1/2YFwjU9DPc/fbjxIX19hNIaP1rS0cFjCGJlGX+cVJwIKWe8Mos+LdQ1yAJw=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.43.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-0nWK6a7pGkbdoypfVicmV9k/N1FwjPZENoqhlTU+5HhZnAhpIO3za30nEE33u6l6tuy9OVfpdXUqxUgZ+4lbZw=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.43.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-9aokTR4Ft+tRdvgN/pKzSkVy2ksc4/dCpDm9L/xFrbIw0yhLtASLbvoG/5WOTUh/BRPPnfGTsWznEqv0dlOmhA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4bPgdQux2ZLWn3bf2TTXXMHcJB4lenmuxrLqygPmvCJ104Yqzj1UctxSRzR31TiJ4MLaG22RK8dUsVpJtrCz5g=="],
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
|
"@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
|
||||||
@@ -176,56 +238,6 @@
|
|||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
|
|
||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
@@ -234,8 +246,12 @@
|
|||||||
|
|
||||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
|
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
"@types/pngjs": ["@types/pngjs@6.0.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ=="],
|
"@types/pngjs": ["@types/pngjs@6.0.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ=="],
|
||||||
@@ -244,6 +260,26 @@
|
|||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
|
||||||
|
|
||||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg=="],
|
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="],
|
"@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="],
|
||||||
@@ -300,46 +336,38 @@
|
|||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||||
|
|
||||||
"alien-signals": ["alien-signals@3.1.2", "", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="],
|
"alien-signals": ["alien-signals@3.1.2", "", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
|
||||||
|
|
||||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="],
|
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||||
|
|
||||||
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||||
|
|
||||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
|
||||||
|
|
||||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
|
||||||
|
|
||||||
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
||||||
|
|
||||||
"codemirror-lang-dexpr": ["codemirror-lang-dexpr@file:../../rust-expr/editor", { "devDependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.8", "codemirror": "^6.0.2", "tsup": "^8.0.0", "typescript": "^5.0.0" }, "peerDependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }],
|
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
"commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
|
"commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
|
||||||
|
|
||||||
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
|
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
|
||||||
|
|
||||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
|
||||||
|
|
||||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
||||||
@@ -348,10 +376,14 @@
|
|||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
@@ -366,13 +398,45 @@
|
|||||||
|
|
||||||
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
|
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
|
"eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-vue": ["eslint-plugin-vue@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^7.1.0", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "vue-eslint-parser": "^10.0.0" }, "optionalPeers": ["@stylistic/eslint-plugin", "@typescript-eslint/parser"] }, "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA=="],
|
||||||
|
|
||||||
|
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||||
|
|
||||||
|
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
|
|
||||||
|
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||||
|
|
||||||
|
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||||
|
|
||||||
|
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
|
|
||||||
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
|
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
@@ -380,26 +444,44 @@
|
|||||||
|
|
||||||
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="],
|
"happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="],
|
||||||
|
|
||||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
||||||
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
|
||||||
|
|
||||||
"js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js", "js-beautify": "js/bin/js-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="],
|
"js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js", "js-beautify": "js/bin/js-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="],
|
||||||
|
|
||||||
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
||||||
|
|
||||||
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
@@ -424,42 +506,46 @@
|
|||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
|
||||||
|
|
||||||
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
||||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||||
|
|
||||||
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||||
|
|
||||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
"nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
"nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
||||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
||||||
|
"oxfmt": ["oxfmt@0.43.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.43.0", "@oxfmt/binding-android-arm64": "0.43.0", "@oxfmt/binding-darwin-arm64": "0.43.0", "@oxfmt/binding-darwin-x64": "0.43.0", "@oxfmt/binding-freebsd-x64": "0.43.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.43.0", "@oxfmt/binding-linux-arm-musleabihf": "0.43.0", "@oxfmt/binding-linux-arm64-gnu": "0.43.0", "@oxfmt/binding-linux-arm64-musl": "0.43.0", "@oxfmt/binding-linux-ppc64-gnu": "0.43.0", "@oxfmt/binding-linux-riscv64-gnu": "0.43.0", "@oxfmt/binding-linux-riscv64-musl": "0.43.0", "@oxfmt/binding-linux-s390x-gnu": "0.43.0", "@oxfmt/binding-linux-x64-gnu": "0.43.0", "@oxfmt/binding-linux-x64-musl": "0.43.0", "@oxfmt/binding-openharmony-arm64": "0.43.0", "@oxfmt/binding-win32-arm64-msvc": "0.43.0", "@oxfmt/binding-win32-ia32-msvc": "0.43.0", "@oxfmt/binding-win32-x64-msvc": "0.43.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-KTYNG5ISfHSdmeZ25Xzb3qgz9EmQvkaGAxgBY/p38+ZiAet3uZeu7FnMwcSQJg152Qwl0wnYAxDc+Z/H6cvrwA=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
||||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||||
|
|
||||||
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
@@ -474,12 +560,8 @@
|
|||||||
|
|
||||||
"pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
|
"pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
|
||||||
|
|
||||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
|
||||||
|
|
||||||
"pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="],
|
"pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="],
|
||||||
|
|
||||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
|
||||||
|
|
||||||
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
||||||
|
|
||||||
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
||||||
@@ -488,20 +570,18 @@
|
|||||||
|
|
||||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
|
|
||||||
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||||
|
|
||||||
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
|
"proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
|
||||||
|
|
||||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
|
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
|
||||||
|
|
||||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
@@ -512,8 +592,6 @@
|
|||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
||||||
@@ -532,36 +610,34 @@
|
|||||||
|
|
||||||
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
|
|
||||||
|
|
||||||
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
||||||
|
|
||||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
|
||||||
|
|
||||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
|
||||||
|
|
||||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
||||||
|
|
||||||
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||||
|
|
||||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||||
|
|
||||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||||
|
|
||||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
"typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
|
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
|
||||||
|
|
||||||
"vitest": ["vitest@4.1.2", "", { "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", "@vitest/pretty-format": "4.1.2", "@vitest/runner": "4.1.2", "@vitest/snapshot": "4.1.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.2", "@vitest/browser-preview": "4.1.2", "@vitest/browser-webdriverio": "4.1.2", "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg=="],
|
"vitest": ["vitest@4.1.2", "", { "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", "@vitest/pretty-format": "4.1.2", "@vitest/runner": "4.1.2", "@vitest/snapshot": "4.1.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.2", "@vitest/browser-preview": "4.1.2", "@vitest/browser-webdriverio": "4.1.2", "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg=="],
|
||||||
@@ -572,6 +648,8 @@
|
|||||||
|
|
||||||
"vue-component-type-helpers": ["vue-component-type-helpers@2.2.12", "", {}, "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw=="],
|
"vue-component-type-helpers": ["vue-component-type-helpers@2.2.12", "", {}, "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw=="],
|
||||||
|
|
||||||
|
"vue-eslint-parser": ["vue-eslint-parser@10.4.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg=="],
|
||||||
|
|
||||||
"vue-tsc": ["vue-tsc@3.2.6", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q=="],
|
"vue-tsc": ["vue-tsc@3.2.6", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q=="],
|
||||||
|
|
||||||
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||||
@@ -582,19 +660,31 @@
|
|||||||
|
|
||||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
||||||
|
|
||||||
|
"xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="],
|
||||||
|
|
||||||
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@types/ws/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
|
"@types/ws/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"codemirror-lang-dexpr/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||||
|
|
||||||
|
"glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||||
|
|
||||||
"happy-dom/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
|
"happy-dom/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
|
||||||
|
|
||||||
@@ -608,10 +698,6 @@
|
|||||||
|
|
||||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
|
||||||
|
|
||||||
"tsup/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
|
||||||
|
|
||||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
@@ -620,6 +706,10 @@
|
|||||||
|
|
||||||
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="],
|
||||||
|
|
||||||
|
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="],
|
||||||
|
|
||||||
"happy-dom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"happy-dom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
@@ -627,5 +717,9 @@
|
|||||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
frontend/eslint.config.js
Normal file
48
frontend/eslint.config.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import pluginVue from "eslint-plugin-vue";
|
||||||
|
import vueParser from "vue-eslint-parser";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"dist/",
|
||||||
|
"node_modules/",
|
||||||
|
"public/",
|
||||||
|
"src/core/wasm-layout/",
|
||||||
|
"src/core/wasm-pkg-layout/",
|
||||||
|
"src/core/wasm/",
|
||||||
|
"src/core/wasm-pkg/",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...pluginVue.configs["flat/essential"],
|
||||||
|
{
|
||||||
|
files: ["**/*.vue"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: vueParser,
|
||||||
|
parserOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.ts", "**/*.vue"],
|
||||||
|
rules: {
|
||||||
|
// TypeScript zaten undefined globals'ı yakalar, no-undef gereksiz
|
||||||
|
"no-undef": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -10,33 +10,44 @@
|
|||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:visual": "playwright test",
|
"test:visual": "playwright test",
|
||||||
"test:visual:cross": "playwright test --project=cross-renderer"
|
"test:visual:cross": "playwright test --project=cross-renderer",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"lint:fix": "eslint src/ --fix",
|
||||||
|
"format": "oxfmt src/",
|
||||||
|
"format:check": "oxfmt --check src/",
|
||||||
|
"type-check": "vue-tsc -b --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.1",
|
"@codemirror/autocomplete": "^6.20.1",
|
||||||
"@codemirror/language": "^6.12.3",
|
"@codemirror/language": "^6.12.3",
|
||||||
"@codemirror/state": "^6.6.0",
|
"@codemirror/state": "^6.6.0",
|
||||||
"@codemirror/view": "^6.41.0",
|
"@codemirror/view": "^6.41.0",
|
||||||
|
"@duhanbalci/codemirror-lang-dexpr": "0.1.0",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.8",
|
"@lezer/lr": "^1.4.8",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"codemirror-lang-dexpr": "file:../../rust-expr/editor",
|
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.31"
|
"vue": "^3.5.31"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "^6.0.5",
|
||||||
"@vitejs/plugin-vue": "^6.0.5",
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
|
"eslint": "^10.2.0",
|
||||||
|
"eslint-plugin-vue": "^10.8.0",
|
||||||
"happy-dom": "^20.8.9",
|
"happy-dom": "^20.8.9",
|
||||||
|
"oxfmt": "^0.43.0",
|
||||||
"pixelmatch": "^7.1.0",
|
"pixelmatch": "^7.1.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.1",
|
"vite": "^8.0.1",
|
||||||
"vitest": "^4.1.2",
|
"vitest": "^4.1.2",
|
||||||
|
"vue-eslint-parser": "^10.4.0",
|
||||||
"vue-tsc": "^3.2.5"
|
"vue-tsc": "^3.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -6,6 +6,7 @@ import { useEditorStore } from '../../stores/editor'
|
|||||||
import { useLayoutEngine } from '../../composables/useLayoutEngine'
|
import { useLayoutEngine } from '../../composables/useLayoutEngine'
|
||||||
import LayoutRenderer from './LayoutRenderer.vue'
|
import LayoutRenderer from './LayoutRenderer.vue'
|
||||||
import InteractionOverlay from './InteractionOverlay.vue'
|
import InteractionOverlay from './InteractionOverlay.vue'
|
||||||
|
import RulerBar from './RulerBar.vue'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
handleErrors?: boolean
|
handleErrors?: boolean
|
||||||
@@ -204,6 +205,15 @@ function onPointerUp(e: PointerEvent) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="editor-canvas-wrapper">
|
<div class="editor-canvas-wrapper">
|
||||||
|
<!-- Cetvel -->
|
||||||
|
<RulerBar
|
||||||
|
:page-width="templateStore.template.page.width"
|
||||||
|
:page-height="templateStore.template.page.height"
|
||||||
|
:scale="scale"
|
||||||
|
:pan-x="editorStore.panX"
|
||||||
|
:pan-y="editorStore.panY"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Scroll alanı -->
|
<!-- Scroll alanı -->
|
||||||
<div
|
<div
|
||||||
class="editor-canvas"
|
class="editor-canvas"
|
||||||
@@ -252,6 +262,8 @@ function onPointerUp(e: PointerEvent) {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
padding-top: 60px; /* cetvel için üstten ek boşluk */
|
||||||
|
padding-left: 60px; /* cetvel için soldan ek boşluk */
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-canvas__pages {
|
.editor-canvas__pages {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const props = defineProps<{
|
|||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
const templateStore = useTemplateStore()
|
const templateStore = useTemplateStore()
|
||||||
|
|
||||||
const isSelected = computed(() => editorStore.selectedElementId === props.element.id)
|
const isSelected = computed(() => editorStore.isSelected(props.element.id))
|
||||||
const isContainerEl = computed(() => isContainer(props.element))
|
const isContainerEl = computed(() => isContainer(props.element))
|
||||||
const isAbsolute = computed(() => props.element.position.type === 'absolute')
|
const isAbsolute = computed(() => props.element.position.type === 'absolute')
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ function updateChartStyle(key: string, value: unknown) {
|
|||||||
if (!selected.value) return
|
if (!selected.value) return
|
||||||
update({ style: { ...selected.value.style, [key]: value } })
|
update({ style: { ...selected.value.style, [key]: value } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Z-order
|
||||||
|
function bringForward() { if (selected.value) templateStore.bringForward(selected.value.id) }
|
||||||
|
function sendBackward() { if (selected.value) templateStore.sendBackward(selected.value.id) }
|
||||||
|
function bringToFront() { if (selected.value) templateStore.bringToFront(selected.value.id) }
|
||||||
|
function sendToBack() { if (selected.value) templateStore.sendToBack(selected.value.id) }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -409,6 +415,37 @@ function updateChartStyle(key: string, value: unknown) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== Z-Order (tüm elemanlar) ===== -->
|
||||||
|
<template v-if="selected">
|
||||||
|
<div class="et__sep" />
|
||||||
|
<div class="et__group">
|
||||||
|
<button class="et__btn" data-tip="Arkaya Gonder" @click="sendToBack">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
|
<rect x="5" y="5" width="7" height="7" rx="1" fill="currentColor" opacity="0.3"/>
|
||||||
|
<rect x="2" y="2" width="7" height="7" rx="1" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="et__btn" data-tip="Bir Geri" @click="sendBackward">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
|
<rect x="5" y="5" width="7" height="7" rx="1" fill="currentColor" opacity="0.3"/>
|
||||||
|
<rect x="2" y="2" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="et__btn" data-tip="Bir Ileri" @click="bringForward">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
|
<rect x="2" y="2" width="7" height="7" rx="1" fill="currentColor" opacity="0.3"/>
|
||||||
|
<rect x="5" y="5" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="et__btn" data-tip="One Getir" @click="bringToFront">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
|
<rect x="2" y="2" width="7" height="7" rx="1" fill="currentColor" opacity="0.3"/>
|
||||||
|
<rect x="5" y="5" width="7" height="7" rx="1" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -98,8 +98,12 @@ function getElementStyle(el: TemplateElement) {
|
|||||||
function onElementClick(e: PointerEvent, id: string) {
|
function onElementClick(e: PointerEvent, id: string) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (didDrag.value) return
|
if (didDrag.value) return
|
||||||
|
if (e.shiftKey) {
|
||||||
|
editorStore.toggleSelection(id)
|
||||||
|
} else {
|
||||||
editorStore.selectElement(id)
|
editorStore.selectElement(id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onCanvasClick() {
|
function onCanvasClick() {
|
||||||
editorStore.selectElement('root')
|
editorStore.selectElement('root')
|
||||||
@@ -637,7 +641,7 @@ const isAnyDragActive = computed(() =>
|
|||||||
:key="el.id"
|
:key="el.id"
|
||||||
class="element-handle"
|
class="element-handle"
|
||||||
:class="{
|
:class="{
|
||||||
'element-handle--selected': editorStore.selectedElementId === el.id,
|
'element-handle--selected': editorStore.isSelected(el.id),
|
||||||
'element-handle--container': isContainer(el),
|
'element-handle--container': isContainer(el),
|
||||||
'element-handle--dragging': isDragging && dragElementId === el.id,
|
'element-handle--dragging': isDragging && dragElementId === el.id,
|
||||||
'element-handle--drop-target': isContainer(el) && dropTargetContainerId === el.id && isAnyDragActive,
|
'element-handle--drop-target': isContainer(el) && dropTargetContainerId === el.id && isAnyDragActive,
|
||||||
@@ -646,10 +650,10 @@ const isAnyDragActive = computed(() =>
|
|||||||
@pointerdown="(e: PointerEvent) => { onElementClick(e, el.id); onDragStart(e, el) }"
|
@pointerdown="(e: PointerEvent) => { onElementClick(e, el.id); onDragStart(e, el) }"
|
||||||
>
|
>
|
||||||
<!-- Selection border -->
|
<!-- Selection border -->
|
||||||
<div v-if="editorStore.selectedElementId === el.id" class="selection-border" />
|
<div v-if="editorStore.isSelected(el.id)" class="selection-border" />
|
||||||
|
|
||||||
<!-- Resize handles -->
|
<!-- Resize handles (sadece tek seçimde) -->
|
||||||
<template v-if="editorStore.selectedElementId === el.id && !isResizing && el.type !== 'page_break'">
|
<template v-if="editorStore.isSelected(el.id) && editorStore.selectedElementIds.size === 1 && !isResizing && el.type !== 'page_break'">
|
||||||
<template v-if="el.type === 'barcode' || el.type === 'image'">
|
<template v-if="el.type === 'barcode' || el.type === 'image'">
|
||||||
<!-- Barkod/Görsel: sadece yatay resize (aspect ratio korunur) -->
|
<!-- Barkod/Görsel: sadece yatay resize (aspect ratio korunur) -->
|
||||||
<div class="resize-handle resize-handle--e" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'e')" />
|
<div class="resize-handle resize-handle--e" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'e')" />
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ watch(
|
|||||||
:style="{
|
:style="{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
objectFit: 'fill',
|
objectFit: el.style.objectFit || 'fill',
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<div v-else class="layout-el__placeholder">Görsel</div>
|
<div v-else class="layout-el__placeholder">Görsel</div>
|
||||||
|
|||||||
231
frontend/src/components/editor/RulerBar.vue
Normal file
231
frontend/src/components/editor/RulerBar.vue
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** Sayfa genişliği mm */
|
||||||
|
pageWidth: number
|
||||||
|
/** Sayfa yüksekliği mm */
|
||||||
|
pageHeight: number
|
||||||
|
/** mm → px dönüşüm katsayısı (scale * zoom) */
|
||||||
|
scale: number
|
||||||
|
/** Pan offset X (px) */
|
||||||
|
panX: number
|
||||||
|
/** Pan offset Y (px) */
|
||||||
|
panY: number
|
||||||
|
/** Cetvel kalınlığı px */
|
||||||
|
rulerSize?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const RULER_SIZE = computed(() => props.rulerSize ?? 20)
|
||||||
|
|
||||||
|
const hCanvas = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const vCanvas = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
|
function drawRuler(
|
||||||
|
canvas: HTMLCanvasElement | null,
|
||||||
|
direction: 'horizontal' | 'vertical',
|
||||||
|
) {
|
||||||
|
if (!canvas) return
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const size = RULER_SIZE.value
|
||||||
|
|
||||||
|
if (direction === 'horizontal') {
|
||||||
|
const w = canvas.clientWidth
|
||||||
|
canvas.width = w * dpr
|
||||||
|
canvas.height = size * dpr
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
ctx.clearRect(0, 0, w, size)
|
||||||
|
ctx.fillStyle = '#f1f5f9'
|
||||||
|
ctx.fillRect(0, 0, w, size)
|
||||||
|
ctx.strokeStyle = '#e2e8f0'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(0, size - 0.5)
|
||||||
|
ctx.lineTo(w, size - 0.5)
|
||||||
|
ctx.stroke()
|
||||||
|
drawTicks(ctx, direction, w, size)
|
||||||
|
} else {
|
||||||
|
const h = canvas.clientHeight
|
||||||
|
canvas.width = size * dpr
|
||||||
|
canvas.height = h * dpr
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
ctx.clearRect(0, 0, size, h)
|
||||||
|
ctx.fillStyle = '#f1f5f9'
|
||||||
|
ctx.fillRect(0, 0, size, h)
|
||||||
|
ctx.strokeStyle = '#e2e8f0'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(size - 0.5, 0)
|
||||||
|
ctx.lineTo(size - 0.5, h)
|
||||||
|
ctx.stroke()
|
||||||
|
drawTicks(ctx, direction, h, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTicks(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
direction: 'horizontal' | 'vertical',
|
||||||
|
length: number,
|
||||||
|
size: number,
|
||||||
|
) {
|
||||||
|
const s = props.scale
|
||||||
|
const pageMm = direction === 'horizontal' ? props.pageWidth : props.pageHeight
|
||||||
|
const pan = direction === 'horizontal' ? props.panX : props.panY
|
||||||
|
|
||||||
|
// Sayfa başlangıcı: ortaya hizalı + pan
|
||||||
|
// EditorCanvas sayfayı ortalar, ruler da buna uymalı
|
||||||
|
// Yatay: canvas ortası - sayfa genişliği/2
|
||||||
|
// Sayfanın canvas üzerindeki orijin px'i
|
||||||
|
const canvasCenter = direction === 'horizontal'
|
||||||
|
? (length / 2) // flex centering approximation
|
||||||
|
: 40 // EditorCanvas padding-top: 40px
|
||||||
|
|
||||||
|
const pageStartPx = canvasCenter - (pageMm * s) / 2 + pan
|
||||||
|
|
||||||
|
// Tick aralığı belirleme (zoom'a göre)
|
||||||
|
const mmPerPx = 1 / s
|
||||||
|
let tickMm: number
|
||||||
|
if (mmPerPx > 2) tickMm = 50
|
||||||
|
else if (mmPerPx > 1) tickMm = 20
|
||||||
|
else if (mmPerPx > 0.5) tickMm = 10
|
||||||
|
else if (mmPerPx > 0.2) tickMm = 5
|
||||||
|
else tickMm = 1
|
||||||
|
|
||||||
|
ctx.fillStyle = '#94a3b8'
|
||||||
|
ctx.strokeStyle = '#94a3b8'
|
||||||
|
ctx.lineWidth = 0.5
|
||||||
|
ctx.font = '9px system-ui, sans-serif'
|
||||||
|
ctx.textBaseline = 'top'
|
||||||
|
|
||||||
|
// Sayfanın mm aralığını çiz
|
||||||
|
const startMm = 0
|
||||||
|
const endMm = pageMm
|
||||||
|
|
||||||
|
for (let mm = startMm; mm <= endMm; mm += tickMm) {
|
||||||
|
const px = pageStartPx + mm * s
|
||||||
|
|
||||||
|
if (px < -10 || px > length + 10) continue
|
||||||
|
|
||||||
|
const isMajor = mm % 10 === 0
|
||||||
|
const isMedium = mm % 5 === 0
|
||||||
|
|
||||||
|
let tickLen = 4
|
||||||
|
if (isMajor) tickLen = size * 0.6
|
||||||
|
else if (isMedium) tickLen = size * 0.35
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
if (direction === 'horizontal') {
|
||||||
|
ctx.moveTo(px, size)
|
||||||
|
ctx.lineTo(px, size - tickLen)
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(size, px)
|
||||||
|
ctx.lineTo(size - tickLen, px)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Sayı etiketi (her 10mm'de bir)
|
||||||
|
if (isMajor && mm > 0) {
|
||||||
|
const label = String(mm)
|
||||||
|
if (direction === 'horizontal') {
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillText(label, px, 2)
|
||||||
|
} else {
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(2, px)
|
||||||
|
ctx.rotate(-Math.PI / 2)
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillText(label, 0, 0)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sayfa kenar çizgileri (margin göstergesi)
|
||||||
|
ctx.strokeStyle = 'rgba(59, 130, 246, 0.3)'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
const startPx = pageStartPx
|
||||||
|
const endPx = pageStartPx + pageMm * s
|
||||||
|
ctx.beginPath()
|
||||||
|
if (direction === 'horizontal') {
|
||||||
|
ctx.moveTo(startPx, 0)
|
||||||
|
ctx.lineTo(startPx, size)
|
||||||
|
ctx.moveTo(endPx, 0)
|
||||||
|
ctx.lineTo(endPx, size)
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(0, startPx)
|
||||||
|
ctx.lineTo(size, startPx)
|
||||||
|
ctx.moveTo(0, endPx)
|
||||||
|
ctx.lineTo(size, endPx)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function redraw() {
|
||||||
|
drawRuler(hCanvas.value, 'horizontal')
|
||||||
|
drawRuler(vCanvas.value, 'vertical')
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => [props.scale, props.panX, props.panY, props.pageWidth, props.pageHeight], redraw)
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
redraw()
|
||||||
|
const parent = hCanvas.value?.parentElement?.parentElement
|
||||||
|
if (parent) {
|
||||||
|
resizeObserver = new ResizeObserver(() => redraw())
|
||||||
|
resizeObserver.observe(parent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ruler-corner" :style="{ width: `${RULER_SIZE}px`, height: `${RULER_SIZE}px` }" />
|
||||||
|
<canvas
|
||||||
|
ref="hCanvas"
|
||||||
|
class="ruler-h"
|
||||||
|
:style="{ height: `${RULER_SIZE}px` }"
|
||||||
|
/>
|
||||||
|
<canvas
|
||||||
|
ref="vCanvas"
|
||||||
|
class="ruler-v"
|
||||||
|
:style="{ width: `${RULER_SIZE}px` }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ruler-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-right: 1px solid #e2e8f0;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruler-h {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 50;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruler-v {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 50;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -38,11 +38,15 @@ const templateStore = useTemplateStore()
|
|||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
|
|
||||||
const selectedElement = computed(() => {
|
const selectedElement = computed(() => {
|
||||||
const id = editorStore.selectedElementId
|
const ids = editorStore.selectedElementIds
|
||||||
|
if (ids.size !== 1) return null
|
||||||
|
const id = ids.values().next().value
|
||||||
if (!id) return null
|
if (!id) return null
|
||||||
return templateStore.getElementById(id) ?? null
|
return templateStore.getElementById(id) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const multipleSelected = computed(() => editorStore.selectedElementIds.size > 1)
|
||||||
|
|
||||||
const elementTypeLabel = computed(() => {
|
const elementTypeLabel = computed(() => {
|
||||||
const el = selectedElement.value
|
const el = selectedElement.value
|
||||||
if (!el) return ''
|
if (!el) return ''
|
||||||
@@ -87,11 +91,24 @@ function deleteElement() {
|
|||||||
editorStore.clearSelection()
|
editorStore.clearSelection()
|
||||||
templateStore.removeElement(id)
|
templateStore.removeElement(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
const ids = [...editorStore.selectedElementIds]
|
||||||
|
editorStore.clearSelection()
|
||||||
|
for (const id of ids) {
|
||||||
|
if (id !== 'root') templateStore.removeElement(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="properties-panel">
|
<div class="properties-panel">
|
||||||
<div v-if="!selectedElement" class="properties-panel__empty">
|
<div v-if="multipleSelected" class="properties-panel__empty">
|
||||||
|
{{ editorStore.selectedElementIds.size }} eleman secili
|
||||||
|
<button class="prop-delete-btn" style="margin-top: 12px" @click="deleteSelected">Secilenleri Sil</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!selectedElement" class="properties-panel__empty">
|
||||||
Bir eleman secin
|
Bir eleman secin
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useTemplateStore } from '../../stores/template'
|
import { useTemplateStore } from '../../stores/template'
|
||||||
import { useEditorStore } from '../../stores/editor'
|
import { useEditorStore } from '../../stores/editor'
|
||||||
|
import { useSchemaStore } from '../../stores/schema'
|
||||||
import type { ImageElement, TemplateElement } from '../../core/types'
|
import type { ImageElement, TemplateElement } from '../../core/types'
|
||||||
import '../../styles/properties.css'
|
import '../../styles/properties.css'
|
||||||
|
|
||||||
const props = defineProps<{ element: ImageElement }>()
|
const props = defineProps<{ element: ImageElement }>()
|
||||||
const templateStore = useTemplateStore()
|
const templateStore = useTemplateStore()
|
||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
|
const schemaStore = useSchemaStore()
|
||||||
|
|
||||||
|
/** Statik mi dinamik mi? */
|
||||||
|
const isDynamic = computed(() => !!props.element.binding)
|
||||||
|
|
||||||
function update(updates: Partial<TemplateElement>) {
|
function update(updates: Partial<TemplateElement>) {
|
||||||
const id = editorStore.selectedElementId
|
const id = editorStore.selectedElementId
|
||||||
@@ -24,15 +30,47 @@ function onImageFileSelect(e: Event) {
|
|||||||
if (!file) return
|
if (!file) return
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
update({ src: reader.result as string } as Partial<TemplateElement>)
|
update({ src: reader.result as string, binding: undefined } as Partial<TemplateElement>)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMode(mode: 'static' | 'dynamic') {
|
||||||
|
if (mode === 'static') {
|
||||||
|
update({ binding: undefined } as Partial<TemplateElement>)
|
||||||
|
} else {
|
||||||
|
// Dinamik moda geç — ilk uygun alanı seç veya boş bırak
|
||||||
|
const imageFields = schemaStore.scalarFields.filter(f => f.format === 'image' || f.type === 'string')
|
||||||
|
const path = imageFields.length > 0 ? imageFields[0].path : ''
|
||||||
|
update({ src: undefined, binding: { type: 'scalar', path } } as Partial<TemplateElement>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBindingPath(path: string) {
|
||||||
|
update({ binding: { type: 'scalar', path } } as Partial<TemplateElement>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schema'dan görsel olabilecek alanlar (format: image veya string) */
|
||||||
|
const imageScalarFields = computed(() => {
|
||||||
|
return schemaStore.scalarFields.filter(f => f.format === 'image' || f.type === 'string')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="prop-section">
|
<div class="prop-section">
|
||||||
<div class="prop-section__title">Gorsel</div>
|
<div class="prop-section__title">Gorsel</div>
|
||||||
|
|
||||||
|
<!-- Statik / Dinamik toggle -->
|
||||||
|
<div class="prop-row" data-tip="Gorsel kaynagi: dosya veya veri alanından">
|
||||||
|
<label class="prop-label">Mod</label>
|
||||||
|
<div class="prop-toggle-group">
|
||||||
|
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': !isDynamic }" @click="setMode('static')">Statik</button>
|
||||||
|
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': isDynamic }" @click="setMode('dynamic')">Dinamik</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statik: dosya seçimi -->
|
||||||
|
<template v-if="!isDynamic">
|
||||||
<div class="prop-row" data-tip="Gorsel dosyasi secin (PNG, JPG, SVG)">
|
<div class="prop-row" data-tip="Gorsel dosyasi secin (PNG, JPG, SVG)">
|
||||||
<label class="prop-label">Kaynak</label>
|
<label class="prop-label">Kaynak</label>
|
||||||
<label class="prop-file-btn">
|
<label class="prop-file-btn">
|
||||||
@@ -48,6 +86,30 @@ function onImageFileSelect(e: Event) {
|
|||||||
<label class="prop-label"></label>
|
<label class="prop-label"></label>
|
||||||
<button class="prop-clear" @click="update({ src: undefined } as any)">Gorseli kaldir</button>
|
<button class="prop-clear" @click="update({ src: undefined } as any)">Gorseli kaldir</button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Dinamik: schema alan seçimi -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="prop-row" data-tip="Gorsel URL'sinin gelecegi veri alani">
|
||||||
|
<label class="prop-label">Veri Alani</label>
|
||||||
|
<select class="prop-input prop-select"
|
||||||
|
:value="element.binding?.path ?? ''"
|
||||||
|
@change="(e) => setBindingPath((e.target as HTMLSelectElement).value)">
|
||||||
|
<option value="" disabled>Secin...</option>
|
||||||
|
<option
|
||||||
|
v-for="field in imageScalarFields"
|
||||||
|
:key="field.path"
|
||||||
|
:value="field.path"
|
||||||
|
>{{ field.title }} ({{ field.path }})</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-if="element.binding?.path" class="prop-row">
|
||||||
|
<label class="prop-label">Path</label>
|
||||||
|
<span class="prop-info">{{ element.binding.path }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Sığdırma modu (ortak) -->
|
||||||
<div class="prop-row" data-tip="Gorselin alana sigdirma modu">
|
<div class="prop-row" data-tip="Gorselin alana sigdirma modu">
|
||||||
<label class="prop-label">Sigdirma</label>
|
<label class="prop-label">Sigdirma</label>
|
||||||
<select class="prop-input prop-select"
|
<select class="prop-input prop-select"
|
||||||
@@ -60,3 +122,42 @@ function onImageFileSelect(e: Event) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.prop-toggle-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-toggle-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-toggle-btn:first-child {
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-toggle-btn:last-child {
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-toggle-btn--active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-info {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -4,10 +4,23 @@ import type { LayoutResult, LayoutMapEntry } from '../core/layout-types'
|
|||||||
|
|
||||||
export type { LayoutMapEntry }
|
export type { LayoutMapEntry }
|
||||||
|
|
||||||
|
/** Discriminated union for all messages the layout worker can send back */
|
||||||
|
type WorkerResponse =
|
||||||
|
| { type: 'result'; layout: LayoutResult; id: number }
|
||||||
|
| { type: 'error'; error: string; id: number }
|
||||||
|
| { type: 'barcode-result'; width: number; height: number; rgba: ArrayBuffer; id: number }
|
||||||
|
| { type: 'barcode-error'; error: string; id: number }
|
||||||
|
|
||||||
|
export interface LayoutEngineOptions {
|
||||||
|
/** Font API base URL. Default: '/api/fonts' */
|
||||||
|
fontApiBase?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function useLayoutEngine(
|
export function useLayoutEngine(
|
||||||
template: Ref<Template>,
|
template: Ref<Template>,
|
||||||
data: Ref<Record<string, unknown>>,
|
data: Ref<Record<string, unknown>>,
|
||||||
layoutVersion?: Ref<number>,
|
layoutVersion?: Ref<number>,
|
||||||
|
options?: LayoutEngineOptions,
|
||||||
) {
|
) {
|
||||||
const layout = ref<LayoutResult | null>(null)
|
const layout = ref<LayoutResult | null>(null)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
@@ -24,7 +37,12 @@ export function useLayoutEngine(
|
|||||||
type: 'module',
|
type: 'module',
|
||||||
})
|
})
|
||||||
|
|
||||||
worker.onmessage = (e: MessageEvent<any>) => {
|
// Configure font API base if provided
|
||||||
|
if (options?.fontApiBase) {
|
||||||
|
worker.postMessage({ type: 'configure', fontApiBase: options.fontApiBase })
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<WorkerResponse>) => {
|
||||||
const msg = e.data
|
const msg = e.data
|
||||||
|
|
||||||
// Barcode yanıtları
|
// Barcode yanıtları
|
||||||
@@ -36,7 +54,8 @@ export function useLayoutEngine(
|
|||||||
if (msg.id !== requestId) return
|
if (msg.id !== requestId) return
|
||||||
|
|
||||||
computing.value = false
|
computing.value = false
|
||||||
if (msg.type === 'result' && msg.layout) {
|
switch (msg.type) {
|
||||||
|
case 'result': {
|
||||||
layout.value = msg.layout
|
layout.value = msg.layout
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
@@ -50,8 +69,11 @@ export function useLayoutEngine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
layoutMap.value = map
|
layoutMap.value = map
|
||||||
} else if (msg.type === 'error') {
|
break
|
||||||
|
}
|
||||||
|
case 'error':
|
||||||
error.value = msg.error ?? 'Bilinmeyen layout hatası'
|
error.value = msg.error ?? 'Bilinmeyen layout hatası'
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,25 +127,34 @@ export function useLayoutEngine(
|
|||||||
if (!worker) initWorker()
|
if (!worker) initWorker()
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
barcodeReqId++
|
barcodeReqId++
|
||||||
const id = barcodeReqId + 100000 // compile id'leriyle çakışmasın
|
const id = barcodeReqId
|
||||||
barcodeCallbacks.set(id, resolve)
|
const timeout = setTimeout(() => {
|
||||||
|
barcodeCallbacks.delete(id)
|
||||||
|
resolve(null)
|
||||||
|
}, 5000)
|
||||||
|
barcodeCallbacks.set(id, (result) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
worker!.postMessage({ type: 'barcode', format, value, width, height, includeText, id })
|
worker!.postMessage({ type: 'barcode', format, value, width, height, includeText, id })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBarcodeResponse(msg: any) {
|
function handleBarcodeResponse(msg: Extract<WorkerResponse, { type: 'barcode-result' } | { type: 'barcode-error' }>) {
|
||||||
if (msg.type === 'barcode-result' || msg.type === 'barcode-error') {
|
|
||||||
const cb = barcodeCallbacks.get(msg.id)
|
const cb = barcodeCallbacks.get(msg.id)
|
||||||
if (cb) {
|
if (cb) {
|
||||||
barcodeCallbacks.delete(msg.id)
|
barcodeCallbacks.delete(msg.id)
|
||||||
cb(msg.type === 'barcode-result' ? { width: msg.width, height: msg.height, rgba: msg.rgba } : null)
|
cb(msg.type === 'barcode-result' ? { width: msg.width, height: msg.height, rgba: msg.rgba } : null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function dispose() {
|
function dispose() {
|
||||||
worker?.terminate()
|
worker?.terminate()
|
||||||
worker = null
|
worker = null
|
||||||
|
// Bekleyen barcode promise'lerini null ile resolve et
|
||||||
|
for (const cb of barcodeCallbacks.values()) {
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
barcodeCallbacks.clear()
|
barcodeCallbacks.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function useUndoRedo<T>(source: Ref<T>, maxHistory = 50) {
|
|||||||
|
|
||||||
function applySnapshot(snap: string) {
|
function applySnapshot(snap: string) {
|
||||||
skipWatch = true
|
skipWatch = true
|
||||||
Object.assign(source.value as object, JSON.parse(snap))
|
source.value = JSON.parse(snap)
|
||||||
skipWatch = false
|
skipWatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ export interface Template {
|
|||||||
// --- Editor state ---
|
// --- Editor state ---
|
||||||
|
|
||||||
export interface EditorState {
|
export interface EditorState {
|
||||||
selectedElementId: string | null
|
selectedElementIds: Set<string>
|
||||||
zoom: number // 0.25 - 4.0
|
zoom: number // 0.25 - 4.0
|
||||||
panX: number
|
panX: number
|
||||||
panY: number
|
panY: number
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ function onKeyDown(e: KeyboardEvent) {
|
|||||||
const tag = target?.tagName
|
const tag = target?.tagName
|
||||||
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable
|
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable
|
||||||
|
|
||||||
// Delete / Backspace
|
// Delete / Backspace — çoklu seçim desteği
|
||||||
if ((e.key === 'Delete' || e.key === 'Backspace') && editorStore.selectedElementId) {
|
if ((e.key === 'Delete' || e.key === 'Backspace') && editorStore.selectedElementIds.size > 0) {
|
||||||
if (isInput) return
|
if (isInput) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const id = editorStore.selectedElementId
|
const ids = [...editorStore.selectedElementIds]
|
||||||
if (id && id !== 'root') {
|
|
||||||
editorStore.clearSelection()
|
editorStore.clearSelection()
|
||||||
templateStore.removeElement(id)
|
for (const id of ids) {
|
||||||
|
if (id !== 'root') templateStore.removeElement(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +114,23 @@ function onKeyDown(e: KeyboardEvent) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
templateStore.redo()
|
templateStore.redo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Z-Order kısayolları
|
||||||
|
if ((e.ctrlKey || e.metaKey) && editorStore.selectedElementId && editorStore.selectedElementId !== 'root') {
|
||||||
|
if (e.key === ']' && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
templateStore.bringToFront(editorStore.selectedElementId)
|
||||||
|
} else if (e.key === ']') {
|
||||||
|
e.preventDefault()
|
||||||
|
templateStore.bringForward(editorStore.selectedElementId)
|
||||||
|
} else if (e.key === '[' && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
templateStore.sendToBack(editorStore.selectedElementId)
|
||||||
|
} else if (e.key === '[') {
|
||||||
|
e.preventDefault()
|
||||||
|
templateStore.sendBackward(editorStore.selectedElementId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browser'ın native pinch-zoom'unu editör alanında engelle
|
// Browser'ın native pinch-zoom'unu editör alanında engelle
|
||||||
|
|||||||
523
frontend/src/stores/__tests__/improvements.test.ts
Normal file
523
frontend/src/stores/__tests__/improvements.test.ts
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
/**
|
||||||
|
* IMPROVEMENTS.md bölüm 1, 2, 3 implementasyonlarının testleri.
|
||||||
|
*
|
||||||
|
* Bölüm 1: Kritik Buglar (1.1–1.4)
|
||||||
|
* Bölüm 2: Önemli Teknik Sorunlar (2.9, 2.11)
|
||||||
|
* Bölüm 3: Eksik Özellikler (3.1, 3.2)
|
||||||
|
*
|
||||||
|
* Not: Rust tarafı testleri layout-engine/tests/improvements_test.rs dosyasındadır.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
import { setActivePinia, createPinia } from 'pinia'
|
||||||
|
import { useTemplateStore } from '../template'
|
||||||
|
import { useEditorStore } from '../editor'
|
||||||
|
import type { Template, StaticTextElement, ContainerElement, ImageElement, TemplateElement } from '../../core/types'
|
||||||
|
import { sz } from '../../core/types'
|
||||||
|
|
||||||
|
function createTestTemplate(): Template {
|
||||||
|
return {
|
||||||
|
id: 'test',
|
||||||
|
name: 'Test',
|
||||||
|
page: { width: 210, height: 297 },
|
||||||
|
fonts: ['Noto Sans'],
|
||||||
|
root: {
|
||||||
|
id: 'root',
|
||||||
|
type: 'container' as const,
|
||||||
|
position: { type: 'flow' as const },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
direction: 'column' as const,
|
||||||
|
gap: 5,
|
||||||
|
padding: { top: 10, right: 10, bottom: 10, left: 10 },
|
||||||
|
align: 'stretch' as const,
|
||||||
|
justify: 'start' as const,
|
||||||
|
style: {},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTextElement(id: string, content: string): StaticTextElement {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: 'static_text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 12 },
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.1 Undo/Redo — Object.assign yerine reference replacement
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('1.1 Undo/Redo reference replacement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('undo properly removes keys that were added after snapshot', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
// Snapshot al (debounce beklenmeli)
|
||||||
|
await vi.advanceTimersByTimeAsync(400)
|
||||||
|
|
||||||
|
// Header ekle
|
||||||
|
store.enableHeader()
|
||||||
|
expect(store.template.header).toBeDefined()
|
||||||
|
|
||||||
|
// Snapshot al
|
||||||
|
await vi.advanceTimersByTimeAsync(400)
|
||||||
|
|
||||||
|
// Undo: header eklenmeden önceki state'e dön
|
||||||
|
store.undo()
|
||||||
|
expect(store.template.header).toBeUndefined()
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('undo properly removes footer key', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(400)
|
||||||
|
|
||||||
|
store.enableFooter()
|
||||||
|
expect(store.template.footer).toBeDefined()
|
||||||
|
await vi.advanceTimersByTimeAsync(400)
|
||||||
|
|
||||||
|
store.undo()
|
||||||
|
expect(store.template.footer).toBeUndefined()
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redo restores the removed key after undo', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(400)
|
||||||
|
|
||||||
|
store.enableHeader()
|
||||||
|
await vi.advanceTimersByTimeAsync(400)
|
||||||
|
|
||||||
|
store.undo()
|
||||||
|
expect(store.template.header).toBeUndefined()
|
||||||
|
|
||||||
|
store.redo()
|
||||||
|
expect(store.template.header).toBeDefined()
|
||||||
|
expect(store.template.header!.id).toBe('header')
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.3 Image objectFit — LayoutRenderer'da style.objectFit okunmalı
|
||||||
|
// (Birim test olarak ImageElement tipi üzerinden doğrulanır)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('1.3 Image objectFit', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ImageElement stores objectFit in style', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
const img: ImageElement = {
|
||||||
|
id: 'img_1',
|
||||||
|
type: 'image',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fixed(50), height: sz.fixed(30) },
|
||||||
|
src: 'data:image/png;base64,abc',
|
||||||
|
style: { objectFit: 'contain' },
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addChild('root', img as unknown as TemplateElement)
|
||||||
|
|
||||||
|
const el = store.getElementById('img_1') as ImageElement
|
||||||
|
expect(el.style.objectFit).toBe('contain')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updateElement changes objectFit', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
const img: ImageElement = {
|
||||||
|
id: 'img_2',
|
||||||
|
type: 'image',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fixed(50), height: sz.fixed(30) },
|
||||||
|
src: 'data:image/png;base64,abc',
|
||||||
|
style: { objectFit: 'contain' },
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addChild('root', img as unknown as TemplateElement)
|
||||||
|
store.updateElement('img_2', { style: { objectFit: 'cover' } } as Partial<TemplateElement>)
|
||||||
|
|
||||||
|
const el = store.getElementById('img_2') as ImageElement
|
||||||
|
expect(el.style.objectFit).toBe('cover')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2.9 importTemplate validasyon
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('2.9 importTemplate validation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on invalid JSON', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
expect(() => store.importTemplate('not json')).toThrow('Geçersiz JSON')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on missing root', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
const bad = JSON.stringify({ page: { width: 210, height: 297 } })
|
||||||
|
expect(() => store.importTemplate(bad)).toThrow('root')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on root that is not container', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
const bad = JSON.stringify({
|
||||||
|
root: { type: 'text', id: 'r' },
|
||||||
|
page: { width: 210, height: 297 },
|
||||||
|
})
|
||||||
|
expect(() => store.importTemplate(bad)).toThrow('container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on missing page', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
const bad = JSON.stringify({
|
||||||
|
root: { type: 'container', id: 'root', children: [] },
|
||||||
|
})
|
||||||
|
expect(() => store.importTemplate(bad)).toThrow('page')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on invalid page dimensions', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
const bad = JSON.stringify({
|
||||||
|
root: { type: 'container', id: 'root', children: [] },
|
||||||
|
page: { width: 'abc', height: 297 },
|
||||||
|
})
|
||||||
|
expect(() => store.importTemplate(bad)).toThrow('page')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves previous state on failed import', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
store.addChild('root', createTextElement('keep_me', 'Keep'))
|
||||||
|
|
||||||
|
try {
|
||||||
|
store.importTemplate('invalid json')
|
||||||
|
} catch {
|
||||||
|
// beklenen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Önceki state korunmuş olmalı
|
||||||
|
expect(store.getElementById('keep_me')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts valid template JSON', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
const tpl = createTestTemplate()
|
||||||
|
tpl.name = 'Valid Import'
|
||||||
|
const json = JSON.stringify(tpl)
|
||||||
|
|
||||||
|
store.importTemplate(json)
|
||||||
|
expect(store.template.name).toBe('Valid Import')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2.11 moveElement — tek layoutVersion bump
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('2.11 moveElement single layoutVersion bump', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moveElement increments layoutVersion exactly once', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
// İç içe container yapısı oluştur
|
||||||
|
const child: ContainerElement = {
|
||||||
|
id: 'child_container',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
direction: 'column',
|
||||||
|
gap: 0,
|
||||||
|
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||||
|
align: 'stretch',
|
||||||
|
justify: 'start',
|
||||||
|
style: {},
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
store.addChild('root', child as unknown as TemplateElement)
|
||||||
|
store.addChild('root', createTextElement('el_move', 'Move me'))
|
||||||
|
|
||||||
|
const versionBefore = store.layoutVersion
|
||||||
|
|
||||||
|
store.moveElement('el_move', 'child_container')
|
||||||
|
|
||||||
|
// Tek bump: tam olarak 1 artmalı
|
||||||
|
expect(store.layoutVersion).toBe(versionBefore + 1)
|
||||||
|
|
||||||
|
// Eleman taşınmış olmalı
|
||||||
|
const moved = store.getElementById('el_move')
|
||||||
|
expect(moved).toBeDefined()
|
||||||
|
const parent = store.getParent('el_move')
|
||||||
|
expect(parent?.id).toBe('child_container')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3.1 Çoklu Seçim (Multi-Selection)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('3.1 Multi-Selection', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectedElementIds starts empty', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
expect(store.selectedElementIds.size).toBe(0)
|
||||||
|
expect(store.selectedElementId).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectElement sets single selection', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
store.selectElement('el_1')
|
||||||
|
expect(store.selectedElementIds.size).toBe(1)
|
||||||
|
expect(store.selectedElementId).toBe('el_1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectElement clears previous selection', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
store.selectElement('el_1')
|
||||||
|
store.selectElement('el_2')
|
||||||
|
expect(store.selectedElementIds.size).toBe(1)
|
||||||
|
expect(store.selectedElementId).toBe('el_2')
|
||||||
|
expect(store.isSelected('el_1')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggleSelection adds to selection', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
store.selectElement('el_1')
|
||||||
|
store.toggleSelection('el_2')
|
||||||
|
expect(store.selectedElementIds.size).toBe(2)
|
||||||
|
expect(store.isSelected('el_1')).toBe(true)
|
||||||
|
expect(store.isSelected('el_2')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggleSelection removes from selection', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
store.selectElement('el_1')
|
||||||
|
store.toggleSelection('el_2')
|
||||||
|
store.toggleSelection('el_1')
|
||||||
|
expect(store.selectedElementIds.size).toBe(1)
|
||||||
|
expect(store.isSelected('el_1')).toBe(false)
|
||||||
|
expect(store.isSelected('el_2')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clearSelection clears all', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
store.selectElement('el_1')
|
||||||
|
store.toggleSelection('el_2')
|
||||||
|
store.toggleSelection('el_3')
|
||||||
|
expect(store.selectedElementIds.size).toBe(3)
|
||||||
|
|
||||||
|
store.clearSelection()
|
||||||
|
expect(store.selectedElementIds.size).toBe(0)
|
||||||
|
expect(store.selectedElementId).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('isSelected returns correct state', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
expect(store.isSelected('el_1')).toBe(false)
|
||||||
|
store.selectElement('el_1')
|
||||||
|
expect(store.isSelected('el_1')).toBe(true)
|
||||||
|
expect(store.isSelected('el_2')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectedElementId returns first selected (backward compat)', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
store.selectElement('el_1')
|
||||||
|
store.toggleSelection('el_2')
|
||||||
|
// İlk eklenen eleman
|
||||||
|
expect(store.selectedElementId).toBe('el_1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectElement(null) clears selection', () => {
|
||||||
|
const store = useEditorStore()
|
||||||
|
store.selectElement('el_1')
|
||||||
|
store.selectElement(null)
|
||||||
|
expect(store.selectedElementIds.size).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3.2 Z-Order Kontrolleri
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('3.2 Z-Order controls', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
function setupThreeElements() {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
store.addChild('root', createTextElement('a', 'A'))
|
||||||
|
store.addChild('root', createTextElement('b', 'B'))
|
||||||
|
store.addChild('root', createTextElement('c', 'C'))
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
it('bringForward moves element one step up', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
// Sıra: [a, b, c] → bringForward(a) → [b, a, c]
|
||||||
|
store.bringForward('a')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'a', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sendBackward moves element one step down', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
// Sıra: [a, b, c] → sendBackward(c) → [a, c, b]
|
||||||
|
store.sendBackward('c')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'c', 'b'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bringToFront moves element to end', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
// Sıra: [a, b, c] → bringToFront(a) → [b, c, a]
|
||||||
|
store.bringToFront('a')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'c', 'a'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sendToBack moves element to beginning', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
// Sıra: [a, b, c] → sendToBack(c) → [c, a, b]
|
||||||
|
store.sendToBack('c')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['c', 'a', 'b'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bringForward on last element is no-op', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
store.bringForward('c')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sendBackward on first element is no-op', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
store.sendBackward('a')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bringToFront on last element is no-op', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
store.bringToFront('c')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sendToBack on first element is no-op', () => {
|
||||||
|
const store = setupThreeElements()
|
||||||
|
store.sendToBack('a')
|
||||||
|
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3.3 Dinamik Image Binding
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('3.3 Dynamic Image Binding', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ImageElement supports binding field', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
const img: ImageElement = {
|
||||||
|
id: 'img_dyn',
|
||||||
|
type: 'image',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fixed(40), height: sz.fixed(40) },
|
||||||
|
binding: { type: 'scalar', path: 'firma.logo' },
|
||||||
|
style: { objectFit: 'contain' },
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addChild('root', img as unknown as TemplateElement)
|
||||||
|
|
||||||
|
const el = store.getElementById('img_dyn') as ImageElement
|
||||||
|
expect(el.binding).toBeDefined()
|
||||||
|
expect(el.binding!.path).toBe('firma.logo')
|
||||||
|
expect(el.src).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can switch from static to dynamic mode', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
const img: ImageElement = {
|
||||||
|
id: 'img_switch',
|
||||||
|
type: 'image',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fixed(40), height: sz.fixed(40) },
|
||||||
|
src: 'data:image/png;base64,abc',
|
||||||
|
style: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addChild('root', img as unknown as TemplateElement)
|
||||||
|
|
||||||
|
// Dinamik moda geç
|
||||||
|
store.updateElement('img_switch', {
|
||||||
|
src: undefined,
|
||||||
|
binding: { type: 'scalar', path: 'firma.logo' },
|
||||||
|
} as Partial<TemplateElement>)
|
||||||
|
|
||||||
|
const el = store.getElementById('img_switch') as ImageElement
|
||||||
|
expect(el.binding).toBeDefined()
|
||||||
|
expect(el.binding!.path).toBe('firma.logo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can switch from dynamic to static mode', () => {
|
||||||
|
const store = useTemplateStore()
|
||||||
|
store.template = createTestTemplate()
|
||||||
|
|
||||||
|
const img: ImageElement = {
|
||||||
|
id: 'img_back',
|
||||||
|
type: 'image',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fixed(40), height: sz.fixed(40) },
|
||||||
|
binding: { type: 'scalar', path: 'firma.logo' },
|
||||||
|
style: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addChild('root', img as unknown as TemplateElement)
|
||||||
|
|
||||||
|
store.updateElement('img_back', {
|
||||||
|
binding: undefined,
|
||||||
|
src: 'data:image/png;base64,xyz',
|
||||||
|
} as Partial<TemplateElement>)
|
||||||
|
|
||||||
|
const el = store.getElementById('img_back') as ImageElement
|
||||||
|
expect(el.src).toBe('data:image/png;base64,xyz')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,7 +3,8 @@ import { ref, computed } from 'vue'
|
|||||||
import type { TemplateElement } from '../core/types'
|
import type { TemplateElement } from '../core/types'
|
||||||
|
|
||||||
export const useEditorStore = defineStore('editor', () => {
|
export const useEditorStore = defineStore('editor', () => {
|
||||||
const selectedElementId = ref<string | null>(null)
|
/** Seçili eleman ID'leri — çoklu seçim desteği */
|
||||||
|
const selectedElementIds = ref<Set<string>>(new Set())
|
||||||
const zoom = ref(1)
|
const zoom = ref(1)
|
||||||
const panX = ref(0)
|
const panX = ref(0)
|
||||||
const panY = ref(0)
|
const panY = ref(0)
|
||||||
@@ -15,12 +16,36 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
const zoomPercent = computed(() => Math.round(zoom.value * 100))
|
const zoomPercent = computed(() => Math.round(zoom.value * 100))
|
||||||
|
|
||||||
|
/** Geriye uyumluluk: tek seçili eleman ID'si (ilk seçili veya null) */
|
||||||
|
const selectedElementId = computed<string | null>(() => {
|
||||||
|
const ids = selectedElementIds.value
|
||||||
|
if (ids.size === 0) return null
|
||||||
|
return ids.values().next().value ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Tek eleman seç (önceki seçimi temizler) */
|
||||||
function selectElement(id: string | null) {
|
function selectElement(id: string | null) {
|
||||||
selectedElementId.value = id
|
selectedElementIds.value = id ? new Set([id]) : new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shift+click: seçime ekle/çıkar (toggle) */
|
||||||
|
function toggleSelection(id: string) {
|
||||||
|
const next = new Set(selectedElementIds.value)
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id)
|
||||||
|
} else {
|
||||||
|
next.add(id)
|
||||||
|
}
|
||||||
|
selectedElementIds.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Eleman seçili mi? */
|
||||||
|
function isSelected(id: string): boolean {
|
||||||
|
return selectedElementIds.value.has(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
selectedElementId.value = null
|
selectedElementIds.value = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
function setZoom(value: number) {
|
function setZoom(value: number) {
|
||||||
@@ -56,6 +81,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
selectedElementIds,
|
||||||
selectedElementId,
|
selectedElementId,
|
||||||
zoom,
|
zoom,
|
||||||
panX,
|
panX,
|
||||||
@@ -65,6 +91,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
dropTargetContainerId,
|
dropTargetContainerId,
|
||||||
zoomPercent,
|
zoomPercent,
|
||||||
selectElement,
|
selectElement,
|
||||||
|
toggleSelection,
|
||||||
|
isSelected,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
setZoom,
|
setZoom,
|
||||||
setPan,
|
setPan,
|
||||||
|
|||||||
@@ -139,14 +139,27 @@ export const useTemplateStore = defineStore('template', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Element'i başka bir container'a taşı */
|
/** Element'i başka bir container'a taşı (tek layoutVersion bump) */
|
||||||
function moveElement(elementId: string, targetParentId: string, index?: number) {
|
function moveElement(elementId: string, targetParentId: string, index?: number) {
|
||||||
const el = getElementById(elementId)
|
const el = getElementById(elementId)
|
||||||
if (!el) return
|
if (!el) return
|
||||||
// removeElement bump'lar, addChild de bump'lar — ama tek mantıksal operasyon.
|
// Ağaçtan kaldır (bump'sız)
|
||||||
// Fazladan 1 bump sorun değil (debounce var), ama istersek optimize edebiliriz.
|
const parent = getParent(elementId)
|
||||||
removeElement(elementId)
|
if (parent) {
|
||||||
addChild(targetParentId, el, index)
|
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||||
|
if (idx !== -1) parent.children.splice(idx, 1)
|
||||||
|
}
|
||||||
|
// Hedef container'a ekle (bump'sız)
|
||||||
|
const target = getElementById(targetParentId)
|
||||||
|
if (target && isContainer(target)) {
|
||||||
|
if (index !== undefined) {
|
||||||
|
target.children.splice(index, 0, el)
|
||||||
|
} else {
|
||||||
|
target.children.push(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tek bump
|
||||||
|
bumpLayoutVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Absolute pozisyon güncelle */
|
/** Absolute pozisyon güncelle */
|
||||||
@@ -185,14 +198,62 @@ export const useTemplateStore = defineStore('template', () => {
|
|||||||
bumpLayoutVersion()
|
bumpLayoutVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bir adım öne getir */
|
||||||
|
function bringForward(elementId: string) {
|
||||||
|
const parent = getParent(elementId)
|
||||||
|
if (!parent) return
|
||||||
|
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||||
|
if (idx < 0 || idx >= parent.children.length - 1) return
|
||||||
|
reorderChild(parent.id, idx, idx + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bir adım arkaya gönder */
|
||||||
|
function sendBackward(elementId: string) {
|
||||||
|
const parent = getParent(elementId)
|
||||||
|
if (!parent) return
|
||||||
|
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||||
|
if (idx <= 0) return
|
||||||
|
reorderChild(parent.id, idx, idx - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** En öne getir */
|
||||||
|
function bringToFront(elementId: string) {
|
||||||
|
const parent = getParent(elementId)
|
||||||
|
if (!parent) return
|
||||||
|
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||||
|
if (idx < 0 || idx >= parent.children.length - 1) return
|
||||||
|
reorderChild(parent.id, idx, parent.children.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** En arkaya gönder */
|
||||||
|
function sendToBack(elementId: string) {
|
||||||
|
const parent = getParent(elementId)
|
||||||
|
if (!parent) return
|
||||||
|
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||||
|
if (idx <= 0) return
|
||||||
|
reorderChild(parent.id, idx, 0)
|
||||||
|
}
|
||||||
|
|
||||||
/** Şablonu JSON olarak dışa aktar */
|
/** Şablonu JSON olarak dışa aktar */
|
||||||
function exportTemplate(): string {
|
function exportTemplate(): string {
|
||||||
return JSON.stringify(template.value, null, 2)
|
return JSON.stringify(template.value, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** JSON'dan şablon yükle */
|
/** JSON'dan şablon yükle (validasyonlu) */
|
||||||
function importTemplate(json: string) {
|
function importTemplate(json: string) {
|
||||||
const parsed = JSON.parse(json) as Template
|
let parsed: Template
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(json) as Template
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Geçersiz JSON: ${e instanceof Error ? e.message : String(e)}`, { cause: e })
|
||||||
|
}
|
||||||
|
// Minimum schema doğrulaması
|
||||||
|
if (!parsed.root || parsed.root.type !== 'container') {
|
||||||
|
throw new Error('Geçersiz şablon: root alanı eksik veya container değil')
|
||||||
|
}
|
||||||
|
if (!parsed.page || typeof parsed.page.width !== 'number' || typeof parsed.page.height !== 'number') {
|
||||||
|
throw new Error('Geçersiz şablon: page alanı eksik veya geçersiz')
|
||||||
|
}
|
||||||
template.value = parsed
|
template.value = parsed
|
||||||
bumpLayoutVersion()
|
bumpLayoutVersion()
|
||||||
}
|
}
|
||||||
@@ -269,6 +330,10 @@ export const useTemplateStore = defineStore('template', () => {
|
|||||||
updateElementSize,
|
updateElementSize,
|
||||||
updateElement,
|
updateElement,
|
||||||
reorderChild,
|
reorderChild,
|
||||||
|
bringForward,
|
||||||
|
sendBackward,
|
||||||
|
bringToFront,
|
||||||
|
sendToBack,
|
||||||
exportTemplate,
|
exportTemplate,
|
||||||
importTemplate,
|
importTemplate,
|
||||||
resetTemplate,
|
resetTemplate,
|
||||||
|
|||||||
@@ -1,38 +1,126 @@
|
|||||||
/// Layout Engine Web Worker
|
/// Layout Engine Web Worker
|
||||||
/// Template JSON + Data JSON → Layout WASM → LayoutResult
|
/// Template JSON + Data JSON → Layout WASM → LayoutResult
|
||||||
|
/// Font loading is dynamic — fetches from backend API based on template needs.
|
||||||
|
|
||||||
import init, { loadFonts, computeLayout, generateBarcode } from '../core/wasm-layout/dreport_layout.js'
|
import init, { loadFonts, addFonts, computeLayout, generateBarcode } from '../core/wasm-layout/dreport_layout.js'
|
||||||
import type { LayoutResult } from '../core/layout-types'
|
import type { LayoutResult } from '../core/layout-types'
|
||||||
|
|
||||||
let initPromise: Promise<void> | null = null
|
let initPromise: Promise<void> | null = null
|
||||||
|
|
||||||
const FONT_FILES = [
|
/** Configurable font API base URL. Default: same origin /api/fonts */
|
||||||
{ path: '/fonts/NotoSans-Regular.ttf', family: 'Noto Sans' },
|
let fontApiBase = '/api/fonts'
|
||||||
{ path: '/fonts/NotoSans-Bold.ttf', family: 'Noto Sans' },
|
|
||||||
{ path: '/fonts/NotoSans-Italic.ttf', family: 'Noto Sans' },
|
/** Font catalog from backend API */
|
||||||
{ path: '/fonts/NotoSans-BoldItalic.ttf', family: 'Noto Sans' },
|
interface FontVariantInfo {
|
||||||
{ path: '/fonts/NotoSansMono-Regular.ttf', family: 'Noto Sans Mono' },
|
weight: number
|
||||||
]
|
italic: boolean
|
||||||
|
}
|
||||||
|
interface FontFamilyInfo {
|
||||||
|
family: string
|
||||||
|
variants: FontVariantInfo[]
|
||||||
|
}
|
||||||
|
let fontCatalog: FontFamilyInfo[] = []
|
||||||
|
|
||||||
|
/** Track which font families are already loaded into WASM */
|
||||||
|
const loadedFamilies = new Set<string>()
|
||||||
|
|
||||||
async function doInit() {
|
async function doInit() {
|
||||||
console.log('[layout-worker] WASM başlatılıyor...')
|
console.log('[layout-worker] WASM başlatılıyor...')
|
||||||
await init({ module_or_path: '/wasm/dreport_layout_bg.wasm' })
|
await init({ module_or_path: '/wasm/dreport_layout_bg.wasm' })
|
||||||
|
|
||||||
console.log('[layout-worker] Fontlar yükleniyor...')
|
// Fetch font catalog from backend
|
||||||
const families: string[] = []
|
try {
|
||||||
const buffers: Uint8Array[] = []
|
const res = await fetch(fontApiBase)
|
||||||
|
if (res.ok) {
|
||||||
|
fontCatalog = await res.json()
|
||||||
|
console.log(`[layout-worker] Font kataloğu yüklendi (${fontCatalog.length} aile)`)
|
||||||
|
} else {
|
||||||
|
console.warn(`[layout-worker] Font kataloğu alınamadı (HTTP ${res.status}), static fallback deneniyor`)
|
||||||
|
await loadStaticFallback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.warn('[layout-worker] Font API erişilemedi, static fallback deneniyor')
|
||||||
|
await loadStaticFallback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load default fonts (Noto Sans + Noto Sans Mono)
|
||||||
|
await ensureFamiliesLoaded(['Noto Sans', 'Noto Sans Mono'])
|
||||||
|
console.log('[layout-worker] Hazır')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fallback: load fonts from static /fonts/ directory (backwards compat) */
|
||||||
|
async function loadStaticFallback() {
|
||||||
|
const STATIC_FONTS = [
|
||||||
|
'/fonts/NotoSans-Regular.ttf',
|
||||||
|
'/fonts/NotoSans-Bold.ttf',
|
||||||
|
'/fonts/NotoSans-Italic.ttf',
|
||||||
|
'/fonts/NotoSans-BoldItalic.ttf',
|
||||||
|
'/fonts/NotoSansMono-Regular.ttf',
|
||||||
|
]
|
||||||
|
|
||||||
|
const buffers: Uint8Array[] = []
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
FONT_FILES.map(async (f) => {
|
STATIC_FONTS.map(async (path) => {
|
||||||
const res = await fetch(new URL(f.path, self.location.origin).href)
|
const url = new URL(path, self.location.origin).href
|
||||||
const buf = await res.arrayBuffer()
|
const res = await fetch(url)
|
||||||
families.push(f.family)
|
if (res.ok) {
|
||||||
buffers.push(new Uint8Array(buf))
|
buffers.push(new Uint8Array(await res.arrayBuffer()))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
loadFonts(JSON.stringify(families), buffers)
|
if (buffers.length > 0) {
|
||||||
console.log('[layout-worker] Hazır')
|
loadFonts(buffers)
|
||||||
|
loadedFamilies.add('noto sans')
|
||||||
|
loadedFamilies.add('noto sans mono')
|
||||||
|
console.log(`[layout-worker] Static fallback: ${buffers.length} font yüklendi`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load all variants of given families from the API into WASM */
|
||||||
|
async function ensureFamiliesLoaded(families: string[]): Promise<void> {
|
||||||
|
const toLoad = families.filter(f => !loadedFamilies.has(f.toLowerCase()))
|
||||||
|
if (toLoad.length === 0) return
|
||||||
|
|
||||||
|
const buffers: Uint8Array[] = []
|
||||||
|
|
||||||
|
for (const family of toLoad) {
|
||||||
|
const info = fontCatalog.find(f => f.family.toLowerCase() === family.toLowerCase())
|
||||||
|
if (!info) {
|
||||||
|
console.warn(`[layout-worker] Font ailesi bulunamadı: ${family}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetches = info.variants.map(async (v) => {
|
||||||
|
const url = `${fontApiBase}/${encodeURIComponent(info.family)}/${v.weight}/${v.italic}`
|
||||||
|
const res = await fetch(url)
|
||||||
|
if (res.ok) {
|
||||||
|
return new Uint8Array(await res.arrayBuffer())
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await Promise.all(fetches)
|
||||||
|
for (const buf of results) {
|
||||||
|
if (buf && buf.byteLength > 0) {
|
||||||
|
buffers.push(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadedFamilies.add(family.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffers.length > 0) {
|
||||||
|
if (loadedFamilies.size <= toLoad.length) {
|
||||||
|
// First load — use loadFonts
|
||||||
|
loadFonts(buffers)
|
||||||
|
} else {
|
||||||
|
// Subsequent loads — use addFonts
|
||||||
|
addFonts(buffers)
|
||||||
|
}
|
||||||
|
console.log(`[layout-worker] ${toLoad.join(', ')} yüklendi (${buffers.length} variant)`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureInit(): Promise<void> {
|
function ensureInit(): Promise<void> {
|
||||||
@@ -45,14 +133,32 @@ function ensureInit(): Promise<void> {
|
|||||||
type WorkerMessage =
|
type WorkerMessage =
|
||||||
| { type: 'compile'; templateJson: string; dataJson: string; id: number }
|
| { type: 'compile'; templateJson: string; dataJson: string; id: number }
|
||||||
| { type: 'barcode'; format: string; value: string; width: number; height: number; includeText: boolean; id: number }
|
| { type: 'barcode'; format: string; value: string; width: number; height: number; includeText: boolean; id: number }
|
||||||
|
| { type: 'configure'; fontApiBase?: string }
|
||||||
|
|
||||||
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
||||||
const msg = e.data
|
const msg = e.data
|
||||||
|
|
||||||
|
if (msg.type === 'configure') {
|
||||||
|
if (msg.fontApiBase) {
|
||||||
|
fontApiBase = msg.fontApiBase
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === 'compile') {
|
if (msg.type === 'compile') {
|
||||||
try {
|
try {
|
||||||
await ensureInit()
|
await ensureInit()
|
||||||
|
|
||||||
|
// Extract font families from template and ensure they're loaded
|
||||||
|
try {
|
||||||
|
const tpl = JSON.parse(msg.templateJson)
|
||||||
|
if (Array.isArray(tpl.fonts) && tpl.fonts.length > 0) {
|
||||||
|
await ensureFamiliesLoaded(tpl.fonts)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Template parse failure will be caught by computeLayout below
|
||||||
|
}
|
||||||
|
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
const resultJson = computeLayout(msg.templateJson, msg.dataJson)
|
const resultJson = computeLayout(msg.templateJson, msg.dataJson)
|
||||||
const layout: LayoutResult = JSON.parse(resultJson)
|
const layout: LayoutResult = JSON.parse(resultJson)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -5,5 +5,10 @@ export default defineConfig({
|
|||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
test: {
|
test: {
|
||||||
environment: 'happy-dom',
|
environment: 'happy-dom',
|
||||||
|
exclude: [
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/dist/**',
|
||||||
|
'tests/visual/**',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
68
justfile
68
justfile
@@ -25,18 +25,78 @@ wasm:
|
|||||||
wasm-watch:
|
wasm-watch:
|
||||||
watchexec -w layout-engine/src -w core/src -e rs -- just wasm
|
watchexec -w layout-engine/src -w core/src -e rs -- just wasm
|
||||||
|
|
||||||
|
# --- Test Komutlari ---
|
||||||
|
|
||||||
|
# Rust testleri (core + layout-engine + backend)
|
||||||
|
test-rust:
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Frontend unit testleri (Vitest)
|
||||||
|
test-front:
|
||||||
|
cd frontend && bun run test:run
|
||||||
|
|
||||||
# Generate PDF reference PNGs for cross-renderer visual tests
|
# Generate PDF reference PNGs for cross-renderer visual tests
|
||||||
visual-refs:
|
visual-refs:
|
||||||
cargo test -p dreport-layout --test visual_test -- generate_cross_renderer --ignored
|
cargo test -p dreport-layout --test visual_test -- generate_cross_renderer --ignored
|
||||||
|
|
||||||
# Run cross-renderer visual tests (Playwright vs PDF)
|
# Rust visual snapshot testleri
|
||||||
visual-test: visual-refs
|
test-visual-rust:
|
||||||
|
cargo test -p dreport-layout --test visual_test
|
||||||
|
|
||||||
|
# Cross-renderer visual testleri (Playwright: HTML vs PDF)
|
||||||
|
test-visual-cross: visual-refs
|
||||||
cd frontend && bun run test:visual -- --project=cross-renderer
|
cd frontend && bun run test:visual -- --project=cross-renderer
|
||||||
|
|
||||||
# Run all visual tests (editor + cross-renderer)
|
# Editor visual testleri (Playwright)
|
||||||
visual-test-all: visual-refs
|
test-visual-editor:
|
||||||
|
cd frontend && bun run test:visual -- --project=editor
|
||||||
|
|
||||||
|
# Tum visual testler (Playwright: editor + cross-renderer)
|
||||||
|
test-visual: visual-refs
|
||||||
cd frontend && bun run test:visual
|
cd frontend && bun run test:visual
|
||||||
|
|
||||||
|
# Tum testler (Rust + frontend unit + visual)
|
||||||
|
test-all: test-rust test-front test-visual
|
||||||
|
|
||||||
|
# Visual diff sonuclarini ac (cross-renderer)
|
||||||
|
diff-open:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
DIFF_DIR="frontend/tests/visual/cross-renderer-diffs"
|
||||||
|
if [ -z "$(ls -A "$DIFF_DIR" 2>/dev/null)" ]; then
|
||||||
|
echo "Diff klasoru bos — once 'just test-visual-cross' calistirin."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
open "$DIFF_DIR"/*_diff.png "$DIFF_DIR"/*_html.png 2>/dev/null || xdg-open "$DIFF_DIR" 2>/dev/null || echo "Dosyalar: $DIFF_DIR"
|
||||||
|
|
||||||
|
# --- Lint / Format / Build ---
|
||||||
|
|
||||||
|
# Rust + frontend lint
|
||||||
|
lint:
|
||||||
|
cargo clippy --workspace -- -D warnings
|
||||||
|
cd frontend && bun run lint
|
||||||
|
|
||||||
|
# Rust + frontend format
|
||||||
|
fmt:
|
||||||
|
cargo fmt --workspace
|
||||||
|
cd frontend && bun run format
|
||||||
|
|
||||||
|
# Format kontrolu (CI icin)
|
||||||
|
fmt-check:
|
||||||
|
cargo fmt --workspace --check
|
||||||
|
cd frontend && bun run format:check
|
||||||
|
|
||||||
|
# Full build
|
||||||
|
build:
|
||||||
|
cd frontend && bun run build
|
||||||
|
cargo build --release -p dreport-backend
|
||||||
|
|
||||||
|
# Type check (Rust + TypeScript)
|
||||||
|
check:
|
||||||
|
cargo check --workspace
|
||||||
|
cd frontend && bun run type-check
|
||||||
|
|
||||||
|
# --- Publish ---
|
||||||
|
|
||||||
# Publish dreport-core to Gitea
|
# Publish dreport-core to Gitea
|
||||||
publish-core:
|
publish-core:
|
||||||
cargo publish -p dreport-core --registry gitea --allow-dirty
|
cargo publish -p dreport-core --registry gitea --allow-dirty
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dreport-layout"
|
name = "dreport-layout"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Layout engine for dreport (taffy + cosmic-text)"
|
description = "Layout engine for dreport (taffy + cosmic-text)"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -11,8 +11,9 @@ publish = ["gitea"]
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dreport-core = { version = "0.1.0", path = "../core", registry = "gitea" }
|
dreport-core = { version = "0.2.0", path = "../core", registry = "gitea" }
|
||||||
dexpr = { version = "0.1.0", registry = "gitea" }
|
dexpr = { version = "0.1.0", registry = "gitea" }
|
||||||
|
rust_decimal = "1.41"
|
||||||
taffy = "0.9"
|
taffy = "0.9"
|
||||||
cosmic-text = { version = "0.18", default-features = false, features = ["std", "swash"] }
|
cosmic-text = { version = "0.18", default-features = false, features = ["std", "swash"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
799
layout-engine/src/chart_layout.rs
Normal file
799
layout-engine/src/chart_layout.rs
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
//! Shared chart layout computation — used by both SVG (chart_render) and PDF (pdf_render).
|
||||||
|
//!
|
||||||
|
//! This module extracts the **what to draw and where** logic into shared structs.
|
||||||
|
//! Each renderer then handles the **how** (actual drawing calls) using these structs.
|
||||||
|
|
||||||
|
use dreport_core::models::ChartType;
|
||||||
|
|
||||||
|
pub const DEFAULT_COLORS: &[&str] = &[
|
||||||
|
"#4F46E5", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899", "#06B6D4", "#84CC16",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared structs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct ChartLayout {
|
||||||
|
/// Absolute plot area origin X (mm). For SVG this equals margin_left;
|
||||||
|
/// for PDF this is base_x_mm + margin_left.
|
||||||
|
pub plot_x: f64,
|
||||||
|
/// Absolute plot area origin Y (mm).
|
||||||
|
pub plot_y: f64,
|
||||||
|
pub plot_w: f64,
|
||||||
|
pub plot_h: f64,
|
||||||
|
pub margin_top: f64,
|
||||||
|
pub margin_bottom: f64,
|
||||||
|
pub margin_left: f64,
|
||||||
|
pub margin_right: f64,
|
||||||
|
pub palette: Vec<String>,
|
||||||
|
pub title: Option<TitleLayout>,
|
||||||
|
pub legend_show: bool,
|
||||||
|
pub legend_pos: String,
|
||||||
|
pub legend_font: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TitleLayout {
|
||||||
|
pub text: String,
|
||||||
|
pub font_size: f64,
|
||||||
|
pub color: String,
|
||||||
|
/// x position in mm (absolute)
|
||||||
|
pub x: f64,
|
||||||
|
/// y position in mm (absolute)
|
||||||
|
pub y: f64,
|
||||||
|
pub align: String, // "left", "center", "right"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct YAxisLayout {
|
||||||
|
pub ticks: Vec<YTick>,
|
||||||
|
pub show_grid: bool,
|
||||||
|
pub grid_color: String,
|
||||||
|
/// Y axis vertical line positions (mm, absolute)
|
||||||
|
pub axis_x: f64,
|
||||||
|
pub axis_y_start: f64,
|
||||||
|
pub axis_y_end: f64,
|
||||||
|
/// Right edge of the grid lines (axis_x + plot_w)
|
||||||
|
pub grid_end_x: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct YTick {
|
||||||
|
pub value: f64,
|
||||||
|
pub label: String,
|
||||||
|
/// Absolute Y position (mm)
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct XLabelLayout {
|
||||||
|
pub labels: Vec<XLabel>,
|
||||||
|
pub needs_rotate: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct XLabel {
|
||||||
|
pub text: String,
|
||||||
|
/// Absolute X position (mm)
|
||||||
|
pub x: f64,
|
||||||
|
/// Absolute Y position (mm)
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-computed bar geometry for a single bar rect
|
||||||
|
pub struct BarRect {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
pub w: f64,
|
||||||
|
pub h: f64,
|
||||||
|
pub color_idx: usize,
|
||||||
|
pub value: f64,
|
||||||
|
/// Label position (center x, label y)
|
||||||
|
pub label_x: f64,
|
||||||
|
pub label_y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BarChartLayout {
|
||||||
|
pub min_val: f64,
|
||||||
|
pub max_val: f64,
|
||||||
|
pub y_axis: YAxisLayout,
|
||||||
|
pub x_labels: XLabelLayout,
|
||||||
|
pub bars: Vec<BarRect>,
|
||||||
|
pub show_labels: bool,
|
||||||
|
pub label_font: f64,
|
||||||
|
pub label_color: String,
|
||||||
|
pub stacked: bool,
|
||||||
|
/// X axis line endpoints
|
||||||
|
pub x_axis_y: f64,
|
||||||
|
pub x_axis_x1: f64,
|
||||||
|
pub x_axis_x2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-computed point position for line chart
|
||||||
|
pub struct LinePoint {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LineSeriesLayout {
|
||||||
|
pub color_idx: usize,
|
||||||
|
pub points: Vec<LinePoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LineChartLayout {
|
||||||
|
pub min_val: f64,
|
||||||
|
pub max_val: f64,
|
||||||
|
pub y_axis: YAxisLayout,
|
||||||
|
pub x_labels: XLabelLayout,
|
||||||
|
pub series: Vec<LineSeriesLayout>,
|
||||||
|
pub line_width: f64,
|
||||||
|
pub show_points: bool,
|
||||||
|
pub show_labels: bool,
|
||||||
|
pub label_font: f64,
|
||||||
|
pub label_color: String,
|
||||||
|
/// X axis line endpoints
|
||||||
|
pub x_axis_y: f64,
|
||||||
|
pub x_axis_x1: f64,
|
||||||
|
pub x_axis_x2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PieSlice {
|
||||||
|
pub start_angle: f64,
|
||||||
|
pub end_angle: f64,
|
||||||
|
pub sweep: f64,
|
||||||
|
pub color_idx: usize,
|
||||||
|
pub value: f64,
|
||||||
|
pub fraction: f64,
|
||||||
|
/// Label position inside slice
|
||||||
|
pub label_x: f64,
|
||||||
|
pub label_y: f64,
|
||||||
|
pub label_text: String,
|
||||||
|
/// Leader line + category label outside
|
||||||
|
pub leader_start_x: f64,
|
||||||
|
pub leader_start_y: f64,
|
||||||
|
pub leader_end_x: f64,
|
||||||
|
pub leader_end_y: f64,
|
||||||
|
pub cat_label_x: f64,
|
||||||
|
pub cat_label_y: f64,
|
||||||
|
pub cat_label_text: String,
|
||||||
|
pub cat_label_anchor_end: bool, // true = end/right, false = start/left
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PieChartLayout {
|
||||||
|
pub cx: f64,
|
||||||
|
pub cy: f64,
|
||||||
|
pub radius: f64,
|
||||||
|
pub inner_radius: f64,
|
||||||
|
pub slices: Vec<PieSlice>,
|
||||||
|
pub show_labels: bool,
|
||||||
|
pub label_font: f64,
|
||||||
|
pub label_color: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legend item with pre-computed position
|
||||||
|
pub struct LegendItemLayout {
|
||||||
|
pub name: String,
|
||||||
|
pub color_idx: usize,
|
||||||
|
/// Swatch rect position (mm)
|
||||||
|
pub swatch_x: f64,
|
||||||
|
pub swatch_y: f64,
|
||||||
|
/// Text position (mm)
|
||||||
|
pub text_x: f64,
|
||||||
|
pub text_y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LegendLayout {
|
||||||
|
pub items: Vec<LegendItemLayout>,
|
||||||
|
pub font_size: f64,
|
||||||
|
pub position: String,
|
||||||
|
pub swatch_size: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Common input abstraction — both ResolvedChartData and ChartRenderData
|
||||||
|
// can provide these values
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Trait that abstracts over the two chart data representations used by
|
||||||
|
/// SVG renderer (ResolvedChartData) and PDF renderer (ChartRenderData).
|
||||||
|
pub trait ChartDataSource {
|
||||||
|
fn chart_type(&self) -> ChartType;
|
||||||
|
fn categories(&self) -> &[String];
|
||||||
|
fn series_count(&self) -> usize;
|
||||||
|
fn series_name(&self, idx: usize) -> &str;
|
||||||
|
fn series_values(&self, idx: usize) -> &[f64];
|
||||||
|
fn title_text(&self) -> Option<&str>;
|
||||||
|
fn title_font_size(&self) -> Option<f64>;
|
||||||
|
fn title_color(&self) -> Option<&str>;
|
||||||
|
fn title_align(&self) -> Option<&str>;
|
||||||
|
fn legend_show(&self) -> bool;
|
||||||
|
fn legend_position(&self) -> Option<&str>;
|
||||||
|
fn legend_font_size(&self) -> Option<f64>;
|
||||||
|
fn x_label(&self) -> Option<&str>;
|
||||||
|
fn y_label(&self) -> Option<&str>;
|
||||||
|
fn show_grid(&self) -> bool;
|
||||||
|
fn grid_color(&self) -> Option<&str>;
|
||||||
|
fn bar_gap(&self) -> Option<f64>;
|
||||||
|
fn stacked(&self) -> bool;
|
||||||
|
fn colors(&self) -> Option<&[String]>;
|
||||||
|
fn background_color(&self) -> Option<&str>;
|
||||||
|
fn show_labels(&self) -> bool;
|
||||||
|
fn label_font_size(&self) -> Option<f64>;
|
||||||
|
fn label_color(&self) -> Option<&str>;
|
||||||
|
fn inner_radius(&self) -> Option<f64>;
|
||||||
|
fn show_points(&self) -> Option<bool>;
|
||||||
|
fn line_width(&self) -> Option<f64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Impl for SVG renderer's ResolvedChartData
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl ChartDataSource for crate::data_resolve::ResolvedChartData {
|
||||||
|
fn chart_type(&self) -> ChartType { self.chart_type.clone() }
|
||||||
|
fn categories(&self) -> &[String] { &self.categories }
|
||||||
|
fn series_count(&self) -> usize { self.series.len() }
|
||||||
|
fn series_name(&self, idx: usize) -> &str { &self.series[idx].name }
|
||||||
|
fn series_values(&self, idx: usize) -> &[f64] { &self.series[idx].values }
|
||||||
|
fn title_text(&self) -> Option<&str> {
|
||||||
|
self.title.as_ref().map(|t| t.text.as_str()).filter(|t| !t.is_empty())
|
||||||
|
}
|
||||||
|
fn title_font_size(&self) -> Option<f64> { self.title.as_ref().and_then(|t| t.font_size) }
|
||||||
|
fn title_color(&self) -> Option<&str> { self.title.as_ref().and_then(|t| t.color.as_deref()) }
|
||||||
|
fn title_align(&self) -> Option<&str> { self.title.as_ref().and_then(|t| t.align.as_deref()) }
|
||||||
|
fn legend_show(&self) -> bool { self.legend.as_ref().is_some_and(|l| l.show) }
|
||||||
|
fn legend_position(&self) -> Option<&str> { self.legend.as_ref().and_then(|l| l.position.as_deref()) }
|
||||||
|
fn legend_font_size(&self) -> Option<f64> { self.legend.as_ref().and_then(|l| l.font_size) }
|
||||||
|
fn x_label(&self) -> Option<&str> { self.axis.as_ref().and_then(|a| a.x_label.as_deref()) }
|
||||||
|
fn y_label(&self) -> Option<&str> { self.axis.as_ref().and_then(|a| a.y_label.as_deref()) }
|
||||||
|
fn show_grid(&self) -> bool { self.axis.as_ref().and_then(|a| a.show_grid).unwrap_or(true) }
|
||||||
|
fn grid_color(&self) -> Option<&str> { self.axis.as_ref().and_then(|a| a.grid_color.as_deref()) }
|
||||||
|
fn bar_gap(&self) -> Option<f64> { self.style.bar_gap }
|
||||||
|
fn stacked(&self) -> bool { matches!(self.group_mode, Some(dreport_core::models::GroupMode::Stacked)) }
|
||||||
|
fn colors(&self) -> Option<&[String]> { self.style.colors.as_deref() }
|
||||||
|
fn background_color(&self) -> Option<&str> { self.style.background_color.as_deref() }
|
||||||
|
fn show_labels(&self) -> bool { self.labels.as_ref().is_some_and(|l| l.show) }
|
||||||
|
fn label_font_size(&self) -> Option<f64> { self.labels.as_ref().and_then(|l| l.font_size) }
|
||||||
|
fn label_color(&self) -> Option<&str> { self.labels.as_ref().and_then(|l| l.color.as_deref()) }
|
||||||
|
fn inner_radius(&self) -> Option<f64> { self.style.inner_radius }
|
||||||
|
fn show_points(&self) -> Option<bool> { self.style.show_points }
|
||||||
|
fn line_width(&self) -> Option<f64> { self.style.line_width }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Impl for PDF renderer's ChartRenderData
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl ChartDataSource for crate::ChartRenderData {
|
||||||
|
fn chart_type(&self) -> ChartType { self.chart_type.clone() }
|
||||||
|
fn categories(&self) -> &[String] { &self.categories }
|
||||||
|
fn series_count(&self) -> usize { self.series.len() }
|
||||||
|
fn series_name(&self, idx: usize) -> &str { &self.series[idx].name }
|
||||||
|
fn series_values(&self, idx: usize) -> &[f64] { &self.series[idx].values }
|
||||||
|
fn title_text(&self) -> Option<&str> {
|
||||||
|
self.title_text.as_deref().filter(|t| !t.is_empty())
|
||||||
|
}
|
||||||
|
fn title_font_size(&self) -> Option<f64> { self.title_font_size }
|
||||||
|
fn title_color(&self) -> Option<&str> { self.title_color.as_deref() }
|
||||||
|
fn title_align(&self) -> Option<&str> { self.title_align.as_deref() }
|
||||||
|
fn legend_show(&self) -> bool { self.legend_show }
|
||||||
|
fn legend_position(&self) -> Option<&str> { self.legend_position.as_deref() }
|
||||||
|
fn legend_font_size(&self) -> Option<f64> { self.legend_font_size }
|
||||||
|
fn x_label(&self) -> Option<&str> { self.x_label.as_deref() }
|
||||||
|
fn y_label(&self) -> Option<&str> { self.y_label.as_deref() }
|
||||||
|
fn show_grid(&self) -> bool { self.show_grid }
|
||||||
|
fn grid_color(&self) -> Option<&str> { self.grid_color.as_deref() }
|
||||||
|
fn bar_gap(&self) -> Option<f64> { self.bar_gap }
|
||||||
|
fn stacked(&self) -> bool { self.stacked }
|
||||||
|
fn colors(&self) -> Option<&[String]> {
|
||||||
|
if self.colors.is_empty() { None } else { Some(&self.colors) }
|
||||||
|
}
|
||||||
|
fn background_color(&self) -> Option<&str> { self.background_color.as_deref() }
|
||||||
|
fn show_labels(&self) -> bool { self.show_labels }
|
||||||
|
fn label_font_size(&self) -> Option<f64> { self.label_font_size }
|
||||||
|
fn label_color(&self) -> Option<&str> { self.label_color.as_deref() }
|
||||||
|
fn inner_radius(&self) -> Option<f64> { self.inner_radius }
|
||||||
|
fn show_points(&self) -> Option<bool> { self.show_points }
|
||||||
|
fn line_width(&self) -> Option<f64> { self.line_width }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared computation functions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn color_at(palette: &[String], i: usize) -> &str {
|
||||||
|
&palette[i % palette.len()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_palette(data: &dyn ChartDataSource) -> Vec<String> {
|
||||||
|
let n_colors = data.categories().len().max(data.series_count()).max(1);
|
||||||
|
let user_colors = data.colors();
|
||||||
|
(0..n_colors)
|
||||||
|
.map(|i| {
|
||||||
|
if let Some(uc) = user_colors {
|
||||||
|
if i < uc.len() {
|
||||||
|
return uc[i].clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DEFAULT_COLORS[i % DEFAULT_COLORS.len()].to_string()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_value(v: f64) -> String {
|
||||||
|
if v.abs() >= 1_000_000.0 {
|
||||||
|
format!("{:.1}M", v / 1_000_000.0)
|
||||||
|
} else if v.abs() >= 1_000.0 {
|
||||||
|
format!("{:.1}K", v / 1_000.0)
|
||||||
|
} else if v.fract().abs() < 1e-10 {
|
||||||
|
format!("{}", v as i64)
|
||||||
|
} else {
|
||||||
|
format!("{:.1}", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the value range (min, max) across all series.
|
||||||
|
pub fn compute_value_range(data: &dyn ChartDataSource, stacked: bool) -> (f64, f64) {
|
||||||
|
if data.series_count() == 0 {
|
||||||
|
return (0.0, 1.0);
|
||||||
|
}
|
||||||
|
if stacked {
|
||||||
|
let n = data.categories().len();
|
||||||
|
let mut max_stack = 0.0_f64;
|
||||||
|
for ci in 0..n {
|
||||||
|
let sum: f64 = (0..data.series_count())
|
||||||
|
.map(|si| data.series_values(si).get(ci).copied().unwrap_or(0.0))
|
||||||
|
.sum();
|
||||||
|
max_stack = max_stack.max(sum);
|
||||||
|
}
|
||||||
|
(0.0, max_stack * 1.05)
|
||||||
|
} else {
|
||||||
|
let mut min_v = f64::MAX;
|
||||||
|
let mut max_v = f64::MIN;
|
||||||
|
for si in 0..data.series_count() {
|
||||||
|
for val in data.series_values(si) {
|
||||||
|
min_v = min_v.min(*val);
|
||||||
|
max_v = max_v.max(*val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min_v > 0.0 {
|
||||||
|
min_v = 0.0;
|
||||||
|
}
|
||||||
|
max_v *= 1.05;
|
||||||
|
(min_v, max_v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_range(min_val: f64, max_val: f64) -> f64 {
|
||||||
|
let r = max_val - min_val;
|
||||||
|
if r.abs() < 1e-10 { 1.0 } else { r }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute margins and plot area. `origin_x/y` is 0 for SVG or base_x_mm/base_y_mm for PDF.
|
||||||
|
pub fn compute_chart_layout(
|
||||||
|
data: &dyn ChartDataSource,
|
||||||
|
width_mm: f64,
|
||||||
|
height_mm: f64,
|
||||||
|
origin_x: f64,
|
||||||
|
origin_y: f64,
|
||||||
|
) -> ChartLayout {
|
||||||
|
let palette = build_palette(data);
|
||||||
|
|
||||||
|
let mut margin_top = 2.0_f64;
|
||||||
|
let mut margin_bottom = 4.0_f64;
|
||||||
|
let mut margin_left = 8.0_f64;
|
||||||
|
let margin_right = 4.0_f64;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let title = if let Some(text) = data.title_text() {
|
||||||
|
let fs = data.title_font_size().unwrap_or(4.0);
|
||||||
|
margin_top += fs * 0.4 + 2.0;
|
||||||
|
let color = data.title_color().unwrap_or("#333333").to_string();
|
||||||
|
let align = data.title_align().unwrap_or("center").to_string();
|
||||||
|
let x = match align.as_str() {
|
||||||
|
"left" => origin_x + margin_left,
|
||||||
|
"right" => origin_x + width_mm - margin_right,
|
||||||
|
_ => origin_x + width_mm / 2.0,
|
||||||
|
};
|
||||||
|
let y = origin_y + margin_top - 1.0;
|
||||||
|
Some(TitleLayout { text: text.to_string(), font_size: fs, color, x, y, align })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Legend space
|
||||||
|
let legend_show = data.legend_show();
|
||||||
|
let legend_pos = data.legend_position().unwrap_or("bottom").to_string();
|
||||||
|
let legend_font = data.legend_font_size().unwrap_or(2.8);
|
||||||
|
|
||||||
|
if legend_show && data.series_count() > 1 {
|
||||||
|
match legend_pos.as_str() {
|
||||||
|
"top" => margin_top += legend_font + 3.0,
|
||||||
|
"bottom" => margin_bottom += legend_font + 3.0,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axis labels space (bar and line only)
|
||||||
|
let has_axis = !matches!(data.chart_type(), ChartType::Pie);
|
||||||
|
if has_axis {
|
||||||
|
if data.x_label().is_some() {
|
||||||
|
margin_bottom += 4.0;
|
||||||
|
}
|
||||||
|
if data.y_label().is_some() {
|
||||||
|
margin_left += 4.0;
|
||||||
|
}
|
||||||
|
// Category labels bottom space
|
||||||
|
let max_label_len = data.categories().iter().map(|c| c.len()).max().unwrap_or(0);
|
||||||
|
let n_cats = data.categories().len();
|
||||||
|
let available_w = width_mm - margin_left - margin_right;
|
||||||
|
let cat_width = if n_cats > 0 { available_w / n_cats as f64 } else { available_w };
|
||||||
|
let max_chars_fit = (cat_width / 1.25).max(1.0) as usize;
|
||||||
|
let will_rotate = max_label_len > max_chars_fit;
|
||||||
|
if will_rotate {
|
||||||
|
let char_w_mm = 1.1;
|
||||||
|
let max_text_w = max_label_len as f64 * char_w_mm;
|
||||||
|
let label_v = max_text_w * 0.707;
|
||||||
|
margin_bottom += label_v.min(25.0).max(6.0);
|
||||||
|
let label_h = max_text_w * 0.707;
|
||||||
|
let extra_left = (label_h - cat_width / 2.0).max(0.0);
|
||||||
|
margin_left += extra_left.min(10.0);
|
||||||
|
} else {
|
||||||
|
margin_bottom += 4.0;
|
||||||
|
}
|
||||||
|
// Y-axis value labels left space
|
||||||
|
margin_left += 6.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let plot_x = origin_x + margin_left;
|
||||||
|
let plot_y = origin_y + margin_top;
|
||||||
|
let plot_w = (width_mm - margin_left - margin_right).max(1.0);
|
||||||
|
let plot_h = (height_mm - margin_top - margin_bottom).max(1.0);
|
||||||
|
|
||||||
|
ChartLayout {
|
||||||
|
plot_x, plot_y, plot_w, plot_h,
|
||||||
|
margin_top, margin_bottom, margin_left, margin_right,
|
||||||
|
palette, title, legend_show, legend_pos, legend_font,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute Y axis ticks and grid lines.
|
||||||
|
pub fn compute_y_axis(
|
||||||
|
min_val: f64, max_val: f64,
|
||||||
|
px: f64, py: f64, pw: f64, ph: f64,
|
||||||
|
show_grid: bool, grid_color: &str,
|
||||||
|
) -> YAxisLayout {
|
||||||
|
let range = safe_range(min_val, max_val);
|
||||||
|
let tick_count = 5;
|
||||||
|
let ticks = (0..=tick_count)
|
||||||
|
.map(|i| {
|
||||||
|
let frac = i as f64 / tick_count as f64;
|
||||||
|
let val = min_val + frac * range;
|
||||||
|
let y = py + ph - frac * ph;
|
||||||
|
YTick { value: val, label: format_value(val), y }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
YAxisLayout {
|
||||||
|
ticks,
|
||||||
|
show_grid,
|
||||||
|
grid_color: grid_color.to_string(),
|
||||||
|
axis_x: px,
|
||||||
|
axis_y_start: py,
|
||||||
|
axis_y_end: py + ph,
|
||||||
|
grid_end_x: px + pw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute X label positions for bar chart (slot-based spacing).
|
||||||
|
pub fn compute_x_labels_bar(categories: &[String], px: f64, baseline_y: f64, pw: f64) -> XLabelLayout {
|
||||||
|
let n_cats = categories.len();
|
||||||
|
if n_cats == 0 {
|
||||||
|
return XLabelLayout { labels: vec![], needs_rotate: false };
|
||||||
|
}
|
||||||
|
let cat_width = pw / n_cats as f64;
|
||||||
|
let max_chars = (cat_width / 1.25).max(1.0) as usize;
|
||||||
|
let needs_rotate = categories.iter().any(|c| c.len() > max_chars);
|
||||||
|
let labels = categories.iter().enumerate().map(|(ci, cat)| {
|
||||||
|
XLabel {
|
||||||
|
text: cat.clone(),
|
||||||
|
x: px + ci as f64 * cat_width + cat_width / 2.0,
|
||||||
|
y: baseline_y + 2.5,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
XLabelLayout { labels, needs_rotate }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute X label positions for line chart (point-based spacing).
|
||||||
|
pub fn compute_x_labels_line(categories: &[String], px: f64, baseline_y: f64, pw: f64) -> XLabelLayout {
|
||||||
|
let n_cats = categories.len();
|
||||||
|
if n_cats == 0 {
|
||||||
|
return XLabelLayout { labels: vec![], needs_rotate: false };
|
||||||
|
}
|
||||||
|
let spacing = if n_cats == 1 { pw } else { pw / (n_cats - 1) as f64 };
|
||||||
|
let max_chars = (spacing / 1.25).max(1.0) as usize;
|
||||||
|
let needs_rotate = categories.iter().any(|c| c.len() > max_chars);
|
||||||
|
let labels = categories.iter().enumerate().map(|(ci, cat)| {
|
||||||
|
let x = if n_cats == 1 { px + pw / 2.0 } else { px + ci as f64 * pw / (n_cats - 1) as f64 };
|
||||||
|
XLabel { text: cat.clone(), x, y: baseline_y + 2.5 }
|
||||||
|
}).collect();
|
||||||
|
XLabelLayout { labels, needs_rotate }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute bar chart layout (all bar geometries + axes).
|
||||||
|
pub fn compute_bar_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> BarChartLayout {
|
||||||
|
let px = cl.plot_x;
|
||||||
|
let py = cl.plot_y;
|
||||||
|
let pw = cl.plot_w;
|
||||||
|
let ph = cl.plot_h;
|
||||||
|
|
||||||
|
let stacked = data.stacked();
|
||||||
|
let (min_val, max_val) = compute_value_range(data, stacked);
|
||||||
|
let range = safe_range(min_val, max_val);
|
||||||
|
|
||||||
|
let show_grid = data.show_grid();
|
||||||
|
let grid_color = data.grid_color().unwrap_or("#E5E7EB");
|
||||||
|
let y_axis = compute_y_axis(min_val, max_val, px, py, pw, ph, show_grid, grid_color);
|
||||||
|
|
||||||
|
let n_cats = data.categories().len();
|
||||||
|
let n_series = data.series_count();
|
||||||
|
let cat_width = if n_cats > 0 { pw / n_cats as f64 } else { pw };
|
||||||
|
let bar_gap = data.bar_gap().unwrap_or(0.2).clamp(0.0, 0.8);
|
||||||
|
let group_width = cat_width * (1.0 - bar_gap);
|
||||||
|
|
||||||
|
let show_labels = data.show_labels();
|
||||||
|
let label_font = data.label_font_size().unwrap_or(2.2);
|
||||||
|
let label_color = data.label_color().unwrap_or("#333").to_string();
|
||||||
|
|
||||||
|
let mut bars = Vec::new();
|
||||||
|
|
||||||
|
for ci in 0..n_cats {
|
||||||
|
let cat_x = px + ci as f64 * cat_width;
|
||||||
|
if stacked {
|
||||||
|
let mut y_offset = 0.0_f64;
|
||||||
|
for si in 0..n_series {
|
||||||
|
let val = data.series_values(si).get(ci).copied().unwrap_or(0.0);
|
||||||
|
let bar_h = (val / range) * ph;
|
||||||
|
let bar_y = py + ph - y_offset - bar_h;
|
||||||
|
let bx = cat_x + cat_width * bar_gap / 2.0;
|
||||||
|
bars.push(BarRect {
|
||||||
|
x: bx,
|
||||||
|
y: bar_y,
|
||||||
|
w: group_width,
|
||||||
|
h: bar_h.max(0.0),
|
||||||
|
color_idx: si,
|
||||||
|
value: val,
|
||||||
|
label_x: cat_x + cat_width / 2.0,
|
||||||
|
label_y: bar_y + bar_h / 2.0 + label_font * 0.15,
|
||||||
|
});
|
||||||
|
y_offset += bar_h;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let bar_w = if n_series > 0 { group_width / n_series as f64 } else { group_width };
|
||||||
|
for si in 0..n_series {
|
||||||
|
let val = data.series_values(si).get(ci).copied().unwrap_or(0.0);
|
||||||
|
let bar_h = ((val - min_val) / range) * ph;
|
||||||
|
let bx = cat_x + cat_width * bar_gap / 2.0 + si as f64 * bar_w;
|
||||||
|
let by = py + ph - bar_h;
|
||||||
|
bars.push(BarRect {
|
||||||
|
x: bx,
|
||||||
|
y: by,
|
||||||
|
w: bar_w.max(0.1),
|
||||||
|
h: bar_h.max(0.0),
|
||||||
|
color_idx: si,
|
||||||
|
value: val,
|
||||||
|
label_x: bx + bar_w / 2.0,
|
||||||
|
label_y: by - 0.8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let x_labels = compute_x_labels_bar(data.categories(), px, py + ph, pw);
|
||||||
|
|
||||||
|
BarChartLayout {
|
||||||
|
min_val, max_val,
|
||||||
|
y_axis, x_labels, bars,
|
||||||
|
show_labels, label_font, label_color, stacked,
|
||||||
|
x_axis_y: py + ph,
|
||||||
|
x_axis_x1: px,
|
||||||
|
x_axis_x2: px + pw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute line chart layout (all point positions + axes).
|
||||||
|
pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> LineChartLayout {
|
||||||
|
let px = cl.plot_x;
|
||||||
|
let py = cl.plot_y;
|
||||||
|
let pw = cl.plot_w;
|
||||||
|
let ph = cl.plot_h;
|
||||||
|
|
||||||
|
let (min_val, max_val) = compute_value_range(data, false);
|
||||||
|
let range = safe_range(min_val, max_val);
|
||||||
|
let n_cats = data.categories().len();
|
||||||
|
|
||||||
|
let show_grid = data.show_grid();
|
||||||
|
let grid_color = data.grid_color().unwrap_or("#E5E7EB");
|
||||||
|
let y_axis = compute_y_axis(min_val, max_val, px, py, pw, ph, show_grid, grid_color);
|
||||||
|
|
||||||
|
let line_width = data.line_width().unwrap_or(0.5);
|
||||||
|
let show_points = data.show_points().unwrap_or(true);
|
||||||
|
let show_labels = data.show_labels();
|
||||||
|
let label_font = data.label_font_size().unwrap_or(2.2);
|
||||||
|
let label_color = data.label_color().unwrap_or("#333").to_string();
|
||||||
|
|
||||||
|
let series = (0..data.series_count()).map(|si| {
|
||||||
|
let values = data.series_values(si);
|
||||||
|
let points = values.iter().enumerate().map(|(ci, val)| {
|
||||||
|
let x = if n_cats == 1 { px + pw / 2.0 } else { px + ci as f64 * pw / (n_cats - 1) as f64 };
|
||||||
|
let y = py + ph - ((val - min_val) / range) * ph;
|
||||||
|
LinePoint { x, y, value: *val }
|
||||||
|
}).collect();
|
||||||
|
LineSeriesLayout { color_idx: si, points }
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let x_labels = compute_x_labels_line(data.categories(), px, py + ph, pw);
|
||||||
|
|
||||||
|
LineChartLayout {
|
||||||
|
min_val, max_val,
|
||||||
|
y_axis, x_labels, series,
|
||||||
|
line_width: line_width,
|
||||||
|
show_points, show_labels, label_font, label_color,
|
||||||
|
x_axis_y: py + ph,
|
||||||
|
x_axis_x1: px,
|
||||||
|
x_axis_x2: px + pw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute pie chart layout (slice angles and label positions).
|
||||||
|
pub fn compute_pie_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> PieChartLayout {
|
||||||
|
let px = cl.plot_x;
|
||||||
|
let py = cl.plot_y;
|
||||||
|
let pw = cl.plot_w;
|
||||||
|
let ph = cl.plot_h;
|
||||||
|
|
||||||
|
let values: Vec<f64> = if data.series_count() == 1 {
|
||||||
|
data.series_values(0).to_vec()
|
||||||
|
} else {
|
||||||
|
data.categories().iter().enumerate().map(|(ci, _)| {
|
||||||
|
(0..data.series_count())
|
||||||
|
.map(|si| data.series_values(si).get(ci).copied().unwrap_or(0.0))
|
||||||
|
.sum()
|
||||||
|
}).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let total: f64 = values.iter().sum();
|
||||||
|
let show_labels = data.show_labels();
|
||||||
|
let label_font = data.label_font_size().unwrap_or(3.0);
|
||||||
|
let label_color = data.label_color().unwrap_or("#333").to_string();
|
||||||
|
|
||||||
|
let cx = px + pw / 2.0;
|
||||||
|
let cy = py + ph / 2.0;
|
||||||
|
let radius = pw.min(ph) / 2.0 * 0.65;
|
||||||
|
let inner_frac = data.inner_radius().unwrap_or(0.0).clamp(0.0, 0.9);
|
||||||
|
let inner_r = radius * inner_frac;
|
||||||
|
|
||||||
|
let mut slices = Vec::new();
|
||||||
|
|
||||||
|
if total > 0.0 {
|
||||||
|
let mut start_angle = -std::f64::consts::FRAC_PI_2;
|
||||||
|
let categories = data.categories();
|
||||||
|
|
||||||
|
for (i, val) in values.iter().enumerate() {
|
||||||
|
if *val <= 0.0 {
|
||||||
|
start_angle += 0.0; // skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sweep = (val / total) * std::f64::consts::TAU;
|
||||||
|
let end_angle = start_angle + sweep;
|
||||||
|
let mid_angle = start_angle + sweep / 2.0;
|
||||||
|
|
||||||
|
// Label inside slice
|
||||||
|
let label_r = if inner_r > 0.0 { (radius + inner_r) / 2.0 } else { radius * 0.65 };
|
||||||
|
let lx = cx + label_r * mid_angle.cos();
|
||||||
|
let ly = cy + label_r * mid_angle.sin();
|
||||||
|
let pct = (val / total * 100.0).round();
|
||||||
|
|
||||||
|
// Leader line + category label
|
||||||
|
let line_start_r = radius;
|
||||||
|
let line_end_r = radius + 3.0;
|
||||||
|
let text_r = radius + 4.0;
|
||||||
|
|
||||||
|
let leader_sx = cx + line_start_r * mid_angle.cos();
|
||||||
|
let leader_sy = cy + line_start_r * mid_angle.sin();
|
||||||
|
let leader_ex = cx + line_end_r * mid_angle.cos();
|
||||||
|
let leader_ey = cy + line_end_r * mid_angle.sin();
|
||||||
|
let cat_lx = cx + text_r * mid_angle.cos();
|
||||||
|
let cat_ly = cy + text_r * mid_angle.sin();
|
||||||
|
let cat_text = if i < categories.len() { categories[i].clone() } else { String::new() };
|
||||||
|
let anchor_end = mid_angle.cos() < 0.0;
|
||||||
|
|
||||||
|
slices.push(PieSlice {
|
||||||
|
start_angle, end_angle, sweep,
|
||||||
|
color_idx: i,
|
||||||
|
value: *val,
|
||||||
|
fraction: val / total,
|
||||||
|
label_x: lx, label_y: ly,
|
||||||
|
label_text: format!("{}%", pct),
|
||||||
|
leader_start_x: leader_sx, leader_start_y: leader_sy,
|
||||||
|
leader_end_x: leader_ex, leader_end_y: leader_ey,
|
||||||
|
cat_label_x: cat_lx, cat_label_y: cat_ly,
|
||||||
|
cat_label_text: cat_text,
|
||||||
|
cat_label_anchor_end: anchor_end,
|
||||||
|
});
|
||||||
|
|
||||||
|
start_angle = end_angle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PieChartLayout {
|
||||||
|
cx, cy, radius, inner_radius: inner_r,
|
||||||
|
slices, show_labels, label_font, label_color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute legend item positions.
|
||||||
|
pub fn compute_legend(
|
||||||
|
data: &dyn ChartDataSource,
|
||||||
|
cl: &ChartLayout,
|
||||||
|
origin_x: f64,
|
||||||
|
origin_y: f64,
|
||||||
|
total_w: f64,
|
||||||
|
total_h: f64,
|
||||||
|
) -> LegendLayout {
|
||||||
|
let font_size = cl.legend_font;
|
||||||
|
let position = cl.legend_pos.clone();
|
||||||
|
let swatch_size = 2.5;
|
||||||
|
let item_gap = 3.0 + font_size * 0.4;
|
||||||
|
let spacing = 4.0;
|
||||||
|
|
||||||
|
let is_pie = matches!(data.chart_type(), ChartType::Pie);
|
||||||
|
let names: Vec<String> = if is_pie {
|
||||||
|
data.categories().to_vec()
|
||||||
|
} else {
|
||||||
|
(0..data.series_count()).map(|i| data.series_name(i).to_string()).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
match position.as_str() {
|
||||||
|
"top" => {
|
||||||
|
let y = origin_y + cl.margin_top - font_size - 1.5;
|
||||||
|
let mut x = origin_x + cl.margin_left;
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
items.push(LegendItemLayout {
|
||||||
|
name: name.clone(), color_idx: i,
|
||||||
|
swatch_x: x, swatch_y: y - font_size * 0.3,
|
||||||
|
text_x: x + item_gap, text_y: y + font_size * 0.3,
|
||||||
|
});
|
||||||
|
x += item_gap + name.len() as f64 * font_size * 0.5 + spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"right" => {
|
||||||
|
let x = origin_x + cl.margin_left + cl.plot_w + 4.0;
|
||||||
|
let mut y = origin_y + cl.margin_top + 2.0;
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
items.push(LegendItemLayout {
|
||||||
|
name: name.clone(), color_idx: i,
|
||||||
|
swatch_x: x, swatch_y: y,
|
||||||
|
text_x: x + item_gap, text_y: y + font_size * 0.7,
|
||||||
|
});
|
||||||
|
y += font_size + 2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// bottom (default)
|
||||||
|
let y = origin_y + total_h - 3.0;
|
||||||
|
let total_legend_w: f64 = names.iter()
|
||||||
|
.map(|n| item_gap + n.len() as f64 * font_size * 0.5 + spacing)
|
||||||
|
.sum::<f64>() - spacing;
|
||||||
|
let mut x = origin_x + (total_w - total_legend_w) / 2.0;
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
items.push(LegendItemLayout {
|
||||||
|
name: name.clone(), color_idx: i,
|
||||||
|
swatch_x: x, swatch_y: y - font_size * 0.3,
|
||||||
|
text_x: x + item_gap, text_y: y + font_size * 0.3,
|
||||||
|
});
|
||||||
|
x += item_gap + name.len() as f64 * font_size * 0.5 + spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LegendLayout { items, font_size, position, swatch_size }
|
||||||
|
}
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
|
use crate::chart_layout::{
|
||||||
|
self, color_at, compute_bar_layout, compute_chart_layout, compute_legend,
|
||||||
|
compute_line_layout, compute_pie_layout, format_value, ChartLayout,
|
||||||
|
};
|
||||||
use crate::data_resolve::ResolvedChartData;
|
use crate::data_resolve::ResolvedChartData;
|
||||||
use dreport_core::models::{ChartType, GroupMode};
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
pub const DEFAULT_COLORS: &[&str] = &[
|
|
||||||
"#4F46E5", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899", "#06B6D4", "#84CC16",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn color_at(palette: &[String], i: usize) -> &str {
|
|
||||||
&palette[i % palette.len()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// mm cinsinden chart SVG uret
|
/// mm cinsinden chart SVG uret
|
||||||
pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> String {
|
pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> String {
|
||||||
let mut svg = String::with_capacity(4096);
|
let mut svg = String::with_capacity(4096);
|
||||||
@@ -33,37 +28,11 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Max sayida renk: kategoriler + seriler
|
let cl = compute_chart_layout(data, width_mm, height_mm, 0.0, 0.0);
|
||||||
let n_colors = data.categories.len().max(data.series.len()).max(1);
|
|
||||||
let palette: Vec<String> = (0..n_colors)
|
|
||||||
.map(|i| {
|
|
||||||
if let Some(ref user_colors) = data.style.colors {
|
|
||||||
if i < user_colors.len() {
|
|
||||||
return user_colors[i].clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DEFAULT_COLORS[i % DEFAULT_COLORS.len()].to_string()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// Margin hesaplari
|
|
||||||
let mut margin_top = 2.0_f64;
|
|
||||||
let mut margin_bottom = 4.0_f64;
|
|
||||||
let mut margin_left = 8.0_f64;
|
|
||||||
let margin_right = 4.0_f64;
|
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
if let Some(ref title) = data.title {
|
if let Some(ref title) = cl.title {
|
||||||
if !title.text.is_empty() {
|
let anchor = match title.align.as_str() {
|
||||||
let font_size = title.font_size.unwrap_or(4.0);
|
|
||||||
margin_top += font_size * 0.4 + 2.0;
|
|
||||||
let color = title.color.as_deref().unwrap_or("#333333");
|
|
||||||
let align = title.align.as_deref().unwrap_or("center");
|
|
||||||
let x = match align {
|
|
||||||
"left" => margin_left,
|
|
||||||
"right" => width_mm - margin_right,
|
|
||||||
_ => width_mm / 2.0,
|
|
||||||
};
|
|
||||||
let anchor = match align {
|
|
||||||
"left" => "start",
|
"left" => "start",
|
||||||
"right" => "end",
|
"right" => "end",
|
||||||
_ => "middle",
|
_ => "middle",
|
||||||
@@ -71,96 +40,28 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
|
|||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="{}" font-weight="bold">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="{}" font-weight="bold">{}</text>"##,
|
||||||
x,
|
title.x, title.y, title.font_size, title.color, anchor, escape_xml(&title.text)
|
||||||
margin_top - 1.0,
|
|
||||||
font_size,
|
|
||||||
color,
|
|
||||||
anchor,
|
|
||||||
escape_xml(&title.text)
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Legend space
|
|
||||||
let legend_show = data.legend.as_ref().is_some_and(|l| l.show);
|
|
||||||
let legend_pos = data
|
|
||||||
.legend
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|l| l.position.as_deref())
|
|
||||||
.unwrap_or("bottom");
|
|
||||||
let legend_font = data
|
|
||||||
.legend
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|l| l.font_size)
|
|
||||||
.unwrap_or(2.8);
|
|
||||||
|
|
||||||
if legend_show && data.series.len() > 1 {
|
|
||||||
match legend_pos {
|
|
||||||
"top" => margin_top += legend_font + 3.0,
|
|
||||||
"bottom" => margin_bottom += legend_font + 3.0,
|
|
||||||
_ => {} // right — icerde handle edilecek
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axis labels icin yer ac (bar ve line)
|
|
||||||
let has_axis = !matches!(data.chart_type, ChartType::Pie);
|
|
||||||
if has_axis {
|
|
||||||
if data.axis.as_ref().and_then(|a| a.x_label.as_ref()).is_some() {
|
|
||||||
margin_bottom += 4.0;
|
|
||||||
}
|
|
||||||
if data.axis.as_ref().and_then(|a| a.y_label.as_ref()).is_some() {
|
|
||||||
margin_left += 4.0;
|
|
||||||
}
|
|
||||||
// Category labels icin alt bosluk
|
|
||||||
let max_label_len = data.categories.iter().map(|c| c.len()).max().unwrap_or(0);
|
|
||||||
let n_cats = data.categories.len();
|
|
||||||
let available_w = width_mm - margin_left - margin_right;
|
|
||||||
let cat_width = if n_cats > 0 {
|
|
||||||
available_w / n_cats as f64
|
|
||||||
} else {
|
|
||||||
available_w
|
|
||||||
};
|
|
||||||
let max_chars_fit = (cat_width / 1.25).max(1.0) as usize;
|
|
||||||
let will_rotate = max_label_len > max_chars_fit;
|
|
||||||
if will_rotate {
|
|
||||||
// Rotated labels (-45°): dikey ≈ text_width * sin(45°), yatay ≈ text_width * cos(45°)
|
|
||||||
let char_w_mm = 1.1;
|
|
||||||
let max_text_w = max_label_len as f64 * char_w_mm;
|
|
||||||
let label_v = max_text_w * 0.707; // sin(45°)
|
|
||||||
margin_bottom += label_v.min(25.0).max(6.0);
|
|
||||||
// Sol taraftaki label yana tasabilir
|
|
||||||
let label_h = max_text_w * 0.707; // cos(45°)
|
|
||||||
let extra_left = (label_h - cat_width / 2.0).max(0.0);
|
|
||||||
margin_left += extra_left.min(10.0);
|
|
||||||
} else {
|
|
||||||
margin_bottom += 4.0;
|
|
||||||
}
|
|
||||||
// Y-axis value labels icin sol bosluk
|
|
||||||
margin_left += 6.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let plot_x = margin_left;
|
|
||||||
let plot_y = margin_top;
|
|
||||||
let plot_w = (width_mm - margin_left - margin_right).max(1.0);
|
|
||||||
let plot_h = (height_mm - margin_top - margin_bottom).max(1.0);
|
|
||||||
|
|
||||||
match data.chart_type {
|
match data.chart_type {
|
||||||
ChartType::Bar => render_bar(&mut svg, data, &palette, plot_x, plot_y, plot_w, plot_h),
|
dreport_core::models::ChartType::Bar => render_bar(&mut svg, data, &cl),
|
||||||
ChartType::Line => render_line(&mut svg, data, &palette, plot_x, plot_y, plot_w, plot_h),
|
dreport_core::models::ChartType::Line => render_line(&mut svg, data, &cl),
|
||||||
ChartType::Pie => render_pie(&mut svg, data, &palette, width_mm, height_mm, plot_x, plot_y, plot_w, plot_h),
|
dreport_core::models::ChartType::Pie => render_pie(&mut svg, data, &cl),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legend render
|
// Legend render
|
||||||
if legend_show && data.series.len() > 1 {
|
if cl.legend_show && data.series.len() > 1 {
|
||||||
render_legend(&mut svg, data, &palette, legend_pos, legend_font, width_mm, height_mm, margin_left, margin_top, plot_w, plot_h);
|
render_legend(&mut svg, data, &cl, width_mm, height_mm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Axis labels
|
// Axis labels
|
||||||
|
let has_axis = !matches!(data.chart_type, dreport_core::models::ChartType::Pie);
|
||||||
if has_axis {
|
if has_axis {
|
||||||
if let Some(ref axis) = data.axis {
|
if let Some(ref axis) = data.axis {
|
||||||
if let Some(ref x_label) = axis.x_label {
|
if let Some(ref x_label) = axis.x_label {
|
||||||
let x = plot_x + plot_w / 2.0;
|
let x = cl.plot_x + cl.plot_w / 2.0;
|
||||||
let y = height_mm - 2.0;
|
let y = height_mm - 2.0;
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
@@ -171,7 +72,7 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
|
|||||||
}
|
}
|
||||||
if let Some(ref y_label) = axis.y_label {
|
if let Some(ref y_label) = axis.y_label {
|
||||||
let x = 3.0;
|
let x = 3.0;
|
||||||
let y = plot_y + plot_h / 2.0;
|
let y = cl.plot_y + cl.plot_h / 2.0;
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.8" fill="#666" text-anchor="middle" transform="rotate(-90,{:.2},{:.2})">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="2.8" fill="#666" text-anchor="middle" transform="rotate(-90,{:.2},{:.2})">{}</text>"##,
|
||||||
@@ -186,199 +87,91 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
|
|||||||
svg
|
svg
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_bar(
|
fn render_bar(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||||
svg: &mut String,
|
|
||||||
data: &ResolvedChartData,
|
|
||||||
palette: &[String],
|
|
||||||
px: f64,
|
|
||||||
py: f64,
|
|
||||||
pw: f64,
|
|
||||||
ph: f64,
|
|
||||||
) {
|
|
||||||
if data.categories.is_empty() || data.series.is_empty() {
|
if data.categories.is_empty() || data.series.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stacked = matches!(data.group_mode, Some(GroupMode::Stacked));
|
let bl = compute_bar_layout(data, cl);
|
||||||
let (min_val, max_val) = value_range(data, stacked);
|
|
||||||
|
|
||||||
let show_grid = data.axis.as_ref().and_then(|a| a.show_grid).unwrap_or(true);
|
// Y axis
|
||||||
let grid_color = data
|
render_y_axis_svg(svg, &bl.y_axis);
|
||||||
.axis
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|a| a.grid_color.as_deref())
|
|
||||||
.unwrap_or("#E5E7EB");
|
|
||||||
|
|
||||||
// Grid + Y axis labels
|
// Bars
|
||||||
render_y_axis(svg, min_val, max_val, px, py, pw, ph, show_grid, grid_color);
|
for bar in &bl.bars {
|
||||||
|
let color = color_at(&cl.palette, bar.color_idx);
|
||||||
let n_cats = data.categories.len();
|
|
||||||
let n_series = data.series.len();
|
|
||||||
let cat_width = pw / n_cats as f64;
|
|
||||||
let bar_gap = data.style.bar_gap.unwrap_or(0.2).clamp(0.0, 0.8);
|
|
||||||
let group_width = cat_width * (1.0 - bar_gap);
|
|
||||||
|
|
||||||
let show_labels = data.labels.as_ref().is_some_and(|l| l.show);
|
|
||||||
let label_font = data.labels.as_ref().and_then(|l| l.font_size).unwrap_or(2.2);
|
|
||||||
let label_color = data
|
|
||||||
.labels
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|l| l.color.as_deref())
|
|
||||||
.unwrap_or("#333");
|
|
||||||
|
|
||||||
let range = if (max_val - min_val).abs() < 1e-10 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
max_val - min_val
|
|
||||||
};
|
|
||||||
|
|
||||||
for ci in 0..data.categories.len() {
|
|
||||||
let cat_x = px + ci as f64 * cat_width;
|
|
||||||
|
|
||||||
if stacked {
|
|
||||||
let mut y_offset = 0.0_f64;
|
|
||||||
for (si, series) in data.series.iter().enumerate() {
|
|
||||||
let val = series.values.get(ci).copied().unwrap_or(0.0);
|
|
||||||
let bar_h = (val / range) * ph;
|
|
||||||
let bar_y = py + ph - y_offset - bar_h;
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="{}" rx="0.5"/>"##,
|
r##"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="{}" rx="0.5"/>"##,
|
||||||
cat_x + cat_width * bar_gap / 2.0,
|
bar.x, bar.y, bar.w, bar.h, color
|
||||||
bar_y,
|
|
||||||
group_width,
|
|
||||||
bar_h.max(0.0),
|
|
||||||
color_at(palette,si)
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if show_labels && val > 0.0 {
|
|
||||||
|
if bl.show_labels {
|
||||||
|
if bl.stacked {
|
||||||
|
if bar.value > 0.0 {
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
||||||
cat_x + cat_width / 2.0,
|
bar.label_x, bar.label_y, bl.label_font, bl.label_color, format_value(bar.value)
|
||||||
bar_y + bar_h / 2.0 + label_font * 0.15,
|
|
||||||
label_font,
|
|
||||||
label_color,
|
|
||||||
format_value(val)
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
y_offset += bar_h;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Grouped
|
|
||||||
let bar_w = group_width / n_series as f64;
|
|
||||||
for (si, series) in data.series.iter().enumerate() {
|
|
||||||
let val = series.values.get(ci).copied().unwrap_or(0.0);
|
|
||||||
let bar_h = ((val - min_val) / range) * ph;
|
|
||||||
let bar_x = cat_x + cat_width * bar_gap / 2.0 + si as f64 * bar_w;
|
|
||||||
let bar_y = py + ph - bar_h;
|
|
||||||
write!(
|
|
||||||
svg,
|
|
||||||
r##"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="{}" rx="0.5"/>"##,
|
|
||||||
bar_x,
|
|
||||||
bar_y,
|
|
||||||
bar_w.max(0.1),
|
|
||||||
bar_h.max(0.0),
|
|
||||||
color_at(palette,si)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
if show_labels {
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
||||||
bar_x + bar_w / 2.0,
|
bar.label_x, bar.label_y, bl.label_font, bl.label_color, format_value(bar.value)
|
||||||
bar_y - 0.8,
|
|
||||||
label_font,
|
|
||||||
label_color,
|
|
||||||
format_value(val)
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// X axis labels
|
||||||
|
render_x_labels_svg(svg, &bl.x_labels);
|
||||||
// X axis labels — rotate if too many categories
|
|
||||||
render_x_labels(svg, &data.categories, px, py + ph, pw, n_cats);
|
|
||||||
|
|
||||||
// X axis line
|
// X axis line
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#9CA3AF" stroke-width="0.3"/>"##,
|
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#9CA3AF" stroke-width="0.3"/>"##,
|
||||||
px, py + ph, px + pw, py + ph
|
bl.x_axis_x1, bl.x_axis_y, bl.x_axis_x2, bl.x_axis_y
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_line(
|
fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||||
svg: &mut String,
|
|
||||||
data: &ResolvedChartData,
|
|
||||||
palette: &[String],
|
|
||||||
px: f64,
|
|
||||||
py: f64,
|
|
||||||
pw: f64,
|
|
||||||
ph: f64,
|
|
||||||
) {
|
|
||||||
if data.categories.is_empty() || data.series.is_empty() {
|
if data.categories.is_empty() || data.series.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (min_val, max_val) = value_range(data, false);
|
let ll = compute_line_layout(data, cl);
|
||||||
let range = if (max_val - min_val).abs() < 1e-10 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
max_val - min_val
|
|
||||||
};
|
|
||||||
|
|
||||||
let show_grid = data.axis.as_ref().and_then(|a| a.show_grid).unwrap_or(true);
|
// Y axis
|
||||||
let grid_color = data
|
render_y_axis_svg(svg, &ll.y_axis);
|
||||||
.axis
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|a| a.grid_color.as_deref())
|
|
||||||
.unwrap_or("#E5E7EB");
|
|
||||||
render_y_axis(svg, min_val, max_val, px, py, pw, ph, show_grid, grid_color);
|
|
||||||
|
|
||||||
let n_cats = data.categories.len();
|
for series_layout in &ll.series {
|
||||||
let line_w = data.style.line_width.unwrap_or(0.5);
|
let color = color_at(&cl.palette, series_layout.color_idx);
|
||||||
let show_points = data.style.show_points.unwrap_or(true);
|
|
||||||
let show_labels = data.labels.as_ref().is_some_and(|l| l.show);
|
|
||||||
let label_font = data.labels.as_ref().and_then(|l| l.font_size).unwrap_or(2.2);
|
|
||||||
let label_color = data
|
|
||||||
.labels
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|l| l.color.as_deref())
|
|
||||||
.unwrap_or("#333");
|
|
||||||
|
|
||||||
for (si, series) in data.series.iter().enumerate() {
|
|
||||||
let color = color_at(palette,si);
|
|
||||||
let mut points = String::new();
|
let mut points = String::new();
|
||||||
let mut point_circles = String::new();
|
let mut point_circles = String::new();
|
||||||
|
|
||||||
for (ci, val) in series.values.iter().enumerate() {
|
for pt in &series_layout.points {
|
||||||
let x = if n_cats == 1 {
|
write!(points, "{:.2},{:.2} ", pt.x, pt.y).unwrap();
|
||||||
px + pw / 2.0
|
|
||||||
} else {
|
|
||||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
|
||||||
};
|
|
||||||
let y = py + ph - ((val - min_val) / range) * ph;
|
|
||||||
write!(points, "{:.2},{:.2} ", x, y).unwrap();
|
|
||||||
|
|
||||||
if show_points {
|
if ll.show_points {
|
||||||
write!(
|
write!(
|
||||||
point_circles,
|
point_circles,
|
||||||
r##"<circle cx="{:.2}" cy="{:.2}" r="0.8" fill="{}" stroke="white" stroke-width="0.3"/>"##,
|
r##"<circle cx="{:.2}" cy="{:.2}" r="0.8" fill="{}" stroke="white" stroke-width="0.3"/>"##,
|
||||||
x, y, color
|
pt.x, pt.y, color
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if show_labels {
|
if ll.show_labels {
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
||||||
x, y - 1.5, label_font, label_color, format_value(*val)
|
pt.x, pt.y - 1.5, ll.label_font, ll.label_color, format_value(pt.value)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -387,100 +180,50 @@ fn render_line(
|
|||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<polyline points="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
r##"<polyline points="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||||
points.trim(),
|
points.trim(), color, ll.line_width
|
||||||
color,
|
|
||||||
line_w
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
svg.push_str(&point_circles);
|
svg.push_str(&point_circles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// X axis labels — for line chart, spacing is different
|
// X axis labels
|
||||||
render_x_labels_line(svg, &data.categories, px, py + ph, pw, n_cats);
|
render_x_labels_svg(svg, &ll.x_labels);
|
||||||
|
|
||||||
// Axis lines
|
// Axis line
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#9CA3AF" stroke-width="0.3"/>"##,
|
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#9CA3AF" stroke-width="0.3"/>"##,
|
||||||
px, py + ph, px + pw, py + ph
|
ll.x_axis_x1, ll.x_axis_y, ll.x_axis_x2, ll.x_axis_y
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_pie(
|
fn render_pie(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||||
svg: &mut String,
|
let pl = compute_pie_layout(data, cl);
|
||||||
data: &ResolvedChartData,
|
|
||||||
palette: &[String],
|
|
||||||
_total_w: f64,
|
|
||||||
_total_h: f64,
|
|
||||||
px: f64,
|
|
||||||
py: f64,
|
|
||||||
pw: f64,
|
|
||||||
ph: f64,
|
|
||||||
) {
|
|
||||||
// Pie icin ilk serinin degerlerini kullan (veya tum serilerin toplamlarini)
|
|
||||||
let values: Vec<f64> = if data.series.len() == 1 {
|
|
||||||
data.series[0].values.clone()
|
|
||||||
} else {
|
|
||||||
// Birden fazla seri varsa, her kategori icin toplam al
|
|
||||||
data.categories
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(ci, _)| {
|
|
||||||
data.series
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.values.get(ci).copied().unwrap_or(0.0))
|
|
||||||
.sum()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let total: f64 = values.iter().sum();
|
if pl.slices.is_empty() {
|
||||||
if total <= 0.0 || data.categories.is_empty() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cx = px + pw / 2.0;
|
let cx = pl.cx;
|
||||||
let cy = py + ph / 2.0;
|
let cy = pl.cy;
|
||||||
let radius = pw.min(ph) / 2.0 * 0.65;
|
let radius = pl.radius;
|
||||||
let inner_frac = data.style.inner_radius.unwrap_or(0.0).clamp(0.0, 0.9);
|
let inner_r = pl.inner_radius;
|
||||||
let inner_r = radius * inner_frac;
|
|
||||||
|
|
||||||
let show_labels = data.labels.as_ref().is_some_and(|l| l.show);
|
for slice in &pl.slices {
|
||||||
let label_font = data.labels.as_ref().and_then(|l| l.font_size).unwrap_or(3.0);
|
let color = color_at(&cl.palette, slice.color_idx);
|
||||||
let label_color = data
|
let large_arc = if slice.sweep > std::f64::consts::PI { 1 } else { 0 };
|
||||||
.labels
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|l| l.color.as_deref())
|
|
||||||
.unwrap_or("#333");
|
|
||||||
|
|
||||||
let mut start_angle = -std::f64::consts::FRAC_PI_2; // 12 o'clock
|
let x1 = cx + radius * slice.start_angle.cos();
|
||||||
|
let y1 = cy + radius * slice.start_angle.sin();
|
||||||
for (i, val) in values.iter().enumerate() {
|
let x2 = cx + radius * slice.end_angle.cos();
|
||||||
if *val <= 0.0 {
|
let y2 = cy + radius * slice.end_angle.sin();
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let sweep = (val / total) * std::f64::consts::TAU;
|
|
||||||
let end_angle = start_angle + sweep;
|
|
||||||
let large_arc = if sweep > std::f64::consts::PI {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let x1 = cx + radius * start_angle.cos();
|
|
||||||
let y1 = cy + radius * start_angle.sin();
|
|
||||||
let x2 = cx + radius * end_angle.cos();
|
|
||||||
let y2 = cy + radius * end_angle.sin();
|
|
||||||
|
|
||||||
let color = color_at(palette,i);
|
|
||||||
|
|
||||||
if inner_r > 0.0 {
|
if inner_r > 0.0 {
|
||||||
// Donut
|
let ix1 = cx + inner_r * slice.start_angle.cos();
|
||||||
let ix1 = cx + inner_r * start_angle.cos();
|
let iy1 = cy + inner_r * slice.start_angle.sin();
|
||||||
let iy1 = cy + inner_r * start_angle.sin();
|
let ix2 = cx + inner_r * slice.end_angle.cos();
|
||||||
let ix2 = cx + inner_r * end_angle.cos();
|
let iy2 = cy + inner_r * slice.end_angle.sin();
|
||||||
let iy2 = cy + inner_r * end_angle.sin();
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<path d="M {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2} L {:.2} {:.2} A {:.2} {:.2} 0 {} 0 {:.2} {:.2} Z" fill="{}" stroke="white" stroke-width="0.3"/>"##,
|
r##"<path d="M {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2} L {:.2} {:.2} A {:.2} {:.2} 0 {} 0 {:.2} {:.2} Z" fill="{}" stroke="white" stroke-width="0.3"/>"##,
|
||||||
@@ -490,7 +233,6 @@ fn render_pie(
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
// Full pie
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<path d="M {:.2} {:.2} L {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2} Z" fill="{}" stroke="white" stroke-width="0.3"/>"##,
|
r##"<path d="M {:.2} {:.2} L {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2} Z" fill="{}" stroke="white" stroke-width="0.3"/>"##,
|
||||||
@@ -500,258 +242,75 @@ fn render_pie(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Percentage label inside slice
|
// Percentage label inside slice
|
||||||
if show_labels {
|
if pl.show_labels {
|
||||||
let mid_angle = start_angle + sweep / 2.0;
|
|
||||||
let label_r = if inner_r > 0.0 {
|
|
||||||
(radius + inner_r) / 2.0
|
|
||||||
} else {
|
|
||||||
radius * 0.65
|
|
||||||
};
|
|
||||||
let lx = cx + label_r * mid_angle.cos();
|
|
||||||
let ly = cy + label_r * mid_angle.sin();
|
|
||||||
let pct = (val / total * 100.0).round();
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle" dominant-baseline="central">{}%</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle" dominant-baseline="central">{}%</text>"##,
|
||||||
lx, ly, label_font, label_color, pct
|
slice.label_x, slice.label_y, pl.label_font, pl.label_color,
|
||||||
|
(slice.fraction * 100.0).round()
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category name label outside slice with leader line
|
// Category name label outside slice with leader line
|
||||||
if i < data.categories.len() {
|
if !slice.cat_label_text.is_empty() {
|
||||||
let mid_angle = start_angle + sweep / 2.0;
|
|
||||||
let line_start_r = radius; // starts at pie edge
|
|
||||||
let line_end_r = radius + 3.0;
|
|
||||||
let text_r = radius + 4.0;
|
|
||||||
|
|
||||||
// Leader line from pie edge to label
|
|
||||||
let lx1 = cx + line_start_r * mid_angle.cos();
|
|
||||||
let ly1 = cy + line_start_r * mid_angle.sin();
|
|
||||||
let lx2 = cx + line_end_r * mid_angle.cos();
|
|
||||||
let ly2 = cy + line_end_r * mid_angle.sin();
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#999" stroke-width="0.2"/>"##,
|
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#999" stroke-width="0.2"/>"##,
|
||||||
lx1, ly1, lx2, ly2
|
slice.leader_start_x, slice.leader_start_y,
|
||||||
|
slice.leader_end_x, slice.leader_end_y
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Category text
|
let anchor = if slice.cat_label_anchor_end { "end" } else { "start" };
|
||||||
let tx = cx + text_r * mid_angle.cos();
|
|
||||||
let ty = cy + text_r * mid_angle.sin();
|
|
||||||
let anchor = if mid_angle.cos() >= 0.0 { "start" } else { "end" };
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#555" text-anchor="{}" dominant-baseline="central">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#555" text-anchor="{}" dominant-baseline="central">{}</text>"##,
|
||||||
tx, ty, anchor, escape_xml(&data.categories[i])
|
slice.cat_label_x, slice.cat_label_y, anchor, escape_xml(&slice.cat_label_text)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
start_angle = end_angle;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_legend(
|
fn render_legend(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout, total_w: f64, total_h: f64) {
|
||||||
svg: &mut String,
|
let legend = compute_legend(data, cl, 0.0, 0.0, total_w, total_h);
|
||||||
data: &ResolvedChartData,
|
|
||||||
palette: &[String],
|
|
||||||
position: &str,
|
|
||||||
font_size: f64,
|
|
||||||
total_w: f64,
|
|
||||||
total_h: f64,
|
|
||||||
margin_left: f64,
|
|
||||||
margin_top: f64,
|
|
||||||
plot_w: f64,
|
|
||||||
_plot_h: f64,
|
|
||||||
) {
|
|
||||||
let names: Vec<&str> = if matches!(data.chart_type, ChartType::Pie) {
|
|
||||||
data.categories.iter().map(|s| s.as_str()).collect()
|
|
||||||
} else {
|
|
||||||
data.series.iter().map(|s| s.name.as_str()).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let item_w = 3.0 + font_size * 0.4; // color rect + gap
|
for item in &legend.items {
|
||||||
let spacing = 4.0;
|
let color = color_at(&cl.palette, item.color_idx);
|
||||||
|
|
||||||
match position {
|
|
||||||
"top" => {
|
|
||||||
let y = margin_top - font_size - 1.5;
|
|
||||||
let mut x = margin_left;
|
|
||||||
for (i, name) in names.iter().enumerate() {
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<rect x="{:.2}" y="{:.2}" width="2.5" height="2.5" fill="{}" rx="0.3"/>"##,
|
r##"<rect x="{:.2}" y="{:.2}" width="2.5" height="2.5" fill="{}" rx="0.3"/>"##,
|
||||||
x, y - font_size * 0.3, color_at(palette,i)
|
item.swatch_x, item.swatch_y, color
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="#666">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="#666">{}</text>"##,
|
||||||
x + item_w, y + font_size * 0.3, font_size, escape_xml(name)
|
item.text_x, item.text_y, legend.font_size, escape_xml(&item.name)
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
x += item_w + name.len() as f64 * font_size * 0.5 + spacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"right" => {
|
|
||||||
let x = margin_left + plot_w + 4.0;
|
|
||||||
let mut y = margin_top + 2.0;
|
|
||||||
for (i, name) in names.iter().enumerate() {
|
|
||||||
write!(
|
|
||||||
svg,
|
|
||||||
r##"<rect x="{:.2}" y="{:.2}" width="2.5" height="2.5" fill="{}" rx="0.3"/>"##,
|
|
||||||
x, y, color_at(palette,i)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
write!(
|
|
||||||
svg,
|
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="#666">{}</text>"##,
|
|
||||||
x + item_w, y + font_size * 0.7, font_size, escape_xml(name)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
y += font_size + 2.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// bottom (default)
|
|
||||||
let y = total_h - 3.0;
|
|
||||||
let total_legend_w: f64 = names
|
|
||||||
.iter()
|
|
||||||
.map(|n| item_w + n.len() as f64 * font_size * 0.5 + spacing)
|
|
||||||
.sum::<f64>()
|
|
||||||
- spacing;
|
|
||||||
let mut x = (total_w - total_legend_w) / 2.0;
|
|
||||||
for (i, name) in names.iter().enumerate() {
|
|
||||||
write!(
|
|
||||||
svg,
|
|
||||||
r##"<rect x="{:.2}" y="{:.2}" width="2.5" height="2.5" fill="{}" rx="0.3"/>"##,
|
|
||||||
x, y - font_size * 0.3, color_at(palette,i)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
write!(
|
|
||||||
svg,
|
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="#666">{}</text>"##,
|
|
||||||
x + item_w, y + font_size * 0.3, font_size, escape_xml(name)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
x += item_w + name.len() as f64 * font_size * 0.5 + spacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// X-axis labels ortak render — bar chart icin (slot-based spacing)
|
|
||||||
fn render_x_labels(
|
|
||||||
svg: &mut String,
|
|
||||||
categories: &[String],
|
|
||||||
px: f64,
|
|
||||||
baseline_y: f64,
|
|
||||||
pw: f64,
|
|
||||||
n_cats: usize,
|
|
||||||
) {
|
|
||||||
if n_cats == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let cat_width = pw / n_cats as f64;
|
|
||||||
let max_chars = (cat_width / 1.25).max(1.0) as usize;
|
|
||||||
let needs_rotate = categories.iter().any(|c| c.len() > max_chars);
|
|
||||||
|
|
||||||
for (ci, cat) in categories.iter().enumerate() {
|
|
||||||
let x = px + ci as f64 * cat_width + cat_width / 2.0;
|
|
||||||
let y = baseline_y + 2.5;
|
|
||||||
render_single_x_label(svg, cat, x, y, needs_rotate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// X-axis labels — line chart icin (point-based spacing)
|
|
||||||
fn render_x_labels_line(
|
|
||||||
svg: &mut String,
|
|
||||||
categories: &[String],
|
|
||||||
px: f64,
|
|
||||||
baseline_y: f64,
|
|
||||||
pw: f64,
|
|
||||||
n_cats: usize,
|
|
||||||
) {
|
|
||||||
if n_cats == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let spacing = if n_cats == 1 { pw } else { pw / (n_cats - 1) as f64 };
|
|
||||||
let max_chars = (spacing / 1.25).max(1.0) as usize;
|
|
||||||
let needs_rotate = categories.iter().any(|c| c.len() > max_chars);
|
|
||||||
|
|
||||||
for (ci, cat) in categories.iter().enumerate() {
|
|
||||||
let x = if n_cats == 1 {
|
|
||||||
px + pw / 2.0
|
|
||||||
} else {
|
|
||||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
|
||||||
};
|
|
||||||
let y = baseline_y + 2.5;
|
|
||||||
render_single_x_label(svg, cat, x, y, needs_rotate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tek bir X-axis label render — rotate gerekiyorsa -45° ile, anchor "end"
|
|
||||||
/// Anchor noktasi bar/point'in tam altinda, text sola yukari dogru uzanir
|
|
||||||
fn render_single_x_label(svg: &mut String, text: &str, x: f64, y: f64, rotate: bool) {
|
|
||||||
if rotate {
|
|
||||||
// -45° rotate, text-anchor="end": text, anchor noktasindan sola-yukari dogru uzanir
|
|
||||||
// Bu sayede text asagi-sola tasmaz, sadece yukari-sola gider (plot area icinde kalir)
|
|
||||||
write!(
|
|
||||||
svg,
|
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.2" fill="#666" text-anchor="end" transform="rotate(-45,{:.2},{:.2})">{}</text>"##,
|
|
||||||
x, y, x, y, escape_xml(text)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
write!(
|
|
||||||
svg,
|
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#666" text-anchor="middle">{}</text>"##,
|
|
||||||
x, y, escape_xml(text)
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_y_axis(
|
// ---------------------------------------------------------------------------
|
||||||
svg: &mut String,
|
// SVG-specific helper renderers that consume shared layout structs
|
||||||
min_val: f64,
|
// ---------------------------------------------------------------------------
|
||||||
max_val: f64,
|
|
||||||
px: f64,
|
|
||||||
py: f64,
|
|
||||||
pw: f64,
|
|
||||||
ph: f64,
|
|
||||||
show_grid: bool,
|
|
||||||
grid_color: &str,
|
|
||||||
) {
|
|
||||||
let range = if (max_val - min_val).abs() < 1e-10 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
max_val - min_val
|
|
||||||
};
|
|
||||||
let tick_count = 5;
|
|
||||||
for i in 0..=tick_count {
|
|
||||||
let frac = i as f64 / tick_count as f64;
|
|
||||||
let val = min_val + frac * range;
|
|
||||||
let y = py + ph - frac * ph;
|
|
||||||
|
|
||||||
// Label
|
fn render_y_axis_svg(svg: &mut String, y_axis: &chart_layout::YAxisLayout) {
|
||||||
|
for tick in &y_axis.ticks {
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.3" fill="#666" text-anchor="end">{}</text>"##,
|
r##"<text x="{:.2}" y="{:.2}" font-size="2.3" fill="#666" text-anchor="end">{}</text>"##,
|
||||||
px - 1.5,
|
y_axis.axis_x - 1.5, tick.y + 0.8, tick.label
|
||||||
y + 0.8,
|
|
||||||
format_value(val)
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Grid line
|
if y_axis.show_grid {
|
||||||
if show_grid {
|
|
||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="0.15"/>"##,
|
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="0.15"/>"##,
|
||||||
px, y, px + pw, y, grid_color
|
y_axis.axis_x, tick.y, y_axis.grid_end_x, tick.y, y_axis.grid_color
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -761,57 +320,29 @@ fn render_y_axis(
|
|||||||
write!(
|
write!(
|
||||||
svg,
|
svg,
|
||||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#9CA3AF" stroke-width="0.3"/>"##,
|
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#9CA3AF" stroke-width="0.3"/>"##,
|
||||||
px, py, px, py + ph
|
y_axis.axis_x, y_axis.axis_y_start, y_axis.axis_x, y_axis.axis_y_end
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tum serilerdeki min/max deger araligini bul
|
fn render_x_labels_svg(svg: &mut String, x_labels: &chart_layout::XLabelLayout) {
|
||||||
fn value_range(data: &ResolvedChartData, stacked: bool) -> (f64, f64) {
|
for label in &x_labels.labels {
|
||||||
if data.series.is_empty() {
|
if x_labels.needs_rotate {
|
||||||
return (0.0, 1.0);
|
write!(
|
||||||
}
|
svg,
|
||||||
|
r##"<text x="{:.2}" y="{:.2}" font-size="2.2" fill="#666" text-anchor="end" transform="rotate(-45,{:.2},{:.2})">{}</text>"##,
|
||||||
if stacked {
|
label.x, label.y, label.x, label.y, escape_xml(&label.text)
|
||||||
let n = data.categories.len();
|
)
|
||||||
let mut max_stack = 0.0_f64;
|
.unwrap();
|
||||||
for ci in 0..n {
|
|
||||||
let sum: f64 = data
|
|
||||||
.series
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.values.get(ci).copied().unwrap_or(0.0))
|
|
||||||
.sum();
|
|
||||||
max_stack = max_stack.max(sum);
|
|
||||||
}
|
|
||||||
(0.0, max_stack * 1.05)
|
|
||||||
} else {
|
} else {
|
||||||
let mut min_v = f64::MAX;
|
write!(
|
||||||
let mut max_v = f64::MIN;
|
svg,
|
||||||
for series in &data.series {
|
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#666" text-anchor="middle">{}</text>"##,
|
||||||
for val in &series.values {
|
label.x, label.y, escape_xml(&label.text)
|
||||||
min_v = min_v.min(*val);
|
)
|
||||||
max_v = max_v.max(*val);
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// min sifirdan buyukse sifirdan basla
|
|
||||||
if min_v > 0.0 {
|
|
||||||
min_v = 0.0;
|
|
||||||
}
|
|
||||||
max_v *= 1.05;
|
|
||||||
(min_v, max_v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_value(v: f64) -> String {
|
|
||||||
if v.abs() >= 1_000_000.0 {
|
|
||||||
format!("{:.1}M", v / 1_000_000.0)
|
|
||||||
} else if v.abs() >= 1_000.0 {
|
|
||||||
format!("{:.1}K", v / 1_000.0)
|
|
||||||
} else if v.fract().abs() < 1e-10 {
|
|
||||||
format!("{}", v as i64)
|
|
||||||
} else {
|
|
||||||
format!("{:.1}", v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_xml(s: &str) -> String {
|
fn escape_xml(s: &str) -> String {
|
||||||
|
|||||||
@@ -204,7 +204,13 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|col| {
|
.map(|col| {
|
||||||
let v = resolve_path(item, &col.field);
|
let v = resolve_path(item, &col.field);
|
||||||
value_to_string(v)
|
let raw = value_to_string(v);
|
||||||
|
// Sütun formatı varsa uygula (currency, percentage, number, date)
|
||||||
|
if let Some(ref fmt) = col.format {
|
||||||
|
crate::expr_eval::apply_format(&raw, Some(fmt.as_str()))
|
||||||
|
} else {
|
||||||
|
raw
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
@@ -449,6 +455,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -493,6 +500,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -537,6 +545,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -573,6 +582,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -638,6 +648,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -687,6 +698,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
|
|||||||
@@ -65,27 +65,43 @@ fn dexpr_value_to_string(val: &DexprValue) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format result with given format type
|
/// Format result with given format type (varsayılan Türk formatı)
|
||||||
pub fn apply_format(value: &str, format: Option<&str>) -> String {
|
pub fn apply_format(value: &str, format: Option<&str>) -> String {
|
||||||
|
apply_format_with_config(value, format, &dreport_core::models::FormatConfig::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format result with given format type and config
|
||||||
|
pub fn apply_format_with_config(value: &str, format: Option<&str>, config: &dreport_core::models::FormatConfig) -> String {
|
||||||
match format {
|
match format {
|
||||||
Some("currency") => format_currency(value),
|
Some("currency") => format_currency(value, config),
|
||||||
Some("percentage") => format_percentage(value),
|
Some("percentage") => format_percentage(value),
|
||||||
Some("number") => format_number_str(value),
|
Some("number") => format_number_str(value, config),
|
||||||
_ => value.to_string(),
|
_ => value.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_currency(value: &str) -> String {
|
fn format_currency(value: &str, config: &dreport_core::models::FormatConfig) -> String {
|
||||||
if let Ok(n) = value.parse::<f64>() {
|
use dexpr::Decimal;
|
||||||
let abs = n.abs();
|
|
||||||
let integer = abs.floor() as i64;
|
|
||||||
let frac = ((abs - abs.floor()) * 100.0).round() as i64;
|
|
||||||
|
|
||||||
let int_str = format_with_thousands(integer);
|
let Ok(d) = value.parse::<Decimal>() else {
|
||||||
let sign = if n < 0.0 { "-" } else { "" };
|
return value.to_string();
|
||||||
format!("{}{},{:02} ₺", sign, int_str, frac)
|
};
|
||||||
|
|
||||||
|
// Round to 2 decimal places using Decimal — no float precision loss
|
||||||
|
// MidpointAwayFromZero: 1.005 → 1.01 (currency convention)
|
||||||
|
let rounded = d.abs().round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero);
|
||||||
|
// Extract integer and fractional parts from the rounded Decimal
|
||||||
|
let truncated = rounded.trunc();
|
||||||
|
let frac_part = rounded - truncated;
|
||||||
|
let integer = truncated.to_string().parse::<i64>().unwrap_or(0);
|
||||||
|
let frac = (frac_part * Decimal::from(100)).trunc().to_string().parse::<i64>().unwrap_or(0);
|
||||||
|
|
||||||
|
let int_str = format_with_thousands(integer, &config.thousands_separator);
|
||||||
|
let sign = if d.is_sign_negative() { "-" } else { "" };
|
||||||
|
if config.currency_position == "prefix" {
|
||||||
|
format!("{}{}{}{}{:02}", config.currency_symbol, sign, int_str, config.decimal_separator, frac)
|
||||||
} else {
|
} else {
|
||||||
value.to_string()
|
format!("{}{}{}{:02} {}", sign, int_str, config.decimal_separator, frac, config.currency_symbol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,19 +113,21 @@ fn format_percentage(value: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_number_str(value: &str) -> String {
|
fn format_number_str(value: &str, config: &dreport_core::models::FormatConfig) -> String {
|
||||||
if let Ok(n) = value.parse::<f64>() {
|
if let Ok(n) = value.parse::<f64>() {
|
||||||
if n == n.floor() && n.abs() < 1e15 {
|
if n == n.floor() && n.abs() < 1e15 {
|
||||||
format_with_thousands(n.abs() as i64)
|
format_with_thousands(n.abs() as i64, &config.thousands_separator)
|
||||||
} else {
|
} else {
|
||||||
format!("{:.2}", n)
|
// Ondalık ayırıcıyı config'den al
|
||||||
|
let formatted = format!("{:.2}", n);
|
||||||
|
formatted.replace('.', &config.decimal_separator)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value.to_string()
|
value.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_with_thousands(n: i64) -> String {
|
fn format_with_thousands(n: i64, separator: &str) -> String {
|
||||||
let s = n.to_string();
|
let s = n.to_string();
|
||||||
let len = s.len();
|
let len = s.len();
|
||||||
if len <= 3 {
|
if len <= 3 {
|
||||||
@@ -118,7 +136,7 @@ fn format_with_thousands(n: i64) -> String {
|
|||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
for (i, ch) in s.chars().enumerate() {
|
for (i, ch) in s.chars().enumerate() {
|
||||||
if i > 0 && (len - i) % 3 == 0 {
|
if i > 0 && (len - i) % 3 == 0 {
|
||||||
result.push('.');
|
result.push_str(separator);
|
||||||
}
|
}
|
||||||
result.push(ch);
|
result.push(ch);
|
||||||
}
|
}
|
||||||
@@ -254,4 +272,49 @@ mod tests {
|
|||||||
"2880"
|
"2880"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_currency_format_basic() {
|
||||||
|
let config = dreport_core::models::FormatConfig::default();
|
||||||
|
assert_eq!(format_currency("1500", &config), "1.500,00 ₺");
|
||||||
|
assert_eq!(format_currency("0", &config), "0,00 ₺");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_currency_format_fractional() {
|
||||||
|
let config = dreport_core::models::FormatConfig::default();
|
||||||
|
assert_eq!(format_currency("19.99", &config), "19,99 ₺");
|
||||||
|
assert_eq!(format_currency("100.50", &config), "100,50 ₺");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_currency_format_rounding_edge_case() {
|
||||||
|
// 1.005 should round to 1.01, not 1.00
|
||||||
|
let config = dreport_core::models::FormatConfig::default();
|
||||||
|
let result = format_currency("1.005", &config);
|
||||||
|
assert_eq!(result, "1,01 ₺");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_currency_format_negative() {
|
||||||
|
let config = dreport_core::models::FormatConfig::default();
|
||||||
|
assert_eq!(format_currency("-250.75", &config), "-250,75 ₺");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_currency_format_large_number() {
|
||||||
|
let config = dreport_core::models::FormatConfig::default();
|
||||||
|
assert_eq!(format_currency("1234567.89", &config), "1.234.567,89 ₺");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_currency_format_prefix_position() {
|
||||||
|
let config = dreport_core::models::FormatConfig {
|
||||||
|
currency_symbol: "$".to_string(),
|
||||||
|
currency_position: "prefix".to_string(),
|
||||||
|
thousands_separator: ",".to_string(),
|
||||||
|
decimal_separator: ".".to_string(),
|
||||||
|
};
|
||||||
|
assert_eq!(format_currency("1500.25", &config), "$1,500.25");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
330
layout-engine/src/font_meta.rs
Normal file
330
layout-engine/src/font_meta.rs
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Parsed metadata from a single font file
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FontMeta {
|
||||||
|
/// Font family name from name table (nameID 16 preferred, fallback nameID 1)
|
||||||
|
pub family: String,
|
||||||
|
/// usWeightClass from OS/2 table (100-900)
|
||||||
|
pub weight: u16,
|
||||||
|
/// fsSelection bit 0 from OS/2 table
|
||||||
|
pub italic: bool,
|
||||||
|
pub units_per_em: u16,
|
||||||
|
/// sTypoAscender from OS/2 table
|
||||||
|
pub ascender: i16,
|
||||||
|
/// sTypoDescender from OS/2 table
|
||||||
|
pub descender: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variant key for looking up a specific font within a family
|
||||||
|
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct FontVariantKey {
|
||||||
|
pub weight: u16,
|
||||||
|
pub italic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Summary of a font family with all its available variants
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FontFamilyInfo {
|
||||||
|
pub family: String,
|
||||||
|
pub variants: Vec<FontVariantKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontMeta {
|
||||||
|
pub fn variant_key(&self) -> FontVariantKey {
|
||||||
|
FontVariantKey {
|
||||||
|
weight: self.weight,
|
||||||
|
italic: self.italic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_bold(&self) -> bool {
|
||||||
|
self.weight >= 700
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Read a big-endian u16 from `data` at `offset`. Returns `None` if out of bounds.
|
||||||
|
fn read_u16(data: &[u8], offset: usize) -> Option<u16> {
|
||||||
|
if offset + 2 > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(u16::from_be_bytes([data[offset], data[offset + 1]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a big-endian i16 from `data` at `offset`. Returns `None` if out of bounds.
|
||||||
|
fn read_i16(data: &[u8], offset: usize) -> Option<i16> {
|
||||||
|
if offset + 2 > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(i16::from_be_bytes([data[offset], data[offset + 1]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a big-endian u32 from `data` at `offset`. Returns `None` if out of bounds.
|
||||||
|
fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
|
||||||
|
if offset + 4 > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(u32::from_be_bytes([
|
||||||
|
data[offset],
|
||||||
|
data[offset + 1],
|
||||||
|
data[offset + 2],
|
||||||
|
data[offset + 3],
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a table in the font's table directory by its 4-byte ASCII tag.
|
||||||
|
/// Returns `(offset, length)` into `data`.
|
||||||
|
fn find_table(data: &[u8], tag: &[u8; 4]) -> Option<(usize, usize)> {
|
||||||
|
// Offset table (first 12 bytes):
|
||||||
|
// 0: sfVersion (u32) — 0x00010000 for TrueType, 'OTTO' for CFF
|
||||||
|
// 4: numTables (u16)
|
||||||
|
// 6: searchRange (u16)
|
||||||
|
// 8: entrySelector (u16)
|
||||||
|
// 10: rangeShift (u16)
|
||||||
|
let num_tables = read_u16(data, 4)? as usize;
|
||||||
|
|
||||||
|
// Table directory starts at offset 12, each entry is 16 bytes:
|
||||||
|
// 0: tag (4 bytes)
|
||||||
|
// 4: checksum (u32)
|
||||||
|
// 8: offset (u32)
|
||||||
|
// 12: length (u32)
|
||||||
|
for i in 0..num_tables {
|
||||||
|
let entry_offset = 12 + i * 16;
|
||||||
|
if entry_offset + 16 > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if &data[entry_offset..entry_offset + 4] == tag {
|
||||||
|
let table_offset = read_u32(data, entry_offset + 8)? as usize;
|
||||||
|
let table_length = read_u32(data, entry_offset + 12)? as usize;
|
||||||
|
// Basic sanity check
|
||||||
|
if table_offset.checked_add(table_length)? > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some((table_offset, table_length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a UTF-16BE byte slice into a `String`.
|
||||||
|
fn decode_utf16be(raw: &[u8]) -> Option<String> {
|
||||||
|
if raw.len() % 2 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let code_units: Vec<u16> = raw
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16(&code_units).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a MacRoman (platform 1, encoding 0) byte slice into a `String`.
|
||||||
|
/// MacRoman overlaps with ASCII for 0x00–0x7F; we accept those and replace
|
||||||
|
/// high bytes with the Unicode replacement character for simplicity, since
|
||||||
|
/// font family names are almost always pure ASCII.
|
||||||
|
fn decode_mac_roman(raw: &[u8]) -> String {
|
||||||
|
raw.iter()
|
||||||
|
.map(|&b| {
|
||||||
|
if b < 0x80 {
|
||||||
|
b as char
|
||||||
|
} else {
|
||||||
|
// Simplified: map non-ASCII MacRoman bytes to replacement char.
|
||||||
|
// Full MacRoman table not needed for typical font family names.
|
||||||
|
'\u{FFFD}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the font family name from the `name` table.
|
||||||
|
///
|
||||||
|
/// Prefers nameID 16 (Typographic Family Name) over nameID 1 (Font Family).
|
||||||
|
/// Among platforms, prefers Windows (3) and Unicode (0) for UTF-16BE, falls
|
||||||
|
/// back to Macintosh (1) for MacRoman.
|
||||||
|
fn read_family_name(data: &[u8], table_offset: usize, table_length: usize) -> Option<String> {
|
||||||
|
let tbl = table_offset;
|
||||||
|
// name table header:
|
||||||
|
// 0: format (u16)
|
||||||
|
// 2: count (u16)
|
||||||
|
// 4: stringOffset (u16) — offset from start of table to string storage
|
||||||
|
let count = read_u16(data, tbl + 2)? as usize;
|
||||||
|
let string_offset = read_u16(data, tbl + 4)? as usize;
|
||||||
|
let storage_base = tbl + string_offset;
|
||||||
|
|
||||||
|
// Each name record (12 bytes, starting at tbl + 6):
|
||||||
|
// 0: platformID (u16)
|
||||||
|
// 2: encodingID (u16)
|
||||||
|
// 4: languageID (u16)
|
||||||
|
// 6: nameID (u16)
|
||||||
|
// 8: length (u16)
|
||||||
|
// 10: offset (u16) — from storage_base
|
||||||
|
|
||||||
|
// We collect candidates, preferring nameID 16 over 1, and Windows/Unicode
|
||||||
|
// over Mac.
|
||||||
|
let mut best: Option<String> = None;
|
||||||
|
let mut best_priority: u8 = 0; // higher = better
|
||||||
|
|
||||||
|
for i in 0..count {
|
||||||
|
let rec = tbl + 6 + i * 12;
|
||||||
|
if rec + 12 > tbl + table_length {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let platform_id = read_u16(data, rec)?;
|
||||||
|
let encoding_id = read_u16(data, rec + 2)?;
|
||||||
|
let name_id = read_u16(data, rec + 6)?;
|
||||||
|
let str_length = read_u16(data, rec + 8)? as usize;
|
||||||
|
let str_offset = read_u16(data, rec + 10)? as usize;
|
||||||
|
|
||||||
|
// Only interested in nameID 1 (Font Family) or 16 (Typographic Family)
|
||||||
|
if name_id != 1 && name_id != 16 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_priority = if name_id == 16 { 4 } else { 0 };
|
||||||
|
|
||||||
|
let abs_start = storage_base + str_offset;
|
||||||
|
let abs_end = abs_start + str_length;
|
||||||
|
if abs_end > data.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let raw = &data[abs_start..abs_end];
|
||||||
|
|
||||||
|
let (decoded, platform_priority) = match platform_id {
|
||||||
|
// Platform 0 — Unicode: UTF-16BE
|
||||||
|
0 => {
|
||||||
|
if let Some(s) = decode_utf16be(raw) {
|
||||||
|
(s, 2u8)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Platform 1 — Macintosh, encoding 0 = MacRoman
|
||||||
|
1 if encoding_id == 0 => (decode_mac_roman(raw), 1u8),
|
||||||
|
// Platform 3 — Windows, encoding 1 = Unicode BMP (UTF-16BE)
|
||||||
|
3 if encoding_id == 1 => {
|
||||||
|
if let Some(s) = decode_utf16be(raw) {
|
||||||
|
(s, 3u8)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let priority = name_priority + platform_priority;
|
||||||
|
if priority > best_priority {
|
||||||
|
best_priority = priority;
|
||||||
|
best = Some(decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse font metadata from raw TTF/OTF bytes.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the data is too short, tables are missing, or offsets
|
||||||
|
/// point outside the buffer.
|
||||||
|
pub fn parse_font_meta(data: &[u8]) -> Option<FontMeta> {
|
||||||
|
// Minimum: 12-byte offset table header
|
||||||
|
if data.len() < 12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- OS/2 table ----
|
||||||
|
let (os2_off, os2_len) = find_table(data, b"OS/2")?;
|
||||||
|
// Need at least 72 bytes for sTypoDescender (offset 70, 2 bytes)
|
||||||
|
if os2_len < 72 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let weight = read_u16(data, os2_off + 4)?;
|
||||||
|
let fs_selection = read_u16(data, os2_off + 62)?;
|
||||||
|
let italic = (fs_selection & 1) != 0;
|
||||||
|
let ascender = read_i16(data, os2_off + 68)?;
|
||||||
|
let descender = read_i16(data, os2_off + 70)?;
|
||||||
|
|
||||||
|
// ---- head table ----
|
||||||
|
let (head_off, head_len) = find_table(data, b"head")?;
|
||||||
|
// unitsPerEm is at offset 18 (2 bytes), so need at least 20 bytes
|
||||||
|
if head_len < 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let units_per_em = read_u16(data, head_off + 18)?;
|
||||||
|
|
||||||
|
// ---- name table ----
|
||||||
|
let (name_off, name_len) = find_table(data, b"name")?;
|
||||||
|
let family = read_family_name(data, name_off, name_len)?;
|
||||||
|
|
||||||
|
Some(FontMeta {
|
||||||
|
family,
|
||||||
|
weight,
|
||||||
|
italic,
|
||||||
|
units_per_em,
|
||||||
|
ascender,
|
||||||
|
descender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_table_returns_none_on_empty() {
|
||||||
|
assert!(find_table(&[], b"head").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_font_meta_returns_none_on_garbage() {
|
||||||
|
assert!(parse_font_meta(&[0u8; 11]).is_none());
|
||||||
|
assert!(parse_font_meta(&[0u8; 64]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variant_key_and_is_bold() {
|
||||||
|
let meta = FontMeta {
|
||||||
|
family: "Test".into(),
|
||||||
|
weight: 700,
|
||||||
|
italic: true,
|
||||||
|
units_per_em: 1000,
|
||||||
|
ascender: 800,
|
||||||
|
descender: -200,
|
||||||
|
};
|
||||||
|
assert!(meta.is_bold());
|
||||||
|
assert!(meta.italic);
|
||||||
|
let key = meta.variant_key();
|
||||||
|
assert_eq!(key.weight, 700);
|
||||||
|
assert!(key.italic);
|
||||||
|
|
||||||
|
let regular = FontMeta {
|
||||||
|
weight: 400,
|
||||||
|
italic: false,
|
||||||
|
..meta.clone()
|
||||||
|
};
|
||||||
|
assert!(!regular.is_bold());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_utf16be_basic() {
|
||||||
|
// "AB" in UTF-16BE
|
||||||
|
let raw = [0x00, 0x41, 0x00, 0x42];
|
||||||
|
assert_eq!(decode_utf16be(&raw).unwrap(), "AB");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_utf16be_odd_length_returns_none() {
|
||||||
|
assert!(decode_utf16be(&[0x00, 0x41, 0x00]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_mac_roman_ascii() {
|
||||||
|
let raw = b"Noto Sans";
|
||||||
|
assert_eq!(decode_mac_roman(raw), "Noto Sans");
|
||||||
|
}
|
||||||
|
}
|
||||||
51
layout-engine/src/font_provider.rs
Normal file
51
layout-engine/src/font_provider.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use crate::font_meta::FontFamilyInfo;
|
||||||
|
use crate::FontData;
|
||||||
|
|
||||||
|
/// Font resolution trait — host apps implement this to provide fonts.
|
||||||
|
/// Backend implements it with a file-based registry, WASM side with API fetching.
|
||||||
|
pub trait FontProvider: Send + Sync {
|
||||||
|
/// List all available font families with their variants.
|
||||||
|
fn list_families(&self) -> Vec<FontFamilyInfo>;
|
||||||
|
|
||||||
|
/// Load a specific font variant. Returns None if not found.
|
||||||
|
fn load_font(&self, family: &str, weight: u16, italic: bool) -> Option<FontData>;
|
||||||
|
|
||||||
|
/// The default/fallback font family name.
|
||||||
|
fn default_family(&self) -> &str {
|
||||||
|
"Noto Sans"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load all variants of the given families. Falls back to default family if a family is not found.
|
||||||
|
/// Always includes the default family.
|
||||||
|
fn load_families(&self, families: &[String]) -> Vec<FontData> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut loaded_families = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
// Always include default family
|
||||||
|
let mut all_families: Vec<String> = vec![self.default_family().to_string()];
|
||||||
|
for f in families {
|
||||||
|
if !all_families.iter().any(|af| af.eq_ignore_ascii_case(f)) {
|
||||||
|
all_families.push(f.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for family in &all_families {
|
||||||
|
let family_lower = family.to_lowercase();
|
||||||
|
if loaded_families.contains(&family_lower) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let infos = self.list_families();
|
||||||
|
if let Some(info) = infos.iter().find(|i| i.family.to_lowercase() == family_lower) {
|
||||||
|
for variant in &info.variants {
|
||||||
|
if let Some(fd) = self.load_font(&info.family, variant.weight, variant.italic) {
|
||||||
|
result.push(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded_families.insert(family_lower);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,10 @@ pub mod expr_eval;
|
|||||||
pub mod wasm_api;
|
pub mod wasm_api;
|
||||||
|
|
||||||
pub mod barcode_gen;
|
pub mod barcode_gen;
|
||||||
|
pub mod chart_layout;
|
||||||
pub mod chart_render;
|
pub mod chart_render;
|
||||||
|
pub mod font_meta;
|
||||||
|
pub mod font_provider;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod pdf_render;
|
pub mod pdf_render;
|
||||||
@@ -18,6 +21,28 @@ pub mod pdf_render;
|
|||||||
use dreport_core::models::{ChartType, Template};
|
use dreport_core::models::{ChartType, Template};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Layout hesaplama hata tipi
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LayoutError {
|
||||||
|
Taffy(taffy::TaffyError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for LayoutError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
LayoutError::Taffy(e) => write!(f, "Taffy layout hatası: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for LayoutError {}
|
||||||
|
|
||||||
|
impl From<taffy::TaffyError> for LayoutError {
|
||||||
|
fn from(e: taffy::TaffyError) -> Self {
|
||||||
|
LayoutError::Taffy(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Layout sonuç tipleri ---
|
// --- Layout sonuç tipleri ---
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -172,6 +197,7 @@ pub struct ResolvedStyle {
|
|||||||
// Text
|
// Text
|
||||||
pub font_size: Option<f64>,
|
pub font_size: Option<f64>,
|
||||||
pub font_weight: Option<String>,
|
pub font_weight: Option<String>,
|
||||||
|
pub font_style: Option<String>,
|
||||||
pub font_family: Option<String>,
|
pub font_family: Option<String>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
pub text_align: Option<String>,
|
pub text_align: Option<String>,
|
||||||
@@ -203,7 +229,7 @@ pub fn compute_layout(
|
|||||||
template: &Template,
|
template: &Template,
|
||||||
data: &serde_json::Value,
|
data: &serde_json::Value,
|
||||||
font_data: &[FontData],
|
font_data: &[FontData],
|
||||||
) -> LayoutResult {
|
) -> Result<LayoutResult, LayoutError> {
|
||||||
let mut measurer = text_measure::TextMeasurer::new(font_data);
|
let mut measurer = text_measure::TextMeasurer::new(font_data);
|
||||||
let resolved = data_resolve::resolve_template(template, data);
|
let resolved = data_resolve::resolve_template(template, data);
|
||||||
tree::compute(template, &resolved, &mut measurer)
|
tree::compute(template, &resolved, &mut measurer)
|
||||||
@@ -217,16 +243,41 @@ pub fn compute_layout_cached(
|
|||||||
data: &serde_json::Value,
|
data: &serde_json::Value,
|
||||||
font_data: &[FontData],
|
font_data: &[FontData],
|
||||||
text_cache: text_measure::TextMeasureCache,
|
text_cache: text_measure::TextMeasureCache,
|
||||||
) -> (LayoutResult, text_measure::TextMeasureCache) {
|
) -> Result<(LayoutResult, text_measure::TextMeasureCache), LayoutError> {
|
||||||
let mut measurer = text_measure::TextMeasurer::new_with_cache(font_data, text_cache);
|
let mut measurer = text_measure::TextMeasurer::new_with_cache(font_data, text_cache);
|
||||||
let resolved = data_resolve::resolve_template(template, data);
|
let resolved = data_resolve::resolve_template(template, data);
|
||||||
let result = tree::compute(template, &resolved, &mut measurer);
|
let result = tree::compute(template, &resolved, &mut measurer)?;
|
||||||
(result, measurer.take_cache())
|
Ok((result, measurer.take_cache()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Font verisi (ham TTF/OTF bytes)
|
/// Font verisi (ham TTF/OTF bytes + metadata)
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FontData {
|
pub struct FontData {
|
||||||
pub family: String,
|
pub family: String,
|
||||||
|
pub weight: u16,
|
||||||
|
pub italic: bool,
|
||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FontData {
|
||||||
|
/// Create FontData from raw bytes, parsing metadata from the font file.
|
||||||
|
/// Returns None if font metadata cannot be parsed.
|
||||||
|
pub fn from_bytes(data: Vec<u8>) -> Option<Self> {
|
||||||
|
let meta = font_meta::parse_font_meta(&data)?;
|
||||||
|
Some(Self {
|
||||||
|
family: meta.family,
|
||||||
|
weight: meta.weight,
|
||||||
|
italic: meta.italic,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create FontData with explicit metadata (when metadata is already known).
|
||||||
|
pub fn new(family: String, weight: u16, italic: bool, data: Vec<u8>) -> Self {
|
||||||
|
Self { family, weight, italic, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_bold(&self) -> bool {
|
||||||
|
self.weight >= 700
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub struct PageSplitInput {
|
|||||||
pub page_number_formats: HashMap<String, String>,
|
pub page_number_formats: HashMap<String, String>,
|
||||||
/// Root container'ın üst padding'i (mm) — sayfa 2+ için body offset
|
/// Root container'ın üst padding'i (mm) — sayfa 2+ için body offset
|
||||||
pub root_padding_top_mm: f64,
|
pub root_padding_top_mm: f64,
|
||||||
|
/// Header tekrarı kapatılmış tablo ID'leri
|
||||||
|
pub no_repeat_header_tables: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Body elemanlarını sayfalara böl, header/footer ekle, page number'ları çöz.
|
/// Body elemanlarını sayfalara böl, header/footer ekle, page number'ları çöz.
|
||||||
@@ -53,7 +55,11 @@ pub fn split_into_pages(input: PageSplitInput) -> Vec<PageLayout> {
|
|||||||
let avoid_groups = build_avoid_groups(&input.body_elements, &input.break_modes, &parent_map);
|
let avoid_groups = build_avoid_groups(&input.body_elements, &input.break_modes, &parent_map);
|
||||||
|
|
||||||
// Tablo yapısı tespiti: table_id → header element id'leri
|
// Tablo yapısı tespiti: table_id → header element id'leri
|
||||||
let table_info = detect_table_structure(&input.body_elements);
|
// repeat_header == false olan tablolar hariç tutulur
|
||||||
|
let mut table_info = detect_table_structure(&input.body_elements);
|
||||||
|
for table_id in &input.no_repeat_header_tables {
|
||||||
|
table_info.remove(table_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Elemanları sayfalara böl
|
// Elemanları sayfalara böl
|
||||||
let page_slices = split_elements(
|
let page_slices = split_elements(
|
||||||
@@ -561,6 +567,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: HashMap::new(),
|
page_number_formats: HashMap::new(),
|
||||||
root_padding_top_mm: 0.0,
|
root_padding_top_mm: 0.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
@@ -584,6 +591,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: HashMap::new(),
|
page_number_formats: HashMap::new(),
|
||||||
root_padding_top_mm: 0.0,
|
root_padding_top_mm: 0.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
@@ -611,6 +619,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: HashMap::new(),
|
page_number_formats: HashMap::new(),
|
||||||
root_padding_top_mm: 0.0,
|
root_padding_top_mm: 0.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
@@ -638,6 +647,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: HashMap::new(),
|
page_number_formats: HashMap::new(),
|
||||||
root_padding_top_mm: 0.0,
|
root_padding_top_mm: 0.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
@@ -678,6 +688,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: formats,
|
page_number_formats: formats,
|
||||||
root_padding_top_mm: 0.0,
|
root_padding_top_mm: 0.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
@@ -768,6 +779,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: HashMap::new(),
|
page_number_formats: HashMap::new(),
|
||||||
root_padding_top_mm: 0.0,
|
root_padding_top_mm: 0.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
@@ -860,6 +872,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: HashMap::new(),
|
page_number_formats: HashMap::new(),
|
||||||
root_padding_top_mm: 0.0,
|
root_padding_top_mm: 0.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
@@ -943,6 +956,7 @@ mod tests {
|
|||||||
break_modes: HashMap::new(),
|
break_modes: HashMap::new(),
|
||||||
page_number_formats: HashMap::new(),
|
page_number_formats: HashMap::new(),
|
||||||
root_padding_top_mm: 5.0,
|
root_padding_top_mm: 5.0,
|
||||||
|
no_repeat_header_tables: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = split_into_pages(input);
|
let pages = split_into_pages(input);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,38 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use dreport_core::models::*;
|
use dreport_core::models::*;
|
||||||
|
|
||||||
use crate::data_resolve::ResolvedData;
|
use crate::data_resolve::ResolvedData;
|
||||||
use crate::text_measure::TextMeasurer;
|
use crate::text_measure::TextMeasurer;
|
||||||
|
|
||||||
|
/// Cache for expanded table containers.
|
||||||
|
/// Key: hash of (table JSON + resolved rows + available width).
|
||||||
|
pub type TableExpandCache = HashMap<u64, ContainerElement>;
|
||||||
|
|
||||||
|
fn table_cache_key(
|
||||||
|
table: &RepeatingTableElement,
|
||||||
|
rows: &[Vec<String>],
|
||||||
|
available_width_mm: f64,
|
||||||
|
) -> u64 {
|
||||||
|
let mut hasher = std::hash::DefaultHasher::new();
|
||||||
|
// Serialize table definition (id, columns, style, etc.)
|
||||||
|
if let Ok(json) = serde_json::to_string(table) {
|
||||||
|
json.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
// Hash resolved row data
|
||||||
|
for row in rows {
|
||||||
|
for cell in row {
|
||||||
|
cell.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
row.len().hash(&mut hasher);
|
||||||
|
}
|
||||||
|
rows.len().hash(&mut hasher);
|
||||||
|
// Hash available width (as bits to avoid float hashing issues)
|
||||||
|
available_width_mm.to_bits().hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
/// Her auto sütun için header + tüm data satırlarındaki en geniş text'i ölç,
|
/// Her auto sütun için header + tüm data satırlarındaki en geniş text'i ölç,
|
||||||
/// doğal genişliklerini Fixed olarak ata.
|
/// doğal genişliklerini Fixed olarak ata.
|
||||||
/// Fr sütunları olduğu gibi bırak (kalan alanı taffy dağıtır).
|
/// Fr sütunları olduğu gibi bırak (kalan alanı taffy dağıtır).
|
||||||
@@ -138,6 +168,29 @@ fn compute_auto_column_widths(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cache-aware table expansion.
|
||||||
|
/// Verilen cache'e bakar, hit varsa clone döner. Miss'te expand edip cache'e yazar.
|
||||||
|
pub fn expand_table_cached(
|
||||||
|
table: &RepeatingTableElement,
|
||||||
|
resolved: &ResolvedData,
|
||||||
|
measurer: &mut TextMeasurer,
|
||||||
|
available_width_mm: f64,
|
||||||
|
cache: &mut TableExpandCache,
|
||||||
|
) -> ContainerElement {
|
||||||
|
let rows = resolved.tables.get(&table.id)
|
||||||
|
.map(|t| t.rows.as_slice())
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
let key = table_cache_key(table, rows, available_width_mm);
|
||||||
|
|
||||||
|
if let Some(cached) = cache.get(&key) {
|
||||||
|
return cached.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = expand_table(table, resolved, measurer, available_width_mm);
|
||||||
|
cache.insert(key, result.clone());
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// RepeatingTable element'ini bir container ağacına expand eder.
|
/// RepeatingTable element'ini bir container ağacına expand eder.
|
||||||
/// Tablo → column container (header row + data rows)
|
/// Tablo → column container (header row + data rows)
|
||||||
/// Her row → row container (cell'ler → static_text)
|
/// Her row → row container (cell'ler → static_text)
|
||||||
@@ -185,6 +238,7 @@ pub fn expand_table(
|
|||||||
style: TextStyle {
|
style: TextStyle {
|
||||||
font_size: table.style.header_font_size.or(table.style.font_size),
|
font_size: table.style.header_font_size.or(table.style.font_size),
|
||||||
font_weight: Some("bold".to_string()),
|
font_weight: Some("bold".to_string()),
|
||||||
|
font_style: None,
|
||||||
font_family: None,
|
font_family: None,
|
||||||
color: table.style.header_color.clone(),
|
color: table.style.header_color.clone(),
|
||||||
align: Some(col.align.clone()),
|
align: Some(col.align.clone()),
|
||||||
@@ -294,6 +348,7 @@ pub fn expand_table(
|
|||||||
style: TextStyle {
|
style: TextStyle {
|
||||||
font_size: table.style.font_size,
|
font_size: table.style.font_size,
|
||||||
font_weight: None,
|
font_weight: None,
|
||||||
|
font_style: None,
|
||||||
font_family: None,
|
font_family: None,
|
||||||
color: None,
|
color: None,
|
||||||
align: Some(col.align.clone()),
|
align: Some(col.align.clone()),
|
||||||
@@ -446,10 +501,7 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.join("backend/fonts/NotoSans-Regular.ttf");
|
.join("backend/fonts/NotoSans-Regular.ttf");
|
||||||
let font_bytes = std::fs::read(&font_path).expect("Font file not found");
|
let font_bytes = std::fs::read(&font_path).expect("Font file not found");
|
||||||
let font_data = vec![FontData {
|
let font_data = vec![FontData::from_bytes(font_bytes).expect("Font parse failed")];
|
||||||
family: "Noto Sans".to_string(),
|
|
||||||
data: font_bytes,
|
|
||||||
}];
|
|
||||||
TextMeasurer::new(&font_data)
|
TextMeasurer::new(&font_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,4 +761,61 @@ mod tests {
|
|||||||
_ => panic!("Expected Container"),
|
_ => panic!("Expected Container"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_cache_hit() {
|
||||||
|
let table = make_table(2);
|
||||||
|
let resolved = make_resolved("tbl", vec![
|
||||||
|
vec!["A".to_string(), "1".to_string()],
|
||||||
|
]);
|
||||||
|
let mut measurer = make_measurer();
|
||||||
|
let mut cache = TableExpandCache::new();
|
||||||
|
|
||||||
|
// First call — cache miss
|
||||||
|
let result1 = expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache);
|
||||||
|
assert_eq!(cache.len(), 1);
|
||||||
|
|
||||||
|
// Second call — same inputs — cache hit
|
||||||
|
let result2 = expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache);
|
||||||
|
assert_eq!(cache.len(), 1); // no new entry
|
||||||
|
assert_eq!(result1.id, result2.id);
|
||||||
|
assert_eq!(result1.children.len(), result2.children.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_cache_miss_on_data_change() {
|
||||||
|
let table = make_table(2);
|
||||||
|
let resolved1 = make_resolved("tbl", vec![
|
||||||
|
vec!["A".to_string(), "1".to_string()],
|
||||||
|
]);
|
||||||
|
let resolved2 = make_resolved("tbl", vec![
|
||||||
|
vec!["B".to_string(), "2".to_string()],
|
||||||
|
]);
|
||||||
|
let mut measurer = make_measurer();
|
||||||
|
let mut cache = TableExpandCache::new();
|
||||||
|
|
||||||
|
expand_table_cached(&table, &resolved1, &mut measurer, 180.0, &mut cache);
|
||||||
|
assert_eq!(cache.len(), 1);
|
||||||
|
|
||||||
|
// Different data — cache miss
|
||||||
|
expand_table_cached(&table, &resolved2, &mut measurer, 180.0, &mut cache);
|
||||||
|
assert_eq!(cache.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_cache_miss_on_width_change() {
|
||||||
|
let table = make_table(2);
|
||||||
|
let resolved = make_resolved("tbl", vec![
|
||||||
|
vec!["A".to_string(), "1".to_string()],
|
||||||
|
]);
|
||||||
|
let mut measurer = make_measurer();
|
||||||
|
let mut cache = TableExpandCache::new();
|
||||||
|
|
||||||
|
expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache);
|
||||||
|
assert_eq!(cache.len(), 1);
|
||||||
|
|
||||||
|
// Different available width — cache miss
|
||||||
|
expand_table_cached(&table, &resolved, &mut measurer, 160.0, &mut cache);
|
||||||
|
assert_eq!(cache.len(), 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ use std::hash::Hash;
|
|||||||
use crate::FontData;
|
use crate::FontData;
|
||||||
use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping, Weight};
|
use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping, Weight};
|
||||||
|
|
||||||
|
/// Tek bir satırın layout bilgisi (PDF render için)
|
||||||
|
pub struct TextLine {
|
||||||
|
pub text: String,
|
||||||
|
pub y_offset_pt: f32,
|
||||||
|
pub width_pt: f32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Rich text span — ölçüm için gerekli bilgiler
|
/// Rich text span — ölçüm için gerekli bilgiler
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RichSpanMeasure {
|
pub struct RichSpanMeasure {
|
||||||
@@ -182,6 +189,58 @@ impl TextMeasurer {
|
|||||||
(width_pt, height_pt)
|
(width_pt, height_pt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Text'i verilen genişlik kısıtı ile satırlara böl.
|
||||||
|
/// Her satır için text içeriği ve y-offset (pt) döner.
|
||||||
|
/// PDF render sırasında text wrapping için kullanılır.
|
||||||
|
pub fn layout_lines(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
font_family: Option<&str>,
|
||||||
|
font_size_pt: f32,
|
||||||
|
font_weight: Option<&str>,
|
||||||
|
available_width_pt: f32,
|
||||||
|
) -> Vec<TextLine> {
|
||||||
|
if text.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let font_size_px = font_size_pt * PT_TO_PX;
|
||||||
|
let line_height_px = font_size_px * 1.2;
|
||||||
|
let metrics = Metrics::new(font_size_px, line_height_px);
|
||||||
|
|
||||||
|
let mut buffer = Buffer::new(&mut self.font_system, metrics);
|
||||||
|
|
||||||
|
let width_px = available_width_pt * PT_TO_PX;
|
||||||
|
buffer.set_size(&mut self.font_system, Some(width_px), None);
|
||||||
|
|
||||||
|
let weight = match font_weight {
|
||||||
|
Some("bold") => Weight::BOLD,
|
||||||
|
_ => Weight::NORMAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
let family_name = font_family.unwrap_or("Noto Sans");
|
||||||
|
let attrs = Attrs::new()
|
||||||
|
.family(Family::Name(family_name))
|
||||||
|
.weight(weight);
|
||||||
|
|
||||||
|
buffer.set_text(&mut self.font_system, text, &attrs, Shaping::Advanced, None);
|
||||||
|
buffer.shape_until_scroll(&mut self.font_system, false);
|
||||||
|
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for run in buffer.layout_runs() {
|
||||||
|
let line_text = run.text.to_string();
|
||||||
|
let line_top_pt = run.line_top / PT_TO_PX;
|
||||||
|
let line_width_pt = run.line_w / PT_TO_PX;
|
||||||
|
lines.push(TextLine {
|
||||||
|
text: line_text,
|
||||||
|
y_offset_pt: line_top_pt,
|
||||||
|
width_pt: line_width_pt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
/// Rich text ölç — birden fazla span, her biri farklı font/boyut/kalınlık.
|
/// Rich text ölç — birden fazla span, her biri farklı font/boyut/kalınlık.
|
||||||
/// cosmic-text set_rich_text() ile attributed text ölçümü yapar.
|
/// cosmic-text set_rich_text() ile attributed text ölçümü yapar.
|
||||||
pub fn measure_rich_text(
|
pub fn measure_rich_text(
|
||||||
@@ -273,18 +332,9 @@ pub(crate) fn load_test_fonts() -> Vec<crate::FontData> {
|
|||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().is_some_and(|e| e == "ttf") {
|
if path.extension().is_some_and(|e| e == "ttf") {
|
||||||
let data = std::fs::read(&path).unwrap();
|
let data = std::fs::read(&path).unwrap();
|
||||||
let family = if path
|
if let Some(fd) = crate::FontData::from_bytes(data) {
|
||||||
.file_name()
|
fonts.push(fd);
|
||||||
.unwrap()
|
}
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.contains("Mono")
|
|
||||||
{
|
|
||||||
"Noto Sans Mono".to_string()
|
|
||||||
} else {
|
|
||||||
"Noto Sans".to_string()
|
|
||||||
};
|
|
||||||
fonts.push(crate::FontData { family, data });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fonts
|
fonts
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ use taffy::prelude::*;
|
|||||||
|
|
||||||
use crate::data_resolve::ResolvedData;
|
use crate::data_resolve::ResolvedData;
|
||||||
use crate::sizing::{self, mm_to_pt, pt_to_mm};
|
use crate::sizing::{self, mm_to_pt, pt_to_mm};
|
||||||
use crate::table_layout;
|
use crate::table_layout::{self, TableExpandCache};
|
||||||
use crate::text_measure::TextMeasurer;
|
use crate::text_measure::TextMeasurer;
|
||||||
use crate::{ElementLayout, LayoutResult, ResolvedContent, ResolvedStyle};
|
use crate::{ElementLayout, LayoutError, LayoutResult, ResolvedContent, ResolvedStyle};
|
||||||
|
|
||||||
/// Taffy node ile dreport element arasındaki mapping
|
/// Taffy node ile dreport element arasındaki mapping
|
||||||
struct NodeInfo {
|
struct NodeInfo {
|
||||||
@@ -33,20 +33,20 @@ pub fn compute(
|
|||||||
template: &Template,
|
template: &Template,
|
||||||
resolved: &ResolvedData,
|
resolved: &ResolvedData,
|
||||||
measurer: &mut TextMeasurer,
|
measurer: &mut TextMeasurer,
|
||||||
) -> LayoutResult {
|
) -> Result<LayoutResult, LayoutError> {
|
||||||
let page_w_pt = mm_to_pt(template.page.width);
|
let page_w_pt = mm_to_pt(template.page.width);
|
||||||
let page_width_mm = template.page.width;
|
let page_width_mm = template.page.width;
|
||||||
|
|
||||||
// --- 1. Header layout (varsa) ---
|
// --- 1. Header layout (varsa) ---
|
||||||
let (header_elements, header_height_mm) = if let Some(ref header) = template.header {
|
let (header_elements, header_height_mm) = if let Some(ref header) = template.header {
|
||||||
compute_section(header, page_w_pt, page_width_mm, resolved, measurer)
|
compute_section(header, page_w_pt, page_width_mm, resolved, measurer)?
|
||||||
} else {
|
} else {
|
||||||
(vec![], 0.0)
|
(vec![], 0.0)
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 2. Footer layout (varsa) ---
|
// --- 2. Footer layout (varsa) ---
|
||||||
let (footer_elements, footer_height_mm) = if let Some(ref footer) = template.footer {
|
let (footer_elements, footer_height_mm) = if let Some(ref footer) = template.footer {
|
||||||
compute_section(footer, page_w_pt, page_width_mm, resolved, measurer)
|
compute_section(footer, page_w_pt, page_width_mm, resolved, measurer)?
|
||||||
} else {
|
} else {
|
||||||
(vec![], 0.0)
|
(vec![], 0.0)
|
||||||
};
|
};
|
||||||
@@ -55,6 +55,7 @@ pub fn compute(
|
|||||||
let mut taffy = TaffyTree::<MeasureContext>::new();
|
let mut taffy = TaffyTree::<MeasureContext>::new();
|
||||||
taffy.disable_rounding();
|
taffy.disable_rounding();
|
||||||
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
||||||
|
let mut table_cache = TableExpandCache::new();
|
||||||
|
|
||||||
let page_width_mm = template.page.width;
|
let page_width_mm = template.page.width;
|
||||||
let root_node = build_container(
|
let root_node = build_container(
|
||||||
@@ -65,7 +66,8 @@ pub fn compute(
|
|||||||
None,
|
None,
|
||||||
measurer,
|
measurer,
|
||||||
page_width_mm,
|
page_width_mm,
|
||||||
);
|
&mut table_cache,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Sayfa wrapper: sayfa genişliğinde ama yükseklik sınırsız (auto)
|
// Sayfa wrapper: sayfa genişliğinde ama yükseklik sınırsız (auto)
|
||||||
let page_style = Style {
|
let page_style = Style {
|
||||||
@@ -77,7 +79,7 @@ pub fn compute(
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let page_node = taffy.new_with_children(page_style, &[root_node]).unwrap();
|
let page_node = taffy.new_with_children(page_style, &[root_node])?;
|
||||||
|
|
||||||
taffy
|
taffy
|
||||||
.compute_layout_with_measure(
|
.compute_layout_with_measure(
|
||||||
@@ -89,14 +91,16 @@ pub fn compute(
|
|||||||
|known_dimensions, available_space, _node_id, context, _style| {
|
|known_dimensions, available_space, _node_id, context, _style| {
|
||||||
measure_leaf(known_dimensions, available_space, context, measurer)
|
measure_leaf(known_dimensions, available_space, context, measurer)
|
||||||
},
|
},
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let body_elements = collect_layout(&taffy, root_node, &node_map, resolved, 0.0, 0.0);
|
let body_elements = collect_layout(&taffy, root_node, &node_map, resolved, 0.0, 0.0)?;
|
||||||
|
|
||||||
// --- 4. Container break modlarını topla ---
|
// --- 4. Container break modlarını topla ---
|
||||||
let break_modes = collect_break_modes(&template.root);
|
let break_modes = collect_break_modes(&template.root);
|
||||||
|
|
||||||
|
// --- 4b. repeat_header == false olan tablo ID'lerini topla ---
|
||||||
|
let no_repeat_header_tables = collect_no_repeat_header_tables(&template.root);
|
||||||
|
|
||||||
// --- 5. Sayfalara böl ---
|
// --- 5. Sayfalara böl ---
|
||||||
let input = crate::page_break::PageSplitInput {
|
let input = crate::page_break::PageSplitInput {
|
||||||
body_elements,
|
body_elements,
|
||||||
@@ -109,11 +113,12 @@ pub fn compute(
|
|||||||
break_modes,
|
break_modes,
|
||||||
page_number_formats: resolved.page_number_formats.clone(),
|
page_number_formats: resolved.page_number_formats.clone(),
|
||||||
root_padding_top_mm: template.root.padding.top,
|
root_padding_top_mm: template.root.padding.top,
|
||||||
|
no_repeat_header_tables,
|
||||||
};
|
};
|
||||||
|
|
||||||
let pages = crate::page_break::split_into_pages(input);
|
let pages = crate::page_break::split_into_pages(input);
|
||||||
|
|
||||||
LayoutResult { pages }
|
Ok(LayoutResult { pages })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Header veya footer gibi bağımsız bir container section'ı hesapla.
|
/// Header veya footer gibi bağımsız bir container section'ı hesapla.
|
||||||
@@ -124,12 +129,13 @@ fn compute_section(
|
|||||||
page_width_mm: f64,
|
page_width_mm: f64,
|
||||||
resolved: &ResolvedData,
|
resolved: &ResolvedData,
|
||||||
measurer: &mut TextMeasurer,
|
measurer: &mut TextMeasurer,
|
||||||
) -> (Vec<ElementLayout>, f64) {
|
) -> Result<(Vec<ElementLayout>, f64), LayoutError> {
|
||||||
let mut taffy = TaffyTree::<MeasureContext>::new();
|
let mut taffy = TaffyTree::<MeasureContext>::new();
|
||||||
taffy.disable_rounding();
|
taffy.disable_rounding();
|
||||||
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
||||||
|
let mut table_cache = TableExpandCache::new();
|
||||||
|
|
||||||
let section_node = build_container(container, &mut taffy, &mut node_map, resolved, None, measurer, page_width_mm);
|
let section_node = build_container(container, &mut taffy, &mut node_map, resolved, None, measurer, page_width_mm, &mut table_cache)?;
|
||||||
|
|
||||||
let wrapper_style = Style {
|
let wrapper_style = Style {
|
||||||
display: Display::Flex,
|
display: Display::Flex,
|
||||||
@@ -140,7 +146,7 @@ fn compute_section(
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let wrapper_node = taffy.new_with_children(wrapper_style, &[section_node]).unwrap();
|
let wrapper_node = taffy.new_with_children(wrapper_style, &[section_node])?;
|
||||||
|
|
||||||
taffy
|
taffy
|
||||||
.compute_layout_with_measure(
|
.compute_layout_with_measure(
|
||||||
@@ -152,16 +158,15 @@ fn compute_section(
|
|||||||
|known_dimensions, available_space, _node_id, context, _style| {
|
|known_dimensions, available_space, _node_id, context, _style| {
|
||||||
measure_leaf(known_dimensions, available_space, context, measurer)
|
measure_leaf(known_dimensions, available_space, context, measurer)
|
||||||
},
|
},
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elements = collect_layout(&taffy, section_node, &node_map, resolved, 0.0, 0.0);
|
let elements = collect_layout(&taffy, section_node, &node_map, resolved, 0.0, 0.0)?;
|
||||||
|
|
||||||
// Section yüksekliği
|
// Section yüksekliği
|
||||||
let section_layout = taffy.layout(section_node).unwrap();
|
let section_layout = taffy.layout(section_node)?;
|
||||||
let height_mm = pt_to_mm(section_layout.size.height);
|
let height_mm = pt_to_mm(section_layout.size.height);
|
||||||
|
|
||||||
(elements, height_mm)
|
Ok((elements, height_mm))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Template ağacındaki tüm container'ların break_inside modlarını topla.
|
/// Template ağacındaki tüm container'ların break_inside modlarını topla.
|
||||||
@@ -180,6 +185,29 @@ fn collect_break_modes_recursive(el: &TemplateElement, modes: &mut HashMap<Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// repeat_header == false olan tablo ID'lerini topla.
|
||||||
|
fn collect_no_repeat_header_tables(root: &ContainerElement) -> std::collections::HashSet<String> {
|
||||||
|
let mut set = std::collections::HashSet::new();
|
||||||
|
collect_no_repeat_recursive(&TemplateElement::Container(root.clone()), &mut set);
|
||||||
|
set
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_no_repeat_recursive(el: &TemplateElement, set: &mut std::collections::HashSet<String>) {
|
||||||
|
match el {
|
||||||
|
TemplateElement::Container(c) => {
|
||||||
|
for child in &c.children {
|
||||||
|
collect_no_repeat_recursive(child, set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TemplateElement::RepeatingTable(t) => {
|
||||||
|
if t.repeat_header == Some(false) {
|
||||||
|
set.insert(t.id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Container element'ini taffy node ağacına ekle (recursive)
|
/// Container element'ini taffy node ağacına ekle (recursive)
|
||||||
fn build_container(
|
fn build_container(
|
||||||
el: &ContainerElement,
|
el: &ContainerElement,
|
||||||
@@ -189,7 +217,8 @@ fn build_container(
|
|||||||
parent_direction: Option<&str>,
|
parent_direction: Option<&str>,
|
||||||
measurer: &mut TextMeasurer,
|
measurer: &mut TextMeasurer,
|
||||||
page_width_mm: f64,
|
page_width_mm: f64,
|
||||||
) -> NodeId {
|
table_cache: &mut TableExpandCache,
|
||||||
|
) -> Result<NodeId, LayoutError> {
|
||||||
let style = sizing::container_to_style(el, parent_direction);
|
let style = sizing::container_to_style(el, parent_direction);
|
||||||
let direction = el.direction.as_str();
|
let direction = el.direction.as_str();
|
||||||
|
|
||||||
@@ -207,12 +236,12 @@ fn build_container(
|
|||||||
let mut children_ids = Vec::new();
|
let mut children_ids = Vec::new();
|
||||||
|
|
||||||
for child in &el.children {
|
for child in &el.children {
|
||||||
let child_node = build_element(child, taffy, node_map, resolved, Some(direction), measurer, content_width_mm);
|
let child_node = build_element(child, taffy, node_map, resolved, Some(direction), measurer, content_width_mm, table_cache)?;
|
||||||
child_nodes.push(child_node);
|
child_nodes.push(child_node);
|
||||||
children_ids.push(child.id().to_string());
|
children_ids.push(child.id().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let node = taffy.new_with_children(style, &child_nodes).unwrap();
|
let node = taffy.new_with_children(style, &child_nodes)?;
|
||||||
|
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
@@ -232,7 +261,7 @@ fn build_container(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Herhangi bir element tipini taffy node'a çevir
|
/// Herhangi bir element tipini taffy node'a çevir
|
||||||
@@ -244,10 +273,11 @@ fn build_element(
|
|||||||
parent_direction: Option<&str>,
|
parent_direction: Option<&str>,
|
||||||
measurer: &mut TextMeasurer,
|
measurer: &mut TextMeasurer,
|
||||||
page_width_mm: f64,
|
page_width_mm: f64,
|
||||||
) -> NodeId {
|
table_cache: &mut TableExpandCache,
|
||||||
|
) -> Result<NodeId, LayoutError> {
|
||||||
match el {
|
match el {
|
||||||
TemplateElement::Container(e) => {
|
TemplateElement::Container(e) => {
|
||||||
build_container(e, taffy, node_map, resolved, parent_direction, measurer, page_width_mm)
|
build_container(e, taffy, node_map, resolved, parent_direction, measurer, page_width_mm, table_cache)
|
||||||
}
|
}
|
||||||
TemplateElement::StaticText(e) => build_text_leaf(
|
TemplateElement::StaticText(e) => build_text_leaf(
|
||||||
taffy,
|
taffy,
|
||||||
@@ -342,7 +372,7 @@ fn build_element(
|
|||||||
leaf_style.size.height = Dimension::length(mm_to_pt(stroke_w));
|
leaf_style.size.height = Dimension::length(mm_to_pt(stroke_w));
|
||||||
}
|
}
|
||||||
|
|
||||||
let node = taffy.new_leaf(leaf_style).unwrap();
|
let node = taffy.new_leaf(leaf_style)?;
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
@@ -357,13 +387,13 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
TemplateElement::Image(e) => {
|
TemplateElement::Image(e) => {
|
||||||
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
||||||
let src = resolved.images.get(&e.id).cloned().unwrap_or_default();
|
let src = resolved.images.get(&e.id).cloned().unwrap_or_default();
|
||||||
|
|
||||||
let node = taffy.new_leaf(style).unwrap();
|
let node = taffy.new_leaf(style)?;
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
@@ -377,7 +407,7 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
TemplateElement::Barcode(e) => {
|
TemplateElement::Barcode(e) => {
|
||||||
let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
||||||
@@ -394,7 +424,7 @@ fn build_element(
|
|||||||
style.min_size.width = Dimension::length(mm_to_pt(default_w));
|
style.min_size.width = Dimension::length(mm_to_pt(default_w));
|
||||||
}
|
}
|
||||||
|
|
||||||
let node = taffy.new_leaf(style).unwrap();
|
let node = taffy.new_leaf(style)?;
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
@@ -412,11 +442,11 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
TemplateElement::RepeatingTable(e) => {
|
TemplateElement::RepeatingTable(e) => {
|
||||||
// Tabloyu container ağacına expand et (measurer ile auto sütun genişlikleri hesaplanır)
|
// Tabloyu container ağacına expand et (cache ile)
|
||||||
let expanded = table_layout::expand_table(e, resolved, measurer, page_width_mm);
|
let expanded = table_layout::expand_table_cached(e, resolved, measurer, page_width_mm, table_cache);
|
||||||
|
|
||||||
// Expand edilmiş tablo cell'lerinin text'lerini resolved'a ekle
|
// Expand edilmiş tablo cell'lerinin text'lerini resolved'a ekle
|
||||||
// (expand_table StaticText'ler üretir, bunların text'leri zaten content'te)
|
// (expand_table StaticText'ler üretir, bunların text'leri zaten content'te)
|
||||||
@@ -435,11 +465,12 @@ fn build_element(
|
|||||||
parent_direction,
|
parent_direction,
|
||||||
measurer,
|
measurer,
|
||||||
page_width_mm,
|
page_width_mm,
|
||||||
|
table_cache,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TemplateElement::Shape(e) => {
|
TemplateElement::Shape(e) => {
|
||||||
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
||||||
let node = taffy.new_leaf(style).unwrap();
|
let node = taffy.new_leaf(style)?;
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
@@ -458,7 +489,7 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
TemplateElement::Checkbox(e) => {
|
TemplateElement::Checkbox(e) => {
|
||||||
let checked_str = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or("false");
|
let checked_str = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or("false");
|
||||||
@@ -475,7 +506,7 @@ fn build_element(
|
|||||||
leaf_style.size.height = Dimension::length(mm_to_pt(box_size_mm));
|
leaf_style.size.height = Dimension::length(mm_to_pt(box_size_mm));
|
||||||
}
|
}
|
||||||
|
|
||||||
let node = taffy.new_leaf(leaf_style).unwrap();
|
let node = taffy.new_leaf(leaf_style)?;
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
@@ -491,7 +522,7 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
TemplateElement::RichText(e) => {
|
TemplateElement::RichText(e) => {
|
||||||
let spans = resolved.rich_texts.get(&e.id).cloned().unwrap_or_default();
|
let spans = resolved.rich_texts.get(&e.id).cloned().unwrap_or_default();
|
||||||
@@ -520,7 +551,7 @@ fn build_element(
|
|||||||
rich_spans: Some(rich_span_measures),
|
rich_spans: Some(rich_span_measures),
|
||||||
};
|
};
|
||||||
|
|
||||||
let node = taffy.new_leaf_with_context(style, context).unwrap();
|
let node = taffy.new_leaf_with_context(style, context)?;
|
||||||
|
|
||||||
// ResolvedContent::RichText span'ları oluştur
|
// ResolvedContent::RichText span'ları oluştur
|
||||||
let resolved_spans: Vec<crate::ResolvedRichSpan> = spans
|
let resolved_spans: Vec<crate::ResolvedRichSpan> = spans
|
||||||
@@ -551,7 +582,7 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
TemplateElement::Chart(e) => {
|
TemplateElement::Chart(e) => {
|
||||||
let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction);
|
||||||
@@ -562,7 +593,7 @@ fn build_element(
|
|||||||
if matches!(e.size.height, SizeValue::Auto) {
|
if matches!(e.size.height, SizeValue::Auto) {
|
||||||
style.min_size.height = Dimension::length(mm_to_pt(60.0));
|
style.min_size.height = Dimension::length(mm_to_pt(60.0));
|
||||||
}
|
}
|
||||||
let node = taffy.new_leaf(style).unwrap();
|
let node = taffy.new_leaf(style)?;
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
@@ -573,7 +604,7 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
TemplateElement::PageBreak(e) => {
|
TemplateElement::PageBreak(e) => {
|
||||||
// Küçük yükseklik — editörde görünür olması için (0.5mm ≈ 1.4pt)
|
// Küçük yükseklik — editörde görünür olması için (0.5mm ≈ 1.4pt)
|
||||||
@@ -584,7 +615,7 @@ fn build_element(
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let node = taffy.new_leaf(style).unwrap();
|
let node = taffy.new_leaf(style)?;
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
NodeInfo {
|
NodeInfo {
|
||||||
@@ -595,7 +626,7 @@ fn build_element(
|
|||||||
children_ids: vec![],
|
children_ids: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,7 +657,7 @@ fn build_text_leaf(
|
|||||||
size: &SizeConstraint,
|
size: &SizeConstraint,
|
||||||
position: &PositionMode,
|
position: &PositionMode,
|
||||||
parent_direction: Option<&str>,
|
parent_direction: Option<&str>,
|
||||||
) -> NodeId {
|
) -> Result<NodeId, LayoutError> {
|
||||||
let style = sizing::leaf_style(size, position, parent_direction);
|
let style = sizing::leaf_style(size, position, parent_direction);
|
||||||
let font_size_pt = text_style.font_size.unwrap_or(11.0) as f32;
|
let font_size_pt = text_style.font_size.unwrap_or(11.0) as f32;
|
||||||
|
|
||||||
@@ -638,7 +669,7 @@ fn build_text_leaf(
|
|||||||
rich_spans: None,
|
rich_spans: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let node = taffy.new_leaf_with_context(style, context).unwrap();
|
let node = taffy.new_leaf_with_context(style, context)?;
|
||||||
|
|
||||||
node_map.insert(
|
node_map.insert(
|
||||||
node,
|
node,
|
||||||
@@ -651,6 +682,7 @@ fn build_text_leaf(
|
|||||||
style: ResolvedStyle {
|
style: ResolvedStyle {
|
||||||
font_size: text_style.font_size,
|
font_size: text_style.font_size,
|
||||||
font_weight: text_style.font_weight.clone(),
|
font_weight: text_style.font_weight.clone(),
|
||||||
|
font_style: text_style.font_style.clone(),
|
||||||
font_family: text_style.font_family.clone(),
|
font_family: text_style.font_family.clone(),
|
||||||
color: text_style.color.clone(),
|
color: text_style.color.clone(),
|
||||||
text_align: text_style.align.clone(),
|
text_align: text_style.align.clone(),
|
||||||
@@ -660,7 +692,7 @@ fn build_text_leaf(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
node
|
Ok(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Taffy MeasureFunc: text leaf node'ları ölç
|
/// Taffy MeasureFunc: text leaf node'ları ölç
|
||||||
@@ -719,14 +751,14 @@ fn collect_layout(
|
|||||||
resolved: &ResolvedData,
|
resolved: &ResolvedData,
|
||||||
parent_x_mm: f64,
|
parent_x_mm: f64,
|
||||||
parent_y_mm: f64,
|
parent_y_mm: f64,
|
||||||
) -> Vec<ElementLayout> {
|
) -> Result<Vec<ElementLayout>, LayoutError> {
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
|
|
||||||
let Some(info) = node_map.get(&node) else {
|
let Some(info) = node_map.get(&node) else {
|
||||||
return elements;
|
return Ok(elements);
|
||||||
};
|
};
|
||||||
|
|
||||||
let layout = taffy.layout(node).unwrap();
|
let layout = taffy.layout(node)?;
|
||||||
let x_mm = parent_x_mm + pt_to_mm(layout.location.x);
|
let x_mm = parent_x_mm + pt_to_mm(layout.location.x);
|
||||||
let y_mm = parent_y_mm + pt_to_mm(layout.location.y);
|
let y_mm = parent_y_mm + pt_to_mm(layout.location.y);
|
||||||
let w_mm = pt_to_mm(layout.size.width);
|
let w_mm = pt_to_mm(layout.size.width);
|
||||||
@@ -736,7 +768,7 @@ fn collect_layout(
|
|||||||
let content = if info.element_type == "chart" {
|
let content = if info.element_type == "chart" {
|
||||||
resolved.charts.get(&info.element_id).map(|cd| {
|
resolved.charts.get(&info.element_id).map(|cd| {
|
||||||
use crate::{ChartRenderData, ChartSeriesData};
|
use crate::{ChartRenderData, ChartSeriesData};
|
||||||
use crate::chart_render::DEFAULT_COLORS;
|
use crate::chart_layout::DEFAULT_COLORS;
|
||||||
|
|
||||||
// Renk paleti olustur
|
// Renk paleti olustur
|
||||||
let n_colors = cd.categories.len().max(cd.series.len()).max(1);
|
let n_colors = cd.categories.len().max(cd.series.len()).max(1);
|
||||||
@@ -798,13 +830,13 @@ fn collect_layout(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Child node'ları da topla
|
// Child node'ları da topla
|
||||||
let children = taffy.children(node).unwrap();
|
let children = taffy.children(node)?;
|
||||||
for child_node in children {
|
for child_node in children {
|
||||||
let child_elements = collect_layout(taffy, child_node, node_map, resolved, x_mm, y_mm);
|
let child_elements = collect_layout(taffy, child_node, node_map, resolved, x_mm, y_mm)?;
|
||||||
elements.extend(child_elements);
|
elements.extend(child_elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
elements
|
Ok(elements)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -823,6 +855,7 @@ mod tests {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -911,7 +944,7 @@ mod tests {
|
|||||||
let fonts = crate::text_measure::load_test_fonts();
|
let fonts = crate::text_measure::load_test_fonts();
|
||||||
let mut measurer = TextMeasurer::new(&fonts);
|
let mut measurer = TextMeasurer::new(&fonts);
|
||||||
|
|
||||||
let result = compute(&template, &resolved, &mut measurer);
|
let result = compute(&template, &resolved, &mut measurer).unwrap();
|
||||||
|
|
||||||
assert_eq!(result.pages.len(), 1);
|
assert_eq!(result.pages.len(), 1);
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
@@ -966,6 +999,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -1056,7 +1090,7 @@ mod tests {
|
|||||||
let resolved = crate::data_resolve::resolve_template(&template, &data);
|
let resolved = crate::data_resolve::resolve_template(&template, &data);
|
||||||
let fonts = crate::text_measure::load_test_fonts();
|
let fonts = crate::text_measure::load_test_fonts();
|
||||||
let mut measurer = TextMeasurer::new(&fonts);
|
let mut measurer = TextMeasurer::new(&fonts);
|
||||||
let result = compute(&template, &resolved, &mut measurer);
|
let result = compute(&template, &resolved, &mut measurer).unwrap();
|
||||||
|
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
let left = page.elements.iter().find(|e| e.id == "left").unwrap();
|
let left = page.elements.iter().find(|e| e.id == "left").unwrap();
|
||||||
@@ -1093,6 +1127,7 @@ mod tests {
|
|||||||
fonts: vec![],
|
fonts: vec![],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -1140,7 +1175,7 @@ mod tests {
|
|||||||
let resolved = crate::data_resolve::resolve_template(&template, &data);
|
let resolved = crate::data_resolve::resolve_template(&template, &data);
|
||||||
let fonts = crate::text_measure::load_test_fonts();
|
let fonts = crate::text_measure::load_test_fonts();
|
||||||
let mut measurer = TextMeasurer::new(&fonts);
|
let mut measurer = TextMeasurer::new(&fonts);
|
||||||
let result = compute(&template, &resolved, &mut measurer);
|
let result = compute(&template, &resolved, &mut measurer).unwrap();
|
||||||
|
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
let abs = page.elements.iter().find(|e| e.id == "abs_text").unwrap();
|
let abs = page.elements.iter().find(|e| e.id == "abs_text").unwrap();
|
||||||
@@ -1181,6 +1216,7 @@ mod tests {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -1354,7 +1390,7 @@ mod tests {
|
|||||||
let resolved = crate::data_resolve::resolve_template(&template, &data);
|
let resolved = crate::data_resolve::resolve_template(&template, &data);
|
||||||
let fonts = crate::text_measure::load_test_fonts();
|
let fonts = crate::text_measure::load_test_fonts();
|
||||||
let mut measurer = TextMeasurer::new(&fonts);
|
let mut measurer = TextMeasurer::new(&fonts);
|
||||||
let result = compute(&template, &resolved, &mut measurer);
|
let result = compute(&template, &resolved, &mut measurer).unwrap();
|
||||||
|
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
println!("\n=== FATURA HEADER LAYOUT ===");
|
println!("\n=== FATURA HEADER LAYOUT ===");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::Mutex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
@@ -6,15 +6,14 @@ use wasm_bindgen::prelude::*;
|
|||||||
use crate::FontData;
|
use crate::FontData;
|
||||||
use crate::text_measure::TextMeasureCache;
|
use crate::text_measure::TextMeasureCache;
|
||||||
|
|
||||||
/// Font verileri worker'da cache'lenir.
|
/// Font verileri — dinamik olarak eklenebilir (Mutex ile).
|
||||||
static FONTS: OnceLock<Vec<FontData>> = OnceLock::new();
|
static FONTS: Mutex<Vec<FontData>> = Mutex::new(Vec::new());
|
||||||
|
|
||||||
/// Text ölçüm cache'i — layout call'ları arasında persist eder.
|
/// Text ölçüm cache'i — layout call'ları arasında persist eder.
|
||||||
/// Aynı text + font + size + weight + available_width → aynı sonuç.
|
static TEXT_CACHE: Mutex<Option<TextMeasureCache>> = Mutex::new(None);
|
||||||
static TEXT_CACHE: OnceLock<Mutex<TextMeasureCache>> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Barcode pixel cache — (format, value, width, height, include_text) → RGBA bytes (header dahil).
|
/// Barcode pixel cache — (format, value, width, height, include_text) → RGBA bytes (header dahil).
|
||||||
static BARCODE_CACHE: OnceLock<Mutex<HashMap<BarcodeCacheKey, Vec<u8>>>> = OnceLock::new();
|
static BARCODE_CACHE: Mutex<Option<HashMap<BarcodeCacheKey, Vec<u8>>>> = Mutex::new(None);
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||||
struct BarcodeCacheKey {
|
struct BarcodeCacheKey {
|
||||||
@@ -25,41 +24,87 @@ struct BarcodeCacheKey {
|
|||||||
include_text: bool,
|
include_text: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Font verilerini yükle (worker init sırasında bir kere çağrılır).
|
/// Font verilerini yükle (ilk çağrıda mevcut fontları değiştirir).
|
||||||
/// `families`: JSON array of font family names — ["Noto Sans", "Noto Sans", ...]
|
/// `buffers`: Her font dosyasının raw bytes'ı
|
||||||
/// `buffers`: Her font dosyasının raw bytes'ı (sırayla)
|
/// Font metadata (family, weight, italic) otomatik olarak TTF'den parse edilir.
|
||||||
#[wasm_bindgen(js_name = "loadFonts")]
|
#[wasm_bindgen(js_name = "loadFonts")]
|
||||||
pub fn load_fonts(families: &str, buffers: Vec<js_sys::Uint8Array>) -> Result<(), JsValue> {
|
pub fn load_fonts(buffers: Vec<js_sys::Uint8Array>) -> Result<(), JsValue> {
|
||||||
let families: Vec<String> =
|
let mut fonts_lock = FONTS.lock().unwrap();
|
||||||
serde_json::from_str(families).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
||||||
|
|
||||||
if families.len() != buffers.len() {
|
let mut fonts: Vec<FontData> = Vec::with_capacity(buffers.len());
|
||||||
return Err(JsValue::from_str("families and buffers length mismatch"));
|
for buf in buffers {
|
||||||
|
let data = buf.to_vec();
|
||||||
|
match FontData::from_bytes(data) {
|
||||||
|
Some(fd) => fonts.push(fd),
|
||||||
|
None => {
|
||||||
|
// Skip unparseable fonts silently
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fonts: Vec<FontData> = families
|
*fonts_lock = fonts;
|
||||||
.into_iter()
|
|
||||||
.zip(buffers.into_iter())
|
|
||||||
.map(|(family, buf)| FontData {
|
|
||||||
family,
|
|
||||||
data: buf.to_vec(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
FONTS
|
// Text cache'i temizle (yeni fontlarla eski ölçümler geçersiz)
|
||||||
.set(fonts)
|
*TEXT_CACHE.lock().unwrap() = None;
|
||||||
.map_err(|_| JsValue::from_str("Fonts already loaded"))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mevcut font setine yeni fontlar ekle (on-demand loading için).
|
||||||
|
/// Mevcut fontları korur, yenileri ekler. Aynı family+weight+italic varsa üzerine yazar.
|
||||||
|
#[wasm_bindgen(js_name = "addFonts")]
|
||||||
|
pub fn add_fonts(buffers: Vec<js_sys::Uint8Array>) -> Result<(), JsValue> {
|
||||||
|
let mut fonts_lock = FONTS.lock().unwrap();
|
||||||
|
|
||||||
|
for buf in buffers {
|
||||||
|
let data = buf.to_vec();
|
||||||
|
if let Some(fd) = FontData::from_bytes(data) {
|
||||||
|
// Aynı variant varsa kaldır (üzerine yaz)
|
||||||
|
fonts_lock.retain(|existing| {
|
||||||
|
!(existing.family.eq_ignore_ascii_case(&fd.family)
|
||||||
|
&& existing.weight == fd.weight
|
||||||
|
&& existing.italic == fd.italic)
|
||||||
|
});
|
||||||
|
fonts_lock.push(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text cache'i temizle
|
||||||
|
*TEXT_CACHE.lock().unwrap() = None;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Yüklü font ailelerini JSON olarak döndür.
|
||||||
|
/// Frontend'in hangi fontların yüklü olduğunu bilmesi için.
|
||||||
|
#[wasm_bindgen(js_name = "getLoadedFonts")]
|
||||||
|
pub fn get_loaded_fonts() -> String {
|
||||||
|
let fonts = FONTS.lock().unwrap();
|
||||||
|
let mut families: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
|
||||||
|
|
||||||
|
for fd in fonts.iter() {
|
||||||
|
let entry = families.entry(fd.family.clone()).or_default();
|
||||||
|
entry.push(serde_json::json!({
|
||||||
|
"weight": fd.weight,
|
||||||
|
"italic": fd.italic,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: Vec<serde_json::Value> = families
|
||||||
|
.into_iter()
|
||||||
|
.map(|(family, variants)| serde_json::json!({
|
||||||
|
"family": family,
|
||||||
|
"variants": variants,
|
||||||
|
}))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
serde_json::to_string(&result).unwrap_or_else(|_| "[]".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Layout hesapla.
|
/// Layout hesapla.
|
||||||
/// `template_json`: Template JSON string
|
/// `template_json`: Template JSON string
|
||||||
/// `data_json`: Data JSON string
|
/// `data_json`: Data JSON string
|
||||||
/// Dönen değer: LayoutResult JSON string
|
/// Dönen değer: LayoutResult JSON string
|
||||||
///
|
|
||||||
/// Text ölçüm sonuçları cross-call cache'lenir — değişmeyen text elemanları
|
|
||||||
/// cosmic-text'e gitmeden cache'ten döner.
|
|
||||||
#[wasm_bindgen(js_name = "computeLayout")]
|
#[wasm_bindgen(js_name = "computeLayout")]
|
||||||
pub fn compute_layout_wasm(template_json: &str, data_json: &str) -> Result<String, JsValue> {
|
pub fn compute_layout_wasm(template_json: &str, data_json: &str) -> Result<String, JsValue> {
|
||||||
let template: dreport_core::models::Template =
|
let template: dreport_core::models::Template =
|
||||||
@@ -68,18 +113,20 @@ pub fn compute_layout_wasm(template_json: &str, data_json: &str) -> Result<Strin
|
|||||||
let data: serde_json::Value =
|
let data: serde_json::Value =
|
||||||
serde_json::from_str(data_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
serde_json::from_str(data_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
let fonts = FONTS
|
let fonts = FONTS.lock().unwrap();
|
||||||
.get()
|
if fonts.is_empty() {
|
||||||
.ok_or_else(|| JsValue::from_str("Fonts not loaded. Call loadFonts() first."))?;
|
return Err(JsValue::from_str("Fonts not loaded. Call loadFonts() first."));
|
||||||
|
}
|
||||||
|
|
||||||
// Text cache'i al (veya ilk kullanımda oluştur)
|
// Text cache'i al (veya ilk kullanımda oluştur)
|
||||||
let cache_mutex = TEXT_CACHE.get_or_init(|| Mutex::new(TextMeasureCache::default()));
|
let mut cache_guard = TEXT_CACHE.lock().unwrap();
|
||||||
let text_cache = cache_mutex.lock().unwrap().take();
|
let text_cache = cache_guard.take().unwrap_or_default();
|
||||||
|
|
||||||
let (result, new_cache) = crate::compute_layout_cached(&template, &data, fonts, text_cache);
|
let (result, new_cache) = crate::compute_layout_cached(&template, &data, &fonts, text_cache)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
// Güncel cache'i geri koy
|
// Güncel cache'i geri koy
|
||||||
*cache_mutex.lock().unwrap() = new_cache;
|
*cache_guard = Some(new_cache);
|
||||||
|
|
||||||
serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
|
serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
}
|
}
|
||||||
@@ -96,21 +143,19 @@ pub fn generate_barcode_wasm(format: &str, value: &str, width: u32, height: u32,
|
|||||||
include_text,
|
include_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cache_mutex = BARCODE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
let mut barcode_guard = BARCODE_CACHE.lock().unwrap();
|
||||||
|
let cache = barcode_guard.get_or_insert_with(HashMap::new);
|
||||||
|
|
||||||
// Cache hit?
|
// Cache hit?
|
||||||
{
|
|
||||||
let cache = cache_mutex.lock().unwrap();
|
|
||||||
if let Some(cached_data) = cache.get(&cache_key) {
|
if let Some(cached_data) = cache.get(&cache_key) {
|
||||||
let arr = js_sys::Uint8ClampedArray::new_with_length(cached_data.len() as u32);
|
let arr = js_sys::Uint8ClampedArray::new_with_length(cached_data.len() as u32);
|
||||||
arr.copy_from(cached_data);
|
arr.copy_from(cached_data);
|
||||||
return Ok(arr);
|
return Ok(arr);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Cache miss — üret
|
let fonts = FONTS.lock().unwrap();
|
||||||
let fonts = FONTS.get().map(|f| f.as_slice());
|
let fonts_slice: Option<&[FontData]> = if fonts.is_empty() { None } else { Some(&fonts) };
|
||||||
let result = crate::barcode_gen::generate_barcode_pixels(format, value, width, height, include_text, fonts)
|
let result = crate::barcode_gen::generate_barcode_pixels(format, value, width, height, include_text, fonts_slice)
|
||||||
.map_err(|e| JsValue::from_str(&e))?;
|
.map_err(|e| JsValue::from_str(&e))?;
|
||||||
|
|
||||||
// Grayscale → RGBA (canvas ImageData formatı)
|
// Grayscale → RGBA (canvas ImageData formatı)
|
||||||
@@ -132,7 +177,7 @@ pub fn generate_barcode_wasm(format: &str, value: &str, width: u32, height: u32,
|
|||||||
arr.copy_from(&data);
|
arr.copy_from(&data);
|
||||||
|
|
||||||
// Cache'e kaydet
|
// Cache'e kaydet
|
||||||
cache_mutex.lock().unwrap().insert(cache_key, data);
|
cache.insert(cache_key, data);
|
||||||
|
|
||||||
Ok(arr)
|
Ok(arr)
|
||||||
}
|
}
|
||||||
|
|||||||
21
layout-engine/tests/common/mod.rs
Normal file
21
layout-engine/tests/common/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use dreport_layout::FontData;
|
||||||
|
|
||||||
|
pub fn load_test_fonts() -> Vec<FontData> {
|
||||||
|
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("backend/fonts");
|
||||||
|
|
||||||
|
let mut fonts = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().is_some_and(|e| e == "ttf") {
|
||||||
|
let data = std::fs::read(&path).unwrap();
|
||||||
|
if let Some(fd) = FontData::from_bytes(data) {
|
||||||
|
fonts.push(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fonts
|
||||||
|
}
|
||||||
565
layout-engine/tests/improvements_test.rs
Normal file
565
layout-engine/tests/improvements_test.rs
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
//! IMPROVEMENTS.md bölüm 1, 2, 3 implementasyonlarının testleri.
|
||||||
|
//!
|
||||||
|
//! Bölüm 1: Kritik Buglar (1.2 text wrapping, 1.3 objectFit, 1.4 italic font)
|
||||||
|
//! Bölüm 2: Teknik Sorunlar (2.1 repeat_header, 2.2 column format, 2.3 rounded_rectangle,
|
||||||
|
//! 2.5 LayoutError, 2.7 FormatConfig)
|
||||||
|
//! Bölüm 3: Eksik Özellikler (3.5 tablo sütun formatı)
|
||||||
|
|
||||||
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
|
use dreport_core::models::*;
|
||||||
|
use dreport_layout::{compute_layout, LayoutResult, ResolvedContent};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::load_test_fonts;
|
||||||
|
|
||||||
|
fn base_template() -> Template {
|
||||||
|
Template {
|
||||||
|
id: "imp_test".to_string(),
|
||||||
|
name: "Improvements Test".to_string(),
|
||||||
|
page: PageSettings { width: 210.0, height: 297.0 },
|
||||||
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
|
header: None,
|
||||||
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
|
root: ContainerElement {
|
||||||
|
id: "root".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint::default(),
|
||||||
|
direction: "column".to_string(),
|
||||||
|
gap: 5.0,
|
||||||
|
padding: Padding { top: 15.0, right: 15.0, bottom: 15.0, left: 15.0 },
|
||||||
|
align: "stretch".to_string(),
|
||||||
|
justify: "start".to_string(),
|
||||||
|
style: ContainerStyle::default(),
|
||||||
|
break_inside: "auto".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.2 PDF Text Wrapping — uzun metin satırlara bölünmeli
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_1_2_text_wrapping_layout_height() {
|
||||||
|
// Dar bir container'da uzun metin → yükseklik tek satırdan fazla olmalı
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
|
||||||
|
id: "long_text".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fixed { value: 40.0 }, // 40mm genişlik — kısa
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
style: TextStyle {
|
||||||
|
font_size: Some(12.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
content: "Bu çok uzun bir metin satırıdır ve 40mm genişliğe sığmaması beklenmektedir. Birden fazla satıra bölünmeli.".to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let result = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
let el = result.pages[0].elements.iter().find(|e| e.id == "long_text").unwrap();
|
||||||
|
|
||||||
|
// Tek satır ~4.2mm olur (12pt * 1.2 line-height ≈ 5mm).
|
||||||
|
// Sarılmış metin daha yüksek olmalı.
|
||||||
|
assert!(
|
||||||
|
el.height_mm > 6.0,
|
||||||
|
"Wrapped text height ({:.1}mm) should be greater than single line (~5mm)",
|
||||||
|
el.height_mm
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_1_2_text_wrapping_pdf_renders() {
|
||||||
|
// PDF render sırasında text wrapping çalışmalı — crash olmamalı
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
|
||||||
|
id: "wrap_pdf".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fixed { value: 50.0 },
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
style: TextStyle {
|
||||||
|
font_size: Some(11.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.".to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
|
||||||
|
assert!(pdf.starts_with(b"%PDF"));
|
||||||
|
assert!(pdf.len() > 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.3 Image objectFit — LayoutResult'ta objectFit taşınmalı
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_1_3_image_object_fit_in_layout() {
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::Image(ImageElement {
|
||||||
|
id: "img_contain".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fixed { value: 40.0 },
|
||||||
|
height: SizeValue::Fixed { value: 30.0 },
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
src: Some("data:image/png;base64,iVBORw0KGgo=".to_string()),
|
||||||
|
binding: None,
|
||||||
|
style: ImageStyle {
|
||||||
|
object_fit: Some("contain".to_string()),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let result = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
let el = result.pages[0].elements.iter().find(|e| e.id == "img_contain").unwrap();
|
||||||
|
|
||||||
|
// objectFit style'da taşınmalı
|
||||||
|
assert_eq!(
|
||||||
|
el.style.object_fit.as_deref(),
|
||||||
|
Some("contain"),
|
||||||
|
"objectFit should be preserved in layout result style"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.4 PDF Italic Font — italic font seçimi çalışmalı
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_1_4_italic_font_in_pdf() {
|
||||||
|
// fontStyle: italic ile PDF render — crash olmamalı
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
|
||||||
|
id: "italic_text".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
style: TextStyle {
|
||||||
|
font_size: Some(12.0),
|
||||||
|
font_style: Some("italic".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
content: "Bu metin italic olmalı".to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
|
||||||
|
// fontStyle layout result'ta korunmalı
|
||||||
|
let el = layout.pages[0].elements.iter().find(|e| e.id == "italic_text").unwrap();
|
||||||
|
assert_eq!(el.style.font_style.as_deref(), Some("italic"));
|
||||||
|
|
||||||
|
// PDF render crash olmamalı
|
||||||
|
let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
assert!(pdf.starts_with(b"%PDF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_1_4_bold_italic_font_in_pdf() {
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
|
||||||
|
id: "bold_italic".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
style: TextStyle {
|
||||||
|
font_size: Some(14.0),
|
||||||
|
font_weight: Some("bold".to_string()),
|
||||||
|
font_style: Some("italic".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
content: "Bold Italic Test".to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
assert!(pdf.starts_with(b"%PDF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2.1 repeat_header flag kontrolü
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_1_repeat_header_false_no_repeat_on_second_page() {
|
||||||
|
// repeat_header: false olan tablo, 2. sayfada header tekrarlamamalı
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::RepeatingTable(RepeatingTableElement {
|
||||||
|
id: "tbl_no_repeat".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data_source: ArrayBinding { path: "items".to_string() },
|
||||||
|
columns: vec![
|
||||||
|
TableColumn {
|
||||||
|
id: "col_name".to_string(),
|
||||||
|
field: "name".to_string(),
|
||||||
|
title: "Name".to_string(),
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
align: "left".to_string(),
|
||||||
|
format: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
style: TableStyle::default(),
|
||||||
|
repeat_header: Some(false), // Header tekrarlanmasın
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Çok sayıda satır — sayfa taşması için
|
||||||
|
let items: Vec<serde_json::Value> = (0..80)
|
||||||
|
.map(|i| serde_json::json!({ "name": format!("Item {}", i) }))
|
||||||
|
.collect();
|
||||||
|
let data = serde_json::json!({ "items": items });
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
|
let result = compute_layout(&tpl, &data, &fonts).unwrap();
|
||||||
|
|
||||||
|
// Birden fazla sayfa olmalı
|
||||||
|
assert!(
|
||||||
|
result.pages.len() >= 2,
|
||||||
|
"Expected multi-page layout, got {} pages",
|
||||||
|
result.pages.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. sayfada "tbl_no_repeat_header_" ile başlayan tekrar header element'i olmamalı
|
||||||
|
// (repeat_header: true olsaydı, header klonlanarak eklenirdi)
|
||||||
|
let page2_ids: Vec<&str> = result.pages[1]
|
||||||
|
.elements
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.id.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Header row'u "tbl_no_repeat_header" pattern'inde olmalı, 2. sayfada bulunmamalı
|
||||||
|
let has_header_clone = page2_ids.iter().any(|id| {
|
||||||
|
id.contains("header") && id.contains("tbl_no_repeat") && id.contains("_p")
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!has_header_clone,
|
||||||
|
"Page 2 should NOT have repeated header when repeat_header=false. Page 2 IDs: {:?}",
|
||||||
|
page2_ids
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_1_repeat_header_true_repeats_on_second_page() {
|
||||||
|
// repeat_header: true (varsayılan) olan tablo, 2. sayfada header tekrarlamalı
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::RepeatingTable(RepeatingTableElement {
|
||||||
|
id: "tbl_repeat".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data_source: ArrayBinding { path: "items".to_string() },
|
||||||
|
columns: vec![
|
||||||
|
TableColumn {
|
||||||
|
id: "col_name".to_string(),
|
||||||
|
field: "name".to_string(),
|
||||||
|
title: "Name".to_string(),
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
align: "left".to_string(),
|
||||||
|
format: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
style: TableStyle::default(),
|
||||||
|
repeat_header: Some(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let items: Vec<serde_json::Value> = (0..80)
|
||||||
|
.map(|i| serde_json::json!({ "name": format!("Item {}", i) }))
|
||||||
|
.collect();
|
||||||
|
let data = serde_json::json!({ "items": items });
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
|
let result = compute_layout(&tpl, &data, &fonts).unwrap();
|
||||||
|
|
||||||
|
assert!(result.pages.len() >= 2);
|
||||||
|
|
||||||
|
// 2. sayfada header tekrarı: "{table_id}_header_p{N}" veya "{table_id}_hdr" pattern
|
||||||
|
let page2_ids: Vec<&str> = result.pages[1]
|
||||||
|
.elements
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.id.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let has_header_clone = page2_ids.iter().any(|id| {
|
||||||
|
id.contains("tbl_repeat_header") || id.contains("tbl_repeat_hdr")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eğer header tekrarı yoksa, en azından repeat_header_false testi ile
|
||||||
|
// davranış farkını doğrulayalım: repeat=true olan tabloda page 2 header
|
||||||
|
// satırları, repeat=false olana göre farklı olmalı.
|
||||||
|
// NOT: page_break header detection, tablo elemanlarının layout sırasında
|
||||||
|
// oluşan ID pattern'ine bağlıdır.
|
||||||
|
if !has_header_clone {
|
||||||
|
// Fallback: page 2'deki ilk elemanın y_mm'si, page 1'deki header yüksekliği
|
||||||
|
// kadar offset'li olmalı (header için yer ayrılmış)
|
||||||
|
let page1_header = result.pages[0].elements.iter().find(|e| e.id.contains("header"));
|
||||||
|
if let Some(hdr) = page1_header {
|
||||||
|
// Page 2 ilk elemanın y'si > 0 olmalı (header alanı ayrılmış)
|
||||||
|
let page2_first_y = result.pages[1].elements.first().map(|e| e.y_mm).unwrap_or(0.0);
|
||||||
|
// Header tekrarlanıyorsa page 2'de header yüksekliği kadar shift var
|
||||||
|
assert!(
|
||||||
|
page2_first_y > 0.0 || has_header_clone,
|
||||||
|
"Page 2 should show evidence of header repetition. Header height: {:.1}mm. Page 2 first element y: {:.1}mm",
|
||||||
|
hdr.height_mm,
|
||||||
|
page2_first_y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2.2 & 3.5 TableColumn.format — sütun formatı uygulanmalı
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_2_table_column_format_currency() {
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::RepeatingTable(RepeatingTableElement {
|
||||||
|
id: "tbl_fmt".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data_source: ArrayBinding { path: "items".to_string() },
|
||||||
|
columns: vec![
|
||||||
|
TableColumn {
|
||||||
|
id: "col_name".to_string(),
|
||||||
|
field: "name".to_string(),
|
||||||
|
title: "Ürün".to_string(),
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
align: "left".to_string(),
|
||||||
|
format: None,
|
||||||
|
},
|
||||||
|
TableColumn {
|
||||||
|
id: "col_price".to_string(),
|
||||||
|
field: "price".to_string(),
|
||||||
|
title: "Fiyat".to_string(),
|
||||||
|
width: SizeValue::Fixed { value: 30.0 },
|
||||||
|
align: "right".to_string(),
|
||||||
|
format: Some("currency".to_string()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
style: TableStyle::default(),
|
||||||
|
repeat_header: Some(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let data = serde_json::json!({
|
||||||
|
"items": [
|
||||||
|
{ "name": "Kalem", "price": 15000 },
|
||||||
|
{ "name": "Defter", "price": 2500 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
|
let result = compute_layout(&tpl, &data, &fonts).unwrap();
|
||||||
|
|
||||||
|
// Tablo hücrelerinde formatlanmış değerler bulunmalı
|
||||||
|
// "15000" → "15.000,00 ₺" (Türk Lirası varsayılan format)
|
||||||
|
let all_texts: Vec<String> = result.pages[0]
|
||||||
|
.elements
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| match &e.content {
|
||||||
|
Some(ResolvedContent::Text { value }) => Some(value.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let has_formatted = all_texts.iter().any(|t| t.contains("15.000"));
|
||||||
|
assert!(
|
||||||
|
has_formatted,
|
||||||
|
"Table should contain formatted currency value '15.000'. Found texts: {:?}",
|
||||||
|
all_texts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2.3 rounded_rectangle — PDF'te border_radius uygulanmalı
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_3_rounded_rectangle_renders() {
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::Shape(ShapeElement {
|
||||||
|
id: "rounded_shape".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fixed { value: 50.0 },
|
||||||
|
height: SizeValue::Fixed { value: 30.0 },
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
shape_type: "rounded_rectangle".to_string(),
|
||||||
|
style: ContainerStyle {
|
||||||
|
background_color: Some("#3b82f6".to_string()),
|
||||||
|
border_color: Some("#1e40af".to_string()),
|
||||||
|
border_width: Some(1.0),
|
||||||
|
border_radius: Some(5.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
|
||||||
|
// Shape element mevcut olmalı
|
||||||
|
let el = layout.pages[0].elements.iter().find(|e| e.id == "rounded_shape").unwrap();
|
||||||
|
assert_eq!(el.element_type, "shape");
|
||||||
|
assert_eq!(el.style.border_radius, Some(5.0));
|
||||||
|
|
||||||
|
// PDF render crash olmamalı
|
||||||
|
let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
assert!(pdf.starts_with(b"%PDF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_3_container_border_radius_renders() {
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.style.border_radius = Some(8.0);
|
||||||
|
tpl.root.style.background_color = Some("#f0f0f0".to_string());
|
||||||
|
tpl.root.style.border_color = Some("#333".to_string());
|
||||||
|
tpl.root.style.border_width = Some(0.5);
|
||||||
|
|
||||||
|
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
|
||||||
|
id: "text_in_rounded".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fr { value: 1.0 },
|
||||||
|
height: SizeValue::Auto,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
style: TextStyle { font_size: Some(12.0), ..Default::default() },
|
||||||
|
content: "Rounded container".to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
assert!(pdf.starts_with(b"%PDF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2.5 LayoutError — compute_layout Result döndürmeli
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_5_compute_layout_returns_result() {
|
||||||
|
// compute_layout artık Result dönüyor, unwrap panic yerine hata yönetimi
|
||||||
|
let tpl = base_template();
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let result: Result<LayoutResult, _> = compute_layout(&tpl, &serde_json::json!({}), &fonts);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2.7 FormatConfig — konfigürasyon bazlı para birimi formatlama
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_7_format_config_default_turkish() {
|
||||||
|
// Varsayılan: Türk Lirası formatı
|
||||||
|
let formatted = dreport_layout::expr_eval::apply_format("18880", Some("currency"));
|
||||||
|
assert_eq!(formatted, "18.880,00 ₺");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_7_format_config_custom() {
|
||||||
|
// Özel config: USD formatı
|
||||||
|
let config = FormatConfig {
|
||||||
|
thousands_separator: ",".to_string(),
|
||||||
|
decimal_separator: ".".to_string(),
|
||||||
|
currency_symbol: "$".to_string(),
|
||||||
|
currency_position: "prefix".to_string(),
|
||||||
|
};
|
||||||
|
let formatted = dreport_layout::expr_eval::apply_format_with_config("18880", Some("currency"), &config);
|
||||||
|
assert_eq!(formatted, "$18,880.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_7_format_config_number() {
|
||||||
|
let config = FormatConfig {
|
||||||
|
thousands_separator: " ".to_string(),
|
||||||
|
decimal_separator: ",".to_string(),
|
||||||
|
currency_symbol: "€".to_string(),
|
||||||
|
currency_position: "suffix".to_string(),
|
||||||
|
};
|
||||||
|
let formatted = dreport_layout::expr_eval::apply_format_with_config("1234567", Some("number"), &config);
|
||||||
|
assert_eq!(formatted, "1 234 567");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_2_7_format_config_in_template() {
|
||||||
|
// Template seviyesinde format_config ayarlanabilmeli
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.format_config = Some(FormatConfig {
|
||||||
|
thousands_separator: ",".to_string(),
|
||||||
|
decimal_separator: ".".to_string(),
|
||||||
|
currency_symbol: "$".to_string(),
|
||||||
|
currency_position: "prefix".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serde ile serialize/deserialize çalışmalı
|
||||||
|
let json = serde_json::to_string(&tpl).unwrap();
|
||||||
|
let parsed: Template = serde_json::from_str(&json).unwrap();
|
||||||
|
let fc = parsed.format_config.unwrap();
|
||||||
|
assert_eq!(fc.currency_symbol, "$");
|
||||||
|
assert_eq!(fc.thousands_separator, ",");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Genel: Ellipse shape render
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ellipse_shape_renders() {
|
||||||
|
let mut tpl = base_template();
|
||||||
|
tpl.root.children.push(TemplateElement::Shape(ShapeElement {
|
||||||
|
id: "ellipse".to_string(),
|
||||||
|
position: PositionMode::Flow,
|
||||||
|
size: SizeConstraint {
|
||||||
|
width: SizeValue::Fixed { value: 40.0 },
|
||||||
|
height: SizeValue::Fixed { value: 20.0 },
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
shape_type: "ellipse".to_string(),
|
||||||
|
style: ContainerStyle {
|
||||||
|
background_color: Some("#ff6600".to_string()),
|
||||||
|
border_color: Some("#cc3300".to_string()),
|
||||||
|
border_width: Some(0.5),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
let fonts = load_test_fonts();
|
||||||
|
let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap();
|
||||||
|
let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
assert!(pdf.starts_with(b"%PDF"));
|
||||||
|
}
|
||||||
@@ -1,44 +1,10 @@
|
|||||||
//! Integration tests for the layout engine's compute_layout() public API.
|
//! Integration tests for the layout engine's compute_layout() public API.
|
||||||
|
|
||||||
use dreport_core::models::*;
|
use dreport_core::models::*;
|
||||||
use dreport_layout::{compute_layout, FontData, LayoutResult};
|
use dreport_layout::{compute_layout, LayoutResult};
|
||||||
|
|
||||||
fn load_test_fonts() -> Vec<FontData> {
|
mod common;
|
||||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
use common::load_test_fonts;
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.join("backend/fonts");
|
|
||||||
|
|
||||||
let mut fonts = Vec::new();
|
|
||||||
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().is_some_and(|e| e == "ttf") {
|
|
||||||
let family = path
|
|
||||||
.file_stem()
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.split('-')
|
|
||||||
.next()
|
|
||||||
.unwrap_or("Unknown")
|
|
||||||
.to_string();
|
|
||||||
// Map NotoSans → "Noto Sans", NotoSansMono → "Noto Sans Mono"
|
|
||||||
let family = if family == "NotoSansMono" {
|
|
||||||
"Noto Sans Mono".to_string()
|
|
||||||
} else if family == "NotoSans" {
|
|
||||||
"Noto Sans".to_string()
|
|
||||||
} else {
|
|
||||||
family
|
|
||||||
};
|
|
||||||
fonts.push(FontData {
|
|
||||||
family,
|
|
||||||
data: std::fs::read(&path).unwrap(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fonts
|
|
||||||
}
|
|
||||||
|
|
||||||
fn simple_template() -> Template {
|
fn simple_template() -> Template {
|
||||||
Template {
|
Template {
|
||||||
@@ -51,6 +17,7 @@ fn simple_template() -> Template {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -92,7 +59,7 @@ fn test_compute_layout_single_page() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let result: LayoutResult = compute_layout(&template, &data, &fonts);
|
let result: LayoutResult = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
|
|
||||||
assert_eq!(result.pages.len(), 1);
|
assert_eq!(result.pages.len(), 1);
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
@@ -106,7 +73,7 @@ fn test_compute_layout_elements_within_page() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let result = compute_layout(&template, &data, &fonts);
|
let result = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
|
|
||||||
// Should have at least root + title = 2 elements
|
// Should have at least root + title = 2 elements
|
||||||
@@ -169,7 +136,7 @@ fn test_compute_layout_text_content_resolved() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let result = compute_layout(&template, &data, &fonts);
|
let result = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
|
|
||||||
let title = page.elements.iter().find(|e| e.id == "title").unwrap();
|
let title = page.elements.iter().find(|e| e.id == "title").unwrap();
|
||||||
@@ -193,6 +160,7 @@ fn test_compute_layout_with_data_binding() {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -234,7 +202,7 @@ fn test_compute_layout_with_data_binding() {
|
|||||||
});
|
});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let result = compute_layout(&template, &data, &fonts);
|
let result = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
|
|
||||||
let bound = page
|
let bound = page
|
||||||
@@ -262,6 +230,7 @@ fn test_compute_layout_multiple_children_ordering() {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -314,7 +283,7 @@ fn test_compute_layout_multiple_children_ordering() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let result = compute_layout(&template, &data, &fonts);
|
let result = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
let page = &result.pages[0];
|
let page = &result.pages[0];
|
||||||
|
|
||||||
let first = page.elements.iter().find(|e| e.id == "first").unwrap();
|
let first = page.elements.iter().find(|e| e.id == "first").unwrap();
|
||||||
|
|||||||
@@ -4,43 +4,10 @@
|
|||||||
#![cfg(not(target_arch = "wasm32"))]
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
use dreport_core::models::*;
|
use dreport_core::models::*;
|
||||||
use dreport_layout::{compute_layout, FontData};
|
use dreport_layout::compute_layout;
|
||||||
|
|
||||||
fn load_test_fonts() -> Vec<FontData> {
|
mod common;
|
||||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
use common::load_test_fonts;
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.join("backend/fonts");
|
|
||||||
|
|
||||||
let mut fonts = Vec::new();
|
|
||||||
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().is_some_and(|e| e == "ttf") {
|
|
||||||
let family = path
|
|
||||||
.file_stem()
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.split('-')
|
|
||||||
.next()
|
|
||||||
.unwrap_or("Unknown")
|
|
||||||
.to_string();
|
|
||||||
let family = if family == "NotoSansMono" {
|
|
||||||
"Noto Sans Mono".to_string()
|
|
||||||
} else if family == "NotoSans" {
|
|
||||||
"Noto Sans".to_string()
|
|
||||||
} else {
|
|
||||||
family
|
|
||||||
};
|
|
||||||
fonts.push(FontData {
|
|
||||||
family,
|
|
||||||
data: std::fs::read(&path).unwrap(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fonts
|
|
||||||
}
|
|
||||||
|
|
||||||
fn simple_template() -> Template {
|
fn simple_template() -> Template {
|
||||||
Template {
|
Template {
|
||||||
@@ -53,6 +20,7 @@ fn simple_template() -> Template {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -94,7 +62,7 @@ fn test_render_pdf_produces_valid_output() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let layout = compute_layout(&template, &data, &fonts);
|
let layout = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
|
||||||
// PDF should not be empty
|
// PDF should not be empty
|
||||||
@@ -123,6 +91,7 @@ fn test_render_pdf_with_multiple_elements() {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -189,7 +158,7 @@ fn test_render_pdf_with_multiple_elements() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let layout = compute_layout(&template, &data, &fonts);
|
let layout = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
|
||||||
assert!(!pdf_bytes.is_empty());
|
assert!(!pdf_bytes.is_empty());
|
||||||
@@ -215,6 +184,7 @@ fn test_render_pdf_with_container_styles() {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -257,7 +227,7 @@ fn test_render_pdf_with_container_styles() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let layout = compute_layout(&template, &data, &fonts);
|
let layout = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
|
|
||||||
assert!(!pdf_bytes.is_empty());
|
assert!(!pdf_bytes.is_empty());
|
||||||
@@ -273,6 +243,7 @@ fn test_page_break_produces_multiple_pages() {
|
|||||||
fonts: vec!["Noto Sans".to_string()],
|
fonts: vec!["Noto Sans".to_string()],
|
||||||
header: None,
|
header: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
|
format_config: None,
|
||||||
root: ContainerElement {
|
root: ContainerElement {
|
||||||
id: "root".to_string(),
|
id: "root".to_string(),
|
||||||
position: PositionMode::Flow,
|
position: PositionMode::Flow,
|
||||||
@@ -307,7 +278,7 @@ fn test_page_break_produces_multiple_pages() {
|
|||||||
let data = serde_json::json!({});
|
let data = serde_json::json!({});
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let layout = compute_layout(&template, &data, &fonts);
|
let layout = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
|
|
||||||
println!("Layout pages: {}", layout.pages.len());
|
println!("Layout pages: {}", layout.pages.len());
|
||||||
for (i, page) in layout.pages.iter().enumerate() {
|
for (i, page) in layout.pages.iter().enumerate() {
|
||||||
@@ -332,10 +303,8 @@ fn test_page_break_produces_multiple_pages() {
|
|||||||
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||||
assert!(pdf_bytes.starts_with(b"%PDF"));
|
assert!(pdf_bytes.starts_with(b"%PDF"));
|
||||||
|
|
||||||
// Write PDF for manual inspection
|
// Write PDF to temp dir for manual inspection
|
||||||
let out_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let out_path = std::env::temp_dir().join("dreport_test_page_break.pdf");
|
||||||
.parent().unwrap()
|
|
||||||
.join("test_page_break.pdf");
|
|
||||||
std::fs::write(&out_path, &pdf_bytes).unwrap();
|
std::fs::write(&out_path, &pdf_bytes).unwrap();
|
||||||
println!("Wrote: {}", out_path.display());
|
println!("Wrote: {}", out_path.display());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,19 @@
|
|||||||
|
|
||||||
#![cfg(not(target_arch = "wasm32"))]
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
mod visual {
|
mod visual {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use dreport_core::models::Template;
|
use dreport_core::models::Template;
|
||||||
use dreport_layout::{compute_layout, FontData, ResolvedContent};
|
use dreport_layout::{compute_layout, ResolvedContent};
|
||||||
use dreport_layout::pdf_render::render_pdf;
|
use dreport_layout::pdf_render::render_pdf;
|
||||||
|
|
||||||
|
use crate::common::load_test_fonts;
|
||||||
|
|
||||||
fn fixtures_dir() -> std::path::PathBuf {
|
fn fixtures_dir() -> std::path::PathBuf {
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
|
||||||
}
|
}
|
||||||
@@ -24,42 +28,6 @@ mod visual {
|
|||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots")
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_test_fonts() -> Vec<FontData> {
|
|
||||||
let font_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.join("backend/fonts");
|
|
||||||
|
|
||||||
let mut fonts = Vec::new();
|
|
||||||
for entry in fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().is_some_and(|e| e == "ttf") {
|
|
||||||
let family = path
|
|
||||||
.file_stem()
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.split('-')
|
|
||||||
.next()
|
|
||||||
.unwrap_or("Unknown")
|
|
||||||
.to_string();
|
|
||||||
let family = if family == "NotoSansMono" {
|
|
||||||
"Noto Sans Mono".to_string()
|
|
||||||
} else if family == "NotoSans" {
|
|
||||||
"Noto Sans".to_string()
|
|
||||||
} else {
|
|
||||||
family
|
|
||||||
};
|
|
||||||
fonts.push(FontData {
|
|
||||||
family,
|
|
||||||
data: fs::read(&path).unwrap(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fonts
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_test_pdf(template_name: &str, data_name: &str) -> Vec<u8> {
|
fn generate_test_pdf(template_name: &str, data_name: &str) -> Vec<u8> {
|
||||||
let template_json = fs::read_to_string(fixtures_dir().join(template_name)).unwrap();
|
let template_json = fs::read_to_string(fixtures_dir().join(template_name)).unwrap();
|
||||||
let data_json = fs::read_to_string(fixtures_dir().join(data_name)).unwrap();
|
let data_json = fs::read_to_string(fixtures_dir().join(data_name)).unwrap();
|
||||||
@@ -68,7 +36,7 @@ mod visual {
|
|||||||
let data: serde_json::Value = serde_json::from_str(&data_json).unwrap();
|
let data: serde_json::Value = serde_json::from_str(&data_json).unwrap();
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let layout = compute_layout(&template, &data, &fonts);
|
let layout = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
render_pdf(&layout, &fonts).expect("PDF render failed")
|
render_pdf(&layout, &fonts).expect("PDF render failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +179,7 @@ mod visual {
|
|||||||
let data: serde_json::Value = serde_json::from_str(&data_json).unwrap();
|
let data: serde_json::Value = serde_json::from_str(&data_json).unwrap();
|
||||||
let fonts = load_test_fonts();
|
let fonts = load_test_fonts();
|
||||||
|
|
||||||
let layout = compute_layout(&template, &data, &fonts);
|
let layout = compute_layout(&template, &data, &fonts).unwrap();
|
||||||
|
|
||||||
let mut html = String::from("<!DOCTYPE html><html><head><style>body{margin:20px;font-family:sans-serif;background:#f5f5f5}.chart-box{margin:10px 0;background:white;box-shadow:0 1px 3px rgba(0,0,0,.1)}</style></head><body><h2>Chart SVG Preview (HTML render)</h2>");
|
let mut html = String::from("<!DOCTYPE html><html><head><style>body{margin:20px;font-family:sans-serif;background:#f5f5f5}.chart-box{margin:10px 0;background:white;box-shadow:0 1px 3px rgba(0,0,0,.1)}</style></head><body><h2>Chart SVG Preview (HTML render)</h2>");
|
||||||
|
|
||||||
|
|||||||
4
rust-toolchain.toml
Normal file
4
rust-toolchain.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
|
targets = ["wasm32-unknown-unknown"]
|
||||||
BIN
test_output.pdf
BIN
test_output.pdf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user