for-humanity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -0
  4. package/dist/cli.css +962 -0
  5. package/dist/cli.js +1241 -0
  6. package/dist/client.js +5 -0
  7. package/package.json +92 -0
  8. package/src/app.tsx +57 -0
  9. package/src/asset/favicon.svg +5 -0
  10. package/src/asset/font/architects-daughter/LICENSE.txt +93 -0
  11. package/src/asset/font/architects-daughter/architects-daughter-latin-400-normal.woff2 +0 -0
  12. package/src/asset/font/architects-daughter/architects-daughter.css +10 -0
  13. package/src/asset/font/jetbrains-mono/LICENSE +93 -0
  14. package/src/asset/font/jetbrains-mono/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  15. package/src/asset/font/jetbrains-mono/jetbrains-mono.css +11 -0
  16. package/src/asset/font/pretendard/LICENSE.txt +94 -0
  17. package/src/asset/font/pretendard/pretendardvariable-dynamic-subset.css +1008 -0
  18. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.0.woff2 +0 -0
  19. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.1.woff2 +0 -0
  20. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.10.woff2 +0 -0
  21. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.11.woff2 +0 -0
  22. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.12.woff2 +0 -0
  23. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.13.woff2 +0 -0
  24. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.14.woff2 +0 -0
  25. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.15.woff2 +0 -0
  26. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.16.woff2 +0 -0
  27. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.17.woff2 +0 -0
  28. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.18.woff2 +0 -0
  29. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.19.woff2 +0 -0
  30. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.2.woff2 +0 -0
  31. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.20.woff2 +0 -0
  32. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.21.woff2 +0 -0
  33. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.22.woff2 +0 -0
  34. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.23.woff2 +0 -0
  35. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.24.woff2 +0 -0
  36. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.25.woff2 +0 -0
  37. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.26.woff2 +0 -0
  38. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.27.woff2 +0 -0
  39. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.28.woff2 +0 -0
  40. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.29.woff2 +0 -0
  41. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.3.woff2 +0 -0
  42. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.30.woff2 +0 -0
  43. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.31.woff2 +0 -0
  44. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.32.woff2 +0 -0
  45. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.33.woff2 +0 -0
  46. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.34.woff2 +0 -0
  47. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.35.woff2 +0 -0
  48. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.36.woff2 +0 -0
  49. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.37.woff2 +0 -0
  50. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.38.woff2 +0 -0
  51. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.39.woff2 +0 -0
  52. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.4.woff2 +0 -0
  53. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.40.woff2 +0 -0
  54. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.41.woff2 +0 -0
  55. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.42.woff2 +0 -0
  56. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.43.woff2 +0 -0
  57. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.44.woff2 +0 -0
  58. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.45.woff2 +0 -0
  59. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.46.woff2 +0 -0
  60. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.47.woff2 +0 -0
  61. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.48.woff2 +0 -0
  62. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.49.woff2 +0 -0
  63. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.5.woff2 +0 -0
  64. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.50.woff2 +0 -0
  65. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.51.woff2 +0 -0
  66. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.52.woff2 +0 -0
  67. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.53.woff2 +0 -0
  68. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.54.woff2 +0 -0
  69. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.55.woff2 +0 -0
  70. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.56.woff2 +0 -0
  71. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.57.woff2 +0 -0
  72. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.58.woff2 +0 -0
  73. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.59.woff2 +0 -0
  74. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.6.woff2 +0 -0
  75. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.60.woff2 +0 -0
  76. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.61.woff2 +0 -0
  77. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.62.woff2 +0 -0
  78. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.63.woff2 +0 -0
  79. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.64.woff2 +0 -0
  80. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.65.woff2 +0 -0
  81. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.66.woff2 +0 -0
  82. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.67.woff2 +0 -0
  83. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.68.woff2 +0 -0
  84. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.69.woff2 +0 -0
  85. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.7.woff2 +0 -0
  86. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.70.woff2 +0 -0
  87. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.71.woff2 +0 -0
  88. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.72.woff2 +0 -0
  89. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.73.woff2 +0 -0
  90. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.74.woff2 +0 -0
  91. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.75.woff2 +0 -0
  92. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.76.woff2 +0 -0
  93. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.77.woff2 +0 -0
  94. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.78.woff2 +0 -0
  95. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.79.woff2 +0 -0
  96. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.8.woff2 +0 -0
  97. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.80.woff2 +0 -0
  98. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.81.woff2 +0 -0
  99. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.82.woff2 +0 -0
  100. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.83.woff2 +0 -0
  101. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.84.woff2 +0 -0
  102. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.85.woff2 +0 -0
  103. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.86.woff2 +0 -0
  104. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.87.woff2 +0 -0
  105. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.88.woff2 +0 -0
  106. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.89.woff2 +0 -0
  107. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.9.woff2 +0 -0
  108. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.90.woff2 +0 -0
  109. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.91.woff2 +0 -0
  110. package/src/cli.ts +162 -0
  111. package/src/client.ts +322 -0
  112. package/src/component/widget/prose/_constant/heading-highlight.ts +4 -0
  113. package/src/component/widget/prose/_constant/section.ts +7 -0
  114. package/src/component/widget/prose/_constant/status.ts +9 -0
  115. package/src/component/widget/prose/_function/has-block-content.ts +30 -0
  116. package/src/component/widget/prose/_function/rehype-head.ts +31 -0
  117. package/src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts +20 -0
  118. package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +21 -0
  119. package/src/component/widget/prose/_function/rehype-tables.ts +22 -0
  120. package/src/component/widget/prose/_function/remark-blocks.ts +112 -0
  121. package/src/component/widget/prose/_function/remark-flow.ts +26 -0
  122. package/src/component/widget/prose/_function/remark-links.ts +33 -0
  123. package/src/component/widget/prose/_function/remark-parts.ts +26 -0
  124. package/src/component/widget/prose/_function/remark-status.ts +59 -0
  125. package/src/component/widget/prose/_function/remark-swatch.ts +16 -0
  126. package/src/component/widget/prose/_function/remark-unknown-directives.ts +25 -0
  127. package/src/component/widget/prose/_type/doc-section.ts +9 -0
  128. package/src/component/widget/prose/wg-prose.css +381 -0
  129. package/src/component/widget/prose/wg-prose.tsx +43 -0
  130. package/src/component/widget/shell/_constant/cursor-face.ts +9 -0
  131. package/src/component/widget/shell/_constant/navigation.ts +4 -0
  132. package/src/component/widget/shell/_constant/reading-line.ts +4 -0
  133. package/src/component/widget/shell/_function/bind-mobile-navigation.ts +140 -0
  134. package/src/component/widget/shell/_function/to-toc-groups.ts +38 -0
  135. package/src/component/widget/shell/_type/doc-outline.ts +16 -0
  136. package/src/component/widget/shell/_type/toc-group.ts +39 -0
  137. package/src/component/widget/shell/_wg-shell-nav.css +134 -0
  138. package/src/component/widget/shell/_wg-shell-nav.tsx +75 -0
  139. package/src/component/widget/shell/_wg-shell-toc.css +268 -0
  140. package/src/component/widget/shell/_wg-shell-toc.tsx +57 -0
  141. package/src/component/widget/shell/wg-shell.css +261 -0
  142. package/src/component/widget/shell/wg-shell.tsx +147 -0
  143. package/src/constant/asset.ts +48 -0
  144. package/src/constant/cli.ts +19 -0
  145. package/src/constant/copy.ts +108 -0
  146. package/src/constant/doc.ts +13 -0
  147. package/src/constant/font.ts +39 -0
  148. package/src/constant/locale.ts +9 -0
  149. package/src/constant/site.ts +9 -0
  150. package/src/constant/status.ts +15 -0
  151. package/src/content/create-processor.ts +59 -0
  152. package/src/content/read-docs.test.ts +242 -0
  153. package/src/content/read-docs.ts +75 -0
  154. package/src/content/read-home.ts +32 -0
  155. package/src/content/rehype-heading-ids.ts +34 -0
  156. package/src/content/remark-report.ts +15 -0
  157. package/src/content/to-doc-groups.ts +18 -0
  158. package/src/dev.ts +79 -0
  159. package/src/env.d.ts +6 -0
  160. package/src/page/doc/pg-doc.tsx +35 -0
  161. package/src/page/home/pg-home.tsx +40 -0
  162. package/src/style/base.css +28 -0
  163. package/src/style/token.css +162 -0
  164. package/src/type/doc-content.ts +15 -0
  165. package/src/type/doc-data.ts +33 -0
  166. package/src/type/doc-file-data.ts +21 -0
  167. package/src/type/doc-heading.ts +17 -0
  168. package/src/type/doc.ts +16 -0
  169. package/src/type/site-assets.ts +17 -0
  170. package/src/type/site-config.ts +55 -0
  171. package/src/type/vfile-data.d.ts +10 -0
  172. package/src/util/dom/find-hash-target.ts +18 -0
  173. package/src/util/dom/reveal-hash-target.ts +15 -0
  174. package/src/util/error/to-error-message.ts +10 -0
  175. package/src/util/font/to-font-css.test.ts +41 -0
  176. package/src/util/font/to-font-css.ts +28 -0
  177. package/src/util/mermaid/render-flow.js +321 -0
