@ai-markdown/core 3.3.0 → 3.4.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.
@@ -1,3 +1,4 @@
1
+ import { StreamingLanguageDetector } from "@ai-markdown/code-language-detector";
1
2
  function extractCode(node) {
2
3
  if (node?.tagName !== "pre" || node.children.length !== 1 || Object.keys(node.properties).length) return null;
3
4
  const child = node.children[0];
@@ -43,19 +44,26 @@ function text(node) {
43
44
  if (node.tagName === "img" || node.tagName === "cross-chunk-image") return String(node.properties.alt ?? "");
44
45
  return node.children.map(text).join("");
45
46
  }
47
+ var ExportRefused = class extends Error {
48
+ reason;
49
+ constructor(reason) {
50
+ super(reason);
51
+ this.reason = reason;
52
+ }
53
+ };
46
54
  function projectTable(node) {
47
55
  try {
48
- if (node?.tagName !== "table") throw new Error("Table source is unavailable");
56
+ if (node?.tagName !== "table") throw new ExportRefused("unavailable");
49
57
  const rows = [];
50
58
  const walk = (parent) => {
51
59
  for (const child of parent.children) {
52
60
  if (child.type === "text" && !child.value.trim()) continue;
53
- if (child.type !== "element") throw new Error("Unsupported table content");
61
+ if (child.type !== "element") throw new ExportRefused("unsupportedContent");
54
62
  if (child.tagName === "tr") {
55
63
  const cells = [];
56
64
  for (const cell of child.children) {
57
65
  if (cell.type === "text" && !cell.value.trim()) continue;
58
- if (cell.type !== "element" || !["th", "td"].includes(cell.tagName) || Number(cell.properties.colSpan ?? 1) !== 1 || Number(cell.properties.rowSpan ?? 1) !== 1) throw new Error("Only tables without merged cells can be exported");
66
+ if (cell.type !== "element" || !["th", "td"].includes(cell.tagName) || Number(cell.properties.colSpan ?? 1) !== 1 || Number(cell.properties.rowSpan ?? 1) !== 1) throw new ExportRefused("mergedCells");
59
67
  cells.push(text(cell));
60
68
  }
61
69
  rows.push(cells);
@@ -64,14 +72,14 @@ function projectTable(node) {
64
72
  "tbody",
65
73
  "tfoot"
66
74
  ].includes(child.tagName) && parent === node) walk(child);
67
- else throw new Error("Unsupported table structure");
75
+ else throw new ExportRefused("unsupportedStructure");
68
76
  }
69
77
  };
70
78
  walk(node);
71
- if (!rows.length || !rows[0].length || rows.some((r) => r.length !== rows[0].length)) throw new Error("Only rectangular tables can be exported");
79
+ if (!rows.length || !rows[0].length || rows.some((r) => r.length !== rows[0].length)) throw new ExportRefused("notRectangular");
72
80
  return { rows };
73
81
  } catch (error) {
74
- return { reason: error instanceof Error ? error.message : "Table cannot be exported" };
82
+ return { reason: error instanceof ExportRefused ? error.reason : "unavailable" };
75
83
  }
76
84
  }
77
85
  const formulaPrefix = /^[\s\u0000-\u001f]*[=+@-]/;
@@ -81,6 +89,57 @@ function serializeTable(rows, delimiter = ",") {
81
89
  return /["\r\n\t,]/.test(value) ? `"${value.replaceAll("\"", "\"\"")}"` : value;
82
90
  }).join(delimiter)).join("\r\n");
83
91
  }
