feat: add repeated dual-QR A4 resource headers

This commit is contained in:
user
2026-07-15 21:31:34 +02:00
parent da1e37eea9
commit 01a5f18414
7 changed files with 1601 additions and 114 deletions
+14
View File
@@ -20,6 +20,7 @@ card-layouts/
├── schemas/ ├── schemas/
│ ├── card-source.schema.json │ ├── card-source.schema.json
│ ├── card-template.schema.json │ ├── card-template.schema.json
│ ├── reference-registry.schema.json
│ └── series.schema.json │ └── series.schema.json
├── templates/ ├── templates/
│ ├── karta-klasyczna.json │ ├── karta-klasyczna.json
@@ -107,6 +108,11 @@ WE (wymaganie edukacyjne)
Każda sekcja i każdy krok mogą wskazywać te same drzewka przez `tree_refs`. Każda sekcja i każdy krok mogą wskazywać te same drzewka przez `tree_refs`.
Dzięki temu TeX i HTML pokazują identyczne powiązanie treści z efektami. Dzięki temu TeX i HTML pokazują identyczne powiązanie treści z efektami.
Sekcje mogą także deklarować strukturalne `assets` (najczęściej zrzuty ekranu
z `doc/assets`). Generator wstawia je do TeX/PDF i HTML, zachowuje podpis,
tekst alternatywny i stabilną etykietę oraz przygotowuje link do pełnego
obrazu w wersji ekranowej.
Szczegóły modelu karty opisuje [docs/CARD.md](docs/CARD.md), a manifestu Szczegóły modelu karty opisuje [docs/CARD.md](docs/CARD.md), a manifestu
serii [docs/SERIES.md](docs/SERIES.md). serii [docs/SERIES.md](docs/SERIES.md).
@@ -126,6 +132,14 @@ ich katalog przez:
Brak słowników nie blokuje generowania, jeśli karta zawiera kompletne pola Brak słowników nie blokuje generowania, jeśli karta zawiera kompletne pola
`display` i `text` we własnych drzewkach. `display` i `text` we własnych drzewkach.
## Rejestry referencji
Wspólnej strategii, innej karty, treści ani wzoru nie kopiujemy do karty.
`card_source.json` przechowuje stabilne UUID-y, a generator rozwiązuje je przez
lokalny kanoniczny rejestr JSON. Widocznym hiperłączem jest kod strategii
(`Mxx`), nazwa karty albo etykieta treści/wzoru. Model danych i przykład są w
[docs/CARD.md](docs/CARD.md#referencje-zewnętrzne-bez-kopiowania-treści).
## Zasada zmian ## Zasada zmian
- zmiana treści należy do `card_source.json`; - zmiana treści należy do `card_source.json`;
+101 -6
View File
@@ -23,11 +23,25 @@ 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` dodaje do pierwszego nagłówka tekstową Opcjonalny obiekt `title_block` włącza tabliczkę zasobu o wysokości dokładnie
tabliczkę dokumentu: osoby odpowiedzialne, kategorię, tytuł, URL, rewizję, 3 cm na każdej stronie, także pierwszej, bibliografii, słownika i stronach
datę wydania, serię, typ, narzędzie i arkusz. Pole `url` jest kodowane także kontynuacji. Tabliczka zastępuje dużą tabelę metadanych strony tytułowej.
lokalnie w QR (SVG w HTML i pakiet `qrcode` w TeX). Gdy URL nie jest podany, Zawiera kategorię, tytuł, metadane karty, numer strony oraz widoczny,
generator pokazuje w tej komórce informację `brak URL`. 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
repozytorium. Przy dwóch kodach komórki mają po 27 mm: Gitea znajduje się po
lewej, a wyrenderowany zasób po prawej. Jedna zamknięta ramka biegnie dokładnie
między marginesami strony. W HTML widok ekranowy ma rozmiar A4 (210 × 297 mm) i
nagłówek na każdej logicznej karcie, a wydruk używa `@page { size: A4; }` oraz
pól marginesu, dlatego nagłówek powtarza się również po automatycznym podziale
długiej sekcji.
Pola `height_cm: 3`, `repeat_on_every_page: true` i
`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
której powstaje `https://<url_host_uuid>/<doc_uuid>`.
`show_repository_qr: true` wymaga `repository_url`. Adres podglądu `localhost`
nie powinien być publikowany na karcie.
### Efekty i kryteria ### Efekty i kryteria
@@ -49,6 +63,42 @@ Pole `sections[]` porządkuje treść. Kroki `sections[].steps[]` mają:
- `tree_refs` wskazujące drzewka z marginesów; - `tree_refs` wskazujące drzewka z marginesów;
- opcjonalne odwołania do wymagań, efektów, kryteriów, równań i figur. - opcjonalne odwołania do wymagań, efektów, kryteriów, równań i figur.
### Referencje zewnętrzne bez kopiowania treści
Karta może ładować kanoniczne rejestry współdzielonych strategii, kart, treści
i wzorów. Lokalny plik JSON służy do walidacji oraz deterministycznego buildu:
```json
{
"reference_registries": [
{
"uuid": "03403a8a-e20d-5b17-9004-26d9fe6c3003",
"source": "../../../tools/debugging-strategies/json/reference_registry.json"
}
]
}
```
Krok przechowuje tylko rodzaj, UUID rejestru i UUID wpisu:
```json
{
"references": [
{
"kind": "strategy",
"registry_uuid": "03403a8a-e20d-5b17-9004-26d9fe6c3003",
"uuid": "1ecfc474-4fbd-5a56-8ef1-80ae557761d0"
}
]
}
```
Generator pobiera z rejestru etykietę, tytuł, ścieżkę i publiczny adres.
Strategię pokazuje jako kod `Mxx`, kartę jako jej nazwę, a treść lub wzór jako
kanoniczną etykietę. W TeX/PDF i HTML etykieta jest hiperłączem. Nazw ani URL-i
nie kopiujemy do `card_source.json`; brak rejestru, UUID albo lokalnego pliku
przerywa generowanie.
Layouty 5a/5b obsługują również: Layouty 5a/5b obsługują również:
- `model_block.steps` — kroki modelowania; - `model_block.steps` — kroki modelowania;
@@ -60,12 +110,57 @@ renderowany jako osobna, opisana ramka w sekcji implementacyjnej, a jego
`web_steps` zasila niezależną checklistę HTML. Kolejność głównych bloków `web_steps` zasila niezależną checklistę HTML. Kolejność głównych bloków
strony jest opisana przez `block_map` wybranego szablonu. strony jest opisana przez `block_map` wybranego szablonu.
### Zrzuty ekranu i inne assety sekcji
Zrzutów nie wpisujemy jako ręcznego `\\includegraphics` w wygenerowanym TeX.
Sekcja może deklarować listę `assets`, a generator umieszcza każdy element w
TeX/PDF i HTML, kopiuje plik do `figures/` strony oraz dodaje link do pełnego
rozmiaru:
```json
{
"title": "Pomiar w debuggerze",
"content_tex": "Zatrzymaj program przed pierwszą alokacją.",
"assets": [
{
"path": "assets/task04-first-allocation-annotated.png",
"caption": "Task04 przed pierwszym przydziałem pamięci.",
"label": "fig:task04-first-allocation",
"alt": "Termdebug: kod C, listing, rejestry i allocbuf",
"kind": "screenshot",
"width": 1.0
}
]
}
```
- `path` jest liczony względem katalogu `doc/` karty; standardem jest
`doc/assets/`;
- obsługiwane są PNG, JPG/JPEG i PDF;
- `width` jest ułamkiem szerokości kolumny od `0.1` do `1.0`;
- `label` jest stabilnym identyfikatorem używanym przez `figure_refs`;
- `full_size_link` domyślnie włącza otwieranie zrzutu HTML w pełnym rozmiarze.
### Zadania ### Zadania
`tasks` jest słownikiem zadań, a `tasks_order` określa ich kolejność. Każde `tasks` jest słownikiem zadań, a `tasks_order` określa ich kolejność. Każde
zadanie posiada polecenie `prompt_tex`, kryterium zaliczenia i odwołania do zadanie posiada polecenie `prompt_tex`, kryterium zaliczenia i odwołania do
modelu edukacyjnego. modelu edukacyjnego.
Rozbudowane zadanie przechowuje przebieg dydaktyczny w uporządkowanym
`tasks.<id>.flow[]`:
- `kind: "block"` grupuje jedną część objaśnienia lub demonstracji;
- opcjonalne `block.steps[]` zachowują kolejność czynności wewnątrz bloku;
- `kind: "exercise"` jest samodzielną pracą ucznia należącą bezpośrednio do
zadania i podaje `prompt_tex`, `evidence_tex` oraz `criterion`;
- `exercise.based_on[]` może wskazać wcześniejsze bloki lub ich kroki.
Bloki i ćwiczenia są rodzeństwem w jednym `flow`, dzięki czemu renderer nie
gubi kolejności nauczania. `steps` nie może występować bezpośrednio w obiekcie
zadania. Dawne `prompt_tex` i `criterion` pozostają wymagane dla zgodności ze
starszymi konsumentami; gdy istnieje `flow`, renderer używa jego treści.
## Zalecany README karty ## Zalecany README karty
README konkretnej karty powinien zawierać: README konkretnej karty powinien zawierać:
@@ -89,4 +184,4 @@ python3 tools/render_card.py /sciezka/do/mojej-karty
``` ```
Generator najpierw sprawdza spójność identyfikatorów WE/EN/EK/KW, kroków, Generator najpierw sprawdza spójność identyfikatorów WE/EN/EK/KW, kroków,
zadań i odwołań, a dopiero potem zapisuje oba formaty. zadań, rejestrów UUID i odwołań, a dopiero potem zapisuje oba formaty.
+6 -1
View File
@@ -34,13 +34,18 @@
"checked_by": "—", "checked_by": "—",
"approved_by": "—", "approved_by": "—",
"url": "https://demo.example.edu/cards/demo-01", "url": "https://demo.example.edu/cards/demo-01",
"repository_url": "https://gitea.example.edu/edu/cards/demo-01",
"revision": "1", "revision": "1",
"issued_on": "15.07.2026", "issued_on": "15.07.2026",
"series": "DEMO-01", "series": "DEMO-01",
"document_type": "karta pracy", "document_type": "karta pracy",
"tool": "card-layouts", "tool": "card-layouts",
"sheet": "1 / 11", "sheet": "1 / 11",
"show_qr": true "show_qr": true,
"show_repository_qr": true,
"height_cm": 3,
"repeat_on_every_page": true,
"replace_front_matter": true
}, },
"front_page_break": false, "front_page_break": false,
"metric_fill_fields": [ "metric_fill_fields": [
+203 -5
View File
@@ -47,6 +47,23 @@
"side_margin_tree_layout": { "side_margin_tree_layout": {
"$ref": "#/$defs/sideMarginLayout" "$ref": "#/$defs/sideMarginLayout"
}, },
"reference_registries": {
"type": "array",
"items": {
"type": "object",
"required": ["uuid", "source"],
"properties": {
"uuid": {
"$ref": "#/$defs/uuid"
},
"source": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false
}
},
"learning_effects": { "learning_effects": {
"type": "object", "type": "object",
"minProperties": 1, "minProperties": 1,
@@ -147,6 +164,26 @@
} }
}, },
"$defs": { "$defs": {
"uuid": {
"type": "string",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
},
"externalReference": {
"type": "object",
"required": ["kind", "registry_uuid", "uuid"],
"properties": {
"kind": {
"enum": ["strategy", "card", "content", "formula"]
},
"registry_uuid": {
"$ref": "#/$defs/uuid"
},
"uuid": {
"$ref": "#/$defs/uuid"
}
},
"additionalProperties": false
},
"stringList": { "stringList": {
"type": "array", "type": "array",
"items": { "items": {
@@ -174,6 +211,10 @@
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"series_title": {
"type": "string",
"minLength": 1
},
"number": { "number": {
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
@@ -242,18 +283,65 @@
"checked_by": { "type": "string" }, "checked_by": { "type": "string" },
"approved_by": { "type": "string" }, "approved_by": { "type": "string" },
"title": { "type": "string" }, "title": { "type": "string" },
"url": { "type": "string" }, "url": {
"url_host_uuid": { "type": "string" }, "type": "string",
"pattern": "^https?://"
},
"repository_url": {
"type": "string",
"pattern": "^https?://"
},
"url_host_uuid": {
"type": "string",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
},
"url_port": { "type": ["string", "integer"] }, "url_port": { "type": ["string", "integer"] },
"doc_uuid": { "type": "string" }, "doc_uuid": {
"type": "string",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
},
"revision": { "type": "string" }, "revision": { "type": "string" },
"issued_on": { "type": "string" }, "issued_on": { "type": "string" },
"series": { "type": "string" }, "series": { "type": "string" },
"document_type": { "type": "string" }, "document_type": { "type": "string" },
"tool": { "type": "string" }, "tool": { "type": "string" },
"sheet": { "type": "string" }, "sheet": {
"show_qr": { "type": "boolean" } "type": "string",
"description": "Legacy static sheet label. Repeated page headers compute page/total dynamically."
}, },
"show_qr": { "type": "boolean" },
"show_repository_qr": { "type": "boolean" },
"height_cm": { "const": 3 },
"repeat_on_every_page": { "const": true },
"replace_front_matter": { "const": true }
},
"allOf": [
{
"if": {
"required": ["show_qr"],
"properties": {
"show_qr": { "const": true }
}
},
"then": {
"anyOf": [
{ "required": ["url"] },
{ "required": ["url_host_uuid", "doc_uuid"] }
]
}
},
{
"if": {
"required": ["show_repository_qr"],
"properties": {
"show_repository_qr": { "const": true }
}
},
"then": {
"required": ["repository_url"]
}
}
],
"additionalProperties": false "additionalProperties": false
}, },
"sideMarginLayout": { "sideMarginLayout": {
@@ -469,10 +557,49 @@
"items": { "items": {
"$ref": "#/$defs/sectionStep" "$ref": "#/$defs/sectionStep"
} }
},
"assets": {
"type": "array",
"items": {
"$ref": "#/$defs/sectionAsset"
}
} }
}, },
"additionalProperties": true "additionalProperties": true
}, },
"sectionAsset": {
"type": "object",
"required": ["path", "caption", "label"],
"properties": {
"path": {
"type": "string",
"minLength": 1
},
"caption": {
"type": "string",
"minLength": 1
},
"label": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9:.-]*$"
},
"alt": {
"type": "string"
},
"kind": {
"enum": ["screenshot", "figure", "diagram"]
},
"width": {
"type": "number",
"minimum": 0.1,
"maximum": 1.0
},
"full_size_link": {
"type": "boolean"
}
},
"additionalProperties": false
},
"sectionStep": { "sectionStep": {
"type": "object", "type": "object",
"required": ["id", "title", "tree_refs"], "required": ["id", "title", "tree_refs"],
@@ -506,6 +633,12 @@
}, },
"figure_refs": { "figure_refs": {
"$ref": "#/$defs/stringList" "$ref": "#/$defs/stringList"
},
"references": {
"type": "array",
"items": {
"$ref": "#/$defs/externalReference"
}
} }
}, },
"additionalProperties": true "additionalProperties": true
@@ -520,6 +653,21 @@
"criterion": { "criterion": {
"type": "string" "type": "string"
}, },
"title": {
"type": "string",
"minLength": 1
},
"uuid": {
"type": "string",
"minLength": 1
},
"flow": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/taskFlowItem"
}
},
"assessment_criterion_ref": { "assessment_criterion_ref": {
"type": "string" "type": "string"
}, },
@@ -532,6 +680,56 @@
}, },
"additionalProperties": true "additionalProperties": true
}, },
"taskFlowItem": {
"oneOf": [
{ "$ref": "#/$defs/taskBlock" },
{ "$ref": "#/$defs/taskExercise" }
]
},
"taskBlock": {
"type": "object",
"required": ["kind", "id", "title"],
"properties": {
"kind": { "const": "block" },
"id": { "type": "string", "minLength": 1 },
"title": { "type": "string", "minLength": 1 },
"content_tex": { "type": "string" },
"steps": {
"type": "array",
"items": { "$ref": "#/$defs/taskBlockStep" }
}
},
"additionalProperties": false
},
"taskBlockStep": {
"type": "object",
"required": ["id", "title"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"title": { "type": "string", "minLength": 1 },
"content_tex": { "type": "string" },
"tree_refs": { "$ref": "#/$defs/stringList" },
"references": {
"type": "array",
"items": { "$ref": "#/$defs/externalReference" }
}
},
"additionalProperties": false
},
"taskExercise": {
"type": "object",
"required": ["kind", "id", "title", "prompt_tex", "criterion"],
"properties": {
"kind": { "const": "exercise" },
"id": { "type": "string", "minLength": 1 },
"title": { "type": "string", "minLength": 1 },
"prompt_tex": { "type": "string", "minLength": 1 },
"evidence_tex": { "type": "string" },
"criterion": { "type": "string", "minLength": 1 },
"based_on": { "$ref": "#/$defs/stringList" }
},
"additionalProperties": false
},
"procedureStep": { "procedureStep": {
"type": "object", "type": "object",
"required": ["step", "action", "condition"], "required": ["step", "action", "condition"],
+74
View File
@@ -0,0 +1,74 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "reference-registry.schema.json",
"title": "Rejestr współdzielonych referencji dydaktycznych",
"type": "object",
"required": ["schema", "uuid", "name", "title", "root_path", "base_url", "entries"],
"properties": {
"$schema": {
"type": "string"
},
"schema": {
"const": "esc-reference-registry.v1"
},
"uuid": {
"$ref": "#/$defs/uuid"
},
"name": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"title": {
"type": "string",
"minLength": 1
},
"root_path": {
"type": "string",
"minLength": 1
},
"base_url": {
"type": "string",
"format": "uri",
"minLength": 1
},
"entries": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/entry"
}
}
},
"additionalProperties": false,
"$defs": {
"uuid": {
"type": "string",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
},
"entry": {
"type": "object",
"required": ["uuid", "kind", "title", "path"],
"properties": {
"uuid": {
"$ref": "#/$defs/uuid"
},
"kind": {
"enum": ["strategy", "card", "content", "formula"]
},
"code": {
"type": "string",
"minLength": 1
},
"title": {
"type": "string",
"minLength": 1
},
"path": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false
}
}
}
+161 -2
View File
@@ -2,9 +2,12 @@
from __future__ import annotations from __future__ import annotations
import json import json
import base64
import os import os
import shutil
import subprocess import subprocess
import sys import sys
import tempfile
from pathlib import Path from pathlib import Path
@@ -86,18 +89,174 @@ def check_render() -> None:
raise AssertionError(f"TeX nie zawiera znacznika {marker!r}") raise AssertionError(f"TeX nie zawiera znacznika {marker!r}")
if marker not in html: if marker not in html:
raise AssertionError(f"HTML nie zawiera znacznika {marker!r}") raise AssertionError(f"HTML nie zawiera znacznika {marker!r}")
if "\x00" in html:
if r"\qrcode[height=" not in tex: raise AssertionError("HTML zawiera bajt NUL po rozwinięciu placeholderów")
if not any(r"\qrcode[" in line and "height=" in line for line in tex.splitlines()):
raise AssertionError("TeX nie zawiera kodu QR z tabliczki") raise AssertionError("TeX nie zawiera kodu QR z tabliczki")
for expected_url in (
"https://demo.example.edu/cards/demo-01",
"https://gitea.example.edu/edu/cards/demo-01",
):
if expected_url not in tex or expected_url not in html:
raise AssertionError(f"brak adresu nagłówka {expected_url!r}")
if tex.count(r"\qrcode[level=L,height=22mm]") < 2:
raise AssertionError("TeX nie zawiera obu kodów QR nagłówka")
for marker in ("resource-qr-repository", "resource-qr-page"):
if marker not in html:
raise AssertionError(f"HTML nie zawiera komórki {marker!r}")
for marker in ('class="title-qr"', 'class="code-subblock"'): for marker in ('class="title-qr"', 'class="code-subblock"'):
if marker not in html: if marker not in html:
raise AssertionError(f"HTML nie zawiera znacznika {marker!r}") raise AssertionError(f"HTML nie zawiera znacznika {marker!r}")
def check_section_assets() -> None:
with tempfile.TemporaryDirectory(prefix="card-layouts-assets-") as temp_dir:
card_root = Path(temp_dir) / "card"
shutil.copytree(EXAMPLE_CARD, card_root, ignore=shutil.ignore_patterns("build"))
source_path = card_root / "json" / "card_source.json"
source = load_json(source_path)
source["template"] = str(ROOT / "templates" / "karta-klasyczna.json")
source["generated"] = {
"tex": "build/tex/main.tex",
"html": "build/html/index.html",
"html_css": "build/html/style.css",
}
source["sections"][0]["content_tex"] += (
r" \textbf{Widok \texttt{GDB}.}"
"\\begin{verbatim}stemctl test demo\\end{verbatim}"
)
source["sections"][0]["assets"] = [
{
"path": "assets/debug-screen.png",
"caption": "Kontrolny zrzut debuggera.",
"label": "fig:debug-screen",
"alt": "Zrzut debuggera",
"kind": "screenshot",
"width": 0.96,
}
]
source["sections"][0]["steps"][0]["figure_refs"] = ["fig:debug-screen"]
source_path.write_text(json.dumps(source, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
asset_path = card_root / "doc" / "assets" / "debug-screen.png"
asset_path.parent.mkdir(parents=True, exist_ok=True)
asset_path.write_bytes(
base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
)
)
subprocess.run(
[sys.executable, str(ROOT / "tools" / "render_card.py"), str(card_root)],
cwd=ROOT,
check=True,
stdout=subprocess.DEVNULL,
)
tex = (card_root / "build" / "tex" / "main.tex").read_text(encoding="utf-8")
html = (card_root / "build" / "html" / "index.html").read_text(encoding="utf-8")
copied_asset = card_root / "build" / "html" / "figures" / "debug-screen.png"
for marker in ("fig:debug-screen", "debug-screen.png", "Kontrolny zrzut debuggera"):
if marker not in tex:
raise AssertionError(f"TeX assetu nie zawiera znacznika {marker!r}")
if marker not in html:
raise AssertionError(f"HTML assetu nie zawiera znacznika {marker!r}")
if "\x00" in html:
raise AssertionError("HTML assetu zawiera bajt NUL")
if r"\begin{verbatim}" in html or r"\end{verbatim}" in html:
raise AssertionError("HTML zawiera surowe środowisko verbatim")
if "<pre><code>stemctl test demo</code></pre>" not in html:
raise AssertionError("HTML nie zawiera wyrenderowanego bloku verbatim")
if not copied_asset.is_file():
raise AssertionError(f"asset nie został skopiowany: {copied_asset}")
def check_external_references() -> None:
with tempfile.TemporaryDirectory(prefix="card-layouts-references-") as temp_dir:
root = Path(temp_dir)
card_root = root / "card"
registry_root = root / "registry"
shutil.copytree(EXAMPLE_CARD, card_root, ignore=shutil.ignore_patterns("build"))
(registry_root / "json").mkdir(parents=True)
(registry_root / "docs").mkdir()
registry_uuid = "03403a8a-e20d-5b17-9004-26d9fe6c3003"
strategy_uuid = "1ecfc474-4fbd-5a56-8ef1-80ae557761d0"
card_uuid = "4abec2d9-79e9-5dca-9040-c60669f51ff8"
registry = {
"schema": "esc-reference-registry.v1",
"uuid": registry_uuid,
"name": "test-registry",
"title": "Rejestr testowy",
"root_path": "..",
"base_url": "https://example.test/materials/",
"entries": [
{
"uuid": strategy_uuid,
"kind": "strategy",
"code": "M02",
"title": "Breakpointy",
"path": "docs/M02.md",
},
{
"uuid": card_uuid,
"kind": "card",
"title": "Wskaźniki i tablice",
"path": "docs/card.md",
},
],
}
registry_path = registry_root / "json" / "reference_registry.json"
registry_path.write_text(
json.dumps(registry, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
(registry_root / "docs" / "M02.md").write_text("# M02\n", encoding="utf-8")
(registry_root / "docs" / "card.md").write_text("# Karta\n", encoding="utf-8")
source_path = card_root / "json" / "card_source.json"
source = load_json(source_path)
source["template"] = str(ROOT / "templates" / "karta-klasyczna.json")
source["reference_registries"] = [
{"uuid": registry_uuid, "source": str(registry_path)}
]
source["sections"][0]["steps"][0]["references"] = [
{"kind": "strategy", "registry_uuid": registry_uuid, "uuid": strategy_uuid},
{"kind": "card", "registry_uuid": registry_uuid, "uuid": card_uuid},
]
source_path.write_text(
json.dumps(source, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
subprocess.run(
[sys.executable, str(ROOT / "tools" / "render_card.py"), str(card_root)],
cwd=ROOT,
check=True,
stdout=subprocess.DEVNULL,
)
tex = (card_root / "build" / "tex" / "main.tex").read_text(encoding="utf-8")
html = (card_root / "build" / "html" / "index.html").read_text(encoding="utf-8")
expected = (
"https://example.test/materials/docs/M02.md",
"https://example.test/materials/docs/card.md",
"M02",
"Wskaźniki i tablice",
)
for marker in expected:
if marker not in tex:
raise AssertionError(f"TeX referencji nie zawiera {marker!r}")
if marker not in html:
raise AssertionError(f"HTML referencji nie zawiera {marker!r}")
if f'data-reference-uuid="{strategy_uuid}"' not in html:
raise AssertionError("HTML nie zachowuje UUID rozwiązanej strategii")
if "<code>M02</code>" not in html:
raise AssertionError("HTML nie pokazuje kodu strategii jako etykiety")
def main() -> int: def main() -> int:
check_json_files() check_json_files()
check_optional_json_schema() check_optional_json_schema()
check_render() check_render()
check_section_assets()
check_external_references()
print("PASS: schematy, layouty, przykład serii oraz render TeX/HTML są spójne.") print("PASS: schematy, layouty, przykład serii oraz render TeX/HTML są spójne.")
return 0 return 0
+1037 -95
View File
File diff suppressed because it is too large Load Diff