Add dafna-decision-template skill — knowledge for template-based DOCX export
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 6s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 6s
Documents the rules and decisions behind building DOCX files from דפנה's
decision template (טיוטת החלטה.dotx). The implementation lives in
mcp-server/src/legal_mcp/services/analysis_docx_exporter.py; this skill
captures the "why" so future improvements don't need to rediscover it.
Contents:
SKILL.md 5 critical rules, style mapping table,
export flow, line classification,
dash policy, placeholder handling,
troubleshooting, future TODOs
references/dotx-to-docx.md why python-docx can't open .dotx +
the conversion recipe
references/rtl-runs.md why <w:rtl/> is required on every run
(otherwise Hebrew falls back to
Times New Roman)
references/style-mapping.md XML dump of every template style,
with the Title-via-theme gotcha
references/line-classification.md the 7 regex categories in
_classify_line() with real examples
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
292
skills/dafna-decision-template/SKILL.md
Normal file
292
skills/dafna-decision-template/SKILL.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
---
|
||||||
|
name: dafna-decision-template
|
||||||
|
description: >
|
||||||
|
ייצוא מסמכי DOCX עבור ועדת ערר לתכנון ובניה מחוז ירושלים (יו"ר עו"ד דפנה תמיר),
|
||||||
|
באמצעות שימוש בסגנונות המוגדרים בטמפלט Word של דפנה (`טיוטת החלטה.dotx`).
|
||||||
|
הסקיל מחיל את סגנונות הטמפלט (Normal/Heading 1/Heading 2/Quote/List Paragraph)
|
||||||
|
על תוכן שנכתב מתוכנתית — בלי להגדיר פונט/גודל/RTL/שוליים ידנית.
|
||||||
|
|
||||||
|
טריגרים: "ייצוא החלטה", "ייצוא ניתוח משפטי", "DOCX של דפנה",
|
||||||
|
"טמפלט החלטה", "סגנונות החלטה", "Word עם David", "מסמך ועדת ערר",
|
||||||
|
"הורדת החלטה", "פסקה ממוספרת בעברית", "(א) (ב) (ג)",
|
||||||
|
כל בקשה להוציא מסמך Word המבוסס על טמפלט דפנה.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dafna Decision Template — ייצוא DOCX מסגנונות טמפלט
|
||||||
|
|
||||||
|
## מה זה עושה
|
||||||
|
|
||||||
|
סקיל זה הוא **layer דק מעל `python-docx`** שמטעין את הטמפלט של דפנה
|
||||||
|
([skills/docx/decision_template.docx](../docx/decision_template.docx) —
|
||||||
|
מומר מ-`data/training/טיוטת החלטה.dotx`) וכותב תוכן חדש על בסיסו, **על ידי
|
||||||
|
שיוך שמות סגנונות בלבד** (`paragraph.style = "Heading 2"`). העיצוב —
|
||||||
|
פונט David, RTL, גדלים, הזחות, מספור אוטומטי — מגיע מה-`styles.xml`
|
||||||
|
של הטמפלט.
|
||||||
|
|
||||||
|
השירות המעשי נמצא ב-
|
||||||
|
[`mcp-server/src/legal_mcp/services/analysis_docx_exporter.py`](../../mcp-server/src/legal_mcp/services/analysis_docx_exporter.py).
|
||||||
|
הסקיל מתעד את **הכללים** שה-service מיישם, כך שניתן לשחזר/להרחיב אותם
|
||||||
|
בסקריפטים אחרים ללא צורך לגלות הכל מחדש.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 קריטי — 5 עקרונות שחייבים לזכור
|
||||||
|
|
||||||
|
### 1. **לא להגדיר font/size/indent ידנית** — תמיד style
|
||||||
|
```python
|
||||||
|
# ❌ אל
|
||||||
|
run.font.name = "David"; run.font.size = Pt(13)
|
||||||
|
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
|
||||||
|
# ✅ כן
|
||||||
|
paragraph = doc.add_paragraph(style="Normal") # כל השאר מגיע מהטמפלט
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **RTL חייב דגלים ב-paragraph וב-run — גם אם הסגנון כולל bidi**
|
||||||
|
```python
|
||||||
|
# ❌ אל — עברית תצא ב-Times New Roman כי Word לא יודע שזה complex-script
|
||||||
|
p = doc.add_paragraph(style="Normal")
|
||||||
|
p.add_run("טקסט בעברית")
|
||||||
|
|
||||||
|
# ✅ כן — מסמנים את ה-run כ-RTL
|
||||||
|
p = doc.add_paragraph(style="Normal")
|
||||||
|
_mark_paragraph_rtl(p)
|
||||||
|
run = p.add_run("טקסט בעברית")
|
||||||
|
_mark_run_rtl(run) # ← מוסיף <w:rtl/> ל-rPr
|
||||||
|
```
|
||||||
|
בלי הדגל הזה, Word נופל בחזרה ל-`ascii` font (Times New Roman) במקום
|
||||||
|
ל-`cs` font (David). זו הסיבה הנפוצה ביותר לתוצאה "עברית ב-Times New Roman".
|
||||||
|
|
||||||
|
ראה [references/rtl-runs.md](references/rtl-runs.md) לפרטים מלאים.
|
||||||
|
|
||||||
|
### 3. **`.dotx` לא נטען ישירות** — יש להמיר ל-`.docx` פעם אחת
|
||||||
|
python-docx פותח רק `.docx`. להמרה: `scripts/convert_decision_template.py`
|
||||||
|
(בשורש הפרויקט). יש להסיר:
|
||||||
|
- `word/glossary/*` parts
|
||||||
|
- Override entries שמצביעים אליהם ב-`[Content_Types].xml`
|
||||||
|
- Relationship `glossaryDocument` ב-`word/_rels/document.xml.rels`
|
||||||
|
- להחליף content-type מ-`...template.main+xml` ל-`...document.main+xml`
|
||||||
|
|
||||||
|
ראה [references/dotx-to-docx.md](references/dotx-to-docx.md).
|
||||||
|
|
||||||
|
### 4. **Title לא טוב ככותרת עברית** — השתמש ב-Heading 1
|
||||||
|
הסגנון `Title` בטמפלט מפנה ל-theme fonts (`majorFont`). ב-theme1.xml:
|
||||||
|
`majorFont.latin = "Aptos Display"`, `majorFont.cs = ""` (ריק).
|
||||||
|
לכן עברית תרונדר ב-Latin fallback.
|
||||||
|
|
||||||
|
`Heading 1` יורש cs="David" מ-`Normal` — השתמש בו לכותרת ראשית.
|
||||||
|
|
||||||
|
### 5. **מספור אוטומטי רק ב-`List Paragraph`** — decimal בלבד
|
||||||
|
`List Paragraph` (styleId `a0`) מקושר ל-`numId=1 → numFmt=decimal`.
|
||||||
|
כלומר Word יוסיף אוטומטית "1.", "2.", "3." לכל פסקה עם הסגנון הזה.
|
||||||
|
|
||||||
|
- שורות שמתחילות ב-`N.` → הסר את המספר מהטקסט, החל `List Paragraph`.
|
||||||
|
- שורות שמתחילות ב-`(א)` `(ב)` → השתמש ב-`List Paragraph` **עם הסרת `<w:numPr>`**,
|
||||||
|
כי המספור בעברית נכתב בעצמו על ידי המחבר.
|
||||||
|
- Bullets (`- `, `• `) → הסר את הסימן, השאר `Normal`.
|
||||||
|
|
||||||
|
ראה [references/style-mapping.md](references/style-mapping.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## הטמפלט — מיפוי סגנונות
|
||||||
|
|
||||||
|
מיפוי מלא של הסגנונות בטמפלט ל-content type. זה ה"חוזה" של הסקיל —
|
||||||
|
כל שינוי דרך השירות צריך להיצמד אליו.
|
||||||
|
|
||||||
|
| תוכן | style name | הערה |
|
||||||
|
|------|-----------|------|
|
||||||
|
| כותרת מסמך ("ניתוח משפטי וכתיבת עמדה בערר X") | `Heading 1` | יורש cs="David" |
|
||||||
|
| כותרת מקטע ראשי (רקע דיוני, פסיקה כללית, סוגיות להכרעה, מסקנות) | `Heading 2` | bold + underline |
|
||||||
|
| כותרת משנה בתוך סוגיה (טענה (claim), תשובה, ניתוח, נקודות פתוחות, …) | `Heading 2` | |
|
||||||
|
| שם subsection (טענת סף 1, סוגיה 2) | `Normal` + bold run | |
|
||||||
|
| פסקת רקע רגילה | `Normal` | David 13pt, justify, RTL |
|
||||||
|
| פסקת רשימה ממוספרת (1., 2., 3.) | `List Paragraph` | Word ימספר |
|
||||||
|
| פסקת רשימה בעברית ((א), (ב)) | `List Paragraph` + `_strip_numpr()` | המספור נשאר בטקסט |
|
||||||
|
| ציטוט פסיקה | `Quote` | bold + הזחה |
|
||||||
|
| citation / מקור | `Normal` + italic | |
|
||||||
|
|
||||||
|
ראה [references/style-mapping.md](references/style-mapping.md) לטבלה
|
||||||
|
מורחבת עם ה-XML של כל סגנון.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תוכן המקור — `analysis-and-research.md`
|
||||||
|
|
||||||
|
הקובץ נכתב על ידי `legal-analyst` agent. מבנה מצופה:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# ניתוח משפטי וכתיבת עמדה — ערר {case_number}
|
||||||
|
תאריך: DD.MM.YYYY
|
||||||
|
|
||||||
|
## רקע דיוני
|
||||||
|
{prose content}
|
||||||
|
|
||||||
|
## עובדות מוסכמות
|
||||||
|
1. ...
|
||||||
|
|
||||||
|
## עובדות שנויות במחלוקת
|
||||||
|
1. ...
|
||||||
|
|
||||||
|
## טענות סף
|
||||||
|
### טענה {n}: {title}
|
||||||
|
**עמדת המבקשת:** ...
|
||||||
|
**עמדת ועדת הערר:** [ימולא ע"י יו"ר הוועדה]
|
||||||
|
|
||||||
|
## סוגיות להכרעה
|
||||||
|
### סוגיה {n}: {title}
|
||||||
|
**ממצאים עובדתיים:**
|
||||||
|
...
|
||||||
|
**טענה (claim):**
|
||||||
|
העורר טוען כי:
|
||||||
|
(א) ...
|
||||||
|
(ב) ...
|
||||||
|
**ניתוח:**
|
||||||
|
...
|
||||||
|
- **נקודות פתוחות:**
|
||||||
|
1. ...
|
||||||
|
- **הערכה ראשונית:** {prose}
|
||||||
|
**עמדת ועדת הערר:** [ימולא ע"י יו"ר הוועדה]
|
||||||
|
|
||||||
|
## מסקנות
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
ה-parser (`research_md.py`) חולק את זה ל-`threshold_claims[]`, `issues[]`,
|
||||||
|
prose sections, ו-`conclusions`. חשוב: **שמות sections חייבים להכיל את
|
||||||
|
המילים המופתח** (`רקע דיוני`, `טענות סף`, …) — ה-parser עובד לפי matching של
|
||||||
|
keywords, לא לפי מספר ה-H2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## זרימת הייצוא
|
||||||
|
|
||||||
|
סדר המקטעים ב-DOCX (גם אם סדר ה-H2 ב-MD שונה):
|
||||||
|
|
||||||
|
1. **כותרת** (Heading 1) + שורת תאריך
|
||||||
|
2. **רקע** (Heading 2) — `represented_party`, `procedural_background`,
|
||||||
|
`agreed_facts`, `disputed_facts` (אם קיימים)
|
||||||
|
3. **פסיקה כללית** (Heading 2) — `case_precedents` עם `section_id=NULL`
|
||||||
|
4. **טענות סף** (Heading 2) — לכל subsection: title, fields, chair_position, precedents
|
||||||
|
5. **סוגיות להכרעה** (Heading 2) — אותה חלוקה
|
||||||
|
6. **מסקנות** (Heading 2) — בסוף
|
||||||
|
|
||||||
|
הסיבה: בקריאה משפטית נכון להציג תחילה רקע ועובדות, ואז את הדיון. פסיקה
|
||||||
|
כללית מופיעה לפני הסוגיות כי היא רוחבית.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## עיבוד שורות — `_classify_line()`
|
||||||
|
|
||||||
|
ה-service מפרש כל שורה של content לאחת מ-6 קטגוריות:
|
||||||
|
|
||||||
|
| kind | דוגמה | מה קורה |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `label_heading` | `**נקודות פתוחות:**` (שורה שלמה, כולל `- **X:**`) | Heading 2 |
|
||||||
|
| `label_heading` (plain) | `העורר טוען כי:` (שורה קצרה שמסתיימת ב-`:`) | Heading 2 |
|
||||||
|
| `inline_label` | `**שאלה עקרונית:** מה...` | Normal עם label bold inline |
|
||||||
|
| `numbered` | `1. הנספח אינו מחייב` | List Paragraph, המספר מוסר |
|
||||||
|
| `bullet` | `- nevo (קלאסי)...` | Normal, הסימן מוסר |
|
||||||
|
| `heb_letter` | `(א) הנספח אינו...` | List Paragraph + strip numPr |
|
||||||
|
| `plain` | רגיל | Normal |
|
||||||
|
|
||||||
|
בנוסף, **inline `**...**`** מעובד בכל ריצה דרך `_add_runs_with_inline_bold`
|
||||||
|
— כל `**word**` הופך ל-run נפרד עם `bold=True`.
|
||||||
|
|
||||||
|
ראה [references/line-classification.md](references/line-classification.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## מקפים — מדיניות
|
||||||
|
|
||||||
|
המשתמש (דפנה) ביקשה: **"לא רוצה מקפים בכלל"**. הסקיל מסיר:
|
||||||
|
|
||||||
|
- `—` (em-dash, U+2014)
|
||||||
|
- `–` (en-dash, U+2013)
|
||||||
|
|
||||||
|
מכל טקסט שהקוד כותב למסמך (גם תוכן מהמקור). מקפים רגילים (`-`)
|
||||||
|
נשמרים. הפונקציה: `_no_dash()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## שדות ריקים — placeholder
|
||||||
|
|
||||||
|
שדה `chair_position` (עמדת ועדת הערר) שמכיל אחד מהסימנים הריקים
|
||||||
|
(`[ימולא ע"י יו"ר הוועדה]`, `[טרם מולא]`, וכד') → מוחלף ב-
|
||||||
|
`[טרם מולאה עמדת ועדת הערר]` בסגנון italic. זה סימן ויזואלי ברור
|
||||||
|
שנשאר עדיין להשלים.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## שימוש — API
|
||||||
|
|
||||||
|
```python
|
||||||
|
from legal_mcp.services.analysis_docx_exporter import build_analysis_docx
|
||||||
|
|
||||||
|
path = await build_analysis_docx("8070-25")
|
||||||
|
# → data/cases/8070-25/exports/ניתוח-משפטי-v{N}.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
Endpoint ציבורי: `GET /api/cases/{case_number}/research/analysis/export-docx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## התמודדות עם בעיות נפוצות
|
||||||
|
|
||||||
|
### "עברית יוצאת ב-Times New Roman"
|
||||||
|
- חסר `<w:rtl/>` ב-run. הוסף `_mark_run_rtl(run)` אחרי כל `add_run`.
|
||||||
|
- בדוק: הסגנון שאתה משתמש בו יש בו `cs="David"` (או יורש מ-Normal שיש לו)?
|
||||||
|
|
||||||
|
### "המספור כפול: '1. (א) ...'"
|
||||||
|
- אתה משתמש ב-`List Paragraph` על שורה עם `(א)`. צריך `_strip_numpr(para)`.
|
||||||
|
|
||||||
|
### "כוכביות `**...**` מופיעות במסמך"
|
||||||
|
- הקפד להעביר תוכן דרך `_add_runs_with_inline_bold()`, לא `paragraph.add_run()`.
|
||||||
|
|
||||||
|
### "התבנית לא נטענת"
|
||||||
|
- הרץ מחדש `python scripts/convert_decision_template.py`. בדוק שה-docx
|
||||||
|
שנוצר פותחיb ב-Word ללא שגיאות.
|
||||||
|
|
||||||
|
### "המספור ברשימה השנייה ממשיך מהראשונה (4,5,6 במקום 1,2,3)"
|
||||||
|
- ידוע. הפתרון: להוסיף override של `numId` ברמת ה-paragraph הראשון של
|
||||||
|
הרשימה החדשה. עדיין לא מיושם — ראה "שיפורים עתידיים".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## שיפורים עתידיים (TODO)
|
||||||
|
|
||||||
|
- [ ] Reset של מספור List Paragraph בין רשימות נפרדות (לא רציף).
|
||||||
|
- [ ] תמיכה ב-`פיסקת רשימה - ללא מספור` (styleId `-`) לbullets.
|
||||||
|
- [ ] עיצוב מותאם לסוג הערר (1xxx/8xxx/9xxx) — כרגע אחיד.
|
||||||
|
- [ ] אפשרות להוריד רק מקטעים נבחרים (רק טענות סף, רק מסקנות, וכו').
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## מבנה קבצים
|
||||||
|
|
||||||
|
```
|
||||||
|
skills/dafna-decision-template/
|
||||||
|
├── SKILL.md ← הקובץ הזה
|
||||||
|
└── references/
|
||||||
|
├── dotx-to-docx.md ← איך ממירים .dotx ל-.docx
|
||||||
|
├── rtl-runs.md ← למה `<w:rtl/>` חשוב בכל run
|
||||||
|
├── style-mapping.md ← מיפוי מלא של סגנונות הטמפלט
|
||||||
|
└── line-classification.md ← לוגיקת _classify_line()
|
||||||
|
|
||||||
|
mcp-server/src/legal_mcp/services/
|
||||||
|
└── analysis_docx_exporter.py ← המימוש המעשי
|
||||||
|
|
||||||
|
scripts/
|
||||||
|
└── convert_decision_template.py ← המרת dotx → docx (חד-פעמי)
|
||||||
|
|
||||||
|
skills/docx/
|
||||||
|
└── decision_template.docx ← הטמפלט המומר (artifact)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## היסטוריה
|
||||||
|
|
||||||
|
| גרסה | תאריך | שינוי |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| v1.0 | 2026-04-16 | יצירת הסקיל. מיפוי ראשוני של סגנונות, תמיכה ב-RTL runs, inline bold, (א)(ב), Heading 1/2, מקטעי רקע. |
|
||||||
58
skills/dafna-decision-template/references/dotx-to-docx.md
Normal file
58
skills/dafna-decision-template/references/dotx-to-docx.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# המרת `.dotx` → `.docx` עבור python-docx
|
||||||
|
|
||||||
|
## למה
|
||||||
|
|
||||||
|
python-docx **לא יודע לפתוח** קובצי Word Template (`.dotx`). ניסיון לפתיחה
|
||||||
|
זורק:
|
||||||
|
```
|
||||||
|
ValueError: file 'X.dotx' is not a Word file, content type is
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml'
|
||||||
|
```
|
||||||
|
|
||||||
|
כי `main document part` של `.dotx` מסומן כ-template, לא כ-document.
|
||||||
|
|
||||||
|
## הפתרון — המרה חד-פעמית
|
||||||
|
|
||||||
|
קובץ `.dotx` הוא ZIP שכולל את אותם parts כמו `.docx` + `word/glossary/`
|
||||||
|
(building blocks). להמרה:
|
||||||
|
|
||||||
|
1. פתח את ה-ZIP.
|
||||||
|
2. **הסר** את כל ה-parts תחת `word/glossary/`.
|
||||||
|
3. **תקן** `[Content_Types].xml`:
|
||||||
|
- החלף `template.main+xml` ב-`document.main+xml`
|
||||||
|
- הסר `<Override>` entries שמצביעים ל-`/word/glossary/...`
|
||||||
|
4. **תקן** `word/_rels/document.xml.rels`:
|
||||||
|
- הסר את ה-`<Relationship>` עם
|
||||||
|
`Type=".../relationships/glossaryDocument"`
|
||||||
|
5. שמור מחדש כ-ZIP עם סיומת `.docx`.
|
||||||
|
|
||||||
|
## הסקריפט
|
||||||
|
|
||||||
|
[`scripts/convert_decision_template.py`](../../../scripts/convert_decision_template.py)
|
||||||
|
(בשורש הפרויקט) עושה את זה. הרץ אותו:
|
||||||
|
|
||||||
|
- פעם אחת אחרי clone של הפרויקט (אם `skills/docx/decision_template.docx`
|
||||||
|
לא קיים).
|
||||||
|
- בכל פעם שדפנה מעדכנת את `data/training/טיוטת החלטה.dotx`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/convert_decision_template.py
|
||||||
|
# → skills/docx/decision_template.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
הסקריפט כולל verification שבודק שהקובץ שנוצר נטען נקי ב-python-docx
|
||||||
|
ושהסגנונות הקריטיים (Normal, Heading 2, Quote, List Paragraph, Title)
|
||||||
|
נמצאים בו.
|
||||||
|
|
||||||
|
## למה לא `docxtpl`?
|
||||||
|
|
||||||
|
`docxtpl` מיועד ל-**placeholder substitution** סגנון Jinja2
|
||||||
|
(`{{ variable }}`). הטמפלט שלנו לא מכיל placeholders — אנחנו מרכיבים
|
||||||
|
תוכן דינמי. `python-docx` על טמפלט `.docx` נקי מספיק לגמרי.
|
||||||
|
|
||||||
|
## הימנע מ-
|
||||||
|
|
||||||
|
- **אל תנסה** להוריד את ה-glossary רק מ-`[Content_Types].xml` בלי להסיר
|
||||||
|
את ה-`<Relationship>` שמפנה אליו → תקבל dangling reference.
|
||||||
|
- **אל תנסה** להשתמש ב-`.dotx` ישירות דרך `zipfile` + לבנות Document
|
||||||
|
ידנית — חוסך 10 שורות אבל מאבד את כל ה-robustness של python-docx.
|
||||||
115
skills/dafna-decision-template/references/line-classification.md
Normal file
115
skills/dafna-decision-template/references/line-classification.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# סיווג שורות — `_classify_line()`
|
||||||
|
|
||||||
|
כל שורה של content מה-MD עוברת דרך `_classify_line()` שמחזירה
|
||||||
|
`(kind, clean_text)`. הקטגוריה מכתיבה איזה סגנון Word יוחל.
|
||||||
|
|
||||||
|
## טבלת הקטגוריות
|
||||||
|
|
||||||
|
| kind | regex | clean_text | נמפה ל-style |
|
||||||
|
|------|--------|-----------|--------------|
|
||||||
|
| `label_heading` | `^\s*\*\*([^\n*]+?):\*\*\s*$` | `"$1"` | `Heading 2` |
|
||||||
|
| `label_heading` (plain) | `^\s*([^\n:]{2,40}):\s*$` | `"$1"` | `Heading 2` |
|
||||||
|
| `inline_label` | `^\s*\*\*([^\n*]+?):\*\*\s+(.+)$` | `"$1\x00$2"` | `Normal` + bold label + value |
|
||||||
|
| `numbered` | `^\s*(\d+)[.)]\s+(.+)$` | `"$2"` | `List Paragraph` |
|
||||||
|
| `bullet` | `^\s*[\-\u2022\*\u25CF\u25E6]\s+(.+)$` | `"$1"` | `Normal` (marker stripped) |
|
||||||
|
| `heb_letter` | `^\s*\([א-ת]\)\s+` | **full line** (marker kept) | `List Paragraph` + `_strip_numpr()` |
|
||||||
|
| `plain` | fallback | line | `Normal` |
|
||||||
|
|
||||||
|
## סדר הבדיקות (חשוב!)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. STANDALONE_LABEL_RE (**X:**)
|
||||||
|
2. INLINE_LABEL_RE (**X:** value)
|
||||||
|
3. NUMBERED_LINE_RE (1. X)
|
||||||
|
4. BULLET_LINE_RE (- X) + re-check inside:
|
||||||
|
4a. STANDALONE inside → label_heading
|
||||||
|
4b. INLINE inside → inline_label
|
||||||
|
5. HEB_LETTER_LINE_RE ((א) X)
|
||||||
|
6. PLAIN_LABEL_RE (X:) — last because it's broad
|
||||||
|
7. plain
|
||||||
|
```
|
||||||
|
|
||||||
|
## דוגמאות מהשטח
|
||||||
|
|
||||||
|
### input: `- **נקודות פתוחות:**`
|
||||||
|
- `BULLET_LINE_RE` תופס → `inner = "**נקודות פתוחות:**"`
|
||||||
|
- `STANDALONE_LABEL_RE` על ה-inner → `label_heading`, text = `"נקודות פתוחות"`
|
||||||
|
- יוצא כ-Heading 2.
|
||||||
|
|
||||||
|
### input: `- **נקודות פתוחות:** האם המקדם...`
|
||||||
|
- `BULLET_LINE_RE` תופס → inner = `"**נקודות פתוחות:** האם..."`
|
||||||
|
- `INLINE_LABEL_RE` על ה-inner → `inline_label`
|
||||||
|
- יוצא כ-Normal עם label "נקודות פתוחות:" bold + value רגיל.
|
||||||
|
|
||||||
|
### input: `1. **שאלה עקרונית:** האם נספח...`
|
||||||
|
- `NUMBERED_LINE_RE` תופס → `"**שאלה עקרונית:** האם נספח..."`
|
||||||
|
- יוצא כ-List Paragraph. ה-`**...**` בתוכו יעובד על ידי
|
||||||
|
`_add_runs_with_inline_bold()` (bold inline run).
|
||||||
|
|
||||||
|
### input: `(א) נספח הבינוי של תכנית...`
|
||||||
|
- `HEB_LETTER_LINE_RE` תופס
|
||||||
|
- יוצא כ-List Paragraph עם `_strip_numpr()` — כי המחבר כבר כתב "(א)".
|
||||||
|
|
||||||
|
### input: `העורר טוען כי:`
|
||||||
|
- לא תואם regex ספציפי
|
||||||
|
- `PLAIN_LABEL_RE` תופס (23 תווים, מסתיים ב-`:`)
|
||||||
|
- יוצא כ-Heading 2.
|
||||||
|
|
||||||
|
### input: `פסקה ארוכה: עם עוד תוכן ופסיק, וסוגיה מורכבת.`
|
||||||
|
- `PLAIN_LABEL_RE` לא תופס (יותר מ-40 תווים לפני `:`)
|
||||||
|
- נשאר `plain` → Normal.
|
||||||
|
|
||||||
|
## למה הגבלת `PLAIN_LABEL_RE` ל-`{2,40}`
|
||||||
|
|
||||||
|
בלי הגבלה, כל פסקה עם `:` במקום כלשהו הייתה הופכת ל-Heading 2. דוגמה
|
||||||
|
שצריך למנוע:
|
||||||
|
```
|
||||||
|
טענה חשובה כאן: היא שהוועדה שגתה בכל אופן.
|
||||||
|
```
|
||||||
|
אין כאן כוונה לכותרת — `:` הוא חלק ממשפט. ההגבלה ל-40 תווים מסננת את
|
||||||
|
רוב המקרים האלה כי רוב headings אמיתיים הם קצרים.
|
||||||
|
|
||||||
|
40 תווים זה ניחוש — אפשר לכוון אם מגלים false positives/negatives.
|
||||||
|
|
||||||
|
## inline bold — `_add_runs_with_inline_bold()`
|
||||||
|
|
||||||
|
אחרי סיווג, הטקסט עדיין יכול להכיל `**word**` באמצע. הפונקציה מחלקת
|
||||||
|
את המחרוזת ל-runs מתחלפים:
|
||||||
|
|
||||||
|
```
|
||||||
|
"העורר טוען **שהתוצאה** שגויה"
|
||||||
|
→ [
|
||||||
|
Run("העורר טוען ", bold=None),
|
||||||
|
Run("שהתוצאה", bold=True),
|
||||||
|
Run(" שגויה", bold=None),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
כל run מסומן RTL בנפרד. יוצא ב-Word עם הדגש המקומי בלבד על המילה
|
||||||
|
`שהתוצאה`.
|
||||||
|
|
||||||
|
## השלמת התמונה
|
||||||
|
|
||||||
|
```
|
||||||
|
md content
|
||||||
|
↓ splitlines()
|
||||||
|
for line in lines:
|
||||||
|
↓ _classify_line(line)
|
||||||
|
→ (kind, clean_text)
|
||||||
|
↓ _emit_content_line(doc, line)
|
||||||
|
→ paragraph with chosen style
|
||||||
|
↓ _add_runs_with_inline_bold(paragraph, clean_text)
|
||||||
|
→ runs with inline **bold** rendered
|
||||||
|
↓ _mark_run_rtl / _mark_paragraph_rtl
|
||||||
|
→ Hebrew renders in David (cs slot)
|
||||||
|
```
|
||||||
|
|
||||||
|
## הוספת קטגוריה חדשה
|
||||||
|
|
||||||
|
אם יש דפוס שלא מזוהה ורצוי למפות אותו:
|
||||||
|
|
||||||
|
1. הוסף regex constant (למעלה בקובץ, אחרי הקיימים).
|
||||||
|
2. הוסף branch ב-`_classify_line()` לפי הסדר הנכון (ספציפי לפני כללי).
|
||||||
|
3. הוסף branch ב-`_emit_content_line()` עם הסגנון המתאים.
|
||||||
|
4. הוסף test case ב-references/line-classification.md (כאן).
|
||||||
|
5. הרץ על תיק מייצג (למשל 8070-25) וראה שהתוצאה נכונה.
|
||||||
81
skills/dafna-decision-template/references/rtl-runs.md
Normal file
81
skills/dafna-decision-template/references/rtl-runs.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# למה `<w:rtl/>` חובה בכל run
|
||||||
|
|
||||||
|
## הבעיה
|
||||||
|
|
||||||
|
כשאתה יוצר `run` ב-python-docx על סגנון עברי מוגדר היטב (למשל Normal עם
|
||||||
|
`cs="David"`) — עברית עדיין יוצאת ב-Times New Roman.
|
||||||
|
|
||||||
|
## הסיבה
|
||||||
|
|
||||||
|
Word משתמש ב-3 font slots בתוך `<w:rFonts>`:
|
||||||
|
- `w:ascii` — תווים לטיניים
|
||||||
|
- `w:hAnsi` — אותיות מיוחדות אירופיות
|
||||||
|
- `w:cs` (complex script) — עברית, ערבית, תאית
|
||||||
|
|
||||||
|
ההחלטה איזה slot להשתמש נעשית **לפי סוג הטקסט ב-run** ולפי **דגל רמת
|
||||||
|
הריצה `<w:rtl/>`**. בלי הדגל, Word יכול להתייחס לטקסט העברי כ-LTR
|
||||||
|
(למשל כשהוא מתערבב עם ספרות/לטינית) ולבחור את `ascii` — Times New Roman.
|
||||||
|
|
||||||
|
## הפתרון
|
||||||
|
|
||||||
|
מסמן כל run עברי כ-complex-script:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
|
||||||
|
def _mark_run_rtl(run):
|
||||||
|
rPr = run._r.get_or_add_rPr()
|
||||||
|
if rPr.find(qn("w:rtl")) is None:
|
||||||
|
rPr.append(OxmlElement("w:rtl"))
|
||||||
|
```
|
||||||
|
|
||||||
|
וגם ברמת ה-paragraph (למקרה ש-paragraph mark עצמו משפיע):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _mark_paragraph_rtl(paragraph):
|
||||||
|
pPr = paragraph._p.get_or_add_pPr()
|
||||||
|
rPr = pPr.find(qn("w:rPr"))
|
||||||
|
if rPr is None:
|
||||||
|
rPr = OxmlElement("w:rPr"); pPr.append(rPr)
|
||||||
|
if rPr.find(qn("w:rtl")) is None:
|
||||||
|
rPr.append(OxmlElement("w:rtl"))
|
||||||
|
```
|
||||||
|
|
||||||
|
## תופעות לוואי של חוסר RTL ברמת ה-run
|
||||||
|
|
||||||
|
1. **Font fallback ל-Times New Roman** — הסימפטום הנפוץ ביותר.
|
||||||
|
2. **BiDi reordering של פיסוק** — נקודתיים, פסיקים, סוגריים עוברים למקום
|
||||||
|
הלא נכון. הסימפטום: `"(א)"` הופך ל-`")א("`.
|
||||||
|
3. **מספרים "נוגדים" ברצף עברי** — `"בשנת 2024 פסקנו"` יכול להיראות
|
||||||
|
עם המספר במיקום הלא נכון.
|
||||||
|
|
||||||
|
## איך לבדוק שה-RTL חל
|
||||||
|
|
||||||
|
```python
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
|
||||||
|
for p in doc.paragraphs:
|
||||||
|
for r in p.runs:
|
||||||
|
rPr = r._r.find(qn("w:rPr"))
|
||||||
|
has_rtl = rPr is not None and rPr.find(qn("w:rtl")) is not None
|
||||||
|
if not has_rtl and any('\u0590' <= c <= '\u05FF' for c in r.text):
|
||||||
|
print(f"Missing RTL: {r.text[:40]!r}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## זה לא מספיק רק ברמת הסגנון
|
||||||
|
|
||||||
|
זו תפיסה מוטעית נפוצה: "אם הסגנון כולל `<w:rtl/>` ב-`rPr`, ירש כל ריצה".
|
||||||
|
**לא נכון**. סגנון נותן ברירת מחדל ל-runs שעדיין לא נוצרו ב-Word GUI —
|
||||||
|
אבל runs שנוצרו דרך python-docx מקבלים `rPr` ריק, שלא תורש אוטומטית
|
||||||
|
את ה-rtl מהסגנון. לכן חייבים להוסיף ידנית.
|
||||||
|
|
||||||
|
## הטמפלט של דפנה כדוגמה
|
||||||
|
|
||||||
|
בוחנים את `word/document.xml` של הטמפלט המקורי — כל ריצה עברית כוללת:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<w:r><w:rPr><w:rFonts w:hint="cs"/><w:rtl/></w:rPr><w:t>רקע</w:t></w:r>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<w:rtl/>` נמצא שם **במפורש**. אנחנו מחקים את זה.
|
||||||
155
skills/dafna-decision-template/references/style-mapping.md
Normal file
155
skills/dafna-decision-template/references/style-mapping.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# מיפוי סגנונות — `טיוטת החלטה.dotx`
|
||||||
|
|
||||||
|
מבוסס על ניתוח `word/styles.xml` של הטמפלט. כל הגדרות מצוטטות מה-XML
|
||||||
|
המקורי.
|
||||||
|
|
||||||
|
## סגנונות בסיסיים (Word-idioms)
|
||||||
|
|
||||||
|
### `Normal` (styleId: `a1`)
|
||||||
|
ברירת המחדל לפסקאות רגילות. **כל שאר הסגנונות יורשים ממנו**
|
||||||
|
(`basedOn="a1"`).
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<rPr>
|
||||||
|
<rFonts ascii="Times New Roman" hAnsi="Times New Roman" cs="David"/>
|
||||||
|
<sz val="26"/> <!-- 13pt -->
|
||||||
|
<szCs val="26"/>
|
||||||
|
</rPr>
|
||||||
|
<pPr>
|
||||||
|
<bidi/> <!-- RTL -->
|
||||||
|
<spacing before="120" after="120" line="360" lineRule="auto"/>
|
||||||
|
<ind left="-454"/> <!-- negative left indent -->
|
||||||
|
<jc val="both"/> <!-- justify -->
|
||||||
|
</pPr>
|
||||||
|
```
|
||||||
|
|
||||||
|
**מה זה אומר לך:**
|
||||||
|
- עברית תצא ב-David 13pt (דרך `cs`)
|
||||||
|
- לטינית תצא ב-Times New Roman 13pt (דרך `ascii`)
|
||||||
|
- יישור justify (דו-צדדי), RTL
|
||||||
|
- מרווח 1.5 שורות (`line=360` = 1.5 × 240)
|
||||||
|
|
||||||
|
### `Heading 1` (styleId: `10`, name: `heading 1`)
|
||||||
|
לכותרת ראשית (כמו "ניתוח משפטי וכתיבת עמדה בערר X").
|
||||||
|
```xml
|
||||||
|
<basedOn val="a1"/> <!-- יורש מ-Normal → cs="David" -->
|
||||||
|
<rPr><rFonts asciiTheme="majorHAnsi" .../><sz val="40"/></rPr> <!-- 20pt -->
|
||||||
|
```
|
||||||
|
Heading 1 **אינו** מציין cs fonts מפורשות, אבל יורש `cs="David"` מ-Normal.
|
||||||
|
זה מבדיל אותו מ-`Title`, שמפנה ל-theme ריק.
|
||||||
|
|
||||||
|
### `Heading 2` (styleId: `2`)
|
||||||
|
לכותרות מקטעים ותת-מקטעים.
|
||||||
|
```xml
|
||||||
|
<basedOn val="a1"/>
|
||||||
|
<pPr><keepNext/><spacing before="160"/><ind left="-567"/></pPr>
|
||||||
|
<rPr><b/><bCs/><u val="single"/></rPr> <!-- bold + underline -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Title` (styleId: `a5`) — ⚠️ הימנע
|
||||||
|
```xml
|
||||||
|
<rPr><rFonts asciiTheme="majorHAnsi" cstheme="majorBidi" .../></rPr>
|
||||||
|
```
|
||||||
|
מפנה ל-theme. ב-`theme1.xml`: `majorFont.cs = ""` (ריק). → עברית נופלת
|
||||||
|
ל-`majorFont.latin = "Aptos Display"`. **השתמש ב-Heading 1 במקום.**
|
||||||
|
|
||||||
|
### `Subtitle` (styleId: `a7`) — ⚠️ אותה בעיה של Title
|
||||||
|
לא בשימוש.
|
||||||
|
|
||||||
|
## סגנונות תוכן
|
||||||
|
|
||||||
|
### `Quote` (styleId: `a9`)
|
||||||
|
```xml
|
||||||
|
<basedOn val="a1"/>
|
||||||
|
<pPr><spacing before="0" after="0" line="276"/>
|
||||||
|
<ind left="680" right="170"/></pPr> <!-- הזחה דו-צדדית -->
|
||||||
|
<rPr><b/><bCs/></rPr> <!-- bold -->
|
||||||
|
```
|
||||||
|
לציטוטי פסיקה. הזחה פנימה, bold.
|
||||||
|
|
||||||
|
### `List Paragraph` (styleId: `a0`)
|
||||||
|
```xml
|
||||||
|
<basedOn val="a1"/>
|
||||||
|
<pPr>
|
||||||
|
<numPr><numId val="1"/></numPr> <!-- auto-numbering -->
|
||||||
|
<spacing after="0"/>
|
||||||
|
<ind left="-125" hanging="357"/>
|
||||||
|
</pPr>
|
||||||
|
<rPr><rFonts ascii="David" hAnsi="David"/><sz val="28"/></rPr> <!-- 14pt -->
|
||||||
|
```
|
||||||
|
**numId=1 מפנה ל-abstractNumId=16**, שמוגדר כ-`decimal` עם `lvlText="%1."`.
|
||||||
|
כלומר Word יוסיף "1.", "2.", "3." אוטומטית לפני הטקסט.
|
||||||
|
|
||||||
|
**חשוב:** `List Paragraph` הוא היחיד בטמפלט עם numbering אוטומטי. אין
|
||||||
|
bullet style מוכן. לרשימות עם (א)(ב), **הסר את ה-numPr** ברמת הפסקה:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
|
||||||
|
def _strip_numpr(paragraph):
|
||||||
|
pPr = paragraph._p.get_or_add_pPr()
|
||||||
|
for numPr in pPr.findall(qn("w:numPr")):
|
||||||
|
pPr.remove(numPr)
|
||||||
|
```
|
||||||
|
|
||||||
|
## סגנונות נוספים בטמפלט (לא בשימוש כרגע)
|
||||||
|
|
||||||
|
| styleId | שם | מה |
|
||||||
|
|---------|----|----|
|
||||||
|
| `P00`, `P11`, `P22` | תבניות מותאמות אישית | ישן, נראה שלא בשימוש פעיל |
|
||||||
|
| `12`, `21`, `31` | פיסקת רשימה 1/2/3 | ללא numPr — שימוש ויזואלי בלבד |
|
||||||
|
| `-` | פיסקת רשימה - ללא מספור | מיועד ל-bullets ללא מספור |
|
||||||
|
| `14` | ציטוט1 | וריאציה ישנה של Quote |
|
||||||
|
| `af2` / `af4` | header / footer | לכותרות עליונות/תחתונות |
|
||||||
|
|
||||||
|
## theme (`word/theme/theme1.xml`)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<majorFont>
|
||||||
|
<latin typeface="Aptos Display"/>
|
||||||
|
<cs typeface=""/> <!-- ריק! -->
|
||||||
|
</majorFont>
|
||||||
|
<minorFont>
|
||||||
|
<latin typeface="Aptos"/>
|
||||||
|
<cs typeface=""/> <!-- ריק! -->
|
||||||
|
</minorFont>
|
||||||
|
```
|
||||||
|
|
||||||
|
**ה-cs ריק ב-theme** — זו הסיבה ש-`Title` (שמפנה ל-theme) לא עובד
|
||||||
|
לעברית. אל תסמוך על theme; השתמש ב-styles שמגדירים cs מפורש (Normal +
|
||||||
|
כל מה שיורש ממנו).
|
||||||
|
|
||||||
|
## numbering definitions
|
||||||
|
|
||||||
|
`word/numbering.xml` כולל ~22 abstractNum מוגדרים. הרלוונטי לנו:
|
||||||
|
|
||||||
|
```
|
||||||
|
numId=1 → abstractNumId=16 → numFmt=decimal, lvlText="%1."
|
||||||
|
```
|
||||||
|
|
||||||
|
יש גם hebrew1 (`numFmt=hebrew1` = א., ב., ג.) ב-`abstractNumId=0, 1`.
|
||||||
|
אף סגנון מוכן לא מפנה אליהם. אם תרצה בעתיד רשימה ממוספרת בעברית עם
|
||||||
|
Word auto-numbering — יש להזריק `numPr` ידנית עם `numId=29` (שמפנה
|
||||||
|
ל-abstractNumId=0).
|
||||||
|
|
||||||
|
## גדלים — רפרנס מהיר
|
||||||
|
|
||||||
|
| style | ascii | cs (עברית) |
|
||||||
|
|-------|-------|-----------|
|
||||||
|
| Normal | 13pt | 13pt |
|
||||||
|
| Heading 1 | 20pt | 18pt |
|
||||||
|
| Heading 2 | 13pt (ירושה) + bold + underline | אותו דבר |
|
||||||
|
| Title | 28pt (Aptos Display — לא עברית!) | — |
|
||||||
|
| List Paragraph | 14pt (David) | 14pt |
|
||||||
|
| Quote | 13pt (ירושה) + bold | 13pt |
|
||||||
|
|
||||||
|
## טיפ
|
||||||
|
|
||||||
|
כדי לבדוק את כל הסגנונות שבמסמך:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from docx import Document
|
||||||
|
d = Document("skills/docx/decision_template.docx")
|
||||||
|
for s in sorted(d.styles, key=lambda x: x.name):
|
||||||
|
print(s.type, s.name, s.style_id)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user