agentic-mermaid 0.1.0 → 0.1.1

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 (66) hide show
  1. package/AGENT_NATIVE.md +8 -3
  2. package/CHANGELOG.md +5 -0
  3. package/Instructions_for_agents.md +7 -1
  4. package/README.md +1 -1
  5. package/TODO.md +53 -31
  6. package/assets/fonts/FONT-LICENSES.md +9 -5
  7. package/assets/fonts/Inter-Bold.ttf +0 -0
  8. package/assets/fonts/Inter-Medium.ttf +0 -0
  9. package/assets/fonts/Inter-Regular.ttf +0 -0
  10. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  11. package/dist/agent.d.ts +59 -5
  12. package/dist/agent.js +965 -479
  13. package/dist/agentic-mermaid-mcp.js +976 -485
  14. package/dist/am.js +8516 -4326
  15. package/dist/{index-x2Cj1dVv.d.ts → index-wjRjbsP-.d.ts} +13 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +634 -456
  18. package/docs/agent-api-cookbook.md +2 -2
  19. package/docs/api.md +13 -5
  20. package/docs/comparison.md +1 -1
  21. package/docs/contributing/releasing.md +16 -5
  22. package/docs/design/families/journey-experience-curve-implementation.png +0 -0
  23. package/docs/design/families/journey-migration-parity.md +11 -5
  24. package/docs/design/families/journey-section-overlap-after.png +0 -0
  25. package/docs/design/families/journey-section-overlap-before.png +0 -0
  26. package/docs/design/families/journey.md +33 -5
  27. package/docs/features.md +3 -3
  28. package/docs/fork-differences.md +1 -1
  29. package/docs/layout-characterization/contact-sheet-families.md +2 -0
  30. package/docs/layout-characterization/visual-quality.md +12 -12
  31. package/docs/layout-characterization/visual-snapshots/journey.svg +32 -52
  32. package/docs/quality.md +11 -8
  33. package/llms.txt +2 -2
  34. package/package.json +3 -1
  35. package/server.json +41 -0
  36. package/src/agent/describe.ts +5 -1
  37. package/src/agent/facts.ts +23 -0
  38. package/src/agent/families-builtin.ts +49 -63
  39. package/src/agent/family-layouts.ts +7 -1
  40. package/src/agent/font-coverage.ts +288 -0
  41. package/src/agent/journey-body.ts +111 -151
  42. package/src/agent/mutation-ops.ts +1 -1
  43. package/src/agent/op-schema.ts +6 -2
  44. package/src/agent/png.ts +61 -7
  45. package/src/agent/types.ts +19 -3
  46. package/src/agent/verify.ts +34 -2
  47. package/src/ascii/journey.ts +48 -68
  48. package/src/ascii/timeline.ts +21 -8
  49. package/src/ascii/wrap.ts +131 -0
  50. package/src/cli/agent-instructions.ts +1 -1
  51. package/src/cli/am-bin.ts +2 -2
  52. package/src/cli/index.ts +24 -9
  53. package/src/cli/run-entrypoint.ts +10 -0
  54. package/src/family-rubric.ts +425 -0
  55. package/src/font-manifest.ts +4 -0
  56. package/src/journey/layout.ts +95 -61
  57. package/src/journey/parse-core.ts +251 -0
  58. package/src/journey/parser.ts +35 -152
  59. package/src/journey/renderer.ts +126 -20
  60. package/src/mcp/sdk-decl.ts +7 -3
  61. package/src/mcp/tool-surface.ts +1 -1
  62. package/src/render-family-hooks.ts +1 -1
  63. package/src/shared/color-math.ts +21 -0
  64. package/src/theme.ts +86 -15
  65. package/src/types.ts +8 -0
  66. package/src/xychart/colors.ts +2 -2
package/dist/agent.js CHANGED
@@ -67,7 +67,8 @@ var WARNING_SEVERITY = {
67
67
  DECISION_BRANCH_UNLABELED: "warning",
68
68
  COMMENT_DROPPED: "warning",
69
69
  UNSUPPORTED_SYNTAX: "warning",
70
- CONTENT_DROPPED_ON_ROUNDTRIP: "warning"
70
+ CONTENT_DROPPED_ON_ROUNDTRIP: "warning",
71
+ INEFFECTIVE_CONFIG: "warning"
71
72
  };
