schematex 0.1.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.
Files changed (181) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +379 -0
  3. package/dist/chunk-2MQWZ2XY.cjs +453 -0
  4. package/dist/chunk-2MQWZ2XY.cjs.map +1 -0
  5. package/dist/chunk-2UKC6ZCY.cjs +1803 -0
  6. package/dist/chunk-2UKC6ZCY.cjs.map +1 -0
  7. package/dist/chunk-34X3ZJ6E.cjs +783 -0
  8. package/dist/chunk-34X3ZJ6E.cjs.map +1 -0
  9. package/dist/chunk-3FTUWAXK.cjs +1220 -0
  10. package/dist/chunk-3FTUWAXK.cjs.map +1 -0
  11. package/dist/chunk-3J7TFUOC.js +745 -0
  12. package/dist/chunk-3J7TFUOC.js.map +1 -0
  13. package/dist/chunk-47ZC6EMJ.js +1009 -0
  14. package/dist/chunk-47ZC6EMJ.js.map +1 -0
  15. package/dist/chunk-4DBRNOPA.cjs +750 -0
  16. package/dist/chunk-4DBRNOPA.cjs.map +1 -0
  17. package/dist/chunk-4G7ZIBHN.js +778 -0
  18. package/dist/chunk-4G7ZIBHN.js.map +1 -0
  19. package/dist/chunk-5C7DPDHQ.js +1321 -0
  20. package/dist/chunk-5C7DPDHQ.js.map +1 -0
  21. package/dist/chunk-ADOXGKAK.js +1251 -0
  22. package/dist/chunk-ADOXGKAK.js.map +1 -0
  23. package/dist/chunk-BE5HNDA5.cjs +874 -0
  24. package/dist/chunk-BE5HNDA5.cjs.map +1 -0
  25. package/dist/chunk-CZRM7LT7.js +889 -0
  26. package/dist/chunk-CZRM7LT7.js.map +1 -0
  27. package/dist/chunk-D4JTSPOL.js +1795 -0
  28. package/dist/chunk-D4JTSPOL.js.map +1 -0
  29. package/dist/chunk-DS47NTWZ.cjs +1034 -0
  30. package/dist/chunk-DS47NTWZ.cjs.map +1 -0
  31. package/dist/chunk-FDLZEKEB.js +449 -0
  32. package/dist/chunk-FDLZEKEB.js.map +1 -0
  33. package/dist/chunk-FGPTCDUT.cjs +1851 -0
  34. package/dist/chunk-FGPTCDUT.cjs.map +1 -0
  35. package/dist/chunk-HDKDQAEQ.cjs +86 -0
  36. package/dist/chunk-HDKDQAEQ.cjs.map +1 -0
  37. package/dist/chunk-IX554O5K.js +346 -0
  38. package/dist/chunk-IX554O5K.js.map +1 -0
  39. package/dist/chunk-KLJEK547.js +71 -0
  40. package/dist/chunk-KLJEK547.js.map +1 -0
  41. package/dist/chunk-LMFSHK45.js +1028 -0
  42. package/dist/chunk-LMFSHK45.js.map +1 -0
  43. package/dist/chunk-MDICUK6F.cjs +1258 -0
  44. package/dist/chunk-MDICUK6F.cjs.map +1 -0
  45. package/dist/chunk-N7KOXOMX.cjs +363 -0
  46. package/dist/chunk-N7KOXOMX.cjs.map +1 -0
  47. package/dist/chunk-NYCIK4SU.cjs +775 -0
  48. package/dist/chunk-NYCIK4SU.cjs.map +1 -0
  49. package/dist/chunk-PDPHRZZT.js +770 -0
  50. package/dist/chunk-PDPHRZZT.js.map +1 -0
  51. package/dist/chunk-ROFLJ74T.js +1212 -0
  52. package/dist/chunk-ROFLJ74T.js.map +1 -0
  53. package/dist/chunk-S6BK5DB6.cjs +845 -0
  54. package/dist/chunk-S6BK5DB6.cjs.map +1 -0
  55. package/dist/chunk-U4I37IBN.js +874 -0
  56. package/dist/chunk-U4I37IBN.js.map +1 -0
  57. package/dist/chunk-U5GGE6PJ.js +839 -0
  58. package/dist/chunk-U5GGE6PJ.js.map +1 -0
  59. package/dist/chunk-UHLYS3W5.cjs +1015 -0
  60. package/dist/chunk-UHLYS3W5.cjs.map +1 -0
  61. package/dist/chunk-URSKIHSY.cjs +881 -0
  62. package/dist/chunk-URSKIHSY.cjs.map +1 -0
  63. package/dist/chunk-V6WO7RK7.cjs +1056 -0
  64. package/dist/chunk-V6WO7RK7.cjs.map +1 -0
  65. package/dist/chunk-VFQCTXOX.js +869 -0
  66. package/dist/chunk-VFQCTXOX.js.map +1 -0
  67. package/dist/chunk-XQ52ICHU.cjs +895 -0
  68. package/dist/chunk-XQ52ICHU.cjs.map +1 -0
  69. package/dist/chunk-XX4BKS7Y.js +1051 -0
  70. package/dist/chunk-XX4BKS7Y.js.map +1 -0
  71. package/dist/chunk-XXU36667.js +1844 -0
  72. package/dist/chunk-XXU36667.js.map +1 -0
  73. package/dist/chunk-ZX7QKZK2.cjs +1326 -0
  74. package/dist/chunk-ZX7QKZK2.cjs.map +1 -0
  75. package/dist/diagrams/blockdiagram/index.cjs +25 -0
  76. package/dist/diagrams/blockdiagram/index.cjs.map +1 -0
  77. package/dist/diagrams/blockdiagram/index.d.cts +67 -0
  78. package/dist/diagrams/blockdiagram/index.d.ts +67 -0
  79. package/dist/diagrams/blockdiagram/index.js +4 -0
  80. package/dist/diagrams/blockdiagram/index.js.map +1 -0
  81. package/dist/diagrams/circuit/index.cjs +34 -0
  82. package/dist/diagrams/circuit/index.cjs.map +1 -0
  83. package/dist/diagrams/circuit/index.d.cts +138 -0
  84. package/dist/diagrams/circuit/index.d.ts +138 -0
  85. package/dist/diagrams/circuit/index.js +5 -0
  86. package/dist/diagrams/circuit/index.js.map +1 -0
  87. package/dist/diagrams/ecomap/index.cjs +30 -0
  88. package/dist/diagrams/ecomap/index.cjs.map +1 -0
  89. package/dist/diagrams/ecomap/index.d.cts +15 -0
  90. package/dist/diagrams/ecomap/index.d.ts +15 -0
  91. package/dist/diagrams/ecomap/index.js +5 -0
  92. package/dist/diagrams/ecomap/index.js.map +1 -0
  93. package/dist/diagrams/entity/index.cjs +26 -0
  94. package/dist/diagrams/entity/index.cjs.map +1 -0
  95. package/dist/diagrams/entity/index.d.cts +54 -0
  96. package/dist/diagrams/entity/index.d.ts +54 -0
  97. package/dist/diagrams/entity/index.js +5 -0
  98. package/dist/diagrams/entity/index.js.map +1 -0
  99. package/dist/diagrams/fishbone/index.cjs +34 -0
  100. package/dist/diagrams/fishbone/index.cjs.map +1 -0
  101. package/dist/diagrams/fishbone/index.d.cts +185 -0
  102. package/dist/diagrams/fishbone/index.d.ts +185 -0
  103. package/dist/diagrams/fishbone/index.js +5 -0
  104. package/dist/diagrams/fishbone/index.js.map +1 -0
  105. package/dist/diagrams/flowchart/index.cjs +34 -0
  106. package/dist/diagrams/flowchart/index.cjs.map +1 -0
  107. package/dist/diagrams/flowchart/index.d.cts +2 -0
  108. package/dist/diagrams/flowchart/index.d.ts +2 -0
  109. package/dist/diagrams/flowchart/index.js +5 -0
  110. package/dist/diagrams/flowchart/index.js.map +1 -0
  111. package/dist/diagrams/genogram/index.cjs +38 -0
  112. package/dist/diagrams/genogram/index.cjs.map +1 -0
  113. package/dist/diagrams/genogram/index.d.cts +20 -0
  114. package/dist/diagrams/genogram/index.d.ts +20 -0
  115. package/dist/diagrams/genogram/index.js +5 -0
  116. package/dist/diagrams/genogram/index.js.map +1 -0
  117. package/dist/diagrams/ladder/index.cjs +26 -0
  118. package/dist/diagrams/ladder/index.cjs.map +1 -0
  119. package/dist/diagrams/ladder/index.d.cts +49 -0
  120. package/dist/diagrams/ladder/index.d.ts +49 -0
  121. package/dist/diagrams/ladder/index.js +5 -0
  122. package/dist/diagrams/ladder/index.js.map +1 -0
  123. package/dist/diagrams/logic/index.cjs +26 -0
  124. package/dist/diagrams/logic/index.cjs.map +1 -0
  125. package/dist/diagrams/logic/index.d.cts +73 -0
  126. package/dist/diagrams/logic/index.d.ts +73 -0
  127. package/dist/diagrams/logic/index.js +5 -0
  128. package/dist/diagrams/logic/index.js.map +1 -0
  129. package/dist/diagrams/orgchart/index.cjs +30 -0
  130. package/dist/diagrams/orgchart/index.cjs.map +1 -0
  131. package/dist/diagrams/orgchart/index.d.cts +100 -0
  132. package/dist/diagrams/orgchart/index.d.ts +100 -0
  133. package/dist/diagrams/orgchart/index.js +5 -0
  134. package/dist/diagrams/orgchart/index.js.map +1 -0
  135. package/dist/diagrams/pedigree/index.cjs +30 -0
  136. package/dist/diagrams/pedigree/index.cjs.map +1 -0
  137. package/dist/diagrams/pedigree/index.d.cts +15 -0
  138. package/dist/diagrams/pedigree/index.d.ts +15 -0
  139. package/dist/diagrams/pedigree/index.js +5 -0
  140. package/dist/diagrams/pedigree/index.js.map +1 -0
  141. package/dist/diagrams/phylo/index.cjs +30 -0
  142. package/dist/diagrams/phylo/index.cjs.map +1 -0
  143. package/dist/diagrams/phylo/index.d.cts +32 -0
  144. package/dist/diagrams/phylo/index.d.ts +32 -0
  145. package/dist/diagrams/phylo/index.js +5 -0
  146. package/dist/diagrams/phylo/index.js.map +1 -0
  147. package/dist/diagrams/sld/index.cjs +26 -0
  148. package/dist/diagrams/sld/index.cjs.map +1 -0
  149. package/dist/diagrams/sld/index.d.cts +58 -0
  150. package/dist/diagrams/sld/index.d.ts +58 -0
  151. package/dist/diagrams/sld/index.js +5 -0
  152. package/dist/diagrams/sld/index.js.map +1 -0
  153. package/dist/diagrams/sociogram/index.cjs +26 -0
  154. package/dist/diagrams/sociogram/index.cjs.map +1 -0
  155. package/dist/diagrams/sociogram/index.d.cts +76 -0
  156. package/dist/diagrams/sociogram/index.d.ts +76 -0
  157. package/dist/diagrams/sociogram/index.js +5 -0
  158. package/dist/diagrams/sociogram/index.js.map +1 -0
  159. package/dist/diagrams/timing/index.cjs +21 -0
  160. package/dist/diagrams/timing/index.cjs.map +1 -0
  161. package/dist/diagrams/timing/index.d.cts +9 -0
  162. package/dist/diagrams/timing/index.d.ts +9 -0
  163. package/dist/diagrams/timing/index.js +4 -0
  164. package/dist/diagrams/timing/index.js.map +1 -0
  165. package/dist/diagrams/venn/index.cjs +38 -0
  166. package/dist/diagrams/venn/index.cjs.map +1 -0
  167. package/dist/diagrams/venn/index.d.cts +69 -0
  168. package/dist/diagrams/venn/index.d.ts +69 -0
  169. package/dist/diagrams/venn/index.js +5 -0
  170. package/dist/diagrams/venn/index.js.map +1 -0
  171. package/dist/index-BSlza1YY.d.ts +150 -0
  172. package/dist/index-BXefHVce.d.cts +150 -0
  173. package/dist/index.cjs +2033 -0
  174. package/dist/index.cjs.map +1 -0
  175. package/dist/index.d.cts +29 -0
  176. package/dist/index.d.ts +29 -0
  177. package/dist/index.js +1944 -0
  178. package/dist/index.js.map +1 -0
  179. package/dist/types-DqfcYkcY.d.cts +741 -0
  180. package/dist/types-DqfcYkcY.d.ts +741 -0
  181. package/package.json +163 -0
