#!/usr/bin/env node /** * create-legal-doc.js — תבנית בסיסית למסמך משפטי בעברית * * שימוש: העתק לתיקיית העבודה, ערוך את CONTENT, הרץ עם node. * * הסקריפט כולל את כל הגדרות ה-RTL הנדרשות ב-5 רמות: * 1. Document defaults (styles) * 2. Section properties (bidi: true) * 3. Per-paragraph (bidirectional + alignment) * 4. Per-run (rightToLeft + font) * 5. Numbering (alignment: START, not RIGHT!) * * שינוי פונט: החלף "David" ב-"FrankRuehl" או "Miriam" * שינוי שוליים: שנה MARGIN_CM * * v3.0 — תיקון באג: AlignmentType.START במקום RIGHT במספור */ const fs = require('fs'); const { Document, Packer, Paragraph, TextRun, Header, Footer, AlignmentType, HeadingLevel, PageNumber, LevelFormat, Table, TableRow, TableCell, WidthType, BorderStyle, LineRuleType } = require('docx'); // ═══════════════════════════════════════════════ // CONFIGURATION — שנה כאן // ═══════════════════════════════════════════════ const FONT = "David"; const FONT_SIZE = 24; // half-points (24 = 12pt) const HEADING1_SIZE = 32; // 16pt const HEADING2_SIZE = 28; // 14pt const MARGIN_CM = 2.5; const MARGIN_DXA = Math.round(MARGIN_CM / 2.54 * 1440); const OUTPUT_FILE = "legal-document.docx"; const HEADER_TEXT = ""; // שם המשרד — השאר ריק אם לא צריך const FOOTER_CONFIDENTIAL = ""; // ═══════════════════════════════════════════════ // HELPERS — פונקציות עזר // ═══════════════════════════════════════════════ // TextRun עם RTL const rtlRun = (text, opts = {}) => new TextRun({ text, ...opts, font: opts.font || FONT, size: opts.size || FONT_SIZE, rightToLeft: true, // ← חובה! תמיד אחרון — לא ניתן לדריסה }); // Paragraph עם RTL const rtlPara = (children, opts = {}) => new Paragraph({ bidirectional: true, // ← חובה! alignment: opts.alignment || AlignmentType.BOTH, // ✅ BOTH, לא JUSTIFIED spacing: opts.spacing, children: Array.isArray(children) ? children : [children], ...(opts.heading ? { heading: opts.heading } : {}), ...(opts.numbering ? { numbering: opts.numbering } : {}), }); // כותרת ראשית const heading1 = (text) => rtlPara( rtlRun(text, { bold: true, size: HEADING1_SIZE }), { heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER } ); // כותרת משנה const heading2 = (text) => rtlPara( rtlRun(text, { bold: true, size: HEADING2_SIZE }), { heading: HeadingLevel.HEADING_2, alignment: AlignmentType.START } // ✅ START ); // רווח const spacer = (after = 200) => rtlPara(rtlRun(""), { spacing: { after } }); // ═══════════════════════════════════════════════ // CONTENT — ערוך כאן את תוכן המסמך // ═══════════════════════════════════════════════ const CONTENT = [ // כותרת heading1("הסכם שירותים משפטיים"), // תאריך rtlPara(rtlRun("נערך ונחתם בתל אביב ביום ___________"), { spacing: { after: 200 } }), // צדדים heading2("בין הצדדים"), rtlPara([ rtlRun("מצד אחד: ", { bold: true }), rtlRun('[שם], ח.פ./ת.ז. [מספר], מ[כתובת] (להלן: "'), rtlRun("המזמין", { bold: true }), rtlRun('")'), ], { spacing: { after: 120 } }), rtlPara([ rtlRun("מצד שני: ", { bold: true }), rtlRun('[שם], ח.פ./ת.ז. [מספר], מ[כתובת] (להלן: "'), rtlRun("הספק", { bold: true }), rtlRun('")'), ], { spacing: { after: 200 } }), // הואילים heading2("הואיל:"), rtlPara(rtlRun("1. והואיל ו[תנאי ראשון];"), { spacing: { after: 120 } }), rtlPara(rtlRun("2. והואיל ו[תנאי שני];"), { spacing: { after: 120 } }), rtlPara(rtlRun("3. והואיל והצדדים מעוניינים להסדיר את תנאי ההתקשרות ביניהם;"), { spacing: { after: 200 } }), // מעבר rtlPara(rtlRun("לפיכך הוסכם, הותנה והוצהר בין הצדדים כדלקמן:", { bold: true }), { alignment: AlignmentType.CENTER, spacing: { before: 200, after: 200 } }), // סעיפים heading2("1. היקף השירותים"), rtlPara(rtlRun("1.1 [תוכן הסעיף]"), { spacing: { after: 120 } }), rtlPara(rtlRun("1.2 [תוכן הסעיף]"), { spacing: { after: 120 } }), heading2("2. התמורה"), rtlPara(rtlRun("2.1 [תוכן הסעיף]"), { spacing: { after: 120 } }), // חתימות rtlPara(rtlRun("ולראיה באו הצדדים על החתום:", { bold: true }), { alignment: AlignmentType.CENTER, spacing: { before: 600, after: 400 } }), // טבלת חתימות new Table({ visuallyRightToLeft: true, // ✅ קריטי! width: { size: 9072, type: WidthType.DXA }, columnWidths: [4536, 4536], rows: [ new TableRow({ children: [ new TableCell({ borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.NONE }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } }, children: [ rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER }), rtlPara(rtlRun("המזמין"), { alignment: AlignmentType.CENTER }) ] }), new TableCell({ borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.NONE }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } }, children: [ rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER }), rtlPara(rtlRun("הספק"), { alignment: AlignmentType.CENTER }) ] }) ] }) ] }) ]; // ═══════════════════════════════════════════════ // DOCUMENT GENERATION — לא לשנות (אלא אם יודע מה עושים) // ═══════════════════════════════════════════════ const doc = new Document({ // רמה 1: Document defaults styles: { default: { document: { run: { font: FONT, size: FONT_SIZE, rightToLeft: true }, paragraph: { bidirectional: true, alignment: AlignmentType.BOTH } } }, paragraphStyles: [ { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: HEADING1_SIZE, bold: true, font: FONT, rightToLeft: true }, paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0, bidirectional: true, alignment: AlignmentType.CENTER } }, { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: HEADING2_SIZE, bold: true, font: FONT, rightToLeft: true }, paragraph: { spacing: { before: 200, after: 200 }, outlineLevel: 1, bidirectional: true, alignment: AlignmentType.START } // ✅ START }, ] }, // מספור סעיפים numbering: { config: [{ reference: "legal-clauses", levels: [ { level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.START, // ✅ START — לא RIGHT! suffix: "tab", style: { paragraph: { indent: { left: 720, hanging: 360 } } } }, { level: 1, format: LevelFormat.DECIMAL, text: "%1.%2", alignment: AlignmentType.START, // ✅ START — לא RIGHT! suffix: "tab", style: { paragraph: { indent: { left: 1440, hanging: 500 } } } }, ] }] }, sections: [{ // רמה 2: Section properties properties: { page: { size: { width: 11906, height: 16838 }, // A4 margin: { top: MARGIN_DXA, right: MARGIN_DXA, bottom: MARGIN_DXA, left: MARGIN_DXA } }, bidi: true, // ← חובה! }, headers: HEADER_TEXT ? { default: new Header({ children: [rtlPara(rtlRun(HEADER_TEXT, { bold: true, size: 20 }), { alignment: AlignmentType.CENTER })] }) } : undefined, footers: { default: new Footer({ children: [new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER, children: [ rtlRun("עמוד ", { size: 18 }), new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18 }), ...(FOOTER_CONFIDENTIAL ? [rtlRun(` | ${FOOTER_CONFIDENTIAL}`, { size: 18 })] : []), ] })] }) }, children: CONTENT }] }); Packer.toBuffer(doc).then(buffer => { fs.writeFileSync(OUTPUT_FILE, buffer); console.log(`✅ ${OUTPUT_FILE} created successfully`); console.log(` Font: ${FONT} ${FONT_SIZE/2}pt`); console.log(` Margins: ${MARGIN_CM}cm (${MARGIN_DXA} DXA)`); console.log(` Size: ${(buffer.length / 1024).toFixed(1)} KB`); console.log(` RTL: bidi + bidirectional + rightToLeft ✓`); console.log(` Alignment: START/END (not LEFT/RIGHT) ✓`); });