@gilav21/shadcn-angular-rte 0.1.1 → 0.1.2

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.
@@ -72,6 +72,494 @@ function prefersReducedMotion() {
72
72
  return globalThis.window?.matchMedia('(prefers-reduced-motion: reduce)').matches ?? false;
73
73
  }
74
74
 
75
+ /**
76
+ * The syntax tokenizer, shared by every surface that shows code.
77
+ *
78
+ * It began inside `ui-code-block` as private methods over a private pattern
79
+ * table. The rich text editor and its read-only view need exactly the same
80
+ * tokens — `rich-text-markdown.service.ts` has always stored and round-tripped
81
+ * a code block's language, and the sanitizer has always allowed the
82
+ * `language-*` and `token-*` classes — so the tokenizer moved here rather than
83
+ * being written a second time. Rendering the whole `ui-code-block` COMPONENT
84
+ * inside a document was the alternative and is wrong: it brings a copy button,
85
+ * a line-number gutter, fold chevrons and a hard-coded dark surface into prose,
86
+ * and none of that can live inside a contenteditable.
87
+ *
88
+ * Two rules the callers rely on:
89
+ *
90
+ * 1. **A line is tokenized on its own.** Highlighting never spans lines, so a
91
+ * multi-line string or block comment is coloured per line. That is what lets
92
+ * the editor re-highlight one line without re-reading the block.
93
+ * 2. **An unknown language is not a guess.** {@link languagePatternsFor}
94
+ * returns `null` for a language it does not know, and for none at all.
95
+ * `ui-code-block` is a code viewer and keeps its documented TypeScript
96
+ * fallback, but a DOCUMENT is mostly prose: a fence with no language, or one
97
+ * tagged `text`, `log` or `diff`, would otherwise be coloured by TypeScript
98
+ * rules, inventing keywords in the reader's notes.
99
+ */
100
+ /**
101
+ * Build a word-boundary keyword matcher from a list. Constructed at runtime so
102
+ * the (legitimately long) per-language keyword set is a maintainable array
103
+ * rather than one giant, hard-to-read regex literal.
104
+ */
105
+ const keywordPattern = (words) => new RegExp(String.raw `\b(${words.join('|')})\b`);
106
+ const TS_KEYWORDS = ['const', 'let', 'var', 'function', 'class', 'import', 'from', 'return', 'if', 'else', 'for', 'while', 'export', 'interface', 'type', 'public', 'private', 'protected', 'implements', 'extends', 'new', 'this', 'true', 'false', 'null', 'undefined', 'void', 'async', 'await'];
107
+ const JS_KEYWORDS = ['const', 'let', 'var', 'function', 'class', 'import', 'from', 'return', 'if', 'else', 'for', 'while', 'export', 'new', 'this', 'true', 'false', 'null', 'undefined', 'void', 'async', 'await'];
108
+ const PYTHON_KEYWORDS = ['def', 'class', 'import', 'from', 'if', 'else', 'elif', 'for', 'while', 'return', 'try', 'except', 'finally', 'with', 'as', 'pass', 'break', 'continue', 'lambda', 'yield', 'async', 'await', 'True', 'False', 'None'];
109
+ const JAVA_KEYWORDS = ['public', 'private', 'protected', 'class', 'interface', 'enum', 'extends', 'implements', 'new', 'this', 'super', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'void', 'int', 'boolean', 'char', 'byte', 'short', 'long', 'float', 'double', 'static', 'final', 'abstract', 'synchronized', 'volatile', 'transient', 'native', 'strictfp', 'instanceof', 'null', 'true', 'false'];
110
+ const CSHARP_KEYWORDS = ['public', 'private', 'protected', 'internal', 'class', 'struct', 'record', 'interface', 'enum', 'delegate', 'event', 'void', 'int', 'string', 'bool', 'var', 'async', 'await', 'Task', 'return', 'if', 'else', 'for', 'foreach', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'new', 'this', 'base', 'using', 'namespace', 'static', 'readonly', 'const', 'override', 'virtual', 'abstract', 'sealed', 'get', 'set', 'value'];
111
+ /** The built-in language rules, keyed by lowercase language name. */
112
+ const LANGUAGE_PATTERNS = {
113
+ typescript: [
114
+ { type: 'comment', regex: /\/\/.*/ },
115
+ { type: 'string', regex: /(["'])(?:(?=(\\?))\2.)*?\1/ },
116
+ { type: 'keyword', regex: keywordPattern(TS_KEYWORDS) },
117
+ { type: 'number', regex: /\b\d+\b/ },
118
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
119
+ ],
120
+ javascript: [
121
+ { type: 'comment', regex: /\/\/.*/ },
122
+ { type: 'string', regex: /(["'])(?:(?=(\\?))\2.)*?\1/ },
123
+ { type: 'keyword', regex: keywordPattern(JS_KEYWORDS) },
124
+ { type: 'number', regex: /\b\d+\b/ },
125
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
126
+ ],
127
+ python: [
128
+ { type: 'comment', regex: /#.*/ },
129
+ { type: 'string', regex: /(["'])(?:(?=(\\?))\2.)*?\1/ },
130
+ { type: 'decorator', regex: /@[\w.]+/ },
131
+ { type: 'keyword', regex: keywordPattern(PYTHON_KEYWORDS) },
132
+ { type: 'number', regex: /\b\d+\b/ },
133
+ { type: 'function', regex: /\b[a-zA-Z_]\w*(?=\()/ },
134
+ ],
135
+ java: [
136
+ { type: 'comment', regex: /\/\/.*/ },
137
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
138
+ { type: 'keyword', regex: keywordPattern(JAVA_KEYWORDS) },
139
+ { type: 'decorator', regex: /@\w+/ },
140
+ { type: 'number', regex: /\b\d+\b/ },
141
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
142
+ ],
143
+ html: [
144
+ { type: 'comment', regex: /<!--[\s\S]*?-->/ },
145
+ { type: 'tag', regex: /<\/?[a-z0-9-]+/i },
146
+ { type: 'attr', regex: /[a-z0-9-]{1,256}(?==)/i },
147
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
148
+ ],
149
+ xml: [
150
+ { type: 'comment', regex: /<!--[\s\S]*?-->/ },
151
+ { type: 'decorator', regex: /<\?xml[\s\S]*?\?>|<!\[CDATA\[[\s\S]*?\]\]>|<!DOCTYPE[^>]*>/i },
152
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
153
+ { type: 'tag', regex: /<\/?[a-zA-Z_][\w.\-:]{0,256}/ },
154
+ { type: 'attr', regex: /[a-zA-Z_][\w.\-:]{0,256}(?==)/ },
155
+ { type: 'keyword', regex: /&(?:[a-zA-Z]+|#\d+|#x[0-9a-fA-F]+);/ },
156
+ { type: 'number', regex: /\b\d+\b/ },
157
+ ],
158
+ css: [
159
+ { type: 'comment', regex: /\/\*[\s\S]*?\*\// },
160
+ { type: 'selector', regex: /[.#]?[a-zA-Z0-9_-]{1,256}(?=\{)/ },
161
+ { type: 'property', regex: /[a-z0-9-]{1,256}(?=:)/i },
162
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
163
+ { type: 'number', regex: /\b\d+(?:px|rem|em|%|vh|vw|s|ms|deg)?\b/ },
164
+ ],
165
+ json: [
166
+ { type: 'function', regex: /"[^"]+":/ },
167
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
168
+ { type: 'keyword', regex: /\b(true|false|null)\b/ },
169
+ { type: 'number', regex: /\b\d+\b/ },
170
+ ],
171
+ csharp: [
172
+ { type: 'comment', regex: /\/\/.*/ },
173
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
174
+ { type: 'decorator', regex: /\[[a-zA-Z]\w*\]/ },
175
+ { type: 'keyword', regex: keywordPattern(CSHARP_KEYWORDS) },
176
+ { type: 'number', regex: /\b\d+\b/ },
177
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
178
+ ],
179
+ yaml: [
180
+ { type: 'comment', regex: /#.*/ },
181
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
182
+ { type: 'attr', regex: /[a-zA-Z0-9_-]{1,256}(?=:)/ },
183
+ { type: 'keyword', regex: /\b(true|false|null|yes|no|on|off)\b/ },
184
+ { type: 'number', regex: /\b\d+(\.\d+)?\b/ },
185
+ ],
186
+ bash: [
187
+ { type: 'comment', regex: /#.*/ },
188
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
189
+ { type: 'keyword', regex: /\b(echo|ls|cd|pwd|mkdir|rm|cp|mv|touch|cat|grep|open|ssh|git|npm|node|ng|sudo|chmod|chown)\b/ },
190
+ { type: 'decorator', regex: /\$\w+/ },
191
+ ],
192
+ };
193
+ /**
194
+ * Spellings an author writes in a fence that mean a language the table already
195
+ * knows. A markdown fence carries whatever the author typed, and "ts" is far
196
+ * more common than "typescript".
197
+ */
198
+ const LANGUAGE_ALIASES = {
199
+ ts: 'typescript',
200
+ tsx: 'typescript',
201
+ js: 'javascript',
202
+ jsx: 'javascript',
203
+ mjs: 'javascript',
204
+ cjs: 'javascript',
205
+ py: 'python',
206
+ 'c#': 'csharp',
207
+ cs: 'csharp',
208
+ yml: 'yaml',
209
+ sh: 'bash',
210
+ shell: 'bash',
211
+ zsh: 'bash',
212
+ htm: 'html',
213
+ svg: 'xml',
214
+ scss: 'css',
215
+ };
216
+ /**
217
+ * The rules for a language, or `null` when it is one this module does not know.
218
+ *
219
+ * `null` is a real answer, not a failure: see rule 2 in the module comment. The
220
+ * caller decides what to do with it — `ui-code-block` substitutes TypeScript,
221
+ * a document leaves the code alone.
222
+ */
223
+ function languagePatternsFor(language) {
224
+ if (!language)
225
+ return null;
226
+ const key = language.toLowerCase();
227
+ return LANGUAGE_PATTERNS[LANGUAGE_ALIASES[key] ?? key] ?? null;
228
+ }
229
+ /**
230
+ * Tokenize ONE line. See rule 1 in the module comment: highlighting never spans
231
+ * lines, so the caller splits and calls this per line.
232
+ */
233
+ function tokenizeLine(line, patterns) {
234
+ if (!line)
235
+ return [];
236
+ const tokens = [];
237
+ let cursor = 0;
238
+ while (cursor < line.length) {
239
+ const remaining = line.slice(cursor);
240
+ const best = findBestMatch(remaining, patterns);
241
+ if (best?.index === 0) {
242
+ tokens.push({ type: best.type, text: best.text });
243
+ cursor += best.text.length;
244
+ }
245
+ else if (best) {
246
+ tokens.push({ type: 'text', text: remaining.slice(0, best.index) });
247
+ cursor += best.index;
248
+ }
249
+ else {
250
+ tokens.push({ type: 'text', text: remaining });
251
+ cursor += remaining.length;
252
+ }
253
+ }
254
+ return tokens;
255
+ }
256
+ function findBestMatch(remaining, patterns) {
257
+ let best = null;
258
+ for (const pattern of patterns) {
259
+ const match = pattern.regex.exec(remaining);
260
+ if (match && (best === null || match.index < best.index)) {
261
+ best = { type: pattern.type, text: match[0], index: match.index };
262
+ }
263
+ }
264
+ return best;
265
+ }
266
+ /**
267
+ * The class a document's highlighter marks a token with.
268
+ *
269
+ * `token` plus `token-<type>`, which `rich-text-sanitizer.service.ts` has
270
+ * allowed since before there was a highlighter: markup coloured elsewhere and
271
+ * pasted in already survived sanitization with these exact classes, so the
272
+ * colours this produces and the colours a paste brings are one set of rules.
273
+ * The colours themselves live in RICH_TEXT_PROSE_CLASSES, where the editor and
274
+ * the view share them and light and dark each get their own.
275
+ *
276
+ * A `text` run gets no class at all: it is the code's own colour, and a span
277
+ * per unstyled run would triple the node count for nothing.
278
+ */
279
+ function tokenClassFor(type) {
280
+ return type === 'text' ? null : `token token-${type}`;
281
+ }
282
+ /**
283
+ * A code block's text with its line breaks, whichever form each one takes.
284
+ *
285
+ * The rule every reader of a code block has to follow: a line break is a line
286
+ * break, whether it is a `\n` character or a `<br>` element. `textContent`
287
+ * does not follow it — a `<br>` contributes nothing — and Shift+Enter, the
288
+ * editor's key for a new line inside a block, inserts `<br>`. So a function
289
+ * typed line by line was saved as one line, and the highlighter, which refused
290
+ * any block holding an element, stopped colouring from the first new line on.
291
+ */
292
+ function readCodeLines(node) {
293
+ const lines = { text: '', breaks: [], trailingBr: false };
294
+ collectCodeLines(node, lines);
295
+ if (lines.breaks.at(-1) === 'br' && lines.text.endsWith('\n') && endsWithBr(node)) {
296
+ lines.text = lines.text.slice(0, -1);
297
+ lines.breaks.pop();
298
+ lines.trailingBr = true;
299
+ }
300
+ return lines;
301
+ }
302
+ function collectCodeLines(node, into) {
303
+ for (const child of Array.from(node.childNodes)) {
304
+ if (child.nodeType === Node.TEXT_NODE) {
305
+ const data = child.data;
306
+ into.text += data;
307
+ into.breaks.push(...Array.from(data.matchAll(/\n/g), () => 'newline'));
308
+ }
309
+ else if (child.nodeName === 'BR') {
310
+ into.text += '\n';
311
+ into.breaks.push('br');
312
+ }
313
+ else if (child.nodeType === Node.ELEMENT_NODE) {
314
+ collectCodeLines(child, into);
315
+ }
316
+ }
317
+ }
318
+ /** Whether the last thing in `node`, looking into nested elements, is a `<br>`. */
319
+ function endsWithBr(node) {
320
+ let last = node.lastChild;
321
+ while (last?.nodeType === Node.ELEMENT_NODE && last.nodeName !== 'BR')
322
+ last = last.lastChild;
323
+ return last?.nodeName === 'BR';
324
+ }
325
+ /** A code block's text with every line break as `\n`; see {@link readCodeLines}. */
326
+ function codeTextOf(node) {
327
+ return readCodeLines(node).text;
328
+ }
329
+ /**
330
+ * Elements that only STYLE the code text around them, which the paint reads
331
+ * through and does not keep.
332
+ *
333
+ * A browser writes these into a contenteditable on its own. Editing next to a
334
+ * coloured token, Chrome preserves the token's computed look as literal markup:
335
+ * `<font color="…">` with the keyword colour, `<b>` for its weight,
336
+ * `<span style="font-weight: 400">` to undo it. The bold, italic, underline and
337
+ * strike buttons write the rest. None of it can survive a save — a fence is
338
+ * text — and the colour of code is the highlighter's to decide. Refusing a
339
+ * block that held any of it switched highlighting off for the whole block the
340
+ * first time the browser did this, and it never came back.
341
+ */
342
+ const STYLE_ONLY_TAGS = new Set(['SPAN', 'FONT', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'STRIKE', 'DEL']);
343
+ /**
344
+ * Whether the paint can rebuild the block without losing anything.
345
+ *
346
+ * The paint is rebuilt from the block's lines, so it may only run where lines
347
+ * are all there is. A code block CAN hold more: the markdown writer keeps an
348
+ * image in one by writing the block in its tag form, and rebuilding that block
349
+ * deleted the image outright; a link is content too. A `<br>` is a line break
350
+ * and is rebuilt. Styling wrappers carry nothing but a look (see
351
+ * {@link STYLE_ONLY_TAGS}), and a span this module painted is exactly what the
352
+ * rebuild replaces.
353
+ */
354
+ function isTextOnlyCode(code) {
355
+ return Array.from(code.querySelectorAll('*')).every((element) => element.nodeName === 'BR' || STYLE_ONLY_TAGS.has(element.nodeName));
356
+ }
357
+ /**
358
+ * Marks a document's `<pre><code>` element up with token spans, in place.
359
+ *
360
+ * The element's own TEXT is the source of truth and is never changed — the
361
+ * children are rebuilt from `textContent`, so running this twice is the same as
362
+ * running it once and a block that was already highlighted re-highlights
363
+ * cleanly. That idempotence is what lets the editor re-run it on every quiet
364
+ * moment without the markup growing.
365
+ *
366
+ * Returns whether anything was painted, so a caller that has to restore a caret
367
+ * can skip the work when nothing moved.
368
+ */
369
+ function highlightCodeElement(code, language) {
370
+ const painted = paintedFragment(code, language);
371
+ if (!painted)
372
+ return false;
373
+ code.replaceChildren(painted);
374
+ return true;
375
+ }
376
+ /**
377
+ * The token markup for a block, built detached, or `null` when the block is not
378
+ * one to paint. Built apart from the element so a caller can compare it with
379
+ * what is already there BEFORE any node is replaced.
380
+ */
381
+ function paintedFragment(code, language) {
382
+ const patterns = languagePatternsFor(language);
383
+ if (!patterns || !isTextOnlyCode(code))
384
+ return null;
385
+ const { text, breaks, trailingBr } = readCodeLines(code);
386
+ if (text === '')
387
+ return null;
388
+ const doc = code.ownerDocument;
389
+ const painted = doc.createDocumentFragment();
390
+ const lines = text.split('\n');
391
+ lines.forEach((line, index) => {
392
+ // Every break is written back as its own node AND in the form it had.
393
+ // As `\n` inside the last token, a trailing blank line was dropped on
394
+ // every pass; converted from `<br>` to `\n`, the empty line a
395
+ // Shift+Enter had just opened collapsed and took the caret with it.
396
+ if (index > 0)
397
+ painted.appendChild(breaks[index - 1] === 'br' ? doc.createElement('br') : doc.createTextNode('\n'));
398
+ for (const token of tokenizeLine(line, patterns)) {
399
+ const className = tokenClassFor(token.type);
400
+ if (className === null) {
401
+ painted.appendChild(doc.createTextNode(token.text));
402
+ continue;
403
+ }
404
+ const span = doc.createElement('span');
405
+ span.className = className;
406
+ span.textContent = token.text;
407
+ painted.appendChild(span);
408
+ }
409
+ });
410
+ if (trailingBr)
411
+ painted.appendChild(doc.createElement('br'));
412
+ return painted;
413
+ }
414
+ /** A fragment's markup, for comparing with an element's `innerHTML`. */
415
+ function markupOf(fragment) {
416
+ const holder = fragment.ownerDocument.createElement('div');
417
+ holder.appendChild(fragment.cloneNode(true));
418
+ return holder.innerHTML;
419
+ }
420
+ /** The language a rendered `<pre><code>` carries: `data-language`, else the `language-*` class. */
421
+ function languageOfCodeElement(code) {
422
+ const attribute = code.dataset['language'];
423
+ if (attribute)
424
+ return attribute;
425
+ const fromClass = /(?:^|\s)language-([\w+#.-]+)/.exec(code.className);
426
+ return fromClass?.[1] ?? null;
427
+ }
428
+ /**
429
+ * Highlights every code block under `root` that names a language this module
430
+ * knows, and returns how many it painted.
431
+ */
432
+ function highlightCodeBlocks(root) {
433
+ let painted = 0;
434
+ for (const code of Array.from(root.querySelectorAll('pre > code'))) {
435
+ if (highlightCodeElement(code, languageOfCodeElement(code)))
436
+ painted++;
437
+ }
438
+ return painted;
439
+ }
440
+ /**
441
+ * Removes the spans the highlighter painted, in place.
442
+ *
443
+ * Only from a block whose language {@link languagePatternsFor} knows, which is
444
+ * exactly the set this module paints. Spans in a block it does NOT know came
445
+ * from the author — a paste from an already-highlighted source, which the
446
+ * sanitizer deliberately keeps — and are content, not decoration.
447
+ *
448
+ * The spans are unwrapped rather than the element flattened to its text: a code
449
+ * block may hold an image (the markdown writer keeps one by writing the block
450
+ * in its tag form), and `textContent = textContent` would delete it.
451
+ */
452
+ function stripCodeHighlighting(root) {
453
+ for (const code of Array.from(root.querySelectorAll('pre > code'))) {
454
+ if (!languagePatternsFor(languageOfCodeElement(code)))
455
+ continue;
456
+ for (const span of Array.from(code.querySelectorAll('span.token'))) {
457
+ span.replaceWith(...Array.from(span.childNodes));
458
+ }
459
+ code.normalize();
460
+ }
461
+ }
462
+ /**
463
+ * How many characters come before the boundary (`container`, `offset`) in
464
+ * `root`, counting each `<br>` as one — the newline it stands for.
465
+ *
466
+ * `Range.toString()` counts a `<br>` as nothing, so on a line opened with
467
+ * Shift+Enter the caret was remembered one character early per break above it,
468
+ * and every repaint walked it back towards the top of the block.
469
+ */
470
+ function characterOffsetIn(root, container, offset) {
471
+ if (!root.contains(container))
472
+ return null;
473
+ const measure = root.ownerDocument.createRange();
474
+ measure.setStart(root, 0);
475
+ measure.setEnd(container, offset);
476
+ return rawLengthOf(measure.cloneContents());
477
+ }
478
+ /** Text length with every `<br>` counted as one character, the trailing one included. */
479
+ function rawLengthOf(node) {
480
+ let length = 0;
481
+ for (const child of Array.from(node.childNodes)) {
482
+ if (child.nodeType === Node.TEXT_NODE)
483
+ length += child.data.length;
484
+ else if (child.nodeName === 'BR')
485
+ length += 1;
486
+ else
487
+ length += rawLengthOf(child);
488
+ }
489
+ return length;
490
+ }
491
+ function collapsedRange(doc, place) {
492
+ const range = doc.createRange();
493
+ place(range);
494
+ range.collapse(true);
495
+ return range;
496
+ }
497
+ /**
498
+ * A collapsed range `offset` characters into `root`, counted as
499
+ * {@link characterOffsetIn} counts them, or `null` when the block is empty.
500
+ */
501
+ function rangeAtCharacterOffset(root, offset) {
502
+ const doc = root.ownerDocument;
503
+ const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
504
+ let seen = 0;
505
+ let tail = null;
506
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
507
+ if (node.nodeType === Node.TEXT_NODE) {
508
+ const text = node;
509
+ if (seen + text.data.length >= offset)
510
+ return collapsedRange(doc, (range) => range.setStart(text, offset - seen));
511
+ seen += text.data.length;
512
+ tail = text;
513
+ }
514
+ else if (node.nodeName === 'BR') {
515
+ const br = node;
516
+ if (seen >= offset)
517
+ return collapsedRange(doc, (range) => range.setStartBefore(br));
518
+ seen += 1;
519
+ tail = br;
520
+ }
521
+ }
522
+ const end = tail;
523
+ if (!end)
524
+ return null;
525
+ // Past a break with nothing after it: the empty line a Shift+Enter opened.
526
+ if (end.nodeType === Node.TEXT_NODE)
527
+ return collapsedRange(doc, (range) => range.setStart(end, end.data.length));
528
+ return collapsedRange(doc, (range) => range.setStartAfter(end));
529
+ }
530
+ /**
531
+ * Highlights one code block and puts the caret back where it was.
532
+ *
533
+ * The caret is remembered as a CHARACTER OFFSET into the block's text, not as a
534
+ * node and an index: every node inside the block is replaced, so a remembered
535
+ * node would be detached by the time it was restored — the caret would jump to
536
+ * the top of the document and the next keystroke would land there. The text
537
+ * itself does not change, so the offset always still means the same place.
538
+ *
539
+ * Does nothing when the block is already painted exactly as it would be, so a
540
+ * keystroke that changes no token does not move the selection at all. That
541
+ * comparison is made on the DETACHED paint, before any node is replaced: made
542
+ * after, as it first was, the nodes were already swapped, the "nothing changed"
543
+ * exit skipped the restore, and the browser dropped the caret to the start of
544
+ * the block -- so every second keystroke in a word landed at the front.
545
+ */
546
+ function highlightCodeElementKeepingCaret(code, language, selection) {
547
+ const painted = paintedFragment(code, language);
548
+ if (!painted || markupOf(painted) === code.innerHTML)
549
+ return false;
550
+ const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
551
+ const caret = range?.collapsed ? characterOffsetIn(code, range.startContainer, range.startOffset) : null;
552
+ code.replaceChildren(painted);
553
+ if (caret !== null && selection) {
554
+ const restored = rangeAtCharacterOffset(code, caret);
555
+ if (restored) {
556
+ selection.removeAllRanges();
557
+ selection.addRange(restored);
558
+ }
559
+ }
560
+ return true;
561
+ }
562
+
75
563
  /**
76
564
  * Counting and cutting text the way a reader sees it.
77
565
  */
@@ -829,6 +1317,32 @@ function firstTextNode(node) {
829
1317
  return null;
830
1318
  return doc.createTreeWalker(node, NodeFilter.SHOW_TEXT).nextNode();
831
1319
  }
1320
+ /**
1321
+ * How deep blocks nest before the markdown reader leaves the rest as text; see
1322
+ * parseToggleBlocks and buildBlockquote in rich-text-markdown.service.ts. The
1323
+ * reader counts a level for every details body, quote holding a nested quote
1324
+ * and list item's block, so a details block nested through a list or a quote
1325
+ * meets the same cap; a sub-list, read in its parent's pass, adds none. The
1326
+ * writer counts the same list items and every quote (see nestingDepthOf), which
1327
+ * is never less.
1328
+ *
1329
+ * It lives here rather than beside the reader so the EDITOR can respect the
1330
+ * same ceiling. A structure typed past it is unwrapped the next time the
1331
+ * document is read back, so building one is building something that will not
1332
+ * survive; the nested-blockquote input rule clamps to this for that reason.
1333
+ */
1334
+ const MAX_NESTING_DEPTH = 32;
1335
+ /** How many `<blockquote>` elements `node` sits inside, within `root`. */
1336
+ function blockquoteDepthOf(node, root) {
1337
+ let depth = 0;
1338
+ let at = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1339
+ while (at && at !== root && root.contains(at)) {
1340
+ if (at.tagName === 'BLOCKQUOTE')
1341
+ depth++;
1342
+ at = at.parentElement;
1343
+ }
1344
+ return depth;
1345
+ }
832
1346
 
833
1347
  const SVG_CHECK_LENGTH = 256;
834
1348
  function isJpeg(b) {
@@ -2864,14 +3378,15 @@ function parseListLine(line) {
2864
3378
  // runs after this one, so the refusal has to live here.
2865
3379
  if (THEMATIC_BREAK.test(line))
2866
3380
  return null;
2867
- const taskMatch = new RegExp(/^(\s*)[-*+]\s+\[([ xX])\]\s*(\S.*|)$/).exec(line);
3381
+ const taskMatch = new RegExp(/^(\s*)([-*+])\s+\[([ xX])\]\s*(\S.*|)$/).exec(line);
2868
3382
  if (taskMatch) {
2869
- const checked = taskMatch[2] !== ' ';
3383
+ const checked = taskMatch[3] !== ' ';
2870
3384
  return {
2871
3385
  indent: taskMatch[1].length,
2872
3386
  type: 'task',
2873
- content: `[${checked ? 'x' : ' '}] ${taskMatch[3]}`,
3387
+ content: `[${checked ? 'x' : ' '}] ${taskMatch[4]}`,
2874
3388
  column: contentColumn(line, taskMatch[1].length + 1),
3389
+ marker: taskMatch[2],
2875
3390
  };
2876
3391
  }
2877
3392
  // The content group is OPTIONAL. Requiring \s+ then a non-space meant an
@@ -2881,27 +3396,56 @@ function parseListLine(line) {
2881
3396
  // when a user opens a bullet and clicks away, so this hit an everyday
2882
3397
  // keystroke; for <ol> the second list also renumbered from 1.
2883
3398
  // A rule (--- / *** / ___) still does not match: it has no space.
2884
- const ulMatch = new RegExp(/^([ \t]*)[-*+]([ \t].*)?$/).exec(line);
3399
+ const ulMatch = new RegExp(/^([ \t]*)([-*+])([ \t].*)?$/).exec(line);
2885
3400
  if (ulMatch) {
2886
3401
  return {
2887
3402
  indent: ulMatch[1].length,
2888
3403
  type: 'ul',
2889
- content: listItemContent(ulMatch[2]),
3404
+ content: listItemContent(ulMatch[3]),
2890
3405
  column: contentColumn(line, ulMatch[1].length + 1),
3406
+ marker: ulMatch[2],
2891
3407
  };
2892
3408
  }
2893
- const olMatch = new RegExp(/^([ \t]*)(\d{1,9})\.([ \t].*)?$/).exec(line);
3409
+ // Both ordered delimiters, "." and ")". CommonMark has always allowed the
3410
+ // second, and the writer needs it to end one ordered list before the next.
3411
+ const olMatch = new RegExp(/^([ \t]*)(\d{1,9})([.)])([ \t].*)?$/).exec(line);
2894
3412
  if (olMatch) {
2895
3413
  return {
2896
3414
  indent: olMatch[1].length,
2897
3415
  type: 'ol',
2898
- content: listItemContent(olMatch[3]),
3416
+ content: listItemContent(olMatch[4]),
2899
3417
  column: contentColumn(line, olMatch[1].length + olMatch[2].length + 1),
2900
3418
  number: Number.parseInt(olMatch[2], 10),
3419
+ marker: olMatch[3],
2901
3420
  };
2902
3421
  }
2903
3422
  return null;
2904
3423
  }
3424
+ /** The two markers each kind of list alternates between; see {@link listMarkerFor}. */
3425
+ const UL_MARKERS = ['-', '*'];
3426
+ const OL_MARKERS = ['.', ')'];
3427
+ /** The previous sibling when it is a list of the same kind, so the two would merge. */
3428
+ function adjacentSiblingList(element) {
3429
+ const previous = element.previousElementSibling;
3430
+ return previous?.tagName === element.tagName ? previous : null;
3431
+ }
3432
+ /**
3433
+ * The marker a list is written with, alternating along a run of sibling lists.
3434
+ *
3435
+ * A blank line does not end a list -- CommonMark ends one at a marker CHANGE --
3436
+ * so two `<ol>`s side by side were written `1. a` / `1. b` and read back as ONE
3437
+ * list with b renumbered to 2. Alternating the bullet character (for an ordered
3438
+ * list, the delimiter after the number) puts a boundary the reader honours
3439
+ * between every adjacent pair, and leaves a list with no list beside it written
3440
+ * exactly as before.
3441
+ */
3442
+ function listMarkerFor(listEl, type) {
3443
+ const markers = type === 'ol' ? OL_MARKERS : UL_MARKERS;
3444
+ let runs = 0;
3445
+ for (let previous = adjacentSiblingList(listEl); previous; previous = adjacentSiblingList(previous))
3446
+ runs++;
3447
+ return markers[runs % markers.length];
3448
+ }
2905
3449
  /** How far a line is indented. */
2906
3450
  function indentOf(line) {
2907
3451
  return line.length - line.trimStart().length;
@@ -2991,7 +3535,7 @@ function holdLine(line, continuation, pendingBlank) {
2991
3535
  pendingBlank.length = 0;
2992
3536
  }
2993
3537
  /** Close the list levels that a line at `indent` of kind `type` does not continue. */
2994
- function closeLevelsFor(stack, indent, type) {
3538
+ function closeLevelsFor(stack, indent, type, marker) {
2995
3539
  // Pop only DEEPER levels. Popping the current one too (`>=`) threw away the
2996
3540
  // list a sibling belongs to, so every line started a fresh one — an ordered
2997
3541
  // list renumbered from 1 on every row, and a screen reader announced
@@ -3000,8 +3544,12 @@ function closeLevelsFor(stack, indent, type) {
3000
3544
  stack.pop();
3001
3545
  }
3002
3546
  // A same-indent line of a DIFFERENT kind (bullet after numbered) is its own
3003
- // list, so that one context is replaced rather than appended.
3004
- if (stack.at(-1)?.indent === indent && stack.at(-1)?.type !== type) {
3547
+ // list, so that one context is replaced rather than appended. A different
3548
+ // MARKER of the same kind ends it just as surely -- that is CommonMark's
3549
+ // only way to put two sibling lists of one kind next to each other, and the
3550
+ // writer relies on it (see listMarkerFor).
3551
+ const open = stack.at(-1);
3552
+ if (open?.indent === indent && (open.type !== type || open.marker !== marker)) {
3005
3553
  stack.pop();
3006
3554
  }
3007
3555
  }
@@ -3012,7 +3560,7 @@ function pushListItem(stack, rootLists, line) {
3012
3560
  parent.items.push(item);
3013
3561
  return;
3014
3562
  }
3015
- const list = { type: line.type, items: [item], indent: line.indent, start: line.number ?? 1 };
3563
+ const list = { type: line.type, items: [item], indent: line.indent, start: line.number ?? 1, marker: line.marker };
3016
3564
  if (parent)
3017
3565
  parent.items.at(-1)?.parts.push(list);
3018
3566
  else
@@ -3317,8 +3865,12 @@ function touchesWord(element) {
3317
3865
  || touches(nodeBeside(element, 'nextSibling'), /^[\p{L}\p{N}]/u);
3318
3866
  }
3319
3867
  function touches(node, letter) {
3868
+ // A BLOCK sibling is not on this line and cannot run into the delimiters.
3869
+ // Counted as touching, a task row's nested <ul> flipped the bold beside it
3870
+ // to its tag form on the SECOND save -- the first save had moved the list
3871
+ // next to it -- so the document kept changing after it had been saved once.
3320
3872
  if (node?.nodeType === Node.ELEMENT_NODE)
3321
- return (node.textContent ?? '') !== '';
3873
+ return isPhrasing(node) && (node.textContent ?? '') !== '';
3322
3874
  return node?.nodeType === Node.TEXT_NODE && letter.test(node.textContent ?? '');
3323
3875
  }
3324
3876
  /**
@@ -3616,10 +4168,34 @@ function buildListContextHtml(ctx) {
3616
4168
  // half already does for parentheses. A flat [^\\]]* meant "see [1]" or
3617
4169
  // "[Draft] spec" -- everyday link and alt text -- did not match at all, so the
3618
4170
  // anchor was destroyed on save and its markdown source shown as page text.
4171
+ const TARGET_BODY = String.raw `(?:[^()]|\([^()]*\))`;
4172
+ const LINK_TEXT_BODY = String.raw `(?:[^[\]]|\[[^[\]]*\])`;
3619
4173
  const MEDIA_TARGET_PATTERN = {
3620
- image: /!\[((?:[^[\]]|\[[^[\]]*\]){0,4096})\]\(((?:[^()]|\([^()]*\))+)\)/g,
3621
- link: /\[((?:[^[\]]|\[[^[\]]*\]){1,4096})\]\(((?:[^()]|\([^()]*\)){1,4096})\)/g,
4174
+ image: new RegExp(String.raw `!\[(${LINK_TEXT_BODY}{0,4096})\]\((${TARGET_BODY}+)\)`, 'g'),
4175
+ link: new RegExp(String.raw `\[(${LINK_TEXT_BODY}{1,4096})\]\((${TARGET_BODY}{1,4096})\)`, 'g'),
3622
4176
  };
4177
+ /**
4178
+ * The writer's half of {@link TARGET_BODY}: an address the reader hands back
4179
+ * unchanged. It also refuses a backslash, which `protectEscapes` consumes
4180
+ * before any target is matched, so a plain address holding one would not
4181
+ * survive either.
4182
+ */
4183
+ const PLAIN_TARGET = /^(?:[^()\\]|\([^()\\]*\))*$/;
4184
+ /**
4185
+ * A link or image address as markdown source.
4186
+ *
4187
+ * The reader's grammar allows at most ONE level of balanced parentheses, so an
4188
+ * address holding an odd or a nested ")" was cut at that character: the link
4189
+ * pointed at the truncated prefix and the rest of the address landed on the
4190
+ * page as visible text. CommonMark's answer is the backslash escape, and
4191
+ * `protectEscapes` parks those before any target is matched, so no parenthesis
4192
+ * is left for the grammar to trip on. An address the grammar already returns
4193
+ * unchanged keeps its plain form, so ordinary URLs -- Wikipedia's
4194
+ * "X_(disambiguation)" among them -- are written exactly as before.
4195
+ */
4196
+ function markdownTarget(target) {
4197
+ return PLAIN_TARGET.test(target) ? target : target.replaceAll(/[\\()]/g, String.raw `\$&`);
4198
+ }
3623
4199
  /**
3624
4200
  * Marker standing in for a character with Markdown meaning while the emphasis
3625
4201
  * passes run. Those passes regex over the WHOLE string, attribute values
@@ -3690,6 +4266,38 @@ const INDENTED_FENCE_TOKEN = /^\s+\d{1,9}\s*$/;
3690
4266
  * list item above it, whatever block it turns out to hold.
3691
4267
  */
3692
4268
  const CONTINUATION_LINE = /^ {2,}\S/;
4269
+ /** Columns a tab advances to, per CommonMark. */
4270
+ const TAB_STOP = 4;
4271
+ /**
4272
+ * Every line's leading whitespace with its tabs advanced to the next
4273
+ * four-column tab stop.
4274
+ *
4275
+ * CommonMark measures block indentation in COLUMNS (§2.2), while every
4276
+ * indentation predicate here counts characters -- CONTINUATION_LINE, indentOf,
4277
+ * contentColumn. A tab therefore read as one column, fell short of the item's
4278
+ * continuation column, and the block escaped its list item: "- a" followed by a
4279
+ * tab-indented quote came back as a list and a separate paragraph, and the next
4280
+ * save wrote the quote marker as the literal characters "\> q". Expanding the
4281
+ * tabs once, up front, lets all of those predicates stay character-based.
4282
+ *
4283
+ * Only the leading run is rewritten: a tab inside prose is content, not
4284
+ * structure. Fenced and inline code are parked before this runs, so no tab
4285
+ * inside code is ever seen.
4286
+ */
4287
+ function expandLeadingTabs(text) {
4288
+ if (!text.includes('\t'))
4289
+ return text;
4290
+ return text.split('\n').map(expandLineIndent).join('\n');
4291
+ }
4292
+ function expandLineIndent(line) {
4293
+ const indent = /^[ \t]*/.exec(line)?.[0] ?? '';
4294
+ if (!indent.includes('\t'))
4295
+ return line;
4296
+ let column = 0;
4297
+ for (const ch of indent)
4298
+ column += ch === '\t' ? TAB_STOP - (column % TAB_STOP) : 1;
4299
+ return ' '.repeat(column) + line.slice(indent.length);
4300
+ }
3693
4301
  /** Private-use delimiters parking a raw HTML tag during the inline passes. */
3694
4302
  const RAW_TAG_OPEN = '';
3695
4303
  const RAW_TAG_CLOSE = '';
@@ -3971,6 +4579,10 @@ class RichTextMarkdownService {
3971
4579
  // -- rendering "</div>" as visible "&lt;/div&gt;".
3972
4580
  const protectedCode = [];
3973
4581
  html = this.protectCodeFences(html, protectedCode);
4582
+ // AFTER the fences are parked, so a tab inside code is never rewritten,
4583
+ // and BEFORE every block pass, all of which measure indentation in
4584
+ // characters. See expandLeadingTabs.
4585
+ html = expandLeadingTabs(html);
3974
4586
  // Inline code is lifted out with the fences and for the same reason: a
3975
4587
  // code span is inert text. parseInlineCode ran LAST, after the emphasis
3976
4588
  // and line-break passes had already rewritten its contents, so
@@ -4003,8 +4615,8 @@ class RichTextMarkdownService {
4003
4615
  // before this pass ever ran, and the break was silently dropped.
4004
4616
  html = this.parseLineBreaks(html);
4005
4617
  html = this.parseParagraphs(html, protectedTags);
4006
- html = this.parseImages(html, protectedInline, inlineSources, protectedTags);
4007
- html = this.parseLinks(html, inlineSources, protectedTags);
4618
+ html = this.parseImages(html, protectedInline, inlineSources, protectedTags, protectedEscapes);
4619
+ html = this.parseLinks(html, inlineSources, protectedTags, protectedEscapes);
4008
4620
  html = this.parseBoldItalic(html);
4009
4621
  html = this.parseStrikethrough(html);
4010
4622
  // After the emphasis passes, so the characters hidden in link and image
@@ -4469,7 +5081,7 @@ class RichTextMarkdownService {
4469
5081
  result.push(...orphans, line);
4470
5082
  continue;
4471
5083
  }
4472
- closeLevelsFor(stack, parsed.indent, parsed.type);
5084
+ closeLevelsFor(stack, parsed.indent, parsed.type, parsed.marker);
4473
5085
  flushContinuation();
4474
5086
  pushListItem(stack, rootLists, parsed);
4475
5087
  }
@@ -4734,9 +5346,17 @@ class RichTextMarkdownService {
4734
5346
  * the query string broke. A target with a space is no target; only one that
4735
5347
  * held a code span or a tag is checked for it, since a plain target with a
4736
5348
  * space has always been read as a link.
4737
- */
4738
- targetSource(target, inlineSources, tagStore) {
4739
- const typed = decodeCharacterReferences(restoreParked(restoreParked(target, INLINE_CODE_OPEN, INLINE_CODE_CLOSE, inlineSources), RAW_TAG_OPEN, RAW_TAG_CLOSE, tagStore));
5349
+ *
5350
+ * A backslash escape is resolved LAST, after the character references: it is
5351
+ * the author saying "this character, literally", so what it hands back must
5352
+ * not be read as syntax again -- `\&amp;` means those five characters, not
5353
+ * an ampersand. Resolving it here at all is what lets the writer escape a
5354
+ * parenthesis the target grammar cannot carry (see markdownTarget): the real
5355
+ * character reaches the sanitizer and the attribute, rather than the parked
5356
+ * token, which sanitizeImageSrc percent-encoded into the address.
5357
+ */
5358
+ targetSource(target, inlineSources, tagStore, escapes) {
5359
+ const typed = restoreParked(decodeCharacterReferences(restoreParked(restoreParked(target, INLINE_CODE_OPEN, INLINE_CODE_CLOSE, inlineSources), RAW_TAG_OPEN, RAW_TAG_CLOSE, tagStore)), ESCAPED_OPEN, ESCAPED_CLOSE, escapes);
4740
5360
  return holdsParkedToken(target) && /\s/.test(typed) ? null : typed;
4741
5361
  }
4742
5362
  /**
@@ -4748,12 +5368,49 @@ class RichTextMarkdownService {
4748
5368
  altText(alt, inlineStore, tagStore) {
4749
5369
  return restoreParked(resolveInlineCodeText(alt, inlineStore), RAW_TAG_OPEN, RAW_TAG_CLOSE, tagStore, (tag) => this.escapeHtml(tag));
4750
5370
  }
4751
- parseImages(html, inlineStore, inlineSources, tagStore) {
5371
+ /**
5372
+ * Park a tag this pass wrote in the raw-tag store, so no later pass can see
5373
+ * inside it.
5374
+ *
5375
+ * The tag is written UNSHIELDED because parking supersedes the shield:
5376
+ * restoreRawTags runs after unshieldUrls, so a shielded character parked in
5377
+ * here would never be given back.
5378
+ */
5379
+ parkWrittenTag(tag, tagStore) {
5380
+ const token = `${RAW_TAG_OPEN}${tagStore.length}${RAW_TAG_CLOSE}`;
5381
+ tagStore.push(tag);
5382
+ return token;
5383
+ }
5384
+ /**
5385
+ * An `<img>` this pass wrote, with its attributes escaped but not shielded.
5386
+ *
5387
+ * Its alt text is DOCUMENT TEXT, which escapeHtmlInContent has already
5388
+ * escaped, so only the quote is escaped again here -- escaping it twice
5389
+ * brought a "<" back as the characters "&lt;", one more layer per save.
5390
+ */
5391
+ imageTag(attribute, target, alt) {
5392
+ return `<img ${attribute}="${this.escapeHtml(target)}" alt="${alt.replaceAll('"', '&quot;')}">`;
5393
+ }
5394
+ /**
5395
+ * Parse images ![alt](src).
5396
+ *
5397
+ * The tag is parked, not returned live. Left in the stream, its alt
5398
+ * attribute was still document text as far as the passes that follow were
5399
+ * concerned: parseLinks read `![a [b](u) c](i.png)` as a link INSIDE the
5400
+ * attribute and wrote an `<a href="` into it, which ended the attribute and
5401
+ * put the rest of the tag on the page. Shielding only hid `* _ \` ~`, so it
5402
+ * could never have covered the bracket forms.
5403
+ */
5404
+ parseImages(html, inlineStore, inlineSources, tagStore, escapes) {
4752
5405
  return html.replaceAll(MEDIA_TARGET_PATTERN.image, (match, alt, target) => {
4753
- const src = this.targetSource(target, inlineSources, tagStore);
5406
+ const src = this.targetSource(target, inlineSources, tagStore, escapes);
4754
5407
  if (src === null)
4755
5408
  return match;
4756
5409
  const safeSrc = this.sanitizer.sanitizeImageSrc(src);
5410
+ // An alt attribute is plain text: a parked code span restored in
5411
+ // there would land as the literal string "<code>x</code>". Resolve
5412
+ // it back to the text the author typed instead.
5413
+ const plainAlt = this.altText(alt, inlineStore, tagStore);
4757
5414
  if (!safeSrc) {
4758
5415
  // A POLICY-blocked image keeps its element and alt, so the
4759
5416
  // reader sees a labelled frame rather than nothing and the block
@@ -4763,22 +5420,17 @@ class RichTextMarkdownService {
4763
5420
  const blocked = this.sanitizer.takeBlockedByPolicy();
4764
5421
  if (blocked === null)
4765
5422
  return '';
4766
- const blockedAlt = this.altText(alt, inlineStore, tagStore);
4767
- return `<img data-blocked-src="${this.attr(blocked)}" alt="${this.textAttr(blockedAlt)}">`;
5423
+ return this.parkWrittenTag(this.imageTag('data-blocked-src', blocked, plainAlt), tagStore);
4768
5424
  }
4769
- // An alt attribute is plain text: a parked code span restored in
4770
- // there would land as the literal string "<code>x</code>". Resolve
4771
- // it back to the text the author typed instead.
4772
- const plainAlt = this.altText(alt, inlineStore, tagStore);
4773
- return `<img src="${this.attr(safeSrc)}" alt="${this.textAttr(plainAlt)}">`;
5425
+ return this.parkWrittenTag(this.imageTag('src', safeSrc, plainAlt), tagStore);
4774
5426
  });
4775
5427
  }
4776
5428
  /**
4777
5429
  * Parse links [text](url).
4778
5430
  */
4779
- parseLinks(html, inlineSources, tagStore) {
5431
+ parseLinks(html, inlineSources, tagStore, escapes) {
4780
5432
  return html.replaceAll(MEDIA_TARGET_PATTERN.link, (match, text, target) => {
4781
- const url = this.targetSource(target, inlineSources, tagStore);
5433
+ const url = this.targetSource(target, inlineSources, tagStore, escapes);
4782
5434
  if (url === null)
4783
5435
  return match;
4784
5436
  const safeUrl = this.sanitizer.sanitizeUrl(url);
@@ -5070,7 +5722,7 @@ class RichTextMarkdownService {
5070
5722
  // of the link fused.
5071
5723
  if (inner.trim() === '')
5072
5724
  return `<a href="${this.escapeHtml(href)}"></a>${inner}`;
5073
- return `[${inner}](${href})`;
5725
+ return `[${inner}](${markdownTarget(href)})`;
5074
5726
  }
5075
5727
  handleImageTag(element) {
5076
5728
  const alt = element.getAttribute('alt') ?? '';
@@ -5087,7 +5739,7 @@ class RichTextMarkdownService {
5087
5739
  // the paragraph inside the attribute, backticks or asterisks were read as
5088
5740
  // syntax, and "&copy;" or "<i>" came back as a character or a tag.
5089
5741
  const text = escapeMarkdownText(alt.replaceAll(/[ \t\n\r\f]+/g, ' '));
5090
- return `![${text}](${src})`;
5742
+ return `![${text}](${markdownTarget(src)})`;
5091
5743
  }
5092
5744
  /**
5093
5745
  * Unwrap every span that is written as its text alone, so the text on either
@@ -5192,7 +5844,16 @@ class RichTextMarkdownService {
5192
5844
  // from opening and the code came back as paragraphs.
5193
5845
  const language = element.querySelector('code')?.dataset['language'] ?? '';
5194
5846
  const lang = /^[\w+#.-]+$/.test(language) ? language : '';
5195
- const codeContent = element.textContent ?? '';
5847
+ // A fence is TEXT: it cannot carry an image, and a block written from
5848
+ // textContent alone deleted one outright on the first save. The tag form
5849
+ // can carry it and reads back verbatim, which is the answer inline code
5850
+ // already gives for the same content.
5851
+ if (element.querySelector('img'))
5852
+ return this.preTagForm(element, lang, inListItem);
5853
+ // Every line break, including a <br>. textContent counts a <br> as
5854
+ // nothing, and Shift+Enter -- how a line is added inside a block --
5855
+ // inserts one, so a function typed line by line was saved on one line.
5856
+ const codeContent = codeTextOf(element);
5196
5857
  const indent = inListItem ? ' ' : '';
5197
5858
  const body = codeContent
5198
5859
  .split('\n')
@@ -5202,6 +5863,21 @@ class RichTextMarkdownService {
5202
5863
  const fence = '`'.repeat(Math.max(3, longestRun + 1));
5203
5864
  return `\n${indent}${fence}${lang}\n${body}\n${indent}${fence}\n`;
5204
5865
  }
5866
+ /**
5867
+ * A code block as its tag pair, on one markdown line.
5868
+ *
5869
+ * One line, with every newline written as a character reference, because a
5870
+ * raw newline inside the pair would let the block passes split it: the
5871
+ * `<pre>` and `</pre>` are parked separately and the lines between them are
5872
+ * read as markdown. The reference decodes to the newline when the sanitizer
5873
+ * parses the restored tag, so the code keeps its own line breaks.
5874
+ */
5875
+ preTagForm(element, language, inListItem) {
5876
+ const code = element.querySelector('code') ?? element;
5877
+ const lang = language ? ` data-language="${this.escapeHtml(language)}"` : '';
5878
+ const indent = inListItem ? ' ' : '';
5879
+ return `\n${indent}<pre><code${lang}>${this.codeTagContent(code)}</code></pre>\n`;
5880
+ }
5205
5881
  handleUlTag(element) {
5206
5882
  const result = ['\n'];
5207
5883
  const isTask = 'taskList' in element.dataset;
@@ -5393,10 +6069,11 @@ class RichTextMarkdownService {
5393
6069
  listToMarkdown(listEl, type, indent, result) {
5394
6070
  const items = Array.from(listEl.children);
5395
6071
  const first = type === 'ol' ? listStartOf(listEl) : 1;
6072
+ const marker = listMarkerFor(listEl, type);
5396
6073
  items.forEach((li, index) => {
5397
6074
  const [lead, ...rest] = itemRuns(li);
5398
6075
  const ordinal = first + index <= MAX_LIST_NUMBER ? first + index : first;
5399
- result.push(this.formatListItem(type, li, this.leadContent(li, lead, indent), indent, ordinal));
6076
+ result.push(this.formatListItem(type, li, this.leadContent(li, lead, indent), indent, ordinal, marker));
5400
6077
  // Every sub-list, in order, with the content after each where it
5401
6078
  // falls. Only the last sub-list was kept, so an item holding two --
5402
6079
  // bullets under numbers, which indenting beside an existing sub-list of
@@ -5442,15 +6119,15 @@ class RichTextMarkdownService {
5442
6119
  // text vanished from the saved markdown while the HTML looked fine.
5443
6120
  return node.nodeType === Node.TEXT_NODE ? textToMarkdown(node) : this.elementToMarkdown(node);
5444
6121
  }
5445
- formatListItem(type, li, content, indent, ordinal) {
6122
+ formatListItem(type, li, content, indent, ordinal, marker) {
5446
6123
  if (type === 'task') {
5447
6124
  const checked = li.dataset['checked'] === 'true';
5448
- return `${indent}- [${checked ? 'x' : ' '}] ${breaksAsTags(content)}\n`;
6125
+ return `${indent}${marker} [${checked ? 'x' : ' '}] ${breaksAsTags(content)}\n`;
5449
6126
  }
5450
6127
  if (type === 'ol') {
5451
- return `${indent}${ordinal}. ${content}\n`;
6128
+ return `${indent}${ordinal}${marker} ${content}\n`;
5452
6129
  }
5453
- return `${indent}- ${content}\n`;
6130
+ return `${indent}${marker} ${content}\n`;
5454
6131
  }
5455
6132
  detectNestedListType(nestedList) {
5456
6133
  if ('taskList' in nestedList.dataset)
@@ -5611,15 +6288,6 @@ const SUBTREE_INDEPENDENT_TAGS = new Set(['table', 'img', 'input', 'pre', 'ul',
5611
6288
  const MAX_TABLE_ROWSPAN = 1000;
5612
6289
  /** Widest row a table may emit, bounding paste amplification. */
5613
6290
  const MAX_TABLE_COLUMNS = 1000;
5614
- /**
5615
- * How deep blocks nest before the reader leaves the rest as text; see
5616
- * parseToggleBlocks and buildBlockquote. The reader counts a level for every
5617
- * details body, quote holding a nested quote and list item's block, so a details
5618
- * block nested through a list or a quote meets the same cap; a sub-list, read in
5619
- * its parent's pass, adds none. The writer counts the same list items and every
5620
- * quote (see nestingDepthOf), which is never less.
5621
- */
5622
- const MAX_NESTING_DEPTH = 32;
5623
6291
  /** HTML's own limit for a column span. */
5624
6292
  const MAX_COLSPAN = 1000;
5625
6293
  /** Blank-line boundary between blocks, captured so joining restores the text. */
@@ -9335,7 +10003,9 @@ const BLOCK_RULES = [
9335
10003
  { kind: 'heading3', pattern: /^#{3}$/, terminators: [' '] },
9336
10004
  { kind: 'bulletList', pattern: /^[-*]$/, terminators: [' '] },
9337
10005
  { kind: 'orderedList', pattern: /^\d{1,3}\.$/, terminators: [' '] },
9338
- { kind: 'blockquote', pattern: /^>$/, terminators: [' '] },
10006
+ // A RUN of markers, one quote each. `/^>$/` matched exactly one, so a reader
10007
+ // could load and round-trip a nested quote but an author could not type one.
10008
+ { kind: 'blockquote', pattern: /^(>+)$/, terminators: [' '] },
9339
10009
  { kind: 'taskUnchecked', pattern: /^\[]$/, terminators: [' '] },
9340
10010
  { kind: 'taskChecked', pattern: /^\[[xX]]$/, terminators: [' '] },
9341
10011
  { kind: 'horizontalRule', pattern: /^-{3}$/, terminators: [''] },
@@ -9364,9 +10034,11 @@ function matchBlockInputRule(textBeforeCaret, terminator) {
9364
10034
  if (!match)
9365
10035
  continue;
9366
10036
  const markerLength = textBeforeCaret.length + terminator.length;
9367
- return rule.kind === 'codeBlock'
9368
- ? { kind: rule.kind, markerLength, language: match[1] ?? '' }
9369
- : { kind: rule.kind, markerLength };
10037
+ if (rule.kind === 'codeBlock')
10038
+ return { kind: rule.kind, markerLength, language: match[1] ?? '' };
10039
+ if (rule.kind === 'blockquote')
10040
+ return { kind: rule.kind, markerLength, depth: match[1].length };
10041
+ return { kind: rule.kind, markerLength };
9370
10042
  }
9371
10043
  return null;
9372
10044
  }
@@ -9683,6 +10355,25 @@ const RICH_TEXT_PROSE_CLASSES = [
9683
10355
  '[&_details>:not(summary)]:px-3 [&_details>:not(summary)]:py-2',
9684
10356
  '[&_hr]:border-t [&_hr]:border-border [&_hr]:my-4',
9685
10357
  '[&_blockquote]:border-s-4 [&_blockquote]:border-border [&_blockquote]:ps-4 [&_blockquote]:py-1 [&_blockquote]:my-2 [&_blockquote]:text-muted-foreground [&_blockquote]:italic',
10358
+ // Syntax highlighting. The classes are the ones the sanitizer has always
10359
+ // allowed (`token`, `token-*`), so a document highlighted here and one
10360
+ // pasted in already highlighted colour by one set of rules — see
10361
+ // `lib/code-highlight.ts`.
10362
+ //
10363
+ // A code block sits on `bg-muted`, which is light in the light theme and
10364
+ // dark in the dark one, so every colour needs both. `ui-code-block`'s own
10365
+ // palette is not reused: it is tuned for that component's fixed
10366
+ // `bg-zinc-950` surface and is unreadable on a light page.
10367
+ '[&_.token-keyword]:text-violet-700 dark:[&_.token-keyword]:text-violet-300 [&_.token-keyword]:font-semibold',
10368
+ '[&_.token-string]:text-emerald-700 dark:[&_.token-string]:text-emerald-300',
10369
+ '[&_.token-comment]:text-muted-foreground [&_.token-comment]:italic',
10370
+ '[&_.token-number]:text-amber-700 dark:[&_.token-number]:text-amber-300',
10371
+ '[&_.token-function]:text-blue-700 dark:[&_.token-function]:text-blue-300',
10372
+ '[&_.token-decorator]:text-fuchsia-700 dark:[&_.token-decorator]:text-fuchsia-300',
10373
+ '[&_.token-tag]:text-rose-700 dark:[&_.token-tag]:text-rose-300',
10374
+ '[&_.token-selector]:text-rose-700 dark:[&_.token-selector]:text-rose-300 [&_.token-selector]:font-semibold',
10375
+ '[&_.token-attr]:text-sky-700 dark:[&_.token-attr]:text-sky-300',
10376
+ '[&_.token-property]:text-sky-700 dark:[&_.token-property]:text-sky-300',
9686
10377
  ];
9687
10378
 
9688
10379
  /**
@@ -10039,6 +10730,9 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
10039
10730
  if (!this.editorDiv)
10040
10731
  return;
10041
10732
  this.editorDiv.nativeElement.innerHTML = html;
10733
+ // Every block, because every block is new. This is the only path that
10734
+ // repaints the whole document; typing repaints the caret's block alone.
10735
+ highlightCodeBlocks(this.editorDiv.nativeElement);
10042
10736
  this.labelBlockedImages();
10043
10737
  this.selectedImageNode.set(null);
10044
10738
  this.tableCellSelected.set([]);
@@ -10228,6 +10922,18 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
10228
10922
  * on every keystroke.
10229
10923
  */
10230
10924
  this.findDebounceMs = input(150, ...(ngDevMode ? [{ debugName: "findDebounceMs" }] : /* istanbul ignore next */ []));
10925
+ /**
10926
+ * Milliseconds of quiet before the caret's code block is re-highlighted, so
10927
+ * a burst of keystrokes repaints once. `0` repaints synchronously on every
10928
+ * keystroke, which is what the unit suite uses.
10929
+ *
10930
+ * Highlighting is presentational only: the spans it paints never reach the
10931
+ * value (`readContentFromEditor` unwraps them) and never reach the history,
10932
+ * so this knob changes when colour catches up with typing and nothing else.
10933
+ * Set it to a large number, or highlight nothing, by giving your fences a
10934
+ * language the highlighter does not know.
10935
+ */
10936
+ this.codeHighlightDebounceMs = input(150, ...(ngDevMode ? [{ debugName: "codeHighlightDebounceMs" }] : /* istanbul ignore next */ []));
10231
10937
  /**
10232
10938
  * Hosts whose remote images and CSS backgrounds may load. Empty (the
10233
10939
  * default) means no policy: any `https://` source is permitted, which is how
@@ -10493,6 +11199,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
10493
11199
  .replace('{total}', String(total));
10494
11200
  }, ...(ngDevMode ? [{ debugName: "findCounterText" }] : /* istanbul ignore next */ []));
10495
11201
  this.findDebounceTimer = null;
11202
+ this.codeHighlightTimer = null;
10496
11203
  this.findOverlay = null;
10497
11204
  this.findRepaintHandle = null;
10498
11205
  this.findResizeObserver = null;
@@ -11068,7 +11775,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
11068
11775
  this.lastInputRule = null;
11069
11776
  const transformed = this.applyInputRules(event);
11070
11777
  this.sweepSpentCaretAnchors(div);
11071
- const html = this.sanitizer.sanitize(div.innerHTML).replaceAll('\u200B', '');
11778
+ const html = this.sanitizer.sanitize(this.undecoratedMarkup(div)).replaceAll('\u200B', '');
11072
11779
  const triggerTextContent = this.buildTriggerAwareText(div.innerHTML);
11073
11780
  const selection = this.document.getSelection();
11074
11781
  const hasSelection = !!selection && selection.rangeCount > 0;
@@ -11087,6 +11794,10 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
11087
11794
  else {
11088
11795
  this.scheduleDebouncedHistoryPush();
11089
11796
  }
11797
+ // LAST, and after the value has already been read: the repaint must
11798
+ // never be what a reader of the document sees, only what the author
11799
+ // does.
11800
+ this.scheduleCodeHighlight();
11090
11801
  }
11091
11802
  /**
11092
11803
  * Template-bound `keydown` handler on the editable area, in strict priority
@@ -14840,6 +15551,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
14840
15551
  closeFindReplace() {
14841
15552
  const current = this.findMatches()[this.findCurrentIndex()]?.cloneRange();
14842
15553
  this.cancelPendingFind();
15554
+ this.cancelPendingCodeHighlight();
14843
15555
  this.teardownFindOverlay();
14844
15556
  this.findReplaceVisible.set(false);
14845
15557
  this.findQuery.set('');
@@ -15482,10 +16194,30 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
15482
16194
  const editorElement = this.getEditorElement();
15483
16195
  if (!editorElement)
15484
16196
  return null;
15485
- const html = this.sanitizer.sanitize(editorElement.innerHTML).replaceAll('\u200B', '');
16197
+ const html = this.sanitizer.sanitize(this.undecoratedMarkup(editorElement)).replaceAll('\u200B', '');
15486
16198
  this.htmlContent.set(html);
15487
16199
  return html;
15488
16200
  }
16201
+ /**
16202
+ * The editable's markup with the highlighter's spans unwrapped.
16203
+ *
16204
+ * Syntax colour is DECORATION: the author typed the code, not the spans
16205
+ * around it, so they must not reach the value, the history or the markdown.
16206
+ * Left in, every keystroke inside a fence emitted a different HTML string
16207
+ * for the same code, and each undo step carried a repaint of its own.
16208
+ *
16209
+ * Read from a copy, so the live DOM \u2014 and with it the caret and the colour
16210
+ * the author is looking at \u2014 is never touched. The copy is skipped entirely
16211
+ * when the document carries no token span, which is every document with no
16212
+ * code in it.
16213
+ */
16214
+ undecoratedMarkup(editorElement) {
16215
+ if (!editorElement.querySelector('pre > code span.token'))
16216
+ return editorElement.innerHTML;
16217
+ const copy = editorElement.cloneNode(true);
16218
+ stripCodeHighlighting(copy);
16219
+ return copy.innerHTML;
16220
+ }
15489
16221
  getMentionElementsInSelection() {
15490
16222
  const editor = this.getEditorElement();
15491
16223
  const selection = this.document.getSelection();
@@ -16260,6 +16992,29 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
16260
16992
  const quoted = line ? this.quoteLines([line]) : [];
16261
16993
  return quoted[0] ?? block;
16262
16994
  }
16995
+ /**
16996
+ * Quote one block `levels` deep, or as deep as the markdown reader's
16997
+ * ceiling still allows.
16998
+ *
16999
+ * The ceiling is the READER's ({@link MAX_NESTING_DEPTH}), not one this rule
17000
+ * invents: a quote nested past it is unwrapped when the document is read
17001
+ * back, so building one here types a structure the next load throws away.
17002
+ * Quotes the block ALREADY sits in count towards it, which is also what
17003
+ * makes typing ">" inside a quote nest one level deeper instead of doing
17004
+ * nothing.
17005
+ *
17006
+ * Every level is built inside the one snapshot `tryBlockRule` took, so the
17007
+ * whole transform is a single undo step and Backspace restores all of the
17008
+ * markers the author typed, not just the last one.
17009
+ */
17010
+ quoteBlockToDepth(block, levels) {
17011
+ const editor = this.editorDiv?.nativeElement;
17012
+ const room = MAX_NESTING_DEPTH - (editor ? blockquoteDepthOf(block, editor) : 0);
17013
+ let quoted = block;
17014
+ for (let level = 0; level < Math.min(levels, room); level++)
17015
+ quoted = this.quoteBlock(quoted);
17016
+ return quoted;
17017
+ }
16263
17018
  /**
16264
17019
  * Wrap lines in one `<blockquote>` and return them as its lines.
16265
17020
  *
@@ -17181,7 +17936,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
17181
17936
  case 'orderedList':
17182
17937
  return this.finishBlockRule(this.wrapBlockInList(block, 'ol'));
17183
17938
  case 'blockquote':
17184
- return this.finishBlockRule(this.quoteBlock(block));
17939
+ return this.finishBlockRule(this.quoteBlockToDepth(block, match.depth ?? 1));
17185
17940
  case 'taskUnchecked':
17186
17941
  return this.buildTaskBlock(block, false);
17187
17942
  case 'taskChecked':
@@ -17839,6 +18594,55 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
17839
18594
  this.tableCellSelecting = false;
17840
18595
  this.tableCellSelectAnchor = null;
17841
18596
  }
18597
+ /**
18598
+ * Repaint the caret's code block after {@link codeHighlightDebounceMs} of
18599
+ * quiet. A debounce of 0 repaints inline.
18600
+ */
18601
+ scheduleCodeHighlight() {
18602
+ this.cancelPendingCodeHighlight();
18603
+ const delay = this.codeHighlightDebounceMs();
18604
+ if (delay <= 0) {
18605
+ this.highlightCaretCodeBlock();
18606
+ return;
18607
+ }
18608
+ this.codeHighlightTimer = setTimeout(() => {
18609
+ this.codeHighlightTimer = null;
18610
+ this.highlightCaretCodeBlock();
18611
+ }, delay);
18612
+ }
18613
+ cancelPendingCodeHighlight() {
18614
+ if (this.codeHighlightTimer !== null) {
18615
+ clearTimeout(this.codeHighlightTimer);
18616
+ this.codeHighlightTimer = null;
18617
+ }
18618
+ }
18619
+ /**
18620
+ * Repaint the one code block the caret is in.
18621
+ *
18622
+ * The caret's block alone, not the document: a repaint replaces every node
18623
+ * in the block, and doing that to a block the author is not in would detach
18624
+ * whatever else held a reference to it for no visible gain. A wholesale
18625
+ * load repaints everything, through {@link replaceEditorHtml}.
18626
+ *
18627
+ * Nothing here touches the history or the value. The spans are decoration:
18628
+ * `readContentFromEditor` unwraps them before anything reads the document,
18629
+ * so typing in a code block produces the same value it always did, and an
18630
+ * undo step never has a repaint of its own to step through.
18631
+ */
18632
+ highlightCaretCodeBlock() {
18633
+ const editor = this.editorDiv?.nativeElement;
18634
+ if (!editor || this.isDisabled() || this.readonly())
18635
+ return;
18636
+ const selection = this.document.getSelection();
18637
+ if (!selection || selection.rangeCount === 0)
18638
+ return;
18639
+ const start = selection.getRangeAt(0).startContainer;
18640
+ const node = start.nodeType === Node.TEXT_NODE ? start.parentElement : start;
18641
+ const code = node?.closest('pre > code');
18642
+ if (!code || !editor.contains(code))
18643
+ return;
18644
+ highlightCodeElementKeepingCaret(code, languageOfCodeElement(code), selection);
18645
+ }
17842
18646
  ngOnDestroy() {
17843
18647
  this.shortcutHandle?.unregister();
17844
18648
  this.shortcutHandle = null;
@@ -17853,7 +18657,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
17853
18657
  this.removeFloatingScrollListener();
17854
18658
  }
17855
18659
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RichTextEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
17856
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RichTextEditorComponent, isStandalone: true, selector: "ui-rich-text-editor", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, toolbarItems: { classPropertyName: "toolbarItems", publicName: "toolbarItems", isSignal: true, isRequired: false, transformFunction: null }, customToolbarItems: { classPropertyName: "customToolbarItems", publicName: "customToolbarItems", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, markdownShortcuts: { classPropertyName: "markdownShortcuts", publicName: "markdownShortcuts", isSignal: true, isRequired: false, transformFunction: null }, counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, history: { classPropertyName: "history", publicName: "history", isSignal: true, isRequired: false, transformFunction: null }, findDebounceMs: { classPropertyName: "findDebounceMs", publicName: "findDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, allowedImageHosts: { classPropertyName: "allowedImageHosts", publicName: "allowedImageHosts", isSignal: true, isRequired: false, transformFunction: null }, allowedLinkSchemes: { classPropertyName: "allowedLinkSchemes", publicName: "allowedLinkSchemes", isSignal: true, isRequired: false, transformFunction: null }, blockedImageMessage: { classPropertyName: "blockedImageMessage", publicName: "blockedImageMessage", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { customToolbarAction: "customToolbarAction", htmlChange: "htmlChange", markdownChange: "markdownChange", wordCountChange: "wordCountChange", historyChange: "historyChange", imageBlocked: "imageBlocked", focused: "focused", blurred: "blurred" }, host: { classAttribute: "block" }, providers: [
18660
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RichTextEditorComponent, isStandalone: true, selector: "ui-rich-text-editor", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, toolbarItems: { classPropertyName: "toolbarItems", publicName: "toolbarItems", isSignal: true, isRequired: false, transformFunction: null }, customToolbarItems: { classPropertyName: "customToolbarItems", publicName: "customToolbarItems", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, markdownShortcuts: { classPropertyName: "markdownShortcuts", publicName: "markdownShortcuts", isSignal: true, isRequired: false, transformFunction: null }, counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, history: { classPropertyName: "history", publicName: "history", isSignal: true, isRequired: false, transformFunction: null }, findDebounceMs: { classPropertyName: "findDebounceMs", publicName: "findDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, codeHighlightDebounceMs: { classPropertyName: "codeHighlightDebounceMs", publicName: "codeHighlightDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, allowedImageHosts: { classPropertyName: "allowedImageHosts", publicName: "allowedImageHosts", isSignal: true, isRequired: false, transformFunction: null }, allowedLinkSchemes: { classPropertyName: "allowedLinkSchemes", publicName: "allowedLinkSchemes", isSignal: true, isRequired: false, transformFunction: null }, blockedImageMessage: { classPropertyName: "blockedImageMessage", publicName: "blockedImageMessage", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { customToolbarAction: "customToolbarAction", htmlChange: "htmlChange", markdownChange: "markdownChange", wordCountChange: "wordCountChange", historyChange: "historyChange", imageBlocked: "imageBlocked", focused: "focused", blurred: "blurred" }, host: { classAttribute: "block" }, providers: [
17857
18661
  {
17858
18662
  provide: NG_VALUE_ACCESSOR,
17859
18663
  useExisting: forwardRef(() => RichTextEditorComponent),
@@ -17915,7 +18719,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
17915
18719
  }], tableContextMenuRef: [{
17916
18720
  type: ViewChild,
17917
18721
  args: ['tableContextMenuRef']
17918
- }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], toolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbarItems", required: false }] }], customToolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "customToolbarItems", required: false }] }], customToolbarAction: [{ type: i0.Output, args: ["customToolbarAction"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], markdownShortcuts: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdownShortcuts", required: false }] }], counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], history: [{ type: i0.Input, args: [{ isSignal: true, alias: "history", required: false }] }], findDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "findDebounceMs", required: false }] }], allowedImageHosts: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedImageHosts", required: false }] }], allowedLinkSchemes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedLinkSchemes", required: false }] }], blockedImageMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockedImageMessage", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], htmlChange: [{ type: i0.Output, args: ["htmlChange"] }], markdownChange: [{ type: i0.Output, args: ["markdownChange"] }], wordCountChange: [{ type: i0.Output, args: ["wordCountChange"] }], historyChange: [{ type: i0.Output, args: ["historyChange"] }], imageBlocked: [{ type: i0.Output, args: ["imageBlocked"] }], focused: [{ type: i0.Output, args: ["focused"] }], blurred: [{ type: i0.Output, args: ["blurred"] }] } });
18722
+ }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], toolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbarItems", required: false }] }], customToolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "customToolbarItems", required: false }] }], customToolbarAction: [{ type: i0.Output, args: ["customToolbarAction"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], markdownShortcuts: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdownShortcuts", required: false }] }], counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], history: [{ type: i0.Input, args: [{ isSignal: true, alias: "history", required: false }] }], findDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "findDebounceMs", required: false }] }], codeHighlightDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "codeHighlightDebounceMs", required: false }] }], allowedImageHosts: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedImageHosts", required: false }] }], allowedLinkSchemes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedLinkSchemes", required: false }] }], blockedImageMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockedImageMessage", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], htmlChange: [{ type: i0.Output, args: ["htmlChange"] }], markdownChange: [{ type: i0.Output, args: ["markdownChange"] }], wordCountChange: [{ type: i0.Output, args: ["wordCountChange"] }], historyChange: [{ type: i0.Output, args: ["historyChange"] }], imageBlocked: [{ type: i0.Output, args: ["imageBlocked"] }], focused: [{ type: i0.Output, args: ["focused"] }], blurred: [{ type: i0.Output, args: ["blurred"] }] } });
17919
18723
  /** The last child element of `parent` whose tag is `tagName`, or null. */
17920
18724
  function lastChildTagged(parent, tagName) {
17921
18725
  for (let child = parent.lastElementChild; child; child = child.previousElementSibling) {
@@ -30778,7 +31582,7 @@ class RichTextFileImportDirective {
30778
31582
  async importDocx(file) {
30779
31583
  this.assertImportableSize(file);
30780
31584
  const bytes = new Uint8Array(await file.arrayBuffer());
30781
- const { parseDocx } = await import('./gilav21-shadcn-angular-rte-docx-parser-CFx_4GC5.mjs');
31585
+ const { parseDocx } = await import('./gilav21-shadcn-angular-rte-docx-parser-B8BeJWtY.mjs');
30782
31586
  const { renderDocxForEditor } = await import('./gilav21-shadcn-angular-rte-docx-to-editor-html-Cl9eIhkC.mjs');
30783
31587
  const html = renderDocxForEditor(parseDocx(bytes));
30784
31588
  this.insertImported(html);
@@ -37740,5 +38544,5 @@ const RTE_FULL = [
37740
38544
  * Generated bundle index. Do not edit.
37741
38545
  */
37742
38546
 
37743
- export { RichTextColorsButtonComponent as $, ACTION_ATTRS as A, RICH_TEXT_LOCALES as B, RICH_TEXT_MENTIONS_LOCALES as C, DEFAULT_COLOR_PALETTE as D, EMPTINESS_FIXTURES as E, FIND_MAX_PAINTED_RECTS as F, RICH_TEXT_OUTLINE_CONTEXT as G, HEADER_BYTES as H, IMAGE_IMPORT_ACCEPT as I, RICH_TEXT_OUTLINE_LOCALES as J, RICH_TEXT_PROSE_CLASSES as K, RICH_TEXT_SHORTCUT_DEFINITIONS as L, RICH_TEXT_SLASH_COMMANDS_LOCALES as M, RICH_TEXT_TABLES_BUTTON_CONTEXT as N, RICH_TEXT_TABLES_LOCALES as O, PDF_MIME as P, RICH_TEXT_TYPOGRAPHY_BUTTON_CONTEXT as Q, RICH_TEXT_ACTIONS_SANITIZER_RULES as R, RICH_TEXT_TYPOGRAPHY_LOCALES as S, RTE_FULL as T, RichTextActionsBindDirective as U, RichTextActionsDirective as V, RichTextAiChipComponent as W, RichTextAiDirective as X, RichTextAiPanelComponent as Y, RichTextAllowDirective as Z, RichTextAllowHost as _, ACTION_PARAMS as a, hostOf as a$, RichTextColorsDirective as a0, RichTextCommandRegistry as a1, RichTextEditorAddonHost as a2, RichTextEditorComponent as a3, RichTextEmojiButtonComponent as a4, RichTextEmojiDirective as a5, RichTextFileImportButtonComponent as a6, RichTextFileImportDirective as a7, RichTextFileImportOverlayComponent as a8, RichTextHistoryDirective as a9, addonSetting as aA, anchorOverlay as aB, applyImageAlignment as aC, applyStarterStyle as aD, assertFlatParams as aE, bindRichTextActions as aF, buildDefaultSlashCommands as aG, buildEntityInsertNode as aH, buildEntityRenderContext as aI, buildFindIndex as aJ, buildLineIndex as aK, caretPosition as aL, classifyImport as aM, compileFindRegex as aN, computeSeedStyleString as aO, containsCssUrl as aP, createImageElement as aQ, cssFunctionCalls as aR, dataUrlToFile as aS, decodeCssEscapes as aT, detectTrigger as aU, directionOf as aV, dragHasSupportedDocument as aW, extractCssUrls as aX, flattenIntoRowText as aY, hasUnsafeCssFunction as aZ, holdsNothing as a_, RichTextHistoryPanelComponent as aa, RichTextImageResizerComponent as ab, RichTextImagesButtonComponent as ac, RichTextImagesDirective as ad, RichTextImagesOverlayComponent as ae, RichTextLinksButtonComponent as af, RichTextLinksDirective as ag, RichTextLinksFormComponent as ah, RichTextMarkdownService as ai, RichTextMentionPopoverComponent as aj, RichTextMentionsDirective as ak, RichTextOutlineDirective as al, RichTextOutlinePanelComponent as am, RichTextPasteNormalizerService as an, RichTextSanitizerService as ao, RichTextSlashCommandsDirective as ap, RichTextSlashCommandsMenuComponent as aq, RichTextTablesButtonComponent as ar, RichTextTablesDirective as as, RichTextToolbarComponent as at, RichTextToolbarViewContext as au, RichTextTypographyButtonComponent as av, RichTextTypographyDirective as aw, TEXT_STYLE_OPTIONS as ax, TOOLBAR_BUTTONS as ay, TRANSPARENT_PIXEL as az, AddonSlotRegistry as b, hoverCardAction as b0, hoverCardHandlers as b1, isCombinedOnElement as b2, isHostAllowed as b3, isHostBearingUrl as b4, isInlineHoldingBlock as b5, isLineOwner as b6, isNestedList as b7, isPdfHeader as b8, isPhrasing as b9, parseImageSize as bA, placeCaretIn as bB, placeImageAtSelection as bC, positionAfterLine as bD, rangeShowsNothing as bE, readActions as bF, readFileAsDataUrl as bG, remoteHostOf as bH, removeAction as bI, reportResourceDecisions as bJ, resolveFromPage as bK, richTextHasMedia as bL, richTextMaxLength as bM, richTextMinWords as bN, richTextRequired as bO, richTextVisibleText as bP, rowRunsOf as bQ, selectTriggerRange as bR, separateListKinds as bS, simpleHash as bT, stripCssComments as bU, stripStyleIfMatches as bV, structureAround as bW, validateActionId as bX, validateActionParams as bY, writeAction as bZ, writeCombined as b_, isRichTextEmpty as ba, isSupportedDocumentFile as bb, isZipHeader as bc, labelBlockedImages as bd, lastOwnInlineNode as be, lineAbove as bf, lineBelow as bg, lineIsEmpty as bh, lineIsTextOnly as bi, lineKeepsItsElement as bj, lineOf as bk, lineOwnNodes as bl, lineTagIsFixed as bm, lineText as bn, linesBetween as bo, linesMayJoin as bp, linkedPreviewDialogAction as bq, linkedPreviewDialogHandlers as br, matchBlockInputRule as bs, matchInlineInputRule as bt, mountTopLayer as bu, nodeShowsNothing as bv, normalizeHostEntry as bw, offsetToPosition as bx, openDialogAction as by, openDialogHandlers as bz, DEFAULT_FONT_FAMILIES as c, DEFAULT_HIGHLIGHT_PALETTE as d, DEFAULT_LINK_SCHEMES as e, DEFAULT_TOOLBAR_ITEMS as f, DOCUMENT_IMPORT_ACCEPT as g, DOCX_MIME as h, isValidImageMagicBytes as i, FIND_MAX_QUERY_LENGTH as j, INLINE_RULE_LOOKBEHIND as k, PresetDialogComponent as l, PresetHoverCardComponent as m, RICH_TEXT_AI_CONTEXT as n, RICH_TEXT_AI_LOCALES as o, RICH_TEXT_COLORS_LOCALES as p, RICH_TEXT_COLOR_BUTTON_CONTEXT as q, RICH_TEXT_EMOJI_CONTEXT as r, RICH_TEXT_EMOJI_LOCALES as s, RICH_TEXT_FILE_IMPORT_BUTTON_CONTEXT as t, RICH_TEXT_FILE_IMPORT_LOCALES as u, RICH_TEXT_HISTORY_LOCALES as v, RICH_TEXT_IMAGES_BUTTON_CONTEXT as w, RICH_TEXT_IMAGES_LOCALES as x, RICH_TEXT_LINKS_BUTTON_CONTEXT as y, RICH_TEXT_LINKS_LOCALES as z };
37744
- //# sourceMappingURL=gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-DWSBW7Gw.mjs.map
38547
+ export { RichTextAllowHost as $, ACTION_ATTRS as A, RICH_TEXT_LOCALES as B, RICH_TEXT_MENTIONS_LOCALES as C, DEFAULT_COLOR_PALETTE as D, EMPTINESS_FIXTURES as E, FIND_MAX_PAINTED_RECTS as F, RICH_TEXT_OUTLINE_CONTEXT as G, HEADER_BYTES as H, IMAGE_IMPORT_ACCEPT as I, RICH_TEXT_OUTLINE_LOCALES as J, RICH_TEXT_PROSE_CLASSES as K, RICH_TEXT_SHORTCUT_DEFINITIONS as L, MAX_NESTING_DEPTH as M, RICH_TEXT_SLASH_COMMANDS_LOCALES as N, RICH_TEXT_TABLES_BUTTON_CONTEXT as O, PDF_MIME as P, RICH_TEXT_TABLES_LOCALES as Q, RICH_TEXT_ACTIONS_SANITIZER_RULES as R, RICH_TEXT_TYPOGRAPHY_BUTTON_CONTEXT as S, RICH_TEXT_TYPOGRAPHY_LOCALES as T, RTE_FULL as U, RichTextActionsBindDirective as V, RichTextActionsDirective as W, RichTextAiChipComponent as X, RichTextAiDirective as Y, RichTextAiPanelComponent as Z, RichTextAllowDirective as _, ACTION_PARAMS as a, hasUnsafeCssFunction as a$, RichTextColorsButtonComponent as a0, RichTextColorsDirective as a1, RichTextCommandRegistry as a2, RichTextEditorAddonHost as a3, RichTextEditorComponent as a4, RichTextEmojiButtonComponent as a5, RichTextEmojiDirective as a6, RichTextFileImportButtonComponent as a7, RichTextFileImportDirective as a8, RichTextFileImportOverlayComponent as a9, TRANSPARENT_PIXEL as aA, addonSetting as aB, anchorOverlay as aC, applyImageAlignment as aD, applyStarterStyle as aE, assertFlatParams as aF, bindRichTextActions as aG, blockquoteDepthOf as aH, buildDefaultSlashCommands as aI, buildEntityInsertNode as aJ, buildEntityRenderContext as aK, buildFindIndex as aL, buildLineIndex as aM, caretPosition as aN, classifyImport as aO, compileFindRegex as aP, computeSeedStyleString as aQ, containsCssUrl as aR, createImageElement as aS, cssFunctionCalls as aT, dataUrlToFile as aU, decodeCssEscapes as aV, detectTrigger as aW, directionOf as aX, dragHasSupportedDocument as aY, extractCssUrls as aZ, flattenIntoRowText as a_, RichTextHistoryDirective as aa, RichTextHistoryPanelComponent as ab, RichTextImageResizerComponent as ac, RichTextImagesButtonComponent as ad, RichTextImagesDirective as ae, RichTextImagesOverlayComponent as af, RichTextLinksButtonComponent as ag, RichTextLinksDirective as ah, RichTextLinksFormComponent as ai, RichTextMarkdownService as aj, RichTextMentionPopoverComponent as ak, RichTextMentionsDirective as al, RichTextOutlineDirective as am, RichTextOutlinePanelComponent as an, RichTextPasteNormalizerService as ao, RichTextSanitizerService as ap, RichTextSlashCommandsDirective as aq, RichTextSlashCommandsMenuComponent as ar, RichTextTablesButtonComponent as as, RichTextTablesDirective as at, RichTextToolbarComponent as au, RichTextToolbarViewContext as av, RichTextTypographyButtonComponent as aw, RichTextTypographyDirective as ax, TEXT_STYLE_OPTIONS as ay, TOOLBAR_BUTTONS as az, AddonSlotRegistry as b, writeAction as b$, holdsNothing as b0, hostOf as b1, hoverCardAction as b2, hoverCardHandlers as b3, isCombinedOnElement as b4, isHostAllowed as b5, isHostBearingUrl as b6, isInlineHoldingBlock as b7, isLineOwner as b8, isNestedList as b9, openDialogAction as bA, openDialogHandlers as bB, parseImageSize as bC, placeCaretIn as bD, placeImageAtSelection as bE, positionAfterLine as bF, rangeShowsNothing as bG, readActions as bH, readFileAsDataUrl as bI, remoteHostOf as bJ, removeAction as bK, reportResourceDecisions as bL, resolveFromPage as bM, richTextHasMedia as bN, richTextMaxLength as bO, richTextMinWords as bP, richTextRequired as bQ, richTextVisibleText as bR, rowRunsOf as bS, selectTriggerRange as bT, separateListKinds as bU, simpleHash as bV, stripCssComments as bW, stripStyleIfMatches as bX, structureAround as bY, validateActionId as bZ, validateActionParams as b_, isPdfHeader as ba, isPhrasing as bb, isRichTextEmpty as bc, isSupportedDocumentFile as bd, isZipHeader as be, labelBlockedImages as bf, lastOwnInlineNode as bg, lineAbove as bh, lineBelow as bi, lineIsEmpty as bj, lineIsTextOnly as bk, lineKeepsItsElement as bl, lineOf as bm, lineOwnNodes as bn, lineTagIsFixed as bo, lineText as bp, linesBetween as bq, linesMayJoin as br, linkedPreviewDialogAction as bs, linkedPreviewDialogHandlers as bt, matchBlockInputRule as bu, matchInlineInputRule as bv, mountTopLayer as bw, nodeShowsNothing as bx, normalizeHostEntry as by, offsetToPosition as bz, DEFAULT_FONT_FAMILIES as c, writeCombined as c0, DEFAULT_HIGHLIGHT_PALETTE as d, DEFAULT_LINK_SCHEMES as e, DEFAULT_TOOLBAR_ITEMS as f, DOCUMENT_IMPORT_ACCEPT as g, DOCX_MIME as h, isValidImageMagicBytes as i, FIND_MAX_QUERY_LENGTH as j, INLINE_RULE_LOOKBEHIND as k, PresetDialogComponent as l, PresetHoverCardComponent as m, RICH_TEXT_AI_CONTEXT as n, RICH_TEXT_AI_LOCALES as o, RICH_TEXT_COLORS_LOCALES as p, RICH_TEXT_COLOR_BUTTON_CONTEXT as q, RICH_TEXT_EMOJI_CONTEXT as r, RICH_TEXT_EMOJI_LOCALES as s, RICH_TEXT_FILE_IMPORT_BUTTON_CONTEXT as t, RICH_TEXT_FILE_IMPORT_LOCALES as u, RICH_TEXT_HISTORY_LOCALES as v, RICH_TEXT_IMAGES_BUTTON_CONTEXT as w, RICH_TEXT_IMAGES_LOCALES as x, RICH_TEXT_LINKS_BUTTON_CONTEXT as y, RICH_TEXT_LINKS_LOCALES as z };
38548
+ //# sourceMappingURL=gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-CfjU_y-y.mjs.map