@arsedizioni/ars-utils 22.1.5 → 22.5.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.
Files changed (78) hide show
  1. package/README.md +158 -10
  2. package/clipper.ui/clipper.scss +564 -0
  3. package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs +1 -1
  4. package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs.map +1 -1
  5. package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs +11 -7
  6. package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs.map +1 -1
  7. package/fesm2022/arsedizioni-ars-utils-core.date.mjs +609 -0
  8. package/fesm2022/arsedizioni-ars-utils-core.date.mjs.map +1 -0
  9. package/fesm2022/arsedizioni-ars-utils-core.markdown.mjs +591 -0
  10. package/fesm2022/arsedizioni-ars-utils-core.markdown.mjs.map +1 -0
  11. package/fesm2022/arsedizioni-ars-utils-core.mjs +270 -1449
  12. package/fesm2022/arsedizioni-ars-utils-core.mjs.map +1 -1
  13. package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs +1 -1
  14. package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs.map +1 -1
  15. package/fesm2022/arsedizioni-ars-utils-support.common.mjs +1 -1
  16. package/fesm2022/arsedizioni-ars-utils-support.common.mjs.map +1 -1
  17. package/fesm2022/arsedizioni-ars-utils-ui.application.mjs +14 -4108
  18. package/fesm2022/arsedizioni-ars-utils-ui.application.mjs.map +1 -1
  19. package/fesm2022/arsedizioni-ars-utils-ui.controls.mjs +962 -0
  20. package/fesm2022/arsedizioni-ars-utils-ui.controls.mjs.map +1 -0
  21. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.auth.mjs +506 -0
  22. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.auth.mjs.map +1 -0
  23. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.mjs +450 -0
  24. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.mjs.map +1 -0
  25. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.prompt.mjs +184 -0
  26. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.prompt.mjs.map +1 -0
  27. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.select.mjs +825 -0
  28. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.select.mjs.map +1 -0
  29. package/fesm2022/arsedizioni-ars-utils-ui.files.mjs +708 -0
  30. package/fesm2022/arsedizioni-ars-utils-ui.files.mjs.map +1 -0
  31. package/fesm2022/arsedizioni-ars-utils-ui.filters.mjs +426 -0
  32. package/fesm2022/arsedizioni-ars-utils-ui.filters.mjs.map +1 -0
  33. package/fesm2022/arsedizioni-ars-utils-ui.help.mjs +531 -0
  34. package/fesm2022/arsedizioni-ars-utils-ui.help.mjs.map +1 -0
  35. package/fesm2022/arsedizioni-ars-utils-ui.mjs +54 -1227
  36. package/fesm2022/arsedizioni-ars-utils-ui.mjs.map +1 -1
  37. package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs +1049 -0
  38. package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs.map +1 -0
  39. package/fesm2022/arsedizioni-ars-utils-ui.notifications.mjs +263 -0
  40. package/fesm2022/arsedizioni-ars-utils-ui.notifications.mjs.map +1 -0
  41. package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs +4 -3
  42. package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs.map +1 -1
  43. package/fesm2022/arsedizioni-ars-utils-ui.paginator.mjs +53 -0
  44. package/fesm2022/arsedizioni-ars-utils-ui.paginator.mjs.map +1 -0
  45. package/fesm2022/arsedizioni-ars-utils-ui.shell.mjs +517 -0
  46. package/fesm2022/arsedizioni-ars-utils-ui.shell.mjs.map +1 -0
  47. package/fesm2022/arsedizioni-ars-utils-ui.tinymce.mjs +246 -0
  48. package/fesm2022/arsedizioni-ars-utils-ui.tinymce.mjs.map +1 -0
  49. package/fesm2022/arsedizioni-ars-utils.mjs +13 -12
  50. package/fesm2022/arsedizioni-ars-utils.mjs.map +1 -1
  51. package/help/package.json +4 -0
  52. package/package.json +80 -14
  53. package/styles/ui.colors.scss +23 -0
  54. package/support.ui/package.json +4 -0
  55. package/tinymce/package.json +4 -0
  56. package/types/arsedizioni-ars-utils-clipper.common.d.ts +1 -2
  57. package/types/arsedizioni-ars-utils-clipper.ui.d.ts +4 -1
  58. package/types/arsedizioni-ars-utils-core.d.ts +684 -1004
  59. package/types/arsedizioni-ars-utils-core.date.d.ts +304 -0
  60. package/types/arsedizioni-ars-utils-core.markdown.d.ts +159 -0
  61. package/types/arsedizioni-ars-utils-ui.application.d.ts +10 -2022
  62. package/types/arsedizioni-ars-utils-ui.controls.d.ts +413 -0
  63. package/types/arsedizioni-ars-utils-ui.d.ts +183 -566
  64. package/types/arsedizioni-ars-utils-ui.dialogs.auth.d.ts +224 -0
  65. package/types/arsedizioni-ars-utils-ui.dialogs.d.ts +228 -0
  66. package/types/arsedizioni-ars-utils-ui.dialogs.prompt.d.ts +47 -0
  67. package/types/arsedizioni-ars-utils-ui.dialogs.select.d.ts +309 -0
  68. package/types/arsedizioni-ars-utils-ui.files.d.ts +271 -0
  69. package/types/arsedizioni-ars-utils-ui.filters.d.ts +163 -0
  70. package/types/arsedizioni-ars-utils-ui.help.d.ts +220 -0
  71. package/types/arsedizioni-ars-utils-ui.navigation.d.ts +650 -0
  72. package/types/arsedizioni-ars-utils-ui.notifications.d.ts +73 -0
  73. package/types/arsedizioni-ars-utils-ui.paginator.d.ts +23 -0
  74. package/types/arsedizioni-ars-utils-ui.shell.d.ts +345 -0
  75. package/types/arsedizioni-ars-utils-ui.tinymce.d.ts +63 -0
  76. package/types/arsedizioni-ars-utils.d.ts +4 -10
  77. package/ui.tinymce/README.md +12 -0
  78. package/ui.tinymce/langs/it.js +466 -0
