"""Генерира docs/razdeljane-na-sekcii.pdf от съседния .md (кирилица, Calibri).""" from __future__ import annotations import html import re from pathlib import Path from reportlab.lib.colors import HexColor from reportlab.lib.enums import TA_JUSTIFY, TA_LEFT from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle from reportlab.lib.units import mm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus import ( HRFlowable, ListFlowable, ListItem, Paragraph, Preformatted, SimpleDocTemplate, Spacer, Table, TableStyle, ) HERE = Path(__file__).resolve().parent MD_PATH = HERE / "razdeljane-na-sekcii.md" PDF_PATH = HERE / "razdeljane-na-sekcii.pdf" FONT_DIR = Path(r"C:\Windows\Fonts") BODY = "Calibri" MONO = "ConsolasDoc" def _register_fonts() -> None: pairs = [ (BODY, "", "calibri.ttf"), (BODY, "Bold", "calibrib.ttf"), (BODY, "Italic", "calibrii.ttf"), (BODY, "BoldItalic", "calibriz.ttf"), ] for family, face, fname in pairs: path = FONT_DIR / fname if not path.is_file(): raise SystemExit(f"Липсва шрифт: {path}") name = family if not face else f"{family}-{face}" pdfmetrics.registerFont(TTFont(name, str(path))) pdfmetrics.registerFontFamily( BODY, normal=BODY, bold=f"{BODY}-Bold", italic=f"{BODY}-Italic", boldItalic=f"{BODY}-BoldItalic", ) consola = FONT_DIR / "consola.ttf" if consola.is_file(): pdfmetrics.registerFont(TTFont(MONO, str(consola))) else: pdfmetrics.registerFont(TTFont(MONO, str(FONT_DIR / "arial.ttf"))) def _inline(text: str) -> str: parts: list[str] = [] i = 0 code_re = re.compile(r"`([^`]+)`") bold_re = re.compile(r"\*\*([^*]+)\*\*") while i < len(text): m_code = code_re.search(text, i) m_bold = bold_re.search(text, i) candidates = [m for m in (m_code, m_bold) if m] if not candidates: parts.append(html.escape(text[i:])) break m = min(candidates, key=lambda x: x.start()) parts.append(html.escape(text[i:m.start()])) if m is m_code: inner = html.escape(m.group(1)) parts.append(f'{inner}') else: parts.append(f"{html.escape(m.group(1))}") i = m.end() return "".join(parts) def _styles() -> dict[str, ParagraphStyle]: accent = HexColor("#1e4a8c") muted = HexColor("#4a4a58") text = HexColor("#1a1a1f") return { "h1": ParagraphStyle( "H1", fontName=f"{BODY}-Bold", fontSize=16, leading=20, textColor=accent, spaceAfter=10, spaceBefore=0, ), "h2": ParagraphStyle( "H2", fontName=f"{BODY}-Bold", fontSize=13, leading=17, textColor=accent, spaceBefore=12, spaceAfter=6, ), "h3": ParagraphStyle( "H3", fontName=f"{BODY}-Bold", fontSize=11.5, leading=15, textColor=HexColor("#2d5fb0"), spaceBefore=8, spaceAfter=4, ), "body": ParagraphStyle( "Body", fontName=BODY, fontSize=10, leading=14, textColor=text, alignment=TA_JUSTIFY, spaceAfter=6, ), "li": ParagraphStyle( "Li", fontName=BODY, fontSize=10, leading=14, textColor=text, alignment=TA_LEFT, leftIndent=4, ), "code": ParagraphStyle( "Code", fontName=MONO, fontSize=8, leading=11, textColor=text, backColor=HexColor("#eef0f4"), leftIndent=6, rightIndent=6, spaceBefore=4, spaceAfter=8, ), "caption": ParagraphStyle( "Cap", fontName=BODY, fontSize=8, leading=11, textColor=muted, alignment=TA_LEFT, spaceAfter=10, ), "th": ParagraphStyle( "Th", fontName=f"{BODY}-Bold", fontSize=9, leading=12, textColor=text, ), "td": ParagraphStyle( "Td", fontName=BODY, fontSize=9, leading=12, textColor=text, ), "footer": ParagraphStyle( "Foot", fontName=BODY, fontSize=8, leading=10, textColor=muted, ), } def _split_table_row(line: str) -> list[str]: cells = [c.strip() for c in line.strip().strip("|").split("|")] return cells def _is_table_sep(line: str) -> bool: s = line.strip().strip("|").replace(" ", "") return bool(s) and all(set(c) <= {"-", ":"} and "-" in c for c in s.split("|")) def md_to_flowables(md: str, styles: dict[str, ParagraphStyle]) -> list: lines = md.replace("\r\n", "\n").split("\n") story: list = [] i = 0 n = len(lines) def flush_para(buf: list[str]) -> None: text = " ".join(x.strip() for x in buf if x.strip()) if text: story.append(Paragraph(_inline(text), styles["body"])) while i < n: line = lines[i] stripped = line.strip() if stripped.startswith("```"): i += 1 block: list[str] = [] while i < n and not lines[i].strip().startswith("```"): block.append(lines[i]) i += 1 i += 1 story.append(Preformatted("\n".join(block) or " ", styles["code"])) continue if stripped == "---": story.append(Spacer(1, 4)) story.append(HRFlowable(width="100%", thickness=0.4, color=HexColor("#b7c9e3"))) story.append(Spacer(1, 8)) i += 1 continue if stripped.startswith("# "): story.append(Paragraph(_inline(stripped[2:]), styles["h1"])) i += 1 continue if stripped.startswith("## "): story.append(Paragraph(_inline(stripped[3:]), styles["h2"])) i += 1 continue if stripped.startswith("### "): story.append(Paragraph(_inline(stripped[4:]), styles["h3"])) i += 1 continue if stripped.startswith("|") and i + 1 < n and _is_table_sep(lines[i + 1]): headers = _split_table_row(stripped) i += 2 rows = [[Paragraph(_inline(h), styles["th"]) for h in headers]] while i < n and lines[i].strip().startswith("|"): cells = _split_table_row(lines[i]) while len(cells) < len(headers): cells.append("") rows.append([Paragraph(_inline(c), styles["td"]) for c in cells[: len(headers)]]) i += 1 col_w = (A4[0] - 36 * mm) / max(len(headers), 1) tbl = Table(rows, colWidths=[col_w] * len(headers), hAlign="LEFT") tbl.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), HexColor("#e8f0fa")), ("GRID", (0, 0), (-1, -1), 0.3, HexColor("#d8dce3")), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (-1, -1), 5), ("RIGHTPADDING", (0, 0), (-1, -1), 5), ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), ])) story.append(tbl) story.append(Spacer(1, 8)) continue if stripped.startswith(("- ", "* ")): items: list[ListItem] = [] while i < n and lines[i].strip().startswith(("- ", "* ")): items.append(ListItem(Paragraph(_inline(lines[i].strip()[2:]), styles["li"]))) i += 1 story.append(ListFlowable( items, bulletType="bullet", leftIndent=16, bulletFontName=BODY, bulletFontSize=10, spaceAfter=6, )) continue if re.match(r"^\d+\.\s+", stripped): items = [] while i < n and re.match(r"^\d+\.\s+", lines[i].strip()): text = re.sub(r"^\d+\.\s+", "", lines[i].strip()) items.append(ListItem(Paragraph(_inline(text), styles["li"]))) i += 1 story.append(ListFlowable( items, bulletType="1", leftIndent=18, bulletFontName=BODY, bulletFontSize=10, spaceAfter=6, )) continue if not stripped: i += 1 continue buf = [line] i += 1 while i < n: nxt = lines[i] ns = nxt.strip() if (not ns or ns.startswith("#") or ns.startswith("|") or ns.startswith("- ") or ns.startswith("* ") or ns.startswith("```") or ns == "---" or re.match(r"^\d+\.\s+", ns)): break buf.append(nxt) i += 1 flush_para(buf) return story def _footer(canvas, doc) -> None: canvas.saveState() canvas.setFillColor(HexColor("#6a6a78")) canvas.setFont(BODY, 8) canvas.drawString(18 * mm, 12 * mm, "RIP Help System — разделяне на секции") canvas.drawRightString(A4[0] - 18 * mm, 12 * mm, f"{doc.page}") canvas.restoreState() def build_pdf() -> Path: _register_fonts() md = MD_PATH.read_text(encoding="utf-8") styles = _styles() doc = SimpleDocTemplate( str(PDF_PATH), pagesize=A4, leftMargin=18 * mm, rightMargin=18 * mm, topMargin=16 * mm, bottomMargin=18 * mm, title="Разделяне на произволен текст на секции", author="RIP Help System", ) story = md_to_flowables(md, styles) doc.build(story, onFirstPage=_footer, onLaterPages=_footer) return PDF_PATH if __name__ == "__main__": out = build_pdf() print(out)