Add dafna-decision-template skill — knowledge for template-based DOCX export
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:
2026-04-16 18:57:57 +00:00
parent 726498126d
commit bfec8bdaa3
5 changed files with 701 additions and 0 deletions

View 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, מקטעי רקע. |

View 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.

View 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) וראה שהתוצאה נכונה.

View 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/>` נמצא שם **במפורש**. אנחנו מחקים את זה.

View 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)
```