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