@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.
@@ -37,11 +37,172 @@ 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/element.ts
200
+ var import_el_base2 = require("@duskmoon-dev/el-base");
201
+ var import_markdown_body = require("@duskmoon-dev/core/components/markdown-body");
202
+
40
203
  // 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`
204
+ var import_el_base = require("@duskmoon-dev/el-base");
205
+ var elementStyles = import_el_base.css`
45
206
  /* ── Custom property defaults with design-system fallbacks ─────────── */
46
207
  :host {
47
208
  --md-border: var(--color-outline, #d0d7de);
@@ -90,6 +251,7 @@ var init_css = __esm(() => {
90
251
  background: var(--md-bg);
91
252
  color: var(--md-text);
92
253
  overflow: hidden;
254
+ height: inherit;
93
255
  }
94
256
 
95
257
  .editor:focus-within {
@@ -139,74 +301,62 @@ var init_css = __esm(() => {
139
301
  border-radius: 3px;
140
302
  }
141
303
 
142
- /* ── Write area (backdrop + textarea overlay) ───────────────────────── */
304
+ /* ── Write area (render-layer + textarea overlay) ──────────────────── */
305
+ /*
306
+ * CodeMirror-style render model: .render-layer sits in normal flow and
307
+ * drives the container height; the textarea is absolutely positioned on
308
+ * top. No scroll sync required — both layers always share the same size.
309
+ */
143
310
  .write-area {
144
311
  position: relative;
145
312
  min-height: 12rem;
146
- flex: 1;
313
+ flex: 1 1 auto;
314
+ }
315
+
316
+ .write-area[hidden] {
317
+ display: none;
147
318
  }
148
319
 
149
320
  /*
150
- * Backdrop: renders syntax-highlighted HTML behind the transparent textarea.
151
- * Must share IDENTICAL font metrics with the textarea to stay pixel-aligned.
321
+ * Render layer: highlighted HTML in normal flow. Drives container height.
322
+ * pointer-events: none lets clicks pass through to the textarea underneath.
323
+ * Font metrics MUST match the textarea exactly for pixel-aligned overlay.
152
324
  */
153
- .backdrop {
154
- position: absolute;
155
- inset: 0;
325
+ .render-layer {
326
+ position: relative;
327
+ z-index: 1;
156
328
  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
-
329
+ min-height: 12rem;
177
330
  font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
178
331
  font-size: 0.875rem;
179
332
  line-height: 1.6;
180
333
  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
334
  white-space: pre-wrap;
191
335
  word-wrap: break-word;
192
336
  overflow-wrap: break-word;
193
- /* Prism token colours are injected via a separate <style id="prism-theme"> */
337
+ color: var(--md-text);
194
338
  }
195
339
 
340
+ /*
341
+ * Textarea: absolute overlay on top of the render layer. Transparent text
342
+ * lets highlighted content show through; caret-color keeps cursor visible.
343
+ * overflow: hidden — the render layer drives height, not the textarea.
344
+ */
196
345
  textarea {
197
- position: relative;
346
+ position: absolute;
347
+ inset: 0;
348
+ z-index: 2;
198
349
  display: block;
199
350
  width: 100%;
200
- min-height: 12rem;
351
+ height: 100%;
201
352
  border: none;
202
353
  outline: none;
203
- resize: vertical;
354
+ resize: none;
204
355
  background: transparent;
205
356
  color: transparent;
206
357
  caret-color: var(--md-text);
207
358
  box-sizing: border-box;
208
-
209
- /* MUST match .backdrop exactly */
359
+ overflow: hidden;
210
360
  font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
211
361
  font-size: 0.875rem;
212
362
  line-height: 1.6;
@@ -229,11 +379,100 @@ var init_css = __esm(() => {
229
379
  .preview-body {
230
380
  padding: 0.75rem;
231
381
  min-height: 12rem;
382
+ height: stretch;
383
+ flex: 1 1 auto;
384
+ display: flex;
385
+ flex-direction: column;
232
386
  overflow-y: auto;
233
387
  color: var(--md-text);
234
388
  /* .markdown-body styles come from @duskmoon-dev/core via the element */
235
389
  }
236
390
 
391
+ .preview-body[hidden] {
392
+ display: none;
393
+ }
394
+
395
+ /* ── Preview skeleton (shown while render pipeline loads) ──────────── */
396
+ .preview-skeleton {
397
+ display: flex;
398
+ flex-direction: column;
399
+ gap: 0.75rem;
400
+ padding: 0.5rem 0;
401
+ }
402
+
403
+ .skeleton-line {
404
+ height: 0.875rem;
405
+ background: linear-gradient(
406
+ 90deg,
407
+ var(--md-bg-toolbar) 25%,
408
+ var(--md-bg-hover) 50%,
409
+ var(--md-bg-toolbar) 75%
410
+ );
411
+ background-size: 200% 100%;
412
+ border-radius: 4px;
413
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
414
+ }
415
+
416
+ @keyframes skeleton-shimmer {
417
+ 0% {
418
+ background-position: 200% 0;
419
+ }
420
+ 100% {
421
+ background-position: -200% 0;
422
+ }
423
+ }
424
+
425
+ @media (prefers-reduced-motion: reduce) {
426
+ .skeleton-line {
427
+ animation: none;
428
+ background: var(--md-bg-hover);
429
+ }
430
+ }
431
+
432
+ /* ── Mermaid diagram blocks ────────────────────────────────────────── */
433
+ .mermaid-diagram {
434
+ display: flex;
435
+ justify-content: center;
436
+ margin: 1rem 0;
437
+ overflow-x: auto;
438
+ }
439
+
440
+ .mermaid-error {
441
+ border-left: 3px solid var(--md-color-error);
442
+ opacity: 0.7;
443
+ position: relative;
444
+ }
445
+
446
+ .mermaid-error::before {
447
+ content: 'Mermaid render failed';
448
+ display: block;
449
+ font-size: 0.75rem;
450
+ color: var(--md-color-error);
451
+ font-family: inherit;
452
+ margin-bottom: 0.25rem;
453
+ padding-left: 0.5rem;
454
+ }
455
+
456
+ /* ── Render error fallback ──────────────────────────────────────────── */
457
+ .render-error-fallback {
458
+ white-space: pre-wrap;
459
+ word-wrap: break-word;
460
+ font-size: 0.875rem;
461
+ opacity: 0.8;
462
+ border-left: 3px solid var(--md-color-error);
463
+ padding-left: 0.75rem;
464
+ color: var(--md-text-muted);
465
+ }
466
+
467
+ .render-error-fallback::before {
468
+ content: 'Preview render failed — showing raw markdown';
469
+ display: block;
470
+ font-size: 0.75rem;
471
+ color: var(--md-color-error);
472
+ font-family: inherit;
473
+ margin-bottom: 0.5rem;
474
+ }
475
+
237
476
  /* ── Status bar ─────────────────────────────────────────────────────── */
238
477
  .status-bar {
239
478
  display: flex;
@@ -269,6 +508,12 @@ var init_css = __esm(() => {
269
508
  background: var(--md-bg-hover);
270
509
  }
271
510
 
511
+ .attach-btn:disabled {
512
+ cursor: not-allowed;
513
+ opacity: 0.5;
514
+ pointer-events: none;
515
+ }
516
+
272
517
  .attach-btn:focus-visible {
273
518
  outline: 2px solid var(--md-accent);
274
519
  outline-offset: 1px;
@@ -403,14 +648,38 @@ var init_css = __esm(() => {
403
648
  text-overflow: ellipsis;
404
649
  white-space: nowrap;
405
650
  }
651
+
652
+ /* ── Reduced motion: disable all transitions and animations ──────── */
653
+ @media (prefers-reduced-motion: reduce) {
654
+ .tab-btn,
655
+ .attach-btn,
656
+ .ac-item,
657
+ .upload-bar {
658
+ transition: none;
659
+ }
660
+ }
406
661
  `;