72
73
  var WARNING_TIER = {
73
74
  EMPTY_DIAGRAM: "structural",
@@ -91,7 +92,8 @@ var WARNING_TIER = {
91
92
  DECISION_BRANCH_UNLABELED: "lint",
92
93
  COMMENT_DROPPED: "lint",
93
94
  UNSUPPORTED_SYNTAX: "lint",
94
- CONTENT_DROPPED_ON_ROUNDTRIP: "lint"
95
+ CONTENT_DROPPED_ON_ROUNDTRIP: "lint",
96
+ INEFFECTIVE_CONFIG: "lint"
95
97
  };
96
98
  var DEFAULT_LABEL_CHAR_CAP = 40;
97
99
 
@@ -686,7 +688,7 @@ var MUTATION_OPS_BY_FAMILY = {
686
688
  timeline: ["set_title", "add_section", "remove_section", "set_section_label", "add_period", "remove_period", "set_period_label", "add_event", "remove_event", "set_event_text"],
687
689
  class: ["set_title", "add_class", "remove_class", "rename_class", "add_member", "remove_member", "add_relation", "remove_relation", "add_note", "remove_note"],
688
690
  er: ["add_entity", "remove_entity", "rename_entity", "add_attribute", "remove_attribute", "add_relation", "remove_relation"],
689
- journey: ["set_title", "add_section", "remove_section", "set_section_label", "add_task", "remove_task", "set_task_text", "set_task_score", "set_task_actors", "rename_actor"],
691
+ journey: ["set_title", "add_section", "remove_section", "set_section_label", "add_task", "remove_task", "set_task_text", "set_task_score", "set_task_actors", "rename_actor", "move_task", "move_section", "set_accessibility_title", "set_accessibility_description"],
690
692
  architecture: ["add_service", "remove_service", "rename_service", "set_service_label", "set_service_icon", "move_service", "add_group", "remove_group", "add_edge", "remove_edge"],
691
693
  xychart: ["set_title", "set_x_axis", "set_y_axis", "add_series", "remove_series", "set_series_values", "set_series_name", "reorder_series"],
692
694
  pie: ["set_title", "set_show_data", "add_slice", "remove_slice", "rename_slice", "set_slice_value", "reorder_slice"],
@@ -1904,33 +1906,164 @@ function mutateTimeline(input, op) {
1904
1906
  return ok(body);
1905
1907
  }
1906
1908
 
1907
- // src/agent/journey-body.ts
1908
- var TITLE_RE2 = /^title\s+(.+)$/i;
1909
+ // src/journey/parse-core.ts
1910
+ var JOURNEY_MIN_SCORE = 1;
1911
+ var JOURNEY_MAX_SCORE = 5;
1912
+ var JOURNEY_TITLE_RE = /^title\s+(.+)$/i;
1913
+ var JOURNEY_SECTION_RE = /^section\s+(.+)$/i;
1914
+ var JOURNEY_TASK_RE = /^(.+?)\s*:\s*([0-9]+)\s*(?::\s*(.*))?$/;
1915
+ var TASK_LIKE_RE = /^(.+?)\s*:\s*([^:]+?)(?:\s*:\s*.*)?$/;
1909
1916
  var ACC_LINE_RE = (directive) => new RegExp(`^${directive}\\s*:[ \\t]*(.+)$`, "i");
1910
- var SECTION_RE = /^section\s+(.+)$/i;
1911
- var TASK_RE = /^(.+?)\s*:\s*([0-9]+)\s*(?::\s*(.*))?$/;
1912
- function normalizeText(value) {
1913
- return normalizeJourneyLabel(value).split(/\r?\n/).map((part) => part.trim()).filter(Boolean).join("\n");
1914
- }
1915
- function normalizeActorText(value) {
1916
- return normalizeText(value).split(/\r?\n/).map((part) => part.trim()).filter(Boolean).join(" / ");
1917
- }
1917
+ var ACC_DESCR_BLOCK_START_RE = /^accDescr\s*:?\s*\{\s*(.*)$/i;
1918
1918
  function normalizeJourneyLabel(label) {
1919
1919
  return label.replace(/<br\s*\/?>/gi, "\n").replace(/\\n/g, "\n").replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>");
1920
1920
  }
1921
- function formatJourneyInline(value) {
1922
- return value.split(/\r?\n/).map((part) => part.trim()).join("<br>");
1921
+ function normalizeJourneyText(value) {
1922
+ return normalizeJourneyLabel(value).split(/\r?\n/).map((part) => part.trim()).filter(Boolean).join("\n");
1923
+ }
1924
+ function normalizeJourneyActor(value) {
1925
+ return normalizeJourneyText(value).split("\n").join(" / ");
1923
1926
  }
1924
1927
  function isJourneyComment(line2) {
1925
1928
  return line2.startsWith("%%") || line2.startsWith("#") || line2.startsWith("%");
1926
1929
  }
1927
- function validScore(score) {
1928
- return Number.isInteger(score) && score >= 1 && score <= 5;
1930
+ function isValidJourneyScore(score) {
1931
+ return Number.isInteger(score) && score >= JOURNEY_MIN_SCORE && score <= JOURNEY_MAX_SCORE;
1932
+ }
1933
+ function walkJourneyLines(lines, startIndex, events) {
1934
+ for (let i2 = startIndex; i2 < lines.length; i2++) {
1935
+ const line2 = lines[i2].trim();
1936
+ if (!line2) continue;
1937
+ if (isJourneyComment(line2)) continue;
1938
+ const accDescrStart = line2.match(ACC_DESCR_BLOCK_START_RE);
1939
+ if (accDescrStart) {
1940
+ const collected = collectJourneyAccessibilityBlock(accDescrStart[1] ?? "", lines, i2);
1941
+ if (!collected) {
1942
+ const outcome = events.issue?.({
1943
+ code: "unclosed_accdescr",
1944
+ lineIndex: i2,
1945
+ statement: line2,
1946
+ detail: 'Journey accDescr block is missing a closing "}"'
1947
+ });
1948
+ if (outcome === "stop") return;
1949
+ return;
1950
+ }
1951
+ events.accDescr?.(normalizeJourneyText(collected.text), i2);
1952
+ i2 = collected.nextIndex;
1953
+ continue;
1954
+ }
1955
+ for (const statement of splitJourneyStatements(line2)) {
1956
+ const outcome = classifyStatement(statement, i2, events);
1957
+ if (outcome === "stop") return;
1958
+ }
1959
+ }
1960
+ }
1961
+ function classifyStatement(statement, lineIndex, events) {
1962
+ const issue = (code, detail) => events.issue?.({ code, lineIndex, statement, detail });
1963
+ const accTitle = matchAccessibilityLine(statement, "accTitle");
1964
+ if (accTitle !== void 0) {
1965
+ events.accTitle?.(accTitle, lineIndex);
1966
+ return;
1967
+ }
1968
+ const accDescr = matchAccessibilityLine(statement, "accDescr");
1969
+ if (accDescr !== void 0) {
1970
+ events.accDescr?.(accDescr, lineIndex);
1971
+ return;
1972
+ }
1973
+ const titleMatch = statement.match(JOURNEY_TITLE_RE);
1974
+ if (titleMatch) {
1975
+ const text3 = normalizeJourneyText(titleMatch[1]);
1976
+ if (!text3) return issue("empty_title", `Journey title is empty: "${statement}"`);
1977
+ events.title?.(text3, lineIndex);
1978
+ return;
1979
+ }
1980
+ const sectionMatch = statement.match(JOURNEY_SECTION_RE);
1981
+ if (sectionMatch) {
1982
+ const label = normalizeJourneyText(sectionMatch[1]);
1983
+ if (!label || label.includes(":")) {
1984
+ return issue("section_colon", `Journey section labels must not contain ':': "${statement}"`);
1985
+ }
1986
+ events.section?.(label, lineIndex);
1987
+ return;
1988
+ }
1989
+ const taskMatch = statement.match(JOURNEY_TASK_RE);
1990
+ if (taskMatch) {
1991
+ const text3 = normalizeJourneyText(taskMatch[1]);
1992
+ if (!text3) return issue("empty_task_text", `Journey task text is empty: "${statement}"`);
1993
+ const rawScore = taskMatch[2];
1994
+ const score = Number.parseInt(rawScore, 10);
1995
+ if (!isValidJourneyScore(score)) return issue("invalid_score", invalidScoreDetail(text3, rawScore));
1996
+ const actors = (taskMatch[3] ?? "").split(",").map(normalizeJourneyActor).filter(Boolean);
1997
+ events.task?.(text3, score, actors, lineIndex);
1998
+ return;
1999
+ }
2000
+ const taskLike = statement.match(TASK_LIKE_RE);
2001
+ if (taskLike) {
2002
+ const text3 = normalizeJourneyText(taskLike[1]);
2003
+ const rawScore = taskLike[2].trim();
2004
+ if (text3 && rawScore) return issue("invalid_score", invalidScoreDetail(text3, rawScore));
2005
+ }
2006
+ return issue(
2007
+ "unrecognized_line",
2008
+ `Invalid user journey line: "${statement}". Expected title, section, accessibility metadata, or "Task name: 3: Actor"`
2009
+ );
2010
+ }
2011
+ function invalidScoreDetail(text3, rawScore) {
2012
+ return `Journey task "${text3}" has invalid score ${rawScore}. Expected an integer from ${JOURNEY_MIN_SCORE} through ${JOURNEY_MAX_SCORE}`;
2013
+ }
2014
+ var HTML_ENTITY_RE = /^&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#[0-9]{1,7}|#x[0-9a-fA-F]{1,6});$/;
2015
+ function splitJourneyStatements(line2) {
2016
+ if (!line2.includes(";")) return [line2];
2017
+ const parts = [];
2018
+ let start = 0;
2019
+ for (let i2 = 0; i2 < line2.length; i2++) {
2020
+ if (line2[i2] !== ";") continue;
2021
+ const amp = line2.lastIndexOf("&", i2);
2022
+ if (amp >= start && HTML_ENTITY_RE.test(line2.slice(amp, i2 + 1))) continue;
2023
+ parts.push(line2.slice(start, i2));
2024
+ start = i2 + 1;
2025
+ }
2026
+ parts.push(line2.slice(start));
2027
+ return parts.map((part) => part.trim()).filter((part) => part && !isJourneyComment(part));
2028
+ }
2029
+ function matchAccessibilityLine(line2, directive) {
2030
+ const match = line2.match(ACC_LINE_RE(directive));
2031
+ return match ? normalizeJourneyText(match[1]) : void 0;
2032
+ }
2033
+ function collectJourneyAccessibilityBlock(initial, lines, startIndex) {
2034
+ const initialEnd = initial.indexOf("}");
2035
+ if (initialEnd !== -1) {
2036
+ return {
2037
+ text: initial.slice(0, initialEnd).trim(),
2038
+ nextIndex: startIndex
2039
+ };
2040
+ }
2041
+ const parts = [initial.trim()].filter(Boolean);
2042
+ for (let i2 = startIndex + 1; i2 < lines.length; i2++) {
2043
+ const line2 = lines[i2];
2044
+ const end = line2.indexOf("}");
2045
+ if (end !== -1) {
2046
+ const beforeBrace = line2.slice(0, end).trim();
2047
+ if (beforeBrace) parts.push(beforeBrace);
2048
+ return {
2049
+ text: parts.join("\n"),
2050
+ nextIndex: i2
2051
+ };
2052
+ }
2053
+ parts.push(line2);
2054
+ }
2055
+ return null;
2056
+ }
2057
+
2058
+ // src/agent/journey-body.ts
2059
+ function formatJourneyInline(value) {
2060
+ return value.split(/\r?\n/).map((part) => part.trim()).join("<br>");
1929
2061
  }
1930
2062
  function parseJourneyBody(lines) {
1931
2063
  const body = { kind: "journey", sections: [] };
1932
2064
  let currentSection;
1933
2065
  let sIdx = 0, tIdx = 0;
2066
+ let firstIssue;
1934
2067
  const implicitSection = () => {
1935
2068
  if (!currentSection) {
1936
2069
  currentSection = { id: `section-${sIdx++}`, tasks: [] };
@@ -1938,60 +2071,36 @@ function parseJourneyBody(lines) {
1938
2071
  }
1939
2072
  return currentSection;
1940
2073
  };
1941
- for (let i2 = 0; i2 < lines.length; i2++) {
1942
- const raw2 = lines[i2];
1943
- const line2 = raw2.trim();
1944
- if (!line2) continue;
1945
- if (isJourneyComment(line2)) continue;
1946
- const accTitle = parseAccessibilityLine(line2, "accTitle");
1947
- if (accTitle !== void 0) {
1948
- body.accessibilityTitle = accTitle;
1949
- continue;
1950
- }
1951
- const accDescrStart = line2.match(/^accDescr\s*:?\s*\{\s*(.*)$/i);
1952
- if (accDescrStart) {
1953
- const initial = accDescrStart[1] ?? "";
1954
- const parsed = collectAccessibilityBlock(initial, lines, i2);
1955
- if (!parsed) return null;
1956
- body.accessibilityDescription = normalizeAccessibilityText(parsed.text);
1957
- i2 = parsed.nextIndex;
1958
- continue;
1959
- }
1960
- const accDescr = parseAccessibilityLine(line2, "accDescr");
1961
- if (accDescr !== void 0) {
1962
- body.accessibilityDescription = accDescr;
1963
- continue;
1964
- }
1965
- const tm = line2.match(TITLE_RE2);
1966
- if (tm) {
1967
- const title = normalizeText(tm[1]);
1968
- if (!title) return null;
1969
- body.title = title;
1970
- continue;
1971
- }
1972
- const sm = line2.match(SECTION_RE);
1973
- if (sm) {
1974
- const label = normalizeText(sm[1]);
1975
- if (!label || label.includes(":")) return null;
2074
+ walkJourneyLines(lines, 0, {
2075
+ title: (text3) => {
2076
+ body.title = text3;
2077
+ },
2078
+ accTitle: (text3) => {
2079
+ body.accessibilityTitle = text3;
2080
+ },
2081
+ accDescr: (text3) => {
2082
+ body.accessibilityDescription = text3;
2083
+ },
2084
+ section: (label) => {
1976
2085
  currentSection = { id: `section-${sIdx++}`, label, tasks: [] };
1977
2086
  body.sections.push(currentSection);
1978
- continue;
1979
- }
1980
- const km = line2.match(TASK_RE);
1981
- if (km) {
1982
- const text3 = normalizeText(km[1]);
1983
- const score = Number.parseInt(km[2], 10);
1984
- if (!text3 || !validScore(score)) return null;
1985
- const actors = (km[3] ?? "").split(",").map(normalizeActorText).filter(Boolean);
2087
+ },
2088
+ task: (text3, score, actors) => {
1986
2089
  implicitSection().tasks.push({ id: `task-${tIdx++}`, text: text3, score, actors });
1987
- continue;
2090
+ },
2091
+ issue: (issue) => {
2092
+ firstIssue = issue;
2093
+ return "stop";
1988
2094
  }
1989
- return null;
1990
- }
2095
+ });
2096
+ if (firstIssue) return { ok: false, issue: firstIssue };
1991
2097
  if (body.sections.length === 0 && body.sections.every((s2) => s2.tasks.length === 0) && !body.title && !body.accessibilityTitle && !body.accessibilityDescription) {
1992
- return null;
2098
+ return {
2099
+ ok: false,
2100
+ issue: { code: "empty_journey", lineIndex: 0, statement: "", detail: "Journey has no title, sections, or scored tasks" }
2101
+ };
1993
2102
  }
1994
- return body;
2103
+ return { ok: true, body };
1995
2104
  }
1996
2105
  function renderJourney(body) {
1997
2106
  const lines = ["journey"];
@@ -2046,53 +2155,38 @@ function makeIdAllocator(body) {
2046
2155
  }
2047
2156
  function normalizeOpText(value, field, opts = {}) {
2048
2157
  if (typeof value !== "string") return err({ code: "INVALID_OP", message: `Journey ${field} must be a string` });
2049
- const normalized = normalizeText(value);
2158
+ const normalized = normalizeJourneyText(value);
2050
2159
  if (!normalized || !opts.allowColon && normalized.includes(":")) {
2051
2160
  return err({ code: "INVALID_OP", message: `Journey ${field} must be non-empty and must not contain :` });
2052
2161
  }
2053
2162
  return ok(normalized);
2054
2163
  }
2164
+ function resolveInsertIndex(index, length) {
2165
+ if (index === void 0) return ok(length);
2166
+ if (!Number.isInteger(index) || index < 0 || index > length) {
2167
+ return err({ code: "INVALID_OP", message: `Journey insert index ${index} out of range (0..${length})` });
2168
+ }
2169
+ return ok(index);
2170
+ }
2171
+ function normalizeAccessibilityOpText(value, field) {
2172
+ if (value === null) return ok(void 0);
2173
+ if (typeof value !== "string") return err({ code: "INVALID_OP", message: `Journey ${field} must be a string or null` });
2174
+ const normalized = normalizeJourneyText(value);
2175
+ if (!normalized || /[;{}]/.test(normalized)) {
2176
+ return err({ code: "INVALID_OP", message: `Journey ${field} must be non-empty and must not contain ;, {, or }` });
2177
+ }
2178
+ return ok(normalized);
2179
+ }
2055
2180
  function normalizeActors(actors) {
2056
2181
  const out = [];
2057
2182
  for (const raw2 of actors ?? []) {
2058
2183
  if (typeof raw2 !== "string") return err({ code: "INVALID_OP", message: "Journey actor must be a string" });
2059
- const normalized = normalizeActorText(raw2);
2184
+ const normalized = normalizeJourneyActor(raw2);
2060
2185
  if (!normalized) return err({ code: "INVALID_OP", message: "Journey actor must be non-empty" });
2061
2186
  out.push(normalized);
2062
2187
  }
2063
2188
  return ok(out);
2064
2189
  }
2065
- function normalizeAccessibilityText(value) {
2066
- return normalizeJourneyLabel(value).split(/\r?\n/).map((part) => part.trim()).filter(Boolean).join("\n");
2067
- }
2068
- function parseAccessibilityLine(line2, directive) {
2069
- const match = line2.match(ACC_LINE_RE(directive));
2070
- return match ? normalizeAccessibilityText(match[1]) : void 0;
2071
- }
2072
- function collectAccessibilityBlock(initial, lines, startIndex) {
2073
- const initialEnd = initial.indexOf("}");
2074
- if (initialEnd !== -1) {
2075
- return {
2076
- text: initial.slice(0, initialEnd).trim(),
2077
- nextIndex: startIndex
2078
- };
2079
- }
2080
- const parts = [initial.trim()].filter(Boolean);
2081
- for (let i2 = startIndex + 1; i2 < lines.length; i2++) {
2082
- const line2 = lines[i2];
2083
- const end = line2.indexOf("}");
2084
- if (end !== -1) {
2085
- const beforeBrace = line2.slice(0, end).trim();
2086
- if (beforeBrace) parts.push(beforeBrace);
2087
- return {
2088
- text: parts.join("\n"),
2089
- nextIndex: i2
2090
- };
2091
- }
2092
- parts.push(line2);
2093
- }
2094
- return null;
2095
- }
2096
2190
  function mutateJourney(body, op) {
2097
2191
  const next = cloneJourney(body);
2098
2192
  const nextId = makeIdAllocator(next);
@@ -2111,7 +2205,9 @@ function mutateJourney(body, op) {
2111
2205
  case "add_section": {
2112
2206
  const label = normalizeOpText(op.label, "section label");
2113
2207
  if (!label.ok) return label;
2114
- next.sections.push({ id: nextId("section"), label: label.value, tasks: [] });
2208
+ const index = resolveInsertIndex(op.index, next.sections.length);
2209
+ if (!index.ok) return index;
2210
+ next.sections.splice(index.value, 0, { id: nextId("section"), label: label.value, tasks: [] });
2115
2211
  break;
2116
2212
  }
2117
2213
  case "remove_section": {
@@ -2132,10 +2228,12 @@ function mutateJourney(body, op) {
2132
2228
  if (!s2) return err({ code: "SECTION_NOT_FOUND", message: `No section at index ${op.sectionIndex}` });
2133
2229
  const text3 = normalizeOpText(op.text, "task text");
2134
2230
  if (!text3.ok) return text3;
2135
- if (!validScore(op.score)) return err({ code: "INVALID_OP", message: `Journey score must be an integer 1..5, got ${op.score}` });
2231
+ if (!isValidJourneyScore(op.score)) return err({ code: "INVALID_OP", message: `Journey score must be an integer 1..5, got ${op.score}` });
2136
2232
  const actors = normalizeActors(op.actors);
2137
2233
  if (!actors.ok) return actors;
2138
- s2.tasks.push({ id: nextId("task"), text: text3.value, score: op.score, actors: actors.value });
2234
+ const index = resolveInsertIndex(op.index, s2.tasks.length);
2235
+ if (!index.ok) return index;
2236
+ s2.tasks.splice(index.value, 0, { id: nextId("task"), text: text3.value, score: op.score, actors: actors.value });
2139
2237
  break;
2140
2238
  }
2141
2239
  case "remove_task": {
@@ -2157,7 +2255,7 @@ function mutateJourney(body, op) {
2157
2255
  case "set_task_score": {
2158
2256
  const t2 = getTask(op.sectionIndex, op.taskIndex);
2159
2257
  if (!t2) return err({ code: "TASK_NOT_FOUND", message: `No task at (${op.sectionIndex},${op.taskIndex})` });
2160
- if (!validScore(op.score)) return err({ code: "INVALID_OP", message: `Journey score must be an integer 1..5, got ${op.score}` });
2258
+ if (!isValidJourneyScore(op.score)) return err({ code: "INVALID_OP", message: `Journey score must be an integer 1..5, got ${op.score}` });
2161
2259
  t2.score = op.score;
2162
2260
  break;
2163
2261
  }
@@ -2169,6 +2267,45 @@ function mutateJourney(body, op) {
2169
2267
  t2.actors = actors.value;
2170
2268
  break;
2171
2269
  }
2270
+ case "move_task": {
2271
+ const from = getSection(op.fromSection);
2272
+ if (!from) return err({ code: "SECTION_NOT_FOUND", message: `No section at index ${op.fromSection}` });
2273
+ if (!from.tasks[op.fromIndex]) return err({ code: "TASK_NOT_FOUND", message: `No task at index ${op.fromIndex}` });
2274
+ const to = getSection(op.toSection);
2275
+ if (!to) return err({ code: "SECTION_NOT_FOUND", message: `No section at index ${op.toSection}` });
2276
+ const [task] = from.tasks.splice(op.fromIndex, 1);
2277
+ if (!Number.isInteger(op.toIndex) || op.toIndex < 0 || op.toIndex > to.tasks.length) {
2278
+ return err({ code: "TASK_NOT_FOUND", message: `No insert position ${op.toIndex} in section ${op.toSection} (0..${to.tasks.length})` });
2279
+ }
2280
+ to.tasks.splice(op.toIndex, 0, task);
2281
+ if (from !== to && from.label === void 0 && from.tasks.length === 0) {
2282
+ next.sections.splice(next.sections.indexOf(from), 1);
2283
+ }
2284
+ break;
2285
+ }
2286
+ case "move_section": {
2287
+ if (!getSection(op.from)) return err({ code: "SECTION_NOT_FOUND", message: `No section at index ${op.from}` });
2288
+ if (!Number.isInteger(op.to) || op.to < 0 || op.to >= next.sections.length) {
2289
+ return err({ code: "SECTION_NOT_FOUND", message: `No section position ${op.to} (0..${next.sections.length - 1})` });
2290
+ }
2291
+ const [section] = next.sections.splice(op.from, 1);
2292
+ next.sections.splice(op.to, 0, section);
2293
+ break;
2294
+ }
2295
+ case "set_accessibility_title": {
2296
+ const title = normalizeAccessibilityOpText(op.title, "accessibility title");
2297
+ if (!title.ok) return title;
2298
+ if (title.value === void 0) delete next.accessibilityTitle;
2299
+ else next.accessibilityTitle = title.value;
2300
+ break;
2301
+ }
2302
+ case "set_accessibility_description": {
2303
+ const description = normalizeAccessibilityOpText(op.description, "accessibility description");
2304
+ if (!description.ok) return description;
2305
+ if (description.value === void 0) delete next.accessibilityDescription;
2306
+ else next.accessibilityDescription = description.value;
2307
+ break;
2308
+ }
2172
2309
  case "rename_actor": {
2173
2310
  const from = normalizeOpText(op.from, "actor", { allowColon: true });
2174
2311
  if (!from.ok) return from;
@@ -2234,7 +2371,7 @@ var JUNCTION_RE = new RegExp(`^junction\\s+(${IDENT})(?:\\s+in\\s+(${IDENT}))?\\
2234
2371
  var SOURCE_RE = new RegExp(`^(${IDENT}):(L|R|T|B)$`);
2235
2372
  var TARGET_RE = new RegExp(`^(L|R|T|B):(${IDENT})$`);
2236
2373
  var SIDES = /* @__PURE__ */ new Set(["L", "R", "T", "B"]);
2237
- function normalizeText2(value) {
2374
+ function normalizeText(value) {
2238
2375
  return value.split(/\r?\n/).map((part) => part.trim()).filter(Boolean).join(" ");
2239
2376
  }
2240
2377
  function parseArrow(token) {
@@ -2245,7 +2382,7 @@ function parseArrow(token) {
2245
2382
  if (t2 === "--") return { hasArrowStart: false, hasArrowEnd: false };
2246
2383
  const m2 = t2.match(/^(<)?-\[(.*)\]-(>)?$/);
2247
2384
  if (!m2) return null;
2248
- const label = normalizeText2(m2[2] ?? "") || void 0;
2385
+ const label = normalizeText(m2[2] ?? "") || void 0;
2249
2386
  if (label && (label.includes("[") || label.includes("]"))) return null;
2250
2387
  return { label, hasArrowStart: Boolean(m2[1]), hasArrowEnd: Boolean(m2[3]) };
2251
2388
  }
@@ -2285,7 +2422,7 @@ function parseArchitectureBody(lines) {
2285
2422
  ids.add(id);
2286
2423
  groupIds.add(id);
2287
2424
  const icon = gm[2]?.trim() || void 0;
2288
- const label = normalizeText2(gm[3] ?? id);
2425
+ const label = normalizeText(gm[3] ?? id);
2289
2426
  const parentId = gm[4] ?? void 0;
2290
2427
  if (parentId) pendingParents.push({ parent: parentId });
2291
2428
  groups.push({ id, label, icon, parentId });
@@ -2298,7 +2435,7 @@ function parseArchitectureBody(lines) {
2298
2435
  ids.add(id);
2299
2436
  endpointIds.add(id);
2300
2437
  const icon = sm[2]?.trim() || void 0;
2301
- const label = normalizeText2(sm[3] ?? id);
2438
+ const label = normalizeText(sm[3] ?? id);
2302
2439
  const parentId = sm[4] ?? void 0;
2303
2440
  if (parentId) pendingParents.push({ parent: parentId });
2304
2441
  services.push({ id, label, icon, parentId });
@@ -2389,7 +2526,7 @@ function validId(value, field) {
2389
2526
  }
2390
2527
  function normalizeLabel(value, field) {
2391
2528
  if (typeof value !== "string") return err({ code: "INVALID_OP", message: `Architecture ${field} must be a string` });
2392
- const normalized = normalizeText2(value);
2529
+ const normalized = normalizeText(value);
2393
2530
  if (!normalized || normalized.includes("[") || normalized.includes("]")) {
2394
2531
  return err({ code: "INVALID_OP", message: `Architecture ${field} must be non-empty and must not contain [ or ]` });
2395
2532
  }
@@ -2603,7 +2740,7 @@ function isFiniteNumber(n2) {
2603
2740
  var NUMBER = String.raw`[+-]?(?:\d+(?:\.\d+)?|\.\d+)`;
2604
2741
  var RANGE_RE = new RegExp(`^(${NUMBER})\\s*-->\\s*(${NUMBER})$`);
2605
2742
  var BARE_TEXT_RE = /^[^"'\[\];]+$/;
2606
- function normalizeText3(value) {
2743
+ function normalizeText2(value) {
2607
2744
  return value.split(/\r?\n/).map((part) => part.trim()).filter(Boolean).join(" ");
2608
2745
  }
2609
2746
  function isBareText(value) {
@@ -2705,7 +2842,7 @@ function parseXyChartBody(lines) {
2705
2842
  if (line2.startsWith("%%")) continue;
2706
2843
  const titleMatch = line2.match(/^title\s+(.+)$/);
2707
2844
  if (titleMatch) {
2708
- const title = bareToken(normalizeText3(titleMatch[1]));
2845
+ const title = bareToken(normalizeText2(titleMatch[1]));
2709
2846
  if (title === null) return null;
2710
2847
  body.title = title;
2711
2848
  continue;
@@ -2783,7 +2920,7 @@ function makeSeriesIdAllocator(body) {
2783
2920
  }
2784
2921
  function validBareText(value, field) {
2785
2922
  if (typeof value !== "string") return err({ code: "INVALID_OP", message: `XY chart ${field} must be a string` });
2786
- const normalized = normalizeText3(value);
2923
+ const normalized = normalizeText2(value);
2787
2924
  if (!isBareText(normalized)) {
2788
2925
  return err({ code: "INVALID_OP", message: `XY chart ${field} must be non-empty bare text (no quotes, brackets, ; or -->)` });
2789
2926
  }
@@ -3142,7 +3279,7 @@ function formatNumber3(n2) {
3142
3279
  function isCoord(n2) {
3143
3280
  return typeof n2 === "number" && Number.isFinite(n2) && n2 >= 0 && n2 <= 1;
3144
3281
  }
3145
- var TITLE_RE3 = /^title\s+(.+)$/i;
3282
+ var TITLE_RE2 = /^title\s+(.+)$/i;
3146
3283
  var AXIS_RE = /^([xy])-axis\s+(.+)$/i;
3147
3284
  var QUADRANT_RE = /^quadrant-([1-4])\s+(.+)$/i;
3148
3285
  var POINT_RE = /^(.+?)\s*:\s*\[\s*([^,\]]+)\s*,\s*([^,\]]+)\s*\]\s*$/;
@@ -3177,7 +3314,7 @@ function parseQuadrantBody(lines) {
3177
3314
  if (line2.startsWith("%%")) continue;
3178
3315
  if (/^classDef\b/i.test(line2) || line2.includes(":::")) return null;
3179
3316
  let m2;
3180
- if (m2 = line2.match(TITLE_RE3)) {
3317
+ if (m2 = line2.match(TITLE_RE2)) {
3181
3318
  const title = m2[1].trim();
3182
3319
  if (!title) return null;
3183
3320
  body.title = title;
@@ -4840,8 +4977,8 @@ function applyGanttFrontmatterConfig(model, frontmatter) {
4840
4977
  // src/agent/gantt-body.ts
4841
4978
  var DIRECTIVE_LINE_RE = /^(dateFormat|axisFormat|tickInterval|inclusiveEndDates|topAxis|excludes|includes|todayMarker|weekday|weekend|click|accTitle|accDescr)\b/i;
4842
4979
  var ACC_DESCR_BLOCK_OPEN_RE = /^accDescr\s*\{\s*$/i;
4843
- var TITLE_RE4 = /^title\s+(.+)$/i;
4844
- var SECTION_RE2 = /^section\s+(.+)$/i;
4980
+ var TITLE_RE3 = /^title\s+(.+)$/i;
4981
+ var SECTION_RE = /^section\s+(.+)$/i;
4845
4982
  var TASK_ID_RE = /^[\w-]+$/;
4846
4983
  function parseGanttBody(trimmedLines, rawLines) {
4847
4984
  const raw2 = rawLines ?? trimmedLines;
@@ -4890,13 +5027,13 @@ function parseGanttBody(trimmedLines, rawLines) {
4890
5027
  continue;
4891
5028
  }
4892
5029
  let m2;
4893
- if (m2 = line2.match(TITLE_RE4)) {
5030
+ if (m2 = line2.match(TITLE_RE3)) {
4894
5031
  body.title = m2[1].trim();
4895
5032
  statements.push({ kind: "title" });
4896
5033
  i2++;
4897
5034
  continue;
4898
5035
  }
4899
- if (m2 = line2.match(SECTION_RE2)) {
5036
+ if (m2 = line2.match(SECTION_RE)) {
4900
5037
  body.sections.push({ id: `section-${sIdx++}`, label: m2[1].trim(), tasks: [] });
4901
5038
  currentSection = body.sections.length - 1;
4902
5039
  statements.push({ kind: "section", ref: currentSection });
@@ -5028,7 +5165,7 @@ function validateTask(task) {
5028
5165
  const colon = line2.indexOf(":");
5029
5166
  const label = colon > 0 ? line2.slice(0, colon).trim() : "";
5030
5167
  const meta = colon > 0 ? parseGanttTaskMeta(line2.slice(colon + 1).trim()) : null;
5031
- const reparsesAsDirective = TITLE_RE4.test(line2) || SECTION_RE2.test(line2) || DIRECTIVE_LINE_RE.test(line2);
5168
+ const reparsesAsDirective = TITLE_RE3.test(line2) || SECTION_RE.test(line2) || DIRECTIVE_LINE_RE.test(line2);
5032
5169
  if (!meta || label !== task.label || reparsesAsDirective) {
5033
5170
  return { code: "INVALID_OP", message: `Gantt task does not round-trip through its canonical line ("${line2}")` };
5034
5171
  }
@@ -5043,7 +5180,7 @@ function validateSectionLabel(label) {
5043
5180
  return { code: "INVALID_OP", message: 'Gantt section label must be non-empty and must not contain ":" or newlines' };
5044
5181
  }
5045
5182
  const line2 = `section ${label.trim()}`;
5046
- if (!SECTION_RE2.test(line2)) return { code: "INVALID_OP", message: "Gantt section label does not round-trip" };
5183
+ if (!SECTION_RE.test(line2)) return { code: "INVALID_OP", message: "Gantt section label does not round-trip" };
5047
5184
  return null;
5048
5185
  }
5049
5186
  function allTaskIds(body) {
@@ -6182,78 +6319,54 @@ registerFamily({
6182
6319
  function extractJourneyLabels(source) {
6183
6320
  const out = [];
6184
6321
  const lines = source.split(/\r?\n/);
6185
- for (let i2 = 0; i2 < lines.length; i2++) {
6186
- const raw2 = lines[i2].trim();
6187
- if (!raw2 || raw2.startsWith("%%")) continue;
6188
- let m2;
6189
- if (m2 = raw2.match(/^accDescr\s*:?\s*\{\s*(.*)$/i)) {
6190
- const first = m2[1];
6191
- const end = first.indexOf("}");
6192
- if (end >= 0) {
6193
- const text3 = first.slice(0, end).trim();
6194
- if (text3) out.push({ text: text3, target: `line${i2 + 1}` });
6195
- } else {
6196
- const text3 = first.trim();
6197
- if (text3) out.push({ text: text3, target: `line${i2 + 1}` });
6198
- for (i2 += 1; i2 < lines.length; i2++) {
6199
- const line2 = lines[i2].trim();
6200
- const close = line2.indexOf("}");
6201
- const text4 = (close >= 0 ? line2.slice(0, close) : line2).trim();
6202
- if (text4) out.push({ text: text4, target: `line${i2 + 1}` });
6203
- if (close >= 0) break;
6204
- }
6205
- }
6206
- } else if (m2 = raw2.match(/^accTitle\s*:?\s*(.+)$/i)) {
6207
- out.push({ text: m2[1].trim(), target: `line${i2 + 1}` });
6208
- } else if (m2 = raw2.match(/^accDescr\s*:?\s*(.+)$/i)) {
6209
- out.push({ text: m2[1].replace(/^\{/, "").replace(/\}$/, "").trim(), target: `line${i2 + 1}` });
6210
- } else if (m2 = raw2.match(/^title\s+(.+)$/i)) {
6211
- out.push({ text: m2[1].trim(), target: `line${i2 + 1}` });
6212
- } else if (m2 = raw2.match(/^section\s+(.+)$/i)) {
6213
- out.push({ text: m2[1].trim(), target: `line${i2 + 1}` });
6214
- } else if (m2 = raw2.match(/^(.+?):\s*\d+(?:\s*:\s*(.*))?$/)) {
6215
- out.push({ text: m2[1].trim(), target: `line${i2 + 1}` });
6216
- for (const actor of (m2[2] ?? "").split(",")) {
6217
- const text3 = actor.trim();
6218
- if (text3) out.push({ text: text3, target: `line${i2 + 1}` });
6219
- }
6322
+ const headerIndex = lines.findIndex((line2) => /^\s*journey\b/i.test(line2));
6323
+ const push = (text3, lineIndex) => {
6324
+ for (const part of text3.split("\n")) {
6325
+ if (part) out.push({ text: part, target: `line${lineIndex + 1}` });
6220
6326
  }
6221
- }
6327
+ };
6328
+ walkJourneyLines(lines, headerIndex + 1, {
6329
+ title: push,
6330
+ accTitle: push,
6331
+ accDescr: push,
6332
+ section: push,
6333
+ task: (text3, _score, actors, lineIndex) => {
6334
+ push(text3, lineIndex);
6335
+ for (const actor of actors) push(actor, lineIndex);
6336
+ }
6337
+ });
6222
6338
  return out;
6223
6339
  }
6224
6340
  function verifyOpaqueJourney(body) {
6225
6341
  if (body.kind !== "opaque" || body.family !== "journey") return [];
6226
6342
  const warnings = [];
6227
6343
  const lines = body.source.split(/\r?\n/);
6228
- for (let i2 = 0; i2 < lines.length; i2++) {
6229
- const diagnostic = invalidJourneyScoreDiagnostic(lines[i2].trim());
6230
- if (!diagnostic) continue;
6344
+ const headerIndex = lines.findIndex((line2) => /^\s*journey\b/i.test(line2));
6345
+ let blocking;
6346
+ walkJourneyLines(lines, headerIndex + 1, {
6347
+ issue: (issue) => {
6348
+ if (issue.code === "invalid_score") {
6349
+ warnings.push({
6350
+ code: "UNSUPPORTED_SYNTAX",
6351
+ line: issue.lineIndex + 1,
6352
+ syntax: "journey_invalid_score",
6353
+ message: `${issue.detail}.`
6354
+ });
6355
+ return;
6356
+ }
6357
+ blocking ??= issue;
6358
+ }
6359
+ });
6360
+ if (blocking) {
6231
6361
  warnings.push({
6232
6362
  code: "UNSUPPORTED_SYNTAX",
6233
- line: i2 + 1,
6234
- syntax: "journey_invalid_score",
6235
- message: `Journey task "${diagnostic.text}" uses invalid score ${diagnostic.rawScore}; Mermaid Journey scores must be integers from 1 through 5.`
6363
+ line: blocking.lineIndex + 1,
6364
+ syntax: `journey_${blocking.code}`,
6365
+ message: `${blocking.detail}. The diagram is preserved verbatim as source; fix this line to unlock typed journey mutation.`
6236
6366
  });
6237
6367
  }
6238
6368
  return warnings;
6239
6369
  }
6240
- function invalidJourneyScoreDiagnostic(raw2) {
6241
- if (!raw2 || raw2.startsWith("%%")) return null;
6242
- if (/^(?:journey|title|section)\b/i.test(raw2)) return null;
6243
- if (/^acc(?:Title|Descr)\b/i.test(raw2)) return null;
6244
- const numeric = raw2.match(/^(.+?)\s*:\s*([0-9]+)\s*(?::\s*(.*))?$/);
6245
- if (numeric) {
6246
- const score = Number.parseInt(numeric[2], 10);
6247
- if (Number.isInteger(score) && score >= 1 && score <= 5) return null;
6248
- return { text: numeric[1].trim(), rawScore: numeric[2] };
6249
- }
6250
- const taskLike = raw2.match(/^(.+?)\s*:\s*([^:]+?)(?:\s*:\s*.*)?$/);
6251
- if (!taskLike) return null;
6252
- const text3 = taskLike[1].trim();
6253
- const rawScore = taskLike[2].trim();
6254
- if (!text3 || !rawScore) return null;
6255
- return { text: text3, rawScore };
6256
- }
6257
6370
  registerFamily({
6258
6371
  id: "journey",
6259
6372
  detect: (l2) => l2.startsWith("journey"),
@@ -6263,7 +6376,10 @@ registerFamily({
6263
6376
  verify: (body, opts) => body.kind === "journey" ? verifyJourney(body, opts) : verifyOpaqueJourney(body),
6264
6377
  ...structuredFamilyHooks("journey", {
6265
6378
  headerOk: (h2) => /^journey\s*$/i.test(h2),
6266
- parseBody: parseJourneyBody,
6379
+ parseBody: (lines) => {
6380
+ const outcome = parseJourneyBody(lines);
6381
+ return outcome.ok ? outcome.body : null;
6382
+ },
6267
6383
  serialize: renderJourney,
6268
6384
  mutate: mutateJourney
6269
6385
  })
@@ -7052,15 +7168,19 @@ var ER_SCHEMA = {
7052
7168
  };
7053
7169
  var JOURNEY_SCHEMA = {
7054
7170
  set_title: { fields: { title: strOrNull() } },
7055
- add_section: { fields: { label: str() } },
7171
+ add_section: { fields: { label: str(), index: withNote(num(false), "insert position; omit to append") } },
7056
7172
  remove_section: { fields: { index: num() } },
7057
7173
  set_section_label: { fields: { index: num(), label: str() } },
7058
- add_task: { fields: { sectionIndex: num(), text: str(), score: withNote(num(), "integer 1..5"), actors: strArr(false) } },
7174
+ add_task: { fields: { sectionIndex: num(), text: str(), score: withNote(num(), "integer 1..5"), actors: strArr(false), index: withNote(num(false), "insert position; omit to append") } },
7059
7175
  remove_task: { fields: { sectionIndex: num(), taskIndex: num() } },
7060
7176
  set_task_text: { fields: { sectionIndex: num(), taskIndex: num(), text: str() } },
7061
7177
  set_task_score: { fields: { sectionIndex: num(), taskIndex: num(), score: withNote(num(), "integer 1..5") } },
7062
7178
  set_task_actors: { fields: { sectionIndex: num(), taskIndex: num(), actors: strArr() } },
7063
- rename_actor: { fields: { from: str(), to: str() } }
7179
+ rename_actor: { fields: { from: str(), to: str() } },
7180
+ move_task: { fields: { fromSection: num(), fromIndex: num(), toSection: num(), toIndex: withNote(num(), "insert position in the target section, applied after removal") } },
7181
+ move_section: { fields: { from: num(), to: num() } },
7182
+ set_accessibility_title: { fields: { title: strOrNull() } },
7183
+ set_accessibility_description: { fields: { description: strOrNull() } }
7064
7184
  };
7065
7185
  var ARCHITECTURE_SCHEMA = {
7066
7186
  add_service: { fields: { id: str(), label: str(false), icon: strOrNull(false), group: strOrNull(false) } },
@@ -7503,6 +7623,22 @@ function isSixDigitHex(s2) {
7503
7623
  function luma255(r4, g2, b2) {
7504
7624
  return (r4 * 299 + g2 * 587 + b2 * 114) / 1e3;
7505
7625
  }
7626
+ function wcagRelativeLuminance(hex) {
7627
+ const rgb = tryParseHex(hex);
7628
+ if (!rgb) return null;
7629
+ const lin = (channel) => {
7630
+ const c2 = channel / 255;
7631
+ return c2 <= 0.04045 ? c2 / 12.92 : ((c2 + 0.055) / 1.055) ** 2.4;
7632
+ };
7633
+ return 0.2126 * lin(rgb[0]) + 0.7152 * lin(rgb[1]) + 0.0722 * lin(rgb[2]);
7634
+ }
7635
+ function wcagContrastRatio(a2, b2) {
7636
+ const la = wcagRelativeLuminance(a2);
7637
+ const lb = wcagRelativeLuminance(b2);
7638
+ if (la === null || lb === null) return null;
7639
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
7640
+ return (hi + 0.05) / (lo + 0.05);
7641
+ }
7506
7642
 
7507
7643
  // src/theme.ts
7508
7644
  var DEFAULTS = {
@@ -7703,12 +7839,37 @@ function isColorDark(color) {
7703
7839
  const [r4, g2, b2] = parseHex(color);
7704
7840
  return luma255(r4, g2, b2) / 255 < 0.4;
7705
7841
  }
7842
+ function isPlainFontFamily(font) {
7843
+ return /^[A-Za-z0-9][A-Za-z0-9 _-]*$/.test(font);
7844
+ }
7845
+ var RASTER_FONT_FALLBACK = "'Inter'";
7846
+ function resolveRasterFontFamily(fallback) {
7847
+ if (!fallback.includes("var(")) return fallback;
7848
+ const inner = /^var\(\s*--[\w-]+\s*,\s*([^()]+)\)$/.exec(fallback)?.[1]?.trim();
7849
+ return inner || RASTER_FONT_FALLBACK;
7850
+ }
7706
7851
  function inlineFontVarForRaster(svg2) {
7707
- return svg2.replace(/var\(--font,\s*('[^']*')\)/g, "$1");
7852
+ const marker = "var(--font,";
7853
+ let out = "";
7854
+ let i2 = 0;
7855
+ while (true) {
7856
+ const at = svg2.indexOf(marker, i2);
7857
+ if (at === -1) return out + svg2.slice(i2);
7858
+ out += svg2.slice(i2, at);
7859
+ let depth = 1;
7860
+ let j2 = at + marker.length;
7861
+ while (j2 < svg2.length && depth > 0) {
7862
+ if (svg2[j2] === "(") depth++;
7863
+ else if (svg2[j2] === ")") depth--;
7864
+ j2++;
7865
+ }
7866
+ out += resolveRasterFontFamily(svg2.slice(at + marker.length, j2 - 1).trim());
7867
+ i2 = j2;
7868
+ }
7708
7869
  }
7709
7870
  function buildStyleBlock(font, hasMonoFont, shadow, embedFontImport = true) {
7710
7871
  const fontImports = embedFontImport ? [
7711
- `@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(font)}:wght@400;500;600;700&amp;display=swap');`,
7872
+ ...isPlainFontFamily(font) ? [`@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(font)}:wght@400;500;600;700&amp;display=swap');`] : [],
7712
7873
  ...hasMonoFont ? [`@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&amp;display=swap');`] : []
7713
7874
  ] : [];
7714
7875
  const derivedVars = `
@@ -7726,8 +7887,9 @@ function buildStyleBlock(font, hasMonoFont, shadow, embedFontImport = true) {
7726
7887
  --_inner-stroke: color-mix(in srgb, var(--fg) ${MIX.innerStroke}%, var(--bg));
7727
7888
  --_key-badge: color-mix(in srgb, var(--fg) ${MIX.keyBadge}%, var(--bg));`;
7728
7889
  const shadowRules = shadow ? '\n .node, .class-node, .entity, .actor[data-type="participant"], .note, .block, .timeline-event, .journey-task { filter: url(#bm-shadow); }' : "";
7729
- const fontFamilyDecl = ` text { font-family: var(--font, '${font}'), system-ui, sans-serif; }`;
7730
- const styleHead = embedFontImport ? [` ${fontImports.join("\n ")}`, fontFamilyDecl] : [fontFamilyDecl];
7890
+ const fontFallback = isPlainFontFamily(font) ? `'${font}'` : font;
7891
+ const fontFamilyDecl = ` text { font-family: var(--font, ${fontFallback}), system-ui, sans-serif; }`;
7892
+ const styleHead = fontImports.length > 0 ? [` ${fontImports.join("\n ")}`, fontFamilyDecl] : [fontFamilyDecl];
7731
7893
  return [
7732
7894
  "<style>",
7733
7895
  ...styleHead,
@@ -7805,6 +7967,11 @@ function inlineResolvedColors(svg2, colors) {
7805
7967
  vars.set("_key-badge", rc.keyBadge);
7806
7968
  let text3 = svg2;
7807
7969
  const SKIP_VARS = /* @__PURE__ */ new Set(["font"]);
7970
+ if (colors.font) {
7971
+ for (const m2 of colors.font.matchAll(/var\(\s*--([\w-]+)/g)) {
7972
+ if (!vars.has(m2[1])) SKIP_VARS.add(m2[1]);
7973
+ }
7974
+ }
7808
7975
  for (let pass = 0; pass < 10; pass++) {
7809
7976
  const prev = text3;
7810
7977
  text3 = text3.replace(/var\(--([\w-]+)\)/g, (match, name) => {
@@ -15268,7 +15435,7 @@ function indentLines(s2, n2) {
15268
15435
  ${pad}`);
15269
15436
  }
15270
15437
  function group(fields) {
15271
- const join2 = fields.join ?? "\n";
15438
+ const join3 = fields.join ?? "\n";
15272
15439
  const segments = [fields.open];
15273
15440
  for (const child of fields.children) {
15274
15441
  segments.push(indentLines(child.node.crisp, child.indent));
@@ -15276,13 +15443,13 @@ function group(fields) {
15276
15443
  segments.push(fields.close);
15277
15444
  return {
15278
15445
  kind: "group",
15279
- crisp: segments.join(join2),
15446
+ crisp: segments.join(join3),
15280
15447
  id: fields.id,
15281
15448
  role: fields.role,
15282
15449
  open: fields.open,
15283
15450
  close: fields.close,
15284
15451
  children: fields.children,
15285
- join: join2,
15452
+ join: join3,
15286
15453
  channels: fields.channels
15287
15454
  };
15288
15455
  }
@@ -15305,8 +15472,8 @@ function prelude(fields, crisp) {
15305
15472
  }
15306
15473
 
15307
15474
  // src/scene/backend.ts
15308
- function composeGroup(open, close, join2, children) {
15309
- return [open, ...children.map((c2) => indentLines(c2.serialized, c2.indent)), close].join(join2);
15475
+ function composeGroup(open, close, join3, children) {
15476
+ return [open, ...children.map((c2) => indentLines(c2.serialized, c2.indent)), close].join(join3);
15310
15477
  }
15311
15478
  function pageRectFor(doc, ctx) {
15312
15479
  const prelude2 = doc.parts[0];
@@ -16131,21 +16298,21 @@ function parseSequenceDiagram(lines) {
16131
16298
  const blockStack = [];
16132
16299
  for (let i2 = 1; i2 < lines.length; i2++) {
16133
16300
  const line2 = lines[i2];
16134
- const accTitle = parseAccessibilityLine2(line2, "accTitle");
16301
+ const accTitle = parseAccessibilityLine(line2, "accTitle");
16135
16302
  if (accTitle !== void 0) {
16136
16303
  diagram.accessibilityTitle = accTitle;
16137
16304
  continue;
16138
16305
  }
16139
16306
  const accDescrStart = line2.match(/^accDescr\s*:?\s*\{\s*(.*)$/i);
16140
16307
  if (accDescrStart) {
16141
- const parsed = collectAccessibilityBlock2(accDescrStart[1] ?? "", lines, i2);
16308
+ const parsed = collectAccessibilityBlock(accDescrStart[1] ?? "", lines, i2);
16142
16309
  if (parsed) {
16143
16310
  diagram.accessibilityDescription = normalizeBrTags(parsed.text);
16144
16311
  i2 = parsed.nextIndex;
16145
16312
  }
16146
16313
  continue;
16147
16314
  }
16148
- const accDescr = parseAccessibilityLine2(line2, "accDescr");
16315
+ const accDescr = parseAccessibilityLine(line2, "accDescr");
16149
16316
  if (accDescr !== void 0) {
16150
16317
  diagram.accessibilityDescription = accDescr;
16151
16318
  continue;
@@ -16263,11 +16430,11 @@ function parseSequenceDiagram(lines) {
16263
16430
  }
16264
16431
  return diagram;
16265
16432
  }
16266
- function parseAccessibilityLine2(line2, directive) {
16433
+ function parseAccessibilityLine(line2, directive) {
16267
16434
  const match = line2.match(new RegExp(`^${directive}\\s*:?[ \\t]+(.+)$`, "i"));
16268
16435
  return match ? normalizeBrTags(match[1].trim()) : void 0;
16269
16436
  }
16270
- function collectAccessibilityBlock2(initial, lines, startIndex) {
16437
+ function collectAccessibilityBlock(initial, lines, startIndex) {
16271
16438
  const initialEnd = initial.indexOf("}");
16272
16439
  if (initialEnd !== -1) return { text: initial.slice(0, initialEnd).trim(), nextIndex: startIndex };
16273
16440
  const parts = [initial.trim()].filter(Boolean);
@@ -17136,19 +17303,19 @@ function parseClassDiagram(lines) {
17136
17303
  let braceDepth = 0;
17137
17304
  for (let i2 = 1; i2 < lines.length; i2++) {
17138
17305
  const line2 = lines[i2];
17139
- const accTitle = parseAccessibilityLine3(line2, "accTitle");
17306
+ const accTitle = parseAccessibilityLine2(line2, "accTitle");
17140
17307
  if (accTitle !== void 0) {
17141
17308
  diagram.accessibilityTitle = accTitle;
17142
17309
  continue;
17143
17310
  }
17144
17311
  const accDescrStart = line2.match(/^accDescr\s*:?\s*\{\s*(.*)$/i);
17145
17312
  if (accDescrStart) {
17146
- const parsed = collectAccessibilityBlock3(accDescrStart[1] ?? "", lines, i2);
17313
+ const parsed = collectAccessibilityBlock2(accDescrStart[1] ?? "", lines, i2);
17147
17314
  diagram.accessibilityDescription = normalizeBrTags(parsed.text);
17148
17315
  i2 = parsed.nextIndex;
17149
17316
  continue;
17150
17317
  }
17151
- const accDescr = parseAccessibilityLine3(line2, "accDescr");
17318
+ const accDescr = parseAccessibilityLine2(line2, "accDescr");
17152
17319
  if (accDescr !== void 0) {
17153
17320
  diagram.accessibilityDescription = accDescr;
17154
17321
  continue;
@@ -17247,11 +17414,11 @@ function parseClassDiagram(lines) {
17247
17414
  diagram.classes = [...classMap.values()];
17248
17415
  return diagram;
17249
17416
  }
17250
- function parseAccessibilityLine3(line2, directive) {
17417
+ function parseAccessibilityLine2(line2, directive) {
17251
17418
  const match = line2.match(new RegExp(`^${directive}\\s*:?[ \\t]+(.+)$`, "i"));
17252
17419
  return match ? normalizeBrTags(match[1].trim()) : void 0;
17253
17420
  }
17254
- function collectAccessibilityBlock3(initial, lines, startIndex) {
17421
+ function collectAccessibilityBlock2(initial, lines, startIndex) {
17255
17422
  const initialEnd = initial.indexOf("}");
17256
17423
  if (initialEnd !== -1) return { text: initial.slice(0, initialEnd).trim(), nextIndex: startIndex };
17257
17424
  const parts = [initial.trim()].filter(Boolean);
@@ -17999,19 +18166,19 @@ function parseErDiagram(lines) {
17999
18166
  let currentEntity = null;
18000
18167
  for (let i2 = 1; i2 < lines.length; i2++) {
18001
18168
  const line2 = lines[i2];
18002
- const accTitle = parseAccessibilityLine4(line2, "accTitle");
18169
+ const accTitle = parseAccessibilityLine3(line2, "accTitle");
18003
18170
  if (accTitle !== void 0) {
18004
18171
  diagram.accessibilityTitle = accTitle;
18005
18172
  continue;
18006
18173
  }
18007
18174
  const accDescrStart = line2.match(/^accDescr\s*:?\s*\{\s*(.*)$/i);
18008
18175
  if (accDescrStart) {
18009
- const parsed = collectAccessibilityBlock4(accDescrStart[1] ?? "", lines, i2);
18176
+ const parsed = collectAccessibilityBlock3(accDescrStart[1] ?? "", lines, i2);
18010
18177
  diagram.accessibilityDescription = normalizeBrTags(parsed.text);
18011
18178
  i2 = parsed.nextIndex;
18012
18179
  continue;
18013
18180
  }
18014
- const accDescr = parseAccessibilityLine4(line2, "accDescr");
18181
+ const accDescr = parseAccessibilityLine3(line2, "accDescr");
18015
18182
  if (accDescr !== void 0) {
18016
18183
  diagram.accessibilityDescription = accDescr;
18017
18184
  continue;
@@ -18045,11 +18212,11 @@ function parseErDiagram(lines) {
18045
18212
  diagram.entities = [...entityMap.values()];
18046
18213
  return diagram;
18047
18214
  }
18048
- function parseAccessibilityLine4(line2, directive) {
18215
+ function parseAccessibilityLine3(line2, directive) {
18049
18216
  const match = line2.match(new RegExp(`^${directive}\\s*:?[ \\t]+(.+)$`, "i"));
18050
18217
  return match ? normalizeBrTags(match[1].trim()) : void 0;
18051
18218
  }
18052
- function collectAccessibilityBlock4(initial, lines, startIndex) {
18219
+ function collectAccessibilityBlock3(initial, lines, startIndex) {
18053
18220
  const initialEnd = initial.indexOf("}");
18054
18221
  if (initialEnd !== -1) return { text: initial.slice(0, initialEnd).trim(), nextIndex: startIndex };
18055
18222
  const parts = [initial.trim()].filter(Boolean);
@@ -19678,15 +19845,6 @@ function readTimelineScale(themeVariables, prefix, index) {
19678
19845
  }
19679
19846
 
19680
19847
  // src/journey/parser.ts
19681
- function normalizeActorLabel(label) {
19682
- return normalizeJourneyLabel2(label.trim()).split("\n").map((part) => part.trim()).filter(Boolean).join(" / ");
19683
- }
19684
- function normalizeJourneyLabel2(label) {
19685
- return label.replace(/<br\s*\/?>/gi, "\n").replace(/\\n/g, "\n").replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>");
19686
- }
19687
- function isJourneyComment2(line2) {
19688
- return line2.startsWith("%%") || line2.startsWith("#") || line2.startsWith("%");
19689
- }
19690
19848
  function parseJourneyDiagram(lines) {
19691
19849
  const diagram = { sections: [] };
19692
19850
  let currentSection;
@@ -19701,67 +19859,25 @@ function parseJourneyDiagram(lines) {
19701
19859
  diagram.sections.push(currentSection);
19702
19860
  return currentSection;
19703
19861
  };
19704
- for (let i2 = 1; i2 < lines.length; i2++) {
19705
- const line2 = lines[i2];
19706
- if (/^journey\b/i.test(line2)) continue;
19707
- if (isJourneyComment2(line2)) continue;
19708
- const accTitle = parseAccessibilityLine5(line2, "accTitle");
19709
- if (accTitle !== void 0) {
19710
- diagram.accessibilityTitle = accTitle;
19711
- continue;
19712
- }
19713
- const accDescrStart = line2.match(/^accDescr\s*:?\s*\{\s*(.*)$/i);
19714
- if (accDescrStart) {
19715
- const initial = accDescrStart[1] ?? "";
19716
- const parsed = collectAccessibilityBlock5(initial, lines, i2);
19717
- diagram.accessibilityDescription = normalizeJourneyLabel2(parsed.text);
19718
- i2 = parsed.nextIndex;
19719
- continue;
19720
- }
19721
- const accDescr = parseAccessibilityLine5(line2, "accDescr");
19722
- if (accDescr !== void 0) {
19723
- diagram.accessibilityDescription = accDescr;
19724
- continue;
19725
- }
19726
- const titleMatch = line2.match(/^title\s+(.+)$/i);
19727
- if (titleMatch) {
19728
- diagram.title = normalizeJourneyLabel2(titleMatch[1].trim());
19729
- continue;
19730
- }
19731
- const sectionMatch = line2.match(/^section\s+(.+)$/i);
19732
- if (sectionMatch) {
19733
- const label = normalizeJourneyLabel2(sectionMatch[1].trim());
19734
- if (label.includes(":")) {
19735
- throw syntaxError({
19736
- what: `Invalid user journey section: "${line2}"`,
19737
- expectedForm: "section Section name",
19738
- example: "section Go to work"
19739
- });
19740
- }
19862
+ walkJourneyLines(lines, 1, {
19863
+ title: (text3) => {
19864
+ diagram.title = text3;
19865
+ },
19866
+ accTitle: (text3) => {
19867
+ diagram.accessibilityTitle = text3;
19868
+ },
19869
+ accDescr: (text3) => {
19870
+ diagram.accessibilityDescription = text3;
19871
+ },
19872
+ section: (label) => {
19741
19873
  currentSection = {
19742
19874
  id: `section-${sectionIndex++}`,
19743
19875
  label,
19744
19876
  tasks: []
19745
19877
  };
19746
19878
  diagram.sections.push(currentSection);
19747
- continue;
19748
- }
19749
- const taskMatch = line2.match(/^(.+?)\s*:\s*([0-9]+)\s*(?::\s*(.*))?$/);
19750
- if (taskMatch) {
19751
- const text3 = normalizeJourneyLabel2(taskMatch[1].trim());
19752
- const rawScore = taskMatch[2];
19753
- const score = Number.parseInt(rawScore, 10);
19754
- if (!text3) {
19755
- throw syntaxError({
19756
- what: `Invalid user journey task: "${line2}"`,
19757
- expectedForm: "Task name: score: Actor[, Actor\u2026]",
19758
- example: "Pay: 3: Shopper"
19759
- });
19760
- }
19761
- if (!Number.isInteger(score) || score < 1 || score > 5) {
19762
- throw invalidJourneyScoreError(text3, rawScore);
19763
- }
19764
- const actors = (taskMatch[3] ?? "").split(",").map(normalizeActorLabel).filter(Boolean);
19879
+ },
19880
+ task: (text3, score, actors) => {
19765
19881
  const task = {
19766
19882
  id: `task-${taskIndex++}`,
19767
19883
  text: text3,
@@ -19769,57 +19885,38 @@ function parseJourneyDiagram(lines) {
19769
19885
  actors
19770
19886
  };
19771
19887
  ensureSection().tasks.push(task);
19772
- continue;
19773
- }
19774
- const invalidScore = parseInvalidTaskScore(line2);
19775
- if (invalidScore) {
19776
- throw invalidJourneyScoreError(invalidScore.text, invalidScore.rawScore);
19888
+ },
19889
+ issue: (issue) => {
19890
+ throw journeyIssueError(issue);
19777
19891
  }
19778
- throw new Error(`Invalid user journey line: "${line2}". Expected title, section, accessibility metadata, or "Task name: 3: Actor"`);
19779
- }
19892
+ });
19780
19893
  if (diagram.sections.length === 0 && !diagram.title && diagram.sections.every((section) => section.tasks.length === 0) && !diagram.accessibilityTitle && !diagram.accessibilityDescription) {
19781
19894
  throw new Error("Journey diagram must include at least one scored task, a section, or a title");
19782
19895
  }
19783
19896
  return diagram;
19784
19897
  }
19785
- function parseInvalidTaskScore(line2) {
19786
- const match = line2.match(/^(.+?)\s*:\s*([^:]+?)(?:\s*:\s*.*)?$/);
19787
- if (!match) return null;
19788
- const text3 = normalizeJourneyLabel2(match[1].trim());
19789
- const rawScore = match[2].trim();
19790
- if (!text3 || !rawScore) return null;
19791
- return { text: text3, rawScore };
19792
- }
19793
- function invalidJourneyScoreError(text3, rawScore) {
19794
- return new Error(`Journey task "${text3}" has invalid score ${rawScore}. Expected an integer from 1 through 5`);
19795
- }
19796
- function parseAccessibilityLine5(line2, directive) {
19797
- const match = line2.match(new RegExp(`^${directive}\\s*:[ \\t]*(.+)$`, "i"));
19798
- return match ? normalizeJourneyLabel2(match[1].trim()) : void 0;
19799
- }
19800
- function collectAccessibilityBlock5(initial, lines, startIndex) {
19801
- const initialEnd = initial.indexOf("}");
19802
- if (initialEnd !== -1) {
19803
- return {
19804
- text: initial.slice(0, initialEnd).trim(),
19805
- nextIndex: startIndex
19806
- };
19807
- }
19808
- const parts = [initial.trim()].filter(Boolean);
19809
- for (let i2 = startIndex + 1; i2 < lines.length; i2++) {
19810
- const line2 = lines[i2];
19811
- const end = line2.indexOf("}");
19812
- if (end !== -1) {
19813
- const beforeBrace = line2.slice(0, end).trim();
19814
- if (beforeBrace) parts.push(beforeBrace);
19815
- return {
19816
- text: parts.join("\n"),
19817
- nextIndex: i2
19818
- };
19819
- }
19820
- parts.push(line2);
19898
+ function journeyIssueError(issue) {
19899
+ switch (issue.code) {
19900
+ case "section_colon":
19901
+ return syntaxError({
19902
+ what: `Invalid user journey section: "${issue.statement}"`,
19903
+ expectedForm: "section Section name",
19904
+ example: "section Go to work"
19905
+ });
19906
+ case "empty_task_text":
19907
+ return syntaxError({
19908
+ what: `Invalid user journey task: "${issue.statement}"`,
19909
+ expectedForm: "Task name: score: Actor[, Actor\u2026]",
19910
+ example: "Pay: 3: Shopper"
19911
+ });
19912
+ case "invalid_score":
19913
+ case "unclosed_accdescr":
19914
+ return new Error(issue.detail);
19915
+ case "empty_title":
19916
+ case "unrecognized_line":
19917
+ case "empty_journey":
19918
+ return new Error(`Invalid user journey line: "${issue.statement}". Expected title, section, accessibility metadata, or "Task name: 3: Actor"`);
19821
19919
  }
19822
- throw new Error('Journey accDescr block is missing a closing "}"');
19823
19920
  }
19824
19921
 
19825
19922
  // src/journey/layout.ts
@@ -19835,6 +19932,7 @@ var JY = {
19835
19932
  sectionHeaderPadX: 16,
19836
19933
  sectionTaskGap: 30,
19837
19934
  sectionSpanPadX: 10,
19935
+ sectionGap: 8,
19838
19936
  legendGap: 26,
19839
19937
  legendMinWidth: 96,
19840
19938
  legendTitleGap: 26,
@@ -19855,8 +19953,10 @@ var JY = {
19855
19953
  taskToGuideGap: 56,
19856
19954
  scoreStep: 40,
19857
19955
  markerRadius: 16,
19858
- baselineGap: 58,
19859
- minPlotWidth: 320
19956
+ baselineGap: 32,
19957
+ baselineClearance: 6,
19958
+ arrowOverhang: 28,
19959
+ minPlotWidth: 200
19860
19960
  };
19861
19961
  var JOURNEY_STYLE_DEFAULTS = {
19862
19962
  nodeLabelFontSize: JY.taskFontSize,
@@ -19931,7 +20031,12 @@ function layoutJourneyDiagram(diagram, options = {}) {
19931
20031
  )
19932
20032
  ) : 0;
19933
20033
  const sectionMetrics = diagram.sections.map((section) => {
19934
- const label = section.label ? applyTextTransform(section.label, style.groupTextTransform) : void 0;
20034
+ const label = section.label ? wrapLabelToWidth(
20035
+ applyTextTransform(section.label, style.groupTextTransform),
20036
+ visual.maxLabelWidth,
20037
+ style.groupHeaderFontSize,
20038
+ style.groupHeaderFontWeight
20039
+ ) : void 0;
19935
20040
  const labelWidth = label ? measureMultilineText(label, style.groupHeaderFontSize, style.groupHeaderFontWeight).width : 0;
19936
20041
  const tasks = section.tasks.map((task) => measureTask(task.text, task.actors.length, style, visual));
19937
20042
  const emptyWidth = Math.max(visual.taskMinWidth, labelWidth + JY.sectionHeaderPadX * 2);
@@ -19959,57 +20064,53 @@ function layoutJourneyDiagram(diagram, options = {}) {
19959
20064
  for (let sectionIndex = 0; sectionIndex < diagram.sections.length; sectionIndex++) {
19960
20065
  const section = diagram.sections[sectionIndex];
19961
20066
  const metric = sectionMetrics[sectionIndex];
20067
+ const framed = !!metric.label;
20068
+ if (sectionIndex > 0) cursorX += JY.sectionGap;
20069
+ const taskSpan = metric.tasks.length > 0 ? metric.tasks.reduce((sum, task) => sum + task.width, 0) + (metric.tasks.length - 1) * visual.taskGap : 0;
20070
+ const spanWidth = framed ? Math.max(
20071
+ taskSpan > 0 ? taskSpan + JY.sectionSpanPadX * 2 : metric.emptyWidth,
20072
+ metric.labelWidth + JY.sectionHeaderPadX * 2
20073
+ ) : Math.max(taskSpan, metric.tasks.length === 0 ? metric.emptyWidth : 0);
20074
+ const spanX = cursorX;
19962
20075
  const tasks = [];
19963
- let sectionStartX = cursorX;
19964
- let sectionEndX = cursorX;
19965
- if (section.tasks.length === 0) {
19966
- if (cursorX > plotLeft) cursorX += visual.taskGap;
19967
- sectionStartX = cursorX;
19968
- cursorX += metric.emptyWidth;
19969
- sectionEndX = cursorX;
19970
- } else {
19971
- for (let taskIndex = 0; taskIndex < section.tasks.length; taskIndex++) {
19972
- const task = section.tasks[taskIndex];
19973
- const taskMetric = metric.tasks[taskIndex];
19974
- if (cursorX > plotLeft) cursorX += visual.taskGap;
19975
- if (tasks.length === 0) sectionStartX = cursorX;
19976
- const x2 = cursorX;
19977
- const centerX = x2 + taskMetric.width / 2;
19978
- const markerY = scoreToY(task.score, guideTop);
19979
- const actorDots = positionActorDots(task, actorIndex, centerX, taskY + taskMetric.height - style.nodePaddingY - JY.actorDotRadius);
19980
- tasks.push({
19981
- id: task.id,
19982
- sectionId: section.id,
19983
- text: taskMetric.text,
19984
- score: task.score,
19985
- actors: task.actors,
19986
- x: x2,
19987
- y: taskY,
19988
- width: taskMetric.width,
19989
- height: taskMetric.height,
19990
- textX: centerX,
19991
- textY: taskY + style.nodePaddingY + taskMetric.textHeight / 2,
19992
- centerX,
19993
- track: {
19994
- x: centerX,
19995
- y1: taskY + taskMetric.height,
19996
- y2: baselineY
19997
- },
19998
- marker: {
19999
- cx: centerX,
20000
- cy: markerY,
20001
- r: JY.markerRadius,
20002
- score: task.score
20003
- },
20004
- actorDots
20005
- });
20006
- cursorX += taskMetric.width;
20007
- sectionEndX = cursorX;
20008
- }
20076
+ let taskX = spanX + (spanWidth - taskSpan) / 2;
20077
+ for (let taskIndex = 0; taskIndex < section.tasks.length; taskIndex++) {
20078
+ const task = section.tasks[taskIndex];
20079
+ const taskMetric = metric.tasks[taskIndex];
20080
+ if (taskIndex > 0) taskX += visual.taskGap;
20081
+ const x2 = taskX;
20082
+ const centerX = x2 + taskMetric.width / 2;
20083
+ const markerY = scoreToY(task.score, guideTop);
20084
+ const actorDots = positionActorDots(task, actorIndex, centerX, taskY + taskMetric.height - style.nodePaddingY - JY.actorDotRadius);
20085
+ tasks.push({
20086
+ id: task.id,
20087
+ sectionId: section.id,
20088
+ text: taskMetric.text,
20089
+ score: task.score,
20090
+ actors: task.actors,
20091
+ x: x2,
20092
+ y: taskY,
20093
+ width: taskMetric.width,
20094
+ height: taskMetric.height,
20095
+ textX: centerX,
20096
+ textY: taskY + style.nodePaddingY + taskMetric.textHeight / 2,
20097
+ centerX,
20098
+ track: {
20099
+ x: centerX,
20100
+ y1: taskY + taskMetric.height,
20101
+ y2: baselineY
20102
+ },
20103
+ marker: {
20104
+ cx: centerX,
20105
+ cy: markerY,
20106
+ r: JY.markerRadius,
20107
+ score: task.score
20108
+ },
20109
+ actorDots
20110
+ });
20111
+ taskX += taskMetric.width;
20009
20112
  }
20010
- const framed = !!metric.label;
20011
- const spanX = framed ? sectionStartX - JY.sectionSpanPadX : sectionStartX;
20012
- const spanWidth = framed ? Math.max(metric.labelWidth + JY.sectionHeaderPadX * 2, sectionEndX - sectionStartX + JY.sectionSpanPadX * 2) : Math.max(0, sectionEndX - sectionStartX);
20113
+ cursorX = spanX + spanWidth;
20013
20114
  sections.push({
20014
20115
  id: section.id,
20015
20116
  label: metric.label,
@@ -20024,7 +20125,7 @@ function layoutJourneyDiagram(diagram, options = {}) {
20024
20125
  tasks
20025
20126
  });
20026
20127
  }
20027
- const naturalPlotRight = cursorX;
20128
+ const naturalPlotRight = cursorX + JY.arrowOverhang;
20028
20129
  const plotRight = Math.max(naturalPlotRight, plotLeft + JY.minPlotWidth);
20029
20130
  const maxSectionRight = Math.max(plotRight, ...sections.map((section) => section.x + section.width));
20030
20131
  const width = maxSectionRight + visual.paddingX;
@@ -20043,7 +20144,7 @@ function layoutJourneyDiagram(diagram, options = {}) {
20043
20144
  });
20044
20145
  const legendBottom = actors.length > 0 ? actors[actors.length - 1].y + JY.legendDotRadius : 0;
20045
20146
  const height = Math.max(
20046
- baselineY + JY.markerRadius + visual.paddingY,
20147
+ baselineY + JY.baselineClearance + visual.paddingY,
20047
20148
  legendBottom + visual.paddingY
20048
20149
  );
20049
20150
  return {
@@ -20084,7 +20185,12 @@ function layoutJourneyDiagram(diagram, options = {}) {
20084
20185
  };
20085
20186
  }
20086
20187
  function measureTask(text3, actorCount, style, visual) {
20087
- const taskText = applyTextTransform(text3, style.nodeTextTransform);
20188
+ const taskText = wrapLabelToWidth(
20189
+ applyTextTransform(text3, style.nodeTextTransform),
20190
+ visual.maxLabelWidth,
20191
+ style.nodeLabelFontSize,
20192
+ style.nodeLabelFontWeight
20193
+ );
20088
20194
  const textMetrics = measureMultilineText(taskText, style.nodeLabelFontSize, style.nodeLabelFontWeight);
20089
20195
  const actorDotsWidth = actorCount > 0 ? actorCount * JY.actorDotRadius * 2 + (actorCount - 1) * JY.actorDotGap : 0;
20090
20196
  const width = Math.max(
@@ -20147,7 +20253,8 @@ function breakWordToWidth(word, maxWidth, fontSize, fontWeight) {
20147
20253
  for (const char of [...word]) {
20148
20254
  const candidate = current + char;
20149
20255
  if (current && measureTextWidth(stripFormattingTags(candidate), fontSize, fontWeight) > maxWidth) {
20150
- lines.push(`${current}-`);
20256
+ const breakIsFullwidth = isFullwidthChar(current[current.length - 1]) && isFullwidthChar(char);
20257
+ lines.push(breakIsFullwidth ? current : `${current}-`);
20151
20258
  current = char;
20152
20259
  } else {
20153
20260
  current = candidate;
@@ -20156,6 +20263,9 @@ function breakWordToWidth(word, maxWidth, fontSize, fontWeight) {
20156
20263
  if (current) lines.push(current);
20157
20264
  return lines.join("\n");
20158
20265
  }
20266
+ function isFullwidthChar(char) {
20267
+ return /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩\u{1F300}-\u{1FAFF}\u{20000}-\u{2FA1F}]/u.test(char);
20268
+ }
20159
20269
  function cssFontSizeToPx(value, fallback) {
20160
20270
  if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
20161
20271
  if (typeof value !== "string") return fallback;
@@ -20191,9 +20301,9 @@ function positionActorDots(task, actorIndex, centerX, y2) {
20191
20301
  });
20192
20302
  }
20193
20303
  function scoreToY(score, guideTop) {
20194
- return guideTop + (JOURNEY_MAX_SCORE - score) * JY.scoreStep;
20304
+ return guideTop + (JOURNEY_MAX_SCORE2 - score) * JY.scoreStep;
20195
20305
  }
20196
- var JOURNEY_MAX_SCORE = 5;
20306
+ var JOURNEY_MAX_SCORE2 = 5;
20197
20307
 
20198
20308
  // src/xychart/colors.ts
20199
20309
  var CHART_ACCENT_FALLBACK = "#3b82f6";
@@ -20284,7 +20394,7 @@ var JY2 = {
20284
20394
  legendLabelGap: 12,
20285
20395
  legendTitleGap: 26
20286
20396
  };
20287
- var JOURNEY_MAX_SCORE2 = 5;
20397
+ var JOURNEY_MAX_SCORE3 = 5;
20288
20398
  function renderJourneySvg(ctx) {
20289
20399
  return DefaultBackend.render(lowerJourneyScene(ctx), { seed: 0 });
20290
20400
  }
@@ -20295,13 +20405,18 @@ function lowerJourneyScene(ctx) {
20295
20405
  const parts = [];
20296
20406
  const style = resolveJourneyStyle(options);
20297
20407
  const visual = resolveJourneyVisualConfig(options);
20298
- const paints = journeyPaints(style, visual, colors);
20408
+ const paints = journeyPaints(style, visual, colors, {
20409
+ sectionCount: diagram.sections.length,
20410
+ actorCount: diagram.actors.length
20411
+ });
20299
20412
  const accessibility = buildJourneyAccessibility(diagram);
20300
20413
  const uid = journeyNamespace(diagram);
20301
20414
  const arrowMarkerId = `${uid}-arrowhead`;
20302
20415
  const titleId = `${uid}-title`;
20303
20416
  const descId = `${uid}-desc`;
20304
- const journeyCss = journeyStyles(style, visual, paints);
20417
+ const curveMarkers = options.journey?.experienceCurve !== false ? diagram.sections.flatMap((section) => section.tasks.map((task) => task.marker)) : [];
20418
+ const drawCurve = curveMarkers.length >= 2;
20419
+ const journeyCss = journeyStyles(style, visual, paints, drawCurve);
20305
20420
  parts.push(prelude(
20306
20421
  {
20307
20422
  id: "prelude",
@@ -20313,7 +20428,7 @@ function lowerJourneyScene(ctx) {
20313
20428
  hasMonoFont: false,
20314
20429
  extraCss: journeyCss
20315
20430
  },
20316
- openJourneySvgTag(diagram, colors, transparent, accessibility, titleId, descId)
20431
+ openJourneySvgTag(diagram, colors, transparent, accessibility, titleId, descId, options.mermaidConfig?.journey?.useMaxWidth === true)
20317
20432
  ));
20318
20433
  if (accessibility.title) {
20319
20434
  parts.push(raw(
@@ -20342,6 +20457,9 @@ function lowerJourneyScene(ctx) {
20342
20457
  parts.push(renderSectionFrame(section, style, paints, index));
20343
20458
  }
20344
20459
  });
20460
+ if (drawCurve) {
20461
+ parts.push(renderExperienceCurve(curveMarkers, style, paints));
20462
+ }
20345
20463
  for (const section of diagram.sections) {
20346
20464
  for (const task of section.tasks) {
20347
20465
  parts.push(renderTask(task, section.label, style, paints));
@@ -20397,8 +20515,9 @@ function buildJourneyAccessibility(diagram) {
20397
20515
  description: diagram.accessibilityDescription
20398
20516
  };
20399
20517
  }
20400
- function openJourneySvgTag(diagram, colors, transparent, accessibility, titleId, descId) {
20518
+ function openJourneySvgTag(diagram, colors, transparent, accessibility, titleId, descId, useMaxWidth) {
20401
20519
  return svgOpenTag(diagram.width, diagram.height, colors, transparent, {
20520
+ ...useMaxWidth ? { width: "100%", height: "100%", style: `max-width:${diagram.width}px` } : {},
20402
20521
  attrs: buildAccessibilityAttrs(accessibility.title, accessibility.description, titleId, descId, "user journey")
20403
20522
  });
20404
20523
  }
@@ -20413,7 +20532,7 @@ function buildJourneyDefs(colors, paints, arrowMarkerId) {
20413
20532
  ${defs.join("\n")}
20414
20533
  </defs>`;
20415
20534
  }
20416
- function journeyStyles(style, visual, paints) {
20535
+ function journeyStyles(style, visual, paints, drawCurve) {
20417
20536
  const sectionPalette = sectionPaletteCss(paints);
20418
20537
  const sectionBandPalette = sectionBandPaletteCss(paints);
20419
20538
  const sectionLabelPalette = sectionLabelPaletteCss(paints);
@@ -20428,7 +20547,8 @@ ${sectionBandPalette}
20428
20547
  ${sectionLabelPalette}
20429
20548
  .journey-task-box { fill: ${paints.nodeFill}; stroke: ${paints.nodeStroke}; stroke-width: ${style.nodeLineWidth}; }
20430
20549
  .journey-task-text { fill: ${style.nodeTextColor ?? "var(--_text)"};${visual.taskFontFamily ? ` font-family: ${visual.taskFontFamily};` : ""} }
20431
- .journey-track { stroke: color-mix(in srgb, ${paints.nodeStroke} 78%, var(--bg)); stroke-width: ${style.lineWidth}; stroke-dasharray: 4 7; }
20550
+ .journey-track { stroke: color-mix(in srgb, ${paints.nodeStroke} 78%, var(--bg)); stroke-width: ${style.lineWidth}; stroke-dasharray: 4 7; }${drawCurve ? `
20551
+ .journey-curve { fill: none; stroke: color-mix(in srgb, ${paints.arrow} 55%, var(--bg)); stroke-width: ${Math.max(2, style.lineWidth * 2)}; stroke-linecap: round; }` : ""}
20432
20552
  .journey-guide { stroke: color-mix(in srgb, ${paints.nodeStroke} 62%, var(--bg)); stroke-width: 1; }
20433
20553
  .journey-score-label { fill: ${style.edgeTextColor ?? "var(--_text-sec)"}; }
20434
20554
  .journey-baseline { stroke: ${paints.arrow}; stroke-width: ${Math.max(2, style.lineWidth * 2)}; }
@@ -20441,6 +20561,34 @@ ${sectionLabelPalette}
20441
20561
  ${actorPalette2}
20442
20562
  </style>`;
20443
20563
  }
20564
+ function renderExperienceCurve(markers, style, paints) {
20565
+ const points = markers.map((marker) => ({ x: marker.cx, y: marker.cy }));
20566
+ const d2 = smoothCurvePath(points);
20567
+ return connector(
20568
+ {
20569
+ id: "experience-curve",
20570
+ role: "series",
20571
+ geometry: { kind: "path", d: d2, points },
20572
+ lineStyle: "solid",
20573
+ paint: {
20574
+ stroke: `color-mix(in srgb, ${paints.arrow} 55%, var(--bg))`,
20575
+ strokeWidth: String(Math.max(2, style.lineWidth * 2))
20576
+ }
20577
+ },
20578
+ `<path class="journey-curve" d="${d2}" />`
20579
+ );
20580
+ }
20581
+ function smoothCurvePath(points) {
20582
+ const first = points[0];
20583
+ let d2 = `M${first.x},${first.y}`;
20584
+ for (let i2 = 1; i2 < points.length; i2++) {
20585
+ const from = points[i2 - 1];
20586
+ const to = points[i2];
20587
+ const dx = (to.x - from.x) / 3;
20588
+ d2 += ` C${from.x + dx},${from.y} ${to.x - dx},${to.y} ${to.x},${to.y}`;
20589
+ }
20590
+ return d2;
20591
+ }
20444
20592
  function renderScoreGuide(guide, style, arrowMarkerId) {
20445
20593
  const children = [];
20446
20594
  for (const tick of guide.ticks) {
@@ -20456,7 +20604,7 @@ function renderScoreGuide(guide, style, arrowMarkerId) {
20456
20604
  stroke: `color-mix(in srgb, ${style.nodeBorderColor ?? "var(--_node-stroke)"} 62%, var(--bg))`,
20457
20605
  strokeWidth: "1"
20458
20606
  },
20459
- channels: { value: tick.score / JOURNEY_MAX_SCORE2 }
20607
+ channels: { value: tick.score / JOURNEY_MAX_SCORE3 }
20460
20608
  },
20461
20609
  `<line class="journey-guide" x1="${tick.x1}" y1="${tick.y}" x2="${tick.x2}" y2="${tick.y}" />`
20462
20610
  )
@@ -20473,7 +20621,7 @@ function renderScoreGuide(guide, style, arrowMarkerId) {
20473
20621
  fontSize: style.edgeLabelFontSize,
20474
20622
  anchor: "end",
20475
20623
  paint: { fill: style.edgeTextColor ?? "var(--_text-sec)" },
20476
- channels: { value: tick.score / JOURNEY_MAX_SCORE2 }
20624
+ channels: { value: tick.score / JOURNEY_MAX_SCORE3 }
20477
20625
  },
20478
20626
  renderMultilineText(
20479
20627
  String(tick.score),
@@ -20664,7 +20812,7 @@ function renderSectionLabelBand(section, style, paints, sectionIndex) {
20664
20812
  function renderTask(task, sectionLabel, style, paints) {
20665
20813
  const sectionAttr = sectionLabel ? ` data-section="${escapeAttr(sectionLabel)}"` : "";
20666
20814
  const actorAttr = task.actors.length > 0 ? ` data-actors="${escapeAttr(task.actors.join(", "))}"` : "";
20667
- const value = task.score / JOURNEY_MAX_SCORE2;
20815
+ const value = task.score / JOURNEY_MAX_SCORE3;
20668
20816
  const channels = sectionLabel ? { value, category: sectionLabel } : { value };
20669
20817
  const children = [
20670
20818
  { indent: 2, node: renderTrack(task.track, task, style, paints) },
@@ -20696,7 +20844,7 @@ function renderTrack(track, task, style, paints) {
20696
20844
  strokeWidth: String(style.lineWidth),
20697
20845
  strokeDasharray: "4 7"
20698
20846
  },
20699
- channels: { value: task.score / JOURNEY_MAX_SCORE2 }
20847
+ channels: { value: task.score / JOURNEY_MAX_SCORE3 }
20700
20848
  },
20701
20849
  `<line class="journey-track" x1="${track.x}" y1="${track.y1}" x2="${track.x}" y2="${track.y2}" />`
20702
20850
  );
@@ -20827,18 +20975,18 @@ function mouthPath(marker) {
20827
20975
  if (marker.score === 2) return `M${cx - 8},${cy + 10} Q${cx},${cy + 2} ${cx + 8},${cy + 10}`;
20828
20976
  return `M${cx - 9},${cy + 11} Q${cx},${cy} ${cx + 9},${cy + 11}`;
20829
20977
  }
20830
- function journeyPaints(style, visual, colors) {
20978
+ function journeyPaints(style, visual, colors, counts) {
20831
20979
  const arrow = style.edgeStrokeColor ?? "var(--_arrow)";
20832
20980
  const nodeFill = style.nodeFillColor ?? "var(--_node-fill)";
20833
20981
  const nodeStroke = style.nodeBorderColor ?? "var(--_node-stroke)";
20834
20982
  const groupFill = style.groupFillColor ?? `color-mix(in srgb, ${arrow} 8%, var(--bg))`;
20835
20983
  const groupStroke = style.groupBorderColor ?? "var(--_node-stroke)";
20836
20984
  const groupText = style.groupTextColor ?? "var(--_text-sec)";
20837
- const sectionBases = paletteBases(arrow, colors, style);
20985
+ const sectionBases = paletteBases(arrow, colors, style, counts.sectionCount);
20838
20986
  const configuredSectionCount = Math.max(
20839
20987
  visual.sectionFills.length,
20840
20988
  visual.sectionColours.length,
20841
- sectionBases.length
20989
+ Math.min(sectionBases.length, Math.max(counts.sectionCount, 1))
20842
20990
  );
20843
20991
  const sectionFills = Array.from({ length: configuredSectionCount }, (_unused, index) => {
20844
20992
  const explicitFill = visual.sectionFills[index % visual.sectionFills.length];
@@ -20846,7 +20994,7 @@ function journeyPaints(style, visual, colors) {
20846
20994
  });
20847
20995
  const sectionBands = Array.from({ length: configuredSectionCount }, (_unused, index) => {
20848
20996
  const explicitFill = visual.sectionFills[index % visual.sectionFills.length];
20849
- return explicitFill ? `color-mix(in srgb, ${explicitFill} ${index === 0 ? 14 : 16}%, var(--bg))` : style.groupHeaderFillColor ?? `color-mix(in srgb, ${sectionBases[index % sectionBases.length]} ${index === 0 ? 14 : 16}%, var(--bg))`;
20997
+ return explicitFill ?? style.groupHeaderFillColor ?? `color-mix(in srgb, ${sectionBases[index % sectionBases.length]} ${index === 0 ? 14 : 16}%, var(--bg))`;
20850
20998
  });
20851
20999
  const sectionStrokes = Array.from({ length: configuredSectionCount }, (_unused, index) => {
20852
21000
  const explicitFill = visual.sectionFills[index % visual.sectionFills.length];
@@ -20854,9 +21002,13 @@ function journeyPaints(style, visual, colors) {
20854
21002
  });
20855
21003
  const sectionTextColors = Array.from(
20856
21004
  { length: configuredSectionCount },
20857
- (_unused, index) => visual.sectionColours[index % visual.sectionColours.length] ?? style.groupTextColor ?? "var(--_text)"
21005
+ (_unused, index) => contrastGuardedLabelColor(
21006
+ visual.sectionColours.length > 0 ? visual.sectionColours[index % visual.sectionColours.length] : void 0,
21007
+ sectionBands[index],
21008
+ style.groupTextColor ?? "var(--_text)"
21009
+ )
20858
21010
  );
20859
- const actorColors = visual.actorColours.length > 0 ? visual.actorColours : actorPalette(arrow, colors, style);
21011
+ const actorColors = visual.actorColours.length > 0 ? visual.actorColours : actorPalette(arrow, colors, style, counts.actorCount);
20860
21012
  return {
20861
21013
  arrow,
20862
21014
  nodeFill,
@@ -20875,8 +21027,8 @@ function journeyPaints(style, visual, colors) {
20875
21027
  scoreFaceInk: style.edgeTextColor ?? style.nodeTextColor ?? "var(--_text)"
20876
21028
  };
20877
21029
  }
20878
- function paletteBases(arrow, colors, style) {
20879
- const series = concreteSeriesColors(colors, style, 6);
21030
+ function paletteBases(arrow, colors, style, sectionCount) {
21031
+ const series = concreteSeriesColors(colors, style, Math.max(6, sectionCount));
20880
21032
  if (series) return series;
20881
21033
  return [
20882
21034
  arrow,
@@ -20887,10 +21039,29 @@ function paletteBases(arrow, colors, style) {
20887
21039
  "var(--fg)"
20888
21040
  ];
20889
21041
  }
20890
- function actorPalette(arrow, colors, style) {
20891
- const series = concreteSeriesColors(colors, style, 8);
20892
- if (series) return series;
20893
- return [
21042
+ function contrastGuardedLabelColor(explicit, band, fallback) {
21043
+ if (!tryParseHex(band)) return explicit ?? fallback;
21044
+ if (explicit) {
21045
+ const ratio = wcagContrastRatio(explicit, band);
21046
+ if (ratio === null || ratio >= 4.5) return explicit;
21047
+ }
21048
+ return contrastTextColor(band) ?? explicit ?? fallback;
21049
+ }
21050
+ function actorPalette(arrow, colors, style, actorCount) {
21051
+ const count = Math.max(actorCount, 1);
21052
+ if (isHexColor(colors.bg) && isHexColor(colors.fg)) {
21053
+ const resolved = resolveColors(colors);
21054
+ const accent = firstHexColor(style.edgeStrokeColor, colors.accent, resolved.arrow);
21055
+ if (accent) {
21056
+ const [accentHue, accentSat] = hexToHsl(accent);
21057
+ const dark = isDarkBackground(colors.bg);
21058
+ const baseHue = accentSat < 20 ? 230 : accentHue;
21059
+ const saturation = Math.min(72, Math.max(42, accentSat < 20 ? 46 : accentSat));
21060
+ const lightness = dark ? 64 : 38;
21061
+ return Array.from({ length: count }, (_unused, index) => hslToHex((baseHue + index * 137.508) % 360, saturation, lightness));
21062
+ }
21063
+ }
21064
+ const fallback = [
20894
21065
  arrow,
20895
21066
  "var(--line, var(--_line))",
20896
21067
  "var(--accent, var(--_arrow))",
@@ -20900,6 +21071,7 @@ function actorPalette(arrow, colors, style) {
20900
21071
  "color-mix(in srgb, var(--accent, var(--_arrow)) 62%, var(--bg))",
20901
21072
  "color-mix(in srgb, var(--line, var(--_line)) 72%, var(--bg))"
20902
21073
  ];
21074
+ return fallback.slice(0, Math.max(count <= fallback.length ? count : fallback.length, 1));
20903
21075
  }
20904
21076
  function concreteSeriesColors(colors, style, count) {
20905
21077
  if (!isHexColor(colors.bg) || !isHexColor(colors.fg)) return void 0;
@@ -22814,7 +22986,7 @@ function letterAttr7(value) {
22814
22986
  }
22815
22987
 
22816
22988
  // src/quadrant/parser.ts
22817
- var TITLE_RE5 = /^title\s+(.+)$/i;
22989
+ var TITLE_RE4 = /^title\s+(.+)$/i;
22818
22990
  var AXIS_RE2 = /^([xy])-axis\s+(.+)$/i;
22819
22991
  var QUADRANT_RE2 = /^quadrant-([1-4])\s+(.+)$/i;
22820
22992
  var POINT_RE2 = /^(.+?)\s*:\s*\[\s*([^,\]]+)\s*,\s*([^,\]]+)\s*\]\s*(.*)$/;
@@ -22844,7 +23016,7 @@ function parseQuadrantChart(lines) {
22844
23016
  continue;
22845
23017
  }
22846
23018
  let m2;
22847
- if (m2 = line2.match(TITLE_RE5)) {
23019
+ if (m2 = line2.match(TITLE_RE4)) {
22848
23020
  title = normalizeBrTags(m2[1].trim());
22849
23021
  continue;
22850
23022
  }
@@ -30013,6 +30185,72 @@ function renderErAscii(text3, config, colorMode, theme) {
30013
30185
  return canvasToString(canvas, { roleCanvas: rc, colorMode, theme });
30014
30186
  }
30015
30187
 
30188
+ // src/ascii/wrap.ts
30189
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
30190
+ function graphemes(text3) {
30191
+ return Array.from(segmenter.segment(text3), (s2) => s2.segment);
30192
+ }
30193
+ function wrapText(text3, maxWidth, options) {
30194
+ if (!maxWidth || !Number.isFinite(maxWidth) || maxWidth <= 0) return text3.split("\n");
30195
+ const limit = Math.max(1, Math.floor(maxWidth));
30196
+ const lines = [];
30197
+ for (const paragraph of text3.split("\n")) {
30198
+ lines.push(...wrapParagraph(paragraph, limit, options));
30199
+ }
30200
+ return lines.length > 0 ? lines : [""];
30201
+ }
30202
+ function wrapParagraph(text3, maxWidth, options) {
30203
+ if (visualWidth(text3) <= maxWidth) return [text3];
30204
+ const words = text3.split(/\s+/).filter(Boolean);
30205
+ if (words.length === 0) return [""];
30206
+ const lines = [];
30207
+ let current = "";
30208
+ for (const word of words) {
30209
+ if (!current) {
30210
+ const chunks2 = breakWord(word, maxWidth, options);
30211
+ lines.push(...chunks2.slice(0, -1));
30212
+ current = chunks2[chunks2.length - 1] ?? "";
30213
+ continue;
30214
+ }
30215
+ const candidate = `${current} ${word}`;
30216
+ if (visualWidth(candidate) <= maxWidth) {
30217
+ current = candidate;
30218
+ continue;
30219
+ }
30220
+ lines.push(current);
30221
+ const chunks = breakWord(word, maxWidth, options);
30222
+ lines.push(...chunks.slice(0, -1));
30223
+ current = chunks[chunks.length - 1] ?? "";
30224
+ }
30225
+ if (current) lines.push(current);
30226
+ return lines;
30227
+ }
30228
+ function breakWord(word, maxWidth, options) {
30229
+ if (visualWidth(word) <= maxWidth) return [word];
30230
+ const hyphenate = options?.hyphenate ?? true;
30231
+ const clusters = graphemes(word);
30232
+ const lines = [];
30233
+ let start = 0;
30234
+ while (start < clusters.length) {
30235
+ let end = start + 1;
30236
+ while (end < clusters.length && fitsWithBreak(clusters, start, end + 1, maxWidth, hyphenate)) end++;
30237
+ const chunk = clusters.slice(start, end).join("");
30238
+ const hyphen = end < clusters.length && breakNeedsHyphen(clusters[end - 1], clusters[end], hyphenate) && visualWidth(chunk) + 1 <= maxWidth;
30239
+ lines.push(hyphen ? `${chunk}-` : chunk);
30240
+ start = end;
30241
+ }
30242
+ return lines;
30243
+ }
30244
+ function fitsWithBreak(clusters, start, end, maxWidth, hyphenate) {
30245
+ const width = visualWidth(clusters.slice(start, end).join(""));
30246
+ const hyphen = end < clusters.length && breakNeedsHyphen(clusters[end - 1], clusters[end], hyphenate) ? 1 : 0;
30247
+ return width + hyphen <= maxWidth;
30248
+ }
30249
+ function breakNeedsHyphen(before, after, hyphenate) {
30250
+ if (!hyphenate) return false;
30251
+ return !(visualWidth(before) === 2 && visualWidth(after) === 2);
30252
+ }
30253
+
30016
30254
  // src/ascii/timeline.ts
30017
30255
  function renderStyledLine(segments, colorMode, theme) {
30018
30256
  const chars = [];
@@ -30025,7 +30263,7 @@ function renderStyledLine(segments, colorMode, theme) {
30025
30263
  }
30026
30264
  return colorizeLine(chars, roles, theme, colorMode);
30027
30265
  }
30028
- function renderTimelineAscii(lines, config, colorMode = "none", theme = DEFAULT_ASCII_THEME) {
30266
+ function renderTimelineAscii(lines, config, colorMode = "none", theme = DEFAULT_ASCII_THEME, maxWidth) {
30029
30267
  const diagram = parseTimelineDiagram(lines);
30030
30268
  const useAscii = config.useAscii;
30031
30269
  const marker = useAscii ? "o" : "\u25CB";
@@ -30039,7 +30277,7 @@ function renderTimelineAscii(lines, config, colorMode = "none", theme = DEFAULT_
30039
30277
  out.push(segments.length === 0 ? "" : renderStyledLine(segments, colorMode, theme));
30040
30278
  };
30041
30279
  if (diagram.title) {
30042
- for (const line2 of diagram.title.split("\n")) {
30280
+ for (const line2 of wrapText(diagram.title, maxWidth)) {
30043
30281
  pushLine([{ text: line2, role: "text" }]);
30044
30282
  }
30045
30283
  pushLine();
@@ -30047,15 +30285,19 @@ function renderTimelineAscii(lines, config, colorMode = "none", theme = DEFAULT_
30047
30285
  for (let sectionIndex = 0; sectionIndex < diagram.sections.length; sectionIndex++) {
30048
30286
  const section = diagram.sections[sectionIndex];
30049
30287
  if (section.label) {
30050
- pushLine([
30051
- { text: "[", role: "border" },
30052
- { text: section.label.replace(/\n/g, " / "), role: "text" },
30053
- { text: "]", role: "border" }
30054
- ]);
30288
+ const labelLines = wrapText(section.label.replace(/\n/g, " / "), maxWidth ? Math.max(1, maxWidth - 2) : void 0);
30289
+ labelLines.forEach((line2, index) => {
30290
+ const segments = [
30291
+ index === 0 ? { text: "[", role: "border" } : { text: " ", role: null },
30292
+ { text: line2, role: "text" }
30293
+ ];
30294
+ if (index === labelLines.length - 1) segments.push({ text: "]", role: "border" });
30295
+ pushLine(segments);
30296
+ });
30055
30297
  }
30056
30298
  for (let periodIndex = 0; periodIndex < section.periods.length; periodIndex++) {
30057
30299
  const period = section.periods[periodIndex];
30058
- const periodLines = period.label.split("\n");
30300
+ const periodLines = wrapText(period.label, maxWidth ? Math.max(1, maxWidth - 3) : void 0);
30059
30301
  pushLine([
30060
30302
  { text: marker, role: "junction" },
30061
30303
  { text: " ", role: null },
@@ -30069,7 +30311,7 @@ function renderTimelineAscii(lines, config, colorMode = "none", theme = DEFAULT_
30069
30311
  }
30070
30312
  for (let eventIndex = 0; eventIndex < period.events.length; eventIndex++) {
30071
30313
  const event = period.events[eventIndex];
30072
- const eventLines = event.text.split("\n");
30314
+ const eventLines = wrapText(event.text, maxWidth ? Math.max(1, maxWidth - 6) : void 0);
30073
30315
  const junction = eventIndex === period.events.length - 1 ? lastBranch : branch;
30074
30316
  pushLine([
30075
30317
  { text: vertical, role: "line" },
@@ -30313,6 +30555,10 @@ function renderScoreSegments(score, useAscii) {
30313
30555
  ];
30314
30556
  return segments.filter((segment) => segment.text.length > 0);
30315
30557
  }
30558
+ var SCORE_BLOCKS = ["\u2581", "\u2582", "\u2584", "\u2586", "\u2588"];
30559
+ function renderScoreStrip(diagram, useAscii) {
30560
+ return diagram.sections.map((section) => section.tasks.map((task) => useAscii ? String(task.score) : SCORE_BLOCKS[task.score - 1]).join("")).filter((group2) => group2.length > 0).join(" ");
30561
+ }
30316
30562
  function renderJourneyAscii(text3, config, colorMode = "none", theme = DEFAULT_ASCII_THEME, maxWidth) {
30317
30563
  const lines = preprocessMermaidLines(text3);
30318
30564
  const diagram = parseJourneyDiagram(lines);
@@ -30327,16 +30573,30 @@ function renderJourneyAscii(text3, config, colorMode = "none", theme = DEFAULT_A
30327
30573
  }
30328
30574
  pushLine();
30329
30575
  }
30576
+ const scoreStrip = renderScoreStrip(diagram, useAscii);
30577
+ if (scoreStrip) {
30578
+ const stripPrefix = "scores: ";
30579
+ const stripLines = wrapText(scoreStrip, maxWidth ? Math.max(1, maxWidth - stripPrefix.length) : void 0, { hyphenate: false });
30580
+ stripLines.forEach((line2, index) => {
30581
+ pushLine([
30582
+ index === 0 ? { text: stripPrefix, role: "border" } : { text: " ".repeat(stripPrefix.length), role: null },
30583
+ { text: line2, role: "arrow" }
30584
+ ]);
30585
+ });
30586
+ pushLine();
30587
+ }
30330
30588
  for (let sectionIndex = 0; sectionIndex < diagram.sections.length; sectionIndex++) {
30331
30589
  const section = diagram.sections[sectionIndex];
30332
30590
  if (section.label) {
30333
- for (const line2 of wrapText(section.label.replace(/\n/g, " / "), maxWidth ? Math.max(1, maxWidth - 2) : void 0)) {
30334
- pushLine([
30335
- { text: "[", role: "border" },
30336
- { text: line2, role: "text" },
30337
- { text: "]", role: "border" }
30338
- ]);
30339
- }
30591
+ const labelLines = wrapText(section.label.replace(/\n/g, " / "), maxWidth ? Math.max(1, maxWidth - 2) : void 0);
30592
+ labelLines.forEach((line2, index) => {
30593
+ const segments = [
30594
+ index === 0 ? { text: "[", role: "border" } : { text: " ", role: null },
30595
+ { text: line2, role: "text" }
30596
+ ];
30597
+ if (index === labelLines.length - 1) segments.push({ text: "]", role: "border" });
30598
+ pushLine(segments);
30599
+ });
30340
30600
  }
30341
30601
  for (let taskIndex = 0; taskIndex < section.tasks.length; taskIndex++) {
30342
30602
  const task = section.tasks[taskIndex];
@@ -30376,60 +30636,6 @@ function renderJourneyAscii(text3, config, colorMode = "none", theme = DEFAULT_A
30376
30636
  }
30377
30637
  return out.join("\n").trimEnd();
30378
30638
  }
30379
- function wrapText(text3, maxWidth) {
30380
- if (!maxWidth || !Number.isFinite(maxWidth) || maxWidth <= 0) return text3.split("\n");
30381
- const limit = Math.max(1, Math.floor(maxWidth));
30382
- const lines = [];
30383
- for (const paragraph of text3.split("\n")) {
30384
- lines.push(...wrapParagraph(paragraph, limit));
30385
- }
30386
- return lines.length > 0 ? lines : [""];
30387
- }
30388
- function wrapParagraph(text3, maxWidth) {
30389
- if (visualWidth(text3) <= maxWidth) return [text3];
30390
- const words = text3.split(/\s+/).filter(Boolean);
30391
- if (words.length === 0) return [""];
30392
- const lines = [];
30393
- let current = "";
30394
- for (const word of words) {
30395
- if (!current) {
30396
- const chunks2 = breakWord(word, maxWidth);
30397
- lines.push(...chunks2.slice(0, -1));
30398
- current = chunks2[chunks2.length - 1] ?? "";
30399
- continue;
30400
- }
30401
- const candidate = `${current} ${word}`;
30402
- if (visualWidth(candidate) <= maxWidth) {
30403
- current = candidate;
30404
- continue;
30405
- }
30406
- lines.push(current);
30407
- const chunks = breakWord(word, maxWidth);
30408
- lines.push(...chunks.slice(0, -1));
30409
- current = chunks[chunks.length - 1] ?? "";
30410
- }
30411
- if (current) lines.push(current);
30412
- return lines;
30413
- }
30414
- function breakWord(word, maxWidth) {
30415
- if (visualWidth(word) <= maxWidth) return [word];
30416
- const lines = [];
30417
- let current = "";
30418
- let width = 0;
30419
- for (const char of word) {
30420
- const charWidth = visualWidth(char);
30421
- if (current && width + charWidth > maxWidth) {
30422
- lines.push(current);
30423
- current = char;
30424
- width = charWidth;
30425
- } else {
30426
- current += char;
30427
- width += charWidth;
30428
- }
30429
- }
30430
- if (current) lines.push(current);
30431
- return lines;
30432
- }
30433
30639
 
30434
30640
  // src/ascii/xychart.ts
30435
30641
  var PLOT_WIDTH = 60;
@@ -31418,7 +31624,7 @@ registerRenderHooks("timeline", {
31418
31624
  layout: (ctx) => layoutResult(layoutTimelineDiagram(parseTimelineDiagram(ctx.source.lines), ctx.options)),
31419
31625
  renderSvg: svg(renderTimelineSvg),
31420
31626
  lowerScene: scene(lowerTimelineScene),
31421
- renderAscii: (ctx) => renderTimelineAscii(ctx.source.lines, ctx.config, ctx.colorMode, ctx.theme)
31627
+ renderAscii: (ctx) => renderTimelineAscii(ctx.source.lines, ctx.config, ctx.colorMode, ctx.theme, ctx.options.maxWidth)
31422
31628
  });
31423
31629
  registerRenderHooks("journey", {
31424
31630
  layout: (ctx) => layoutResult(layoutJourneyDiagram(parseJourneyDiagram(ctx.source.lines), ctx.renderOptions)),
@@ -34446,7 +34652,8 @@ function journeyToRendered(d2) {
34446
34652
  nodes.push({ id: t2.id, x: f3(t2.x), y: f3(t2.y), w: f3(t2.width), h: f3(t2.height), shape: "rectangle", label: t2.text });
34447
34653
  memberIds.push(t2.id);
34448
34654
  }
34449
- groups.push({ id: s2.id, x: f3(s2.x), y: f3(s2.y), w: f3(s2.width), h: f3(s2.height), members: memberIds, label: s2.label });
34655
+ const bottom = s2.tasks.length > 0 ? Math.max(s2.y + s2.height, ...s2.tasks.map((t2) => t2.y + t2.height)) : s2.y + s2.height;
34656
+ groups.push({ id: s2.id, x: f3(s2.x), y: f3(s2.y), w: f3(s2.width), h: f3(bottom - s2.y), members: memberIds, label: s2.label });
34450
34657
  }
34451
34658
  return { version: 1, kind: d2.kind, nodes, edges: [], groups, bounds: { w: f3(positioned.width), h: f3(positioned.height) } };
34452
34659
  } catch {
@@ -34923,6 +35130,35 @@ function withRenderParity(input, result, opts) {
34923
35130
  return { ...result, ok: false, warnings: [...result.warnings, { code: "RENDER_FAILED", reason }] };
34924
35131
  }
34925
35132
  }
35133
+ var JOURNEY_NOOP_CONFIG_FIELDS = [
35134
+ "boxMargin",
35135
+ "boxTextMargin",
35136
+ "noteMargin",
35137
+ "messageMargin",
35138
+ "messageAlign",
35139
+ "bottomMarginAdj",
35140
+ "rightAngles",
35141
+ "activationWidth",
35142
+ "textPlacement"
35143
+ ];
35144
+ function journeyIneffectiveConfigWarnings(d2) {
35145
+ const configs = [
35146
+ d2.meta.frontmatter?.journey,
35147
+ ...d2.meta.initDirectives.map((directive) => directive.parsed?.journey)
35148
+ ];
35149
+ const present = /* @__PURE__ */ new Set();
35150
+ for (const config of configs) {
35151
+ if (!config || typeof config !== "object") continue;
35152
+ for (const field of JOURNEY_NOOP_CONFIG_FIELDS) {
35153
+ if (field in config) present.add(field);
35154
+ }
35155
+ }
35156
+ return [...present].sort().map((field) => ({
35157
+ code: "INEFFECTIVE_CONFIG",
35158
+ field,
35159
+ message: `Journey config field "${field}" is accepted for Mermaid config-shape compatibility but has no effect on journey geometry or paint.`
35160
+ }));
35161
+ }
34926
35162
  function verifyStructure(input, opts = {}) {
34927
35163
  const d2 = typeof input === "string" ? unwrap(input) : input;
34928
35164
  if (!d2) return finalize([{ code: "EMPTY_DIAGRAM" }], emptyRenderedLayout("flowchart"), opts);
@@ -34930,7 +35166,8 @@ function verifyStructure(input, opts = {}) {
34930
35166
  const metaWarnings = d2.meta.droppedComments?.length ? [{ code: "COMMENT_DROPPED", count: d2.meta.droppedComments.length, lines: d2.meta.droppedComments.map((c2) => c2.line) }] : [];
34931
35167
  const sourceWarnings = d2.kind === "flowchart" ? flowchartUnsupportedSyntaxWarnings(d2.canonicalSource) : [];
34932
35168
  const faithfulnessWarnings = roundtripFaithfulnessWarnings(d2);
34933
- const pluginWarnings = dedupedConcat(dedupedConcat(dedupedConcat(metaWarnings, dispatchFamilyVerify(d2, opts)), sourceWarnings), faithfulnessWarnings);
35169
+ const configWarnings = d2.kind === "journey" ? journeyIneffectiveConfigWarnings(d2) : [];
35170
+ const pluginWarnings = dedupedConcat(dedupedConcat(dedupedConcat(dedupedConcat(metaWarnings, dispatchFamilyVerify(d2, opts)), sourceWarnings), faithfulnessWarnings), configWarnings);
34934
35171
  if (d2.body.kind === "sequence") return mergeFinalize(verifySequence(d2, cap, opts), pluginWarnings, opts);
34935
35172
  if (d2.body.kind === "timeline") return mergeFinalize(verifyTimeline(d2, cap, opts), pluginWarnings, opts);
34936
35173
  if (d2.body.kind === "gantt") {
@@ -34943,7 +35180,9 @@ function verifyStructure(input, opts = {}) {
34943
35180
  const layout = layoutFamilyToRendered(d2) ?? emptyRenderedLayout(d2.kind);
34944
35181
  const familyGeometry = d2.body.kind === "class" || d2.body.kind === "er" ? layoutGeometryWarnings(layout, { edgeAnchors: true, nodeOverlaps: true }) : layoutGeometryWarnings(layout, {
34945
35182
  nodeOverlaps: d2.body.kind === "journey",
34946
- groupContainment: d2.body.kind === "xychart" || d2.body.kind === "quadrant"
35183
+ // Journey sections are groups with task members (family-layouts.ts),
35184
+ // so a task laid outside its section band is a reportable breach.
35185
+ groupContainment: d2.body.kind === "xychart" || d2.body.kind === "quadrant" || d2.body.kind === "journey"
34947
35186
  });
34948
35187
  return finalize(dedupedConcat(pluginWarnings, familyGeometry), layout, opts);
34949
35188
  }
@@ -35968,6 +36207,25 @@ function factsJourney(out, body) {
35968
36207
  if (s2.label) add(out, `journey task ${clean(t2.text)} section ${clean(s2.label)}`);
35969
36208
  });
35970
36209
  });
36210
+ const tasks = body.sections.flatMap((s2) => s2.tasks);
36211
+ add(out, `journey sections ${body.sections.length}`);
36212
+ add(out, `journey tasks ${tasks.length}`);
36213
+ const participation = /* @__PURE__ */ new Map();
36214
+ for (const t2 of tasks) for (const a2 of t2.actors) participation.set(a2, (participation.get(a2) ?? 0) + 1);
36215
+ add(out, `journey actors ${participation.size}`);
36216
+ if (tasks.length > 0) {
36217
+ const scores = tasks.map((t2) => t2.score);
36218
+ const min = Math.min(...scores);
36219
+ const max = Math.max(...scores);
36220
+ const average = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length * 100) / 100;
36221
+ add(out, `journey score range ${min}..${max}`);
36222
+ add(out, `journey average score ${num2(average)}`);
36223
+ for (const t2 of tasks) {
36224
+ if (t2.score === min) add(out, `journey lowest task ${clean(t2.text)} score ${num2(t2.score)}`);
36225
+ if (t2.score === max) add(out, `journey highest task ${clean(t2.text)} score ${num2(t2.score)}`);
36226
+ }
36227
+ }
36228
+ for (const [actor, count] of participation) add(out, `journey actor ${clean(actor)} tasks ${count}`);
35971
36229
  }
35972
36230
  function factsArchitecture(out, body) {
35973
36231
  for (const g2 of body.groups) {
@@ -36135,7 +36393,8 @@ function describeMermaidTree(d2) {
36135
36393
  }
36136
36394
  } else if (d2.body.kind === "journey") {
36137
36395
  for (const s2 of d2.body.sections) for (const t2 of s2.tasks) {
36138
- tree.nodes.push({ id: t2.id, label: t2.text });
36396
+ const actors = t2.actors.length ? `; ${t2.actors.join(", ")}` : "";
36397
+ tree.nodes.push({ id: t2.id, label: `${t2.text} (score ${t2.score}${actors})` });
36139
36398
  }
36140
36399
  } else if (d2.body.kind === "architecture") {
36141
36400
  for (const g2 of d2.body.groups) tree.nodes.push({ id: g2.id, label: g2.label || g2.id });
@@ -36790,16 +37049,223 @@ function sourceLineHints(d2) {
36790
37049
 
36791
37050
  // src/agent/png.ts
36792
37051
  import { fileURLToPath } from "url";
36793
- import { join, dirname } from "path";
37052
+ import { join as join2, dirname } from "path";
36794
37053
  import { existsSync } from "fs";
36795
37054
  import { Resvg } from "@resvg/resvg-js";
37055
+
37056
+ // src/agent/font-coverage.ts
37057
+ import { readdirSync, readFileSync, statSync } from "fs";
37058
+ import { join } from "path";
37059
+ import { decodeXML as decodeXML3 } from "entities";
37060
+ var TAG_TTCF = 1953784678;
37061
+ var TAG_OTTO = 1330926671;
37062
+ var TAG_TRUE = 1953658213;
37063
+ var TAG_CMAP = 1668112752;
37064
+ function parseFontFile(bytes) {
37065
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
37066
+ try {
37067
+ if (dv.getUint32(0) === TAG_TTCF) {
37068
+ const numFonts = dv.getUint32(8);
37069
+ const lookups = [];
37070
+ for (let i2 = 0; i2 < numFonts; i2++) lookups.push(...parseSfnt(dv, dv.getUint32(12 + i2 * 4)));
37071
+ return lookups;
37072
+ }
37073
+ return parseSfnt(dv, 0);
37074
+ } catch {
37075
+ return [];
37076
+ }
37077
+ }
37078
+ function parseSfnt(dv, base) {
37079
+ const version = dv.getUint32(base);
37080
+ if (version !== 65536 && version !== TAG_OTTO && version !== TAG_TRUE) return [];
37081
+ const numTables = dv.getUint16(base + 4);
37082
+ for (let i2 = 0; i2 < numTables; i2++) {
37083
+ const rec = base + 12 + i2 * 16;
37084
+ if (dv.getUint32(rec) === TAG_CMAP) return parseCmapTable(dv, dv.getUint32(rec + 8));
37085
+ }
37086
+ return [];
37087
+ }
37088
+ function parseCmapTable(dv, cmap) {
37089
+ const lookups = [];
37090
+ const numTables = dv.getUint16(cmap + 2);
37091
+ for (let i2 = 0; i2 < numTables; i2++) {
37092
+ const rec = cmap + 4 + i2 * 8;
37093
+ const platform = dv.getUint16(rec);
37094
+ const encoding = dv.getUint16(rec + 2);
37095
+ if (!(platform === 0 || platform === 3 && (encoding === 1 || encoding === 10))) continue;
37096
+ const sub = cmap + dv.getUint32(rec + 4);
37097
+ const format = dv.getUint16(sub);
37098
+ if (format === 4) lookups.push(format4Lookup(dv, sub));
37099
+ else if (format === 12) lookups.push(format12Lookup(dv, sub));
37100
+ else if (format === 6) lookups.push(format6Lookup(dv, sub));
37101
+ else if (format === 0) lookups.push(format0Lookup(dv, sub));
37102
+ }
37103
+ return lookups;
37104
+ }
37105
+ function format4Lookup(dv, sub) {
37106
+ const segCount = dv.getUint16(sub + 6) / 2;
37107
+ const endsAt = sub + 14;
37108
+ const startsAt = endsAt + segCount * 2 + 2;
37109
+ const deltasAt = startsAt + segCount * 2;
37110
+ const rangeOffsetsAt = deltasAt + segCount * 2;
37111
+ return {
37112
+ has(cp) {
37113
+ if (cp > 65535) return false;
37114
+ let lo = 0;
37115
+ let hi = segCount - 1;
37116
+ while (lo < hi) {
37117
+ const mid = lo + hi >> 1;
37118
+ if (dv.getUint16(endsAt + mid * 2) < cp) lo = mid + 1;
37119
+ else hi = mid;
37120
+ }
37121
+ if (dv.getUint16(startsAt + lo * 2) > cp || cp === 65535) return false;
37122
+ const rangeOffset = dv.getUint16(rangeOffsetsAt + lo * 2);
37123
+ if (rangeOffset === 0) return (cp + dv.getInt16(deltasAt + lo * 2) & 65535) !== 0;
37124
+ const glyphAt = rangeOffsetsAt + lo * 2 + rangeOffset + (cp - dv.getUint16(startsAt + lo * 2)) * 2;
37125
+ if (glyphAt + 2 > dv.byteLength) return false;
37126
+ const glyph = dv.getUint16(glyphAt);
37127
+ return glyph !== 0 && (glyph + dv.getInt16(deltasAt + lo * 2) & 65535) !== 0;
37128
+ }
37129
+ };
37130
+ }
37131
+ function format12Lookup(dv, sub) {
37132
+ const numGroups = dv.getUint32(sub + 12);
37133
+ const groupsAt = sub + 16;
37134
+ return {
37135
+ has(cp) {
37136
+ let lo = 0;
37137
+ let hi = numGroups - 1;
37138
+ while (lo <= hi) {
37139
+ const mid = lo + hi >> 1;
37140
+ const at = groupsAt + mid * 12;
37141
+ if (cp < dv.getUint32(at)) hi = mid - 1;
37142
+ else if (cp > dv.getUint32(at + 4)) lo = mid + 1;
37143
+ else return dv.getUint32(at + 8) + (cp - dv.getUint32(at)) !== 0;
37144
+ }
37145
+ return false;
37146
+ }
37147
+ };
37148
+ }
37149
+ function format6Lookup(dv, sub) {
37150
+ const first = dv.getUint16(sub + 6);
37151
+ const count = dv.getUint16(sub + 8);
37152
+ return {
37153
+ has: (cp) => cp >= first && cp < first + count && dv.getUint16(sub + 10 + (cp - first) * 2) !== 0
37154
+ };
37155
+ }
37156
+ function format0Lookup(dv, sub) {
37157
+ return { has: (cp) => cp < 256 && dv.getUint8(sub + 6 + cp) !== 0 };
37158
+ }
37159
+ var FONT_EXT = /\.(ttf|otf|ttc|otc)$/i;
37160
+ var fileLookupCache = /* @__PURE__ */ new Map();
37161
+ function fontFilesUnder(dir, depth = 4) {
37162
+ if (depth < 0) return [];
37163
+ let entries;
37164
+ try {
37165
+ entries = readdirSync(dir).sort();
37166
+ } catch {
37167
+ return [];
37168
+ }
37169
+ const files = [];
37170
+ for (const entry of entries) {
37171
+ const path = join(dir, entry);
37172
+ try {
37173
+ const stat = statSync(path);
37174
+ if (stat.isDirectory()) files.push(...fontFilesUnder(path, depth - 1));
37175
+ else if (FONT_EXT.test(entry)) files.push(path);
37176
+ } catch {
37177
+ }
37178
+ }
37179
+ return files;
37180
+ }
37181
+ function lookupsForDirs(dirs) {
37182
+ const lookups = [];
37183
+ for (const dir of dirs) {
37184
+ for (const file of fontFilesUnder(dir)) {
37185
+ let parsed = fileLookupCache.get(file);
37186
+ if (!parsed) {
37187
+ try {
37188
+ parsed = parseFontFile(readFileSync(file));
37189
+ } catch {
37190
+ parsed = [];
37191
+ }
37192
+ fileLookupCache.set(file, parsed);
37193
+ }
37194
+ lookups.push(...parsed);
37195
+ }
37196
+ }
37197
+ return lookups;
37198
+ }
37199
+ function extractSvgTextContent(svg2) {
37200
+ let out = "";
37201
+ for (const m2 of svg2.matchAll(/<text\b[^>]*>([\s\S]*?)<\/text>/g)) {
37202
+ out += m2[1].replace(/<[^>]*>/g, "");
37203
+ }
37204
+ return decodeXML3(out);
37205
+ }
37206
+ function isIgnorable(cp) {
37207
+ return cp <= 32 || cp === 127 || cp >= 8203 && cp <= 8207 || // zero-width space/joiners, directional marks
37208
+ cp === 8232 || cp === 8233 || cp >= 65024 && cp <= 65039 || // variation selectors (emoji presentation)
37209
+ cp === 65279;
37210
+ }
37211
+ function classifyScript(cp) {
37212
+ const ranges = [
37213
+ [1328, 1423, "Armenian"],
37214
+ [1424, 1535, "Hebrew"],
37215
+ [1536, 1919, "Arabic"],
37216
+ [2304, 2431, "Devanagari"],
37217
+ [2432, 2559, "Bengali"],
37218
+ [3584, 3711, "Thai"],
37219
+ [4256, 4351, "Georgian"],
37220
+ [4352, 4607, "CJK"],
37221
+ // Hangul jamo
37222
+ [9728, 10175, "emoji"],
37223
+ // misc symbols + dingbats
37224
+ [11904, 40959, "CJK"],
37225
+ // radicals, kana, CJK symbols, unified ideographs
37226
+ [43360, 43391, "CJK"],
37227
+ [44032, 55295, "CJK"],
37228
+ // Hangul syllables
37229
+ [63744, 64255, "CJK"],
37230
+ // compatibility ideographs
37231
+ [65072, 65103, "CJK"],
37232
+ [65280, 65519, "CJK"],
37233
+ // fullwidth/halfwidth forms
37234
+ [127462, 127487, "emoji"],
37235
+ // regional indicators (flags)
37236
+ [126976, 129791, "emoji"],
37237
+ [131072, 262143, "CJK"]
37238
+ // ideograph extensions
37239
+ ];
37240
+ for (const [lo, hi, script] of ranges) if (cp >= lo && cp <= hi) return script;
37241
+ return "other";
37242
+ }
37243
+ function findUncoveredScripts(svg2, fontDirs) {
37244
+ const text3 = extractSvgTextContent(svg2);
37245
+ if (!text3) return [];
37246
+ const unique2 = /* @__PURE__ */ new Set();
37247
+ for (const ch of text3) unique2.add(ch.codePointAt(0));
37248
+ const lookups = lookupsForDirs(fontDirs);
37249
+ const byScript = /* @__PURE__ */ new Map();
37250
+ for (const cp of unique2) {
37251
+ if (isIgnorable(cp)) continue;
37252
+ if (lookups.some((l2) => l2.has(cp))) continue;
37253
+ const script = classifyScript(cp);
37254
+ const bucket = byScript.get(script);
37255
+ if (bucket) bucket.push(cp);
37256
+ else byScript.set(script, [cp]);
37257
+ }
37258
+ return [...byScript.entries()].sort(([a2], [b2]) => a2 < b2 ? -1 : 1).map(([script, cps]) => ({ script, chars: cps.sort((a2, b2) => a2 - b2).map((cp) => String.fromCodePoint(cp)) }));
37259
+ }
37260
+
37261
+ // src/agent/png.ts
36796
37262
  var cachedFontDir = null;
36797
37263
  function resolveFontDir() {
36798
37264
  if (cachedFontDir !== null) return cachedFontDir;
36799
37265
  const here = typeof import.meta.url === "string" ? fileURLToPath(import.meta.url) : __filename;
36800
37266
  let dir = dirname(here);
36801
37267
  for (let i2 = 0; i2 < 6; i2++) {
36802
- const candidate = join(dir, "assets", "fonts");
37268
+ const candidate = join2(dir, "assets", "fonts");
36803
37269
  if (existsSync(candidate)) {
36804
37270
  cachedFontDir = candidate;
36805
37271
  return candidate;
@@ -36818,16 +37284,36 @@ function renderMermaidPNG(input, opts = {}) {
36818
37284
  );
36819
37285
  const scale = opts.scale ?? 2;
36820
37286
  const fontDir = resolveFontDir();
37287
+ const loadSystemFonts = opts.loadSystemFonts ?? false;
37288
+ const fontDirs = [...fontDir ? [fontDir] : [], ...opts.fontDirs ?? []];
37289
+ if (!loadSystemFonts) {
37290
+ const emit = opts.onWarning ?? ((w2) => process.stderr.write(`agentic-mermaid renderMermaidPNG: warning ${w2.code}: ${w2.message}
37291
+ `));
37292
+ for (const { script, chars } of findUncoveredScripts(svg2, fontDirs)) {
37293
+ const examples = chars.slice(0, 5).join(" ");
37294
+ emit({
37295
+ code: "PNG_FONT_COVERAGE",
37296
+ script,
37297
+ chars,
37298
+ message: `no bundled font covers ${chars.length} ${script} character${chars.length === 1 ? "" : "s"} (${examples}); they will draw as empty tofu boxes. Point fontDirs (CLI: --font-dirs <dir>) at a font that covers them, or opt in to system fonts with loadSystemFonts: true (CLI: --system-fonts).`
37299
+ });
37300
+ }
37301
+ }
36821
37302
  const resvgOpts = {
36822
37303
  background: opts.background ?? "white",
36823
37304
  fitTo: opts.fitTo?.width ? { mode: "width", value: opts.fitTo.width } : opts.fitTo?.height ? { mode: "height", value: opts.fitTo.height } : { mode: "zoom", value: scale },
36824
37305
  font: {
36825
- loadSystemFonts: false,
36826
- // Bundled fonts (DejaVu Sans + the faces built-in styles reference)
36827
- // for cross-runtime determinism, plus caller-supplied directories.
36828
- // Falls back to resvg's built-in fonts if nothing is found.
36829
- fontDirs: [...fontDir ? [fontDir] : [], ...opts.fontDirs ?? []],
36830
- defaultFontFamily: "DejaVu Sans"
37306
+ loadSystemFonts,
37307
+ // Bundled fonts (Inter + DejaVu Sans + the faces built-in styles
37308
+ // reference) for cross-runtime determinism, plus caller-supplied
37309
+ // directories. Falls back to resvg's built-in fonts if nothing is found.
37310
+ fontDirs,
37311
+ // Inter is the metrics font: src/text-metrics.ts is calibrated for it
37312
+ // and the SVG @import requests it, so rasterizing with anything else
37313
+ // (DejaVu is ~14% wider) pushes long labels outside their measured
37314
+ // boxes. resvg falls back per-glyph across every loaded font, so
37315
+ // DejaVu still covers glyphs Inter lacks (arrows, math, Armenian, …).
37316
+ defaultFontFamily: "Inter"
36831
37317
  }
36832
37318
  };
36833
37319
  const resvg = new Resvg(svg2, resvgOpts);