@duskmoon-dev/el-markdown-input 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -13,12 +13,180 @@ var __export = (target, all) => {
13
13
  });
14
14
  };
15
15
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
17
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
18
+ }) : x)(function(x) {
19
+ if (typeof require !== "undefined")
20
+ return require.apply(this, arguments);
21
+ throw Error('Dynamic require of "' + x + '" is not supported');
22
+ });
23
+
24
+ // src/sanitize-schema.ts
25
+ import { defaultSchema } from "rehype-sanitize";
26
+ function deepMergeSchemas(base, extension) {
27
+ const result = { ...base };
28
+ if (extension.attributes) {
29
+ result.attributes = { ...base.attributes };
30
+ for (const [tag, attrs] of Object.entries(extension.attributes)) {
31
+ const existing = result.attributes[tag];
32
+ if (Array.isArray(existing)) {
33
+ result.attributes[tag] = [...existing, ...attrs];
34
+ } else {
35
+ result.attributes[tag] = attrs;
36
+ }
37
+ }
38
+ }
39
+ if (extension.tagNames) {
40
+ result.tagNames = [...base.tagNames ?? [], ...extension.tagNames];
41
+ }
42
+ return result;
43
+ }
44
+ var sanitizeSchema;
45
+ var init_sanitize_schema = __esm(() => {
46
+ sanitizeSchema = deepMergeSchemas(defaultSchema, {
47
+ attributes: {
48
+ span: ["className", "style"],
49
+ code: ["className"],
50
+ input: ["type", "checked", "disabled"],
51
+ math: ["xmlns", "display"],
52
+ annotation: ["encoding"],
53
+ mi: ["mathvariant"],
54
+ mo: ["stretchy", "lspace", "rspace"],
55
+ mpadded: ["height", "depth", "width", "lspace", "voffset"],
56
+ mspace: ["height", "depth", "width"],
57
+ mstyle: ["mathsize", "mathcolor", "mathbackground", "displaystyle"]
58
+ },
59
+ tagNames: [
60
+ "math",
61
+ "semantics",
62
+ "mrow",
63
+ "mi",
64
+ "mo",
65
+ "mn",
66
+ "msup",
67
+ "msub",
68
+ "mfrac",
69
+ "mover",
70
+ "munder",
71
+ "msqrt",
72
+ "mtable",
73
+ "mtr",
74
+ "mtd",
75
+ "annotation",
76
+ "mtext",
77
+ "mpadded",
78
+ "mspace",
79
+ "merror",
80
+ "mstyle",
81
+ "ms",
82
+ "mphantom",
83
+ "mmultiscripts",
84
+ "mprescripts"
85
+ ]
86
+ });
87
+ });
88
+
89
+ // src/render.ts
90
+ var exports_render = {};
91
+ __export(exports_render, {
92
+ renderMermaidBlocks: () => renderMermaidBlocks,
93
+ renderMarkdown: () => renderMarkdown
94
+ });
95
+ async function buildProcessor() {
96
+ const [
97
+ { unified },
98
+ { default: remarkParse },
99
+ { default: remarkGfm },
100
+ { default: remarkMath },
101
+ { default: remarkRehype },
102
+ { default: rehypeKatex },
103
+ { default: rehypePrismPlus },
104
+ { default: rehypeSanitize },
105
+ { default: rehypeStringify }
106
+ ] = await Promise.all([
107
+ import("unified"),
108
+ import("remark-parse"),
109
+ import("remark-gfm"),
110
+ import("remark-math"),
111
+ import("remark-rehype"),
112
+ import("rehype-katex"),
113
+ import("rehype-prism-plus"),
114
+ import("rehype-sanitize"),
115
+ import("rehype-stringify")
116
+ ]);
117
+ return unified().use(remarkParse).use(remarkGfm).use(remarkMath).use(remarkRehype, { allowDangerousHtml: false }).use(rehypeKatex).use(rehypePrismPlus, { ignoreMissing: true }).use(rehypeSanitize, sanitizeSchema).use(rehypeStringify);
118
+ }
119
+ function getProcessor() {
120
+ if (!processorPromise) {
121
+ processorPromise = buildProcessor().catch((err) => {
122
+ processorPromise = null;
123
+ throw err;
124
+ });
125
+ }
126
+ return processorPromise;
127
+ }
128
+ async function renderMarkdown(source) {
129
+ const proc = await getProcessor();
130
+ const file = await proc.process(source);
131
+ return String(file);
132
+ }
133
+ function getCurrentTheme() {
134
+ if (typeof document === "undefined")
135
+ return "default";
136
+ return document.documentElement.getAttribute("data-theme") ?? "default";
137
+ }
138
+ async function renderMermaidBlocks(container, mermaidSrc) {
139
+ const blocks = container.querySelectorAll("pre > code.language-mermaid");
140
+ if (blocks.length === 0)
141
+ return;
142
+ if (mermaidSrc !== undefined && !/^https:\/\//i.test(mermaidSrc)) {
143
+ console.warn(`[el-dm-markdown-input] mermaid-src "${mermaidSrc}" rejected — only https: URLs are allowed. Falling back to bundled mermaid.`);
144
+ mermaidSrc = undefined;
145
+ }
146
+ let mermaidModule;
147
+ try {
148
+ mermaidModule = mermaidSrc ? await import(mermaidSrc) : await import("mermaid");
149
+ } catch (err) {
150
+ console.error("[el-dm-markdown-input] Failed to load mermaid: %o", err);
151
+ blocks.forEach((block) => block.parentElement?.classList.add("mermaid-error"));
152
+ return;
153
+ }
154
+ const mermaid = mermaidModule.default ?? mermaidModule;
155
+ mermaid.initialize({
156
+ startOnLoad: false,
157
+ theme: getCurrentTheme() === "moonlight" ? "dark" : "default",
158
+ fontFamily: "inherit"
159
+ });
160
+ for (const [i, block] of [...blocks].entries()) {
161
+ const pre = block.parentElement;
162
+ if (!pre)
163
+ continue;
164
+ const id = `mermaid-${++mermaidIdCounter}-${i}`;
165
+ try {
166
+ const { svg } = await mermaid.render(id, block.textContent ?? "");
167
+ const wrapper = document.createElement("div");
168
+ wrapper.className = "mermaid-diagram";
169
+ wrapper.innerHTML = svg;
170
+ pre.replaceWith(wrapper);
171
+ } catch (err) {
172
+ console.error(`[el-dm-markdown-input] mermaid.render failed for block %s: %o
173
+ Source: %s`, id, err, block.textContent?.slice(0, 200));
174
+ pre.classList.add("mermaid-error");
175
+ }
176
+ }
177
+ }
178
+ var processorPromise = null, mermaidIdCounter = 0;
179
+ var init_render = __esm(() => {
180
+ init_sanitize_schema();
181
+ });
182
+
183
+ // src/element.ts
184
+ import { BaseElement } from "@duskmoon-dev/el-base";
185
+ import { css as markdownBodyCSS } from "@duskmoon-dev/core/components/markdown-body";
16
186
 
17
187
  // src/css.ts
18
188
  import { css } from "@duskmoon-dev/el-base";
