compasso 0.1.0 → 0.2.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 +63 -8
- package/dist/{chunk-E456YKAJ.js → chunk-5B453C4P.js} +40 -10
- package/dist/chunk-5B453C4P.js.map +1 -0
- package/dist/{chunk-5RRRE2GF.js → chunk-5PGOL2KR.js} +3 -3
- package/dist/{chunk-5RRRE2GF.js.map → chunk-5PGOL2KR.js.map} +1 -1
- package/dist/{chunk-L5CYESBI.js → chunk-EHQMKVDM.js} +3 -3
- package/dist/{chunk-L5CYESBI.js.map → chunk-EHQMKVDM.js.map} +1 -1
- package/dist/chunk-P2S7AUOL.js +703 -0
- package/dist/chunk-P2S7AUOL.js.map +1 -0
- package/dist/chunk-TP3JOOJW.js +252 -0
- package/dist/chunk-TP3JOOJW.js.map +1 -0
- package/dist/core/index.cjs +44 -7
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +30 -30
- package/dist/core/index.d.ts +30 -30
- package/dist/core/index.js +1 -1
- package/dist/ecomap/index.cjs +11 -7
- package/dist/ecomap/index.cjs.map +1 -1
- package/dist/ecomap/index.js +2 -2
- package/dist/fault-tree/index.cjs +782 -0
- package/dist/fault-tree/index.cjs.map +1 -0
- package/dist/fault-tree/index.d.cts +148 -0
- package/dist/fault-tree/index.d.ts +148 -0
- package/dist/fault-tree/index.js +4 -0
- package/dist/fault-tree/index.js.map +1 -0
- package/dist/fishbone/index.cjs +314 -0
- package/dist/fishbone/index.cjs.map +1 -0
- package/dist/fishbone/index.d.cts +91 -0
- package/dist/fishbone/index.d.ts +91 -0
- package/dist/fishbone/index.js +4 -0
- package/dist/fishbone/index.js.map +1 -0
- package/dist/genogram/index.cjs +11 -7
- package/dist/genogram/index.cjs.map +1 -1
- package/dist/genogram/index.d.cts +3 -2
- package/dist/genogram/index.d.ts +3 -2
- package/dist/genogram/index.js +2 -2
- package/dist/index.cjs +1040 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -3
- package/dist/labels-CYbM5XV7.d.cts +83 -0
- package/dist/labels-CYbM5XV7.d.ts +83 -0
- package/dist/locales/pt-br.cjs +36 -0
- package/dist/locales/pt-br.cjs.map +1 -1
- package/dist/locales/pt-br.d.cts +6 -1
- package/dist/locales/pt-br.d.ts +6 -1
- package/dist/locales/pt-br.js +34 -1
- package/dist/locales/pt-br.js.map +1 -1
- package/dist/text-DuO_PwYw.d.cts +45 -0
- package/dist/text-DuO_PwYw.d.ts +45 -0
- package/dist/xml-DDae1eUr.d.cts +4 -0
- package/dist/xml-DDae1eUr.d.ts +4 -0
- package/package.json +70 -18
- package/dist/chunk-E456YKAJ.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# compasso
|
|
2
2
|
|
|
3
|
-
Standards-faithful
|
|
3
|
+
Standards-faithful technical diagrams as **pure SVG strings**. Deterministic, zero
|
|
4
4
|
runtime dependencies, server-safe (no DOM, no canvas, no clock, no randomness).
|
|
5
5
|
|
|
6
|
-
**
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
**Four diagrams shipped:** the **genogram** (McGoldrick family-systems notation), the
|
|
7
|
+
**ecomap** (radial person↔environment ties), the **fault tree** (NUREG-0492 / IEC 61025
|
|
8
|
+
distinctive-shape notation) and the **fishbone** (Ishikawa cause-and-effect). The
|
|
9
|
+
architecture is built for more — see the roadmap.
|
|
9
10
|
|
|
10
11
|
## Principles
|
|
11
12
|
|
|
@@ -85,6 +86,59 @@ Arrowheads appear **only** for a declared `direction` (`"in"` toward the center,
|
|
|
85
86
|
`"out"` toward the system, `"both"`); `null` draws no arrow — never a default. Large
|
|
86
87
|
sets split onto two alternating rings; node spacing is overlap-proof by construction.
|
|
87
88
|
|
|
89
|
+
## Fault tree
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { faultTreeSvg } from "compasso/fault-tree";
|
|
93
|
+
|
|
94
|
+
const { svg, layout } = faultTreeSvg({
|
|
95
|
+
topId: 1,
|
|
96
|
+
events: [
|
|
97
|
+
{ id: 1, kind: "intermediate", label: "Pump system fails", code: "TOP" },
|
|
98
|
+
{ id: 2, kind: "basic", label: "Motor winding failure", code: "B1" },
|
|
99
|
+
{ id: 3, kind: "undeveloped", label: "Control logic fault", code: "U1" },
|
|
100
|
+
],
|
|
101
|
+
gates: [{ id: 1, type: "or", eventId: 1, inputIds: [2, 3] }],
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
NUREG-0492 distinctive shapes: rectangles (intermediate events), circles (basic),
|
|
106
|
+
diamonds (undeveloped), house (expected external events), oval (conditioning events
|
|
107
|
+
attached to INHIBIT gates), transfer triangles. Gates: `and | or | xor | inhibit |
|
|
108
|
+
vote` (k-of-n, drawn with its threshold). The layout is deterministic, orthogonal and
|
|
109
|
+
overlap-proof (proven by its own test harness).
|
|
110
|
+
|
|
111
|
+
A fault tree is a **logic artifact**, so structurally invalid input is **refused**, not
|
|
112
|
+
repaired: `faultTreeSvg` throws `FaultTreeValidationError` listing **every** issue with
|
|
113
|
+
a stable machine-readable `code` (`event-without-gate`, `cycle`, `unknown-input`, …).
|
|
114
|
+
Honest incompleteness has standard notation instead — that's what `undeveloped` and
|
|
115
|
+
`transfer` are for.
|
|
116
|
+
|
|
117
|
+
## Fishbone
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { fishboneSvg } from "compasso/fishbone";
|
|
121
|
+
|
|
122
|
+
const svg = fishboneSvg({
|
|
123
|
+
effectLabel: "Late deliveries",
|
|
124
|
+
categories: [
|
|
125
|
+
{
|
|
126
|
+
id: 1,
|
|
127
|
+
label: "People",
|
|
128
|
+
causes: [
|
|
129
|
+
{ id: 1, label: "Driver shortage", subCauses: [{ id: 1, label: "High turnover" }] },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Classic Ishikawa: spine into the effect head, category bones alternating above/below,
|
|
137
|
+
horizontal cause twigs, one level of sub-causes, arrowheads converging toward the
|
|
138
|
+
effect at every level (suppressible via `arrowheads: false`). **Declared order is
|
|
139
|
+
honored** — significance-near-the-head is the analyst's call, never re-sorted. Label
|
|
140
|
+
spacing is driven by measured text widths, so labels never collide.
|
|
141
|
+
|
|
88
142
|
## Localization
|
|
89
143
|
|
|
90
144
|
```ts
|
|
@@ -110,10 +164,11 @@ with a negation list. PRs for new locales are welcome.
|
|
|
110
164
|
## Roadmap
|
|
111
165
|
|
|
112
166
|
The goal is a family of standards-faithful technical diagrams sharing this core
|
|
113
|
-
(text metrics, stroke vocabulary, escaping, legend machinery):
|
|
114
|
-
|
|
115
|
-
symbol-library diagrams (P&ID,
|
|
116
|
-
standard. AST-first; text
|
|
167
|
+
(text metrics, stroke vocabulary, escaping, legend machinery). Shipped: genogram,
|
|
168
|
+
ecomap, fault tree, fishbone. Next: more tree-based diagrams (phylogenetic, org
|
|
169
|
+
charts), flow/layered diagrams (PRISMA, UML), and symbol-library diagrams (P&ID,
|
|
170
|
+
single-line, ladder logic) — each built from its public standard. AST-first; text
|
|
171
|
+
DSLs may come later.
|
|
117
172
|
|
|
118
173
|
## Provenance & license
|
|
119
174
|
|
|
@@ -13,19 +13,27 @@ var CHAR_W = 0.6;
|
|
|
13
13
|
function estimateTextWidth(text, fontPx) {
|
|
14
14
|
return text.length * fontPx * CHAR_W;
|
|
15
15
|
}
|
|
16
|
-
function wrapLabel(label, perLine) {
|
|
16
|
+
function wrapLabel(label, perLine, maxLines = 2) {
|
|
17
17
|
if (label.length <= perLine) return [label];
|
|
18
18
|
const cap = (s) => s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + "\u2026" : s;
|
|
19
|
-
|
|
20
|
-
let line2 = "";
|
|
19
|
+
const lines = [""];
|
|
21
20
|
for (const word of label.split(/\s+/)) {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const last = lines.length - 1;
|
|
22
|
+
const current = lines[last];
|
|
23
|
+
if (current === "" || (current + " " + word).length <= perLine) {
|
|
24
|
+
lines[last] = current === "" ? word : `${current} ${word}`;
|
|
25
|
+
} else if (lines.length < maxLines) {
|
|
26
|
+
lines.push(word);
|
|
24
27
|
} else {
|
|
25
|
-
|
|
28
|
+
lines[last] = `${current} ${word}`;
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
|
-
|
|
31
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
32
|
+
return lines.map(cap);
|
|
33
|
+
}
|
|
34
|
+
function wrapLabelBalanced(label, maxLines) {
|
|
35
|
+
const perLine = Math.min(26, Math.max(14, Math.ceil(label.length / 2) + 2));
|
|
36
|
+
return wrapLabel(label, perLine, maxLines);
|
|
29
37
|
}
|
|
30
38
|
function clampLabel(label, maxChars) {
|
|
31
39
|
if (maxChars === void 0 || label.length <= maxChars) return label;
|
|
@@ -33,6 +41,28 @@ function clampLabel(label, maxChars) {
|
|
|
33
41
|
}
|
|
34
42
|
var FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
35
43
|
|
|
44
|
+
// src/core/legend.ts
|
|
45
|
+
var LEGEND_ROW_H = 18;
|
|
46
|
+
var LEGEND_PAD = 16;
|
|
47
|
+
var LEGEND_SWATCH_W = 22;
|
|
48
|
+
var LEGEND_GAP = 14;
|
|
49
|
+
var LEGEND_FONT = 11;
|
|
50
|
+
var LEGEND_TEXT_FILL = "#52525b";
|
|
51
|
+
function legendBlock(entries, startY) {
|
|
52
|
+
if (entries.length === 0) return { svg: "", width: 0, height: startY };
|
|
53
|
+
const rows = entries.map((entry, i) => {
|
|
54
|
+
const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;
|
|
55
|
+
const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;
|
|
56
|
+
return entry.swatch(LEGEND_PAD, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT}" fill="${LEGEND_TEXT_FILL}">${xmlEscape(entry.label)}</text>`;
|
|
57
|
+
});
|
|
58
|
+
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);
|
|
59
|
+
return {
|
|
60
|
+
svg: `<g data-compasso-legend="true">${rows.join("")}</g>`,
|
|
61
|
+
width: LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD,
|
|
62
|
+
height: startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
36
66
|
// src/core/stroke.ts
|
|
37
67
|
var EDGE_STROKE = {
|
|
38
68
|
plain: { width: 1.5, dash: null, opacity: 0.6 },
|
|
@@ -81,6 +111,6 @@ function qualityLineStyle(quality, lexicon = QUALITY_LEXICON_EN) {
|
|
|
81
111
|
return matched.length === 1 ? matched[0] : "plain";
|
|
82
112
|
}
|
|
83
113
|
|
|
84
|
-
export { CHAR_W, EDGE_STROKE, FONT_FAMILY, QUALITY_LEXICON_EN, clampLabel, estimateTextWidth, normalizeText, pathData, qualityLineStyle, wrapLabel, xmlEscape };
|
|
85
|
-
//# sourceMappingURL=chunk-
|
|
86
|
-
//# sourceMappingURL=chunk-
|
|
114
|
+
export { CHAR_W, EDGE_STROKE, FONT_FAMILY, LEGEND_FONT, LEGEND_GAP, LEGEND_PAD, LEGEND_ROW_H, LEGEND_SWATCH_W, QUALITY_LEXICON_EN, clampLabel, estimateTextWidth, legendBlock, normalizeText, pathData, qualityLineStyle, wrapLabel, wrapLabelBalanced, xmlEscape };
|
|
115
|
+
//# sourceMappingURL=chunk-5B453C4P.js.map
|
|
116
|
+
//# sourceMappingURL=chunk-5B453C4P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/xml.ts","../src/core/geometry.ts","../src/core/text.ts","../src/core/legend.ts","../src/core/stroke.ts"],"names":[],"mappings":"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","file":"chunk-5B453C4P.js","sourcesContent":["// XML/SVG escaping — every interpolated text in an emitted SVG MUST pass through\n// this. Diagram labels are typically user/author-controlled, and the SVG string may\n// be injected via innerHTML in a browser or embedded as vectors in a PDF — an\n// unescaped label is an XSS / `</svg>`-breakout vector on every surface at once.\n\n/** Escapes a string for use in SVG/XML text content AND attribute values. */\nexport function xmlEscape(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","// Shared geometry primitives.\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** \"M x y L x y …\" path data from a polyline. */\nexport function pathData(points: Point[]): string {\n return points.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`).join(\" \");\n}\n","// Pure, deterministic text metrics — no DOM, no canvas, no font files. Every layout\n// in this library reserves space from these estimates and every emitter draws with\n// the same constants, so what is proven collision-free is what is drawn.\n\n/**\n * Conservative per-character advance, as a fraction of the font size. The pure module\n * can't measure real glyphs (no DOM/canvas), so it estimates width = chars * font *\n * CHAR_W. It is deliberately a touch WIDER than Helvetica's ~0.5 average advance: a\n * layout reserves slightly more room than the text needs, so the real render always\n * fits inside its reserved box.\n */\nexport const CHAR_W = 0.6;\n\n/** Pure, deterministic width estimate for a single line of text at `fontPx`. */\nexport function estimateTextWidth(text: string, fontPx: number): number {\n return text.length * fontPx * CHAR_W;\n}\n\n/**\n * Wraps a label onto up to `maxLines` lines (default 2) so long labels don't sprawl.\n * Greedy word fill; each line capped at `perLine` chars (…-truncated only if a line\n * still overflows). The LAST allowed line absorbs every remaining word before the cap\n * applies — words are never dropped mid-label, only visibly elided. Pure + shared so\n * every renderer wraps identically. The full text is always kept elsewhere (the\n * node's `label`, an SVG <title>, or a side list), so nothing is silently lost.\n *\n * INVARIANT: with the default `maxLines` the output is byte-identical to the\n * historical two-line implementation (pinned by test/core/text.test.ts) — both\n * shipped renderers wrap through here.\n */\nexport function wrapLabel(label: string, perLine: number, maxLines = 2): string[] {\n if (label.length <= perLine) return [label];\n const cap = (s: string): string =>\n s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + \"…\" : s;\n const lines: string[] = [\"\"];\n for (const word of label.split(/\\s+/)) {\n const last = lines.length - 1;\n const current = lines[last]!;\n if (current === \"\" || (current + \" \" + word).length <= perLine) {\n lines[last] = current === \"\" ? word : `${current} ${word}`;\n } else if (lines.length < maxLines) {\n lines.push(word);\n } else {\n lines[last] = `${current} ${word}`;\n }\n }\n // A trailing-whitespace \"word\" (\"a b \".split(/\\s+/) → [\"a\",\"b\",\"\"]) may open a line\n // it never fills; the historical code dropped that empty line, so this does too.\n if (lines.length > 1 && lines[lines.length - 1] === \"\") lines.pop();\n return lines.map(cap);\n}\n\n/**\n * House \"balanced\" wrap policy (extracted verbatim from the ecomap's tie-label wrap,\n * itself mirroring the genogram's): per-line budget `min(26, max(14, ceil(len/2)+2))`\n * splits a medium label into two roughly even lines instead of one long + one short,\n * then the greedy `wrapLabel` fill (and its `maxLines` semantics) applies. The shipped\n * renderers keep their local copies for now — new modules consume this one; migrating\n * them is a separate provably-zero-change refactor.\n */\nexport function wrapLabelBalanced(label: string, maxLines?: number): string[] {\n const perLine = Math.min(26, Math.max(14, Math.ceil(label.length / 2) + 2));\n return wrapLabel(label, perLine, maxLines);\n}\n\n/** Caps a verbatim label for a COMPACT render (preview); full text kept by the caller. */\nexport function clampLabel(label: string, maxChars: number | undefined): string {\n if (maxChars === undefined || label.length <= maxChars) return label;\n return label.slice(0, Math.max(1, maxChars - 1)) + \"…\";\n}\n\n/** Font stack shared by every emitter; PDF embedders typically map it onto Helvetica. */\nexport const FONT_FAMILY = \"Helvetica, Arial, sans-serif\";\n","// Shared legend machinery — the exact row format the genogram and ecomap emitters\n// currently duplicate character-for-character (`<g data-compasso-legend=\"true\">` with\n// swatch-closure rows). Extracted so new diagram modules don't add a third copy;\n// the two shipped emitters keep their local blocks for now (migrating them is a\n// separate, provably-zero-change refactor — pinned byte-for-byte against the ecomap's\n// emitted legend in test/core/legend.test.ts).\n//\n// The caller stays in charge of the HONESTY RULE half: it decides WHICH entries exist\n// (used-keys-only) and what each swatch draws; this module only owns the row geometry,\n// the text emission (xmlEscape — labels may come from caller-supplied packs), and the\n// width/height bookkeeping both renderers compute identically.\n\nimport { FONT_FAMILY, estimateTextWidth } from \"./text\";\nimport { xmlEscape } from \"./xml\";\n\n// Row metrics shared by every legend in the library. Exported because swatch builders\n// need them (a line swatch spans LEGEND_SWATCH_W; a glyph swatch centers on it).\nexport const LEGEND_ROW_H = 18;\nexport const LEGEND_PAD = 16;\nexport const LEGEND_SWATCH_W = 22;\nexport const LEGEND_GAP = 14;\nexport const LEGEND_FONT = 11;\n\n// Legend text ink — the zinc glyph stroke both shipped emitters use for legend labels.\nconst LEGEND_TEXT_FILL = \"#52525b\";\n\nexport interface LegendEntry {\n /**\n * Swatch markup builder, called with the swatch's left x and the row's center y.\n * The builder owns its own escaping/rounding (it typically reuses the module's\n * glyph/line emitters); it should draw within LEGEND_SWATCH_W of x.\n */\n swatch: (x: number, yCenter: number) => string;\n /** Display label (escaped here; verbatim pack text in, escaped SVG out). */\n label: string;\n}\n\nexport interface LegendBlock {\n /** `<g data-compasso-legend=\"true\">…</g>` — empty string when there are no entries. */\n svg: string;\n /** Minimum canvas width the legend needs; callers take `max(width, block.width)`. */\n width: number;\n /** New total canvas height including the legend; callers assign it directly. */\n height: number;\n}\n\n/**\n * Emits the legend group for the given entries below `startY` (the diagram's current\n * height). Pure + deterministic; coordinates are interpolated exactly as the shipped\n * emitters do. Zero entries (used-keys-only found nothing) → no markup, zero width\n * contribution, height unchanged — safe to call unconditionally.\n */\nexport function legendBlock(entries: readonly LegendEntry[], startY: number): LegendBlock {\n if (entries.length === 0) return { svg: \"\", width: 0, height: startY };\n const rows = entries.map((entry, i) => {\n const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;\n const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;\n return (\n entry.swatch(LEGEND_PAD, rowCenterY) +\n `<text x=\"${textX}\" y=\"${rowCenterY + LEGEND_FONT * 0.32}\" font-family=\"${FONT_FAMILY}\" font-size=\"${LEGEND_FONT}\" fill=\"${LEGEND_TEXT_FILL}\">${xmlEscape(entry.label)}</text>`\n );\n });\n const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);\n return {\n svg: `<g data-compasso-legend=\"true\">${rows.join(\"\")}</g>`,\n width: LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD,\n height: startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2,\n };\n}\n","// Edge line styles — a neutral, presentation-only style for a relationship line.\n// NOT a judgment: a deterministic, lexical hint derived from the author's own quality\n// word. The literal word always rides the element's <title>, so a style never replaces\n// what was said. Unknown/ambiguous quality → \"plain\".\n\nexport type EdgeLineStyle = \"plain\" | \"close\" | \"distant\" | \"conflict\" | \"cutoff\";\n\n/** Stroke attributes per style — shared by every renderer so SVG and PDF match. */\nexport interface EdgeStroke {\n width: number;\n /** SVG dash array / PDF dash pattern as [dash, gap]; null = solid. */\n dash: [number, number] | null;\n opacity: number;\n}\n\nexport const EDGE_STROKE: Record<EdgeLineStyle, EdgeStroke> = {\n plain: { width: 1.5, dash: null, opacity: 0.6 },\n close: { width: 3, dash: null, opacity: 0.85 },\n distant: { width: 1.5, dash: [4, 4], opacity: 0.55 },\n conflict: { width: 2, dash: [2, 2], opacity: 0.75 },\n cutoff: { width: 1.5, dash: [6, 5], opacity: 0.4 },\n};\n\n/**\n * A pluggable lexicon mapping free-text quality words onto line styles. Needles are\n * diacritic-free, lowercase substrings of the author's own words; `negations` are\n * whole words that flip the meaning of the very word a needle would match (\"not\n * close\"), so their presence makes the matcher abstain to \"plain\" rather than risk\n * an inverted visual. Locale packs (e.g. `compasso/locales/pt-br`) ship alternates.\n */\nexport interface QualityLexicon {\n buckets: readonly { style: Exclude<EdgeLineStyle, \"plain\">; needles: readonly string[] }[];\n negations: readonly string[];\n}\n\n// English default. Conservative stems: each needle is a substring match on the\n// normalized text, and the SINGLE-BUCKET rule below keeps competing signals honest.\n// \"no\" alone is deliberately NOT a negation: \"no contact\" is a genuine cutoff signal.\nexport const QUALITY_LEXICON_EN: QualityLexicon = {\n buckets: [\n {\n style: \"close\",\n needles: [\"close\", \"warm\", \"support\", \"lov\", \"affection\", \"caring\", \"tight\", \"harmon\", \"healthy\"],\n },\n {\n style: \"distant\",\n needles: [\"distant\", \"detach\", \"absent\", \"cold\", \"drift\"],\n },\n {\n style: \"conflict\",\n needles: [\"conflict\", \"fight\", \"tens\", \"difficult\", \"hostil\", \"violen\", \"abus\", \"aggress\", \"complicat\", \"toxic\", \"argu\"],\n },\n {\n style: \"cutoff\",\n needles: [\"estrang\", \"cut off\", \"cutoff\", \"no contact\", \"broken off\", \"sever\"],\n },\n ],\n negations: [\"not\", \"never\", \"no longer\", \"hardly\"],\n};\n\n/** Lowercase + strip diacritics so matching ignores accents. */\nexport function normalizeText(text: string): string {\n return text.normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\").toLowerCase();\n}\n\nconst escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * Maps a free-text relationship quality to a neutral line style. Deterministic and\n * conservative: returns a specific style ONLY when exactly one lexical bucket matches;\n * null/empty/ambiguous/negated/unknown all fall back to \"plain\". The caller still\n * keeps the verbatim quality word, so nothing the author said is ever lost.\n */\nexport function qualityLineStyle(\n quality: string | null,\n lexicon: QualityLexicon = QUALITY_LEXICON_EN,\n): EdgeLineStyle {\n if (quality === null) return \"plain\";\n const haystack = normalizeText(quality);\n if (haystack.trim() === \"\") return \"plain\";\n\n if (lexicon.negations.length > 0) {\n const negation = new RegExp(`\\\\b(${lexicon.negations.map(escapeRegExp).join(\"|\")})\\\\b`);\n if (negation.test(haystack)) return \"plain\";\n }\n\n const matched: EdgeLineStyle[] = [];\n for (const { style, needles } of lexicon.buckets) {\n if (needles.some((n) => haystack.includes(n))) matched.push(style);\n }\n return matched.length === 1 ? matched[0]! : \"plain\";\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeText, estimateTextWidth, qualityLineStyle, xmlEscape, FONT_FAMILY, EDGE_STROKE, wrapLabel } from './chunk-
|
|
1
|
+
import { normalizeText, estimateTextWidth, qualityLineStyle, xmlEscape, FONT_FAMILY, EDGE_STROKE, wrapLabel } from './chunk-5B453C4P.js';
|
|
2
2
|
|
|
3
3
|
// src/genogram/types.ts
|
|
4
4
|
var UNION_STATUSES = [
|
|
@@ -1121,5 +1121,5 @@ function genogramSvg(input, opts = {}) {
|
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
1123
|
export { EDGE_FONT, GENOGRAM_SVG_LABELS_EN, GENOGRAM_TITLE_LABELS_EN, KINSHIP_EN, LABEL_FONT, LABEL_LINE_H, NODE_SIZE, PARENT_REL_ID_BASE, PROMOTED_REL_ID_BASE, UNION_NOTATION, UNION_REL_ID_BASE, UNION_STATUSES, classifyRelationshipType, computeGenogramLayout, genogramLayoutSvg, genogramSvg, latestUnionPerPair, relationshipTypeTokens };
|
|
1124
|
-
//# sourceMappingURL=chunk-
|
|
1125
|
-
//# sourceMappingURL=chunk-
|
|
1124
|
+
//# sourceMappingURL=chunk-5PGOL2KR.js.map
|
|
1125
|
+
//# sourceMappingURL=chunk-5PGOL2KR.js.map
|