@diagrammo/dgmo 0.25.2 → 0.25.4
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 +69 -884
- package/package.json +1 -1
- package/src/cli.ts +0 -9
- package/src/d3.ts +180 -46
- package/src/render.ts +80 -39
- package/src/utils/svg-embed.ts +12 -3
- package/dist/advanced.cjs +0 -62454
- package/dist/advanced.d.cts +0 -5684
- package/dist/advanced.d.ts +0 -5684
- package/dist/advanced.js +0 -62207
- package/dist/auto.cjs +0 -59764
- package/dist/auto.css +0 -214
- package/dist/auto.d.cts +0 -39
- package/dist/auto.d.ts +0 -39
- package/dist/auto.js +0 -437
- package/dist/auto.mjs +0 -59774
- package/dist/cli.cjs +0 -465
- package/dist/editor.cjs +0 -432
- package/dist/editor.d.cts +0 -26
- package/dist/editor.d.ts +0 -26
- package/dist/editor.js +0 -401
- package/dist/highlight.cjs +0 -720
- package/dist/highlight.d.cts +0 -32
- package/dist/highlight.d.ts +0 -32
- package/dist/highlight.js +0 -690
- package/dist/index.cjs +0 -58947
- package/dist/index.d.cts +0 -375
- package/dist/index.d.ts +0 -375
- package/dist/index.js +0 -58951
- package/dist/internal.cjs +0 -62456
- package/dist/internal.d.cts +0 -5684
- package/dist/internal.d.ts +0 -5684
- package/dist/internal.js +0 -62207
- package/dist/map-data/PROVENANCE.json +0 -1
- package/dist/map-data/gazetteer.json +0 -1
- package/dist/map-data/lakes.json +0 -1
- package/dist/map-data/mountain-ranges.json +0 -1
- package/dist/map-data/na-lakes.json +0 -1
- package/dist/map-data/na-land.json +0 -1
- package/dist/map-data/region-names.json +0 -1
- package/dist/map-data/rivers.json +0 -1
- package/dist/map-data/us-states.json +0 -1
- package/dist/map-data/water-bodies.json +0 -1
- package/dist/map-data/world-coarse.json +0 -1
- package/dist/map-data/world-detail.json +0 -1
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -554
- package/dist/pert.d.ts +0 -554
- package/dist/pert.js +0 -294
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.js +0 -35
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1506,15 +1506,6 @@ async function main(): Promise<void> {
|
|
|
1506
1506
|
opts.theme === 'dark' ? 'dark' : 'light'
|
|
1507
1507
|
];
|
|
1508
1508
|
|
|
1509
|
-
// Word clouds require Canvas APIs (HTMLCanvasElement.getContext('2d'))
|
|
1510
|
-
// which are unavailable in Node.js — check before attempting render.
|
|
1511
|
-
const wordcloudRe = /^\s*chart\s*:\s*wordcloud\b/im;
|
|
1512
|
-
if (wordcloudRe.test(content)) {
|
|
1513
|
-
exitWithJsonError(
|
|
1514
|
-
'Error: Word clouds are not supported in the CLI (requires Canvas). Use the desktop app or browser instead.'
|
|
1515
|
-
);
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
1509
|
// Parse first to collect diagnostics
|
|
1519
1510
|
const { diagnostics } = parseDgmo(content);
|
|
1520
1511
|
const errors = diagnostics.filter((d) => d.severity === 'error');
|
package/src/d3.ts
CHANGED
|
@@ -5771,6 +5771,106 @@ function getRotateFn(mode: WordCloudRotate): () => number {
|
|
|
5771
5771
|
return () => 0;
|
|
5772
5772
|
}
|
|
5773
5773
|
|
|
5774
|
+
/**
|
|
5775
|
+
* d3-cloud rasterizes each glyph to a canvas sprite for pixel-perfect
|
|
5776
|
+
* collision detection. In headless Node (jsdom), `getContext('2d')` returns
|
|
5777
|
+
* null, so d3-cloud throws (`getImageData` on null). This detects whether a
|
|
5778
|
+
* usable 2D canvas exists; when it doesn't, we fall back to a canvas-free
|
|
5779
|
+
* spiral packer so word clouds still render in SSG (remark wrappers), the MCP
|
|
5780
|
+
* server, and the CLI.
|
|
5781
|
+
*/
|
|
5782
|
+
function hasCanvas2d(): boolean {
|
|
5783
|
+
try {
|
|
5784
|
+
if (typeof document === 'undefined') return false;
|
|
5785
|
+
const canvas = document.createElement('canvas');
|
|
5786
|
+
return typeof canvas.getContext === 'function' && !!canvas.getContext('2d');
|
|
5787
|
+
} catch {
|
|
5788
|
+
return false;
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5792
|
+
/** Average glyph advance for Inter as a fraction of the font size. Slightly
|
|
5793
|
+
* generous so estimated boxes err toward not overlapping. */
|
|
5794
|
+
const WORDCLOUD_GLYPH_ADVANCE = 0.62;
|
|
5795
|
+
|
|
5796
|
+
function estimateWordWidth(text: string, size: number): number {
|
|
5797
|
+
return text.length * size * WORDCLOUD_GLYPH_ADVANCE;
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
type PlacedCloudWord = WordCloudWord & {
|
|
5801
|
+
size: number;
|
|
5802
|
+
x: number;
|
|
5803
|
+
y: number;
|
|
5804
|
+
rotate: number;
|
|
5805
|
+
};
|
|
5806
|
+
|
|
5807
|
+
/**
|
|
5808
|
+
* Canvas-free word-cloud layout. Places the largest words first at the centre
|
|
5809
|
+
* and walks an Archimedean spiral outward, using axis-aligned bounding-box
|
|
5810
|
+
* overlap tests (text width estimated from font metrics). Returns words in
|
|
5811
|
+
* placement order with `x`/`y` relative to the cloud centre — the same shape
|
|
5812
|
+
* d3-cloud hands to its `end` callback — so the existing draw code is reused.
|
|
5813
|
+
* Words that can't be placed within the box are dropped, matching d3-cloud.
|
|
5814
|
+
*/
|
|
5815
|
+
function layoutWordsNoCanvas(
|
|
5816
|
+
words: Array<WordCloudWord & { size: number }>,
|
|
5817
|
+
width: number,
|
|
5818
|
+
height: number,
|
|
5819
|
+
padding: number,
|
|
5820
|
+
rotateFn: () => number
|
|
5821
|
+
): PlacedCloudWord[] {
|
|
5822
|
+
const sorted = [...words].sort((a, b) => b.size - a.size);
|
|
5823
|
+
const placed: Array<PlacedCloudWord & { halfW: number; halfH: number }> = [];
|
|
5824
|
+
const maxR = Math.sqrt(width * width + height * height) / 2;
|
|
5825
|
+
// Bias the spiral to the box aspect so wide clouds spread horizontally.
|
|
5826
|
+
const aspect = width > 0 ? height / width : 1;
|
|
5827
|
+
|
|
5828
|
+
for (const w of sorted) {
|
|
5829
|
+
const rotate = rotateFn();
|
|
5830
|
+
const rawW = estimateWordWidth(w.text, w.size) + padding * 2;
|
|
5831
|
+
const rawH = w.size + padding * 2;
|
|
5832
|
+
const rad = (rotate * Math.PI) / 180;
|
|
5833
|
+
const cos = Math.abs(Math.cos(rad));
|
|
5834
|
+
const sin = Math.abs(Math.sin(rad));
|
|
5835
|
+
const halfW = (rawW * cos + rawH * sin) / 2;
|
|
5836
|
+
const halfH = (rawW * sin + rawH * cos) / 2;
|
|
5837
|
+
|
|
5838
|
+
let spot: { x: number; y: number } | null = null;
|
|
5839
|
+
for (let t = 0; t < 4000; t++) {
|
|
5840
|
+
const a = t * 0.25;
|
|
5841
|
+
const r = a * 1.4;
|
|
5842
|
+
if (r > maxR) break;
|
|
5843
|
+
const x = Math.cos(a) * r;
|
|
5844
|
+
const y = Math.sin(a) * r * aspect;
|
|
5845
|
+
if (
|
|
5846
|
+
x - halfW < -width / 2 ||
|
|
5847
|
+
x + halfW > width / 2 ||
|
|
5848
|
+
y - halfH < -height / 2 ||
|
|
5849
|
+
y + halfH > height / 2
|
|
5850
|
+
) {
|
|
5851
|
+
continue;
|
|
5852
|
+
}
|
|
5853
|
+
let collides = false;
|
|
5854
|
+
for (const p of placed) {
|
|
5855
|
+
if (
|
|
5856
|
+
Math.abs(x - p.x) < halfW + p.halfW &&
|
|
5857
|
+
Math.abs(y - p.y) < halfH + p.halfH
|
|
5858
|
+
) {
|
|
5859
|
+
collides = true;
|
|
5860
|
+
break;
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
if (!collides) {
|
|
5864
|
+
spot = { x, y };
|
|
5865
|
+
break;
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
if (!spot) continue;
|
|
5869
|
+
placed.push({ ...w, rotate, x: spot.x, y: spot.y, halfW, halfH });
|
|
5870
|
+
}
|
|
5871
|
+
return placed;
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5774
5874
|
// ============================================================
|
|
5775
5875
|
// Word Cloud Renderer
|
|
5776
5876
|
// ============================================================
|
|
@@ -5839,40 +5939,55 @@ export function renderWordCloud(
|
|
|
5839
5939
|
`translate(${width / 2},${sTitleHeight + cloudHeight / 2})`
|
|
5840
5940
|
);
|
|
5841
5941
|
|
|
5942
|
+
const sized = words.map((w) => ({ ...w, size: fontSize(w.weight) }));
|
|
5943
|
+
|
|
5944
|
+
const draw = (
|
|
5945
|
+
layoutWords: Array<{
|
|
5946
|
+
text?: string;
|
|
5947
|
+
size?: number;
|
|
5948
|
+
x?: number;
|
|
5949
|
+
y?: number;
|
|
5950
|
+
rotate?: number;
|
|
5951
|
+
}>
|
|
5952
|
+
): void => {
|
|
5953
|
+
g.selectAll('text')
|
|
5954
|
+
.data(layoutWords)
|
|
5955
|
+
.join('text')
|
|
5956
|
+
.style('font-size', (d) => `${d.size}px`)
|
|
5957
|
+
.style('font-family', FONT_FAMILY)
|
|
5958
|
+
.style('font-weight', '600')
|
|
5959
|
+
// colors is non-empty; modulo guarantees in-bounds.
|
|
5960
|
+
.style('fill', (_d, i) => colors[i % colors.length]!)
|
|
5961
|
+
.style('cursor', (d) =>
|
|
5962
|
+
onClickItem && (d as WordCloudWord).lineNumber ? 'pointer' : 'default'
|
|
5963
|
+
)
|
|
5964
|
+
.attr('text-anchor', 'middle')
|
|
5965
|
+
.attr('transform', (d) => `translate(${d.x},${d.y}) rotate(${d.rotate})`)
|
|
5966
|
+
.attr('data-line-number', (d) => {
|
|
5967
|
+
const ln = (d as WordCloudWord).lineNumber;
|
|
5968
|
+
return ln ? String(ln) : null;
|
|
5969
|
+
})
|
|
5970
|
+
.text((d) => d.text!)
|
|
5971
|
+
.on('click', (_event, d) => {
|
|
5972
|
+
const ln = (d as WordCloudWord).lineNumber;
|
|
5973
|
+
if (onClickItem && ln) onClickItem(ln);
|
|
5974
|
+
});
|
|
5975
|
+
};
|
|
5976
|
+
|
|
5977
|
+
// No real 2D canvas (headless Node) → fall back to the spiral packer.
|
|
5978
|
+
if (!hasCanvas2d()) {
|
|
5979
|
+
draw(layoutWordsNoCanvas(sized, width, cloudHeight, sPadding, rotateFn));
|
|
5980
|
+
return;
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5842
5983
|
cloud<WordCloudWord & cloud.Word>()
|
|
5843
5984
|
.size([width, cloudHeight])
|
|
5844
|
-
.words(
|
|
5985
|
+
.words(sized)
|
|
5845
5986
|
.padding(sPadding)
|
|
5846
5987
|
.rotate(rotateFn)
|
|
5847
5988
|
.fontSize((d) => d.size!)
|
|
5848
5989
|
.font(FONT_FAMILY)
|
|
5849
|
-
.on('end',
|
|
5850
|
-
g.selectAll('text')
|
|
5851
|
-
.data(layoutWords)
|
|
5852
|
-
.join('text')
|
|
5853
|
-
.style('font-size', (d) => `${d.size}px`)
|
|
5854
|
-
.style('font-family', FONT_FAMILY)
|
|
5855
|
-
.style('font-weight', '600')
|
|
5856
|
-
// colors is non-empty; modulo guarantees in-bounds.
|
|
5857
|
-
.style('fill', (_d, i) => colors[i % colors.length]!)
|
|
5858
|
-
.style('cursor', (d) =>
|
|
5859
|
-
onClickItem && (d as WordCloudWord).lineNumber ? 'pointer' : 'default'
|
|
5860
|
-
)
|
|
5861
|
-
.attr('text-anchor', 'middle')
|
|
5862
|
-
.attr(
|
|
5863
|
-
'transform',
|
|
5864
|
-
(d) => `translate(${d.x},${d.y}) rotate(${d.rotate})`
|
|
5865
|
-
)
|
|
5866
|
-
.attr('data-line-number', (d) => {
|
|
5867
|
-
const ln = (d as WordCloudWord).lineNumber;
|
|
5868
|
-
return ln ? String(ln) : null;
|
|
5869
|
-
})
|
|
5870
|
-
.text((d) => d.text!)
|
|
5871
|
-
.on('click', (_event, d) => {
|
|
5872
|
-
const ln = (d as WordCloudWord).lineNumber;
|
|
5873
|
-
if (onClickItem && ln) onClickItem(ln);
|
|
5874
|
-
});
|
|
5875
|
-
})
|
|
5990
|
+
.on('end', draw)
|
|
5876
5991
|
.start();
|
|
5877
5992
|
}
|
|
5878
5993
|
|
|
@@ -5942,30 +6057,49 @@ function renderWordCloudAsync(
|
|
|
5942
6057
|
`translate(${width / 2},${titleHeight + cloudHeight / 2})`
|
|
5943
6058
|
);
|
|
5944
6059
|
|
|
6060
|
+
const sized = words.map((w) => ({ ...w, size: fontSize(w.weight) }));
|
|
6061
|
+
|
|
6062
|
+
const draw = (
|
|
6063
|
+
layoutWords: Array<{
|
|
6064
|
+
text?: string;
|
|
6065
|
+
size?: number;
|
|
6066
|
+
x?: number;
|
|
6067
|
+
y?: number;
|
|
6068
|
+
rotate?: number;
|
|
6069
|
+
}>
|
|
6070
|
+
): void => {
|
|
6071
|
+
g.selectAll('text')
|
|
6072
|
+
.data(layoutWords)
|
|
6073
|
+
.join('text')
|
|
6074
|
+
.style('font-size', (d) => `${d.size}px`)
|
|
6075
|
+
.style('font-family', FONT_FAMILY)
|
|
6076
|
+
.style('font-weight', '600')
|
|
6077
|
+
// colors is non-empty; modulo guarantees in-bounds.
|
|
6078
|
+
.style('fill', (_d, i) => colors[i % colors.length]!)
|
|
6079
|
+
.attr('text-anchor', 'middle')
|
|
6080
|
+
.attr(
|
|
6081
|
+
'transform',
|
|
6082
|
+
(d) => `translate(${d.x},${d.y}) rotate(${d.rotate})`
|
|
6083
|
+
)
|
|
6084
|
+
.text((d) => d.text!);
|
|
6085
|
+
resolve();
|
|
6086
|
+
};
|
|
6087
|
+
|
|
6088
|
+
// No real 2D canvas (headless Node: SSG wrappers, MCP, CLI) → d3-cloud's
|
|
6089
|
+
// sprite rasterization can't run. Use the canvas-free spiral packer.
|
|
6090
|
+
if (!hasCanvas2d()) {
|
|
6091
|
+
draw(layoutWordsNoCanvas(sized, width, cloudHeight, 2, rotateFn));
|
|
6092
|
+
return;
|
|
6093
|
+
}
|
|
6094
|
+
|
|
5945
6095
|
cloud<WordCloudWord & cloud.Word>()
|
|
5946
6096
|
.size([width, cloudHeight])
|
|
5947
|
-
.words(
|
|
6097
|
+
.words(sized)
|
|
5948
6098
|
.padding(2)
|
|
5949
6099
|
.rotate(rotateFn)
|
|
5950
6100
|
.fontSize((d) => d.size!)
|
|
5951
6101
|
.font(FONT_FAMILY)
|
|
5952
|
-
.on('end',
|
|
5953
|
-
g.selectAll('text')
|
|
5954
|
-
.data(layoutWords)
|
|
5955
|
-
.join('text')
|
|
5956
|
-
.style('font-size', (d) => `${d.size}px`)
|
|
5957
|
-
.style('font-family', FONT_FAMILY)
|
|
5958
|
-
.style('font-weight', '600')
|
|
5959
|
-
// colors is non-empty; modulo guarantees in-bounds.
|
|
5960
|
-
.style('fill', (_d, i) => colors[i % colors.length]!)
|
|
5961
|
-
.attr('text-anchor', 'middle')
|
|
5962
|
-
.attr(
|
|
5963
|
-
'transform',
|
|
5964
|
-
(d) => `translate(${d.x},${d.y}) rotate(${d.rotate})`
|
|
5965
|
-
)
|
|
5966
|
-
.text((d) => d.text!);
|
|
5967
|
-
resolve();
|
|
5968
|
-
})
|
|
6102
|
+
.on('end', draw)
|
|
5969
6103
|
.start();
|
|
5970
6104
|
});
|
|
5971
6105
|
}
|
package/src/render.ts
CHANGED
|
@@ -5,45 +5,81 @@ import type { DgmoError } from './diagnostics';
|
|
|
5
5
|
import { getPalette } from './palettes/registry';
|
|
6
6
|
import type { CompactViewState } from './sharing';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
// DOM globals installed for Node-side D3 rendering, scoped with ref-counting.
|
|
9
|
+
//
|
|
10
|
+
// These need to exist on `globalThis` while a D3 renderer runs (it reaches for
|
|
11
|
+
// `document`). The naive approach — install them once and leave them — leaks a
|
|
12
|
+
// jsdom `window` into the host Node process forever. That breaks hosts that run
|
|
13
|
+
// their OWN SSR/SSG in the same process after calling render(): notably
|
|
14
|
+
// Docusaurus static export, whose theme then believes it is in a browser
|
|
15
|
+
// (`canUseDOM` true) and crashes on bare globals this shim does NOT define
|
|
16
|
+
// (`requestAnimationFrame`, `MutationObserver`) or on opaque-origin
|
|
17
|
+
// `localStorage`. So we install on the first concurrent render and tear down
|
|
18
|
+
// once the last one finishes, leaving the host a clean Node environment.
|
|
19
|
+
const DOM_GLOBALS = [
|
|
20
|
+
'document',
|
|
21
|
+
'window',
|
|
22
|
+
'navigator',
|
|
23
|
+
'HTMLElement',
|
|
24
|
+
'SVGElement',
|
|
25
|
+
] as const;
|
|
26
|
+
let domRefCount = 0;
|
|
27
|
+
let domInstallPromise: Promise<void> | null = null;
|
|
28
|
+
let domInstalledByUs = false;
|
|
15
29
|
|
|
30
|
+
async function installDom(): Promise<void> {
|
|
16
31
|
const { JSDOM } = await loadJsdom();
|
|
17
|
-
|
|
32
|
+
// Concrete URL → non-opaque origin, so host code that touches
|
|
33
|
+
// window.localStorage during a same-process render doesn't throw.
|
|
34
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
35
|
+
url: 'http://localhost/',
|
|
36
|
+
});
|
|
18
37
|
const win = dom.window;
|
|
38
|
+
const values: Record<(typeof DOM_GLOBALS)[number], unknown> = {
|
|
39
|
+
document: win.document,
|
|
40
|
+
window: win,
|
|
41
|
+
navigator: win.navigator,
|
|
42
|
+
HTMLElement: win.HTMLElement,
|
|
43
|
+
SVGElement: win.SVGElement,
|
|
44
|
+
};
|
|
45
|
+
for (const key of DOM_GLOBALS) {
|
|
46
|
+
Object.defineProperty(globalThis, key, {
|
|
47
|
+
value: values[key],
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
domInstalledByUs = true;
|
|
52
|
+
}
|
|
19
53
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Make DOM globals available for the duration of a render. No-ops in a real
|
|
56
|
+
* browser or any host that already provides `document` (we never touch globals
|
|
57
|
+
* we did not install). Pair every successful call with `releaseDom()`.
|
|
58
|
+
*/
|
|
59
|
+
async function acquireDom(): Promise<void> {
|
|
60
|
+
if (typeof document !== 'undefined' && !domInstalledByUs) return;
|
|
61
|
+
domRefCount++;
|
|
62
|
+
if (!domInstallPromise) domInstallPromise = installDom();
|
|
63
|
+
await domInstallPromise;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Tear down the jsdom globals once no render is in flight. */
|
|
67
|
+
function releaseDom(): void {
|
|
68
|
+
if (!domInstalledByUs) return;
|
|
69
|
+
if (--domRefCount > 0) return;
|
|
70
|
+
for (const key of DOM_GLOBALS) {
|
|
71
|
+
delete (globalThis as Record<string, unknown>)[key];
|
|
72
|
+
}
|
|
73
|
+
domInstalledByUs = false;
|
|
74
|
+
domInstallPromise = null;
|
|
75
|
+
domRefCount = 0;
|
|
40
76
|
}
|
|
41
77
|
|
|
42
78
|
/**
|
|
43
79
|
* Load jsdom server-side. The specifier is constructed at runtime so
|
|
44
80
|
* downstream bundlers (Vite, Rollup, esbuild, webpack) cannot statically
|
|
45
81
|
* resolve it. Without this indirection, every browser bundle of
|
|
46
|
-
* @diagrammo/dgmo emits a 5+ MB jsdom chunk even though `
|
|
82
|
+
* @diagrammo/dgmo emits a 5+ MB jsdom chunk even though `acquireDom()`
|
|
47
83
|
* guards execution with a `typeof document` check — the guard prevents
|
|
48
84
|
* runtime evaluation, but the static dependency edge still pulls jsdom
|
|
49
85
|
* into the bundle.
|
|
@@ -128,16 +164,21 @@ export async function render(
|
|
|
128
164
|
}
|
|
129
165
|
|
|
130
166
|
// Visualization/diagram and unknown/null types all go through the unified renderer
|
|
131
|
-
await
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
await acquireDom();
|
|
168
|
+
let svg: string;
|
|
169
|
+
try {
|
|
170
|
+
svg = await renderForExport(content, theme, paletteColors, viewState, {
|
|
171
|
+
...(options?.c4Level !== undefined && { c4Level: options.c4Level }),
|
|
172
|
+
...(options?.c4System !== undefined && { c4System: options.c4System }),
|
|
173
|
+
...(options?.c4Container !== undefined && {
|
|
174
|
+
c4Container: options.c4Container,
|
|
175
|
+
}),
|
|
176
|
+
...(options?.tagGroup !== undefined && { tagGroup: options.tagGroup }),
|
|
177
|
+
...(options?.mapData !== undefined && { mapData: options.mapData }),
|
|
178
|
+
});
|
|
179
|
+
} finally {
|
|
180
|
+
releaseDom();
|
|
181
|
+
}
|
|
141
182
|
|
|
142
183
|
// The map pipeline resolves names AFTER parsing (gazetteer/ISO lookup), so its
|
|
143
184
|
// unknown-place / unknown-subdivision errors live on the ResolvedMap, not the
|
package/src/utils/svg-embed.ts
CHANGED
|
@@ -204,9 +204,18 @@ function computeBBox(
|
|
|
204
204
|
const y = attr(tag, 'y');
|
|
205
205
|
if (x !== null && y !== null) {
|
|
206
206
|
const w = text.length * 7;
|
|
207
|
-
// text-anchor
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
// Honor text-anchor so the horizontal extent points the right way:
|
|
208
|
+
// `start` (SVG default) grows rightward from x, `end` grows leftward,
|
|
209
|
+
// `middle` straddles x. Assuming middle for everything under-measures
|
|
210
|
+
// start-anchored text (e.g. pyramid right-column descriptions), which
|
|
211
|
+
// collapses the tight viewBox and clips that text in embeds.
|
|
212
|
+
const anchor = tag.match(/\btext-anchor="([^"]*)"/)?.[1] ?? 'start';
|
|
213
|
+
const left =
|
|
214
|
+
anchor === 'middle' ? x - w / 2 : anchor === 'end' ? x - w : x;
|
|
215
|
+
const right =
|
|
216
|
+
anchor === 'middle' ? x + w / 2 : anchor === 'end' ? x : x + w;
|
|
217
|
+
push(left, y - 14);
|
|
218
|
+
push(right, y + 4);
|
|
210
219
|
}
|
|
211
220
|
}
|
|
212
221
|
|