407
- });
408
662
 
409
663
  // src/highlight.ts
664
+ var PRISM_BASE = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0";
665
+ var PRISM_CORE_URL = `${PRISM_BASE}/prism.min.js`;
666
+ var PRISM_AUTOLOADER_URL = `${PRISM_BASE}/plugins/autoloader/prism-autoloader.min.js`;
667
+ var PRISM_SRI = {
668
+ [PRISM_CORE_URL]: "sha512-7Z9J3l1+EYfeaPKcGXu3MS/7T+w19WtKQY/n+xzmw4hZhJ9tyYmcUS+4QqAlzhicE5LAfMQSF3iFTK9bQdTxXg==",
669
+ [PRISM_AUTOLOADER_URL]: "sha512-SkmBfuA2hqjzEVpmnMt/LINrjop3GKWqsuLSSB3e7iBmYK7JuWw4ldmmxwD9mdm2IRTTi0OxSAfEGvgEi0i2Kw=="
670
+ };
671
+ var PRISM_THEME_DARK_URL = `${PRISM_BASE}/themes/prism-tomorrow.min.css`;
672
+ var PRISM_THEME_LIGHT_URL = `${PRISM_BASE}/themes/prism-coy.min.css`;
673
+ var _prismReady = null;
410
674
  function _loadScript(src) {
411
675
  return new Promise((resolve) => {
412
676
  const script = document.createElement("script");
413
677
  script.src = src;
678
+ const integrity = PRISM_SRI[src];
679
+ if (integrity) {
680
+ script.integrity = integrity;
681
+ script.crossOrigin = "anonymous";
682
+ }
414
683
  script.onload = () => resolve();
415
684
  script.onerror = () => resolve();
416
685
  document.head.appendChild(script);
@@ -422,19 +691,22 @@ function ensurePrism() {
422
691
  if (_prismReady)
423
692
  return _prismReady;
424
693
  _prismReady = _loadScript(PRISM_CORE_URL).then(() => {
425
- if (!window.Prism)
694
+ if (!window.Prism) {
695
+ _prismReady = null;
426
696
  return;
697
+ }
427
698
  window.Prism.manual = true;
428
699
  return _loadScript(PRISM_AUTOLOADER_URL).then(() => {
429
700
  if (window.Prism?.plugins?.autoloader) {
430
701
  window.Prism.plugins.autoloader.languages_path = `${PRISM_BASE}/components/`;
431
702
  }
703
+ return _loadScript(`${PRISM_BASE}/components/prism-markdown.min.js`);
432
704
  });
433
705
  });
434
706
  return _prismReady;
435
707
  }
436
708
  function escapeHtml(text) {
437
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
709
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
438
710
  }
439
711
  function highlightMarkdown(text) {
440
712
  const escaped = escapeHtml(text);
@@ -456,20 +728,35 @@ function applyPrismTheme(shadowRoot, dark) {
456
728
  styleEl.id = "prism-theme";
457
729
  shadowRoot.appendChild(styleEl);
458
730
  }
459
- const expected = `@import url("${themeUrl}");`;
731
+ const previewOverrides = `
732
+ .preview-body pre[class*="language-"] {
733
+ background: transparent;
734
+ margin: 0;
735
+ padding: 0;
736
+ overflow: visible;
737
+ position: static;
738
+ }
739
+ .preview-body pre[class*="language-"] > code {
740
+ background: transparent;
741
+ border: none;
742
+ box-shadow: none;
743
+ padding: 0;
744
+ background-image: none;
745
+ display: block;
746
+ overflow: auto;
747
+ max-height: none;
748
+ height: auto;
749
+ }`;
750
+ const expected = `@import url("${themeUrl}");${previewOverrides}`;
460
751
  if (styleEl.textContent !== expected) {
461
752
  styleEl.textContent = expected;
462
753
  }
463
754
  }
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
755
 
472
756
  // src/upload.ts
757
+ var ACCEPTED_MIME_PREFIXES = ["image/"];
758
+ var ACCEPTED_MIME_EXACT = ["application/pdf"];
759
+ var ACCEPTED_EXTENSIONS = [".zip", ".txt", ".csv", ".json", ".md"];
473
760
  function isAcceptedType(file) {
474
761
  const type = file.type.toLowerCase();
475
762
  if (ACCEPTED_MIME_PREFIXES.some((p) => type.startsWith(p)))
@@ -482,12 +769,19 @@ function isAcceptedType(file) {
482
769
  return false;
483
770
  }
484
771
  function fileToMarkdown(file, url) {
772
+ const isSafeUrl = /^https:\/\//i.test(url) || /^\//.test(url) || /^\.\.?\//.test(url);
773
+ const safeUrl = isSafeUrl ? url.replace(/\(/g, "%28").replace(/\)/g, "%29") : "#unsafe-url";
774
+ const safeName = file.name.replace(/[[\]]/g, "\\$&");
485
775
  if (file.type.startsWith("image/")) {
486
- return `![${file.name}](${url})`;
776
+ return `![${safeName}](${safeUrl})`;
487
777
  }
488
- return `[${file.name}](${url})`;
778
+ return `[${safeName}](${safeUrl})`;
489
779
  }
490
780
  function uploadFile(file, uploadUrl, onProgress) {
781
+ const isSafeUploadUrl = /^https:\/\//i.test(uploadUrl) || /^\//.test(uploadUrl) || /^\.\.?\//.test(uploadUrl);
782
+ if (!isSafeUploadUrl) {
783
+ return Promise.reject(new Error(`[el-dm-markdown-input] upload-url "${uploadUrl}" rejected — only https: and relative URLs are allowed.`));
784
+ }
491
785
  return new Promise((resolve, reject) => {
492
786
  const xhr = new XMLHttpRequest;
493
787
  const body = new FormData;
@@ -504,27 +798,21 @@ function uploadFile(file, uploadUrl, onProgress) {
504
798
  if (data.url) {
505
799
  resolve(data.url);
506
800
  } else {
507
- reject("Upload response missing url field");
801
+ reject(new Error("Upload response missing url field"));
508
802
  }
509
803
  } catch {
510
- reject("Upload response is not valid JSON");
804
+ reject(new Error("Upload response is not valid JSON"));
511
805
  }
512
806
  } else {
513
- reject(`Upload failed with status ${xhr.status}`);
807
+ reject(new Error(`Upload failed with status ${xhr.status}`));
514
808
  }
515
809
  });
516
- xhr.addEventListener("error", () => reject("Network error during upload"));
517
- xhr.addEventListener("abort", () => reject("Upload aborted"));
810
+ xhr.addEventListener("error", () => reject(new Error("Network error during upload")));
811
+ xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
518
812
  xhr.open("POST", uploadUrl);
519
813
  xhr.send(body);
520
814
  });
521
815
  }
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
816
 
529
817
  // src/autocomplete.ts
530
818
  function detectTrigger(value, cursorPos) {
@@ -535,13 +823,13 @@ function detectTrigger(value, cursorPos) {
535
823
  const query = value.slice(i + 1, cursorPos);
536
824
  if (!/\s/.test(query)) {
537
825
  const before = i > 0 ? value[i - 1] : null;
538
- if (before === null || /[\s\n]/.test(before)) {
826
+ if (before === null || /\s/.test(before)) {
539
827
  return { trigger: ch, query, triggerPos: i };
540
828
  }
541
829
  }
542
830
  return null;
543
831
  }
544
- if (/[\s\n]/.test(ch)) {
832
+ if (/\s/.test(ch)) {
545
833
  return null;
546
834
  }
547
835
  i--;
@@ -552,8 +840,11 @@ function confirmSuggestion(value, triggerPos, cursorPos, trigger, replacement) {
552
840
  const before = value.slice(0, triggerPos);
553
841
  const after = value.slice(cursorPos);
554
842
  const inserted = `${trigger}${replacement}`;
555
- const newValue = before + inserted + after;
556
- const newCursorPos = triggerPos + inserted.length;
843
+ const needsSpace = after.length === 0 || after[0] !== " " && after[0] !== `
844
+ `;
845
+ const suffix = needsSpace ? " " : "";
846
+ const newValue = before + inserted + suffix + after;
847
+ const newCursorPos = triggerPos + inserted.length + suffix.length;
557
848
  return { newValue, newCursorPos };
558
849
  }
559
850
  function renderDropdown(suggestions, selectedIndex) {
@@ -569,7 +860,66 @@ function renderDropdown(suggestions, selectedIndex) {
569
860
  return items;
570
861
  }
571
862
  function escapeHtml2(text) {
572
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
863
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
864
+ }
865
+
866
+ // src/pairs.ts
867
+ function handlePairKey(ta, key) {
868
+ if (key !== "`")
869
+ return false;
870
+ const start = ta.selectionStart;
871
+ const end = ta.selectionEnd;
872
+ const value = ta.value;
873
+ if (start !== end) {
874
+ const selected = value.slice(start, end);
875
+ ta.value = value.slice(0, start) + "`" + selected + "`" + value.slice(end);
876
+ ta.setSelectionRange(start + 1, end + 1);
877
+ return true;
878
+ }
879
+ if (start >= 2 && value.slice(start - 2, start) === "``") {
880
+ ta.value = value.slice(0, start) + "`\n\n```" + value.slice(end);
881
+ ta.setSelectionRange(start + 2, start + 2);
882
+ return true;
883
+ }
884
+ ta.value = value.slice(0, start) + "``" + value.slice(end);
885
+ ta.setSelectionRange(start + 1, start + 1);
886
+ return true;
887
+ }
888
+ function handleEnterKey(ta, e) {
889
+ if (e.key !== "Enter")
890
+ return false;
891
+ const pos = ta.selectionStart;
892
+ const value = ta.value;
893
+ if (ta.selectionEnd !== pos)
894
+ return false;
895
+ const lineStart = value.lastIndexOf(`
896
+ `, pos - 1) + 1;
897
+ const currentLine = value.slice(lineStart, pos);
898
+ const result = getLineContinuation(currentLine);
899
+ if (result === null)
900
+ return false;
901
+ e.preventDefault();
902
+ if (result.eraseCurrentLine) {
903
+ const newValue = value.slice(0, lineStart) + value.slice(pos);
904
+ ta.value = newValue;
905
+ ta.setSelectionRange(lineStart, lineStart);
906
+ } else {
907
+ const newValue = value.slice(0, pos) + `
908
+ ` + result.prefix + value.slice(ta.selectionEnd);
909
+ const newPos = pos + 1 + result.prefix.length;
910
+ ta.value = newValue;
911
+ ta.setSelectionRange(newPos, newPos);
912
+ }
913
+ return true;
914
+ }
915
+ function getLineContinuation(line) {
916
+ if (line === "* ")
917
+ return { eraseCurrentLine: true };
918
+ if (/^\* ./.test(line))
919
+ return { prefix: "* ", eraseCurrentLine: false };
920
+ if (/^#{1,6} ./.test(line))
921
+ return { prefix: "", eraseCurrentLine: false };
922
+ return null;
573
923
  }
574
924
 
575
925
  // src/status-bar.ts
@@ -599,155 +949,156 @@ function renderStatusCount(wordCount, charCount, maxWords) {
599
949
  }
600
950
 
601
951
  // 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`
952
+ var import_el_base3 = require("@duskmoon-dev/el-base");
953
+ var coreMarkdownStyles = import_markdown_body.css.replace(/@layer\s+components\s*\{/, "").replace(/\}\s*$/, "");
954
+ var markdownBodySheet = import_el_base3.css`
627
955
  ${coreMarkdownStyles}
628
956
  `;
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;
957
+
958
+ class ElDmMarkdownInput extends import_el_base2.BaseElement {
959
+ static formAssociated = true;
960
+ static properties = {
961
+ name: { type: String, reflect: true, default: "" },
962
+ value: { type: String, default: "" },
963
+ placeholder: { type: String, reflect: true, default: "Write markdown…" },
964
+ disabled: { type: Boolean, reflect: true },
965
+ readonly: { type: Boolean, reflect: true },
966
+ required: { type: Boolean, reflect: true },
967
+ uploadUrl: { type: String, reflect: true, attribute: "upload-url" },
968
+ maxWords: { type: Number, reflect: true, attribute: "max-words" },
969
+ dark: { type: Boolean, reflect: true },
970
+ livePreview: { type: Boolean, reflect: true, attribute: "live-preview" },
971
+ debounce: { type: Number, reflect: true, default: 300 },
972
+ katexCssUrl: { type: String, reflect: true, attribute: "katex-css-url" },
973
+ mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" }
974
+ };
975
+ #internals;
976
+ #initialized = false;
977
+ #activeTab = "write";
978
+ #highlightTimer = null;
979
+ #statusTimer = null;
980
+ #textarea = null;
981
+ #renderLayer = null;
982
+ #writeArea = null;
983
+ #previewBody = null;
984
+ #statusCount = null;
985
+ #acDropdown = null;
986
+ #uploadList = null;
987
+ #fileInput = null;
988
+ #acSuggestions = [];
989
+ #acSelectedIndex = -1;
990
+ #acTriggerPos = -1;
991
+ #acTrigger = null;
992
+ #acGeneration = 0;
993
+ #prevDark = false;
994
+ #renderFn = null;
995
+ #mermaidFn = null;
996
+ #livePreviewTimer = null;
997
+ #renderAbortController = null;
998
+ #lastRenderedSource = null;
999
+ #katexCssInjected = false;
1000
+ #uploadIdCounter = 0;
1001
+ constructor() {
1002
+ super();
1003
+ this.#internals = this.attachInternals();
1004
+ this.attachStyles([elementStyles, markdownBodySheet]);
1005
+ }
1006
+ connectedCallback() {
1007
+ super.connectedCallback();
1008
+ }
1009
+ disconnectedCallback() {
1010
+ this.#renderAbortController?.abort();
1011
+ if (this.#livePreviewTimer !== null)
1012
+ clearTimeout(this.#livePreviewTimer);
1013
+ super.disconnectedCallback();
1014
+ }
1015
+ update() {
1016
+ if (!this.#initialized) {
1017
+ super.update();
1018
+ this.#initialized = true;
1019
+ this.#cacheDOMRefs();
1020
+ this.#attachEventHandlers();
1021
+ this.#initHighlight();
1022
+ const initVal = this.value ?? "";
1023
+ if (this.#textarea) {
1024
+ this.#textarea.value = initVal;
671
1025
  this.#syncFormValue();
1026
+ if (initVal)
1027
+ this.#scheduleHighlight();
672
1028
  }
1029
+ this.#updateStatusBarNow();
1030
+ return;
673
1031
  }
674
- disconnectedCallback() {
675
- this.#resizeObserver?.disconnect();
676
- super.disconnectedCallback();
1032
+ this.#patchDynamicRegions();
1033
+ }
1034
+ #patchDynamicRegions() {
1035
+ const ta = this.#textarea;
1036
+ if (!ta)
1037
+ return;
1038
+ const placeholder = this.placeholder ?? "Write markdown…";
1039
+ ta.placeholder = placeholder;
1040
+ ta.disabled = !!this.disabled;
1041
+ ta.readOnly = !!this.readonly;
1042
+ const attachBtn = this.shadowRoot.querySelector(".attach-btn");
1043
+ if (attachBtn) {
1044
+ attachBtn.disabled = ta.disabled || ta.readOnly;
677
1045
  }
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;
1046
+ const propVal = this.value ?? "";
1047
+ if (propVal !== ta.value) {
1048
+ ta.value = propVal;
1049
+ this.#syncFormValue();
1050
+ this.#scheduleHighlight();
1051
+ if (this.#activeTab === "preview" && this.#previewBody) {
1052
+ this.#renderPreview(propVal);
693
1053
  }
694
- this.#patchDynamicRegions();
695
1054
  }
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;
707
- }
708
- const propVal = this.value ?? "";
709
- if (propVal !== ta.value) {
710
- ta.value = propVal;
711
- this.#syncFormValue();
712
- this.#scheduleHighlight();
1055
+ const dark = !!this.dark;
1056
+ applyPrismTheme(this.shadowRoot, dark);
1057
+ if (dark !== this.#prevDark) {
1058
+ this.#prevDark = dark;
1059
+ if (this.#activeTab === "preview" && this.#previewBody) {
1060
+ this.#lastRenderedSource = null;
1061
+ this.#renderPreview(ta.value);
713
1062
  }
714
- const dark = !!this.dark;
715
- applyPrismTheme(this.shadowRoot, dark);
716
- this.#updateStatusBarNow();
717
1063
  }
718
- render() {
719
- const ph = this.placeholder ?? "Write markdown…";
720
- const disabled = !!this.disabled;
721
- const readonly = !!this.readonly;
722
- return `
1064
+ this.#updateStatusBarNow();
1065
+ }
1066
+ render() {
1067
+ const ph = this.placeholder ?? "Write markdown…";
1068
+ const disabled = !!this.disabled;
1069
+ const readonly = !!this.readonly;
1070
+ return `
723
1071
  <div class="editor">
724
1072
  <div class="toolbar" role="tablist" aria-label="Editor mode">
725
1073
  <button
726
1074
  class="tab-btn"
1075
+ id="tab-write"
727
1076
  data-tab="write"
728
1077
  role="tab"
729
1078
  aria-selected="true"
730
1079
  aria-controls="write-panel"
1080
+ tabindex="0"
731
1081
  >Write</button>
732
1082
  <button
733
1083
  class="tab-btn"
1084
+ id="tab-preview"
734
1085
  data-tab="preview"
735
1086
  role="tab"
736
1087
  aria-selected="false"
737
1088
  aria-controls="preview-panel"
1089
+ tabindex="-1"
738
1090
  >Preview</button>
739
1091
  </div>
740
1092
 
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>
1093
+ <div class="write-area" id="write-panel" role="tabpanel" aria-labelledby="tab-write">
1094
+ <div class="render-layer" aria-hidden="true"></div>
745
1095
  <textarea
746
1096
  aria-label="Markdown editor"
747
1097
  aria-haspopup="listbox"
1098
+ aria-expanded="false"
748
1099
  aria-autocomplete="list"
749
1100
  aria-controls="ac-dropdown"
750
- placeholder="${ph}"
1101
+ placeholder="${escapeHtmlStr(ph)}"
751
1102
  ${disabled ? "disabled" : ""}
752
1103
  ${readonly ? "readonly" : ""}
753
1104
  spellcheck="false"
@@ -761,7 +1112,7 @@ var init_element = __esm(() => {
761
1112
  class="preview-body markdown-body"
762
1113
  id="preview-panel"
763
1114
  role="tabpanel"
764
- aria-label="Markdown preview"
1115
+ aria-labelledby="tab-preview"
765
1116
  hidden
766
1117
  ></div>
767
1118
 
@@ -784,401 +1135,494 @@ var init_element = __esm(() => {
784
1135
  </div>
785
1136
  <ul id="ac-dropdown" class="ac-dropdown" role="listbox" aria-label="Suggestions" hidden></ul>
786
1137
  `;
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)
1138
+ }
1139
+ #cacheDOMRefs() {
1140
+ this.#textarea = this.shadowRoot.querySelector("textarea");
1141
+ this.#renderLayer = this.shadowRoot.querySelector(".render-layer");
1142
+ this.#writeArea = this.shadowRoot.querySelector(".write-area");
1143
+ this.#previewBody = this.shadowRoot.querySelector(".preview-body");
1144
+ this.#statusCount = this.shadowRoot.querySelector(".status-bar-count");
1145
+ this.#acDropdown = this.shadowRoot.querySelector(".ac-dropdown");
1146
+ this.#uploadList = this.shadowRoot.querySelector(".upload-list");
1147
+ this.#fileInput = this.shadowRoot.querySelector(".file-input");
1148
+ }
1149
+ #attachEventHandlers() {
1150
+ const ta = this.#textarea;
1151
+ if (!ta)
1152
+ return;
1153
+ ta.addEventListener("input", () => {
1154
+ this.#syncFormValue();
1155
+ this.emit("change", { value: ta.value });
1156
+ this.#scheduleHighlight();
1157
+ this.#scheduleStatusUpdate();
1158
+ this.#handleAutocompleteInput();
1159
+ this.#scheduleLivePreview();
1160
+ });
1161
+ ta.addEventListener("blur", () => {
1162
+ setTimeout(() => {
1163
+ if (!this.shadowRoot?.activeElement) {
1164
+ this.#closeDropdown();
1165
+ }
1166
+ }, 150);
1167
+ });
1168
+ ta.addEventListener("keydown", (e) => {
1169
+ if (this.#acSuggestions.length > 0 && !this.#acDropdown?.hidden) {
1170
+ this.#handleDropdownKeydown(e);
1171
+ if (e.defaultPrevented)
1172
+ return;
1173
+ }
1174
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "P") {
1175
+ e.preventDefault();
1176
+ this.#switchTab(this.#activeTab === "write" ? "preview" : "write");
1177
+ return;
1178
+ }
1179
+ if (this.disabled || this.readonly) {
802
1180
  return;
803
- ta.addEventListener("input", () => {
1181
+ }
1182
+ if (!e.ctrlKey && !e.metaKey && !e.altKey && handlePairKey(ta, e.key)) {
1183
+ e.preventDefault();
804
1184
  this.#syncFormValue();
805
1185
  this.emit("change", { value: ta.value });
806
1186
  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");
1187
+ return;
1188
+ }
1189
+ if (e.key === "Enter" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1190
+ if (handleEnterKey(ta, e)) {
1191
+ this.#syncFormValue();
1192
+ this.emit("change", { value: ta.value });
1193
+ this.#scheduleHighlight();
1194
+ this.#scheduleStatusUpdate();
830
1195
  }
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
1196
  }
850
- ta.addEventListener("paste", (e) => {
1197
+ });
1198
+ const writeArea = this.#writeArea;
1199
+ if (writeArea) {
1200
+ writeArea.addEventListener("dragover", (e) => {
1201
+ if (this.disabled)
1202
+ return;
851
1203
  if (this.readonly)
852
1204
  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
- }
1205
+ e.preventDefault();
1206
+ writeArea.style.opacity = "0.8";
858
1207
  });
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);
1208
+ writeArea.addEventListener("dragleave", () => {
1209
+ writeArea.style.opacity = "";
865
1210
  });
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);
1211
+ writeArea.addEventListener("drop", (e) => {
1212
+ e.preventDefault();
1213
+ writeArea.style.opacity = "";
1214
+ if (this.disabled)
1215
+ return;
1216
+ if (this.readonly)
1217
+ return;
1218
+ const files = Array.from(e.dataTransfer?.files ?? []).filter(isAcceptedType);
870
1219
  files.forEach((f) => this.#startUpload(f));
871
- if (this.#fileInput)
872
- this.#fileInput.value = "";
873
1220
  });
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
- });
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
1221
  }
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)
1222
+ ta.addEventListener("paste", (e) => {
1223
+ if (this.disabled)
917
1224
  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 ?? "");
1225
+ if (this.readonly)
1226
+ return;
1227
+ const imageFiles = Array.from(e.clipboardData?.files ?? []).filter((f) => f.type.startsWith("image/"));
1228
+ if (imageFiles.length > 0) {
1229
+ e.preventDefault();
1230
+ imageFiles.forEach((f) => this.#startUpload(f));
1231
+ }
1232
+ });
1233
+ const toolbar = this.shadowRoot.querySelector(".toolbar");
1234
+ toolbar?.addEventListener("click", (e) => {
1235
+ const btn = e.target.closest(".tab-btn");
1236
+ const tab = btn?.dataset.tab;
1237
+ if (tab)
1238
+ this.#switchTab(tab);
1239
+ });
1240
+ toolbar?.addEventListener("keydown", (e) => {
1241
+ const kev = e;
1242
+ if (kev.key === "ArrowLeft" || kev.key === "ArrowRight") {
1243
+ kev.preventDefault();
1244
+ const nextTab = this.#activeTab === "write" ? "preview" : "write";
1245
+ this.#switchTab(nextTab);
1246
+ const nextBtn = this.shadowRoot.querySelector(`.tab-btn[data-tab="${nextTab}"]`);
1247
+ nextBtn?.focus();
1248
+ }
1249
+ });
1250
+ const attachBtn = this.shadowRoot.querySelector(".attach-btn");
1251
+ attachBtn?.addEventListener("click", () => this.#fileInput?.click());
1252
+ this.#fileInput?.addEventListener("change", () => {
1253
+ const files = Array.from(this.#fileInput?.files ?? []).filter(isAcceptedType);
1254
+ files.forEach((f) => this.#startUpload(f));
1255
+ if (this.#fileInput)
1256
+ this.#fileInput.value = "";
1257
+ });
1258
+ this.#acDropdown?.addEventListener("click", (e) => {
1259
+ const item = e.target.closest("[data-ac-index]");
1260
+ if (item) {
1261
+ const idx = parseInt(item.dataset.acIndex ?? "-1", 10);
1262
+ if (idx >= 0) {
1263
+ this.#acSelectedIndex = idx;
1264
+ this.#confirmAutocomplete();
929
1265
  }
930
- } else {
931
- this.#writeArea?.removeAttribute("hidden");
932
- this.#previewBody?.setAttribute("hidden", "");
933
1266
  }
1267
+ });
1268
+ }
1269
+ #initHighlight() {
1270
+ const dark = !!this.dark;
1271
+ applyPrismTheme(this.shadowRoot, dark);
1272
+ ensurePrism().then(() => {
1273
+ if (this.#textarea && this.#renderLayer) {
1274
+ this.#renderLayer.innerHTML = highlightMarkdown(this.#textarea.value);
1275
+ }
1276
+ });
1277
+ }
1278
+ #scheduleHighlight() {
1279
+ if (this.#highlightTimer !== null)
1280
+ clearTimeout(this.#highlightTimer);
1281
+ this.#highlightTimer = setTimeout(() => {
1282
+ this.#highlightTimer = null;
1283
+ if (this.#renderLayer && this.#textarea) {
1284
+ this.#renderLayer.innerHTML = highlightMarkdown(this.#textarea.value);
1285
+ }
1286
+ }, 60);
1287
+ }
1288
+ #switchTab(tab) {
1289
+ if (tab === this.#activeTab)
1290
+ return;
1291
+ this.#activeTab = tab;
1292
+ const writeBtns = this.shadowRoot.querySelectorAll(".tab-btn");
1293
+ writeBtns.forEach((btn) => {
1294
+ const isActive = btn.dataset.tab === tab;
1295
+ btn.setAttribute("aria-selected", String(isActive));
1296
+ btn.setAttribute("tabindex", isActive ? "0" : "-1");
1297
+ });
1298
+ if (tab === "preview") {
1299
+ this.#writeArea?.setAttribute("hidden", "");
1300
+ if (this.#previewBody) {
1301
+ this.#previewBody.removeAttribute("hidden");
1302
+ this.#renderPreview(this.#textarea?.value ?? "");
1303
+ }
1304
+ } else {
1305
+ this.#writeArea?.removeAttribute("hidden");
1306
+ this.#previewBody?.setAttribute("hidden", "");
934
1307
  }
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;
1308
+ }
1309
+ async#loadRenderPipeline() {
1310
+ if (this.#renderFn && this.#mermaidFn) {
1311
+ return { renderMarkdown: this.#renderFn, renderMermaidBlocks: this.#mermaidFn };
1312
+ }
1313
+ const mod = await Promise.resolve().then(() => (init_render(), exports_render));
1314
+ this.#renderFn = mod.renderMarkdown;
1315
+ this.#mermaidFn = mod.renderMermaidBlocks;
1316
+ return { renderMarkdown: this.#renderFn, renderMermaidBlocks: this.#mermaidFn };
1317
+ }
1318
+ async#renderPreview(source, force = false) {
1319
+ const preview = this.#previewBody;
1320
+ if (!preview)
1321
+ return;
1322
+ if (!force && this.#lastRenderedSource === source && this.#renderFn !== null) {
1323
+ return;
979
1324
  }
980
- #syncFormValue() {
981
- this.#internals?.setFormValue(this.#textarea?.value ?? "");
1325
+ this.#renderAbortController?.abort();
1326
+ const controller = new AbortController;
1327
+ this.#renderAbortController = controller;
1328
+ this.emit("render-start", {});
1329
+ preview.setAttribute("aria-busy", "true");
1330
+ if (!this.#renderFn) {
1331
+ preview.innerHTML = `
1332
+ <div class="preview-skeleton" aria-label="Loading preview…">
1333
+ <div class="skeleton-line" style="width:90%"></div>
1334
+ <div class="skeleton-line" style="width:75%"></div>
1335
+ <div class="skeleton-line" style="width:85%"></div>
1336
+ <div class="skeleton-line" style="width:60%"></div>
1337
+ </div>`;
982
1338
  }
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");
1339
+ try {
1340
+ const { renderMarkdown: renderMarkdown2, renderMermaidBlocks: renderMermaidBlocks2 } = await this.#loadRenderPipeline();
1341
+ if (controller.signal.aborted) {
1342
+ preview.removeAttribute("aria-busy");
990
1343
  return;
991
1344
  }
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)
1345
+ const html = await renderMarkdown2(source);
1346
+ if (controller.signal.aborted) {
1347
+ preview.removeAttribute("aria-busy");
1348
+ return;
1349
+ }
1350
+ preview.innerHTML = html;
1351
+ preview.removeAttribute("aria-busy");
1352
+ this.#ensureKatexCss();
1353
+ const mermaidSrc = this.mermaidSrc;
1354
+ await renderMermaidBlocks2(preview, mermaidSrc);
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
+ this.#lastRenderedSource = source;
1360
+ this.emit("render-done", { html });
1361
+ } catch (err) {
1362
+ if (controller.signal.aborted) {
1363
+ preview.removeAttribute("aria-busy");
1364
+ return;
1365
+ }
1366
+ preview.removeAttribute("aria-busy");
1367
+ preview.innerHTML = `<pre class="render-error-fallback">${escapeHtmlStr(source)}</pre>`;
1368
+ this.emit("render-error", { error: err instanceof Error ? err : new Error(String(err)) });
1369
+ }
1370
+ }
1371
+ #ensureKatexCss() {
1372
+ if (this.#katexCssInjected)
1373
+ return;
1374
+ this.#katexCssInjected = true;
1375
+ const rawUrl = this.katexCssUrl ?? "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
1376
+ const katexUrl = /^https:\/\//i.test(rawUrl) ? rawUrl : "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
1377
+ if (katexUrl !== rawUrl) {
1378
+ console.warn(`[el-dm-markdown-input] katex-css-url "${rawUrl}" rejected — only https: URLs are allowed.`);
1379
+ }
1380
+ const link = document.createElement("link");
1381
+ link.id = "katex-css";
1382
+ link.rel = "stylesheet";
1383
+ link.href = katexUrl;
1384
+ this.shadowRoot.appendChild(link);
1385
+ }
1386
+ #scheduleLivePreview() {
1387
+ if (!this.livePreview)
1388
+ return;
1389
+ if (this.#activeTab !== "preview")
1390
+ return;
1391
+ if (this.#livePreviewTimer !== null)
1392
+ clearTimeout(this.#livePreviewTimer);
1393
+ const ms = this.debounce ?? 300;
1394
+ this.#livePreviewTimer = setTimeout(() => {
1395
+ this.#livePreviewTimer = null;
1396
+ this.#renderPreview(this.#textarea?.value ?? "");
1397
+ }, ms);
1398
+ }
1399
+ #syncFormValue() {
1400
+ this.#internals?.setFormValue(this.#textarea?.value ?? "");
1401
+ }
1402
+ #startUpload(file) {
1403
+ this.emit("upload-start", { file });
1404
+ const id = `upload-${++this.#uploadIdCounter}`;
1405
+ const uploadUrl = this.uploadUrl;
1406
+ if (!uploadUrl) {
1407
+ this.emit("upload-error", { file, error: "no upload-url set" });
1408
+ this.#showUploadError(file, "no upload-url set");
1409
+ return;
1410
+ }
1411
+ this.#addProgressRow(id, file.name);
1412
+ uploadFile(file, uploadUrl, (pct) => {
1413
+ this.#updateProgressRow(id, pct);
1414
+ }).then((url) => {
1415
+ this.#removeUploadRow(id);
1416
+ const markdown = fileToMarkdown(file, url);
1417
+ this.insertText(markdown);
1418
+ this.emit("upload-done", { file, url, markdown });
1419
+ }).catch((err) => {
1420
+ this.#removeUploadRow(id);
1421
+ const errorMsg = err instanceof Error ? err.message : "Upload failed";
1422
+ this.emit("upload-error", { file, error: errorMsg });
1423
+ this.#showUploadError(file, errorMsg);
1424
+ });
1425
+ }
1426
+ #addProgressRow(id, filename) {
1427
+ if (!this.#uploadList)
1428
+ return;
1429
+ const row = document.createElement("div");
1430
+ row.className = "upload-row";
1431
+ row.id = id;
1432
+ row.innerHTML = `
1014
1433
  <span class="upload-filename">${escapeHtmlStr(filename)}</span>
1015
- <div class="upload-bar-track">
1434
+ <div class="upload-bar-track" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Uploading ${escapeHtmlStr(filename)}">
1016
1435
  <div class="upload-bar" style="width: 0%"></div>
1017
1436
  </div>
1018
1437
  `;
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 = `
1438
+ this.#uploadList.appendChild(row);
1439
+ }
1440
+ #updateProgressRow(id, pct) {
1441
+ const track = this.#uploadList?.querySelector(`#${id} .upload-bar-track`);
1442
+ if (track)
1443
+ track.setAttribute("aria-valuenow", String(pct));
1444
+ const bar = this.#uploadList?.querySelector(`#${id} .upload-bar`);
1445
+ if (bar)
1446
+ bar.style.width = `${pct}%`;
1447
+ }
1448
+ #removeUploadRow(id) {
1449
+ this.#uploadList?.querySelector(`#${id}`)?.remove();
1450
+ }
1451
+ #showUploadError(file, message) {
1452
+ if (!this.#uploadList)
1453
+ return;
1454
+ const row = document.createElement("div");
1455
+ row.className = "upload-error-row";
1456
+ row.setAttribute("role", "alert");
1457
+ row.innerHTML = `
1035
1458
  <span class="upload-error-msg">${escapeHtmlStr(file.name)}: ${escapeHtmlStr(message)}</span>
1036
1459
  `;
1037
- this.#uploadList.appendChild(row);
1038
- setTimeout(() => row.remove(), 4000);
1460
+ this.#uploadList.appendChild(row);
1461
+ setTimeout(() => row.remove(), 4000);
1462
+ }
1463
+ #handleAutocompleteInput() {
1464
+ const ta = this.#textarea;
1465
+ if (!ta)
1466
+ return;
1467
+ const result = detectTrigger(ta.value, ta.selectionStart ?? 0);
1468
+ if (!result) {
1469
+ this.#closeDropdown();
1470
+ return;
1039
1471
  }
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
- }
1472
+ const { trigger, query, triggerPos } = result;
1473
+ this.#acTrigger = trigger;
1474
+ this.#acTriggerPos = triggerPos;
1475
+ const gen = ++this.#acGeneration;
1476
+ const resolve = (list) => {
1477
+ if (gen === this.#acGeneration)
1478
+ this.setSuggestions(list);
1479
+ };
1480
+ if (trigger === "@") {
1481
+ this.emit("mention-query", { trigger, query, resolve });
1482
+ } else {
1483
+ this.emit("reference-query", { trigger, query, resolve });
1058
1484
  }
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":
1485
+ }
1486
+ #handleDropdownKeydown(e) {
1487
+ const len = this.#acSuggestions.length;
1488
+ if (len === 0)
1489
+ return;
1490
+ switch (e.key) {
1491
+ case "ArrowDown":
1492
+ e.preventDefault();
1493
+ this.#acSelectedIndex = (this.#acSelectedIndex + 1) % len;
1494
+ this.#updateDropdown();
1495
+ break;
1496
+ case "ArrowUp":
1497
+ e.preventDefault();
1498
+ this.#acSelectedIndex = (this.#acSelectedIndex - 1 + len) % len;
1499
+ this.#updateDropdown();
1500
+ break;
1501
+ case "Enter":
1502
+ case "Tab":
1503
+ if (this.#acSelectedIndex >= 0) {
1070
1504
  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
- }
1505
+ this.#confirmAutocomplete();
1506
+ }
1507
+ break;
1508
+ case "Escape":
1509
+ this.#closeDropdown();
1510
+ break;
1111
1511
  }
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
- }
1512
+ }
1513
+ #confirmAutocomplete() {
1514
+ const ta = this.#textarea;
1515
+ if (!ta || this.#acSelectedIndex < 0 || !this.#acTrigger)
1516
+ return;
1517
+ const suggestion = this.#acSuggestions[this.#acSelectedIndex];
1518
+ if (!suggestion)
1519
+ return;
1520
+ const { newValue, newCursorPos } = confirmSuggestion(ta.value, this.#acTriggerPos, ta.selectionStart ?? ta.value.length, this.#acTrigger, suggestion.id);
1521
+ ta.value = newValue;
1522
+ ta.setSelectionRange(newCursorPos, newCursorPos);
1523
+ this.#syncFormValue();
1524
+ this.emit("change", { value: ta.value });
1525
+ this.#scheduleHighlight();
1526
+ this.#scheduleStatusUpdate();
1527
+ this.#closeDropdown();
1528
+ }
1529
+ #closeDropdown() {
1530
+ this.#acGeneration++;
1531
+ this.#acSuggestions = [];
1532
+ this.#acSelectedIndex = -1;
1533
+ this.#acTrigger = null;
1534
+ this.#acTriggerPos = -1;
1535
+ if (this.#acDropdown) {
1536
+ this.#acDropdown.innerHTML = "";
1537
+ this.#acDropdown.hidden = true;
1127
1538
  }
1128
- #scheduleStatusUpdate() {
1129
- if (this.#statusTimer !== null)
1130
- clearTimeout(this.#statusTimer);
1131
- this.#statusTimer = setTimeout(() => {
1132
- this.#statusTimer = null;
1133
- this.#updateStatusBarNow();
1134
- }, 100);
1539
+ this.#textarea?.setAttribute("aria-expanded", "false");
1540
+ this.#textarea?.removeAttribute("aria-activedescendant");
1541
+ }
1542
+ #updateDropdown() {
1543
+ if (!this.#acDropdown)
1544
+ return;
1545
+ if (this.#acSuggestions.length === 0) {
1546
+ this.#acDropdown.hidden = true;
1547
+ this.#textarea?.setAttribute("aria-expanded", "false");
1548
+ this.#textarea?.removeAttribute("aria-activedescendant");
1549
+ return;
1135
1550
  }
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);
1551
+ this.#acDropdown.innerHTML = renderDropdown(this.#acSuggestions, this.#acSelectedIndex);
1552
+ this.#acDropdown.hidden = false;
1553
+ this.#textarea?.setAttribute("aria-expanded", "true");
1554
+ if (this.#acSelectedIndex >= 0) {
1555
+ this.#textarea?.setAttribute("aria-activedescendant", `ac-item-${this.#acSelectedIndex}`);
1556
+ } else {
1557
+ this.#textarea?.removeAttribute("aria-activedescendant");
1144
1558
  }
1145
- getValue() {
1146
- return this.#textarea?.value ?? "";
1559
+ }
1560
+ #scheduleStatusUpdate() {
1561
+ if (this.#statusTimer !== null)
1562
+ clearTimeout(this.#statusTimer);
1563
+ this.#statusTimer = setTimeout(() => {
1564
+ this.#statusTimer = null;
1565
+ this.#updateStatusBarNow();
1566
+ }, 100);
1567
+ }
1568
+ #updateStatusBarNow() {
1569
+ if (!this.#statusCount)
1570
+ return;
1571
+ const text = this.#textarea?.value ?? "";
1572
+ const words = countWords(text);
1573
+ const chars = text.length;
1574
+ const maxWords = this.maxWords ?? null;
1575
+ this.#statusCount.innerHTML = renderStatusCount(words, chars, maxWords);
1576
+ const isRequired = !!this.required;
1577
+ if (maxWords && words > maxWords) {
1578
+ this.#internals?.setValidity({ customError: true }, `Content exceeds ${maxWords} word limit (${words} words)`, this.#textarea ?? undefined);
1579
+ } else if (isRequired && text.trim() === "") {
1580
+ this.#internals?.setValidity({ valueMissing: true }, "Please fill in this field.", this.#textarea ?? undefined);
1581
+ } else {
1582
+ this.#internals?.setValidity({});
1147
1583
  }
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;
1584
+ }
1585
+ getValue() {
1586
+ return this.#textarea?.value ?? "";
1587
+ }
1588
+ setValue(str) {
1589
+ if (this.#textarea) {
1590
+ this.#textarea.value = str;
1591
+ this.#syncFormValue();
1592
+ this.#scheduleHighlight();
1593
+ this.#updateStatusBarNow();
1594
+ if (this.#activeTab === "preview" && this.#previewBody) {
1595
+ this.#renderPreview(str);
1156
1596
  }
1597
+ } else {
1598
+ this.value = str;
1157
1599
  }
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
- });
1600
+ }
1601
+ insertText(str) {
1602
+ const ta = this.#textarea;
1603
+ if (!ta)
1604
+ return;
1605
+ const start = ta.selectionStart ?? ta.value.length;
1606
+ const end = ta.selectionEnd ?? ta.value.length;
1607
+ ta.value = ta.value.slice(0, start) + str + ta.value.slice(end);
1608
+ const newPos = start + str.length;
1609
+ ta.setSelectionRange(newPos, newPos);
1610
+ ta.dispatchEvent(new Event("input", { bubbles: false }));
1611
+ }
1612
+ setSuggestions(list) {
1613
+ this.#acSuggestions = list;
1614
+ this.#acSelectedIndex = list.length > 0 ? 0 : -1;
1615
+ this.#updateDropdown();
1616
+ }
1617
+ }
1618
+ function escapeHtmlStr(s) {
1619
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1620
+ }
1176
1621
 
1177
1622
  // src/register.ts
1178
- init_element();
1179
1623
  if (!customElements.get("el-dm-markdown-input")) {
1180
1624
  customElements.define("el-dm-markdown-input", ElDmMarkdownInput);
1181
1625
  }
1182
1626
 
1183
- //# debugId=05F4B33017FA700264756E2164756E21
1627
+ //# debugId=CBBAC14DC8E0779464756E2164756E21
1184
1628
  //# sourceMappingURL=register.js.map