@@ -0,0 +1,1795 @@
1
+ import { resolveGenogramTheme, STROKE_WIDTH, cssCustomProperties } from './chunk-IX554O5K.js';
2
+ import { title, text, group, el, rect, pattern, path, circle, defs, polygon, line, desc, svgRoot } from './chunk-KLJEK547.js';
3
+
4
+ // src/diagrams/genogram/parser.ts
5
+ var ParseError = class extends Error {
6
+ constructor(message, line2, column, source) {
7
+ super(`Line ${line2}, col ${column}: ${message}
8
+ \u2192 ${source}`);
9
+ this.line = line2;
10
+ this.column = column;
11
+ this.source = source;
12
+ this.name = "ParseError";
13
+ }
14
+ line;
15
+ column;
16
+ source;
17
+ };
18
+ var COUPLE_OPS = [
19
+ { token: "-x-", type: "divorced" },
20
+ { token: "-/-", type: "separated" },
21
+ { token: "-o-", type: "engaged" },
22
+ { token: "==", type: "consanguineous" },
23
+ { token: "--", type: "married" },
24
+ { token: "~", type: "cohabiting" }
25
+ ];
26
+ var VALID_SEX = /* @__PURE__ */ new Set(["male", "female", "unknown", "other"]);
27
+ var VALID_STATUS = /* @__PURE__ */ new Set([
28
+ "deceased",
29
+ "stillborn",
30
+ "miscarriage",
31
+ "abortion"
32
+ ]);
33
+ var SPECIAL_CHILD_PROPS = /* @__PURE__ */ new Set([
34
+ "adopted",
35
+ "foster",
36
+ "twin-identical",
37
+ "twin-fraternal"
38
+ ]);
39
+ var VALID_FILLS = /* @__PURE__ */ new Set([
40
+ "full",
41
+ "half-left",
42
+ "half-right",
43
+ "half-top",
44
+ "half-bottom",
45
+ "quad-tl",
46
+ "quad-tr",
47
+ "quad-bl",
48
+ "quad-br",
49
+ "quarter",
50
+ "striped",
51
+ "dotted"
52
+ ]);
53
+ var EMOTIONAL_TYPES = /* @__PURE__ */ new Set([
54
+ "harmony",
55
+ "close",
56
+ "bestfriends",
57
+ "love",
58
+ "inlove",
59
+ "friendship",
60
+ "hostile",
61
+ "conflict",
62
+ "enmity",
63
+ "distant-hostile",
64
+ "cutoff",
65
+ "close-hostile",
66
+ "fused",
67
+ "fused-hostile",
68
+ "distant",
69
+ "normal",
70
+ "nevermet",
71
+ "abuse",
72
+ "physical-abuse",
73
+ "emotional-abuse",
74
+ "sexual-abuse",
75
+ "neglect",
76
+ "manipulative",
77
+ "controlling",
78
+ "jealous",
79
+ "focused",
80
+ "focused-neg",
81
+ "distrust",
82
+ "admirer",
83
+ "limerence"
84
+ ]);
85
+ function parseGenogram(text2) {
86
+ const rawLines = text2.split("\n");
87
+ const state = { lines: rawLines, currentLine: 0 };
88
+ skipBlankAndComments(state);
89
+ const metadata = {};
90
+ const headerLine = currentLineText(state);
91
+ if (headerLine === void 0) {
92
+ throw new ParseError("Empty input", 1, 1, "");
93
+ }
94
+ const headerTrimmed = headerLine.trim();
95
+ if (!headerTrimmed.toLowerCase().startsWith("genogram")) {
96
+ throw new ParseError(
97
+ 'Expected "genogram" header',
98
+ state.currentLine + 1,
99
+ 1,
100
+ headerLine
101
+ );
102
+ }
103
+ const titleMatch = headerTrimmed.match(/^genogram\s+"([^"]*)"$/i);
104
+ if (titleMatch) {
105
+ metadata.title = titleMatch[1];
106
+ }
107
+ state.currentLine++;
108
+ const individualsMap = /* @__PURE__ */ new Map();
109
+ const relationships = [];
110
+ const childSpecialProps = /* @__PURE__ */ new Map();
111
+ skipBlankAndComments(state);
112
+ while (state.currentLine < state.lines.length) {
113
+ skipBlankAndComments(state);
114
+ if (state.currentLine >= state.lines.length) break;
115
+ const lineText = state.lines[state.currentLine];
116
+ const trimmed = lineText.trim();
117
+ if (trimmed === "" || trimmed.startsWith("#")) {
118
+ state.currentLine++;
119
+ continue;
120
+ }
121
+ const emotionalMatch = detectEmotionalOp(trimmed);
122
+ if (emotionalMatch) {
123
+ const { leftId, emotionalType, rightId: emRightId, directional, label: emLabel } = emotionalMatch;
124
+ const lineNum = state.currentLine + 1;
125
+ const leftKey = leftId.toLowerCase();
126
+ const rightKey = emRightId.toLowerCase();
127
+ if (!individualsMap.has(leftKey)) {
128
+ throw new ParseError(`Unknown individual '${leftId}'`, lineNum, 1, lineText);
129
+ }
130
+ if (!individualsMap.has(rightKey)) {
131
+ throw new ParseError(`Unknown individual '${emRightId}'`, lineNum, 1, lineText);
132
+ }
133
+ const rel = {
134
+ type: emotionalType,
135
+ from: leftKey,
136
+ to: rightKey
137
+ };
138
+ if (directional) rel.directional = true;
139
+ if (emLabel) rel.label = emLabel;
140
+ relationships.push(rel);
141
+ state.currentLine++;
142
+ continue;
143
+ }
144
+ const coupleMatch = detectCoupleOp(trimmed);
145
+ if (coupleMatch) {
146
+ const { leftId, op, rightRaw } = coupleMatch;
147
+ const lineNum = state.currentLine + 1;
148
+ const { cleaned: rightCleaned, label: relLabel } = extractRelLabel(rightRaw);
149
+ const { id: rightId, props: rightProps } = parseIdWithOptionalProps(rightCleaned);
150
+ const leftKey = leftId.toLowerCase();
151
+ if (!individualsMap.has(leftKey)) {
152
+ throw new ParseError(
153
+ `Unknown individual '${leftId}'`,
154
+ lineNum,
155
+ 1,
156
+ lineText
157
+ );
158
+ }
159
+ const rightKey = rightId.toLowerCase();
160
+ if (rightProps) {
161
+ const rightIndividual = buildIndividual(rightId, rightProps, lineNum, lineText);
162
+ individualsMap.set(rightKey, rightIndividual);
163
+ } else if (!individualsMap.has(rightKey)) {
164
+ throw new ParseError(
165
+ `Unknown individual '${rightId}'`,
166
+ lineNum,
167
+ 1,
168
+ lineText
169
+ );
170
+ }
171
+ const rel = { type: op.type, from: leftKey, to: rightKey };
172
+ if (relLabel) rel.label = relLabel;
173
+ relationships.push(rel);
174
+ const coupleIndent = getIndent(lineText);
175
+ state.currentLine++;
176
+ while (state.currentLine < state.lines.length) {
177
+ const childLine = state.lines[state.currentLine];
178
+ const childTrimmed = childLine.trim();
179
+ if (childTrimmed === "" || childTrimmed.startsWith("#")) {
180
+ state.currentLine++;
181
+ continue;
182
+ }
183
+ const childIndent = getIndent(childLine);
184
+ if (childIndent <= coupleIndent) break;
185
+ const childLineNum = state.currentLine + 1;
186
+ const { id: childId, propsStr } = splitIdAndProps(childTrimmed);
187
+ const childKey = childId.toLowerCase();
188
+ const individual = buildIndividual(
189
+ childId,
190
+ propsStr,
191
+ childLineNum,
192
+ childLine
193
+ );
194
+ if (propsStr) {
195
+ for (const sp of SPECIAL_CHILD_PROPS) {
196
+ if (propsTokens(propsStr).includes(sp)) {
197
+ childSpecialProps.set(childKey, sp);
198
+ }
199
+ }
200
+ }
201
+ individualsMap.set(childKey, individual);
202
+ const coupleKey = `${leftKey}+${rightKey}`;
203
+ const pcType = childSpecialProps.get(childKey);
204
+ const relType = pcType && (pcType === "adopted" || pcType === "foster") ? pcType : "parent-child";
205
+ relationships.push({ type: relType, from: coupleKey, to: childKey });
206
+ state.currentLine++;
207
+ }
208
+ } else {
209
+ const lineNum = state.currentLine + 1;
210
+ const { id, propsStr } = splitIdAndProps(trimmed);
211
+ const key = id.toLowerCase();
212
+ const individual = buildIndividual(id, propsStr, lineNum, lineText);
213
+ const existing = individualsMap.get(key);
214
+ if (existing) {
215
+ individualsMap.set(key, mergeIndividual(existing, individual));
216
+ } else {
217
+ individualsMap.set(key, individual);
218
+ }
219
+ state.currentLine++;
220
+ }
221
+ }
222
+ const twinGroups = /* @__PURE__ */ new Map();
223
+ for (const [childKey, prop] of childSpecialProps) {
224
+ if (prop === "twin-identical" || prop === "twin-fraternal") {
225
+ const pcRel = relationships.find(
226
+ (r) => r.to === childKey && (r.type === "parent-child" || r.type === "adopted" || r.type === "foster")
227
+ );
228
+ if (pcRel) {
229
+ const groupKey = `${pcRel.from}:${prop}`;
230
+ const group2 = twinGroups.get(groupKey) ?? [];
231
+ group2.push(childKey);
232
+ twinGroups.set(groupKey, group2);
233
+ }
234
+ }
235
+ }
236
+ for (const [groupKey, members] of twinGroups) {
237
+ const twinType = groupKey.split(":")[1];
238
+ for (let i = 0; i < members.length - 1; i++) {
239
+ relationships.push({
240
+ type: twinType,
241
+ from: members[i],
242
+ to: members[i + 1]
243
+ });
244
+ }
245
+ }
246
+ return {
247
+ type: "genogram",
248
+ individuals: Array.from(individualsMap.values()),
249
+ relationships,
250
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0
251
+ };
252
+ }
253
+ function currentLineText(state) {
254
+ return state.lines[state.currentLine];
255
+ }
256
+ function skipBlankAndComments(state) {
257
+ while (state.currentLine < state.lines.length) {
258
+ const t = state.lines[state.currentLine].trim();
259
+ if (t === "" || t.startsWith("#")) {
260
+ state.currentLine++;
261
+ } else {
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ function getIndent(line2) {
267
+ const match = line2.match(/^(\s*)/);
268
+ return match ? match[1].length : 0;
269
+ }
270
+ function detectEmotionalOp(trimmed) {
271
+ const match = trimmed.match(
272
+ /^([a-zA-Z][a-zA-Z0-9_-]*)\s+-([\w-]+)->(.*)|^([a-zA-Z][a-zA-Z0-9_-]*)\s+-([\w-]+)-\s+(.*)/
273
+ );
274
+ if (!match) return null;
275
+ const directional = !!match[1];
276
+ const leftId = directional ? match[1] : match[4];
277
+ const emotionalType = directional ? match[2] : match[5];
278
+ const rest = (directional ? match[3] : match[6]).trim();
279
+ if (!EMOTIONAL_TYPES.has(emotionalType)) return null;
280
+ const { id: rightId, label } = extractIdAndLabel(rest);
281
+ if (!rightId || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(rightId)) return null;
282
+ return { leftId, emotionalType, rightId, directional, label };
283
+ }
284
+ function extractIdAndLabel(raw) {
285
+ const labelMatch = raw.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s+"([^"]*)"$/);
286
+ if (labelMatch) return { id: labelMatch[1], label: labelMatch[2] };
287
+ const idOnly = raw.match(/^([a-zA-Z][a-zA-Z0-9_-]*)$/);
288
+ if (idOnly) return { id: idOnly[1], label: null };
289
+ return { id: raw.trim(), label: null };
290
+ }
291
+ function extractRelLabel(rightRaw) {
292
+ const match = rightRaw.match(/^(.*?)\s+"([^"]*)"$/);
293
+ if (match) return { cleaned: match[1].trim(), label: match[2] };
294
+ return { cleaned: rightRaw, label: null };
295
+ }
296
+ function detectCoupleOp(trimmed) {
297
+ for (const op of COUPLE_OPS) {
298
+ const parts = splitByOperator(trimmed, op.token);
299
+ if (parts) {
300
+ return { leftId: parts.left.trim(), op, rightRaw: parts.right.trim() };
301
+ }
302
+ }
303
+ return null;
304
+ }
305
+ function splitByOperator(line2, op) {
306
+ let bracketDepth = 0;
307
+ for (let i = 0; i < line2.length; i++) {
308
+ if (line2[i] === "[") bracketDepth++;
309
+ if (line2[i] === "]") bracketDepth--;
310
+ if (bracketDepth > 0) continue;
311
+ if (line2.substring(i, i + op.length) === op) {
312
+ const left = line2.substring(0, i).trim();
313
+ const right = line2.substring(i + op.length).trim();
314
+ if (left && right) {
315
+ const leftId = left.match(/^[a-zA-Z][a-zA-Z0-9_-]*$/);
316
+ if (leftId) return { left, right };
317
+ }
318
+ }
319
+ }
320
+ return null;
321
+ }
322
+ function parseIdWithOptionalProps(raw) {
323
+ const bracketIdx = raw.indexOf("[");
324
+ if (bracketIdx === -1) {
325
+ return { id: raw.trim(), props: null };
326
+ }
327
+ const id = raw.substring(0, bracketIdx).trim();
328
+ const endBracket = raw.lastIndexOf("]");
329
+ const propsStr = raw.substring(bracketIdx + 1, endBracket === -1 ? raw.length : endBracket);
330
+ return { id, props: propsStr };
331
+ }
332
+ function splitIdAndProps(trimmed) {
333
+ const bracketIdx = trimmed.indexOf("[");
334
+ if (bracketIdx === -1) {
335
+ return { id: trimmed.trim(), propsStr: null };
336
+ }
337
+ const id = trimmed.substring(0, bracketIdx).trim();
338
+ const endBracket = trimmed.lastIndexOf("]");
339
+ const propsStr = trimmed.substring(
340
+ bracketIdx + 1,
341
+ endBracket === -1 ? trimmed.length : endBracket
342
+ );
343
+ return { id, propsStr };
344
+ }
345
+ function propsTokens(propsStr) {
346
+ return splitProps(propsStr).map((t) => t.trim().toLowerCase());
347
+ }
348
+ function splitProps(propsStr) {
349
+ const result = [];
350
+ let current = "";
351
+ let parenDepth = 0;
352
+ for (const ch of propsStr) {
353
+ if (ch === "(") parenDepth++;
354
+ if (ch === ")") parenDepth--;
355
+ if (ch === "," && parenDepth === 0) {
356
+ result.push(current);
357
+ current = "";
358
+ } else {
359
+ current += ch;
360
+ }
361
+ }
362
+ if (current.trim()) result.push(current);
363
+ return result;
364
+ }
365
+ function buildIndividual(id, propsStr, lineNum, lineText) {
366
+ const individual = {
367
+ id: id.toLowerCase(),
368
+ label: id,
369
+ sex: "unknown",
370
+ status: "alive"
371
+ };
372
+ if (!propsStr) return individual;
373
+ const tokens = splitProps(propsStr);
374
+ for (const rawToken of tokens) {
375
+ const token = rawToken.trim();
376
+ const tokenLower = token.toLowerCase();
377
+ if (VALID_SEX.has(tokenLower)) {
378
+ individual.sex = tokenLower;
379
+ } else if (VALID_STATUS.has(tokenLower)) {
380
+ individual.status = tokenLower;
381
+ } else if (tokenLower === "index") {
382
+ if (!individual.markers) individual.markers = [];
383
+ individual.markers.push("index-person");
384
+ } else if (SPECIAL_CHILD_PROPS.has(tokenLower)) {
385
+ continue;
386
+ } else if (/^\d{4}$/.test(tokenLower)) {
387
+ if (individual.birthYear !== void 0) {
388
+ individual.deathYear = parseInt(token, 10);
389
+ } else {
390
+ individual.birthYear = parseInt(token, 10);
391
+ }
392
+ } else if (tokenLower.startsWith("conditions:")) {
393
+ individual.conditions = parseConditions(
394
+ token.substring("conditions:".length).trim(),
395
+ lineNum,
396
+ lineText
397
+ );
398
+ } else if (token.includes(":")) {
399
+ const colonIdx = token.indexOf(":");
400
+ const key = token.substring(0, colonIdx).trim().toLowerCase();
401
+ const value = token.substring(colonIdx + 1).trim();
402
+ if (key === "age") {
403
+ const ageNum = parseInt(value, 10);
404
+ if (!isNaN(ageNum)) individual.age = ageNum;
405
+ } else if (key === "death") {
406
+ const deathNum = parseInt(value, 10);
407
+ if (!isNaN(deathNum)) individual.deathYear = deathNum;
408
+ } else if (key === "label") {
409
+ individual.label = value.replace(/^"|"$/g, "");
410
+ } else {
411
+ if (!individual.properties) individual.properties = {};
412
+ individual.properties[key] = value;
413
+ }
414
+ } else {
415
+ throw new ParseError(
416
+ `Unknown property '${token}'. Valid: male, female, unknown, deceased, stillborn, miscarriage, abortion, adopted, foster, twin-identical, twin-fraternal, index, a 4-digit year, conditions:..., age:N, death:YYYY, or key:value`,
417
+ lineNum,
418
+ 1,
419
+ lineText
420
+ );
421
+ }
422
+ }
423
+ return individual;
424
+ }
425
+ function parseConditions(raw, lineNum, lineText) {
426
+ const parts = raw.split("+").map((s) => s.trim());
427
+ const conditions = [];
428
+ for (const part of parts) {
429
+ const match = part.match(/^([a-zA-Z0-9_-]+)\(([^)]+)\)$/);
430
+ if (!match) {
431
+ throw new ParseError(
432
+ `Invalid condition format '${part}'. Expected: name(fill) or name(fill, #color)`,
433
+ lineNum,
434
+ 1,
435
+ lineText
436
+ );
437
+ }
438
+ const [, label, innerRaw] = match;
439
+ const innerParts = innerRaw.split(",").map((s) => s.trim());
440
+ const fill = innerParts[0];
441
+ const color = innerParts[1];
442
+ if (!VALID_FILLS.has(fill)) {
443
+ throw new ParseError(
444
+ `Invalid fill pattern '${fill}'. Valid: full, half-left, half-right, half-top, half-bottom, quad-tl, quad-tr, quad-bl, quad-br, quarter, striped, dotted`,
445
+ lineNum,
446
+ 1,
447
+ lineText
448
+ );
449
+ }
450
+ const cond = { label, fill };
451
+ if (color) cond.color = color;
452
+ conditions.push(cond);
453
+ }
454
+ return conditions;
455
+ }
456
+ function mergeIndividual(existing, incoming) {
457
+ return {
458
+ ...existing,
459
+ sex: incoming.sex !== "unknown" ? incoming.sex : existing.sex,
460
+ status: incoming.status !== "alive" ? incoming.status : existing.status,
461
+ birthYear: incoming.birthYear ?? existing.birthYear,
462
+ deathYear: incoming.deathYear ?? existing.deathYear,
463
+ conditions: incoming.conditions ?? existing.conditions,
464
+ properties: {
465
+ ...existing.properties,
466
+ ...incoming.properties
467
+ }
468
+ };
469
+ }
470
+
471
+ // src/diagrams/genogram/layout.ts
472
+ function layoutGenogram(ast, config) {
473
+ const graph = buildGraph(ast);
474
+ assignGenerations(graph);
475
+ const ordered = orderNodesInGenerations(graph);
476
+ const positions = assignPositions(ordered, graph, config);
477
+ const edges = computeEdges(graph, positions, config);
478
+ const emotionalEdges = computeEmotionalEdges(ast.relationships, positions, config);
479
+ return packageResult(positions, [...edges, ...emotionalEdges], graph, config);
480
+ }
481
+ function buildGraph(ast) {
482
+ const individuals = /* @__PURE__ */ new Map();
483
+ for (const ind of ast.individuals) {
484
+ individuals.set(ind.id, ind);
485
+ }
486
+ const familyUnits = [];
487
+ const childOf = /* @__PURE__ */ new Map();
488
+ const coupleRels = ast.relationships.filter(
489
+ (r) => r.type === "married" || r.type === "divorced" || r.type === "separated" || r.type === "engaged" || r.type === "cohabiting" || r.type === "consanguineous"
490
+ );
491
+ for (const rel of coupleRels) {
492
+ const fuId = `${rel.from}+${rel.to}`;
493
+ const indA = individuals.get(rel.from);
494
+ const indB = individuals.get(rel.to);
495
+ let partners;
496
+ if (indA && indB) {
497
+ if (indA.sex === "male" && indB.sex === "female") {
498
+ partners = [rel.from, rel.to];
499
+ } else if (indA.sex === "female" && indB.sex === "male") {
500
+ partners = [rel.to, rel.from];
501
+ } else {
502
+ const yearA = indA.birthYear ?? 9999;
503
+ const yearB = indB.birthYear ?? 9999;
504
+ partners = yearA <= yearB ? [rel.from, rel.to] : [rel.to, rel.from];
505
+ }
506
+ } else {
507
+ partners = [rel.from, rel.to];
508
+ }
509
+ const children = [];
510
+ for (const r of ast.relationships) {
511
+ if ((r.type === "parent-child" || r.type === "adopted" || r.type === "foster") && r.from === fuId) {
512
+ children.push(r.to);
513
+ childOf.set(r.to, fuId);
514
+ }
515
+ }
516
+ children.sort((a, b) => {
517
+ const indChildA = individuals.get(a);
518
+ const indChildB = individuals.get(b);
519
+ return (indChildA?.birthYear ?? 9999) - (indChildB?.birthYear ?? 9999);
520
+ });
521
+ familyUnits.push({
522
+ id: fuId,
523
+ partners,
524
+ relationship: rel.type,
525
+ label: rel.label,
526
+ children
527
+ });
528
+ }
529
+ return { individuals, familyUnits, generations: /* @__PURE__ */ new Map(), childOf };
530
+ }
531
+ function assignGenerations(graph) {
532
+ const { individuals, familyUnits, childOf, generations } = graph;
533
+ const allIds = Array.from(individuals.keys());
534
+ const roots = allIds.filter((id) => !childOf.has(id));
535
+ if (roots.length === 0 && allIds.length > 0) {
536
+ for (const id of allIds) generations.set(id, 0);
537
+ return;
538
+ }
539
+ for (const root of roots) {
540
+ if (!generations.has(root)) {
541
+ generations.set(root, 0);
542
+ }
543
+ }
544
+ let changed = true;
545
+ while (changed) {
546
+ changed = false;
547
+ for (const fu of familyUnits) {
548
+ const gen0 = generations.get(fu.partners[0]);
549
+ const gen1 = generations.get(fu.partners[1]);
550
+ let partnerGen;
551
+ if (gen0 !== void 0 && gen1 !== void 0) {
552
+ partnerGen = Math.max(gen0, gen1);
553
+ if (gen0 !== partnerGen) {
554
+ generations.set(fu.partners[0], partnerGen);
555
+ changed = true;
556
+ }
557
+ if (gen1 !== partnerGen) {
558
+ generations.set(fu.partners[1], partnerGen);
559
+ changed = true;
560
+ }
561
+ } else if (gen0 !== void 0) {
562
+ partnerGen = gen0;
563
+ generations.set(fu.partners[1], partnerGen);
564
+ changed = true;
565
+ } else if (gen1 !== void 0) {
566
+ partnerGen = gen1;
567
+ generations.set(fu.partners[0], partnerGen);
568
+ changed = true;
569
+ } else {
570
+ continue;
571
+ }
572
+ for (const childId of fu.children) {
573
+ const childGen = partnerGen + 1;
574
+ const existing = generations.get(childId);
575
+ if (existing === void 0 || existing < childGen) {
576
+ generations.set(childId, childGen);
577
+ changed = true;
578
+ }
579
+ }
580
+ }
581
+ }
582
+ for (const id of allIds) {
583
+ if (!generations.has(id)) {
584
+ generations.set(id, 0);
585
+ }
586
+ }
587
+ }
588
+ function orderNodesInGenerations(graph, _config) {
589
+ const { generations, familyUnits } = graph;
590
+ const genGroups = /* @__PURE__ */ new Map();
591
+ for (const [id, gen] of generations) {
592
+ const group2 = genGroups.get(gen) ?? [];
593
+ group2.push(id);
594
+ genGroups.set(gen, group2);
595
+ }
596
+ const genIndices = Array.from(genGroups.keys()).sort((a, b) => a - b);
597
+ const result = [];
598
+ for (const genIdx of genIndices) {
599
+ const nodeIds = genGroups.get(genIdx) ?? [];
600
+ const ordered = orderGeneration(nodeIds, genIdx, graph, familyUnits);
601
+ result.push({ index: genIdx, nodeIds: ordered });
602
+ }
603
+ return result;
604
+ }
605
+ function orderGeneration(nodeIds, _genIdx, graph, familyUnits) {
606
+ if (nodeIds.length <= 1) return [...nodeIds];
607
+ const nodeSet = new Set(nodeIds);
608
+ const placed = /* @__PURE__ */ new Set();
609
+ const ordered = [];
610
+ const fuInGen = familyUnits.filter(
611
+ (fu) => nodeSet.has(fu.partners[0]) || nodeSet.has(fu.partners[1])
612
+ );
613
+ const personToFUs = /* @__PURE__ */ new Map();
614
+ for (const fu of fuInGen) {
615
+ for (const p of fu.partners) {
616
+ if (nodeSet.has(p)) {
617
+ const arr = personToFUs.get(p) ?? [];
618
+ arr.push(fu);
619
+ personToFUs.set(p, arr);
620
+ }
621
+ }
622
+ }
623
+ function placePersonAndExpand(id) {
624
+ if (placed.has(id)) return;
625
+ ordered.push(id);
626
+ placed.add(id);
627
+ const fus = personToFUs.get(id) ?? [];
628
+ const sortedFUs = [...fus].sort((a, b) => {
629
+ const scoreA = a.relationship === "divorced" || a.relationship === "separated" ? 0 : 1;
630
+ const scoreB = b.relationship === "divorced" || b.relationship === "separated" ? 0 : 1;
631
+ return scoreA - scoreB;
632
+ });
633
+ for (const fu of sortedFUs) {
634
+ const partner = fu.partners[0] === id ? fu.partners[1] : fu.partners[0];
635
+ if (!placed.has(partner) && nodeSet.has(partner)) {
636
+ ordered.push(partner);
637
+ placed.add(partner);
638
+ const partnerFUs = personToFUs.get(partner) ?? [];
639
+ for (const pfu of partnerFUs) {
640
+ if (pfu.id !== fu.id) {
641
+ const otherPartner = pfu.partners[0] === partner ? pfu.partners[1] : pfu.partners[0];
642
+ placePersonAndExpand(otherPartner);
643
+ }
644
+ }
645
+ }
646
+ }
647
+ }
648
+ for (const fu of fuInGen) {
649
+ if (!placed.has(fu.partners[0]) && nodeSet.has(fu.partners[0])) {
650
+ placePersonAndExpand(fu.partners[0]);
651
+ }
652
+ if (!placed.has(fu.partners[1]) && nodeSet.has(fu.partners[1])) {
653
+ placePersonAndExpand(fu.partners[1]);
654
+ }
655
+ }
656
+ const remaining = nodeIds.filter((id) => !placed.has(id));
657
+ remaining.sort((a, b) => {
658
+ const indA = graph.individuals.get(a);
659
+ const indB = graph.individuals.get(b);
660
+ return (indA?.birthYear ?? 9999) - (indB?.birthYear ?? 9999);
661
+ });
662
+ for (const id of remaining) {
663
+ ordered.push(id);
664
+ placed.add(id);
665
+ }
666
+ return ordered;
667
+ }
668
+ var LABEL_HEIGHT = 20;
669
+ var LABEL_GAP = 6;
670
+ function assignPositions(orderedGens, graph, config) {
671
+ const positions = /* @__PURE__ */ new Map();
672
+ const { nodeWidth, nodeSpacingX } = config;
673
+ const half = nodeWidth / 2;
674
+ const coupleGap = nodeWidth + nodeSpacingX * 0.6;
675
+ const familyGap = nodeWidth + nodeSpacingX * 1.5;
676
+ const genStepY = config.nodeHeight + LABEL_HEIGHT + LABEL_GAP + config.nodeSpacingY;
677
+ for (const gen of orderedGens) {
678
+ const y = gen.index * genStepY + half;
679
+ const segments = buildSegments(gen.nodeIds, gen.index, graph);
680
+ let xCursor = half;
681
+ for (let s = 0; s < segments.length; s++) {
682
+ const seg = segments[s];
683
+ if (s > 0) xCursor += familyGap;
684
+ if (seg.type === "couple") {
685
+ positions.set(seg.ids[0], { id: seg.ids[0], x: xCursor, y, generation: gen.index });
686
+ xCursor += coupleGap;
687
+ positions.set(seg.ids[1], { id: seg.ids[1], x: xCursor, y, generation: gen.index });
688
+ } else {
689
+ positions.set(seg.ids[0], { id: seg.ids[0], x: xCursor, y, generation: gen.index });
690
+ }
691
+ }
692
+ }
693
+ resolveOverlaps(positions, orderedGens, config);
694
+ centerChildrenUnderParents(positions, graph, config);
695
+ resolveOverlaps(positions, orderedGens, config);
696
+ return positions;
697
+ }
698
+ function buildSegments(nodeIds, _genIdx, graph) {
699
+ const nodeSet = new Set(nodeIds);
700
+ const personFUs = /* @__PURE__ */ new Map();
701
+ for (const fu of graph.familyUnits) {
702
+ if (nodeSet.has(fu.partners[0]) && nodeSet.has(fu.partners[1])) {
703
+ for (const p of fu.partners) {
704
+ const arr = personFUs.get(p) ?? [];
705
+ arr.push(fu);
706
+ personFUs.set(p, arr);
707
+ }
708
+ }
709
+ }
710
+ const multiMarried = /* @__PURE__ */ new Set();
711
+ for (const [id, fus] of personFUs) {
712
+ if (fus.length > 1) multiMarried.add(id);
713
+ }
714
+ const segments = [];
715
+ const placed = /* @__PURE__ */ new Set();
716
+ for (const id of nodeIds) {
717
+ if (placed.has(id)) continue;
718
+ if (multiMarried.has(id)) {
719
+ const fus = personFUs.get(id) ?? [];
720
+ const sorted = [...fus].sort((a, b) => {
721
+ const sa = a.relationship === "divorced" || a.relationship === "separated" ? 0 : 1;
722
+ const sb = b.relationship === "divorced" || b.relationship === "separated" ? 0 : 1;
723
+ return sa - sb;
724
+ });
725
+ for (const fu2 of sorted) {
726
+ const partner = fu2.partners[0] === id ? fu2.partners[1] : fu2.partners[0];
727
+ if (!placed.has(partner)) {
728
+ if (!placed.has(id)) {
729
+ segments.push({ type: "couple", ids: [partner, id], fuId: fu2.id });
730
+ placed.add(partner);
731
+ placed.add(id);
732
+ } else {
733
+ segments.push({ type: "couple", ids: [id, partner], fuId: fu2.id });
734
+ placed.add(partner);
735
+ }
736
+ }
737
+ }
738
+ if (!placed.has(id)) {
739
+ segments.push({ type: "single", ids: [id] });
740
+ placed.add(id);
741
+ }
742
+ continue;
743
+ }
744
+ const fu = graph.familyUnits.find(
745
+ (f) => (f.partners[0] === id || f.partners[1] === id) && !placed.has(f.partners[0]) && !placed.has(f.partners[1]) && nodeSet.has(f.partners[0]) && nodeSet.has(f.partners[1])
746
+ );
747
+ if (fu) {
748
+ segments.push({
749
+ type: "couple",
750
+ ids: [fu.partners[0], fu.partners[1]],
751
+ fuId: fu.id
752
+ });
753
+ placed.add(fu.partners[0]);
754
+ placed.add(fu.partners[1]);
755
+ } else {
756
+ segments.push({ type: "single", ids: [id] });
757
+ placed.add(id);
758
+ }
759
+ }
760
+ return segments;
761
+ }
762
+ function centerChildrenUnderParents(positions, graph, config) {
763
+ const coupleGap = config.nodeSpacingX + config.nodeWidth;
764
+ const personToFUs = /* @__PURE__ */ new Map();
765
+ for (const fu of graph.familyUnits) {
766
+ for (const p of fu.partners) {
767
+ const arr = personToFUs.get(p) ?? [];
768
+ arr.push(fu);
769
+ personToFUs.set(p, arr);
770
+ }
771
+ }
772
+ for (let pass = 0; pass < 3; pass++) {
773
+ for (const fu of graph.familyUnits) {
774
+ if (fu.children.length === 0) continue;
775
+ const posA = positions.get(fu.partners[0]);
776
+ const posB = positions.get(fu.partners[1]);
777
+ if (!posA || !posB) continue;
778
+ const parentMidX = (posA.x + posB.x) / 2;
779
+ const sortedChildren = [...fu.children].sort((a, b) => {
780
+ const indA = graph.individuals.get(a);
781
+ const indB = graph.individuals.get(b);
782
+ return (indA?.birthYear ?? 9999) - (indB?.birthYear ?? 9999);
783
+ });
784
+ const childSpacing = config.nodeSpacingX + config.nodeWidth;
785
+ if (sortedChildren.length > 1) {
786
+ const gaps = [];
787
+ for (let gi = 0; gi < sortedChildren.length - 1; gi++) {
788
+ let gap = childSpacing;
789
+ const nextChild = sortedChildren[gi + 1];
790
+ const nextFUs = personToFUs.get(nextChild) ?? [];
791
+ for (const cfu of nextFUs) {
792
+ const pid = cfu.partners[0] === nextChild ? cfu.partners[1] : cfu.partners[0];
793
+ const cInd = graph.individuals.get(nextChild);
794
+ const pInd = graph.individuals.get(pid);
795
+ if (!(cInd?.sex === "male" || cInd?.sex !== "female" && pInd?.sex === "female")) {
796
+ gap += coupleGap;
797
+ }
798
+ }
799
+ const currChild = sortedChildren[gi];
800
+ const currFUs = personToFUs.get(currChild) ?? [];
801
+ for (const cfu of currFUs) {
802
+ const pid = cfu.partners[0] === currChild ? cfu.partners[1] : cfu.partners[0];
803
+ const cInd = graph.individuals.get(currChild);
804
+ const pInd = graph.individuals.get(pid);
805
+ if (cInd?.sex === "male" || cInd?.sex !== "female" && pInd?.sex === "female") {
806
+ gap += coupleGap;
807
+ }
808
+ }
809
+ gaps.push(gap);
810
+ }
811
+ const totalWidth = gaps.reduce((s, g) => s + g, 0);
812
+ const startX = parentMidX - totalWidth / 2;
813
+ let cx = startX;
814
+ for (let i = 0; i < sortedChildren.length; i++) {
815
+ const pos = positions.get(sortedChildren[i]);
816
+ if (pos) pos.x = cx;
817
+ if (i < gaps.length) cx += gaps[i];
818
+ }
819
+ } else {
820
+ const pos = positions.get(sortedChildren[0]);
821
+ if (pos) pos.x = parentMidX;
822
+ }
823
+ for (const childId of fu.children) {
824
+ const childPos = positions.get(childId);
825
+ if (!childPos) continue;
826
+ const childFUs = personToFUs.get(childId) ?? [];
827
+ for (const childFU of childFUs) {
828
+ const partnerId = childFU.partners[0] === childId ? childFU.partners[1] : childFU.partners[0];
829
+ const partnerPos = positions.get(partnerId);
830
+ if (!partnerPos) continue;
831
+ const childInd = graph.individuals.get(childId);
832
+ const partnerInd = graph.individuals.get(partnerId);
833
+ if (childInd?.sex === "male" || childInd?.sex !== "female" && partnerInd?.sex === "female") {
834
+ partnerPos.x = childPos.x + coupleGap;
835
+ } else {
836
+ partnerPos.x = childPos.x - coupleGap;
837
+ }
838
+ }
839
+ }
840
+ }
841
+ }
842
+ }
843
+ function resolveOverlaps(positions, orderedGens, config) {
844
+ const minGap = config.nodeWidth + config.nodeSpacingX;
845
+ for (const gen of orderedGens) {
846
+ const genNodes = gen.nodeIds.map((id) => positions.get(id)).filter((p) => p !== void 0);
847
+ genNodes.sort((a, b) => a.x - b.x);
848
+ for (let i = 1; i < genNodes.length; i++) {
849
+ const gap = genNodes[i].x - genNodes[i - 1].x;
850
+ if (gap < minGap) {
851
+ const shift = minGap - gap;
852
+ for (let j = i; j < genNodes.length; j++) {
853
+ genNodes[j].x += shift;
854
+ }
855
+ }
856
+ }
857
+ }
858
+ let minX = Infinity;
859
+ for (const pos of positions.values()) {
860
+ if (pos.x < minX) minX = pos.x;
861
+ }
862
+ if (minX < config.nodeWidth / 2) {
863
+ const shift = config.nodeWidth / 2 - minX;
864
+ for (const pos of positions.values()) {
865
+ pos.x += shift;
866
+ }
867
+ }
868
+ }
869
+ function computeEdges(graph, positions, config) {
870
+ const edges = [];
871
+ const half = config.nodeWidth / 2;
872
+ const dropY_offset = config.nodeHeight / 2 + LABEL_HEIGHT + LABEL_GAP + config.nodeSpacingY * 0.35;
873
+ for (const fu of graph.familyUnits) {
874
+ const posA = positions.get(fu.partners[0]);
875
+ const posB = positions.get(fu.partners[1]);
876
+ if (!posA || !posB) continue;
877
+ const leftPos = posA.x < posB.x ? posA : posB;
878
+ const rightPos = posA.x < posB.x ? posB : posA;
879
+ const leftId = posA.x < posB.x ? fu.partners[0] : fu.partners[1];
880
+ const rightId = posA.x < posB.x ? fu.partners[1] : fu.partners[0];
881
+ const coupleRel = {
882
+ type: fu.relationship,
883
+ from: leftId,
884
+ to: rightId,
885
+ label: fu.label
886
+ };
887
+ const couplePath = `M ${leftPos.x + half} ${leftPos.y} L ${rightPos.x - half} ${rightPos.y}`;
888
+ edges.push({
889
+ from: leftId,
890
+ to: rightId,
891
+ relationship: coupleRel,
892
+ path: couplePath
893
+ });
894
+ if (fu.children.length > 0) {
895
+ const midX = (posA.x + posB.x) / 2;
896
+ const coupleY = posA.y;
897
+ const dropY = coupleY + dropY_offset;
898
+ const childPositions = fu.children.map((cid) => ({
899
+ id: cid,
900
+ pos: positions.get(cid)
901
+ })).filter(
902
+ (c) => c.pos !== void 0
903
+ );
904
+ if (childPositions.length === 0) continue;
905
+ childPositions.sort((a, b) => a.pos.x - b.pos.x);
906
+ const leftX = childPositions[0].pos.x;
907
+ const rightX = childPositions[childPositions.length - 1].pos.x;
908
+ const dropPath = `M ${midX} ${coupleY} L ${midX} ${dropY}`;
909
+ edges.push({
910
+ from: fu.partners[0],
911
+ to: fu.partners[1],
912
+ relationship: { type: "parent-child", from: fu.id, to: "_drop" },
913
+ path: dropPath
914
+ });
915
+ if (childPositions.length > 1) {
916
+ const sibPath = `M ${leftX} ${dropY} L ${rightX} ${dropY}`;
917
+ edges.push({
918
+ from: fu.partners[0],
919
+ to: fu.partners[1],
920
+ relationship: { type: "parent-child", from: fu.id, to: "_sibship" },
921
+ path: sibPath
922
+ });
923
+ }
924
+ for (const child of childPositions) {
925
+ const childTop = child.pos.y - config.nodeHeight / 2;
926
+ let childPath;
927
+ if (childPositions.length === 1) {
928
+ if (Math.abs(child.pos.x - midX) < 1) {
929
+ childPath = `M ${midX} ${coupleY} L ${midX} ${childTop}`;
930
+ } else {
931
+ childPath = `M ${midX} ${dropY} L ${child.pos.x} ${dropY} L ${child.pos.x} ${childTop}`;
932
+ }
933
+ } else {
934
+ childPath = `M ${child.pos.x} ${dropY} L ${child.pos.x} ${childTop}`;
935
+ }
936
+ const pcRel = findParentChildRel(graph, fu.id, child.id);
937
+ edges.push({
938
+ from: fu.id,
939
+ to: child.id,
940
+ relationship: pcRel ?? {
941
+ type: "parent-child",
942
+ from: fu.id,
943
+ to: child.id
944
+ },
945
+ path: childPath
946
+ });
947
+ }
948
+ }
949
+ }
950
+ return edges;
951
+ }
952
+ function findParentChildRel(graph, _fuId, childId) {
953
+ const fuId = graph.childOf.get(childId);
954
+ if (!fuId) return void 0;
955
+ return {
956
+ type: "parent-child",
957
+ from: fuId,
958
+ to: childId
959
+ };
960
+ }
961
+ function packageResult(positions, edges, graph, config) {
962
+ const padding = 40;
963
+ const nodes = [];
964
+ for (const [id, pos] of positions) {
965
+ const ind = graph.individuals.get(id);
966
+ if (!ind) continue;
967
+ nodes.push({
968
+ id,
969
+ x: pos.x - config.nodeWidth / 2 + padding,
970
+ y: pos.y - config.nodeHeight / 2 + padding,
971
+ width: config.nodeWidth,
972
+ height: config.nodeHeight,
973
+ generation: pos.generation,
974
+ individual: ind
975
+ });
976
+ }
977
+ const shiftedEdges = edges.map((e) => ({
978
+ ...e,
979
+ path: shiftPath(e.path, padding, padding)
980
+ }));
981
+ let maxX = 0;
982
+ let maxY = 0;
983
+ let minX = Infinity;
984
+ for (const node of nodes) {
985
+ const cx = node.x + node.width / 2;
986
+ const labelText = estimateLabelText(node.individual);
987
+ const labelHalfWidth = labelText.length * 3.8;
988
+ const right = Math.max(node.x + node.width, cx + labelHalfWidth);
989
+ const left = Math.min(node.x, cx - labelHalfWidth);
990
+ const bottom = node.y + node.height;
991
+ if (right > maxX) maxX = right;
992
+ if (bottom > maxY) maxY = bottom;
993
+ if (left < minX) minX = left;
994
+ }
995
+ if (minX < 0) {
996
+ const shift = -minX;
997
+ for (const node of nodes) {
998
+ node.x += shift;
999
+ }
1000
+ for (const edge of shiftedEdges) {
1001
+ edge.path = shiftPath(edge.path, shift, 0);
1002
+ }
1003
+ maxX += shift;
1004
+ }
1005
+ return {
1006
+ width: maxX + padding,
1007
+ height: maxY + padding + LABEL_GAP + LABEL_HEIGHT + 10,
1008
+ nodes,
1009
+ edges: shiftedEdges
1010
+ };
1011
+ }
1012
+ function estimateLabelText(ind) {
1013
+ const name = ind.label || ind.id;
1014
+ if (ind.birthYear && ind.deathYear) return `${name} (${ind.birthYear}\u2013${ind.deathYear})`;
1015
+ if (ind.birthYear) return `${name} (b. ${ind.birthYear})`;
1016
+ return name;
1017
+ }
1018
+ var EMOTIONAL_REL_TYPES = /* @__PURE__ */ new Set([
1019
+ "harmony",
1020
+ "close",
1021
+ "bestfriends",
1022
+ "love",
1023
+ "inlove",
1024
+ "friendship",
1025
+ "hostile",
1026
+ "conflict",
1027
+ "enmity",
1028
+ "distant-hostile",
1029
+ "cutoff",
1030
+ "close-hostile",
1031
+ "fused",
1032
+ "fused-hostile",
1033
+ "distant",
1034
+ "normal",
1035
+ "nevermet",
1036
+ "abuse",
1037
+ "physical-abuse",
1038
+ "emotional-abuse",
1039
+ "sexual-abuse",
1040
+ "neglect",
1041
+ "manipulative",
1042
+ "controlling",
1043
+ "jealous",
1044
+ "focused",
1045
+ "focused-neg",
1046
+ "distrust",
1047
+ "admirer",
1048
+ "limerence"
1049
+ ]);
1050
+ function computeEmotionalEdges(relationships, positions, config) {
1051
+ const edges = [];
1052
+ const half = config.nodeWidth / 2;
1053
+ const emotionalRels = relationships.filter((r) => EMOTIONAL_REL_TYPES.has(r.type));
1054
+ for (const rel of emotionalRels) {
1055
+ const posA = positions.get(rel.from);
1056
+ const posB = positions.get(rel.to);
1057
+ if (!posA || !posB) continue;
1058
+ const ax = posA.x;
1059
+ const ay = posA.y;
1060
+ const bx = posB.x;
1061
+ const by = posB.y;
1062
+ let pathData;
1063
+ if (posA.generation === posB.generation) {
1064
+ const midX = (ax + bx) / 2;
1065
+ const curveY = ay + half + 30;
1066
+ pathData = `M ${ax} ${ay + half} Q ${midX} ${curveY} ${bx} ${by + half}`;
1067
+ } else {
1068
+ const midY = (ay + by) / 2;
1069
+ const maxX = Math.max(ax, bx);
1070
+ const curveX = maxX + half + 30;
1071
+ pathData = `M ${ax} ${ay} Q ${curveX} ${midY} ${bx} ${by}`;
1072
+ }
1073
+ edges.push({
1074
+ from: rel.from,
1075
+ to: rel.to,
1076
+ relationship: rel,
1077
+ path: pathData
1078
+ });
1079
+ }
1080
+ return edges;
1081
+ }
1082
+ function shiftPath(pathData, dx, dy) {
1083
+ return pathData.replace(
1084
+ /([\d.-]+)\s+([\d.-]+)/g,
1085
+ (_match, xStr, yStr) => {
1086
+ const x = parseFloat(xStr) + dx;
1087
+ const y = parseFloat(yStr) + dy;
1088
+ return `${x} ${y}`;
1089
+ }
1090
+ );
1091
+ }
1092
+
1093
+ // src/diagrams/genogram/symbols.ts
1094
+ function renderIndividualSymbol(individual, x, y, size) {
1095
+ const half = size / 2;
1096
+ const classes = [
1097
+ "schematex-genogram-node",
1098
+ `schematex-genogram-${individual.sex === "other" ? "unknown" : individual.sex}`
1099
+ ];
1100
+ if (individual.status === "deceased") classes.push("schematex-genogram-deceased");
1101
+ const titleText = formatTitle(individual);
1102
+ const children = [title(titleText)];
1103
+ const isIndex = individual.markers?.includes("index-person");
1104
+ if (isIndex) {
1105
+ children.push(indexBorder(individual.sex, half));
1106
+ classes.push("schematex-genogram-index-person");
1107
+ }
1108
+ children.push(baseShape(individual.sex, half));
1109
+ if (individual.conditions?.length) {
1110
+ for (const cond of individual.conditions) {
1111
+ children.push(conditionFillElement(individual.sex, half, cond));
1112
+ }
1113
+ }
1114
+ if (individual.status === "deceased") {
1115
+ children.push(...deceasedOverlay(individual.sex, half));
1116
+ }
1117
+ const ageToShow = individual.age ?? calcAge(individual);
1118
+ if (ageToShow !== void 0) {
1119
+ children.push(
1120
+ text(
1121
+ {
1122
+ x: 0,
1123
+ y: 5,
1124
+ class: "schematex-genogram-age",
1125
+ "text-anchor": "middle",
1126
+ "font-size": "11"
1127
+ },
1128
+ String(ageToShow)
1129
+ )
1130
+ );
1131
+ }
1132
+ return group(
1133
+ {
1134
+ class: classes.join(" "),
1135
+ "data-individual-id": individual.id,
1136
+ transform: `translate(${x}, ${y})`
1137
+ },
1138
+ children
1139
+ );
1140
+ }
1141
+ function getRequiredDefs(individuals, includeArrowMarker = false) {
1142
+ const neededFills = /* @__PURE__ */ new Set();
1143
+ for (const ind of individuals) {
1144
+ if (ind.conditions) {
1145
+ for (const cond of ind.conditions) {
1146
+ neededFills.add(cond.fill);
1147
+ }
1148
+ }
1149
+ }
1150
+ const children = [];
1151
+ if (neededFills.has("half-left")) {
1152
+ children.push(
1153
+ el("clipPath", { id: "schematex-genogram-clip-half-left-rect" }, [
1154
+ rect({ x: "0", y: "0", width: "50%", height: "100%" })
1155
+ ]),
1156
+ el("clipPath", { id: "schematex-genogram-clip-half-left-circle" }, [
1157
+ rect({ x: "-50", y: "-50", width: "50", height: "100" })
1158
+ ])
1159
+ );
1160
+ }
1161
+ if (neededFills.has("half-right")) {
1162
+ children.push(
1163
+ el("clipPath", { id: "schematex-genogram-clip-half-right-rect" }, [
1164
+ rect({ x: "50%", y: "0", width: "50%", height: "100%" })
1165
+ ]),
1166
+ el("clipPath", { id: "schematex-genogram-clip-half-right-circle" }, [
1167
+ rect({ x: "0", y: "-50", width: "50", height: "100" })
1168
+ ])
1169
+ );
1170
+ }
1171
+ if (neededFills.has("half-bottom")) {
1172
+ children.push(
1173
+ el("clipPath", { id: "schematex-genogram-clip-half-bottom-rect" }, [
1174
+ rect({ x: "0", y: "50%", width: "100%", height: "50%" })
1175
+ ]),
1176
+ el("clipPath", { id: "schematex-genogram-clip-half-bottom-circle" }, [
1177
+ rect({ x: "-50", y: "0", width: "100", height: "50" })
1178
+ ])
1179
+ );
1180
+ }
1181
+ if (neededFills.has("quarter")) {
1182
+ children.push(
1183
+ el("clipPath", { id: "schematex-genogram-clip-quarter-rect" }, [
1184
+ rect({ x: "0", y: "0", width: "50%", height: "50%" })
1185
+ ]),
1186
+ el("clipPath", { id: "schematex-genogram-clip-quarter-circle" }, [
1187
+ rect({ x: "-50", y: "-50", width: "50", height: "50" })
1188
+ ])
1189
+ );
1190
+ }
1191
+ if (neededFills.has("half-top")) {
1192
+ children.push(
1193
+ el("clipPath", { id: "schematex-genogram-clip-half-top-rect" }, [
1194
+ rect({ x: "0", y: "0", width: "100%", height: "50%" })
1195
+ ]),
1196
+ el("clipPath", { id: "schematex-genogram-clip-half-top-circle" }, [
1197
+ rect({ x: "-50", y: "-50", width: "100", height: "50" })
1198
+ ])
1199
+ );
1200
+ }
1201
+ if (neededFills.has("quad-tl")) {
1202
+ children.push(
1203
+ el("clipPath", { id: "schematex-genogram-clip-quad-tl-rect" }, [
1204
+ rect({ x: "0", y: "0", width: "50%", height: "50%" })
1205
+ ]),
1206
+ el("clipPath", { id: "schematex-genogram-clip-quad-tl-circle" }, [
1207
+ rect({ x: "-50", y: "-50", width: "50", height: "50" })
1208
+ ])
1209
+ );
1210
+ }
1211
+ if (neededFills.has("quad-tr")) {
1212
+ children.push(
1213
+ el("clipPath", { id: "schematex-genogram-clip-quad-tr-rect" }, [
1214
+ rect({ x: "50%", y: "0", width: "50%", height: "50%" })
1215
+ ]),
1216
+ el("clipPath", { id: "schematex-genogram-clip-quad-tr-circle" }, [
1217
+ rect({ x: "0", y: "-50", width: "50", height: "50" })
1218
+ ])
1219
+ );
1220
+ }
1221
+ if (neededFills.has("quad-bl")) {
1222
+ children.push(
1223
+ el("clipPath", { id: "schematex-genogram-clip-quad-bl-rect" }, [
1224
+ rect({ x: "0", y: "50%", width: "50%", height: "50%" })
1225
+ ]),
1226
+ el("clipPath", { id: "schematex-genogram-clip-quad-bl-circle" }, [
1227
+ rect({ x: "-50", y: "0", width: "50", height: "50" })
1228
+ ])
1229
+ );
1230
+ }
1231
+ if (neededFills.has("quad-br")) {
1232
+ children.push(
1233
+ el("clipPath", { id: "schematex-genogram-clip-quad-br-rect" }, [
1234
+ rect({ x: "50%", y: "50%", width: "50%", height: "50%" })
1235
+ ]),
1236
+ el("clipPath", { id: "schematex-genogram-clip-quad-br-circle" }, [
1237
+ rect({ x: "0", y: "0", width: "50", height: "50" })
1238
+ ])
1239
+ );
1240
+ }
1241
+ if (neededFills.has("striped")) {
1242
+ children.push(
1243
+ pattern(
1244
+ {
1245
+ id: "schematex-genogram-pattern-striped",
1246
+ patternUnits: "userSpaceOnUse",
1247
+ width: "4",
1248
+ height: "4"
1249
+ },
1250
+ [
1251
+ path({
1252
+ d: "M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2",
1253
+ stroke: "#333",
1254
+ "stroke-width": "1"
1255
+ })
1256
+ ]
1257
+ )
1258
+ );
1259
+ }
1260
+ if (neededFills.has("dotted")) {
1261
+ children.push(
1262
+ pattern(
1263
+ {
1264
+ id: "schematex-genogram-pattern-dotted",
1265
+ patternUnits: "userSpaceOnUse",
1266
+ width: "6",
1267
+ height: "6"
1268
+ },
1269
+ [
1270
+ circle({ cx: "3", cy: "3", r: "1", fill: "#333" })
1271
+ ]
1272
+ )
1273
+ );
1274
+ }
1275
+ if (includeArrowMarker) {
1276
+ children.push(
1277
+ el("marker", {
1278
+ id: "schematex-genogram-arrow",
1279
+ viewBox: "0 0 10 10",
1280
+ refX: "10",
1281
+ refY: "5",
1282
+ markerWidth: "6",
1283
+ markerHeight: "6",
1284
+ orient: "auto"
1285
+ }, [
1286
+ path({ d: "M 0 0 L 10 5 L 0 10 z", fill: "#333" })
1287
+ ])
1288
+ );
1289
+ }
1290
+ return defs(children);
1291
+ }
1292
+ function capitalize(s) {
1293
+ return s.charAt(0).toUpperCase() + s.slice(1);
1294
+ }
1295
+ function formatTitle(ind) {
1296
+ const name = capitalize(ind.label || ind.id);
1297
+ if (ind.birthYear && ind.deathYear) {
1298
+ return `${name} (${ind.birthYear}-${ind.deathYear})`;
1299
+ }
1300
+ if (ind.birthYear) {
1301
+ return `${name} (${ind.birthYear})`;
1302
+ }
1303
+ return name;
1304
+ }
1305
+ function calcAge(ind) {
1306
+ if (!ind.birthYear) return void 0;
1307
+ if (ind.deathYear) return ind.deathYear - ind.birthYear;
1308
+ if (ind.status === "deceased") return void 0;
1309
+ return void 0;
1310
+ }
1311
+ function indexBorder(sex, half) {
1312
+ const outer = half + 4;
1313
+ switch (sex) {
1314
+ case "male":
1315
+ return rect({
1316
+ x: -outer,
1317
+ y: -outer,
1318
+ width: outer * 2,
1319
+ height: outer * 2,
1320
+ class: "schematex-genogram-index-border"
1321
+ });
1322
+ case "female":
1323
+ return circle({
1324
+ cx: 0,
1325
+ cy: 0,
1326
+ r: outer,
1327
+ class: "schematex-genogram-index-border"
1328
+ });
1329
+ case "unknown":
1330
+ case "other":
1331
+ case "nonbinary":
1332
+ case "intersex":
1333
+ return polygon({
1334
+ points: `0,${-outer} ${outer},0 0,${outer} ${-outer},0`,
1335
+ class: "schematex-genogram-index-border"
1336
+ });
1337
+ }
1338
+ }
1339
+ function baseShape(sex, half) {
1340
+ switch (sex) {
1341
+ case "male":
1342
+ return rect({
1343
+ x: -half,
1344
+ y: -half,
1345
+ width: half * 2,
1346
+ height: half * 2,
1347
+ class: "schematex-genogram-shape"
1348
+ });
1349
+ case "female":
1350
+ return circle({
1351
+ cx: 0,
1352
+ cy: 0,
1353
+ r: half,
1354
+ class: "schematex-genogram-shape"
1355
+ });
1356
+ case "unknown":
1357
+ case "other":
1358
+ case "nonbinary":
1359
+ case "intersex":
1360
+ return polygon({
1361
+ points: `0,${-half} ${half},0 0,${half} ${-half},0`,
1362
+ class: "schematex-genogram-shape"
1363
+ });
1364
+ }
1365
+ }
1366
+ function deceasedOverlay(sex, half) {
1367
+ const extend = sex === "female" ? half * 0.707 : half;
1368
+ return [
1369
+ line({
1370
+ x1: -extend,
1371
+ y1: -extend,
1372
+ x2: extend,
1373
+ y2: extend,
1374
+ class: "schematex-genogram-deceased-mark",
1375
+ stroke: "#333",
1376
+ "stroke-width": "2"
1377
+ }),
1378
+ line({
1379
+ x1: extend,
1380
+ y1: -extend,
1381
+ x2: -extend,
1382
+ y2: extend,
1383
+ class: "schematex-genogram-deceased-mark",
1384
+ stroke: "#333",
1385
+ "stroke-width": "2"
1386
+ })
1387
+ ];
1388
+ }
1389
+ function conditionFillElement(sex, half, cond) {
1390
+ const fillColor = cond.color ?? "#333";
1391
+ const attrs = {};
1392
+ if (cond.fill === "full") {
1393
+ attrs.fill = fillColor;
1394
+ } else if (cond.fill === "striped") {
1395
+ attrs.fill = "url(#schematex-genogram-pattern-striped)";
1396
+ } else if (cond.fill === "dotted") {
1397
+ attrs.fill = "url(#schematex-genogram-pattern-dotted)";
1398
+ } else {
1399
+ const clipSuffix = sex === "female" ? "circle" : "rect";
1400
+ attrs["clip-path"] = `url(#schematex-genogram-clip-${cond.fill}-${clipSuffix})`;
1401
+ attrs.fill = fillColor;
1402
+ }
1403
+ attrs.class = `schematex-genogram-condition-fill schematex-genogram-condition-${cond.label}`;
1404
+ switch (sex) {
1405
+ case "male":
1406
+ return rect({ x: -half, y: -half, width: half * 2, height: half * 2, ...attrs });
1407
+ case "female":
1408
+ return circle({ cx: 0, cy: 0, r: half, ...attrs });
1409
+ case "unknown":
1410
+ case "other":
1411
+ case "nonbinary":
1412
+ case "intersex":
1413
+ default:
1414
+ return polygon({
1415
+ points: `0,${-half} ${half},0 0,${half} ${-half},0`,
1416
+ ...attrs
1417
+ });
1418
+ }
1419
+ }
1420
+
1421
+ // src/diagrams/genogram/renderer.ts
1422
+ function renderGenogram(layout, config, ast) {
1423
+ const hasDirectional = layout.edges.some((e) => e.relationship.directional);
1424
+ const defsStr = getRequiredDefs(layout.nodes.map((n) => n.individual), hasDirectional);
1425
+ const styleStr = buildStyles(config);
1426
+ const genGroups = groupByGeneration(layout.nodes);
1427
+ const structuralEdges = layout.edges.filter((e) => !EMOTIONAL_REL_TYPES2.has(e.relationship.type));
1428
+ const emotionalEdges = layout.edges.filter((e) => EMOTIONAL_REL_TYPES2.has(e.relationship.type));
1429
+ const edgeLayers = renderEdges(structuralEdges);
1430
+ const emotionalLayer = renderEmotionalEdges(emotionalEdges);
1431
+ const nodeLayers = renderNodes(genGroups);
1432
+ const labelLayer = renderLabels(layout.nodes, config);
1433
+ const edgeLabelLayer = renderEdgeLabels(structuralEdges, config);
1434
+ const nodeCount = layout.nodes.length;
1435
+ const genCount = genGroups.size;
1436
+ const chartTitle = ast?.metadata?.title;
1437
+ const titleHeight = chartTitle ? 40 : 0;
1438
+ const totalHeight = layout.height + titleHeight;
1439
+ const layers = [
1440
+ title(chartTitle ? `Genogram: ${chartTitle}` : "Genogram"),
1441
+ desc(
1442
+ `Genogram diagram with ${nodeCount} individuals across ${genCount} generations`
1443
+ ),
1444
+ defsStr,
1445
+ styleStr
1446
+ ];
1447
+ if (chartTitle) {
1448
+ layers.push(
1449
+ text(
1450
+ {
1451
+ x: layout.width / 2,
1452
+ y: 28,
1453
+ class: "schematex-genogram-title",
1454
+ "text-anchor": "middle",
1455
+ "font-size": "20",
1456
+ "font-weight": "bold",
1457
+ "font-family": config.fontFamily
1458
+ },
1459
+ chartTitle
1460
+ )
1461
+ );
1462
+ }
1463
+ const contentGroup = group(
1464
+ { transform: titleHeight > 0 ? `translate(0, ${titleHeight})` : void 0 },
1465
+ [edgeLayers, emotionalLayer, ...nodeLayers, labelLayer, edgeLabelLayer]
1466
+ );
1467
+ layers.push(contentGroup);
1468
+ return svgRoot(
1469
+ {
1470
+ viewBox: `0 0 ${layout.width} ${totalHeight}`,
1471
+ class: "schematex-diagram schematex-genogram",
1472
+ width: layout.width,
1473
+ height: totalHeight
1474
+ },
1475
+ layers
1476
+ );
1477
+ }
1478
+ function buildStyles(config) {
1479
+ const t = resolveGenogramTheme(config.theme);
1480
+ const css = `
1481
+ .schematex-genogram {${cssCustomProperties(t)}
1482
+ background: ${t.bg};
1483
+ }
1484
+ .schematex-genogram-shape { fill: ${t.fill}; stroke: ${t.stroke}; stroke-width: ${STROKE_WIDTH.normal}; stroke-linejoin: round; }
1485
+ .schematex-genogram-male .schematex-genogram-shape { fill: ${t.maleFill}; }
1486
+ .schematex-genogram-female .schematex-genogram-shape { fill: ${t.femaleFill}; }
1487
+ .schematex-genogram-unknown .schematex-genogram-shape { fill: ${t.unknownFill}; }
1488
+ .schematex-genogram-label { font-family: ${config.fontFamily}; font-size: ${config.fontSize}px; text-anchor: middle; fill: ${t.text}; }
1489
+ .schematex-genogram-edge { stroke: ${t.neutral}; stroke-width: ${STROKE_WIDTH.normal}; fill: none; stroke-linecap: round; stroke-linejoin: round; }
1490
+ .schematex-genogram-edge-cohabiting path { stroke-dasharray: 6,4; }
1491
+ .schematex-genogram-edge-divorced .schematex-genogram-divorce-mark { stroke: ${t.neutral}; stroke-width: ${STROKE_WIDTH.normal}; }
1492
+ .schematex-genogram-edge-separated .schematex-genogram-separation-mark { stroke: ${t.neutral}; stroke-width: ${STROKE_WIDTH.normal}; }
1493
+ .schematex-genogram-deceased-mark { stroke: ${t.deceasedMark}; stroke-width: ${STROKE_WIDTH.normal}; stroke-linecap: round; }
1494
+ .schematex-genogram-condition-fill { fill: ${t.conditionFill}; }
1495
+ .schematex-genogram-age { font-family: ${config.fontFamily}; fill: ${t.text}; pointer-events: none; }
1496
+ .schematex-genogram-title { fill: ${t.text}; }
1497
+ .schematex-genogram-edge-label { font-family: ${config.fontFamily}; fill: ${t.text}; }
1498
+ .schematex-genogram-index-border { stroke: ${t.warn}; stroke-width: ${STROKE_WIDTH.thick}; fill: none; }
1499
+ `;
1500
+ return el("style", {}, css);
1501
+ }
1502
+ var EMOTIONAL_REL_TYPES2 = /* @__PURE__ */ new Set([
1503
+ "harmony",
1504
+ "close",
1505
+ "bestfriends",
1506
+ "love",
1507
+ "inlove",
1508
+ "friendship",
1509
+ "hostile",
1510
+ "conflict",
1511
+ "enmity",
1512
+ "distant-hostile",
1513
+ "cutoff",
1514
+ "close-hostile",
1515
+ "fused",
1516
+ "fused-hostile",
1517
+ "distant",
1518
+ "normal",
1519
+ "nevermet",
1520
+ "abuse",
1521
+ "physical-abuse",
1522
+ "emotional-abuse",
1523
+ "sexual-abuse",
1524
+ "neglect",
1525
+ "manipulative",
1526
+ "controlling",
1527
+ "jealous",
1528
+ "focused",
1529
+ "focused-neg",
1530
+ "distrust",
1531
+ "admirer",
1532
+ "limerence"
1533
+ ]);
1534
+ function getEmotionalColor(type) {
1535
+ if (["harmony", "close", "bestfriends", "love", "inlove", "friendship"].includes(type)) return "#4caf50";
1536
+ if (["hostile", "conflict", "enmity", "distant-hostile", "cutoff"].includes(type)) return "#e53935";
1537
+ if (["close-hostile", "fused", "fused-hostile"].includes(type)) return "#9c27b0";
1538
+ if (["distant", "normal", "nevermet"].includes(type)) return "#9e9e9e";
1539
+ if (["abuse", "physical-abuse", "emotional-abuse", "sexual-abuse", "neglect"].includes(type)) return "#b71c1c";
1540
+ if (["manipulative", "controlling", "jealous"].includes(type)) return "#e65100";
1541
+ return "#1565c0";
1542
+ }
1543
+ function getEmotionalLineStyle(type) {
1544
+ if (["hostile", "conflict", "enmity", "distant-hostile", "close-hostile", "fused-hostile"].includes(type)) {
1545
+ return "stroke-dasharray: 8,3,2,3;";
1546
+ }
1547
+ if (["distant", "distant-hostile", "nevermet"].includes(type)) {
1548
+ return "stroke-dasharray: 6,4;";
1549
+ }
1550
+ if (type === "cutoff") {
1551
+ return "stroke-dasharray: 2,8;";
1552
+ }
1553
+ return "";
1554
+ }
1555
+ function getEmotionalStrokeWidth(type) {
1556
+ if (["fused", "fused-hostile", "bestfriends"].includes(type)) return 4;
1557
+ if (["close", "close-hostile", "love", "inlove"].includes(type)) return 3;
1558
+ return 2;
1559
+ }
1560
+ function renderEmotionalEdges(edges) {
1561
+ if (edges.length === 0) return group({ class: "schematex-genogram-emotional-edges" }, []);
1562
+ const children = [];
1563
+ for (const edge of edges) {
1564
+ const type = edge.relationship.type;
1565
+ const color = getEmotionalColor(type);
1566
+ const lineStyle = getEmotionalLineStyle(type);
1567
+ const strokeWidth = getEmotionalStrokeWidth(type);
1568
+ const directional = edge.relationship.directional;
1569
+ const elements = [
1570
+ el("path", {
1571
+ d: edge.path,
1572
+ fill: "none",
1573
+ stroke: color,
1574
+ "stroke-width": strokeWidth,
1575
+ style: lineStyle || void 0,
1576
+ "marker-end": directional ? "url(#schematex-genogram-arrow)" : void 0
1577
+ })
1578
+ ];
1579
+ if (edge.relationship.label) {
1580
+ const mid = pathMidpoint(edge.path);
1581
+ if (mid) {
1582
+ elements.push(
1583
+ text(
1584
+ {
1585
+ x: mid.x,
1586
+ y: mid.y - 6,
1587
+ class: "schematex-genogram-edge-label",
1588
+ "text-anchor": "middle",
1589
+ "font-size": "10",
1590
+ fill: color
1591
+ },
1592
+ edge.relationship.label
1593
+ )
1594
+ );
1595
+ }
1596
+ }
1597
+ children.push(
1598
+ group(
1599
+ {
1600
+ class: `schematex-genogram-emotional schematex-genogram-emotional-${type}`,
1601
+ "data-from": edge.from,
1602
+ "data-to": edge.to,
1603
+ "data-relationship-type": type
1604
+ },
1605
+ elements
1606
+ )
1607
+ );
1608
+ }
1609
+ return group({ class: "schematex-genogram-emotional-edges" }, children);
1610
+ }
1611
+ function renderEdgeLabels(edges, config) {
1612
+ const labels = [];
1613
+ for (const edge of edges) {
1614
+ if (!edge.relationship.label) continue;
1615
+ const mid = pathMidpoint(edge.path);
1616
+ if (!mid) continue;
1617
+ labels.push(
1618
+ text(
1619
+ {
1620
+ x: mid.x,
1621
+ y: mid.y - 6,
1622
+ class: "schematex-genogram-edge-label",
1623
+ "text-anchor": "middle",
1624
+ "font-size": "10",
1625
+ "font-family": config.fontFamily
1626
+ },
1627
+ edge.relationship.label
1628
+ )
1629
+ );
1630
+ }
1631
+ return group({ class: "schematex-genogram-edge-labels" }, labels);
1632
+ }
1633
+ function renderEdges(edges) {
1634
+ const children = [];
1635
+ for (const edge of edges) {
1636
+ const relType = edge.relationship.type;
1637
+ const cssClass = `schematex-genogram-edge schematex-genogram-edge-${relType}`;
1638
+ const elements = [
1639
+ el("path", { d: edge.path, class: "schematex-genogram-edge-path" })
1640
+ ];
1641
+ if (relType === "divorced") {
1642
+ const mid = pathMidpoint(edge.path);
1643
+ if (mid) {
1644
+ elements.push(
1645
+ el("line", {
1646
+ x1: mid.x - 4,
1647
+ y1: mid.y - 6,
1648
+ x2: mid.x + 4,
1649
+ y2: mid.y + 6,
1650
+ class: "schematex-genogram-divorce-mark",
1651
+ stroke: "#333",
1652
+ "stroke-width": "2"
1653
+ }),
1654
+ el("line", {
1655
+ x1: mid.x - 4 + 6,
1656
+ y1: mid.y - 6,
1657
+ x2: mid.x + 4 + 6,
1658
+ y2: mid.y + 6,
1659
+ class: "schematex-genogram-divorce-mark",
1660
+ stroke: "#333",
1661
+ "stroke-width": "2"
1662
+ })
1663
+ );
1664
+ }
1665
+ }
1666
+ if (relType === "separated") {
1667
+ const mid = pathMidpoint(edge.path);
1668
+ if (mid) {
1669
+ elements.push(
1670
+ el("line", {
1671
+ x1: mid.x - 4,
1672
+ y1: mid.y - 6,
1673
+ x2: mid.x + 4,
1674
+ y2: mid.y + 6,
1675
+ class: "schematex-genogram-separation-mark",
1676
+ stroke: "#333",
1677
+ "stroke-width": "2"
1678
+ })
1679
+ );
1680
+ }
1681
+ }
1682
+ children.push(
1683
+ group(
1684
+ {
1685
+ class: cssClass,
1686
+ "data-from": edge.from,
1687
+ "data-to": edge.to
1688
+ },
1689
+ elements
1690
+ )
1691
+ );
1692
+ }
1693
+ return group({ class: "schematex-genogram-edges" }, children);
1694
+ }
1695
+ function pathMidpoint(pathData) {
1696
+ const coords = pathData.match(/[\d.-]+/g);
1697
+ if (!coords || coords.length < 4) return null;
1698
+ const x1 = parseFloat(coords[0]);
1699
+ const y1 = parseFloat(coords[1]);
1700
+ const x2 = parseFloat(coords[2]);
1701
+ const y2 = parseFloat(coords[3]);
1702
+ return { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
1703
+ }
1704
+ function groupByGeneration(nodes) {
1705
+ const groups = /* @__PURE__ */ new Map();
1706
+ for (const node of nodes) {
1707
+ const gen = node.generation;
1708
+ const group2 = groups.get(gen) ?? [];
1709
+ group2.push(node);
1710
+ groups.set(gen, group2);
1711
+ }
1712
+ return groups;
1713
+ }
1714
+ function renderNodes(genGroups) {
1715
+ const layers = [];
1716
+ const sortedGens = Array.from(genGroups.keys()).sort((a, b) => a - b);
1717
+ for (const genIdx of sortedGens) {
1718
+ const nodes = genGroups.get(genIdx) ?? [];
1719
+ const nodeElements = [];
1720
+ for (const node of nodes) {
1721
+ const cx = node.x + node.width / 2;
1722
+ const cy = node.y + node.height / 2;
1723
+ nodeElements.push(
1724
+ renderIndividualSymbol(node.individual, cx, cy, node.width)
1725
+ );
1726
+ }
1727
+ layers.push(
1728
+ group(
1729
+ {
1730
+ class: `schematex-genogram-generation schematex-genogram-generation-${genIdx}`,
1731
+ "data-generation": genIdx
1732
+ },
1733
+ nodeElements
1734
+ )
1735
+ );
1736
+ }
1737
+ return layers;
1738
+ }
1739
+ function renderLabels(nodes, config) {
1740
+ const labels = [];
1741
+ for (const node of nodes) {
1742
+ const ind = node.individual;
1743
+ const label = ind.label || ind.id;
1744
+ const cx = node.x + node.width / 2;
1745
+ const labelY = node.y + node.height + 6 + config.fontSize;
1746
+ let labelText = label.charAt(0).toUpperCase() + label.slice(1);
1747
+ if (ind.birthYear && ind.deathYear) {
1748
+ labelText += ` (${ind.birthYear}\u2013${ind.deathYear})`;
1749
+ } else if (ind.birthYear) {
1750
+ labelText += ` (b. ${ind.birthYear})`;
1751
+ }
1752
+ labels.push(
1753
+ text(
1754
+ {
1755
+ x: cx,
1756
+ y: labelY,
1757
+ class: "schematex-genogram-label",
1758
+ "data-individual-id": ind.id
1759
+ },
1760
+ labelText
1761
+ )
1762
+ );
1763
+ }
1764
+ return group({ class: "schematex-genogram-labels" }, labels);
1765
+ }
1766
+
1767
+ // src/diagrams/genogram/index.ts
1768
+ var genogram = {
1769
+ type: "genogram",
1770
+ detect(text2) {
1771
+ const firstLine = text2.trim().split("\n")[0]?.trim().toLowerCase() ?? "";
1772
+ return firstLine === "genogram" || firstLine.startsWith("genogram ");
1773
+ },
1774
+ render(text2, config) {
1775
+ const ast = parseGenogram(text2);
1776
+ const layoutConfig = {
1777
+ nodeSpacingX: 80,
1778
+ nodeSpacingY: 100,
1779
+ nodeWidth: 40,
1780
+ nodeHeight: 40
1781
+ };
1782
+ const layout = layoutGenogram(ast, layoutConfig);
1783
+ const renderConfig = {
1784
+ fontFamily: config?.fontFamily ?? "system-ui, -apple-system, sans-serif",
1785
+ fontSize: config?.fontSize ?? 12,
1786
+ theme: config?.theme ?? "default",
1787
+ padding: config?.padding ?? 20
1788
+ };
1789
+ return renderGenogram(layout, renderConfig, ast);
1790
+ }
1791
+ };
1792
+
1793
+ export { ParseError, genogram, getRequiredDefs, layoutGenogram, parseGenogram, renderGenogram, renderIndividualSymbol };
1794
+ //# sourceMappingURL=chunk-D4JTSPOL.js.map
1795
+ //# sourceMappingURL=chunk-D4JTSPOL.js.map