92
+ function codeBody(code) {
93
+ return code.endsWith("\n") ? code.slice(0, -1) : code;
94
+ }
95
+ function startsWithText(text, prefix) {
96
+ return text.length >= prefix.length && text.slice(0, prefix.length) === prefix;
97
+ }
98
+ function extendsCode(next, previous) {
99
+ return startsWithText(next, codeBody(previous));
100
+ }
101
+ function createCodeLanguageTracker(onSettle) {
102
+ const detector = new StreamingLanguageDetector();
103
+ let followed;
104
+ let settled = null;
105
+ let timer;
106
+ const cancel = () => {
107
+ if (timer !== void 0) clearTimeout(timer);
108
+ timer = void 0;
109
+ };
110
+ const judge = (from, text, streaming) => (streaming && settled !== text ? from.update(text) : from.finalize(text)).language;
111
+ return {
112
+ read(code, streaming) {
113
+ if (followed !== void 0 && extendsCode(code, followed)) return detector.current.language;
114
+ return judge(new StreamingLanguageDetector(), codeBody(code), streaming);
115
+ },
116
+ observe(code, streaming) {
117
+ cancel();
118
+ const text = codeBody(code);
119
+ const starting = followed === void 0 || !extendsCode(code, followed);
120
+ if (starting) detector.reset();
121
+ const before = detector.current.language;
122
+ followed = code;
123
+ const after = judge(detector, text, streaming);
124
+ if (streaming && settled !== text) timer = setTimeout(() => {
125
+ timer = void 0;
126
+ settled = text;
127
+ detector.finalize(text);
128
+ onSettle();
129
+ }, 300);
130
+ return !starting && before !== after;
131
+ },
132
+ dispose: cancel
133
+ };
134
+ }
135
+ function nextCodeFrame(previous, code, language) {
136
+ if (previous?.code === code && previous.language === language) return previous;
137
+ return {
138
+ code,
139
+ language,
140
+ generation: previous && language === previous.language && extendsCode(code, previous.code) ? previous.generation : {}
141
+ };
142
+ }
84
143
  function createCodeFrame(initial, publish) {
85
144
  let shown = initial;
86
145
  let latest = initial;
@@ -98,7 +157,7 @@ function createCodeFrame(initial, publish) {
98
157
  };
99
158
  return {
100
159
  update(next, streaming, interval) {
101
- const replaced = next.language !== latest.language || !next.code.startsWith(latest.code);
160
+ const replaced = next.generation !== latest.generation;
102
161
  latest = next;
103
162
  if (delay !== interval) cancel();
104
163
  delay = interval;
@@ -160,16 +219,17 @@ function createJsonCompletenessScanner() {
160
219
  let invalid = false;
161
220
  let last = "";
162
221
  return (text) => {
222
+ const body = codeBody(text);
163
223
  let from = previous.length;
164
- if (!text.startsWith(previous)) {
224
+ if (!startsWithText(body, previous)) {
165
225
  from = 0;
166
226
  depth = 0;
167
227
  quoted = escaped = invalid = false;
168
228
  last = "";
169
229
  }
170
- previous = text;
171
- for (let i = from; i < text.length; i++) {
172
- const c = text[i];
230
+ previous = body;
231
+ for (let i = from; i < body.length; i++) {
232
+ const c = body[i];
173
233
  if (!/\s/.test(c)) last = c;
174
234
  if (quoted) {
175
235
  if (escaped) escaped = false;
@@ -188,6 +248,89 @@ function createJsonCompletenessScanner() {
188
248
  function jsonLooksComplete(text) {
189
249
  return createJsonCompletenessScanner()(text);
190
250
  }
251
+ function createCodeBlockSession(onChange) {
252
+ let committed;
253
+ let shown;
254
+ let frames;
255
+ let token;
256
+ let committing = false;
257
+ const tracker = createCodeLanguageTracker(() => onChange());
258
+ const scanJson = createJsonCompletenessScanner();
259
+ let formatted;
260
+ const generationOf = (input) => {
261
+ if (!committed) return 0;
262
+ return input.identity === committed.identity && input.language === committed.language && extendsCode(input.code, committed.code) ? committed.generation : committed.generation + 1;
263
+ };
264
+ const languageOf = (input) => input.language || (input.autoDetect ? tracker.read(input.code, input.streaming) ?? "" : "");
265
+ const pretty = (source, nested) => {
266
+ if (formatted?.source !== source || formatted.nested !== nested) formatted = {
267
+ source,
268
+ nested,
269
+ text: prettyPrintJson(source, nested)
270
+ };
271
+ return formatted.text;
272
+ };
273
+ return {
274
+ read(input) {
275
+ const generation = generationOf(input);
276
+ const language = languageOf(input);
277
+ const displayed = !shown || !input.streaming || input.highlightIntervalMs === 0 || shown.generation !== generation || shown.language !== language ? input.code : shown.code;
278
+ return {
279
+ text: input.formatJson && displayed && language === "json" && (!input.streaming || scanJson(displayed)) ? pretty(displayed, input.expandNestedJson) : displayed,
280
+ language,
281
+ generation
282
+ };
283
+ },
284
+ commit(input) {
285
+ const generation = generationOf(input);
286
+ const moved = input.autoDetect && !input.language ? tracker.observe(input.code, input.streaming) : (tracker.dispose(), false);
287
+ const language = languageOf(input);
288
+ committed = {
289
+ code: input.code,
290
+ language: input.language,
291
+ identity: input.identity,
292
+ generation
293
+ };
294
+ const key = `${generation}:${language}`;
295
+ if (token?.key !== key) token = {
296
+ key,
297
+ value: {}
298
+ };
299
+ const frame = {
300
+ code: input.code,
301
+ language,
302
+ generation: token.value
303
+ };
304
+ committing = true;
305
+ try {
306
+ if (!frames) {
307
+ shown = {
308
+ code: input.code,
309
+ language,
310
+ generation
311
+ };
312
+ frames = createCodeFrame(frame, (next) => {
313
+ shown = {
314
+ code: next.code,
315
+ language: next.language,
316
+ generation: committed.generation
317
+ };
318
+ if (!committing) onChange();
319
+ });
320
+ }
321
+ frames.update(frame, input.streaming, input.highlightIntervalMs);
322
+ if (shown.code === input.code && shown.language === language) shown.generation = generation;
323
+ } finally {
324
+ committing = false;
325
+ }
326
+ if (moved) onChange();
327
+ },
328
+ dispose() {
329
+ frames?.dispose();
330
+ tracker.dispose();
331
+ }
332
+ };
333
+ }
191
334
  function createRenderQueue() {
192
335
  const pending = /* @__PURE__ */ new Map();
193
336
  let running = false;
@@ -229,9 +372,6 @@ function createRenderQueue() {
229
372
  const mermaidRenderQueue = createRenderQueue();
230
373
  const SAFE_SECURITY_LEVELS = /* @__PURE__ */ new Set(["strict", "sandbox"]);
231
374
  let ownedThemes = /* @__PURE__ */ new WeakMap();
232
- const resetMermaidInitializationForTests = () => {
233
- ownedThemes = /* @__PURE__ */ new WeakMap();
234
- };
235
375
  const readSiteConfig = (mermaid) => {
236
376
  const api = mermaid.mermaidAPI;
237
377
  return api?.getSiteConfig?.() ?? api?.getConfig?.() ?? {};
@@ -269,94 +409,265 @@ const ensureMermaidInitialized = (mermaid, isDark) => {
269
409
  }
270
410
  mermaid.initialize(next);
271
411
  };
272
- let nextId = 0;
412
+ function diagramErrorText(error, labels) {
413
+ if (error.reason === "tooLarge") return labels.tooLarge(error.length, error.limit);
414
+ return error.message || labels.renderFailed;
415
+ }
416
+ const ERROR_MESSAGE_MAX_CHARS = 500;
417
+ const LOAD_RETRIES = 3;
418
+ const LOAD_RETRY_MS = 1500;
419
+ function engineMessage(error) {
420
+ const text = (error instanceof Error ? error.message : typeof error === "string" ? error : "").replace(/\r\n?/g, "\n").trim();
421
+ return text.length > ERROR_MESSAGE_MAX_CHARS ? `${text.slice(0, ERROR_MESSAGE_MAX_CHARS)}…` : text;
422
+ }
423
+ var DiagramFailure = class {
424
+ view;
425
+ constructor(view) {
426
+ this.view = view;
427
+ }
428
+ };
429
+ const diagramId = () => `mermaid-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
273
430
  function createDiagramController(load, publish) {
274
431
  const owner = {};
432
+ let request;
433
+ let current;
434
+ let shown;
435
+ let frames;
436
+ let updating = false;
437
+ let view = { kind: "source" };
438
+ let loadFailed = false;
439
+ let previousStreaming = false;
440
+ let needsCorrective = false;
441
+ let lastSuccess;
442
+ let loadFailures = 0;
443
+ let nonce = 0;
444
+ let ran;
275
445
  let epoch = 0;
276
- let previous;
277
- let svg = "";
278
- let bind;
279
- let timer;
446
+ let retryTimer;
447
+ const sameError = (a, b) => a.reason === "tooLarge" ? b.reason === "tooLarge" && a.length === b.length && a.limit === b.limit : b.reason === a.reason && a.message === b.message;
448
+ const emit = () => publish({
449
+ view,
450
+ loadFailed
451
+ });
452
+ const show = (next) => {
453
+ if (next.kind === "source" && view.kind === "source") return;
454
+ if (next.kind === "error" && view.kind === "error" && sameError(next, view)) return;
455
+ view = next;
456
+ emit();
457
+ };
280
458
  const cancel = () => {
281
459
  epoch++;
282
- if (timer !== void 0) clearTimeout(timer);
283
- timer = void 0;
284
460
  mermaidRenderQueue.cancel(owner);
461
+ if (retryTimer !== void 0) clearTimeout(retryTimer);
462
+ retryTimer = void 0;
285
463
  };
286
- return {
287
- update(request) {
288
- const replaced = !previous || previous.resetKey !== request.resetKey || previous.dark !== request.dark || !request.code.startsWith(previous.code);
289
- const pending = timer !== void 0;
290
- epoch++;
291
- mermaidRenderQueue.cancel(owner);
292
- if (replaced) {
293
- svg = "";
294
- bind = void 0;
295
- if (timer !== void 0) clearTimeout(timer);
296
- timer = void 0;
297
- }
298
- previous = request;
299
- if (!request.active) {
300
- cancel();
301
- return;
464
+ const attempt = async (code, dark, streaming, generation) => {
465
+ const stale = () => generation !== epoch;
466
+ let engine;
467
+ try {
468
+ engine = await load();
469
+ } catch {
470
+ if (stale()) return;
471
+ if (loadFailures < LOAD_RETRIES) {
472
+ loadFailures += 1;
473
+ retryTimer = setTimeout(() => {
474
+ retryTimer = void 0;
475
+ nonce++;
476
+ run();
477
+ }, LOAD_RETRY_MS);
478
+ } else if (!loadFailed) {
479
+ loadFailed = true;
480
+ emit();
302
481
  }
303
- publish({
304
- status: "loading",
482
+ return;
483
+ }
484
+ loadFailures = 0;
485
+ let reason = "render";
486
+ try {
487
+ if (stale()) return;
488
+ ensureMermaidInitialized(engine, dark);
489
+ const maxTextSize = getMermaidMaxTextSize(engine);
490
+ if (code.length > maxTextSize) throw new DiagramFailure({
491
+ kind: "error",
492
+ reason: "tooLarge",
493
+ length: code.length,
494
+ limit: maxTextSize
495
+ });
496
+ reason = "parse";
497
+ if (!await engine.parse(code)) throw new DiagramFailure({
498
+ kind: "error",
499
+ reason: "parse",
500
+ message: ""
501
+ });
502
+ reason = "render";
503
+ if (stale()) return;
504
+ const { svg, bindFunctions, diagramType } = await engine.render(diagramId(), code);
505
+ if (stale()) return;
506
+ needsCorrective = false;
507
+ lastSuccess = {
508
+ code,
509
+ dark
510
+ };
511
+ loadFailed = false;
512
+ show({
513
+ kind: "diagram",
305
514
  svg,
306
- bind
515
+ chartType: diagramType ?? "",
516
+ bind: bindFunctions
307
517
  });
308
- const attempt = () => {
309
- timer = void 0;
310
- const current = previous;
311
- const generation = epoch;
312
- const valid = () => generation === epoch;
313
- let phase = "load";
314
- load().then((engine) => {
315
- phase = "render";
316
- if (!valid()) return;
317
- return mermaidRenderQueue.enqueue(owner, async () => {
318
- if (!valid()) return;
319
- ensureMermaidInitialized(engine, current.dark);
320
- if (current.code.length > getMermaidMaxTextSize(engine)) throw new Error("Diagram exceeds Mermaid’s maximum text size");
321
- const parsed = await engine.parse(current.code, { suppressErrors: true });
322
- if (!valid()) return;
323
- if (parsed === false) throw new Error("Diagram syntax is incomplete or invalid");
324
- const result = await engine.render(`aimd-diagram-${++nextId}`, current.code);
325
- if (!valid()) return;
326
- svg = result.svg;
327
- bind = result.bindFunctions;
328
- publish({
329
- status: "ready",
330
- svg,
331
- bind
332
- });
333
- });
334
- }).catch((error) => {
335
- if (valid()) publish({
336
- status: current.streaming && phase === "render" ? "loading" : "error",
337
- svg,
338
- bind,
339
- error: (phase === "load" ? "Diagram engine could not load: " : "") + (error instanceof Error ? error.message : String(error)).slice(0, 500)
340
- });
341
- });
518
+ } catch (error) {
519
+ if (stale()) return;
520
+ if (streaming) return;
521
+ const failure = error instanceof DiagramFailure ? error.view : {
522
+ kind: "error",
523
+ reason,
524
+ message: engineMessage(error)
342
525
  };
343
- if (!request.streaming || request.interval <= 0) {
344
- if (timer !== void 0) clearTimeout(timer);
345
- attempt();
346
- } else if (!pending || replaced) timer = setTimeout(attempt, request.interval);
526
+ if (needsCorrective) {
527
+ needsCorrective = false;
528
+ show(failure);
529
+ return;
530
+ }
531
+ if (view.kind === "diagram") return;
532
+ show(failure);
533
+ }
534
+ };
535
+ function run() {
536
+ if (!request || !current || !shown) return;
537
+ const { dark, active, streaming, interval } = request;
538
+ const code = !streaming || interval === 0 || current.generation !== shown.generation ? request.code : shown.code;
539
+ if (ran && ran.code === code && ran.dark === dark && ran.active === active && ran.streaming === streaming && ran.nonce === nonce) return;
540
+ ran = {
541
+ code,
542
+ dark,
543
+ active,
544
+ streaming,
545
+ nonce
546
+ };
547
+ cancel();
548
+ if (streaming && !previousStreaming) {
549
+ needsCorrective = false;
550
+ lastSuccess = void 0;
551
+ loadFailures = 0;
552
+ if (view.kind !== "source" || loadFailed) {
553
+ loadFailed = false;
554
+ view = { kind: "source" };
555
+ emit();
556
+ }
557
+ } else if (!streaming && previousStreaming) needsCorrective = true;
558
+ previousStreaming = streaming;
559
+ if (!code || !active) return;
560
+ if (lastSuccess?.code === code && lastSuccess.dark === dark) {
561
+ needsCorrective = false;
562
+ return;
563
+ }
564
+ const generation = epoch;
565
+ mermaidRenderQueue.enqueue(owner, () => attempt(code, dark, streaming, generation));
566
+ }
567
+ return {
568
+ update(next) {
569
+ request = next;
570
+ updating = true;
571
+ try {
572
+ if (!frames || !current) {
573
+ current = shown = nextCodeFrame(void 0, next.code, "");
574
+ frames = createCodeFrame(current, (frame) => {
575
+ shown = frame;
576
+ if (!updating) run();
577
+ });
578
+ } else current = nextCodeFrame(current, next.code, "");
579
+ frames.update(current, next.streaming, next.interval);
580
+ } finally {
581
+ updating = false;
582
+ }
583
+ run();
347
584
  },
348
- dispose: cancel
585
+ retry() {
586
+ loadFailures = 0;
587
+ if (loadFailed) {
588
+ loadFailed = false;
589
+ emit();
590
+ }
591
+ nonce++;
592
+ run();
593
+ },
594
+ dispose() {
595
+ cancel();
596
+ frames?.dispose();
597
+ ran = void 0;
598
+ }
349
599
  };
350
600
  }
601
+ function resolveLabels(defaults, option) {
602
+ const overrides = typeof option === "function" ? option() : option;
603
+ if (!overrides) return defaults;
604
+ const labels = { ...defaults };
605
+ for (const key of Object.keys(overrides)) {
606
+ const value = overrides[key];
607
+ if (value !== void 0) labels[key] = value;
608
+ }
609
+ return labels;
610
+ }
611
+ const defaultCodeBlockLabels = Object.freeze({
612
+ actions: "Code block actions",
613
+ plainText: "text",
614
+ copy: "Copy code",
615
+ copied: "Copied",
616
+ copyFailed: "Copy failed",
617
+ showSource: "Show source",
618
+ showPreview: "Show preview",
619
+ expand: "Expand",
620
+ collapse: "Collapse",
621
+ previewFailed: "Preview failed. Source is available below."
622
+ });
623
+ const defaultDiagramLabels = Object.freeze({
624
+ waiting: "Waiting for diagram…",
625
+ rendering: "Rendering diagram…",
626
+ loadFailed: "Diagram engine could not load.",
627
+ retry: "Retry diagram",
628
+ renderFailed: "Mermaid could not render this diagram.",
629
+ tooLarge: (length, limit) => `Diagram source is ${length} characters, above mermaid's maxTextSize of ${limit}.`
630
+ });
631
+ const defaultImageLabels = Object.freeze({
632
+ close: "Close preview",
633
+ prev: "Previous image",
634
+ next: "Next image",
635
+ rotateLeft: "Rotate left",
636
+ rotateRight: "Rotate right",
637
+ zoomIn: "Zoom in",
638
+ zoomOut: "Zoom out",
639
+ flipX: "Flip horizontally",
640
+ flipY: "Flip vertically",
641
+ loading: "Loading image…",
642
+ loadFailed: "Image could not be loaded.",
643
+ open: (alt) => `Preview image${alt ? `: ${alt}` : ""}`,
644
+ preview: "Image preview",
645
+ actions: "Image preview actions"
646
+ });
647
+ const defaultTableLabels = Object.freeze({
648
+ actions: "Table actions",
649
+ copy: "Copy table",
650
+ download: "Download CSV",
651
+ copied: "Table copied",
652
+ copyFailed: "Copy failed",
653
+ scrollRegion: "Scrollable table",
654
+ mergedCells: "Only tables without merged cells can be exported",
655
+ notRectangular: "Only rectangular tables can be exported",
656
+ unsupportedStructure: "Unsupported table structure",
657
+ unsupportedContent: "Unsupported table content",
658
+ unavailable: "Table source is unavailable"
659
+ });
351
660
  const locks = /* @__PURE__ */ new WeakMap();
352
- function lockImagePreviewScroll(target) {
661
+ function lockImagePreviewScroll(target, scrollbarWidth = 0) {
353
662
  let lock = locks.get(target);
354
663
  if (!lock) {
355
664
  lock = {
356
665
  count: 0,
357
- overflow: target.style.overflow
666
+ overflow: target.style.overflow,
667
+ paddingRight: target.style.paddingRight
358
668
  };
359
669
  locks.set(target, lock);
670
+ if (scrollbarWidth > 0) target.style.paddingRight = lock.paddingRight ? `calc(${lock.paddingRight} + ${scrollbarWidth}px)` : `${scrollbarWidth}px`;
360
671
  }
361
672
  lock.count++;
362
673
  target.style.overflow = "hidden";
@@ -366,10 +677,374 @@ function lockImagePreviewScroll(target) {
366
677
  released = true;
367
678
  if (--lock.count === 0) {
368
679
  if (target.style.overflow === "hidden") target.style.overflow = lock.overflow;
680
+ target.style.paddingRight = lock.paddingRight;
369
681
  locks.delete(target);
370
682
  }
371
683
  };
372
684
  }
685
+ const initialImageTransform = () => ({
686
+ x: 0,
687
+ y: 0,
688
+ rotate: 0,
689
+ scale: 1,
690
+ flipX: false,
691
+ flipY: false
692
+ });
693
+ function imageTransformStyle(transform) {
694
+ const { x, y, scale, rotate, flipX, flipY } = transform;
695
+ return `translate3d(${x}px, ${y}px, 0) scale3d(${flipX ? "-" : ""}${scale}, ${flipY ? "-" : ""}${scale}, 1) rotate(${rotate}deg)`;
696
+ }
697
+ function zoomImage(transform, ratio, geometry, centerX = geometry.viewportWidth / 2, centerY = geometry.viewportHeight / 2, touch = false) {
698
+ if (!Number.isFinite(ratio) || ratio <= 0) return transform;
699
+ const scale = Math.min(50, touch ? Math.max(.01, transform.scale * ratio) : Math.max(1, transform.scale * ratio));
700
+ const difference = scale / transform.scale - 1;
701
+ let x = transform.x - difference * (centerX - transform.x - geometry.left - geometry.width / 2);
702
+ let y = transform.y - difference * (centerY - transform.y - geometry.top - geometry.height / 2);
703
+ if (ratio < 1 && scale === 1 && geometry.width <= geometry.viewportWidth && geometry.height <= geometry.viewportHeight) x = y = 0;
704
+ return {
705
+ ...transform,
706
+ x,
707
+ y,
708
+ scale
709
+ };
710
+ }
711
+ function imageWheelRatio(delta, mode = 0, pinch = false) {
712
+ if (!Number.isFinite(delta) || delta === 0) return 1;
713
+ if (pinch) return Math.min(1.5, Math.max(1 / 1.5, Math.exp(-delta / 100)));
714
+ const pixels = mode === 1 ? delta * 100 / 3 : mode === 2 ? delta * 100 : delta;
715
+ const ratio = 1 + Math.min(Math.abs(pixels / 100), 1) * .5;
716
+ return pixels > 0 ? 1 / ratio : ratio;
717
+ }
718
+ function reboundImage(transform, geometry, bounds) {
719
+ if (transform.scale < 1) return {
720
+ ...transform,
721
+ x: 0,
722
+ y: 0,
723
+ scale: 1
724
+ };
725
+ const rotated = transform.rotate % 180 !== 0;
726
+ const width = (rotated ? geometry.height : geometry.width) * transform.scale;
727
+ const height = (rotated ? geometry.width : geometry.height) * transform.scale;
728
+ if (width <= geometry.viewportWidth && height <= geometry.viewportHeight) return {
729
+ ...transform,
730
+ x: 0,
731
+ y: 0
732
+ };
733
+ const fix = (position, start, size, viewport) => {
734
+ const offset = (size - viewport) / 2;
735
+ if (size > viewport) {
736
+ if (start > 0) return offset;
737
+ if (start < 0 && start + size < viewport) return -offset;
738
+ } else if (start < 0 || start + size > viewport) return start < 0 ? offset : -offset;
739
+ return position;
740
+ };
741
+ return {
742
+ ...transform,
743
+ x: fix(transform.x, bounds.left, width, geometry.viewportWidth),
744
+ y: fix(transform.y, bounds.top, height, geometry.viewportHeight)
745
+ };
746
+ }
747
+ function imagePinchCenter(oldA, oldB, a, b) {
748
+ const distanceA = Math.hypot(a.x - oldA.x, a.y - oldA.y), distanceB = Math.hypot(b.x - oldB.x, b.y - oldB.y);
749
+ const ratio = distanceA + distanceB ? distanceA / (distanceA + distanceB) : 0;
750
+ return {
751
+ x: oldA.x + ratio * (oldB.x - oldA.x),
752
+ y: oldA.y + ratio * (oldB.y - oldA.y)
753
+ };
754
+ }
755
+ const ZOOM_STEP = 1.5;
756
+ const MAX_SCALE = 50;
757
+ const CLOSE_MS = 300;
758
+ const closedState = Object.freeze({
759
+ phase: "closed",
760
+ items: Object.freeze([]),
761
+ current: -1,
762
+ item: void 0,
763
+ transform: initialImageTransform(),
764
+ status: "loading",
765
+ moving: false,
766
+ canPrevious: false,
767
+ canNext: false,
768
+ canZoomIn: false,
769
+ canZoomOut: false
770
+ });
771
+ function createImagePreviewController(host, publish, options = {}) {
772
+ let phase = "closed";
773
+ let items = [];
774
+ let selected;
775
+ let current = -1;
776
+ let transform = initialImageTransform();
777
+ let status = "loading";
778
+ let moving = false;
779
+ let unsubscribe;
780
+ let cancelPublish;
781
+ let cancelOpening;
782
+ let closeTimer;
783
+ const pointers = /* @__PURE__ */ new Map();
784
+ let grip;
785
+ let displaced = false;
786
+ const snapshot = () => {
787
+ if (phase === "closed") return closedState;
788
+ const ready = status === "ready";
789
+ return {
790
+ phase,
791
+ items,
792
+ current,
793
+ item: items[current],
794
+ transform,
795
+ status,
796
+ moving,
797
+ canPrevious: current > 0,
798
+ canNext: current < items.length - 1,
799
+ canZoomIn: ready && transform.scale < MAX_SCALE,
800
+ canZoomOut: ready && transform.scale > 1
801
+ };
802
+ };
803
+ const emit = () => {
804
+ cancelPublish?.();
805
+ cancelPublish = void 0;
806
+ publish(snapshot());
807
+ };
808
+ const emitNextFrame = () => {
809
+ cancelPublish ??= host.frame(() => {
810
+ cancelPublish = void 0;
811
+ publish(snapshot());
812
+ });
813
+ };
814
+ const endGesture = () => {
815
+ pointers.clear();
816
+ grip = void 0;
817
+ displaced = false;
818
+ moving = false;
819
+ };
820
+ const startImage = () => {
821
+ transform = initialImageTransform();
822
+ status = "loading";
823
+ endGesture();
824
+ };
825
+ const zoom = (ratio, x, y, touch = false) => {
826
+ const box = host.geometry();
827
+ if (box && status === "ready") transform = zoomImage(transform, ratio, box, x, y, touch);
828
+ };
829
+ const rebound = () => {
830
+ if (transform.scale < 1) {
831
+ transform = {
832
+ ...transform,
833
+ x: 0,
834
+ y: 0,
835
+ scale: 1
836
+ };
837
+ return;
838
+ }
839
+ const box = host.geometry();
840
+ if (!box) return;
841
+ const turned = transform.rotate % 180 !== 0;
842
+ const width = (turned ? box.height : box.width) * transform.scale;
843
+ const height = (turned ? box.width : box.height) * transform.scale;
844
+ transform = reboundImage(transform, box, {
845
+ left: box.left + box.width / 2 + transform.x - width / 2,
846
+ top: box.top + box.height / 2 + transform.y - height / 2
847
+ });
848
+ };
849
+ const finishClosing = () => {
850
+ closeTimer = void 0;
851
+ phase = "closed";
852
+ items = [];
853
+ selected = void 0;
854
+ current = -1;
855
+ emit();
856
+ };
857
+ const close = () => {
858
+ if (phase === "closed" || phase === "closing") return;
859
+ cancelOpening?.();
860
+ cancelOpening = void 0;
861
+ unsubscribe?.();
862
+ unsubscribe = void 0;
863
+ endGesture();
864
+ phase = "closing";
865
+ const duration = typeof options.closeDuration === "function" ? options.closeDuration() : options.closeDuration ?? CLOSE_MS;
866
+ if (duration > 0) {
867
+ emit();
868
+ closeTimer = setTimeout(finishClosing, duration);
869
+ } else finishClosing();
870
+ };
871
+ const go = (delta) => {
872
+ const target = current + delta;
873
+ if (phase === "closed" || phase === "closing" || target < 0 || target >= items.length || target === current) return;
874
+ current = target;
875
+ selected = items[target].id;
876
+ startImage();
877
+ emit();
878
+ };
879
+ const follow = (gallery) => {
880
+ const next = gallery.items();
881
+ const index = next.findIndex((item) => item.id === selected);
882
+ if (index < 0) return close();
883
+ if (next[index].src !== items[current]?.src) startImage();
884
+ items = next;
885
+ current = index;
886
+ emit();
887
+ };
888
+ return {
889
+ state: snapshot,
890
+ open(gallery, id) {
891
+ const list = gallery.items();
892
+ const index = list.findIndex((item) => item.id === id);
893
+ if (index < 0) return false;
894
+ if (closeTimer !== void 0) clearTimeout(closeTimer);
895
+ closeTimer = void 0;
896
+ unsubscribe?.();
897
+ cancelOpening?.();
898
+ items = list;
899
+ current = index;
900
+ selected = id;
901
+ startImage();
902
+ unsubscribe = gallery.subscribe(() => follow(gallery));
903
+ if (phase === "open") {
904
+ emit();
905
+ return true;
906
+ }
907
+ cancelOpening = void 0;
908
+ phase = "opening";
909
+ emit();
910
+ cancelOpening = host.frame(() => {
911
+ cancelOpening = host.frame(() => {
912
+ cancelOpening = void 0;
913
+ phase = "open";
914
+ emit();
915
+ });
916
+ });
917
+ return true;
918
+ },
919
+ close,
920
+ go,
921
+ action(name) {
922
+ if (phase === "closed" || phase === "closing") return;
923
+ if (name === "close") return close();
924
+ if (name === "prev") return go(-1);
925
+ if (name === "next") return go(1);
926
+ if (status !== "ready") return;
927
+ if (name === "zoomIn") zoom(ZOOM_STEP);
928
+ else if (name === "zoomOut") zoom(1 / ZOOM_STEP);
929
+ else if (name === "rotateLeft") transform = {
930
+ ...transform,
931
+ rotate: transform.rotate - 90
932
+ };
933
+ else if (name === "rotateRight") transform = {
934
+ ...transform,
935
+ rotate: transform.rotate + 90
936
+ };
937
+ else if (name === "flipX") transform = {
938
+ ...transform,
939
+ flipX: !transform.flipX
940
+ };
941
+ else if (name === "flipY") transform = {
942
+ ...transform,
943
+ flipY: !transform.flipY
944
+ };
945
+ emit();
946
+ },
947
+ loaded() {
948
+ if (phase === "closed" || status === "ready") return;
949
+ status = "ready";
950
+ emit();
951
+ },
952
+ failed() {
953
+ if (phase === "closed" || status === "error") return;
954
+ status = "error";
955
+ endGesture();
956
+ emit();
957
+ },
958
+ pointerDown(pointer) {
959
+ if (phase !== "open" && phase !== "opening" || status !== "ready") return false;
960
+ if (pointer.button !== void 0 && pointer.button !== 0) return false;
961
+ if (pointers.size >= 2 && !pointers.has(pointer.id)) return false;
962
+ pointers.set(pointer.id, {
963
+ x: pointer.x,
964
+ y: pointer.y
965
+ });
966
+ grip = pointers.size === 1 ? {
967
+ x: pointer.x - transform.x,
968
+ y: pointer.y - transform.y
969
+ } : void 0;
970
+ moving = true;
971
+ emit();
972
+ return true;
973
+ },
974
+ pointerMove(pointer) {
975
+ const previous = pointers.get(pointer.id);
976
+ if (!previous || previous.x === pointer.x && previous.y === pointer.y) return;
977
+ const before = [...pointers.values()];
978
+ pointers.set(pointer.id, {
979
+ x: pointer.x,
980
+ y: pointer.y
981
+ });
982
+ if (pointers.size === 2) {
983
+ const [a, b] = [...pointers.values()];
984
+ const span = Math.hypot(before[0].x - before[1].x, before[0].y - before[1].y);
985
+ const center = imagePinchCenter(before[0], before[1], a, b);
986
+ if (span > 0) zoom(Math.hypot(a.x - b.x, a.y - b.y) / span, center.x, center.y, true);
987
+ } else if (grip) transform = {
988
+ ...transform,
989
+ x: pointer.x - grip.x,
990
+ y: pointer.y - grip.y
991
+ };
992
+ displaced = true;
993
+ emitNextFrame();
994
+ },
995
+ pointerUp(id) {
996
+ if (!pointers.delete(id)) return;
997
+ const [remaining] = pointers.values();
998
+ if (remaining) {
999
+ grip = {
1000
+ x: remaining.x - transform.x,
1001
+ y: remaining.y - transform.y
1002
+ };
1003
+ return;
1004
+ }
1005
+ const settle = displaced;
1006
+ endGesture();
1007
+ if (settle) rebound();
1008
+ emit();
1009
+ },
1010
+ wheel(input) {
1011
+ if (phase !== "open" || status !== "ready" || !input.deltaY) return false;
1012
+ zoom(imageWheelRatio(input.deltaY, input.mode, input.pinch), input.x, input.y);
1013
+ emitNextFrame();
1014
+ return true;
1015
+ },
1016
+ doubleClick(x, y) {
1017
+ if (phase !== "open" || status !== "ready") return;
1018
+ if (transform.scale !== 1) transform = {
1019
+ ...transform,
1020
+ x: 0,
1021
+ y: 0,
1022
+ scale: 1
1023
+ };
1024
+ else zoom(ZOOM_STEP, x, y);
1025
+ emit();
1026
+ },
1027
+ key(key) {
1028
+ if (phase !== "open" && phase !== "opening" || items.length < 2) return false;
1029
+ if (key !== "ArrowLeft" && key !== "ArrowRight") return false;
1030
+ go(key === "ArrowLeft" ? -1 : 1);
1031
+ return true;
1032
+ },
1033
+ dispose() {
1034
+ if (closeTimer !== void 0) clearTimeout(closeTimer);
1035
+ closeTimer = void 0;
1036
+ cancelPublish?.();
1037
+ cancelOpening?.();
1038
+ unsubscribe?.();
1039
+ cancelPublish = cancelOpening = unsubscribe = void 0;
1040
+ endGesture();
1041
+ phase = "closed";
1042
+ items = [];
1043
+ selected = void 0;
1044
+ current = -1;
1045
+ }
1046
+ };
1047
+ }
373
1048
  const imageIconPaths = {
374
1049
  rotateLeft: ["M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8", "M3 3v5h5"],
375
1050
  rotateRight: ["M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8", "M21 3v5h-5"],
@@ -416,16 +1091,20 @@ const imageIconPaths = {
416
1091
  "M11 9a2 2 0 1 1-4 0 2 2 0 0 1 4 0"
417
1092
  ]
418
1093
  };
419
- const imageGroupSelector = "[data-aimd-image-scope], .aim-typography-root, .aimd-vue, .mantine-Typography-root";
420
1094
  const galleries = /* @__PURE__ */ new WeakMap();
421
1095
  function getImageGallery(scope) {
422
1096
  const existing = galleries.get(scope);
423
1097
  if (existing) return existing;
424
1098
  const entries = /* @__PURE__ */ new Map();
425
1099
  const listeners = /* @__PURE__ */ new Set();
426
- const notify = () => listeners.forEach((listener) => listener());
1100
+ let sorted;
1101
+ const notify = () => {
1102
+ sorted = void 0;
1103
+ listeners.forEach((listener) => listener());
1104
+ };
427
1105
  const gallery = {
428
- items: () => [...entries.values()].sort((a, b) => a.precedes(b.id) ? -1 : b.precedes(a.id) ? 1 : 0),
1106
+ items: () => sorted ??= [...entries.values()].sort((a, b) => a.precedes(b.id) ? -1 : b.precedes(a.id) ? 1 : 0),
1107
+ size: () => entries.size,
429
1108
  subscribe(listener) {
430
1109
  listeners.add(listener);
431
1110
  return () => {
@@ -445,84 +1124,17 @@ function getImageGallery(scope) {
445
1124
  galleries.set(scope, gallery);
446
1125
  return gallery;
447
1126
  }
448
- const imageActionLabels = {
449
- close: "Close preview",
450
- prev: "Previous image",
451
- next: "Next image",
452
- rotateLeft: "Rotate left",
453
- rotateRight: "Rotate right",
454
- zoomIn: "Zoom in",
455
- zoomOut: "Zoom out",
456
- flipX: "Flip horizontally",
457
- flipY: "Flip vertically"
458
- };
459
- const initialImageTransform = () => ({
460
- x: 0,
461
- y: 0,
462
- rotate: 0,
463
- scale: 1,
464
- flipX: false,
465
- flipY: false
466
- });
467
- function imageTransformStyle(transform) {
468
- const { x, y, scale, rotate, flipX, flipY } = transform;
469
- return `translate3d(${x}px, ${y}px, 0) scale3d(${flipX ? "-" : ""}${scale}, ${flipY ? "-" : ""}${scale}, 1) rotate(${rotate}deg)`;
470
- }
471
- function zoomImage(transform, ratio, geometry, centerX = geometry.viewportWidth / 2, centerY = geometry.viewportHeight / 2, touch = false) {
472
- if (!Number.isFinite(ratio) || ratio <= 0) return transform;
473
- const scale = Math.min(50, touch ? Math.max(.01, transform.scale * ratio) : Math.max(1, transform.scale * ratio));
474
- const difference = scale / transform.scale - 1;
475
- let x = transform.x - difference * (centerX - transform.x - geometry.left - geometry.width / 2);
476
- let y = transform.y - difference * (centerY - transform.y - geometry.top - geometry.height / 2);
477
- if (ratio < 1 && scale === 1 && geometry.width <= geometry.viewportWidth && geometry.height <= geometry.viewportHeight) x = y = 0;
478
- return {
479
- ...transform,
480
- x,
481
- y,
482
- scale
483
- };
484
- }
485
- function imageWheelRatio(delta) {
486
- const ratio = 1 + Math.min(Math.abs(delta / 100), 1) * .5;
487
- return delta > 0 ? 1 / ratio : ratio;
488
- }
489
- function reboundImage(transform, geometry, bounds) {
490
- if (transform.scale < 1) return {
491
- ...transform,
492
- x: 0,
493
- y: 0,
494
- scale: 1
495
- };
496
- const rotated = transform.rotate % 180 !== 0;
497
- const width = (rotated ? geometry.height : geometry.width) * transform.scale;
498
- const height = (rotated ? geometry.width : geometry.height) * transform.scale;
499
- if (width <= geometry.viewportWidth && height <= geometry.viewportHeight) return {
500
- ...transform,
501
- x: 0,
502
- y: 0
503
- };
504
- const fix = (position, start, size, viewport) => {
505
- const offset = (size - viewport) / 2;
506
- if (size > viewport) {
507
- if (start > 0) return offset;
508
- if (start < 0 && start + size < viewport) return -offset;
509
- } else if (start < 0 || start + size > viewport) return start < 0 ? offset : -offset;
510
- return position;
511
- };
512
- return {
513
- ...transform,
514
- x: fix(transform.x, bounds.left, width, geometry.viewportWidth),
515
- y: fix(transform.y, bounds.top, height, geometry.viewportHeight)
516
- };
517
- }
518
- function imagePinchCenter(oldA, oldB, a, b) {
519
- const distanceA = Math.hypot(a.x - oldA.x, a.y - oldA.y), distanceB = Math.hypot(b.x - oldB.x, b.y - oldB.y);
520
- const ratio = distanceA + distanceB ? distanceA / (distanceA + distanceB) : 0;
521
- return {
522
- x: oldA.x + ratio * (oldB.x - oldA.x),
523
- y: oldA.y + ratio * (oldB.y - oldA.y)
524
- };
525
- }
526
- export { MERMAID_DEFAULT_MAX_TEXT_SIZE, createCodeFrame, createDiagramController, createJsonCompletenessScanner, createRenderQueue, ensureMermaidInitialized, extractCode, getImageGallery, getMermaidMaxTextSize, imageActionLabels, imageGroupSelector, imageIconPaths, imagePinchCenter, imageTransformStyle, imageWheelRatio, initialImageTransform, jsonLooksComplete, lockImagePreviewScroll, mermaidRenderQueue, normalizeRenderers, prettyPrintJson, projectTable, reboundImage, resetMermaidInitializationForTests, serializeTable, zoomImage };
1127
+ const imageActionNames = Object.freeze([
1128
+ "close",
1129
+ "prev",
1130
+ "next",
1131
+ "rotateLeft",
1132
+ "rotateRight",
1133
+ "zoomIn",
1134
+ "zoomOut",
1135
+ "flipX",
1136
+ "flipY"
1137
+ ]);
1138
+ export { createCodeBlockSession, createCodeFrame, createCodeLanguageTracker, createDiagramController, createImagePreviewController, createJsonCompletenessScanner, defaultCodeBlockLabels, defaultDiagramLabels, defaultImageLabels, defaultTableLabels, diagramErrorText, ensureMermaidInitialized, extendsCode, extractCode, getImageGallery, getMermaidMaxTextSize, imageActionNames, imageIconPaths, imagePinchCenter, imageTransformStyle, imageWheelRatio, initialImageTransform, jsonLooksComplete, lockImagePreviewScroll, mermaidRenderQueue, nextCodeFrame, normalizeRenderers, prettyPrintJson, projectTable, reboundImage, resolveLabels, serializeTable, zoomImage };
527
1139
 
528
1140
  //# sourceMappingURL=index.js.map