feat: add repeated dual-QR A4 resource headers
This commit is contained in:
@@ -20,6 +20,7 @@ card-layouts/
|
||||
├── schemas/
|
||||
│ ├── card-source.schema.json
|
||||
│ ├── card-template.schema.json
|
||||
│ ├── reference-registry.schema.json
|
||||
│ └── series.schema.json
|
||||
├── templates/
|
||||
│ ├── 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`.
|
||||
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
|
||||
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
|
||||
`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
|
||||
|
||||
- zmiana treści należy do `card_source.json`;
|
||||
|
||||
+101
-6
@@ -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`
|
||||
wybiera layout.
|
||||
|
||||
Opcjonalny obiekt `title_block` dodaje do pierwszego nagłówka tekstową
|
||||
tabliczkę dokumentu: osoby odpowiedzialne, kategorię, tytuł, URL, rewizję,
|
||||
datę wydania, serię, typ, narzędzie i arkusz. Pole `url` jest kodowane także
|
||||
lokalnie w QR (SVG w HTML i pakiet `qrcode` w TeX). Gdy URL nie jest podany,
|
||||
generator pokazuje w tej komórce informację `brak URL`.
|
||||
Opcjonalny obiekt `title_block` włącza tabliczkę zasobu o wysokości dokładnie
|
||||
3 cm na każdej stronie, także pierwszej, bibliografii, słownika i stronach
|
||||
kontynuacji. Tabliczka zastępuje dużą tabelę metadanych strony tytułowej.
|
||||
Zawiera kategorię, tytuł, metadane karty, numer strony oraz widoczny,
|
||||
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
|
||||
|
||||
@@ -49,6 +63,42 @@ Pole `sections[]` porządkuje treść. Kroki `sections[].steps[]` mają:
|
||||
- `tree_refs` wskazujące drzewka z marginesów;
|
||||
- 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ż:
|
||||
|
||||
- `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
|
||||
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
|
||||
|
||||
`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
|
||||
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
|
||||
|
||||
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,
|
||||
zadań i odwołań, a dopiero potem zapisuje oba formaty.
|
||||
zadań, rejestrów UUID i odwołań, a dopiero potem zapisuje oba formaty.
|
||||
|
||||
@@ -34,13 +34,18 @@
|
||||
"checked_by": "—",
|
||||
"approved_by": "—",
|
||||
"url": "https://demo.example.edu/cards/demo-01",
|
||||
"repository_url": "https://gitea.example.edu/edu/cards/demo-01",
|
||||
"revision": "1",
|
||||
"issued_on": "15.07.2026",
|
||||
"series": "DEMO-01",
|
||||
"document_type": "karta pracy",
|
||||
"tool": "card-layouts",
|
||||
"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,
|
||||
"metric_fill_fields": [
|
||||
|
||||
@@ -47,6 +47,23 @@
|
||||
"side_margin_tree_layout": {
|
||||
"$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": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
@@ -147,6 +164,26 @@
|
||||
}
|
||||
},
|
||||
"$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": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -174,6 +211,10 @@
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"series_title": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"number": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
@@ -242,18 +283,65 @@
|
||||
"checked_by": { "type": "string" },
|
||||
"approved_by": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"url_host_uuid": { "type": "string" },
|
||||
"url": {
|
||||
"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"] },
|
||||
"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" },
|
||||
"issued_on": { "type": "string" },
|
||||
"series": { "type": "string" },
|
||||
"document_type": { "type": "string" },
|
||||
"tool": { "type": "string" },
|
||||
"sheet": { "type": "string" },
|
||||
"show_qr": { "type": "boolean" }
|
||||
"sheet": {
|
||||
"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
|
||||
},
|
||||
"sideMarginLayout": {
|
||||
@@ -469,10 +557,49 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/sectionStep"
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sectionAsset"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "tree_refs"],
|
||||
@@ -506,6 +633,12 @@
|
||||
},
|
||||
"figure_refs": {
|
||||
"$ref": "#/$defs/stringList"
|
||||
},
|
||||
"references": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/externalReference"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
@@ -520,6 +653,21 @@
|
||||
"criterion": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"flow": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/taskFlowItem"
|
||||
}
|
||||
},
|
||||
"assessment_criterion_ref": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -532,6 +680,56 @@
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": ["step", "action", "condition"],
|
||||
|
||||
@@ -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
@@ -2,9 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -86,18 +89,174 @@ def check_render() -> None:
|
||||
raise AssertionError(f"TeX nie zawiera znacznika {marker!r}")
|
||||
if marker not in html:
|
||||
raise AssertionError(f"HTML nie zawiera znacznika {marker!r}")
|
||||
|
||||
if r"\qrcode[height=" not in tex:
|
||||
if "\x00" in html:
|
||||
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")
|
||||
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"'):
|
||||
if marker not in html:
|
||||
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:
|
||||
check_json_files()
|
||||
check_optional_json_schema()
|
||||
check_render()
|
||||
check_section_assets()
|
||||
check_external_references()
|
||||
print("PASS: schematy, layouty, przykład serii oraz render TeX/HTML są spójne.")
|
||||
return 0
|
||||
|
||||
|
||||
+1042
-100
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user