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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compasso",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Standards-faithful relational diagrams (genogram, ecomap) as pure SVG strings. Deterministic, zero dependencies, server-safe.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Standards-faithful relational and analytical diagrams (genogram, ecomap, fault tree, fishbone) as pure SVG strings. Deterministic, zero dependencies, server-safe.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Victor Canô",
|
|
7
7
|
"repository": {
|
|
@@ -15,29 +15,74 @@
|
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"require": {
|
|
23
|
+
"types": "./dist/index.d.cts",
|
|
24
|
+
"default": "./dist/index.cjs"
|
|
25
|
+
}
|
|
21
26
|
},
|
|
22
27
|
"./core": {
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/core/index.d.ts",
|
|
30
|
+
"default": "./dist/core/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/core/index.d.cts",
|
|
34
|
+
"default": "./dist/core/index.cjs"
|
|
35
|
+
}
|
|
26
36
|
},
|
|
27
37
|
"./genogram": {
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/genogram/index.d.ts",
|
|
40
|
+
"default": "./dist/genogram/index.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/genogram/index.d.cts",
|
|
44
|
+
"default": "./dist/genogram/index.cjs"
|
|
45
|
+
}
|
|
31
46
|
},
|
|
32
47
|
"./ecomap": {
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/ecomap/index.d.ts",
|
|
50
|
+
"default": "./dist/ecomap/index.js"
|
|
51
|
+
},
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/ecomap/index.d.cts",
|
|
54
|
+
"default": "./dist/ecomap/index.cjs"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"./fault-tree": {
|
|
58
|
+
"import": {
|
|
59
|
+
"types": "./dist/fault-tree/index.d.ts",
|
|
60
|
+
"default": "./dist/fault-tree/index.js"
|
|
61
|
+
},
|
|
62
|
+
"require": {
|
|
63
|
+
"types": "./dist/fault-tree/index.d.cts",
|
|
64
|
+
"default": "./dist/fault-tree/index.cjs"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"./fishbone": {
|
|
68
|
+
"import": {
|
|
69
|
+
"types": "./dist/fishbone/index.d.ts",
|
|
70
|
+
"default": "./dist/fishbone/index.js"
|
|
71
|
+
},
|
|
72
|
+
"require": {
|
|
73
|
+
"types": "./dist/fishbone/index.d.cts",
|
|
74
|
+
"default": "./dist/fishbone/index.cjs"
|
|
75
|
+
}
|
|
36
76
|
},
|
|
37
77
|
"./locales/pt-br": {
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
78
|
+
"import": {
|
|
79
|
+
"types": "./dist/locales/pt-br.d.ts",
|
|
80
|
+
"default": "./dist/locales/pt-br.js"
|
|
81
|
+
},
|
|
82
|
+
"require": {
|
|
83
|
+
"types": "./dist/locales/pt-br.d.cts",
|
|
84
|
+
"default": "./dist/locales/pt-br.cjs"
|
|
85
|
+
}
|
|
41
86
|
}
|
|
42
87
|
},
|
|
43
88
|
"files": [
|
|
@@ -48,11 +93,16 @@
|
|
|
48
93
|
"test": "vitest run",
|
|
49
94
|
"test:watch": "vitest",
|
|
50
95
|
"typecheck": "tsc --noEmit",
|
|
51
|
-
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
96
|
+
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build",
|
|
97
|
+
"lint": "eslint ."
|
|
52
98
|
},
|
|
53
99
|
"keywords": [
|
|
54
100
|
"genogram",
|
|
55
101
|
"ecomap",
|
|
102
|
+
"fault-tree",
|
|
103
|
+
"fta",
|
|
104
|
+
"fishbone",
|
|
105
|
+
"ishikawa",
|
|
56
106
|
"svg",
|
|
57
107
|
"diagram",
|
|
58
108
|
"mcgoldrick",
|
|
@@ -65,8 +115,10 @@
|
|
|
65
115
|
"packageManager": "pnpm@10.33.2",
|
|
66
116
|
"devDependencies": {
|
|
67
117
|
"@types/node": "^24",
|
|
118
|
+
"eslint": "^10.4.1",
|
|
68
119
|
"tsup": "^8",
|
|
69
120
|
"typescript": "^5",
|
|
121
|
+
"typescript-eslint": "^8.61.0",
|
|
70
122
|
"vitest": "4.1.8"
|
|
71
123
|
}
|
|
72
124
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/xml.ts","../src/core/geometry.ts","../src/core/text.ts","../src/core/stroke.ts"],"names":[],"mappings":"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","file":"chunk-E456YKAJ.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 TWO lines so long labels don't sprawl. Greedy word fill;\n * each line capped at `perLine` chars (…-truncated only if a single word overflows).\n * Pure + shared so every renderer wraps identically. The full text is always kept\n * elsewhere (the node's `label`, an SVG <title>, or a side list), so nothing is\n * silently lost.\n */\nexport function wrapLabel(label: string, perLine: number): 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 let line1 = \"\";\n let line2 = \"\";\n for (const word of label.split(/\\s+/)) {\n if (line2 === \"\" && (line1 === \"\" || (line1 + \" \" + word).length <= perLine)) {\n line1 = line1 === \"\" ? word : `${line1} ${word}`;\n } else {\n line2 = line2 === \"\" ? word : `${line2} ${word}`;\n }\n }\n return line2 === \"\" ? [cap(line1)] : [cap(line1), cap(line2)];\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","// 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"]}
|