@diagrammo/dgmo 0.45.0 → 0.47.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 (250) hide show
  1. package/dist/advanced.cjs +31 -25
  2. package/dist/advanced.d.cts +3 -855
  3. package/dist/advanced.d.ts +3 -855
  4. package/dist/advanced.js +541 -70101
  5. package/dist/analyzer-UYKBXAKR.js +15 -0
  6. package/dist/auto.cjs +500 -341
  7. package/dist/auto.css +342 -159
  8. package/dist/auto.d.cts +10 -8
  9. package/dist/auto.d.ts +10 -8
  10. package/dist/auto.js +373 -265
  11. package/dist/auto.mjs +500 -341
  12. package/dist/block.cjs +31 -25
  13. package/dist/block.js +83 -69411
  14. package/dist/calculator-LDYQCDDY.js +11 -0
  15. package/dist/charts-d3-QOZHKTCT.js +25 -0
  16. package/dist/chunk-2DXMZRWJ.js +800 -0
  17. package/dist/chunk-2LACINWN.js +786 -0
  18. package/dist/chunk-33LCUH4H.js +893 -0
  19. package/dist/chunk-3CIOJ5YJ.js +35 -0
  20. package/dist/chunk-3HDBAXF5.js +86 -0
  21. package/dist/chunk-3JLKYGBQ.js +290 -0
  22. package/dist/chunk-3MQQNXPD.js +415 -0
  23. package/dist/chunk-3R5POCTZ.js +967 -0
  24. package/dist/chunk-3SMGIJDD.js +112 -0
  25. package/dist/chunk-44CM72SQ.js +409 -0
  26. package/dist/chunk-4GRI7MEB.js +1604 -0
  27. package/dist/chunk-4OHACBGX.js +606 -0
  28. package/dist/chunk-5DH3KYFH.js +21 -0
  29. package/dist/chunk-5MLYVTYM.js +76 -0
  30. package/dist/chunk-5QXL4NYS.js +553 -0
  31. package/dist/chunk-5WRI5ZAA.js +31 -0
  32. package/dist/chunk-6EK55ART.js +117 -0
  33. package/dist/chunk-6FZYCKXB.js +2065 -0
  34. package/dist/chunk-6HO5ZTXK.js +277 -0
  35. package/dist/chunk-6KFWC4CO.js +7 -0
  36. package/dist/chunk-6MTWIQVO.js +121 -0
  37. package/dist/chunk-6NYWYNBI.js +329 -0
  38. package/dist/chunk-6SM5NJQH.js +620 -0
  39. package/dist/chunk-725LDASD.js +422 -0
  40. package/dist/chunk-76OWZ3BA.js +130 -0
  41. package/dist/chunk-7JEGTRLD.js +338 -0
  42. package/dist/chunk-A44USUV7.js +923 -0
  43. package/dist/chunk-A4GOY3PE.js +25 -0
  44. package/dist/chunk-A4VFQH5G.js +251 -0
  45. package/dist/chunk-AELOBFW5.js +375 -0
  46. package/dist/chunk-AN2MOO5T.js +522 -0
  47. package/dist/chunk-BMR4CIEO.js +41 -0
  48. package/dist/chunk-BS7SV7C3.js +51 -0
  49. package/dist/chunk-BUPDIHML.js +613 -0
  50. package/dist/chunk-BUVJFRPY.js +887 -0
  51. package/dist/chunk-C3Z4DX3V.js +112 -0
  52. package/dist/chunk-CI2G3OEB.js +1818 -0
  53. package/dist/chunk-CKNVO24D.js +54 -0
  54. package/dist/chunk-CUVHQHZ7.js +391 -0
  55. package/dist/chunk-CXK4DZH3.js +114 -0
  56. package/dist/chunk-DKEPS5LH.js +315 -0
  57. package/dist/chunk-DU7W62N5.js +487 -0
  58. package/dist/chunk-DZAQBW35.js +751 -0
  59. package/dist/chunk-EB6FPQUP.js +316 -0
  60. package/dist/chunk-EMS2SK26.js +376 -0
  61. package/dist/chunk-ERLBPDGU.js +510 -0
  62. package/dist/chunk-EVCVDTXE.js +732 -0
  63. package/dist/chunk-FCIIHT24.js +384 -0
  64. package/dist/chunk-FFUN5D66.js +97 -0
  65. package/dist/chunk-FITOLXSX.js +714 -0
  66. package/dist/chunk-FMDSF3OV.js +68 -0
  67. package/dist/chunk-FNB3PNGF.js +410 -0
  68. package/dist/chunk-G2H74XJZ.js +2104 -0
  69. package/dist/chunk-GMR5MFHJ.js +339 -0
  70. package/dist/chunk-H6PI3KJT.js +305 -0
  71. package/dist/chunk-I46X5VUA.js +59 -0
  72. package/dist/chunk-I5IGB56C.js +255 -0
  73. package/dist/chunk-IH6ZMVQ5.js +3950 -0
  74. package/dist/chunk-IJZFN3HM.js +463 -0
  75. package/dist/chunk-IPWPWM6R.js +3079 -0
  76. package/dist/chunk-IYULMRBE.js +23 -0
  77. package/dist/chunk-IZMRON6C.js +167 -0
  78. package/dist/chunk-J5FIXQRT.js +813 -0
  79. package/dist/chunk-JED6ZDEE.js +73 -0
  80. package/dist/chunk-JEUJOX5X.js +136 -0
  81. package/dist/chunk-JM4LEPY3.js +436 -0
  82. package/dist/chunk-L3UHP6TD.js +101 -0
  83. package/dist/chunk-L66GEFB6.js +524 -0
  84. package/dist/chunk-LIOHW36Z.js +469 -0
  85. package/dist/chunk-LP7EC4MI.js +16 -0
  86. package/dist/chunk-MITI6NOL.js +548 -0
  87. package/dist/chunk-MO5YHHMQ.js +168 -0
  88. package/dist/chunk-MZSBFAPJ.js +885 -0
  89. package/dist/chunk-NNAAJVBU.js +9 -0
  90. package/dist/chunk-NOTA6TEI.js +47 -0
  91. package/dist/chunk-NURGB5Z4.js +335 -0
  92. package/dist/chunk-NX4EQS3U.js +1062 -0
  93. package/dist/chunk-NXMESY4J.js +1 -0
  94. package/dist/chunk-OJR3MQYV.js +1458 -0
  95. package/dist/chunk-OURHMODR.js +454 -0
  96. package/dist/chunk-P235SVAH.js +1057 -0
  97. package/dist/chunk-P4RH4OSP.js +1038 -0
  98. package/dist/chunk-P6T66CCZ.js +226 -0
  99. package/dist/chunk-PBAHEPLQ.js +223 -0
  100. package/dist/chunk-PBHE5UG6.js +1530 -0
  101. package/dist/chunk-PDKUANDZ.js +35 -0
  102. package/dist/chunk-PWIHZ6NJ.js +296 -0
  103. package/dist/chunk-QEBUY5NY.js +1685 -0
  104. package/dist/chunk-QLCCJEJI.js +253 -0
  105. package/dist/chunk-QOOUC2J5.js +52 -0
  106. package/dist/chunk-QYTPPDAO.js +402 -0
  107. package/dist/chunk-R3XI2VC7.js +801 -0
  108. package/dist/chunk-RCERWOJR.js +154 -0
  109. package/dist/chunk-RJE5JTVK.js +362 -0
  110. package/dist/chunk-RTLBSQ3G.js +1155 -0
  111. package/dist/chunk-RUNNI6VE.js +448 -0
  112. package/dist/chunk-RVMRZAPB.js +354 -0
  113. package/dist/chunk-RYEGFMUO.js +955 -0
  114. package/dist/chunk-RZCF2VVM.js +528 -0
  115. package/dist/chunk-SML67VPV.js +1025 -0
  116. package/dist/chunk-SQ7IY53Q.js +44 -0
  117. package/dist/chunk-SQAG3UXH.js +161 -0
  118. package/dist/chunk-SRTU6HMQ.js +1851 -0
  119. package/dist/chunk-T2OE3YQB.js +550 -0
  120. package/dist/chunk-T3UBLACL.js +299 -0
  121. package/dist/chunk-TLVIHFRO.js +296 -0
  122. package/dist/chunk-TYS6CSS5.js +52 -0
  123. package/dist/chunk-U6EQI3U6.js +130 -0
  124. package/dist/chunk-UJ4FJLBF.js +86 -0
  125. package/dist/chunk-ULOEATBZ.js +347 -0
  126. package/dist/chunk-UNCCZ3NU.js +127 -0
  127. package/dist/chunk-USZW2HHU.js +197 -0
  128. package/dist/chunk-UWN33VNX.js +1610 -0
  129. package/dist/chunk-UZ7EZW3B.js +40 -0
  130. package/dist/chunk-VOKGH627.js +37 -0
  131. package/dist/chunk-WBRZJOEC.js +204 -0
  132. package/dist/chunk-WCSOJFU3.js +380 -0
  133. package/dist/chunk-WCT3NYCV.js +160 -0
  134. package/dist/chunk-WVAJFAIA.js +529 -0
  135. package/dist/chunk-X5LCYCL5.js +394 -0
  136. package/dist/chunk-XAKEUMEJ.js +1256 -0
  137. package/dist/chunk-Y2ZE3MSG.js +590 -0
  138. package/dist/chunk-Y3J7VR52.js +646 -0
  139. package/dist/chunk-Y5QGVTVC.js +33 -0
  140. package/dist/chunk-YAA6IID3.js +548 -0
  141. package/dist/chunk-YGJTDXVR.js +692 -0
  142. package/dist/chunk-YRYKTYMQ.js +186 -0
  143. package/dist/chunk-YYVTBZZC.js +61 -0
  144. package/dist/chunk-Z5CGSFJB.js +647 -0
  145. package/dist/chunk-Z72XRXD4.js +787 -0
  146. package/dist/chunk-ZGR7ULFJ.js +978 -0
  147. package/dist/chunk-ZJEL4H2R.js +851 -0
  148. package/dist/chunk-ZO27FGJS.js +554 -0
  149. package/dist/cli.cjs +112 -112
  150. package/dist/collapse-CLPJD2RL.js +11 -0
  151. package/dist/collapse-SLINJQW6.js +9 -0
  152. package/dist/collapse-ZWUFFCK2.js +9 -0
  153. package/dist/compute-G55RG53N.js +8 -0
  154. package/dist/dgmo-router-DZCIDUO5.js +73 -0
  155. package/dist/dimensions-J3FHIMJ6.js +22 -0
  156. package/dist/element.cjs +492 -325
  157. package/dist/element.js +367 -259
  158. package/dist/element.mjs +492 -325
  159. package/dist/flowchart-parser-ZNSKIBVR.js +21 -0
  160. package/dist/flowchart-renderer-ZAEOL4DX.js +35 -0
  161. package/dist/index.cjs +34 -28
  162. package/dist/index.d.cts +3 -469
  163. package/dist/index.d.ts +3 -469
  164. package/dist/index.js +137 -69679
  165. package/dist/layout-46N4JJYK.js +17 -0
  166. package/dist/layout-5RA6QWR6.js +26 -0
  167. package/dist/layout-5T6RH5M6.js +14 -0
  168. package/dist/layout-7QXCNOVS.js +13 -0
  169. package/dist/layout-EE6GE46Y.js +17 -0
  170. package/dist/layout-G23F7LDH.js +9 -0
  171. package/dist/layout-HRBZPFCN.js +23 -0
  172. package/dist/layout-HWDSRX5E.js +20 -0
  173. package/dist/layout-HYZPWAK5.js +18 -0
  174. package/dist/layout-N76NR4ZO.js +19 -0
  175. package/dist/layout-OB3RI63S.js +14 -0
  176. package/dist/layout-S2QBYSK2.js +15 -0
  177. package/dist/layout-UM5F4DJB.js +15 -0
  178. package/dist/layout-search-OYV77HHS.js +1655 -0
  179. package/dist/load-data-EHDDZ5ZS.js +8 -0
  180. package/dist/palettes-PRJ2RI6D.js +56 -0
  181. package/dist/parser-2MFP2H2E.js +11 -0
  182. package/dist/parser-3STE62GB.js +21 -0
  183. package/dist/parser-4FL2CMDG.js +18 -0
  184. package/dist/parser-4OGOBJJ4.js +13 -0
  185. package/dist/parser-6BTRLKRZ.js +15 -0
  186. package/dist/parser-AQETEVRW.js +21 -0
  187. package/dist/parser-CFZD5IBL.js +12 -0
  188. package/dist/parser-FUSKFGZN.js +16 -0
  189. package/dist/parser-I6KXEYFK.js +13 -0
  190. package/dist/parser-IXSZZQHY.js +18 -0
  191. package/dist/parser-JAL2HD4I.js +13 -0
  192. package/dist/parser-KNSQSKQO.js +20 -0
  193. package/dist/parser-N5O4IHUN.js +13 -0
  194. package/dist/parser-NGU3ETO6.js +16 -0
  195. package/dist/parser-NRAI6O25.js +19 -0
  196. package/dist/parser-O5J4VBII.js +11 -0
  197. package/dist/parser-OV7NOBW6.js +21 -0
  198. package/dist/parser-PQ3POX45.js +23 -0
  199. package/dist/parser-TFZFBVWN.js +11 -0
  200. package/dist/parser-TRTHDFPP.js +18 -0
  201. package/dist/parser-VQP2UJ6W.js +11 -0
  202. package/dist/parser-XDVELHRK.js +13 -0
  203. package/dist/parser-XKRP7QKP.js +11 -0
  204. package/dist/parser-Y6WTBU76.js +14 -0
  205. package/dist/parser-ZSDPZYQP.js +12 -0
  206. package/dist/renderer-2XQQQ6QR.js +28 -0
  207. package/dist/renderer-5NDVNDAZ.js +26 -0
  208. package/dist/renderer-6EEZHMH4.js +26 -0
  209. package/dist/renderer-6W5462RB.js +43 -0
  210. package/dist/renderer-7MVLWWQD.js +22 -0
  211. package/dist/renderer-AWWAF6XD.js +33 -0
  212. package/dist/renderer-BYCNMBF7.js +34 -0
  213. package/dist/renderer-CLJHQPBJ.js +29 -0
  214. package/dist/renderer-GHPZMFU2.js +15 -0
  215. package/dist/renderer-JGBKUCZD.js +16 -0
  216. package/dist/renderer-KQB7UGDS.js +19 -0
  217. package/dist/renderer-PGX2TKVS.js +24 -0
  218. package/dist/renderer-QIWVHWZO.js +15 -0
  219. package/dist/renderer-RPOA5YPZ.js +35 -0
  220. package/dist/renderer-SMIBCRVB.js +24 -0
  221. package/dist/renderer-T7YKC5VP.js +28 -0
  222. package/dist/renderer-TOCOVLHS.js +37 -0
  223. package/dist/renderer-UWA5XS74.js +39 -0
  224. package/dist/renderer-VACVQYML.js +25 -0
  225. package/dist/renderer-W4M6QLIJ.js +37 -0
  226. package/dist/renderer-WZRREQ2P.js +23 -0
  227. package/dist/renderer-YY2OSF6E.js +27 -0
  228. package/dist/renderer-Z7I3W2PP.js +24 -0
  229. package/dist/renderer-ZGNTUWC4.js +22 -0
  230. package/dist/renderer-ZTR3DER3.js +19 -0
  231. package/dist/renderer-radial-C32GPJQ7.js +21 -0
  232. package/dist/resolver-NOAA7ID7.js +13 -0
  233. package/dist/state-collapse-HCGD6IVJ.js +8 -0
  234. package/dist/state-parser-Z2V23ATR.js +18 -0
  235. package/dist/state-renderer-RFYHSP4K.js +34 -0
  236. package/dist/themes-1-CuKpeH.d.cts +908 -0
  237. package/dist/themes-1-CuKpeH.d.ts +908 -0
  238. package/package.json +1 -1
  239. package/src/auto/index.ts +55 -57
  240. package/src/auto/shared.ts +142 -142
  241. package/src/auto/styles.ts +44 -199
  242. package/src/c4/renderer.ts +2 -2
  243. package/src/element/index.ts +29 -21
  244. package/src/infra/renderer.ts +1 -1
  245. package/src/render.ts +34 -14
  246. package/src/sitemap/renderer.ts +1 -1
  247. package/src/timeline/renderer.ts +1 -1
  248. package/src/utils/hover-styles.ts +6 -1
  249. package/src/utils/legend-d3.ts +1 -4
  250. package/src/utils/legend-svg.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/auto/index.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Drop a `<script src="…/auto.js">` on any page; on `DOMContentLoaded`
