w-flowchart 1.0.2 → 1.0.4

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 (103) hide show
  1. package/README.md +2 -0
  2. package/dist/w-flowchart.umd.js +2 -2
  3. package/dist/w-flowchart.umd.js.map +1 -1
  4. package/docs/WFlowchart.mjs.html +3 -3
  5. package/docs/global.html +3 -3
  6. package/docs/index.html +1 -1
  7. package/package.json +3 -22
  8. package/src/WFlowchart.mjs +2 -2
  9. package/src/common/cdn.mjs +72 -0
  10. package/src/p1/README.md +1 -1
  11. package/src/p1/gen.mjs +11 -8
  12. package/src/p10/README.md +1 -1
  13. package/src/p10/gen.mjs +11 -11
  14. package/src/p2/README.md +2 -2
  15. package/src/p2/gen.mjs +6 -20
  16. package/src/p3/README.md +2 -2
  17. package/src/p3/gen.mjs +18 -14
  18. package/src/p4/README.md +2 -2
  19. package/src/p4/gen.mjs +7 -11
  20. package/src/p5/gen.mjs +2 -2
  21. package/src/p6/README.md +1 -1
  22. package/src/p6/gen.mjs +6 -9
  23. package/src/p7/README.md +2 -2
  24. package/src/p7/gen.mjs +18 -15
  25. package/src/p8/README.md +2 -2
  26. package/src/p8/gen.mjs +11 -11
  27. package/src/p9/README.md +1 -1
  28. package/src/p9/gen.mjs +8 -8
  29. package/src/p9/page9.html +1 -1
  30. package/test/cases.mjs +82 -1
  31. package/test/pics/p1/newline-label.png +0 -0
  32. package/test/pics/p1/newline-lr.png +0 -0
  33. package/test/pics/p1/newline-multi.png +0 -0
  34. package/test/pics/p10/cycle.svg +1 -1
  35. package/test/pics/p10/deep-nest.svg +1 -1
  36. package/test/pics/p10/dia-1in-1out.svg +1 -1
  37. package/test/pics/p10/dia-1in-2out.svg +1 -1
  38. package/test/pics/p10/dia-1in.svg +1 -1
  39. package/test/pics/p10/dia-1out.svg +1 -1
  40. package/test/pics/p10/dia-2in-1out.svg +1 -1
  41. package/test/pics/p10/dia-2in-2out-dd.svg +1 -1
  42. package/test/pics/p10/dia-2in-2out-ss.svg +1 -1
  43. package/test/pics/p10/dia-2in.svg +1 -1
  44. package/test/pics/p10/dia-2out.svg +1 -1
  45. package/test/pics/p10/diamond.svg +1 -1
  46. package/test/pics/p10/edges-mix.svg +1 -1
  47. package/test/pics/p10/english.svg +1 -1
  48. package/test/pics/p10/flat-tb.svg +1 -1
  49. package/test/pics/p10/fold-tall.svg +1 -1
  50. package/test/pics/p10/fold-wide.svg +1 -1
  51. package/test/pics/p10/group-edge.svg +1 -1
  52. package/test/pics/p10/items-node.svg +1 -1
  53. package/test/pics/p10/long-label.svg +1 -1
  54. package/test/pics/p10/lr-flat.svg +1 -1
  55. package/test/pics/p10/nested-group.svg +1 -1
  56. package/test/pics/p10/newline-label.png +0 -0
  57. package/test/pics/p10/newline-label.svg +5 -0
  58. package/test/pics/p10/newline-lr.png +0 -0
  59. package/test/pics/p10/newline-lr.svg +7 -0
  60. package/test/pics/p10/newline-multi.png +0 -0
  61. package/test/pics/p10/newline-multi.svg +7 -0
  62. package/test/pics/p10/no-edges.svg +1 -1
  63. package/test/pics/p10/title-overflow.svg +1 -1
  64. package/test/pics/p10/valign.svg +1 -1
  65. package/test/pics/p10//345/267/245/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.svg +1 -1
  66. package/test/pics/p10//345/276/256/346/234/215/345/213/231/345/217/242/351/233/206/346/236/266/346/247/213/345/234/226.svg +1 -1
  67. package/test/pics/p10//346/234/203/345/223/241/350/250/273/345/206/212/346/265/201/347/250/213/345/234/226.svg +1 -1
  68. package/test/pics/p10//346/250/241/347/265/204/347/265/204/346/210/220/345/234/226.svg +1 -1
  69. package/test/pics/p10//346/254/212/351/231/220/350/247/222/350/211/262/351/227/234/350/201/257/345/234/226.svg +1 -1
  70. package/test/pics/p10//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.svg +1 -1
  71. package/test/pics/p10//347/263/273/347/265/261/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.svg +1 -1
  72. package/test/pics/p10//350/250/202/345/226/256/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.svg +1 -1
  73. package/test/pics/p10//350/263/207/346/226/231/346/265/201/345/220/221/345/234/226.svg +1 -1
  74. package/test/pics/p10//350/263/207/346/226/231/346/270/205/346/264/227/346/265/201/347/250/213/345/234/226.svg +1 -1
  75. package/test/pics/p10//351/233/273/345/225/206/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.svg +1 -1
  76. package/test/pics/p10//351/240/201/351/235/242/345/260/216/350/246/275/351/227/234/350/201/257/345/234/226.svg +1 -1
  77. package/test/pics/p2/newline-label.png +0 -0
  78. package/test/pics/p2/newline-lr.png +0 -0
  79. package/test/pics/p2/newline-multi.png +0 -0
  80. package/test/pics/p3/newline-label.png +0 -0
  81. package/test/pics/p3/newline-lr.png +0 -0
  82. package/test/pics/p3/newline-multi.png +0 -0
  83. package/test/pics/p4/newline-label.png +0 -0
  84. package/test/pics/p4/newline-lr.png +0 -0
  85. package/test/pics/p4/newline-multi.png +0 -0
  86. package/test/pics/p5/newline-label.png +0 -0
  87. package/test/pics/p5/newline-lr.png +0 -0
  88. package/test/pics/p5/newline-multi.png +0 -0
  89. package/test/pics/p6/newline-label.png +0 -0
  90. package/test/pics/p6/newline-lr.png +0 -0
  91. package/test/pics/p6/newline-multi.png +0 -0
  92. package/test/pics/p7/newline-label.png +0 -0
  93. package/test/pics/p7/newline-lr.png +0 -0
  94. package/test/pics/p7/newline-multi.png +0 -0
  95. package/test/pics/p8/newline-label.png +0 -0
  96. package/test/pics/p8/newline-lr.png +0 -0
  97. package/test/pics/p8/newline-multi.png +0 -0
  98. package/test/pics/p9/newline-label.png +0 -0
  99. package/test/pics/p9/newline-lr.png +0 -0
  100. package/test/pics/p9/newline-multi.png +0 -0
  101. package/toolg/gDistRollup.mjs +1 -20
  102. package/src/common/pkg.mjs +0 -31
  103. package/test/pics/p6//347/211/251/350/201/257/347/266/262/345/271/263/345/217/260/346/236/266/346/247/213/345/234/226.new.png +0 -0
package/src/p10/gen.mjs CHANGED
@@ -5,10 +5,9 @@
5
5
  // 原則: 字型全 JhengHei; 群組容器由 ELK 正確包住成員(不逃逸); 緊湊; 中文不爆框; 邊標籤白光暈不遮線。
6
6
  import { chromium } from 'playwright'
7
7
  import { colorOf, isGroupCls, EDGE } from '../common/palette.mjs'
8
- import { pkgScript } from '../common/pkg.mjs'
8
+ import { cdnScript } from '../common/cdn.mjs'
9
9
 
10
- // elkjs 由本機 node_modules 內聯注入(取代 CDN, 斷網環境可用)
11
- const ELK_JS = pkgScript('elkjs/lib/elk.bundled.js')
10
+ // elkjs jsDelivr CDN 載入(免安裝; 版本鎖定見 common/cdn.mjs)
12
11
 
13
12
  // 正規化數據 → spec(節點/群組(可巢狀)/邊)