19
- var elementStyles;
20
- var init_css = __esm(() => {
21
- elementStyles = css`
189
+ var elementStyles = css`
22
190
  /* ── Custom property defaults with design-system fallbacks ─────────── */
23
191
  :host {
24
192
  --md-border: var(--color-outline, #d0d7de);
@@ -67,6 +235,7 @@ var init_css = __esm(() => {
67
235
  background: var(--md-bg);
68
236
  color: var(--md-text);
69
237
  overflow: hidden;
238
+ height: inherit;
70
239
  }
71
240
 
72
241
  .editor:focus-within {
@@ -116,74 +285,62 @@ var init_css = __esm(() => {
116
285
  border-radius: 3px;
117
286
  }
118
287
 
119
- /* ── Write area (backdrop + textarea overlay) ───────────────────────── */
288
+ /* ── Write area (render-layer + textarea overlay) ──────────────────── */
289
+ /*
290
+ * CodeMirror-style render model: .render-layer sits in normal flow and
291
+ * drives the container height; the textarea is absolutely positioned on
292
+ * top. No scroll sync required — both layers always share the same size.
293
+ */
120
294
  .write-area {
121
295
  position: relative;
122
296
  min-height: 12rem;
123
- flex: 1;
297
+ flex: 1 1 auto;
298
+ }
299
+
300
+ .write-area[hidden] {
301
+ display: none;
124
302
  }
125
303
 
126
304
  /*
127
- * Backdrop: renders syntax-highlighted HTML behind the transparent textarea.
128
- * Must share IDENTICAL font metrics with the textarea to stay pixel-aligned.
305
+ * Render layer: highlighted HTML in normal flow. Drives container height.
306
+ * pointer-events: none lets clicks pass through to the textarea underneath.
307
+ * Font metrics MUST match the textarea exactly for pixel-aligned overlay.
129
308
  */
130
- .backdrop {
131
- position: absolute;
132
- inset: 0;
309
+ .render-layer {
310
+ position: relative;
311
+ z-index: 1;
133
312
  pointer-events: none;
134
- /*
135
- * Use overflow: auto (not overflow: hidden) so the backdrop reserves
136
- * the same scrollbar gutter as the textarea when content overflows.
137
- * Without this, the textarea scrollbar narrows its text area but the
138
- * backdrop stays full-width — lines wrap at different points — causing
139
- * the cursor to appear misaligned with the highlighted text.
140
- */
141
- overflow: auto;
142
- scrollbar-width: none; /* Firefox */
143
- border: none;
144
- background: transparent;
145
- /*
146
- * Do NOT put white-space: pre-wrap here. The backdrop div contains a
147
- * backdrop-content child, and the HTML template has whitespace text
148
- * nodes (newline + indent) between them. With pre-wrap on the parent
149
- * those text nodes render as a visible leading newline, shifting all
150
- * content down by one line and misaligning the cursor vertically.
151
- * pre-wrap lives on .backdrop-content instead.
152
- */
153
-
313
+ min-height: 12rem;
154
314
  font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
155
315
  font-size: 0.875rem;
156
316
  line-height: 1.6;
157
317
  padding: 0.75rem;
158
- color: var(--md-text);
159
- }
160
-
161
- .backdrop::-webkit-scrollbar {
162
- display: none; /* Chrome / Safari */
163
- }
164
-
165
- .backdrop-content {
166
- display: block;
167
318
  white-space: pre-wrap;
168
319
  word-wrap: break-word;
169
320
  overflow-wrap: break-word;
170
- /* Prism token colours are injected via a separate <style id="prism-theme"> */
321
+ color: var(--md-text);
171
322
  }
172
323
 
324
+ /*
325
+ * Textarea: absolute overlay on top of the render layer. Transparent text
326
+ * lets highlighted content show through; caret-color keeps cursor visible.
327
+ * overflow: hidden — the render layer drives height, not the textarea.
328
+ */
173
329
  textarea {
174
- position: relative;
330
+ position: absolute;
331
+ inset: 0;
332
+ z-index: 2;
175
333
  display: block;
176
334
  width: 100%;
177
- min-height: 12rem;
335
+ height: 100%;
178
336
  border: none;
179
337
  outline: none;
180
- resize: vertical;
338
+ resize: none;
181
339
  background: transparent;
182
340
  color: transparent;
183
341
  caret-color: var(--md-text);
184
342
  box-sizing: border-box;
185
-
186
- /* MUST match .backdrop exactly */
343
+ overflow: hidden;
187
344
  font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
188
345
  font-size: 0.875rem;
189
346
  line-height: 1.6;
@@ -206,11 +363,100 @@ var init_css = __esm(() => {
206
363
  .preview-body {
207
364
  padding: 0.75rem;
208
365
  min-height: 12rem;
366
+ height: stretch;
367
+ flex: 1 1 auto;
368
+ display: flex;
369
+ flex-direction: column;
209
370
  overflow-y: auto;
210
371
  color: var(--md-text);
211
372
  /* .markdown-body styles come from @duskmoon-dev/core via the element */
212
373
  }
213
374
 
375
+ .preview-body[hidden] {
376
+ display: none;
377
+ }
378
+
379
+ /* ── Preview skeleton (shown while render pipeline loads) ──────────── */
380
+ .preview-skeleton {
381
+ display: flex;
382
+ flex-direction: column;
383
+ gap: 0.75rem;
384
+ padding: 0.5rem 0;
385
+ }
386
+
387
+ .skeleton-line {
388
+ height: 0.875rem;
389
+ background: linear-gradient(
390
+ 90deg,
391
+ var(--md-bg-toolbar) 25%,
392
+ var(--md-bg-hover) 50%,
393
+ var(--md-bg-toolbar) 75%
394
+ );
395
+ background-size: 200% 100%;
396
+ border-radius: 4px;
397
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
398
+ }
399
+
400
+ @keyframes skeleton-shimmer {
401
+ 0% {
402
+ background-position: 200% 0;
403
+ }
404
+ 100% {
405
+ background-position: -200% 0;
406
+ }
407
+ }
408
+
409
+ @media (prefers-reduced-motion: reduce) {
410
+ .skeleton-line {
411
+ animation: none;
412
+ background: var(--md-bg-hover);
413
+ }
414
+ }
415
+
416
+ /* ── Mermaid diagram blocks ────────────────────────────────────────── */
417
+ .mermaid-diagram {
418
+ display: flex;
419
+ justify-content: center;
420
+ margin: 1rem 0;
421
+ overflow-x: auto;
422
+ }
423
+
424
+ .mermaid-error {
425
+ border-left: 3px solid var(--md-color-error);
426
+ opacity: 0.7;
427
+ position: relative;
428
+ }
429
+
430
+ .mermaid-error::before {
431
+ content: 'Mermaid render failed';
432
+ display: block;
433
+ font-size: 0.75rem;
434
+ color: var(--md-color-error);
435
+ font-family: inherit;
436
+ margin-bottom: 0.25rem;
437
+ padding-left: 0.5rem;
438
+ }
439
+
440
+ /* ── Render error fallback ──────────────────────────────────────────── */
441
+ .render-error-fallback {
442
+ white-space: pre-wrap;
443
+ word-wrap: break-word;
444
+ font-size: 0.875rem;
445
+ opacity: 0.8;
446
+ border-left: 3px solid var(--md-color-error);
447
+ padding-left: 0.75rem;
448
+ color: var(--md-text-muted);
449
+ }
450
+
451
+ .render-error-fallback::before {
452
+ content: 'Preview render failed — showing raw markdown';
453
+ display: block;
454
+ font-size: 0.75rem;
455
+ color: var(--md-color-error);
456
+ font-family: inherit;
457
+ margin-bottom: 0.5rem;
458
+ }
459
+
214
460
  /* ── Status bar ─────────────────────────────────────────────────────── */
215
461
  .status-bar {
216
462
  display: flex;
@@ -246,6 +492,12 @@ var init_css = __esm(() => {
246
492
  background: var(--md-bg-hover);
247
493
  }
248
494
 
495
+ .attach-btn:disabled {
496
+ cursor: not-allowed;
497
+ opacity: 0.5;
498
+ pointer-events: none;
499
+ }
500
+
249
501
  .attach-btn:focus-visible {
250
502
  outline: 2px solid var(--md-accent);
251
503
  outline-offset: 1px;
@@ -380,14 +632,38 @@ var init_css = __esm(() => {
380
632
  text-overflow: ellipsis;
381
633
  white-space: nowrap;
382
634
  }
635
+
636
+ /* ── Reduced motion: disable all transitions and animations ──────── */
637
+ @media (prefers-reduced-motion: reduce) {
638
+ .tab-btn,
639
+ .attach-btn,
640
+ .ac-item,
641
+ .upload-bar {
642
+ transition: none;
643
+ }
644
+ }
383
645
  `;
384
- });
385
646
 
386
647
  // src/highlight.ts
648
+ var PRISM_BASE = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0";
649
+ var PRISM_CORE_URL = `${PRISM_BASE}/prism.min.js`;
650
+ var PRISM_AUTOLOADER_URL = `${PRISM_BASE}/plugins/autoloader/prism-autoloader.min.js`;
651
+ var PRISM_SRI = {
652
+ [PRISM_CORE_URL]: "sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==",
653
+ [PRISM_AUTOLOADER_URL]: "sha512-SkmBfuA2hqjzEVpmnMt/LINrjop3GKWqsuLSSB3e7iBmYK7JuWw4ldmmxwD9mdm2IRTTi0OxSAfEGvgEi0i2Kw=="
654
+ };
655
+ var PRISM_THEME_DARK_URL = `${PRISM_BASE}/themes/prism-tomorrow.min.css`;
656
+ var PRISM_THEME_LIGHT_URL = `${PRISM_BASE}/themes/prism-coy.min.css`;
657
+ var _prismReady = null;
387
658
  function _loadScript(src) {
388
659
  return new Promise((resolve) => {
389
660
  const script = document.createElement("script");
390
661
  script.src = src;
662
+ const integrity = PRISM_SRI[src];
663
+ if (integrity) {
664
+ script.integrity = integrity;
665
+ script.crossOrigin = "anonymous";
666
+ }
391
667
  script.onload = () => resolve();
392
668
  script.onerror = () => resolve();
393
669
  document.head.appendChild(script);
@@ -399,19 +675,22 @@ function ensurePrism() {
399
675
  if (_prismReady)
400
676
  return _prismReady;
401
677
  _prismReady = _loadScript(PRISM_CORE_URL).then(() => {
402
- if (!window.Prism)
678
+ if (!window.Prism) {
679
+ _prismReady = null;
403
680
  return;
681
+ }
404
682
  window.Prism.manual = true;
405
683
  return _loadScript(PRISM_AUTOLOADER_URL).then(() => {
406
684
  if (window.Prism?.plugins?.autoloader) {
407
685
  window.Prism.plugins.autoloader.languages_path = `${PRISM_BASE}/components/`;
408
686
  }
687
+ return _loadScript(`${PRISM_BASE}/components/prism-markdown.min.js`);
409
688
  });
410
689
  });
411
690
  return _prismReady;
412
691
  }
413
692
  function escapeHtml(text) {
414
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
693
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
415
694
  }
416
695
  function highlightMarkdown(text) {
417
696
  const escaped = escapeHtml(text);
@@ -433,20 +712,35 @@ function applyPrismTheme(shadowRoot, dark) {
433
712
  styleEl.id = "prism-theme";
434
713
  shadowRoot.appendChild(styleEl);
435
714
  }
436
- const expected = `@import url("${themeUrl}");`;
715
+ const previewOverrides = `
716
+ .preview-body pre[class*="language-"] {
717
+ background: transparent;
718
+ margin: 0;
719
+ padding: 0;
720
+ overflow: visible;
721
+ position: static;
722
+ }
723
+ .preview-body pre[class*="language-"] > code {
724
+ background: transparent;
725
+ border: none;
726
+ box-shadow: none;
727
+ padding: 0;
728
+ background-image: none;
729
+ display: block;
730
+ overflow: auto;
731
+ max-height: none;
732
+ height: auto;
733
+ }`;
734
+ const expected = `@import url("${themeUrl}");${previewOverrides}`;
437
735
  if (styleEl.textContent !== expected) {
438
736
  styleEl.textContent = expected;
439
737
  }
440
738
  }
441
- var PRISM_BASE = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0", PRISM_CORE_URL, PRISM_AUTOLOADER_URL, PRISM_THEME_DARK_URL, PRISM_THEME_LIGHT_URL, _prismReady = null;
442
- var init_highlight = __esm(() => {
443
- PRISM_CORE_URL = `${PRISM_BASE}/prism.min.js`;
444
- PRISM_AUTOLOADER_URL = `${PRISM_BASE}/plugins/autoloader/prism-autoloader.min.js`;
445
- PRISM_THEME_DARK_URL = `${PRISM_BASE}/themes/prism-tomorrow.min.css`;
446
- PRISM_THEME_LIGHT_URL = `${PRISM_BASE}/themes/prism-coy.min.css`;
447
- });
448
739
 
449
740
  // src/upload.ts
741
+ var ACCEPTED_MIME_PREFIXES = ["image/"];
742
+ var ACCEPTED_MIME_EXACT = ["application/pdf"];
743
+ var ACCEPTED_EXTENSIONS = [".zip", ".txt", ".csv", ".json", ".md"];
450
744
  function isAcceptedType(file) {
451
745
  const type = file.type.toLowerCase();
452
746
  if (ACCEPTED_MIME_PREFIXES.some((p) => type.startsWith(p)))
@@ -459,12 +753,19 @@ function isAcceptedType(file) {
459
753
  return false;
460
754
  }
461
755
  function fileToMarkdown(file, url) {
756
+ const isSafeUrl = /^https:\/\//i.test(url) || /^\//.test(url) || /^\.\.?\//.test(url);
757
+ const safeUrl = isSafeUrl ? url.replace(/\(/g, "%28").replace(/\)/g, "%29") : "#unsafe-url";
758
+ const safeName = file.name.replace(/[[\]]/g, "\\$&");
462
759
  if (file.type.startsWith("image/")) {
463
- return `![${file.name}](${url})`;
760
+ return `![${safeName}](${safeUrl})`;
464
761
  }
465
- return `[${file.name}](${url})`;
762
+ return `[${safeName}](${safeUrl})`;
466
763
  }
467
764
  function uploadFile(file, uploadUrl, onProgress) {
765
+ const isSafeUploadUrl = /^https:\/\//i.test(uploadUrl) || /^\//.test(uploadUrl) || /^\.\.?\//.test(uploadUrl);
766
+ if (!isSafeUploadUrl) {
767
+ return Promise.reject(new Error(`[el-dm-markdown-input] upload-url "${uploadUrl}" rejected — only https: and relative URLs are allowed.`));
768
+ }
468
769
  return new Promise((resolve, reject) => {
469
770
  const xhr = new XMLHttpRequest;
470
771
  const body = new FormData;
@@ -481,27 +782,21 @@ function uploadFile(file, uploadUrl, onProgress) {
481
782
  if (data.url) {
482
783
  resolve(data.url);
483
784
  } else {
484
- reject("Upload response missing url field");
785
+ reject(new Error("Upload response missing url field"));
485
786
  }
486
787
  } catch {
487
- reject("Upload response is not valid JSON");
788
+ reject(new Error("Upload response is not valid JSON"));
488
789
  }
489
790
  } else {
490
- reject(`Upload failed with status ${xhr.status}`);
791
+ reject(new Error(`Upload failed with status ${xhr.status}`));
491
792
  }
492
793
  });
493
- xhr.addEventListener("error", () => reject("Network error during upload"));
494
- xhr.addEventListener("abort", () => reject("Upload aborted"));
794
+ xhr.addEventListener("error", () => reject(new Error("Network error during upload")));
795
+ xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
495
796
  xhr.open("POST", uploadUrl);
496
797
  xhr.send(body);
497
798
  });
498
799
  }
499
- var ACCEPTED_MIME_PREFIXES, ACCEPTED_MIME_EXACT, ACCEPTED_EXTENSIONS;
500
- var init_upload = __esm(() => {
501
- ACCEPTED_MIME_PREFIXES = ["image/"];
502
- ACCEPTED_MIME_EXACT = ["application/pdf"];
503
- ACCEPTED_EXTENSIONS = [".zip", ".txt", ".csv", ".json", ".md"];
504
- });
505
800
 
506
801
  // src/autocomplete.ts
507
802
  function detectTrigger(value, cursorPos) {
@@ -512,13 +807,13 @@ function detectTrigger(value, cursorPos) {
512
807
  const query = value.slice(i + 1, cursorPos);
513
808
  if (!/\s/.test(query)) {
514
809
  const before = i > 0 ? value[i - 1] : null;
515
- if (before === null || /[\s\n]/.test(before)) {
810
+ if (before === null || /\s/.test(before)) {
516
811
  return { trigger: ch, query, triggerPos: i };
517
812
  }
518
813
  }
519
814
  return null;
520
815
  }
521
- if (/[\s\n]/.test(ch)) {
816
+ if (/\s/.test(ch)) {
522
817
  return null;
523
818
  }
524
819
  i--;
@@ -529,8 +824,11 @@ function confirmSuggestion(value, triggerPos, cursorPos, trigger, replacement) {
529
824
  const before = value.slice(0, triggerPos);
530
825
  const after = value.slice(cursorPos);
531
826
  const inserted = `${trigger}${replacement}`;
532
- const newValue = before + inserted + after;
533
- const newCursorPos = triggerPos + inserted.length;
827
+ const needsSpace = after.length === 0 || after[0] !== " " && after[0] !== `
828
+ `;
829
+ const suffix = needsSpace ? " " : "";
830
+ const newValue = before + inserted + suffix + after;
831
+ const newCursorPos = triggerPos + inserted.length + suffix.length;
534
832
  return { newValue, newCursorPos };
535
833
  }
536
834
  function renderDropdown(suggestions, selectedIndex) {
@@ -546,7 +844,66 @@ function renderDropdown(suggestions, selectedIndex) {
546
844
  return items;
547
845
  }
548
846
  function escapeHtml2(text) {
549
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
847
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
848
+ }
849
+
850
+ // src/pairs.ts
851
+ function handlePairKey(ta, key) {
852
+ if (key !== "`")
853
+ return false;
854
+ const start = ta.selectionStart;
855
+ const end = ta.selectionEnd;
856
+ const value = ta.value;
857
+ if (start !== end) {
858
+ const selected = value.slice(start, end);
859
+ ta.value = value.slice(0, start) + "`" + selected + "`" + value.slice(end);
860
+ ta.setSelectionRange(start + 1, end + 1);
861
+ return true;
862
+ }
863
+ if (start >= 2 && value.slice(start - 2, start) === "``") {
864
+ ta.value = value.slice(0, start) + "`\n\n```" + value.slice(end);
865
+ ta.setSelectionRange(start + 2, start + 2);
866
+ return true;
867
+ }
868
+ ta.value = value.slice(0, start) + "``" + value.slice(end);
869
+ ta.setSelectionRange(start + 1, start + 1);
870
+ return true;
871
+ }
872
+ function handleEnterKey(ta, e) {
873
+ if (e.key !== "Enter")
874
+ return false;
875
+ const pos = ta.selectionStart;
876
+ const value = ta.value;
877
+ if (ta.selectionEnd !== pos)
878
+ return false;
879
+ const lineStart = value.lastIndexOf(`
880
+ `, pos - 1) + 1;
881
+ const currentLine = value.slice(lineStart, pos);
882
+ const result = getLineContinuation(currentLine);
883
+ if (result === null)
884
+ return false;
885
+ e.preventDefault();
886
+ if (result.eraseCurrentLine) {
887
+ const newValue = value.slice(0, lineStart) + value.slice(pos);
888
+ ta.value = newValue;
889
+ ta.setSelectionRange(lineStart, lineStart);
890
+ } else {
891
+ const newValue = value.slice(0, pos) + `
892
+ ` + result.prefix + value.slice(ta.selectionEnd);
893
+ const newPos = pos + 1 + result.prefix.length;
894
+ ta.value = newValue;
895
+ ta.setSelectionRange(newPos, newPos);
896
+ }
897
+ return true;
898
+ }
899
+ function getLineContinuation(line) {
900
+ if (line === "* ")
901
+ return { eraseCurrentLine: true };
902
+ if (/^\* ./.test(line))
903
+ return { prefix: "* ", eraseCurrentLine: false };
904
+ if (/^#{1,6} ./.test(line))
905
+ return { prefix: "", eraseCurrentLine: false };
906
+ return null;
550
907
  }
551
908
 
552
909
  // src/status-bar.ts
@@ -576,155 +933,156 @@ function renderStatusCount(wordCount, charCount, maxWords) {
576
933
  }
577
934
 
578
935
  // src/element.ts
579
- var exports_element = {};
580
- __export(exports_element, {
581
- ElDmMarkdownInput: () => ElDmMarkdownInput
582
- });
583
- import { BaseElement } from "@duskmoon-dev/el-base";
584
- import { css as markdownBodyCSS } from "@duskmoon-dev/core/components/markdown-body";
585
936
  import { css as css2 } from "@duskmoon-dev/el-base";
586
- function sanitizeUrl(url) {
587
- const trimmed = url.trim();
588
- if (!/^[a-z][a-z\d+\-.]*:/i.test(trimmed))
589
- return trimmed;
590
- if (/^https?:/i.test(trimmed))
591
- return trimmed;
592
- return "#";
593
- }
594
- function escapeHtmlStr(s) {
595
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
596
- }
597
- var coreMarkdownStyles, markdownBodySheet, ElDmMarkdownInput;
598
- var init_element = __esm(() => {
599
- init_css();
600
- init_highlight();
601
- init_upload();
602
- coreMarkdownStyles = markdownBodyCSS.replace(/@layer\s+components\s*\{/, "").replace(/\}\s*$/, "");
603
- markdownBodySheet = css2`
937
+ var coreMarkdownStyles = markdownBodyCSS.replace(/@layer\s+components\s*\{/, "").replace(/\}\s*$/, "");
938
+ var markdownBodySheet = css2`
604
939
  ${coreMarkdownStyles}
605
940
  `;
606
- ElDmMarkdownInput = class ElDmMarkdownInput extends BaseElement {
607
- static formAssociated = true;
608
- static properties = {
609
- name: { type: String, reflect: true, default: "" },
610
- value: { type: String, default: "" },
611
- placeholder: { type: String, reflect: true, default: "Write markdown…" },
612
- disabled: { type: Boolean, reflect: true },
613
- readonly: { type: Boolean, reflect: true },
614
- uploadUrl: { type: String, reflect: true, attribute: "upload-url" },
615
- maxWords: { type: Number, reflect: true, attribute: "max-words" },
616
- dark: { type: Boolean, reflect: true }
617
- };
618
- #internals;
619
- #initialized = false;
620
- #activeTab = "write";
621
- #highlightTimer = null;
622
- #statusTimer = null;
623
- #textarea = null;
624
- #backdrop = null;
625
- #backdropContent = null;
626
- #writeArea = null;
627
- #previewBody = null;
628
- #statusCount = null;
629
- #acDropdown = null;
630
- #uploadList = null;
631
- #fileInput = null;
632
- #resizeObserver = null;
633
- #acSuggestions = [];
634
- #acSelectedIndex = -1;
635
- #acTriggerPos = -1;
636
- #acTrigger = null;
637
- #uploadIdCounter = 0;
638
- constructor() {
639
- super();
640
- this.#internals = this.attachInternals();
641
- this.attachStyles([elementStyles, markdownBodySheet]);
642
- }
643
- connectedCallback() {
644
- super.connectedCallback();
645
- const initial = this.value ?? "";
646
- if (initial && this.#textarea) {
647
- this.#textarea.value = initial;
941
+
942
+ class ElDmMarkdownInput extends BaseElement {
943
+ static formAssociated = true;
944
+ static properties = {
945
+ name: { type: String, reflect: true, default: "" },
946
+ value: { type: String, default: "" },
947
+ placeholder: { type: String, reflect: true, default: "Write markdown…" },
948
+ disabled: { type: Boolean, reflect: true },
949
+ readonly: { type: Boolean, reflect: true },
950
+ required: { type: Boolean, reflect: true },
951
+ uploadUrl: { type: String, reflect: true, attribute: "upload-url" },
952
+ maxWords: { type: Number, reflect: true, attribute: "max-words" },
953
+ dark: { type: Boolean, reflect: true },
954
+ livePreview: { type: Boolean, reflect: true, attribute: "live-preview" },
955
+ debounce: { type: Number, reflect: true, default: 300 },
956
+ katexCssUrl: { type: String, reflect: true, attribute: "katex-css-url" },
957
+ mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" }
958
+ };
959
+ #internals;
960
+ #initialized = false;
961
+ #activeTab = "write";
962
+ #highlightTimer = null;
963
+ #statusTimer = null;
964
+ #textarea = null;
965
+ #renderLayer = null;
966
+ #writeArea = null;
967
+ #previewBody = null;
968
+ #statusCount = null;
969
+ #acDropdown = null;
970
+ #uploadList = null;
971
+ #fileInput = null;
972
+ #acSuggestions = [];
973
+ #acSelectedIndex = -1;
974
+ #acTriggerPos = -1;
975
+ #acTrigger = null;
976
+ #acGeneration = 0;
977
+ #prevDark = false;
978
+ #renderFn = null;
979
+ #mermaidFn = null;
980
+ #livePreviewTimer = null;
981
+ #renderAbortController = null;
982
+ #lastRenderedSource = null;
983
+ #katexCssInjected = false;
984
+ #uploadIdCounter = 0;
985
+ constructor() {
986
+ super();
987
+ this.#internals = this.attachInternals();
988
+ this.attachStyles([elementStyles, markdownBodySheet]);
989
+ }
990
+ connectedCallback() {
991
+ super.connectedCallback();
992
+ }
993
+ disconnectedCallback() {
994
+ this.#renderAbortController?.abort();
995
+ if (this.#livePreviewTimer !== null)
996
+ clearTimeout(this.#livePreviewTimer);
997
+ super.disconnectedCallback();
998
+ }
999
+ update() {
1000
+ if (!this.#initialized) {
1001
+ super.update();
1002
+ this.#initialized = true;
1003
+ this.#cacheDOMRefs();
1004
+ this.#attachEventHandlers();
1005
+ this.#initHighlight();
1006
+ const initVal = this.value ?? "";
1007
+ if (this.#textarea) {
1008
+ this.#textarea.value = initVal;
648
1009
  this.#syncFormValue();
1010
+ if (initVal)
1011
+ this.#scheduleHighlight();
649
1012
  }
1013
+ this.#updateStatusBarNow();
1014
+ return;
650
1015
  }
651
- disconnectedCallback() {
652
- this.#resizeObserver?.disconnect();
653
- super.disconnectedCallback();
1016
+ this.#patchDynamicRegions();
1017
+ }
1018
+ #patchDynamicRegions() {
1019
+ const ta = this.#textarea;
1020
+ if (!ta)
1021
+ return;
1022
+ const placeholder = this.placeholder ?? "Write markdown…";
1023
+ ta.placeholder = placeholder;
1024
+ ta.disabled = !!this.disabled;
1025
+ ta.readOnly = !!this.readonly;
1026
+ const attachBtn = this.shadowRoot.querySelector(".attach-btn");
1027
+ if (attachBtn) {
1028
+ attachBtn.disabled = ta.disabled || ta.readOnly;
654
1029
  }
655
- update() {
656
- if (!this.#initialized) {
657
- super.update();
658
- this.#initialized = true;
659
- this.#cacheDOMRefs();
660
- this.#attachEventHandlers();
661
- this.#initHighlight();
662
- this.#updateStatusBarNow();
663
- const initVal = this.value ?? "";
664
- if (initVal && this.#textarea) {
665
- this.#textarea.value = initVal;
666
- this.#syncFormValue();
667
- this.#scheduleHighlight();
668
- }
669
- return;
1030
+ const propVal = this.value ?? "";
1031
+ if (propVal !== ta.value) {
1032
+ ta.value = propVal;
1033
+ this.#syncFormValue();
1034
+ this.#scheduleHighlight();
1035
+ if (this.#activeTab === "preview" && this.#previewBody) {
1036
+ this.#renderPreview(propVal);
670
1037
  }
671
- this.#patchDynamicRegions();
672
1038
  }
673
- #patchDynamicRegions() {
674
- const ta = this.#textarea;
675
- if (!ta)
676
- return;
677
- const placeholder = this.placeholder ?? "Write markdown…";
678
- ta.placeholder = placeholder;
679
- ta.disabled = !!this.disabled;
680
- ta.readOnly = !!this.readonly;
681
- const attachBtn = this.shadowRoot.querySelector(".attach-btn");
682
- if (attachBtn) {
683
- attachBtn.disabled = ta.disabled || ta.readOnly;
1039
+ const dark = !!this.dark;
1040
+ applyPrismTheme(this.shadowRoot, dark);
1041
+ if (dark !== this.#prevDark) {
1042
+ this.#prevDark = dark;
1043
+ if (this.#activeTab === "preview" && this.#previewBody) {
1044
+ this.#lastRenderedSource = null;
1045
+ this.#renderPreview(ta.value);
684
1046
  }
685
- const propVal = this.value ?? "";
686
- if (propVal !== ta.value) {
687
- ta.value = propVal;
688
- this.#syncFormValue();
689
- this.#scheduleHighlight();
690
- }
691
- const dark = !!this.dark;
692
- applyPrismTheme(this.shadowRoot, dark);
693
- this.#updateStatusBarNow();
694
1047
  }
695
- render() {
696
- const ph = this.placeholder ?? "Write markdown…";
697
- const disabled = !!this.disabled;
698
- const readonly = !!this.readonly;
699
- return `
1048
+ this.#updateStatusBarNow();
1049
+ }
1050
+ render() {
1051
+ const ph = this.placeholder ?? "Write markdown…";
1052
+ const disabled = !!this.disabled;
1053
+ const readonly = !!this.readonly;
1054
+ return `
700
1055
  <div class="editor">
701
1056
  <div class="toolbar" role="tablist" aria-label="Editor mode">
702
1057
  <button
703
1058
  class="tab-btn"
1059
+ id="tab-write"
704
1060
  data-tab="write"
705
1061
  role="tab"
706
1062
  aria-selected="true"
707
1063
  aria-controls="write-panel"
1064
+ tabindex="0"
708
1065
  >Write</button>
709
1066
  <button
710
1067
  class="tab-btn"
1068
+ id="tab-preview"
711
1069
  data-tab="preview"
712
1070
  role="tab"
713
1071
  aria-selected="false"
714
1072
  aria-controls="preview-panel"
1073
+ tabindex="-1"
715
1074
  >Preview</button>
716
1075
  </div>
717
1076
 
718
- <div class="write-area" id="write-panel" role="tabpanel" aria-label="Markdown editor">
719
- <div class="backdrop" aria-hidden="true">
720
- <div class="backdrop-content"></div>
721
- </div>
1077
+ <div class="write-area" id="write-panel" role="tabpanel" aria-labelledby="tab-write">
1078
+ <div class="render-layer" aria-hidden="true"></div>
722
1079
  <textarea
723
1080
  aria-label="Markdown editor"
724
1081
  aria-haspopup="listbox"
1082
+ aria-expanded="false"
725
1083
  aria-autocomplete="list"
726
1084
  aria-controls="ac-dropdown"
727
- placeholder="${ph}"
1085
+ placeholder="${escapeHtmlStr(ph)}"
728
1086
  ${disabled ? "disabled" : ""}
729
1087
  ${readonly ? "readonly" : ""}
730
1088
  spellcheck="false"
@@ -738,7 +1096,7 @@ var init_element = __esm(() => {
738
1096
  class="preview-body markdown-body"
739
1097
  id="preview-panel"
740
1098
  role="tabpanel"
741
- aria-label="Markdown preview"
1099
+ aria-labelledby="tab-preview"
742
1100
  hidden
743
1101
  ></div>
744
1102
 
@@ -761,403 +1119,493 @@ var init_element = __esm(() => {
761
1119
  </div>
762
1120
  <ul id="ac-dropdown" class="ac-dropdown" role="listbox" aria-label="Suggestions" hidden></ul>
763
1121
  `;
764
- }
765
- #cacheDOMRefs() {
766
- this.#textarea = this.shadowRoot.querySelector("textarea");
767
- this.#backdrop = this.shadowRoot.querySelector(".backdrop");
768
- this.#backdropContent = this.shadowRoot.querySelector(".backdrop-content");
769
- this.#writeArea = this.shadowRoot.querySelector(".write-area");
770
- this.#previewBody = this.shadowRoot.querySelector(".preview-body");
771
- this.#statusCount = this.shadowRoot.querySelector(".status-bar-count");
772
- this.#acDropdown = this.shadowRoot.querySelector(".ac-dropdown");
773
- this.#uploadList = this.shadowRoot.querySelector(".upload-list");
774
- this.#fileInput = this.shadowRoot.querySelector(".file-input");
775
- }
776
- #attachEventHandlers() {
777
- const ta = this.#textarea;
778
- if (!ta)
1122
+ }
1123
+ #cacheDOMRefs() {
1124
+ this.#textarea = this.shadowRoot.querySelector("textarea");
1125
+ this.#renderLayer = this.shadowRoot.querySelector(".render-layer");
1126
+ this.#writeArea = this.shadowRoot.querySelector(".write-area");
1127
+ this.#previewBody = this.shadowRoot.querySelector(".preview-body");
1128
+ this.#statusCount = this.shadowRoot.querySelector(".status-bar-count");
1129
+ this.#acDropdown = this.shadowRoot.querySelector(".ac-dropdown");
1130
+ this.#uploadList = this.shadowRoot.querySelector(".upload-list");
1131
+ this.#fileInput = this.shadowRoot.querySelector(".file-input");
1132
+ }
1133
+ #attachEventHandlers() {
1134
+ const ta = this.#textarea;
1135
+ if (!ta)
1136
+ return;
1137
+ ta.addEventListener("input", () => {
1138
+ this.#syncFormValue();
1139
+ this.emit("change", { value: ta.value });
1140
+ this.#scheduleHighlight();
1141
+ this.#scheduleStatusUpdate();
1142
+ this.#handleAutocompleteInput();
1143
+ this.#scheduleLivePreview();
1144
+ });
1145
+ ta.addEventListener("blur", () => {
1146
+ setTimeout(() => {
1147
+ if (!this.shadowRoot?.activeElement) {
1148
+ this.#closeDropdown();
1149
+ }
1150
+ }, 150);
1151
+ });
1152
+ ta.addEventListener("keydown", (e) => {
1153
+ if (this.#acSuggestions.length > 0 && !this.#acDropdown?.hidden) {
1154
+ this.#handleDropdownKeydown(e);
1155
+ if (e.defaultPrevented)
1156
+ return;
1157
+ }
1158
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "P") {
1159
+ e.preventDefault();
1160
+ this.#switchTab(this.#activeTab === "write" ? "preview" : "write");
1161
+ return;
1162
+ }
1163
+ if (this.disabled || this.readonly) {
779
1164
  return;
780
- ta.addEventListener("input", () => {
1165
+ }
1166
+ if (!e.ctrlKey && !e.metaKey && !e.altKey && handlePairKey(ta, e.key)) {
1167
+ e.preventDefault();
781
1168
  this.#syncFormValue();
782
1169
  this.emit("change", { value: ta.value });
783
1170
  this.#scheduleHighlight();
784
- this.#scheduleStatusUpdate();
785
- this.#handleAutocompleteInput();
786
- });
787
- ta.addEventListener("scroll", () => {
788
- if (this.#backdrop) {
789
- this.#backdrop.scrollTop = ta.scrollTop;
790
- this.#backdrop.scrollLeft = ta.scrollLeft;
791
- }
792
- });
793
- ta.addEventListener("blur", () => {
794
- setTimeout(() => {
795
- if (!this.shadowRoot?.activeElement) {
796
- this.#closeDropdown();
797
- }
798
- }, 150);
799
- });
800
- ta.addEventListener("keydown", (e) => {
801
- if (this.#acSuggestions.length > 0 && !this.#acDropdown?.hidden) {
802
- this.#handleDropdownKeydown(e);
803
- }
804
- if (e.ctrlKey && e.shiftKey && e.key === "P") {
805
- e.preventDefault();
806
- this.#switchTab(this.#activeTab === "write" ? "preview" : "write");
1171
+ return;
1172
+ }
1173
+ if (e.key === "Enter" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1174
+ if (handleEnterKey(ta, e)) {
1175
+ this.#syncFormValue();
1176
+ this.emit("change", { value: ta.value });
1177
+ this.#scheduleHighlight();
1178
+ this.#scheduleStatusUpdate();
807
1179
  }
808
- });
809
- const writeArea = this.#writeArea;
810
- if (writeArea) {
811
- writeArea.addEventListener("dragover", (e) => {
812
- e.preventDefault();
813
- writeArea.style.opacity = "0.8";
814
- });
815
- writeArea.addEventListener("dragleave", () => {
816
- writeArea.style.opacity = "";
817
- });
818
- writeArea.addEventListener("drop", (e) => {
819
- e.preventDefault();
820
- writeArea.style.opacity = "";
821
- if (this.readonly)
822
- return;
823
- const files = Array.from(e.dataTransfer?.files ?? []).filter(isAcceptedType);
824
- files.forEach((f) => this.#startUpload(f));
825
- });
826
1180
  }
827
- ta.addEventListener("paste", (e) => {
1181
+ });
1182
+ const writeArea = this.#writeArea;
1183
+ if (writeArea) {
1184
+ writeArea.addEventListener("dragover", (e) => {
1185
+ if (this.disabled)
1186
+ return;
828
1187
  if (this.readonly)
829
1188
  return;
830
- const imageFiles = Array.from(e.clipboardData?.files ?? []).filter((f) => f.type.startsWith("image/"));
831
- if (imageFiles.length > 0) {
832
- e.preventDefault();
833
- imageFiles.forEach((f) => this.#startUpload(f));
834
- }
1189
+ e.preventDefault();
1190
+ writeArea.style.opacity = "0.8";
835
1191
  });
836
- const toolbar = this.shadowRoot.querySelector(".toolbar");
837
- toolbar?.addEventListener("click", (e) => {
838
- const btn = e.target.closest(".tab-btn");
839
- const tab = btn?.dataset.tab;
840
- if (tab)
841
- this.#switchTab(tab);
1192
+ writeArea.addEventListener("dragleave", () => {
1193
+ writeArea.style.opacity = "";
842
1194
  });
843
- const attachBtn = this.shadowRoot.querySelector(".attach-btn");
844
- attachBtn?.addEventListener("click", () => this.#fileInput?.click());
845
- this.#fileInput?.addEventListener("change", () => {
846
- const files = Array.from(this.#fileInput?.files ?? []).filter(isAcceptedType);
1195
+ writeArea.addEventListener("drop", (e) => {
1196
+ e.preventDefault();
1197
+ writeArea.style.opacity = "";
1198
+ if (this.disabled)
1199
+ return;
1200
+ if (this.readonly)
1201
+ return;
1202
+ const files = Array.from(e.dataTransfer?.files ?? []).filter(isAcceptedType);
847
1203
  files.forEach((f) => this.#startUpload(f));
848
- if (this.#fileInput)
849
- this.#fileInput.value = "";
850
1204
  });
851
- this.#acDropdown?.addEventListener("click", (e) => {
852
- const item = e.target.closest("[data-ac-index]");
853
- if (item) {
854
- const idx = parseInt(item.dataset.acIndex ?? "-1", 10);
855
- if (idx >= 0) {
856
- this.#acSelectedIndex = idx;
857
- this.#confirmAutocomplete();
858
- }
859
- }
860
- });
861
- if (typeof ResizeObserver !== "undefined") {
862
- this.#resizeObserver = new ResizeObserver(() => {
863
- if (this.#backdrop && this.#textarea) {
864
- this.#backdrop.style.height = `${this.#textarea.offsetHeight}px`;
865
- }
866
- });
867
- this.#resizeObserver.observe(ta);
868
- }
869
- }
870
- #initHighlight() {
871
- const dark = !!this.dark;
872
- applyPrismTheme(this.shadowRoot, dark);
873
- ensurePrism().then(() => {
874
- if (this.#textarea && this.#backdropContent) {
875
- this.#backdropContent.innerHTML = highlightMarkdown(this.#textarea.value);
876
- }
877
- });
878
- }
879
- #scheduleHighlight() {
880
- if (this.#highlightTimer !== null)
881
- clearTimeout(this.#highlightTimer);
882
- this.#highlightTimer = setTimeout(() => {
883
- this.#highlightTimer = null;
884
- if (this.#backdropContent && this.#textarea) {
885
- this.#backdropContent.innerHTML = highlightMarkdown(this.#textarea.value);
886
- }
887
- if (this.#backdrop && this.#textarea) {
888
- this.#backdrop.scrollTop = this.#textarea.scrollTop;
889
- }
890
- }, 60);
891
1205
  }
892
- #switchTab(tab) {
893
- if (tab === this.#activeTab)
1206
+ ta.addEventListener("paste", (e) => {
1207
+ if (this.disabled)
894
1208
  return;
895
- this.#activeTab = tab;
896
- const writeBtns = this.shadowRoot.querySelectorAll(".tab-btn");
897
- writeBtns.forEach((btn) => {
898
- const isActive = btn.dataset.tab === tab;
899
- btn.setAttribute("aria-selected", String(isActive));
900
- });
901
- if (tab === "preview") {
902
- this.#writeArea?.setAttribute("hidden", "");
903
- if (this.#previewBody) {
904
- this.#previewBody.removeAttribute("hidden");
905
- this.#previewBody.innerHTML = this.#renderMarkdown(this.#textarea?.value ?? "");
1209
+ if (this.readonly)
1210
+ return;
1211
+ const imageFiles = Array.from(e.clipboardData?.files ?? []).filter((f) => f.type.startsWith("image/"));
1212
+ if (imageFiles.length > 0) {
1213
+ e.preventDefault();
1214
+ imageFiles.forEach((f) => this.#startUpload(f));
1215
+ }
1216
+ });
1217
+ const toolbar = this.shadowRoot.querySelector(".toolbar");
1218
+ toolbar?.addEventListener("click", (e) => {
1219
+ const btn = e.target.closest(".tab-btn");
1220
+ const tab = btn?.dataset.tab;
1221
+ if (tab)
1222
+ this.#switchTab(tab);
1223
+ });
1224
+ toolbar?.addEventListener("keydown", (e) => {
1225
+ const kev = e;
1226
+ if (kev.key === "ArrowLeft" || kev.key === "ArrowRight") {
1227
+ kev.preventDefault();
1228
+ const nextTab = this.#activeTab === "write" ? "preview" : "write";
1229
+ this.#switchTab(nextTab);
1230
+ const nextBtn = this.shadowRoot.querySelector(`.tab-btn[data-tab="${nextTab}"]`);
1231
+ nextBtn?.focus();
1232
+ }
1233
+ });
1234
+ const attachBtn = this.shadowRoot.querySelector(".attach-btn");
1235
+ attachBtn?.addEventListener("click", () => this.#fileInput?.click());
1236
+ this.#fileInput?.addEventListener("change", () => {
1237
+ const files = Array.from(this.#fileInput?.files ?? []).filter(isAcceptedType);
1238
+ files.forEach((f) => this.#startUpload(f));
1239
+ if (this.#fileInput)
1240
+ this.#fileInput.value = "";
1241
+ });
1242
+ this.#acDropdown?.addEventListener("click", (e) => {
1243
+ const item = e.target.closest("[data-ac-index]");
1244
+ if (item) {
1245
+ const idx = parseInt(item.dataset.acIndex ?? "-1", 10);
1246
+ if (idx >= 0) {
1247
+ this.#acSelectedIndex = idx;
1248
+ this.#confirmAutocomplete();
906
1249
  }
907
- } else {
908
- this.#writeArea?.removeAttribute("hidden");
909
- this.#previewBody?.setAttribute("hidden", "");
910
1250
  }
1251
+ });
1252
+ }
1253
+ #initHighlight() {
1254
+ const dark = !!this.dark;
1255
+ applyPrismTheme(this.shadowRoot, dark);
1256
+ ensurePrism().then(() => {
1257
+ if (this.#textarea && this.#renderLayer) {
1258
+ this.#renderLayer.innerHTML = highlightMarkdown(this.#textarea.value);
1259
+ }
1260
+ });
1261
+ }
1262
+ #scheduleHighlight() {
1263
+ if (this.#highlightTimer !== null)
1264
+ clearTimeout(this.#highlightTimer);
1265
+ this.#highlightTimer = setTimeout(() => {
1266
+ this.#highlightTimer = null;
1267
+ if (this.#renderLayer && this.#textarea) {
1268
+ this.#renderLayer.innerHTML = highlightMarkdown(this.#textarea.value);
1269
+ }
1270
+ }, 60);
1271
+ }
1272
+ #switchTab(tab) {
1273
+ if (tab === this.#activeTab)
1274
+ return;
1275
+ this.#activeTab = tab;
1276
+ const writeBtns = this.shadowRoot.querySelectorAll(".tab-btn");
1277
+ writeBtns.forEach((btn) => {
1278
+ const isActive = btn.dataset.tab === tab;
1279
+ btn.setAttribute("aria-selected", String(isActive));
1280
+ btn.setAttribute("tabindex", isActive ? "0" : "-1");
1281
+ });
1282
+ if (tab === "preview") {
1283
+ this.#writeArea?.setAttribute("hidden", "");
1284
+ if (this.#previewBody) {
1285
+ this.#previewBody.removeAttribute("hidden");
1286
+ this.#renderPreview(this.#textarea?.value ?? "");
1287
+ }
1288
+ } else {
1289
+ this.#writeArea?.removeAttribute("hidden");
1290
+ this.#previewBody?.setAttribute("hidden", "");
911
1291
  }
912
- #renderMarkdown(md) {
913
- if (!md.trim())
914
- return "<p></p>";
915
- const CB_PH = "⁠CB";
916
- const IC_PH = "⁠IC";
917
- const codeBlocks = [];
918
- let html = md.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
919
- const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
920
- const idx = codeBlocks.push(`<pre><code class="language-${lang || "text"}">${escaped}</code></pre>`) - 1;
921
- return `${CB_PH}${idx}${CB_PH}`;
922
- });
923
- const inlineCodes = [];
924
- html = html.replace(/`([^`\n]+)`/g, (_, code) => {
925
- const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
926
- const idx = inlineCodes.push(`<code>${escaped}</code>`) - 1;
927
- return `${IC_PH}${idx}${IC_PH}`;
928
- });
929
- html = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
930
- html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => `<img src="${sanitizeUrl(url)}" alt="${alt}">`);
931
- html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => `<a href="${sanitizeUrl(url)}">${text}</a>`);
932
- html = html.replace(/^###### (.+)$/gm, "<h6>$1</h6>").replace(/^##### (.+)$/gm, "<h5>$1</h5>").replace(/^#### (.+)$/gm, "<h4>$1</h4>").replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>");
933
- html = html.replace(/^[-*_]{3,}$/gm, "<hr>");
934
- html = html.replace(/^&gt; (.+)$/gm, "<blockquote>$1</blockquote>");
935
- html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>").replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/___(.+?)___/g, "<strong><em>$1</em></strong>").replace(/__(.+?)__/g, "<strong>$1</strong>").replace(/_(.+?)_/g, "<em>$1</em>");
936
- html = html.replace(/~~(.+?)~~/g, "<del>$1</del>");
937
- html = html.replace(/^[ \t]*[-*+] (.+)$/gm, '<li data-list="ul">$1</li>');
938
- html = html.replace(/^[ \t]*\d+\. (.+)$/gm, '<li data-list="ol">$1</li>');
939
- html = html.replace(/(<li data-list="ul">[^\n]*<\/li>(?:\n<li data-list="ul">[^\n]*<\/li>)*)/g, (match) => "<ul>" + match.replace(/ data-list="ul"/g, "") + "</ul>");
940
- html = html.replace(/(<li data-list="ol">[^\n]*<\/li>(?:\n<li data-list="ol">[^\n]*<\/li>)*)/g, (match) => "<ol>" + match.replace(/ data-list="ol"/g, "") + "</ol>");
941
- const lines = html.split(`
942
-
943
- `);
944
- html = lines.map((block) => {
945
- const t = block.trim();
946
- if (!t)
947
- return "";
948
- if (/^<(h[1-6]|ul|ol|li|blockquote|hr|pre|img)/.test(t) || t.startsWith(CB_PH))
949
- return t;
950
- return `<p>${t.replace(/\n/g, "<br>")}</p>`;
951
- }).filter(Boolean).join(`
952
- `);
953
- html = html.replace(new RegExp(`${CB_PH}(\\d+)${CB_PH}`, "g"), (_, i) => codeBlocks[parseInt(i, 10)] ?? "");
954
- html = html.replace(new RegExp(`${IC_PH}(\\d+)${IC_PH}`, "g"), (_, i) => inlineCodes[parseInt(i, 10)] ?? "");
955
- return html;
1292
+ }
1293
+ async#loadRenderPipeline() {
1294
+ if (this.#renderFn && this.#mermaidFn) {
1295
+ return { renderMarkdown: this.#renderFn, renderMermaidBlocks: this.#mermaidFn };
1296
+ }
1297
+ const mod = await Promise.resolve().then(() => (init_render(), exports_render));
1298
+ this.#renderFn = mod.renderMarkdown;
1299
+ this.#mermaidFn = mod.renderMermaidBlocks;
1300
+ return { renderMarkdown: this.#renderFn, renderMermaidBlocks: this.#mermaidFn };
1301
+ }
1302
+ async#renderPreview(source, force = false) {
1303
+ const preview = this.#previewBody;
1304
+ if (!preview)
1305
+ return;
1306
+ if (!force && this.#lastRenderedSource === source && this.#renderFn !== null) {
1307
+ return;
956
1308
  }
957
- #syncFormValue() {
958
- this.#internals?.setFormValue(this.#textarea?.value ?? "");
1309
+ this.#renderAbortController?.abort();
1310
+ const controller = new AbortController;
1311
+ this.#renderAbortController = controller;
1312
+ this.emit("render-start", {});
1313
+ preview.setAttribute("aria-busy", "true");
1314
+ if (!this.#renderFn) {
1315
+ preview.innerHTML = `
1316
+ <div class="preview-skeleton" aria-label="Loading preview…">
1317
+ <div class="skeleton-line" style="width:90%"></div>
1318
+ <div class="skeleton-line" style="width:75%"></div>
1319
+ <div class="skeleton-line" style="width:85%"></div>
1320
+ <div class="skeleton-line" style="width:60%"></div>
1321
+ </div>`;
959
1322
  }
960
- #startUpload(file) {
961
- this.emit("upload-start", { file });
962
- const id = `upload-${++this.#uploadIdCounter}`;
963
- const uploadUrl = this.uploadUrl;
964
- if (!uploadUrl) {
965
- this.emit("upload-error", { file, error: "no upload-url set" });
966
- this.#showUploadError(file, "no upload-url set");
1323
+ try {
1324
+ const { renderMarkdown: renderMarkdown2, renderMermaidBlocks: renderMermaidBlocks2 } = await this.#loadRenderPipeline();
1325
+ if (controller.signal.aborted) {
1326
+ preview.removeAttribute("aria-busy");
967
1327
  return;
968
1328
  }
969
- this.#addProgressRow(id, file.name);
970
- uploadFile(file, uploadUrl, (pct) => {
971
- this.#updateProgressRow(id, pct);
972
- }).then((url) => {
973
- this.#removeUploadRow(id);
974
- const markdown = fileToMarkdown(file, url);
975
- this.insertText(markdown);
976
- this.emit("upload-done", { file, url, markdown });
977
- }).catch((err) => {
978
- this.#removeUploadRow(id);
979
- const errorMsg = typeof err === "string" ? err : "Upload failed";
980
- this.emit("upload-error", { file, error: errorMsg });
981
- this.#showUploadError(file, errorMsg);
982
- });
983
- }
984
- #addProgressRow(id, filename) {
985
- if (!this.#uploadList)
1329
+ const html = await renderMarkdown2(source);
1330
+ if (controller.signal.aborted) {
1331
+ preview.removeAttribute("aria-busy");
1332
+ return;
1333
+ }
1334
+ preview.innerHTML = html;
1335
+ preview.removeAttribute("aria-busy");
1336
+ this.#ensureKatexCss();
1337
+ const mermaidSrc = this.mermaidSrc;
1338
+ await renderMermaidBlocks2(preview, mermaidSrc);
1339
+ if (controller.signal.aborted) {
1340
+ preview.removeAttribute("aria-busy");
1341
+ return;
1342
+ }
1343
+ this.#lastRenderedSource = source;
1344
+ this.emit("render-done", { html });
1345
+ } catch (err) {
1346
+ if (controller.signal.aborted) {
1347
+ preview.removeAttribute("aria-busy");
986
1348
  return;
987
- const row = document.createElement("div");
988
- row.className = "upload-row";
989
- row.id = id;
990
- row.innerHTML = `
1349
+ }
1350
+ preview.removeAttribute("aria-busy");
1351
+ preview.innerHTML = `<pre class="render-error-fallback">${escapeHtmlStr(source)}</pre>`;
1352
+ this.emit("render-error", { error: err instanceof Error ? err : new Error(String(err)) });
1353
+ }
1354
+ }
1355
+ #ensureKatexCss() {
1356
+ if (this.#katexCssInjected)
1357
+ return;
1358
+ this.#katexCssInjected = true;
1359
+ const rawUrl = this.katexCssUrl ?? "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
1360
+ const katexUrl = /^https:\/\//i.test(rawUrl) ? rawUrl : "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
1361
+ if (katexUrl !== rawUrl) {
1362
+ console.warn(`[el-dm-markdown-input] katex-css-url "${rawUrl}" rejected — only https: URLs are allowed.`);
1363
+ }
1364
+ const link = document.createElement("link");
1365
+ link.id = "katex-css";
1366
+ link.rel = "stylesheet";
1367
+ link.href = katexUrl;
1368
+ this.shadowRoot.appendChild(link);
1369
+ }
1370
+ #scheduleLivePreview() {
1371
+ if (!this.livePreview)
1372
+ return;
1373
+ if (this.#activeTab !== "preview")
1374
+ return;
1375
+ if (this.#livePreviewTimer !== null)
1376
+ clearTimeout(this.#livePreviewTimer);
1377
+ const ms = this.debounce ?? 300;
1378
+ this.#livePreviewTimer = setTimeout(() => {
1379
+ this.#livePreviewTimer = null;
1380
+ this.#renderPreview(this.#textarea?.value ?? "");
1381
+ }, ms);
1382
+ }
1383
+ #syncFormValue() {
1384
+ this.#internals?.setFormValue(this.#textarea?.value ?? "");
1385
+ }
1386
+ #startUpload(file) {
1387
+ this.emit("upload-start", { file });
1388
+ const id = `upload-${++this.#uploadIdCounter}`;
1389
+ const uploadUrl = this.uploadUrl;
1390
+ if (!uploadUrl) {
1391
+ this.emit("upload-error", { file, error: "no upload-url set" });
1392
+ this.#showUploadError(file, "no upload-url set");
1393
+ return;
1394
+ }
1395
+ this.#addProgressRow(id, file.name);
1396
+ uploadFile(file, uploadUrl, (pct) => {
1397
+ this.#updateProgressRow(id, pct);
1398
+ }).then((url) => {
1399
+ this.#removeUploadRow(id);
1400
+ const markdown = fileToMarkdown(file, url);
1401
+ this.insertText(markdown);
1402
+ this.emit("upload-done", { file, url, markdown });
1403
+ }).catch((err) => {
1404
+ this.#removeUploadRow(id);
1405
+ const errorMsg = err instanceof Error ? err.message : "Upload failed";
1406
+ this.emit("upload-error", { file, error: errorMsg });
1407
+ this.#showUploadError(file, errorMsg);
1408
+ });
1409
+ }
1410
+ #addProgressRow(id, filename) {
1411
+ if (!this.#uploadList)
1412
+ return;
1413
+ const row = document.createElement("div");
1414
+ row.className = "upload-row";
1415
+ row.id = id;
1416
+ row.innerHTML = `
991
1417
  <span class="upload-filename">${escapeHtmlStr(filename)}</span>
992
- <div class="upload-bar-track">
1418
+ <div class="upload-bar-track" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Uploading ${escapeHtmlStr(filename)}">
993
1419
  <div class="upload-bar" style="width: 0%"></div>
994
1420
  </div>
995
1421
  `;
996
- this.#uploadList.appendChild(row);
997
- }
998
- #updateProgressRow(id, pct) {
999
- const bar = this.#uploadList?.querySelector(`#${id} .upload-bar`);
1000
- if (bar)
1001
- bar.style.width = `${pct}%`;
1002
- }
1003
- #removeUploadRow(id) {
1004
- this.#uploadList?.querySelector(`#${id}`)?.remove();
1005
- }
1006
- #showUploadError(file, message) {
1007
- if (!this.#uploadList)
1008
- return;
1009
- const row = document.createElement("div");
1010
- row.className = "upload-error-row";
1011
- row.innerHTML = `
1422
+ this.#uploadList.appendChild(row);
1423
+ }
1424
+ #updateProgressRow(id, pct) {
1425
+ const track = this.#uploadList?.querySelector(`#${id} .upload-bar-track`);
1426
+ if (track)
1427
+ track.setAttribute("aria-valuenow", String(pct));
1428
+ const bar = this.#uploadList?.querySelector(`#${id} .upload-bar`);
1429
+ if (bar)
1430
+ bar.style.width = `${pct}%`;
1431
+ }
1432
+ #removeUploadRow(id) {
1433
+ this.#uploadList?.querySelector(`#${id}`)?.remove();
1434
+ }
1435
+ #showUploadError(file, message) {
1436
+ if (!this.#uploadList)
1437
+ return;
1438
+ const row = document.createElement("div");
1439
+ row.className = "upload-error-row";
1440
+ row.setAttribute("role", "alert");
1441
+ row.innerHTML = `
1012
1442
  <span class="upload-error-msg">${escapeHtmlStr(file.name)}: ${escapeHtmlStr(message)}</span>
1013
1443
  `;
1014
- this.#uploadList.appendChild(row);
1015
- setTimeout(() => row.remove(), 4000);
1444
+ this.#uploadList.appendChild(row);
1445
+ setTimeout(() => row.remove(), 4000);
1446
+ }
1447
+ #handleAutocompleteInput() {
1448
+ const ta = this.#textarea;
1449
+ if (!ta)
1450
+ return;
1451
+ const result = detectTrigger(ta.value, ta.selectionStart ?? 0);
1452
+ if (!result) {
1453
+ this.#closeDropdown();
1454
+ return;
1016
1455
  }
1017
- #handleAutocompleteInput() {
1018
- const ta = this.#textarea;
1019
- if (!ta)
1020
- return;
1021
- const result = detectTrigger(ta.value, ta.selectionStart ?? 0);
1022
- if (!result) {
1023
- this.#closeDropdown();
1024
- return;
1025
- }
1026
- const { trigger, query, triggerPos } = result;
1027
- this.#acTrigger = trigger;
1028
- this.#acTriggerPos = triggerPos;
1029
- const resolve = (list) => this.setSuggestions(list);
1030
- if (trigger === "@") {
1031
- this.emit("mention-query", { trigger, query, resolve });
1032
- } else {
1033
- this.emit("reference-query", { trigger, query, resolve });
1034
- }
1456
+ const { trigger, query, triggerPos } = result;
1457
+ this.#acTrigger = trigger;
1458
+ this.#acTriggerPos = triggerPos;
1459
+ const gen = ++this.#acGeneration;
1460
+ const resolve = (list) => {
1461
+ if (gen === this.#acGeneration)
1462
+ this.setSuggestions(list);
1463
+ };
1464
+ if (trigger === "@") {
1465
+ this.emit("mention-query", { trigger, query, resolve });
1466
+ } else {
1467
+ this.emit("reference-query", { trigger, query, resolve });
1035
1468
  }
1036
- #handleDropdownKeydown(e) {
1037
- const len = this.#acSuggestions.length;
1038
- if (len === 0)
1039
- return;
1040
- switch (e.key) {
1041
- case "ArrowDown":
1042
- e.preventDefault();
1043
- this.#acSelectedIndex = (this.#acSelectedIndex + 1) % len;
1044
- this.#updateDropdown();
1045
- break;
1046
- case "ArrowUp":
1469
+ }
1470
+ #handleDropdownKeydown(e) {
1471
+ const len = this.#acSuggestions.length;
1472
+ if (len === 0)
1473
+ return;
1474
+ switch (e.key) {
1475
+ case "ArrowDown":
1476
+ e.preventDefault();
1477
+ this.#acSelectedIndex = (this.#acSelectedIndex + 1) % len;
1478
+ this.#updateDropdown();
1479
+ break;
1480
+ case "ArrowUp":
1481
+ e.preventDefault();
1482
+ this.#acSelectedIndex = (this.#acSelectedIndex - 1 + len) % len;
1483
+ this.#updateDropdown();
1484
+ break;
1485
+ case "Enter":
1486
+ case "Tab":
1487
+ if (this.#acSelectedIndex >= 0) {
1047
1488
  e.preventDefault();
1048
- this.#acSelectedIndex = (this.#acSelectedIndex - 1 + len) % len;
1049
- this.#updateDropdown();
1050
- break;
1051
- case "Enter":
1052
- case "Tab":
1053
- if (this.#acSelectedIndex >= 0) {
1054
- e.preventDefault();
1055
- this.#confirmAutocomplete();
1056
- }
1057
- break;
1058
- case "Escape":
1059
- this.#closeDropdown();
1060
- break;
1061
- }
1062
- }
1063
- #confirmAutocomplete() {
1064
- const ta = this.#textarea;
1065
- if (!ta || this.#acSelectedIndex < 0 || !this.#acTrigger)
1066
- return;
1067
- const suggestion = this.#acSuggestions[this.#acSelectedIndex];
1068
- if (!suggestion)
1069
- return;
1070
- const { newValue, newCursorPos } = confirmSuggestion(ta.value, this.#acTriggerPos, ta.selectionStart ?? ta.value.length, this.#acTrigger, suggestion.id);
1071
- ta.value = newValue;
1072
- ta.setSelectionRange(newCursorPos, newCursorPos);
1073
- this.#syncFormValue();
1074
- this.emit("change", { value: ta.value });
1075
- this.#scheduleHighlight();
1076
- this.#scheduleStatusUpdate();
1077
- this.#closeDropdown();
1078
- }
1079
- #closeDropdown() {
1080
- this.#acSuggestions = [];
1081
- this.#acSelectedIndex = -1;
1082
- this.#acTrigger = null;
1083
- this.#acTriggerPos = -1;
1084
- if (this.#acDropdown) {
1085
- this.#acDropdown.innerHTML = "";
1086
- this.#acDropdown.hidden = true;
1087
- }
1489
+ this.#confirmAutocomplete();
1490
+ }
1491
+ break;
1492
+ case "Escape":
1493
+ this.#closeDropdown();
1494
+ break;
1088
1495
  }
1089
- #updateDropdown() {
1090
- if (!this.#acDropdown)
1091
- return;
1092
- if (this.#acSuggestions.length === 0) {
1093
- this.#acDropdown.hidden = true;
1094
- this.#textarea?.removeAttribute("aria-activedescendant");
1095
- return;
1096
- }
1097
- this.#acDropdown.innerHTML = renderDropdown(this.#acSuggestions, this.#acSelectedIndex);
1098
- this.#acDropdown.hidden = false;
1099
- if (this.#acSelectedIndex >= 0) {
1100
- this.#textarea?.setAttribute("aria-activedescendant", `ac-item-${this.#acSelectedIndex}`);
1101
- } else {
1102
- this.#textarea?.removeAttribute("aria-activedescendant");
1103
- }
1496
+ }
1497
+ #confirmAutocomplete() {
1498
+ const ta = this.#textarea;
1499
+ if (!ta || this.#acSelectedIndex < 0 || !this.#acTrigger)
1500
+ return;
1501
+ const suggestion = this.#acSuggestions[this.#acSelectedIndex];
1502
+ if (!suggestion)
1503
+ return;
1504
+ const { newValue, newCursorPos } = confirmSuggestion(ta.value, this.#acTriggerPos, ta.selectionStart ?? ta.value.length, this.#acTrigger, suggestion.id);
1505
+ ta.value = newValue;
1506
+ ta.setSelectionRange(newCursorPos, newCursorPos);
1507
+ this.#syncFormValue();
1508
+ this.emit("change", { value: ta.value });
1509
+ this.#scheduleHighlight();
1510
+ this.#scheduleStatusUpdate();
1511
+ this.#closeDropdown();
1512
+ }
1513
+ #closeDropdown() {
1514
+ this.#acGeneration++;
1515
+ this.#acSuggestions = [];
1516
+ this.#acSelectedIndex = -1;
1517
+ this.#acTrigger = null;
1518
+ this.#acTriggerPos = -1;
1519
+ if (this.#acDropdown) {
1520
+ this.#acDropdown.innerHTML = "";
1521
+ this.#acDropdown.hidden = true;
1104
1522
  }
1105
- #scheduleStatusUpdate() {
1106
- if (this.#statusTimer !== null)
1107
- clearTimeout(this.#statusTimer);
1108
- this.#statusTimer = setTimeout(() => {
1109
- this.#statusTimer = null;
1110
- this.#updateStatusBarNow();
1111
- }, 100);
1523
+ this.#textarea?.setAttribute("aria-expanded", "false");
1524
+ this.#textarea?.removeAttribute("aria-activedescendant");
1525
+ }
1526
+ #updateDropdown() {
1527
+ if (!this.#acDropdown)
1528
+ return;
1529
+ if (this.#acSuggestions.length === 0) {
1530
+ this.#acDropdown.hidden = true;
1531
+ this.#textarea?.setAttribute("aria-expanded", "false");
1532
+ this.#textarea?.removeAttribute("aria-activedescendant");
1533
+ return;
1112
1534
  }
1113
- #updateStatusBarNow() {
1114
- if (!this.#statusCount)
1115
- return;
1116
- const text = this.#textarea?.value ?? "";
1117
- const words = countWords(text);
1118
- const chars = text.length;
1119
- const maxWords = this.maxWords ?? null;
1120
- this.#statusCount.innerHTML = renderStatusCount(words, chars, maxWords);
1535
+ this.#acDropdown.innerHTML = renderDropdown(this.#acSuggestions, this.#acSelectedIndex);
1536
+ this.#acDropdown.hidden = false;
1537
+ this.#textarea?.setAttribute("aria-expanded", "true");
1538
+ if (this.#acSelectedIndex >= 0) {
1539
+ this.#textarea?.setAttribute("aria-activedescendant", `ac-item-${this.#acSelectedIndex}`);
1540
+ } else {
1541
+ this.#textarea?.removeAttribute("aria-activedescendant");
1121
1542
  }
1122
- getValue() {
1123
- return this.#textarea?.value ?? "";
1543
+ }
1544
+ #scheduleStatusUpdate() {
1545
+ if (this.#statusTimer !== null)
1546
+ clearTimeout(this.#statusTimer);
1547
+ this.#statusTimer = setTimeout(() => {
1548
+ this.#statusTimer = null;
1549
+ this.#updateStatusBarNow();
1550
+ }, 100);
1551
+ }
1552
+ #updateStatusBarNow() {
1553
+ if (!this.#statusCount)
1554
+ return;
1555
+ const text = this.#textarea?.value ?? "";
1556
+ const words = countWords(text);
1557
+ const chars = text.length;
1558
+ const maxWords = this.maxWords ?? null;
1559
+ this.#statusCount.innerHTML = renderStatusCount(words, chars, maxWords);
1560
+ const isRequired = !!this.required;
1561
+ if (maxWords && words > maxWords) {
1562
+ this.#internals?.setValidity({ customError: true }, `Content exceeds ${maxWords} word limit (${words} words)`, this.#textarea ?? undefined);
1563
+ } else if (isRequired && text.trim() === "") {
1564
+ this.#internals?.setValidity({ valueMissing: true }, "Please fill in this field.", this.#textarea ?? undefined);
1565
+ } else {
1566
+ this.#internals?.setValidity({});
1124
1567
  }
1125
- setValue(str) {
1126
- if (this.#textarea) {
1127
- this.#textarea.value = str;
1128
- this.#syncFormValue();
1129
- this.#scheduleHighlight();
1130
- this.#updateStatusBarNow();
1131
- } else {
1132
- this.value = str;
1568
+ }
1569
+ getValue() {
1570
+ return this.#textarea?.value ?? "";
1571
+ }
1572
+ setValue(str) {
1573
+ if (this.#textarea) {
1574
+ this.#textarea.value = str;
1575
+ this.#syncFormValue();
1576
+ this.#scheduleHighlight();
1577
+ this.#updateStatusBarNow();
1578
+ if (this.#activeTab === "preview" && this.#previewBody) {
1579
+ this.#renderPreview(str);
1133
1580
  }
1581
+ } else {
1582
+ this.value = str;
1134
1583
  }
1135
- insertText(str) {
1136
- const ta = this.#textarea;
1137
- if (!ta)
1138
- return;
1139
- const start = ta.selectionStart ?? ta.value.length;
1140
- const end = ta.selectionEnd ?? ta.value.length;
1141
- ta.value = ta.value.slice(0, start) + str + ta.value.slice(end);
1142
- const newPos = start + str.length;
1143
- ta.setSelectionRange(newPos, newPos);
1144
- ta.dispatchEvent(new Event("input", { bubbles: true }));
1145
- }
1146
- setSuggestions(list) {
1147
- this.#acSuggestions = list;
1148
- this.#acSelectedIndex = list.length > 0 ? 0 : -1;
1149
- this.#updateDropdown();
1150
- }
1151
- };
1152
- });
1153
-
1584
+ }
1585
+ insertText(str) {
1586
+ const ta = this.#textarea;
1587
+ if (!ta)
1588
+ return;
1589
+ const start = ta.selectionStart ?? ta.value.length;
1590
+ const end = ta.selectionEnd ?? ta.value.length;
1591
+ ta.value = ta.value.slice(0, start) + str + ta.value.slice(end);
1592
+ const newPos = start + str.length;
1593
+ ta.setSelectionRange(newPos, newPos);
1594
+ ta.dispatchEvent(new Event("input", { bubbles: false }));
1595
+ }
1596
+ setSuggestions(list) {
1597
+ this.#acSuggestions = list;
1598
+ this.#acSelectedIndex = list.length > 0 ? 0 : -1;
1599
+ this.#updateDropdown();
1600
+ }
1601
+ }
1602
+ function escapeHtmlStr(s) {
1603
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1604
+ }
1154
1605
  // src/index.ts
1155
- init_element();
1156
1606
  function register() {
1157
1607
  if (!customElements.get("el-dm-markdown-input")) {
1158
- Promise.resolve().then(() => (init_element(), exports_element)).then(({ ElDmMarkdownInput: ElDmMarkdownInput2 }) => {
1159
- customElements.define("el-dm-markdown-input", ElDmMarkdownInput2);
1160
- });
1608
+ customElements.define("el-dm-markdown-input", ElDmMarkdownInput);
1161
1609
  }
1162
1610
  }
1163
1611
  var MarkdownInputHook = {
@@ -1185,5 +1633,5 @@ export {
1185
1633
  ElDmMarkdownInput
1186
1634
  };
1187
1635
 
1188
- //# debugId=765ABA8108F4BB0A64756E2164756E21
1636
+ //# debugId=5F508B6BDB08E0B664756E2164756E21
1189
1637
  //# sourceMappingURL=index.js.map