1094 lines
46 KiB
Python
1094 lines
46 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Publication-Grade Document Compiler for LibreOffice Flat XML (.fodt) and PDF.
|
|
Milestone 3 - Retraining & Live Counting Documentation
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import re
|
|
import base64
|
|
import xml.etree.ElementTree as ET
|
|
from PIL import Image
|
|
|
|
WORKSPACE_ROOT = "/home/asus/feedmill/reTraining"
|
|
MD_PATH = os.path.join(WORKSPACE_ROOT, "docs/PANDUAN_SISTEM_LENGKAP.md")
|
|
FODT_PATH = os.path.join(WORKSPACE_ROOT, "docs/PANDUAN_SISTEM_LENGKAP.fodt")
|
|
PDF_PATH = os.path.join(WORKSPACE_ROOT, "docs/PANDUAN_SISTEM_LENGKAP.pdf")
|
|
|
|
def escape_xml(s):
|
|
if s is None:
|
|
return ""
|
|
return (s.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace('"', """)
|
|
.replace("'", "'"))
|
|
|
|
def escape_html(s):
|
|
if s is None:
|
|
return ""
|
|
return (s.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace('"', """))
|
|
|
|
def get_image_info(rel_path):
|
|
# Determine full path
|
|
if rel_path.startswith("docs/"):
|
|
full_path = os.path.join(WORKSPACE_ROOT, rel_path)
|
|
elif os.path.exists(os.path.join(WORKSPACE_ROOT, "docs", rel_path)):
|
|
full_path = os.path.join(WORKSPACE_ROOT, "docs", rel_path)
|
|
elif os.path.exists(os.path.join(WORKSPACE_ROOT, rel_path)):
|
|
full_path = os.path.join(WORKSPACE_ROOT, rel_path)
|
|
else:
|
|
raise FileNotFoundError(f"Image not found: {rel_path}")
|
|
|
|
with open(full_path, "rb") as f:
|
|
data = f.read()
|
|
b64_str = base64.b64encode(data).decode("ascii")
|
|
|
|
with Image.open(full_path) as img:
|
|
w_px, h_px = img.size
|
|
|
|
# Printable width in A4 is 17.0 cm (21.0 - 2.0 - 2.0)
|
|
aspect = w_px / h_px
|
|
if aspect > 1.7:
|
|
# Diagram (16:9)
|
|
width_cm = 16.5
|
|
height_cm = width_cm / aspect
|
|
else:
|
|
# 16:10 screenshots
|
|
width_cm = 15.8
|
|
height_cm = width_cm / aspect
|
|
|
|
return {
|
|
"full_path": full_path,
|
|
"base64": b64_str,
|
|
"width_px": w_px,
|
|
"height_px": h_px,
|
|
"width_cm": round(width_cm, 2),
|
|
"height_cm": round(height_cm, 2),
|
|
"data_uri": f"data:image/png;base64,{b64_str}"
|
|
}
|
|
|
|
def parse_inlines_to_odf(text):
|
|
# Tokenize inline formatting: code, bolditalic, bold, italic, links, math
|
|
pattern = re.compile(r"(`[^`]+`|\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|\$[^$]+\$)")
|
|
pos = 0
|
|
result = []
|
|
for m in pattern.finditer(text):
|
|
if m.start() > pos:
|
|
result.append(escape_xml(text[pos:m.start()]))
|
|
token = m.group(0)
|
|
if token.startswith("`") and token.endswith("`"):
|
|
code_text = escape_xml(token[1:-1])
|
|
result.append(f'<text:span text:style-name="T_Code">{code_text}</text:span>')
|
|
elif token.startswith("***") and token.endswith("***"):
|
|
t = escape_xml(token[3:-3])
|
|
result.append(f'<text:span text:style-name="T_BoldItalic">{t}</text:span>')
|
|
elif token.startswith("**") and token.endswith("**"):
|
|
t = escape_xml(token[2:-2])
|
|
result.append(f'<text:span text:style-name="T_Bold">{t}</text:span>')
|
|
elif token.startswith("*") and token.endswith("*"):
|
|
t = escape_xml(token[1:-1])
|
|
result.append(f'<text:span text:style-name="T_Italic">{t}</text:span>')
|
|
elif token.startswith("[") and "]" in token and "(" in token:
|
|
lm = re.match(r"\[([^\]]+)\]\(([^)]+)\)", token)
|
|
if lm:
|
|
lt, lu = lm.groups()
|
|
result.append(f'<text:a xlink:type="simple" xlink:href="{escape_xml(lu)}"><text:span text:style-name="T_Link">{escape_xml(lt)}</text:span></text:a>')
|
|
else:
|
|
result.append(escape_xml(token))
|
|
elif token.startswith("$") and token.endswith("$"):
|
|
t = escape_xml(token[1:-1])
|
|
result.append(f'<text:span text:style-name="T_Italic">{t}</text:span>')
|
|
else:
|
|
result.append(escape_xml(token))
|
|
pos = m.end()
|
|
if pos < len(text):
|
|
result.append(escape_xml(text[pos:]))
|
|
return "".join(result)
|
|
|
|
def parse_inlines_to_html(text):
|
|
text = escape_html(text)
|
|
# Inline code
|
|
text = re.sub(r"`([^`]+)`", r'<code class="inline-code">\1</code>', text)
|
|
# Bold italic
|
|
text = re.sub(r"\*\*\*([^*]+)\*\*\*", r"<strong><em>\1</em></strong>", text)
|
|
# Bold
|
|
text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", text)
|
|
# Italic
|
|
text = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", text)
|
|
# Links
|
|
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', text)
|
|
# Math var
|
|
text = re.sub(r"\$([^$]+)\$", r'<span class="math-var">\1</span>', text)
|
|
return text
|
|
|
|
def parse_markdown_blocks(content):
|
|
lines = content.split("\n")
|
|
blocks = []
|
|
i = 0
|
|
n = len(lines)
|
|
|
|
while i < n:
|
|
line = lines[i]
|
|
stripped = line.strip()
|
|
|
|
# Empty line
|
|
if not stripped:
|
|
i += 1
|
|
continue
|
|
|
|
# Horizontal Rule
|
|
if stripped in ["---", "***", "___"]:
|
|
blocks.append({"type": "hr"})
|
|
i += 1
|
|
continue
|
|
|
|
# Code block
|
|
if stripped.startswith("```"):
|
|
lang = stripped[3:].strip()
|
|
code_lines = []
|
|
i += 1
|
|
while i < n and not lines[i].strip().startswith("```"):
|
|
code_lines.append(lines[i])
|
|
i += 1
|
|
if i < n: # consume closing ```
|
|
i += 1
|
|
blocks.append({"type": "code", "lang": lang, "content": "\n".join(code_lines)})
|
|
continue
|
|
|
|
# Headings
|
|
if stripped.startswith("#"):
|
|
level = 0
|
|
while level < len(stripped) and stripped[level] == "#":
|
|
level += 1
|
|
title = stripped[level:].strip()
|
|
blocks.append({"type": "heading", "level": level, "text": title})
|
|
i += 1
|
|
continue
|
|
|
|
# Table
|
|
if stripped.startswith("|") and stripped.endswith("|"):
|
|
table_lines = []
|
|
while i < n and lines[i].strip().startswith("|") and lines[i].strip().endswith("|"):
|
|
table_lines.append(lines[i].strip())
|
|
i += 1
|
|
if len(table_lines) >= 2:
|
|
header = [c.strip() for c in table_lines[0].split("|")[1:-1]]
|
|
rows = []
|
|
for r in table_lines[2:]:
|
|
cols = [c.strip() for c in r.split("|")[1:-1]]
|
|
rows.append(cols)
|
|
blocks.append({"type": "table", "header": header, "rows": rows})
|
|
continue
|
|
|
|
# Image
|
|
img_match = re.match(r"^!\[(.*?)\]\((.*?)\)$", stripped)
|
|
if img_match:
|
|
alt, src = img_match.groups()
|
|
blocks.append({"type": "image", "alt": alt, "src": src})
|
|
i += 1
|
|
continue
|
|
|
|
# Numbered list: 1.
|
|
num_match = re.match(r"^(\d+)\.\s+(.*)$", stripped)
|
|
if num_match:
|
|
list_items = []
|
|
while i < n:
|
|
curr_stripped = lines[i].strip()
|
|
nm = re.match(r"^(\d+)\.\s+(.*)$", curr_stripped)
|
|
if nm:
|
|
list_items.append(nm.group(2))
|
|
i += 1
|
|
else:
|
|
break
|
|
blocks.append({"type": "ordered_list", "items": list_items})
|
|
continue
|
|
|
|
# Bullet list: - or *
|
|
bullet_match = re.match(r"^[-*]\s+(.*)$", stripped)
|
|
if bullet_match and not re.match(r"^\*[^*]+\*$", stripped):
|
|
bullet_items = []
|
|
while i < n:
|
|
curr_stripped = lines[i].strip()
|
|
bm = re.match(r"^[-*]\s+(.*)$", curr_stripped)
|
|
if bm and not re.match(r"^\*[^*]+\*$", curr_stripped):
|
|
bullet_items.append(bm.group(1))
|
|
i += 1
|
|
else:
|
|
break
|
|
blocks.append({"type": "unordered_list", "items": bullet_items})
|
|
continue
|
|
|
|
# Callout / Admonition (*Perhatian:...* or *Invarian penting:...*)
|
|
if stripped.startswith("*Perhatian:") or stripped.startswith("*Invarian penting:") or stripped.startswith("*Catatan:") or stripped.startswith("*Peringatan:"):
|
|
blocks.append({"type": "admonition", "text": stripped})
|
|
i += 1
|
|
continue
|
|
|
|
# Caption or italic paragraph under image (*Gambar X:...* or *Unduh format...*)
|
|
if stripped.startswith("*Gambar ") or stripped.startswith("*Unduh format vektor"):
|
|
blocks.append({"type": "caption", "text": stripped})
|
|
i += 1
|
|
continue
|
|
|
|
# Standard paragraph
|
|
p_lines = [stripped]
|
|
i += 1
|
|
while i < n:
|
|
next_line = lines[i]
|
|
next_stripped = next_line.strip()
|
|
if (not next_stripped or
|
|
next_stripped.startswith("#") or
|
|
next_stripped.startswith("```") or
|
|
next_stripped.startswith("|") or
|
|
next_stripped.startswith("---") or
|
|
next_stripped.startswith("![") or
|
|
re.match(r"^\d+\.\s+", next_stripped) or
|
|
re.match(r"^[-*]\s+", next_stripped) or
|
|
next_stripped.startswith("*Perhatian:") or
|
|
next_stripped.startswith("*Invarian penting:") or
|
|
next_stripped.startswith("*Gambar ") or
|
|
next_stripped.startswith("*Unduh format")):
|
|
break
|
|
p_lines.append(next_stripped)
|
|
i += 1
|
|
blocks.append({"type": "paragraph", "text": " ".join(p_lines)})
|
|
|
|
return blocks
|
|
|
|
print("Parser module loaded successfully.")
|
|
|
|
def generate_fodt(blocks):
|
|
print("Generating OASIS OpenDocument Text Flat XML (.fodt)...")
|
|
|
|
# Header
|
|
out = []
|
|
out.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
out.append('<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"')
|
|
out.append(' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"')
|
|
out.append(' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"')
|
|
out.append(' xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"')
|
|
out.append(' xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"')
|
|
out.append(' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"')
|
|
out.append(' xmlns:xlink="http://www.w3.org/1999/xlink"')
|
|
out.append(' xmlns:dc="http://purl.org/dc/elements/1.1/"')
|
|
out.append(' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"')
|
|
out.append(' xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"')
|
|
out.append(' xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"')
|
|
out.append(' office:version="1.3"')
|
|
out.append(' office:mimetype="application/vnd.oasis.opendocument.text">')
|
|
|
|
# Meta
|
|
out.append(' <office:meta>')
|
|
out.append(' <dc:title>PANDUAN SISTEM LENGKAP: RETRAINING, ANOTASI & LIVE COUNTING KARUNG KONVEYOR</dc:title>')
|
|
out.append(' <dc:description>Dokumentasi Arsitektur, Prosedur Operasional Standar, dan Panduan Referensi Teknis Produksi v4.2.0</dc:description>')
|
|
out.append(' <dc:language>id-ID</dc:language>')
|
|
out.append(' <dc:creator>Senior Computer Vision & Platform Engineer</dc:creator>')
|
|
out.append(' <meta:creation-date>2026-08-27T00:00:00Z</meta:creation-date>')
|
|
out.append(' </office:meta>')
|
|
|
|
# Font face declarations
|
|
out.append(' <office:font-face-decls>')
|
|
out.append(' <style:font-face style:name="Liberation Sans" svg:font-family="Liberation Sans, Inter, Arial, sans-serif"/>')
|
|
out.append(' <style:font-face style:name="Liberation Mono" svg:font-family="Liberation Mono, JetBrains Mono, Courier New, monospace"/>')
|
|
out.append(' </office:font-face-decls>')
|
|
|
|
# Styles
|
|
out.append(' <office:styles>')
|
|
# Default graphic
|
|
out.append(' <style:default-style style:family="graphic">')
|
|
out.append(' <style:graphic-properties svg:stroke-color="#cbd5e1" svg:stroke-width="0.5pt" draw:fill="none"/>')
|
|
out.append(' </style:default-style>')
|
|
# Default paragraph
|
|
out.append(' <style:default-style style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0.25cm" fo:line-height="120%" fo:text-align="justify"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="9.5pt" fo:color="#1e293b"/>')
|
|
out.append(' </style:default-style>')
|
|
# Default table
|
|
out.append(' <style:default-style style:family="table">')
|
|
out.append(' <style:table-properties table:border-model="collapsing"/>')
|
|
out.append(' </style:default-style>')
|
|
# Default table cell
|
|
out.append(' <style:default-style style:family="table-cell">')
|
|
out.append(' <style:table-cell-properties fo:padding="0.15cm" fo:border="0.5pt solid #cbd5e1"/>')
|
|
out.append(' </style:default-style>')
|
|
|
|
# Named Styles
|
|
out.append(' <style:style style:name="Title" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.4cm" fo:margin-bottom="0.25cm" fo:text-align="center"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="18pt" fo:font-weight="bold" fo:color="#0f172a"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Subtitle" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0.6cm" fo:text-align="center"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="11pt" fo:font-style="italic" fo:color="#475569"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Heading_20_1" style:display-name="Heading 1" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.8cm" fo:margin-bottom="0.3cm" fo:break-before="page" fo:keep-with-next="always"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="15pt" fo:font-weight="bold" fo:color="#0284c7"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Heading_20_2" style:display-name="Heading 2" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.5cm" fo:margin-bottom="0.2cm" fo:keep-with-next="always"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="12pt" fo:font-weight="bold" fo:color="#0f172a"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Heading_20_3" style:display-name="Heading 3" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.4cm" fo:margin-bottom="0.15cm" fo:keep-with-next="always"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="10.5pt" fo:font-weight="bold" fo:color="#334155"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Heading_20_4" style:display-name="Heading 4" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.3cm" fo:margin-bottom="0.1cm" fo:keep-with-next="always"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="9.5pt" fo:font-weight="bold" fo:color="#475569"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Standard" style:family="paragraph"/>')
|
|
|
|
out.append(' <style:style style:name="Body_20_Text" style:display-name="Body Text" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0.25cm" fo:line-height="120%" fo:text-align="justify"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="9.5pt" fo:color="#1e293b"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="CodeBlock" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.2cm" fo:margin-bottom="0.3cm" fo:background-color="#0f172a" fo:padding="0.25cm" fo:line-height="115%"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Mono" fo:font-size="8pt" fo:color="#f8fafc"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Admonition" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.25cm" fo:margin-bottom="0.35cm" fo:background-color="#f0f9ff" fo:border-left="3pt solid #0284c7" fo:border-top="none" fo:border-right="none" fo:border-bottom="none" fo:padding-left="0.3cm" fo:padding-top="0.2cm" fo:padding-bottom="0.2cm" fo:padding-right="0.2cm"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="9pt" fo:color="#0369a1"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="AdmonitionWarning" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.25cm" fo:margin-bottom="0.35cm" fo:background-color="#fef2f2" fo:border-left="3pt solid #ef4444" fo:border-top="none" fo:border-right="none" fo:border-bottom="none" fo:padding-left="0.3cm" fo:padding-top="0.2cm" fo:padding-bottom="0.2cm" fo:padding-right="0.2cm"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="9pt" fo:color="#991b1b"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Caption" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.15cm" fo:margin-bottom="0.4cm" fo:text-align="center"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="8.5pt" fo:font-style="italic" fo:color="#64748b"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Illustration" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0.3cm" fo:margin-bottom="0.1cm" fo:text-align="center" fo:keep-with-next="always"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="TableHeaderText" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0cm"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="8.5pt" fo:font-weight="bold" fo:color="#ffffff"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="TableBodyText" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0cm"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="8.5pt" fo:color="#1e293b"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="ListItemText" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0.12cm" fo:margin-left="0.6cm" fo:text-indent="-0.4cm"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="9.5pt" fo:color="#1e293b"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Header" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:text-align="right" fo:border-bottom="0.5pt solid #cbd5e1" fo:padding-bottom="0.1cm"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="8pt" fo:color="#64748b"/>')
|
|
out.append(' </style:style>')
|
|
|
|
out.append(' <style:style style:name="Footer" style:family="paragraph">')
|
|
out.append(' <style:paragraph-properties fo:text-align="center" fo:border-top="0.5pt solid #cbd5e1" fo:padding-top="0.1cm"/>')
|
|
out.append(' <style:text-properties style:font-name="Liberation Sans" fo:font-size="8pt" fo:color="#64748b"/>')
|
|
out.append(' </style:style>')
|
|
|
|
# List styles
|
|
out.append(' <text:list-style style:name="L_Bullet">')
|
|
out.append(' <text:list-level-style-bullet text:level="1" text:bullet-char="•">')
|
|
out.append(' <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">')
|
|
out.append(' <style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.6cm" fo:text-indent="-0.4cm" fo:margin-left="0.6cm"/>')
|
|
out.append(' </style:list-level-properties>')
|
|
out.append(' </text:list-level-style-bullet>')
|
|
out.append(' </text:list-style>')
|
|
|
|
out.append(' <text:list-style style:name="L_Number">')
|
|
out.append(' <text:list-level-style-number text:level="1" style:num-format="1" style:num-suffix=".">')
|
|
out.append(' <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">')
|
|
out.append(' <style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.6cm" fo:text-indent="-0.4cm" fo:margin-left="0.6cm"/>')
|
|
out.append(' </style:list-level-properties>')
|
|
out.append(' </text:list-level-style-number>')
|
|
out.append(' </text:list-style>')
|
|
out.append(' </office:styles>')
|
|
|
|
# Automatic styles
|
|
out.append(' <office:automatic-styles>')
|
|
# Page layout A4 (21.0 x 29.7 cm, 2.0 cm margins)
|
|
out.append(' <style:page-layout style:name="PM1">')
|
|
out.append(' <style:page-layout-properties fo:page-width="21.0cm" fo:page-height="29.7cm" style:num-format="1" style:print-orientation="portrait" fo:margin-top="2.0cm" fo:margin-bottom="2.0cm" fo:margin-left="2.0cm" fo:margin-right="2.0cm"/>')
|
|
out.append(' <style:header-style>')
|
|
out.append(' <style:header-footer-properties fo:min-height="0.6cm" fo:margin-bottom="0.4cm"/>')
|
|
out.append(' </style:header-style>')
|
|
out.append(' <style:footer-style>')
|
|
out.append(' <style:header-footer-properties fo:min-height="0.6cm" fo:margin-top="0.4cm"/>')
|
|
out.append(' </style:footer-style>')
|
|
out.append(' </style:page-layout>')
|
|
|
|
# Character styles
|
|
out.append(' <style:style style:name="T_Bold" style:family="text"><style:text-properties fo:font-weight="bold"/></style:style>')
|
|
out.append(' <style:style style:name="T_Italic" style:family="text"><style:text-properties fo:font-style="italic"/></style:style>')
|
|
out.append(' <style:style style:name="T_BoldItalic" style:family="text"><style:text-properties fo:font-weight="bold" fo:font-style="italic"/></style:style>')
|
|
out.append(' <style:style style:name="T_Code" style:family="text"><style:text-properties style:font-name="Liberation Mono" fo:font-size="8.5pt" fo:background-color="#f1f5f9" fo:color="#0f172a"/></style:style>')
|
|
out.append(' <style:style style:name="T_Link" style:family="text"><style:text-properties fo:color="#0284c7" fo:text-underline-style="solid"/></style:style>')
|
|
|
|
# Graphic Frame style
|
|
out.append(' <style:style style:name="FrameImage" style:family="graphic">')
|
|
out.append(' <style:graphic-properties style:horizontal-pos="center" style:horizontal-rel="paragraph" svg:stroke-color="#cbd5e1" svg:stroke-width="0.5pt" draw:fill="none" fo:margin-top="0.1cm" fo:margin-bottom="0.1cm"/>')
|
|
out.append(' </style:style>')
|
|
|
|
# Table styles
|
|
out.append(' <style:style style:name="TableAuto" style:family="table">')
|
|
out.append(' <style:table-properties style:width="17.0cm" table:align="center" fo:margin-top="0.2cm" fo:margin-bottom="0.35cm"/>')
|
|
out.append(' </style:style>')
|
|
out.append(' <style:style style:name="TableCellHeader" style:family="table-cell">')
|
|
out.append(' <style:table-cell-properties fo:background-color="#0f172a" fo:padding="0.15cm" fo:border="0.5pt solid #cbd5e1"/>')
|
|
out.append(' </style:style>')
|
|
out.append(' <style:style style:name="TableCellData" style:family="table-cell">')
|
|
out.append(' <style:table-cell-properties fo:background-color="#ffffff" fo:padding="0.15cm" fo:border="0.5pt solid #cbd5e1"/>')
|
|
out.append(' </style:style>')
|
|
out.append(' <style:style style:name="TableCellDataAlt" style:family="table-cell">')
|
|
out.append(' <style:table-cell-properties fo:background-color="#f8fafc" fo:padding="0.15cm" fo:border="0.5pt solid #cbd5e1"/>')
|
|
out.append(' </style:style>')
|
|
out.append(' </office:automatic-styles>')
|
|
|
|
# Master styles
|
|
out.append(' <office:master-styles>')
|
|
out.append(' <style:master-page style:name="Standard" style:page-layout-name="PM1">')
|
|
out.append(' <style:header>')
|
|
out.append(' <text:p text:style-name="Header">PANDUAN SISTEM LENGKAP — RETRAINING, ANOTASI & LIVE COUNTING</text:p>')
|
|
out.append(' </style:header>')
|
|
out.append(' <style:footer>')
|
|
out.append(' <text:p text:style-name="Footer">Dokumentasi Teknis Produksi v4.2.0 | Halaman <text:page-number text:select-page="current">1</text:page-number> dari <text:page-count>1</text:page-count></text:p>')
|
|
out.append(' </style:footer>')
|
|
out.append(' </style:master-page>')
|
|
out.append(' </office:master-styles>')
|
|
|
|
# Document Body
|
|
out.append(' <office:body>')
|
|
out.append(' <office:text>')
|
|
|
|
img_idx = 0
|
|
tbl_idx = 0
|
|
|
|
first_h1 = True
|
|
for block in blocks:
|
|
btype = block["type"]
|
|
|
|
if btype == "hr":
|
|
# Horizontal separator
|
|
out.append(' <text:p text:style-name="Body_20_Text"/>')
|
|
|
|
elif btype == "heading":
|
|
level = block["level"]
|
|
text = block["text"]
|
|
if level == 1:
|
|
# Check if it is Document Title or Chapter
|
|
if first_h1 and "PANDUAN SISTEM LENGKAP" in text:
|
|
out.append(f' <text:p text:style-name="Title">{parse_inlines_to_odf(text)}</text:p>')
|
|
first_h1 = False
|
|
else:
|
|
out.append(f' <text:h text:outline-level="1" text:style-name="Heading_20_1">{parse_inlines_to_odf(text)}</text:h>')
|
|
elif level == 2:
|
|
out.append(f' <text:h text:outline-level="2" text:style-name="Heading_20_2">{parse_inlines_to_odf(text)}</text:h>')
|
|
elif level == 3:
|
|
out.append(f' <text:h text:outline-level="3" text:style-name="Heading_20_3">{parse_inlines_to_odf(text)}</text:h>')
|
|
else:
|
|
out.append(f' <text:h text:outline-level="4" text:style-name="Heading_20_4">{parse_inlines_to_odf(text)}</text:h>')
|
|
|
|
elif btype == "paragraph":
|
|
text = block["text"]
|
|
if text.startswith("**Dokumentasi Arsitektur"):
|
|
out.append(f' <text:p text:style-name="Subtitle">{parse_inlines_to_odf(text)}</text:p>')
|
|
else:
|
|
out.append(f' <text:p text:style-name="Body_20_Text">{parse_inlines_to_odf(text)}</text:p>')
|
|
|
|
elif btype == "admonition":
|
|
text = block["text"]
|
|
style = "AdmonitionWarning" if "Perhatian" in text or "Peringatan" in text else "Admonition"
|
|
out.append(f' <text:p text:style-name="{style}">{parse_inlines_to_odf(text)}</text:p>')
|
|
|
|
elif btype == "code":
|
|
code_text = block["content"]
|
|
lines = code_text.split("\n")
|
|
while lines and not lines[-1].strip():
|
|
lines.pop()
|
|
xml_lines = []
|
|
for cline in lines:
|
|
escaped = escape_xml(cline)
|
|
def replace_spaces(match):
|
|
count = len(match.group(0))
|
|
return f'<text:s text:c="{count}"/>'
|
|
processed_line = re.sub(r" {2,}", replace_spaces, escaped)
|
|
xml_lines.append(processed_line)
|
|
inner = "<text:line-break/>".join(xml_lines)
|
|
out.append(f' <text:p text:style-name="CodeBlock">{inner}</text:p>')
|
|
|
|
elif btype == "image":
|
|
img_idx += 1
|
|
src = block["src"]
|
|
alt = block["alt"]
|
|
img_info = get_image_info(src)
|
|
w_cm = img_info["width_cm"]
|
|
h_cm = img_info["height_cm"]
|
|
b64 = img_info["base64"]
|
|
out.append(' <text:p text:style-name="Illustration">')
|
|
out.append(f' <draw:frame draw:style-name="FrameImage" draw:name="Img_{img_idx}" text:anchor-type="paragraph" svg:width="{w_cm}cm" svg:height="{h_cm}cm" draw:z-index="0">')
|
|
out.append(' <draw:image>')
|
|
out.append(f' <office:binary-data>{b64}</office:binary-data>')
|
|
out.append(' </draw:image>')
|
|
out.append(' </draw:frame>')
|
|
out.append(' </text:p>')
|
|
|
|
elif btype == "caption":
|
|
text = block["text"]
|
|
out.append(f' <text:p text:style-name="Caption">{parse_inlines_to_odf(text)}</text:p>')
|
|
|
|
elif btype == "table":
|
|
tbl_idx += 1
|
|
header = block["header"]
|
|
rows = block["rows"]
|
|
cols_count = len(header)
|
|
out.append(f' <table:table table:name="Table_{tbl_idx}" table:style-name="TableAuto">')
|
|
out.append(f' <table:table-column table:number-columns-repeated="{cols_count}"/>')
|
|
out.append(' <table:table-header-rows>')
|
|
out.append(' <table:table-row>')
|
|
for cell in header:
|
|
out.append(' <table:table-cell table:style-name="TableCellHeader" office:value-type="string">')
|
|
out.append(f' <text:p text:style-name="TableHeaderText">{parse_inlines_to_odf(cell)}</text:p>')
|
|
out.append(' </table:table-cell>')
|
|
out.append(' </table:table-row>')
|
|
out.append(' </table:table-header-rows>')
|
|
for r_idx, row in enumerate(rows):
|
|
cell_style = "TableCellDataAlt" if r_idx % 2 == 1 else "TableCellData"
|
|
out.append(' <table:table-row>')
|
|
for cell in row:
|
|
out.append(f' <table:table-cell table:style-name="{cell_style}" office:value-type="string">')
|
|
out.append(f' <text:p text:style-name="TableBodyText">{parse_inlines_to_odf(cell)}</text:p>')
|
|
out.append(' </table:table-cell>')
|
|
out.append(' </table:table-row>')
|
|
out.append(' </table:table>')
|
|
|
|
elif btype == "ordered_list":
|
|
items = block["items"]
|
|
out.append(' <text:list text:style-name="L_Number">')
|
|
for item in items:
|
|
out.append(' <text:list-item>')
|
|
out.append(f' <text:p text:style-name="ListItemText">{parse_inlines_to_odf(item)}</text:p>')
|
|
out.append(' </text:list-item>')
|
|
out.append(' </text:list>')
|
|
|
|
elif btype == "unordered_list":
|
|
items = block["items"]
|
|
out.append(' <text:list text:style-name="L_Bullet">')
|
|
for item in items:
|
|
out.append(' <text:list-item>')
|
|
out.append(f' <text:p text:style-name="ListItemText">{parse_inlines_to_odf(item)}</text:p>')
|
|
out.append(' </text:list-item>')
|
|
out.append(' </text:list>')
|
|
|
|
out.append(' </office:text>')
|
|
out.append(' </office:body>')
|
|
out.append('</office:document>')
|
|
|
|
fodt_content = "\n".join(out)
|
|
with open(FODT_PATH, "w", encoding="utf-8") as f:
|
|
f.write(fodt_content)
|
|
print(f"FODT successfully written to {FODT_PATH} ({os.path.getsize(FODT_PATH)} bytes)")
|
|
|
|
|
|
def generate_html_and_pdf(blocks):
|
|
print("Generating Publication-Grade HTML and compiling to PDF via Playwright...")
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
html = []
|
|
html.append('<!DOCTYPE html>')
|
|
html.append('<html lang="id">')
|
|
html.append('<head>')
|
|
html.append(' <meta charset="UTF-8">')
|
|
html.append(' <title>PANDUAN SISTEM LENGKAP: RETRAINING, ANOTASI & LIVE COUNTING KARUNG KONVEYOR</title>')
|
|
html.append(' <style>')
|
|
html.append("""
|
|
@page {
|
|
size: A4 portrait;
|
|
margin: 20mm 18mm 22mm 18mm;
|
|
}
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", sans-serif;
|
|
font-size: 9.5pt;
|
|
line-height: 1.45;
|
|
color: #1e293b;
|
|
background: #ffffff;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.cover-container {
|
|
padding-top: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.cover-top-bar {
|
|
height: 5px;
|
|
background: linear-gradient(90deg, #0284c7 0%, #0369a1 50%, #0f172a 100%);
|
|
border-radius: 3px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.doc-title {
|
|
font-size: 18pt;
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
line-height: 1.25;
|
|
margin: 0 0 6px 0;
|
|
letter-spacing: -0.3px;
|
|
text-align: left;
|
|
}
|
|
.doc-subtitle {
|
|
font-size: 10.5pt;
|
|
font-weight: 500;
|
|
color: #475569;
|
|
margin: 0 0 16px 0;
|
|
line-height: 1.35;
|
|
}
|
|
.meta-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 8px;
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 6px;
|
|
padding: 10px 14px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.meta-item {
|
|
font-size: 8.5pt;
|
|
color: #334155;
|
|
}
|
|
.meta-label {
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
.toc-card {
|
|
background: #ffffff;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 6px;
|
|
padding: 12px 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.toc-title {
|
|
font-size: 11pt;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
margin: 0 0 8px 0;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
padding-bottom: 4px;
|
|
}
|
|
.toc-list {
|
|
margin: 0;
|
|
padding-left: 18px;
|
|
font-size: 8.5pt;
|
|
color: #0284c7;
|
|
line-height: 1.4;
|
|
}
|
|
.toc-list li {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
/* Chapter and Headings */
|
|
.chapter-title {
|
|
page-break-before: always;
|
|
break-before: page;
|
|
font-size: 15pt;
|
|
font-weight: 800;
|
|
color: #0284c7;
|
|
border-bottom: 2px solid #0284c7;
|
|
padding-bottom: 4px;
|
|
margin-top: 0;
|
|
margin-bottom: 12px;
|
|
letter-spacing: -0.2px;
|
|
}
|
|
h2.section-title {
|
|
font-size: 12pt;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
margin-top: 16px;
|
|
margin-bottom: 8px;
|
|
break-after: avoid;
|
|
page-break-after: avoid;
|
|
border-left: 3px solid #0284c7;
|
|
padding-left: 8px;
|
|
}
|
|
h3.subsection-title {
|
|
font-size: 10.5pt;
|
|
font-weight: 700;
|
|
color: #334155;
|
|
margin-top: 12px;
|
|
margin-bottom: 6px;
|
|
break-after: avoid;
|
|
page-break-after: avoid;
|
|
}
|
|
h4.sub-subsection-title {
|
|
font-size: 9.5pt;
|
|
font-weight: 700;
|
|
color: #475569;
|
|
margin-top: 10px;
|
|
margin-bottom: 4px;
|
|
break-after: avoid;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
p {
|
|
margin-top: 0;
|
|
margin-bottom: 8px;
|
|
text-align: justify;
|
|
}
|
|
|
|
/* Code Block */
|
|
pre.code-block {
|
|
background-color: #0f172a;
|
|
color: #f8fafc;
|
|
font-family: 'Liberation Mono', 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
font-size: 8pt;
|
|
line-height: 1.35;
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid #1e293b;
|
|
margin: 10px 0 12px 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
code.inline-code {
|
|
background-color: #f1f5f9;
|
|
color: #0f172a;
|
|
font-family: 'Liberation Mono', 'JetBrains Mono', Consolas, monospace;
|
|
font-size: 8.5pt;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
.math-var {
|
|
font-style: italic;
|
|
font-family: "Times New Roman", serif;
|
|
font-weight: 600;
|
|
color: #0f172a;
|
|
}
|
|
|
|
/* Admonitions */
|
|
.admonition-info {
|
|
background-color: #f0f9ff;
|
|
border-left: 4px solid #0284c7;
|
|
color: #0369a1;
|
|
padding: 8px 12px;
|
|
border-radius: 0 6px 6px 0;
|
|
margin: 10px 0;
|
|
font-size: 9pt;
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
.admonition-warning {
|
|
background-color: #fef2f2;
|
|
border-left: 4px solid #ef4444;
|
|
color: #991b1b;
|
|
padding: 8px 12px;
|
|
border-radius: 0 6px 6px 0;
|
|
margin: 10px 0;
|
|
font-size: 9pt;
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
.admonition-tip {
|
|
background-color: #f8fafc;
|
|
border-left: 4px solid #0ea5e9;
|
|
color: #0f172a;
|
|
padding: 8px 12px;
|
|
border-radius: 0 6px 6px 0;
|
|
margin: 10px 0;
|
|
font-size: 9pt;
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
|
|
/* Tables */
|
|
table.doc-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 10px 0 14px 0;
|
|
font-size: 8.5pt;
|
|
page-break-inside: auto;
|
|
}
|
|
table.doc-table thead {
|
|
background-color: #0f172a;
|
|
color: #ffffff;
|
|
display: table-header-group;
|
|
}
|
|
table.doc-table th {
|
|
padding: 6px 8px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
border: 1px solid #cbd5e1;
|
|
}
|
|
table.doc-table td {
|
|
padding: 5px 8px;
|
|
border: 1px solid #cbd5e1;
|
|
line-height: 1.35;
|
|
}
|
|
table.doc-table tbody tr:nth-child(even) {
|
|
background-color: #f8fafc;
|
|
}
|
|
table.doc-table tbody tr:nth-child(odd) {
|
|
background-color: #ffffff;
|
|
}
|
|
table.doc-table tr {
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
|
|
/* Figures */
|
|
.figure-container {
|
|
text-align: center;
|
|
margin: 12px 0 8px 0;
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
.figure-img {
|
|
max-width: 98%;
|
|
height: auto;
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 6px;
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
|
display: block;
|
|
margin: 0 auto;
|
|
}
|
|
.figure-caption {
|
|
font-size: 8.5pt;
|
|
font-style: italic;
|
|
color: #64748b;
|
|
text-align: center;
|
|
margin-top: 5px;
|
|
margin-bottom: 12px;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
/* Lists */
|
|
ul, ol {
|
|
margin-top: 0;
|
|
margin-bottom: 8px;
|
|
padding-left: 20px;
|
|
}
|
|
li {
|
|
margin-bottom: 3px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
a {
|
|
color: #0284c7;
|
|
text-decoration: none;
|
|
}
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
""")
|
|
html.append(' </style>')
|
|
html.append('</head>')
|
|
html.append('<body>')
|
|
|
|
first_h1 = True
|
|
in_meta_section = False
|
|
|
|
for block in blocks:
|
|
btype = block["type"]
|
|
|
|
if btype == "hr":
|
|
if in_meta_section:
|
|
in_meta_section = False
|
|
|
|
elif btype == "heading":
|
|
level = block["level"]
|
|
text = block["text"]
|
|
if level == 1:
|
|
if first_h1 and "PANDUAN SISTEM LENGKAP" in text:
|
|
html.append('<div class="cover-container">')
|
|
html.append(' <div class="cover-top-bar"></div>')
|
|
html.append(f' <h1 class="doc-title">{parse_inlines_to_html(text)}</h1>')
|
|
first_h1 = False
|
|
else:
|
|
html.append(f'<h1 class="chapter-title">{parse_inlines_to_html(text)}</h1>')
|
|
elif level == 2:
|
|
if text == "Daftar Isi":
|
|
html.append('<div class="toc-card">')
|
|
html.append(' <div class="toc-title">Daftar Isi</div>')
|
|
else:
|
|
html.append(f'<h2 class="section-title">{parse_inlines_to_html(text)}</h2>')
|
|
elif level == 3:
|
|
if text == "Informasi Dokumen":
|
|
in_meta_section = True
|
|
html.append('<div class="meta-grid">')
|
|
else:
|
|
html.append(f'<h3 class="subsection-title">{parse_inlines_to_html(text)}</h3>')
|
|
else:
|
|
html.append(f'<h4 class="sub-subsection-title">{parse_inlines_to_html(text)}</h4>')
|
|
|
|
elif btype == "paragraph":
|
|
text = block["text"]
|
|
if text.startswith("**Dokumentasi Arsitektur"):
|
|
html.append(f'<p class="doc-subtitle">{parse_inlines_to_html(text)}</p>')
|
|
else:
|
|
html.append(f'<p>{parse_inlines_to_html(text)}</p>')
|
|
|
|
elif btype == "admonition":
|
|
text = block["text"]
|
|
cls = "admonition-warning" if ("Perhatian" in text or "Peringatan" in text) else "admonition-info"
|
|
html.append(f'<div class="{cls}">{parse_inlines_to_html(text)}</div>')
|
|
|
|
elif btype == "code":
|
|
code_text = block["content"]
|
|
html.append(f'<pre class="code-block"><code>{escape_html(code_text)}</code></pre>')
|
|
|
|
elif btype == "image":
|
|
src = block["src"]
|
|
alt = block["alt"]
|
|
img_info = get_image_info(src)
|
|
data_uri = img_info["data_uri"]
|
|
html.append('<div class="figure-container">')
|
|
html.append(f' <img class="figure-img" src="{data_uri}" alt="{escape_html(alt)}">')
|
|
html.append('</div>')
|
|
|
|
elif btype == "caption":
|
|
text = block["text"]
|
|
html.append(f'<div class="figure-caption">{parse_inlines_to_html(text)}</div>')
|
|
|
|
elif btype == "table":
|
|
header = block["header"]
|
|
rows = block["rows"]
|
|
html.append('<table class="doc-table">')
|
|
html.append(' <thead><tr>')
|
|
for cell in header:
|
|
html.append(f' <th>{parse_inlines_to_html(cell)}</th>')
|
|
html.append(' </tr></thead>')
|
|
html.append(' <tbody>')
|
|
for row in rows:
|
|
html.append(' <tr>')
|
|
for cell in row:
|
|
html.append(f' <td>{parse_inlines_to_html(cell)}</td>')
|
|
html.append(' </tr>')
|
|
html.append(' </tbody>')
|
|
html.append('</table>')
|
|
|
|
elif btype == "ordered_list":
|
|
items = block["items"]
|
|
# Check if this is the Table of Contents list
|
|
# If so, close toc-card
|
|
html.append('<ol class="toc-list" style="margin-bottom: 0;">' if "Bab 1:" in items[0] else '<ol>')
|
|
for item in items:
|
|
html.append(f' <li>{parse_inlines_to_html(item)}</li>')
|
|
html.append('</ol>')
|
|
if "Bab 1:" in items[0]:
|
|
html.append('</div>') # close toc-card
|
|
html.append('</div>') # close cover-container
|
|
|
|
elif btype == "unordered_list":
|
|
items = block["items"]
|
|
if in_meta_section:
|
|
# Format as meta cards
|
|
for item in items:
|
|
parts = item.split(":", 1)
|
|
if len(parts) == 2:
|
|
lbl, val = parts
|
|
html.append(f' <div class="meta-item"><span class="meta-label">{parse_inlines_to_html(lbl.strip("* "))}:</span> {parse_inlines_to_html(val.strip())}</div>')
|
|
else:
|
|
html.append(f' <div class="meta-item">{parse_inlines_to_html(item)}</div>')
|
|
html.append('</div>') # close meta-grid
|
|
in_meta_section = False
|
|
else:
|
|
html.append('<ul>')
|
|
for item in items:
|
|
html.append(f' <li>{parse_inlines_to_html(item)}</li>')
|
|
html.append('</ul>')
|
|
|
|
html.append('</body>')
|
|
html.append('</html>')
|
|
|
|
html_content = "\n".join(html)
|
|
|
|
# Save debug html
|
|
debug_html_path = os.path.join(WORKSPACE_ROOT, "docs/panduan_debug.html")
|
|
with open(debug_html_path, "w", encoding="utf-8") as f:
|
|
f.write(html_content)
|
|
print(f"Debug HTML written to {debug_html_path}")
|
|
|
|
# Compile with Playwright
|
|
header_tpl = (
|
|
'<div style="font-size: 7.5pt; color: #64748b; width: 100%; text-align: right; '
|
|
'padding: 0 18mm; font-family: Inter, \'Liberation Sans\', sans-serif; '
|
|
'border-bottom: 1px solid #e2e8f0; padding-bottom: 3px;">'
|
|
'PANDUAN SISTEM LENGKAP: RETRAINING, ANOTASI & LIVE COUNTING KARUNG KONVEYOR'
|
|
'</div>'
|
|
)
|
|
|
|
footer_tpl = (
|
|
'<div style="font-size: 7.5pt; color: #64748b; width: 100%; display: flex; '
|
|
'justify-content: space-between; padding: 0 18mm; font-family: Inter, \'Liberation Sans\', sans-serif; '
|
|
'border-top: 1px solid #e2e8f0; padding-top: 3px;">'
|
|
'<span>Dokumentasi Teknis Produksi v4.2.0</span>'
|
|
'<span>Halaman <span class="pageNumber"></span> dari <span class="totalPages"></span></span>'
|
|
'</div>'
|
|
)
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch()
|
|
page = browser.new_page()
|
|
page.set_content(html_content, wait_until="networkidle")
|
|
page.pdf(
|
|
path=PDF_PATH,
|
|
format="A4",
|
|
print_background=True,
|
|
display_header_footer=True,
|
|
header_template=header_tpl,
|
|
footer_template=footer_tpl,
|
|
margin={
|
|
"top": "22mm",
|
|
"bottom": "22mm",
|
|
"left": "18mm",
|
|
"right": "18mm"
|
|
}
|
|
)
|
|
browser.close()
|
|
|
|
print(f"PDF successfully compiled to {PDF_PATH} ({os.path.getsize(PDF_PATH)} bytes)")
|
|
|
|
|
|
def main():
|
|
print("=== STARTING FULL PUBLICATION BUILD ===")
|
|
if not os.path.exists(MD_PATH):
|
|
raise FileNotFoundError(f"Source markdown not found: {MD_PATH}")
|
|
|
|
with open(MD_PATH, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
blocks = parse_markdown_blocks(content)
|
|
print(f"Parsed {len(blocks)} markdown blocks.")
|
|
|
|
# 1. Generate FODT
|
|
generate_fodt(blocks)
|
|
|
|
# 2. Generate PDF
|
|
generate_html_and_pdf(blocks)
|
|
|
|
print("=== BUILD COMPLETED SUCCESSFULLY ===")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|