diff --git a/.claude/docs-guidelines/repo-conventions.md b/.claude/docs-guidelines/repo-conventions.md index b142e23db..ac38f16de 100644 --- a/.claude/docs-guidelines/repo-conventions.md +++ b/.claude/docs-guidelines/repo-conventions.md @@ -127,6 +127,11 @@ Do not write `index.html` stubs under `docs/`. - `class="bordered"` on every product screenshot (436 uses in the tree). - `width="50%"` or `width="70%"` for dialogs and modals, no width for full-screen views. Keep the width consistent within one page. +- A screenshot needs a width small enough for its pixels, or it prints coarse: an image of 900 pixels + across the full 16 cm column prints at 143 ppi. `poetry run dec-tool image-widths` lists every raster + image in a page that is not generated that prints below 150 ppi, and `--fix` writes the `width="NN%"` + that reaches it — the pixel width divided by the column and the target, rounded down. The same width + then governs the site. - `.off-glb` opts an image out of the glightbox lightbox — used for inline icons and decorative images. - File names: lowercase, hyphen-separated, descriptive (style guide 5.5). - Delete screenshots that are no longer referenced. Orphan check for one page directory: @@ -204,6 +209,24 @@ Known drift to fix when touching a page: 32 `shell-session` blocks in hand-writt numbering restarts at 1 in the rendered page. Check the rendered result for any step list you touch. - `sane_lists` is enabled: an unordered item does not continue an ordered list. +## Leaving content out of the print edition + +The print edition of the PDF (`task pdf:print`, `tasks/spec.md` §10) can leave out content that only makes sense online. + +- A subtree or a single page: an `omit` entry under `sections` in `tools/pdf/print.yml`, keyed by its `docs/` path. + The page itself carries no marker, so this works for generated pages too. +- A part of a page: the class `print-exclude` on the block. The print edition prints a note in its place that points + to the page online; the site and the screen PDF show the part unchanged. + +| Block | Markup | +| --- | --- | +| code block | `sql { .print-exclude }` right after the backticks of the opening fence | +| admonition or collapsible block | `??? example print-exclude "INSERT query"` | +| paragraph | `{ .print-exclude }` on the line after the paragraph | +| several blocks | ``, with blank lines around the content | + +A generated page cannot carry the class: the next generator run removes it. + ## Editorial decisions beyond the style guide `style-guide.md` is a transcription of the Confluence page and is not extended locally. diff --git a/.github/workflows/pdf.yml b/.github/workflows/pdf.yml index 61c481a27..65d23083c 100644 --- a/.github/workflows/pdf.yml +++ b/.github/workflows/pdf.yml @@ -1,18 +1,24 @@ --- name: PDF -# Builds the PDF of the whole documentation after every push to main, merged -# pull requests included, and keeps it as an artifact of the workflow run. +# Builds every PDF of the documentation after a push to main or to the +# print-on-demand branch: the screen edition, the print edition (the book block +# for print on demand), its PDF/X-4 copy in CMYK and the greyscale preview. The +# run summary lists all four with their download links, so they need not be dug +# out of the artifact section. on: push: - branches: ["main"] + branches: ["main", "feature/print-on-demand"] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: permissions: contents: read -# A newer push to main makes a running build obsolete. +# A newer push to the same branch makes a running build obsolete; main and a +# branch do not cancel each other. concurrency: - group: "pdf" + group: "pdf-${{ github.ref }}" cancel-in-progress: true env: @@ -62,8 +68,10 @@ jobs: - name: update packages run: sudo apt-get update -y + # Poppler carries the pdffonts, pdfimages and pdftotext the preflight report + # reads. Ghostscript comes from a container below, not from apt. - name: install packages - run: sudo apt-get install -y libffi-dev libz-dev + run: sudo apt-get install -y libffi-dev libz-dev poppler-utils - name: install pandoc run: | @@ -79,14 +87,119 @@ jobs: | tar -xJ -C "$RUNNER_TEMP" sudo install -m 0755 "$RUNNER_TEMP/typst-x86_64-unknown-linux-musl/typst" /usr/local/bin/typst - - name: build pdf + # The ISO Coated v2 profile of the output intent is not in the repository + # (its licence forbids it), so it is fetched once and kept between runs. + - name: cache the output intent profile + uses: actions/cache@v4 + with: + path: dist/icc + key: icc-iso-coated-v2-eci + + # Ubuntu ships Ghostscript 10.02, where `-dPDFX=4` is still a boolean, so + # the 4 raises `/typecheck in --pdfmark--` and PDF/X-4 cannot be written at + # all. A container carries a newer one, behind a shim that runs it at the + # paths the build uses, so the PDFs, the pdfmark prefix and the profiles + # resolve inside the container exactly as they do outside. The grey output + # intent profile comes out of the same image. + - name: ghostscript from a container + run: | + printf 'FROM alpine:edge\nRUN apk add --no-cache ghostscript\n' | docker build -q -t pdfx-gs - + W="$GITHUB_WORKSPACE" + sudo tee /usr/local/bin/gs-docker >/dev/null < dist/icc/default_gray.icc + { + echo "GHOSTSCRIPT=/usr/local/bin/gs-docker" + echo "PDF_GRAY_PROFILE=$W/dist/icc/default_gray.icc" + } >> "$GITHUB_ENV" + + - name: build the screen pdf run: task pdf - - name: upload pdf + - name: build the print pdfs + run: task pdf:print -- --normalize --gray + + - name: upload the screen pdf + id: screen uses: actions/upload-artifact@v4 with: - name: documentation-eccenca-com-pdf - path: dist/documentation-eccenca-com-*.pdf + name: pdf-screen + path: | + dist/documentation-eccenca-com-*.pdf + !dist/documentation-eccenca-com-*-print*.pdf if-no-files-found: error + retention-days: 30 # A PDF is compressed already. compression-level: 0 + + - name: upload the print pdf + id: print + uses: actions/upload-artifact@v4 + with: + name: pdf-print + path: dist/documentation-eccenca-com-*-print.pdf + if-no-files-found: error + retention-days: 30 + compression-level: 0 + + - name: upload the pdf/x-4 pdf + id: pdfx + uses: actions/upload-artifact@v4 + with: + name: pdf-print-x4 + path: dist/documentation-eccenca-com-*-print-x4.pdf + if-no-files-found: error + retention-days: 30 + compression-level: 0 + + - name: upload the greyscale pdf + id: gray + uses: actions/upload-artifact@v4 + with: + name: pdf-print-gray-x4 + path: dist/documentation-eccenca-com-*-print-gray-x4.pdf + if-no-files-found: error + retention-days: 30 + compression-level: 0 + + # The table is built in one go and printed to the log as well, so the + # editions and their links are readable even if the summary ever stays + # empty - which is how the first run of this step ended. + - name: list the pdfs in the run summary + run: | + # $1 is a glob; $4, when given, drops what the glob also matches - the + # screen edition is the one file whose name carries no `-print`. + describe() { + file="$(ls $1 2>/dev/null | grep -v -- "${4:-^$}" | head -1)" + if [ -z "$file" ]; then + printf '| %s | - | - | missing |\n' "$2" + return + fi + printf '| %s | %s | %s | [%s](%s) |\n' "$2" \ + "$(pdfinfo "$file" 2>/dev/null | awk '/^Pages/ {print $2}')" \ + "$(du -h "$file" | cut -f1)" "$(basename "$file")" "$3" + } + { + echo "## PDF editions of ${GITHUB_REF_NAME}" + echo + echo "| Edition | Pages | Size | Download |" + echo "| :------ | ----: | ---: | :------- |" + describe "dist/documentation-eccenca-com-*.pdf" \ + "Screen" "${{ steps.screen.outputs.artifact-url }}" "-print" + describe "dist/documentation-eccenca-com-*-print.pdf" \ + "Print, book block" "${{ steps.print.outputs.artifact-url }}" + describe "dist/documentation-eccenca-com-*-print-x4.pdf" \ + "Print, PDF/X-4 in CMYK" "${{ steps.pdfx.outputs.artifact-url }}" + describe "dist/documentation-eccenca-com-*-print-gray-x4.pdf" \ + "Print, greyscale preview" "${{ steps.gray.outputs.artifact-url }}" + echo + echo "The print editions carry the preflight report of \`dec-tool pdf-preflight\`;" + echo "a failed check fails this job." + } | tee -a "$GITHUB_STEP_SUMMARY" + echo "summary file: $GITHUB_STEP_SUMMARY, $(wc -c < "$GITHUB_STEP_SUMMARY") bytes" diff --git a/.gitignore b/.gitignore index e0d7d8f02..42afcc295 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ tmp .python-version dist/ .env +*.sw[a-p] +*~ diff --git a/Taskfile.yml b/Taskfile.yml index 837b9c496..4a4b6c45a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -69,6 +69,7 @@ tasks: cmds: - task: check:links - task: check:rumdl + - task: check:images - task: check:yamllint - task: check:navigation - task: check:output @@ -82,6 +83,18 @@ tasks: # CI run agree: GitHub runners have no user-level yamllint config. - poetry run yamllint . + check:images: + desc: Fail if a screenshot prints below 150 ppi in the print edition + summary: | + Runs `dec-tool image-widths`: every raster image in a page that is not + generated must carry a width small enough for its pixels, or it prints + coarse (tasks/spec.md, §11, D18). `dec-tool image-widths --fix` writes + the width a listed image needs. + deps: + - install + cmds: + - poetry run dec-tool image-widths + check:navigation: desc: Fail if nav.yml is out of sync with the docs/**/.pages files deps: @@ -200,6 +213,60 @@ tasks: cmds: - poetry run dec-tool build-pdf + pdf:print: + desc: Build the print edition of the PDF - the book block for print on demand + summary: | + Runs `task build`, then `dec-tool build-pdf --edition print`: the same + merge, pandoc run and fonts as `task pdf`, typeset as the book block of + a printed book (BoD, A4, black and white). tasks/spec.md describes it: + mirrored page furniture, right-hand part starts, title page and + imprint, links printed as page references and footnotes, and images + normalized to 300 ppi without transparency. + + tools/pdf/print.yml configures the edition: the publisher, the authors' + names and exclusions, and how each navigation section is printed (full, + list or omit). tools/pdf/authors.yml is the imprint's author list; + refresh it with `task pdf:authors`. + + Writes dist/documentation-eccenca-com--print.pdf; the merged HTML, + the Typst source and the normalized images stay in dist/pdf/print/. The + screen PDF of `task pdf` is not affected. + + The build lists images below 150 ppi at their printed size in + dist/pdf/print/low-resolution.tsv and runs the preflight report + (`dec-tool pdf-preflight`), failing on an error. `task pdf:print -- + --normalize` (or PDF_NORMALIZE=1) also writes a PDF/X-4 copy in CMYK + with Ghostscript, -print-x4.pdf, and checks that one; the ISO Coated v2 + profile is fetched from the ECI into dist/icc/ unless PDF_ICC_PROFILE + names one. `task pdf:print -- --gray` (or PDF_GRAY=1) writes a + greyscale preview, -print-gray-x4.pdf, to check on screen how the book + prints in black and white; it is PDF/X-4 by the grey profile Ghostscript + ships, and the two flags combine. + deps: + - task: build + env: + BUILD_VERSION: '{{.CURRENT_VERSION}}' + cmds: + - poetry run dec-tool build-pdf --edition print {{.CLI_ARGS}} + + pdf:authors: + desc: Refresh the author list of the print edition's imprint + summary: | + Writes tools/pdf/authors.yml from the commits to what the print edition + prints - its pages that are not generated, and the images they + reference: most commits first, with the names the GitHub profiles show, + without anonymous commits, bot accounts, software agents and the IDs + excluded in tools/pdf/print.yml. Adds each author that + print.yml does not list yet to its authors.names, without a name, and + warns about each author who would print as a GitHub ID. Commit both + files. Uses GITHUB_TOKEN or GH_TOKEN, else the token of a logged-in + GitHub CLI (`gh auth login`); without a token GitHub allows 60 + requests an hour, and a run takes one per author. + deps: + - task: install + cmds: + - poetry run dec-tool pdf-authors + update:icons: desc: update the used eccenca icons from carbon cmds: diff --git a/docs/automate/processing-data-with-variable-input-workflows/index.md b/docs/automate/processing-data-with-variable-input-workflows/index.md index 1470475dc..169067499 100644 --- a/docs/automate/processing-data-with-variable-input-workflows/index.md +++ b/docs/automate/processing-data-with-variable-input-workflows/index.md @@ -42,7 +42,7 @@ Click the :eccenca-module-marketplace: **Packages** icon in the main menu under Search for the required ontologies / vocabularies and click the **Install** button. Wait for a package installation to complete (the **Install** button will change to **Uninstall**) before installing the next package. -![Search and install ontologies](pdwviw-marketplace-schema-search.png){ class="bordered" } +![Search and install ontologies](pdwviw-marketplace-schema-search.png){ class="bordered" width="90%" } ## 2 Create a new project diff --git a/docs/build/extracting-data-from-a-web-api/index.md b/docs/build/extracting-data-from-a-web-api/index.md index 975525b72..54e7005af 100644 --- a/docs/build/extracting-data-from-a-web-api/index.md +++ b/docs/build/extracting-data-from-a-web-api/index.md @@ -95,7 +95,7 @@ To create a JSON-to-RDF-mapping within Corporate Memory, we have to first regist 1. Click the **Create** button (top right) in the data integration workspace and select the type **JSON**. - ![Create JSON dataset](create-new-json-dataset.png) + ![Create JSON dataset](create-new-json-dataset.png){ width="71%" } 2. Upload the JSON file [repos.json](repos.json) (API response) as a Dataset into Corporate Memory. diff --git a/docs/build/lift-data-from-json-and-xml-sources/index.md b/docs/build/lift-data-from-json-and-xml-sources/index.md index c3e97ec47..3098a6f75 100644 --- a/docs/build/lift-data-from-json-and-xml-sources/index.md +++ b/docs/build/lift-data-from-json-and-xml-sources/index.md @@ -309,7 +309,7 @@ The transformation defines how an input dataset (e.g.: JSON or XML) will be tran === "JSON" - ![Examples of target data JSON](ldfjaxs-json-examples-target-data.png){ class="bordered" } + ![Examples of target data JSON](ldfjaxs-json-examples-target-data.png){ class="bordered" width="64%" } === "XML" diff --git a/docs/build/loading-jdbc-datasets-incrementally/index.md b/docs/build/loading-jdbc-datasets-incrementally/index.md index 3461908b3..bd38907f6 100644 --- a/docs/build/loading-jdbc-datasets-incrementally/index.md +++ b/docs/build/loading-jdbc-datasets-incrementally/index.md @@ -37,13 +37,13 @@ To extract data from a relational database, you need to first register a **JDBC 1. In Corporate Memory, click Projects under **BUILD** in the navigation on the left side of the page. - ![Menu BUILD > Projects](incr-jdbc-menu-build-projects.png){ class="bordered" } + ![Menu BUILD > Projects](incr-jdbc-menu-build-projects.png){ class="bordered" width="29%" } 2. Click **Create** at the top of the page. 3. In the Create new item window, select Project and click Add. The Create new item of type Project window appears. 4. In the Create new item window, select Dataset under **Item Type**, search for JDBC endpoint, and click **Add**. - ![Create JDBC Dataset](create-dataset-JDBC.png){ class="bordered" } + ![Create JDBC Dataset](create-dataset-JDBC.png){ class="bordered" width="71%" } 5. Provide the required configuration details for the JDBC endpoint: - **Label**: Provide a table name. diff --git a/docs/build/reference/transformer/Replace/excelMap.md b/docs/build/reference/transformer/Replace/excelMap.md index ae0489f48..a04d8873c 100644 --- a/docs/build/reference/transformer/Replace/excelMap.md +++ b/docs/build/reference/transformer/Replace/excelMap.md @@ -1,6 +1,6 @@ --- title: "Excel map" -description: "Replaces values based on a map of values read from a file in Open XML format (XLSX). The XLSX file may contain several sheets of the form: ```mapFrom,mapTo ,``` and more. An empty string can be created in Excel and alternatives by inserting `=''` in the input line of a cell. If there are multiple values for a single key, all values will be returned for the given key. Note that the mapping table will be cached in memory. If the Excel file is updated (even while transforming), the map will be reloaded within seconds." +description: "Replaces values based on a map of values read from a file in Open XML format (XLSX). The XLSX file may contain several sheets of the form: `mapFrom,mapTo ,` and more. An empty string can be created in Excel and alternatives by inserting `=''` in the input line of a cell. If there are multiple values for a single key, all values will be returned for the given key. Note that the mapping table will be cached in memory. If the Excel file is updated (even while transforming), the map will be reloaded within seconds." icon: octicons/cross-reference-24 tags: - TransformOperator diff --git a/docs/build/reference/transformer/index.md b/docs/build/reference/transformer/index.md index 74762b5d3..a9e31201b 100644 --- a/docs/build/reference/transformer/index.md +++ b/docs/build/reference/transformer/index.md @@ -94,8 +94,7 @@ Transform operators transform a one or more sequences of string values to a sequ | [Excel Ln](Excel/Excel_LN.md) | Excel | Excel LN(number): Returns the natural logarithm based on the constant e of the given number. | | [Excel Log](Excel/Excel_LOG.md) | Excel | Excel LOG(number; base): Returns the logarithm of the given number to the specified base. Base is the base for the logarithm calculation. | | [Excel Log10](Excel/Excel_LOG10.md) | Excel | Excel LOG10(number): Returns the base-10 logarithm of the given number. | - - | [Excel map](Replace/excelMap.md) | Replace | Replaces values based on a map of values read from a file in Open XML format (XLSX). The XLSX file may contain several sheets of the form: ```mapFrom,mapTo ,``` and more. An empty string can be created in Excel and alternatives by inserting `=""` in the input line of a cell. If there are multiple values for a single key, all values will be returned for the given key. Note that the mapping table will be cached in memory. If the Excel file is updated (even while transforming), the map will be reloaded within seconds. | + | [Excel map](Replace/excelMap.md) | Replace | Replaces values based on a map of values read from a file in Open XML format (XLSX). The XLSX file may contain several sheets of the form: `mapFrom,mapTo ,` and more. An empty string can be created in Excel and alternatives by inserting `=""` in the input line of a cell. If there are multiple values for a single key, all values will be returned for the given key. Note that the mapping table will be cached in memory. If the Excel file is updated (even while transforming), the map will be reloaded within seconds. | | [Excel Max](Excel/Excel_MAX.md) | Excel | Excel MAX(number_1; number_2; ... number_30): Returns the maximum value in a list of arguments. Number_1; number_2; ... number_30 are numerical values or ranges. | | [Excel Maxa](Excel/Excel_MAXA.md) | Excel | Excel MAXA(value_1; value_2; ... value_30): Returns the maximum value in a list of arguments. Unlike MAX, text can be entered. The value of the text is 0. Value_1; value_2; ... value_30 are values or ranges. | | [Excel Median](Excel/Excel_MEDIAN.md) | Excel | Excel MEDIAN(number_1; number_2; ... number_30): Returns the median of a set of numbers. Number_1; number_2; ... number_30 are values or ranges, which represent a sample. Each number can also be replaced by a reference. | diff --git a/docs/build/rule-blocks/index.md b/docs/build/rule-blocks/index.md index 6be5611ba..a000e09dd 100644 --- a/docs/build/rule-blocks/index.md +++ b/docs/build/rule-blocks/index.md @@ -89,7 +89,7 @@ Dragging **New input port** onto the canvas creates a port as well. Existing ports are listed below that entry. Click the **:material-pencil-outline: edit** icon of a port to open the **Edit input port** dialog, or the **:material-delete-outline: delete** icon to remove it: -![The Edit input port dialog](rule-blocks-input-port.png "The Edit input port dialog") +![The Edit input port dialog](rule-blocks-input-port.png "The Edit input port dialog"){ width="62%" } **Label** : The name of the port. @@ -145,7 +145,7 @@ It has one input handle per input port, in the port order defined in the rule bl Its context menu offers the actions specific to the reference: -![The context menu of a rule block node](rule-blocks-node-menu.png "The context menu of a rule block node") +![The context menu of a rule block node](rule-blocks-node-menu.png "The context menu of a rule block node"){ width="32%" } **Clone** : Adds a second reference to the same rule block. @@ -187,7 +187,7 @@ That also means a change can break those rules, so the editor restricts what may Click the **:material-information-outline: Usage status** button next to **Save** to see the current state: -![The usage status of a rule block](rule-blocks-usage-status.png "The usage status of a rule block") +![The usage status of a rule block](rule-blocks-usage-status.png "The usage status of a rule block"){ width="75%" } While a rule block is in use: diff --git a/docs/build/rule-operators/index.md b/docs/build/rule-operators/index.md index e7a94e531..f39516b03 100644 --- a/docs/build/rule-operators/index.md +++ b/docs/build/rule-operators/index.md @@ -54,7 +54,7 @@ The following parameters can be set for each comparison: !!! info inline end "" - ![Confidence graph](confidence-graph.png) + ![Confidence graph](confidence-graph.png){ width="38%" } The threshold is used to convert the computed distance to a confidence between -1.0 and 1.0. Links will be generated for confidences above 0 while higher confidence values imply a higher similarity between the compared entities. diff --git a/docs/build/snowflake-tutorial/index.md b/docs/build/snowflake-tutorial/index.md index 3f898c32c..67b1084b3 100644 --- a/docs/build/snowflake-tutorial/index.md +++ b/docs/build/snowflake-tutorial/index.md @@ -89,7 +89,7 @@ Here you can populate some test data with the following SQL query: ??? example "INSERT query" - ```sql + ```sql { .print-exclude } INSERT INTO product(product_id,product_name,height,width,depth,weigth,product_manager,price) VALUES ('I241-8776317','Strain Compensator',12,68,15,8,'Baldwin.Dirksen@company.org','0,50 EUR') ,('D215-3449390','Gauge Crystal',77,58,19,15,'Wanja.Hoffmann@company.org','2,00 EUR') @@ -1210,7 +1210,7 @@ Here you can populate some test data with the following SQL query: - Select the **target property** according to transformation requirements, for example name, id, etc., then select the **value path** according to the target property as the product name, product id etc. This step will help in mapping the data from the source to the target property. - ![image](snowflake-target-property.png){ class="bordered" } + ![image](snowflake-target-property.png){ class="bordered" width="91%" } - Type the label name **product name**, then click on **Save**. @@ -1232,7 +1232,7 @@ Here you can populate some test data with the following SQL query: Suggestion appears as below can select as per the requirement. - ![image](snowflake-suggestion-result.png){ class="bordered" } + ![image](snowflake-suggestion-result.png){ class="bordered" width="91%" } !!! note @@ -1240,7 +1240,7 @@ Here you can populate some test data with the following SQL query: - **Tick** the box to select the suggestions to be added, then click on **Add**. -![image](snowflake-tick.png){ class="bordered" } +![image](snowflake-tick.png){ class="bordered" width="93%" } ## 4. Create a knowledge graph diff --git a/docs/build/tutorial-how-to-link-ids-to-osint/define-the-interfaces/index.md b/docs/build/tutorial-how-to-link-ids-to-osint/define-the-interfaces/index.md index 23fe2db7c..c936fd293 100644 --- a/docs/build/tutorial-how-to-link-ids-to-osint/define-the-interfaces/index.md +++ b/docs/build/tutorial-how-to-link-ids-to-osint/define-the-interfaces/index.md @@ -10,21 +10,21 @@ In this tutorial, we will show how to (1) define the information available in an In the previous tutorial, we have defined the use cases in contact with the humans, ie. analysts (see figure 1)
-![Figure 1. We need to imagine an interface where analysts can list the IoCs during the incident and read all their documentations.](use_cases_with_interfaces.png) +![Figure 1. We need to imagine an interface where analysts can list the IoCs during the incident and read all their documentations.](use_cases_with_interfaces.png){ width="50%" }
Figure 1. We need to imagine an interface where analysts can list the IoCs during the incident and read all their documentations.
The classic Splunk interface is a set of panels, like "static table" panel. This table panel can show a table of cells and also one cell with a text via in input a Splunk Search Processing Language (SPL). With the plugin [Linked Data App](../link-IDS-event-to-KG/eccenca_commands.tar.gz) ([tutorial page](../link-IDS-event-to-KG/index.md)), we can insert a SPARQL query and select the part of your knowledge graph to print (figure 2).
-![Figure 2. An analyst can insert a SPARQL query with Splunk token in input of one "static table" panel of his dashboard with the plugin "Linked Data App"](demo-splunk-with-kg-edit.png) +![Figure 2. An analyst can insert a SPARQL query with Splunk token in input of one "static table" panel of his dashboard with the plugin "Linked Data App"](demo-splunk-with-kg-edit.png){ width="75%" }
Figure 2. An analyst can insert a SPARQL query with Splunk token in input of one "static table" panel of his dashboard with the plugin "Linked Data App"
The first dashboard to do for our use cases is the list of IoCs with classic SPL queries of analysts via a static table and allow the analyst to select one IoC via a click in the table. The dashboard with this selected row can save the ID of IoC in a global variable for the other panels in the same dashboard ([a Splunk token](https://docs.splunk.com/Documentation/Splunk/9.0.5/Viz/tokens)). When this variable (Splunk token) is changed by the user, Splunk is able to recalculate automatically the queries with this variable in the other static tables. So with this mechanism, we can print the details in the knowledge graph (with SPARQL queries) and the IoC statistics in the Splunk indexes (with SPL queries) around of one selected IoC. With these knowledge about Splunk dashboard, we proposed to analysts a first naive interface in the figure 3.
-![Figure 3. Imagine the expected Splunk dashboard with its interaction](interface.png) +![Figure 3. Imagine the expected Splunk dashboard with its interaction](interface.png){ width="57%" }
Figure 3. Imagine the expected Splunk dashboard with its interaction
diff --git a/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-STIX-2.1-data-of-mitre-attack/index.md b/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-STIX-2.1-data-of-mitre-attack/index.md index 8d176897b..22e50d0df 100644 --- a/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-STIX-2.1-data-of-mitre-attack/index.md +++ b/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-STIX-2.1-data-of-mitre-attack/index.md @@ -77,7 +77,7 @@ Each domain dataset is saved in GitHub: !!! Success Now, you can see these JSON datasets in Corporate Memory: - ![](23-1-import-JSON-result.png) + ![](23-1-import-JSON-result.png){ width="80%" } ### Create named graphs @@ -118,7 +118,7 @@ Create one RDF dataset for each Mitre dataset: !!! Success Now, you can see these RDF datasets in Corporate Memory: - ![](23-1-create-RDF-dataset-result.png) + ![](23-1-create-RDF-dataset-result.png){ width="80%" } !!! Tip @@ -404,7 +404,7 @@ Create one RDF dataset for each Mitre dataset: You can test the result when you search the Mitre ID via the explorer of knowledge graph "MA Entreprise": - ![](23-1-success-extract-rdfslabel.png) + ![](23-1-success-extract-rdfslabel.png){ width="55%" } ### Create a workflow @@ -754,7 +754,7 @@ After this tutorial, you want probably to navigate in your new knowledge graph b 5. Create a new workflow "MITRE ATT&CK® workflow" where you will insert the other workflows, like that: - ![](23-1-ex-workflow-global.png) + ![](23-1-ex-workflow-global.png){ width="75%" } !!! Success diff --git a/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-YAML-data-of-hayabusa-sigma/index.md b/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-YAML-data-of-hayabusa-sigma/index.md index ec1c1fa19..08db384d9 100644 --- a/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-YAML-data-of-hayabusa-sigma/index.md +++ b/docs/build/tutorial-how-to-link-ids-to-osint/lift-data-from-YAML-data-of-hayabusa-sigma/index.md @@ -134,7 +134,7 @@ This new transformer are building the following RDF model for your use case: - replace `\./hayabusa-rules/` by `https://github.com/Yamato-Security/hayabusa-rules/blob/main/` - replace `\./sigma/` by `https://github.com/SigmaHQ/sigma/blob/master/` - ![](23-1-rules-isdefinedby.png) + ![](23-1-rules-isdefinedby.png){ width="95%" } So the rulepath `./sigma/rules/windows/process_creation/proc_creation_win_bcdedit_boot_conf_tamper.yml` becomes the link `https://github.com/SigmaHQ/sigma/blob/master/rules/windows/process_creation/proc_creation_win_bcdedit_boot_conf_tamper.yml` and `./hayabusa-rules/hayabusa/sysmon/Sysmon_15_Info_ADS-Created.yml`becomes `https://github.com/Yamato-Security/hayabusa-rules/blob/main/hayabusa/sysmon/Sysmon_11_Med_FileCreated_RuleAlert.yml` @@ -155,7 +155,7 @@ This new transformer are building the following RDF model for your use case: And don't forget to allow the replacement of JSON dataset because it allows to replace this specific JSON by all other rules during the execution of this worflow. - ![](23-1-workflow-allow-replacement.png) + ![](23-1-workflow-allow-replacement.png){ width="65%" } ![](23-1-add-worflow.gif) diff --git a/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG-via-cmem/index.md b/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG-via-cmem/index.md index cc9e47626..2e49803f6 100644 --- a/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG-via-cmem/index.md +++ b/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG-via-cmem/index.md @@ -56,17 +56,17 @@ For each investigation, an analyst selects and navigates in the data with two da
-![Figure 2: High-level investigation dashboard with the list of computers implicated in the incident](poc-app_high_level_computers.png) +![Figure 2: High-level investigation dashboard with the list of computers implicated in the incident](poc-app_high_level_computers.png){ width="68%" }
Figure 2: High-level investigation dashboard with the list of computers implicated in the incident
-![Figure 3: High-level investigation dashboard with the panel to select a specific period during an incident according to IoCs details](poc-app_high_level_period.png) +![Figure 3: High-level investigation dashboard with the panel to select a specific period during an incident according to IoCs details](poc-app_high_level_period.png){ width="69%" }
Figure 3: High-level investigation dashboard with the panel to select a specific period during an incident according to IoCs details
-![Figure 4: low-level investigation dashboard contains the command lines of Windows processus rised Suricata alerts during the period selected by the analyst in a high-level investigation dashboard](poc-app_low_level.png) +![Figure 4: low-level investigation dashboard contains the command lines of Windows processus rised Suricata alerts during the period selected by the analyst in a high-level investigation dashboard](poc-app_low_level.png){ width="73%" }
Figure 4: low-level investigation dashboard contains the command lines of Windows processus rised Suricata alerts during the period selected by the analyst in a high-level investigation dashboard
@@ -75,7 +75,7 @@ The idea is the analyst can do an investigation high-level without using a lot o To follow the calculation of investigations and free memory when one investigation is closed, we developed another dashboard "investigation_list.xml" (see figure 5). This dashboard prints the status of investigations actually in the knowledge graph. Here, each investigation is saved in tempory graphs and the analyst can create and delete them directly in Splunk.
-![Figure 5: The dashboard "investigation list" shows all the tempory graph actually in the knowledge graph. The analyst can open an investigation, see the SPL query generated when he has created an investigation and delete it when he want.](poc-app_list_investigations.png) +![Figure 5: The dashboard "investigation list" shows all the tempory graph actually in the knowledge graph. The analyst can open an investigation, see the SPL query generated when he has created an investigation and delete it when he want.](poc-app_list_investigations.png){ width="82%" }
Figure 5: The dashboard "investigation list" shows all the tempory graph actually in the knowledge graph. The analyst can open an investigation, see the SPL query generated when he has created an investigation and delete it when he want.
diff --git a/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG/index.md b/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG/index.md index 776a74185..3e4fa775d 100644 --- a/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG/index.md +++ b/docs/build/tutorial-how-to-link-ids-to-osint/link-IDS-event-to-KG/index.md @@ -27,7 +27,7 @@ The "Linked Data App" extends Splunk Search Processing Language (SPL) to support 2. Open the App window in Splunk via the icon "tools" (see figure 1)
-![Figure 1: In the top of the list of installed Splunk apps, you need to click on the icon "tools" to open the window to manage your apps](splunk_apps_menu.png) +![Figure 1: In the top of the list of installed Splunk apps, you need to click on the icon "tools" to open the window to manage your apps](splunk_apps_menu.png){ width="30%" }
Figure 1: In the top of the list of installed Splunk apps, you need to click on the icon "tools" to open the window to manage your apps
diff --git a/docs/build/variables/index.md b/docs/build/variables/index.md index a55d06ce0..213c19e85 100644 --- a/docs/build/variables/index.md +++ b/docs/build/variables/index.md @@ -114,7 +114,7 @@ In order to add project variables, login to eccenca Corporate Memory, select the On the right side of the page you can see the variable section Click on :eccenca-item-add-artefact: to add the variables. -![](di-var-add-variable.png){ class="bordered" } +![](di-var-add-variable.png){ class="bordered" width="80%" } The variable definition dialog opens. Type the name, value and description of the variable. @@ -138,7 +138,7 @@ Type name as `email_ids`, in values we have updated all the email id’s of the The email_ids variable is added as shown below. - ![](di-var-email-defined.png){ class="bordered" } + ![](di-var-email-defined.png){ class="bordered" width="84%" } ## Execution Variables @@ -225,7 +225,7 @@ Select the `project.email_ids` and include the double curly brackets at last. Same as above, add the variable click on :eccenca-item-add-artefact: and type name as `password`, value as `xmhnjnnjkmnlbbhbvfhnbjkm` and in the description `Gmail account password` as shown below then click on **Add**. -![](di-var-password-definition.png){ class="bordered" } +![](di-var-password-definition.png){ class="bordered" width="76%" } !!! success "Step Result" diff --git a/docs/build/workflows/index.md b/docs/build/workflows/index.md index c4c8ca338..313f42bf2 100644 --- a/docs/build/workflows/index.md +++ b/docs/build/workflows/index.md @@ -55,7 +55,7 @@ The **Clear Dataset** operator empties the dataset connected to its output befor Place the Clear Dataset operator in the workflow and connect its output to the dataset that should be cleared. The operator takes no data inputs; connect it using a **dependency connection** from the upstream node that must complete first, or leave it unconnected if the dataset should be cleared before any subsequent nodes execute. -![Clear Datasets](wf-clear-datasets.png) +![Clear Datasets](wf-clear-datasets.png){ width="57%" } Some datasets historically provided their own clear attributes (e.g., `Clear graph before workflow execution` on the Knowledge Graph dataset). These per-dataset attributes are **deprecated** and should no longer be used. Use the Clear Dataset operator instead, which works uniformly across all dataset types. @@ -67,7 +67,7 @@ The operators of a workflow can be reconfigured completely in the context of a w During its execution, new parameters are loaded from any possible source and translated by a transformation task to allow an injection into the dataset configuration that overwrites originally set parameters. To reconfigure a workflow operator, the transformation task has to be connected to the red dot at the top of this operator as shown in the following image: -![Workflow config port](wf-config-port.png) +![Workflow config port](wf-config-port.png){ width="86%" } Although this feature has been developed to support the ingestion of database deltas, the possible applications are various since any parameter can be overwritten to make workflow operators even more dynamic and reusable in various contexts. The incremental ingestion of database content that was implemented as a first use-case can be found the application section of this page. diff --git a/docs/consume/consuming-graphs-in-power-bi/index.md b/docs/consume/consuming-graphs-in-power-bi/index.md index 68f2d7a8b..25050c158 100644 --- a/docs/consume/consuming-graphs-in-power-bi/index.md +++ b/docs/consume/consuming-graphs-in-power-bi/index.md @@ -37,7 +37,7 @@ The latest (unsigned) version of our Power-BI-Connector is available from its so - The registry path is `HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Power BI Desktop` . Make sure the path exists, or create it. - Add a new value under the path specified above. The type should be “Multi-String Value” ( `REG_MULTI_SZ` ), and it should be called `TrustedCertificateThumbprints` - Add the thumbprints of the certificates you want to trust. You can add multiple certificates by using “\0” as a delimiter, or in the registry editor, right click → modify and put each thumbprint on a new line. - ![PowerBI-Registry-Editor.png](22-1-PowerBI-Registry-Editor.png) + ![PowerBI-Registry-Editor.png](22-1-PowerBI-Registry-Editor.png){ width="95%" } - (Re-)Start Power BI Desktop If you wish to automate this setup you can use the reg windows command line tool to make this entry like: diff --git a/docs/consume/consuming-graphs-with-sql-databases/index.md b/docs/consume/consuming-graphs-with-sql-databases/index.md index 096c825a8..8d3e5bb50 100644 --- a/docs/consume/consuming-graphs-with-sql-databases/index.md +++ b/docs/consume/consuming-graphs-with-sql-databases/index.md @@ -29,7 +29,7 @@ In the following, we have a more detailed look at each of the three operators. Create a dataset of the type Knowledge Graph (embedded) and set the graph parameter to the URI of the graph that contains the resources to be exported: -![create-dataset](22-1-1-create-dataset.png) +![create-dataset](22-1-1-create-dataset.png){ width="70%" } ### Create Transformation diff --git a/docs/consume/populate-data-to-neo4j/index.md b/docs/consume/populate-data-to-neo4j/index.md index d29c7ae79..656163eaf 100644 --- a/docs/consume/populate-data-to-neo4j/index.md +++ b/docs/consume/populate-data-to-neo4j/index.md @@ -36,20 +36,20 @@ This tutorial uses the _Multi CSV Zip_ format to efficiently handle the followin To upload multiple files together as an input: 1. In Corporate Memory, click Projects under **BUILD** in the navigation on the left side of the page. - ![Menu > BUILD > Projects](pdtn-menu-projects.png){width="50%"} + ![Menu > BUILD > Projects](pdtn-menu-projects.png){width="29%"} 2. Click Create at the top of the page. 3. In Create new item window, select Project and click Add. The Create new item of type Project window appears. 4. Fill in the required details such as Title and Description.  Alternatively, import the existing project by clicking Import Project File and selecting the file from your system. 5. Click Create. Your project (Northwind) is created. - ![Project Northwind](pdtn-project-northwind.png) + ![Project Northwind](pdtn-project-northwind.png){ width="79%" } 6. In your project, click Create Item. 7. In the Create new item window, select Multi CSV ZIP and click Add. - ![Create Multi CSV Zip dataset](pdtn-create-dataset-multi-csv.png) + ![Create Multi CSV Zip dataset](pdtn-create-dataset-multi-csv.png){ width="64%" } 8. Specify a Label of the dataset in the Create new item of type Multi CSV ZIP window. 9. Select the Upload new file option as you have the files. The Multi CSV ZIP file containing the above-listed files is available here. If it is an existing project, you can select the files from the project. For the remaining parameters, the default settings are used. 10. Click Create. You can see the message northwind.zip was successfully uploaded in Green. 11. You can see the Multi CSV ZIP file is uploaded with the datasets, and the item has been created. - ![Upload file to dataset](pdtn-create-dataset-upload.png) + ![Upload file to dataset](pdtn-create-dataset-upload.png){ width="71%" } 12. Click the Play button and review the dataset in the Data preview section. You can see the contents of the loaded zip file consisting of the CSV files introduced above. ## 2 Create a Neo4j dataset @@ -59,7 +59,7 @@ Perform the following steps to create a Neo4j dataset: 1. In your existing project, click Create to create a new item. 2. In the item category Dataset select Neo4j. - ![Create Neo4j dataset](pdtn-create-dataset-neo4j.png) + ![Create Neo4j dataset](pdtn-create-dataset-neo4j.png){ width="68%" } 3. Click Add. 4. Enter the following details: - Label: _Name of the item_.   @@ -81,7 +81,7 @@ The provided vocabulary is inspired by this structure from the original Neo4j tu The vocabulary for the Northwind project can be visualized like this: -![Northwind vocabulary](pdtn-vocabulary.png) +![Northwind vocabulary](pdtn-vocabulary.png){ width="96%" } ## 4 Create Transformations @@ -263,7 +263,7 @@ Click the Play icon to validate the results. The nodes and edges created are sto ## 6 Results in Neo4j -![Result in Neo4j](pdtn-result-in-neo4j.png) +![Result in Neo4j](pdtn-result-in-neo4j.png){ width="89%" } ## 7 Results in Knowledge Graph @@ -272,4 +272,4 @@ Optionally, you can use the same transformation and workflow to render the resul 1. Add a Knowledge Graph dataset and use this as an additional target in your workflow: ![Add Knowledge Graph output](pdtn-add-kg-dataset-into-workflow.png) 2. The results can then be reviewed in the Knowledge Graph module, e.g., explored visually: - ![Results in Corporate Memory](pdtn-ontodia-result-view.png) + ![Results in Corporate Memory](pdtn-ontodia-result-view.png){ width="72%" } diff --git a/docs/deploy-and-configure/configuration/keycloak/change-passwords-and-keys/index.md b/docs/deploy-and-configure/configuration/keycloak/change-passwords-and-keys/index.md index 8eed7ed7a..498504306 100644 --- a/docs/deploy-and-configure/configuration/keycloak/change-passwords-and-keys/index.md +++ b/docs/deploy-and-configure/configuration/keycloak/change-passwords-and-keys/index.md @@ -50,7 +50,7 @@ To change the default passwords, select the Cmem Realm and open Users in the lef ![users-lookup](22-1-users-lookup.png) -![user-details](22-1-user-details.png) +![user-details](22-1-user-details.png){ width="75%" } Then, select "View all users" and choose an account you want to change the password for (we start with admin) diff --git a/docs/develop/packages/installation/index.md b/docs/develop/packages/installation/index.md index 49788a23a..0da014ce3 100644 --- a/docs/develop/packages/installation/index.md +++ b/docs/develop/packages/installation/index.md @@ -74,7 +74,7 @@ Depending on the content types inside it, an installed package appears in differ !!! info inline "" - ![Example: Graphs](example-vocabulary.png "Example: Graphs") + ![Example: Graphs](example-vocabulary.png "Example: Graphs"){ width="68%" } **Graphs** such as data graphs but also **Vocabularies** or **Shapes Catalogs** are listed in [**Explore > Graphs**](../../../explore-and-author/graph-exploration/index.md#graphs). diff --git a/docs/develop/python-plugins/development/index.md b/docs/develop/python-plugins/development/index.md index 0c9c13ad7..a1b95218b 100644 --- a/docs/develop/python-plugins/development/index.md +++ b/docs/develop/python-plugins/development/index.md @@ -350,7 +350,7 @@ The method returns a list of `Autocompletion` objects, which represent the possi The [cmem-plugin-base](https://github.com/eccenca/cmem-plugin-base/) package describes [context objects](https://github.com/eccenca/cmem-plugin-base/blob/main/cmem_plugin_base/dataintegration/context.py), which are passed to the plugin depending on the executed method. -![context-api-flow-diagram](23-1-context-api-flow-diagram.png) +![context-api-flow-diagram](23-1-context-api-flow-diagram.png){ width="75%" } ### Basic Understanding @@ -453,7 +453,7 @@ The following image shows these terms and their relationships. (1) 1. The concrete implementation details of entities can be found in the [entity module](https://github.com/eccenca/cmem-plugin-base/blob/main/cmem_plugin_base/dataintegration/entity.py) of the cmem-plugin-base package. -![entities-flow-diagram](22-2-entities-flow-diagram.png) +![entities-flow-diagram](22-2-entities-flow-diagram.png){ width="90%" } | Class | Description | | -------------- | ------------------------------------------------------------------ | diff --git a/docs/distribution/marketplace/index.md b/docs/distribution/marketplace/index.md index af7fb3888..dd7b27a27 100644 --- a/docs/distribution/marketplace/index.md +++ b/docs/distribution/marketplace/index.md @@ -61,7 +61,7 @@ They can be combined. !!! info inline "" - ![The open Package type drop-down](marketplace-filter-package-type.png "The open Package type drop-down") + ![The open Package type drop-down](marketplace-filter-package-type.png "The open Package type drop-down"){ width="21%" } #### Package type @@ -73,7 +73,7 @@ Show only **Vocabulary** or only **Project** packages, see [Package Types](#pack !!! info inline "" - ![The open License drop-down](marketplace-filter-license.png "The open License drop-down") + ![The open License drop-down](marketplace-filter-license.png "The open License drop-down"){ width="32%" } #### License @@ -93,7 +93,7 @@ Show only packages whose name or description contains the entered keyword. !!! info inline "" - ![The Installed toggle, switched on](marketplace-filter-installed.png "The Installed toggle, switched on") + ![The Installed toggle, switched on](marketplace-filter-installed.png "The Installed toggle, switched on"){ width="14%" } #### Installed @@ -105,7 +105,7 @@ Switch on to show only the packages currently installed in your instance. !!! info inline "" - ![The open Marketplace drop-down](marketplace-filter-server.png "The open Marketplace drop-down") + ![The open Marketplace drop-down](marketplace-filter-server.png "The open Marketplace drop-down"){ width="38%" } #### Marketplace (top right) diff --git a/docs/explore-and-author/bke-module/visually-authoring-ontologies/index.md b/docs/explore-and-author/bke-module/visually-authoring-ontologies/index.md index fcc002f0e..d203c85d2 100644 --- a/docs/explore-and-author/bke-module/visually-authoring-ontologies/index.md +++ b/docs/explore-and-author/bke-module/visually-authoring-ontologies/index.md @@ -46,7 +46,7 @@ To create a new ontology graph: 1. In Corporate Memory, click **Knowledge graphs** under **EXPLORE** in the navigation on the left side of the page. - ![](explore-kgs.png){ class="bordered" width="50%" } + ![](explore-kgs.png){ class="bordered" width="29%" } 2. In the **Graphs** drop-down menu, click the **(+)** button and select **New Ontology (owl:Ontology)**. @@ -62,7 +62,7 @@ To create a new ontology graph: 1. In Corporate Memory, click **Business Knowledge Editor** under **EXPLORE** in the navigation on the left side of the page. - ![](explore-bke.png){ class="bordered" width="50%" } + ![](explore-bke.png){ class="bordered" width="29%" } 2. Select the target graph using the drop-down menu. @@ -82,7 +82,7 @@ New elements can be created from the entries listed in **Classes** on the left s 1. Drag and drop **Class** from the bottom left list into the canvas. - ![](classes.png){ class="bordered" width="50%" } + ![](classes.png){ class="bordered" width="28%" } !!! info @@ -130,7 +130,7 @@ New elements can be created from the entries listed in **Classes** on the left s 7. Drag and drop **New Class** into the canvas. - ![](drag-drop-new-class.png){ class="bordered" width="50%" } + ![](drag-drop-new-class.png){ class="bordered" width="49%" } 8. Click the newly created class to open its form, then fill out the required fields and any optional fields you want to define. @@ -181,7 +181,7 @@ Save the changes as a named visualization so you can edit your classes and prope 3. Enter a name for the visualization. - ![](save-changes.png) + ![](save-changes.png){ width="76%" } 4. Click **Save**. diff --git a/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/sparql-constraints/index.md b/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/sparql-constraints/index.md index f062de3aa..7be926f60 100644 --- a/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/sparql-constraints/index.md +++ b/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/sparql-constraints/index.md @@ -38,7 +38,7 @@ Click the :eccenca-module-marketplace: **Packages** icon in the main menu under Search for the required ontologies / vocabularies, then click the **Install** button to install the "The RDF Schema Vocabulary" and "Friend of a Friend (FOAF)" vocabulary. Wait until each installation is complete (the **Install** button will change to **Uninstall**) before installing the next package. -![Search and install ontologies](bacui-marketplace-schema-search.png){ class="bordered" } +![Search and install ontologies](bacui-marketplace-schema-search.png){ class="bordered" width="90%" } ### Create Shape Catalog diff --git a/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/workflow-trigger/index.md b/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/workflow-trigger/index.md index b1bb2432e..ac311bfb0 100644 --- a/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/workflow-trigger/index.md +++ b/docs/explore-and-author/graph-exploration/building-a-customized-user-interface/workflow-trigger/index.md @@ -36,7 +36,7 @@ A `shui:WidgetIntegration` resource needs to be created from the previously defi Once the widget is created, it can be attached to a Node Shape by using a special-purpose non-validating [Widget property](../node-shapes/index.md#widgets). -![](adding_widget_to_node.png) +![](adding_widget_to_node.png){ width="62%" } ## Payload Structure diff --git a/docs/explore-and-author/graph-exploration/statement-annotations/index.md b/docs/explore-and-author/graph-exploration/statement-annotations/index.md index cfb39d898..94cbd6779 100644 --- a/docs/explore-and-author/graph-exploration/statement-annotations/index.md +++ b/docs/explore-and-author/graph-exploration/statement-annotations/index.md @@ -76,7 +76,7 @@ These Annotation Resources are based on specific Shapes which are enabled as Sta Reification Resources as well as Annotation Resources are managed in a Statement Annotation Graph, which need to be configured on a Graph as well as imported to this Graph. The following illustration depicts this schema with boxes and arrows: -![](20-10-StatementAnnotationSchema.png){ class="bordered" } +![](20-10-StatementAnnotationSchema.png){ class="bordered" width="91%" } !!! note "Some notes on this:" diff --git a/docs/explore-and-author/workspace-configuration/index.md b/docs/explore-and-author/workspace-configuration/index.md index 639fa07ec..14e03ee4d 100644 --- a/docs/explore-and-author/workspace-configuration/index.md +++ b/docs/explore-and-author/workspace-configuration/index.md @@ -25,7 +25,7 @@ Click the drop-down list and click the Application view you want to open. Click the :eccenca-module-workspace-configuration: **Application view configuration** icon in the main menu. -![Configuration module](configuration.png){ class="bordered" } +![Configuration module](configuration.png){ class="bordered" width="29%" } Select the Application view from the dropdown menu to configure. diff --git a/poetry.lock b/poetry.lock index 78664caa9..fe425c224 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1194,6 +1194,131 @@ hyperscan = ["hyperscan (>=0.7)"] optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] +[[package]] +name = "pillow" +version = "11.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, + {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, + {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}, + {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}, + {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}, + {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}, + {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}, + {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}, + {file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}, + {file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}, + {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}, + {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}, + {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}, + {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}, + {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}, + {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}, + {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}, + {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}, + {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}, + {file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}, + {file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}, + {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}, + {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, + {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, + {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}, + {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}, + {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}, + {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}, + {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}, + {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}, + {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}, + {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}, + {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}, + {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}, + {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}, + {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}, + {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}, + {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}, + {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}, + {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}, + {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}, + {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}, + {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}, + {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}, + {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}, + {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}, + {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}, + {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}, + {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}, + {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}, + {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}, + {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}, + {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}, + {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}, + {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}, + {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}, + {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}, + {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}, + {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}, + {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}, + {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}, + {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}, + {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}, + {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}, + {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}, + {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}, + {file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}, + {file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}, + {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}, + {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}, + {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}, + {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}, + {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}, + {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}, + {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}, + {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}, + {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}, + {file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}, + {file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}, + {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}, + {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"}, + {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"}, + {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"}, + {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"}, + {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"}, + {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"}, + {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"}, + {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"}, + {file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"}, + {file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"}, + {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"}, + {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}, + {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}, + {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}, + {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}, + {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}, + {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}, + {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}, + {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}, + {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}, + {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}, + {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}, + {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}, + {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}, + {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}, + {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions ; python_version < \"3.10\""] +xmp = ["defusedxml"] + [[package]] name = "platformdirs" version = "4.11.8" @@ -1557,6 +1682,28 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pypdf" +version = "6.19.0" +description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14"}, + {file = "pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155"}, +] + +[package.extras] +crypto = ["cryptography (>3.0)"] +cryptodome = ["PyCryptodome"] +dev = ["flit", "pip-tools", "pre-commit", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"] +docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] +fonts = ["fonttools"] +full = ["Pillow (>=8.0.0)", "arabic-reshaper", "cryptography (>3.0)", "fonttools", "python-bidi"] +image = ["Pillow (>=8.0.0)"] +rtl-text = ["arabic-reshaper", "python-bidi"] + [[package]] name = "pytest" version = "9.1.1" @@ -2127,4 +2274,4 @@ tomli = ">=2.4.0" [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "972f99668ce7d19c84d91a6adf50dff73bf6d25a027eed6cdd24ac974d0eba34" +content-hash = "958cf277fc91a7ebe12226353501b35afbcb3c5f2fa267d0d3f4d7725c1c5a89" diff --git a/pyproject.toml b/pyproject.toml index 498e5ef67..18e0713f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,19 @@ jinja2 = "^3.1.6" # release. This narrowness is intentional -- bump it manually and re-run # `task build` plus `tools/check_zensical_output.py` when doing so. zensical = "^0.0.63" +pillow = "^11.3.0" +# tools/pdf_normalize.py and tools/pdf_preflight.py read the PDFs the print +# edition writes: the PDF/X-4 marker, the output intent, page sizes and annotations. +pypdf = "^6.18" +# Every dec-tool subcommand is a click command (tools/__init__.py). click, markdown +# and requests used to arrive as dependencies of other packages; a tool that imports +# them directly declares them, or an upstream bump takes them away. +click = "^8.5" +# tools/build_pdf.py renders the Markdown fields of data/plugins.json for the +# operator reference of the print edition. +markdown = "^3.10" +# tools/update_icons.py fetches the eccenca icon set from the gui-elements repository. +requests = "^2.32" [tool.poetry.group.dev.dependencies] linkcheckmd = "^1.4.0" diff --git a/tasks/backlog.md b/tasks/backlog.md index 5dea765fe..2152a6576 100644 --- a/tasks/backlog.md +++ b/tasks/backlog.md @@ -1,270 +1,716 @@ -# Backlog: temporary tag-listing renderer +# Backlog: print-on-demand book block -Work breakdown for [spec.md](spec.md). **Removed 2026-09-03** - see spec.md's Removal -section; superseded by Zensical 0.0.58's native `tags` listings. +Work breakdown for [spec.md](spec.md). **Status 2026-09-16: P0-P16 and P18-P26 done and verified; +P17 (the cover) open.** -**B0-B8: done** (2026-08-23). Listings render on `/tags/` and `/tutorials/`, guarded by -three required checks, 30 unit tests, `task preview` added for accurate previews. +The previous content of this file (the temporary tag-listing renderer) is in the git history. -**C1-C4: done** (2026-08-23). Every page tag chip links to its section on `/tags/` - -703 chips across 531 pages - guarded by two more required checks and 10 more unit tests. -Q6 settled as recommended. +Every task names how it is verified. A task is not done until that verification passes. --- -## B0 — Decide the open questions +## P0 - Decisions - done -Spec §5. My recommendations, all "match production / fail loudly": +Spec §4, D1-D11: BoD, A4, black and white on 80 g, no ISBN, authors by name (first GitHub IDs, revised 2026-09-15), separate screen and +print editions, section modes with A.3 and Release Notes as lists, page references and URL footnotes, +no logo or version on text pages, 10 pt body, optional Ghostscript normalization. D12-D14, decided +2026-09-15: excluding content from the print edition (spec §10), implemented by P18. D15-D18, decided +2026-09-15: the findings of the pull request review (spec §11), backlog P19-P24. -| # | Question | Recommendation | -|---|---|---| -| 1 | Icons for the 14 unmapped tags | Render plain, as production does | -| 2 | `Graph-Insights` vs `GraphInsights` duplicate | Out of scope; separate content fix | -| 3 | `Load Balancer` mapping missing from `HEAD` | Restore the two `mkdocs.yml` lines | -| 4 | Marker present but unrenderable | Fail the build | -| 5 | `/tags/` size (45 sections, 531 refs) | Include everything | +--- + +## P1 - Print edition - **done** + +`task pdf:print` builds `dist/documentation-eccenca-com--print.pdf` next to the screen PDF: +the same merge and pandoc run, Typst compiled with `--input edition=print`. `style.typ` reads the input +once and branches where the editions differ. `task pdf` stays as it is. + +**Verify:** both PDFs build; the screen PDF still has 1680 pages and an unchanged page 4. +**Est:** small. **Depends on:** nothing. -**Output:** decisions recorded in the spec. **Est:** one review pass. +**Done 2026-09-15:** `dec-tool build-pdf --edition print` (`PDF_EDITION`), intermediates in +`dist/pdf/print/`, flag `print-edition` in `style.typ`. Against a baseline built from `c20d74b94` +before any change: the screen PDF has 1680 pages, the text of all pages is identical and page 4 differs +by 0 pixels. The print edition builds (1585 pages at that point). --- -## B1 — Tag index builder +## P2 - Mirrored page geometry - **done** -Walk `docs/**/*.md`, parse front matter, build `{tag: [(title, src_path)]}`. +A4, `binding: left`, margins as `inside`/`outside` in the print edition; the peach bands' outset +mirrors with them. -- title = front-matter `title:`, else first body `# ` heading, else skip with a warning -- skip pages with no `tags:` -- tolerate malformed YAML without crashing the build -- pure function over `docs/`, no `site/` knowledge — keeps it unit-testable +**Verify:** render a spread (an even and the following odd page): the text blocks mirror, and the +text width is unchanged. +**Est:** small. **Depends on:** P1. -**Verify:** 531 tagged pages, 45 distinct tags, `TransformOperator` = 237. -**Est:** small. **Depends on:** nothing. +**Done 2026-09-15:** inside 3.0 cm, outside 2.0 cm. The header of the print edition is empty (P3), so +its top margin is 2.5 cm instead of 3.9 cm; the bands' outset is symmetric and needs no mirroring. +Measured with `pdftotext -bbox` on pages 20 and 21: even page margins 2.00 cm left and 3.00 cm right, +odd page 3.00 cm left and 2.00 cm right, text width 16.00 cm on both. --- -## B2 — Marker parser +## P3 - Running titles and page numbers - **done** -Recognise both forms in built HTML and extract the filter: +- verso: page number at the outer left, part title; recto: page title, page number at the outer right +- no `| total`, no logo, no version stamp on text pages +- none on the title page, imprint, part covers and blank pages -``` - -> no filter - -> include=[BeginnersTutorial] -``` +**Verify:** a script reads `pdftotext -bbox` and asserts, for every page with a number, x below 20 % +of the page width on even pages and above 80 % on odd pages. Render two spreads. +**Est:** medium - the footer queries already exist; the parity and the exclusions are new. +**Depends on:** P2. + +**Done 2026-09-15:** `print-footer()` and `bare-page()` in `style.typ`; the print header is empty. The +check over all 1604 pages of the print edition: 1575 pages carry a footer and each has its page number +at the outer edge (left on even, right on odd pages); the 29 pages without one are exactly the title +page, the imprint, the 9 part covers and the 18 blank pages. The checker becomes part of the preflight +report (P14). + +--- + +## P4 - Recto starts and blank pages - **done** + +- title page 1, imprint 2, front contents 3 +- part cover, part contents and part text each start recto +- a blank page has no furniture: a state set by the page break, read by header and footer +- total padded to even + +**Verify:** every part cover and the front contents on an odd page; every blank page has no text in +`pdftotext`; page count even. +**Est:** medium - suppressing furniture on inserted blank pages is the fiddly part (spec §1). +**Depends on:** P3. + +**Done 2026-09-15:** `recto-break()` brackets `pagebreak(weak: true, to: "odd")` with two metadata +markers, and a page strictly between them counts as blank. Typst cannot pad to an even count itself - +a page break that depends on the page count never converges - so the build reads the unpadded count +with `typst eval` (`unpadded_pages`) and compiles with `--input pad=true` when it is odd (1603 → 1604). +Checked on all pages: title page 1, imprint 2, front contents 3; the 9 part covers and the 9 part +contents start on odd pages; 18 blank pages, all even, all without text, never three in a row. + +--- -The argument is YAML-ish but not valid YAML (unquoted `[X]` inside braces parses fine, but -do not assume). Parse defensively; an unrecognised argument is an error, not a silent -no-filter. +## P5 - Title page - **done** -**Verify:** finds exactly 4 markers across 2 files in the current build. +Publisher **eccenca GmbH** on the title block; site link and copyright move to the imprint. + +**Verify:** render page 1. +**Est:** trivial. **Depends on:** P4. + +**Done 2026-09-15:** rendered page 1: logo, the house title block and version, the publisher from +`tools/pdf/print.yml` at the foot of the page; no date, link or copyright. The screen title page is +unchanged. + +--- + +## P6 - Author list - **done** + +`dec-tool pdf-authors` (run by `task pdf:authors`) writes `tools/pdf/authors.yml`: GitHub ID and +commits, most commits first, ties by ID case-insensitive. + +- source: the GitHub contributors API without anonymous entries +- excludes accounts of type `Bot` and agent IDs matching `claude` or `codex` +- committed, so the PDF build stays offline and reproducible and a changed list shows in review + +**Verify:** unit tests for ordering, tie-break and exclusion; the generated file matches the spec §3 +table (22 IDs). **Est:** small. **Depends on:** nothing. +**Done 2026-09-15:** `tools/pdf_authors.py`, `tests/test_pdf_authors.py` (4 tests: order and tie-break, +exclusion of bots, agents and anonymous entries, pagination, file format), `task pdf:authors`. The +generated `tools/pdf/authors.yml` lists the 22 IDs of spec §3 in the same order and passes yamllint - +list items are indented, which PyYAML's default dumper does not do. + +**Revised 2026-09-15 - names instead of IDs (D4):** `dec-tool pdf-authors` also writes the name each +GitHub profile shows (`GET /users/`), and skips the IDs in `authors.exclude` of `tools/pdf/print.yml` +before any lookup. The build applies `authors.names` - for a profile without a name, or to add a title - +and the exclusions to the committed list (`load_imprint_names`), passes the names to Typst as the +`authors` input, and warns about each author it still prints as an ID. 16 of the 22 profiles show a +name; `rpietzsch`, `annamakor`, `MaximilianWenzel`, `adelahaye-ecc`, `dgrtner-ecc` and `pkgut` need an +entry. Each run also adds the authors `authors.names` does not list yet, without a name and with the +profile's name in a comment (`prefill_names`); it edits `print.yml` as text, so comments stay, and reads +it back to check that only those entries changed. A name left empty prints the profile's. First run: all +22 IDs added; a second run adds none. Requests use GITHUB_TOKEN or GH_TOKEN, else the token of a +logged-in GitHub CLI; without one GitHub allows 60 requests an hour, and a run takes one per author. A +failed request - rate limit with its reset time, rejected token, unreachable API - ends the command with +a message instead of a traceback, before any file is written. `tests/test_pdf_authors.py` has 28 tests: +rules, exclusion, profile names, the order of names, the prefill, token sources and API failures. + +--- + +## P7 - Imprint - **done** + +Page 2 as spec R3, with the publisher address `eccenca GmbH, Hainstraße 8, 04109 Leipzig, Germany`, no +ISBN. Typst reads `tools/pdf/authors.yml` with `yaml()`; edition, commit and date come from the +existing `--input` values. + +**Verify:** render page 2; the author order matches `authors.yml`. +**Est:** small. **Depends on:** P5, P6. + +**Done 2026-09-15:** `imprint()` in `style.typ`, set at the foot of page 2: edition stamp with commit, +publisher and address from `tools/pdf/print.yml`, the 22 author IDs, licence and copyright, the online +edition - with a sentence that the print edition shortens sections whenever `print.yml` lists one as +`list` or `omit` - and the typesetting. Checked on the rendered page and in its text: the author IDs match +`authors.yml` in order. The licence URL is set as a string, because Typst links URLs written in markup. + +**Revised 2026-09-15:** the imprint prints the authors' names, not their IDs. `imprint()` no longer +reads `authors.yml`; the build passes the names as the `authors` input (P6). + --- -## B3 — HTML renderer +## P8 - Section modes - **done** + +`tools/pdf/print.yml` maps `nav.yml` section paths to `full`, `list` or `omit`; `tools/build_pdf.py` +reads it for the print edition only. + +- `omit`: the section's pages are not merged; the part contents name the online edition instead +- `list` for the operator reference (`build/reference/`): merge the section page and the five overview + pages, drop their children; links from the overview tables to dropped pages print as plain text +- `list` for Release Notes (`release-notes/`): replace the release pages with one generated table, + Release | Summary, the summary being the release page's first paragraph or, without one, its component + headings +- a section in `list` or `omit` mode starts with a sentence naming its online URL +- an unknown path or mode in `print.yml` fails the build + +**Verify:** unit tests per mode on small fixtures; with the default configuration A.3 takes about +17 pages and Release Notes about 3; switching either to `full` restores today's pages; no internal +link targets a dropped page. +**Est:** medium - the release summary fallback and the links into dropped pages are the fiddly parts. +**Depends on:** P1. + +**Done 2026-09-15:** `load_section_rules`, `apply_section_rules` (`list_section`, `omit_section`) and +`render_generated` in `tools/build_pdf.py`; `tests/test_build_pdf_print.py` (10 tests). A `list` section +keeps its own page and its subsections' overview pages; a page no overview lists goes into a table under +its heading, one per run of pages. Release Notes have no overview pages, so they become 8 tables - one +per year - with the 22 releases. Measured on the print edition: A.3 takes 17 pages (pp. 23-39, 389 pages +dropped), the Release Notes part 6 pages including cover, contents and blank pages; the book goes from +1604 to 1012 pages. Setting both sections back to `full` gives 1604 pages again. The two notes name +`…/26.2/build/reference/` and, since `release-notes/` has no page, the first release page. Links from +the overview tables to dropped pages print as text; 105 links from other pages to dropped operator +pages lead to the published site - a dropped page has no label, so no internal link can target one. -Emit the markup in spec §3 for one listing. +--- + +## P9 - Links on paper - **done** + +In the print edition: internal links print their text plus `(p. N)`; external links print their text +with a footnote holding the URL; no colour, underline, arrow or link annotation. -- anchor id `tag:` + lowercase, spaces → hyphens -- chip class from `extra.tags`; bare `md-tag` when unmapped (pending Q1) -- relative href from the listing page to each target -- escape titles -- ordering per spec §3 +**Verify:** sample pages; the number of footnotes equals the number of external links in the printed +sections; no page reference is `p. 0`; `strings` finds no `/Annots` in the book block. +**Est:** medium - thousands of page lookups; watch for Typst's "layout did not converge" warning. +**Depends on:** P1, P8. -**Verify:** byte-compare one rendered block against the production sample in the spec. -**Est:** medium — the relative-URL computation is the fiddly part. -**Depends on:** B1, B2, Q1. +**Done 2026-09-15:** the print branch of `show link` in `style.typ`; contents entries are laid out +without their link. Of 524 external links, 73 print their address as their own text and get no +footnote; 451 get one, 2 of them repeating the address their text already shows. 2154 page references, +none `p. 0`. A reference printed only for a target on another page never let the layout converge - it +moves lines, which moves the target back - so it is printed always, and the build now has no +convergence warning. Deviation from the check above: Typst's own footnotes link marker and entry, so +the book block keeps 902 internal link annotations; none of them leads to an address (`/S /URI`: 0). +The optional Ghostscript pass (P13) removes them. --- -## B4 — `tools/render_tag_listings.py` +## P10 - Typography for print - **done** -Wire B1–B3 into a CLI matching `localize_bundle_assets.py`'s shape: takes `[site_dir]`, -prints `[OK]` lines per marker, exits non-zero with a problem list. +- paragraph spacing 1.2 em and block spacing 1.0 em (measured −6.5 % pages) +- hyphenation on, widow and orphan costs +- a 6 pt floor for shrinking terminal tables +- measure Regular instead of Light for the body; decide with a greyscale print sample +- body stays 10 pt; 9 or 8 pt only as a fallback if the preflight page limit is exceeded (D10) -- reads `mkdocs.yml` for `extra.tags` and the two `listings_*_sort_by` settings -- idempotent: re-running on an already-rendered `site/` is a no-op, not an error - (`localize_bundle_assets.py` needed this and it was easy to get wrong) -- fails if any marker remains after processing +**Verify:** page count after each change; render pages with justified text. +**Est:** small. **Depends on:** P1. -**Verify:** `python tools/render_tag_listings.py site` twice in a row, second run clean. -**Est:** small once B1–B3 exist. **Depends on:** B1, B2, B3, Q4. +**Done 2026-09-15:** paragraph spacing 1.2 em and block spacing 1.0 em take the book from 1012 to 962 +pages; hyphenation on for body text, off for titles; code in a shrinking terminal table never below +6 pt. Typst's defaults already cost widows and orphans at 100 % (`text.costs`, checked), so they needed +no setting. A Regular body costs 2 pages (963 instead of 961 before padding); `body-weight` in +`tools/pdf/print.yml` switches it, and it stays `light` until a printed sample decides. Sample pages 26 +and 268 checked on the render: justified text without gaps, code and terminal table legible. --- -## B5 — Wire into the build +## P11 - Black-and-white palette - **done** -Add to `build` in `Taskfile.yml`, after `zensical build --strict` and alongside -`localize_bundle_assets.py`. Order relative to the localizer does not matter — they touch -disjoint files — but keep the localizer first so the more security-relevant step runs -regardless. +- grey areas at least 20 % black (BoD): `ec-wash` code and note grounds, peach bands - raise to 20 % or + replace fills with rules +- print-edition values for orange, link blue and the admonition accents that stay distinguishable in + grey +- check the red frames of the four part diagrams -**Verify:** `task clean build` renders listings; `task check` still passes. -**Est:** trivial. **Depends on:** B4. +**Verify:** render sample pages with the greyscale preview of P13 and compare with the screen +edition; the preflight measures the lightest fill. +**Est:** small. **Depends on:** P1. + +**Done 2026-09-15:** in print, text and alarm accents are black, the orange a dark grey, the peach bands +20 % black; code, code spans and admonitions have no ground - a thin frame marks a code block, the bar on +the left an admonition. The greyscale preview of P13 did not exist then (it does now: `--gray`), so Ghostscript's `pnggray` +rendered all 962 pages instead: on the 610 pages without images no area is lighter than 20 % black, and +the bands measure 204 (20 % black) on 36 pages. The part diagrams' red frames are part of their images and +stay visible as a dark frame on the covers. --- -## B6 — Promote the guard +## P12 - Print images - **done** + +In `tools/build_pdf.py`, for the print edition: every image Typst embeds is a normalized copy in +`dist/pdf/images/`. -In `check_zensical_output.py`, `tag-listings` currently sits in PENDING and reports -unexpanded markers. Once we render them ourselves it becomes a feature we own, so it -belongs in REQUIRED — same reasoning as redirects and comments. +- alpha composited onto white, so the book block carries no transparency +- resampled (Lanczos) to 300 ppi at its printed width: text width (16 cm) when the image is wider than + the column, its natural size otherwise +- no longer scaled up beyond its natural size (spec §6) +- originals under `site/` untouched; cached by content hash, so a rebuild does not resample again -Keep a separate PENDING probe for "Zensical started doing this itself", so the removal -signal in spec §6 still fires. Distinguishing the two is the fiddly bit: our own output and -Zensical's would both look like a populated listing. Suggest keying the PENDING probe on a -marker being *already expanded before* our script runs. +Needs an image library. Pillow is importable today only as a transitive dependency (11.3.0) and is not +declared in `pyproject.toml` - declare it before relying on it. ImageMagick is the alternative: installed +locally, not yet in CI. + +**Verify:** unit tests on fixture PNGs (alpha, small, large); in the book block `pdfimages -list` shows +no soft mask and no image below 300 ppi; render pages with former low-resolution screenshots. +**Est:** medium - the printed-width rule must match how Typst sizes images. **Depends on:** P1. + +**Done 2026-09-15:** `print_image` and `printed_width_pt` in `tools/build_pdf.py`, Pillow declared in +`pyproject.toml` (it was installed only as a leftover of CairoSVG); `tests/test_build_pdf_images.py` +(7 tests). The printed width follows how Typst sizes an image, measured in a probe: a percentage of the +16 cm column, otherwise pixels × 72 / declared dpi (72 without one), never wider than the column and +never scaled up - so "no longer scaled up" needed no change. The copy declares 300 dpi and keeps that +printed width. In the book block: 586 images, 0 soft masks, the lowest at 300 ppi, none below; 571 +copies in `dist/pdf/print/images/`. The PDF grows from 72 to 165 MB with the lossless upsampled copies. + +--- -**Verify:** deliberately skip the render step; `task check` must fail. -**Est:** medium — mostly deciding the probe. **Depends on:** B4, B5. +## P13 - Normalization pass (optional) - **done** + +Done 2026-09-15: `dec-tool pdf-normalize` (`tools/pdf_normalize.py`), run by `task pdf:print -- --normalize` +or PDF_NORMALIZE=1, writes `…-print-x4.pdf`. Refinements: + +- **Profile not vendored:** its copyright reads "All Rights Reserved" (spec §7). It is fetched from the ECI + into `dist/icc/` with a SHA-256 check; `--icc-profile` / PDF_ICC_PROFILE name a copy. +- **No prefix file in the repository:** the pdfmark prefix is generated with the title and the profile's path. +- **Transparency first:** Ghostscript 10.08 segfaults on Typst's colour emoji and leaves out transparent + SVG content (spec §7). The print edition therefore renders the 5 transparent SVGs and the 10 emoji sequences + to PNG with Typst; the style swaps emoji by show rule, in code too. The screen PDF is verified unchanged. +- **Failures are loud:** the run fails on a non-zero exit, on `error executing PDF token` and on a missing + PDF/X marker, and deletes the output. +- **Checked twice:** the build runs the preflight (P14) on the book block before Ghostscript, and on the + PDF/X-4 copy with `--pdfx` after. +- **Greyscale preview:** `task pdf:print -- --gray` (PDF_GRAY=1; `dec-tool pdf-normalize --gray` for an + existing PDF) writes `…-print-gray-x4.pdf`. It is the same pass in DeviceGray, and it serves as a screen + check of the black-and-white print for P11. It combines with `--normalize`. Verified on the 868-page + book block: 181 s, 59 MB, all 556 images grey, A4 unchanged. Since 2026-09-16 it is PDF/X-4 as well, + by the generic `default_gray.icc` of Ghostscript (condition `sGray`, one component, no registry). + Verified on the 872-page book: 176 s, 57 MB, the marker and a one-component intent, all 556 images grey, + and `dec-tool pdf-preflight --pdfx --intent sGray` passes; four unit tests. + +Verified on the full book block: 868 pages, Ghostscript 218 s, 175 MB; the preflight passes with +`--pdfx`: PDF/X-4 marker, FOGRA39 intent, 556 images all CMYK and none above 300 ppi, fonts embedded, no +Type 3 font, no transparency, no annotations. Rendered pages 26 (card icons), 472 (Excalidraw diagram) +and 662 (emoji) match the RGB file. 9 unit tests, 5 more for the rendered emoji and SVGs. + +`dec-tool pdf-normalize ` (run by `task pdf:print` with `--normalize` / `PDF_NORMALIZE=1`), spec §7: + +- Ghostscript pdfwrite with `-dPDFX=4`, CMYK conversion, bicubic downsampling of colour and grey + images above 300 dpi, Flate re-encoding, `-dPreserveAnnots=false` +- prefix file `tools/pdf/PDFX_def.ps`, derived from Ghostscript's `lib/PDFX_def.ps`, with title and + a FOGRA39 output intent; the profile passed with `--permit-file-read` +- ISO Coated v2 (FOGRA39) profile vendored under `tools/pdf/icc/` with its ECI licence, listed in the + README asset table - or downloaded in CI if the licence does not allow vendoring +- a `--gray` preview variant for P11 +- fails when the output lacks `GTS_PDFXVersion (PDF/X-4)` - Ghostscript silently falls back to plain + PDF when a page still carries an annotation +- never `-sOutputICCProfile` together with `-dPDFX` (crashes, truncated file) + +**Verify:** on the full book block: PDF/X-4 marker in info and XMP, one output intent naming FOGRA39, +`pdfimages -list` shows only `cmyk` images and none above 300 ppi, `pdffonts` all embedded, page count +unchanged; render sample pages before and after and compare. +**Est:** small - the recipe is verified on 38 pages. **Depends on:** P9 (no annotations), P12. --- -## B7 — Tests +## P14 - Preflight report - **done** -`tests/test_render_tag_listings.py`, following `tests/test_update_di_reference.py`. +Done 2026-09-15: `dec-tool pdf-preflight [--pdfx] [--max-pages]` (`tools/pdf_preflight.py`). Page +sizes, annotations and fill colours come from pypdf, fonts, images and word boxes from poppler's +`pdffonts`, `pdfimages -list` and `pdftotext -bbox`. Light areas are a **warning** that does not fail the +run - artwork and emoji carry light fills too; every other check is an error. With `--pdfx` it also +requires the PDF/X-4 marker, a FOGRA39 output intent and CMYK or grey images at most 300 ppi. +`build-pdf --edition print` runs it on its final file, the normalized one when P13 ran, and fails on an +error after writing the PDF. Verified: 9 unit tests; the print book block (868 pages) passes every check +in 7 s; the screen PDF fails as it should - 778 pages with annotations, 840 page numbers on the inner +edge, soft masks, light fills. Refinement: a fill of exactly 20 % black (`0.8 g`, 0.19999… in floating +point) counts as passing - the first run reported it on 102 pages. -- title resolution: front matter wins over H1; H1 fallback; neither → warn -- slugification: `Load Balancer` → `tag:load-balancer` -- sort orders: casefold for tags, title for items, marker order for `include:` -- unmapped tag → no `md-tag-icon` class -- marker parsing: both forms, plus a malformed one -- relative hrefs from different depths +`dec-tool pdf-preflight `: A4 page size, page count at most 1,200 and even, fonts embedded, images +below 300 ppi, soft masks, lightest fill below 20 % black, page-number position (P3's check), blank +pages blank. Non-zero exit on a violation; run by `task pdf:print`, on the normalized file when P13 ran. -**Verify:** `task test:unit` stays green. -**Est:** medium. **Depends on:** B1–B4. +**Verify:** unit tests on small fixture PDFs; deliberately break one rule, the report must fail. +**Est:** medium. **Depends on:** P3, P4, P8. --- -## B8 — Documentation +## P15 - Low-resolution originals - **done** -Extend the README section added during the migration. It currently lists tag listings among -the three missing features; that becomes "reimplemented locally, tracked for removal", -leaving social cards and revision dates as the genuinely-missing two. +49 images were below 150 ppi at printed size; resampling (P12) hides that from the preflight but adds no +detail. After P18, 43 images in the printed pages that are not generated print below 150 ppi, 37 of them +without a `width` - P24 sizes those first (spec §11). Work through what stays below 150 ppi: replace it +with a fresh screenshot, or accept it in spec §8. -**Est:** trivial. **Depends on:** B5. +**Verify:** every entry replaced or accepted. +**Est:** medium, mostly content. **Depends on:** P12, P24. + +Done 2026-09-16 by the revised P24: no screenshot had to be replaced. Narrowing the 42 entries to the +width their pixels support empties the report, so `accepted-low-resolution` in `tools/pdf/print.yml` +stays an empty list, and the report keeps watch over new screenshots. + +Tooling done 2026-09-15: the print build collects every original below 150 ppi at its printed size +(`resolve_images(..., low_resolution)`), leaves out those listed under `accepted-low-resolution` in +`tools/pdf/print.yml`, writes the rest lowest first to `dist/pdf/print/low-resolution.tsv` - density, the +width the page declares (the `width="NN%"` of the Markdown source, empty where there is none) and the +image - and prints their count. Accepting moved from spec §8 to `print.yml`, next to the other print +settings. The 42 entries it listed - 85 to 149 ppi, most in `consume/populate-data-to-neo4j` (8), +`explore-and-author/bke-module` (5) and `distribution/marketplace` (4) - are resolved by the revised P24: +the report is empty. --- -## Sequencing +## P16 - CI - **done** + +Done 2026-09-16: `.github/workflows/pdf.yml` builds every edition after a push to `main` or to +`feature/print-on-demand`, and on demand from the Actions tab - the screen PDF from `task pdf`, and the +book block, its PDF/X-4 copy in CMYK and the greyscale preview from `task pdf:print -- --normalize --gray`. + +- **The runner gets poppler; Ghostscript comes from a container.** The preflight shells out to `pdffonts`, + `pdfimages` and `pdftotext`, so poppler is installed with apt. Ghostscript is not: Ubuntu ships 10.02, + where `-dPDFX=4` is a boolean and the 4 raises `/typecheck in --pdfmark--` (spec §7). The workflow builds + an `alpine:edge` image with Ghostscript 10.07 and writes a shim to `/usr/local/bin/gs-docker` that runs + it with the workspace and `/tmp` mounted at the paths they already have, so the PDFs, the pdfmark prefix + and the profiles resolve inside the container as they do outside. `GHOSTSCRIPT` points the build at the + shim, and `default_gray.icc` is copied out of the same image into `dist/icc`, which the greyscale pass + finds through `PDF_GRAY_PROFILE`. Verified end to end against that image: the CMYK copy carries the + FOGRA39 intent with four components, the preview the `sGray` intent with one. +- **Four artifacts**, one per edition (`pdf-screen`, `pdf-print`, `pdf-print-x4`, `pdf-print-gray-x4`), + kept 30 days and stored uncompressed, since a PDF is compressed already. +- **The run summary** lists each edition with its page count, size and download link, so nothing has to be + dug out of the artifact section (user decision 2026-09-16: summary links, no release page). The first + run wrote nothing there, although the step passed, so the table is now built in one block, printed to + the log with `tee` as well, and followed by the byte count of the summary file - a silent non-write + cannot pass for success again. **Open:** the next run has to show the table in both places. +- **The output intent profile** is cached in `dist/icc` between runs; its licence keeps it out of the + repository, and CI would otherwise fetch it from the ECI every time. +- **The grey profile** is located with `find` and passed as `PDF_GRAY_PROFILE`, because distributions put + Ghostscript's `default_gray.icc` in different places; `ensure_gray_profile` honours that variable. +- Concurrency is per ref, so a push to `main` no longer cancels a branch build. + +**Verify:** a push produces the four artifacts and the summary table; a failing preflight fails the job. +**Est:** small. **Depends on:** P1, P13, P14. -``` -B0 ─┬─> B1 ─┬─> B3 ──> B4 ──> B5 ──> B6 ──> B8 - └─> B2 ─┘ └──> B7 -``` +--- -B1 and B2 are independent and can start as soon as Q1/Q4 are settled. +## P17 - Cover (separate deliverable) -## Risks +Front, spine and back; the spine width follows from the final page count and 80 g paper. Tracked here +so it is not forgotten; it has its own spec. -- **Relative URL computation** is where this most likely breaks — mike serves the site under - `/latest/` and `/26.2/`, so anything absolute fails silently in one context. Acceptance - requires resolving every generated link against `site/` on disk. -- **`/tags/` is large.** 531 references in one page. Watch build time; if it becomes - noticeable, that is an argument for revisiting Q5. -- **Divergence from production markup.** We match it today, but a Material update could - change the markup and this becomes a slow drift. Mitigated by the whole thing being - temporary and by the spec pinning a production sample. -- **The removal signal is the weak point** (B6). If it never fires, this "temporary" script - becomes permanent. Worth a calendar reminder to re-check backlog #38 rather than relying - only on the probe. +**Depends on:** P8, P14. --- -# Part 2: linked tag chips (spec §9) +## P18 - Content exclusion - **done** + +Spec §10, D12-D14: leave subtrees, pages and parts of a page out of the print edition. + +- `sections` keys in `tools/pdf/print.yml` may name a page (`.md`), which accepts `omit` only; a nested + key, or one that matches no page in `nav.yml`, fails the build +- `omit` drops a page or a subtree without a title or a note. For directories this revises P8, which + left the section's title and a note naming the online edition +- dropping a section's index page keeps its other pages under a heading with the navigation title +- in the print edition, elements with the class `print-exclude` are removed from the page's article + before ids, links and headings are processed. A note takes their place: "This print edition leaves + out a part of this page. The online edition has the full details:" and the page's online address, + with the anchor of the heading before the part; consecutive parts share one note. The build logs the + parts removed per page +- the site and the screen PDF ignore the class +- documentation: the `sections` comment in `print.yml`, and a line for authors in + `.claude/docs-guidelines/repo-conventions.md` on the class, which generated pages cannot carry +- configuration: `develop/cmem-client-api/: omit` (P26 widened it to all of `develop/`), + `build/tutorial-how-to-link-ids-to-osint/: omit` (all + 7 pages), and `{ .sql .print-exclude }` on the SQL code block of the Snowflake tutorial - lines 92-1094 + of `docs/build/snowflake-tutorial/index.md` on 2026-09-15, inside `??? example "INSERT query"`, which + keeps its title + +**Verify:** unit tests for page keys, the mode check, nesting, a dropped index page, the removal with +one note per run of parts and its address, and no removal in the screen edition. `task pdf:print` with +the three examples: G.5 and A.16 leave no heading or note, the `INSERT query` block of +the Snowflake tutorial holds the note instead of the listing, and the page count drops by about 300 +(spec §10). `task pdf`: page count and text unchanged apart +from the stamp. `task check` passes. +**Est:** small to medium. **Depends on:** P8, P9. + +**Done 2026-09-15:** + +- `SectionRule` knows page rules (`page`, `matches`). `load_section_rules` accepts page keys with `omit` + only and rejects nested keys. +- `omit_section` drops a page or a subtree without a trace. An index page omitted on its own leaves its + navigation title as a heading. +- `exclude_parts` replaces each run of `.print-exclude` elements with the note, whose address carries the + anchor of the heading before the part. `merge_pages` returns the parts left out per page, which the + build logs. List tables skip marked elements as well. +- `print.yml` omits `develop/cmem-client-api/` (75 pages, widened to all of `develop/` by P26) and + `build/tutorial-how-to-link-ids-to-osint/` (7 pages). The SQL block of the Snowflake tutorial carries `sql { .print-exclude }`. Authors find the + markup in `.claude/docs-guidelines/repo-conventions.md`. +- Measured: + - The print edition drops from 966 to 666 pages, the Snowflake tutorial from 46 to 22. + - No bookmark is left for G.5 or A.16. + - The note stands inside the `INSERT query` block (p. 120), with the anchor + `#1-create-a-database-in-snowflake`. + - No page runs into the footer and no line runs past the text column. + - The screen PDF keeps 1683 pages, its text unchanged and the listing in it. + - 134 unit tests pass. + - The `task check` steps pass; yamllint passes on the tracked files, while the untracked `scratch/` + folder still fails it. -Rendering the listings created 45 anchors that nothing points at. Material links every -per-page tag chip to its section on `/tags/`; Zensical emits inert ``s. Measured: -**531 pages, 703 chips, 0 links.** +--- -Smaller than Part 1 and a different mechanism - a **template override**, not another -post-build pass, so it also works under `task serve`. +## P19 - Part label in the footer - **done** -## Q6 — Decide before coding — **done**, all as recommended +Spec §11: the running footer of a left-hand page prints the part as the part band does, `Part A: Build`. +The contents, the bookmarks and the right-hand footer keep theirs. `print-footer` in `tools/pdf/style.typ`. -Spec §9 open questions: +**Verify:** render a left-hand and a right-hand page of two parts; the screen PDF is unchanged. +**Est:** small. **Depends on:** P3. -| # | Question | Recommendation | -|---|---|---| -| 1 | Slug computed twice (MiniJinja + Python) | Accept the duplication, but assert reachability in the check | -| 2 | Tag whose only page is the listing page itself → dead anchor | Covered by the same assert; no special case | -| 3 | `hide: tags` pages | Keep the existing template branch; no decision | +**Done 2026-09-15:** `print-footer` prints a part on a left-hand page as `Part A: Build`; right-hand footers, +contents and bookmarks keep `A Build`. 313 left-hand pages carried the label in the build before P23; +the screen PDF is unchanged. -**Est:** one review pass. +--- + +## P20 - Web addresses as endnotes - **done** + +Spec §11, D15. In the print edition, a link out of the book prints its text and a superscript number +instead of a footnote. The numbers run within a part and start again at 1 in the next; an address cited +twice in a part keeps its first number. A list "Web addresses" on a new page, under an unnumbered heading, +closes each part that cites any: number, address and the pages citing it, laid out without link +annotations. Links within the book keep their page reference. + +**Verify:** no footnote left in the book block; each part's list holds every number of that part with its +address and correct pages; numbering restarts per part; no link annotations from notes; Typst reports no +layout that failed to converge; compile time and page count before and after. +**Est:** medium. **Depends on:** P9. -Decision: go with all above recommendations! +**Done 2026-09-15:** + +- `web-address()` in `style.typ` places the address as metadata and prints a superscript number, counted + within the part with `context` and `query`. `part-addresses()` sets the list on a new page under an + unnumbered heading that is bookmarked but not in the contents: number, address and the pages citing it. +- In the print edition, `merge_pages` puts a `part-end` marker at the end of every part, and + `filter.lua` turns it into `#part-addresses()`; a unit test covers the markers. +- Measured with P23 in place: 8 lists with 390 entries, 159 of them in part A. Typst compiles in 4 s + without a convergence warning; the screen PDF is unchanged. --- -## C1 — `overrides/partials/tags.html` — **done** +## P21 - Author order from the printed content - **done** -Copy Zensical's `partials/tags.html` verbatim and replace only the `tag.url` branch: +Spec §11, D16. `dec-tool pdf-authors` counts only the commits to the files the print edition +prints: the pages left after the section rules and the images in their directories, from +`git log --no-merges` following renames, each commit once, generated pages left out. The GitHub +commits API maps each commit to its account. Bots, agents, anonymous commits and `authors.exclude` +stay excluded; the imprint says "most commits to the printed pages first". -```jinja -{% set anchor = "tags/" | url ~ "#tag:" ~ (tag.name | lower | replace(" ", "-")) %} -{{- tag.name -}} -``` +**Verify:** unit tests with a fake history for file selection, rename following, counting and account +mapping; a real run shows the new order next to today's; the imprint follows it. +**Est:** medium. **Depends on:** P6, P18. -Keep everything else byte-identical, as `tabs-item.html` does, so it can be re-synced. -Retain the `hide: tags` branch and the `md-tag-shadow` / `md-tag--` class logic. +**Done 2026-09-15:** -**Done:** output is byte-identical to production's, e.g. -`Configuration`. -The override differs from Zensical's stock template by exactly the one branch. +- `tools/pdf_authors.py` counts the commits to the printed files. `printed_pages` applies the section + rules, and `printed_files` returns the pages that are not generated plus the images they reference. + `file_commits` runs `git log --no-merges --follow` per file, and `commit_accounts` with + `count_commits` maps each commit once to its account, falling back to the author e-mail. + `fetch_commits` replaces the contributors API. +- The imprint reads "most commits to the printed pages first". +- `tests/test_pdf_authors.py` has 31 tests; new are printed files, counting and rename following in an + isolated git repository. +- Measured: 661 commits to 638 printed files, 18 authors instead of 19 - `haschek` has no commit to + printed content. The order starts with `rpietzsch` (249), `seebi` (156), `sobo` (46), + `muddymudskipper` (45) and `irangareddy` (37). --- -## C2 — Assert every chip anchor resolves — **done** +## P22 - Cards of equal height - **done** + +Spec §11: `cards()` in `tools/pdf/style.typ` lays out its grid row by row and gives both cards of a row +the height of the taller one; a card alone in the last row keeps its own height. The rounded frame and +`breakable: false` stay; the screen PDF changes with it. + +**Verify:** render the card grids of the part pages in both editions; facing cards end on one line; no +card breaks across pages. +**Est:** small. **Depends on:** nothing. + +**Done 2026-09-15:** `cards()` measures the cards of each row at the column width and frames both with +`card-frame()` at the height of the taller one; `card()` now only passes the content through. Checked on +the card grids of the Build page (p. 11) and the reference (p. 30); the screen PDF keeps its page count +and text. + +--- -New required check in `check_zensical_output.py`: for each `href="…/tags/#tag:X"` in the -output, `#tag:X` must exist on `/tags/`. This is what keeps the MiniJinja slug and -`render_tag_listings.tag_slug()` from silently drifting apart. +## P23 - Operator reference in a compact format - **done** + +Spec §11, D17. A section mode `reference` prints `build/reference/` as one compact, harmonized +entry per operator; the entries replace the overview tables. + +- **Structure:** each type chapter keeps its introduction. Operators follow alphabetically, numbered + A.3.x.y and listed in the part contents; the transformer category moves into the entry. +- **Entry:** the title and a field line with type, category, plugin ID, `Python plugin` and a distance + range. Then the rendered description without `## Examples` and without the Python plugin note, its + headings as run-in labels. Then one parameter table with the columns Parameter and its ID, Type, Default + and Description, an `Advanced` row and `parent.child` sub-parameters, and a `Related:` line with page + references. +- **Sparse data prints as nothing:** no table without parameters, `–` for a missing default, and + `see below` with a code block for a multi-line default. The data types map to the vocabulary of spec + §11. +- **Data:** structure and parameters come from `data/plugins.json`, the description from the site page. + The build fails when pages and JSON disagree. +- **Configuration:** `print.yml` switches `build/reference/` from `list` to `reference`. The A.3 + introduction names the online examples and Python plugins once. + +**Verify:** unit tests build entries from a JSON fixture - an operator without parameters, one with +advanced and sub-parameters, a multi-line default, every data type - and cover the page/JSON check. The +print build shows 389 entries, no Examples heading and no `None`. The page count is measured against the +estimate of spec §11 (about 860) and BoD's limit (P14). Render a transformer, a dataset and a custom task +with more than 20 parameters. +**Est:** medium to large. **Depends on:** P8. + +**Done 2026-09-15:** + +- Section mode `reference` in `tools/build_pdf.py`. `reference_section` places a note after the section + page and the operators of each type after its overview page, alphabetically, drops the category + headings, and fails when pages and `data/plugins.json` disagree. `merge_pages` removes the overview + tables. +- `operator_entries` renders each entry: `operator_fields`; `operator_description` without the title, + the Python plugin note and the template sections, with headings as run-in labels; `parameter_table` + with the type vocabulary of `data_type`, an `Advanced` row, `parent.child` sub-parameters, `see below` + defaults and Markdown descriptions; and `operator_related`. +- `operator-fields()` in `style.typ` and its mapping in `filter.lua`. `print.yml` switches + `build/reference/` to `reference`. `tests/test_build_pdf_reference.py` has 5 tests. +- Measured: 389 entries with bookmarks; A.3 takes 213 pages (pp. 29-241) and the book 870 (spec §11 + estimated 860). No `Advanced Parameter` heading and no generated example is left; 9 defaults print as + `see below`, 80 entries have a `Related:` line, and 1 web address still points to an operator page. + The screen PDF is unchanged. +- Open: example subsections the plugins write into their own documentation - `5. Example` in Pivot, + `6. Examples` in RDF file, `Example usage` in Knowledge Graph - still print as run-in labels with their + text; only the generated `## Examples` sections are dropped. -Cheap: collect the anchor ids from `/tags/` once, then set-compare against the hrefs. +--- -**Done:** `tag-chips-linked` (703 chips on 531 pages) and `tag-chips-resolve` (every -anchor exists), both required. Verified by removing `replace(" ", "-")` from the template: -the check failed on `#tag:load balancer` and `#tag:application view`, the only two -multi-word tags. +## P24 - Image widths in the sources - **done** + +Revised on 2026-09-16, once the report of P15 made the effect visible: the width follows from the density +an image *prints* at, not from the density it was *captured* at. `dec-tool image-widths` +(`tools/image_widths.py`) narrows every raster image that prints below 150 ppi in the 16 cm column, in +pages that are not generated, and leaves the rest as it is. + +- **Width:** `pixels / (6.3 inches * 150)`, rounded down to a whole percent - not to a multiple of 5. The + width a page already declares cancels out of `declared * density / target`, so the pixels alone decide; + one further step down covers the case where the rounding of the density leaves it a pixel short. +- **Measured at** the width the page declares, quoted or not - two of 187 are written `width=11%` - and an + image without one fills the column, so it counts as 100 %. +- **No floor:** the smallest results are `marketplace-filter-installed.png` at 14 % and + `marketplace-filter-package-type.png` at 21 %. Both show a single snippet of a dialog, so the user chose + the calculated value over a floor. +- **Scope:** raster images only; SVGs, remote images and fenced code stay untouched, and generated pages + belong to their generators - none of them held an image below the target. + +**Verified:** `--fix` wrote 47 widths into 21 pages; the report of P15 is empty afterwards and the book +shrinks from 872 to 864 pages. The gate was a throwaway PDF of just those images at their new widths. +Five of the 47 sit in the IDS/OSINT tutorial, which print omits, so they change the site only. rumdl +clean; the unit tests cover the density, the target width, and the rewriting of quoted, unquoted and +missing width attributes. + +**Superseded:** the first implementation (2026-09-15, D18 as written) took the natural width from the +capture scale, rounded it to 5 % and skipped anything at 100 %. Its 24 widths stay where the new rule does +not narrow them further. `task check` runs the check as `check:images`. --- -## C3 — Tests — **done** +## P25 - A heading with a single line at the foot of a page - **done** -Extend `tests/test_render_tag_listings.py`, or a sibling, with the slug-parity case: the -Python `tag_slug()` and the template's expression must agree for every tag in use - -including `Load Balancer` (space) and `Graph-Insights` (existing hyphen). +Review finding of 2026-09-16: A.3 Task and Operator Reference sat at the foot of page 29 with its +intended-audience line under it, and the section started on the next page. A heading is sticky, so it is +never last on a page - but a one-line lead satisfies that, and the break falls after the line. -Template rendering itself is covered by C2 against the real build rather than by unit test; -MiniJinja is not worth mocking for three filters. +Done the same day: `keep_lead_with_heading` in `tools/build_pdf.py` (print edition only) wraps a paragraph +that directly follows a heading of level 1 to 4 and is at most 200 characters long in +`div.keep-with-next`; `filter.lua` maps it to `keep-with-next` in `style.typ`, a sticky block, so the lead +carries heading and line to the block that follows. Levels 5 and 6 are left out: they are the operator +entries, which have their own sticky field blocks. -**Done:** 10 tests reading the override and asserting its slug expression matches -`tag_slug()`. 59 tests pass in total. +**Verified:** 304 lead lines wrapped; A.3 now starts a page with its content; across the book, headings +with at most one line under them at a page foot fall from 23 to 6, and the book grows from 868 to 872 +pages. Measured against levels 1 to 3 alone, which leaves 17 of them at 868 pages: the four pages buy +eleven fewer stranded headings, and one new one appears (B.2.2.6). `LEAD_HEADINGS` is the one place to +change if the shorter book matters more. The screen edition does not run the step. One unit test. --- -## C4 — Documentation — **done** +## P26 - The first edition is a user guide - **done** + +Decided 2026-09-16: the first print edition serves users, so what an administrator or a developer needs +stays in the online edition. `tools/pdf/print.yml` omits part E (`deploy-and-configure/`, 34 pages) and +part G (`develop/`, 89 pages) whole, and the narrower `develop/cmem-client-api/` key goes, since section +keys do not nest. -README: the "reimplemented here" table gains a row for the chip links, pointing at the same -`#38` and the same removal trigger. Spec §7 already lists the override for deletion. +**Verified:** the book drops from 864 to 668 pages and the preflight passes unchanged. No cross-reference +dies: a link from a printed page into an omitted part becomes a web address in its part's endnote list - +the JDBC driver setup, for one, prints as +`https://documentation.eccenca.com/latest/deploy-and-configure/configuration/dataintegration/jdbc/` - +and internal links without a target fall from 17 to 4, because most of those lived in the dropped trees. -**Done.** +**Open:** the imprint's author list counts commits to the printed pages (D16), and 123 pages left the +book, so `task pdf:authors` should run before the edition goes to print. --- ## Sequencing +```text +P1 ──> P2 ──> P3 ──> P4 ──> P5 ──> P7 + │ │ P6 ──┘ + ├──> P8 ──> P9 ──────┼─────────────┐ + ├──> P10 │ │ + ├──> P11 │ │ + └──> P12 ──> P15 │ │ + └──────────────┼──> P13 <────┘ + └──> P14 ──> P16 +P17 after P8 and P14 +P18 after P8 and P9 +P19 after P3, P20 after P9, P21 after P6 and P18, P22 any time, P23 after P8 +P24 before P15 ``` -Q6 ──> C1 ──> C2 ──> C3 - └──> C4 -``` + +P1, P6, P8, P10, P11 and P12 can start now. ## Risks -- **Slug drift** between the template and the Python renderer is the only real one, and C2 - exists specifically to make it loud. Do not skip C2 to save time. -- **Chips link to `/tags/`, which is a large page.** 45 sections, 531 references. Jumping - to an anchor there is fine, but it is a heavy page to load from a chip click. Matches - production, so not a regression - noting it because it is the sort of thing that gets - raised later as if it were new. -- **Override drift** if Zensical changes its `tags.html`. Same exposure as - `tabs-item.html`; mitigated by keeping the copy byte-identical apart from the one branch. +- **The page budget is an estimate.** It adds measured savings that were taken separately; P8 and P10 + must re-measure, and P14 enforces the limit. +- **Ghostscript falls back to plain PDF silently.** One annotation left anywhere and the output is not + PDF/X; P13 checks the marker instead of trusting the exit code. +- **CMYK conversion and downsampling change screenshots.** Compare renders before and after P13 on pages + with dense UI text; Flate keeps the re-encoding lossless, the bicubic resampling does not. +- **Upsampled images look sharper in the preflight than on paper.** P15 exists because P12 cannot add + detail that is not there. +- **Black and white loses colour cues** in screenshots and in the part diagrams' red frames, and today's + light fills fall below BoD's 20 % rule; only a greyscale sample shows how much. +- **Page references cost compile passes.** Thousands of `(p. N)` lookups can move page breaks that move + page numbers; Typst stops after five layout attempts and warns. +- **80 g paper is for publishers only at BoD.** Without a publisher account the book prints on 90 g, + where the limit is 1,050 pages. +- **The screen PDF must not drift.** Every print change goes behind the `edition=print` switch (P1), and + the screen PDF's page count is part of every verification. diff --git a/tasks/spec.md b/tasks/spec.md index ad9359c9e..65eef7e49 100644 --- a/tasks/spec.md +++ b/tasks/spec.md @@ -1,267 +1,774 @@ -# Spec: temporary tag-listing renderer - -**Status:** accepted 2026-08-23, fully implemented §1-§9, **removed 2026-09-03**. -Zensical 0.0.58 ships native `tags` listings and populates `tag.url` itself; the -"Removal" steps below were carried out as part of the 0.0.57 -> 0.0.58 bump. -**Replaced:** Material's `tags` plugin listings, which Zensical did not implement -([zensical/backlog#38](https://github.com/zensical/backlog/issues/38)). -**Lifetime:** delete the moment Zensical ships listings. See "Removal" below. +# Spec: print-on-demand book block + +**Status:** accepted 2026-09-14 - all decisions made (§4). Backlog P0-P16 and P18-P26 are implemented; +P17 (the cover) is open. The backlog's "Done" notes record where the implementation +refines this spec. §10, excluding content from the print edition, was decided on 2026-09-15 (D12-D14) +and implemented the same day (backlog P18): the print edition has 666 pages. §11 collects the pull request +review of 2026-09-15, decided the same day (D15-D18); backlog P19-P24, of which P19-P23 were implemented +on 2026-09-15: the print edition has 870 pages. P13, P14 and P24, and the tooling of P15, followed the same +day: PDF/X-4 normalization (§7, with the transparency Ghostscript cannot convert rendered to images), the +preflight report, image widths in the sources and the list of low-resolution originals. On 2026-09-16 the +width rule was revised (D18, P24, which closed P15) and a short lead line under a heading became sticky +(P25). On 2026-09-16 the first edition's scope was set to a user guide, omitting parts E and G whole +(P26): the print edition has 668 pages. +**Branch:** `feature/print-on-demand`, based on `main` at `c20d74b94` (PDF export merged). +**Goal:** a *book block* - the interior file of a printed, perfect-bound book - built next to the +screen PDF, which BoD accepts without rework. +**Not in scope:** the cover file (front, spine, back); see §9. + +The previous content of this file (the temporary tag-listing renderer, removed 2026-09-03) is in the +git history. --- -## 1. Problem - -`tools/check_zensical_output.py` reports this every build: - -``` -[PEND] tag-listings: /tags/ article has 3 words, 4 unexpanded - marker(s) on 2 page(s) (backlog #38) -``` - -Per-page tag *chips* work — Zensical renders them, and the icon CSS with them. What is -missing is the *listings*: the generated index of which pages carry which tag. - -Two pages are affected, carrying four markers between them: - -| Page | Marker | Renders today | -|---|---|---| -| `docs/tags.md` | `` | nothing — page body is just its title | -| `docs/tutorials/index.md` | `` | nothing | -| `docs/tutorials/index.md` | `` | nothing | -| `docs/tutorials/index.md` | `` | nothing | - -`/tutorials/` is the more visible loss: it is a landing page whose entire purpose is the -generated list, and it currently shows an intro paragraph followed by blank space. - -## 2. Approach - -**Post-build HTML injection**, mirroring the existing `tools/localize_bundle_assets.py`. - -Zensical passes the markers through to the output verbatim as HTML comments: - -```html - - -``` - -so they are addressable in `site/**/*.html` after the build. A script walks the corpus for -tag front matter, renders the listing HTML, and substitutes it for each marker. - -### Why this shape - -- **Sources stay untouched.** The pages keep Material's own marker syntax, so when - Zensical implements listings they light up natively and this script is deleted. No - migration back. -- **Precedent in-tree.** `localize_bundle_assets.py` already post-processes `site/` inside - `task build`, with the same "assert loudly if the expected pattern is missing" contract. -- **No new dependencies, no template overrides.** Icon styling already works (below). - -### Alternatives rejected - -| Option | Why not | -|---|---| -| Pre-build: expand markers into `docs/*.md` | Mutates tracked sources; dirty tree after every build | -| Generate a partial + `--8<--` snippets include | Requires editing both pages away from Material syntax, then back later; needs two-phase build | -| Override a Zensical template | Zensical has no listings template to override — the feature is absent, not broken | -| Write a Zensical plugin | Zensical has no plugin API | - -## 3. Output contract - -Reproduce production's markup exactly. Sample from -`https://documentation.eccenca.com/latest/tutorials/`: - -```html -

-BeginnersTutorial¤

- -``` - -### Rules, all verified against production - -**Anchor id** — `tag:` + tag lowercased with spaces replaced by hyphens. -`Load Balancer` → `tag:load-balancer`, `Graph-Insights` → `tag:graph-insights`. - -**Tag chip class** — `md-tag md-tag-icon md-tag--` where `` is -`extra.tags[]` from `mkdocs.yml`. **Zensical already emits the backing CSS** -(`.md-tag.md-tag--beginners{--md-tag-icon:url(...)}`) on every page, so no CSS work is -needed. For tags absent from `extra.tags`, production omits `md-tag-icon` and the -`md-tag--` modifier, emitting a bare ``. 14 of the 45 tags in -use are unmapped — see §5. - -**Item title** — front-matter `title:` if present, otherwise the first `# ` heading in the -body. Verified on three pages; none of the sampled pages set `title:`, and all three -listing titles match their H1 exactly. - -**Item link** — relative from the listing page to the target, so it survives mike's -versioned `/latest/`, `/26.2/` prefixes. Never absolute. - -**Ordering** — from `mkdocs.yml`: -- tags within an un-filtered listing: casefolded tag name - (`listings_tags_sort_by: tag_name_casefold`) — confirmed, `/tags/` runs - AdvancedTutorial, API, Application View, Automate, BeginnersTutorial… -- items within a tag: page title (`listings_sort_by: item_title`) -- markers with `include:` render in **marker order**, not sorted — `/tutorials/` shows - Beginners, Advanced, Expert, matching source order - -**Scope** — a bare `` lists every tag in use (45 on `/tags/`). An -`include: [X]` marker lists only tag X. - -**Self-inclusion** — not an issue: neither `tags.md` nor `tutorials/index.md` carries tag -front matter, so neither can appear in its own listing. Guard anyway. - -## 4. Corpus facts - -- 531 of 581 pages carry `tags:` front matter -- 45 distinct tags; largest is `TransformOperator` (237 pages), then `WorkflowTask` (77), - `PythonPlugin` (61), `cmemc` (47) -- The `/tags/` listing is therefore large — production's is ~2100 words - -## 5. Open questions — resolved - -1. **Unmapped tags.** 14 tags in use have no `extra.tags` icon: `Build`, `Dataset`, - `DistanceMeasure`, `EvaluateTemplate`, `Explainer`, `Files`, `Graph-Insights`, - `GraphInsights`, `Introduction`, `LinkRules`, `Load Balancer`, `Plugin`, - `TransformOperator`, `WorkflowTask`. Match production (render plain, no icon), or take - the opportunity to map them? **Recommend: match production**, keep this change - behaviour-neutral. -> decision: match production, no icon - -2. **`Graph-Insights` vs `GraphInsights`** are both in use and look like an accidental - split — 2 tags where 1 was meant. Out of scope for this task, but the listing will make - it visible on `/tags/`. Worth a separate content fix. -> decision: separate fix - -3. **`Load Balancer` icon.** The T7 work added `overrides/.icons/other/load-balancer.svg` - (committed, tracked) but the two `mkdocs.yml` lines that reference it are **not** in - `HEAD` — line 63 still reads `# "Load Balancer": simple-awselasticloadbalancing`. The - icon file is currently dead weight and the tag renders bare. Probably an accidental - partial revert. Restore those two lines, or drop the SVG? **Recommend: restore**, it - was verified working. -> decision: restore it, verify it works (again) - -4. **Failure mode.** If a marker is present but the script cannot render it, should - `task build` fail, or warn and leave the marker? **Recommend: fail**, consistent with - `localize_bundle_assets.py`, since a silently empty listing is the exact failure this - whole guard-rail effort exists to prevent. -> decision: fail - -5. **`/tags/` page size.** 45 sections listing 531 page references. Acceptable, or should - the bare marker exclude high-cardinality tags like `TransformOperator`? **Recommend: - include everything**, matching production. -> decision: match prod, include everything - -## 6. Known limitation: `task serve` - -`zensical serve` rebuilds into `site/` on every change, so it would overwrite anything a -post-build step writes. The live-reload preview therefore shows the raw -`` markers, not the listings - measured: 0 tag sections, 3 markers -left on `/tutorials/`. The same applies to the glightbox localisation. +## 1. Starting point + +Measured on the level-4 build of `main` (`dist/documentation-eccenca-com-26-2.pdf`). + +| Aspect | Today | +| :-- | :-- | +| Size | 1680 pages, A4, 118 MB | +| Type | body 10 pt Roboto Light (`size-base`, one step below the house style's measured 11 pt); tables 8.6 pt, admonitions and cards 9.1 pt, code 7.7 pt; 3 terminal tables shrink to fit, the widest (114 columns) to about 6.3 pt | +| Line length | about 104 characters per full body line (75th percentile, pages 40-80) | +| Parts | A Build pp. 3-734 (732) · B Explore 735-861 (127) · C Consume 862-900 (39) · D Distribution 901-910 (10) · E Deploy and Configure 911-1075 (165) · F Automate 1076-1192 (117) · G Develop 1193-1490 (298) · H Release Notes 1491-1677 (187) · I Tutorials 1678-1680 (3) | +| Largest blocks | A.3 Task and Operator Reference pp. 23-480 (458) · G Develop 298 · H Release Notes 187 | +| Geometry | symmetric margins: left/right 2.5 cm, top 3.9 cm, bottom 2.2 cm (`style.typ`, `set page`) | +| Header | logo left on every page; `Version 26.2` / `Generated ()` right on every page but page 1 | +| Footer | running title (`A Build › A.3 …`) left, `Page n \| total` right, orange, on every page but page 1 | +| Pagination | page 1 title page, page 2 parts contents; a part starts on whichever page follows | +| Spacing | paragraph spacing 2.24 em, block spacing 1.52 em (house style) | +| Links | 3193 internal jumps, 614 external links plus 70 into the published site; blue, underlined, external ones with `↗`; no footnotes | +| Images | 641, all ICC-based RGB; 565 carry a soft mask (transparency). Effective resolution in the print scope (§5), 586 images: 331 below 300 ppi, 144 below 200, 49 below 150 | +| Colour | orange, peach, slate and link blue; grey code ground; red for danger admonitions; the part diagrams mark their part with a red frame | +| Text | justified, hyphenation off | +| Tables | 273 header rows emitted by pandoc as `table.header`, which Typst repeats on every page | +| PDF | Typst 0.15.1 writes PDF 1.4-2.0, PDF/A and PDF/UA, **not PDF/X**; Ghostscript 10.08 is available (§7) | + +Verified in a Typst 0.15.1 probe: `set page(binding: left, margin: (inside: …, outside: …))`, +`pagebreak(to: "odd")`, `calc.odd(here().page())` in header and footer, `text(costs: (widow: …, +orphan: …))` and `footnote`. A blank page inserted by `pagebreak(to: "odd")` **still gets header and +footer**; suppressing them needs its own rule. + +## 2. Provider: BoD + +Decided (D2). Researched 2026-09-14; re-check the current specification before ordering. + +| Requirement | BoD | Source | +| :-- | :-- | :-- | +| Pages, A4 paperback | 1,200 on 80 g, 1,050 on 90 g white, 900 on 90 g cream, 120 g or 130 g | Buch drucken | +| Paper | white or cream 80 g is **for publishers only**; 90 g for everyone | Grafiken und Farbmanagement | +| Image resolution | at least **300 dpi** at printed size; line art without grey levels (bitmap) **1200 dpi** | Grafiken und Farbmanagement | +| Grey areas | at least **20 % black**, not lighter; grey steps in charts at least 20 % apart | Grafiken und Farbmanagement | +| Transparency | to be reduced before delivery; otherwise BoD reduces it, which "can change objects" | Grafiken und Farbmanagement | +| Colour | RGB recommended - BoD converts to CMYK for its presses; untagged RGB is read as sRGB, CMYK as ISO Coated v2 (FOGRA39); no colour separation | Grafiken und Farbmanagement | +| Bleed | 5 mm per side, only for content that runs off the page; no crop marks | Grafiken und Farbmanagement | +| Fonts and images | embedded | Hilfe | + +Sources: +[BoD Buch drucken](https://www.bod.de/buch-drucken.html), +[BoD Grafiken und Farbmanagement](https://www.bod.de/bodfiles/GLOBAL-Storage/documents/help-documents/bod-grafiken-und-farbmanagement.pdf), +[BoD Hilfe](https://www.bod.de/hilfe/hilfe-und-service.html). + +For comparison, not pursued: Amazon KDP allows 828 pages (black ink on white), Lulu 800. + +## 3. Requirements + +### R1 - Page furniture for verso and recto pages + +- Page numbers sit on the **outer edge**: left on even (verso) pages, right on odd (recto) pages. +- Running titles mirror too, following book convention (the larger unit on the left page): + - verso: `1234` at the outer left, `Part A: Build` beside it (revised after the review, §11) + - recto: `A.3 Task and Operator Reference` beside `1235` at the outer right +- `| total` is dropped - it has no meaning on paper. +- No logo and no version stamp on text pages (D9); both stay on the title page and the imprint. +- Margins become `inside`/`outside` with `binding: left` on A4 (D7). The gutter must grow with the + spine; a starting proposal is inside 3.0 cm, outside 2.0 cm, which keeps today's 16 cm text width. +- No header and no footer on the title page, the imprint, blank pages and part covers. + +### R2 - The title page is page 1 of the book block + +- Page 1, recto. It keeps the house title block (eyebrow, title, version) and names the publisher, + **eccenca GmbH**. +- The site link and the copyright line move to the imprint. +- Counted as page 1, number not printed. + +### R3 - Imprint on page 2 + +Page 2, verso, no page furniture. Contents: + +- title and edition: `eccenca Corporate Memory - Documentation, Version 26.2`, print edition, + generated `` from commit `` (moves here from the running header) +- publisher: **eccenca GmbH, Hainstraße 8, 04109 Leipzig, Germany** +- authors: the list below +- licence: CC BY-SA 4.0, as `README.md` states, with the copyright line from `mkdocs.yml` +- online edition: `https://documentation.eccenca.com/26.2/`, named as the complete reference for what + the print edition shortens (§5) +- colophon: typeset with Typst from the Markdown sources; fonts Roboto, Roboto Mono, Noto Color + Emoji, DejaVu +- no ISBN (D3) + +#### Authors + +Rule (D4, revised 2026-09-15): GitHub accounts of the contributors to +[eccenca/documentation.eccenca.com](https://github.com/eccenca/documentation.eccenca.com), **most +commits first**, printed with their **names**: the name in the hand-maintained `authors.names` of +`tools/pdf/print.yml` - for a profile without a name, or to add a title - else the name the GitHub +profile shows, else the GitHub ID. Not listed: anonymous contributions (commits whose e-mail maps to no +GitHub account), bot accounts, software agents (codex, claude) and the IDs in `authors.exclude`, whose +names are not looked up. Ties are ordered by ID, case-insensitive. `task pdf:authors` adds each author +that `authors.names` does not list yet, without a name and with the profile's name in a comment, so all +names are maintained in one place; an entry left empty prints the profile's name. + +Measured with `gh api repos/eccenca/documentation.eccenca.com/contributors` on 2026-09-14: + +| # | Commits | GitHub ID | +| --: | --: | :-- | +| 1 | 639 | rpietzsch | +| 2 | 532 | seebi | +| 3 | 66 | irangareddy | +| 4 | 62 | muddymudskipper | +| 5 | 61 | mgns | +| 6 | 53 | sobo | +| 7 | 29 | annamakor | +| 8 | 25 | msaipraneeth | +| 9 | 22 | saradaimi786 | +| 10 | 20 | louiswesterheide | +| 11 | 18 | robertisele | +| 12 | 12 | spl0tt | +| 13 | 11 | white-gecko | +| 14 | 10 | BorderCloud | +| 15 | 10 | haschek | +| 16 | 9 | MaximilianWenzel | +| 17 | 7 | tomatophantastico | +| 18 | 5 | adelahaye-ecc | +| 19 | 1 | dgrtner-ecc | +| 20 | 1 | looooph | +| 21 | 1 | peterfreytag | +| 22 | 1 | pkgut | + +Dropped: two anonymous entries (33 and 2 commits). + +Names, checked with `gh api users/` on 2026-09-15: 16 of the 22 profiles show a name. `rpietzsch`, +`annamakor`, `MaximilianWenzel`, `adelahaye-ecc`, `dgrtner-ecc` and `pkgut` show none; they need an entry +in `authors.names`, or they print as their ID, and the build warns about each. + +- **No software agent is in the list today.** Claude's commits carry a `Co-Authored-By` trailer and + count for the human author; GitHub does not list co-authors as contributors. The exclusion rule + still belongs in the tooling. +- GitHub counts commits on the default branch only. + +### R4 - Links on paper + +In the print edition (D8): internal links print their text plus a page reference `(p. 34)`; external +links print their text with a superscript number, and each part ends with a list of its web addresses +(D15, §11); no colour, underline or arrow. Link annotations +are left out of the book block - Ghostscript refuses PDF/X output while a page carries one (§7). + +## 4. Decisions + +D1-D11 made 2026-09-14; D12-D18 made 2026-09-15 (§10, §11). + +| # | Question | Decision | +| :-- | :-- | :-- | +| D1 | Scope | The print edition reduces sections instead of printing them in full, **configurable per section** as `full`, `list` or `omit`. Default: A.3 Task and Operator Reference → `list`, H Release Notes → `list`, everything else `full` (§5) | +| D2 | Provider | **BoD** (§2) | +| D3 | ISBN | none for now; may come later | +| D4 | Authors | names, most commits first: from `authors.names` in `print.yml`, else the GitHub profile, else the ID; no anonymous entries, bots, agents or excluded IDs (§3). Revised 2026-09-15 - first decided as GitHub IDs; commits count only on printed pages (D16) | +| D5 | Colour and paper | black and white interior on 80 g paper. BoD offers 80 g to publishers only - eccenca GmbH needs a publisher account | +| D6 | Editions | separate screen and print PDFs. `task pdf` stays the screen edition, unchanged; `task pdf:print` builds the book block, switched by a Typst input (`edition=print`) and a print configuration | +| D7 | Trim size | A4 | +| D8 | Links on paper | page references and URL footnotes, no link styling (R4); the footnotes become endnotes per part (D15) | +| D9 | Logo and version in the running header | title page and imprint only (R1) | +| D10 | Body type size | keep 10 pt and tighten the spacing (§5). A smaller body, 9 pt or even 8 pt, only if the page limit is still exceeded - not needed for the default configuration | +| D11 | Normalization | an **optional** Ghostscript pass after Typst: PDF/X-4, CMYK, all images at 300 dpi (§7) | +| D12 | Subtrees and pages | path rules in `tools/pdf/print.yml`: a `sections` key may name a page, which accepts `omit` only; no front matter property, no `.pdfexclude` (§10) | +| D13 | Parts of a page | the class `print-exclude`, effective in the print edition only; the site and the screen PDF are unchanged (§10) | +| D14 | What stands in for excluded content | pages and subtrees: nothing, also for a directory's `omit` (revises §5); parts of a page: one note per run of parts, pointing to the page in the online edition for the full details (§10) | +| D15 | Web addresses on paper | endnotes: a superscript number per address, numbered within each part; a list "Web addresses" with each address and the pages citing it closes each part (§11; revises R4 and D8) | +| D16 | Author order | commits to the printed pages and their images, renames followed, each commit once; merges and generated pages do not count (§11) | +| D17 | Operator reference | section mode `reference`: one compact entry per operator from `data/plugins.json` and the rendered description, without examples, replacing the overview tables; operators in alphabetical order with the category as a field, listed in the part contents; one type vocabulary (§11) | +| D18 | Screenshot widths | written into the sources for raster images without a width: pixel width / capture scale / the full page width (the 16 cm text column), rounded to 5 %, at most 100 %; capture scale 2 for 144 dpi, otherwise the declared density / 96, 1 without one (§11) | + +## 5. Page budget + +### Type size and spacing + +Measured by compiling variants of the current book (`dist/pdf/book.typ`) with modified copies of +`style.typ`: + +| Variant | Pages | Characters per line | +| :-- | --: | --: | +| today: 10 pt, paragraph spacing 2.24 em, block spacing 1.52 em | 1680 | 104 | +| body 9 pt | 1512 (−10 %) | 115 | +| paragraph spacing 1.2 em, block spacing 1.0 em | 1571 (−6.5 %) | 105 | +| both | 1414 (−16 %) | 115 | + +At 10 pt a full line already holds about 104 characters, well above the 45-75 recommended for +continuous reading; 9 pt makes it 115, and thinner light strokes on black-and-white 80 g paper make it +worse. The spacing saves two thirds as much without touching legibility. + +A two-column A4 layout would bring lines to a comfortable length, but wide tables, code blocks and +screenshots would need to break out of the columns. Not proposed. + +A heavier weight than Light for the print body (Regular) may print more reliably in black and white; +measure its page cost before deciding. + +### Section modes + +`full` prints the section as today. `omit` leaves it out without a trace in the text (D14, revised +2026-09-15); the imprint names the online edition as the complete reference. `list` reduces it to a +two-column table: + +| Section | `list` renders | Measured | +| :-- | :-- | :-- | +| A.3 Task and Operator Reference | the section's own page and its five overview pages (Aggregators, Custom Workflow Tasks, Datasets, Distance Measures, Transformers), which already are generated `Name \| Description` tables; the 176 individual operator pages are dropped, and the navigation's operator categories under Transformers print as one **Category** \| **Transformers** table instead of empty headings | 458 → about 17 pages | +| H Release Notes | one table: **Release** (e.g. `Corporate Memory 26.2.1`) and **Summary** (the release page's introductory paragraph; where a release has none, the components it lists, e.g. `DataIntegration v26.2.0, Explore v26.2.2, …`) | 22 releases, 187 → about 3 pages | -`task preview` covers the gap: it runs a full `task build` and serves the result on port -8001. Verified: 45 sections on `/tags/`, 3 on `/tutorials/`, 0 markers, 0 unpkg references. -This is inherent to post-build processing and goes away with the script. +In `list` mode, links from the kept tables to dropped pages print as plain text, not as footnotes; the +section opens with a sentence naming the online reference URL. -## 7. Removal +The configuration lives in `tools/pdf/print.yml`, keyed by the section's path in `nav.yml` +(`build/reference/`, `release-notes/`), so a section can be switched without touching code. -The trigger is already wired. `check_zensical_output.py` tracks `tag-listings` as `PEND` -and prints a `NEW` banner the moment Zensical renders a listing itself. On that signal: +### Estimate for the default configuration -1. delete `tools/render_tag_listings.py` -2. drop its line from `build` in `Taskfile.yml` -3. delete `overrides/partials/tags.html` if §9 was implemented - Zensical will populate - `tag.url` itself once it has a listing index, and its stock template already handles it -4. drop the `tag-listings-*` checks from `check_zensical_output.py` +| Step | Pages | +| :-- | --: | +| today | 1680 | +| A.3 as `list` | ≈ 1239 | +| H Release Notes as `list` | ≈ 1055 | +| tighter spacing (−6.5 %) | ≈ 985 | +| imprint, recto starts and blank pages (at most 28) | ≈ 1015 | -Sources need no changes, because they were never changed. +About 1,015 pages: within BoD's 1,200 for A4 on 80 g. + +## 6. Further changes towards a print-ready book block + +### Pagination + +- Parts start on a recto page: cover recto, then contents recto, then the part's text recto + (`pagebreak(to: "odd")`). Costs at most three blank pages per part, 27 in total. +- The front contents start on page 3, recto. +- Blank pages stay empty - no header, no footer, no page number. +- The final page count is even. +- A heading never ends a page with a single line under it. Headings are sticky, which keeps one block with + them; a short lead - the intended audience of a section, say - satisfies that and leaves the two of them + at the foot of the page. The build therefore makes a lead of at most 200 characters under a heading of + level 1 to 4 sticky as well (backlog P25), so both move to where the content starts. + +### Typography + +- Hyphenation on for the print edition (`lang: "en"`); justified text without it opens wide gaps. +- Widow and orphan costs, so no single line is left at the top or bottom of a page. +- Minimum type size: code 7.7 pt and the 6.3 pt terminal tables are acceptable on A4; set a floor in + `codeblock()` so a wider table cannot shrink below 6 pt. +- Hairlines: table rules are 0.3 pt, dividers 0.5 pt - above the usual 0.25 pt minimum. +- Tighter paragraph and block spacing (§5). + +### Black and white + +- **Grey areas at least 20 % black** (BoD). Today's grounds are far lighter: the code and note ground + `ec-wash` (`#F3F5F6`) and the peach bands print as a few percent grey. Either raise them to 20 % or + drop the fills in favour of rules. +- Orange footer text, the link blue and the admonition accents need values that stay distinguishable + in grey; the orange running title in particular turns into a pale grey. +- The part diagrams mark their part with a red frame; in grey the frame stays visible but no longer + stands out. Check the four diagrams. +- Screenshots print in grey; UI states that differ only by colour lose their meaning. + +### Images + +- In the print scope 331 of 586 images are below 300 ppi at their printed size, 49 below 150. +- Stop scaling small screenshots up to the text width, which lowers their resolution further - the + cheapest way to bring images closer to 300 ppi. +- 565 images carry transparency, almost all of them PNG screenshots with an alpha channel over a white + page. BoD wants transparency reduced before delivery, and PDF/X-4 does not remove it (§7). Composite + the alpha channel onto white while normalizing images, before Typst embeds them - lossless for these + images, and the book block then carries no transparency at all. + +### PDF output + +- The optional normalization pass (§7). +- A preflight report per build: page size, page count against 1,200 and parity, all fonts embedded + (`pdffonts`), images below 300 ppi (`pdfimages -list`), soft masks left, page-number position per page + (`pdftotext -bbox`), blank pages really blank. + +### Content + +- Web-only phrasing: "click here", "Next chapter:" links, embedded videos printed as + `[iframe: ]`. Print them as a footnote, or as a QR code for the few videos. +- Tutorial step headings that number themselves print as `A.9.3 1 Install …` - a content cleanup. +- An index at the back from the front-matter tags (45 tags on 531 pages) would give a paper reader a + second way in. Optional. + +### Delivery + +- CI builds the book block as a second artifact next to the screen PDF. +- The cover (§9) needs the final page count and paper for its spine width, so it comes last. + +## 7. Optional normalization pass (Ghostscript) + +An optional post-processing step on the print PDF: `task pdf:print` runs it when asked +(`--normalize` / `PDF_NORMALIZE=1`) and writes `…-print-x4.pdf` next to the unnormalized file. BoD +accepts either; the normalized file is what a provider preflight sees. + +### What it does + +| Target | How | +| :-- | :-- | +| PDF/X-4 | `-dPDFX=4` with a prefix file derived from Ghostscript's `lib/PDFX_def.ps`, which writes `/GTS_PDFXVersion (PDF/X-4)`, `/Trapped /False` and the output intent | +| CMYK | `-sColorConversionStrategy=CMYK -sProcessColorModel=DeviceCMYK`; the output intent profile is ISO Coated v2 (FOGRA39), the space BoD assumes for CMYK | +| Images at 300 dpi | down: `-dDownsampleColorImages=true -dColorImageDownsampleType=/Bicubic -dColorImageResolution=300 -dColorImageDownsampleThreshold=1.0`, the same for grey images; lossless re-encoding with `/FlateEncode`. **Up: not possible in Ghostscript** (below) | +| No annotations | `-dPreserveAnnots=false` | + +### Verified with Ghostscript 10.08.0 + +Converted pages 3-40 of the current PDF on 2026-09-14: + +- the file carries `/GTS_PDFXVersion (PDF/X-4)` in the document info and `pdfxid:GTS_PDFXVersion='PDF/X-4'` + in XMP, one output intent, `/Trapped /False`, no annotations, all 154 fonts embedded +- all 7 images converted from ICC-based RGB to CMYK; the 610 ppi image came out at 300 ppi +- the 215 ppi image **stayed at 215 ppi: Ghostscript downsamples, it never upsamples** +- all 7 soft masks survived - PDF/X-4 keeps transparency +- the Build cover rendered from the CMYK file shows no visible colour shift against the RGB original at + screen resolution; the red frame, the orange and the peach band stay distinct +- 38 pages in 6 s: the whole book block takes a few minutes +- Ghostscript 10.08's documentation (`doc/src/VectorDevices.rst`) confirms `-dPDFX` values 1, 3 and 4 + (default 3) and PDF/X-4 support; the rendered manual page still says X-1 and X-3 only + +Traps found: + +- **A page with any annotation makes Ghostscript revert to normal PDF output**, with only a one-line + warning. The book has about 3,900 link annotations - drop them (`-dPreserveAnnots=false`) and fail the + task if the output lacks the PDF/X marker. +- `-dPDFX=4` alone writes **no** PDF/X marker and no output intent; the prefix file is required. +- `-sOutputICCProfile` with `-dPDFX` crashes the PDF interpreter (`/undefined in --runpdf--`) and leaves a + truncated file - supply the profile only through the prefix file, which reads it with + `--permit-file-read=`. +- **PDF/X-4 needs Ghostscript 10.03 or newer.** Before that, pdfwrite declares `PDFX` a boolean + (`gs_param_type_bool` in `devices/vector/gdevpdfp.c`), so the 4 of `-dPDFX=4` raises + `/typecheck in --pdfmark--` while the prefix runs. Ubuntu 24.04 ships 10.02 and cannot write PDF/X-4 at + all; its own `PDFX_def.ps` only ever writes `PDF/X-3:2002`. The build says so before it starts, and CI + runs Ghostscript from a container instead (backlog P16). +- Ghostscript ships a generic `default_cmyk.icc`, not FOGRA39. The ISO Coated v2 profile comes from the + ECI. **Not vendored** (checked 2026-09-15): the ECI only says the profiles may be "freely downloaded", + and the profile's own copyright reads "© Heidelberger Druckmaschinen AG. All Rights Reserved". The + normalization fetches `ECI_Offset_2009/ISOcoated_v2_eci.icc` from + `https://www.eci.org/lib/exe/eci_offset_2009.zip` once, checks its SHA-256 + (`128dc02f…94b8`) and caches it in the gitignored `dist/icc/`; `--icc-profile` or `PDF_ICC_PROFILE` + names a local copy instead, for CI without network access. +- **Ghostscript 10.08 cannot convert transparency drawn by Typst** (found on the full book block, + 2026-09-15). Typst writes colour emoji as a Type 3 font whose glyphs carry shadings and soft masks, and + embeds SVGs that use opacity or masks with transparency groups and soft masks: + - with CMYK conversion, the emoji pages make pdfwrite **segfault** (exit -11) when it closes the file - + not on every run, and after all pages, leaving a truncated file + - with `-dPDFX=4`, it reports `error executing PDF token`, calls the error repaired, exits 0 and + **leaves out** what it could not draw: the ⚠️ emoji, most of the CKAN card icon + - `-dNOTRANSPARENCY` avoids the error but ignores the masks: an Excalidraw diagram then prints its + arrow through the label + - `qpdf --check` finds nothing wrong in the input, and without `-dPDFX` and CMYK the pages convert + + So the print edition hands Ghostscript no transparency (backlog P13): the build renders SVGs that use + opacity, masks or filters, and every colour emoji sequence, to PNG on white with Typst, and the style + swaps each emoji sequence for its image with a show rule, in code too. The preflight fails on + transparency and Type 3 fonts, and the normalization fails on a token error instead of trusting exit 0. + +### Upsampling to 300 dpi + +Ghostscript cannot raise an image's resolution. Images below 300 ppi at their printed size have to be +resampled **before** Typst embeds them, in the build: + +- the printed width is known at build time for the common case: an image wider than the text column + prints at the text width (16 cm), a narrower one at its natural size +- the build writes a resampled copy (Lanczos) to `dist/pdf/images/` with enough pixels for 300 ppi at + that width, and points Typst at it; the originals under `site/` stay untouched +- the same step composites alpha onto white (§6), so resampling and transparency reduction happen once + +Upsampling adds no detail - BoD says so itself: an image does not get better by raising its resolution +in a graphics program. It gives the printer a 300 dpi image to rasterize instead of leaving the +interpolation to the press, and it makes the preflight report clean. The originals below 150 ppi still +need replacing (P14). + +### Black and white + +The book prints in black and white, but BoD converts RGB itself and the pass is specified as CMYK. A +greyscale variant (`-sColorConversionStrategy=Gray`) shows the final tones on screen and gives smaller +files. Kept CMYK as specified; greyscale is a preview mode of the same pass for checking the palette +(P11), built on 2026-09-15 as `--gray`: DeviceGray with the same image handling and failure checks. It is +PDF/X-4 as well (2026-09-16), with the generic grey profile Ghostscript ships, `default_gray.icc`, as its +output intent - one component instead of four, the condition `sGray`, and no registry, since the +condition is not a registered one. `--icc-profile` names another grey profile. ## 8. Acceptance -- `/tags/` renders 45 tag sections; `/tutorials/` renders 3, in marker order -- zero `` comments remain in `site/**/*.html` -- `check_zensical_output.py` no longer reports unexpanded markers -- every generated link resolves to a real file in `site/` -- tag chips in listings render with icons for the 31 mapped tags -- `task build` still clean under `--strict`; page count unchanged at 584 -- output stable across two consecutive builds (no ordering nondeterminism) - -## 9. Link page tag chips to their listing anchor - -**Status:** implemented 2026-08-23 as `overrides/partials/tags.html`. All three open -questions settled as recommended; the reachability assertion is `tag-chips-resolve` in -`check_zensical_output.py`. - -### Problem - -Rendering the listings only solved half the feature. Material also turns each per-page tag -chip into a link to its section on `/tags/`, so a reader can jump from "this page is tagged -Docker" to "everything tagged Docker". Zensical emits the chips as inert ``s. - -Measured on the current build: **531 pages carry a tag nav, 703 chips in total, 0 of them -links.** Production, for the same page: - -```html -Configuration +- `task pdf` builds the screen PDF exactly as before: 1680 pages, unchanged look +- `task pdf:print` builds the book block with the configuration in `tools/pdf/print.yml` +- page 1 is the title page naming eccenca GmbH as publisher; page 2 is the imprint with the publisher's + address and the authors by name, from `tools/pdf/authors.yml` and `print.yml`, most commits first +- on every numbered page the page number sits at the outer edge: left on even pages, right on odd + pages - checked by the preflight report, not by eye +- title page, imprint, part covers and blank pages carry no header and no footer; text pages carry no + logo and no version stamp +- the front contents and every part cover start on an odd page +- A.3 and H Release Notes print as two-column lists in the default configuration; switching either to + `full` or `omit` in `print.yml` works without code changes +- internal links print page references, external links footnotes; the book block has no link annotations +- page count even and at most 1,200 +- grey areas at least 20 % black; all fonts embedded; no soft masks left +- every image at 300 ppi or more at its printed size; an original below 150 ppi is listed in + `dist/pdf/print/low-resolution.tsv`, which `dec-tool image-widths` empties by narrowing the image to the + width its pixels support - `accepted-low-resolution` in `print.yml` keeps one at its size instead +- with normalization: the file declares PDF/X-4, carries a FOGRA39 output intent, CMYK images only, no + image above 300 ppi +- a page key in `print.yml` drops that page; `omit` leaves no title and no note for a page or a section +- in the print edition, each run of `.print-exclude` parts is replaced by one note with the page's + online address; the site and the screen PDF show the parts unchanged +- `task check` and `task test:unit` pass + +## 9. Out of scope + +- the cover file: front, spine and back, spine width from page count and paper, barcode +- ISBN and retail distribution (D3, maybe later) +- EPUB or other e-book formats +- translations + +## 10. Excluding content from the print edition + +**Status:** decided and implemented 2026-09-15 (D12-D14 in §4, backlog P18). With the three examples +configured, the print edition has 666 pages instead of 966. + +The section modes (§5) shorten whole reference sections. Some content is unfit for paper at a finer +grain: + +| Grain | Example | Print pages | +| :-- | :-- | --: | +| subtree | G.5 cmem-client: Python API, `develop/cmem-client-api/` - 75 pages, 74 of them generated | 207 | +| subtree | A.16 How to link IDS to OSINT, `build/tutorial-how-to-link-ids-to-osint/` - all 7 pages (decided 2026-09-15) | 68 | +| page | none configured yet; a page key leaves out a single page when one needs it | - | +| part of a page | A.14 Connect to Snowflake: the SQL code block of the collapsed `??? example "INSERT query"` block, lines 92-1094 of `docs/build/snowflake-tutorial/index.md` on 2026-09-15 - about 1,000 lines (decided 2026-09-15) | 25 (pp. 122-146) | + +Measured in the print edition of 2026-09-15 (966 pages). Excluding all three saves about 300 pages. + +### Constraints + +- The build reads the rendered site, not the Markdown. A marker for a part of a page must survive + Zensical's rendering; a rule for a page or subtree must be decidable from the paths in `nav.yml`. +- Generated pages are rewritten wholesale: `task update:cmemc` and `task update:cmem-client-api` run + `rm -rf /*`, and `dec-tool update-di-reference` deletes its whole tree. Whatever is stored in a + generated page, or next to it, is lost on the next run. +- The site and the screen PDF stay as they are (D6). + +### Options + +| Option | Subtree | Page | Part | Verdict | +| :-- | :-- | :-- | :-- | :-- | +| front matter property, e.g. `print: exclude` | each page marked | yes | no | rejected: lost on generated pages; a subtree means marking every page; no single place shows what the book leaves out | +| `.pdfexclude` file in gitignore syntax | yes | yes | no | rejected: a second configuration next to `print.yml`, with a new format and a discovery rule, spread over the tree; deleted with a generated directory | +| path rules in `tools/pdf/print.yml` | yes - `omit` exists | yes, once a key may name a page | no | **chosen** for subtrees and pages (D12) | +| comment pair `` … `` | no | no | yes | works, not proposed: the pair must stay siblings, a single block costs two extra lines, and a misspelt marker is ignored without a trace | +| class `print-exclude` on the rendered element | no | no | yes | **chosen** for parts of a page (D13) | +| CSS selectors per page in `print.yml` | no | no | yes | rejected: ties the configuration to theme markup | +| `exclude_docs` or `not_in_nav` in `mkdocs.yml` | yes | yes | no | rejected: removes the pages from the site as well | + +Verified on 2026-09-15 in a scratch project with Zensical 0.0.62 and the Markdown extensions of +`mkdocs.yml`: + +- **Comment pairs:** they arrive in the HTML as unescaped comments. Both comments of a pair stay + side by side, at top level and inside an admonition, a content tab and a list item. +- **The class:** it lands on the element in every spelling listed below. Removing the `.print-exclude` + elements leaves exactly the unmarked content. +- **Front matter:** an unknown front matter key builds without a warning. + +### Design + +#### Subtrees and pages: path rules in `print.yml` (D12) + +A key under `sections` names a docs directory, ending in `/` as today, or a single page, ending in +`.md`: + +```yaml +sections: + deploy-and-configure/: omit + develop/: omit + build/tutorial-how-to-link-ids-to-osint/: omit + # a single page: /index.md: omit ``` -versus ours: - -```html -Configuration +- A directory key keeps its three modes (§5). `omit` drops the section without a title or a note (D14). + The first implementation (P8) left both; the imprint names the online edition as the complete + reference instead. +- A page key accepts `omit` only; any other mode fails the build. The page is dropped without a note. +- Dropping the index page of a section keeps its other pages, under a heading with the section's + navigation title - the shape Release Notes already has (§3). +- **Scope of the first edition (decided 2026-09-16, backlog P26):** it is a user guide, so the parts an + administrator or a developer needs are omitted whole - E `deploy-and-configure/` (34 pages) and + G `develop/` (89 pages). The narrower `develop/cmem-client-api/` key goes with it, because keys do not + nest. A reference from a printed page into an omitted part is not lost: it becomes a web address in its + part's endnote list (D15), pointing at the online edition. +- Keys do not nest: a key inside a directory that another key shortens fails the build. +- A key that matches no page in `nav.yml` fails the build, as today. +- A link to a dropped page prints the page's online address in a footnote, like any link that leaves + the book (R4). Within a section that `list` or `omit` shortens, it prints as text, as today. + +#### Parts of a page: the class `print-exclude` (D13) + +One class, spelled the way the element takes it: + +````markdown +??? example print-exclude "INSERT query" + + ```sql + INSERT INTO product(...) VALUES ... + ``` + +```{ .sql .print-exclude } +SELECT ... ``` -Now that §1-§8 generate those anchors, every one of these links has a valid destination. -The anchors exist; nothing points at them. - -### Approach: template override, *not* post-build injection - -`zensical/templates/partials/tags.html` is **byte-identical to Material 9.7.7's**. It -already contains the branch that produces a link: - -```jinja -{% if tag.url %} - {{ tag.name }} -{% else %} - {{ tag.name }} -{% endif %} -``` - -The template is not the problem - `tag.url` is simply never populated, because Zensical has -no listing index to point at. So the fix is to compute the URL in an override rather than -depend on `tag.url`, exactly as `overrides/partials/tabs-item.html` does for tab icons. - -This is strictly better than extending `render_tag_listings.py`: - -| | template override | post-build injection | -| :-- | :-- | :-- | -| Works in `task serve` | **yes** | no (see §6) | -| Extra build step | none | another pass over 531 pages | -| Removal when #38 lands | delete one file | more code to unpick | - -### Verified feasible - -A spike produced output byte-identical to production's, then was reverted. MiniJinja -supports the three filters needed (`url`, `lower`, `replace`): - -```jinja -{% set anchor = "tags/" | url ~ "#tag:" ~ (tag.name | lower | replace(" ", "-")) %} -{{- tag.name -}} +A paragraph the print edition leaves out. +{ .print-exclude } + + +```` + +- The print edition removes every element with the class from a page's article, before ids, links and + headings are processed. The class has no effect on the site and the screen PDF. +- A heading inside a removed part leaves the numbering and the contents. Links to it print as text, the + rule for any link without a target. +- The build logs how many parts it removed per page, so a marker that no longer matches shows. +- A note takes the place of a removed part (D14): *This print edition leaves out a part of this page. + The online edition has the full details:* followed by the page's online address, with the anchor of + the heading the part belongs to. Consecutive removed parts share one note. +- In the Snowflake tutorial the class goes on the SQL code block, as `{ .sql .print-exclude }` on its + opening fence: exactly lines 92-1094 are removed. The `??? example "INSERT query"` block around it keeps + its title and holds the note. +- A generated page cannot carry the class: its generator has to emit it, or a path rule drops the page. +- The name follows the edition. The site does not style the class; a later `@media print` rule could + use it for printing from the browser. + +### Implementation outline + +- **Rules:** `load_section_rules` accepts `.md` keys, rejects any mode but `omit` for them and rejects + nested keys. +- **Page `omit`:** `apply_section_rules` drops the page's entry. For an index page it inserts a heading + entry with the navigation title, which `NavEntry.title` already carries. +- **Parts:** in the print edition, `page_article` replaces the `.print-exclude` elements with the note - + one per run of consecutive parts - and counts them. +- **`omit` for directories:** no title and no note any more. `test_omit_drops_the_pages_and_leaves_a_heading_with_a_note` + and `test_merge_renders_an_omitted_section_as_its_title_and_a_note` change with it. +- **Tests:** `tests/test_build_pdf_print.py` gets page keys, the mode check, nesting, a dropped index + page, and the removal with its note and online address in the print edition only. +- **Documentation:** the `sections` comment in `print.yml`, and for authors a line in + `.claude/docs-guidelines/repo-conventions.md` on the class, including that generated pages cannot + carry it. +- **Effort:** small to medium; backlog P18. + +## 11. Review of the first print build + +**Status:** findings of the pull request review, 2026-09-15, decided the same day as D15-D18 (§4). +P19-P24 and P13 are implemented; P15 keeps 42 images to replace or accept. + +| Finding | Today | Proposal | Backlog | +| :-- | :-- | :-- | :-- | +| A left-hand page's footer should read `Part A: Build` | `A Build` (R1) | the part label the part band prints | P19 | +| Web addresses belong at the end of the document | a footnote for each link out of the book (R4) | endnotes, listed at the end of each part (D15) | P20 | +| The author order should follow the printed content | commits to the whole repository (§3) | commits to the printed pages (D16) | P21 | +| Facing cards in a two-column grid should be equally high | each card as high as its text | one height per row | P22 | +| The operator reference should print descriptions and parameters | overview tables, `list` mode (§5) | one compact entry per operator, replacing the overview tables (D17) | P23 | +| PDF/X-4 and CMYK | specified, not built (§7) | unchanged; done, with transparency rendered to images first | P13 | +| Low-resolution images mostly lack `width` in the Markdown | an image without `width` prints at its declared density | widths written into the sources so every image prints at 150 ppi or better (D18, revised 2026-09-16); done, and P15 with it | P24, then P15 | + +### Footer: the part label + +R1 names the part beside the page number of a left-hand page as `A Build`. The part band already prints +`Part A: Build`, while the contents and the bookmarks print `A Build`. The left-hand footer follows the +band: `1234 Part A: Build`. The contents, the bookmarks and the right-hand footer stay as they are. + +### Web addresses as endnotes (D15) + +Today every link out of the book prints its address in a footnote on the same page (R4). Decided: + +- A link out of the book prints its text and a superscript number. +- The numbers run within a part and start again at 1 in the next one. An address cited more than once in + a part keeps its first number there. +- A list "Web addresses", on a new page under an unnumbered heading, closes each part that cites any. It + holds each number, its address and the pages that cite it, for example + `17 https://github.com/eccenca/cmemc (pp. 412, 530)`. +- The list is laid out without link annotations, like the contents (P9). Footnotes link marker and entry + internally, which Typst cannot switch off, and PDF/X output wants no annotation at all (§7). +- Links within the book keep their page reference. + +Numbering in a `context` rule over every link costs layout passes; measure the compile time and watch +for Typst's warning that the layout did not converge. + +### Author order from the printed content (D16) + +Today `tools/pdf/authors.yml` counts a contributor's commits to the whole repository, from the GitHub +contributors API. Tooling, generated references and content the print edition leaves out weigh as much +as the printed pages. The repository has 1,633 commits, 1,292 of them touching `docs/`. Decided: + +- The printed files: the pages the print edition prints after the section rules (§5, §10), and the + images in their directories. +- The commits: `git log --no-merges` for those files, following renames. A commit counts once, however + many printed files it touches. +- The account of each commit: `author.login` from the GitHub commits API, which maps a commit's e-mail + to a GitHub account - one request per 100 commits, about 17. +- The existing rules stay: no anonymous commits, bots or agents; `authors.exclude`; names from + `authors.names` or the profile. +- `authors.yml` records these counts. The imprint reads "most commits to the printed pages first". + +Commits to generated pages credit whoever ran the generator, so they do not count. + +### Cards of equal height + +`cards()` sets `card()` blocks in two columns, each as high as its text, so facing cards end at +different heights. Plan: lay the grid out row by row, measure both cards of a row at the column +width, and give both the taller height. A card alone in the last row keeps its own height. The rounded +frame and `breakable: false` stay. The screen PDF uses the same grid and changes with it. + +### The operator reference in a compact format (D17) + +A.3 prints as `list` (§5): the overview pages with their `Name | Description` tables, while the 389 +operator pages are dropped. The review asks for the operators themselves, with their descriptions and +parameters but without examples. In the print edition the operator entries replace the overview tables. +The format below was decided as proposed (D17). + +All 389 operators share one structure. `tools/templates/plugin.md` generates their pages, and the +generator dumps the same data to `data/plugins.json`, which is tracked and was last regenerated together +with the pages on 2026-09-02. Much of the structure is sparse (measured 2026-09-15): + +| Part | Measured | +| :-- | :-- | +| parameters | 1,040 in all; 59 operators have none, 316 have no advanced ones. Median per operator: 4 for custom workflow tasks (at most 24), 3 for datasets, 1 for distance measures and transformers, 0 for aggregators | +| defaults | 431 parameters have none; 8 are multi-line, 4 structured | +| parameter descriptions | 27 are empty | +| sub-parameters | 21 | +| descriptions | median 263 characters; 154 under 200, 43 over 2,000 | +| examples | on 91 operators, 19 % of the documentation text | +| related plugins | on 81 operators | +| Python plugins | 71 | + +A generated page spends a heading and three bullets on every parameter (`ID`, `Datatype`, +`Default Value`). It writes `None` both for a missing default and for an empty advanced section, and +repeats the Python plugin note on every Python operator. + +#### Structure + +- **Chapters:** each operator type keeps its chapter and its introduction, A.3.1 Aggregators to A.3.5 + Transformers. The chapter's operator entries replace its overview table. +- **Order:** operators follow in alphabetical order, as in the overview tables. The transformer category + becomes a field of the entry instead of a level of headings. So every operator sits at the same level, + A.3.x.y, and the part contents list them with their pages. +- **Introduction:** the introduction of A.3 says once what an entry shows, that the examples are part of + the online edition, and what a Python plugin needs. + +#### An entry + +```text +A.3.5.42 Constant transformer · Value · constant + +Generates a constant value. + +Parameter Type Default Description +Value text – The constant value to be generated +value ``` -`| url` resolves `tags/` relative to the current page, so it survives mike's versioned -prefixes without special handling. - -### Open questions - -1. **Slug duplication.** The template computes the slug in MiniJinja; `render_tag_listings. - tag_slug()` computes it in Python. They must agree or every link dangles. Accept the - duplication with a comment in both places, or have the renderer assert that each anchor - it emits is reachable? **Recommend: assert** - it is a cheap required check and the - failure mode is otherwise silent. - -2. **Tags with no listing section.** A tag carried *only* by the listing page itself - produces no section (self-exclusion, §3), so its chip would link to a dead anchor. Does - not occur today. **Recommend: cover it with the check in Q1** rather than special-casing. - -3. **`hide: tags` pages.** The existing template already honours it; the override must keep - that branch. No decision needed, just do not drop it. +- **Heading:** the operator's title, numbered, kept together with what follows. +- **Field line:** the operator type, the transformer category, the plugin ID in monospace, `Python plugin` + where it applies, and a distance measure's range. It is small, grey and one line long. +- **Description:** the rendered description from the site page, without its `## Examples` section and + without the Python plugin note. Its own second-level headings, such as `## Characteristics`, print as + run-in labels, not as numbered sections. +- **Parameters:** one table per operator, in the plugin's order. The columns are Parameter (the title, and + the ID in monospace below it), Type, Default and Description. Advanced parameters follow in the same + table after a row labelled `Advanced`; a sub-parameter follows its parameter as `parent.child`. +- **Related:** one line, for example `Related: Merge (p. 214), Zip (p. 230)`. + +Sparse data prints as nothing: + +- An operator without parameters has no table, and one without advanced parameters no `Advanced` row. +- A parameter without a default shows `–`; a password never shows one. +- A multi-line or structured default shows `see below`, and a code block follows the table. +- An operator without related plugins has no `Related` line, and an empty parameter description leaves + its cell empty. + +The data types print in one vocabulary: + +| Data type | Printed | +| :-- | :-- | +| `string`, `multiline string` | text | +| `int`, `Long` | integer | +| `double` | number | +| `boolean` | boolean | +| `char` | character | +| `enumeration` | choice | +| `password` | password | +| `resource` | file | +| `scheme:string` | URI - to confirm | +| `traversable[string]` | list of text | +| `stringmap` | map | +| `code-sparql` | SPARQL | + +#### Data source + +The build takes the structure and the parameters from `data/plugins.json` and only the description from +the rendered site page. The JSON tells a missing default apart from the text `None`, and it carries the +types, the advanced flags and the sub-parameters that the page shows only as bullets. The build fails when +an operator page and the JSON disagree - an operator without an entry, or an entry without a page - so a +regeneration that updated only one of them shows. + +#### Budget + +A rough count - 95 characters per line of description, one row per parameter and more for a long +description, four lines for heading and field line - gives about 180 pages for the 389 entries. The part +contents add about 8 pages. The book would grow from 666 to about 860 pages; the operator pages in full +took about 458 (§5). BoD takes 1,200 pages on 80 g and 1,050 on 90 g (D5). Measured after P23: A.3 takes 213 pages and the +book 870. + +The site and the generator stay as they are. The same format could later serve the site as well; that is +not part of this proposal. + +### PDF/X-4 and CMYK + +Unchanged: the optional normalization pass (§7, D11), backlog P13. The review confirms that it is +needed. + +### Image widths in the sources (D18) + +Typst prints an image without `width` at the pixel density it declares, 72 dpi when it declares none, +and never wider than the column. On 2026-09-15 the printed pages that are not generated hold 500 raster +images, 352 of them without `width`. The declared densities: none on 231, 144 dpi on 180 (Retina +captures on macOS), 120 dpi on 80, 96 dpi on 8, 192 dpi on 1. 43 images print below 150 ppi, 37 of them +without `width`. + +Decided as D18 on 2026-09-15 from the capture density, and **revised on 2026-09-16** once the report of +P15 showed what each image actually prints at. The rule is the printed density: `dec-tool image-widths`, +with `--fix`, narrows every raster image that prints below 150 ppi in pages that are not generated. + +- **Width:** the pixel width divided by the 6.3 inches of the text column and by the 150 ppi target, + rounded **down to a whole percent**, with one further step down where the rounding of the density would + leave it short. The width a page declares cancels out of `declared × density ÷ target`, so the pixels + alone decide it. +- **Measured at** the width the page declares, quoted or not; an image without one fills the column and + counts as 100 %. +- **Scope:** raster images only. SVGs, remote images, fenced code and images already at 150 ppi or more + stay as they are, as do generated pages. +- **No floor:** an image whose pixels only support a thumbnail gets the thumbnail; the two smallest are + 14 % and 21 %, each a snippet of a dialog. +- **Check:** `task check` runs it as `check:images`, so a new screenshot is caught before it is built. + +The same percentage serves the site and the PDF: on paper the image prints at 150 ppi or better, and the +site shows it at the share of the article it fills on the page. This closes P15 - `--fix` wrote 47 widths +into 21 pages and the report is empty - rather than replacing screenshots. The superseded rule, natural +width from the capture scale rounded to 5 %, left 42 images below the target. + +### Decisions + +Decided on 2026-09-15 as D15-D18, recorded in §4: web addresses in a list at the end of each part (D15), +the author order from commits to the printed pages (D16), the compact operator reference as proposed +(D17), and screenshot widths against the full page width (D18). diff --git a/tests/test_build_pdf.py b/tests/test_build_pdf.py index 639731c53..38eb1eb6d 100644 --- a/tests/test_build_pdf.py +++ b/tests/test_build_pdf.py @@ -24,6 +24,7 @@ def soup(markup: str) -> BeautifulSoup: def test_section_opens_with_its_index_page(): + # A page keeps its navigation title; an index page takes its section's. nav = [ {"Build": [ {"Build": "build/index.md"}, @@ -32,10 +33,10 @@ def test_section_opens_with_its_index_page(): ]}, ] assert nav_entries(nav) == [ - NavEntry(depth=0, md="build/index.md"), - NavEntry(depth=1, md="build/rules/index.md"), - NavEntry(depth=2, md="build/rules/linking.md"), - NavEntry(depth=1, md="build/spark.md"), + NavEntry(depth=0, md="build/index.md", title="Build"), + NavEntry(depth=1, md="build/rules/index.md", title="Rules"), + NavEntry(depth=2, md="build/rules/linking.md", title="Linking"), + NavEntry(depth=1, md="build/spark.md", title="Spark"), ] @@ -50,13 +51,13 @@ def test_section_without_index_page_gets_a_heading(): assert nav_entries(nav) == [ NavEntry(depth=0, title="Release Notes"), NavEntry(depth=1, title="2026"), - NavEntry(depth=2, md="release-notes/2026/corporate-memory-26-2/index.md"), + NavEntry(depth=2, md="release-notes/2026/corporate-memory-26-2/index.md", title="v26.2.1"), ] def test_page_listed_twice_appears_once_and_links_are_skipped(): nav = [{"A": ["a/index.md", {"Again": "a/index.md"}, {"Site": "https://example.org/"}]}] - assert nav_entries(nav) == [NavEntry(depth=0, md="a/index.md")] + assert nav_entries(nav) == [NavEntry(depth=0, md="a/index.md", title="A")] def test_merge_breaks_chapters_adds_section_headings_and_drops_web_chrome(tmp_path): @@ -73,7 +74,7 @@ def test_merge_breaks_chapters_adds_section_headings_and_drops_web_chrome(tmp_pa NavEntry(depth=1, md="release-notes/v1/index.md"), NavEntry(depth=1, md="release-notes/v2/index.md"), ] - doc, missing = merge_pages(entries, site, "https://example.org/latest/") + doc, missing, _ = merge_pages(entries, site, "https://example.org/latest/") assert missing == ["release-notes/v2/index.md"] assert str(doc.body) == ( '

Release Notes

' @@ -91,7 +92,7 @@ def test_part_cover_is_the_title_then_the_diagram_the_page_shows_above_it(tmp_pa '

Build

Intro

' '' ) - doc, _ = merge_pages([NavEntry(depth=0, md="build/index.md")], site, "https://example.org/latest/") + doc, _, _ = merge_pages([NavEntry(depth=0, md="build/index.md")], site, "https://example.org/latest/") assert str(doc.body) == ( '