feat: add React card renderer and interactive UML
This commit is contained in:
+358
-37
@@ -699,6 +699,7 @@ def render_preamble(data: dict) -> list[str]:
|
||||
r"\usepackage{fancyhdr}",
|
||||
r"\usepackage{lastpage}",
|
||||
r"\usepackage{microtype}",
|
||||
r"\usepackage[most]{tcolorbox}",
|
||||
r"\usepackage{qrcode}",
|
||||
r"\IfFileExists{references.bib}{%",
|
||||
r" \usepackage[backend=biber,style=numeric,sorting=none]{biblatex}%",
|
||||
@@ -761,6 +762,9 @@ def render_preamble(data: dict) -> list[str]:
|
||||
r" \noindent{\color{black!18}\leaders\hbox{\rule{0.60em}{0.22pt}\hspace{0.38em}}\hfill\kern0pt}%",
|
||||
r" \par\vspace{0.10em}%",
|
||||
r"}",
|
||||
r"\newtcolorbox{ESCTaskFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black,boxsep=0pt,left=1.4mm,right=1.4mm,top=0.8mm,bottom=0.8mm,colbacktitle=black!7,coltitle=black,title={\ttfamily\bfseries TASK\quad #1}}",
|
||||
r"\newtcolorbox{ESCBlockFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!55,boxsep=0pt,left=1.0mm,right=1.0mm,top=0.6mm,bottom=0.6mm,colbacktitle=black!4,coltitle=black,title={\ttfamily\bfseries BLOCK\quad #1}}",
|
||||
r"\newtcolorbox{ESCStepFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!28,boxsep=0pt,left=0.8mm,right=0.8mm,top=0.45mm,bottom=0.45mm,colbacktitle=black!2,coltitle=black,title={\ttfamily STEP\quad #1}}",
|
||||
"",
|
||||
]
|
||||
|
||||
@@ -844,6 +848,48 @@ def render_front_matter(card: dict) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def render_front_scope_table_tex(scope: dict) -> list[str]:
|
||||
table = scope.get("scope_table") or {}
|
||||
rows = table.get("rows") or []
|
||||
if not rows:
|
||||
return []
|
||||
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
||||
default_headers = (
|
||||
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
||||
if has_task_column
|
||||
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
||||
)
|
||||
headers = table.get("headers") or default_headers
|
||||
if len(headers) != len(default_headers):
|
||||
headers = default_headers
|
||||
column_spec = (
|
||||
r"@{}>{\ttfamily}p{1.05cm}>{\ttfamily}p{1.55cm}X>{\raggedright\arraybackslash}p{3.25cm}@{}"
|
||||
if has_task_column
|
||||
else r"@{}>{\ttfamily}p{1.25cm}X>{\raggedright\arraybackslash}p{3.55cm}@{}"
|
||||
)
|
||||
lines = [
|
||||
r"\vspace{0.55em}",
|
||||
rf"\small\begin{{tabularx}}{{\textwidth}}{{{column_spec}}}",
|
||||
" & ".join(rf"\textbf{{{tex_escape(header)}}}" for header in headers) + r" \\ \hline",
|
||||
]
|
||||
for row in rows:
|
||||
cells = [rf"\texttt{{{tex_escape(row.get('chapter', ''))}}}"]
|
||||
if has_task_column:
|
||||
cells.append(rf"\texttt{{{tex_escape(row.get('task', ''))}}}")
|
||||
cells.extend([row.get("idea_tex", ""), tex_escape(row.get("priority", ""))])
|
||||
if str(row.get("task", "")).strip().lower() == "task04":
|
||||
framed_cells = []
|
||||
for index, cell in enumerate(cells):
|
||||
alignment = "|l|" if index == 0 else "l|"
|
||||
framed_cells.append(rf"\multicolumn{{1}}{{{alignment}}}{{\textbf{{{cell}}}}}")
|
||||
lines.append(r"\hline")
|
||||
lines.append(" & ".join(framed_cells) + r" \\ \hline")
|
||||
else:
|
||||
lines.append(" & ".join(cells) + r" \\ \hline")
|
||||
lines.extend([r"\end{tabularx}", r"\normalsize", ""])
|
||||
return lines
|
||||
|
||||
|
||||
def render_front_page_scope(data: dict) -> list[str]:
|
||||
scope = data.get("front_page_scope")
|
||||
if not scope:
|
||||
@@ -853,18 +899,37 @@ def render_front_page_scope(data: dict) -> list[str]:
|
||||
rf"\noindent{{\Large\bfseries {tex_escape(scope.get('title', 'Zakres i cel opracowania'))}}}\par",
|
||||
r"\vspace{0.35em}",
|
||||
scope.get("content_tex", ""),
|
||||
"",
|
||||
r"\vspace{0.8em}",
|
||||
r"\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}}",
|
||||
"",
|
||||
]
|
||||
if scope.get("scope_title"):
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
r"\vspace{0.8em}",
|
||||
r"\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}}",
|
||||
r"\vspace{0.7em}",
|
||||
rf"\noindent{{\Large\bfseries {tex_escape(scope['scope_title'])}}}\par",
|
||||
r"\vspace{0.35em}",
|
||||
scope.get("scope_content_tex", ""),
|
||||
]
|
||||
)
|
||||
lines.extend(render_front_scope_table_tex(scope))
|
||||
if scope.get("scope_footer_tex"):
|
||||
lines.extend([scope["scope_footer_tex"], ""])
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
r"\vspace{0.8em}",
|
||||
r"\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def render_task_flow_tex(task_ref: str, task: dict) -> list[str]:
|
||||
title = task.get("title") or task_ref
|
||||
lines = [
|
||||
rf"\subsection*{{{tex_escape(task_ref.upper())} · {tex_escape(title)}}}",
|
||||
rf"\begin{{ESCTaskFrame}}{{{tex_escape(task_ref.upper())} · {tex_escape(title)}}}",
|
||||
]
|
||||
if task.get("uuid"):
|
||||
lines.append(rf"{{\scriptsize\ttfamily {tex_escape(task['uuid'])}}}\par")
|
||||
@@ -873,32 +938,33 @@ def render_task_flow_tex(task_ref: str, task: dict) -> list[str]:
|
||||
if kind == "block":
|
||||
lines.extend(
|
||||
[
|
||||
r"\par\medskip\noindent\textcolor{black!35}{\rule{\linewidth}{0.35pt}}\par",
|
||||
rf"\noindent{{\large\bfseries Block · {tex_escape(item.get('title', ''))}}}\par",
|
||||
rf"\begin{{ESCBlockFrame}}{{{tex_escape(item.get('title', ''))}}}",
|
||||
]
|
||||
)
|
||||
if item.get("content_tex"):
|
||||
lines.append(str(item["content_tex"]))
|
||||
steps = item.get("steps", []) or []
|
||||
if steps:
|
||||
lines.append(r"\begin{enumerate}[label=\textbf{K\arabic*.},leftmargin=*]")
|
||||
for step in steps:
|
||||
content = str(step.get("content_tex", ""))
|
||||
suffix = f" {content}" if content else ""
|
||||
lines.append(rf" \item \textbf{{{tex_escape(step.get('title', ''))}}}{suffix}")
|
||||
lines.append(r"\end{enumerate}")
|
||||
lines.append(rf"\begin{{ESCStepFrame}}{{{tex_escape(step.get('id', ''))} · {tex_escape(step.get('title', ''))}}}")
|
||||
if content:
|
||||
lines.append(content)
|
||||
lines.append(r"\end{ESCStepFrame}")
|
||||
lines.append(r"\end{ESCBlockFrame}")
|
||||
elif kind == "exercise":
|
||||
lines.extend(
|
||||
[
|
||||
r"\par\medskip\noindent\textcolor{black!55}{\rule{\linewidth}{0.6pt}}\par",
|
||||
rf"\noindent{{\large\bfseries Exercise · {tex_escape(item.get('title', ''))}}}\par",
|
||||
rf"\begin{{ESCBlockFrame}}{{Ćwiczenie · {tex_escape(item.get('title', ''))}}}",
|
||||
str(item.get("prompt_tex", "")),
|
||||
]
|
||||
)
|
||||
if item.get("evidence_tex"):
|
||||
lines.append(rf"\par\textbf{{Evidence:}} {item['evidence_tex']}")
|
||||
lines.append(rf"\par\textbf{{Acceptance:}} {tex_escape(item.get('criterion', ''))}")
|
||||
lines.append(rf"\par\textbf{{Task acceptance:}} {tex_escape(task.get('criterion', ''))}")
|
||||
lines.append(r"\end{ESCBlockFrame}")
|
||||
lines.append(rf"\tcblower\textbf{{Task acceptance:}} {tex_escape(task.get('criterion', ''))}")
|
||||
lines.append(r"\end{ESCTaskFrame}")
|
||||
return lines
|
||||
|
||||
|
||||
@@ -906,7 +972,11 @@ def render_tasks(section: dict, data: dict) -> list[str]:
|
||||
tasks = data["tasks"]
|
||||
lines: list[str] = []
|
||||
legacy_refs: list[str] = []
|
||||
for task_ref in section["task_refs"]:
|
||||
for index, task_ref in enumerate(section["task_refs"]):
|
||||
if index:
|
||||
# Kolejny task jest niezależnym etapem karty: jego ramka ma
|
||||
# zaczynać nową stronę, a nie być kontynuacją poprzedniego.
|
||||
lines.append(r"\clearpage")
|
||||
task = tasks[task_ref]
|
||||
if task.get("flow"):
|
||||
lines.extend(render_task_flow_tex(task_ref, task))
|
||||
@@ -1747,9 +1817,22 @@ def render_section_margin(section: dict, data: dict) -> list[str]:
|
||||
return render_dual_margin_block(general_tags, vocational_tags, voffset="-3.1em", data=data)
|
||||
|
||||
|
||||
def ordered_sections(data: dict) -> list[dict]:
|
||||
return [
|
||||
section
|
||||
for _, section in sorted(
|
||||
enumerate(data.get("sections", [])),
|
||||
key=lambda entry: (entry[1].get("order", 1000), entry[0]),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def render_sections(data: dict) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for section in data["sections"]:
|
||||
for section in ordered_sections(data):
|
||||
if section.get("content_kind") == "tasks":
|
||||
# Pierwszy task danej grupy również dostaje własny początek strony.
|
||||
lines.append(r"\clearpage")
|
||||
command = r"\section*" if section.get("starred") else r"\section"
|
||||
lines.append(r"\ESCSectionBlockStart")
|
||||
lines.append(rf"{command}{{{tex_escape(section['title'])}}}")
|
||||
@@ -2206,7 +2289,7 @@ def collect_numbered_labels(data: dict) -> tuple[dict[str, int], dict[str, int]]
|
||||
figure_numbers: dict[str, int] = {}
|
||||
equation_no = 0
|
||||
figure_no = 0
|
||||
for section in data.get("sections", []):
|
||||
for section in ordered_sections(data):
|
||||
content = section.get("content_tex", "")
|
||||
for match in re.finditer(
|
||||
r"\\begin\{(?P<kind>equation|figure)\}.*?\\label\{(?P<label>[^}]+)\}.*?\\end\{(?P=kind)\}",
|
||||
@@ -2785,6 +2868,7 @@ def render_bibliography_section_html(data: dict) -> str:
|
||||
|
||||
def render_main_html(data: dict) -> str:
|
||||
equation_numbers, figure_numbers = collect_numbered_labels(data)
|
||||
sections = ordered_sections(data)
|
||||
card = data["card"]
|
||||
use_resource_header = bool(data.get("title_block"))
|
||||
show_tree_inspector = data.get("generated", {}).get("html_tree_inspector", True)
|
||||
@@ -2795,10 +2879,10 @@ def render_main_html(data: dict) -> str:
|
||||
)
|
||||
toc = "".join(
|
||||
f'<a href="#{html_escape(html_id("section-" + str(index)))}">{index}. {html_escape(section.get("title", ""))}</a>'
|
||||
for index, section in enumerate(data.get("sections", []), start=1)
|
||||
for index, section in enumerate(sections, start=1)
|
||||
)
|
||||
bibliography_html = render_bibliography_section_html(data)
|
||||
total_pages = 2 + len(data.get("sections", [])) + (1 if bibliography_html else 0)
|
||||
total_pages = 2 + len(sections) + (1 if bibliography_html else 0)
|
||||
rows = [
|
||||
("Projekt", card["project"]),
|
||||
("Seria", card.get("series_title", card["series"])),
|
||||
@@ -2818,7 +2902,7 @@ def render_main_html(data: dict) -> str:
|
||||
data=data,
|
||||
)
|
||||
sections_html: list[str] = []
|
||||
for index, section in enumerate(data.get("sections", []), start=1):
|
||||
for index, section in enumerate(sections, start=1):
|
||||
page_number = index + 1
|
||||
content = (
|
||||
render_tasks_html(section, data, equation_numbers, figure_numbers)
|
||||
@@ -2854,7 +2938,7 @@ def render_main_html(data: dict) -> str:
|
||||
f'</div></article>'
|
||||
)
|
||||
if bibliography_html:
|
||||
bibliography_page = len(data.get("sections", [])) + 2
|
||||
bibliography_page = len(sections) + 2
|
||||
sections_html.append(
|
||||
f'<article class="sheet section-sheet" id="bibliografia-i-zrodla">'
|
||||
f'<div class="sheet-content">'
|
||||
@@ -2868,12 +2952,46 @@ def render_main_html(data: dict) -> str:
|
||||
)
|
||||
|
||||
scope = data.get("front_page_scope", {})
|
||||
scope_html = (
|
||||
f'<section class="front-scope"><h2>{html_escape(scope.get("title", "Zakres i cel opracowania"))}</h2>'
|
||||
f'{render_latex_fragment_html(scope.get("content_tex", ""), equation_numbers, figure_numbers)}</section>'
|
||||
if scope
|
||||
else ""
|
||||
)
|
||||
scope_html = ""
|
||||
if scope:
|
||||
scope_html = (
|
||||
f'<section class="front-scope front-goal"><h2>{html_escape(scope.get("title", "Cel karty"))}</h2>'
|
||||
f'{render_latex_fragment_html(scope.get("content_tex", ""), equation_numbers, figure_numbers)}</section>'
|
||||
)
|
||||
if scope.get("scope_title"):
|
||||
table = scope.get("scope_table") or {}
|
||||
rows = table.get("rows") or []
|
||||
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
||||
default_headers = (
|
||||
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
||||
if has_task_column
|
||||
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
||||
)
|
||||
headers = table.get("headers") or default_headers
|
||||
if len(headers) != len(default_headers):
|
||||
headers = default_headers
|
||||
scope_table_html = ""
|
||||
if rows:
|
||||
table_rows = "".join(
|
||||
f'<tr{" class=\"scope-row--key\"" if str(row.get("task", "")).strip().lower() == "task04" else ""}>'
|
||||
f'<td><code>{html_escape(row.get("chapter", ""))}</code></td>'
|
||||
+ (f'<td><code>{html_escape(row.get("task", ""))}</code></td>' if has_task_column else "")
|
||||
+ f'<td>{latex_inline_to_html(row.get("idea_tex", ""), equation_numbers, figure_numbers)}</td>'
|
||||
+ f'<td>{html_escape(row.get("priority", ""))}</td></tr>'
|
||||
for row in rows
|
||||
)
|
||||
table_class = "scope-table scope-table--task" if has_task_column else "scope-table"
|
||||
scope_table_html = (
|
||||
f'<div class="scope-table-wrap"><table class="{table_class}"><thead><tr>'
|
||||
+ "".join(f'<th>{html_escape(header)}</th>' for header in headers)
|
||||
+ f'</tr></thead><tbody>{table_rows}</tbody></table></div>'
|
||||
)
|
||||
scope_html += (
|
||||
f'<section class="front-scope front-card-scope"><h2>{html_escape(scope["scope_title"])}</h2>'
|
||||
f'{render_latex_fragment_html(scope.get("scope_content_tex", ""), equation_numbers, figure_numbers)}'
|
||||
f'{scope_table_html}'
|
||||
f'{render_latex_fragment_html(scope.get("scope_footer_tex", ""), equation_numbers, figure_numbers)}</section>'
|
||||
)
|
||||
front_intro_html = (
|
||||
scope_html
|
||||
if use_resource_header
|
||||
@@ -2888,14 +3006,15 @@ def render_main_html(data: dict) -> str:
|
||||
css = r"""
|
||||
:root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000;--viewer-canvas-scale:1;--viewer-content-scale:1}
|
||||
*{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:3px 10px;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
|
||||
.topbar-inner{max-width:1120px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:12px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-size:12px;line-height:1.35;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}.toc-links a{font-size:11px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:3px;padding:2px 5px}.viewer-zoom-controls{display:flex;align-items:center;gap:7px;flex:none}.viewer-zoom-status{font:600 10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}.viewer-zoom-reset{border:1px solid #bbb;border-radius:3px;background:#fff;color:#333;padding:1px 6px;font:600 10px/1.35 system-ui,-apple-system,Segoe UI,sans-serif;cursor:pointer}.viewer-zoom-reset:hover{background:#eee}.viewer-zoom-reset:focus-visible{outline:2px solid #555;outline-offset:1px}
|
||||
.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;height:297mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:hidden;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--sheet-effective-scale,1));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm}
|
||||
.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:3px 0;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
|
||||
.topbar-inner{width:calc(100% - 8px);max-width:none;margin:0 4px;display:flex;align-items:center;justify-content:space-between;gap:6px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-size:16px;line-height:1.35;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}.toc-links a{font-size:16px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:3px;padding:2px 5px}.viewer-zoom-controls{display:flex;align-items:center;gap:7px;flex:none}.viewer-zoom-status{font:600 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}.viewer-zoom-reset{border:1px solid #bbb;border-radius:3px;background:#fff;color:#333;padding:1px 6px;font:600 16px/1.35 system-ui,-apple-system,Segoe UI,sans-serif;cursor:pointer}.viewer-zoom-reset:hover{background:#eee}.viewer-zoom-reset:focus-visible{outline:2px solid #555;outline-offset:1px}
|
||||
.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:0;overflow:visible;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--sheet-effective-scale,1));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm}
|
||||
@media(max-width:900px){.sheet{height:auto}}@media print{.sheet{height:auto}}
|
||||
.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}
|
||||
.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: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}
|
||||
.scope-table-wrap{margin:3mm 0;overflow:hidden}.scope-table{width:100%;border-collapse:collapse;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.22}.scope-table th,.scope-table td{border:0;border-bottom:1px solid #888;padding:4px 7px;text-align:left;vertical-align:top}.scope-table th{border-bottom:2px solid #555;background:#f3f3f3;font-weight:700}.scope-table th:first-child,.scope-table td:first-child{width:12%}.scope-table th:last-child,.scope-table td:last-child{width:34%}.scope-table--task th:first-child,.scope-table--task td:first-child{width:9%}.scope-table--task th:nth-child(2),.scope-table--task td:nth-child(2){width:14%}.scope-table--task th:last-child,.scope-table--task td:last-child{width:29%}.scope-table tr.scope-row--key td{font-weight:700;background:#f4f4f4;border-top:2px solid #111;border-bottom:2px solid #111}.scope-table tr.scope-row--key td:first-child{border-left:2px solid #111}.scope-table tr.scope-row--key td:last-child{border-right:2px solid #111}.scope-table td p{font:inherit;margin:0}.scope-table code{font-size:inherit}
|
||||
.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: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{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}
|
||||
@@ -2907,10 +3026,10 @@ p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm
|
||||
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}
|
||||
.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: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}
|
||||
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 #222;margin:1mm 0 2mm;background:#fff}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:6px;padding:5px 7px;background:#eaeaea;border-bottom:1px solid #222}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#222}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#555}.task-block{margin:6px 7px;padding:5px 6px;border:1px solid #777;background:#fff}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:5px;margin-bottom:3px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#555}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{margin:4px 0 0 16px!important;padding:0;display:grid;gap:3px}.block-steps li{margin:0;border:1px solid #b9b9b9;padding:4px 5px;background:#fff}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace;font-weight:700}.block-steps li p:last-child{margin-bottom:0}.task-exercise{margin:6px 7px;border:1px solid #777;border-left:1px solid #222;padding:5px 6px;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:5px;padding-top:4px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:4px 0 0}.task-flow>footer{padding:5px 7px;border-top:1px solid #222;font-size:12px;background:#f6f6f6}.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)}
|
||||
@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{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}.viewer-zoom-status{font-size:10px}}
|
||||
@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;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.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}}
|
||||
@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{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}.viewer-zoom-status{font-size:16px}}
|
||||
@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;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.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,.block-steps li,figure{break-inside:avoid}}
|
||||
"""
|
||||
css += page_resource_header_css(data)
|
||||
if use_resource_header:
|
||||
@@ -2957,7 +3076,8 @@ const viewerSheets = Array.from(document.querySelectorAll('.sheet')).map(sheet =
|
||||
sheet,
|
||||
content: sheet.querySelector('.sheet-content')
|
||||
})).filter(item => item.content);
|
||||
let viewerCanvasScale = 1;
|
||||
const viewerCanvasDefault = 2.18;
|
||||
let viewerCanvasScale = viewerCanvasDefault;
|
||||
let viewerContentScale = 1;
|
||||
function clampViewerScale(value, minimum, maximum){
|
||||
return Math.min(maximum, Math.max(minimum, value));
|
||||
@@ -3029,7 +3149,7 @@ if(viewerPaper){
|
||||
}
|
||||
if(viewerZoomReset){
|
||||
viewerZoomReset.addEventListener('click', () => {
|
||||
viewerCanvasScale = Math.min(1, viewerCanvasMaximum());
|
||||
viewerCanvasScale = Math.min(viewerCanvasDefault, viewerCanvasMaximum());
|
||||
viewerContentScale = 1;
|
||||
refreshViewerLayout();
|
||||
});
|
||||
@@ -6782,7 +6902,8 @@ def prepare_html_figure_assets(data: dict) -> None:
|
||||
for graphic in graphics_paths(data.get("sections", [])):
|
||||
add_graphic(graphic)
|
||||
for asset in section_assets(data.get("sections", [])):
|
||||
add_graphic(str(asset.get("path", "")))
|
||||
add_graphic(str(asset.get("html_path") or asset.get("path", "")))
|
||||
add_graphic(str(asset.get("source_path", "")))
|
||||
for figure in data.get("figures", []):
|
||||
add_graphic(figure.get("path", ""))
|
||||
pdf_links: list[str] = []
|
||||
@@ -6820,6 +6941,205 @@ def prepare_html_figure_assets(data: dict) -> None:
|
||||
shutil.copy2(source, output_dir / source.name)
|
||||
|
||||
|
||||
def react_task_model(
|
||||
task_ref: str,
|
||||
task: dict,
|
||||
equation_numbers: dict[str, int],
|
||||
figure_numbers: dict[str, int],
|
||||
) -> dict:
|
||||
flow: list[dict] = []
|
||||
for item in task.get("flow", []) or []:
|
||||
flow.append(
|
||||
{
|
||||
"id": str(item.get("id", "")),
|
||||
"kind": str(item.get("kind", "block")),
|
||||
"title": str(item.get("title", "")),
|
||||
"content_html": render_latex_fragment_html(
|
||||
str(item.get("content_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
"steps": [
|
||||
{
|
||||
"id": str(step.get("id", "")),
|
||||
"title": str(step.get("title", "")),
|
||||
"content_html": render_latex_fragment_html(
|
||||
str(step.get("content_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
}
|
||||
for step in item.get("steps", []) or []
|
||||
],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"ref": task_ref,
|
||||
"title": str(task.get("title") or task_ref),
|
||||
"uuid": str(task.get("uuid", "")),
|
||||
"criterion": str(task.get("criterion", "")),
|
||||
"prompt_html": render_latex_fragment_html(
|
||||
str(task.get("prompt_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
"flow": flow,
|
||||
}
|
||||
|
||||
|
||||
def react_card_model(data: dict) -> dict:
|
||||
equation_numbers, figure_numbers = collect_numbered_labels(data)
|
||||
sections = ordered_sections(data)
|
||||
page_specs: list[tuple[dict, str | None]] = []
|
||||
for section in sections:
|
||||
task_refs = section.get("task_refs", []) or []
|
||||
if section.get("content_kind") == "tasks" and task_refs:
|
||||
page_specs.extend((section, str(task_ref)) for task_ref in task_refs)
|
||||
else:
|
||||
page_specs.append((section, None))
|
||||
total_pages = 2 + len(page_specs)
|
||||
front_left, front_right = render_dual_margin_html(
|
||||
learning_tree_tags(data, column="ogolne", effect_color="green!50!black"),
|
||||
learning_tree_tags(data, column="zawodowe", effect_color="orange!85!black"),
|
||||
data=data,
|
||||
)
|
||||
scope = data.get("front_page_scope", {}) or {}
|
||||
scope_table = scope.get("scope_table", {}) or {}
|
||||
front = {
|
||||
"header_html": render_page_header_html(data, 1, total_pages),
|
||||
"left_margin_html": front_left,
|
||||
"right_margin_html": front_right,
|
||||
"goal_title": str(scope.get("title", "Cel karty")),
|
||||
"goal_html": render_latex_fragment_html(
|
||||
str(scope.get("content_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
"scope_title": str(scope.get("scope_title", "Zakres karty")),
|
||||
"scope_html": render_latex_fragment_html(
|
||||
str(scope.get("scope_content_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
"scope_headers": list(scope_table.get("headers", [])),
|
||||
"scope_rows": [
|
||||
{
|
||||
"chapter": str(row.get("chapter", "")),
|
||||
"task": str(row.get("task", "")),
|
||||
"idea_html": render_latex_fragment_html(
|
||||
str(row.get("idea_tex", row.get("idea", ""))), equation_numbers, figure_numbers
|
||||
),
|
||||
"priority": str(row.get("priority", "")),
|
||||
"key": bool(row.get("key", str(row.get("task", "")).lower() == "task04")),
|
||||
}
|
||||
for row in scope_table.get("rows", []) or []
|
||||
],
|
||||
}
|
||||
section_models: list[dict] = []
|
||||
for index, (section, task_ref) in enumerate(page_specs, start=1):
|
||||
left_margin, right_margin = render_section_margins_html(section, data)
|
||||
task_refs = section.get("task_refs", []) or []
|
||||
tasks = (
|
||||
[react_task_model(task_ref, data["tasks"][task_ref], equation_numbers, figure_numbers)]
|
||||
if task_ref
|
||||
else []
|
||||
)
|
||||
assets = []
|
||||
include_section_assets = not task_ref or task_ref == str(task_refs[0])
|
||||
for asset in (section.get("assets", []) or []) if include_section_assets else []:
|
||||
source_path = str(asset.get("html_path") or asset.get("path", ""))
|
||||
assets.append(
|
||||
{
|
||||
"label": str(asset.get("label", "")),
|
||||
"caption": str(asset.get("caption", "")),
|
||||
"alt": str(asset.get("alt") or asset.get("caption", "")),
|
||||
"href": html_figure_asset(source_path),
|
||||
"source_href": html_figure_asset(str(asset.get("source_path", "")))
|
||||
if asset.get("source_path")
|
||||
else "",
|
||||
"width": float(asset.get("width", 1.0)),
|
||||
"kind": str(asset.get("kind", "diagram")),
|
||||
"interactive": asset.get("interactive"),
|
||||
}
|
||||
)
|
||||
title = str(section.get("title", ""))
|
||||
if task_ref and len(task_refs) > 1:
|
||||
task_title = str(data["tasks"][task_ref].get("title", "")).strip()
|
||||
title = f'{task_ref.upper()}{" — " + task_title if task_title else ""}'
|
||||
section_models.append(
|
||||
{
|
||||
"id": html_id(f'section-{index}-{task_ref or "content"}'),
|
||||
"index": index,
|
||||
"title": title,
|
||||
"header_html": render_page_header_html(data, index + 1, total_pages),
|
||||
"left_margin_html": left_margin,
|
||||
"right_margin_html": right_margin,
|
||||
"content_html": ""
|
||||
if section.get("content_kind") == "tasks"
|
||||
else render_latex_fragment_html(
|
||||
str(section.get("content_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
"tasks": tasks,
|
||||
"assets": assets,
|
||||
"steps": [
|
||||
{"id": str(step.get("id", "")), "title": str(step.get("title", ""))}
|
||||
for step in section.get("steps", []) or []
|
||||
] if (not task_ref or task_ref == str(task_refs[0])) else [],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema": "esc-card-react-view.v1",
|
||||
"card": data.get("card", {}),
|
||||
"total_pages": total_pages,
|
||||
"front": front,
|
||||
"sections": section_models,
|
||||
"dictionary": {
|
||||
"header_html": render_page_header_html(data, total_pages, total_pages),
|
||||
"content_html": render_learning_tree_overview_html(data),
|
||||
},
|
||||
"toc": [
|
||||
{"id": section["id"], "label": f'{section["index"]}. {section["title"]}'}
|
||||
for section in section_models
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def normalize_generated_text(content: str) -> str:
|
||||
return "\n".join(line.rstrip() for line in content.splitlines()) + "\n"
|
||||
|
||||
|
||||
def write_react_app(data: dict, template: dict, legacy_html: str) -> list[Path]:
|
||||
if not data.get("generated", {}).get("react_app"):
|
||||
return []
|
||||
if template_emitter(template, "html") != "classic":
|
||||
raise RuntimeError("React renderer obsługuje obecnie emitter HTML 'classic'")
|
||||
web_root = html_output_dir(data)
|
||||
web_root.mkdir(parents=True, exist_ok=True)
|
||||
style_match = re.search(r"<style>(.*?)</style>", legacy_html, re.S)
|
||||
base_css = style_match.group(1) if style_match else ""
|
||||
model_path = web_root / "card-data.json"
|
||||
style_path = web_root / "style.css"
|
||||
index_path = web_root / "index.html"
|
||||
model_path.write_text(json.dumps(react_card_model(data), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
style_path.write_text(normalize_generated_text(base_css), encoding="utf-8")
|
||||
index_path.write_text(
|
||||
"""<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="card-renderer" content="react">
|
||||
<title>Karta pracy</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body class="resource-header-enabled">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
build_script = REPO_ROOT / "react" / "build.mjs"
|
||||
subprocess.run(
|
||||
["node", str(build_script), str(web_root)],
|
||||
cwd=REPO_ROOT,
|
||||
check=True,
|
||||
)
|
||||
return [index_path, style_path, model_path, web_root / "app.js", web_root / "app.css"]
|
||||
|
||||
|
||||
def write_outputs(data: dict, template: dict, force_web: bool = False) -> list[Path]:
|
||||
prepare_html_figure_assets(data)
|
||||
outputs = {
|
||||
@@ -6840,9 +7160,10 @@ def write_outputs(data: dict, template: dict, force_web: bool = False) -> list[P
|
||||
continue
|
||||
path = CARD_ROOT / output_ref
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
path.write_text(normalize_generated_text(content), encoding="utf-8")
|
||||
paths.append(path)
|
||||
paths.extend(write_web_outputs(data, template, force_web))
|
||||
paths.extend(write_react_app(data, template, outputs.get("html", "")))
|
||||
return paths
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user