@stacksjs/defaults 0.75.361 → 0.75.366
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/skills/stacks-ai/SKILL.md +22 -0
- package/ai/skills/stacks-native/CATALOG.md +5 -5
- package/ai/skills/stacks-payments/SKILL.md +10 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
- package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
- package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
- package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
- package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
- package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
- package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
- package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
- package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
- package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
- package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
- package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
- package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
- package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
- package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
- package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
- package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
- package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
- package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
- package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
- package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
- package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
- package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
- package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
- package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
- package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
- package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
- package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
- package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
- package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
- package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
- package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
- package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
- package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
- package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
- package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
- package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
- package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
- package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
- package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
- package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
- package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
- package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
- package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
- package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
- package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
- package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
- package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
- package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
- package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
- package/ai/skills/stacks-testing/SKILL.md +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const SIDECAR_STEM_NAMESPACE = /\.~archify-[0-9a-f]{64}$/iu;
|
|
4
|
+
|
|
5
|
+
export function sidecarStemNeedsBounding(stem, suffixes) {
|
|
6
|
+
const fits = (value) => value.length <= 255 && Buffer.byteLength(value, 'utf8') <= 255;
|
|
7
|
+
return !suffixes.every((suffix) => fits(`${stem}${suffix}`));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function boundedSidecarStem(stem, suffixes, { force = false, hashDomain } = {}) {
|
|
11
|
+
if (!force && !sidecarStemNeedsBounding(stem, suffixes)
|
|
12
|
+
&& !SIDECAR_STEM_NAMESPACE.test(stem)) return stem;
|
|
13
|
+
const hashInput = hashDomain ? `${hashDomain}\0${stem}` : stem;
|
|
14
|
+
const marker = `.~archify-${createHash('sha256').update(hashInput).digest('hex')}`;
|
|
15
|
+
const codePoints = [...stem];
|
|
16
|
+
while (codePoints.length
|
|
17
|
+
&& sidecarStemNeedsBounding(`${codePoints.join('')}${marker}`, suffixes)) {
|
|
18
|
+
codePoints.pop();
|
|
19
|
+
}
|
|
20
|
+
return `${codePoints.join('')}${marker}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sidecarStemFromComponent(component) {
|
|
24
|
+
if (component.endsWith('.html')) {
|
|
25
|
+
return {
|
|
26
|
+
stem: component.slice(0, -'.html'.length),
|
|
27
|
+
options: undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
stem: component,
|
|
32
|
+
options: { force: true, hashDomain: 'full-component' },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isBoundedSidecarStem(stem) {
|
|
37
|
+
return SIDECAR_STEM_NAMESPACE.test(stem);
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const files = ['NOTICE', 'LICENSE', 'LICENSE-MIT', 'THIRD_PARTY_NOTICES.md'];
|
|
4
|
+
export const softwareNotices = files.map(name => `${name}\n${fs.readFileSync(new URL(`../../${name}`, import.meta.url), 'utf8').trimEnd()}`).join('\n\n');
|
|
5
|
+
// JSON data preserves the exact notice text without HTML/script delimiters.
|
|
6
|
+
export const softwareNoticesHtml = `<script type="application/json" id="archify-software-notices">${JSON.stringify(softwareNotices).replace(/</g, '\\u003c')}</script>`;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Uniform spatial grid for "which items can this box reach" queries (#8). */
|
|
2
|
+
|
|
3
|
+
// A cell box wider than this is not worth walking: the query returns every
|
|
4
|
+
// inserted item instead, which stays a superset of what the box reaches.
|
|
5
|
+
const DEFAULT_MAX_CELLS = 4096;
|
|
6
|
+
|
|
7
|
+
export function createSpatialGrid(cellSize, { maxCells = DEFAULT_MAX_CELLS } = {}) {
|
|
8
|
+
const buckets = new Map();
|
|
9
|
+
const items = [];
|
|
10
|
+
// Items whose own box cannot be walked are candidates for every query.
|
|
11
|
+
const overflow = [];
|
|
12
|
+
const keyOf = (x, y) => x + ':' + y;
|
|
13
|
+
const rangeOf = (box) => ({
|
|
14
|
+
x0: Math.floor(box.minX / cellSize), x1: Math.floor(box.maxX / cellSize),
|
|
15
|
+
y0: Math.floor(box.minY / cellSize), y1: Math.floor(box.maxY / cellSize),
|
|
16
|
+
});
|
|
17
|
+
// Finite coordinates can still land outside the safe-integer range, where
|
|
18
|
+
// incrementing an index no longer advances it; and a legal coordinate can
|
|
19
|
+
// name more cells than the grid is worth. Both cases stay out of the buckets
|
|
20
|
+
// and are answered by the item list, so they never hang or allocate.
|
|
21
|
+
const walkable = (range) => Number.isSafeInteger(range.x0) && Number.isSafeInteger(range.x1)
|
|
22
|
+
&& Number.isSafeInteger(range.y0) && Number.isSafeInteger(range.y1)
|
|
23
|
+
&& (range.x1 - range.x0 + 1) * (range.y1 - range.y0 + 1) <= maxCells;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
insert(box, item) {
|
|
27
|
+
items.push(item);
|
|
28
|
+
const range = rangeOf(box);
|
|
29
|
+
if (!walkable(range)) {
|
|
30
|
+
overflow.push(item);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (let x = range.x0; x <= range.x1; x += 1) {
|
|
34
|
+
for (let y = range.y0; y <= range.y1; y += 1) {
|
|
35
|
+
const key = keyOf(x, y);
|
|
36
|
+
let bucket = buckets.get(key);
|
|
37
|
+
if (!bucket) { bucket = []; buckets.set(key, bucket); }
|
|
38
|
+
bucket.push(item);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
query(box) {
|
|
43
|
+
const range = rangeOf(box);
|
|
44
|
+
if (!walkable(range)) return items.slice();
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const found = [];
|
|
47
|
+
for (let x = range.x0; x <= range.x1; x += 1) {
|
|
48
|
+
for (let y = range.y0; y <= range.y1; y += 1) {
|
|
49
|
+
const bucket = buckets.get(keyOf(x, y));
|
|
50
|
+
if (!bucket) continue;
|
|
51
|
+
for (const item of bucket) {
|
|
52
|
+
if (seen.has(item)) continue;
|
|
53
|
+
seen.add(item);
|
|
54
|
+
found.push(item);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const item of overflow) {
|
|
59
|
+
if (seen.has(item)) continue;
|
|
60
|
+
seen.add(item);
|
|
61
|
+
found.push(item);
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// 仅分析静态检查器需要的路径子集,不作为通用 SVG 渲染器。
|
|
2
|
+
const NUMBER = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y;
|
|
3
|
+
const COMMANDS = /^[MLHVQZACSTmlhvqzacst]$/;
|
|
4
|
+
const ARITY = { M: 2, L: 2, H: 1, V: 1, Q: 4 };
|
|
5
|
+
|
|
6
|
+
function failure(tokenOffset, reason, command, unsupported = false) {
|
|
7
|
+
return { ok: false, error: {
|
|
8
|
+
code: unsupported ? 'artifact/svg-path-unsupported' : 'artifact/svg-path-malformed',
|
|
9
|
+
tokenOffset, reason, ...(command ? { command } : {}),
|
|
10
|
+
} };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function tokenize(source) {
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let offset = 0;
|
|
16
|
+
let comma = null;
|
|
17
|
+
while (offset < source.length) {
|
|
18
|
+
const character = source[offset];
|
|
19
|
+
if (/[ \t\r\n]/.test(character)) { offset += 1; continue; }
|
|
20
|
+
if (character === ',') {
|
|
21
|
+
if (comma !== null || tokens.at(-1)?.kind !== 'number') return failure(offset, 'unexpected comma');
|
|
22
|
+
comma = offset++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (COMMANDS.test(character)) {
|
|
26
|
+
if (comma !== null) return failure(comma, 'a comma must separate numbers');
|
|
27
|
+
tokens.push({ kind: 'command', value: character, offset: offset++ });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
NUMBER.lastIndex = offset;
|
|
31
|
+
const number = NUMBER.exec(source);
|
|
32
|
+
if (!number) return failure(offset, 'expected a number or supported path command');
|
|
33
|
+
const value = Number(number[0]);
|
|
34
|
+
if (!Number.isFinite(value)) return failure(offset, 'coordinate is not finite');
|
|
35
|
+
tokens.push({ kind: 'number', value, offset });
|
|
36
|
+
offset = NUMBER.lastIndex;
|
|
37
|
+
comma = null;
|
|
38
|
+
}
|
|
39
|
+
return comma === null ? { ok: true, tokens } : failure(comma, 'trailing comma');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 每步消费一个命令或完整参数组;失败不返回部分几何。偏移基于传入的 d 字符串。 */
|
|
43
|
+
export function analyzeSvgPath(source) {
|
|
44
|
+
if (typeof source !== 'string') return failure(0, 'path data must be a string');
|
|
45
|
+
const lexed = tokenize(source);
|
|
46
|
+
if (!lexed.ok) return lexed;
|
|
47
|
+
const { tokens } = lexed;
|
|
48
|
+
const subpaths = [];
|
|
49
|
+
let index = 0;
|
|
50
|
+
let command = null;
|
|
51
|
+
let commandOffset = 0;
|
|
52
|
+
let current = [0, 0];
|
|
53
|
+
let subpath;
|
|
54
|
+
|
|
55
|
+
function move(point) {
|
|
56
|
+
current = point;
|
|
57
|
+
subpath = { points: [point], segments: [], straightSegments: [], closed: false };
|
|
58
|
+
subpaths.push(subpath);
|
|
59
|
+
}
|
|
60
|
+
function line(point) {
|
|
61
|
+
const segment = { start: current, end: point };
|
|
62
|
+
subpath.segments.push(segment);
|
|
63
|
+
subpath.straightSegments.push(segment);
|
|
64
|
+
subpath.points.push(point);
|
|
65
|
+
current = point;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
while (index < tokens.length) {
|
|
69
|
+
if (tokens[index].kind === 'command') {
|
|
70
|
+
command = tokens[index].value;
|
|
71
|
+
commandOffset = tokens[index++].offset;
|
|
72
|
+
} else if (!command) {
|
|
73
|
+
return failure(tokens[index].offset, 'a command is required after closepath');
|
|
74
|
+
}
|
|
75
|
+
const upper = command.toUpperCase();
|
|
76
|
+
if (!subpath && upper !== 'M') return failure(commandOffset, 'the first command must be moveto', command);
|
|
77
|
+
if (!Object.hasOwn(ARITY, upper) && upper !== 'Z') {
|
|
78
|
+
return failure(commandOffset, 'path command is not supported by artifact analysis', command, true);
|
|
79
|
+
}
|
|
80
|
+
if (upper === 'Z') {
|
|
81
|
+
line(subpath.points[0]);
|
|
82
|
+
subpath.closed = true;
|
|
83
|
+
command = null;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const parameters = [];
|
|
87
|
+
const parameterOffset = tokens[index]?.offset ?? source.length;
|
|
88
|
+
for (let count = 0; count < ARITY[upper]; count += 1) {
|
|
89
|
+
const token = tokens[index];
|
|
90
|
+
if (!token || token.kind !== 'number') {
|
|
91
|
+
return failure(token?.offset ?? source.length, `command ${command} requires ${ARITY[upper]} parameters`, command);
|
|
92
|
+
}
|
|
93
|
+
parameters.push(token.value);
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
const relative = command !== upper;
|
|
97
|
+
const point = (x, y) => relative ? [current[0] + x, current[1] + y] : [x, y];
|
|
98
|
+
let end;
|
|
99
|
+
let control;
|
|
100
|
+
if (upper === 'H') end = [relative ? current[0] + parameters[0] : parameters[0], current[1]];
|
|
101
|
+
else if (upper === 'V') end = [current[0], relative ? current[1] + parameters[0] : parameters[0]];
|
|
102
|
+
else if (upper === 'Q') {
|
|
103
|
+
control = point(parameters[0], parameters[1]);
|
|
104
|
+
end = point(parameters[2], parameters[3]);
|
|
105
|
+
} else end = point(parameters[0], parameters[1]);
|
|
106
|
+
if (![...end, ...(control || [])].every(Number.isFinite)) {
|
|
107
|
+
return failure(parameterOffset, 'computed coordinate is not finite', command);
|
|
108
|
+
}
|
|
109
|
+
if (upper === 'M') {
|
|
110
|
+
move(end);
|
|
111
|
+
// moveto 后的额外参数组是 lineto,不能变成额外子路径。
|
|
112
|
+
command = relative ? 'l' : 'L';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (subpath.closed) move(current);
|
|
116
|
+
if (upper !== 'Q') { line(end); continue; }
|
|
117
|
+
|
|
118
|
+
const start = current;
|
|
119
|
+
const cross = (control[0] - start[0]) * (end[1] - start[1]) - (control[1] - start[1]) * (end[0] - start[0]);
|
|
120
|
+
if (Math.abs(cross) <= 1e-9) subpath.straightSegments.push({ start, end });
|
|
121
|
+
// 保留既有八步 Q 采样;非共线曲线不冒充结构框的直线边界。
|
|
122
|
+
for (let step = 1; step <= 8; step += 1) {
|
|
123
|
+
const amount = step / 8;
|
|
124
|
+
const remaining = 1 - amount;
|
|
125
|
+
const next = [0, 1].map(axis => remaining * remaining * start[axis]
|
|
126
|
+
+ 2 * remaining * amount * control[axis] + amount * amount * end[axis]);
|
|
127
|
+
if (!next.every(Number.isFinite)) return failure(parameterOffset, 'sampled coordinate is not finite', command);
|
|
128
|
+
subpath.segments.push({ start: current, end: next });
|
|
129
|
+
subpath.points.push(next);
|
|
130
|
+
current = next;
|
|
131
|
+
}
|
|
132
|
+
current = end;
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, subpaths };
|
|
135
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Node text (`label`, `sublabel`, `tag`) renders as one <text> element with
|
|
4
4
|
// text-anchor="middle" and is never wrapped. Left unmeasured, an over-long
|
|
5
5
|
// value silently spills across its neighbours while validation still reports
|
|
6
|
-
// a clean receipt
|
|
6
|
+
// a clean receipt — the failure mode this module exists to close.
|
|
7
7
|
//
|
|
8
8
|
// Two halves, always used together:
|
|
9
9
|
// - fittedNodeFontSize shrinks the text toward a legible minimum at render
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// `minimum` font sizes are not, because renderers set node text at different
|
|
17
17
|
// sizes (architecture sublabels are 9px, the rest are 7px).
|
|
18
18
|
|
|
19
|
-
import { textUnits } from './utils.mjs';
|
|
19
|
+
import { textUnits, SEMANTIC_SIGIL_INSET, SEMANTIC_SIGIL_SIZE, SEMANTIC_SIGIL_FOOTPRINT, SOURCE_BADGE_FOOTPRINT } from './utils.mjs';
|
|
20
20
|
|
|
21
21
|
// widthFactor: px of advance width per text unit, per px of font size.
|
|
22
22
|
// horizontalPadding: total px reserved inside the box so text never touches
|
|
@@ -27,7 +27,7 @@ export const nodeTextFit = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
// Largest font size at or below `preferred` that fits `text` inside `width`,
|
|
30
|
-
// floored at `minimum`
|
|
30
|
+
// floored at `minimum` — below that the text is no longer legible and the
|
|
31
31
|
// caller should be reporting a problem instead.
|
|
32
32
|
export function fittedNodeFontSize(text, width, preferred, minimum) {
|
|
33
33
|
const units = Math.max(1, textUnits(text));
|
|
@@ -47,3 +47,59 @@ export function minimumNodeTextWidth(text, minimum) {
|
|
|
47
47
|
export function availableNodeTextWidth(width) {
|
|
48
48
|
return width - nodeTextFit.horizontalPadding;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
// Adapted from Souptik Chakraborty's #220: shift only labels which reach a
|
|
52
|
+
// corner icon. Unlike the original hard gate, a narrow valid node uses a
|
|
53
|
+
// separate text row; its authored bounds and acceptance remain unchanged.
|
|
54
|
+
export function nodeLabelLayout({ width, height, rows, side = 'left', brand = false, source = false, step = '' }) {
|
|
55
|
+
const result = { x: width / 2, ys: rows.map(row => row.y), sigilY: SEMANTIC_SIGIL_INSET, sigilSize: SEMANTIC_SIGIL_SIZE };
|
|
56
|
+
const labelWidth = minimumNodeTextWidth(rows[0].text, rows[0].font);
|
|
57
|
+
const stepEnd = step ? (side === 'left' ? 23 : 10) + minimumNodeTextWidth(step, 8) + 3 : 0;
|
|
58
|
+
const left = Math.max(side === 'left' ? SEMANTIC_SIGIL_FOOTPRINT + 2 : 4, stepEnd);
|
|
59
|
+
const right = width - (brand ? 26 : side === 'right' ? SEMANTIC_SIGIL_FOOTPRINT + 2 : 4)
|
|
60
|
+
- (source ? SOURCE_BADGE_FOOTPRINT : 0);
|
|
61
|
+
if (result.x - labelWidth / 2 >= left && result.x + labelWidth / 2 <= right) return result;
|
|
62
|
+
if (labelWidth <= right - left) {
|
|
63
|
+
// Round away from the icon, retaining the node centre whenever possible.
|
|
64
|
+
result.x = Math.min(Math.floor((right - labelWidth / 2) * 10) / 10,
|
|
65
|
+
Math.max(result.x, Math.ceil((left + labelWidth / 2) * 10) / 10));
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
// Keep the existing font sizes and put the text below the decoration rail.
|
|
69
|
+
// Conservative ascent/descent bounds also protect CJK and fallback fonts.
|
|
70
|
+
let bottom = Math.max(brand ? 22 : SEMANTIC_SIGIL_FOOTPRINT, source ? 19 : 0);
|
|
71
|
+
const ys = rows.map(row => {
|
|
72
|
+
const y = Math.max(row.y, Math.ceil((bottom + 2 + row.font * 1.2) * 10) / 10);
|
|
73
|
+
bottom = y + row.font * 0.3;
|
|
74
|
+
return y;
|
|
75
|
+
});
|
|
76
|
+
if (bottom <= height - 2) {
|
|
77
|
+
result.ys = ys;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
if (source) {
|
|
81
|
+
// A source badge adds a second decoration on the right. On short boxes,
|
|
82
|
+
// restoring the original rows would put the title back under that badge.
|
|
83
|
+
// Try compact leading before giving up the dedicated text rail. Retain
|
|
84
|
+
// every font size and the authored box; only this crowded fallback packs
|
|
85
|
+
// the rows, with a full em above each baseline and 0.3 em below it.
|
|
86
|
+
let compactBottom = Math.max(brand ? 22 : SEMANTIC_SIGIL_FOOTPRINT, 19) + 1;
|
|
87
|
+
const compactYs = rows.map(row => {
|
|
88
|
+
const y = Math.ceil((compactBottom + 1 + row.font) * 10) / 10;
|
|
89
|
+
compactBottom = y + row.font * 0.3;
|
|
90
|
+
return y;
|
|
91
|
+
});
|
|
92
|
+
// The compact fallback may also use the otherwise reserved bottom
|
|
93
|
+
// padding; the entire descent still stays inside the fixed box.
|
|
94
|
+
if (compactBottom <= height - 0.5) {
|
|
95
|
+
result.ys = compactYs;
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// A deliberately short fixed box may have no spare row. Preserve its text
|
|
100
|
+
// and geometry, and fit only the decorative sigil in the space above it.
|
|
101
|
+
result.sigilY = 1;
|
|
102
|
+
result.sigilSize = Math.max(1, Math.min(SEMANTIC_SIGIL_SIZE,
|
|
103
|
+
Math.floor(rows[0].y - rows[0].font * 1.2 - 3)));
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export { esc };
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// `extra` appends type-owned definitions (the entity renderer's cardinality
|
|
12
|
+
// markers) inside the same <defs>. Callers that pass nothing keep the exact
|
|
13
|
+
// bytes the five longer-standing renderers already emit.
|
|
14
|
+
export function renderDefinitions(extra = '') {
|
|
12
15
|
return ` <!-- Definitions -->
|
|
13
16
|
<defs>
|
|
14
17
|
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
@@ -25,7 +28,7 @@ export function renderDefinitions() {
|
|
|
25
28
|
</marker>
|
|
26
29
|
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
27
30
|
<path d="M 40 0 L 0 0 0 40" class="c-grid" stroke-width="0.5"/>
|
|
28
|
-
</pattern
|
|
31
|
+
</pattern>${extra}
|
|
29
32
|
</defs>`;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -33,12 +36,13 @@ const SIGIL_TONE = {
|
|
|
33
36
|
frontend: 'frontend',
|
|
34
37
|
start: 'frontend',
|
|
35
38
|
backend: 'backend',
|
|
36
|
-
active: '
|
|
39
|
+
active: 'frontend',
|
|
37
40
|
database: 'database',
|
|
38
|
-
success: '
|
|
41
|
+
success: 'backend',
|
|
39
42
|
cloud: 'cloud',
|
|
40
43
|
waiting: 'cloud',
|
|
41
44
|
security: 'security',
|
|
45
|
+
decision: 'database',
|
|
42
46
|
failure: 'security',
|
|
43
47
|
messagebus: 'messagebus',
|
|
44
48
|
external: 'external',
|
|
@@ -46,6 +50,12 @@ const SIGIL_TONE = {
|
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
const SIGIL_SHAPE = {
|
|
53
|
+
calendar: `<rect x="2" y="3.5" width="12" height="10.5" rx="2"/><path d="M5 2v3M11 2v3M2 7h12M5 10h2M9 10h2"/>`,
|
|
54
|
+
clock: `<circle cx="8" cy="8" r="6"/><path d="M8 4v4l3 2"/>`,
|
|
55
|
+
person: `<circle cx="8" cy="4.5" r="2.5"/><path d="M3 14v-2a5 5 0 0 1 10 0v2"/>`,
|
|
56
|
+
briefcase: `<rect x="2" y="5" width="12" height="9" rx="2"/><path d="M5 5V2h6v3M2 9h12M7 9v2h2V9"/>`,
|
|
57
|
+
flag: `<path d="M3 14V2h10l-2 3 2 3H3"/>`,
|
|
58
|
+
moon: `<path d="M13.5 10A6 6 0 0 1 6 2.5 6 6 0 1 0 13.5 10Z"/>`,
|
|
49
59
|
frontend: `<rect x="2" y="3" width="12" height="10" rx="2"/>
|
|
50
60
|
<path d="M2 6.5h12"/>
|
|
51
61
|
<circle cx="4.1" cy="4.8" r=".7" class="sigil-fill"/>
|
|
@@ -66,20 +76,35 @@ const SIGIL_SHAPE = {
|
|
|
66
76
|
<path d="m7 5.4 3.6 2.6L7 10.6Z" class="sigil-fill"/>`,
|
|
67
77
|
active: `<path d="M2 8h3l1.5-3.5L9 12l1.6-4H14"/>`,
|
|
68
78
|
waiting: `<path d="M4 2.5h8M4 13.5h8M5 3c0 2.8 2 3.2 3 5-1 1.8-3 2.2-3 5M11 3c0 2.8-2 3.2-3 5 1 1.8 3 2.2 3 5"/>`,
|
|
79
|
+
decision: `<path d="M8 2.2 13.8 8 8 13.8 2.2 8Z"/>`,
|
|
69
80
|
success: `<circle cx="8" cy="8" r="5.3"/>
|
|
70
81
|
<path d="m5.2 8 1.8 1.8 3.8-4"/>`,
|
|
71
82
|
failure: `<circle cx="8" cy="8" r="5.3"/>
|
|
72
83
|
<path d="m5.7 5.7 4.6 4.6m0-4.6-4.6 4.6"/>`,
|
|
84
|
+
stop: `<circle cx="8" cy="8" r="5.3"/>
|
|
85
|
+
<rect x="6.1" y="6.1" width="3.8" height="3.8" rx=".5" class="sigil-fill"/>`,
|
|
73
86
|
neutral: `<rect x="3" y="3" width="10" height="10" rx="2"/>
|
|
74
87
|
<circle cx="8" cy="8" r="1.2" class="sigil-fill"/>`,
|
|
75
88
|
};
|
|
76
89
|
|
|
77
|
-
// A quiet, renderer-owned
|
|
90
|
+
// A quiet, renderer-owned corner symbol (type default or authored icon). It is
|
|
91
|
+
// SVG content rather than a
|
|
78
92
|
// viewer overlay, so it survives canonical export while adding no focus target,
|
|
79
93
|
// accessible name, layout box, or interaction state of its own.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
// Shared with label clearance so the reserved rail matches the actual icon.
|
|
95
|
+
export const SEMANTIC_SIGIL_INSET = 6;
|
|
96
|
+
export const SEMANTIC_SIGIL_SIZE = 11;
|
|
97
|
+
export const SEMANTIC_SIGIL_FOOTPRINT = SEMANTIC_SIGIL_INSET + SEMANTIC_SIGIL_SIZE;
|
|
98
|
+
// The viewer installs a runtime "sources" beacon on the node's top-right
|
|
99
|
+
// rail, just left of the brand mark. Layout must reserve the same footprint
|
|
100
|
+
// so labels never sit under the badge.
|
|
101
|
+
export const SOURCE_BADGE_FOOTPRINT = 38;
|
|
102
|
+
|
|
103
|
+
export function renderSemanticSigil(kind, { x, y, size = SEMANTIC_SIGIL_SIZE, icon } = {}) {
|
|
104
|
+
if (icon === 'none') return '';
|
|
105
|
+
const selected = icon ?? kind;
|
|
106
|
+
const normalized = Object.hasOwn(SIGIL_SHAPE, selected) ? selected : 'neutral';
|
|
107
|
+
const tone = SIGIL_TONE[kind] || 'external';
|
|
83
108
|
const scale = size / 16;
|
|
84
109
|
return `<g aria-hidden="true" data-semantic-sigil="${esc(normalized)}" class="semantic-sigil s-${tone}" transform="translate(${x} ${y}) scale(${scale})">
|
|
85
110
|
${SIGIL_SHAPE[normalized]}
|
|
@@ -96,7 +121,7 @@ ${list.map((card) => ` <div class="card">
|
|
|
96
121
|
<h3>${esc(card.title)}</h3>
|
|
97
122
|
</div>
|
|
98
123
|
<ul>
|
|
99
|
-
${card.items.map((item) => ` <li
|
|
124
|
+
${card.items.map((item) => ` <li>${esc(item)}</li>`).join('\n')}
|
|
100
125
|
</ul>
|
|
101
126
|
</div>`).join('\n\n')}
|
|
102
127
|
</div>`;
|
|
@@ -105,7 +130,6 @@ ${card.items.map((item) => ` <li>• ${esc(item)}</li>`).join('\n')}
|
|
|
105
130
|
const SVG_SLOT_RE = / <!-- ARCHIFY:SVG_SLOT_START -->[\s\S]*? <!-- ARCHIFY:SVG_SLOT_END -->/;
|
|
106
131
|
const CARDS_SLOT_RE = / <!-- ARCHIFY:CARDS_SLOT_START -->[\s\S]*? <!-- ARCHIFY:CARDS_SLOT_END -->/;
|
|
107
132
|
const SUBTITLE_SLOT_RE = /^([ \t]*)<p class="subtitle">\[Subtitle description\]<\/p>[ \t]*(\r?\n)?/m;
|
|
108
|
-
const GUIDED_VIEWS_PLACEHOLDER = '<!-- ARCHIFY:GUIDED_VIEWS_DATA -->';
|
|
109
133
|
const SOURCE_EVIDENCE_PLACEHOLDER = ' <!-- ARCHIFY:SOURCE_EVIDENCE_DATA -->';
|
|
110
134
|
const I18N_PLACEHOLDER = ' <!-- ARCHIFY:I18N_DATA -->';
|
|
111
135
|
|
|
@@ -120,7 +144,7 @@ const TEMPLATE_PLACEHOLDERS = [
|
|
|
120
144
|
'<html lang="en" data-theme="dark" data-preset="[VISUAL PRESET]">',
|
|
121
145
|
'<title>[PROJECT NAME] Architecture Diagram</title>',
|
|
122
146
|
'<h1>[PROJECT NAME] Architecture</h1>',
|
|
123
|
-
|
|
147
|
+
I18N_PLACEHOLDER,
|
|
124
148
|
];
|
|
125
149
|
|
|
126
150
|
export function applyTemplate(template, {
|
|
@@ -130,7 +154,6 @@ export function applyTemplate(template, {
|
|
|
130
154
|
cards,
|
|
131
155
|
locale,
|
|
132
156
|
visualPreset = 'classic',
|
|
133
|
-
guidedViews = [],
|
|
134
157
|
sourceEvidence = null,
|
|
135
158
|
}) {
|
|
136
159
|
if (!SVG_SLOT_RE.test(template)) {
|
|
@@ -155,7 +178,6 @@ export function applyTemplate(template, {
|
|
|
155
178
|
}
|
|
156
179
|
// Function replacers: a literal `$&`, `$'`, `$\`` or `$$` in titles, labels,
|
|
157
180
|
// or rendered SVG must not be interpreted as a replacement pattern.
|
|
158
|
-
const guidedViewsJson = serializeScriptJson(guidedViews);
|
|
159
181
|
const sourceEvidenceJson = serializeScriptJson(sourceEvidence);
|
|
160
182
|
const resolvedLocale = resolveLocale(locale);
|
|
161
183
|
const i18nJson = serializeScriptJson({ locale: resolvedLocale, messages: viewerCatalog(resolvedLocale) });
|
|
@@ -163,11 +185,8 @@ export function applyTemplate(template, {
|
|
|
163
185
|
? `<p class="subtitle">${esc(subtitle)}</p>`
|
|
164
186
|
: '';
|
|
165
187
|
const i18nData = ` <script id="archify-i18n-data" type="application/json">${i18nJson}</script>`;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
? localizedTemplate.replace(I18N_PLACEHOLDER, () => i18nData)
|
|
169
|
-
: localizedTemplate.replace(GUIDED_VIEWS_PLACEHOLDER, () => `${i18nData}\n ${GUIDED_VIEWS_PLACEHOLDER}`);
|
|
170
|
-
return templateWithI18n
|
|
188
|
+
return localizeTemplate(template, resolvedLocale)
|
|
189
|
+
.replace(I18N_PLACEHOLDER, () => i18nData)
|
|
171
190
|
.replace(TEMPLATE_PLACEHOLDERS[0], () => `<html lang="${esc(resolvedLocale)}" data-theme="dark" data-preset="${esc(visualPreset)}">`)
|
|
172
191
|
.replace(TEMPLATE_PLACEHOLDERS[1], () => `<title>${esc(translateMessage(resolvedLocale, 'page.title', { title }))}</title>`)
|
|
173
192
|
.replace(TEMPLATE_PLACEHOLDERS[2], () => `<h1>${esc(title)}</h1>`)
|
|
@@ -176,7 +195,6 @@ export function applyTemplate(template, {
|
|
|
176
195
|
: '')
|
|
177
196
|
.replace(SVG_SLOT_RE, () => svg)
|
|
178
197
|
.replace(CARDS_SLOT_RE, () => cards)
|
|
179
|
-
.replace(GUIDED_VIEWS_PLACEHOLDER, () => `<script id="archify-guided-views-data" type="application/json">${guidedViewsJson}</script>`)
|
|
180
198
|
.replace(SOURCE_EVIDENCE_PLACEHOLDER, () => sourceEvidence
|
|
181
199
|
? ` <script id="archify-source-evidence-data" type="application/json">${sourceEvidenceJson}</script>`
|
|
182
200
|
: '');
|
|
@@ -184,7 +202,7 @@ export function applyTemplate(template, {
|
|
|
184
202
|
|
|
185
203
|
// CJK and other wide/fullwidth glyphs render at roughly twice the advance
|
|
186
204
|
// width of ASCII in the monospace stacks the template uses. Keep halfwidth
|
|
187
|
-
// forms (notably U+FF61
|
|
205
|
+
// forms (notably U+FF61–U+FF9F Katakana) out of this set. The explicit ranges
|
|
188
206
|
// also cover vertical punctuation and supplementary East Asian scripts that
|
|
189
207
|
// literal glyph ranges made difficult to audit.
|
|
190
208
|
// Code points that take two columns of advance width: East Asian Wide and
|
|
@@ -200,33 +218,43 @@ export function applyTemplate(template, {
|
|
|
200
218
|
// ranges because V8 has no \p{East_Asian_Width=W} property escape.
|
|
201
219
|
const FULLWIDTH_RE = /[\u1100-\u115F\u231A-\u231B\u2329-\u232A\u23E9-\u23EC\u23F0\u23F3\u25FD-\u25FE\u2614-\u2615\u2630-\u2637\u2648-\u2653\u267F\u268A-\u268F\u2693\u26A1\u26AA-\u26AB\u26BD-\u26BE\u26C4-\u26C5\u26CE\u26D4\u26EA\u26F2-\u26F3\u26F5\u26FA\u26FD\u2705\u270A-\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B-\u2B1C\u2B50\u2B55\u2E80-\uA4CF\uA960-\uA97C\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF01-\uFF60\uFFE0-\uFFE6\u{16FE0}-\u{18DFF}\u{1AFF0}-\u{1AFFF}\u{1B000}-\u{1B2FF}\u{1F000}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u;
|
|
202
220
|
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// presentation,
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// A selector following a base that cannot take emoji presentation is
|
|
213
|
-
// malformed input; measuring it wide is the safe direction here, since
|
|
214
|
-
// over-measuring pads a box while under-measuring spills the label out of it.
|
|
221
|
+
// Variation selectors contribute no separate unit. This is a conservative
|
|
222
|
+
// width estimate, not a measurement of the selected glyph: its actual advance
|
|
223
|
+
// depends on the font and presentation (Unicode UAX #11).
|
|
224
|
+
// VS16 requests emoji presentation, so reserve two units for the sequence.
|
|
225
|
+
// VS15 retains the base's width estimate; forcing every text-presentation
|
|
226
|
+
// sequence to one unit undercounts wide bases, including CJK characters whose
|
|
227
|
+
// font ignores that selector. Neutral bases remain one unit. Some selected
|
|
228
|
+
// text glyphs can be narrower than this estimate; prefer extra space to overflow.
|
|
215
229
|
const VARIATION_SELECTOR_FIRST = 0xfe00;
|
|
216
230
|
const VARIATION_SELECTOR_LAST = 0xfe0f;
|
|
217
|
-
const VARIATION_SELECTOR_TEXT = 0xfe0e;
|
|
218
231
|
const VARIATION_SELECTOR_EMOJI = 0xfe0f;
|
|
219
232
|
|
|
233
|
+
// Width measurement is pure and the same labels are measured many times per
|
|
234
|
+
// compile, so the unit count is memoized by its input string.
|
|
235
|
+
const TEXT_UNITS_CACHE = new Map();
|
|
236
|
+
const MAX_TEXT_UNITS_CACHE_ENTRIES = 4096;
|
|
237
|
+
const MAX_CACHED_TEXT_LENGTH = 1024;
|
|
238
|
+
|
|
220
239
|
export function textUnits(text) {
|
|
221
|
-
const
|
|
240
|
+
const cacheKey = String(text ?? '');
|
|
241
|
+
const cachedUnits = TEXT_UNITS_CACHE.get(cacheKey);
|
|
242
|
+
if (cachedUnits !== undefined) return cachedUnits;
|
|
243
|
+
const chars = Array.from(cacheKey);
|
|
222
244
|
let units = 0;
|
|
223
245
|
for (let i = 0; i < chars.length; i += 1) {
|
|
224
246
|
const codePoint = chars[i].codePointAt(0);
|
|
225
247
|
if (codePoint >= VARIATION_SELECTOR_FIRST && codePoint <= VARIATION_SELECTOR_LAST) continue;
|
|
226
248
|
const next = i + 1 < chars.length ? chars[i + 1].codePointAt(0) : -1;
|
|
227
249
|
if (next === VARIATION_SELECTOR_EMOJI) units += 2;
|
|
228
|
-
else if (next === VARIATION_SELECTOR_TEXT) units += 1;
|
|
229
250
|
else units += FULLWIDTH_RE.test(chars[i]) ? 2 : 1;
|
|
230
251
|
}
|
|
252
|
+
// Keep repeated in-process compiles bounded, including unusually long labels.
|
|
253
|
+
if (cacheKey.length <= MAX_CACHED_TEXT_LENGTH) {
|
|
254
|
+
if (TEXT_UNITS_CACHE.size >= MAX_TEXT_UNITS_CACHE_ENTRIES) {
|
|
255
|
+
TEXT_UNITS_CACHE.delete(TEXT_UNITS_CACHE.keys().next().value);
|
|
256
|
+
}
|
|
257
|
+
TEXT_UNITS_CACHE.set(cacheKey, units);
|
|
258
|
+
}
|
|
231
259
|
return units;
|
|
232
260
|
}
|
|
@@ -57,7 +57,7 @@ export function validateSchema(diagramType, data) {
|
|
|
57
57
|
additionalProperties: [`remove unsupported property ${JSON.stringify(error.params?.additionalProperty)}`],
|
|
58
58
|
required: [`add required property ${JSON.stringify(error.params?.missingProperty)}`],
|
|
59
59
|
type: [`use ${JSON.stringify(error.params?.type)} at ${annotated.path}`],
|
|
60
|
-
enum: [`choose one of
|
|
60
|
+
enum: [`choose one of the allowed values`],
|
|
61
61
|
pattern: [`match the required pattern ${JSON.stringify(error.params?.pattern)}`],
|
|
62
62
|
minimum: [`use a value ${error.params?.comparison || '>='} ${error.params?.limit}`],
|
|
63
63
|
maximum: [`use a value ${error.params?.comparison || '<='} ${error.params?.limit}`],
|
|
@@ -66,13 +66,10 @@ export function validateSchema(diagramType, data) {
|
|
|
66
66
|
minLength: [`provide at least ${error.params?.limit} character(s)`],
|
|
67
67
|
maxLength: [`provide at most ${error.params?.limit} character(s)`],
|
|
68
68
|
}[error.keyword] || [];
|
|
69
|
-
const detail = error.params && Object.keys(error.params).length
|
|
70
|
-
? ` ${JSON.stringify(error.params)}`
|
|
71
|
-
: '';
|
|
72
69
|
return {
|
|
73
70
|
code: `schema/${error.keyword}`,
|
|
74
71
|
severity: 'error',
|
|
75
|
-
message: `${annotatePath(error.instancePath, data)} ${error.message}
|
|
72
|
+
message: `${annotatePath(error.instancePath, data)} ${error.message}`,
|
|
76
73
|
subject,
|
|
77
74
|
evidence,
|
|
78
75
|
supportedFixes,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Event Timeline Renderer
|
|
2
|
+
|
|
3
|
+
Render `diagram_type: "timeline"` JSON files into the standard Archify HTML template.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
node archify/renderers/timeline/render-timeline.mjs input.timeline.json output.html
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`--layout-json` prints the axis segments, breaks, ticks, lanes, and placed
|
|
10
|
+
events (with their millisecond timestamps and x positions) instead of HTML.
|
|
11
|
+
|
|
12
|
+
## Input
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"schema_version": 1,
|
|
17
|
+
"diagram_type": "timeline",
|
|
18
|
+
"meta": { "title": "Payment failure incident", "evidence": "illustrative", "timezone": "Asia/Shanghai" },
|
|
19
|
+
"lanes": [{ "id": "release", "label": "Release" }],
|
|
20
|
+
"events": [{ "id": "deploy", "at": "2026-10-01T10:00:00+08:00", "title": "Deploy v2.4", "lane": "release" }]
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `at` is ISO 8601 with an explicit `Z` or `±HH:MM` offset (schema). An
|
|
25
|
+
impossible date fails `timeline/invalid-timestamp`.
|
|
26
|
+
- `meta.timezone` is an IANA zone (default `UTC`) used for every tick and label;
|
|
27
|
+
ticks fall on that zone's wall clock, so a `+05:30` zone ticks on its own
|
|
28
|
+
hours. An unknown zone fails `timeline/invalid-timezone`.
|
|
29
|
+
- Across timezone-offset changes, daily ticks stay at local midnight and hourly
|
|
30
|
+
ticks use real local instants. Both occurrences of a repeated hour remain;
|
|
31
|
+
skipped hours or dates do not invent ticks. Event timestamps and proportional
|
|
32
|
+
elapsed-time spacing are unchanged, so consecutive daily ticks can be 23 or 25
|
|
33
|
+
hours apart. The existing label filter still hides text that does not fit.
|
|
34
|
+
- 夏令时和时区调整不会把刻度固定在时间段起点的 UTC 偏移上:日刻度保持当地
|
|
35
|
+
午夜,小时刻度仅对应真实存在的当地时刻。回拨产生的两个相同墙钟时刻都保留;
|
|
36
|
+
跳过的小时或整天不补造刻度。事件时间与轴上的实际时间距离不变,因此相邻
|
|
37
|
+
日刻度之间可以是 23 或 25 小时;空间不足的刻度文字仍按原有规则隐藏。
|
|
38
|
+
- `meta.evidence` (`observed` or `illustrative`) is required and drawn as a
|
|
39
|
+
badge at the top-left of the canvas.
|
|
40
|
+
- With `lanes`, every event names a declared lane (`timeline/unknown-lane`).
|
|
41
|
+
`detail` appears in the Semantic Passport; `kind` (`change`, `alert`,
|
|
42
|
+
`action`, `recovery`) colours the card and adds a legend entry.
|
|
43
|
+
|
|
44
|
+
## Axis
|
|
45
|
+
|
|
46
|
+
Events are sorted by time (ties keep authored order). x is linear in time with
|
|
47
|
+
one scale for the whole diagram; every lane shares the same axis, so one
|
|
48
|
+
instant has one x in every lane, whatever offset each timestamp was written in.
|
|
49
|
+
|
|
50
|
+
`layout.breaks: "auto"` (default) compresses a quiet period longer than 8× the
|
|
51
|
+
median gap between distinct timestamps and 10% of the span (at most the eight
|
|
52
|
+
longest that fit while preserving room for proportional time). A break is a
|
|
53
|
+
fixed-width hatched band with a zig-zag on the axis and
|
|
54
|
+
the omitted duration (`≈ 1 d 11 h`); the axis caption states how many periods
|
|
55
|
+
were compressed, and each break carries `data-break-ms` and an accessible label.
|
|
56
|
+
Inside each segment the scale is the same constant, so distances there remain
|
|
57
|
+
exact. `layout.breaks: "none"` draws one proportional axis. `layout.width`
|
|
58
|
+
(default 960) is the axis width.
|
|
59
|
+
|
|
60
|
+
## Cards
|
|
61
|
+
|
|
62
|
+
Each event is a dot on its lane line plus a card with its exact time and title.
|
|
63
|
+
Cards are centred on their instant and sit on either side of the lane line,
|
|
64
|
+
packed into rows outward from it: each takes whichever side lets it sit closest
|
|
65
|
+
to the line without touching an earlier card (ties go below), so simultaneous
|
|
66
|
+
and close events stack instead of overprinting and a burst spreads both ways.
|
|
67
|
+
Stems are drawn behind every card, and every dot sits on one shared halo layer, so events seconds apart merge into one outlined capsule. A card is a quiet surface with a tone accent
|
|
68
|
+
for its kind. Titles wrap between words. On a multi-day axis a tick names its
|
|
69
|
+
date only when the date changes.
|
|
70
|
+
|
|
71
|
+
## Tested size
|
|
72
|
+
|
|
73
|
+
`examples/archify-dev-activity.timeline.json` is observed data (20 tag and
|
|
74
|
+
merge timestamps from this repository's `origin/dev`, including events 19 s
|
|
75
|
+
apart, a `+05:30` committer offset, and five compressed quiet periods). It
|
|
76
|
+
renders in about 0.3s and passes the showcase gates.
|