@@ -0,0 +1,321 @@
1
+ /*
2
+ * ASCII 격자의 빌드 시 SVG 렌더링 · CSS 변수 테마
3
+ * 전각 문자의 폭 0 문자 삽입으로 2칸 확보
4
+ * 공유 blueprint 알고리즘 · Biome 제외 · 실행 로직 변경 시 원본 대조
5
+ */
6
+ import {renderMermaidASCII} from "beautiful-mermaid";
7
+
8
+ const esc = (s) => String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9
+
10
+ const ASCII_OPT = {paddingX: 3, paddingY: 2, boxBorderPadding: 1, colorMode: "none"};
11
+ const WIDE = /[ᄀ-ᇿ -〿㄰-㆏가-힯一-鿿぀-ヿ＀-⦆]/;
12
+ const ZW = "​";
13
+ const widenCjk = (src) => src.replace(new RegExp(WIDE.source, "g"), (c) => c + ZW);
14
+ // 선 문자의 연결 방향 · L R U D · r 둥근 모서리
15
+ const LINES = {"─": "LR", "│": "UD", "┌": "RD", "┐": "LD", "└": "RU", "┘": "LU", "├": "UDR", "┤": "UDL", "┬": "LRD", "┴": "LRU", "┼": "LRUD", "╭": "RDr", "╮": "LDr", "╰": "RUr", "╯": "LUr",
16
+ "═": "LRb", "║": "UDb", "╔": "RDb", "╗": "LDb", "╚": "RUb", "╝": "LUb", "╟": "UDRb", "╢": "UDLb", "╌": "LRd", "╎": "UDd"};
17
+ const ARROWS = {"►": "R", "◄": "L", "▼": "D", "▲": "U", "▶": "R", "◀": "L"};
18
+ // 선·화살촉의 접점: 상자 벽 선의 가운데
19
+ const isWall = (chr) => chr === "│" || chr === "◇" || chr === "├" || chr === "┤";
20
+ const isHBorder = (chr) => chr !== undefined && chr !== " " && chr !== ZW && (LINES[chr] !== undefined || chr === "◇");
21
+
22
+ // ASCII 라벨 구간의 3칸 여백 보정
23
+ // 1) 글자를 가르지 않는 열 삽입 · 라벨 양옆 최소 6칸 확보
24
+ // 2) 라벨 중앙 정렬 · 줄 길이 유지로 세로선 정렬 보존
25
+ function widenLabelGaps(rows) {
26
+ const width = Math.max.apply(null, rows.map((r) => r.length));
27
+ let grid = rows.map((r) => r.padEnd(width));
28
+ const H = new Set("─┬┴├┤┼◇┌┐└┘╭╮╰╯═╌►◄▶◀╔╗╚╝╟╢".split(""));
29
+ const ANCHOR = new Set("├┤┬┴┼└┘┌┐╭╮╰╯".split(""));
30
+ const END = new Set("►▶◄◀┤├┬┴┼┐┘┌└╮╯╭╰│".split(""));
31
+ const isTxt = (chr) => chr !== undefined && chr !== " " && chr !== ZW && chr !== "│" && !H.has(chr);
32
+ const txtish = (chr) => isTxt(chr) || chr === ZW;
33
+ const splittable = (col) => grid.every((r) => !(txtish(r[col - 1]) && txtish(r[col])));
34
+ const filler = (r, col) => {
35
+ const l = r[col - 1], rt = r[col];
36
+ if (l === undefined || rt === undefined) return " ";
37
+ if ((H.has(l) || txtish(l)) && (H.has(rt) || rt === "│" || txtish(rt)) && !(txtish(l) && txtish(rt))) {
38
+ if (!(H.has(l) || H.has(rt))) return " ";
39
+ return l === "╌" || rt === "╌" ? "╌" : "─";
40
+ }
41
+ return " ";
42
+ };
43
+ // 선 위 라벨 · 양쪽 선 또는 모서리·화살표에 닿은 글자 묶음
44
+ const runs = (row) => {
45
+ const out = [];
46
+ for (const m of row.matchAll(/(─*)((?:[가-힣A-Za-z0-9]​?)+)(─*)/g)) {
47
+ if (m[2].length === 0) continue;
48
+ const before = row[m.index - 1], after = row[m.index + m[0].length];
49
+ const leftOk = m[1].length > 0 || ANCHOR.has(before);
50
+ const rightOk = m[3].length > 0 || END.has(after);
51
+ if (leftOk && rightOk && (m[1].length + m[3].length > 0 || (ANCHOR.has(before) && END.has(after)))) {
52
+ out.push({start: m.index, label: m[2], left: m[1].length, right: m[3].length, end: m.index + m[0].length});
53
+ }
54
+ }
55
+ return out;
56
+ };
57
+ const want = 6;
58
+ // 1) 오른쪽부터 열 삽입 · 기존 인덱스 보존
59
+ const inserts = [];
60
+ grid.forEach((row) => {
61
+ for (const run of runs(row)) {
62
+ const missing = want - (run.left + run.right);
63
+ if (missing > 0) inserts.push({at: run.end, alt: run.start + run.left, n: missing});
64
+ }
65
+ });
66
+ inserts.sort((x, y) => y.at - x.at);
67
+ for (const ins of inserts) {
68
+ const candidates = [ins.at, ins.at + 1, ins.at + 2, ins.alt, ins.alt - 1];
69
+ const col = candidates.find((c) => c > 0 && c < width + 40 && splittable(c));
70
+ if (col === undefined) continue;
71
+ grid = grid.map((r) => r.slice(0, col) + filler(r, col).repeat(ins.n) + r.slice(col));
72
+ }
73
+ // 2) 라벨 중앙 정렬
74
+ return grid.map((row) => {
75
+ let out = row;
76
+ for (const run of runs(row).reverse()) {
77
+ const total = run.left + run.right, l = Math.floor(total / 2), rgt = total - l;
78
+ out = out.slice(0, run.start) + "─".repeat(l) + run.label + "─".repeat(rgt) + out.slice(run.end);
79
+ }
80
+ return out;
81
+ });
82
+ }
83
+
84
+ function gridToSvg(ascii) {
85
+ const cw = 7.2, ch = 17, fs = 12;
86
+ const rows = widenLabelGaps(ascii.replace(/\s+$/, "").split("\n"));
87
+ const cols = Math.max.apply(null, rows.map((r) => r.length));
88
+ const f = (n) => n.toFixed(1);
89
+ let path = "", bold = "", dashed = "", arcs = "", tris = "", marks = "", dots = "", texts = "";
90
+
91
+ const isText = (chr) => chr !== undefined && chr !== " " && chr !== ZW && LINES[chr] === undefined && ARROWS[chr] === undefined && chr !== "◇";
92
+ const center = (c) => c * cw + cw / 2;
93
+ const textWidth = (chars) => chars.reduce((w, chr) => w + (WIDE.test(chr) ? fs : cw), 0);
94
+
95
+ rows.forEach((row, r) => {
96
+ const cy = r * ch + ch / 2, y0 = r * ch, y1 = y0 + ch;
97
+ let run = null;
98
+ // 전각 선 라벨의 1칸 오차 보정
99
+ // 벽부터 화살촉까지 중앙 정렬 · 라벨 폭만큼 선 비움
100
+ const skip = new Set();
101
+ for (let c = 0; c < row.length; c++) {
102
+ if (!isText(row[c]) || skip.has(c)) continue;
103
+ let e = c;
104
+ while (e < row.length && (isText(row[e]) || row[e] === ZW || (row[e] === " " && isText(row[e + 1])))) e++;
105
+ // 테두리 라벨로 사라진 분기점 ┬ 복원
106
+ // 테두리·세로선 연결 · 라벨은 다음 줄의 세로선 옆
107
+ if (row[c - 1] === "─" && row[e] === "─") {
108
+ const below = rows[r + 1] || "", above = rows[r - 1] || "";
109
+ let exit = null;
110
+ for (let k = c - 2; k < e + 2 && exit === null; k++) {
111
+ if (below[k] === "│" || below[k] === "▼") exit = {k: k, dir: 1};
112
+ else if (above[k] === "│" || above[k] === "▲") exit = {k: k, dir: -1};
113
+ }
114
+ if (exit) {
115
+ const kx = center(exit.k), chars = row.slice(c, e).split("").filter((chr) => chr !== ZW);
116
+ path += "M" + f(c * cw) + " " + f(cy) + "H" + f(e * cw) + " ";
117
+ path += "M" + f(kx) + " " + f(cy) + "V" + f(exit.dir > 0 ? y1 : y0) + " ";
118
+ texts += '<text x="' + f(kx + cw * 0.8 + textWidth(chars) / 2) + '" y="' + f(cy + exit.dir * ch + fs * 0.35) + '">' + esc(chars.join("")) + "</text>";
119
+ for (let k = c; k < e; k++) skip.add(k);
120
+ c = e - 1;
121
+ continue;
122
+ }
123
+ }
124
+
125
+ let l = c - 1, rr = e;
126
+ while (l >= 0 && row[l] === " ") l--;
127
+ while (rr < row.length && row[rr] === " ") rr++;
128
+ const onLine = (row[l] === "─" || row[l] === "├") && (row[rr] === "─" || ARROWS[row[rr]] !== undefined || row[rr] === "┤");
129
+ if (!onLine) { c = e - 1; continue; }
130
+ let L = l;
131
+ while (L - 1 >= 0 && row[L - 1] === "─") L--;
132
+ // 출발점 ├ 앞의 ASCII 여백을 건너 상자 벽까지 연결
133
+ let segStart = L * cw, drawStart = L * cw;
134
+ if (row[L] === "├" || row[L - 1] === "├") {
135
+ const j = row[L] === "├" ? L : L - 1;
136
+ const above = rows[r - 1] ? rows[r - 1][j] : " ", below = rows[r + 1] ? rows[r + 1][j] : " ";
137
+ const junction = (above !== undefined && LINES[above] !== undefined && /[UD]/.test(LINES[above])) ||
138
+ (below !== undefined && LINES[below] !== undefined && /[UD]/.test(LINES[below]));
139
+ if (junction) {
140
+ // 상자 벽인 ├는 기본 루프에서 유지 · 연결선만 벽 가운데부터 시작
141
+ segStart = center(j);
142
+ drawStart = center(j);
143
+ for (let m = j + 1; m < L; m++) skip.add(m);
144
+ } else {
145
+ // 벽과 떨어진 ├는 별도 표식 없이 연결선으로 대체
146
+ let k = j - 1;
147
+ while (k >= 0 && row[k] === " ") k--;
148
+ segStart = isWall(row[k]) ? center(k) : center(j);
149
+ drawStart = segStart;
150
+ for (let m = j; m < L; m++) skip.add(m);
151
+ }
152
+ } else if (LINES[row[L - 1]] !== undefined || isWall(row[L - 1])) {
153
+ segStart = center(L - 1);
154
+ }
155
+ let R = rr;
156
+ while (R + 1 < row.length && row[R + 1] === "─") R++;
157
+ let segEnd = (R + 1) * cw, drawEnd = (R + 1) * cw;
158
+ if (ARROWS[row[R]] !== undefined) {
159
+ drawEnd = R * cw;
160
+ segEnd = isWall(row[R + 1]) ? center(R + 1) : center(R) + cw * 0.45;
161
+ R--;
162
+ } else if (ARROWS[row[R + 1]] !== undefined) {
163
+ drawEnd = (R + 1) * cw;
164
+ segEnd = isWall(row[R + 2]) ? center(R + 2) : center(R + 1) + cw * 0.45;
165
+ } else if (LINES[row[R + 1]] !== undefined || isWall(row[R + 1])) {
166
+ segEnd = center(R + 1);
167
+ }
168
+ const chars = row.slice(c, e).split("").filter((chr) => chr !== ZW);
169
+ const mid = (segStart + segEnd) / 2, half = textWidth(chars) / 2 + 5;
170
+ path += "M" + f(drawStart) + " " + f(cy) + "H" + f(mid - half) + " ";
171
+ path += "M" + f(mid + half) + " " + f(cy) + "H" + f(drawEnd) + " ";
172
+ texts += '<text x="' + f(mid) + '" y="' + f(cy + fs * 0.35) + '">' + esc(chars.join("")) + "</text>";
173
+ for (let k = L; k <= R; k++) skip.add(k);
174
+ c = e - 1;
175
+ }
176
+ // 영문은 격자 칸별 배치 · 한글 혼합은 예약 칸 중앙의 글자 묶음
177
+ // 상자 내부 라벨은 실제 테두리 중앙 · ASCII의 반 칸 오차 보정
178
+ // 테두리 없는 라벨은 예약 칸 기준 정렬
179
+ const wall = (chr) => chr === "│" || chr === "◇" || chr === "├" || chr === "┤";
180
+ const walls = (from, to) => {
181
+ let l = from - 1, rgt = to;
182
+ while (l >= 0 && (row[l] === " " || row[l] === ZW)) l--;
183
+ while (rgt < row.length && (row[rgt] === " " || row[rgt] === ZW)) rgt++;
184
+ return wall(row[l] || "") && wall(row[rgt] || "") ? [l, rgt] : null;
185
+ };
186
+ // 상자 내부와 라벨의 줄 수 홀짝 차이 · 반 줄 처짐 보정
187
+ const lift = (box) => {
188
+ const col = run.start;
189
+ let top = r - 1, bottom = r + 1;
190
+ while (top >= 0 && !isHBorder((rows[top] || "")[col])) top--;
191
+ while (bottom < rows.length && !isHBorder((rows[bottom] || "")[col])) bottom++;
192
+ // 실제 상자 모서리만 보정 · 시퀀스 생명선 제외
193
+ const corner = (chr) => chr !== undefined && chr !== "─" && chr !== "═" && chr !== "╌" && (LINES[chr] !== undefined || chr === "◇");
194
+ if (top < 0 || bottom >= rows.length || !corner((rows[top] || "")[box[0]]) || !corner((rows[bottom] || "")[box[0]])) return 0;
195
+ let labelRows = 0;
196
+ for (let k = top + 1; k < bottom; k++) {
197
+ const inner = (rows[k] || "").slice(box[0] + 1, box[1]).split(ZW).join("").trim();
198
+ if (inner.length > 0) labelRows++;
199
+ }
200
+ return (bottom - top - 1 - labelRows) % 2 === 1 ? -ch / 2 : 0;
201
+ };
202
+ const flush = () => {
203
+ if (!run) return;
204
+ const box = walls(run.start, run.end);
205
+ const y = f(cy + fs * 0.35 + (box ? lift(box) : 0));
206
+ const x = box ? f(((box[0] + 1) * cw + box[1] * cw) / 2) : run.wide ? f((run.start * cw + run.end * cw) / 2) : run.xs.join(" ");
207
+ texts += '<text x="' + x + '" y="' + y + '">' + esc(run.chars.join("")) + "</text>";
208
+ run = null;
209
+ };
210
+
211
+ for (let c = 0; c < row.length; c++) {
212
+ const chr = row[c];
213
+ const cx = c * cw + cw / 2, x0 = c * cw, x1 = x0 + cw;
214
+
215
+ if (skip.has(c)) { flush(); continue; }
216
+ if (chr === ZW) { if (run) run.end = c + 1; continue; }
217
+
218
+ // 라벨 내부 단일 공백은 묶음 유지 · 연속 공백은 묶음 종료
219
+ if (chr === " ") {
220
+ const next = row[c + 1];
221
+ const joins = run && next !== undefined && next !== " " && next !== ZW && !LINES[next] && !ARROWS[next] && next !== "◇";
222
+ if (!joins) { flush(); continue; }
223
+ }
224
+
225
+ // 세로선 없는 ├·┤는 ASCII 가로선의 출발·도착점
226
+ // 상자와의 여백까지 연결
227
+ const above = rows[r - 1] ? rows[r - 1][c] : " ", below = rows[r + 1] ? rows[r + 1][c] : " ";
228
+ const vertical = (v) => v !== undefined && LINES[v] !== undefined && /[UD]/.test(LINES[v]) || v === "◇";
229
+ if ((chr === "├" || chr === "┤") && !vertical(above) && !vertical(below)) {
230
+ flush();
231
+ let l = c - 1, rgt = c + 1;
232
+ while (l >= 0 && row[l] === " ") l--;
233
+ while (rgt < row.length && row[rgt] === " ") rgt++;
234
+ const gapL = isWall(row[l]) ? l * cw + cw / 2 : x0;
235
+ const gapR = isWall(row[rgt]) ? rgt * cw + cw / 2 : x1;
236
+ path += "M" + f(gapL) + " " + f(cy) + "H" + f(gapR) + " ";
237
+ continue;
238
+ }
239
+
240
+ const ln = LINES[chr];
241
+ if (ln) {
242
+ flush();
243
+ if (ln.indexOf("r") >= 0) {
244
+ const ax = ln.indexOf("L") >= 0 ? x0 : x1, by = ln.indexOf("U") >= 0 ? y0 : y1;
245
+ arcs += "M" + f(ax) + " " + f(cy) + "Q" + f(cx) + " " + f(cy) + " " + f(cx) + " " + f(by) + " ";
246
+ } else {
247
+ // 벽 접점까지 선 연장 · b 이중선, d 점선
248
+ let seg = "";
249
+ if (ln.indexOf("L") >= 0) seg += "M" + f(isWall(row[c - 1]) ? x0 - cw / 2 : x0) + " " + f(cy) + "H" + f(cx) + " ";
250
+ if (ln.indexOf("R") >= 0) seg += "M" + f(cx) + " " + f(cy) + "H" + f(isWall(row[c + 1]) ? x1 + cw / 2 : x1) + " ";
251
+ if (ln.indexOf("U") >= 0) seg += "M" + f(cx) + " " + f(y0) + "V" + f(cy) + " ";
252
+ if (ln.indexOf("D") >= 0) seg += "M" + f(cx) + " " + f(cy) + "V" + f(y1) + " ";
253
+ if (ln.indexOf("b") >= 0) bold += seg; else if (ln.indexOf("d") >= 0) dashed += seg; else path += seg;
254
+ }
255
+ continue;
256
+ }
257
+
258
+ const ar = ARROWS[chr];
259
+ if (ar) {
260
+ flush();
261
+ // 화살촉의 벽 접점 보정
262
+ const w = cw * 0.9, h = ch * 0.42;
263
+ const tipR = isWall(row[c + 1]) ? x1 + cw / 2 : cx + w / 2, tipL = isWall(row[c - 1]) ? x0 - cw / 2 : cx - w / 2;
264
+ const tipD = isHBorder(below) ? y1 + ch / 2 : cy + h / 2, tipU = isHBorder(above) ? y0 - ch / 2 : cy - h / 2;
265
+ if (ar === "R") { path += "M" + f(x0) + " " + f(cy) + "H" + f(tipR - w) + " "; tris += "M" + f(tipR - w) + " " + f(cy - h / 2) + "L" + f(tipR) + " " + f(cy) + "L" + f(tipR - w) + " " + f(cy + h / 2) + "Z "; }
266
+ if (ar === "L") { path += "M" + f(tipL + w) + " " + f(cy) + "H" + f(x1) + " "; tris += "M" + f(tipL + w) + " " + f(cy - h / 2) + "L" + f(tipL) + " " + f(cy) + "L" + f(tipL + w) + " " + f(cy + h / 2) + "Z "; }
267
+ if (ar === "D") { path += "M" + f(cx) + " " + f(y0) + "V" + f(tipD - h) + " "; tris += "M" + f(cx - w / 2) + " " + f(tipD - h) + "L" + f(cx + w / 2) + " " + f(tipD - h) + "L" + f(cx) + " " + f(tipD) + "Z "; }
268
+ if (ar === "U") { path += "M" + f(cx) + " " + f(tipU + h) + "V" + f(y1) + " "; tris += "M" + f(cx - w / 2) + " " + f(tipU + h) + "L" + f(cx + w / 2) + " " + f(tipU + h) + "L" + f(cx) + " " + f(tipU) + "Z "; }
269
+ continue;
270
+ }
271
+
272
+ if (chr === "◇") {
273
+ flush();
274
+ const w = cw * 0.8, h = ch * 0.4;
275
+ marks += "M" + f(cx) + " " + f(cy - h / 2) + "L" + f(cx + w / 2) + " " + f(cy) + "L" + f(cx) + " " + f(cy + h / 2) + "L" + f(cx - w / 2) + " " + f(cy) + "Z ";
276
+ continue;
277
+ }
278
+
279
+ // 상태도 시작 ● · 클래스 상속 △
280
+ if (chr === "●") {
281
+ flush();
282
+ const rr = cw * 0.45;
283
+ dots += "M" + f(cx - rr) + " " + f(cy) + "a" + f(rr) + " " + f(rr) + " 0 1 0 " + f(rr * 2) + " 0a" + f(rr) + " " + f(rr) + " 0 1 0 " + f(-rr * 2) + " 0Z ";
284
+ continue;
285
+ }
286
+
287
+ if (chr === "△") {
288
+ flush();
289
+ const w = cw * 0.9, h = ch * 0.42;
290
+ marks += "M" + f(cx) + " " + f(cy - h / 2) + "L" + f(cx + w / 2) + " " + f(cy + h / 2) + "L" + f(cx - w / 2) + " " + f(cy + h / 2) + "Z ";
291
+ path += "M" + f(cx) + " " + f(cy + h / 2) + "V" + f(y1) + " ";
292
+ continue;
293
+ }
294
+
295
+ if (!run) run = {start: c, end: c + 1, xs: [], chars: [], wide: false};
296
+ run.end = c + 1;
297
+ run.xs.push(f(cx));
298
+ run.chars.push(chr);
299
+ if (WIDE.test(chr)) run.wide = true;
300
+ }
301
+
302
+ flush();
303
+ });
304
+
305
+ const W = f(cols * cw), H = f(rows.length * ch);
306
+
307
+ return '<svg xmlns="http://www.w3.org/2000/svg" class="ascii-flow" viewBox="0 0 ' + W + " " + H + '" width="' + W + '" height="' + H + '">' +
308
+ '<path d="' + path + '" fill="none" stroke="var(--app-color-text-muted)" stroke-width="1"/>' +
309
+ '<path d="' + bold + '" fill="none" stroke="var(--app-color-text-muted)" stroke-width="2"/>' +
310
+ '<path d="' + dashed + '" fill="none" stroke="var(--app-color-text-muted)" stroke-width="1" stroke-dasharray="3 3"/>' +
311
+ '<path d="' + arcs + '" fill="none" stroke="var(--app-color-text-muted)" stroke-width="1"/>' +
312
+ '<path d="' + tris + '" fill="var(--app-color-text-muted)"/>' +
313
+ '<path d="' + dots + '" fill="var(--app-color-text-muted)"/>' +
314
+ '<path d="' + marks + '" fill="var(--app-color-surface)" stroke="var(--app-color-text-muted)" stroke-width="1"/>' +
315
+ '<g font-family="var(--app-font-mono)" font-size="' + fs + '" text-anchor="middle" fill="var(--app-color-text)">' + texts + "</g></svg>";
316
+ }
317
+
318
+ /**
319
+ * Mermaid 원문의 격자 SVG 문자열
320
+ */
321
+ export const renderFlow = (source) => gridToSvg(renderMermaidASCII(widenCjk(source), ASCII_OPT));