feat: add compact dual-QR document frame

This commit is contained in:
user
2026-07-16 08:55:24 +02:00
parent 374f047290
commit 462ce93968
3 changed files with 220 additions and 121 deletions
+6 -4
View File
@@ -23,9 +23,10 @@ Obiekt `card` identyfikuje serię, numer, wersję, UUID, autora i status.
Obiekt `generated` wskazuje ścieżki wyjściowe TeX/HTML/CSS. Pole `template` Obiekt `generated` wskazuje ścieżki wyjściowe TeX/HTML/CSS. Pole `template`
wybiera layout. wybiera layout.
Opcjonalny obiekt `title_block` włącza tabliczkę zasobu o wysokości dokładnie Opcjonalny obiekt `title_block` włącza tabliczkę zasobu o skonfigurowanej
3 cm na każdej stronie, także pierwszej, bibliografii, słownika i stronach wysokości 2,6 lub 3 cm na każdej stronie, także pierwszej, bibliografii,
kontynuacji. Tabliczka zastępuje dużą tabelę metadanych strony tytułowej. słownika i stronach kontynuacji. Tabliczka zastępuje dużą tabelę metadanych
strony tytułowej.
Zawiera kategorię, tytuł, metadane karty, numer strony oraz widoczny, Zawiera kategorię, tytuł, metadane karty, numer strony oraz widoczny,
kanoniczny URL. Ten sam URL jest kodowany lokalnie w QR: jako SVG w HTML i kanoniczny URL. Ten sam URL jest kodowany lokalnie w QR: jako SVG w HTML i
przez pakiet `qrcode` w TeX. Opcjonalny `repository_url` dodaje drugi QR do przez pakiet `qrcode` w TeX. Opcjonalny `repository_url` dodaje drugi QR do
@@ -36,7 +37,8 @@ nagłówek na każdej logicznej karcie, a wydruk używa `@page { size: A4; }` or
pól marginesu, dlatego nagłówek powtarza się również po automatycznym podziale pól marginesu, dlatego nagłówek powtarza się również po automatycznym podziale
długiej sekcji. długiej sekcji.
Pola `height_cm: 3`, `repeat_on_every_page: true` i Pole `height_cm` przyjmuje `2.6` (kompaktowy nagłówek z QR 22 mm i równym
światłem) albo `3`; pola `repeat_on_every_page: true` i
`replace_front_matter: true` dokumentują ten kontrakt. `show_qr: true` wymaga `replace_front_matter: true` dokumentują ten kontrakt. `show_qr: true` wymaga
kanonicznego adresu HTTP(S) w `url` albo pary `url_host_uuid`/`doc_uuid`, z kanonicznego adresu HTTP(S) w `url` albo pary `url_host_uuid`/`doc_uuid`, z
której powstaje `https://<url_host_uuid>/<doc_uuid>`. której powstaje `https://<url_host_uuid>/<doc_uuid>`.
+1 -1
View File
@@ -311,7 +311,7 @@
}, },
"show_qr": { "type": "boolean" }, "show_qr": { "type": "boolean" },
"show_repository_qr": { "type": "boolean" }, "show_repository_qr": { "type": "boolean" },
"height_cm": { "const": 3 }, "height_cm": { "enum": [2.6, 3] },
"repeat_on_every_page": { "const": true }, "repeat_on_every_page": { "const": true },
"replace_front_matter": { "const": true } "replace_front_matter": { "const": true }
}, },
+211 -114
View File
@@ -422,8 +422,8 @@ def validate_source(data: dict) -> None:
errors.append( errors.append(
"title_block.show_qr=true wymaga title_block.url albo pary url_host_uuid/doc_uuid" "title_block.show_qr=true wymaga title_block.url albo pary url_host_uuid/doc_uuid"
) )
if title_block.get("height_cm", 3) not in (3, 3.0): if title_block.get("height_cm", 3) not in (2.6, 3, 3.0):
errors.append("title_block.height_cm musi wynosić dokładnie 3") errors.append("title_block.height_cm musi wynosić 2.6 albo 3")
if title_block.get("repeat_on_every_page", True) is not True: if title_block.get("repeat_on_every_page", True) is not True:
errors.append("title_block.repeat_on_every_page musi być true") errors.append("title_block.repeat_on_every_page musi być true")
if title_block.get("replace_front_matter", True) is not True: if title_block.get("replace_front_matter", True) is not True:
@@ -672,6 +672,10 @@ def macro(name: str, value: str) -> str:
def render_preamble(data: dict) -> list[str]: def render_preamble(data: dict) -> list[str]:
card = data["card"] card = data["card"]
header_height_mm = resource_header_height_mm(data)
source_url = repository_url(data)
doc_uuid = str((data.get("title_block") or {}).get("doc_uuid", ""))
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
return [ return [
r"\documentclass[12pt,a4paper]{article}", r"\documentclass[12pt,a4paper]{article}",
"", "",
@@ -683,7 +687,7 @@ def render_preamble(data: dict) -> list[str]:
r"\usepackage{amsmath}", r"\usepackage{amsmath}",
r"\usepackage{amssymb}", r"\usepackage{amssymb}",
r"\usepackage{xcolor}", r"\usepackage{xcolor}",
r"\usepackage[a4paper,left=2.15cm,right=2.15cm,top=3.75cm,bottom=2.2cm,includehead]{geometry}", rf"\usepackage[a4paper,left=2.15cm,right=2.15cm,top=0.5cm,bottom=0.5cm,headheight={header_height_mm:g}mm,headsep=3mm,footskip=8mm,includehead,includefoot]{{geometry}}",
r"\usepackage{eso-pic}", r"\usepackage{eso-pic}",
r"\usepackage{marginnote}", r"\usepackage{marginnote}",
r"\usepackage{array}", r"\usepackage{array}",
@@ -726,9 +730,9 @@ def render_preamble(data: dict) -> list[str]:
r"\newcommand{\EscWriteRow}[1][2.8em]{\rule{0pt}{#1}}", r"\newcommand{\EscWriteRow}[1][2.8em]{\rule{0pt}{#1}}",
r"\setlength{\parindent}{0pt}", r"\setlength{\parindent}{0pt}",
r"\setlength{\parskip}{0.45em}", r"\setlength{\parskip}{0.45em}",
r"\setlength{\headheight}{30mm}", rf"\setlength{{\headheight}}{{{header_height_mm:g}mm}}",
r"\setlength{\headsep}{3mm}", r"\setlength{\headsep}{3mm}",
r"\setlength{\footskip}{24pt}", r"\setlength{\footskip}{8mm}",
r"\setlength{\marginparwidth}{1.5cm}", r"\setlength{\marginparwidth}{1.5cm}",
r"\setlength{\marginparsep}{0.25cm}", r"\setlength{\marginparsep}{0.25cm}",
r"\renewcommand{\arraystretch}{1.22}", r"\renewcommand{\arraystretch}{1.22}",
@@ -738,10 +742,18 @@ def render_preamble(data: dict) -> list[str]:
r"\pagestyle{fancy}", r"\pagestyle{fancy}",
r"\fancyhf{}", r"\fancyhf{}",
r"\fancyhead[C]{\ESCPageResourceHeader}", r"\fancyhead[C]{\ESCPageResourceHeader}",
r"\lfoot{\scriptsize rewizja \BuildCommit}", r"\fancyfoot[C]{%",
r"\cfoot{\scriptsize \thepage/\pageref{LastPage}}", r" \begin{minipage}{\textwidth}%",
r"\rfoot{\scriptsize \DocumentUUID}", r" \hrule height0.35pt\vspace{0.5mm}%",
r" \fontsize{4.2}{4.8}\selectfont\ttfamily%",
r" \begin{tabularx}{\textwidth}{@{}p{27mm}p{49mm}Xr@{}}%",
rf" \textbf{{KARTA}} {tex_escape(card_number)} & \textbf{{UUID}} {tex_escape(card.get('uuid', ''))} & \textbf{{DOKUMENT}} {tex_escape(doc_uuid)} & \textbf{{ARKUSZ}} \thepage/\pageref{{LastPage}} \\%",
rf" \textbf{{WERSJA}} {tex_escape(card.get('version', ''))} · {tex_escape(card.get('revision_date', ''))} & \multicolumn{{2}}{{l}}{{\fontsize{{3.6}}{{4.1}}\selectfont\textbf{{GITEA}} \href{{{source_url}}}{{\nolinkurl{{{source_url}}}}}}} & \textbf{{AUTOR}} {tex_escape(card.get('author', ''))}%",
r" \end{tabularx}%",
r" \end{minipage}%",
r"}",
r"\renewcommand{\headrulewidth}{0pt}", r"\renewcommand{\headrulewidth}{0pt}",
r"\renewcommand{\footrulewidth}{0pt}",
"", "",
r"\newcommand{\ESCMarginTag}[4]{%", r"\newcommand{\ESCMarginTag}[4]{%",
r" \hspace*{#1}\textcolor{#2}{#3~#4}%", r" \hspace*{#1}\textcolor{#2}{#3~#4}%",
@@ -2849,7 +2861,8 @@ def render_main_html(data: dict) -> str:
f'<section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ""}' f'<section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ""}'
f'{heading}' f'{heading}'
f'{render_step_map_html(section, data)}{content}</section>' f'{render_step_map_html(section, data)}{content}</section>'
f'<aside class="pdf-margin right">{right_margin}</aside></article>' f'<aside class="pdf-margin right">{right_margin}</aside>'
f'{render_page_footer_html(data, page_number, total_pages) if use_resource_header else ""}</article>'
) )
if bibliography_html: if bibliography_html:
bibliography_page = len(data.get("sections", [])) + 2 bibliography_page = len(data.get("sections", [])) + 2
@@ -2859,7 +2872,8 @@ def render_main_html(data: dict) -> str:
f'<aside class="pdf-margin left"></aside>' f'<aside class="pdf-margin left"></aside>'
f'<section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ""}' f'<section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ""}'
f'<h2>Bibliografia i źródła</h2>{bibliography_html}</section>' f'<h2>Bibliografia i źródła</h2>{bibliography_html}</section>'
f'<aside class="pdf-margin right"></aside></article>' f'<aside class="pdf-margin right"></aside>'
f'{render_page_footer_html(data, bibliography_page, total_pages) if use_resource_header else ""}</article>'
) )
scope = data.get("front_page_scope", {}) scope = data.get("front_page_scope", {})
@@ -2882,17 +2896,17 @@ def render_main_html(data: dict) -> str:
css = r""" css = r"""
:root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000} :root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000}
*{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--desk);color:var(--ink);font-family:Georgia,"Times New Roman",serif;line-height:1.42} *{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--desk);color:var(--ink);font-family:"Latin Modern Roman","LM Roman 12",Georgia,"Times New Roman",serif;line-height:1.25}
.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:8px 14px;font-family:system-ui,-apple-system,Segoe UI,sans-serif} .topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:8px 14px;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
.topbar details{max-width:1120px;margin:0 auto}.topbar summary{cursor:pointer;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.toc-links a{font-size:13px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:4px;padding:3px 7px} .topbar details{max-width:1120px;margin:0 auto}.topbar summary{cursor:pointer;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.toc-links a{font-size:13px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:4px;padding:3px 7px}
.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{width:210mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:grid;grid-template-columns:22mm minmax(0,1fr) 22mm;column-gap:4mm;align-content:start;padding:13mm 21.5mm 11mm}.resource-header-enabled .sheet{padding-top:5mm;row-gap:0} .paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:13mm 21.5mm 15mm}.resource-header-enabled .sheet{padding-top:5mm}.sheet>.page-resource-header{margin-bottom:3mm}
.pdf-main{min-width:0}.running-header{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;border-bottom:1px solid #111;padding-bottom:3px;margin-bottom:26px;font-size:15px}.running-header span{white-space:nowrap} .pdf-main{min-width:0;width:100%}.running-header{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;border-bottom:1px solid #111;padding-bottom:3px;margin-bottom:26px;font-size:15px}.running-header span{white-space:nowrap}
.hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px} .hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px}
.meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace} .meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}
.front-scope{padding:26px 0 10px;border-bottom:1px solid var(--line-soft)}.front-scope h2{font-size:28px;margin:0 0 12px}.front-scope p{font-size:18px} .front-scope{padding:0 0 3mm;border-bottom:1px solid var(--line-soft)}.front-scope h2{font-size:24px;line-height:1.15;margin:0 0 3mm}.front-scope p{font-size:16px}
.card-section h2{font-size:28px;line-height:1.2;margin:0 0 18px}.section-heading{display:flex;align-items:flex-start;gap:0}.section-heading .section-index{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:26px;margin-right:20px;font-weight:400}.section-heading .section-title{min-width:0}.task-identity{margin-left:auto;padding-left:22px;text-align:right;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;line-height:1.25;color:#555;white-space:nowrap}.task-identity strong,.task-identity-name,.task-identity code{display:block}.task-identity strong{font-size:12px;color:#111}.task-identity-name{max-width:240px;overflow:hidden;text-overflow:ellipsis}.task-identity code{font-size:9px;color:#777} .card-section h2{font-size:24px;line-height:1.15;margin:0 0 4mm}.section-heading{display:flex;align-items:flex-start;gap:0}.section-heading .section-index{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:23px;margin-right:12mm;font-weight:400}.section-heading .section-title{min-width:0}.task-identity{margin-left:auto;padding-left:22px;text-align:right;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;line-height:1.25;color:#555;white-space:nowrap}.task-identity strong,.task-identity-name,.task-identity code{display:block}.task-identity strong{font-size:12px;color:#111}.task-identity-name{max-width:240px;overflow:hidden;text-overflow:ellipsis}.task-identity code{font-size:9px;color:#777}
p{font-size:17px;margin:0 0 12px}.pdf-main ul{font-size:17px;margin:8px 0 14px 24px;padding:0}.pdf-main li{margin:5px 0} p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm;padding:0}.pdf-main li{margin:1.2mm 0}
.pdf-margin{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:7px;line-height:1.14;color:#333;align-self:start;position:sticky;top:48px;max-height:calc(100vh - 80px);overflow:hidden}.pdf-margin.left{text-align:left}.pdf-margin.right{text-align:left} .pdf-margin{position:absolute;top:34mm;width:18mm;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:5px;line-height:1.12;color:#333;max-height:245mm;overflow:hidden}.pdf-margin.left{left:2mm;text-align:left}.pdf-margin.right{right:2mm;text-align:left}
.margin-title{font-weight:700;color:#666;margin-bottom:3px;letter-spacing:.02em}.margin-list{display:flex;flex-direction:column;align-items:flex-start}.margin-tag{display:block;margin:0 0 1px;padding:0;border:0;background:transparent;font:inherit;line-height:inherit;text-align:left;cursor:pointer;margin-left:var(--indent)}.margin-tag.we{color:var(--we)}.margin-tag.og,.margin-tag.tech{color:var(--og)}.margin-tag.kw{color:var(--kw)}.margin-tag .local{color:#777}.margin-tag:hover{text-decoration:underline}.margin-empty{color:#aaa} .margin-title{font-weight:700;color:#666;margin-bottom:3px;letter-spacing:.02em}.margin-list{display:flex;flex-direction:column;align-items:flex-start}.margin-tag{display:block;margin:0 0 1px;padding:0;border:0;background:transparent;font:inherit;line-height:inherit;text-align:left;cursor:pointer;margin-left:var(--indent)}.margin-tag.we{color:var(--we)}.margin-tag.og,.margin-tag.tech{color:var(--og)}.margin-tag.kw{color:var(--kw)}.margin-tag .local{color:#777}.margin-tag:hover{text-decoration:underline}.margin-empty{color:#aaa}
.step-map{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.3;color:#5c5c5c;background:transparent;border:0;border-top:1px solid #cfcfcf;border-bottom:1px solid #e0e0e0;border-radius:0;padding:8px 0;margin:0 0 16px}.step-map summary{cursor:pointer;font-weight:400;color:#555} .step-map{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.3;color:#5c5c5c;background:transparent;border:0;border-top:1px solid #cfcfcf;border-bottom:1px solid #e0e0e0;border-radius:0;padding:8px 0;margin:0 0 16px}.step-map summary{cursor:pointer;font-weight:400;color:#555}
.tree-legend{display:flex;flex-wrap:wrap;gap:4px;margin:7px 0}.steps{display:grid;gap:6px}.step-row{border-top:1px dashed #c9c9c9;padding-top:6px}.step-row:first-child{border-top:0}.step-title span{font-weight:700;margin-right:7px}.step-trees{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.step-refs{display:flex;flex-wrap:wrap;gap:6px;margin-top:3px}.step-refs a{font-size:10px;color:#666} .tree-legend{display:flex;flex-wrap:wrap;gap:4px;margin:7px 0}.steps{display:grid;gap:6px}.step-row{border-top:1px dashed #c9c9c9;padding-top:6px}.step-row:first-child{border-top:0}.step-title span{font-weight:700;margin-right:7px}.step-trees{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.step-refs{display:flex;flex-wrap:wrap;gap:6px;margin-top:3px}.step-refs a{font-size:10px;color:#666}
@@ -2901,12 +2915,12 @@ p{font-size:17px;margin:0 0 12px}.pdf-main ul{font-size:17px;margin:8px 0 14px 2
figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;margin:0 auto;border:0;border-radius:0}figcaption{font-size:14px;color:#444;margin-top:8px;text-align:left}.card-asset.asset-screenshot img{border:1px solid #aaa}.card-asset a{display:block;cursor:zoom-in} figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;margin:0 auto;border:0;border-radius:0}figcaption{font-size:14px;color:#444;margin-top:8px;text-align:left}.card-asset.asset-screenshot img{border:1px solid #aaa}.card-asset a{display:block;cursor:zoom-in}
.table-wrap{overflow:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:16px}th,td{border:1px solid #111;padding:5px 8px;text-align:left;vertical-align:top}th{font-weight:400;background:white} .table-wrap{overflow:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:16px}th,td{border:1px solid #111;padding:5px 8px;text-align:left;vertical-align:top}th{font-weight:400;background:white}
.empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em} .empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em}
code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:17px}.task-legacy{margin:10px 0}.task-legacy>strong{display:block;font:700 11px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #aaa;margin:8px 0 18px}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:10px;padding:8px 11px;background:#f3f3f3;border-bottom:1px solid #aaa}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#777}.task-block,.task-exercise{margin:10px 12px;padding:0 0 10px;border-bottom:1px solid #ddd}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#666}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{counter-reset:block-step;margin:5px 0 0 24px!important}.block-steps li{margin:7px 0}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace}.task-exercise{border:1px solid #777;border-left:4px solid #111;padding:9px 11px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:8px;padding-top:6px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:7px 0 0}.task-flow>footer{padding:8px 11px;border-top:1px solid #aaa;font-size:12px}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-main{grid-column:2}.dictionary-sheet .pdf-margin{display:none} code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:16px}.task-legacy{margin:2.5mm 0}.task-legacy>strong{display:block;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #aaa;margin:2mm 0 4mm}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:10px;padding:8px 11px;background:#f3f3f3;border-bottom:1px solid #aaa}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#777}.task-block,.task-exercise{margin:10px 12px;padding:0 0 10px;border-bottom:1px solid #ddd}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#666}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{counter-reset:block-step;margin:5px 0 0 24px!important}.block-steps li{margin:7px 0}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace}.task-exercise{border:1px solid #777;border-left:4px solid #111;padding:9px 11px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:8px;padding-top:6px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:7px 0 0}.task-flow>footer{padding:8px 11px;border-top:1px solid #aaa;font-size:12px}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-margin{display:none}
.inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)} .inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)}
@media(max-width:900px){.sheet{display:block;min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}} @media(max-width:900px){.sheet{display:block;min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}}
@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{display:grid;grid-template-columns:22mm minmax(0,1fr) 22mm;column-gap:4mm;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0;row-gap:6mm}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet{padding:0;row-gap:6mm}.sheet>.page-resource-header{display:none!important}.pdf-margin{position:static;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.task-block,.task-exercise,figure{break-inside:avoid}} @media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet{padding:0}.sheet>.page-resource-header,.sheet>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,figure{break-inside:avoid}}
""" """
css += page_resource_header_css() css += page_resource_header_css(data)
if use_resource_header: if use_resource_header:
css += page_resource_print_css(data) css += page_resource_print_css(data)
js = r""" js = r"""
@@ -2974,9 +2988,10 @@ document.addEventListener('click', event => {
{front_intro_html} {front_intro_html}
</section> </section>
<aside class="pdf-margin right">{front_right}</aside> <aside class="pdf-margin right">{front_right}</aside>
{render_page_footer_html(data, 1, total_pages) if use_resource_header else ''}
</article> </article>
{''.join(sections_html)} {''.join(sections_html)}
<article class="sheet dictionary-sheet">{render_page_header_html(data, dictionary_page, total_pages) if use_resource_header else ''}<aside class="pdf-margin left"></aside><section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ''}<h2>Słownik WE/EN/EK/KW</h2>{render_learning_tree_overview_html(data)}</section><aside class="pdf-margin right"></aside></article> <article class="sheet dictionary-sheet">{render_page_header_html(data, dictionary_page, total_pages) if use_resource_header else ''}<aside class="pdf-margin left"></aside><section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ''}<h2>Słownik WE/EN/EK/KW</h2>{render_learning_tree_overview_html(data)}</section><aside class="pdf-margin right"></aside>{render_page_footer_html(data, dictionary_page, total_pages) if use_resource_header else ''}</article>
</main> </main>
{inspector_html} {inspector_html}
<script>{js}</script> <script>{js}</script>
@@ -3084,6 +3099,12 @@ def title_block_fields(data: dict) -> dict[str, str | bool]:
} }
def resource_header_height_mm(data: dict) -> float:
"""Return the configured repeated-header height in millimetres."""
configured = data.get("title_block") or {}
return float(configured.get("height_cm", 3)) * 10
@lru_cache(maxsize=32) @lru_cache(maxsize=32)
def qr_svg(value: str) -> str: def qr_svg(value: str) -> str:
"""Generate a self-contained QR SVG without a network dependency. """Generate a self-contained QR SVG without a network dependency.
@@ -3181,7 +3202,7 @@ def qr_svg_from_tex(value: str) -> str:
def render_page_header_html(data: dict, page: int, total: int) -> str: def render_page_header_html(data: dict, page: int, total: int) -> str:
"""Render the canonical 30 mm resource header used by every page wrapper.""" """Render the configured resource header used by every page wrapper."""
card = data["card"] card = data["card"]
fields = title_block_fields(data) fields = title_block_fields(data)
url = str(fields["url"]) url = str(fields["url"])
@@ -3212,62 +3233,104 @@ def render_page_header_html(data: dict, page: int, total: int) -> str:
else "" else ""
) )
def datum(label: str, value: object, class_name: str = "") -> str:
suffix = f" {class_name}" if class_name else ""
return (
f'<div class="resource-datum{suffix}"><span>{html_escape(label)}</span>'
f'<strong>{html_escape(value or "")}</strong></div>'
)
card_number = f"{card.get('number', '')}/{card.get('count', '')}" card_number = f"{card.get('number', '')}/{card.get('count', '')}"
sheet_number = f"{page}/{total}" sheet_number = f"{page}/{total}"
header_class = "page-resource-header page-resource-header--dual" if repository_qr_html else "page-resource-header" header_class = "page-resource-header page-resource-header--dual" if repository_qr_html else "page-resource-header"
source_link = (
f'<a class="resource-url resource-repository-url" href="{html_escape(source_url)}">' def inline_fields(items: list[tuple[str, object]], strong_first: bool = False) -> str:
f'<span>GITEA</span>{html_escape(source_url)}</a>' rendered: list[str] = []
if source_url for index, (label, value) in enumerate(items):
else "" if index:
rendered.append('<span class="resource-separator">|</span>')
rendered.append(f'<span class="resource-label">{html_escape(label)}</span>')
tag = "strong" if strong_first and index == 0 else "span"
rendered.append(f'<{tag} class="resource-value">{html_escape(value or "")}</{tag}>')
return '<div class="resource-inline">' + "".join(rendered) + "</div>"
primary_rows = [
inline_fields(
[
(fields["category"], f"{card_prefix(card)}: {card.get('title', '')}"),
],
strong_first=True,
),
inline_fields(
[
("Projekt", card.get("project", "")),
("Seria", card.get("series_title", card.get("series", ""))),
]
),
inline_fields(
[
("Przedmiot", card.get("subject", "")),
("Poziom", card.get("level", "")),
]
),
inline_fields([("HTML", url or "brak URL")]),
]
meta_rows = [
inline_fields(
[("Data", card.get("revision_date", "")), ("Status", card.get("status", ""))]
),
inline_fields([("Karta", card_number), ("Wersja", card.get("version", ""))]),
inline_fields([("Autor", card.get("author", "")), ("Arkusz", sheet_number)]),
inline_fields([("UUID", card.get("uuid", ""))]),
]
column_group = (
'<col style="width:24mm"><col><col style="width:39mm"><col style="width:24mm">'
if repository_qr_html
else '<col><col style="width:47mm"><col style="width:24mm">'
) )
rows: list[str] = []
for index, (primary, meta) in enumerate(zip(primary_rows, meta_rows)):
cells: list[str] = []
if index == 0 and repository_qr_html:
cells.append(f'<td rowspan="4" class="resource-qr-cell resource-qr-cell--left">{repository_qr_html}</td>')
cells.append(f'<td class="resource-header-cell resource-header-cell--primary">{primary}</td>')
cells.append(f'<td class="resource-header-cell resource-header-cell--meta">{meta}</td>')
if index == 0:
cells.append(f'<td rowspan="4" class="resource-qr-cell resource-qr-cell--right">{page_qr_html}</td>')
rows.append("<tr>" + "".join(cells) + "</tr>")
return ( return (
f'<header class="{header_class}" data-resource-url="{html_escape(url)}" ' f'<header class="{header_class}" data-resource-url="{html_escape(url)}" '
f'data-repository-url="{html_escape(source_url)}" ' f'data-repository-url="{html_escape(source_url)}" '
f'data-page="{page}" data-page-total="{total}">' f'data-page="{page}" data-page-total="{total}">'
f'{repository_qr_html}' f'<table class="resource-header-table"><colgroup>{column_group}</colgroup>'
'<div class="resource-primary">' f'<tbody>{"".join(rows)}</tbody></table>'
f'<div class="resource-title"><span>{html_escape(fields["category"])}</span>'
f'<strong>{html_escape(card_prefix(card))}: {html_escape(card.get("title", ""))}</strong></div>'
'<div class="resource-details">'
f'{datum("Projekt", card.get("project", ""))}'
f'{datum("Seria", card.get("series_title", card.get("series", "")))}'
f'{datum("Przedmiot", card.get("subject", ""))}'
f'{datum("Poziom", card.get("level", ""), "resource-level")}'
'</div>'
'<div class="resource-urls">'
f'{source_link}'
f'<a class="resource-url" href="{html_escape(url)}"><span>HTML</span>{html_escape(url or "brak URL")}</a>'
'</div>'
'</div>'
'<div class="resource-meta">'
f'{datum("Data", card.get("revision_date", ""))}'
f'{datum("Status", card.get("status", ""))}'
f'{datum("Karta", card_number)}'
f'{datum("Wersja", card.get("version", ""))}'
f'{datum("Autor", card.get("author", ""))}'
f'{datum("Arkusz", sheet_number)}'
f'{datum("UUID", card.get("uuid", ""), "resource-uuid")}'
'</div>'
f'{page_qr_html}'
'</header>' '</header>'
) )
def page_resource_header_css() -> str: def render_page_footer_html(data: dict, page: int, total: int) -> str:
"""Render the compact per-sheet footer used by the HTML A4 preview."""
card = data["card"]
title_block = data.get("title_block") or {}
doc_uuid = str(title_block.get("doc_uuid", ""))
source_url = repository_url(data)
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
return (
'<footer class="page-resource-footer" role="contentinfo"><table><tbody>'
'<tr>'
f'<td><b>KARTA</b> {html_escape(card_number)}</td>'
f'<td><b>UUID</b> {html_escape(card.get("uuid", ""))}</td>'
f'<td><b>DOKUMENT</b> {html_escape(doc_uuid)}</td>'
f'<td class="footer-right"><b>ARKUSZ</b> {page}/{total}</td>'
'</tr><tr>'
f'<td><b>WERSJA</b> {html_escape(card.get("version", ""))} · {html_escape(card.get("revision_date", ""))}</td>'
f'<td colspan="2"><b>GITEA</b> {html_escape(source_url)}</td>'
f'<td class="footer-right"><b>AUTOR</b> {html_escape(card.get("author", ""))}</td>'
'</tr></tbody></table></footer>'
)
def page_resource_header_css(data: dict) -> str:
return r""" return r"""
.page-resource-header{grid-column:1/-1;width:100%;height:30mm;min-height:30mm;max-height:30mm;border:1px solid #1c1c1a;display:grid;grid-template-columns:minmax(0,1fr) 47mm 24mm;background:#fff;color:#111;overflow:hidden;font-family:system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.12}.page-resource-header--dual{grid-template-columns:24mm minmax(0,1fr) 39mm 24mm} .page-resource-header{width:100%;height:__RESOURCE_HEADER_HEIGHT__;min-height:__RESOURCE_HEADER_HEIGHT__;max-height:__RESOURCE_HEADER_HEIGHT__;background:#fff;color:#111;overflow:hidden;font-family:ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;line-height:1.08}
.resource-qr{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.35mm;border-right:1px solid #aaa;padding:.65mm .8mm;color:#555;text-decoration:none;font-size:7px}.resource-qr-page{border-right:0}.resource-qr .title-qr{display:block;width:22mm;height:22mm;max-width:22mm;max-height:22mm;flex:none}.resource-qr-label{font:700 5px/1 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.08em;color:#444}.resource-qr-empty{text-align:center}.resource-qr-empty span,.resource-qr-empty strong{display:block;font:600 5.5px/1.15 ui-monospace,SFMono-Regular,Consolas,monospace} .resource-header-table{width:100%;height:100%;border-collapse:collapse;table-layout:fixed;border:1px solid #111;font:600 5pt/1.05 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace}.resource-header-table tr{height:25%}.resource-header-cell{padding:.25mm .65mm;border-right:1px solid #777;border-bottom:1px solid #aaa;vertical-align:middle;overflow:hidden}.resource-header-table tr:last-child .resource-header-cell{border-bottom:0}.resource-header-cell--meta{border-right:1px solid #777}.resource-inline{display:flex;align-items:baseline;gap:.55mm;min-width:0;white-space:nowrap;overflow:hidden}.resource-label{font-weight:700;text-transform:uppercase;flex:none}.resource-value{font:inherit;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.resource-header-table tr:first-child .resource-header-cell--primary .resource-value{font-weight:800;font-size:6pt}.resource-separator{color:#999;flex:none}
.resource-primary{min-width:0;display:flex;flex-direction:column;justify-content:center;padding:1mm;border-right:1px solid #aaa;font:600 5.3px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace}.resource-title{display:flex;flex-direction:column;min-width:0}.resource-title span,.resource-title strong{font:inherit;color:#111;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.resource-details{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.72fr);column-gap:1mm;min-width:0}.resource-datum{display:flex;gap:.7mm;align-items:baseline;min-width:0}.resource-datum span,.resource-datum strong{font:inherit;color:#111;white-space:nowrap}.resource-datum span{text-transform:uppercase}.resource-datum strong{overflow:hidden;text-overflow:ellipsis}.resource-details .resource-datum:nth-child(2)::before,.resource-meta .resource-datum:nth-child(2)::before,.resource-meta .resource-datum:nth-child(4)::before{content:"·"}.resource-details .resource-datum:nth-child(3),.resource-level{grid-column:1/-1}.resource-urls{min-width:0}.resource-url{display:block;font:inherit;color:#111;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.resource-url span{display:inline-block;width:8mm;font-weight:inherit}.resource-meta{padding:1mm;border-right:1px solid #aaa;display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:.7mm;align-content:center;min-width:0;font:600 5.3px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace}.resource-meta .resource-datum{font:inherit}.resource-meta .resource-datum:nth-child(n+5){grid-column:1/-1}.resource-meta .resource-uuid strong{font:inherit;overflow:visible;text-overflow:clip}.resource-repository-url{min-width:0} .resource-qr-cell{padding:0;vertical-align:middle;overflow:hidden}.resource-qr-cell--left{border-right:1px solid #777}.resource-qr-cell--right{border-left:1px solid #777}.resource-qr{box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.25mm;padding:.45mm;color:#333;text-decoration:none}.resource-qr .title-qr{display:block;width:22mm;height:22mm;max-width:22mm;max-height:22mm;flex:none}.resource-qr-label{font:700 4.5pt/1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;letter-spacing:.08em;color:#333}.resource-qr-empty{text-align:center}.resource-qr-empty span,.resource-qr-empty strong{display:block;font:600 5pt/1.15 ui-monospace,SFMono-Regular,Consolas,monospace}
""" .page-resource-footer{position:absolute;left:21.5mm;right:21.5mm;bottom:5mm;border-top:1px solid #111;padding-top:.6mm;font:500 5pt/1.1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;color:#222;overflow:hidden}.page-resource-footer table{width:100%;border-collapse:collapse;table-layout:fixed;font:inherit}.page-resource-footer td{width:25%;padding:.25mm .6mm;border:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.page-resource-footer td+td{border-left:1px solid #ddd}.page-resource-footer .footer-right{text-align:right}
""".replace("__RESOURCE_HEADER_HEIGHT__", f"{resource_header_height_mm(data):g}mm")
def css_paged_string(value: object) -> str: def css_paged_string(value: object) -> str:
@@ -3281,9 +3344,11 @@ def css_paged_string(value: object) -> str:
def page_resource_print_css(data: dict) -> str: def page_resource_print_css(data: dict) -> str:
"""Render a real 30 mm repeated header in CSS paged-media margin boxes.""" """Render the repeated header in CSS paged-media margin boxes."""
card = data["card"] card = data["card"]
fields = title_block_fields(data) fields = title_block_fields(data)
header_height_mm = resource_header_height_mm(data)
page_top_margin_mm = header_height_mm + 5
url = str(fields["url"]) url = str(fields["url"])
source_url = str(fields["repository_url"]) source_url = str(fields["repository_url"])
@@ -3297,6 +3362,16 @@ def page_resource_print_css(data: dict) -> str:
page_qr_background = qr_background(url, bool(fields["show_qr"])) page_qr_background = qr_background(url, bool(fields["show_qr"]))
repository_qr_enabled = bool(fields["show_repository_qr"]) repository_qr_enabled = bool(fields["show_repository_qr"])
repository_qr_background = qr_background(source_url, repository_qr_enabled) repository_qr_background = qr_background(source_url, repository_qr_enabled)
center_grid_svg = (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119 26" '
'preserveAspectRatio="none">'
'<rect width="119" height="26" fill="white"/>'
'<path d="M80 0V26 M0 6.5H119 M0 13H119 M0 19.5H119" '
'fill="none" stroke="#888" stroke-width="0.14"/>'
'</svg>'
)
center_grid_data = base64.b64encode(center_grid_svg.encode("utf-8")).decode("ascii")
center_grid_background = f'url("data:image/svg+xml;base64,{center_grid_data}")'
middle = "\n".join( middle = "\n".join(
[ [
f"{fields['category']} | {card_prefix(card)}: {card.get('title', '')}", f"{fields['category']} | {card_prefix(card)}: {card.get('title', '')}",
@@ -3320,7 +3395,7 @@ def page_resource_print_css(data: dict) -> str:
] ]
) )
qr_box = ( qr_box = (
"height:30mm;box-sizing:border-box;padding:24mm .8mm .5mm;" f"height:{header_height_mm:g}mm;box-sizing:border-box;padding:24mm .8mm .5mm;"
"border:1px solid #111;background-color:#fff;background-repeat:no-repeat;" "border:1px solid #111;background-color:#fff;background-repeat:no-repeat;"
"background-position:center 1mm;background-size:22mm 22mm;" "background-position:center 1mm;background-size:22mm 22mm;"
"vertical-align:middle;text-align:center;" "vertical-align:middle;text-align:center;"
@@ -3336,55 +3411,60 @@ def page_resource_print_css(data: dict) -> str:
center_before_page = "\n".join( center_before_page = "\n".join(
[ [
header_row( header_row(
fields["category"], f"{fields['category']} | {card_prefix(card)}: {card.get('title', '')}",
f"DATA {card.get('revision_date', '')} · STATUS {card.get('status', '')}", f"DATA {card.get('revision_date', '')} | STATUS {card.get('status', '')}",
), ),
header_row( header_row(
f"{card_prefix(card)}: {card.get('title', '')}", f"PROJEKT {card.get('project', '')} | SERIA {card.get('series_title', card.get('series', ''))}",
f"KARTA {card.get('number', '')}/{card.get('count', '')} · WERSJA {card.get('version', '')}", f"KARTA {card.get('number', '')}/{card.get('count', '')} | WERSJA {card.get('version', '')}",
), ),
header_row( header_row(
f"PROJEKT {card.get('project', '')} · SERIA {card.get('series_title', card.get('series', ''))}", f"PRZEDMIOT {card.get('subject', '')} | POZIOM {card.get('level', '')}",
f"AUTOR {card.get('author', '')}", f"AUTOR {card.get('author', '')} | ARKUSZ ",
), ),
header_row(f"PRZEDMIOT {card.get('subject', '')}", "ARKUSZ "),
] ]
) )
center_after_page = "\n" + "\n".join( center_after_page = "\n" + header_row(
[ f"HTML {url}",
header_row(
f"POZIOM {card.get('level', '')}",
f"UUID {card.get('uuid', '')}", f"UUID {card.get('uuid', '')}",
),
header_row(f"GITEA {source_url}"),
header_row(f"HTML {url}"),
]
) )
footer_left = (
f"KARTA {card.get('number', '')}/{card.get('count', '')}\n"
f"WERSJA {card.get('version', '')} · {card.get('revision_date', '')}"
)
footer_center = f"UUID {card.get('uuid', '')}\nGITEA {source_url}"
footer_right_before_page = "ARKUSZ "
return f""" return f"""
@page{{ @page{{
size:A4; size:A4;
margin:35mm 21.5mm 15mm; margin:{page_top_margin_mm:g}mm 21.5mm 15mm;
@top-left{{content:"GITEA";width:24mm;{qr_box};background-image:{repository_qr_background}}} @top-left{{content:"GITEA";width:24mm;{qr_box};background-image:{repository_qr_background}}}
@top-center{{content:"{css_paged_string(center_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(center_after_page)}";width:119mm;height:30mm;box-sizing:border-box;padding:1mm;border-block:1px solid #111;background-color:#fff;background-image:linear-gradient(#aaa,#aaa);background-repeat:no-repeat;background-position:80mm 0;background-size:1px 100%;white-space:pre;vertical-align:middle;text-align:left;font:600 5.3px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111;overflow:hidden}} @top-center{{content:"{css_paged_string(center_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(center_after_page)}";width:119mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:1mm;border-block:1px solid #111;background-color:#fff;background-image:{center_grid_background};background-repeat:no-repeat;background-position:center;background-size:100% 100%;white-space:pre;vertical-align:middle;text-align:left;font:600 5.3px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111;overflow:hidden}}
@top-right{{content:"HTML";width:24mm;{qr_box};background-image:{page_qr_background}}} @top-right{{content:"HTML";width:24mm;{qr_box};background-image:{page_qr_background}}}
@bottom-left{{content:"{css_paged_string(footer_left)}";width:45mm;height:7mm;box-sizing:border-box;padding:.7mm 1mm 0;border-top:1px solid #111;white-space:pre;text-align:left;font:500 4.8px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace}}
@bottom-center{{content:"{css_paged_string(footer_center)}";width:90mm;height:7mm;box-sizing:border-box;padding:.7mm 1mm 0;border-top:1px solid #111;white-space:pre;text-align:left;font:500 4.8px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:hidden}}
@bottom-right{{content:"{css_paged_string(footer_right_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(chr(10) + 'AUTOR ' + str(card.get('author', '')))}";width:32mm;height:7mm;box-sizing:border-box;padding:.7mm 1mm 0;border-top:1px solid #111;white-space:pre;text-align:right;font:500 4.8px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace}}
}} }}
""" """
return f""" return f"""
@page{{ @page{{
size:A4; size:A4;
margin:35mm 21.5mm 15mm; margin:{page_top_margin_mm:g}mm 21.5mm 15mm;
@top-left{{content:"{css_paged_string(meta_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(meta_after_page)}";width:47mm;height:30mm;box-sizing:border-box;padding:1.5mm 1.7mm;border:1px solid #111;white-space:pre-wrap;vertical-align:middle;text-align:left;font:600 5.7px/1.3 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111}} @top-left{{content:"{css_paged_string(meta_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(meta_after_page)}";width:47mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:1.5mm 1.7mm;border:1px solid #111;white-space:pre-wrap;vertical-align:middle;text-align:left;font:600 5.7px/1.3 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111}}
@top-center{{content:"{css_paged_string(middle)}";width:96mm;height:30mm;box-sizing:border-box;padding:1.5mm 2mm;border-block:1px solid #111;white-space:pre-wrap;vertical-align:middle;text-align:left;font:600 6.2px/1.25 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111}} @top-center{{content:"{css_paged_string(middle)}";width:96mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:1.5mm 2mm;border-block:1px solid #111;white-space:pre-wrap;vertical-align:middle;text-align:left;font:600 6.2px/1.25 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111}}
@top-right{{content:"HTML";width:24mm;{qr_box};background-image:{page_qr_background}}} @top-right{{content:"HTML";width:24mm;{qr_box};background-image:{page_qr_background}}}
}} }}
""" """
def render_page_header_tex_macro(data: dict) -> list[str]: def render_page_header_tex_macro(data: dict) -> list[str]:
"""Define the repeated 30 mm TeX header for the classic emitter.""" """Define the repeated TeX header for the classic emitter."""
card = data["card"] card = data["card"]
fields = title_block_fields(data) fields = title_block_fields(data)
header_height_mm = resource_header_height_mm(data)
outer_height_mm = header_height_mm - 0.5
inner_height_mm = header_height_mm - 1
url = str(fields["url"]) url = str(fields["url"])
source_url = str(fields["repository_url"]) source_url = str(fields["repository_url"])
dual_qr = bool(fields["show_repository_qr"]) dual_qr = bool(fields["show_repository_qr"])
@@ -3407,14 +3487,43 @@ def render_page_header_tex_macro(data: dict) -> list[str]:
card_number = f"{card.get('number', '')}/{card.get('count', '')}" card_number = f"{card.get('number', '')}/{card.get('count', '')}"
primary_width = "76mm" if dual_qr else "89mm" primary_width = "76mm" if dual_qr else "89mm"
meta_width = "39mm" if dual_qr else "47mm" meta_width = "39mm" if dual_qr else "47mm"
repository_meta_line = ( row_height_mm = (inner_height_mm - 0.5) / 4
rf" {{\fontsize{{3.8}}{{4.3}}\selectfont\ttfamily GITEA: \href{{{source_url}}}{{\nolinkurl{{{source_url}}}}}}}%"
if source_url def header_rows_tex(rows: list[str]) -> list[str]:
else "" output: list[str] = []
for index, row in enumerate(rows):
output.append(
" \\parbox[c]["
+ f"{row_height_mm:g}mm"
+ "][c]{\\linewidth}{\\hspace{0.65mm}"
+ row
+ "\\hspace{0.65mm}}%"
) )
if index != len(rows) - 1:
output.extend(
[
r" \par\nointerlineskip%",
r" \hrule height0.35pt%",
r" \nointerlineskip%",
]
)
return output
primary_rows = [
rf"{{\fontsize{{5.0}}{{5.4}}\selectfont\ttfamily {tex_escape(fields['category'])} | \textbf{{{tex_escape(title)}}}}}",
rf"{{\fontsize{{4.8}}{{5.2}}\selectfont\ttfamily \textbf{{PROJEKT}} {tex_escape(card.get('project', ''))} | \textbf{{SERIA}} {tex_escape(card.get('series_title', card.get('series', '')))}}}",
rf"{{\fontsize{{4.5}}{{5.0}}\selectfont\ttfamily \textbf{{PRZEDMIOT}} {tex_escape(card.get('subject', ''))} | \textbf{{POZIOM}} {tex_escape(card.get('level', ''))}}}",
rf"{{\fontsize{{3.8}}{{4.2}}\selectfont\ttfamily \textbf{{HTML}} \href{{{url}}}{{\nolinkurl{{{url}}}}}}}",
]
meta_rows = [
rf"{{\fontsize{{4.6}}{{5.0}}\selectfont\ttfamily \textbf{{DATA}} {tex_escape(card.get('revision_date', ''))} | \textbf{{STATUS}} {tex_escape(card.get('status', ''))}}}",
rf"{{\fontsize{{4.6}}{{5.0}}\selectfont\ttfamily \textbf{{KARTA}} {tex_escape(card_number)} | \textbf{{WERSJA}} {tex_escape(card.get('version', ''))}}}",
rf"{{\fontsize{{4.6}}{{5.0}}\selectfont\ttfamily \textbf{{AUTOR}} {tex_escape(card.get('author', ''))} | \textbf{{ARKUSZ}} \thepage/\pageref{{LastPage}}}}",
rf"{{\fontsize{{3.8}}{{4.2}}\selectfont\ttfamily \textbf{{UUID}} {tex_escape(card.get('uuid', ''))}}}",
]
source_cell = ( source_cell = (
[ [
r" \begin{minipage}[c][29mm][c]{24mm}\centering%", rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{24mm}}\centering%",
" " + source_qr + r"\par%", " " + source_qr + r"\par%",
r" \vspace{0.2mm}{\fontsize{4.5}{4.8}\selectfont\bfseries\ttfamily GITEA}%", r" \vspace{0.2mm}{\fontsize{4.5}{4.8}\selectfont\bfseries\ttfamily GITEA}%",
r" \end{minipage}%", r" \end{minipage}%",
@@ -3430,29 +3539,17 @@ def render_page_header_tex_macro(data: dict) -> list[str]:
r" \setlength{\fboxsep}{0pt}%", r" \setlength{\fboxsep}{0pt}%",
r" \setlength{\fboxrule}{0.35pt}%", r" \setlength{\fboxrule}{0.35pt}%",
r" \noindent\fbox{%", r" \noindent\fbox{%",
r" \begin{minipage}[c][29.5mm][c]{\dimexpr\textwidth-2\fboxrule\relax}%", rf" \begin{{minipage}}[c][{outer_height_mm:g}mm][c]{{\dimexpr\textwidth-2\fboxrule\relax}}%",
*source_cell, *source_cell,
rf" \begin{{minipage}}[c][29mm][c]{{{primary_width}}}%", rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{{primary_width}}}%",
rf" {{\fontsize{{5.5}}{{6.2}}\selectfont\ttfamily\textcolor{{black!58}}{{{tex_escape(fields['category'])}}}}}\par%", *header_rows_tex(primary_rows),
rf" {{\fontsize{{9.2}}{{10.1}}\selectfont\bfseries {tex_escape(title)}}}\par%",
r" \vspace{0.1mm}%",
rf" {{\fontsize{{5.6}}{{6.3}}\selectfont Projekt: \texttt{{{tex_escape(card.get('project', ''))}}}\quad Seria: \texttt{{{tex_escape(card.get('series_title', card.get('series', '')))}}}}}\par%",
rf" {{\fontsize{{5.6}}{{6.3}}\selectfont Przedmiot: \texttt{{{tex_escape(card.get('subject', ''))}}}}}\par%",
rf" {{\fontsize{{5.0}}{{5.7}}\selectfont Poziom: \texttt{{{tex_escape(card.get('level', ''))}}}}}\par%",
rf" {{\fontsize{{4.2}}{{4.8}}\selectfont\ttfamily HTML: \href{{{url}}}{{\nolinkurl{{{url}}}}}}}%",
r" \end{minipage}%", r" \end{minipage}%",
r" \hfill\vrule width0.35pt\hspace{1mm}%", r" \hfill\vrule width0.35pt\hspace{1mm}%",
rf" \begin{{minipage}}[c][29mm][c]{{{meta_width}}}%", rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{{meta_width}}}%",
rf" {{\fontsize{{5.7}}{{6.5}}\selectfont Data: \texttt{{{tex_escape(card.get('revision_date', ''))}}}\hfill Status: \texttt{{{tex_escape(card.get('status', ''))}}}}}\par%", *header_rows_tex(meta_rows),
rf" {{\fontsize{{5.7}}{{6.5}}\selectfont Karta: \texttt{{{tex_escape(card_number)}}}\hfill Wersja: \texttt{{{tex_escape(card.get('version', ''))}}}}}\par%",
rf" {{\fontsize{{5.7}}{{6.5}}\selectfont Autor: \texttt{{{tex_escape(card.get('author', ''))}}}}}\par%",
r" {\fontsize{5.7}{6.5}\selectfont Arkusz: \texttt{\thepage/\pageref{LastPage}}}\par%",
r" \vspace{0.5mm}\hrule\vspace{0.5mm}%",
rf" {{\fontsize{{4.7}}{{5.3}}\selectfont\ttfamily UUID: {tex_escape(card.get('uuid', ''))}}}\par%",
repository_meta_line,
r" \end{minipage}%", r" \end{minipage}%",
r" \hfill\vrule width0.35pt\hspace{1mm}%", r" \hfill\vrule width0.35pt\hspace{1mm}%",
r" \begin{minipage}[c][29mm][c]{24mm}\centering%", rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{24mm}}\centering%",
" " + page_qr + r"\par%", " " + page_qr + r"\par%",
r" \vspace{0.2mm}{\fontsize{4.5}{4.8}\selectfont\bfseries\ttfamily HTML}%", r" \vspace{0.2mm}{\fontsize{4.5}{4.8}\selectfont\bfseries\ttfamily HTML}%",
r" \end{minipage}%", r" \end{minipage}%",
@@ -4333,7 +4430,7 @@ body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items:
.page-footer{display:flex;justify-content:space-between;border-top:1.5px solid #1c1c1a;padding-top:8px;margin-top:14px;font:400 9.5px 'JetBrains Mono',monospace;color:#8a887f;gap:16px} .page-footer{display:flex;justify-content:space-between;border-top:1.5px solid #1c1c1a;padding-top:8px;margin-top:14px;font:400 9.5px 'JetBrains Mono',monospace;color:#8a887f;gap:16px}
@media print{@page{size:A4 landscape;margin:0}body{background:#fff;padding:0;gap:0}.page{box-shadow:none!important;border:none!important;break-after:page}} @media print{@page{size:A4 landscape;margin:0}body{background:#fff;padding:0;gap:0}.page{box-shadow:none!important;border:none!important;break-after:page}}
""" """
css += page_resource_header_css() css += page_resource_header_css(data)
card = data["card"] card = data["card"]
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html lang="pl"> <html lang="pl">
@@ -4514,7 +4611,7 @@ def render_5b_document_page_html(data: dict, page_data: dict, page: int, total:
return render_5b_page_html(data, center, page, total) return render_5b_page_html(data, center, page, total)
def render_main_html_5b_css(template: dict | None = None) -> str: def render_main_html_5b_css(data: dict, template: dict | None = None) -> str:
css = r""" css = r"""
*{-webkit-print-color-adjust:exact;print-color-adjust:exact;box-sizing:border-box} *{-webkit-print-color-adjust:exact;print-color-adjust:exact;box-sizing:border-box}
body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items:center;gap:24px;padding:24px} body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items:center;gap:24px;padding:24px}
@@ -4547,7 +4644,7 @@ body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items:
.page-footer{position:absolute;left:var(--page-rule-left);bottom:95px;width:var(--page-rule-width);display:flex;justify-content:space-between;border-top:0;padding-top:6px;margin-top:0;font:400 8.2px 'JetBrains Mono',monospace;color:#8a887f;gap:10px}.page-footer::before{content:"";position:absolute;left:0;right:0;top:0;border-top:1.4px solid #1c1c1a} .page-footer{position:absolute;left:var(--page-rule-left);bottom:95px;width:var(--page-rule-width);display:flex;justify-content:space-between;border-top:0;padding-top:6px;margin-top:0;font:400 8.2px 'JetBrains Mono',monospace;color:#8a887f;gap:10px}.page-footer::before{content:"";position:absolute;left:0;right:0;top:0;border-top:1.4px solid #1c1c1a}
@media print{@page{size:A4 portrait;margin:0}body{background:#fff;padding:0;gap:0}.page{box-shadow:none!important;border:none!important;break-after:page}} @media print{@page{size:A4 portrait;margin:0}body{background:#fff;padding:0;gap:0}.page{box-shadow:none!important;border:none!important;break-after:page}}
""" """
return css + page_resource_header_css() return css + page_resource_header_css(data)
def render_main_html_5b(data: dict, template: dict) -> str: def render_main_html_5b(data: dict, template: dict) -> str:
@@ -5740,7 +5837,7 @@ def render_html_output(data: dict, template: dict) -> str:
def render_html_css_output(data: dict, template: dict) -> str: def render_html_css_output(data: dict, template: dict) -> str:
if template_emitter(template, "html") == "5b": if template_emitter(template, "html") == "5b":
return render_main_html_5b_css(template) return render_main_html_5b_css(data, template)
return "" return ""