5
5
  * this module scans for `.dgmo, .language-dgmo`, runs `render()`, and
6
- * replaces each match with `<div class="dgmo-rendered">` containing the
7
- * SVG plus an optional collapsible source panel with Copy and
8
- * "Open in editor" actions.
6
+ * replaces each match with the standard DGMO embed block (BL-114,
7
+ * `figure.dgmo.dgmo-rendered`) — the diagram plus a hover-reveal icon
8
+ * toolbar with view-source, Copy, and "Open in editor" actions.
9
9
  *
10
10
  * Public API: frozen `window.dgmo` and alias `window.diagrammo` with
11
11
  * `{ initialize, run, version }`. Configuration is read from the
@@ -24,8 +24,8 @@ import {
24
24
  resolveTheme,
25
25
  ensureStyles,
26
26
  deriveAriaLabel,
27
- buildSourcePanel,
28
- buildErrorBanner,
27
+ buildRenderedBlock,
28
+ buildErrorBlock,
29
29
  buildShareUrl,
30
30
  } from './shared';
31
31
 
@@ -59,7 +59,7 @@ let activeConfig: Required<AutoConfig> = { ...DEFAULTS };
59
59
 
60
60
  // Track wrappers so we can re-render on theme changes / re-runs.
61
61
  interface TrackedWrapper {
62
- wrapper: HTMLDivElement;
62
+ wrapper: HTMLElement;
63
63
  source: string;
64
64
  perElementShowSource: boolean | null;
65
65
  }
