Delete the experimental layout from syntax, renderer geometry, and tests. Document dependency-based column behavior and publish an agent-facing authoring contract that keeps geometry changes in source rather than generated SVG internals.
- commit
- b3c92c7027619e10e42274ac96128f19095f2d82
- parent
- 3d50aae5905eaefea06ba9e4f9e46dff65b1fb13
- author
- Dami <dami.etoile@gmail.com>
@@ -391,7 +391,8 @@ export const diagramTypes = [
391391 format: 'Tabular Recipe Notation text',392392 summary: 'TRN stands for Tabular Recipe Notation. It shows how ingredients and intermediate outcomes combine into one or more final results.',393393 use: 'Use TRN for recipes, crafting trees, assembly instructions, and other nested transformations.',394- syntax: ['Optionally begin with `.layout combined` or `.layout individual`; the default is `combined`.', 'Declare each leaf value with `ingredient`.', 'Declare a meaningful final or reusable result with an `outcome` block.', 'Keep incidental conversions, such as ore to bars, as steps inside the outcome that consumes them.', 'Add a full-width preparation row with `instruction`.', 'Long instruction rows wrap and grow vertically to preserve their complete text.', 'Add a quoted display label after an identifier when needed.', 'Each `+` queues an input for the next `->` action.', 'Combined layout places prerequisite outcomes before direct ingredients in stable FIFO order.', 'Each `->` adds an operation column from left to right.', 'Add `portion` after an outcome label or identifier when the result has a useful quantity.', 'Use `{portion}` in an action to embed that outcome quantity.', 'Start comments with `#`; OCR importers should record uncertain text in comments instead of guessing.'],394+ syntax: ['Optionally begin with `.layout combined` or `.layout individual`; the default is `combined`.', 'Declare each leaf value with `ingredient`.', 'Declare a meaningful final or reusable result with an `outcome` block.', 'Keep incidental conversions, such as ore to bars, as steps inside the outcome that consumes them.', 'Add a full-width preparation row with `instruction`.', 'Long instruction rows wrap and grow vertically to preserve their complete text.', 'Add a quoted display label after an identifier when needed.', 'Each `+` queues an input for the next `->`; place it immediately before the operation that consumes it.', 'Columns represent dependency depth. Independent operations on disjoint inputs can share a column.', 'Consecutive `->` lines without a new input extend the same branch from left to right.', 'Write one complete instruction in each `->` step.', 'Combined layout places prerequisite outcomes before direct ingredients in stable FIFO order.', 'Add `portion` after an outcome label or identifier when the result has a useful quantity.', 'Use `{portion}` in an action to embed that outcome quantity.', 'Start comments with `#`; OCR importers should record uncertain text in comments instead of guessing.'],395+ authoringContract: ['Treat TRN source as the only public geometry control surface.', 'Select combined or individual geometry with the `.layout` directive.', 'Presentation appearance changes palette and canvas treatment. It does not change TRN layout, orientation, wrapping, or packing.', 'Edit the TRN source and request a new render.', 'Do not edit generated SVG, CSS classes, data attributes, viewBox values, or renderer implementation files.', 'If the source format cannot express requested geometry, report the limitation instead of patching renderer output.'],395396 language: 'text',396397 example: [397398 '.layout combined',@@ -413,9 +414,9 @@ export const diagramTypes = [
413414 '}',414415 ],415416 styleSummary: 'TRN colors complete relationship zones as branches. Adjacent zones receive different normalized colors, while ingredients run downward and operations run right.',416- sourceStyle: ['Use `.layout combined` for one merged table or `.layout individual` for dependency-positioned recipe tables.', 'Combined layout places prerequisite outcomes before direct ingredients and preserves source order within each FIFO group.', 'Order `->` lines as operations should proceed from left to right.', 'Use outcomes for meaningful reusable or final results; keep incidental conversions inside the consuming outcome.', 'Use short action labels because operation text is vertical.', 'Full-width `instruction` rows wrap automatically; operation labels remain single-line and vertical.'],417+ sourceStyle: ['Use `.layout combined` for one merged table.', 'Use `.layout individual` for dependency-positioned recipe tables.', 'Combined layout places prerequisite outcomes before direct ingredients and preserves source order within each FIFO group.', 'Place each `+` immediately before the operation that consumes it.', 'Independent branches can reuse a visual column when their inputs do not depend on each other.', 'Order consecutive `->` lines as operations should proceed from left to right.', 'Use outcomes for meaningful reusable or final results; keep incidental conversions inside the consuming outcome.', 'Use concise, complete instructions because combined and individual operation text is vertical.', 'Full-width `instruction` rows wrap automatically; combined and individual operation labels remain single-line.'],417418 rendererOptions: [],418- limitations: ['The source selects one layout; the SVG does not embed an interactive layout toggle.'],419+ limitations: ['The source selects one layout; the SVG does not embed an interactive layout toggle.', 'Combined and individual operation labels do not wrap. Long operation text can overflow.', 'TRN does not expose a source control for custom column packing.'],419420 origin: {420421 creator: 'Michael Chu',421422 site: 'Cooking for Engineers',@@ -29,9 +29,11 @@ The source format is **Tabular Recipe Notation text**. Enter the source in the d
2929 - Add a full-width preparation row with `instruction`.3030 - Long instruction rows wrap and grow vertically to preserve their complete text.3131 - Add a quoted display label after an identifier when needed.32-- Each `+` queues an input for the next `->` action.32+- Each `+` queues an input for the next `->`; place it immediately before the operation that consumes it.33+- Columns represent dependency depth. Independent operations on disjoint inputs can share a column.34+- Consecutive `->` lines without a new input extend the same branch from left to right.35+- Write one complete instruction in each `->` step.3336 - Combined layout places prerequisite outcomes before direct ingredients in stable FIFO order.34-- Each `->` adds an operation column from left to right.3537 - Add `portion` after an outcome label or identifier when the result has a useful quantity.3638 - Use `{portion}` in an action to embed that outcome quantity.3739 - Start comments with `#`; OCR importers should record uncertain text in comments instead of guessing.@@ -60,6 +62,15 @@ outcome bread portion 1 loaf {
6062 6163 <DiagramExample engine="trn" label="TRN" sourceUrl="/examples/trn.json" />6264 65+66+## Public authoring contract67+68+- Treat TRN source as the only public geometry control surface.69+- Select combined or individual geometry with the `.layout` directive.70+- Presentation appearance changes palette and canvas treatment. It does not change TRN layout, orientation, wrapping, or packing.71+- Edit the TRN source and request a new render.72+- Do not edit generated SVG, CSS classes, data attributes, viewBox values, or renderer implementation files.73+- If the source format cannot express requested geometry, report the limitation instead of patching renderer output.6374 ## Origin6475 6576 TRN originates with [Michael Chu](https://www.cookingforengineers.com/) at [Cooking for Engineers](https://www.cookingforengineers.com/). Diagram.zip implements a text DSL and SVG renderer for Chu’s tabular recipe concept.@@ -67,6 +78,8 @@ TRN originates with [Michael Chu](https://www.cookingforengineers.com/) at [Cook
6778 ## Limitations6879 6980 - The source selects one layout; the SVG does not embed an interactive layout toggle.81+- Combined and individual operation labels do not wrap. Long operation text can overflow.82+- TRN does not expose a source control for custom column packing.7083 7184 ## Related pages7285 @@ -33,6 +33,10 @@ Put renderer-specific settings in the top-level `options` object. Option values
3333 3434 ## Appearance3535 36+Appearance is presentation only. It does not change source geometry, notation layout, orientation, wrapping, or packing.37+38+For TRN, use the `.layout` directive in Source. Generated SVG classes and data attributes are output metadata, not an authoring API.39+3640 `raw` keeps the safe renderer output. It enables the custom background, padding, and frame values in the presentation document.3741 3842 The shared appearances apply one Diagram.zip palette to supported renderer output:@@ -14,15 +14,20 @@ Start with the [general presentation settings](/style/presentation). Edit shared
1414 1515 ## Source controls1616 17-- Use `.layout combined` for one merged table or `.layout individual` for dependency-positioned recipe tables.17+- Use `.layout combined` for one merged table.18+- Use `.layout individual` for dependency-positioned recipe tables.1819 - Combined layout places prerequisite outcomes before direct ingredients and preserves source order within each FIFO group.19-- Order `->` lines as operations should proceed from left to right.20+- Place each `+` immediately before the operation that consumes it.21+- Independent branches can reuse a visual column when their inputs do not depend on each other.22+- Order consecutive `->` lines as operations should proceed from left to right.2023 - Use outcomes for meaningful reusable or final results; keep incidental conversions inside the consuming outcome.21-- Use short action labels because operation text is vertical.22-- Full-width `instruction` rows wrap automatically; operation labels remain single-line and vertical.24+- Use concise, complete instructions because combined and individual operation text is vertical.25+- Full-width `instruction` rows wrap automatically; combined and individual operation labels remain single-line.2326 ## Limitations2427 2528 - The source selects one layout; the SVG does not embed an interactive layout toggle.29+- Combined and individual operation labels do not wrap. Long operation text can overflow.30+- TRN does not expose a source control for custom column packing.2631 2732 ## Related pages2833 @@ -116,6 +116,9 @@ let catalog
116116 try { catalog = JSON.parse(catalogRaw) } catch { errors.push('static/diagram-types.json is not valid JSON.') }117117 assert(catalog?.diagramTypes?.length === expected.length, 'Machine catalog does not contain all diagram types.')118118 assert(JSON.stringify((catalog?.diagramTypes ?? []).map(({ id }) => id).sort()) === JSON.stringify(expected), 'Machine catalog IDs do not match the diagram type catalog.')119+const trnCatalog = (catalog?.diagramTypes ?? []).find(({ id }) => id === 'trn')120+assert(Array.isArray(trnCatalog?.authoringContract), 'Machine catalog does not expose the TRN public authoring contract.')121+assert(trnCatalog?.authoringContract?.some((item) => item.includes('Do not edit generated SVG')), 'Machine catalog does not protect the TRN output boundary.')119122 120123 const llms = await source(join(site, 'static', 'llms.txt'))121124 const llmsFull = await source(join(site, 'static', 'llms-full.txt'))@@ -123,9 +126,13 @@ assert(llms.includes('https://docs.diagram.zip/create/'), 'llms.txt does not lin
123126 assert(llms.includes('https://docs.diagram.zip/style/presentation/'), 'llms.txt does not link the Details presentation guide.')124127 assert(llms.includes('TRN stands for Tabular Recipe Notation'), 'llms.txt does not expand TRN.')125128 assert(llms.includes('Michael Chu at Cooking for Engineers'), 'llms.txt does not credit the origin of TRN.')129+assert(llms.includes('Generated SVG and renderer internals are output, not authoring controls.'), 'llms.txt does not protect the public authoring boundary.')126130 assert(llmsFull.includes('# General presentation settings'), 'llms-full.txt does not include the presentation guide.')127131 assert(llmsFull.includes('# Working state and published state'), 'llms-full.txt does not include the working-state guide.')128132 assert(llmsFull.includes('TRN originates with [Michael Chu]'), 'llms-full.txt does not credit the origin of TRN.')133+assert(llmsFull.includes('## Public authoring contract'), 'llms-full.txt does not include the public authoring contract.')134+assert(llmsFull.includes('Presentation appearance changes palette and canvas treatment.'), 'llms-full.txt does not separate TRN layout from appearance.')135+assert(llmsFull.includes('Do not edit generated SVG, CSS classes, data attributes, viewBox values, or renderer implementation files.'), 'llms-full.txt does not protect the TRN output boundary.')129136 130137 const expectedSkillIds = [...diagramSkillIds].sort()131138 const skillEntries = await readdir(skills, { withFileTypes: true })@@ -95,12 +95,14 @@ function options(items) {
9595 9696 function syntaxPage(type, interactive = true) {9797 const limitations = lines(type.limitations)98+ const authoringContract = lines(type.authoringContract)9899 const article = articleFor(type.label)99100 const origin = type.origin100101 ? `## Origin\n\n${type.label} originates with [${type.origin.creator}](${type.origin.siteUrl}) at [${type.origin.site}](${type.origin.siteUrl}). ${type.origin.note}\n\n`101102 : ''102103 return frontmatter(type, 'create', `Create ${article} ${type.label} diagram in diagram.zip.`) +103104 `${interactive ? "import DiagramExample from '@site/src/components/DiagramExample';\n\n" : ''}# ${type.label}\n\n${type.summary}\n\n## Use this type\n\n${type.use}\n\n## Source format\n\nThe source format is **${type.format}**. Enter the source in the diagram.zip editor.\n\n## Syntax essentials\n\n${lines(type.syntax).map((item) => `- ${item}`).join('\n')}\n\n${example(type, interactive)}\n` +105+(authoringContract.length ? `\n## Public authoring contract\n\n${authoringContract.map((item) => `- ${item}`).join('\n')}\n` : '') +104106 (origin) +105107 (limitations.length ? `## Limitations\n\n${limitations.map((item) => `- ${item}`).join('\n')}\n\n` : '') +106108 `## Related pages\n\n- [Style ${type.label}](${stylePath(type.id)})\n- [General presentation settings](/style/presentation)\n- [Open ${type.label} in the editor](${editorLink(type.id)})\n\n## Upstream reference\n\n[${type.label} documentation](${type.upstream})\n`@@ -131,6 +133,7 @@ const catalog = diagramTypes.map((type) => ({
131133 editor: editorLink(type.id),132134 example: `/examples/${type.id}.json`,133135 ...(type.origin === undefined ? {} : { origin: type.origin }),136+ ...(type.authoringContract === undefined ? {} : { authoringContract: type.authoringContract }),134137 }))135138 const coreDocPaths = [136139 join(docs, 'create', 'index.md'),@@ -164,7 +167,7 @@ await Promise.all(diagramTypes.flatMap((type) => [
164167 await Promise.all(diagramSkills.map(({ id }) => cp(join(skillsDir, id), join(publicSkillsDir, id), { recursive: true })))165168 await writeFile(join(staticDir, 'diagram-types.json'), `${JSON.stringify({ version: 1, diagramTypes: catalog }, null, 2)}\n`)166169 await writeFile(join(staticDir, 'diagram-skills.json'), `${JSON.stringify({ version: 1, groups: skillGroups, skills: skillCatalog, standards: standardsSupport }, null, 2)}\n`)167-await writeFile(join(staticDir, 'llms.txt'), `# diagram.zip documentation\n\n> Choose the semantic story first. Use Source for notation and Details for metadata and presentation.\n\n- [Diagram routing skill](https://docs.diagram.zip/skills/diagramming/SKILL.md): Select a diagram family from the reader's question.\n- [Machine-readable semantic catalog](https://docs.diagram.zip/diagram-skills.json): Inspect every skill, use case, exclusion, role, and invocation constraint.\n\n${skillIndex()}\n\n## Renderer and source formats\n\n${diagramTypes.map((type) => `- [${type.label} syntax](https://docs.diagram.zip${syntaxPath(type.id)}/): ${type.fullName === undefined ? '' : `${type.label} stands for ${type.fullName}. `}Create ${articleFor(type.label)} ${type.label} diagram. [Style](https://docs.diagram.zip${stylePath(type.id)}/).${type.origin === undefined ? '' : ` Origin: [${type.origin.creator} at ${type.origin.site}](${type.origin.siteUrl}).`}`).join('\n')}\n\n## General pages\n\n- [Editor workspace](https://docs.diagram.zip/create/)\n- [Editable SVG files and local drafts](https://docs.diagram.zip/create/): Reopen validated enriched SVG files without publishing.\n- [Details JSON and presentation](https://docs.diagram.zip/style/presentation/)\n- [SVG normalization and version contracts](https://docs.diagram.zip/style/svg-normalization/)\n- [Share a diagram](https://docs.diagram.zip/collaboration/sharing/)\n- [Password encryption](https://docs.diagram.zip/collaboration/encryption/)\n- [Working state and saved state](https://docs.diagram.zip/collaboration/working-and-saved-state/)\n\n## Structured catalogs\n\n- [diagram-skills.json](https://docs.diagram.zip/diagram-skills.json)\n- [diagram-types.json](https://docs.diagram.zip/diagram-types.json)\n- [Full documentation text](https://docs.diagram.zip/llms-full.txt)\n- [Source repository](https://github.com/FelineStateMachine/diagramzip)\n`)170+await writeFile(join(staticDir, 'llms.txt'), `# diagram.zip documentation\n\n> Choose the semantic story first. Use Source for notation and Details for metadata and presentation. Generated SVG and renderer internals are output, not authoring controls.\n\n- [Diagram routing skill](https://docs.diagram.zip/skills/diagramming/SKILL.md): Select a diagram family from the reader's question.\n- [Machine-readable semantic catalog](https://docs.diagram.zip/diagram-skills.json): Inspect every skill, use case, exclusion, role, and invocation constraint.\n\n${skillIndex()}\n\n## Renderer and source formats\n\n${diagramTypes.map((type) => `- [${type.label} syntax](https://docs.diagram.zip${syntaxPath(type.id)}/): ${type.fullName === undefined ? '' : `${type.label} stands for ${type.fullName}. `}Create ${articleFor(type.label)} ${type.label} diagram. [Style](https://docs.diagram.zip${stylePath(type.id)}/).${type.origin === undefined ? '' : ` Origin: [${type.origin.creator} at ${type.origin.site}](${type.origin.siteUrl}).`}`).join('\n')}\n\n## General pages\n\n- [Editor workspace](https://docs.diagram.zip/create/)\n- [Editable SVG files and local drafts](https://docs.diagram.zip/create/): Reopen validated enriched SVG files without publishing.\n- [Details JSON and presentation](https://docs.diagram.zip/style/presentation/)\n- [SVG normalization and version contracts](https://docs.diagram.zip/style/svg-normalization/)\n- [Share a diagram](https://docs.diagram.zip/collaboration/sharing/)\n- [Password encryption](https://docs.diagram.zip/collaboration/encryption/)\n- [Working state and saved state](https://docs.diagram.zip/collaboration/working-and-saved-state/)\n\n## Structured catalogs\n\n- [diagram-skills.json](https://docs.diagram.zip/diagram-skills.json)\n- [diagram-types.json](https://docs.diagram.zip/diagram-types.json)\n- [Full documentation text](https://docs.diagram.zip/llms-full.txt)\n- [Source repository](https://github.com/FelineStateMachine/diagramzip)\n`)168171 const llmsPath = join(staticDir, 'llms.txt')169172 const llmsIndex = await readFile(llmsPath, 'utf8')170173 await writeFile(llmsPath, llmsIndex.replace('Working state and saved state', 'Working state and published state'))@@ -318,7 +318,15 @@
318318 "site": "Cooking for Engineers",319319 "siteUrl": "https://www.cookingforengineers.com/",320320 "note": "Diagram.zip implements a text DSL and SVG renderer for Chu’s tabular recipe concept."321- }321+ },322+ "authoringContract": [323+ "Treat TRN source as the only public geometry control surface.",324+ "Select combined or individual geometry with the `.layout` directive.",325+ "Presentation appearance changes palette and canvas treatment. It does not change TRN layout, orientation, wrapping, or packing.",326+ "Edit the TRN source and request a new render.",327+ "Do not edit generated SVG, CSS classes, data attributes, viewBox values, or renderer implementation files.",328+ "If the source format cannot express requested geometry, report the limitation instead of patching renderer output."329+ ]322330 },323331 {324332 "id": "umlet",@@ -133,6 +133,10 @@ Put renderer-specific settings in the top-level `options` object. Option values
133133 134134 ## Appearance135135 136+Appearance is presentation only. It does not change source geometry, notation layout, orientation, wrapping, or packing.137+138+For TRN, use the `.layout` directive in Source. Generated SVG classes and data attributes are output metadata, not an authoring API.139+136140 `raw` keeps the safe renderer output. It enables the custom background, padding, and frame values in the presentation document.137141 138142 The shared appearances apply one Diagram.zip palette to supported renderer output:@@ -2823,9 +2827,11 @@ The source format is **Tabular Recipe Notation text**. Enter the source in the d
28232827 - Add a full-width preparation row with `instruction`.28242828 - Long instruction rows wrap and grow vertically to preserve their complete text.28252829 - Add a quoted display label after an identifier when needed.2826-- Each `+` queues an input for the next `->` action.2830+- Each `+` queues an input for the next `->`; place it immediately before the operation that consumes it.2831+- Columns represent dependency depth. Independent operations on disjoint inputs can share a column.2832+- Consecutive `->` lines without a new input extend the same branch from left to right.2833+- Write one complete instruction in each `->` step.28272834 - Combined layout places prerequisite outcomes before direct ingredients in stable FIFO order.2828-- Each `->` adds an operation column from left to right.28292835 - Add `portion` after an outcome label or identifier when the result has a useful quantity.28302836 - Use `{portion}` in an action to embed that outcome quantity.28312837 - Start comments with `#`; OCR importers should record uncertain text in comments instead of guessing.@@ -2852,6 +2858,15 @@ outcome bread portion 1 loaf {
28522858 }28532859 ```28542860 2861+2862+## Public authoring contract2863+2864+- Treat TRN source as the only public geometry control surface.2865+- Select combined or individual geometry with the `.layout` directive.2866+- Presentation appearance changes palette and canvas treatment. It does not change TRN layout, orientation, wrapping, or packing.2867+- Edit the TRN source and request a new render.2868+- Do not edit generated SVG, CSS classes, data attributes, viewBox values, or renderer implementation files.2869+- If the source format cannot express requested geometry, report the limitation instead of patching renderer output.28552870 ## Origin28562871 28572872 TRN originates with [Michael Chu](https://www.cookingforengineers.com/) at [Cooking for Engineers](https://www.cookingforengineers.com/). Diagram.zip implements a text DSL and SVG renderer for Chu’s tabular recipe concept.@@ -2859,6 +2874,8 @@ TRN originates with [Michael Chu](https://www.cookingforengineers.com/) at [Cook
28592874 ## Limitations28602875 28612876 - The source selects one layout; the SVG does not embed an interactive layout toggle.2877+- Combined and individual operation labels do not wrap. Long operation text can overflow.2878+- TRN does not expose a source control for custom column packing.28622879 28632880 ## Related pages28642881 @@ -2886,15 +2903,20 @@ Start with the [general presentation settings](/style/presentation). Edit shared
28862903 28872904 ## Source controls28882905 2889-- Use `.layout combined` for one merged table or `.layout individual` for dependency-positioned recipe tables.2906+- Use `.layout combined` for one merged table.2907+- Use `.layout individual` for dependency-positioned recipe tables.28902908 - Combined layout places prerequisite outcomes before direct ingredients and preserves source order within each FIFO group.2891-- Order `->` lines as operations should proceed from left to right.2909+- Place each `+` immediately before the operation that consumes it.2910+- Independent branches can reuse a visual column when their inputs do not depend on each other.2911+- Order consecutive `->` lines as operations should proceed from left to right.28922912 - Use outcomes for meaningful reusable or final results; keep incidental conversions inside the consuming outcome.2893-- Use short action labels because operation text is vertical.2894-- Full-width `instruction` rows wrap automatically; operation labels remain single-line and vertical.2913+- Use concise, complete instructions because combined and individual operation text is vertical.2914+- Full-width `instruction` rows wrap automatically; combined and individual operation labels remain single-line.28952915 ## Limitations28962916 28972917 - The source selects one layout; the SVG does not embed an interactive layout toggle.2918+- Combined and individual operation labels do not wrap. Long operation text can overflow.2919+- TRN does not expose a source control for custom column packing.28982920 28992921 ## Related pages29002922 @@ -1,6 +1,6 @@
11 # diagram.zip documentation22 3-> Choose the semantic story first. Use Source for notation and Details for metadata and presentation.3+> Choose the semantic story first. Use Source for notation and Details for metadata and presentation. Generated SVG and renderer internals are output, not authoring controls.44 55 - [Diagram routing skill](https://docs.diagram.zip/skills/diagramming/SKILL.md): Select a diagram family from the reader's question.66 - [Machine-readable semantic catalog](https://docs.diagram.zip/diagram-skills.json): Inspect every skill, use case, exclusion, role, and invocation constraint.@@ -182,7 +182,7 @@ test('applies D2, PlantUML, Svgbob, and neutral SVG family roles', () => {
182182 test('normalizes TRN table structure and relationship zones', () => {183183 const trn = canonicalizeSvg(184184 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40"><rect class="trn-canvas" width="80" height="40" fill="#fbf8dc"></rect><text class="trn-title" fill="#111827">Bread</text><rect class="trn-table-surface" y="10" width="80" height="30" fill="white"></rect><path class="trn-zone-shape trn-branch-4" fill="white" stroke="#65a268"></path><text class="trn-operation-label" fill="#111827">mix</text></svg>',185- metadata, 'trn', 'diagramzip-trn@15',185+ metadata, 'trn', 'diagramzip-trn@16',186186 )187187 188188 assert.match(trn, /data-dz-profile="trn-semantic-4"/)@@ -108,7 +108,7 @@ const versions: Record<EngineId, string> = {
108108 ditaa: 'ditaa-ascii+svgbob@0.7.6/edge-wasm-1',109109 symbolator: 'symbolator@1.2.2/python-translation-1',110110 tikz: '@planktimerr/tikzjax@1.0.63',111- trn: 'diagramzip-trn@15',111+ trn: 'diagramzip-trn@16',112112 umlet: 'diagramzip-umlet-svg@1',113113 }114114 @@ -5,6 +5,6 @@
55 "compatibility_date": "2026-08-20",66 "compatibility_flags": ["nodejs_compat"],77 "observability": { "enabled": true, "logs": { "head_sampling_rate": 1 } },8- "vars": { "RENDERER_BUILD": "diagramzip-trn-15" },8+ "vars": { "RENDERER_BUILD": "diagramzip-trn-16" },99 "routes": [{ "pattern": "trn.render.diagram.zip", "custom_domain": true }]1010 }@@ -4,7 +4,7 @@ import { parseTrn } from '../languages/trn'
44 import { renderTrn } from '../languages/trn-renderer'55 import { edgeFailure, edgeResult } from './types'66 7-const VERSION = 'diagramzip-trn@15'7+const VERSION = 'diagramzip-trn@16'88 99 function validateOptions(options: Record<string, string>): void {1010 for (const name of Object.keys(options)) {@@ -19,9 +19,6 @@ const RECIPE_LINK_CLEARANCE = 4
1919 const FONT_FAMILY = 'Arial,Helvetica,sans-serif'2020 const GRID_COLOR = '#65a268'2121 const BRANCH_COLORS = ['#dbeafe', '#fef3c7', '#ede9fe', '#ccfbf1'] as const22-const TEST_INGREDIENT_WIDTH = 4423-const TEST_OPERATION_FONT_SIZE = 1124-const TEST_INGREDIENT_FONT_SIZE = 102522 2623 interface LeafLayout {2724 row: number@@ -365,79 +362,34 @@ function geometryFor(layout: Layout): Geometry {
365362 return { labelWidth, columnWidths, columnOffsets, rowHeights, rowOffsets, bodyHeight, tableWidth: offset }366363 }367364 368-function testGeometryFor(layout: Layout): Geometry {369- const columnWidths = Array.from({ length: layout.columns }, (_, index) => {370- const actions = layout.stages371- .filter(stage => stage.column === index + 1)372- .map(stage => textWidth(actionText(stage), TEST_OPERATION_FONT_SIZE, 28))373- return Math.ceil(clamp(Math.max(82, ...actions), 82, 280))374- })375- const columnOffsets: number[] = []376- let offset = TEST_INGREDIENT_WIDTH377- for (const width of columnWidths) {378- columnOffsets.push(offset)379- offset += width380- }381- const rowHeights = Array.from({ length: layout.rows }, () => MIN_ROW_HEIGHT)382- for (const leaf of layout.leaves) {383- rowHeights[leaf.row] = Math.ceil(clamp(384- textWidth(ingredientText(leaf), TEST_INGREDIENT_FONT_SIZE, 20),385- 64,386- 240,387- ))388- }389- const rowOffsets: number[] = []390- let bodyHeight = 0391- for (const height of rowHeights) {392- rowOffsets.push(bodyHeight)393- bodyHeight += height394- }395- return {396- labelWidth: TEST_INGREDIENT_WIDTH,397- columnWidths,398- columnOffsets,399- rowHeights,400- rowOffsets,401- bodyHeight,402- tableWidth: offset,403- }404-}405-406365 function leafSvg(407366 leaf: LeafLayout,408367 geometry: Geometry,409368 bodyY: number,410- mirrored = false,411- verticalLabel = false,412369 ): string {413370 const text = ingredientText(leaf)414- const cellX = MARGIN + (mirrored ? geometry.tableWidth - geometry.labelWidth : 0)371+ const cellX = MARGIN415372 const height = geometry.rowHeights[leaf.row]!416373 const top = rowY(leaf.row, bodyY, geometry)417- const centerX = cellX + geometry.labelWidth / 2418374 const centerY = top + height / 2419- const x = verticalLabel ? centerX : cellX + 7420- const y = verticalLabel ? centerY : centerY + 4421- const fontSize = verticalLabel ? TEST_INGREDIENT_FONT_SIZE : 12422- const available = (verticalLabel ? height : geometry.labelWidth) - 14375+ const x = cellX + 7376+ const y = centerY + 4377+ const fontSize = 12378+ const available = geometry.labelWidth - 14423379 const length = Math.max(1, Math.floor(available / (fontSize * 0.56)))424380 const shown = text.length <= length ? text : `${text.slice(0, length - 1)}…`425- const transform = verticalLabel ? ` transform="rotate(90 ${centerX} ${centerY})"` : ''426- return `<g class="trn-ingredient" data-value-id="${escapeXml(leaf.input.id)}"><title>${escapeXml(text)}</title><rect class="trn-ingredient-cell" x="${cellX}" y="${top}" width="${geometry.labelWidth}" height="${height}" fill="white" stroke="${GRID_COLOR}" stroke-width="1"/><text class="trn-ingredient-label" x="${x}" y="${y}" text-anchor="${verticalLabel ? 'middle' : 'start'}" dominant-baseline="${verticalLabel ? 'middle' : 'auto'}" fill="#111827" font-size="${fontSize}" font-family="${FONT_FAMILY}"${transform}>${escapeXml(shown)}</text></g>`381+ return `<g class="trn-ingredient" data-value-id="${escapeXml(leaf.input.id)}"><title>${escapeXml(text)}</title><rect class="trn-ingredient-cell" x="${cellX}" y="${top}" width="${geometry.labelWidth}" height="${height}" fill="white" stroke="${GRID_COLOR}" stroke-width="1"/><text class="trn-ingredient-label" x="${x}" y="${y}" text-anchor="start" dominant-baseline="auto" fill="#111827" font-size="${fontSize}" font-family="${FONT_FAMILY}">${escapeXml(shown)}</text></g>`427382 }428383 429384 function stageSvg(430385 item: StageLayout,431386 geometry: Geometry,432387 bodyY: number,433- mirrored = false,434- horizontalLabel = false,435388 ): string {436389 const width = geometry.columnWidths[item.column - 1]!437390 const columnLeft = (column: number): number => {438391 const offset = geometry.columnOffsets[column - 1]!439- const columnWidth = geometry.columnWidths[column - 1]!440- return MARGIN + (mirrored ? geometry.tableWidth - offset - columnWidth : offset)392+ return MARGIN + offset441393 }442394 const columnRight = (column: number): number => columnLeft(column) + geometry.columnWidths[column - 1]!443395 const x = columnLeft(item.column)@@ -454,8 +406,8 @@ function stageSvg(
454406 const top = rowY(row, bodyY, geometry)455407 const bottom = top + geometry.rowHeights[row]!456408 if (!cells.has(`${column}:${row - 1}`)) edges.push({ start: [left, top], end: [right, top] })457- const rightNeighbor = mirrored ? column - 1 : column + 1458- const leftNeighbor = mirrored ? column + 1 : column - 1409+ const rightNeighbor = column + 1410+ const leftNeighbor = column - 1459411 if (!cells.has(`${rightNeighbor}:${row}`)) edges.push({ start: [right, top], end: [right, bottom] })460412 if (!cells.has(`${column}:${row + 1}`)) edges.push({ start: [right, bottom], end: [left, bottom] })461413 if (!cells.has(`${leftNeighbor}:${row}`)) edges.push({ start: [left, bottom], end: [left, top] })@@ -488,12 +440,12 @@ function stageSvg(
488440 const zonePath = paths.join('')489441 const labelX = x + width / 2490442 const labelY = y + height / 2491- const transform = horizontalLabel ? '' : ` transform="rotate(90 ${labelX} ${labelY})"`443+ const transform = ` transform="rotate(90 ${labelX} ${labelY})"`492444 return [493445 `<g class="trn-operation-zone" data-outcome-id="${escapeXml(item.outcome.id)}" data-column="${item.column}" data-cell-count="${cells.size}" data-arm-count="${item.arms.length}" data-branch-color="${item.branchColor + 1}">`,494446 `<title>${escapeXml(action)} → ${escapeXml(result)}</title>`,495447 `<path class="trn-zone-shape trn-branch-${item.branchColor + 1}" d="${zonePath}" fill="${BRANCH_COLORS[item.branchColor]}" stroke="${GRID_COLOR}" stroke-width="1"/>`,496- `<text class="trn-operation-label" x="${labelX}" y="${labelY}" text-anchor="middle" dominant-baseline="middle" fill="#111827" font-size="${horizontalLabel ? TEST_OPERATION_FONT_SIZE : OPERATION_FONT_SIZE}" font-family="${FONT_FAMILY}"${transform}>${escapeXml(action)}</text>`,448+ `<text class="trn-operation-label" x="${labelX}" y="${labelY}" text-anchor="middle" dominant-baseline="middle" fill="#111827" font-size="${OPERATION_FONT_SIZE}" font-family="${FONT_FAMILY}"${transform}>${escapeXml(action)}</text>`,497449 '</g>',498450 ].join('')499451 }@@ -679,34 +631,7 @@ function renderIndividualTrn(document: TrnDocument): string {
679631 </svg>`680632 }681633 682-function renderTestTrn(document: TrnDocument): string {683- const layout = layoutTree(document)684- const geometry = testGeometryFor(layout)685- const title = document.roots.map(root => root.portion === '' ? root.label : `${root.label} (yields ${root.portion})`).join(' + ')686- const tableY = MARGIN + HEADER_HEIGHT687- const instructionLayout = layoutInstructions(document, geometry.tableWidth)688- const instructionHeight = instructionLayout.height689- const bodyY = tableY + instructionHeight690- const width = MARGIN * 2 + geometry.tableWidth691- const height = MARGIN * 2 + HEADER_HEIGHT + instructionHeight + geometry.bodyHeight692- const instructions = instructionSvg(document, instructionLayout, tableY, geometry.tableWidth)693- const leaves = layout.leaves.map(item => leafSvg(item, geometry, bodyY, true, true)).join('')694- const stages = layout.stages.map(item => stageSvg(item, geometry, bodyY, true, true)).join('')695-696- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" class="trn-diagram trn-layout-test" data-layout="test">697- <title>${escapeXml(title)} test TRN layout</title>698- <desc>Tabular Recipe Notation with horizontal operation labels on the left and vertical ingredient labels on the right.</desc>699- <rect class="trn-canvas" data-dz-role="canvas" x="0" y="0" width="${width}" height="${height}" fill="#fbf8dc"/>700- <text class="trn-title" x="${MARGIN + 7}" y="${MARGIN + 19}" fill="#111827" font-size="14" font-weight="700" font-family="${FONT_FAMILY}">${escapeXml(title)}</text>701- <rect class="trn-table-surface" x="${MARGIN}" y="${tableY}" width="${geometry.tableWidth}" height="${instructionHeight + geometry.bodyHeight}" fill="white" stroke="${GRID_COLOR}" stroke-width="1"/>702- ${instructions}703- ${leaves}704- ${stages}705-</svg>`706-}707-708634 export function renderTrn(document: TrnDocument): string {709635 if (document.layout === 'individual') return renderIndividualTrn(document)710- if (document.layout === 'test') return renderTestTrn(document)711636 return renderCombinedTrn(document)712637 }@@ -26,7 +26,7 @@ export interface TrnOutcome {
2626 line: number2727 }2828 29-export type TrnLayout = 'combined' | 'individual' | 'test'29+export type TrnLayout = 'combined' | 'individual'3030 3131 export interface TrnDocument {3232 layout: TrnLayout@@ -141,7 +141,7 @@ export function parseTrn(source: string): TrnDocument {
141141 sourceError(lineNumber, "Expected '+ value', '-> action', or '}'.")142142 }143143 144- const layoutMatch = line.match(/^\.layout\s+(combined|individual|test)\s*$/)144+ const layoutMatch = line.match(/^\.layout\s+(combined|individual)\s*$/)145145 if (layoutMatch !== null) {146146 if (declarationsStarted) sourceError(lineNumber, 'The .layout directive must appear before recipe declarations.')147147 if (layoutDeclared) sourceError(lineNumber, 'The .layout directive may only appear once.')@@ -149,7 +149,7 @@ export function parseTrn(source: string): TrnDocument {
149149 layoutDeclared = true150150 continue151151 }152- if (line.startsWith('.layout')) sourceError(lineNumber, "Expected '.layout combined', '.layout individual', or '.layout test'.")152+ if (line.startsWith('.layout')) sourceError(lineNumber, "Expected '.layout combined' or '.layout individual'.")153153 154154 const instructionMatch = line.match(/^instruction\s+"((?:[^"\\]|\\.)*)"\s*$/)155155 if (instructionMatch !== null) {@@ -59,7 +59,7 @@ describe('TRN language', () => {
5959 6060 it('validates the source layout directive and rejects renderer options', () => {6161 expect(() => parseTrn(zenith.replace('.layout individual', '.layout grid')))62- .toThrow(/Expected '.layout combined', '.layout individual', or '.layout test'/)62+ .toThrow(/Expected '.layout combined' or '.layout individual'/)6363 expect(() => parseTrn('.layout combined\n.layout individual\ningredient flour\noutcome bread {\n + flour\n -> bake\n}'))6464 .toThrow(/may only appear once/)6565 expect(() => parseTrn('ingredient flour\n.layout individual\noutcome bread {\n + flour\n -> bake\n}'))@@ -87,35 +87,6 @@ describe('TRN language', () => {
8787 }8888 })8989 90- it('renders the test layout with instructions on the left and ingredients on the right', async () => {91- const source = `.layout test\n${marshmallows}`92- const document = parseTrn(source)93- const result = await trnAdapter.render(request(source), new AbortController().signal)94-95- expect(document.layout).toBe('test')96- expect(result.body).toContain('data-layout="test"')97- expect(result.body).toContain('horizontal operation labels on the left and vertical ingredient labels on the right')98- expect(result.body).toContain('class="trn-instruction-cell"')99- expect(result.body).toContain('>Grease 9x13-in. pan and powder with powdered sugar</tspan>')100- expect(result.body).toContain('>powder</text>')101- expect(result.body).toContain('class="trn-operation-zone"')102- expect(result.body).toContain('data-outcome-id="marshmallows" data-column="7"')103- expect(result.body).toContain('class="trn-ingredient" data-value-id="gelatin"')104- expect(result.body).toContain('>3 Tbs. (21 g) gelatin</text>')105- expect(result.body).toContain('class="trn-ingredient" data-value-id="powdered_sugar"')106- expect(result.body).toContain('>Powdered sugar</text>')107-108- const ingredientX = Number(result.body.match(/class="trn-ingredient-cell" x="(\d+)"/)?.[1])109- const ingredientLabel = result.body.match(/class="trn-ingredient-label"[^>]+/)?.[0] ?? ''110- const operationLabel = result.body.match(/class="trn-operation-label"[^>]+/)?.[0] ?? ''111- const operationXs = [...result.body.matchAll(/class="trn-operation-label" x="([\d.]+)"/g)]112- .map(match => Number(match[1]))113- expect(ingredientX).toBeGreaterThan(12)114- expect(ingredientLabel).toContain('transform="rotate(90')115- expect(operationLabel).not.toContain('transform=')116- expect(Math.max(...operationXs)).toBeLessThan(ingredientX)117- })118-11990 it('rejects unknown values and dependency cycles with source locations', () => {12091 expect(() => parseTrn('outcome bread {\n + flour\n -> mix\n}')).toThrow(/Line 2: Unknown value 'flour'/)12192 expect(() => parseTrn('outcome a {\n + b\n -> make\n}\noutcome b {\n + a\n -> make\n}')).toThrow(/Outcome cycle/)@@ -158,7 +158,7 @@ const VERSION_PATTERNS = Object.freeze({
158158 diagramsnet: /diagrams\.net@29\.6\.1/,159159 excalidraw: /@excalidraw\/excalidraw@0\.18\.1/,160160 tikz: /@planktimerr\/tikzjax@1\.0\.63/,161- trn: /diagramzip-trn@15/,161+ trn: /diagramzip-trn@16/,162162 })163163 164164 export function normalizationFor(engine, rendererVersion = '') {