@@ -0,0 +1,591 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Pipe } from '@angular/core';
3
+ import { DomSanitizer } from '@angular/platform-browser';
4
+
5
+ /**
6
+ * Zero-dependency Markdown to HTML converter.
7
+ *
8
+ * Design goals:
9
+ * - Single line-based pass over blocks (no repeated full-string regex passes).
10
+ * - Inline formatting never touches code spans / code blocks (stash & restore).
11
+ * - No HTML entity escaping by default in normal text: the source passes as-is.
12
+ * Exception: the content of code spans / code blocks is ALWAYS escaped, so
13
+ * things like List<string> render correctly (the browser displays the
14
+ * original characters; entities never reach the user or the clipboard).
15
+ * Opt-in full escaping via { escapeHtml: true } for untrusted input.
16
+ * - URL sanitization on links/images (javascript:, vbscript:, data: are dropped).
17
+ * - Bounded result cache for repeated renders (Angular change detection friendly).
18
+ *
19
+ * Supported syntax: headings, paragraphs, hard/soft breaks, hr, blockquotes
20
+ * (nested), fenced code blocks, inline code, bold/italic/strikethrough, links
21
+ * (with title), images, autolinked bare URLs, ordered/unordered lists (nested,
22
+ * task lists, ordered start offset), GFM tables with alignment, raw HTML
23
+ * blocks (e.g. <table>...</table>) passed through verbatim when escapeHtml
24
+ * is false: their newlines stay plain newlines, never converted to <br>.
25
+ *
26
+ * Known simplifications (documented, by design):
27
+ * - A blank line terminates a list.
28
+ * - Setext headings (=== / ---) are not supported, use # syntax.
29
+ * - Reference-style links [text][ref] are not supported.
30
+ */
31
+ class MarkdownUtils {
32
+ // #region Cache
33
+ static { this.cache = new Map(); }
34
+ static { this.CACHE_MAX = 200; }
35
+ /** Clears the internal result cache. */
36
+ static clearCache() {
37
+ this.cache.clear();
38
+ }
39
+ // #endregion
40
+ // #region Public API
41
+ /**
42
+ * Convert markdown to HTML.
43
+ * @param markdown : the markdown source
44
+ * @param options : conversion options
45
+ * @returns : the HTML string ('' when input is falsy)
46
+ */
47
+ static toHtml(markdown, options) {
48
+ if (!markdown)
49
+ return '';
50
+ const opts = { escapeHtml: false, breaks: true, ...options };
51
+ const key = `${opts.escapeHtml ? 1 : 0}${opts.breaks ? 1 : 0}|${markdown}`;
52
+ const hit = this.cache.get(key);
53
+ if (hit !== undefined)
54
+ return hit;
55
+ // Normalize line endings and fix glued sentence punctuation ("dettaglio!Ecco").
56
+ // Only when the next char is uppercase (a new sentence), so URLs with query
57
+ // strings ("/page?id=1") and code identifiers are left untouched.
58
+ const source = markdown
59
+ .replace(/\r\n?/g, '\n')
60
+ .replace(/([a-zà-ÿ][!?])([A-ZÀ-Þ])/g, '$1 $2');
61
+ const html = this.parseBlocks(source.split('\n'), opts).trim();
62
+ if (this.cache.size >= this.CACHE_MAX) {
63
+ this.cache.delete(this.cache.keys().next().value); // FIFO eviction
64
+ }
65
+ this.cache.set(key, html);
66
+ return html;
67
+ }
68
+ // #endregion
69
+ // #region Block parser (single pass)
70
+ /** Block-level HTML tags that start a raw HTML block (passthrough, no <p>/<br>). */
71
+ static { this.htmlBlockTags = new Set([
72
+ 'address', 'article', 'aside', 'blockquote', 'caption', 'colgroup', 'col',
73
+ 'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
74
+ 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
75
+ 'hr', 'iframe', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'summary',
76
+ 'table', 'tbody', 'td', 'tfoot', 'thead', 'th', 'tr', 'ul',
77
+ ]); }
78
+ /** Void/self-contained tags: a single tag, no closing tag expected. */
79
+ static { this.htmlVoidTags = new Set(['hr', 'col', 'img', 'input']); }
80
+ /**
81
+ * Precompiled: first block-level tag occurrence anywhere in a line.
82
+ * Anchored on the '<' literal, so scanning is cheap; the lookahead
83
+ * prevents partial matches ('<td' must not match inside '<tdx').
84
+ */
85
+ static { this.htmlBlockScanRe = new RegExp(`<(\\/?)(${[...MarkdownUtils.htmlBlockTags].join('|')})(?=[\\s/>])`, 'gi'); }
86
+ /** Result of scanning a line for the start of a raw HTML block. */
87
+ static findHtmlBlockStart(line) {
88
+ // HTML comment candidate (skipping occurrences inside inline code spans)
89
+ let comment = line.indexOf('<!--');
90
+ while (comment !== -1 && this.insideCodeSpan(line, comment)) {
91
+ comment = line.indexOf('<!--', comment + 4);
92
+ }
93
+ // Block tag candidate (same code-span guard)
94
+ const re = this.htmlBlockScanRe;
95
+ re.lastIndex = 0;
96
+ let m;
97
+ let tagHit;
98
+ while ((m = re.exec(line)) !== null) {
99
+ if (!this.insideCodeSpan(line, m.index)) {
100
+ tagHit = { index: m.index, tag: m[2].toLowerCase(), isClose: m[1] === '/' };
101
+ break;
102
+ }
103
+ }
104
+ if (comment !== -1 && (!tagHit || comment < tagHit.index)) {
105
+ return { index: comment, tag: '', isClose: false, isComment: true };
106
+ }
107
+ return tagHit ? { ...tagHit, isComment: false } : undefined;
108
+ }
109
+ /** True when `index` falls inside an inline code span (odd backtick count before it). */
110
+ static insideCodeSpan(line, index) {
111
+ let count = 0;
112
+ for (let i = 0; i < index; i++) {
113
+ if (line.charCodeAt(i) === 96 /* ` */)
114
+ count++;
115
+ }
116
+ return (count & 1) === 1;
117
+ }
118
+ /**
119
+ * indexOf-based depth scanner (no regex, no allocations): walks `text`
120
+ * adjusting `depth` for <tag ...> / </tag> occurrences of the SAME tag.
121
+ * Self-closing <tag ... /> forms do not alter depth.
122
+ * @returns [newDepth, endIndex] where endIndex is the position right after
123
+ * the '>' that balanced the element, or -1 when still open.
124
+ */
125
+ static scanHtmlDepth(text, tag, depth) {
126
+ const lower = text.toLowerCase();
127
+ const open = '<' + tag;
128
+ const close = '</' + tag;
129
+ let i = 0;
130
+ while ((i = lower.indexOf('<', i)) !== -1) {
131
+ if (lower.startsWith(close, i) && this.isTagBoundary(lower, i + close.length)) {
132
+ depth--;
133
+ const gt = lower.indexOf('>', i);
134
+ const end = gt === -1 ? lower.length : gt + 1;
135
+ if (depth <= 0)
136
+ return [0, end];
137
+ i = end;
138
+ }
139
+ else if (lower.startsWith(open, i) && this.isTagBoundary(lower, i + open.length)) {
140
+ const gt = lower.indexOf('>', i);
141
+ const selfClosing = gt !== -1 && lower.charCodeAt(gt - 1) === 47 /* / */;
142
+ if (selfClosing) {
143
+ if (depth === 0)
144
+ return [0, gt + 1]; // standalone self-closed element
145
+ }
146
+ else {
147
+ depth++;
148
+ }
149
+ i = gt === -1 ? lower.length : gt + 1;
150
+ }
151
+ else {
152
+ i++;
153
+ }
154
+ }
155
+ return [depth, -1];
156
+ }
157
+ /** A tag token must be followed by whitespace, '/', '>' or end of line. */
158
+ static isTagBoundary(s, idx) {
159
+ if (idx >= s.length)
160
+ return true; // tag opening continues on the next line
161
+ const c = s.charCodeAt(idx);
162
+ return c === 62 /* > */ || c === 47 /* / */ || c === 32 /* space */ || c === 9 /* tab */;
163
+ }
164
+ /**
165
+ * Re-injects the remainder of a partially consumed line so the main loop
166
+ * processes it as markdown. Returns the index the loop should resume from.
167
+ */
168
+ static pushBack(lines, i, remainder) {
169
+ if (remainder.trim()) {
170
+ lines[i] = remainder;
171
+ return i - 1; // the loop's i++ re-processes the remainder
172
+ }
173
+ return i;
174
+ }
175
+ /**
176
+ * Emits a raw HTML block starting at `hit.index` of lines[i], consuming
177
+ * following lines until the element is balanced. Text after the block on
178
+ * the closing line is pushed back for markdown processing.
179
+ * @returns the index of the last consumed line
180
+ */
181
+ static emitHtmlBlock(lines, i, hit, out) {
182
+ const line = lines[i];
183
+ // HTML comment: raw until '-->'
184
+ if (hit.isComment) {
185
+ let end = line.indexOf('-->', hit.index);
186
+ if (end !== -1) {
187
+ out.push(line.slice(hit.index, end + 3));
188
+ return this.pushBack(lines, i, line.slice(end + 3));
189
+ }
190
+ out.push(line.slice(hit.index));
191
+ while (++i < lines.length) {
192
+ end = lines[i].indexOf('-->');
193
+ if (end !== -1) {
194
+ out.push(lines[i].slice(0, end + 3));
195
+ return this.pushBack(lines, i, lines[i].slice(end + 3));
196
+ }
197
+ out.push(lines[i]);
198
+ }
199
+ return lines.length - 1; // unterminated: consumed to EOF
200
+ }
201
+ // Stray closing tag or void tag: emit just the tag, resume after '>'
202
+ if (hit.isClose || this.htmlVoidTags.has(hit.tag)) {
203
+ const gt = line.indexOf('>', hit.index);
204
+ const end = gt === -1 ? line.length : gt + 1;
205
+ out.push(line.slice(hit.index, end));
206
+ return this.pushBack(lines, i, line.slice(end));
207
+ }
208
+ // Opening block tag: raw until the element is balanced (depth === 0)
209
+ let [depth, split] = this.scanHtmlDepth(line.slice(hit.index), hit.tag, 0);
210
+ if (split !== -1) {
211
+ out.push(line.slice(hit.index, hit.index + split));
212
+ return this.pushBack(lines, i, line.slice(hit.index + split));
213
+ }
214
+ out.push(line.slice(hit.index));
215
+ while (depth > 0 && ++i < lines.length) {
216
+ [depth, split] = this.scanHtmlDepth(lines[i], hit.tag, depth);
217
+ if (split !== -1) {
218
+ out.push(lines[i].slice(0, split));
219
+ return this.pushBack(lines, i, lines[i].slice(split));
220
+ }
221
+ out.push(lines[i]);
222
+ }
223
+ return Math.min(i, lines.length - 1);
224
+ }
225
+ static parseBlocks(lines, opts) {
226
+ const out = [];
227
+ let paragraph = [];
228
+ const flush = () => {
229
+ if (paragraph.length > 0) {
230
+ const sep = opts.breaks ? '<br>' : ' ';
231
+ out.push(`<p>${paragraph.map(l => this.inline(l, opts)).join(sep)}</p>`);
232
+ paragraph = [];
233
+ }
234
+ };
235
+ for (let i = 0; i < lines.length; i++) {
236
+ const line = lines[i];
237
+ const trimmed = line.trim();
238
+ // --- Fenced code block -------------------------------------------------
239
+ const fence = trimmed.match(/^```([\w+-]+)?\s*$/);
240
+ if (fence) {
241
+ flush();
242
+ const lang = fence[1] ? ` class="language-${fence[1]}"` : '';
243
+ const code = [];
244
+ i++;
245
+ while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
246
+ code.push(lines[i]);
247
+ i++;
248
+ }
249
+ out.push(`<pre><code${lang}>${this.escape(code.join('\n'))}</code></pre>`);
250
+ continue;
251
+ }
252
+ // --- Blank line --------------------------------------------------------
253
+ if (!trimmed) {
254
+ flush();
255
+ continue;
256
+ }
257
+ // --- Raw HTML block passthrough (only with escaping disabled) ------------
258
+ // A block-level HTML element found ANYWHERE in the line starts a raw
259
+ // block: text before it joins the current paragraph, the element is
260
+ // emitted as-is (plain newlines, no <p>, no <br>) until balanced, and
261
+ // any text after the closing tag resumes markdown processing.
262
+ if (!opts.escapeHtml && line.includes('<')) {
263
+ const hit = this.findHtmlBlockStart(line);
264
+ if (hit) {
265
+ const before = line.slice(0, hit.index).trim();
266
+ if (before)
267
+ paragraph.push(before);
268
+ flush();
269
+ i = this.emitHtmlBlock(lines, i, hit, out);
270
+ continue;
271
+ }
272
+ }
273
+ // --- ATX heading -------------------------------------------------------
274
+ const heading = trimmed.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
275
+ if (heading) {
276
+ flush();
277
+ const level = heading[1].length;
278
+ out.push(`<h${level}>${this.inline(heading[2], opts)}</h${level}>`);
279
+ continue;
280
+ }
281
+ // --- Horizontal rule ---------------------------------------------------
282
+ if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
283
+ flush();
284
+ out.push('<hr>');
285
+ continue;
286
+ }
287
+ // --- Blockquote (consecutive lines, one level stripped, recursive) ------
288
+ if (/^>\s?/.test(trimmed)) {
289
+ flush();
290
+ const quoted = [];
291
+ while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
292
+ quoted.push(lines[i].trim().replace(/^>\s?/, ''));
293
+ i++;
294
+ }
295
+ i--;
296
+ out.push(`<blockquote>${this.parseBlocks(quoted, opts)}</blockquote>`);
297
+ continue;
298
+ }
299
+ // --- Table (header row + separator row) ---------------------------------
300
+ if (trimmed.includes('|') &&
301
+ i + 1 < lines.length &&
302
+ lines[i + 1].includes('-') &&
303
+ /^\|?[\s:\-|]+\|?$/.test(lines[i + 1].trim())) {
304
+ flush();
305
+ const aligns = this.tableAligns(lines[i + 1].trim());
306
+ const head = this.tableRow(trimmed, 'th', aligns, opts);
307
+ const body = [];
308
+ i += 2;
309
+ while (i < lines.length && lines[i].trim().includes('|')) {
310
+ body.push(this.tableRow(lines[i].trim(), 'td', aligns, opts));
311
+ i++;
312
+ }
313
+ i--;
314
+ out.push(`<table>\n<thead>\n${head}\n</thead>` +
315
+ (body.length > 0 ? `\n<tbody>\n${body.join('\n')}\n</tbody>` : '') +
316
+ `\n</table>`);
317
+ continue;
318
+ }
319
+ // --- List ---------------------------------------------------------------
320
+ if (MarkdownUtils.listItemRe.test(line)) {
321
+ flush();
322
+ const [html, last] = this.parseList(lines, i, opts);
323
+ out.push(html);
324
+ i = last;
325
+ continue;
326
+ }
327
+ // --- Plain text: accumulate into current paragraph ----------------------
328
+ paragraph.push(trimmed);
329
+ }
330
+ flush();
331
+ return out.join('\n');
332
+ }
333
+ // #endregion
334
+ // #region Lists
335
+ /** Matches "- item", "* item", "+ item", "1. item", "1) item" with leading indent. */
336
+ static { this.listItemRe = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/; }
337
+ /**
338
+ * Gathers the contiguous list block starting at `start`, builds it (with
339
+ * nesting) and returns [html, indexOfLastConsumedLine].
340
+ */
341
+ static parseList(lines, start, opts) {
342
+ const block = [];
343
+ let i = start;
344
+ while (i < lines.length) {
345
+ const l = lines[i];
346
+ if (!l.trim())
347
+ break; // blank line ends the list
348
+ if (this.listItemRe.test(l) || /^\s{2,}\S/.test(l)) {
349
+ block.push(l);
350
+ i++;
351
+ }
352
+ else
353
+ break;
354
+ }
355
+ return [this.buildList(block, opts), i - 1];
356
+ }
357
+ static buildList(block, opts) {
358
+ const first = block[0].match(this.listItemRe);
359
+ const baseIndent = first[1].length;
360
+ const ordered = /^\d/.test(first[2]);
361
+ const startNum = ordered ? parseInt(first[2], 10) : 1;
362
+ const items = [];
363
+ for (const l of block) {
364
+ const m = l.match(this.listItemRe);
365
+ if (m && m[1].length <= baseIndent) {
366
+ items.push({ text: m[3], childLines: [] });
367
+ }
368
+ else if (items.length > 0) {
369
+ // Continuation or nested content: strip one nesting level of indent
370
+ items[items.length - 1].childLines.push(l.length > baseIndent + 2 ? l.slice(baseIndent + 2) : l.trim());
371
+ }
372
+ }
373
+ const lis = items.map(item => {
374
+ let inner;
375
+ // GFM task list: - [ ] / - [x]
376
+ const task = item.text.match(/^\[([ xX])\]\s+(.*)$/);
377
+ if (task) {
378
+ const checked = task[1] !== ' ' ? ' checked' : '';
379
+ inner = `<input type="checkbox" disabled${checked}> ${this.inline(task[2], opts)}`;
380
+ }
381
+ else {
382
+ inner = this.inline(item.text, opts);
383
+ }
384
+ if (item.childLines.length > 0) {
385
+ inner += '\n' + this.parseBlocks(item.childLines, opts);
386
+ }
387
+ return `<li>${inner}</li>`;
388
+ });
389
+ const tag = ordered ? 'ol' : 'ul';
390
+ const startAttr = ordered && startNum > 1 ? ` start="${startNum}"` : '';
391
+ return `<${tag}${startAttr}>\n${lis.join('\n')}\n</${tag}>`;
392
+ }
393
+ // #endregion
394
+ // #region Tables
395
+ static tableAligns(separator) {
396
+ return separator
397
+ .replace(/^\||\|$/g, '')
398
+ .split('|')
399
+ .map(cell => {
400
+ const t = cell.trim();
401
+ if (t.startsWith(':') && t.endsWith(':'))
402
+ return 'center';
403
+ if (t.endsWith(':'))
404
+ return 'right';
405
+ if (t.startsWith(':'))
406
+ return 'left';
407
+ return undefined;
408
+ });
409
+ }
410
+ static tableRow(row, tag, aligns, opts) {
411
+ const cells = row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
412
+ const cellsHtml = cells
413
+ .map((cell, j) => {
414
+ const align = aligns[j] ? ` align="${aligns[j]}"` : '';
415
+ return `<${tag}${align}>${this.inline(cell, opts)}</${tag}>`;
416
+ })
417
+ .join('');
418
+ return `<tr>${cellsHtml}</tr>`;
419
+ }
420
+ // #endregion
421
+ // #region Inline formatting
422
+ /**
423
+ * Applies inline markdown to a single text segment.
424
+ * Generated HTML (code, links, images, autolinks) is stashed behind \u0000
425
+ * placeholders so later regex passes can never corrupt it.
426
+ */
427
+ static inline(text, opts) {
428
+ const store = [];
429
+ const stash = (html) => `\u0000${store.push(html) - 1}\u0000`;
430
+ // 1. Inline code: stash first so later passes can't touch it.
431
+ // Content is ALWAYS escaped so <, >, & inside code survive in the DOM
432
+ // (the browser renders entities back to the original characters).
433
+ let s = text.replace(/`([^`]+)`/g, (_m, code) => stash(`<code>${this.escape(code)}</code>`));
434
+ // 2. Escape raw HTML as entities (opt-in only)
435
+ if (opts.escapeHtml)
436
+ s = this.escape(s);
437
+ // 3. Images: ![alt](url "title")
438
+ s = s.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+(?:"|&quot;)(.+?)(?:"|&quot;))?\s*\)/g, (_m, alt, src, title) => {
439
+ const url = this.safeUrl(src);
440
+ if (!url)
441
+ return alt;
442
+ const t = title ? ` title="${this.escapeAttr(title)}"` : '';
443
+ return stash(`<img src="${url}" alt="${this.escapeAttr(alt)}"${t} loading="lazy">`);
444
+ });
445
+ // 4. Links: [text](url "title")
446
+ s = s.replace(/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+(?:"|&quot;)(.+?)(?:"|&quot;))?\s*\)/g, (_m, txt, href, title) => {
447
+ const url = this.safeUrl(href);
448
+ if (!url)
449
+ return txt;
450
+ const t = title ? ` title="${this.escapeAttr(title)}"` : '';
451
+ return stash(`<a href="${url}"${t} target="_blank" rel="noopener noreferrer">${txt}</a>`);
452
+ });
453
+ // 5. Autolink remaining bare URLs (markdown links are already stashed)
454
+ s = s.replace(/https?:\/\/[^\s<>"']+/g, url => stash(`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`));
455
+ // 6. Bold / italic / strikethrough.
456
+ // Delimiters must hug the text; underscore emphasis only at word
457
+ // boundaries so identifiers like snake_case are left untouched.
458
+ s = s.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>');
459
+ s = s.replace(/(?<![\w*])___(?=\S)([\s\S]*?\S)___(?![\w*])/g, '<strong><em>$1</em></strong>');
460
+ s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>');
461
+ s = s.replace(/(?<![\w*])__(?=\S)([\s\S]*?\S)__(?![\w*])/g, '<strong>$1</strong>');
462
+ s = s.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '<em>$1</em>');
463
+ s = s.replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>');
464
+ s = s.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
465
+ // 7. Restore stashed HTML
466
+ return s.replace(/\u0000(\d+)\u0000/g, (_m, idx) => store[Number(idx)] ?? '');
467
+ }
468
+ // #endregion
469
+ // #region Security helpers
470
+ /** Escapes quotes for safe interpolation inside a double-quoted HTML attribute. */
471
+ static escapeAttr(s) {
472
+ return s.replace(/&(?!(?:amp|lt|gt|quot|#\d+);)/g, '&amp;').replace(/"/g, '&quot;');
473
+ }
474
+ /** Escapes &, <, > and " for safe HTML interpolation. */
475
+ static escape(s) {
476
+ return s
477
+ .replace(/&/g, '&amp;')
478
+ .replace(/</g, '&lt;')
479
+ .replace(/>/g, '&gt;')
480
+ .replace(/"/g, '&quot;');
481
+ }
482
+ /**
483
+ * Returns a sanitized URL or undefined when the scheme is dangerous.
484
+ * Blocks javascript:, vbscript: and data: (also when obfuscated with
485
+ * whitespace/control characters, e.g. "java\tscript:").
486
+ */
487
+ static safeUrl(url) {
488
+ const compact = url.replace(/[\s\u0000-\u001f]/g, '');
489
+ if (/^(javascript|vbscript|data):/i.test(compact))
490
+ return undefined;
491
+ return url.replace(/"/g, '%22');
492
+ }
493
+ // #endregion
494
+ // #region Clipboard
495
+ /**
496
+ * Copies markdown content to the clipboard in two flavors:
497
+ * - text/html : the rendered HTML (rich paste into Word, Outlook, Gmail...)
498
+ * - text/plain : the original markdown source (paste into editors, IDEs...)
499
+ * @param markdown : the markdown source
500
+ * @param options : conversion options for the HTML flavor
501
+ * @returns : true on success
502
+ */
503
+ static async copyToClipboard(markdown, options) {
504
+ if (!markdown || typeof navigator === 'undefined' || !navigator.clipboard)
505
+ return false;
506
+ try {
507
+ if (typeof ClipboardItem !== 'undefined') {
508
+ const html = this.toHtml(markdown, options);
509
+ await navigator.clipboard.write([
510
+ new ClipboardItem({
511
+ 'text/html': new Blob([html], { type: 'text/html' }),
512
+ 'text/plain': new Blob([markdown], { type: 'text/plain' }),
513
+ }),
514
+ ]);
515
+ }
516
+ else {
517
+ // Older engines: plain text only
518
+ await navigator.clipboard.writeText(markdown);
519
+ }
520
+ return true;
521
+ }
522
+ catch {
523
+ // Clipboard API requires a secure context and user activation
524
+ return false;
525
+ }
526
+ }
527
+ /**
528
+ * Copies plain text (e.g. the content of a single code block) to the clipboard.
529
+ * @param text : the text to copy
530
+ * @returns : true on success
531
+ */
532
+ static async copyText(text) {
533
+ if (!text || typeof navigator === 'undefined' || !navigator.clipboard)
534
+ return false;
535
+ try {
536
+ await navigator.clipboard.writeText(text);
537
+ return true;
538
+ }
539
+ catch {
540
+ return false;
541
+ }
542
+ }
543
+ /**
544
+ * Extracts the visible plain text from a rendered element
545
+ * (what the user sees, entities already decoded by the browser).
546
+ * @param element : the element hosting the rendered markdown
547
+ * @returns : the plain text
548
+ */
549
+ static elementToText(element) {
550
+ return element.innerText ?? element.textContent ?? '';
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Pipe that converts a Markdown string to sanitized HTML using `MarkdownUtils.toHtml`.
556
+ *
557
+ * Usage: `{{ text | formatMarkdown }}`
558
+ */
559
+ class FormatMarkdownPipe {
560
+ constructor() {
561
+ this.sanitizer = inject(DomSanitizer);
562
+ }
563
+ /**
564
+ * Transforms a Markdown string into sanitized HTML.
565
+ * @param value - The Markdown input to convert. Treated as an empty string when `undefined`.
566
+ * @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
567
+ */
568
+ transform(value) {
569
+ return this.sanitizer.bypassSecurityTrustHtml(MarkdownUtils.toHtml(value ?? '', { escapeHtml: false }));
570
+ }
571
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: FormatMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
572
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.3", ngImport: i0, type: FormatMarkdownPipe, isStandalone: true, name: "formatMarkdown" }); }
573
+ }
574
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: FormatMarkdownPipe, decorators: [{
575
+ type: Pipe,
576
+ args: [{
577
+ name: 'formatMarkdown',
578
+ standalone: true
579
+ }]
580
+ }] });
581
+
582
+ /*
583
+ * Public API Surface of @arsedizioni/ars-utils/core.markdown
584
+ */
585
+
586
+ /**
587
+ * Generated bundle index. Do not edit.
588
+ */
589
+
590
+ export { FormatMarkdownPipe, MarkdownUtils };
591
+ //# sourceMappingURL=arsedizioni-ars-utils-core.markdown.mjs.map