dsh-knowhub 0.9.0 → 0.10.0
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/README.md +11 -2
- package/cordis.patch.yml +10 -0
- package/lib/host/model.js +1 -1
- package/lib/host/pipeline/incremental.js +3 -3
- package/lib/host/pipeline/initialize.js +144 -27
- package/lib/host/pipeline/model-output.js +2 -2
- package/package.json +3 -2
- package/skills/archify/LICENSE +202 -0
- package/skills/archify/LICENSE-MIT +22 -0
- package/skills/archify/NOTICE +26 -0
- package/skills/archify/SKILL.md +137 -0
- package/skills/archify/THIRD_PARTY_NOTICES.md +78 -0
- package/skills/archify/assets/JetBrainsMono-OFL.txt +93 -0
- package/skills/archify/assets/template.html +14768 -0
- package/skills/archify/bin/archify.mjs +7289 -0
- package/skills/archify/bin/delivery-update.mjs +92 -0
- package/skills/archify/bin/finalize.mjs +1055 -0
- package/skills/archify/bin/open-artifact.mjs +146 -0
- package/skills/archify/bin/preview.mjs +970 -0
- package/skills/archify/bin/recover-output.mjs +41 -0
- package/skills/archify/bin/visual-check.mjs +2780 -0
- package/skills/archify/brand-marks/README.md +31 -0
- package/skills/archify/brand-marks/catalog.json +131 -0
- package/skills/archify/delta/architecture-delta.mjs +1318 -0
- package/skills/archify/examples/agent-run.lifecycle.json +55 -0
- package/skills/archify/examples/agent-tool-call.workflow.json +74 -0
- package/skills/archify/examples/archify-dev-activity.timeline.json +176 -0
- package/skills/archify/examples/archify-repository.tree.json +197 -0
- package/skills/archify/examples/async-job-roundtrip.sequence.json +56 -0
- package/skills/archify/examples/brand-aware-delivery.architecture.json +34 -0
- package/skills/archify/examples/cache-miss-request.sequence.json +77 -0
- package/skills/archify/examples/checkout-platform.base.architecture.json +32 -0
- package/skills/archify/examples/checkout-platform.head.architecture.json +32 -0
- package/skills/archify/examples/checkout-request.waterfall.json +20 -0
- package/skills/archify/examples/delivery.explainer.json +155 -0
- package/skills/archify/examples/deployment-release.lifecycle.json +72 -0
- package/skills/archify/examples/event-stream.dataflow.json +52 -0
- package/skills/archify/examples/example-rebuild.waterfall.json +212 -0
- package/skills/archify/examples/holiday-planning.workflow.json +143 -0
- package/skills/archify/examples/incident-response.workflow.json +59 -0
- package/skills/archify/examples/locales/de.json +463 -0
- package/skills/archify/examples/locales/fr.json +463 -0
- package/skills/archify/examples/locales/fr.partial.json +22 -0
- package/skills/archify/examples/locales/it.json +463 -0
- package/skills/archify/examples/locales/ja.json +463 -0
- package/skills/archify/examples/locales/pt.json +463 -0
- package/skills/archify/examples/locales/ru.json +463 -0
- package/skills/archify/examples/orders.erd.json +337 -0
- package/skills/archify/examples/payment-incident.timeline.json +24 -0
- package/skills/archify/examples/payment-platform.tree.json +22 -0
- package/skills/archify/examples/payment-processors.class.json +177 -0
- package/skills/archify/examples/payments.class.json +85 -0
- package/skills/archify/examples/product-analytics.dataflow.json +71 -0
- package/skills/archify/examples/production-deployment.architecture.json +51 -0
- package/skills/archify/examples/release-delivery.workflow.json +57 -0
- package/skills/archify/examples/subscription-billing.erd.json +548 -0
- package/skills/archify/examples/web-app.architecture.json +41 -0
- package/skills/archify/explainer/browser-check.mjs +208 -0
- package/skills/archify/explainer/charts.mjs +70 -0
- package/skills/archify/explainer/check.mjs +167 -0
- package/skills/archify/explainer/diagrams.mjs +517 -0
- package/skills/archify/explainer/diff.mjs +124 -0
- package/skills/archify/explainer/inline.mjs +155 -0
- package/skills/archify/explainer/layout.mjs +35 -0
- package/skills/archify/explainer/page-bundle.mjs +16 -0
- package/skills/archify/explainer/parse.mjs +300 -0
- package/skills/archify/explainer/patch.mjs +117 -0
- package/skills/archify/explainer/prepare.mjs +131 -0
- package/skills/archify/explainer/reader.css +472 -0
- package/skills/archify/explainer/reader.js +666 -0
- package/skills/archify/explainer/render.mjs +224 -0
- package/skills/archify/explainer/revision.mjs +20 -0
- package/skills/archify/explainer/sources.mjs +318 -0
- package/skills/archify/explainer/validate.mjs +138 -0
- package/skills/archify/explainer/vendor/pele/LICENSE +40 -0
- package/skills/archify/explainer/vendor/pele/README.md +5 -0
- package/skills/archify/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
- package/skills/archify/explainer/vendor/pele/chunks/42UTT654.js +3 -0
- package/skills/archify/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
- package/skills/archify/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
- package/skills/archify/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
- package/skills/archify/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
- package/skills/archify/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
- package/skills/archify/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
- package/skills/archify/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
- package/skills/archify/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
- package/skills/archify/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
- package/skills/archify/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
- package/skills/archify/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
- package/skills/archify/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
- package/skills/archify/explainer/vendor/pele/core.js +1 -0
- package/skills/archify/explainer/vendor/pele/diagrams/pie.js +1 -0
- package/skills/archify/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
- package/skills/archify/explainer/vendor/pele/diagrams/radar.js +1 -0
- package/skills/archify/explainer/vendor/pele/diagrams/sankey.js +1 -0
- package/skills/archify/explainer/vendor/pele/diagrams/venn.js +1 -0
- package/skills/archify/explainer/vendor/pele/diagrams/xychart.js +1 -0
- package/skills/archify/explainer/writing.mjs +139 -0
- package/skills/archify/locales/README.md +40 -0
- package/skills/archify/locales/ar.json +488 -0
- package/skills/archify/locales/en.json +488 -0
- package/skills/archify/locales/es.json +488 -0
- package/skills/archify/locales/ko.json +488 -0
- package/skills/archify/locales/manifest.json +10 -0
- package/skills/archify/locales/zh-CN.json +488 -0
- package/skills/archify/locales/zh-TW.json +488 -0
- package/skills/archify/migrations/workflow-v2.mjs +280 -0
- package/skills/archify/package.json +17 -0
- package/skills/archify/recipes/scenarios.mjs +455 -0
- package/skills/archify/references/architecture-layout-repair.md +18 -0
- package/skills/archify/references/authoring-contract.md +645 -0
- package/skills/archify/references/authoring-defaults.md +57 -0
- package/skills/archify/references/brand-marks.md +119 -0
- package/skills/archify/references/delivery-contract.md +665 -0
- package/skills/archify/references/explainer-authoring.md +274 -0
- package/skills/archify/references/repository-authoring.md +126 -0
- package/skills/archify/references/update-awareness.md +13 -0
- package/skills/archify/references/viewer-runtime.md +52 -0
- package/skills/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/archify/renderers/architecture/labels.mjs +2 -0
- package/skills/archify/renderers/architecture/render-architecture.mjs +1204 -0
- package/skills/archify/renderers/architecture/routing.mjs +1469 -0
- package/skills/archify/renderers/class/README.md +130 -0
- package/skills/archify/renderers/class/render-class.mjs +1095 -0
- package/skills/archify/renderers/dataflow/README.md +176 -0
- package/skills/archify/renderers/dataflow/dataflow-compiler.mjs +1605 -0
- package/skills/archify/renderers/dataflow/joint-label-routing.mjs +165 -0
- package/skills/archify/renderers/dataflow/label-capacity.mjs +81 -0
- package/skills/archify/renderers/dataflow/render-dataflow.mjs +23 -0
- package/skills/archify/renderers/erd/README.md +246 -0
- package/skills/archify/renderers/erd/grid.mjs +150 -0
- package/skills/archify/renderers/erd/render-erd.mjs +1588 -0
- package/skills/archify/renderers/explainer/render-explainer.mjs +42 -0
- package/skills/archify/renderers/lifecycle/README.md +104 -0
- package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +1134 -0
- package/skills/archify/renderers/sequence/README.md +146 -0
- package/skills/archify/renderers/sequence/render-sequence.mjs +1105 -0
- package/skills/archify/renderers/shared/atomic-output.mjs +1982 -0
- package/skills/archify/renderers/shared/automatic-labels.mjs +290 -0
- package/skills/archify/renderers/shared/brand-marks.mjs +706 -0
- package/skills/archify/renderers/shared/cli.mjs +430 -0
- package/skills/archify/renderers/shared/desktop-readability.mjs +132 -0
- package/skills/archify/renderers/shared/diagnostics.mjs +0 -0
- package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/skills/archify/renderers/shared/generated-validators.mjs +279 -0
- package/skills/archify/renderers/shared/geometry.mjs +2073 -0
- package/skills/archify/renderers/shared/i18n.mjs +328 -0
- package/skills/archify/renderers/shared/layout-report.mjs +57 -0
- package/skills/archify/renderers/shared/legend.mjs +227 -0
- package/skills/archify/renderers/shared/node-card-style.mjs +32 -0
- package/skills/archify/renderers/shared/output-path.mjs +440 -0
- package/skills/archify/renderers/shared/owned-temp-directory.mjs +82 -0
- package/skills/archify/renderers/shared/path-semantics.mjs +1017 -0
- package/skills/archify/renderers/shared/portable-path.mjs +408 -0
- package/skills/archify/renderers/shared/repository-evidence.mjs +394 -0
- package/skills/archify/renderers/shared/repository-location.mjs +71 -0
- package/skills/archify/renderers/shared/route-quality.mjs +670 -0
- package/skills/archify/renderers/shared/sidecar-path.mjs +38 -0
- package/skills/archify/renderers/shared/software-notices.mjs +6 -0
- package/skills/archify/renderers/shared/spatial-grid.mjs +66 -0
- package/skills/archify/renderers/shared/svg-path-analysis.mjs +135 -0
- package/skills/archify/renderers/shared/text-fit.mjs +105 -0
- package/skills/archify/renderers/shared/utils.mjs +260 -0
- package/skills/archify/renderers/shared/validator.mjs +83 -0
- package/skills/archify/renderers/timeline/README.md +76 -0
- package/skills/archify/renderers/timeline/render-timeline.mjs +576 -0
- package/skills/archify/renderers/tree/README.md +96 -0
- package/skills/archify/renderers/tree/render-tree.mjs +573 -0
- package/skills/archify/renderers/waterfall/README.md +73 -0
- package/skills/archify/renderers/waterfall/render-waterfall.mjs +413 -0
- package/skills/archify/renderers/workflow/README.md +337 -0
- package/skills/archify/renderers/workflow/render-workflow.mjs +47 -0
- package/skills/archify/renderers/workflow/workflow-compiler.mjs +5920 -0
- package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +161 -0
- package/skills/archify/renderers/workflow/workflow-placement-repair.mjs +94 -0
- package/skills/archify/schemas/README.md +258 -0
- package/skills/archify/schemas/architecture.schema.json +152 -0
- package/skills/archify/schemas/class.schema.json +345 -0
- package/skills/archify/schemas/common.schema.json +181 -0
- package/skills/archify/schemas/dataflow.schema.json +254 -0
- package/skills/archify/schemas/erd.schema.json +381 -0
- package/skills/archify/schemas/explainer.schema.json +927 -0
- package/skills/archify/schemas/lifecycle.schema.json +179 -0
- package/skills/archify/schemas/sequence.schema.json +234 -0
- package/skills/archify/schemas/timeline.schema.json +160 -0
- package/skills/archify/schemas/tree.schema.json +141 -0
- package/skills/archify/schemas/waterfall.schema.json +147 -0
- package/skills/archify/schemas/workflow.schema.json +439 -0
- package/skills/archify/scripts/check-render-output.mjs +1469 -0
- package/skills/archify/scripts/check-update.mjs +1717 -0
- package/skills/archify/scripts/delivery-update-child.mjs +26 -0
- package/skills/archify/scripts/render-examples.mjs +36 -0
- package/skills/archify/scripts/update-contract.mjs +187 -0
- package/skills/archify/skill-release.json +10 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Single-line node text fitting, shared by every renderer.
|
|
2
|
+
//
|
|
3
|
+
// Node text (`label`, `sublabel`, `tag`) renders as one <text> element with
|
|
4
|
+
// text-anchor="middle" and is never wrapped. Left unmeasured, an over-long
|
|
5
|
+
// value silently spills across its neighbours while validation still reports
|
|
6
|
+
// a clean receipt — the failure mode this module exists to close.
|
|
7
|
+
//
|
|
8
|
+
// Two halves, always used together:
|
|
9
|
+
// - fittedNodeFontSize shrinks the text toward a legible minimum at render
|
|
10
|
+
// time, so ordinary overruns simply get smaller instead of overlapping.
|
|
11
|
+
// - minimumNodeTextWidth reports the width the text still needs once it has
|
|
12
|
+
// shrunk as far as it may, so validation can reject what shrinking cannot
|
|
13
|
+
// save.
|
|
14
|
+
//
|
|
15
|
+
// The geometry constants below are shared; the per-field `preferred` and
|
|
16
|
+
// `minimum` font sizes are not, because renderers set node text at different
|
|
17
|
+
// sizes (architecture sublabels are 9px, the rest are 7px).
|
|
18
|
+
|
|
19
|
+
import { textUnits, SEMANTIC_SIGIL_INSET, SEMANTIC_SIGIL_SIZE, SEMANTIC_SIGIL_FOOTPRINT, SOURCE_BADGE_FOOTPRINT } from './utils.mjs';
|
|
20
|
+
|
|
21
|
+
// widthFactor: px of advance width per text unit, per px of font size.
|
|
22
|
+
// horizontalPadding: total px reserved inside the box so text never touches
|
|
23
|
+
// the border.
|
|
24
|
+
export const nodeTextFit = {
|
|
25
|
+
widthFactor: 0.6,
|
|
26
|
+
horizontalPadding: 8,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Largest font size at or below `preferred` that fits `text` inside `width`,
|
|
30
|
+
// floored at `minimum` — below that the text is no longer legible and the
|
|
31
|
+
// caller should be reporting a problem instead.
|
|
32
|
+
export function fittedNodeFontSize(text, width, preferred, minimum) {
|
|
33
|
+
const units = Math.max(1, textUnits(text));
|
|
34
|
+
const available = Math.max(1, width - nodeTextFit.horizontalPadding);
|
|
35
|
+
const fitted = Math.min(preferred, available / (units * nodeTextFit.widthFactor));
|
|
36
|
+
return Math.max(minimum, Math.floor(fitted * 10) / 10);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Width `text` occupies at its legible minimum. Compare against
|
|
40
|
+
// `width - nodeTextFit.horizontalPadding` to decide whether shrink-to-fit can
|
|
41
|
+
// rescue it.
|
|
42
|
+
export function minimumNodeTextWidth(text, minimum) {
|
|
43
|
+
return textUnits(text) * minimum * nodeTextFit.widthFactor;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Available text width inside a box of `width`.
|
|
47
|
+
export function availableNodeTextWidth(width) {
|
|
48
|
+
return width - nodeTextFit.horizontalPadding;
|
|
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
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml as esc,
|
|
3
|
+
localizeTemplate,
|
|
4
|
+
resolveLocale,
|
|
5
|
+
translateMessage,
|
|
6
|
+
viewerCatalog,
|
|
7
|
+
} from './i18n.mjs';
|
|
8
|
+
|
|
9
|
+
export { esc };
|
|
10
|
+
|
|
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 = '') {
|
|
15
|
+
return ` <!-- Definitions -->
|
|
16
|
+
<defs>
|
|
17
|
+
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
18
|
+
<polygon points="0 0, 10 3.5, 0 7" class="m-default" />
|
|
19
|
+
</marker>
|
|
20
|
+
<marker id="arrowhead-emphasis" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
21
|
+
<polygon points="0 0, 10 3.5, 0 7" class="m-emphasis" />
|
|
22
|
+
</marker>
|
|
23
|
+
<marker id="arrowhead-security" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
24
|
+
<polygon points="0 0, 10 3.5, 0 7" class="m-security" />
|
|
25
|
+
</marker>
|
|
26
|
+
<marker id="arrowhead-dashed" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
27
|
+
<polygon points="0 0, 10 3.5, 0 7" class="m-dashed" />
|
|
28
|
+
</marker>
|
|
29
|
+
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
30
|
+
<path d="M 40 0 L 0 0 0 40" class="c-grid" stroke-width="0.5"/>
|
|
31
|
+
</pattern>${extra}
|
|
32
|
+
</defs>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SIGIL_TONE = {
|
|
36
|
+
frontend: 'frontend',
|
|
37
|
+
start: 'frontend',
|
|
38
|
+
backend: 'backend',
|
|
39
|
+
active: 'frontend',
|
|
40
|
+
database: 'database',
|
|
41
|
+
success: 'backend',
|
|
42
|
+
cloud: 'cloud',
|
|
43
|
+
waiting: 'cloud',
|
|
44
|
+
security: 'security',
|
|
45
|
+
decision: 'database',
|
|
46
|
+
failure: 'security',
|
|
47
|
+
messagebus: 'messagebus',
|
|
48
|
+
external: 'external',
|
|
49
|
+
neutral: 'external',
|
|
50
|
+
};
|
|
51
|
+
|
|
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"/>`,
|
|
59
|
+
frontend: `<rect x="2" y="3" width="12" height="10" rx="2"/>
|
|
60
|
+
<path d="M2 6.5h12"/>
|
|
61
|
+
<circle cx="4.1" cy="4.8" r=".7" class="sigil-fill"/>
|
|
62
|
+
<circle cx="6.3" cy="4.8" r=".7" class="sigil-fill"/>`,
|
|
63
|
+
backend: `<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>`,
|
|
64
|
+
database: `<ellipse cx="8" cy="4" rx="5" ry="2"/>
|
|
65
|
+
<path d="M3 4v8c0 1.1 2.2 2 5 2s5-.9 5-2V4M3 8c0 1.1 2.2 2 5 2s5-.9 5-2"/>`,
|
|
66
|
+
cloud: `<path d="M4.3 12.5h7.3a2.4 2.4 0 0 0 .2-4.8 4 4 0 0 0-7.5-1.3A3.1 3.1 0 0 0 4.3 12.5Z"/>`,
|
|
67
|
+
security: `<path d="M8 2.2 13 4v3.5c0 3.1-1.8 5.4-5 6.5-3.2-1.1-5-3.4-5-6.5V4Z"/>
|
|
68
|
+
<path d="m5.8 8 1.5 1.5 3-3"/>`,
|
|
69
|
+
messagebus: `<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>
|
|
70
|
+
<circle cx="5" cy="4.5" r="1" class="sigil-fill"/>
|
|
71
|
+
<circle cx="10.5" cy="8" r="1" class="sigil-fill"/>
|
|
72
|
+
<circle cx="7" cy="11.5" r="1" class="sigil-fill"/>`,
|
|
73
|
+
external: `<rect x="2.5" y="5" width="8.5" height="8" rx="1.5"/>
|
|
74
|
+
<path d="M8 2.5h5.5V8M13.5 2.5 7.5 8.5"/>`,
|
|
75
|
+
start: `<circle cx="8" cy="8" r="5"/>
|
|
76
|
+
<path d="m7 5.4 3.6 2.6L7 10.6Z" class="sigil-fill"/>`,
|
|
77
|
+
active: `<path d="M2 8h3l1.5-3.5L9 12l1.6-4H14"/>`,
|
|
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"/>`,
|
|
80
|
+
success: `<circle cx="8" cy="8" r="5.3"/>
|
|
81
|
+
<path d="m5.2 8 1.8 1.8 3.8-4"/>`,
|
|
82
|
+
failure: `<circle cx="8" cy="8" r="5.3"/>
|
|
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"/>`,
|
|
86
|
+
neutral: `<rect x="3" y="3" width="10" height="10" rx="2"/>
|
|
87
|
+
<circle cx="8" cy="8" r="1.2" class="sigil-fill"/>`,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// A quiet, renderer-owned corner symbol (type default or authored icon). It is
|
|
91
|
+
// SVG content rather than a
|
|
92
|
+
// viewer overlay, so it survives canonical export while adding no focus target,
|
|
93
|
+
// accessible name, layout box, or interaction state of its own.
|
|
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';
|
|
108
|
+
const scale = size / 16;
|
|
109
|
+
return `<g aria-hidden="true" data-semantic-sigil="${esc(normalized)}" class="semantic-sigil s-${tone}" transform="translate(${x} ${y}) scale(${scale})">
|
|
110
|
+
${SIGIL_SHAPE[normalized]}
|
|
111
|
+
</g>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderCards(cards) {
|
|
115
|
+
const list = Array.isArray(cards) ? cards : [];
|
|
116
|
+
return ` <!-- Info Cards -->
|
|
117
|
+
<div class="cards">
|
|
118
|
+
${list.map((card) => ` <div class="card">
|
|
119
|
+
<div class="card-header">
|
|
120
|
+
<div class="card-dot ${esc(card.dot)}"></div>
|
|
121
|
+
<h3>${esc(card.title)}</h3>
|
|
122
|
+
</div>
|
|
123
|
+
<ul>
|
|
124
|
+
${card.items.map((item) => ` <li>${esc(item)}</li>`).join('\n')}
|
|
125
|
+
</ul>
|
|
126
|
+
</div>`).join('\n\n')}
|
|
127
|
+
</div>`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const SVG_SLOT_RE = / <!-- ARCHIFY:SVG_SLOT_START -->[\s\S]*? <!-- ARCHIFY:SVG_SLOT_END -->/;
|
|
131
|
+
const CARDS_SLOT_RE = / <!-- ARCHIFY:CARDS_SLOT_START -->[\s\S]*? <!-- ARCHIFY:CARDS_SLOT_END -->/;
|
|
132
|
+
const SUBTITLE_SLOT_RE = /^([ \t]*)<p class="subtitle">\[Subtitle description\]<\/p>[ \t]*(\r?\n)?/m;
|
|
133
|
+
const SOURCE_EVIDENCE_PLACEHOLDER = ' <!-- ARCHIFY:SOURCE_EVIDENCE_DATA -->';
|
|
134
|
+
const I18N_PLACEHOLDER = ' <!-- ARCHIFY:I18N_DATA -->';
|
|
135
|
+
|
|
136
|
+
function serializeScriptJson(value) {
|
|
137
|
+
return JSON.stringify(value)
|
|
138
|
+
.replaceAll('<', '\\u003c')
|
|
139
|
+
.replaceAll('>', '\\u003e')
|
|
140
|
+
.replaceAll('&', '\\u0026');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const TEMPLATE_PLACEHOLDERS = [
|
|
144
|
+
'<html lang="en" data-theme="dark" data-preset="[VISUAL PRESET]">',
|
|
145
|
+
'<title>[PROJECT NAME] Architecture Diagram</title>',
|
|
146
|
+
'<h1>[PROJECT NAME] Architecture</h1>',
|
|
147
|
+
I18N_PLACEHOLDER,
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
export function applyTemplate(template, {
|
|
151
|
+
title,
|
|
152
|
+
subtitle,
|
|
153
|
+
svg,
|
|
154
|
+
cards,
|
|
155
|
+
locale,
|
|
156
|
+
visualPreset = 'classic',
|
|
157
|
+
sourceEvidence = null,
|
|
158
|
+
}) {
|
|
159
|
+
if (!SVG_SLOT_RE.test(template)) {
|
|
160
|
+
throw new Error('applyTemplate: template missing ARCHIFY:SVG_SLOT sentinel');
|
|
161
|
+
}
|
|
162
|
+
if (!CARDS_SLOT_RE.test(template)) {
|
|
163
|
+
throw new Error('applyTemplate: template missing ARCHIFY:CARDS_SLOT sentinel');
|
|
164
|
+
}
|
|
165
|
+
if (!SUBTITLE_SLOT_RE.test(template)) {
|
|
166
|
+
throw new Error('applyTemplate: template missing subtitle placeholder');
|
|
167
|
+
}
|
|
168
|
+
for (const ph of TEMPLATE_PLACEHOLDERS) {
|
|
169
|
+
if (!template.includes(ph)) {
|
|
170
|
+
throw new Error(`applyTemplate: template missing placeholder ${JSON.stringify(ph)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Keep existing custom templates compatible when evidence is not requested.
|
|
174
|
+
// Silently dropping verified evidence would be misleading, so the new slot
|
|
175
|
+
// becomes mandatory only for the opt-in evidence path.
|
|
176
|
+
if (sourceEvidence && !template.includes(SOURCE_EVIDENCE_PLACEHOLDER)) {
|
|
177
|
+
throw new Error(`applyTemplate: repository evidence requires placeholder ${JSON.stringify(SOURCE_EVIDENCE_PLACEHOLDER)}`);
|
|
178
|
+
}
|
|
179
|
+
// Function replacers: a literal `$&`, `$'`, `$\`` or `$$` in titles, labels,
|
|
180
|
+
// or rendered SVG must not be interpreted as a replacement pattern.
|
|
181
|
+
const sourceEvidenceJson = serializeScriptJson(sourceEvidence);
|
|
182
|
+
const resolvedLocale = resolveLocale(locale);
|
|
183
|
+
const i18nJson = serializeScriptJson({ locale: resolvedLocale, messages: viewerCatalog(resolvedLocale) });
|
|
184
|
+
const renderedSubtitle = typeof subtitle === 'string' && subtitle.trim()
|
|
185
|
+
? `<p class="subtitle">${esc(subtitle)}</p>`
|
|
186
|
+
: '';
|
|
187
|
+
const i18nData = ` <script id="archify-i18n-data" type="application/json">${i18nJson}</script>`;
|
|
188
|
+
return localizeTemplate(template, resolvedLocale)
|
|
189
|
+
.replace(I18N_PLACEHOLDER, () => i18nData)
|
|
190
|
+
.replace(TEMPLATE_PLACEHOLDERS[0], () => `<html lang="${esc(resolvedLocale)}" data-theme="dark" data-preset="${esc(visualPreset)}">`)
|
|
191
|
+
.replace(TEMPLATE_PLACEHOLDERS[1], () => `<title>${esc(translateMessage(resolvedLocale, 'page.title', { title }))}</title>`)
|
|
192
|
+
.replace(TEMPLATE_PLACEHOLDERS[2], () => `<h1>${esc(title)}</h1>`)
|
|
193
|
+
.replace(SUBTITLE_SLOT_RE, (_match, indent, newline = '') => renderedSubtitle
|
|
194
|
+
? `${indent}${renderedSubtitle}${newline}`
|
|
195
|
+
: '')
|
|
196
|
+
.replace(SVG_SLOT_RE, () => svg)
|
|
197
|
+
.replace(CARDS_SLOT_RE, () => cards)
|
|
198
|
+
.replace(SOURCE_EVIDENCE_PLACEHOLDER, () => sourceEvidence
|
|
199
|
+
? ` <script id="archify-source-evidence-data" type="application/json">${sourceEvidenceJson}</script>`
|
|
200
|
+
: '');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// CJK and other wide/fullwidth glyphs render at roughly twice the advance
|
|
204
|
+
// width of ASCII in the monospace stacks the template uses. Keep halfwidth
|
|
205
|
+
// forms (notably U+FF61–U+FF9F Katakana) out of this set. The explicit ranges
|
|
206
|
+
// also cover vertical punctuation and supplementary East Asian scripts that
|
|
207
|
+
// literal glyph ranges made difficult to audit.
|
|
208
|
+
// Code points that take two columns of advance width: East Asian Wide and
|
|
209
|
+
// Fullwidth per UAX #11, tracking Unicode 17.0. That takes in the BMP symbols
|
|
210
|
+
// carrying emoji presentation (U+2705, U+2B50, U+26A1, U+231B, ...), which
|
|
211
|
+
// render at the same square advance as the supplementary-plane emoji already
|
|
212
|
+
// listed here, and Hangul Jamo Extended-A. Two boundary calls worth naming:
|
|
213
|
+
// Unicode 16.0 reclassified the trigrams (U+2630-U+2637) and the monogram /
|
|
214
|
+
// digram symbols (U+268A-U+268F) from Neutral to Wide, so both are in; and
|
|
215
|
+
// Hangul Jamo Extended-A stops at U+A97C, its last assigned jamo, because
|
|
216
|
+
// U+A97D-U+A97F are unassigned, and unassigned code points outside the CJK
|
|
217
|
+
// ranges UAX #11 names default to Neutral rather than Wide. Spelled out as
|
|
218
|
+
// ranges because V8 has no \p{East_Asian_Width=W} property escape.
|
|
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;
|
|
220
|
+
|
|
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.
|
|
229
|
+
const VARIATION_SELECTOR_FIRST = 0xfe00;
|
|
230
|
+
const VARIATION_SELECTOR_LAST = 0xfe0f;
|
|
231
|
+
const VARIATION_SELECTOR_EMOJI = 0xfe0f;
|
|
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
|
+
|
|
239
|
+
export function textUnits(text) {
|
|
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);
|
|
244
|
+
let units = 0;
|
|
245
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
246
|
+
const codePoint = chars[i].codePointAt(0);
|
|
247
|
+
if (codePoint >= VARIATION_SELECTOR_FIRST && codePoint <= VARIATION_SELECTOR_LAST) continue;
|
|
248
|
+
const next = i + 1 < chars.length ? chars[i + 1].codePointAt(0) : -1;
|
|
249
|
+
if (next === VARIATION_SELECTOR_EMOJI) units += 2;
|
|
250
|
+
else units += FULLWIDTH_RE.test(chars[i]) ? 2 : 1;
|
|
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
|
+
}
|
|
259
|
+
return units;
|
|
260
|
+
}
|