14
13
  export function translate(data) {
@@ -24,7 +23,7 @@ export function translate(data) {
24
23
  }
25
24
 
26
25
  export const PAGE_HTML = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
27
- <script>${ELK_JS}</script>
26
+ ${cdnScript('elkjs')}
28
27
  <style>body{margin:0;background:#fff} .lbl{font-family:'Microsoft JhengHei','微軟正黑體',sans-serif}</style>
29
28
  </head><body>
30
29
  <div id="meas" style="position:absolute;left:-99999px;top:0;font-family:'Microsoft JhengHei',sans-serif;visibility:hidden"></div>
@@ -38,7 +37,8 @@ const VALIGN = { top:'TOP', center:'CENTER', bottom:'BOTTOM' }, DEF_VALIGN = 'to
38
37
  const meas = document.getElementById('meas')
39
38
  function measure(label, fs, maxW){
40
39
  const d=document.createElement('div')
41
- d.style.cssText='display:inline-block;white-space:normal;overflow-wrap:break-word;word-break:normal;text-align:center;line-height:1.4;font-size:'+fs+'px;max-width:'+maxW+'px;padding:0;font-family:'+FONT
40
+ // white-space:pre-line: label 內 \\n 為強制換行(其餘空白照常摺疊); 與渲染端 foreignObject div 一致
41
+ d.style.cssText='display:inline-block;white-space:pre-line;overflow-wrap:break-word;word-break:normal;text-align:center;line-height:1.4;font-size:'+fs+'px;max-width:'+maxW+'px;padding:0;font-family:'+FONT
42
42
  d.textContent=label; meas.appendChild(d); const r=d.getBoundingClientRect(); meas.removeChild(d)
43
43
  return { w:Math.ceil(r.width), h:Math.ceil(r.height) }
44
44
  }
@@ -182,7 +182,7 @@ window.renderFig = async function(spec){
182
182
  const ORDER_OPTS = { 'elk.layered.crossingMinimization.semiInteractive':'true' }
183
183
  const hasCycle = (() => { const adj={}; spec.edges.forEach(e=>{ (adj[e.from]=adj[e.from]||[]).push(e.to) }); const st={}; const dfs=(u)=>{ st[u]=1; for(const v of (adj[u]||[])){ if(st[v]===1) return true; if(!st[v] && dfs(v)) return true } st[u]=2; return false }; return Object.keys(adj).some(u=>!st[u]&&dfs(u)) })()
184
184
  const gOpts = { 'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED', 'elk.padding':'[top=10,left=14,bottom=14,right=14]', 'elk.nodeLabels.placement':'[H_CENTER, V_TOP, INSIDE]', 'elk.algorithm':'layered', 'elk.direction':dir, 'elk.edgeRouting':'ORTHOGONAL', 'elk.hierarchyHandling':'INCLUDE_CHILDREN', 'elk.spacing.nodeNode':'28', 'elk.layered.spacing.nodeNodeBetweenLayers':'30', 'elk.spacing.edgeEdge':'22', 'elk.layered.spacing.edgeEdgeBetweenLayers':'24', ...ORDER_OPTS }
185
- function elkEdge(e){ const lab = e.label ? [{ text:e.label, width: measure(e.label,EFS,300).w, height:18 }] : []; return { id:e.id, sources:[e.from], targets:[e.to], labels:lab } }
185
+ function elkEdge(e){ let lab=[]; if(e.label){ const m=measure(e.label,EFS,300); lab=[{ text:e.label, width:m.w, height:m.h }] } return { id:e.id, sources:[e.from], targets:[e.to], labels:lab } }
186
186
  function build(parentId, padMap, wrapAR){
187
187
  const children=[], edges=[]
188
188
  spec.groups.filter(g=>g.parent===parentId).forEach(g=>{ const sub=build(g.id, padMap, wrapAR); const gm=measure(g.label,GFS,400)
@@ -240,7 +240,7 @@ window.renderFig = async function(spec){
240
240
  groupsOut.sort((a,b)=>a.depth-b.depth).forEach(o=>{
241
241
  svg+='<rect x="'+(o.x)+'" y="'+(o.y)+'" width="'+o.w+'" height="'+o.h+'" rx="8" fill="'+o.g.fill+'" fill-opacity="0.38" stroke="'+o.g.stroke+'" stroke-width="2"/>'
242
242
  const tw=o.lbl?o.lbl.width+12:o.w, th=o.lbl?o.lbl.height+6:24, tx=o.x+Math.max(0,(o.w-tw)/2), ty=o.y+(o.lbl?o.lbl.y:5)
243
- svg+='<foreignObject x="'+tx+'" y="'+ty+'" width="'+tw+'" height="'+th+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:'+GFS+'px;color:'+o.g.stroke+';line-height:1.3;overflow-wrap:break-word;word-break:normal">'+esc(o.g.label)+'</div></foreignObject>'
243
+ svg+='<foreignObject x="'+tx+'" y="'+ty+'" width="'+tw+'" height="'+th+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:'+GFS+'px;color:'+o.g.stroke+';line-height:1.3;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(o.g.label)+'</div></foreignObject>'
244
244
  })
245
245
  // 2) 邊(在節點之下、容器之上)
246
246
  const nodeBoxById={}; nodesOut.forEach(o=>{ nodeBoxById[o.n.id]={ x:o.x, y:o.y, w:o.w, h:o.h, diamond:!!o.n.diamond } })
@@ -366,7 +366,7 @@ window.renderFig = async function(spec){
366
366
  })
367
367
  // 邊標籤(白光暈)
368
368
  ;(e.labels||[]).forEach(l=>{ if(!l.text) return; const lx=o.ox+(l.x||0), ly=o.oy+(l.y||0), lw=l.width||40, lh=l.height||18
369
- svg+='<foreignObject x="'+lx+'" y="'+ly+'" width="'+(lw+6)+'" height="'+(lh+4)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+EFS+'px;color:'+EDGE_TEXT+';text-shadow:0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+';white-space:nowrap">'+esc(l.text)+'</div></foreignObject>' })
369
+ svg+='<foreignObject x="'+lx+'" y="'+ly+'" width="'+(lw+6)+'" height="'+(lh+4)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+EFS+'px;color:'+EDGE_TEXT+';text-shadow:0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+',0 0 3px '+HALO+';white-space:pre-line">'+esc(l.text)+'</div></foreignObject>' })
370
370
  })
371
371
  // 3) 節點(最上)。各文字 div 之斷行 CSS(overflow-wrap:break-word + word-break:normal)須與 measure()/measureItems() 一致,
372
372
  // 否則長英文識別字量測時折行、渲染時不折 → 單行溢出 foreignObject 被裁切
@@ -379,14 +379,14 @@ window.renderFig = async function(spec){
379
379
  svg+='<path d="M '+(o.x+r)+' '+o.y+' H '+(o.x+o.w-r)+' A '+r+' '+r+' 0 0 1 '+(o.x+o.w)+' '+(o.y+r)+' V '+sep+' H '+o.x+' V '+(o.y+r)+' A '+r+' '+r+' 0 0 1 '+(o.x+r)+' '+o.y+' Z" fill="'+mix(n.fill,n.stroke,0.16)+'"/>' // 2 標題背景(上圓角下平)
380
380
  svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="'+r+'" fill="none" stroke="'+n.stroke+'" stroke-width="1.6"/>' // 3 框線(頂層)
381
381
  svg+='<line x1="'+o.x+'" y1="'+sep+'" x2="'+(o.x+o.w)+'" y2="'+sep+'" stroke="'+n.stroke+'" stroke-width="1"/>' // 4 分隔線
382
- svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+headH+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:'+FS+'px;color:'+n.font+'"><div style="width:100%;min-width:0;overflow-wrap:break-word;word-break:normal">'+esc(n.title||n.label)+'</div></div></foreignObject>' // 5 標題文字
382
+ svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+headH+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:'+FS+'px;color:'+n.font+'"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.title||n.label)+'</div></div></foreignObject>' // 5 標題文字
383
383
  svg+='<foreignObject x="'+o.x+'" y="'+sep+'" width="'+o.w+'" height="'+(o.h-headH)+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:'+(FS-1)+'px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:'+n.font+'">'+lis+'</div></foreignObject>' // 6 items文字
384
384
  } else if(n.diamond){ const cx=o.x+o.w/2, cy=o.y+o.h/2
385
385
  svg+='<polygon points="'+cx+','+o.y+' '+(o.x+o.w)+','+cy+' '+cx+','+(o.y+o.h)+' '+o.x+','+cy+'" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.8"/>'
386
- svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'
386
+ svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'
387
387
  } else {
388
388
  svg+='<rect x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'" rx="7" fill="'+n.fill+'" stroke="'+n.stroke+'" stroke-width="1.6"/>'
389
- svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'
389
+ svg+='<foreignObject x="'+o.x+'" y="'+o.y+'" width="'+o.w+'" height="'+o.h+'"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:'+FS+'px;color:'+n.font+';line-height:1.4;padding:0 4px;box-sizing:border-box"><div style="width:100%;min-width:0;white-space:pre-line;overflow-wrap:break-word;word-break:normal">'+esc(n.label)+'</div></div></foreignObject>'
390
390
  }
391
391
  })
392
392
 
package/src/p2/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## 技術核心
4
4
 
5
- - **繪圖庫**:mermaid 第 11 版系列(package.json 以 npm alias `mermaid11` 安裝,與 p1 之第 10 版系列並存)+ `@mermaid-js/layout-elk`。兩者皆為 chunk 式 ESM(無 UMD 版可內聯注入),改以虛擬 origin `https://pkg.local/` 供應本機 `node_modules` 檔案:`routeLocalPkgs()` 對該 page 掛 `page.route(PKG_ORIGIN + '**', ...)`,攔截後以 `fs.readFileSync(pkgFile(rel))` 讀本機檔案回應(含相對 chunk import,因同 origin 而一併被攔截解析),頁內腳本以 `await import('https://pkg.local/mermaid11/dist/mermaid.esm.min.mjs')` 等路徑動態載入;不經 CDN,斷網環境亦可運作。
6
- - **ELK 排版引擎**:啟動前呼叫 `mermaid.registerLayoutLoaders(elk)`,再於 frontmatter(`FM` 常數)中指定 `layout: elk`,使所有圖改由 ELK 演算法自動計算節點/邊位置,取代 mermaid 原生 Dagre。**實測事實**:舊版以 CDN `+esm` 載入 `@mermaid-js/layout-elk` 時,`registerLayoutLoaders` 會靜默失效並悄悄退回 dagre 版面(無錯誤訊息);改為現行本機 ESM chunk 載入後,才確實套用 ELK 排版。
5
+ - **繪圖庫**:mermaid 第 11 版系列(與 p1 之第 10 版系列版本各自獨立鎖定,見 `src/common/cdn.mjs`)+ `@mermaid-js/layout-elk`。兩者皆為 chunk 式 ESM(無 UMD 版可內聯注入),改由 jsDelivr CDN 直接以 ESM `import()` 動態載入:頁內腳本以 `await import('https://cdn.jsdelivr.net/npm/mermaid@11.16.0/dist/mermaid.esm.min.mjs')` jsDelivr 網址載入(相對 chunk import 由 jsDelivr 循同 origin 自動解析);渲染時需連網存取 CDN
6
+ - **ELK 排版引擎**:啟動前呼叫 `mermaid.registerLayoutLoaders(elk)`,再於 frontmatter(`FM` 常數)中指定 `layout: elk`,使所有圖改由 ELK 演算法自動計算節點/邊位置,取代 mermaid 原生 Dagre。**實測事實**:舊版以 CDN `+esm`(esm.sh 轉換端點)載入 `@mermaid-js/layout-elk` 時,`registerLayoutLoaders` 會靜默失效並悄悄退回 dagre 版面(無錯誤訊息);改為現行直接載入 jsDelivr 提供之 dist ESM 檔後,才確實套用 ELK 排版。
7
7
  - **渲染方式**:透過 `mermaid.render('g'+idx, code)` 在頁內取得 SVG 字串,注入 `#box` div,再呼叫 `page.locator('#box svg').screenshot()` 對 SVG 元素精確截圖,回傳 PNG Buffer(不落地寫檔)。
8
8
  - **解析度**:`browser.newPage({ deviceScaleFactor: 2 })` 讓截圖以 2× 像素密度渲染,輸出圖像像素尺寸為 SVG 邏輯尺寸的兩倍。
9
9
  - **SVG 尺寸固定**:`renderFig` 內先讀 `viewBox.baseVal` 取 `w`/`h`,若取不到則 fallback 至 `svgEl.getBBox()`,再以 `setAttribute('width', Math.ceil(w))` / `setAttribute('height', Math.ceil(h))` 鎖定尺寸,防止截圖留白。
package/src/p2/gen.mjs CHANGED
@@ -3,24 +3,11 @@
3
3
  // 版面通用化: dir 取自 data.dir; 子群組方向自動取補方向; classDef/顏色取自 palette;
4
4
  // 無逐圖魔術數字, ELK 全自動排版。
5
5
  import { chromium } from 'playwright'
6
- import fs from 'fs'
7
6
  import { PALETTE, isGroupCls } from '../common/palette.mjs'
8
- import { pkgFile } from '../common/pkg.mjs'
9
-
10
- // mermaid@11 與 layout-elk 為 chunk 式 ESM(無 UMD 可內聯), 改以虛擬 origin 提供本機 node_modules 檔案
11
- // (page.route 攔截 https://pkg.local/**, fs 直讀回應; 相對 chunk import 亦循同 origin 解析; 斷網環境可用)
12
- const PKG_ORIGIN = 'https://pkg.local/'
13
- async function routeLocalPkgs(page) {
14
- await page.route(PKG_ORIGIN + '**', (route) => {
15
- const rel = decodeURIComponent(route.request().url().slice(PKG_ORIGIN.length).split('?')[0])
16
- try {
17
- route.fulfill({ body: fs.readFileSync(pkgFile(rel)), contentType: rel.endsWith('.css') ? 'text/css' : 'text/javascript' })
18
- }
19
- catch (err) {
20
- route.fulfill({ status: 404, body: String(err && err.message || err) })
21
- }
22
- })
23
- }
7
+ import { cdnUrl } from '../common/cdn.mjs'
8
+
9
+ // mermaid@11 與 layout-elk 為 chunk 式 ESM, jsDelivr CDN import() 動態載入(免安裝; 版本鎖定見 common/cdn.mjs)
10
+ // (相對 chunk import jsDelivr 循同 origin 自動解析)
24
11
 
25
12
  const FM = `---\nconfig:\n layout: elk\n---\n`
26
13
 
@@ -151,8 +138,8 @@ const setup = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
151
138
  <div id="box" style="display:inline-block;padding:24px"></div>
152
139
  <script type="module">
153
140
  try {
154
- const mermaid = (await import('https://pkg.local/mermaid11/dist/mermaid.esm.min.mjs')).default
155
- const elk = (await import('https://pkg.local/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs')).default
141
+ const mermaid = (await import('${cdnUrl('mermaid11-esm')}')).default
142
+ const elk = (await import('${cdnUrl('layout-elk-esm')}')).default
156
143
  mermaid.registerLayoutLoaders(elk)
157
144
  mermaid.initialize({ startOnLoad:false, theme:'base', flowchart:{ useMaxWidth:false }, themeVariables:{ fontFamily:"'微軟正黑體','Microsoft JhengHei',sans-serif", fontSize:'17px', primaryColor:'#eef4fb', primaryBorderColor:'#3f6fb0', lineColor:'#44505a', primaryTextColor:'#1c2b36' } })
158
145
  window.renderFig = async (code, idx) => {
@@ -208,7 +195,6 @@ async function renderFigPage(data) {
208
195
  const browser = await chromium.launch()
209
196
  try {
210
197
  const page = await browser.newPage({ deviceScaleFactor: 2 })
211
- await routeLocalPkgs(page)
212
198
  await page.setContent(setup, { waitUntil: 'load' })
213
199
  await page.waitForFunction(() => window.__ready || window.__setupErr, { timeout: 90000 }).catch(() => {})
214
200
  const setupErr = await page.evaluate(() => window.__setupErr)
package/src/p3/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 技術核心
4
4
 
5
- - **繪圖庫**:nomnoml(`nomnoml/dist/nomnoml.js`),與其相依 `graphre`(`graphre/dist/graphre.js`)由本機 `node_modules` 讀出後內聯注入頁面 `<script>`(`common/pkg.mjs` `pkgScript()`),非透過 CDN,斷網環境亦可渲染;nomnoml UMD 模組依賴全域 `graphre`,注入順序須 graphre 先於 nomnoml。npm 版 nomnoml 之佈局由套件自宣告相依的 graphre 驅動,與舊版 CDN bundle(`nomnoml.web.js`,內部改捆 `@dagrejs/dagre`)佈局引擎不同,兩者排版結果可能略有差異。
5
+ - **繪圖庫**:nomnoml(`nomnoml/dist/nomnoml.js`),與其相依 `graphre`(`graphre/dist/graphre.js`)改由 jsDelivr CDN 載入(免安裝),於頁面內以外部 `<script src>` 標籤(`cdnScript()`,版本鎖定於 `src/common/cdn.mjs`)依序載入;nomnoml UMD 模組依賴全域 `graphre`,載入順序須 graphre 先於 nomnoml。npm 版 nomnoml 之佈局由套件自宣告相依的 graphre 驅動,與舊版 CDN bundle(`nomnoml.web.js`,內部改捆 `@dagrejs/dagre`)佈局引擎不同,兩者排版結果可能略有差異。
6
6
  - **渲染方式**:於頁面注入 `window.renderAndDetect(src)` 函式,呼叫 `window.nomnoml.renderSvg(src)` 將 DSL 字串渲染為 SVG,並將結果寫入 `#box` div 的 innerHTML。
7
7
  - **截圖目標**:以 Playwright locator `#box svg` 精確擷取 SVG 元素本身,排除頁面邊距干擾。
8
8
  - **解析度**:啟動 `browser.newPage({ deviceScaleFactor: 2 })`,輸出為 2× 實體像素的高解析度 PNG。
@@ -95,5 +95,5 @@ nomnoml 不為邊標籤畫底色(邊標籤背景透明),緊湊間距下邊
95
95
  - **無原生群組容器框**:改以「容器→成員」關聯邊表達歸屬(見上),視覺上無實體容器外框,僅靠共用色相辨識群組邊界,不若原生 compound graph 直觀。
96
96
  - **線性鏈被拉長**:對節點數多且為線性序列的圖(如長流程鏈),nomnoml 可能把畫布拉得很長,碰撞偵測的間距調整無法改善長寬比。
97
97
  - **碰撞偵測上限**:掃描序列最大值為 85px,若在此間距下仍有碰撞(如標籤極長或節點極密),會以殘留碰撞數最少者輸出。
98
- - **本機套件相依**:nomnoml/graphre 由本機 `node_modules` 讀出內聯注入,需先完成 `npm install`;執行環境本身不需連網。
98
+ - **CDN 相依**:nomnoml/graphre 改由 jsDelivr CDN 載入,免 `npm install`;執行環境須連網始能渲染。
99
99
  </content>
package/src/p3/gen.mjs CHANGED
@@ -15,12 +15,10 @@
15
15
  // 參照 → 單一連通圖、零重複節點。
16
16
  import { chromium } from 'playwright'
17
17
  import { PALETTE, EDGE } from '../common/palette.mjs'
18
- import { pkgScript } from '../common/pkg.mjs'
18
+ import { cdnScript } from '../common/cdn.mjs'
19
19
 
20
- // nomnoml(與其相依 graphre)由本機 node_modules 內聯注入(取代 CDN nomnoml.web.js, 斷網環境可用)
20
+ // nomnoml(與其相依 graphre) jsDelivr CDN 載入(免安裝; 版本鎖定見 common/cdn.mjs)
21
21
  // nomnoml UMD 依賴全域 graphre, 故 graphre 先載
22
- const GRAPHRE_JS = pkgScript('graphre/dist/graphre.js')
23
- const NOMNOML_JS = pkgScript('nomnoml/dist/nomnoml.js')
24
22
 
25
23
  // cls → nomnoml classifier 別名(純小寫字母 [a-z]+, 否則 nomnoml 視為字面文字而外洩)
26
24
  // 逐字映射保唯一: 大寫 G → 'g'、數字 2 → 'b'(其餘非 [a-z] 字元一律 → 'g'),
@@ -66,23 +64,29 @@ function translate(data) {
66
64
  const byId = {}
67
65
  for (const nd of data.nodes) byId[nd.id] = nd
68
66
 
69
- // 所有節點頂層平鋪宣告
70
- const nodeDsls = data.nodes.map(nd => `[${classifier(nd.cls)}${nd.label}]`).join('\n')
67
+ // label 內 \n 強制換行 → nomnoml 節點文字以 ; 斷行(節點宣告與邊參照須同轉換才對得上);
68
+ // 邊標籤不支援多行 降級為空格(避免字面換行使版面錯亂)
69
+ const nl = (s) => String(s).replace(/\n/g, ';')
70
+ const nlSp = (s) => String(s).replace(/\n/g, ' ')
71
+
72
+ // 所有節點頂層平鋪宣告; 菱形(choice)之多行內文會超出斜邊被裁 → \n 降級為空格(單行加寬菱形)
73
+ const nodeLabel = (nd) => nd.cls === 'diamond' ? nlSp(nd.label) : nl(nd.label)
74
+ const nodeDsls = data.nodes.map(nd => `[${classifier(nd.cls)}${nodeLabel(nd)}]`).join('\n')
71
75
 
72
76
  // 群組歸屬邊: 容器(group 所指節點) → 成員, 虛線無箭頭語意以外之標籤
73
77
  // nomnoml 無容器框, 比照 p10/vis 以關聯邊表達歸屬, 讓排版把成員擺在容器旁
74
78
  const assocDsls = data.nodes
75
79
  .filter(nd => nd.group && byId[nd.group])
76
- .map(nd => `[${byId[nd.group].label}] --> [${nd.label}]`)
80
+ .map(nd => `[${nodeLabel(byId[nd.group])}] --> [${nodeLabel(nd)}]`)
77
81
  .join('\n')
78
82
 
79
- // 數據明列之邊: 端點以既有節點之確切標籤參照(不新建節點)
83
+ // 數據明列之邊: 端點以既有節點之確切標籤參照(不新建節點; 轉換須與節點宣告一致才對得上)
80
84
  const edgeDsls = data.edges.map(ed => {
81
- const fromLabel = byId[ed.from]?.label ?? ed.from
82
- const toLabel = byId[ed.to]?.label ?? ed.to
85
+ const fromRef = byId[ed.from] ? nodeLabel(byId[ed.from]) : nl(ed.from)
86
+ const toRef = byId[ed.to] ? nodeLabel(byId[ed.to]) : nl(ed.to)
83
87
  const arrow = ed.kind === 'dashed' ? '-->' : '->'
84
- const edgeLabel = ed.label ? ` ${ed.label} ` : ' '
85
- return `[${fromLabel}]${edgeLabel}${arrow} [${toLabel}]`
88
+ const edgeLabel = ed.label ? ` ${nlSp(ed.label)} ` : ' '
89
+ return `[${fromRef}]${edgeLabel}${arrow} [${toRef}]`
86
90
  }).join('\n')
87
91
 
88
92
  return `#direction: ${dir}
@@ -102,8 +106,8 @@ ${edgeDsls}`
102
106
  // 對策不加大間距, 而以繪製順序解: 將每個邊標籤 <text> append 至 svg 尾端(最後繪製),
103
107
  // 使其永遠疊在容器填色/節點 rect 之上, 任何文字皆不被區塊遮住。
104
108
  const setup = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
105
- <script>${GRAPHRE_JS}</script>
106
- <script>${NOMNOML_JS}</script>
109
+ ${cdnScript('graphre')}
110
+ ${cdnScript('nomnoml')}
107
111
  </head><body style="margin:0;background:#fff;font-family:'Microsoft JhengHei',sans-serif">
108
112
  <div id="box" style="display:inline-block;padding:20px"></div>
109
113
  <script>
package/src/p4/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 技術核心
4
4
 
5
- - **繪圖庫**:Cytoscape.js(`cytoscape/dist/cytoscape.min.js`)搭配 dagre 排版引擎(`dagre/dist/dagre.min.js`)與橋接外掛(`cytoscape-dagre/dist/cytoscape-dagre.js`),三者由本機 `node_modules` 讀出後內聯注入頁面 `<script>`(`common/pkg.mjs` 的 `pkgScript()`),非透過 CDN,斷網環境亦可渲染。
5
+ - **繪圖庫**:Cytoscape.js(`cytoscape/dist/cytoscape.min.js`)搭配 dagre 排版引擎(`dagre/dist/dagre.min.js`)與橋接外掛(`cytoscape-dagre/dist/cytoscape-dagre.js`),三者改由 jsDelivr CDN 載入(免安裝),於頁面內以外部 `<script src>` 標籤(`cdnScript()`,版本鎖定於 `src/common/cdn.mjs`)依序載入;渲染時需連網存取 CDN
6
6
  - **渲染環境**:由 Playwright `chromium.launch()` 開啟無頭瀏覽器,`page.setContent()` 注入含三段內聯 script 的靜態 HTML,頁面掛載一個 `1400×1400px` 的 `<div id="cy">` 作為 Cytoscape 容器。
7
7
  - **截圖與匯出**:排版完成後呼叫 `cy.png({ full:true, scale:2, bg:'#ffffff', output:'base64' })` 取得 base64 字串,存入全域變數 `window.__png`,再由 `page.evaluate()` 讀回 Node 端,以 `Buffer.from(b64, 'base64')` 轉為 PNG Buffer 回傳。`full:true` 使輸出範圍自動裁切至實際元素範圍,`scale:2` 使最終 PNG 解析度為實際佈局尺寸的 2 倍;`deviceScaleFactor` 設為 `1`(由 `browser.newPage({ deviceScaleFactor:1 })` 指定),縮放完全由 `cy.png` 的 `scale:2` 控制。
8
8
  - **呼叫方式**:`genPng(data, opt)` 為單張渲染函式,接受正規化繪圖數據 `{ dir, nodes, edges }`,內部自建 browser/page 並於結束時關閉,回傳 PNG 的 Node Buffer(不落地檔案),由 `src/WFlowchart.mjs` 統一調用(`mode: 'p4'`);同結構之 `genSvg(data, opt)` 經 `cytoscape-svg` 外掛將同一份佈局/樣式匯出為 SVG 字串(canvas 引擎重繪為 SVG,經像素級忠實度驗證)。
@@ -80,5 +80,5 @@ dagre layout 使用三個通用常數,**不因圖而異**:
80
80
  - **diamond 節點在群組容器內偏大**:`diamond` cls 固定使用 `padding:34px` 以撐開菱形可視範圍,在小型群組容器內可能撐爆容器邊界。
81
81
  - **跨容器邊路由**:Cytoscape compound node 下,跨群組的邊由 dagre 自動路由,部分情況下路由線會穿越容器框線,視覺上不夠乾淨,但功能正確。
82
82
  - **無碰撞偵測迴圈**:版面為單次 dagre 計算,若節點數量極多、標籤極長,可能出現節點標籤截斷(超出 `text-max-width:360px`)或節點互疊,無自動調整機制。
83
- - **本機套件相依**:cytoscape/dagre/cytoscape-dagre 由本機 `node_modules` 讀出內聯注入,需先完成 `npm install`;執行環境本身不需連網。
83
+ - **CDN 相依**:cytoscape/dagre/cytoscape-dagre 改由 jsDelivr CDN 載入,免 `npm install`;執行環境須連網始能渲染。
84
84
  </content>
package/src/p4/gen.mjs CHANGED
@@ -2,13 +2,9 @@
2
2
  // genPng(data): 正規化繪圖數據 → 轉 cytoscape els → dagre 自動排版渲染 → 回傳 PNG Buffer
3
3
  // 版面通用化: rankDir 取自數據 dir; nodeSep/rankSep 為通用常數; full:true + 自動尺寸, 無逐圖魔術數字。
4
4
  import { chromium } from 'playwright'
5
- import { pkgScript } from '../common/pkg.mjs'
5
+ import { cdnScript } from '../common/cdn.mjs'
6
6
 
7
- // cytoscape + dagre + cytoscape-dagre + cytoscape-svg 由本機 node_modules 內聯注入(取代 CDN, 斷網環境可用)
8
- const CYTOSCAPE_JS = pkgScript('cytoscape/dist/cytoscape.min.js')
9
- const DAGRE_JS = pkgScript('dagre/dist/dagre.min.js')
10
- const CYTOSCAPE_DAGRE_JS = pkgScript('cytoscape-dagre/dist/cytoscape-dagre.js')
11
- const CYTOSCAPE_SVG_JS = pkgScript('cytoscape-svg/cytoscape-svg.js')
7
+ // cytoscape + dagre + cytoscape-dagre + cytoscape-svg jsDelivr CDN 載入(免安裝; 版本鎖定見 common/cdn.mjs)
12
8
 
13
9
  // 正規化數據 → cytoscape elements(節點帶 parent=group; 邊 dashed→classes)
14
10
  function toEls(data) {
@@ -34,15 +30,15 @@ const style = `[
34
30
  { "selector":"node.greenG2", "style":{ "background-color":"#eaf4ef","border-color":"#348a5c","color":"#348a5c" } },
35
31
  { "selector":"node.orangeG", "style":{ "background-color":"#fdf6ec","border-color":"#c46e1a","color":"#c46e1a" } },
36
32
  { "selector":"node.purpleG", "style":{ "background-color":"#faf7fd","border-color":"#8163ad","color":"#8163ad" } },
37
- { "selector":"edge", "style":{ "width":2.2,"line-color":"#44505a","target-arrow-color":"#44505a","target-arrow-shape":"triangle","curve-style":"bezier","label":"data(label)","font-family":"Microsoft JhengHei, sans-serif","font-size":12.5,"color":"#4a5560","text-background-opacity":0,"text-outline-color":"#ffffff","text-outline-width":3,"text-margin-y":-2 } },
33
+ { "selector":"edge", "style":{ "width":2.2,"line-color":"#44505a","target-arrow-color":"#44505a","target-arrow-shape":"triangle","curve-style":"bezier","label":"data(label)","font-family":"Microsoft JhengHei, sans-serif","font-size":12.5,"color":"#4a5560","text-background-opacity":0,"text-outline-color":"#ffffff","text-outline-width":3,"text-margin-y":-2,"text-wrap":"wrap" } },
38
34
  { "selector":"edge.dashed", "style":{ "line-style":"dashed" } }
39
35
  ]`
40
36
 
41
37
  const html = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
42
- <script>${CYTOSCAPE_JS}</script>
43
- <script>${DAGRE_JS}</script>
44
- <script>${CYTOSCAPE_DAGRE_JS}</script>
45
- <script>${CYTOSCAPE_SVG_JS}</script>
38
+ ${cdnScript('cytoscape')}
39
+ ${cdnScript('dagre')}
40
+ ${cdnScript('cytoscape-dagre')}
41
+ ${cdnScript('cytoscape-svg')}
46
42
  </head><body style="margin:0;background:#fff">
47
43
  <div id="cy" style="width:1400px;height:1400px;background:#fff"></div>
48
44
  <script>
package/src/p5/gen.mjs CHANGED
@@ -48,8 +48,8 @@ function injectEdgeLabelHalo(svg) {
48
48
  return svg.slice(0, p + 1) + EDGE_HALO_STYLE + svg.slice(p + 1)
49
49
  }
50
50
 
51
- // ── D2 escape:$ 需轉義以避免 D2 變數展開 ───────────────────────────────────
52
- const esc = (s) => String(s).replace(/\$/g, '\\$')
51
+ // ── D2 escape:$ 需轉義以避免 D2 變數展開; 字面換行轉 \n 逸出序列(D2 雙引號字串不允許字面換行) ──
52
+ const esc = (s) => String(s).replace(/\$/g, '\\$').replace(/\n/g, '\\n')
53
53
 
54
54
  // ── D2 方向映射:標準數據 dir → D2 direction 關鍵字 ─────────────────────────
55
55
  const DIR_MAP = { TB: 'down', LR: 'right', BT: 'up', RL: 'left' }
package/src/p6/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 技術核心
4
4
 
5
- - **繪圖庫**:`@antv/g6@5`,由本機 `node_modules/@antv/g6/dist/g6.min.js`(`common/pkg.mjs` `pkgScript()` 讀取檔案內容)內聯注入 Playwright 頁面的 `<script>` 執行,取代舊版 CDN 載入,斷網環境亦可執行。`@antv/g6` 仍是套件 `dependencies` 之一(供 `node_modules` 內取得 dist 檔),但 Node 執行期不以 `import` 方式使用它,僅頁面內 `<script>` 使用其全域 `G6`。
5
+ - **繪圖庫**:`@antv/g6@5`,改由 jsDelivr CDN 載入(免安裝),於 Playwright 頁面內以外部 `<script src>` 標籤(`cdnScript('g6')`,版本鎖定於 `src/common/cdn.mjs`)載入執行,渲染時需連網存取 CDN。`@antv/g6` 已從套件 `dependencies` 移除,Node 執行期亦不以 `import` 方式使用它,僅頁面內 `<script>` 取用其全域 `G6`。
6
6
  - **渲染方式**:以 `chromium.launch()` 開啟無頭瀏覽器,`page.setContent(html)` 載入內嵌 G6 頁面,於頁面內呼叫 `window.renderFig()`,以 `G6.Graph` + `layout: { type:'antv-dagre' }` 完成排版與渲染。
7
7
  - **截圖/輸出**:呼叫 `graph.toDataURL({ mode:'overall' })` 取得整圖 DataURL(非 viewport 截圖),透過 `window.snap()` 取回後在 Node 端解 base64,組成 `Buffer` 回傳(不寫檔)。
8
8
  - **解析度**:`browser.newPage({ deviceScaleFactor: 2 })`,輸出為 2× 實體像素解析度的 PNG。
package/src/p6/gen.mjs CHANGED
@@ -12,14 +12,11 @@
12
12
  // 若緊湊間距下仍有節點互疊且非 z-order 可解, 則保持緊湊留下重疊(代表此套不適合此圖, 由挑選階段換別套)。
13
13
  import { chromium } from 'playwright'
14
14
  import { colorOf, isGroupCls, EDGE, FONT } from '../common/palette.mjs'
15
- import { pkgScript } from '../common/pkg.mjs'
15
+ import { cdnScript } from '../common/cdn.mjs'
16
16
 
17
- // G6 v5 由本機 node_modules 內聯注入(取代 CDN, 斷網環境可用)
18
- const G6_JS = pkgScript('@antv/g6/dist/g6.min.js')
17
+ // G6 v5 jsDelivr CDN 載入(免安裝; 版本鎖定見 common/cdn.mjs)
19
18
  // genSvg 專用:@antv/g-svg renderer(取代預設 canvas renderer, 產出 SVG DOM)。
20
19
  // 依賴 @antv/g-lite UMD(暴露全域 window.G)須先於 g-svg UMD 載入(g-svg UMD 讀取既有 window.G 掛載 window.G.SVG)。
21
- const G_LITE_JS = pkgScript('@antv/g-lite/dist/index.umd.min.js')
22
- const G_SVG_JS = pkgScript('@antv/g-svg/dist/index.umd.min.js')
23
20
 
24
21
  // ===== 通用排版常數(全圖共用, 無逐圖客製) =====
25
22
  const CANVAS = 2200 // 大畫布:給足夠空間, 配合 autoFit:'view' 自動縮放, 不逐圖設 w/h
@@ -134,7 +131,7 @@ function translate(data) {
134
131
  // 原則2:combo.style.zIndex 墊底、edge.style.zIndex 最上層(於 translate 各元素已設, graph 預設再兜底)。
135
132
  // 原則3:固定緊湊 nodesep/ranksep, 無碰撞偵測掃描放大。
136
133
  const html = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
137
- <script>${G6_JS}</script>
134
+ ${cdnScript('g6')}
138
135
  </head><body style="margin:0;background:#fff">
139
136
  <div id="g6" style="background:#fff"></div>
140
137
  <script>
@@ -201,9 +198,9 @@ export async function genPng(data, opt = {}) {
201
198
  // 換用 @antv/g-svg renderer(取代預設 canvas renderer)產出 SVG DOM, 其餘 translate/layout/樣式邏輯
202
199
  // (dagre 排版、緊湊間距、z-order、字型)與 genPng 完全共用, 故版面結構/顏色/文字與 PNG 版一致。
203
200
  const htmlSvg = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
204
- <script>${G_LITE_JS}</script>
205
- <script>${G_SVG_JS}</script>
206
- <script>${G6_JS}</script>
201
+ ${cdnScript('g-lite')}
202
+ ${cdnScript('g-svg')}
203
+ ${cdnScript('g6')}
207
204
  </head><body style="margin:0;background:#fff">
208
205
  <div id="g6" style="background:#fff"></div>
209
206
  <script>
package/src/p7/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## 技術核心
4
4
 
5
- - **繪圖庫**:`@joint/core`(JointJS)搭配 `dagre`,由 `common/pkg.mjs` `pkgScript()` 讀取本機 `node_modules/@joint/core/dist/joint.min.js` `node_modules/dagre/dist/dagre.min.js` 內容,內聯注入 Playwright 頁內 `<script>` 標籤(取代 CDN,斷網環境可用)。
6
- - **載入方式**:Node 端以 Playwright 的 `page.setContent(html)` 建立含兩段內聯 `<script>` 的完整 HTML 頁,等候 `window.__ready` 旗標確認兩套庫皆就緒後,再透過 `page.evaluate()` 驅動頁內 JavaScript 函式進行排版與渲染。
5
+ - **繪圖庫**:`@joint/core`(JointJS)搭配 `dagre`,改由 jsDelivr CDN 載入(免安裝),於 Playwright 頁內以外部 `<script src>` 標籤(`cdnScript('joint')`、`cdnScript('dagre')`,版本鎖定於 `src/common/cdn.mjs`)載入;渲染時需連網存取 CDN
6
+ - **載入方式**:Node 端以 Playwright 的 `page.setContent(html)` 建立含兩段外部 `<script src>` 的完整 HTML 頁,等候 `window.__ready` 旗標確認兩套庫皆就緒後,再透過 `page.evaluate()` 驅動頁內 JavaScript 函式進行排版與渲染。
7
7
  - **渲染方式**:排版完成後以 `P.fitToContent({ padding: 28, allowNewOrigin: 'any', useModelGeometry: true })` 讓 JointJS Paper 自適應內容邊界,截圖目標為 `#paper svg`(`page.$('#paper svg').screenshot()`),直接輸出 PNG。
8
8
  - **解析度**:`browser.newPage({ deviceScaleFactor: 2 })` 以 2 倍像素密度開頁,截出的 PNG 實際像素為邏輯尺寸的兩倍,保有高清品質。
9
9
  - **字型**:頁內使用 `Microsoft JhengHei`(微軟正黑體)作為主字型,canvas 量測與 JointJS 渲染均使用同一 `FONT` 字串,確保量測結果與渲染結果一致。
package/src/p7/gen.mjs CHANGED
@@ -3,11 +3,9 @@
3
3
  // 版面通用化: rankDir 取自數據 dir; nodesep/ranksep/GPAD/換行寬度 皆為通用常數或依標籤字寬/行數自動推算, 無逐圖魔術數字。
4
4
  import { chromium } from 'playwright'
5
5
  import { PALETTE } from '../common/palette.mjs'
6
- import { pkgScript } from '../common/pkg.mjs'
6
+ import { cdnScript } from '../common/cdn.mjs'
7
7
 
8
- // @joint/core + dagre 由本機 node_modules 內聯注入(取代 CDN, 斷網環境可用)
9
- const JOINT_JS = pkgScript('@joint/core/dist/joint.min.js')
10
- const DAGRE_JS = pkgScript('dagre/dist/dagre.min.js')
8
+ // @joint/core + dagre jsDelivr CDN 載入(免安裝; 版本鎖定見 common/cdn.mjs)
11
9
 
12
10
  // CLS(JointJS 內部樣式表)由共用色票 PALETTE 推導: fill/stroke/text→color; diamond 帶 shape; *G/*G2 帶 group:true
13
11
  const CLS = Object.fromEntries(Object.entries(PALETTE).map(([cls, p]) => {
@@ -28,8 +26,8 @@ function translate(data) {
28
26
  }
29
27
 
30
28
  const html = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
31
- <script>${JOINT_JS}</script>
32
- <script>${DAGRE_JS}</script>
29
+ ${cdnScript('joint')}
30
+ ${cdnScript('dagre')}
33
31
  </head><body style="margin:0;background:#fff">
34
32
  <div id="paper" style="background:#fff"></div>
35
33
  <script>
@@ -42,16 +40,21 @@ var __cv = document.createElement('canvas'); var __cx = __cv.getContext('2d')
42
40
  function measure(text, fs){ __cx.font = fs + 'px ' + FONT; return __cx.measureText(text).width }
43
41
 
44
42
  // 依字寬自動換行(回傳 lines 陣列 + 最大寬)
43
+ // label 內 \\n 為強制換行: 先依 \\n 切段、各段再逐字寬斷行——渲染端以 join('\\n') 交給 JointJS,
44
+ // 若量測不切 \\n 會令實際行數多於估算行數, 框高低估、文字上下溢出框
45
45
  function wrapText(text, fs, maxW){
46
- // 先嘗試以全形分隔符/括號斷點,否則逐字
47
- var lines = [], cur = ''
48
- for (var i = 0; i < text.length; i++){
49
- var ch = text[i]
50
- var test = cur + ch
51
- if (measure(test, fs) > maxW && cur !== ''){ lines.push(cur); cur = ch }
52
- else cur = test
53
- }
54
- if (cur) lines.push(cur)
46
+ var lines = []
47
+ String(text).split('\\n').forEach(function(seg){
48
+ var cur = ''
49
+ for (var i = 0; i < seg.length; i++){
50
+ var ch = seg[i]
51
+ var test = cur + ch
52
+ if (measure(test, fs) > maxW && cur !== ''){ lines.push(cur); cur = ch }
53
+ else cur = test
54
+ }
55
+ lines.push(cur) // 空段亦保留(連續 \\n 表空行)
56
+ })
57
+ if (lines.length === 1 && lines[0] === '') lines = [] // 空字串輸入維持原行為(零行)
55
58
  var w = 0; lines.forEach(function(l){ w = Math.max(w, measure(l, fs)) })
56
59
  return { lines: lines, w: w }
57
60
  }
package/src/p8/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## 技術核心
4
4
 
5
- - **繪圖庫**:`@logicflow/core`(含 CSS)搭配 `dagre`,由 `common/pkg.mjs` 讀取本機檔案內聯注入 Playwright 頁內(取代 CDN,斷網環境可用):`pkgText('@logicflow/core/dist/index.css')` 內聯進 `<style>`,`pkgScript('@logicflow/core/dist/index.min.js')` 與 `pkgScript('dagre/dist/dagre.min.js')` 各自內聯進 `<script>`。
6
- - **載入方式**:Node 端以 Playwright 建立 Chromium 頁面,將完整 HTML 字串(含內聯 CSS 與兩段內聯 `<script>`)透過 `page.setContent()` 注入;繪圖邏輯全部在頁內 `<script>` 執行,Node 端僅負責驅動與截圖。
5
+ - **繪圖庫**:`@logicflow/core`(含 CSS)搭配 `dagre`,改由 jsDelivr CDN 載入(免安裝):`cdnStyle('logicflow-css')` 以外部 `<link>` 載入 CSS,`cdnScript('logicflow')` 與 `cdnScript('dagre')` 各自以外部 `<script src>` 載入(版本鎖定於 `src/common/cdn.mjs`);渲染時需連網存取 CDN。
6
+ - **載入方式**:Node 端以 Playwright 建立 Chromium 頁面,將完整 HTML 字串(含外部 CSS `<link>` 與兩段外部 `<script src>`)透過 `page.setContent()` 注入;繪圖邏輯全部在頁內 `<script>` 執行,Node 端僅負責驅動與截圖。
7
7
  - **渲染與截圖**:頁內 `window.renderFig(fig)` 依序計算尺寸 → dagre 排版 → `lf.render()` 渲染完後以 `setTimeout(..., 200)` 等待 DOM 穩定,回傳 Promise;Node 端再呼叫 `page.evaluate(() => document.fonts.ready)` 確保字型載入完畢,最後以 `page.locator('#app').screenshot()` 截取 `#app` 元素為 PNG Buffer 回傳(不寫檔)。
8
8
  - **解析度**:`browser.newPage({ deviceScaleFactor: 2 })`,輸出為 2× 實體像素密度(HiDPI),寬高由計算所得邏輯像素乘以 2 輸出。
9
9
  - **調用方式**:本模組匯出 `genPng(data, opt) → Promise<Buffer>` 與 `genSvg(data, opt) → Promise<string>`,輸入皆為正規化繪圖數據 `{ dir, nodes, edges }`;`genSvg` 序列化圖面 svg(`lf-canvas-overlay`),並將 LogicFlow CSS 內嵌進 SVG(文字自動換行之 `foreignObject` 標籤樣式依賴此 CSS);由 `src/WFlowchart.mjs` 統一匯入並依 `p8` 鍵值調用,本身不寫檔、不涉及批次流程。
package/src/p8/gen.mjs CHANGED
@@ -5,12 +5,9 @@
5
5
  // 邊標籤(全域修正 A): 背景透明(不遮轉折線) + 文字加白色光暈(SVG paint-order:stroke + stroke=白 stroke-width≈3.5)。
6
6
  import { chromium } from 'playwright'
7
7
  import { PALETTE, EDGE, FONT, colorOf } from '../common/palette.mjs'
8
- import { pkgScript, pkgText } from '../common/pkg.mjs'
8
+ import { cdnScript, cdnStyle, cdnFetchText } from '../common/cdn.mjs'
9
9
 
10
- // @logicflow/core(js+css)+ dagre 由本機 node_modules 內聯注入(取代 CDN, 斷網環境可用)
11
- const LOGICFLOW_CSS = pkgText('@logicflow/core/dist/index.css')
12
- const LOGICFLOW_JS = pkgScript('@logicflow/core/dist/index.min.js')
13
- const DAGRE_JS = pkgScript('dagre/dist/dagre.min.js')
10
+ // @logicflow/core(js+css)+ dagre jsDelivr CDN 載入(免安裝; 版本鎖定見 common/cdn.mjs)
14
11
 
15
12
  // 正規化數據(dir/nodes/edges) → LF 內部結構: nodes 用 parent 表群組歸屬; edges 用 source/target/dashed。
16
13
  // 套色: 由 cls 查 PALETTE(group 類別自動為淺底容器); 形狀: diamond→決策菱形, 其餘→矩形; dir→dagre rankDir; kind:dashed→虛線。
@@ -27,9 +24,9 @@ const PAL_JSON = JSON.stringify(PAL)
27
24
  const EDGE_JSON = JSON.stringify(EDGE)
28
25
 
29
26
  const html = `<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
30
- <style>${LOGICFLOW_CSS}</style>
31
- <script>${LOGICFLOW_JS}</script>
32
- <script>${DAGRE_JS}</script>
27
+ ${cdnStyle('logicflow-css')}
28
+ ${cdnScript('logicflow')}
29
+ ${cdnScript('dagre')}
33
30
  <style>
34
31
  * { font-family: ${FONT}; }
35
32
  html,body{ margin:0; background:#fff; }
@@ -173,7 +170,8 @@ function layout(P, nodeSep, rankSep){
173
170
  Object.keys(groupIds).sort(function(a,b){ return depth(a)-depth(b) }).forEach(function(gid){
174
171
  var gn = g.node(gid)
175
172
  var pal = PAL[byId[gid].cls] || PAL.blue
176
- lfGroupNodes.push({ id: gid, label: byId[gid].label, _pal: pal,
173
+ // 群組標題帶為固定高(GROUP_TITLE), label \\n 降級為空格(兩行標題會壓到成員節點)
174
+ lfGroupNodes.push({ id: gid, label: String(byId[gid].label).replace(/\\n/g, ' '), _pal: pal,
177
175
  x: Math.round(gn.x), y: Math.round(gn.y), _w: Math.round(gn.width), _h: Math.round(gn.height) })
178
176
  })
179
177
 
@@ -443,8 +441,10 @@ export async function genSvg(data, opt = {}) {
443
441
  .replace('height="100%"', `height="${res.h}"`)
444
442
  openTag = openTag.replace(/>$/, ` viewBox="0 0 ${res.w} ${res.h}">`)
445
443
  svg = openTag + svg.slice(openTagMatch[0].length)
446
- // 嵌入 CSS(LOGICFLOW_CSS + 頁內字型規則) + 白底(比照 #app 背景), 使脫離頁面仍忠實呈現
447
- const inject = `<style>${LOGICFLOW_CSS}\n* { font-family: ${FONT}; }</style>`
444
+ // 嵌入 CSS(LogicFlow CSS + 頁內字型規則) + 白底(比照 #app 背景), 使脫離頁面仍忠實呈現
445
+ // LogicFlow CSS CDN 取回文字內容內嵌(免安裝; 與頁面 <link> 同一版本, 內容一致)
446
+ const logicflowCss = await cdnFetchText('logicflow-css')
447
+ const inject = `<style>${logicflowCss}\n* { font-family: ${FONT}; }</style>`
448
448
  + `<rect x="0" y="0" width="${res.w}" height="${res.h}" fill="#fff"></rect>`
449
449
  svg = svg.replace('<g transform', inject + '<g transform')
450
450
  svg = svg.replace(/&nbsp;/g, '&#160;')
package/src/p9/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## 技術核心
4
4
 
5
5
  - **繪圖庫**:`@xyflow/react@12`(React Flow)搭配 `@dagrejs/dagre@1` 自動排版引擎。
6
- - **載入方式**:`react`、`react-dom`、`@xyflow/react`、`@dagrejs/dagre` 之 UMD 版本與 `@xyflow/react` 樣式表,皆由本機 `node_modules` 讀出後於 `gen.mjs` 模組載入時內聯注入 `page9.html`(取代 CDN `esm.sh`,斷網環境可用):`page9.html` 內以佔位符 `/*__XYFLOW_CSS__*/`、`/*__REACT_JS__*/`、`/*__REACT_DOM_JS__*/`、`/*__XYFLOW_JS__*/`、`/*__DAGRE_JS__*/` 預留插入點,`gen.mjs` 讀入該檔全文後以 `common/pkg.mjs` 的 `pkgText`/`pkgScript`(向上逐層尋找 `node_modules/<rel>`)取代之,`replace` 用函式形式回傳以避免庫碼內 `$` 序列被誤當替換樣板。`@xyflow/react` UMD 之外部依賴含全域 `jsxRuntime`(React 18 UMD 未提供),`page9.html` 內建等價 shim(以 `React.createElement` 實作 `jsx`/`jsxs`/`Fragment`)補上。
6
+ - **載入方式**:`react`、`react-dom`、`@xyflow/react`、`@dagrejs/dagre` 之 UMD 版本與 `@xyflow/react` 樣式表,改由 jsDelivr CDN 載入(免安裝,版本鎖定於 `src/common/cdn.mjs`):`page9.html` 內以佔位符 `/*__XYFLOW_CSS__*/`、`/*__REACT_JS__*/`、`/*__REACT_DOM_JS__*/`、`/*__XYFLOW_JS__*/`、`/*__DAGRE_JS__*/` 預留插入點,`gen.mjs` 讀入該檔全文後以 `common/cdn.mjs` 的 `cdnScript`/`cdnStyle` 將各佔位標籤整段換成對應之外部 `<script src>`/`<link>` 標籤,`replace` 用函式形式回傳以避免庫碼內 `$` 序列被誤當替換樣板;渲染時需連網存取 CDN。`@xyflow/react` UMD 之外部依賴含全域 `jsxRuntime`(React 18 UMD 未提供),`page9.html` 內建等價 shim(以 `React.createElement` 實作 `jsx`/`jsxs`/`Fragment`)補上。
7
7
  - **渲染流程**:`genPng(data, opt)` 每次呼叫皆啟動一個新的 `chromium` browser 與 page(`finally` 內關閉),以 `page.setContent(html)` 注入內聯好各繪圖庫的 `page9.html` 全文(`html` 於模組載入時讀取並完成佔位符替換一次,跨呼叫重用字串),等待 `document.title === 'READY'` 確認 React 與 dagre 啟動完成後,呼叫 `page.evaluate(s => window.renderFig(s), spec)`(`spec` 為 `translate(data)` 之輸出)觸發單張渲染,渲染函式內 `setTimeout 900ms` 後 resolve,確保 React commit 完成。
8
8
  - **截圖目標**:對 `#shot` 元素截圖(`page.locator('#shot').screenshot()`);`#shot` 為 `display:inline-block` 的 div,內含 `#stage`(React Flow 掛載點),外加 `padding:24px` 白邊,截出的 PNG 即為完整含留白之圖片。
9
9
  - **解析度**:`browser.newPage({ deviceScaleFactor: 2 })` 開啟 2x 縮放,所有量測與佈局座標仍以 CSS px 計算,截出的 PNG 實際像素為畫布尺寸之 2 倍,確保中文字型清晰。
package/src/p9/gen.mjs CHANGED
@@ -8,21 +8,21 @@ import { fileURLToPath } from 'url'
8
8
  import { dirname } from 'path'
9
9
  import fs from 'fs'
10
10
 
11
- import { pkgScript, pkgText } from '../common/pkg.mjs'
11
+ import { cdnScript, cdnStyle } from '../common/cdn.mjs'
12
12
 
13
13
  const __dir = dirname(fileURLToPath(import.meta.url))
14
- // React / ReactDOM / @xyflow/react(UMD)/ @dagrejs/dagre 由本機 node_modules 內聯注入(取代 esm.sh, 斷網環境可用)
15
- // replace 用函式形式回傳, 避免庫碼內 $ 序列被當作替換樣板
14
+ // React / ReactDOM / @xyflow/react(UMD, 含 css)/ @dagrejs/dagre jsDelivr CDN 載入(免安裝; 版本鎖定見 common/cdn.mjs)
15
+ // page9.html 內各佔位標籤整段換成外部 <script src>/<link>(非 async, 保留文件順序執行)
16
16
  // 首次呼叫才讀檔組頁(延遲載入): 模組層即讀會令「僅用其他產線」的載入方也付出讀檔成本
17
17
  let _html = null
18
18
  function getHtml() {
19
19
  if (_html === null) {
20
20
  _html = fs.readFileSync(`${__dir}/page9.html`, 'utf8')
21
- .replace('/*__XYFLOW_CSS__*/', () => pkgText('@xyflow/react/dist/style.css'))
22
- .replace('/*__REACT_JS__*/', () => pkgScript('react/umd/react.production.min.js'))
23
- .replace('/*__REACT_DOM_JS__*/', () => pkgScript('react-dom/umd/react-dom.production.min.js'))
24
- .replace('/*__XYFLOW_JS__*/', () => pkgScript('@xyflow/react/dist/umd/index.js'))
25
- .replace('/*__DAGRE_JS__*/', () => pkgScript('@dagrejs/dagre/dist/dagre.min.js'))
21
+ .replace('<style>/*__XYFLOW_CSS__*/</style>', () => cdnStyle('xyflow-css'))
22
+ .replace('<script>/*__REACT_JS__*/</script>', () => cdnScript('react'))
23
+ .replace('<script>/*__REACT_DOM_JS__*/</script>', () => cdnScript('react-dom'))
24
+ .replace('<script>/*__XYFLOW_JS__*/</script>', () => cdnScript('xyflow'))
25
+ .replace('<script>/*__DAGRE_JS__*/</script>', () => cdnScript('dagrejs-dagre'))
26
26
  }
27
27
  return _html
28
28
  }
package/src/p9/page9.html CHANGED
@@ -1,5 +1,5 @@
1
1
  <!doctype html><html lang="zh-Hant"><head><meta charset="utf-8">
2
- <!-- 繪圖庫由本機 node_modules 內聯注入(gen.mjs 讀本檔後替換各佔位符; 取代 esm.sh/CDN, 斷網環境可用) -->
2
+ <!-- 繪圖庫由 jsDelivr CDN 載入(gen.mjs 讀本檔後將各佔位標籤換成外部 <script src>/<link>; 免安裝) -->
3
3
  <style>/*__XYFLOW_CSS__*/</style>
4
4
  <style>
5
5
  html,body{margin:0;background:#fff;font-family:'Microsoft JhengHei',sans-serif}