@@ -220,7 +220,28 @@ function determineReplaceTarget(matched: Element): Element {
220
220
  }
221
221
 
222
222
  interface ProcessOutcome {
223
- wrapper?: HTMLDivElement;
223
+ wrapper?: HTMLElement;
224
+ }
225
+
226
+ /**
227
+ * Swap a failing source element for the standard `.dgmo--error` card (which
228
+ * carries the message AND the offending source — unified error shape across
229
+ * every embed surface). The card is marked `data-dgmo-processed` so a
230
+ * follow-up run() doesn't loop on it; users wanting to retry after editing
231
+ * should insert a fresh source element.
232
+ */
233
+ function replaceWithErrorCard(
234
+ el: Element,
235
+ source: string,
236
+ opts: {
237
+ message: string;
238
+ severity?: string;
239
+ line?: number;
240
+ column?: number;
241
+ }
242
+ ): void {
243
+ const card = buildErrorBlock({ ...opts, source });
244
+ determineReplaceTarget(el).replaceWith(card);
224
245
  }
225
246
 
226
247
  async function processElement(el: Element): Promise<ProcessOutcome> {
@@ -237,15 +258,10 @@ async function processElement(el: Element): Promise<ProcessOutcome> {
237
258
  const source = el.textContent || '';
238
259
  const sourceBytes = new TextEncoder().encode(source).byteLength;
239
260
  if (sourceBytes > SOURCE_BYTE_CAP) {
240
- el.style.visibility = '';
241
- // Keep `data-dgmo-processed` set so a follow-up run() doesn't loop on
242
- // the same broken source. Users wanting to retry after editing should
243
- // clear the attribute manually or replace the element. // un-hide so user can see the source
244
- const banner = buildErrorBanner({
261
+ replaceWithErrorCard(el, source, {
245
262
  message: `DGMO source too large to render — ${SOURCE_BYTE_CAP / 1024} KB max`,
246
263
  severity: 'error',
247
264
  });
248
- el.parentElement?.insertBefore(banner, el);
249
265
  warn('source exceeds 256 KB cap');
250
266
  return {};
251
267
  }
@@ -281,59 +297,40 @@ async function processElement(el: Element): Promise<ProcessOutcome> {
281
297
  palette: cfg.palette,
282
298
  });
283
299
  } catch (err) {
284
- el.style.visibility = '';
285
- // Keep `data-dgmo-processed` set so a follow-up run() doesn't loop on
286
- // the same broken source. Users wanting to retry after editing should
287
- // clear the attribute manually or replace the element.
288
300
  const message =
289
301
  err instanceof Error ? err.message : 'Render failed unexpectedly';
290
- const banner = buildErrorBanner({ message, severity: 'error' });
291
- el.parentElement?.insertBefore(banner, el);
302
+ replaceWithErrorCard(el, source, { message, severity: 'error' });
292
303
  warn('render() rejected:', err);
293
304
  return {};
294
305
  }
295
306
 
296
307
  if (result.diagnostics && result.diagnostics.length > 0) {
297
- el.style.visibility = '';
298
- // Keep `data-dgmo-processed` set so a follow-up run() doesn't loop on
299
- // the same broken source. Users wanting to retry after editing should
300
- // clear the attribute manually or replace the element.
301
308
  const d = result.diagnostics[0]!; // In-bounds by length > 0 check above.
302
309
  // d may have shape { severity, message, line, column } from diagnostics.ts
303
- const banner = buildErrorBanner({
310
+ replaceWithErrorCard(el, source, {
304
311
  message: d.message,
305
312
  severity: d.severity,
306
313
  line: d.line,
307
314
  ...(d.column !== undefined && { column: d.column }),
308
315
  });
309
- el.parentElement?.insertBefore(banner, el);
310
316
  warn('diagnostic:', d.message, d.line, d.column);
311
317
  return {};
312
318
  }
313
319
 
314
320
  if (!result.svg) {
315
- el.style.visibility = '';
316
- // Keep `data-dgmo-processed` set so a follow-up run() doesn't loop on
317
- // the same broken source. Users wanting to retry after editing should
318
- // clear the attribute manually or replace the element.
319
- const banner = buildErrorBanner({
321
+ replaceWithErrorCard(el, source, {
320
322
  message: 'Empty SVG returned from renderer',
321
323
  severity: 'error',
322
324
  });
323
- el.parentElement?.insertBefore(banner, el);
324
325
  return {};
325
326
  }
326
327
 
327
- // Build wrapper.
328
- const wrapper = document.createElement('div');
329
328
  const themeClass =
330
329
  resolvedTheme === 'dark'
331
330
  ? 'dgmo-theme-dark'
332
331
  : resolvedTheme === 'transparent'
333
332
  ? 'dgmo-theme-transparent'
334
333
  : 'dgmo-theme-light';
335
- wrapper.className = `dgmo-rendered ${themeClass}`;
336
- wrapper.dataset['dgmoProcessed'] = 'true';
337
334
 
338
335
  // Insert SVG via a detached holder + post-insertion sanitization. The
339
336
  // spec said "never assign user-supplied content to innerHTML"; we treat
@@ -349,15 +346,10 @@ async function processElement(el: Element): Promise<ProcessOutcome> {
349
346
  svgHolder.innerHTML = result.svg;
350
347
  const svgEl = svgHolder.querySelector('svg');
351
348
  if (!svgEl) {
352
- el.style.visibility = '';
353
- // Keep `data-dgmo-processed` set so a follow-up run() doesn't loop on
354
- // the same broken source. Users wanting to retry after editing should
355
- // clear the attribute manually or replace the element.
356
- const banner = buildErrorBanner({
349
+ replaceWithErrorCard(el, source, {
357
350
  message: 'Empty SVG returned from renderer',
358
351
  severity: 'error',
359
352
  });
360
- el.parentElement?.insertBefore(banner, el);
361
353
  return {};
362
354
  }
363
355
  sanitizeSvgInPlace(svgEl);
@@ -366,24 +358,30 @@ async function processElement(el: Element): Promise<ProcessOutcome> {
366
358
  // control/bidi-strip filter rather than relying on renderer output).
367
359
  svgEl.setAttribute('role', 'img');
368
360
  svgEl.setAttribute('aria-label', ariaLabel);
369
- wrapper.appendChild(svgEl);
370
-
371
- if (showSource) {
372
- // Build share URL for the "Open in editor" action.
373
- let shareUrl: string | null = null;
374
- if (cfg.showEditorLink) {
375
- shareUrl = buildShareUrl(source, {
376
- palette: cfg.palette,
377
- theme: resolvedTheme === 'dark' ? 'dark' : 'light',
378
- editorBase: EDITOR_BASE_URL,
379
- campaign: VERSION,
380
- utmSource: 'auto-embed',
381
- });
382
- }
383
- const panel = buildSourcePanel(source, shareUrl, cfg.showEditorLink);
384
- wrapper.appendChild(panel);
361
+
362
+ // Build share URL for the "Open in editor" toolbar action.
363
+ let shareUrl: string | null = null;
364
+ if (showSource && cfg.showEditorLink) {
365
+ shareUrl = buildShareUrl(source, {
366
+ palette: cfg.palette,
367
+ theme: resolvedTheme === 'dark' ? 'dark' : 'light',
368
+ editorBase: EDITOR_BASE_URL,
369
+ campaign: VERSION,
370
+ utmSource: 'auto-embed',
371
+ });
385
372
  }
386
373
 
374
+ // Standard embed block (BL-114): canonical chrome from src/embed.
375
+ const wrapper = buildRenderedBlock({
376
+ source,
377
+ svgEl,
378
+ themeClass,
379
+ showSource,
380
+ showEditorLink: cfg.showEditorLink,
381
+ shareUrl,
382
+ });
383
+ wrapper.dataset['dgmoProcessed'] = 'true';
384
+
387
385
  // Track wrapper so re-runs / theme changes can update it. Only persist
388
386
  // the explicit 'true'/'false' values; treat any other value as null so
389
387
  // the global default applies on re-render (matches first-render behavior).
@@ -2,15 +2,16 @@
2
2
  * Shared browser-embed helpers for the `@diagrammo/dgmo` client-side
3
3
  * drop-ins (`./auto` script-tag renderer and `./element` custom element).
4
4
  *
5
- * These are the SVG sanitizer, theme resolver, source panel (with Copy +
6
- * "Open in editor"), error banner, aria-label derivation, style injection,
7
- * and share-URL builder — factored out of `auto/index.ts` so the two entries
8
- * share one implementation rather than diverging copies. The `auto` entry's
9
- * public `window.dgmo` surface is unaffected; it imports from here.
5
+ * These are the SVG sanitizer, theme resolver, the standard-block DOM
6
+ * assembler (canonical chrome from `src/embed` — BL-114), the error card,
7
+ * aria-label derivation, style injection, and share-URL builder — factored
8
+ * out of `auto/index.ts` so the two entries share one implementation rather
9
+ * than diverging copies. The `auto` entry's public `window.dgmo` surface is
10
+ * unaffected; it imports from here.
10
11
  */
11
12
 
12
13
  import { encodeDiagramUrl } from '../sharing';
13
- import { highlightDgmo, type HighlightToken } from '../editor/highlight-api';
14
+ import { buildDgmoBlockHtml, errorBlockHtml } from '../embed';
14
15
  import { CSS } from './styles';
15
16
  import { safeHref } from '../utils/safe-href';
16
17
 
@@ -22,10 +23,10 @@ export const VERSION: string =
22
23
  (typeof __DGMO_VERSION__ === 'string' && __DGMO_VERSION__) || 'dev';
23
24
 
24
25
  /** Default hosted editor used by "Open in editor" share links. */
25
- export const EDITOR_BASE_URL = 'https://online.diagrammo.app';
26
+ export { EDITOR_BASE_URL } from '../embed';
26
27
 
27
28
  const ARIA_LABEL_MAX = 200;
28
- const COPIED_INTERACTION_MS = 1200;
29
+ const COPIED_INTERACTION_MS = 1500;
29
30
  const LOG_PREFIX = '[dgmo]';
30
31
 
31
32
  // Bidi override + control char strip for aria-label sanitization. The regex
@@ -148,156 +149,149 @@ export function deriveAriaLabel(source: string): string {
148
149
  }
149
150
 
150
151
  // ============================================================
151
- // Source panel construction
152
+ // Standard block assembly (canonical chrome from src/embed — BL-114)
152
153
  // ============================================================
153
154
 
154
- function buildHighlightedSource(source: string): DocumentFragment {
155
- const frag = document.createDocumentFragment();
156
- let tokens: HighlightToken[];
157
- try {
158
- tokens = highlightDgmo(source);
159
- } catch {
160
- frag.appendChild(document.createTextNode(source));
161
- return frag;
162
- }
163
- for (const tok of tokens) {
164
- if (!tok.text) continue;
165
- const span = document.createElement('span');
166
- span.className = `dgmo-tok-${tok.role}`;
167
- span.textContent = tok.text;
168
- frag.appendChild(span);
169
- }
170
- return frag;
155
+ export interface RenderedBlockOptions {
156
+ /** Trimmed DGMO source (copy payload + highlighted panel content). */
157
+ source: string;
158
+ /** Already-rendered AND sanitized SVG element to mount as the diagram. */
159
+ svgEl: Element;
160
+ /** `dgmo-theme-light` / `dgmo-theme-dark` / `dgmo-theme-transparent`. */
161
+ themeClass: string;
162
+ /** Emit the source disclosure + toolbar (showcase) or diagram-only. */
163
+ showSource: boolean;
164
+ /** Emit the open-in-editor toolbar link at all. */
165
+ showEditorLink: boolean;
166
+ /**
167
+ * Pre-built share URL (with the surface's UTM params) for the
168
+ * open-in-editor link. `null` (encode failed / too large) drops the link.
169
+ */
170
+ shareUrl: string | null;
171
171
  }
172
172
 
173
- // ----- icon SVGs (static author-controlled markup) -----
174
- const COPY_ICON_SVG =
175
- '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5V3a1.5 1.5 0 0 0-1.5-1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h2.5"/></svg>';
176
- const CHECK_ICON_SVG =
177
- '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 8.5 6.5 12 13 4.5"/></svg>';
178
- const EXTERNAL_ICON_SVG =
179
- '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2h5v5"/><path d="M14 2L7 9"/><path d="M13 9v4a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h4"/></svg>';
180
-
181
- function setIcon(el: Element, svg: string): void {
182
- // Static icon strings authored above — no user content.
183
- el.innerHTML = svg;
173
+ /**
174
+ * Assemble the standard DGMO embed block (`figure.dgmo` + hover-reveal icon
175
+ * toolbar + native `<details>` source panel) around an already-sanitized SVG
176
+ * node, and bind the toolbar's copy/open click handling.
177
+ *
178
+ * The markup comes from the canonical `buildDgmoBlockHtml` in `src/embed`;
179
+ * this wrapper only (1) mounts the live SVG node into the `.dgmo-svg` slot,
180
+ * (2) swaps the open-in-editor href for the surface's UTM-tagged share URL,
181
+ * and (3) attaches the click handler that `<details>` can't provide (copy).
182
+ */
183
+ export function buildRenderedBlock(opts: RenderedBlockOptions): HTMLElement {
184
+ const html = buildDgmoBlockHtml(opts.source, '<div class="dgmo-svg"></div>', {
185
+ mode: opts.showSource ? 'showcase' : 'diagram',
186
+ showOpenInEditor: opts.showSource && opts.showEditorLink,
187
+ // Keep the legacy `.dgmo-rendered` + theme hooks so the anti-flash CSS,
188
+ // idempotency filters, and theme-scoped token colors keep working.
189
+ legacyClassNames: ['dgmo-rendered', opts.themeClass],
190
+ });
191
+ const holder = document.createElement('div');
192
+ // buildDgmoBlockHtml output is author-controlled markup with all
193
+ // user-supplied strings escaped; the SVG slot is empty at this point.
194
+ holder.innerHTML = html;
195
+ const wrapper = holder.firstElementChild as HTMLElement;
196
+
197
+ // Mount the sanitized SVG node directly (no serialize/re-parse round trip).
198
+ wrapper.querySelector('.dgmo-svg')?.appendChild(opts.svgEl);
199
+
200
+ // The canonical builder encodes a bare share URL; the browser surfaces
201
+ // carry UTM attribution, so swap in the pre-built URL (or drop the link
202
+ // when encoding failed — copy remains as the fallback affordance).
203
+ const open = wrapper.querySelector<HTMLAnchorElement>('a.dgmo-open');
204
+ if (open) {
205
+ if (opts.shareUrl) open.href = opts.shareUrl;
206
+ else open.remove();
207
+ }
208
+
209
+ bindBlockToolbar(wrapper);
210
+ return wrapper;
184
211
  }
185
212
 
186
- export function buildSourcePanel(
187
- source: string,
188
- shareUrl: string | null,
189
- showEditorLink: boolean
190
- ): HTMLDivElement {
191
- const panel = document.createElement('div');
192
- panel.className = 'dgmo-source-panel';
193
-
194
- const toggle = document.createElement('button');
195
- toggle.type = 'button';
196
- toggle.className = 'dgmo-source-toggle';
197
- toggle.setAttribute('aria-expanded', 'false');
198
- const chevron = document.createElement('span');
199
- chevron.className = 'dgmo-chevron';
200
- chevron.setAttribute('aria-hidden', 'true');
201
- chevron.textContent = '▸';
202
- toggle.appendChild(chevron);
203
- const label = document.createElement('span');
204
- label.textContent = 'DGMO source';
205
- toggle.appendChild(label);
206
-
207
- const body = document.createElement('div');
208
- body.className = 'dgmo-source-body';
209
- const pre = document.createElement('pre');
210
- pre.className = 'dgmo-source-pre';
211
- pre.appendChild(buildHighlightedSource(source));
212
- body.appendChild(pre);
213
-
214
- const actions = document.createElement('div');
215
- actions.className = 'dgmo-source-actions';
216
-
217
- const copyBtn = document.createElement('button');
218
- copyBtn.type = 'button';
219
- copyBtn.className = 'dgmo-btn dgmo-btn-copy';
220
- copyBtn.setAttribute('aria-label', 'Copy DGMO source');
221
- copyBtn.title = 'Copy source';
222
- setIcon(copyBtn, COPY_ICON_SVG);
223
- copyBtn.addEventListener('click', () => {
224
- void copySource(source, copyBtn);
213
+ /**
214
+ * Click handling for the standard block toolbar (mirrors remark-dgmo's
215
+ * `bindDgmo` reference implementation, but bound per-wrapper because these
216
+ * surfaces build their DOM imperatively).
217
+ *
218
+ * The toolbar is the `<summary>` of the source `<details>`: clicks on the
219
+ * `</>` toggle keep the native default (toggling), while clicks on
220
+ * `.dgmo-toolbar-btn` (copy / open) must preventDefault so they don't ALSO
221
+ * toggle — which cancels an anchor's navigation, hence the manual
222
+ * `window.open`.
223
+ */
224
+ function bindBlockToolbar(wrapper: HTMLElement): void {
225
+ wrapper.addEventListener('click', (e) => {
226
+ void handleToolbarClick(e);
225
227
  });
226
- actions.appendChild(copyBtn);
227
-
228
- if (showEditorLink) {
229
- const editorBtn = document.createElement('a');
230
- editorBtn.className = 'dgmo-btn dgmo-btn-editor';
231
- editorBtn.target = '_blank';
232
- editorBtn.rel = 'noopener noreferrer';
233
- editorBtn.setAttribute('aria-label', 'Open in editor');
234
- setIcon(editorBtn, EXTERNAL_ICON_SVG);
235
- if (shareUrl) {
236
- editorBtn.href = shareUrl;
237
- editorBtn.title = 'Open in editor';
228
+ }
229
+
230
+ async function handleToolbarClick(e: Event): Promise<void> {
231
+ const target = e.target as Element | null;
232
+ if (!target || typeof target.closest !== 'function') return;
233
+ const btn = target.closest('.dgmo-toolbar-btn') as HTMLElement | null;
234
+ if (!btn) return;
235
+
236
+ const insideSummary = !!btn.closest('summary');
237
+ if (insideSummary) e.preventDefault();
238
+
239
+ if (btn.matches('button.dgmo-copy')) {
240
+ const copied = await copyText(btn.dataset['dgmoSource'] ?? '');
241
+ if (copied) {
242
+ btn.classList.add('dgmo-copy--success');
243
+ setTimeout(
244
+ () => btn.classList.remove('dgmo-copy--success'),
245
+ COPIED_INTERACTION_MS
246
+ );
238
247
  } else {
239
- editorBtn.setAttribute('aria-disabled', 'true');
240
- editorBtn.title =
241
- 'Diagram too large for share link; copy source and paste into editor';
242
- editorBtn.addEventListener('click', (e) => e.preventDefault());
248
+ sharedWarn('clipboard write failed');
243
249
  }
244
- actions.appendChild(editorBtn);
250
+ return;
245
251
  }
246
252
 
247
- body.appendChild(actions);
248
- panel.appendChild(toggle);
249
- panel.appendChild(body);
250
-
251
- toggle.addEventListener('click', () => {
252
- const open = body.classList.toggle('dgmo-open');
253
- toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
254
- });
255
-
256
- return panel;
253
+ if (insideSummary && btn.matches('a.dgmo-open')) {
254
+ const anchor = btn as HTMLAnchorElement;
255
+ if (anchor.href) {
256
+ window.open(
257
+ anchor.href,
258
+ anchor.target || '_blank',
259
+ 'noopener,noreferrer'
260
+ );
261
+ }
262
+ }
257
263
  }
258
264
 
259
- async function copySource(
260
- source: string,
261
- btn: HTMLButtonElement
262
- ): Promise<void> {
263
- let copied = false;
265
+ async function copyText(text: string): Promise<boolean> {
264
266
  try {
265
267
  if (navigator?.clipboard?.writeText) {
266
- await navigator.clipboard.writeText(source);
267
- copied = true;
268
+ await navigator.clipboard.writeText(text);
269
+ return true;
268
270
  }
269
271
  } catch {
270
272
  // fallthrough to execCommand
271
273
  }
272
- if (!copied && typeof document !== 'undefined') {
274
+ if (typeof document !== 'undefined') {
273
275
  try {
274
276
  const ta = document.createElement('textarea');
275
- ta.value = source;
277
+ ta.value = text;
276
278
  ta.setAttribute('readonly', '');
277
279
  ta.style.position = 'absolute';
278
280
  ta.style.left = '-9999px';
279
281
  document.body.appendChild(ta);
280
282
  ta.select();
281
- copied = document.execCommand('copy');
283
+ const copied = document.execCommand('copy');
282
284
  document.body.removeChild(ta);
285
+ return copied;
283
286
  } catch {
284
- copied = false;
287
+ return false;
285
288
  }
286
289
  }
287
- if (copied) {
288
- setIcon(btn, CHECK_ICON_SVG);
289
- btn.classList.add('dgmo-btn-copied');
290
- setTimeout(() => {
291
- setIcon(btn, COPY_ICON_SVG);
292
- btn.classList.remove('dgmo-btn-copied');
293
- }, COPIED_INTERACTION_MS);
294
- } else {
295
- sharedWarn('clipboard write failed');
296
- }
290
+ return false;
297
291
  }
298
292
 
299
293
  // ============================================================
300
- // Error banner
294
+ // Error card (canonical errorBlockHtml from src/embed)
301
295
  // ============================================================
302
296
 
303
297
  export interface BannerOptions {
@@ -307,26 +301,32 @@ export interface BannerOptions {
307
301
  column?: number;
308
302
  }
309
303
 
310
- export function buildErrorBanner(opts: BannerOptions): HTMLDivElement {
311
- const banner = document.createElement('div');
312
- banner.className = 'dgmo-error-banner';
313
- banner.setAttribute('role', 'alert');
314
-
315
- const title = document.createElement('div');
316
- title.className = 'dgmo-error-banner-title';
317
- title.textContent = `${opts.severity ?? 'error'}: ${opts.message}`;
318
- banner.appendChild(title);
319
-
304
+ /**
305
+ * Build the standard `.dgmo--error` card as a live DOM element. Message +
306
+ * location are folded into one line; the offending source is shown inside
307
+ * the card (canonical shape — same as remark-dgmo and the other adopters).
308
+ */
309
+ export function buildErrorBlock(
310
+ opts: BannerOptions & { source: string }
311
+ ): HTMLElement {
312
+ let message =
313
+ opts.severity && opts.severity !== 'error'
314
+ ? `${opts.severity}: ${opts.message}`
315
+ : opts.message;
320
316
  if (opts.line !== undefined && opts.line > 0) {
321
- const loc = document.createElement('div');
322
- loc.className = 'dgmo-error-banner-loc';
323
- loc.textContent =
317
+ message +=
324
318
  opts.column !== undefined
325
- ? `at ${opts.line}:${opts.column}`
326
- : `at line ${opts.line}`;
327
- banner.appendChild(loc);
319
+ ? ` (at ${opts.line}:${opts.column})`
320
+ : ` (at line ${opts.line})`;
328
321
  }
329
- return banner;
322
+ const holder = document.createElement('div');
323
+ // errorBlockHtml escapes both message and source.
324
+ holder.innerHTML = errorBlockHtml(new Error(message), opts.source);
325
+ const card = holder.firstElementChild as HTMLElement;
326
+ // Mark processed so the auto surface's selectors never re-scan the card
327
+ // (its root carries the `.dgmo` class the scanner matches on).
328
+ card.setAttribute('data-dgmo-processed', 'true');
329
+ return card;
330
330
  }
331
331
 
332
332
  // ============================================================