@arsedizioni/ars-utils 22.0.21 → 22.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/arsedizioni-ars-utils-core.mjs +2263 -1962
- package/fesm2022/arsedizioni-ars-utils-core.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-tinymce.mjs +24 -22
- package/fesm2022/arsedizioni-ars-utils-tinymce.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-ui.application.mjs +129 -98
- package/fesm2022/arsedizioni-ars-utils-ui.application.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-ui.mjs +3228 -3055
- package/fesm2022/arsedizioni-ars-utils-ui.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs +63 -47
- package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs.map +1 -1
- package/package.json +1 -1
- package/types/arsedizioni-ars-utils-core.d.ts +200 -47
- package/types/arsedizioni-ars-utils-tinymce.d.ts +5 -1
- package/types/arsedizioni-ars-utils-ui.application.d.ts +21 -16
- package/types/arsedizioni-ars-utils-ui.d.ts +102 -146
- package/types/arsedizioni-ars-utils-ui.oauth.d.ts +7 -1
|
@@ -1,2091 +1,2008 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import { parseISO, parse, format, getYear, getMonth, getDate, getDay, getDaysInMonth, addYears, addMonths, addDays, formatISO, isDate, isValid, endOfDay } from 'date-fns';
|
|
4
|
-
import { it } from 'date-fns/locale';
|
|
5
|
-
import { TZDate } from '@date-fns/tz';
|
|
6
|
-
import { DomSanitizer } from '@angular/platform-browser';
|
|
2
|
+
import { inject, Injectable, makeEnvironmentProviders, ElementRef, afterNextRender, Directive, input, DestroyRef, HostListener, output, forwardRef, effect, Pipe, EventEmitter, signal, computed, Service, PLATFORM_ID, RendererFactory2 } from '@angular/core';
|
|
7
3
|
import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
|
|
4
|
+
import { TZDate } from '@date-fns/tz';
|
|
5
|
+
import { getYear, getMonth, getDate, getDay, getDaysInMonth, parseISO, parse, format, addYears, addMonths, addDays, formatISO, isDate, isValid, endOfDay } from 'date-fns';
|
|
8
6
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
9
|
-
import { Subject, BehaviorSubject } from 'rxjs';
|
|
10
|
-
import { debounceTime } from 'rxjs/operators';
|
|
7
|
+
import { Subject, filter as filter$1, map as map$1, BehaviorSubject } from 'rxjs';
|
|
8
|
+
import { debounceTime, filter, map } from 'rxjs/operators';
|
|
9
|
+
import { it } from 'date-fns/locale';
|
|
11
10
|
import { NG_VALIDATORS } from '@angular/forms';
|
|
11
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
12
12
|
import { SelectionModel } from '@angular/cdk/collections';
|
|
13
13
|
import { isPlatformBrowser } from '@angular/common';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - Inline formatting never touches code spans / code blocks (stash & restore).
|
|
21
|
-
* - No HTML entity escaping by default in normal text: the source passes as-is.
|
|
22
|
-
* Exception: the content of code spans / code blocks is ALWAYS escaped, so
|
|
23
|
-
* things like List<string> render correctly (the browser displays the
|
|
24
|
-
* original characters; entities never reach the user or the clipboard).
|
|
25
|
-
* Opt-in full escaping via { escapeHtml: true } for untrusted input.
|
|
26
|
-
* - URL sanitization on links/images (javascript:, vbscript:, data: are dropped).
|
|
27
|
-
* - Bounded result cache for repeated renders (Angular change detection friendly).
|
|
28
|
-
*
|
|
29
|
-
* Supported syntax: headings, paragraphs, hard/soft breaks, hr, blockquotes
|
|
30
|
-
* (nested), fenced code blocks, inline code, bold/italic/strikethrough, links
|
|
31
|
-
* (with title), images, autolinked bare URLs, ordered/unordered lists (nested,
|
|
32
|
-
* task lists, ordered start offset), GFM tables with alignment, raw HTML
|
|
33
|
-
* blocks (e.g. <table>...</table>) passed through verbatim when escapeHtml
|
|
34
|
-
* is false: their newlines stay plain newlines, never converted to <br>.
|
|
35
|
-
*
|
|
36
|
-
* Known simplifications (documented, by design):
|
|
37
|
-
* - A blank line terminates a list.
|
|
38
|
-
* - Setext headings (=== / ---) are not supported, use # syntax.
|
|
39
|
-
* - Reference-style links [text][ref] are not supported.
|
|
16
|
+
* Creates an array of the given length, filling each slot with the result of `valueFunction`.
|
|
17
|
+
* @param length - Number of elements to create.
|
|
18
|
+
* @param valueFunction - Factory called with each index to produce the element value.
|
|
19
|
+
* @returns Typed array of `length` elements.
|
|
40
20
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/** Clears the internal result cache. */
|
|
46
|
-
static clearCache() {
|
|
47
|
-
this.cache.clear();
|
|
21
|
+
function range(length, valueFunction) {
|
|
22
|
+
const valuesArray = Array(length);
|
|
23
|
+
for (let i = 0; i < length; i++) {
|
|
24
|
+
valuesArray[i] = valueFunction(i);
|
|
48
25
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
26
|
+
return valuesArray;
|
|
27
|
+
}
|
|
28
|
+
// date-fns doesn't have a way to read/print month names or days of the week directly,
|
|
29
|
+
// so we get them by formatting a date with a format that produces the desired month/day.
|
|
30
|
+
const MONTH_FORMATS = {
|
|
31
|
+
long: 'LLLL',
|
|
32
|
+
short: 'LLL',
|
|
33
|
+
narrow: 'LLLLL',
|
|
34
|
+
};
|
|
35
|
+
const DAY_OF_WEEK_FORMATS = {
|
|
36
|
+
long: 'EEEE',
|
|
37
|
+
short: 'EEE',
|
|
38
|
+
narrow: 'EEEEE',
|
|
39
|
+
};
|
|
40
|
+
const MAT_DATE_FNS_FORMATS = {
|
|
41
|
+
parse: {
|
|
42
|
+
dateInput: 'P',
|
|
43
|
+
},
|
|
44
|
+
display: {
|
|
45
|
+
dateInput: 'P',
|
|
46
|
+
monthYearLabel: 'LLL uuuu',
|
|
47
|
+
dateA11yLabel: 'PP',
|
|
48
|
+
monthYearA11yLabel: 'LLLL uuuu',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* date-fns adapter that integrates Angular Material's date picker with the date-fns library,
|
|
53
|
+
* applying `Europe/Rome` timezone for all parsed and created dates.
|
|
54
|
+
*/
|
|
55
|
+
class DateFnsAdapter extends DateAdapter {
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
const matDateLocale = inject(MAT_DATE_LOCALE, { optional: true });
|
|
59
|
+
// MAT_DATE_LOCALE defaults to LOCALE_ID (a string): only a date-fns
|
|
60
|
+
// Locale OBJECT is usable here, a string would break format()/parse().
|
|
61
|
+
if (matDateLocale && typeof matDateLocale === 'object') {
|
|
62
|
+
this.setLocale(matDateLocale);
|
|
72
63
|
}
|
|
73
|
-
this.cache.set(key, html);
|
|
74
|
-
return html;
|
|
75
64
|
}
|
|
76
|
-
// #endregion
|
|
77
|
-
// #region Block parser (single pass)
|
|
78
|
-
/** Block-level HTML tags that start a raw HTML block (passthrough, no <p>/<br>). */
|
|
79
|
-
static { this.htmlBlockTags = new Set([
|
|
80
|
-
'address', 'article', 'aside', 'blockquote', 'caption', 'colgroup', 'col',
|
|
81
|
-
'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
|
|
82
|
-
'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
|
|
83
|
-
'hr', 'iframe', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'summary',
|
|
84
|
-
'table', 'tbody', 'td', 'tfoot', 'thead', 'th', 'tr', 'ul',
|
|
85
|
-
]); }
|
|
86
|
-
/** Void/self-contained tags: a single tag, no closing tag expected. */
|
|
87
|
-
static { this.htmlVoidTags = new Set(['hr', 'col', 'img', 'input']); }
|
|
88
65
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* prevents partial matches ('<td' must not match inside '<tdx').
|
|
66
|
+
* Returns the year component of the given date.
|
|
67
|
+
* @param date - The source date.
|
|
92
68
|
*/
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
static findHtmlBlockStart(line) {
|
|
96
|
-
// HTML comment candidate (skipping occurrences inside inline code spans)
|
|
97
|
-
let comment = line.indexOf('<!--');
|
|
98
|
-
while (comment !== -1 && this.insideCodeSpan(line, comment)) {
|
|
99
|
-
comment = line.indexOf('<!--', comment + 4);
|
|
100
|
-
}
|
|
101
|
-
// Block tag candidate (same code-span guard)
|
|
102
|
-
const re = this.htmlBlockScanRe;
|
|
103
|
-
re.lastIndex = 0;
|
|
104
|
-
let m;
|
|
105
|
-
let tagHit;
|
|
106
|
-
while ((m = re.exec(line)) !== null) {
|
|
107
|
-
if (!this.insideCodeSpan(line, m.index)) {
|
|
108
|
-
tagHit = { index: m.index, tag: m[2].toLowerCase(), isClose: m[1] === '/' };
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (comment !== -1 && (!tagHit || comment < tagHit.index)) {
|
|
113
|
-
return { index: comment, tag: '', isClose: false, isComment: true };
|
|
114
|
-
}
|
|
115
|
-
return tagHit ? { ...tagHit, isComment: false } : undefined;
|
|
69
|
+
getYear(date) {
|
|
70
|
+
return getYear(date);
|
|
116
71
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
return (count & 1) === 1;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the zero-based month index of the given date (0 = January).
|
|
74
|
+
* @param date - The source date.
|
|
75
|
+
*/
|
|
76
|
+
getMonth(date) {
|
|
77
|
+
return getMonth(date);
|
|
125
78
|
}
|
|
126
79
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* Self-closing <tag ... /> forms do not alter depth.
|
|
130
|
-
* @returns [newDepth, endIndex] where endIndex is the position right after
|
|
131
|
-
* the '>' that balanced the element, or -1 when still open.
|
|
80
|
+
* Returns the day-of-month of the given date (1-based).
|
|
81
|
+
* @param date - The source date.
|
|
132
82
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const open = '<' + tag;
|
|
136
|
-
const close = '</' + tag;
|
|
137
|
-
let i = 0;
|
|
138
|
-
while ((i = lower.indexOf('<', i)) !== -1) {
|
|
139
|
-
if (lower.startsWith(close, i) && this.isTagBoundary(lower, i + close.length)) {
|
|
140
|
-
depth--;
|
|
141
|
-
const gt = lower.indexOf('>', i);
|
|
142
|
-
const end = gt === -1 ? lower.length : gt + 1;
|
|
143
|
-
if (depth <= 0)
|
|
144
|
-
return [0, end];
|
|
145
|
-
i = end;
|
|
146
|
-
}
|
|
147
|
-
else if (lower.startsWith(open, i) && this.isTagBoundary(lower, i + open.length)) {
|
|
148
|
-
const gt = lower.indexOf('>', i);
|
|
149
|
-
const selfClosing = gt !== -1 && lower.charCodeAt(gt - 1) === 47 /* / */;
|
|
150
|
-
if (selfClosing) {
|
|
151
|
-
if (depth === 0)
|
|
152
|
-
return [0, gt + 1]; // standalone self-closed element
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
depth++;
|
|
156
|
-
}
|
|
157
|
-
i = gt === -1 ? lower.length : gt + 1;
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
i++;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return [depth, -1];
|
|
83
|
+
getDate(date) {
|
|
84
|
+
return getDate(date);
|
|
164
85
|
}
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
86
|
+
/**
|
|
87
|
+
* Returns the day-of-week of the given date (0 = Sunday).
|
|
88
|
+
* @param date - The source date.
|
|
89
|
+
*/
|
|
90
|
+
getDayOfWeek(date) {
|
|
91
|
+
return getDay(date);
|
|
171
92
|
}
|
|
172
93
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
94
|
+
* Returns an array of 12 month name strings formatted for the active locale.
|
|
95
|
+
* @param style - One of `'long'`, `'short'`, or `'narrow'`.
|
|
175
96
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return i - 1; // the loop's i++ re-processes the remainder
|
|
180
|
-
}
|
|
181
|
-
return i;
|
|
97
|
+
getMonthNames(style) {
|
|
98
|
+
const pattern = MONTH_FORMATS[style];
|
|
99
|
+
return range(12, i => this.format(new Date(2017, i, 1), pattern));
|
|
182
100
|
}
|
|
183
101
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* the closing line is pushed back for markdown processing.
|
|
187
|
-
* @returns the index of the last consumed line
|
|
102
|
+
* Returns an array of 31 day-of-month label strings formatted using `Intl.DateTimeFormat`
|
|
103
|
+
* when available, falling back to plain numeric strings.
|
|
188
104
|
*/
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
105
|
+
getDateNames() {
|
|
106
|
+
const dtf = typeof Intl !== 'undefined'
|
|
107
|
+
? new Intl.DateTimeFormat(this.locale?.code, {
|
|
108
|
+
day: 'numeric',
|
|
109
|
+
timeZone: 'utc',
|
|
110
|
+
})
|
|
111
|
+
: null;
|
|
112
|
+
return range(31, i => {
|
|
113
|
+
if (dtf) {
|
|
114
|
+
// date-fns doesn't appear to support this functionality.
|
|
115
|
+
// Fall back to `Intl` on supported browsers.
|
|
116
|
+
const date = new Date();
|
|
117
|
+
date.setUTCFullYear(2017, 0, i + 1);
|
|
118
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
119
|
+
return dtf.format(date).replace(/[\u200e\u200f]/g, '');
|
|
197
120
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
end = lines[i].indexOf('-->');
|
|
201
|
-
if (end !== -1) {
|
|
202
|
-
out.push(lines[i].slice(0, end + 3));
|
|
203
|
-
return this.pushBack(lines, i, lines[i].slice(end + 3));
|
|
204
|
-
}
|
|
205
|
-
out.push(lines[i]);
|
|
206
|
-
}
|
|
207
|
-
return lines.length - 1; // unterminated: consumed to EOF
|
|
208
|
-
}
|
|
209
|
-
// Stray closing tag or void tag: emit just the tag, resume after '>'
|
|
210
|
-
if (hit.isClose || this.htmlVoidTags.has(hit.tag)) {
|
|
211
|
-
const gt = line.indexOf('>', hit.index);
|
|
212
|
-
const end = gt === -1 ? line.length : gt + 1;
|
|
213
|
-
out.push(line.slice(hit.index, end));
|
|
214
|
-
return this.pushBack(lines, i, line.slice(end));
|
|
215
|
-
}
|
|
216
|
-
// Opening block tag: raw until the element is balanced (depth === 0)
|
|
217
|
-
let [depth, split] = this.scanHtmlDepth(line.slice(hit.index), hit.tag, 0);
|
|
218
|
-
if (split !== -1) {
|
|
219
|
-
out.push(line.slice(hit.index, hit.index + split));
|
|
220
|
-
return this.pushBack(lines, i, line.slice(hit.index + split));
|
|
221
|
-
}
|
|
222
|
-
out.push(line.slice(hit.index));
|
|
223
|
-
while (depth > 0 && ++i < lines.length) {
|
|
224
|
-
[depth, split] = this.scanHtmlDepth(lines[i], hit.tag, depth);
|
|
225
|
-
if (split !== -1) {
|
|
226
|
-
out.push(lines[i].slice(0, split));
|
|
227
|
-
return this.pushBack(lines, i, lines[i].slice(split));
|
|
228
|
-
}
|
|
229
|
-
out.push(lines[i]);
|
|
230
|
-
}
|
|
231
|
-
return Math.min(i, lines.length - 1);
|
|
232
|
-
}
|
|
233
|
-
static parseBlocks(lines, opts) {
|
|
234
|
-
const out = [];
|
|
235
|
-
let paragraph = [];
|
|
236
|
-
const flush = () => {
|
|
237
|
-
if (paragraph.length > 0) {
|
|
238
|
-
const sep = opts.breaks ? '<br>' : ' ';
|
|
239
|
-
out.push(`<p>${paragraph.map(l => this.inline(l, opts)).join(sep)}</p>`);
|
|
240
|
-
paragraph = [];
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
for (let i = 0; i < lines.length; i++) {
|
|
244
|
-
const line = lines[i];
|
|
245
|
-
const trimmed = line.trim();
|
|
246
|
-
// --- Fenced code block -------------------------------------------------
|
|
247
|
-
const fence = trimmed.match(/^```([\w+-]+)?\s*$/);
|
|
248
|
-
if (fence) {
|
|
249
|
-
flush();
|
|
250
|
-
const lang = fence[1] ? ` class="language-${fence[1]}"` : '';
|
|
251
|
-
const code = [];
|
|
252
|
-
i++;
|
|
253
|
-
while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
|
|
254
|
-
code.push(lines[i]);
|
|
255
|
-
i++;
|
|
256
|
-
}
|
|
257
|
-
out.push(`<pre><code${lang}>${this.escape(code.join('\n'))}</code></pre>`);
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
// --- Blank line --------------------------------------------------------
|
|
261
|
-
if (!trimmed) {
|
|
262
|
-
flush();
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
// --- Raw HTML block passthrough (only with escaping disabled) ------------
|
|
266
|
-
// A block-level HTML element found ANYWHERE in the line starts a raw
|
|
267
|
-
// block: text before it joins the current paragraph, the element is
|
|
268
|
-
// emitted as-is (plain newlines, no <p>, no <br>) until balanced, and
|
|
269
|
-
// any text after the closing tag resumes markdown processing.
|
|
270
|
-
if (!opts.escapeHtml && line.includes('<')) {
|
|
271
|
-
const hit = this.findHtmlBlockStart(line);
|
|
272
|
-
if (hit) {
|
|
273
|
-
const before = line.slice(0, hit.index).trim();
|
|
274
|
-
if (before)
|
|
275
|
-
paragraph.push(before);
|
|
276
|
-
flush();
|
|
277
|
-
i = this.emitHtmlBlock(lines, i, hit, out);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// --- ATX heading -------------------------------------------------------
|
|
282
|
-
const heading = trimmed.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
283
|
-
if (heading) {
|
|
284
|
-
flush();
|
|
285
|
-
const level = heading[1].length;
|
|
286
|
-
out.push(`<h${level}>${this.inline(heading[2], opts)}</h${level}>`);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
// --- Horizontal rule ---------------------------------------------------
|
|
290
|
-
if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
291
|
-
flush();
|
|
292
|
-
out.push('<hr>');
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
// --- Blockquote (consecutive lines, one level stripped, recursive) ------
|
|
296
|
-
if (/^>\s?/.test(trimmed)) {
|
|
297
|
-
flush();
|
|
298
|
-
const quoted = [];
|
|
299
|
-
while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
|
|
300
|
-
quoted.push(lines[i].trim().replace(/^>\s?/, ''));
|
|
301
|
-
i++;
|
|
302
|
-
}
|
|
303
|
-
i--;
|
|
304
|
-
out.push(`<blockquote>${this.parseBlocks(quoted, opts)}</blockquote>`);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
// --- Table (header row + separator row) ---------------------------------
|
|
308
|
-
if (trimmed.includes('|') &&
|
|
309
|
-
i + 1 < lines.length &&
|
|
310
|
-
lines[i + 1].includes('-') &&
|
|
311
|
-
/^\|?[\s:\-|]+\|?$/.test(lines[i + 1].trim())) {
|
|
312
|
-
flush();
|
|
313
|
-
const aligns = this.tableAligns(lines[i + 1].trim());
|
|
314
|
-
const head = this.tableRow(trimmed, 'th', aligns, opts);
|
|
315
|
-
const body = [];
|
|
316
|
-
i += 2;
|
|
317
|
-
while (i < lines.length && lines[i].trim().includes('|')) {
|
|
318
|
-
body.push(this.tableRow(lines[i].trim(), 'td', aligns, opts));
|
|
319
|
-
i++;
|
|
320
|
-
}
|
|
321
|
-
i--;
|
|
322
|
-
out.push(`<table>\n<thead>\n${head}\n</thead>` +
|
|
323
|
-
(body.length > 0 ? `\n<tbody>\n${body.join('\n')}\n</tbody>` : '') +
|
|
324
|
-
`\n</table>`);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
// --- List ---------------------------------------------------------------
|
|
328
|
-
if (MarkdownUtils.listItemRe.test(line)) {
|
|
329
|
-
flush();
|
|
330
|
-
const [html, last] = this.parseList(lines, i, opts);
|
|
331
|
-
out.push(html);
|
|
332
|
-
i = last;
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
// --- Plain text: accumulate into current paragraph ----------------------
|
|
336
|
-
paragraph.push(trimmed);
|
|
337
|
-
}
|
|
338
|
-
flush();
|
|
339
|
-
return out.join('\n');
|
|
121
|
+
return String(i + 1);
|
|
122
|
+
});
|
|
340
123
|
}
|
|
341
|
-
// #endregion
|
|
342
|
-
// #region Lists
|
|
343
|
-
/** Matches "- item", "* item", "+ item", "1. item", "1) item" with leading indent. */
|
|
344
|
-
static { this.listItemRe = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/; }
|
|
345
124
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
125
|
+
* Returns an array of 7 day-of-week name strings formatted for the active locale.
|
|
126
|
+
* @param style - One of `'long'`, `'short'`, or `'narrow'`.
|
|
348
127
|
*/
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
while (i < lines.length) {
|
|
353
|
-
const l = lines[i];
|
|
354
|
-
if (!l.trim())
|
|
355
|
-
break; // blank line ends the list
|
|
356
|
-
if (this.listItemRe.test(l) || /^\s{2,}\S/.test(l)) {
|
|
357
|
-
block.push(l);
|
|
358
|
-
i++;
|
|
359
|
-
}
|
|
360
|
-
else
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
return [this.buildList(block, opts), i - 1];
|
|
364
|
-
}
|
|
365
|
-
static buildList(block, opts) {
|
|
366
|
-
const first = block[0].match(this.listItemRe);
|
|
367
|
-
const baseIndent = first[1].length;
|
|
368
|
-
const ordered = /^\d/.test(first[2]);
|
|
369
|
-
const startNum = ordered ? parseInt(first[2], 10) : 1;
|
|
370
|
-
const items = [];
|
|
371
|
-
for (const l of block) {
|
|
372
|
-
const m = l.match(this.listItemRe);
|
|
373
|
-
if (m && m[1].length <= baseIndent) {
|
|
374
|
-
items.push({ text: m[3], childLines: [] });
|
|
375
|
-
}
|
|
376
|
-
else if (items.length > 0) {
|
|
377
|
-
// Continuation or nested content: strip one nesting level of indent
|
|
378
|
-
items[items.length - 1].childLines.push(l.length > baseIndent + 2 ? l.slice(baseIndent + 2) : l.trim());
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
const lis = items.map(item => {
|
|
382
|
-
let inner;
|
|
383
|
-
// GFM task list: - [ ] / - [x]
|
|
384
|
-
const task = item.text.match(/^\[([ xX])\]\s+(.*)$/);
|
|
385
|
-
if (task) {
|
|
386
|
-
const checked = task[1] !== ' ' ? ' checked' : '';
|
|
387
|
-
inner = `<input type="checkbox" disabled${checked}> ${this.inline(task[2], opts)}`;
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
inner = this.inline(item.text, opts);
|
|
391
|
-
}
|
|
392
|
-
if (item.childLines.length > 0) {
|
|
393
|
-
inner += '\n' + this.parseBlocks(item.childLines, opts);
|
|
394
|
-
}
|
|
395
|
-
return `<li>${inner}</li>`;
|
|
396
|
-
});
|
|
397
|
-
const tag = ordered ? 'ol' : 'ul';
|
|
398
|
-
const startAttr = ordered && startNum > 1 ? ` start="${startNum}"` : '';
|
|
399
|
-
return `<${tag}${startAttr}>\n${lis.join('\n')}\n</${tag}>`;
|
|
400
|
-
}
|
|
401
|
-
// #endregion
|
|
402
|
-
// #region Tables
|
|
403
|
-
static tableAligns(separator) {
|
|
404
|
-
return separator
|
|
405
|
-
.replace(/^\||\|$/g, '')
|
|
406
|
-
.split('|')
|
|
407
|
-
.map(cell => {
|
|
408
|
-
const t = cell.trim();
|
|
409
|
-
if (t.startsWith(':') && t.endsWith(':'))
|
|
410
|
-
return 'center';
|
|
411
|
-
if (t.endsWith(':'))
|
|
412
|
-
return 'right';
|
|
413
|
-
if (t.startsWith(':'))
|
|
414
|
-
return 'left';
|
|
415
|
-
return undefined;
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
static tableRow(row, tag, aligns, opts) {
|
|
419
|
-
const cells = row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
420
|
-
const cellsHtml = cells
|
|
421
|
-
.map((cell, j) => {
|
|
422
|
-
const align = aligns[j] ? ` align="${aligns[j]}"` : '';
|
|
423
|
-
return `<${tag}${align}>${this.inline(cell, opts)}</${tag}>`;
|
|
424
|
-
})
|
|
425
|
-
.join('');
|
|
426
|
-
return `<tr>${cellsHtml}</tr>`;
|
|
128
|
+
getDayOfWeekNames(style) {
|
|
129
|
+
const pattern = DAY_OF_WEEK_FORMATS[style];
|
|
130
|
+
return range(7, i => this.format(new Date(2017, 0, i + 1), pattern));
|
|
427
131
|
}
|
|
428
|
-
// #endregion
|
|
429
|
-
// #region Inline formatting
|
|
430
132
|
/**
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* placeholders so later regex passes can never corrupt it.
|
|
133
|
+
* Returns the four-digit year string for the given date.
|
|
134
|
+
* @param date - The source date.
|
|
434
135
|
*/
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const stash = (html) => `\u0000${store.push(html) - 1}\u0000`;
|
|
438
|
-
// 1. Inline code: stash first so later passes can't touch it.
|
|
439
|
-
// Content is ALWAYS escaped so <, >, & inside code survive in the DOM
|
|
440
|
-
// (the browser renders entities back to the original characters).
|
|
441
|
-
let s = text.replace(/`([^`]+)`/g, (_m, code) => stash(`<code>${this.escape(code)}</code>`));
|
|
442
|
-
// 2. Escape raw HTML as entities (opt-in only)
|
|
443
|
-
if (opts.escapeHtml)
|
|
444
|
-
s = this.escape(s);
|
|
445
|
-
// 3. Images: 
|
|
446
|
-
s = s.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, alt, src, title) => {
|
|
447
|
-
const url = this.safeUrl(src);
|
|
448
|
-
if (!url)
|
|
449
|
-
return alt;
|
|
450
|
-
const t = title ? ` title="${title}"` : '';
|
|
451
|
-
return stash(`<img src="${url}" alt="${alt}"${t} loading="lazy">`);
|
|
452
|
-
});
|
|
453
|
-
// 4. Links: [text](url "title")
|
|
454
|
-
s = s.replace(/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, txt, href, title) => {
|
|
455
|
-
const url = this.safeUrl(href);
|
|
456
|
-
if (!url)
|
|
457
|
-
return txt;
|
|
458
|
-
const t = title ? ` title="${title}"` : '';
|
|
459
|
-
return stash(`<a href="${url}"${t} target="_blank" rel="noopener noreferrer">${txt}</a>`);
|
|
460
|
-
});
|
|
461
|
-
// 5. Autolink remaining bare URLs (markdown links are already stashed)
|
|
462
|
-
s = s.replace(/https?:\/\/[^\s<>"']+/g, url => stash(`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`));
|
|
463
|
-
// 6. Bold / italic / strikethrough.
|
|
464
|
-
// Delimiters must hug the text; underscore emphasis only at word
|
|
465
|
-
// boundaries so identifiers like snake_case are left untouched.
|
|
466
|
-
s = s.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
467
|
-
s = s.replace(/(?<![\w*])___(?=\S)([\s\S]*?\S)___(?![\w*])/g, '<strong><em>$1</em></strong>');
|
|
468
|
-
s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>');
|
|
469
|
-
s = s.replace(/(?<![\w*])__(?=\S)([\s\S]*?\S)__(?![\w*])/g, '<strong>$1</strong>');
|
|
470
|
-
s = s.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '<em>$1</em>');
|
|
471
|
-
s = s.replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>');
|
|
472
|
-
s = s.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
|
|
473
|
-
// 7. Restore stashed HTML
|
|
474
|
-
return s.replace(/\u0000(\d+)\u0000/g, (_m, idx) => store[Number(idx)] ?? '');
|
|
136
|
+
getYearName(date) {
|
|
137
|
+
return this.format(date, 'y');
|
|
475
138
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
return
|
|
481
|
-
.replace(/&/g, '&')
|
|
482
|
-
.replace(/</g, '<')
|
|
483
|
-
.replace(/>/g, '>')
|
|
484
|
-
.replace(/"/g, '"');
|
|
139
|
+
/**
|
|
140
|
+
* Returns the first day of the week for the active locale (0 = Sunday, 1 = Monday, …).
|
|
141
|
+
*/
|
|
142
|
+
getFirstDayOfWeek() {
|
|
143
|
+
return this.locale?.options?.weekStartsOn ?? 0;
|
|
485
144
|
}
|
|
486
145
|
/**
|
|
487
|
-
* Returns
|
|
488
|
-
*
|
|
489
|
-
* whitespace/control characters, e.g. "java\tscript:").
|
|
146
|
+
* Returns the number of days in the month of the given date.
|
|
147
|
+
* @param date - The source date.
|
|
490
148
|
*/
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (/^(javascript|vbscript|data):/i.test(compact))
|
|
494
|
-
return undefined;
|
|
495
|
-
return url.replace(/"/g, '%22');
|
|
149
|
+
getNumDaysInMonth(date) {
|
|
150
|
+
return getDaysInMonth(date);
|
|
496
151
|
}
|
|
497
|
-
// #endregion
|
|
498
|
-
// #region Clipboard
|
|
499
152
|
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* - text/plain : the original markdown source (paste into editors, IDEs...)
|
|
503
|
-
* @param markdown : the markdown source
|
|
504
|
-
* @param options : conversion options for the HTML flavor
|
|
505
|
-
* @returns : true on success
|
|
153
|
+
* Creates an independent copy of the given date.
|
|
154
|
+
* @param date - The date to clone.
|
|
506
155
|
*/
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return false;
|
|
510
|
-
try {
|
|
511
|
-
if (typeof ClipboardItem !== 'undefined') {
|
|
512
|
-
const html = this.toHtml(markdown, options);
|
|
513
|
-
await navigator.clipboard.write([
|
|
514
|
-
new ClipboardItem({
|
|
515
|
-
'text/html': new Blob([html], { type: 'text/html' }),
|
|
516
|
-
'text/plain': new Blob([markdown], { type: 'text/plain' }),
|
|
517
|
-
}),
|
|
518
|
-
]);
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
// Older engines: plain text only
|
|
522
|
-
await navigator.clipboard.writeText(markdown);
|
|
523
|
-
}
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
// Clipboard API requires a secure context and user activation
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
156
|
+
clone(date) {
|
|
157
|
+
return new Date(date.getTime());
|
|
530
158
|
}
|
|
531
159
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* @
|
|
160
|
+
* Creates a `Date` in the `Europe/Rome` timezone for the given year, month, and day.
|
|
161
|
+
* Throws an `Error` when any component is out of range.
|
|
162
|
+
* @param year - Full four-digit year.
|
|
163
|
+
* @param month - Zero-based month index (0 = January, 11 = December).
|
|
164
|
+
* @param date - Day-of-month (1-based).
|
|
535
165
|
*/
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
await navigator.clipboard.writeText(text);
|
|
541
|
-
return true;
|
|
166
|
+
createDate(year, month, date) {
|
|
167
|
+
if (month < 0 || month > 11) {
|
|
168
|
+
throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
|
|
542
169
|
}
|
|
543
|
-
|
|
544
|
-
|
|
170
|
+
if (date < 1) {
|
|
171
|
+
throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
|
|
172
|
+
}
|
|
173
|
+
// Passing the year to the constructor causes year numbers <100 to be converted to 19xx.
|
|
174
|
+
// To work around this we use `setFullYear` and `setHours` instead.
|
|
175
|
+
const result = new Date();
|
|
176
|
+
result.setFullYear(year, month, date);
|
|
177
|
+
result.setHours(0, 0, 0, 0);
|
|
178
|
+
const result2 = new TZDate(result, 'Europe/Rome');
|
|
179
|
+
if (result2.getMonth() !== month) {
|
|
180
|
+
throw Error(`Invalid date "${date}" for month with index "${month}".`);
|
|
545
181
|
}
|
|
182
|
+
return result2;
|
|
546
183
|
}
|
|
547
184
|
/**
|
|
548
|
-
*
|
|
549
|
-
* (what the user sees, entities already decoded by the browser).
|
|
550
|
-
* @param element : the element hosting the rendered markdown
|
|
551
|
-
* @returns : the plain text
|
|
185
|
+
* Returns today's date in the local timezone.
|
|
552
186
|
*/
|
|
553
|
-
|
|
554
|
-
return
|
|
187
|
+
today() {
|
|
188
|
+
return new Date();
|
|
555
189
|
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
var DateFormat;
|
|
559
|
-
(function (DateFormat) {
|
|
560
|
-
DateFormat[DateFormat["Short"] = 1] = "Short";
|
|
561
|
-
DateFormat[DateFormat["Long"] = 2] = "Long";
|
|
562
|
-
DateFormat[DateFormat["LongWithShortMonth"] = 3] = "LongWithShortMonth";
|
|
563
|
-
DateFormat[DateFormat["LongWithWeekDay"] = 4] = "LongWithWeekDay";
|
|
564
|
-
DateFormat[DateFormat["LongWithShortWeekDay"] = 5] = "LongWithShortWeekDay";
|
|
565
|
-
DateFormat[DateFormat["MonthAndYear"] = 6] = "MonthAndYear";
|
|
566
|
-
DateFormat[DateFormat["LongMonthAndYear"] = 7] = "LongMonthAndYear";
|
|
567
|
-
DateFormat[DateFormat["WeekDay"] = 8] = "WeekDay";
|
|
568
|
-
DateFormat[DateFormat["LongWeekDay"] = 9] = "LongWeekDay";
|
|
569
|
-
DateFormat[DateFormat["DayAndMonth"] = 10] = "DayAndMonth";
|
|
570
|
-
DateFormat[DateFormat["ShortUS"] = 11] = "ShortUS";
|
|
571
|
-
DateFormat[DateFormat["ShortISO8601"] = 12] = "ShortISO8601"; // yyyy-mm-dd
|
|
572
|
-
})(DateFormat || (DateFormat = {}));
|
|
573
|
-
class SystemUtils {
|
|
574
|
-
/** Shared collator for locale-aware, case-insensitive string comparison. */
|
|
575
|
-
static { this.collator = new Intl.Collator('it', { sensitivity: 'base', numeric: true }); }
|
|
576
190
|
/**
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* @
|
|
191
|
+
* Parses a value into a `Date`.
|
|
192
|
+
* - Strings are first attempted as ISO 8601, then matched against each format in `parseFormat`.
|
|
193
|
+
* - Numbers are treated as Unix timestamps (milliseconds).
|
|
194
|
+
* - Existing `Date` instances are cloned.
|
|
195
|
+
* @param value - The value to parse.
|
|
196
|
+
* @param parseFormat - A format string or an array of format strings (date-fns tokens).
|
|
197
|
+
* @returns A valid `Date` in `Europe/Rome`, an invalid sentinel, or `null` for unrecognised input.
|
|
582
198
|
*/
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return item;
|
|
199
|
+
parse(value, parseFormat) {
|
|
200
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
201
|
+
const iso8601Date = parseISO(value);
|
|
202
|
+
if (this.isValid(iso8601Date)) {
|
|
203
|
+
return new TZDate(iso8601Date, 'Europe/Rome');
|
|
589
204
|
}
|
|
205
|
+
const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
|
|
206
|
+
if (!formats.length) {
|
|
207
|
+
throw Error('Formats array must not be empty.');
|
|
208
|
+
}
|
|
209
|
+
for (const currentFormat of formats) {
|
|
210
|
+
const fromFormat = parse(value, currentFormat, new Date(), { locale: this.locale });
|
|
211
|
+
if (this.isValid(fromFormat)) {
|
|
212
|
+
return new TZDate(fromFormat, 'Europe/Rome');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return this.invalid();
|
|
590
216
|
}
|
|
591
|
-
|
|
217
|
+
else if (typeof value === 'number') {
|
|
218
|
+
return new Date(value);
|
|
219
|
+
}
|
|
220
|
+
else if (value instanceof Date) {
|
|
221
|
+
return this.clone(value);
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
592
224
|
}
|
|
593
225
|
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* @param
|
|
597
|
-
* @param
|
|
598
|
-
* @returns : the array index or -1 if not found
|
|
226
|
+
* Formats a `Date` using the given date-fns display format string.
|
|
227
|
+
* Throws an `Error` when `date` is not valid.
|
|
228
|
+
* @param date - The date to format.
|
|
229
|
+
* @param displayFormat - A date-fns format string (e.g. `'P'`, `'LLL uuuu'`).
|
|
599
230
|
*/
|
|
600
|
-
|
|
601
|
-
if (!
|
|
602
|
-
|
|
603
|
-
for (let i = 0; i < array.length; i++) {
|
|
604
|
-
if (array[i][key] === value) {
|
|
605
|
-
return i;
|
|
606
|
-
}
|
|
231
|
+
format(date, displayFormat) {
|
|
232
|
+
if (!this.isValid(date)) {
|
|
233
|
+
throw Error('DateFnsAdapter: Cannot format invalid date.');
|
|
607
234
|
}
|
|
608
|
-
return
|
|
235
|
+
return format(date, displayFormat, { locale: this.locale });
|
|
609
236
|
}
|
|
610
237
|
/**
|
|
611
|
-
*
|
|
612
|
-
* @param
|
|
613
|
-
* @param
|
|
614
|
-
* @returns : the property value or null if not found
|
|
238
|
+
* Adds the given number of whole years to a date.
|
|
239
|
+
* @param date - The base date.
|
|
240
|
+
* @param years - Number of years to add (can be negative).
|
|
615
241
|
*/
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return undefined;
|
|
619
|
-
for (const item of array) {
|
|
620
|
-
const i = item;
|
|
621
|
-
if (i['value'] === value) {
|
|
622
|
-
return i['name'] ?? i['id'];
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
return undefined;
|
|
242
|
+
addCalendarYears(date, years) {
|
|
243
|
+
return addYears(date, years);
|
|
626
244
|
}
|
|
627
245
|
/**
|
|
628
|
-
*
|
|
629
|
-
* @param
|
|
630
|
-
* @param
|
|
631
|
-
* @returns : an array of INode objects
|
|
246
|
+
* Adds the given number of whole months to a date.
|
|
247
|
+
* @param date - The base date.
|
|
248
|
+
* @param months - Number of months to add (can be negative).
|
|
632
249
|
*/
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
items.forEach(n => {
|
|
636
|
-
const node = {
|
|
637
|
-
id: n.id.toString(),
|
|
638
|
-
name: n.name,
|
|
639
|
-
count: n.count ? n.count : 0,
|
|
640
|
-
children: undefined,
|
|
641
|
-
parent,
|
|
642
|
-
bag: n,
|
|
643
|
-
};
|
|
644
|
-
nodes.push(node);
|
|
645
|
-
node.children =
|
|
646
|
-
n.children && n.children.length > 0
|
|
647
|
-
? this.arrayToNodes(n.children, node)
|
|
648
|
-
: [];
|
|
649
|
-
});
|
|
650
|
-
return nodes;
|
|
250
|
+
addCalendarMonths(date, months) {
|
|
251
|
+
return addMonths(date, months);
|
|
651
252
|
}
|
|
652
253
|
/**
|
|
653
|
-
*
|
|
654
|
-
* @param
|
|
655
|
-
* @param
|
|
656
|
-
* @returns A comparator function that returns a negative, zero, or positive number.
|
|
254
|
+
* Adds the given number of whole days to a date.
|
|
255
|
+
* @param date - The base date.
|
|
256
|
+
* @param days - Number of days to add (can be negative).
|
|
657
257
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return (a, b) => {
|
|
661
|
-
const varA = a[key];
|
|
662
|
-
const varB = b[key];
|
|
663
|
-
if (varA === varB)
|
|
664
|
-
return 0;
|
|
665
|
-
if (varA === undefined || varA === null)
|
|
666
|
-
return -1 * dir;
|
|
667
|
-
if (varB === undefined || varB === null)
|
|
668
|
-
return 1 * dir;
|
|
669
|
-
if (typeof varA === 'string' && typeof varB === 'string') {
|
|
670
|
-
return SystemUtils.collator.compare(varA, varB) * dir;
|
|
671
|
-
}
|
|
672
|
-
return (varA > varB ? 1 : varA < varB ? -1 : 0) * dir;
|
|
673
|
-
};
|
|
258
|
+
addCalendarDays(date, days) {
|
|
259
|
+
return addDays(date, days);
|
|
674
260
|
}
|
|
675
261
|
/**
|
|
676
|
-
*
|
|
677
|
-
* @param
|
|
678
|
-
* @returns : the formatted string
|
|
262
|
+
* Serialises a date to an ISO 8601 date string (`yyyy-MM-dd`).
|
|
263
|
+
* @param date - The date to serialise.
|
|
679
264
|
*/
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
return `${(gr / 1000000).toFixed(2)} t`;
|
|
683
|
-
else if (gr > 100000)
|
|
684
|
-
return `${(gr / 100000).toFixed(2)} q`;
|
|
685
|
-
else if (gr > 1000)
|
|
686
|
-
return `${(gr / 1000).toFixed(2)} kg`;
|
|
687
|
-
else
|
|
688
|
-
return `${gr} gr`;
|
|
265
|
+
toIso8601(date) {
|
|
266
|
+
return formatISO(date, { representation: 'date' });
|
|
689
267
|
}
|
|
690
268
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
269
|
+
* Returns the given value when it is a valid `Date`, or `null` for an empty string.
|
|
270
|
+
* Deserialises valid ISO 8601 strings into `Date` instances.
|
|
271
|
+
* Delegates all other values to the base-class implementation.
|
|
272
|
+
* @param value - The raw value to deserialise.
|
|
694
273
|
*/
|
|
695
|
-
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
274
|
+
deserialize(value) {
|
|
275
|
+
if (typeof value === 'string') {
|
|
276
|
+
if (!value) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const date = parseISO(value);
|
|
280
|
+
if (this.isValid(date)) {
|
|
281
|
+
return date;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return super.deserialize(value);
|
|
701
285
|
}
|
|
702
286
|
/**
|
|
703
|
-
*
|
|
704
|
-
* @param
|
|
705
|
-
* @param b : string b
|
|
706
|
-
* @returns : 0 if equals, 1 if bigger, -1 if lower
|
|
287
|
+
* Returns `true` when `obj` is an instance of `Date`.
|
|
288
|
+
* @param obj - The object to test.
|
|
707
289
|
*/
|
|
708
|
-
|
|
709
|
-
return
|
|
290
|
+
isDateInstance(obj) {
|
|
291
|
+
return isDate(obj);
|
|
710
292
|
}
|
|
711
293
|
/**
|
|
712
|
-
*
|
|
713
|
-
* @param
|
|
714
|
-
* @returns : the capitalized string
|
|
294
|
+
* Returns `true` when `date` represents a valid point in time.
|
|
295
|
+
* @param date - The date to validate.
|
|
715
296
|
*/
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
297
|
+
isValid(date) {
|
|
298
|
+
return isValid(date);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Returns a sentinel `Date` that represents an invalid date (`new Date(NaN)`).
|
|
302
|
+
*/
|
|
303
|
+
invalid() {
|
|
304
|
+
return new Date(NaN);
|
|
305
|
+
}
|
|
306
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
307
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter }); }
|
|
308
|
+
}
|
|
309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter, decorators: [{
|
|
310
|
+
type: Injectable
|
|
311
|
+
}], ctorParameters: () => [] });
|
|
312
|
+
/**
|
|
313
|
+
* Standalone providers for the ars-utils date-fns adapter.
|
|
314
|
+
*
|
|
315
|
+
* Configures Angular Material to use {@link DateFnsAdapter} (Europe/Rome timezone)
|
|
316
|
+
* and the matching {@link MAT_DATE_FNS_FORMATS}.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* bootstrapApplication(AppComponent, {
|
|
320
|
+
* providers: [provideArsDateFns()]
|
|
321
|
+
* });
|
|
322
|
+
*/
|
|
323
|
+
function provideArsDateFns() {
|
|
324
|
+
return makeEnvironmentProviders([
|
|
325
|
+
{
|
|
326
|
+
provide: DateAdapter,
|
|
327
|
+
useClass: DateFnsAdapter,
|
|
328
|
+
deps: [MAT_DATE_LOCALE],
|
|
329
|
+
},
|
|
330
|
+
{ provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS }
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Directive that moves browser focus to the host element after the first render cycle.
|
|
336
|
+
* Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
|
|
337
|
+
*/
|
|
338
|
+
class AutoFocusDirective {
|
|
339
|
+
constructor() {
|
|
340
|
+
this.elementRef = inject(ElementRef);
|
|
341
|
+
afterNextRender(() => {
|
|
342
|
+
this.elementRef.nativeElement?.focus();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
346
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
|
|
347
|
+
}
|
|
348
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, decorators: [{
|
|
349
|
+
type: Directive,
|
|
350
|
+
args: [{
|
|
351
|
+
selector: '[autoFocus]',
|
|
352
|
+
standalone: true
|
|
353
|
+
}]
|
|
354
|
+
}], ctorParameters: () => [] });
|
|
355
|
+
|
|
356
|
+
class FileInfo {
|
|
357
|
+
isValid() {
|
|
358
|
+
return this.valid;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
class ValueModel {
|
|
362
|
+
}
|
|
363
|
+
class IDModel {
|
|
364
|
+
}
|
|
365
|
+
class GroupModel {
|
|
366
|
+
}
|
|
367
|
+
class DeleteModel extends GroupModel {
|
|
368
|
+
}
|
|
369
|
+
class RelationModel {
|
|
370
|
+
}
|
|
371
|
+
class UpdateRelationsModel {
|
|
372
|
+
}
|
|
373
|
+
class QueryModel {
|
|
374
|
+
}
|
|
375
|
+
class ImportModel {
|
|
376
|
+
}
|
|
377
|
+
class DateInterval {
|
|
378
|
+
get fromAsDate() {
|
|
379
|
+
if (this.from) {
|
|
380
|
+
if (!(this.from instanceof Date)) {
|
|
381
|
+
this.from = new Date(this.from);
|
|
729
382
|
}
|
|
730
|
-
|
|
731
|
-
|
|
383
|
+
if (this.from) {
|
|
384
|
+
return new Date(this.from.getFullYear(), this.from.getMonth(), this.from.getDate(), 2, 0, 0);
|
|
732
385
|
}
|
|
733
386
|
}
|
|
734
|
-
return
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Truncate a string at the last word boundary before `max`.
|
|
738
|
-
* @param s : the string to truncate
|
|
739
|
-
* @param max : the max number of chars
|
|
740
|
-
* @returns : the truncated string
|
|
741
|
-
*/
|
|
742
|
-
static truncate(s, max = 500) {
|
|
743
|
-
if (!s)
|
|
744
|
-
return undefined;
|
|
745
|
-
if (s.length < max)
|
|
746
|
-
return s;
|
|
747
|
-
const i = s.lastIndexOf(' ', max - 1);
|
|
748
|
-
return i > 0 ? s.slice(0, i) : s;
|
|
387
|
+
return undefined;
|
|
749
388
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
* @param max : the maximum resulting string
|
|
755
|
-
* @returns : the joined string
|
|
756
|
-
*/
|
|
757
|
-
static join(items, sep = " ", max = 350) {
|
|
758
|
-
if (!items || items.length === 0)
|
|
759
|
-
return undefined;
|
|
760
|
-
if (items.length > 1) {
|
|
761
|
-
let l = 0;
|
|
762
|
-
let s = "";
|
|
763
|
-
while (s.length < max && items.length > l) {
|
|
764
|
-
if (l > 0) {
|
|
765
|
-
s += sep;
|
|
766
|
-
}
|
|
767
|
-
s += items[l++];
|
|
389
|
+
get toAsDate() {
|
|
390
|
+
if (this.to) {
|
|
391
|
+
if (!(this.to instanceof Date)) {
|
|
392
|
+
this.to = new Date(this.to);
|
|
768
393
|
}
|
|
769
|
-
if (
|
|
770
|
-
|
|
394
|
+
if (this.to) {
|
|
395
|
+
return new Date(this.to.getFullYear(), this.to.getMonth(), this.to.getDate(), 2, 0, 0);
|
|
771
396
|
}
|
|
772
|
-
return s;
|
|
773
397
|
}
|
|
774
|
-
return
|
|
398
|
+
return undefined;
|
|
775
399
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
* @returns : The normalized string, or `undefined` if the input is falsy.
|
|
780
|
-
*/
|
|
781
|
-
static normalizeDisplay(s) {
|
|
782
|
-
if (!s)
|
|
783
|
-
return s;
|
|
784
|
-
return s
|
|
785
|
-
.split(' ')
|
|
786
|
-
.map((word, wordIndex) => {
|
|
787
|
-
if (!word)
|
|
788
|
-
return word;
|
|
789
|
-
// If the word is all uppercase and contains at least one letter, we assume it's an acronym and leave it as is
|
|
790
|
-
if (word === word.toUpperCase() && /[A-Z]/.test(word)) {
|
|
791
|
-
return word;
|
|
792
|
-
}
|
|
793
|
-
// Otherwise, convert to lowercase letter by letter
|
|
794
|
-
const chars = word.split('');
|
|
795
|
-
return chars
|
|
796
|
-
.map((char, charIndex) => {
|
|
797
|
-
if (charIndex === 0 && wordIndex === 0) {
|
|
798
|
-
// First letter of the first word:
|
|
799
|
-
// remains uppercase only if the first 2 letters are both uppercase
|
|
800
|
-
const secondChar = chars[1] ?? '';
|
|
801
|
-
const keepUppercase = char === char.toUpperCase() &&
|
|
802
|
-
secondChar === secondChar.toUpperCase() &&
|
|
803
|
-
/[A-Z]/.test(char) &&
|
|
804
|
-
/[A-Z]/.test(secondChar);
|
|
805
|
-
return keepUppercase ? char : char.toLowerCase();
|
|
806
|
-
}
|
|
807
|
-
return char.toLowerCase();
|
|
808
|
-
})
|
|
809
|
-
.join('');
|
|
810
|
-
})
|
|
811
|
-
.join(' ');
|
|
400
|
+
constructor(from, to) {
|
|
401
|
+
this.from = from;
|
|
402
|
+
this.to = to;
|
|
812
403
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
404
|
+
clear() {
|
|
405
|
+
this.from = undefined;
|
|
406
|
+
this.to = undefined;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Zero-dependency Markdown to HTML converter.
|
|
412
|
+
*
|
|
413
|
+
* Design goals:
|
|
414
|
+
* - Single line-based pass over blocks (no repeated full-string regex passes).
|
|
415
|
+
* - Inline formatting never touches code spans / code blocks (stash & restore).
|
|
416
|
+
* - No HTML entity escaping by default in normal text: the source passes as-is.
|
|
417
|
+
* Exception: the content of code spans / code blocks is ALWAYS escaped, so
|
|
418
|
+
* things like List<string> render correctly (the browser displays the
|
|
419
|
+
* original characters; entities never reach the user or the clipboard).
|
|
420
|
+
* Opt-in full escaping via { escapeHtml: true } for untrusted input.
|
|
421
|
+
* - URL sanitization on links/images (javascript:, vbscript:, data: are dropped).
|
|
422
|
+
* - Bounded result cache for repeated renders (Angular change detection friendly).
|
|
423
|
+
*
|
|
424
|
+
* Supported syntax: headings, paragraphs, hard/soft breaks, hr, blockquotes
|
|
425
|
+
* (nested), fenced code blocks, inline code, bold/italic/strikethrough, links
|
|
426
|
+
* (with title), images, autolinked bare URLs, ordered/unordered lists (nested,
|
|
427
|
+
* task lists, ordered start offset), GFM tables with alignment, raw HTML
|
|
428
|
+
* blocks (e.g. <table>...</table>) passed through verbatim when escapeHtml
|
|
429
|
+
* is false: their newlines stay plain newlines, never converted to <br>.
|
|
430
|
+
*
|
|
431
|
+
* Known simplifications (documented, by design):
|
|
432
|
+
* - A blank line terminates a list.
|
|
433
|
+
* - Setext headings (=== / ---) are not supported, use # syntax.
|
|
434
|
+
* - Reference-style links [text][ref] are not supported.
|
|
435
|
+
*/
|
|
436
|
+
class MarkdownUtils {
|
|
437
|
+
// #region Cache
|
|
438
|
+
static { this.cache = new Map(); }
|
|
439
|
+
static { this.CACHE_MAX = 200; }
|
|
440
|
+
/** Clears the internal result cache. */
|
|
441
|
+
static clearCache() {
|
|
442
|
+
this.cache.clear();
|
|
822
443
|
}
|
|
444
|
+
// #endregion
|
|
445
|
+
// #region Public API
|
|
823
446
|
/**
|
|
824
|
-
* Convert markdown to
|
|
825
|
-
* @param markdown : the markdown
|
|
826
|
-
* @param
|
|
827
|
-
* @returns the
|
|
447
|
+
* Convert markdown to HTML.
|
|
448
|
+
* @param markdown : the markdown source
|
|
449
|
+
* @param options : conversion options
|
|
450
|
+
* @returns : the HTML string ('' when input is falsy)
|
|
828
451
|
*/
|
|
829
|
-
static
|
|
830
|
-
|
|
452
|
+
static toHtml(markdown, options) {
|
|
453
|
+
if (!markdown)
|
|
454
|
+
return '';
|
|
455
|
+
const opts = { escapeHtml: false, breaks: true, ...options };
|
|
456
|
+
const key = `${opts.escapeHtml ? 1 : 0}${opts.breaks ? 1 : 0}|${markdown}`;
|
|
457
|
+
const hit = this.cache.get(key);
|
|
458
|
+
if (hit !== undefined)
|
|
459
|
+
return hit;
|
|
460
|
+
// Normalize line endings and fix glued sentence punctuation ("dettaglio!Ecco").
|
|
461
|
+
// Only when the next char is uppercase (a new sentence), so URLs with query
|
|
462
|
+
// strings ("/page?id=1") and code identifiers are left untouched.
|
|
463
|
+
const source = markdown
|
|
464
|
+
.replace(/\r\n?/g, '\n')
|
|
465
|
+
.replace(/([a-zà-ÿ][!?])([A-ZÀ-Þ])/g, '$1 $2');
|
|
466
|
+
const html = this.parseBlocks(source.split('\n'), opts).trim();
|
|
467
|
+
if (this.cache.size >= this.CACHE_MAX) {
|
|
468
|
+
this.cache.delete(this.cache.keys().next().value); // FIFO eviction
|
|
469
|
+
}
|
|
470
|
+
this.cache.set(key, html);
|
|
471
|
+
return html;
|
|
831
472
|
}
|
|
473
|
+
// #endregion
|
|
474
|
+
// #region Block parser (single pass)
|
|
475
|
+
/** Block-level HTML tags that start a raw HTML block (passthrough, no <p>/<br>). */
|
|
476
|
+
static { this.htmlBlockTags = new Set([
|
|
477
|
+
'address', 'article', 'aside', 'blockquote', 'caption', 'colgroup', 'col',
|
|
478
|
+
'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
|
|
479
|
+
'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
|
|
480
|
+
'hr', 'iframe', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'summary',
|
|
481
|
+
'table', 'tbody', 'td', 'tfoot', 'thead', 'th', 'tr', 'ul',
|
|
482
|
+
]); }
|
|
483
|
+
/** Void/self-contained tags: a single tag, no closing tag expected. */
|
|
484
|
+
static { this.htmlVoidTags = new Set(['hr', 'col', 'img', 'input']); }
|
|
832
485
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* @returns : true if a equals b
|
|
486
|
+
* Precompiled: first block-level tag occurrence anywhere in a line.
|
|
487
|
+
* Anchored on the '<' literal, so scanning is cheap; the lookahead
|
|
488
|
+
* prevents partial matches ('<td' must not match inside '<tdx').
|
|
837
489
|
*/
|
|
838
|
-
static
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
matches--;
|
|
856
|
-
break;
|
|
857
|
-
}
|
|
490
|
+
static { this.htmlBlockScanRe = new RegExp(`<(\\/?)(${[...MarkdownUtils.htmlBlockTags].join('|')})(?=[\\s/>])`, 'gi'); }
|
|
491
|
+
/** Result of scanning a line for the start of a raw HTML block. */
|
|
492
|
+
static findHtmlBlockStart(line) {
|
|
493
|
+
// HTML comment candidate (skipping occurrences inside inline code spans)
|
|
494
|
+
let comment = line.indexOf('<!--');
|
|
495
|
+
while (comment !== -1 && this.insideCodeSpan(line, comment)) {
|
|
496
|
+
comment = line.indexOf('<!--', comment + 4);
|
|
497
|
+
}
|
|
498
|
+
// Block tag candidate (same code-span guard)
|
|
499
|
+
const re = this.htmlBlockScanRe;
|
|
500
|
+
re.lastIndex = 0;
|
|
501
|
+
let m;
|
|
502
|
+
let tagHit;
|
|
503
|
+
while ((m = re.exec(line)) !== null) {
|
|
504
|
+
if (!this.insideCodeSpan(line, m.index)) {
|
|
505
|
+
tagHit = { index: m.index, tag: m[2].toLowerCase(), isClose: m[1] === '/' };
|
|
506
|
+
break;
|
|
858
507
|
}
|
|
859
508
|
}
|
|
860
|
-
|
|
509
|
+
if (comment !== -1 && (!tagHit || comment < tagHit.index)) {
|
|
510
|
+
return { index: comment, tag: '', isClose: false, isComment: true };
|
|
511
|
+
}
|
|
512
|
+
return tagHit ? { ...tagHit, isComment: false } : undefined;
|
|
861
513
|
}
|
|
862
|
-
/**
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
*/
|
|
869
|
-
static cipher(text, key, reverse = false) {
|
|
870
|
-
if (!text || !key)
|
|
871
|
-
return undefined;
|
|
872
|
-
// Surrogate pair limit
|
|
873
|
-
const bound = 0x10000;
|
|
874
|
-
const keyLen = key.length;
|
|
875
|
-
let result = '';
|
|
876
|
-
for (let i = 0; i < text.length; i++) {
|
|
877
|
-
let rotation = key.charCodeAt(i % keyLen);
|
|
878
|
-
if (reverse)
|
|
879
|
-
rotation = -rotation;
|
|
880
|
-
result += String.fromCharCode((text.charCodeAt(i) + rotation + bound) % bound);
|
|
514
|
+
/** True when `index` falls inside an inline code span (odd backtick count before it). */
|
|
515
|
+
static insideCodeSpan(line, index) {
|
|
516
|
+
let count = 0;
|
|
517
|
+
for (let i = 0; i < index; i++) {
|
|
518
|
+
if (line.charCodeAt(i) === 96 /* ` */)
|
|
519
|
+
count++;
|
|
881
520
|
}
|
|
882
|
-
return
|
|
521
|
+
return (count & 1) === 1;
|
|
883
522
|
}
|
|
884
523
|
/**
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
* @returns
|
|
524
|
+
* indexOf-based depth scanner (no regex, no allocations): walks `text`
|
|
525
|
+
* adjusting `depth` for <tag ...> / </tag> occurrences of the SAME tag.
|
|
526
|
+
* Self-closing <tag ... /> forms do not alter depth.
|
|
527
|
+
* @returns [newDepth, endIndex] where endIndex is the position right after
|
|
528
|
+
* the '>' that balanced the element, or -1 when still open.
|
|
889
529
|
*/
|
|
890
|
-
static
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
530
|
+
static scanHtmlDepth(text, tag, depth) {
|
|
531
|
+
const lower = text.toLowerCase();
|
|
532
|
+
const open = '<' + tag;
|
|
533
|
+
const close = '</' + tag;
|
|
534
|
+
let i = 0;
|
|
535
|
+
while ((i = lower.indexOf('<', i)) !== -1) {
|
|
536
|
+
if (lower.startsWith(close, i) && this.isTagBoundary(lower, i + close.length)) {
|
|
537
|
+
depth--;
|
|
538
|
+
const gt = lower.indexOf('>', i);
|
|
539
|
+
const end = gt === -1 ? lower.length : gt + 1;
|
|
540
|
+
if (depth <= 0)
|
|
541
|
+
return [0, end];
|
|
542
|
+
i = end;
|
|
543
|
+
}
|
|
544
|
+
else if (lower.startsWith(open, i) && this.isTagBoundary(lower, i + open.length)) {
|
|
545
|
+
const gt = lower.indexOf('>', i);
|
|
546
|
+
const selfClosing = gt !== -1 && lower.charCodeAt(gt - 1) === 47 /* / */;
|
|
547
|
+
if (selfClosing) {
|
|
548
|
+
if (depth === 0)
|
|
549
|
+
return [0, gt + 1]; // standalone self-closed element
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
depth++;
|
|
553
|
+
}
|
|
554
|
+
i = gt === -1 ? lower.length : gt + 1;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
i++;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return [depth, -1];
|
|
561
|
+
}
|
|
562
|
+
/** A tag token must be followed by whitespace, '/', '>' or end of line. */
|
|
563
|
+
static isTagBoundary(s, idx) {
|
|
564
|
+
if (idx >= s.length)
|
|
565
|
+
return true; // tag opening continues on the next line
|
|
566
|
+
const c = s.charCodeAt(idx);
|
|
567
|
+
return c === 62 /* > */ || c === 47 /* / */ || c === 32 /* space */ || c === 9 /* tab */;
|
|
894
568
|
}
|
|
895
569
|
/**
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
* @param dest - Optional pre-allocated destination object to merge the clone into.
|
|
899
|
-
* @returns A deep copy of `obj`.
|
|
570
|
+
* Re-injects the remainder of a partially consumed line so the main loop
|
|
571
|
+
* processes it as markdown. Returns the index the loop should resume from.
|
|
900
572
|
*/
|
|
901
|
-
static
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
return dest;
|
|
573
|
+
static pushBack(lines, i, remainder) {
|
|
574
|
+
if (remainder.trim()) {
|
|
575
|
+
lines[i] = remainder;
|
|
576
|
+
return i - 1; // the loop's i++ re-processes the remainder
|
|
906
577
|
}
|
|
907
|
-
return
|
|
578
|
+
return i;
|
|
908
579
|
}
|
|
909
580
|
/**
|
|
910
|
-
*
|
|
911
|
-
*
|
|
581
|
+
* Emits a raw HTML block starting at `hit.index` of lines[i], consuming
|
|
582
|
+
* following lines until the element is balanced. Text after the block on
|
|
583
|
+
* the closing line is pushed back for markdown processing.
|
|
584
|
+
* @returns the index of the last consumed line
|
|
912
585
|
*/
|
|
913
|
-
static
|
|
914
|
-
const
|
|
915
|
-
|
|
586
|
+
static emitHtmlBlock(lines, i, hit, out) {
|
|
587
|
+
const line = lines[i];
|
|
588
|
+
// HTML comment: raw until '-->'
|
|
589
|
+
if (hit.isComment) {
|
|
590
|
+
let end = line.indexOf('-->', hit.index);
|
|
591
|
+
if (end !== -1) {
|
|
592
|
+
out.push(line.slice(hit.index, end + 3));
|
|
593
|
+
return this.pushBack(lines, i, line.slice(end + 3));
|
|
594
|
+
}
|
|
595
|
+
out.push(line.slice(hit.index));
|
|
596
|
+
while (++i < lines.length) {
|
|
597
|
+
end = lines[i].indexOf('-->');
|
|
598
|
+
if (end !== -1) {
|
|
599
|
+
out.push(lines[i].slice(0, end + 3));
|
|
600
|
+
return this.pushBack(lines, i, lines[i].slice(end + 3));
|
|
601
|
+
}
|
|
602
|
+
out.push(lines[i]);
|
|
603
|
+
}
|
|
604
|
+
return lines.length - 1; // unterminated: consumed to EOF
|
|
605
|
+
}
|
|
606
|
+
// Stray closing tag or void tag: emit just the tag, resume after '>'
|
|
607
|
+
if (hit.isClose || this.htmlVoidTags.has(hit.tag)) {
|
|
608
|
+
const gt = line.indexOf('>', hit.index);
|
|
609
|
+
const end = gt === -1 ? line.length : gt + 1;
|
|
610
|
+
out.push(line.slice(hit.index, end));
|
|
611
|
+
return this.pushBack(lines, i, line.slice(end));
|
|
612
|
+
}
|
|
613
|
+
// Opening block tag: raw until the element is balanced (depth === 0)
|
|
614
|
+
let [depth, split] = this.scanHtmlDepth(line.slice(hit.index), hit.tag, 0);
|
|
615
|
+
if (split !== -1) {
|
|
616
|
+
out.push(line.slice(hit.index, hit.index + split));
|
|
617
|
+
return this.pushBack(lines, i, line.slice(hit.index + split));
|
|
618
|
+
}
|
|
619
|
+
out.push(line.slice(hit.index));
|
|
620
|
+
while (depth > 0 && ++i < lines.length) {
|
|
621
|
+
[depth, split] = this.scanHtmlDepth(lines[i], hit.tag, depth);
|
|
622
|
+
if (split !== -1) {
|
|
623
|
+
out.push(lines[i].slice(0, split));
|
|
624
|
+
return this.pushBack(lines, i, lines[i].slice(split));
|
|
625
|
+
}
|
|
626
|
+
out.push(lines[i]);
|
|
627
|
+
}
|
|
628
|
+
return Math.min(i, lines.length - 1);
|
|
916
629
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
630
|
+
static parseBlocks(lines, opts) {
|
|
631
|
+
const out = [];
|
|
632
|
+
let paragraph = [];
|
|
633
|
+
const flush = () => {
|
|
634
|
+
if (paragraph.length > 0) {
|
|
635
|
+
const sep = opts.breaks ? '<br>' : ' ';
|
|
636
|
+
out.push(`<p>${paragraph.map(l => this.inline(l, opts)).join(sep)}</p>`);
|
|
637
|
+
paragraph = [];
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
for (let i = 0; i < lines.length; i++) {
|
|
641
|
+
const line = lines[i];
|
|
642
|
+
const trimmed = line.trim();
|
|
643
|
+
// --- Fenced code block -------------------------------------------------
|
|
644
|
+
const fence = trimmed.match(/^```([\w+-]+)?\s*$/);
|
|
645
|
+
if (fence) {
|
|
646
|
+
flush();
|
|
647
|
+
const lang = fence[1] ? ` class="language-${fence[1]}"` : '';
|
|
648
|
+
const code = [];
|
|
649
|
+
i++;
|
|
650
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
|
|
651
|
+
code.push(lines[i]);
|
|
652
|
+
i++;
|
|
653
|
+
}
|
|
654
|
+
out.push(`<pre><code${lang}>${this.escape(code.join('\n'))}</code></pre>`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
// --- Blank line --------------------------------------------------------
|
|
658
|
+
if (!trimmed) {
|
|
659
|
+
flush();
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
// --- Raw HTML block passthrough (only with escaping disabled) ------------
|
|
663
|
+
// A block-level HTML element found ANYWHERE in the line starts a raw
|
|
664
|
+
// block: text before it joins the current paragraph, the element is
|
|
665
|
+
// emitted as-is (plain newlines, no <p>, no <br>) until balanced, and
|
|
666
|
+
// any text after the closing tag resumes markdown processing.
|
|
667
|
+
if (!opts.escapeHtml && line.includes('<')) {
|
|
668
|
+
const hit = this.findHtmlBlockStart(line);
|
|
669
|
+
if (hit) {
|
|
670
|
+
const before = line.slice(0, hit.index).trim();
|
|
671
|
+
if (before)
|
|
672
|
+
paragraph.push(before);
|
|
673
|
+
flush();
|
|
674
|
+
i = this.emitHtmlBlock(lines, i, hit, out);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// --- ATX heading -------------------------------------------------------
|
|
679
|
+
const heading = trimmed.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
680
|
+
if (heading) {
|
|
681
|
+
flush();
|
|
682
|
+
const level = heading[1].length;
|
|
683
|
+
out.push(`<h${level}>${this.inline(heading[2], opts)}</h${level}>`);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
// --- Horizontal rule ---------------------------------------------------
|
|
687
|
+
if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
688
|
+
flush();
|
|
689
|
+
out.push('<hr>');
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
// --- Blockquote (consecutive lines, one level stripped, recursive) ------
|
|
693
|
+
if (/^>\s?/.test(trimmed)) {
|
|
694
|
+
flush();
|
|
695
|
+
const quoted = [];
|
|
696
|
+
while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
|
|
697
|
+
quoted.push(lines[i].trim().replace(/^>\s?/, ''));
|
|
698
|
+
i++;
|
|
699
|
+
}
|
|
700
|
+
i--;
|
|
701
|
+
out.push(`<blockquote>${this.parseBlocks(quoted, opts)}</blockquote>`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
// --- Table (header row + separator row) ---------------------------------
|
|
705
|
+
if (trimmed.includes('|') &&
|
|
706
|
+
i + 1 < lines.length &&
|
|
707
|
+
lines[i + 1].includes('-') &&
|
|
708
|
+
/^\|?[\s:\-|]+\|?$/.test(lines[i + 1].trim())) {
|
|
709
|
+
flush();
|
|
710
|
+
const aligns = this.tableAligns(lines[i + 1].trim());
|
|
711
|
+
const head = this.tableRow(trimmed, 'th', aligns, opts);
|
|
712
|
+
const body = [];
|
|
713
|
+
i += 2;
|
|
714
|
+
while (i < lines.length && lines[i].trim().includes('|')) {
|
|
715
|
+
body.push(this.tableRow(lines[i].trim(), 'td', aligns, opts));
|
|
716
|
+
i++;
|
|
717
|
+
}
|
|
718
|
+
i--;
|
|
719
|
+
out.push(`<table>\n<thead>\n${head}\n</thead>` +
|
|
720
|
+
(body.length > 0 ? `\n<tbody>\n${body.join('\n')}\n</tbody>` : '') +
|
|
721
|
+
`\n</table>`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
// --- List ---------------------------------------------------------------
|
|
725
|
+
if (MarkdownUtils.listItemRe.test(line)) {
|
|
726
|
+
flush();
|
|
727
|
+
const [html, last] = this.parseList(lines, i, opts);
|
|
728
|
+
out.push(html);
|
|
729
|
+
i = last;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
// --- Plain text: accumulate into current paragraph ----------------------
|
|
733
|
+
paragraph.push(trimmed);
|
|
734
|
+
}
|
|
735
|
+
flush();
|
|
736
|
+
return out.join('\n');
|
|
924
737
|
}
|
|
738
|
+
// #endregion
|
|
739
|
+
// #region Lists
|
|
740
|
+
/** Matches "- item", "* item", "+ item", "1. item", "1) item" with leading indent. */
|
|
741
|
+
static { this.listItemRe = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/; }
|
|
925
742
|
/**
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*/
|
|
929
|
-
static emptyUUID() { return "00000000-0000-0000-0000-000000000000"; }
|
|
930
|
-
;
|
|
931
|
-
/**
|
|
932
|
-
* Create a new UUID
|
|
933
|
-
* @returns : the string UUID
|
|
743
|
+
* Gathers the contiguous list block starting at `start`, builds it (with
|
|
744
|
+
* nesting) and returns [html, indexOfLastConsumedLine].
|
|
934
745
|
*/
|
|
935
|
-
static
|
|
936
|
-
|
|
937
|
-
|
|
746
|
+
static parseList(lines, start, opts) {
|
|
747
|
+
const block = [];
|
|
748
|
+
let i = start;
|
|
749
|
+
while (i < lines.length) {
|
|
750
|
+
const l = lines[i];
|
|
751
|
+
if (!l.trim())
|
|
752
|
+
break; // blank line ends the list
|
|
753
|
+
if (this.listItemRe.test(l) || /^\s{2,}\S/.test(l)) {
|
|
754
|
+
block.push(l);
|
|
755
|
+
i++;
|
|
756
|
+
}
|
|
757
|
+
else
|
|
758
|
+
break;
|
|
938
759
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
760
|
+
return [this.buildList(block, opts), i - 1];
|
|
761
|
+
}
|
|
762
|
+
static buildList(block, opts) {
|
|
763
|
+
const first = block[0].match(this.listItemRe);
|
|
764
|
+
const baseIndent = first[1].length;
|
|
765
|
+
const ordered = /^\d/.test(first[2]);
|
|
766
|
+
const startNum = ordered ? parseInt(first[2], 10) : 1;
|
|
767
|
+
const items = [];
|
|
768
|
+
for (const l of block) {
|
|
769
|
+
const m = l.match(this.listItemRe);
|
|
770
|
+
if (m && m[1].length <= baseIndent) {
|
|
771
|
+
items.push({ text: m[3], childLines: [] });
|
|
772
|
+
}
|
|
773
|
+
else if (items.length > 0) {
|
|
774
|
+
// Continuation or nested content: strip one nesting level of indent
|
|
775
|
+
items[items.length - 1].childLines.push(l.length > baseIndent + 2 ? l.slice(baseIndent + 2) : l.trim());
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const lis = items.map(item => {
|
|
779
|
+
let inner;
|
|
780
|
+
// GFM task list: - [ ] / - [x]
|
|
781
|
+
const task = item.text.match(/^\[([ xX])\]\s+(.*)$/);
|
|
782
|
+
if (task) {
|
|
783
|
+
const checked = task[1] !== ' ' ? ' checked' : '';
|
|
784
|
+
inner = `<input type="checkbox" disabled${checked}> ${this.inline(task[2], opts)}`;
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
inner = this.inline(item.text, opts);
|
|
788
|
+
}
|
|
789
|
+
if (item.childLines.length > 0) {
|
|
790
|
+
inner += '\n' + this.parseBlocks(item.childLines, opts);
|
|
791
|
+
}
|
|
792
|
+
return `<li>${inner}</li>`;
|
|
944
793
|
});
|
|
794
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
795
|
+
const startAttr = ordered && startNum > 1 ? ` start="${startNum}"` : '';
|
|
796
|
+
return `<${tag}${startAttr}>\n${lis.join('\n')}\n</${tag}>`;
|
|
945
797
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
798
|
+
// #endregion
|
|
799
|
+
// #region Tables
|
|
800
|
+
static tableAligns(separator) {
|
|
801
|
+
return separator
|
|
802
|
+
.replace(/^\||\|$/g, '')
|
|
803
|
+
.split('|')
|
|
804
|
+
.map(cell => {
|
|
805
|
+
const t = cell.trim();
|
|
806
|
+
if (t.startsWith(':') && t.endsWith(':'))
|
|
807
|
+
return 'center';
|
|
808
|
+
if (t.endsWith(':'))
|
|
809
|
+
return 'right';
|
|
810
|
+
if (t.startsWith(':'))
|
|
811
|
+
return 'left';
|
|
812
|
+
return undefined;
|
|
813
|
+
});
|
|
956
814
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (!value)
|
|
967
|
-
return true;
|
|
968
|
-
return this.emailRegex.test(value.trim().toLowerCase());
|
|
815
|
+
static tableRow(row, tag, aligns, opts) {
|
|
816
|
+
const cells = row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
817
|
+
const cellsHtml = cells
|
|
818
|
+
.map((cell, j) => {
|
|
819
|
+
const align = aligns[j] ? ` align="${aligns[j]}"` : '';
|
|
820
|
+
return `<${tag}${align}>${this.inline(cell, opts)}</${tag}>`;
|
|
821
|
+
})
|
|
822
|
+
.join('');
|
|
823
|
+
return `<tr>${cellsHtml}</tr>`;
|
|
969
824
|
}
|
|
825
|
+
// #endregion
|
|
826
|
+
// #region Inline formatting
|
|
970
827
|
/**
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
* @param value : the email list to parse (e.g. "a@b.it; c@d.it")
|
|
975
|
-
* @returns : true if all emails are valid or the value is empty
|
|
828
|
+
* Applies inline markdown to a single text segment.
|
|
829
|
+
* Generated HTML (code, links, images, autolinks) is stashed behind \u0000
|
|
830
|
+
* placeholders so later regex passes can never corrupt it.
|
|
976
831
|
*/
|
|
977
|
-
static
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
832
|
+
static inline(text, opts) {
|
|
833
|
+
const store = [];
|
|
834
|
+
const stash = (html) => `\u0000${store.push(html) - 1}\u0000`;
|
|
835
|
+
// 1. Inline code: stash first so later passes can't touch it.
|
|
836
|
+
// Content is ALWAYS escaped so <, >, & inside code survive in the DOM
|
|
837
|
+
// (the browser renders entities back to the original characters).
|
|
838
|
+
let s = text.replace(/`([^`]+)`/g, (_m, code) => stash(`<code>${this.escape(code)}</code>`));
|
|
839
|
+
// 2. Escape raw HTML as entities (opt-in only)
|
|
840
|
+
if (opts.escapeHtml)
|
|
841
|
+
s = this.escape(s);
|
|
842
|
+
// 3. Images: 
|
|
843
|
+
s = s.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, alt, src, title) => {
|
|
844
|
+
const url = this.safeUrl(src);
|
|
845
|
+
if (!url)
|
|
846
|
+
return alt;
|
|
847
|
+
const t = title ? ` title="${this.escapeAttr(title)}"` : '';
|
|
848
|
+
return stash(`<img src="${url}" alt="${this.escapeAttr(alt)}"${t} loading="lazy">`);
|
|
849
|
+
});
|
|
850
|
+
// 4. Links: [text](url "title")
|
|
851
|
+
s = s.replace(/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, txt, href, title) => {
|
|
852
|
+
const url = this.safeUrl(href);
|
|
853
|
+
if (!url)
|
|
854
|
+
return txt;
|
|
855
|
+
const t = title ? ` title="${this.escapeAttr(title)}"` : '';
|
|
856
|
+
return stash(`<a href="${url}"${t} target="_blank" rel="noopener noreferrer">${txt}</a>`);
|
|
857
|
+
});
|
|
858
|
+
// 5. Autolink remaining bare URLs (markdown links are already stashed)
|
|
859
|
+
s = s.replace(/https?:\/\/[^\s<>"']+/g, url => stash(`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`));
|
|
860
|
+
// 6. Bold / italic / strikethrough.
|
|
861
|
+
// Delimiters must hug the text; underscore emphasis only at word
|
|
862
|
+
// boundaries so identifiers like snake_case are left untouched.
|
|
863
|
+
s = s.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
864
|
+
s = s.replace(/(?<![\w*])___(?=\S)([\s\S]*?\S)___(?![\w*])/g, '<strong><em>$1</em></strong>');
|
|
865
|
+
s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>');
|
|
866
|
+
s = s.replace(/(?<![\w*])__(?=\S)([\s\S]*?\S)__(?![\w*])/g, '<strong>$1</strong>');
|
|
867
|
+
s = s.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '<em>$1</em>');
|
|
868
|
+
s = s.replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>');
|
|
869
|
+
s = s.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
|
|
870
|
+
// 7. Restore stashed HTML
|
|
871
|
+
return s.replace(/\u0000(\d+)\u0000/g, (_m, idx) => store[Number(idx)] ?? '');
|
|
987
872
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
return
|
|
873
|
+
// #endregion
|
|
874
|
+
// #region Security helpers
|
|
875
|
+
/** Escapes quotes for safe interpolation inside a double-quoted HTML attribute. */
|
|
876
|
+
static escapeAttr(s) {
|
|
877
|
+
return s.replace(/&(?!(?:amp|lt|gt|quot|#\d+);)/g, '&').replace(/"/g, '"');
|
|
878
|
+
}
|
|
879
|
+
/** Escapes &, <, > and " for safe HTML interpolation. */
|
|
880
|
+
static escape(s) {
|
|
881
|
+
return s
|
|
882
|
+
.replace(/&/g, '&')
|
|
883
|
+
.replace(/</g, '<')
|
|
884
|
+
.replace(/>/g, '>')
|
|
885
|
+
.replace(/"/g, '"');
|
|
997
886
|
}
|
|
998
887
|
/**
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
888
|
+
* Returns a sanitized URL or undefined when the scheme is dangerous.
|
|
889
|
+
* Blocks javascript:, vbscript: and data: (also when obfuscated with
|
|
890
|
+
* whitespace/control characters, e.g. "java\tscript:").
|
|
1002
891
|
*/
|
|
1003
|
-
static
|
|
1004
|
-
|
|
892
|
+
static safeUrl(url) {
|
|
893
|
+
const compact = url.replace(/[\s\u0000-\u001f]/g, '');
|
|
894
|
+
if (/^(javascript|vbscript|data):/i.test(compact))
|
|
1005
895
|
return undefined;
|
|
1006
|
-
|
|
1007
|
-
if (value.indexOf("-") !== -1) {
|
|
1008
|
-
const p = value.split("-");
|
|
1009
|
-
if (p.length !== 3)
|
|
1010
|
-
return undefined;
|
|
1011
|
-
parts = [parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2])];
|
|
1012
|
-
}
|
|
1013
|
-
else if (value.indexOf("/") !== -1) {
|
|
1014
|
-
const p = value.split("/");
|
|
1015
|
-
if (p.length !== 3)
|
|
1016
|
-
return undefined;
|
|
1017
|
-
parts = [parseInt(p[2]), parseInt(p[1]) - 1, parseInt(p[0])];
|
|
1018
|
-
}
|
|
1019
|
-
return parts;
|
|
896
|
+
return url.replace(/"/g, '%22');
|
|
1020
897
|
}
|
|
898
|
+
// #endregion
|
|
899
|
+
// #region Clipboard
|
|
1021
900
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
* @
|
|
901
|
+
* Copies markdown content to the clipboard in two flavors:
|
|
902
|
+
* - text/html : the rendered HTML (rich paste into Word, Outlook, Gmail...)
|
|
903
|
+
* - text/plain : the original markdown source (paste into editors, IDEs...)
|
|
904
|
+
* @param markdown : the markdown source
|
|
905
|
+
* @param options : conversion options for the HTML flavor
|
|
906
|
+
* @returns : true on success
|
|
1026
907
|
*/
|
|
1027
|
-
static
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if (isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
|
|
1051
|
-
return undefined;
|
|
1052
|
-
if (isNaN(parts[1]) || parts[1] < 0)
|
|
1053
|
-
return undefined;
|
|
1054
|
-
if (isNaN(parts[2]) || parts[2] < 0)
|
|
1055
|
-
return undefined;
|
|
1056
|
-
if (parts[1] > 11)
|
|
1057
|
-
return undefined;
|
|
1058
|
-
if (parts[1] === 1 && parts[2] > 29)
|
|
1059
|
-
return undefined;
|
|
1060
|
-
else if (parts[1] !== 1 && parts[2] > 31)
|
|
1061
|
-
return undefined;
|
|
1062
|
-
return new TZDate(parts[0], parts[1], parts[2], 12);
|
|
908
|
+
static async copyToClipboard(markdown, options) {
|
|
909
|
+
if (!markdown || typeof navigator === 'undefined' || !navigator.clipboard)
|
|
910
|
+
return false;
|
|
911
|
+
try {
|
|
912
|
+
if (typeof ClipboardItem !== 'undefined') {
|
|
913
|
+
const html = this.toHtml(markdown, options);
|
|
914
|
+
await navigator.clipboard.write([
|
|
915
|
+
new ClipboardItem({
|
|
916
|
+
'text/html': new Blob([html], { type: 'text/html' }),
|
|
917
|
+
'text/plain': new Blob([markdown], { type: 'text/plain' }),
|
|
918
|
+
}),
|
|
919
|
+
]);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
// Older engines: plain text only
|
|
923
|
+
await navigator.clipboard.writeText(markdown);
|
|
924
|
+
}
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
// Clipboard API requires a secure context and user activation
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
1063
931
|
}
|
|
1064
932
|
/**
|
|
1065
|
-
*
|
|
1066
|
-
* @param
|
|
1067
|
-
* @
|
|
1068
|
-
* @param locale : the locale to use (default is IT)
|
|
1069
|
-
* @returns : the formatted string
|
|
933
|
+
* Copies plain text (e.g. the content of a single code block) to the clipboard.
|
|
934
|
+
* @param text : the text to copy
|
|
935
|
+
* @returns : true on success
|
|
1070
936
|
*/
|
|
1071
|
-
static
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
return '';
|
|
1081
|
-
// Format
|
|
1082
|
-
switch (fmt) {
|
|
1083
|
-
case DateFormat.Short: return format(value, "dd/MM/yyyy", { locale: locale });
|
|
1084
|
-
case DateFormat.Long: return format(value, "d MMMM yyyy", { locale: locale });
|
|
1085
|
-
case DateFormat.LongWithShortMonth: return format(value, "d MMM yyyy", { locale: locale });
|
|
1086
|
-
case DateFormat.LongWithWeekDay: return format(value, "EEEE, d MMMM yyyy", { locale: locale });
|
|
1087
|
-
case DateFormat.LongWithShortWeekDay: return format(value, "EEE, d MMMM yyyy", { locale: locale });
|
|
1088
|
-
case DateFormat.MonthAndYear: return format(value, "MMM yyyy", { locale: locale });
|
|
1089
|
-
case DateFormat.LongMonthAndYear: return format(value, "MMMM yyyy", { locale: locale });
|
|
1090
|
-
case DateFormat.WeekDay: return format(value, "EEE, d", { locale: locale });
|
|
1091
|
-
case DateFormat.LongWeekDay: return format(value, "EEEE, d", { locale: locale });
|
|
1092
|
-
case DateFormat.DayAndMonth: return format(value, "d MMMM", { locale: locale });
|
|
1093
|
-
case DateFormat.ShortUS: return format(value, "MM/dd/yyyy", { locale: locale });
|
|
1094
|
-
case DateFormat.ShortISO8601: return format(value, "yyyy-MM-dd", { locale: locale });
|
|
1095
|
-
default:
|
|
1096
|
-
return format(value, fmt, { locale: locale });
|
|
937
|
+
static async copyText(text) {
|
|
938
|
+
if (!text || typeof navigator === 'undefined' || !navigator.clipboard)
|
|
939
|
+
return false;
|
|
940
|
+
try {
|
|
941
|
+
await navigator.clipboard.writeText(text);
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
return false;
|
|
1097
946
|
}
|
|
1098
947
|
}
|
|
1099
948
|
/**
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
* @
|
|
949
|
+
* Extracts the visible plain text from a rendered element
|
|
950
|
+
* (what the user sees, entities already decoded by the browser).
|
|
951
|
+
* @param element : the element hosting the rendered markdown
|
|
952
|
+
* @returns : the plain text
|
|
1103
953
|
*/
|
|
1104
|
-
static
|
|
1105
|
-
|
|
1106
|
-
if (!value)
|
|
1107
|
-
return undefined;
|
|
1108
|
-
// A string
|
|
1109
|
-
if (typeof value === 'string' || value instanceof String)
|
|
1110
|
-
value = this.parseDate(value);
|
|
1111
|
-
// Not a date
|
|
1112
|
-
if (!(value instanceof Date && value.getTime()))
|
|
1113
|
-
return undefined;
|
|
1114
|
-
// Update date
|
|
1115
|
-
return new TZDate(value, "Europe/Rome");
|
|
954
|
+
static elementToText(element) {
|
|
955
|
+
return element.innerText ?? element.textContent ?? '';
|
|
1116
956
|
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
var DateFormat;
|
|
960
|
+
(function (DateFormat) {
|
|
961
|
+
DateFormat[DateFormat["Short"] = 1] = "Short";
|
|
962
|
+
DateFormat[DateFormat["Long"] = 2] = "Long";
|
|
963
|
+
DateFormat[DateFormat["LongWithShortMonth"] = 3] = "LongWithShortMonth";
|
|
964
|
+
DateFormat[DateFormat["LongWithWeekDay"] = 4] = "LongWithWeekDay";
|
|
965
|
+
DateFormat[DateFormat["LongWithShortWeekDay"] = 5] = "LongWithShortWeekDay";
|
|
966
|
+
DateFormat[DateFormat["MonthAndYear"] = 6] = "MonthAndYear";
|
|
967
|
+
DateFormat[DateFormat["LongMonthAndYear"] = 7] = "LongMonthAndYear";
|
|
968
|
+
DateFormat[DateFormat["WeekDay"] = 8] = "WeekDay";
|
|
969
|
+
DateFormat[DateFormat["LongWeekDay"] = 9] = "LongWeekDay";
|
|
970
|
+
DateFormat[DateFormat["DayAndMonth"] = 10] = "DayAndMonth";
|
|
971
|
+
DateFormat[DateFormat["ShortUS"] = 11] = "ShortUS";
|
|
972
|
+
DateFormat[DateFormat["ShortISO8601"] = 12] = "ShortISO8601"; // yyyy-mm-dd
|
|
973
|
+
})(DateFormat || (DateFormat = {}));
|
|
974
|
+
class SystemUtils {
|
|
975
|
+
/** Shared collator for locale-aware, case-insensitive string comparison. */
|
|
976
|
+
static { this.collator = new Intl.Collator('it', { sensitivity: 'base', numeric: true }); }
|
|
1117
977
|
/**
|
|
1118
|
-
*
|
|
1119
|
-
* @param
|
|
1120
|
-
* @param
|
|
1121
|
-
* @param
|
|
1122
|
-
* @
|
|
978
|
+
* Array find by key
|
|
979
|
+
* @param array : the array to scan
|
|
980
|
+
* @param key : key name
|
|
981
|
+
* @param value : the value to search for
|
|
982
|
+
* @returns : the property value or null
|
|
1123
983
|
*/
|
|
1124
|
-
static
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
interval.to = new TZDate(new Date(year, 11, 31), 'Europe/Rome');
|
|
1131
|
-
}
|
|
1132
|
-
else
|
|
1133
|
-
interval.to = new Date(year, 11, 31);
|
|
1134
|
-
}
|
|
1135
|
-
else {
|
|
1136
|
-
let parts = this.getDateParts(value);
|
|
1137
|
-
if (!parts || isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
|
|
1138
|
-
return;
|
|
1139
|
-
const d = new TZDate(new Date(parts[0], parts[1], parts[2]), 'Europe/Rome');
|
|
1140
|
-
if (end)
|
|
1141
|
-
interval.to = d;
|
|
1142
|
-
else if (copy) {
|
|
1143
|
-
interval.from = d;
|
|
1144
|
-
interval.to = d;
|
|
1145
|
-
}
|
|
1146
|
-
else
|
|
1147
|
-
interval.from = d;
|
|
984
|
+
static arrayFindByKey(array, key, value) {
|
|
985
|
+
if (!array)
|
|
986
|
+
return undefined;
|
|
987
|
+
for (const item of array) {
|
|
988
|
+
if (item[key] === value) {
|
|
989
|
+
return item;
|
|
1148
990
|
}
|
|
1149
991
|
}
|
|
992
|
+
return undefined;
|
|
1150
993
|
}
|
|
1151
994
|
/**
|
|
1152
|
-
*
|
|
1153
|
-
* @param
|
|
1154
|
-
* @param
|
|
1155
|
-
* @param
|
|
1156
|
-
* @returns
|
|
995
|
+
* Array find index by key
|
|
996
|
+
* @param array : the array to scan
|
|
997
|
+
* @param key : the key name
|
|
998
|
+
* @param value : the value to search for
|
|
999
|
+
* @returns : the array index or -1 if not found
|
|
1157
1000
|
*/
|
|
1158
|
-
static
|
|
1159
|
-
|
|
1001
|
+
static arrayFindIndexByKey(array, key, value) {
|
|
1002
|
+
if (!array)
|
|
1003
|
+
return -1;
|
|
1004
|
+
for (let i = 0; i < array.length; i++) {
|
|
1005
|
+
if (array[i][key] === value) {
|
|
1006
|
+
return i;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return -1;
|
|
1160
1010
|
}
|
|
1161
1011
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
* @param
|
|
1164
|
-
* @param
|
|
1165
|
-
* @
|
|
1166
|
-
* @param locale - BCP 47 locale tag (default: `'it-IT'`).
|
|
1167
|
-
* @returns The formatted currency string.
|
|
1012
|
+
* Get a value from and array made of name|value items
|
|
1013
|
+
* @param array : the array to scan
|
|
1014
|
+
* @param value : the value to search for
|
|
1015
|
+
* @returns : the property value or null if not found
|
|
1168
1016
|
*/
|
|
1169
|
-
static
|
|
1170
|
-
|
|
1017
|
+
static arrayGetValue(array, value) {
|
|
1018
|
+
if (!array)
|
|
1019
|
+
return undefined;
|
|
1020
|
+
for (const item of array) {
|
|
1021
|
+
const i = item;
|
|
1022
|
+
if (i['value'] === value) {
|
|
1023
|
+
return i['name'] ?? i['id'];
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return undefined;
|
|
1171
1027
|
}
|
|
1172
1028
|
/**
|
|
1173
|
-
*
|
|
1174
|
-
* @param
|
|
1175
|
-
* @
|
|
1029
|
+
* Convert items to nodes into a tree structure
|
|
1030
|
+
* @param items : list of nodes
|
|
1031
|
+
* @param parent : parent node
|
|
1032
|
+
* @returns : an array of INode objects
|
|
1176
1033
|
*/
|
|
1177
|
-
static
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1034
|
+
static arrayToNodes(items, parent) {
|
|
1035
|
+
const nodes = [];
|
|
1036
|
+
items.forEach(n => {
|
|
1037
|
+
const node = {
|
|
1038
|
+
id: n.id.toString(),
|
|
1039
|
+
name: n.name,
|
|
1040
|
+
count: n.count ? n.count : 0,
|
|
1041
|
+
children: undefined,
|
|
1042
|
+
parent,
|
|
1043
|
+
bag: n,
|
|
1044
|
+
};
|
|
1045
|
+
nodes.push(node);
|
|
1046
|
+
node.children =
|
|
1047
|
+
n.children && n.children.length > 0
|
|
1048
|
+
? this.arrayToNodes(n.children, node)
|
|
1049
|
+
: [];
|
|
1050
|
+
});
|
|
1051
|
+
return nodes;
|
|
1181
1052
|
}
|
|
1182
1053
|
/**
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1054
|
+
* Comparator factory for sorting arrays of objects by a given property key.
|
|
1055
|
+
* Pass the element type for compile-time key checking:
|
|
1056
|
+
* `items.sort(SystemUtils.arraySortCompare<Employee>('lastName'))`.
|
|
1057
|
+
* @param key - Name of the property to sort by (autocompleted from `T` when provided).
|
|
1058
|
+
* @param order - Sort direction: `'asc'` (default) or `'desc'`.
|
|
1059
|
+
* @returns A comparator function that returns a negative, zero, or positive number.
|
|
1186
1060
|
*/
|
|
1187
|
-
static
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1061
|
+
static arraySortCompare(key, order = 'asc') {
|
|
1062
|
+
const dir = order === 'desc' ? -1 : 1;
|
|
1063
|
+
return (a, b) => {
|
|
1064
|
+
const varA = a[key];
|
|
1065
|
+
const varB = b[key];
|
|
1066
|
+
if (varA === varB)
|
|
1067
|
+
return 0;
|
|
1068
|
+
if (varA === undefined || varA === null)
|
|
1069
|
+
return -1 * dir;
|
|
1070
|
+
if (varB === undefined || varB === null)
|
|
1071
|
+
return 1 * dir;
|
|
1072
|
+
if (typeof varA === 'string' && typeof varB === 'string') {
|
|
1073
|
+
return SystemUtils.collator.compare(varA, varB) * dir;
|
|
1074
|
+
}
|
|
1075
|
+
return (varA > varB ? 1 : varA < varB ? -1 : 0) * dir;
|
|
1076
|
+
};
|
|
1191
1077
|
}
|
|
1192
1078
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
* @param
|
|
1195
|
-
* @returns
|
|
1079
|
+
* Format weight
|
|
1080
|
+
* @param gr : grams
|
|
1081
|
+
* @returns : the formatted string
|
|
1196
1082
|
*/
|
|
1197
|
-
static
|
|
1198
|
-
if (
|
|
1199
|
-
return
|
|
1200
|
-
|
|
1201
|
-
|
|
1083
|
+
static formatWeight(gr) {
|
|
1084
|
+
if (gr > 1000000)
|
|
1085
|
+
return `${(gr / 1000000).toFixed(2)} t`;
|
|
1086
|
+
else if (gr > 100000)
|
|
1087
|
+
return `${(gr / 100000).toFixed(2)} q`;
|
|
1088
|
+
else if (gr > 1000)
|
|
1089
|
+
return `${(gr / 1000).toFixed(2)} kg`;
|
|
1090
|
+
else
|
|
1091
|
+
return `${gr} gr`;
|
|
1202
1092
|
}
|
|
1203
1093
|
/**
|
|
1204
|
-
*
|
|
1205
|
-
* @
|
|
1094
|
+
* Format file size
|
|
1095
|
+
* @param bytes : number of bytes
|
|
1096
|
+
* @returns : the formatted string
|
|
1206
1097
|
*/
|
|
1207
|
-
static
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
return
|
|
1098
|
+
static formatFileSize(bytes) {
|
|
1099
|
+
const MB = 1024 * 1024;
|
|
1100
|
+
if (bytes >= MB)
|
|
1101
|
+
return `${(bytes / MB).toFixed(1)} MB`;
|
|
1102
|
+
if (bytes >= 1024)
|
|
1103
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1104
|
+
return `${bytes} byte`;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Compare two strings (case-insensitive, locale-aware, accent-insensitive).
|
|
1108
|
+
* @param a : string a
|
|
1109
|
+
* @param b : string b
|
|
1110
|
+
* @returns : 0 if equals, 1 if bigger, -1 if lower
|
|
1111
|
+
*/
|
|
1112
|
+
static compareString(a, b) {
|
|
1113
|
+
return this.collator.compare(a ?? '', b ?? '');
|
|
1223
1114
|
}
|
|
1224
1115
|
/**
|
|
1225
|
-
*
|
|
1226
|
-
* @param
|
|
1227
|
-
* @returns the
|
|
1116
|
+
* Capitalize a string
|
|
1117
|
+
* @param s : the string to capitalize
|
|
1118
|
+
* @returns : the capitalized string
|
|
1228
1119
|
*/
|
|
1229
|
-
static
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (password.length >= 12)
|
|
1239
|
-
score++;
|
|
1240
|
-
else if (password.length >= 10)
|
|
1241
|
-
suggestions.push('Considera di usare più di 12 caratteri.');
|
|
1242
|
-
// Lowercase letters
|
|
1243
|
-
if (/[a-z]/.test(password))
|
|
1244
|
-
score++;
|
|
1245
|
-
else
|
|
1246
|
-
suggestions.push('Aggiungi lettere minuscole.');
|
|
1247
|
-
// Uppercase letters
|
|
1248
|
-
if (/[A-Z]/.test(password))
|
|
1249
|
-
score++;
|
|
1250
|
-
else
|
|
1251
|
-
suggestions.push('Aggiungi lettere maiuscole.');
|
|
1252
|
-
// Numbers
|
|
1253
|
-
if (/\d/.test(password))
|
|
1254
|
-
score++;
|
|
1255
|
-
else
|
|
1256
|
-
suggestions.push('Aggiungi numeri.');
|
|
1257
|
-
// Special characters
|
|
1258
|
-
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password))
|
|
1259
|
-
score++;
|
|
1260
|
-
else
|
|
1261
|
-
suggestions.push('Aggiungi caratteri speciali (!@#$%^&*).');
|
|
1262
|
-
// Common patterns
|
|
1263
|
-
if (/(.)\1{2,}/.test(password)) {
|
|
1264
|
-
score = Math.max(0, score - 1);
|
|
1265
|
-
suggestions.push('Evita di ripetere lo stesso carattere.');
|
|
1266
|
-
}
|
|
1267
|
-
if (/123|abc|qwe/i.test(password)) {
|
|
1268
|
-
score = Math.max(0, score - 1);
|
|
1269
|
-
suggestions.push('Evita sequenze comuni (123, abc, qwe).');
|
|
1270
|
-
}
|
|
1271
|
-
// Label and color
|
|
1272
|
-
let label;
|
|
1273
|
-
let color;
|
|
1274
|
-
let isValid;
|
|
1275
|
-
if (score <= 2) {
|
|
1276
|
-
label = 'Molto debole';
|
|
1277
|
-
color = '#f44336';
|
|
1278
|
-
isValid = false;
|
|
1279
|
-
}
|
|
1280
|
-
else if (score <= 3) {
|
|
1281
|
-
label = 'Debole';
|
|
1282
|
-
color = '#ff9800';
|
|
1283
|
-
isValid = false;
|
|
1284
|
-
}
|
|
1285
|
-
else if (score <= 4) {
|
|
1286
|
-
label = 'Media';
|
|
1287
|
-
color = '#ffc107';
|
|
1288
|
-
isValid = true;
|
|
1120
|
+
static capitalize(s) {
|
|
1121
|
+
if (!s)
|
|
1122
|
+
return undefined;
|
|
1123
|
+
let b = "";
|
|
1124
|
+
let cap = true;
|
|
1125
|
+
for (const char of s) {
|
|
1126
|
+
if (char === " ") {
|
|
1127
|
+
b += char;
|
|
1128
|
+
cap = true;
|
|
1289
1129
|
}
|
|
1290
|
-
else if (
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
isValid = true;
|
|
1130
|
+
else if (cap) {
|
|
1131
|
+
b += char.toUpperCase();
|
|
1132
|
+
cap = false;
|
|
1294
1133
|
}
|
|
1295
1134
|
else {
|
|
1296
|
-
|
|
1297
|
-
color = '#4caf50';
|
|
1298
|
-
isValid = true;
|
|
1135
|
+
b += char.toLowerCase();
|
|
1299
1136
|
}
|
|
1300
|
-
return { score, label, color, suggestions, isValid };
|
|
1301
1137
|
}
|
|
1302
|
-
|
|
1303
|
-
return {
|
|
1304
|
-
score: 0,
|
|
1305
|
-
isValid: false,
|
|
1306
|
-
suggestions: []
|
|
1307
|
-
};
|
|
1138
|
+
return b;
|
|
1308
1139
|
}
|
|
1309
1140
|
/**
|
|
1310
|
-
*
|
|
1311
|
-
* @
|
|
1141
|
+
* Truncate a string at the last word boundary before `max`.
|
|
1142
|
+
* @param s : the string to truncate
|
|
1143
|
+
* @param max : the max number of chars
|
|
1144
|
+
* @returns : the truncated string
|
|
1312
1145
|
*/
|
|
1313
|
-
static
|
|
1314
|
-
|
|
1315
|
-
|
|
1146
|
+
static truncate(s, max = 500) {
|
|
1147
|
+
if (!s)
|
|
1148
|
+
return undefined;
|
|
1149
|
+
if (s.length <= max)
|
|
1150
|
+
return s;
|
|
1151
|
+
const i = s.lastIndexOf(' ', max - 1);
|
|
1152
|
+
return i > 0 ? s.slice(0, i) : s;
|
|
1316
1153
|
}
|
|
1317
1154
|
/**
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
* @
|
|
1155
|
+
* Join a list of strings
|
|
1156
|
+
* @param items : the list of strings
|
|
1157
|
+
* @param sep : the separator string
|
|
1158
|
+
* @param max : the maximum resulting string
|
|
1159
|
+
* @returns : the joined string
|
|
1321
1160
|
*/
|
|
1322
|
-
static
|
|
1323
|
-
|
|
1161
|
+
static join(items, sep = " ", max = 350) {
|
|
1162
|
+
if (!items || items.length === 0)
|
|
1163
|
+
return undefined;
|
|
1164
|
+
if (items.length > 1) {
|
|
1165
|
+
let l = 0;
|
|
1166
|
+
let s = "";
|
|
1167
|
+
while (s.length < max && items.length > l) {
|
|
1168
|
+
if (l > 0) {
|
|
1169
|
+
s += sep;
|
|
1170
|
+
}
|
|
1171
|
+
s += items[l++];
|
|
1172
|
+
}
|
|
1173
|
+
if (items.length > l) {
|
|
1174
|
+
s += "...";
|
|
1175
|
+
}
|
|
1176
|
+
return s;
|
|
1177
|
+
}
|
|
1178
|
+
return items[0];
|
|
1324
1179
|
}
|
|
1325
1180
|
/**
|
|
1326
|
-
*
|
|
1327
|
-
* @param
|
|
1328
|
-
* @returns
|
|
1181
|
+
* Normalize a string by converting it to lowercase and removing extra spaces, with special handling for acronyms and camelCase.
|
|
1182
|
+
* @param s : the string to normalize
|
|
1183
|
+
* @returns : The normalized string, or `undefined` if the input is falsy.
|
|
1329
1184
|
*/
|
|
1330
|
-
static
|
|
1331
|
-
|
|
1185
|
+
static normalizeDisplay(s) {
|
|
1186
|
+
if (!s)
|
|
1187
|
+
return s;
|
|
1188
|
+
return s
|
|
1189
|
+
.split(' ')
|
|
1190
|
+
.map((word, wordIndex) => {
|
|
1191
|
+
if (!word)
|
|
1192
|
+
return word;
|
|
1193
|
+
// If the word is all uppercase and contains at least one letter, we assume it's an acronym and leave it as is
|
|
1194
|
+
if (word === word.toUpperCase() && /[A-Z]/.test(word)) {
|
|
1195
|
+
return word;
|
|
1196
|
+
}
|
|
1197
|
+
// Otherwise, convert to lowercase letter by letter
|
|
1198
|
+
const chars = word.split('');
|
|
1199
|
+
return chars
|
|
1200
|
+
.map((char, charIndex) => {
|
|
1201
|
+
if (charIndex === 0 && wordIndex === 0) {
|
|
1202
|
+
// First letter of the first word:
|
|
1203
|
+
// remains uppercase only if the first 2 letters are both uppercase
|
|
1204
|
+
const secondChar = chars[1] ?? '';
|
|
1205
|
+
const keepUppercase = char === char.toUpperCase() &&
|
|
1206
|
+
secondChar === secondChar.toUpperCase() &&
|
|
1207
|
+
/[A-Z]/.test(char) &&
|
|
1208
|
+
/[A-Z]/.test(secondChar);
|
|
1209
|
+
return keepUppercase ? char : char.toLowerCase();
|
|
1210
|
+
}
|
|
1211
|
+
return char.toLowerCase();
|
|
1212
|
+
})
|
|
1213
|
+
.join('');
|
|
1214
|
+
})
|
|
1215
|
+
.join(' ');
|
|
1332
1216
|
}
|
|
1333
1217
|
/**
|
|
1334
|
-
*
|
|
1335
|
-
* @param
|
|
1336
|
-
* @
|
|
1337
|
-
* @returns : a node list
|
|
1218
|
+
* Wraps bare URLs in the given string with `<a>` anchor tags.
|
|
1219
|
+
* @param s - The plain-text or HTML string to process.
|
|
1220
|
+
* @returns The string with URLs replaced by clickable links, or `''` when `s` is falsy.
|
|
1338
1221
|
*/
|
|
1339
|
-
static
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
id: n.id,
|
|
1344
|
-
name: n.name,
|
|
1345
|
-
count: n.count,
|
|
1346
|
-
parent: parent,
|
|
1347
|
-
children: undefined,
|
|
1348
|
-
bag: n,
|
|
1349
|
-
};
|
|
1350
|
-
const children = n.children ?? n.subFolders ?? [];
|
|
1351
|
-
if (children.length > 0) {
|
|
1352
|
-
node.children = this._toNodes(children, node);
|
|
1353
|
-
}
|
|
1354
|
-
nodes.push(node);
|
|
1355
|
-
});
|
|
1356
|
-
return nodes;
|
|
1222
|
+
static replaceAsHtml(s) {
|
|
1223
|
+
if (!s)
|
|
1224
|
+
return '';
|
|
1225
|
+
return s.replace(/https?:\/\/[^\s<]+/gi, v => `<a href="${v}" target="_blank" rel="noopener">${v}</a>`);
|
|
1357
1226
|
}
|
|
1358
1227
|
/**
|
|
1359
|
-
*
|
|
1360
|
-
* @param
|
|
1361
|
-
* @param
|
|
1362
|
-
* @returns
|
|
1228
|
+
* Convert markdown to html
|
|
1229
|
+
* @param markdown : the markdown data
|
|
1230
|
+
* @param escapeHtml : true to escape HTML. Default is false
|
|
1231
|
+
* @returns the html
|
|
1363
1232
|
*/
|
|
1364
|
-
static
|
|
1365
|
-
|
|
1366
|
-
const items = [];
|
|
1367
|
-
let v = 1;
|
|
1368
|
-
const m = 1 << max;
|
|
1369
|
-
while (v < m) {
|
|
1370
|
-
if ((value & v) === v)
|
|
1371
|
-
items.push(v);
|
|
1372
|
-
v = v << 1;
|
|
1373
|
-
}
|
|
1374
|
-
return items;
|
|
1375
|
-
}
|
|
1376
|
-
else
|
|
1377
|
-
return [];
|
|
1233
|
+
static markdownToHtml(markdown, escapeHtml = false) {
|
|
1234
|
+
return MarkdownUtils.toHtml(markdown, { escapeHtml: escapeHtml });
|
|
1378
1235
|
}
|
|
1379
|
-
/** Cache for resolved color luminance results. */
|
|
1380
|
-
static { this.colorLightCache = new Map(); }
|
|
1381
1236
|
/**
|
|
1382
|
-
*
|
|
1383
|
-
* @param
|
|
1384
|
-
* @param
|
|
1385
|
-
* @returns true if
|
|
1386
|
-
*/
|
|
1387
|
-
static
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
if (
|
|
1393
|
-
return
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
let
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1237
|
+
* Compare two names
|
|
1238
|
+
* @param a : name a
|
|
1239
|
+
* @param b : name b
|
|
1240
|
+
* @returns : true if a equals b
|
|
1241
|
+
*/
|
|
1242
|
+
static compareNames(a, b) {
|
|
1243
|
+
if (a)
|
|
1244
|
+
a = a.trim();
|
|
1245
|
+
if (b)
|
|
1246
|
+
b = b.trim();
|
|
1247
|
+
if (a && b && a.length !== b.length)
|
|
1248
|
+
return false;
|
|
1249
|
+
if (this.compareString(a, b) === 0)
|
|
1250
|
+
return true;
|
|
1251
|
+
const p1 = (a ?? '').split(' ');
|
|
1252
|
+
const p2 = (b ?? '').split(' ');
|
|
1253
|
+
if (p1.length !== p2.length)
|
|
1254
|
+
return false;
|
|
1255
|
+
let matches = p1.length;
|
|
1256
|
+
const used = new Array(p2.length).fill(false);
|
|
1257
|
+
for (const s1 of p1) {
|
|
1258
|
+
for (let j = 0; j < p2.length; j++) {
|
|
1259
|
+
if (!used[j] && this.compareString(s1, p2[j]) === 0) {
|
|
1260
|
+
used[j] = true;
|
|
1261
|
+
matches--;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1406
1265
|
}
|
|
1407
|
-
|
|
1408
|
-
return light;
|
|
1266
|
+
return matches === 0;
|
|
1409
1267
|
}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
/**
|
|
1413
|
-
* General-purpose formatting pipe that converts a raw value to a locale-aware string
|
|
1414
|
-
* based on the specified format type.
|
|
1415
|
-
*
|
|
1416
|
-
* Supported types: `'date'` / `'D'`, `'currency'` / `'C'`, `'number'` / `'N'`,
|
|
1417
|
-
* `'number0'` / `'N0'`, `'percentage'` / `'P'`.
|
|
1418
|
-
*
|
|
1419
|
-
* Usage: `{{ value | format:'currency' }}`
|
|
1420
|
-
*/
|
|
1421
|
-
class FormatPipe {
|
|
1422
1268
|
/**
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
* @
|
|
1426
|
-
*
|
|
1427
|
-
* @
|
|
1428
|
-
*
|
|
1269
|
+
* Obfuscate a text with a key-driven code point rotation.
|
|
1270
|
+
*
|
|
1271
|
+
* @deprecated This is OBFUSCATION, not cryptography: it hides values from
|
|
1272
|
+
* casual inspection but offers no real security. Use {@link encrypt} /
|
|
1273
|
+
* {@link decrypt} (AES-GCM via Web Crypto) for anything sensitive.
|
|
1274
|
+
*
|
|
1275
|
+
* The rotation operates on Unicode code points and skips the surrogate
|
|
1276
|
+
* range, so the output is always a well-formed string: emoji and other
|
|
1277
|
+
* astral characters round-trip correctly (the previous code-unit based
|
|
1278
|
+
* version could emit lone surrogates and corrupt them).
|
|
1279
|
+
*
|
|
1280
|
+
* BREAKING CHANGE: values encoded with the previous code-unit algorithm
|
|
1281
|
+
* cannot be decoded by this implementation.
|
|
1282
|
+
*
|
|
1283
|
+
* @param text : the text to encode or decode
|
|
1284
|
+
* @param key : the key
|
|
1285
|
+
* @param reverse : true to decode, false to encode
|
|
1286
|
+
* @returns : the obfuscated (or restored) text, or undefined when text/key are empty
|
|
1429
1287
|
*/
|
|
1430
|
-
|
|
1431
|
-
if (
|
|
1288
|
+
static cipher(text, key, reverse = false) {
|
|
1289
|
+
if (!text || !key)
|
|
1432
1290
|
return undefined;
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
return new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value);
|
|
1291
|
+
// Valid Unicode scalar values: [0, 0xD7FF] + [0xE000, 0x10FFFF].
|
|
1292
|
+
// We rotate inside this contiguous "logical" space (surrogates excluded)
|
|
1293
|
+
// so the result is always a valid code point.
|
|
1294
|
+
const SURROGATE_START = 0xd800;
|
|
1295
|
+
const SURROGATE_SIZE = 0x800;
|
|
1296
|
+
const SPACE = 0x110000 - SURROGATE_SIZE; // number of valid scalar values
|
|
1297
|
+
const toIndex = (cp) => (cp < SURROGATE_START ? cp : cp - SURROGATE_SIZE);
|
|
1298
|
+
const toCodePoint = (i) => (i < SURROGATE_START ? i : i + SURROGATE_SIZE);
|
|
1299
|
+
const keyLen = key.length;
|
|
1300
|
+
let result = '';
|
|
1301
|
+
let i = 0;
|
|
1302
|
+
for (const ch of text) { // iterates by code point, not code unit
|
|
1303
|
+
let rotation = key.charCodeAt(i % keyLen);
|
|
1304
|
+
if (reverse)
|
|
1305
|
+
rotation = -rotation;
|
|
1306
|
+
const index = toIndex(ch.codePointAt(0));
|
|
1307
|
+
const rotated = ((index + rotation) % SPACE + SPACE) % SPACE;
|
|
1308
|
+
result += String.fromCodePoint(toCodePoint(rotated));
|
|
1309
|
+
i++;
|
|
1453
1310
|
}
|
|
1454
|
-
return
|
|
1311
|
+
return result;
|
|
1455
1312
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1313
|
+
// #region Real encryption (Web Crypto, AES-GCM)
|
|
1314
|
+
/** Payload format version, first byte of the binary envelope. */
|
|
1315
|
+
static { this.CIPHER_VERSION = 1; }
|
|
1316
|
+
/** PBKDF2 salt length in bytes. */
|
|
1317
|
+
static { this.CIPHER_SALT_LENGTH = 16; }
|
|
1318
|
+
/** AES-GCM IV length in bytes (96 bit, the recommended size for GCM). */
|
|
1319
|
+
static { this.CIPHER_IV_LENGTH = 12; }
|
|
1320
|
+
/** PBKDF2-HMAC-SHA-256 iterations (OWASP recommendation). */
|
|
1321
|
+
static { this.CIPHER_ITERATIONS = 600000; }
|
|
1322
|
+
/**
|
|
1323
|
+
* Derives an AES-GCM 256-bit key from a password using PBKDF2-HMAC-SHA-256.
|
|
1324
|
+
* @param password : the user password / passphrase
|
|
1325
|
+
* @param salt : the random salt bound to this payload
|
|
1326
|
+
* @returns : a non-extractable AES-GCM CryptoKey
|
|
1327
|
+
*/
|
|
1328
|
+
static async deriveKey(password, salt) {
|
|
1329
|
+
const material = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']);
|
|
1330
|
+
return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: salt, iterations: this.CIPHER_ITERATIONS, hash: 'SHA-256' }, material, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Encodes bytes as URL-safe Base64 (no '+', '/' or trailing '=').
|
|
1334
|
+
* @param bytes : the bytes to encode
|
|
1335
|
+
* @returns : the base64url string
|
|
1336
|
+
*/
|
|
1337
|
+
static toBase64Url(bytes) {
|
|
1338
|
+
let binary = '';
|
|
1339
|
+
const CHUNK = 0x8000;
|
|
1340
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
1341
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
1342
|
+
}
|
|
1343
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Decodes a URL-safe Base64 string back to bytes.
|
|
1347
|
+
* @param value : the base64url string
|
|
1348
|
+
* @returns : the decoded bytes
|
|
1349
|
+
*/
|
|
1350
|
+
static fromBase64Url(value) {
|
|
1351
|
+
const b64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
1352
|
+
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
|
|
1353
|
+
const binary = atob(padded);
|
|
1354
|
+
const bytes = new Uint8Array(binary.length);
|
|
1355
|
+
for (let i = 0; i < binary.length; i++)
|
|
1356
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1357
|
+
return bytes;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Encrypts a text with a password using AES-GCM (256 bit) and a key derived
|
|
1361
|
+
* via PBKDF2-HMAC-SHA-256 with a random per-message salt.
|
|
1362
|
+
*
|
|
1363
|
+
* The result is a self-contained, URL-safe Base64 payload:
|
|
1364
|
+
* `version (1 byte) | salt (16 bytes) | iv (12 bytes) | ciphertext+tag`.
|
|
1365
|
+
* GCM is authenticated: any tampering makes {@link decrypt} fail.
|
|
1366
|
+
*
|
|
1367
|
+
* Requires a secure context (HTTPS or localhost) for `crypto.subtle`.
|
|
1368
|
+
*
|
|
1369
|
+
* @param text : the plain text to encrypt
|
|
1370
|
+
* @param password : the password / passphrase
|
|
1371
|
+
* @returns : the base64url payload, or undefined when text/password are
|
|
1372
|
+
* empty or the Web Crypto API is unavailable
|
|
1373
|
+
*/
|
|
1374
|
+
static async encrypt(text, password) {
|
|
1375
|
+
if (!text || !password)
|
|
1376
|
+
return undefined;
|
|
1377
|
+
if (typeof crypto === 'undefined' || !crypto.subtle)
|
|
1378
|
+
return undefined;
|
|
1379
|
+
try {
|
|
1380
|
+
const salt = crypto.getRandomValues(new Uint8Array(this.CIPHER_SALT_LENGTH));
|
|
1381
|
+
const iv = crypto.getRandomValues(new Uint8Array(this.CIPHER_IV_LENGTH));
|
|
1382
|
+
const key = await this.deriveKey(password, salt);
|
|
1383
|
+
const cipherText = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, new TextEncoder().encode(text)));
|
|
1384
|
+
const payload = new Uint8Array(1 + salt.length + iv.length + cipherText.length);
|
|
1385
|
+
payload[0] = this.CIPHER_VERSION;
|
|
1386
|
+
payload.set(salt, 1);
|
|
1387
|
+
payload.set(iv, 1 + salt.length);
|
|
1388
|
+
payload.set(cipherText, 1 + salt.length + iv.length);
|
|
1389
|
+
return this.toBase64Url(payload);
|
|
1390
|
+
}
|
|
1391
|
+
catch {
|
|
1392
|
+
return undefined;
|
|
1393
|
+
}
|
|
1477
1394
|
}
|
|
1478
1395
|
/**
|
|
1479
|
-
*
|
|
1480
|
-
* Returns
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1483
|
-
* @param
|
|
1484
|
-
* @
|
|
1396
|
+
* Decrypts a payload produced by {@link encrypt}.
|
|
1397
|
+
* Returns undefined when the password is wrong, the payload was tampered
|
|
1398
|
+
* with, the format is unknown, or the Web Crypto API is unavailable.
|
|
1399
|
+
*
|
|
1400
|
+
* @param payload : the base64url payload returned by {@link encrypt}
|
|
1401
|
+
* @param password : the password / passphrase used to encrypt
|
|
1402
|
+
* @returns : the original plain text, or undefined on any failure
|
|
1485
1403
|
*/
|
|
1486
|
-
|
|
1487
|
-
if (!
|
|
1404
|
+
static async decrypt(payload, password) {
|
|
1405
|
+
if (!payload || !password)
|
|
1488
1406
|
return undefined;
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
* Usage: `<div [innerHTML]="html | safeHtml"></div>`
|
|
1508
|
-
*/
|
|
1509
|
-
class SafeHtmlPipe {
|
|
1510
|
-
constructor() {
|
|
1511
|
-
this.sanitizer = inject(DomSanitizer);
|
|
1407
|
+
if (typeof crypto === 'undefined' || !crypto.subtle)
|
|
1408
|
+
return undefined;
|
|
1409
|
+
try {
|
|
1410
|
+
const bytes = this.fromBase64Url(payload);
|
|
1411
|
+
const headerLength = 1 + this.CIPHER_SALT_LENGTH + this.CIPHER_IV_LENGTH;
|
|
1412
|
+
if (bytes.length <= headerLength || bytes[0] !== this.CIPHER_VERSION)
|
|
1413
|
+
return undefined;
|
|
1414
|
+
const salt = bytes.subarray(1, 1 + this.CIPHER_SALT_LENGTH);
|
|
1415
|
+
const iv = bytes.subarray(1 + this.CIPHER_SALT_LENGTH, headerLength);
|
|
1416
|
+
const cipherText = bytes.subarray(headerLength);
|
|
1417
|
+
const key = await this.deriveKey(password, salt);
|
|
1418
|
+
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, cipherText);
|
|
1419
|
+
return new TextDecoder().decode(plain);
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
// Wrong password or tampered payload: GCM authentication failed
|
|
1423
|
+
return undefined;
|
|
1424
|
+
}
|
|
1512
1425
|
}
|
|
1426
|
+
// #endregion
|
|
1513
1427
|
/**
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
* @
|
|
1428
|
+
* Clone an object (deep copy).
|
|
1429
|
+
* Uses native structuredClone: handles Date, Map, Set, typed arrays and circular references.
|
|
1430
|
+
* @param obj : the object to clone
|
|
1431
|
+
* @returns : a new object
|
|
1517
1432
|
*/
|
|
1518
|
-
|
|
1519
|
-
|
|
1433
|
+
static clone(obj) {
|
|
1434
|
+
if (obj === null || obj === undefined)
|
|
1435
|
+
return obj;
|
|
1436
|
+
return structuredClone(obj);
|
|
1520
1437
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
* when bound to attributes such as `[src]` or `[href]` on iframes, objects, or embeds.
|
|
1535
|
-
*
|
|
1536
|
-
* Usage: `<iframe [src]="url | safeUrl"></iframe>`
|
|
1537
|
-
*/
|
|
1538
|
-
class SafeUrlPipe {
|
|
1539
|
-
constructor() {
|
|
1540
|
-
this.sanitizer = inject(DomSanitizer);
|
|
1438
|
+
/**
|
|
1439
|
+
* Creates a deep clone of an object.
|
|
1440
|
+
* @param obj - The source object to clone.
|
|
1441
|
+
* @param dest - Optional pre-allocated destination object to merge the clone into.
|
|
1442
|
+
* @returns A deep copy of `obj`.
|
|
1443
|
+
*/
|
|
1444
|
+
static deepClone(obj, dest) {
|
|
1445
|
+
const cloned = structuredClone(obj);
|
|
1446
|
+
if (dest) {
|
|
1447
|
+
Object.assign(dest, cloned);
|
|
1448
|
+
return dest;
|
|
1449
|
+
}
|
|
1450
|
+
return cloned;
|
|
1541
1451
|
}
|
|
1542
1452
|
/**
|
|
1543
|
-
*
|
|
1544
|
-
* @param value - The
|
|
1545
|
-
* @returns A `SafeResourceUrl` that can be bound to resource URL attributes without blocking.
|
|
1453
|
+
* Returns `true` when `value` is a syntactically valid UUID string.
|
|
1454
|
+
* @param value - The string to validate.
|
|
1546
1455
|
*/
|
|
1547
|
-
|
|
1548
|
-
|
|
1456
|
+
static parseUUID(value) {
|
|
1457
|
+
if (!value)
|
|
1458
|
+
return false;
|
|
1459
|
+
let s = value.trim();
|
|
1460
|
+
// Strip a matching wrapper pair, if present
|
|
1461
|
+
if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('(') && s.endsWith(')'))) {
|
|
1462
|
+
s = s.slice(1, -1);
|
|
1463
|
+
}
|
|
1464
|
+
return /^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/.test(s);
|
|
1549
1465
|
}
|
|
1550
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
1551
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, isStandalone: true, name: "safeUrl" }); }
|
|
1552
|
-
}
|
|
1553
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, decorators: [{
|
|
1554
|
-
type: Pipe,
|
|
1555
|
-
args: [{
|
|
1556
|
-
name: 'safeUrl',
|
|
1557
|
-
standalone: true
|
|
1558
|
-
}]
|
|
1559
|
-
}] });
|
|
1560
|
-
|
|
1561
|
-
/**
|
|
1562
|
-
* Impure pipe that filters an array using a caller-provided predicate function.
|
|
1563
|
-
* Because the pipe is impure it re-evaluates on every change-detection cycle,
|
|
1564
|
-
* which is necessary when the predicate's captured state changes.
|
|
1565
|
-
*
|
|
1566
|
-
* Usage: `*ngFor="let item of items | callback:myFilter"`
|
|
1567
|
-
*/
|
|
1568
|
-
class SearchCallbackPipe {
|
|
1569
1466
|
/**
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
* @param items - The source array to filter. May be `undefined`.
|
|
1573
|
-
* @param callback - A predicate function that returns `true` for items to keep.
|
|
1574
|
-
* @returns A new filtered array, the original array when no callback is provided,
|
|
1575
|
-
* or `undefined` when `items` is `undefined`.
|
|
1467
|
+
* Returns `true` when `value` is a valid, non-empty (non-zero) UUID.
|
|
1468
|
+
* @param value - The string to validate.
|
|
1576
1469
|
*/
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
return items;
|
|
1580
|
-
return items.filter(item => callback(item));
|
|
1470
|
+
static parseUUIDNotEmpty(value) {
|
|
1471
|
+
return this.parseUUID(value) && !/^[{(]?0{8}-(?:0{4}-){3}0{12}[)}]?$/.test((value ?? '').trim());
|
|
1581
1472
|
}
|
|
1582
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
1583
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, isStandalone: true, name: "callback", pure: false }); }
|
|
1584
|
-
}
|
|
1585
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, decorators: [{
|
|
1586
|
-
type: Pipe,
|
|
1587
|
-
args: [{
|
|
1588
|
-
name: 'callback',
|
|
1589
|
-
pure: false,
|
|
1590
|
-
standalone: true
|
|
1591
|
-
}]
|
|
1592
|
-
}] });
|
|
1593
|
-
|
|
1594
|
-
/**
|
|
1595
|
-
* Impure pipe that filters an array of searchable items against a text query.
|
|
1596
|
-
*
|
|
1597
|
-
* Each item is matched either via its `searchBag.name` property (when present)
|
|
1598
|
-
* or by converting the item itself to a lowercase string. The optional `metadata`
|
|
1599
|
-
* argument is updated in-place with the total item count and the filtered count,
|
|
1600
|
-
* making it usable in the template alongside `*ngFor`.
|
|
1601
|
-
*
|
|
1602
|
-
* Usage:
|
|
1603
|
-
* ```html
|
|
1604
|
-
* <div *ngFor="let item of items | search:filterText:meta">...</div>
|
|
1605
|
-
* <div>Showing {{ meta.count }} of {{ meta.total }}</div>
|
|
1606
|
-
* ```
|
|
1607
|
-
*/
|
|
1608
|
-
class SearchFilterPipe {
|
|
1609
1473
|
/**
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
*
|
|
1474
|
+
* Return an empty UUID
|
|
1475
|
+
* @returns : the empty UUID
|
|
1476
|
+
*/
|
|
1477
|
+
static emptyUUID() { return "00000000-0000-0000-0000-000000000000"; }
|
|
1478
|
+
;
|
|
1479
|
+
/**
|
|
1480
|
+
* Create a new UUID
|
|
1481
|
+
* @returns : the string UUID
|
|
1617
1482
|
*/
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
return text?.toLowerCase().includes(query) ?? false;
|
|
1483
|
+
static generateUUID() {
|
|
1484
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
1485
|
+
return crypto.randomUUID();
|
|
1486
|
+
}
|
|
1487
|
+
// Fallback (non-secure contexts)
|
|
1488
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
1489
|
+
const r = (Math.random() * 16) | 0;
|
|
1490
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
1491
|
+
return v.toString(16);
|
|
1628
1492
|
});
|
|
1629
|
-
metadata.total = items.length;
|
|
1630
|
-
metadata.count = result.length;
|
|
1631
|
-
return result;
|
|
1632
1493
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
/**
|
|
1645
|
-
* Pipe that converts plain-text newlines (`\r\n`, `\r`, `\n`) to HTML `<br>` tags
|
|
1646
|
-
* and marks the result as trusted HTML so Angular does not escape it.
|
|
1647
|
-
*
|
|
1648
|
-
* Usage: `{{ text | formatHtml }}`
|
|
1649
|
-
*/
|
|
1650
|
-
class FormatHtmlPipe {
|
|
1651
|
-
constructor() {
|
|
1652
|
-
this.sanitizer = inject(DomSanitizer);
|
|
1494
|
+
/**
|
|
1495
|
+
* Reconstruct a standard UUID (with dashes) from a 32-char hex string without dashes.
|
|
1496
|
+
* @param value : 32-character hex string
|
|
1497
|
+
* @returns : the formatted UUID or the original string if it doesn't match the expected format
|
|
1498
|
+
*/
|
|
1499
|
+
static inflateUUID(value) {
|
|
1500
|
+
const s = value.trim();
|
|
1501
|
+
if (s.length !== 32)
|
|
1502
|
+
return s;
|
|
1503
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
|
|
1653
1504
|
}
|
|
1505
|
+
/** Precompiled validation patterns. */
|
|
1506
|
+
static { this.emailRegex = /^(?:[a-z0-9+!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; }
|
|
1507
|
+
static { this.urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,12})(:\d{1,5})?([/\w .%-]*)?(\?[^\s#]*)?(#\S*)?$/i; }
|
|
1654
1508
|
/**
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1657
|
-
* @
|
|
1658
|
-
* @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
|
|
1509
|
+
* Parse a text and return true if it is a valid email (or empty)
|
|
1510
|
+
* @param value : email
|
|
1511
|
+
* @returns : true if the email is valid or empty
|
|
1659
1512
|
*/
|
|
1660
|
-
|
|
1661
|
-
|
|
1513
|
+
static parseEmail(value) {
|
|
1514
|
+
if (!value)
|
|
1515
|
+
return true;
|
|
1516
|
+
return this.emailRegex.test(value.trim().toLowerCase());
|
|
1662
1517
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
* directly via `imports: [FormatPipe, SafeHtmlPipe, ...]` instead.
|
|
1681
|
-
*
|
|
1682
|
-
* @example
|
|
1683
|
-
* bootstrapApplication(AppComponent, {
|
|
1684
|
-
* providers: [provideArsCore()]
|
|
1685
|
-
* });
|
|
1686
|
-
*/
|
|
1687
|
-
function provideArsCore() {
|
|
1688
|
-
return makeEnvironmentProviders([
|
|
1689
|
-
SearchFilterPipe,
|
|
1690
|
-
SearchCallbackPipe,
|
|
1691
|
-
SafeHtmlPipe,
|
|
1692
|
-
SafeUrlPipe,
|
|
1693
|
-
ReplacePipe,
|
|
1694
|
-
FormatPipe,
|
|
1695
|
-
FormatHtmlPipe
|
|
1696
|
-
]);
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
/**
|
|
1700
|
-
* Creates an array of the given length, filling each slot with the result of `valueFunction`.
|
|
1701
|
-
* @param length - Number of elements to create.
|
|
1702
|
-
* @param valueFunction - Factory called with each index to produce the element value.
|
|
1703
|
-
* @returns Typed array of `length` elements.
|
|
1704
|
-
*/
|
|
1705
|
-
function range(length, valueFunction) {
|
|
1706
|
-
const valuesArray = Array(length);
|
|
1707
|
-
for (let i = 0; i < length; i++) {
|
|
1708
|
-
valuesArray[i] = valueFunction(i);
|
|
1518
|
+
/**
|
|
1519
|
+
* Parse a text containing one or more email addresses separated by `;` or `,`
|
|
1520
|
+
* and return true if all of them are valid (or the text is empty).
|
|
1521
|
+
* Empty items caused by double or trailing separators are ignored.
|
|
1522
|
+
* @param value : the email list to parse (e.g. "a@b.it; c@d.it")
|
|
1523
|
+
* @returns : true if all emails are valid or the value is empty
|
|
1524
|
+
*/
|
|
1525
|
+
static parseEmails(value) {
|
|
1526
|
+
if (!value || value.trim().length === 0)
|
|
1527
|
+
return true;
|
|
1528
|
+
const items = value
|
|
1529
|
+
.split(/[;,]/)
|
|
1530
|
+
.map(e => e.trim().toLowerCase())
|
|
1531
|
+
.filter(e => e.length > 0);
|
|
1532
|
+
if (items.length === 0)
|
|
1533
|
+
return false; // only separators, no addresses
|
|
1534
|
+
return items.every(e => this.emailRegex.test(e));
|
|
1709
1535
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
if (matDateLocale) {
|
|
1744
|
-
this.setLocale(matDateLocale);
|
|
1536
|
+
/**
|
|
1537
|
+
* Parse a text and return true if it is a valid url (or empty)
|
|
1538
|
+
* @param value : the url to parse
|
|
1539
|
+
* @returns : true if the url is valid or empty
|
|
1540
|
+
*/
|
|
1541
|
+
static parseUrl(value) {
|
|
1542
|
+
if (!value)
|
|
1543
|
+
return true;
|
|
1544
|
+
return this.urlRegex.test(value.trim().toLowerCase());
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Get date parts from a string value
|
|
1548
|
+
* @param value : the string to parse
|
|
1549
|
+
* @returns : an array of numbers with year, month, day
|
|
1550
|
+
*/
|
|
1551
|
+
static getDateParts(value) {
|
|
1552
|
+
if (!value)
|
|
1553
|
+
return undefined;
|
|
1554
|
+
let parts = [];
|
|
1555
|
+
if (value.indexOf("-") !== -1) {
|
|
1556
|
+
const p = value.split("-");
|
|
1557
|
+
if (p.length !== 3)
|
|
1558
|
+
return undefined;
|
|
1559
|
+
parts = [parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2])];
|
|
1560
|
+
}
|
|
1561
|
+
else if (value.indexOf("/") !== -1) {
|
|
1562
|
+
const p = value.split("/");
|
|
1563
|
+
if (p.length !== 3)
|
|
1564
|
+
return undefined;
|
|
1565
|
+
parts = [parseInt(p[2]), parseInt(p[1]) - 1, parseInt(p[0])];
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
return undefined;
|
|
1745
1569
|
}
|
|
1570
|
+
return parts;
|
|
1746
1571
|
}
|
|
1747
1572
|
/**
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1573
|
+
* Checks whether a value is a valid Date instance.
|
|
1574
|
+
* Note: do not use `date.getTime()` as a truthiness test, it is 0 (falsy)
|
|
1575
|
+
* for the Unix epoch (1970-01-01T00:00:00Z) even though the date is valid.
|
|
1576
|
+
* @param d : the value to check
|
|
1577
|
+
* @returns : true when `d` is a Date representing a valid point in time
|
|
1750
1578
|
*/
|
|
1751
|
-
|
|
1752
|
-
return
|
|
1579
|
+
static isValidDate(d) {
|
|
1580
|
+
return d instanceof Date && !isNaN(d.getTime());
|
|
1753
1581
|
}
|
|
1754
1582
|
/**
|
|
1755
|
-
*
|
|
1756
|
-
* @param
|
|
1583
|
+
* Parse a date
|
|
1584
|
+
* @param value : the value to check
|
|
1585
|
+
* @param locale : the locale to use
|
|
1586
|
+
* @returns : a valid Date object or undefined
|
|
1757
1587
|
*/
|
|
1758
|
-
|
|
1759
|
-
|
|
1588
|
+
static parseDate(value, locale = it) {
|
|
1589
|
+
// No value at all
|
|
1590
|
+
if (!value)
|
|
1591
|
+
return undefined;
|
|
1592
|
+
// A Date value
|
|
1593
|
+
if (value instanceof Date)
|
|
1594
|
+
return this.isValidDate(value) ? value : undefined;
|
|
1595
|
+
// Parse known formats using date-fns
|
|
1596
|
+
let d = parseISO(value);
|
|
1597
|
+
if (this.isValidDate(d) && d.getFullYear() > 1750)
|
|
1598
|
+
return d;
|
|
1599
|
+
d = parse(value, 'dd/MM/yyyy', new Date(), { locale: locale });
|
|
1600
|
+
if (this.isValidDate(d) && d.getFullYear() > 1750)
|
|
1601
|
+
return d;
|
|
1602
|
+
d = parse(value, 'yyyy-MM-dd', new Date(), { locale: locale });
|
|
1603
|
+
if (this.isValidDate(d) && d.getFullYear() > 1750)
|
|
1604
|
+
return d;
|
|
1605
|
+
// Parse values manually
|
|
1606
|
+
const parts = this.getDateParts(value);
|
|
1607
|
+
if (!parts)
|
|
1608
|
+
return undefined;
|
|
1609
|
+
if (parts[0] < 100)
|
|
1610
|
+
parts[0] += 2000;
|
|
1611
|
+
if (isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
|
|
1612
|
+
return undefined;
|
|
1613
|
+
if (isNaN(parts[1]) || parts[1] < 0)
|
|
1614
|
+
return undefined;
|
|
1615
|
+
if (isNaN(parts[2]) || parts[2] < 0)
|
|
1616
|
+
return undefined;
|
|
1617
|
+
if (parts[1] > 11)
|
|
1618
|
+
return undefined;
|
|
1619
|
+
if (parts[1] === 1 && parts[2] > 29)
|
|
1620
|
+
return undefined;
|
|
1621
|
+
else if (parts[1] !== 1 && parts[2] > 31)
|
|
1622
|
+
return undefined;
|
|
1623
|
+
return new TZDate(parts[0], parts[1], parts[2], 12);
|
|
1760
1624
|
}
|
|
1761
1625
|
/**
|
|
1762
|
-
*
|
|
1763
|
-
* @param date
|
|
1626
|
+
* Format a date
|
|
1627
|
+
* @param value : the date or string to format
|
|
1628
|
+
* @param fmt : the DateFormat to use or the string pattern
|
|
1629
|
+
* @param locale : the locale to use (default is IT)
|
|
1630
|
+
* @returns : the formatted string
|
|
1764
1631
|
*/
|
|
1765
|
-
|
|
1766
|
-
|
|
1632
|
+
static formatDate(value, fmt = DateFormat.Short, locale = it) {
|
|
1633
|
+
// No value at all
|
|
1634
|
+
if (!value)
|
|
1635
|
+
return '';
|
|
1636
|
+
// A string
|
|
1637
|
+
if (typeof value === 'string' || value instanceof String)
|
|
1638
|
+
value = this.parseDate(value);
|
|
1639
|
+
// Not a date
|
|
1640
|
+
if (!this.isValidDate(value))
|
|
1641
|
+
return '';
|
|
1642
|
+
// Format
|
|
1643
|
+
switch (fmt) {
|
|
1644
|
+
case DateFormat.Short: return format(value, "dd/MM/yyyy", { locale: locale });
|
|
1645
|
+
case DateFormat.Long: return format(value, "d MMMM yyyy", { locale: locale });
|
|
1646
|
+
case DateFormat.LongWithShortMonth: return format(value, "d MMM yyyy", { locale: locale });
|
|
1647
|
+
case DateFormat.LongWithWeekDay: return format(value, "EEEE, d MMMM yyyy", { locale: locale });
|
|
1648
|
+
case DateFormat.LongWithShortWeekDay: return format(value, "EEE, d MMMM yyyy", { locale: locale });
|
|
1649
|
+
case DateFormat.MonthAndYear: return format(value, "MMM yyyy", { locale: locale });
|
|
1650
|
+
case DateFormat.LongMonthAndYear: return format(value, "MMMM yyyy", { locale: locale });
|
|
1651
|
+
case DateFormat.WeekDay: return format(value, "EEE, d", { locale: locale });
|
|
1652
|
+
case DateFormat.LongWeekDay: return format(value, "EEEE, d", { locale: locale });
|
|
1653
|
+
case DateFormat.DayAndMonth: return format(value, "d MMMM", { locale: locale });
|
|
1654
|
+
case DateFormat.ShortUS: return format(value, "MM/dd/yyyy", { locale: locale });
|
|
1655
|
+
case DateFormat.ShortISO8601: return format(value, "yyyy-MM-dd", { locale: locale });
|
|
1656
|
+
default:
|
|
1657
|
+
return format(value, fmt, { locale: locale });
|
|
1658
|
+
}
|
|
1767
1659
|
}
|
|
1768
1660
|
/**
|
|
1769
|
-
*
|
|
1770
|
-
* @param
|
|
1661
|
+
* Return an italian local date
|
|
1662
|
+
* @param value : the date
|
|
1663
|
+
* @returns : the new date
|
|
1771
1664
|
*/
|
|
1772
|
-
|
|
1773
|
-
|
|
1665
|
+
static toLocalDate(value) {
|
|
1666
|
+
// No value at all
|
|
1667
|
+
if (!value)
|
|
1668
|
+
return undefined;
|
|
1669
|
+
// A string
|
|
1670
|
+
if (typeof value === 'string' || value instanceof String)
|
|
1671
|
+
value = this.parseDate(value);
|
|
1672
|
+
// Not a date
|
|
1673
|
+
if (!this.isValidDate(value))
|
|
1674
|
+
return undefined;
|
|
1675
|
+
// Update date
|
|
1676
|
+
return new TZDate(value, "Europe/Rome");
|
|
1774
1677
|
}
|
|
1775
1678
|
/**
|
|
1776
|
-
*
|
|
1777
|
-
* @param
|
|
1679
|
+
* Update a DateInterval object according to a string
|
|
1680
|
+
* @param value : string value
|
|
1681
|
+
* @param interval : DateInterval value to update
|
|
1682
|
+
* @param end : true if must be updated the first or the end value
|
|
1683
|
+
* @param copy : copy the same value (works only if not end element)
|
|
1778
1684
|
*/
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1685
|
+
static changeDateInterval(value, interval, end = false, copy = false) {
|
|
1686
|
+
if (value) {
|
|
1687
|
+
let year = -1;
|
|
1688
|
+
if (value.length === 4 && (year = parseInt(value)) > 1750) {
|
|
1689
|
+
if (!end) {
|
|
1690
|
+
interval.from = new TZDate(new Date(year, 0, 1), 'Europe/Rome');
|
|
1691
|
+
interval.to = new TZDate(new Date(year, 11, 31), 'Europe/Rome');
|
|
1692
|
+
}
|
|
1693
|
+
else
|
|
1694
|
+
interval.to = new Date(year, 11, 31);
|
|
1695
|
+
}
|
|
1696
|
+
else {
|
|
1697
|
+
const parts = this.getDateParts(value);
|
|
1698
|
+
if (!parts || parts.length !== 3)
|
|
1699
|
+
return;
|
|
1700
|
+
if (isNaN(parts[0]) || parts[0] < 1750)
|
|
1701
|
+
return;
|
|
1702
|
+
if (isNaN(parts[1]) || parts[1] < 0 || parts[1] > 11)
|
|
1703
|
+
return;
|
|
1704
|
+
if (isNaN(parts[2]) || parts[2] < 1 || parts[2] > 31)
|
|
1705
|
+
return;
|
|
1706
|
+
const d = new TZDate(new Date(parts[0], parts[1], parts[2]), 'Europe/Rome');
|
|
1707
|
+
if (end)
|
|
1708
|
+
interval.to = d;
|
|
1709
|
+
else if (copy) {
|
|
1710
|
+
interval.from = d;
|
|
1711
|
+
interval.to = d;
|
|
1712
|
+
}
|
|
1713
|
+
else
|
|
1714
|
+
interval.from = d;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1782
1717
|
}
|
|
1718
|
+
/** Cache of `Intl.NumberFormat` instances (their construction is expensive). */
|
|
1719
|
+
static { this.numberFormatCache = new Map(); }
|
|
1783
1720
|
/**
|
|
1784
|
-
* Returns
|
|
1785
|
-
*
|
|
1721
|
+
* Returns a cached `Intl.NumberFormat` for the given options, creating it on first use.
|
|
1722
|
+
* @param locale - BCP 47 locale tag.
|
|
1723
|
+
* @param options - The `Intl.NumberFormat` options.
|
|
1724
|
+
* @returns A shared formatter instance.
|
|
1786
1725
|
*/
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
return
|
|
1795
|
-
if (dtf) {
|
|
1796
|
-
// date-fns doesn't appear to support this functionality.
|
|
1797
|
-
// Fall back to `Intl` on supported browsers.
|
|
1798
|
-
const date = new Date();
|
|
1799
|
-
date.setUTCFullYear(2017, 0, i + 1);
|
|
1800
|
-
date.setUTCHours(0, 0, 0, 0);
|
|
1801
|
-
return dtf.format(date).replace(/[\u200e\u200f]/g, '');
|
|
1802
|
-
}
|
|
1803
|
-
return String(i + 1);
|
|
1804
|
-
});
|
|
1726
|
+
static getNumberFormat(locale, options) {
|
|
1727
|
+
const key = `${locale}|${options.style}|${options.currency ?? ''}|${options.minimumFractionDigits}|${options.maximumFractionDigits}`;
|
|
1728
|
+
let fmt = this.numberFormatCache.get(key);
|
|
1729
|
+
if (!fmt) {
|
|
1730
|
+
fmt = new Intl.NumberFormat(locale, options);
|
|
1731
|
+
this.numberFormatCache.set(key, fmt);
|
|
1732
|
+
}
|
|
1733
|
+
return fmt;
|
|
1805
1734
|
}
|
|
1806
1735
|
/**
|
|
1807
|
-
*
|
|
1808
|
-
* @param
|
|
1736
|
+
* Formats a number using a cached `Intl.NumberFormat`.
|
|
1737
|
+
* @param value - The number to format.
|
|
1738
|
+
* @param decimals - Maximum decimal places (default: `2`).
|
|
1739
|
+
* @param locale - BCP 47 locale tag (default: `'it-IT'`).
|
|
1740
|
+
* @returns The formatted number string.
|
|
1809
1741
|
*/
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
return range(7, i => this.format(new Date(2017, 0, i + 1), pattern));
|
|
1742
|
+
static formatNumber(value, decimals = 2, locale = 'it-IT') {
|
|
1743
|
+
return this.getNumberFormat(locale, { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
|
|
1813
1744
|
}
|
|
1814
1745
|
/**
|
|
1815
|
-
*
|
|
1816
|
-
* @param
|
|
1746
|
+
* Formats a number as a currency string using a cached `Intl.NumberFormat`.
|
|
1747
|
+
* @param value - The number to format.
|
|
1748
|
+
* @param currency - ISO 4217 currency code (default: `'EUR'`).
|
|
1749
|
+
* @param decimals - Maximum decimal places (default: `2`).
|
|
1750
|
+
* @param locale - BCP 47 locale tag (default: `'it-IT'`).
|
|
1751
|
+
* @returns The formatted currency string.
|
|
1817
1752
|
*/
|
|
1818
|
-
|
|
1819
|
-
return this.
|
|
1753
|
+
static formatCurrency(value, currency = 'EUR', decimals = 2, locale = 'it-IT') {
|
|
1754
|
+
return this.getNumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
|
|
1820
1755
|
}
|
|
1821
1756
|
/**
|
|
1822
|
-
*
|
|
1757
|
+
* Percent-encodes a string for safe inclusion in a URL.
|
|
1758
|
+
* @param value - The string to encode.
|
|
1759
|
+
* @returns The encoded string, or `undefined` when `value` is empty.
|
|
1823
1760
|
*/
|
|
1824
|
-
|
|
1825
|
-
return
|
|
1761
|
+
static urlEncode(value) {
|
|
1762
|
+
return value.length > 0
|
|
1763
|
+
? encodeURIComponent(value)
|
|
1764
|
+
: undefined;
|
|
1826
1765
|
}
|
|
1827
1766
|
/**
|
|
1828
|
-
*
|
|
1829
|
-
* @param
|
|
1767
|
+
* Decodes a percent-encoded URL string, treating `+` as a space.
|
|
1768
|
+
* @param value - The encoded string to decode.
|
|
1769
|
+
* @returns The decoded string, or `undefined` when `value` is empty or absent.
|
|
1830
1770
|
*/
|
|
1831
|
-
|
|
1832
|
-
return
|
|
1771
|
+
static urlDecode(value) {
|
|
1772
|
+
return value && value.length > 0
|
|
1773
|
+
? decodeURIComponent(value.replace(/\+/g, '%20'))
|
|
1774
|
+
: undefined;
|
|
1833
1775
|
}
|
|
1834
1776
|
/**
|
|
1835
|
-
*
|
|
1836
|
-
* @param
|
|
1777
|
+
* Reads a query string parameter value from the current page URL.
|
|
1778
|
+
* @param name - The parameter name to look up.
|
|
1779
|
+
* @returns The decoded parameter value, or `undefined` when absent or running server-side.
|
|
1837
1780
|
*/
|
|
1838
|
-
|
|
1839
|
-
|
|
1781
|
+
static getQueryStringValueByName(name) {
|
|
1782
|
+
if (!this.isBrowser())
|
|
1783
|
+
return undefined;
|
|
1784
|
+
const v = new URLSearchParams(window.location.search).get(name);
|
|
1785
|
+
return v && v !== 'null' ? v : undefined;
|
|
1840
1786
|
}
|
|
1841
1787
|
/**
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
* @param year - Full four-digit year.
|
|
1845
|
-
* @param month - Zero-based month index (0 = January, 11 = December).
|
|
1846
|
-
* @param date - Day-of-month (1-based).
|
|
1788
|
+
* Generate a password
|
|
1789
|
+
* @returns : the password string
|
|
1847
1790
|
*/
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1791
|
+
static generatePassword(length = 12) {
|
|
1792
|
+
const lower = 'abcdefghijkmnopqrstuvwxyz'; // no 'l' (ambiguous)
|
|
1793
|
+
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // no 'I'/'O' (ambiguous)
|
|
1794
|
+
const digits = '23456789'; // no '0'/'1' (ambiguous)
|
|
1795
|
+
const symbols = '$!#%&*+-';
|
|
1796
|
+
const all = lower + upper + digits + symbols;
|
|
1797
|
+
// Secure random indices (crypto is available in browsers and Node)
|
|
1798
|
+
const rnd = (max) => {
|
|
1799
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
1800
|
+
const buf = new Uint32Array(1);
|
|
1801
|
+
crypto.getRandomValues(buf);
|
|
1802
|
+
return buf[0] % max;
|
|
1803
|
+
}
|
|
1804
|
+
return Math.floor(Math.random() * max);
|
|
1805
|
+
};
|
|
1806
|
+
// Guarantee at least one char per class, fill the rest, then shuffle
|
|
1807
|
+
const chars = [
|
|
1808
|
+
lower[rnd(lower.length)],
|
|
1809
|
+
upper[rnd(upper.length)],
|
|
1810
|
+
digits[rnd(digits.length)],
|
|
1811
|
+
symbols[rnd(symbols.length)],
|
|
1812
|
+
];
|
|
1813
|
+
while (chars.length < Math.max(length, 8)) {
|
|
1814
|
+
chars.push(all[rnd(all.length)]);
|
|
1854
1815
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
result.setFullYear(year, month, date);
|
|
1859
|
-
result.setHours(0, 0, 0, 0);
|
|
1860
|
-
const result2 = new TZDate(result, 'Europe/Rome');
|
|
1861
|
-
if (result2.getMonth() !== month) {
|
|
1862
|
-
throw Error(`Invalid date "${date}" for month with index "${month}".`);
|
|
1816
|
+
for (let i = chars.length - 1; i > 0; i--) {
|
|
1817
|
+
const j = rnd(i + 1);
|
|
1818
|
+
[chars[i], chars[j]] = [chars[j], chars[i]];
|
|
1863
1819
|
}
|
|
1864
|
-
return
|
|
1820
|
+
return chars.join('');
|
|
1865
1821
|
}
|
|
1866
1822
|
/**
|
|
1867
|
-
*
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
return new Date();
|
|
1871
|
-
}
|
|
1872
|
-
/**
|
|
1873
|
-
* Parses a value into a `Date`.
|
|
1874
|
-
* - Strings are first attempted as ISO 8601, then matched against each format in `parseFormat`.
|
|
1875
|
-
* - Numbers are treated as Unix timestamps (milliseconds).
|
|
1876
|
-
* - Existing `Date` instances are cloned.
|
|
1877
|
-
* @param value - The value to parse.
|
|
1878
|
-
* @param parseFormat - A format string or an array of format strings (date-fns tokens).
|
|
1879
|
-
* @returns A valid `Date` in `Europe/Rome`, an invalid sentinel, or `null` for unrecognised input.
|
|
1823
|
+
* Calculate password strength
|
|
1824
|
+
* @param password: the password to evaluate
|
|
1825
|
+
* @returns the password strength info
|
|
1880
1826
|
*/
|
|
1881
|
-
|
|
1882
|
-
if (
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1827
|
+
static calculatePasswordStrength(password) {
|
|
1828
|
+
if (password && password.length > 0) {
|
|
1829
|
+
let score = 0;
|
|
1830
|
+
const suggestions = [];
|
|
1831
|
+
// Length
|
|
1832
|
+
if (password.length >= 10)
|
|
1833
|
+
score++;
|
|
1834
|
+
else
|
|
1835
|
+
suggestions.push('Usa almeno 10 caratteri.');
|
|
1836
|
+
if (password.length >= 12)
|
|
1837
|
+
score++;
|
|
1838
|
+
else if (password.length >= 10)
|
|
1839
|
+
suggestions.push('Considera di usare più di 12 caratteri.');
|
|
1840
|
+
// Lowercase letters
|
|
1841
|
+
if (/[a-z]/.test(password))
|
|
1842
|
+
score++;
|
|
1843
|
+
else
|
|
1844
|
+
suggestions.push('Aggiungi lettere minuscole.');
|
|
1845
|
+
// Uppercase letters
|
|
1846
|
+
if (/[A-Z]/.test(password))
|
|
1847
|
+
score++;
|
|
1848
|
+
else
|
|
1849
|
+
suggestions.push('Aggiungi lettere maiuscole.');
|
|
1850
|
+
// Numbers
|
|
1851
|
+
if (/\d/.test(password))
|
|
1852
|
+
score++;
|
|
1853
|
+
else
|
|
1854
|
+
suggestions.push('Aggiungi numeri.');
|
|
1855
|
+
// Special characters
|
|
1856
|
+
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password))
|
|
1857
|
+
score++;
|
|
1858
|
+
else
|
|
1859
|
+
suggestions.push('Aggiungi caratteri speciali (!@#$%^&*).');
|
|
1860
|
+
// Common patterns
|
|
1861
|
+
if (/(.)\1{2,}/.test(password)) {
|
|
1862
|
+
score = Math.max(0, score - 1);
|
|
1863
|
+
suggestions.push('Evita di ripetere lo stesso carattere.');
|
|
1886
1864
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1865
|
+
if (/123|abc|qwe/i.test(password)) {
|
|
1866
|
+
score = Math.max(0, score - 1);
|
|
1867
|
+
suggestions.push('Evita sequenze comuni (123, abc, qwe).');
|
|
1868
|
+
}
|
|
1869
|
+
// Label and color
|
|
1870
|
+
let label;
|
|
1871
|
+
let color;
|
|
1872
|
+
let isValid;
|
|
1873
|
+
if (score <= 2) {
|
|
1874
|
+
label = 'Molto debole';
|
|
1875
|
+
color = '#f44336';
|
|
1876
|
+
isValid = false;
|
|
1877
|
+
}
|
|
1878
|
+
else if (score <= 3) {
|
|
1879
|
+
label = 'Debole';
|
|
1880
|
+
color = '#ff9800';
|
|
1881
|
+
isValid = false;
|
|
1882
|
+
}
|
|
1883
|
+
else if (score <= 4) {
|
|
1884
|
+
label = 'Media';
|
|
1885
|
+
color = '#ffc107';
|
|
1886
|
+
isValid = true;
|
|
1887
|
+
}
|
|
1888
|
+
else if (score <= 5) {
|
|
1889
|
+
label = 'Forte';
|
|
1890
|
+
color = '#8bc34a';
|
|
1891
|
+
isValid = true;
|
|
1890
1892
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
}
|
|
1893
|
+
else {
|
|
1894
|
+
label = 'Molto forte';
|
|
1895
|
+
color = '#4caf50';
|
|
1896
|
+
isValid = true;
|
|
1896
1897
|
}
|
|
1897
|
-
return
|
|
1898
|
-
}
|
|
1899
|
-
else if (typeof value === 'number') {
|
|
1900
|
-
return new Date(value);
|
|
1901
|
-
}
|
|
1902
|
-
else if (value instanceof Date) {
|
|
1903
|
-
return this.clone(value);
|
|
1904
|
-
}
|
|
1905
|
-
return null;
|
|
1906
|
-
}
|
|
1907
|
-
/**
|
|
1908
|
-
* Formats a `Date` using the given date-fns display format string.
|
|
1909
|
-
* Throws an `Error` when `date` is not valid.
|
|
1910
|
-
* @param date - The date to format.
|
|
1911
|
-
* @param displayFormat - A date-fns format string (e.g. `'P'`, `'LLL uuuu'`).
|
|
1912
|
-
*/
|
|
1913
|
-
format(date, displayFormat) {
|
|
1914
|
-
if (!this.isValid(date)) {
|
|
1915
|
-
throw Error('DateFnsAdapter: Cannot format invalid date.');
|
|
1898
|
+
return { score, label, color, suggestions, isValid };
|
|
1916
1899
|
}
|
|
1917
|
-
|
|
1900
|
+
else
|
|
1901
|
+
return {
|
|
1902
|
+
score: 0,
|
|
1903
|
+
isValid: false,
|
|
1904
|
+
suggestions: []
|
|
1905
|
+
};
|
|
1918
1906
|
}
|
|
1919
1907
|
/**
|
|
1920
|
-
*
|
|
1921
|
-
* @
|
|
1922
|
-
* @param years - Number of years to add (can be negative).
|
|
1908
|
+
* Check if current browser supports touch
|
|
1909
|
+
* @returns : true if the display is touchable
|
|
1923
1910
|
*/
|
|
1924
|
-
|
|
1925
|
-
return
|
|
1911
|
+
static isTouchable() {
|
|
1912
|
+
return (this.isBrowser() && ("ontouchstart" in window ||
|
|
1913
|
+
navigator.maxTouchPoints > 0));
|
|
1926
1914
|
}
|
|
1927
1915
|
/**
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1930
|
-
* @
|
|
1916
|
+
* This check will prevent 'window' logic to be executed
|
|
1917
|
+
* while executing the server rendering
|
|
1918
|
+
* @returns : true if using the browser
|
|
1931
1919
|
*/
|
|
1932
|
-
|
|
1933
|
-
return
|
|
1920
|
+
static isBrowser() {
|
|
1921
|
+
return typeof (window) !== 'undefined';
|
|
1934
1922
|
}
|
|
1935
1923
|
/**
|
|
1936
|
-
*
|
|
1937
|
-
* @param
|
|
1938
|
-
* @
|
|
1924
|
+
* Convert folders in a tree of Node object.
|
|
1925
|
+
* @param folders : the subfolders group or null to root
|
|
1926
|
+
* @returns : a node list
|
|
1939
1927
|
*/
|
|
1940
|
-
|
|
1941
|
-
return
|
|
1928
|
+
static toNodes(folders) {
|
|
1929
|
+
return this._toNodes(folders, undefined);
|
|
1942
1930
|
}
|
|
1943
1931
|
/**
|
|
1944
|
-
*
|
|
1945
|
-
* @param
|
|
1932
|
+
* Convert folders in a tree of Node object.
|
|
1933
|
+
* @param folders : the children group or null to root
|
|
1934
|
+
* @param parent : the parent node
|
|
1935
|
+
* @returns : a node list
|
|
1946
1936
|
*/
|
|
1947
|
-
|
|
1948
|
-
|
|
1937
|
+
static _toNodes(folders, parent) {
|
|
1938
|
+
const nodes = [];
|
|
1939
|
+
folders.forEach((n) => {
|
|
1940
|
+
const node = {
|
|
1941
|
+
id: n.id,
|
|
1942
|
+
name: n.name,
|
|
1943
|
+
count: n.count,
|
|
1944
|
+
parent: parent,
|
|
1945
|
+
children: undefined,
|
|
1946
|
+
bag: n,
|
|
1947
|
+
};
|
|
1948
|
+
const children = n.children ?? n.subFolders ?? [];
|
|
1949
|
+
if (children.length > 0) {
|
|
1950
|
+
node.children = this._toNodes(children, node);
|
|
1951
|
+
}
|
|
1952
|
+
nodes.push(node);
|
|
1953
|
+
});
|
|
1954
|
+
return nodes;
|
|
1949
1955
|
}
|
|
1950
1956
|
/**
|
|
1951
|
-
* Returns
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
* @
|
|
1957
|
+
* Returns an array of individual power-of-2 flag values that are set in `value`.
|
|
1958
|
+
* @param value - The bitmask to decompose.
|
|
1959
|
+
* @param max - Upper-bound exponent: checks flags from `1` up to `1 << max` (default: `30`).
|
|
1960
|
+
* @returns Array of set flag values, or an empty array when `value` is `0`.
|
|
1955
1961
|
*/
|
|
1956
|
-
|
|
1957
|
-
if (
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1962
|
+
static getFlags(value, max = 30) {
|
|
1963
|
+
if (value !== 0) {
|
|
1964
|
+
const items = [];
|
|
1965
|
+
const top = Math.min(max, 30); // beyond 30 the shift overflows into the sign bit
|
|
1966
|
+
for (let exp = 0; exp <= top; exp++) {
|
|
1967
|
+
const v = 1 << exp;
|
|
1968
|
+
if ((value & v) === v)
|
|
1969
|
+
items.push(v);
|
|
1964
1970
|
}
|
|
1971
|
+
return items;
|
|
1965
1972
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
/**
|
|
1969
|
-
* Returns `true` when `obj` is an instance of `Date`.
|
|
1970
|
-
* @param obj - The object to test.
|
|
1971
|
-
*/
|
|
1972
|
-
isDateInstance(obj) {
|
|
1973
|
-
return isDate(obj);
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Returns `true` when `date` represents a valid point in time.
|
|
1977
|
-
* @param date - The date to validate.
|
|
1978
|
-
*/
|
|
1979
|
-
isValid(date) {
|
|
1980
|
-
return isValid(date);
|
|
1973
|
+
else
|
|
1974
|
+
return [];
|
|
1981
1975
|
}
|
|
1976
|
+
/** Cache for resolved color luminance results. */
|
|
1977
|
+
static { this.colorLightCache = new Map(); }
|
|
1982
1978
|
/**
|
|
1983
|
-
*
|
|
1979
|
+
* Check if a color is light or dark
|
|
1980
|
+
* @param color : the color
|
|
1981
|
+
* @param minimumLuminance : the lumimance to consider
|
|
1982
|
+
* @returns true if the color is light
|
|
1984
1983
|
*/
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
*
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
{
|
|
2008
|
-
provide: DateAdapter,
|
|
2009
|
-
useClass: DateFnsAdapter,
|
|
2010
|
-
deps: [MAT_DATE_LOCALE],
|
|
2011
|
-
},
|
|
2012
|
-
{ provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS }
|
|
2013
|
-
]);
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
/**
|
|
2017
|
-
* Directive that moves browser focus to the host element after the first render cycle.
|
|
2018
|
-
* Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
|
|
2019
|
-
*/
|
|
2020
|
-
class AutoFocusDirective {
|
|
2021
|
-
constructor() {
|
|
2022
|
-
this.elementRef = inject(ElementRef);
|
|
2023
|
-
afterNextRender(() => {
|
|
2024
|
-
this.elementRef.nativeElement?.focus();
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2027
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2028
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
|
|
2029
|
-
}
|
|
2030
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, decorators: [{
|
|
2031
|
-
type: Directive,
|
|
2032
|
-
args: [{
|
|
2033
|
-
selector: '[autoFocus]',
|
|
2034
|
-
standalone: true
|
|
2035
|
-
}]
|
|
2036
|
-
}], ctorParameters: () => [] });
|
|
2037
|
-
|
|
2038
|
-
class FileInfo {
|
|
2039
|
-
isValid() {
|
|
2040
|
-
return this.valid;
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
class ValueModel {
|
|
2044
|
-
}
|
|
2045
|
-
class IDModel {
|
|
2046
|
-
}
|
|
2047
|
-
class GroupModel {
|
|
2048
|
-
}
|
|
2049
|
-
class DeleteModel extends GroupModel {
|
|
2050
|
-
}
|
|
2051
|
-
class RelationModel {
|
|
2052
|
-
}
|
|
2053
|
-
class UpdateRelationsModel {
|
|
2054
|
-
}
|
|
2055
|
-
class QueryModel {
|
|
2056
|
-
}
|
|
2057
|
-
class ImportModel {
|
|
2058
|
-
}
|
|
2059
|
-
class DateInterval {
|
|
2060
|
-
get fromAsDate() {
|
|
2061
|
-
if (this.from) {
|
|
2062
|
-
if (!(this.from instanceof Date)) {
|
|
2063
|
-
this.from = new Date(this.from);
|
|
2064
|
-
}
|
|
2065
|
-
if (this.from) {
|
|
2066
|
-
return new Date(this.from.getFullYear(), this.from.getMonth(), this.from.getDate(), 2, 0, 0);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
return undefined;
|
|
2070
|
-
}
|
|
2071
|
-
get toAsDate() {
|
|
2072
|
-
if (this.to) {
|
|
2073
|
-
if (!(this.to instanceof Date)) {
|
|
2074
|
-
this.to = new Date(this.to);
|
|
2075
|
-
}
|
|
2076
|
-
if (this.to) {
|
|
2077
|
-
return new Date(this.to.getFullYear(), this.to.getMonth(), this.to.getDate(), 2, 0, 0);
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
return undefined;
|
|
2081
|
-
}
|
|
2082
|
-
constructor(from, to) {
|
|
2083
|
-
this.from = from;
|
|
2084
|
-
this.to = to;
|
|
2085
|
-
}
|
|
2086
|
-
clear() {
|
|
2087
|
-
this.from = undefined;
|
|
2088
|
-
this.to = undefined;
|
|
1984
|
+
static isColorLight(color, minimumLuminance = 186) {
|
|
1985
|
+
if (!this.isBrowser())
|
|
1986
|
+
return true; // SSR fallback
|
|
1987
|
+
const cacheKey = `${color}|${minimumLuminance}`;
|
|
1988
|
+
const cached = this.colorLightCache.get(cacheKey);
|
|
1989
|
+
if (cached !== undefined)
|
|
1990
|
+
return cached;
|
|
1991
|
+
const tempDiv = document.createElement('div');
|
|
1992
|
+
tempDiv.style.color = color;
|
|
1993
|
+
document.body.appendChild(tempDiv);
|
|
1994
|
+
const rgb = getComputedStyle(tempDiv).color;
|
|
1995
|
+
document.body.removeChild(tempDiv);
|
|
1996
|
+
const result = rgb.match(/\d+/g);
|
|
1997
|
+
let light = true; // fallback
|
|
1998
|
+
if (result && result.length >= 3) {
|
|
1999
|
+
const r = parseInt(result[0], 10);
|
|
2000
|
+
const g = parseInt(result[1], 10);
|
|
2001
|
+
const b = parseInt(result[2], 10);
|
|
2002
|
+
light = (0.299 * r + 0.587 * g + 0.114 * b) > minimumLuminance;
|
|
2003
|
+
}
|
|
2004
|
+
this.colorLightCache.set(cacheKey, light);
|
|
2005
|
+
return light;
|
|
2089
2006
|
}
|
|
2090
2007
|
}
|
|
2091
2008
|
|
|
@@ -2097,7 +2014,7 @@ class DateInterval {
|
|
|
2097
2014
|
class DateIntervalChangeDirective {
|
|
2098
2015
|
constructor() {
|
|
2099
2016
|
/** The date interval model to update when the input value changes. */
|
|
2100
|
-
this.dateIntervalChange = input(new DateInterval(
|
|
2017
|
+
this.dateIntervalChange = input(new DateInterval(), /* @ts-ignore */
|
|
2101
2018
|
...(ngDevMode ? [{ debugName: "dateIntervalChange" }] : /* istanbul ignore next */ []));
|
|
2102
2019
|
/** When `true`, the directive updates the interval's end date; otherwise the start date. */
|
|
2103
2020
|
this.end = input(false, /* @ts-ignore */
|
|
@@ -2114,19 +2031,27 @@ class DateIntervalChangeDirective {
|
|
|
2114
2031
|
});
|
|
2115
2032
|
}
|
|
2116
2033
|
/**
|
|
2117
|
-
* Handles `
|
|
2118
|
-
*
|
|
2034
|
+
* Handles `keydown` events on the host element.
|
|
2035
|
+
* The space key must be blocked HERE: by the time `keyup` fires the character
|
|
2036
|
+
* has already been inserted into the input, so `preventDefault` on `keyup`
|
|
2037
|
+
* cannot stop it.
|
|
2119
2038
|
* @param e - The keyboard event emitted by the host input.
|
|
2120
2039
|
*/
|
|
2121
|
-
|
|
2040
|
+
onKeydown(e) {
|
|
2122
2041
|
if (e.key === ' ') {
|
|
2123
2042
|
e.preventDefault();
|
|
2124
2043
|
e.stopPropagation();
|
|
2125
2044
|
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Handles `keyup` events on the host element and forwards them to the debounce pipeline.
|
|
2048
|
+
* @param e - The keyboard event emitted by the host input.
|
|
2049
|
+
*/
|
|
2050
|
+
onKeyup(e) {
|
|
2126
2051
|
this.subject.next(e);
|
|
2127
2052
|
}
|
|
2128
2053
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateIntervalChangeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2129
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: DateIntervalChangeDirective, isStandalone: true, selector: "[dateIntervalChange]", inputs: { dateIntervalChange: { classPropertyName: "dateIntervalChange", publicName: "dateIntervalChange", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keyup": "onKeyup($event)" } }, ngImport: i0 }); }
|
|
2054
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: DateIntervalChangeDirective, isStandalone: true, selector: "[dateIntervalChange]", inputs: { dateIntervalChange: { classPropertyName: "dateIntervalChange", publicName: "dateIntervalChange", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onKeydown($event)", "keyup": "onKeyup($event)" } }, ngImport: i0 }); }
|
|
2130
2055
|
}
|
|
2131
2056
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateIntervalChangeDirective, decorators: [{
|
|
2132
2057
|
type: Directive,
|
|
@@ -2134,7 +2059,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2134
2059
|
selector: '[dateIntervalChange]',
|
|
2135
2060
|
standalone: true,
|
|
2136
2061
|
}]
|
|
2137
|
-
}], ctorParameters: () => [], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }],
|
|
2062
|
+
}], ctorParameters: () => [], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], onKeydown: [{
|
|
2063
|
+
type: HostListener,
|
|
2064
|
+
args: ['keydown', ['$event']]
|
|
2065
|
+
}], onKeyup: [{
|
|
2138
2066
|
type: HostListener,
|
|
2139
2067
|
args: ['keyup', ['$event']]
|
|
2140
2068
|
}] } });
|
|
@@ -2163,6 +2091,7 @@ class CopyClipboardDirective {
|
|
|
2163
2091
|
if (SystemUtils.isBrowser()) {
|
|
2164
2092
|
const listener = (clipEvent) => {
|
|
2165
2093
|
clipEvent.clipboardData?.setData('text/html', payload);
|
|
2094
|
+
clipEvent.clipboardData?.setData('text/plain', payload);
|
|
2166
2095
|
clipEvent.preventDefault();
|
|
2167
2096
|
this.copied.emit(payload);
|
|
2168
2097
|
};
|
|
@@ -2230,17 +2159,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2230
2159
|
/**
|
|
2231
2160
|
* Directive that validates that the host control's value equals the value of another control.
|
|
2232
2161
|
* Bind `[equals]="otherControl"`.
|
|
2162
|
+
*
|
|
2163
|
+
* The host control is re-validated whenever the OTHER control's value changes:
|
|
2164
|
+
* without this, typing a new password AFTER the confirmation field was filled
|
|
2165
|
+
* left the form incorrectly valid (the classic password/confirm bug).
|
|
2233
2166
|
*/
|
|
2234
2167
|
class EqualsValidatorDirective {
|
|
2235
2168
|
constructor() {
|
|
2236
2169
|
/** The control whose value must match the host control's value. */
|
|
2237
2170
|
this.equals = input(undefined, /* @ts-ignore */
|
|
2238
2171
|
...(ngDevMode ? [{ debugName: "equals" }] : /* istanbul ignore next */ []));
|
|
2172
|
+
// Re-subscribe whenever the [equals] binding points to a different control,
|
|
2173
|
+
// and trigger host re-validation on every change of the other control.
|
|
2174
|
+
effect(() => {
|
|
2175
|
+
const other = this.equals();
|
|
2176
|
+
this.subscription?.unsubscribe();
|
|
2177
|
+
this.subscription = other?.valueChanges.subscribe(() => this.onValidatorChange?.());
|
|
2178
|
+
});
|
|
2179
|
+
inject(DestroyRef).onDestroy(() => this.subscription?.unsubscribe());
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Registers the callback Angular invokes to re-run this validator.
|
|
2183
|
+
* @param fn - The revalidation callback provided by the forms API.
|
|
2184
|
+
*/
|
|
2185
|
+
registerOnValidatorChange(fn) {
|
|
2186
|
+
this.onValidatorChange = fn;
|
|
2239
2187
|
}
|
|
2240
2188
|
/**
|
|
2241
2189
|
* Validates that the host control value equals the bound control's value.
|
|
2242
2190
|
* Returns `null` (valid) when no control is bound.
|
|
2243
2191
|
* @param control - The form control to validate.
|
|
2192
|
+
* @returns `null` when valid, `{ equals: ... }` otherwise.
|
|
2244
2193
|
*/
|
|
2245
2194
|
validate(control) {
|
|
2246
2195
|
const eq = this.equals();
|
|
@@ -2270,7 +2219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2270
2219
|
],
|
|
2271
2220
|
standalone: true,
|
|
2272
2221
|
}]
|
|
2273
|
-
}], propDecorators: { equals: [{ type: i0.Input, args: [{ isSignal: true, alias: "equals", required: false }] }] } });
|
|
2222
|
+
}], ctorParameters: () => [], propDecorators: { equals: [{ type: i0.Input, args: [{ isSignal: true, alias: "equals", required: false }] }] } });
|
|
2274
2223
|
|
|
2275
2224
|
/**
|
|
2276
2225
|
* Directive that validates a file size against configurable minimum and maximum bounds.
|
|
@@ -2454,18 +2403,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2454
2403
|
/**
|
|
2455
2404
|
* Directive that validates that the host control's value is different from another control's value.
|
|
2456
2405
|
* Bind `[notEqual]="otherControl"`.
|
|
2406
|
+
*
|
|
2407
|
+
* The host control is re-validated whenever the OTHER control's value changes,
|
|
2408
|
+
* so editing either field keeps both error states consistent.
|
|
2457
2409
|
*/
|
|
2458
2410
|
class NotEqualValidatorDirective {
|
|
2459
2411
|
constructor() {
|
|
2460
2412
|
/** The control whose value must differ from the host control's value. */
|
|
2461
2413
|
this.notEqual = input(undefined, /* @ts-ignore */
|
|
2462
2414
|
...(ngDevMode ? [{ debugName: "notEqual" }] : /* istanbul ignore next */ []));
|
|
2415
|
+
effect(() => {
|
|
2416
|
+
const other = this.notEqual();
|
|
2417
|
+
this.subscription?.unsubscribe();
|
|
2418
|
+
this.subscription = other?.valueChanges.subscribe(() => this.onValidatorChange?.());
|
|
2419
|
+
});
|
|
2420
|
+
inject(DestroyRef).onDestroy(() => this.subscription?.unsubscribe());
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Registers the callback Angular invokes to re-run this validator.
|
|
2424
|
+
* @param fn - The revalidation callback provided by the forms API.
|
|
2425
|
+
*/
|
|
2426
|
+
registerOnValidatorChange(fn) {
|
|
2427
|
+
this.onValidatorChange = fn;
|
|
2463
2428
|
}
|
|
2464
2429
|
/**
|
|
2465
2430
|
* Validates that the host control value is not equal to the bound control's value.
|
|
2466
2431
|
* Also clears the `notequal` error on the other control when the host becomes valid.
|
|
2467
2432
|
* Returns `null` (valid) when no control is bound.
|
|
2468
2433
|
* @param control - The form control to validate.
|
|
2434
|
+
* @returns `null` when valid, `{ notequal: true }` otherwise.
|
|
2469
2435
|
*/
|
|
2470
2436
|
validate(control) {
|
|
2471
2437
|
const notEqual = this.notEqual();
|
|
@@ -2477,7 +2443,8 @@ class NotEqualValidatorDirective {
|
|
|
2477
2443
|
control.markAsTouched();
|
|
2478
2444
|
}
|
|
2479
2445
|
else if (notEqual.hasError('notequal')) {
|
|
2480
|
-
|
|
2446
|
+
const { notequal: _removed, ...rest } = notEqual.errors ?? {};
|
|
2447
|
+
notEqual.setErrors(Object.keys(rest).length > 0 ? rest : null);
|
|
2481
2448
|
notEqual.updateValueAndValidity({ onlySelf: true, emitEvent: false });
|
|
2482
2449
|
notEqual.markAsTouched();
|
|
2483
2450
|
notEqual.markAsDirty();
|
|
@@ -2506,7 +2473,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2506
2473
|
],
|
|
2507
2474
|
standalone: true,
|
|
2508
2475
|
}]
|
|
2509
|
-
}], propDecorators: { notEqual: [{ type: i0.Input, args: [{ isSignal: true, alias: "notEqual", required: false }] }] } });
|
|
2476
|
+
}], ctorParameters: () => [], propDecorators: { notEqual: [{ type: i0.Input, args: [{ isSignal: true, alias: "notEqual", required: false }] }] } });
|
|
2510
2477
|
|
|
2511
2478
|
/**
|
|
2512
2479
|
* Directive that validates that a control value is not a future date.
|
|
@@ -2522,8 +2489,11 @@ class NotFutureValidatorDirective {
|
|
|
2522
2489
|
const input = control.value;
|
|
2523
2490
|
if (!input || input.length === 0)
|
|
2524
2491
|
return null;
|
|
2492
|
+
const parsed = SystemUtils.parseDate(input);
|
|
2493
|
+
if (!parsed)
|
|
2494
|
+
return { notFuture: "Non valido." };
|
|
2525
2495
|
const today = endOfDay(new Date());
|
|
2526
|
-
const d = endOfDay(
|
|
2496
|
+
const d = endOfDay(parsed);
|
|
2527
2497
|
return d <= today ? null : { notFuture: "Non valido." };
|
|
2528
2498
|
}
|
|
2529
2499
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotFutureValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -2635,7 +2605,10 @@ class SqlDateValidatorDirective {
|
|
|
2635
2605
|
const input = control.value;
|
|
2636
2606
|
if (!input || input.length === 0)
|
|
2637
2607
|
return null;
|
|
2638
|
-
const
|
|
2608
|
+
const parsed = SystemUtils.parseDate(input);
|
|
2609
|
+
if (!parsed)
|
|
2610
|
+
return { sqlDate: "Non valido." };
|
|
2611
|
+
const d = endOfDay(parsed);
|
|
2639
2612
|
return d.getFullYear() > 1750 ? null : { sqlDate: "Non valido." };
|
|
2640
2613
|
}
|
|
2641
2614
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SqlDateValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -2681,13 +2654,13 @@ class TimeValidatorDirective {
|
|
|
2681
2654
|
const p = value.split(':');
|
|
2682
2655
|
if (p.length !== 2)
|
|
2683
2656
|
return -1;
|
|
2684
|
-
const hh = parseInt(p[0]);
|
|
2685
|
-
if (hh < 0 || hh > 23)
|
|
2657
|
+
const hh = parseInt(p[0], 10);
|
|
2658
|
+
if (isNaN(hh) || hh < 0 || hh > 23)
|
|
2686
2659
|
return -1;
|
|
2687
|
-
const mm = parseInt(p[1]);
|
|
2688
|
-
if (mm < 0 || mm > 59)
|
|
2660
|
+
const mm = parseInt(p[1], 10);
|
|
2661
|
+
if (isNaN(mm) || mm < 0 || mm > 59)
|
|
2689
2662
|
return -1;
|
|
2690
|
-
return
|
|
2663
|
+
return hh * 100 + mm;
|
|
2691
2664
|
}
|
|
2692
2665
|
/**
|
|
2693
2666
|
* Validates that the control value is a valid time string and, when slots are configured,
|
|
@@ -2735,168 +2708,435 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2735
2708
|
],
|
|
2736
2709
|
standalone: true,
|
|
2737
2710
|
}]
|
|
2738
|
-
}], propDecorators: { slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }] } });
|
|
2711
|
+
}], propDecorators: { slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }] } });
|
|
2712
|
+
|
|
2713
|
+
/**
|
|
2714
|
+
* Directive that validates a control value as a well-formed URL.
|
|
2715
|
+
* Apply `url` to a text input that expects a URL.
|
|
2716
|
+
*/
|
|
2717
|
+
class UrlValidatorDirective {
|
|
2718
|
+
/**
|
|
2719
|
+
* Validates that the control value is a well-formed URL.
|
|
2720
|
+
* Returns `null` (valid) when the control is empty.
|
|
2721
|
+
* @param control - The form control to validate.
|
|
2722
|
+
*/
|
|
2723
|
+
validate(control) {
|
|
2724
|
+
const input = control.value;
|
|
2725
|
+
if (!input || input.length === 0)
|
|
2726
|
+
return null;
|
|
2727
|
+
return SystemUtils.parseUrl(input) ? null : { url: "Non valido." };
|
|
2728
|
+
}
|
|
2729
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: UrlValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2730
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: UrlValidatorDirective, isStandalone: true, selector: "[url]", providers: [
|
|
2731
|
+
{
|
|
2732
|
+
provide: NG_VALIDATORS,
|
|
2733
|
+
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2734
|
+
multi: true,
|
|
2735
|
+
},
|
|
2736
|
+
], ngImport: i0 }); }
|
|
2737
|
+
}
|
|
2738
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: UrlValidatorDirective, decorators: [{
|
|
2739
|
+
type: Directive,
|
|
2740
|
+
args: [{
|
|
2741
|
+
selector: "[url]",
|
|
2742
|
+
providers: [
|
|
2743
|
+
{
|
|
2744
|
+
provide: NG_VALIDATORS,
|
|
2745
|
+
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2746
|
+
multi: true,
|
|
2747
|
+
},
|
|
2748
|
+
],
|
|
2749
|
+
standalone: true,
|
|
2750
|
+
}]
|
|
2751
|
+
}] });
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* Directive that validates a control using the host object's `isValid()` method
|
|
2755
|
+
* or a boolean expression passed via `[validIf]`.
|
|
2756
|
+
*/
|
|
2757
|
+
class ValidIfDirective {
|
|
2758
|
+
constructor() {
|
|
2759
|
+
/** When `true`, the control is considered valid regardless of the bound value. */
|
|
2760
|
+
this.validIf = input(false, /* @ts-ignore */
|
|
2761
|
+
...(ngDevMode ? [{ debugName: "validIf" }] : /* istanbul ignore next */ []));
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Validates the control value against a boolean flag or the value's own `isValid()` method.
|
|
2765
|
+
* @param control - The form control to validate.
|
|
2766
|
+
*/
|
|
2767
|
+
validate(control) {
|
|
2768
|
+
let isValid = false;
|
|
2769
|
+
const c = control.value ? control.value : null;
|
|
2770
|
+
if (!c) {
|
|
2771
|
+
isValid = this.validIf() === true;
|
|
2772
|
+
}
|
|
2773
|
+
else {
|
|
2774
|
+
try {
|
|
2775
|
+
isValid = c.isValid();
|
|
2776
|
+
}
|
|
2777
|
+
catch { }
|
|
2778
|
+
}
|
|
2779
|
+
return isValid ? null : { validIf: "Non valido." };
|
|
2780
|
+
}
|
|
2781
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidIfDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2782
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: ValidIfDirective, isStandalone: true, selector: "[validIf]", inputs: { validIf: { classPropertyName: "validIf", publicName: "validIf", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
2783
|
+
{
|
|
2784
|
+
provide: NG_VALIDATORS,
|
|
2785
|
+
useExisting: forwardRef(() => ValidIfDirective),
|
|
2786
|
+
multi: true,
|
|
2787
|
+
},
|
|
2788
|
+
], ngImport: i0 }); }
|
|
2789
|
+
}
|
|
2790
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidIfDirective, decorators: [{
|
|
2791
|
+
type: Directive,
|
|
2792
|
+
args: [{
|
|
2793
|
+
selector: "[validIf]",
|
|
2794
|
+
providers: [
|
|
2795
|
+
{
|
|
2796
|
+
provide: NG_VALIDATORS,
|
|
2797
|
+
useExisting: forwardRef(() => ValidIfDirective),
|
|
2798
|
+
multi: true,
|
|
2799
|
+
},
|
|
2800
|
+
],
|
|
2801
|
+
standalone: true,
|
|
2802
|
+
}]
|
|
2803
|
+
}], propDecorators: { validIf: [{ type: i0.Input, args: [{ isSignal: true, alias: "validIf", required: false }] }] } });
|
|
2804
|
+
|
|
2805
|
+
/**
|
|
2806
|
+
* Directive that delegates validation to an externally provided validator function.
|
|
2807
|
+
* Bind `[validator]="myFn"` where `myFn` is `(c: AbstractControl) => ValidationErrors | null`.
|
|
2808
|
+
*/
|
|
2809
|
+
class ValidatorDirective {
|
|
2810
|
+
constructor() {
|
|
2811
|
+
/** The custom validator function to apply. */
|
|
2812
|
+
this.validator = input(undefined, /* @ts-ignore */
|
|
2813
|
+
...(ngDevMode ? [{ debugName: "validator" }] : /* istanbul ignore next */ []));
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Invokes the provided validator function against the given control.
|
|
2817
|
+
* Returns `null` (valid) when no function is bound.
|
|
2818
|
+
* @param control - The form control to validate.
|
|
2819
|
+
*/
|
|
2820
|
+
validate(control) {
|
|
2821
|
+
const fn = this.validator();
|
|
2822
|
+
return fn ? fn(control) : null;
|
|
2823
|
+
}
|
|
2824
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2825
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: ValidatorDirective, isStandalone: true, selector: "[validator]", inputs: { validator: { classPropertyName: "validator", publicName: "validator", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true }], ngImport: i0 }); }
|
|
2826
|
+
}
|
|
2827
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidatorDirective, decorators: [{
|
|
2828
|
+
type: Directive,
|
|
2829
|
+
args: [{
|
|
2830
|
+
selector: '[validator]',
|
|
2831
|
+
providers: [{ provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true }],
|
|
2832
|
+
standalone: true,
|
|
2833
|
+
}]
|
|
2834
|
+
}], propDecorators: { validator: [{ type: i0.Input, args: [{ isSignal: true, alias: "validator", required: false }] }] } });
|
|
2835
|
+
|
|
2836
|
+
/**
|
|
2837
|
+
* Pipe that converts plain-text newlines (`\r\n`, `\r`, `\n`) to HTML `<br>` tags
|
|
2838
|
+
* and marks the result as trusted HTML so Angular does not escape it.
|
|
2839
|
+
*
|
|
2840
|
+
* Usage: `{{ text | formatHtml }}`
|
|
2841
|
+
*/
|
|
2842
|
+
class FormatHtmlPipe {
|
|
2843
|
+
constructor() {
|
|
2844
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Transforms a plain-text string into sanitized HTML by replacing newline characters
|
|
2848
|
+
* with `<br>` tags.
|
|
2849
|
+
* @param value - The input string to transform. Treated as an empty string when `undefined`.
|
|
2850
|
+
* @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
|
|
2851
|
+
*/
|
|
2852
|
+
transform(value) {
|
|
2853
|
+
const escaped = (value ?? '')
|
|
2854
|
+
.replaceAll('&', '&')
|
|
2855
|
+
.replaceAll('<', '<')
|
|
2856
|
+
.replaceAll('>', '>')
|
|
2857
|
+
.replaceAll('"', '"');
|
|
2858
|
+
return this.sanitizer.bypassSecurityTrustHtml(escaped.replaceAll(/(?:\r\n|\r|\n)/g, '<br>'));
|
|
2859
|
+
}
|
|
2860
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2861
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, isStandalone: true, name: "formatHtml" }); }
|
|
2862
|
+
}
|
|
2863
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, decorators: [{
|
|
2864
|
+
type: Pipe,
|
|
2865
|
+
args: [{
|
|
2866
|
+
name: 'formatHtml',
|
|
2867
|
+
standalone: true
|
|
2868
|
+
}]
|
|
2869
|
+
}] });
|
|
2870
|
+
|
|
2871
|
+
/**
|
|
2872
|
+
* Pipe that converts a Markdown string to sanitized HTML using `SystemUtils.markdownToHtml`.
|
|
2873
|
+
*
|
|
2874
|
+
* Usage: `{{ text | formatMarkdown }}`
|
|
2875
|
+
*/
|
|
2876
|
+
class FormatMarkdownPipe {
|
|
2877
|
+
constructor() {
|
|
2878
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Transforms a Markdown string into sanitized HTML.
|
|
2882
|
+
* @param value - The Markdown input to convert. Treated as an empty string when `undefined`.
|
|
2883
|
+
* @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
|
|
2884
|
+
*/
|
|
2885
|
+
transform(value) {
|
|
2886
|
+
return this.sanitizer.bypassSecurityTrustHtml(SystemUtils.markdownToHtml(value ?? ''));
|
|
2887
|
+
}
|
|
2888
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2889
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, isStandalone: true, name: "formatMarkdown" }); }
|
|
2890
|
+
}
|
|
2891
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, decorators: [{
|
|
2892
|
+
type: Pipe,
|
|
2893
|
+
args: [{
|
|
2894
|
+
name: 'formatMarkdown',
|
|
2895
|
+
standalone: true
|
|
2896
|
+
}]
|
|
2897
|
+
}] });
|
|
2898
|
+
|
|
2899
|
+
/**
|
|
2900
|
+
* General-purpose formatting pipe that converts a raw value to a locale-aware string
|
|
2901
|
+
* based on the specified format type.
|
|
2902
|
+
*
|
|
2903
|
+
* Supported types: `'date'` / `'D'`, `'currency'` / `'C'`, `'number'` / `'N'`,
|
|
2904
|
+
* `'number0'` / `'N0'`, `'percentage'` / `'P'`.
|
|
2905
|
+
*
|
|
2906
|
+
* Usage: `{{ value | format:'currency' }}`
|
|
2907
|
+
*/
|
|
2908
|
+
class FormatPipe {
|
|
2909
|
+
/** Shared formatters: building an `Intl.NumberFormat` per call is expensive. */
|
|
2910
|
+
static { this.currencyFormat = new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }); }
|
|
2911
|
+
static { this.numberFormat = new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 2 }); }
|
|
2912
|
+
static { this.number0Format = new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }); }
|
|
2913
|
+
static { this.percentFormat = new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }); }
|
|
2914
|
+
/**
|
|
2915
|
+
* Formats a value according to the specified type and optional pattern.
|
|
2916
|
+
* Returns `undefined` when the value is `null` or `undefined`, or when the type is unrecognised.
|
|
2917
|
+
* @param value - The raw value to format.
|
|
2918
|
+
* @param type - The format type identifier (default: `'date'`).
|
|
2919
|
+
* @param pattern - The date pattern used when `type` is `'date'` (default: `'dd/MM/yyyy'`).
|
|
2920
|
+
* @returns A formatted string, or `undefined` when the value cannot be formatted.
|
|
2921
|
+
*/
|
|
2922
|
+
transform(value, type = 'date', pattern = 'dd/MM/yyyy') {
|
|
2923
|
+
if (value === undefined || value === null)
|
|
2924
|
+
return undefined;
|
|
2925
|
+
switch (type) {
|
|
2926
|
+
case 'D':
|
|
2927
|
+
case 'date': {
|
|
2928
|
+
const d = SystemUtils.parseDate(value, it);
|
|
2929
|
+
if (d)
|
|
2930
|
+
return format(d, pattern, { locale: it });
|
|
2931
|
+
break;
|
|
2932
|
+
}
|
|
2933
|
+
case 'C':
|
|
2934
|
+
case 'currency':
|
|
2935
|
+
return FormatPipe.currencyFormat.format(value);
|
|
2936
|
+
case 'N':
|
|
2937
|
+
case 'number':
|
|
2938
|
+
return FormatPipe.numberFormat.format(value);
|
|
2939
|
+
case 'N0':
|
|
2940
|
+
case 'number0':
|
|
2941
|
+
return FormatPipe.number0Format.format(value);
|
|
2942
|
+
case 'P':
|
|
2943
|
+
case 'percentage':
|
|
2944
|
+
return FormatPipe.percentFormat.format(value);
|
|
2945
|
+
}
|
|
2946
|
+
return undefined;
|
|
2947
|
+
}
|
|
2948
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2949
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, isStandalone: true, name: "format" }); }
|
|
2950
|
+
}
|
|
2951
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, decorators: [{
|
|
2952
|
+
type: Pipe,
|
|
2953
|
+
args: [{
|
|
2954
|
+
name: 'format',
|
|
2955
|
+
standalone: true
|
|
2956
|
+
}]
|
|
2957
|
+
}] });
|
|
2739
2958
|
|
|
2740
2959
|
/**
|
|
2741
|
-
*
|
|
2742
|
-
*
|
|
2960
|
+
* Pipe that applies a global regex replacement on a string and returns the result
|
|
2961
|
+
* as sanitized HTML. When `regexValue` is `'\n'` and no `replaceValue` is given,
|
|
2962
|
+
* newlines are replaced with `<br>` tags.
|
|
2963
|
+
*
|
|
2964
|
+
* Usage: `{{ text | replace:'\n':'' }}`
|
|
2743
2965
|
*/
|
|
2744
|
-
class
|
|
2966
|
+
class ReplacePipe {
|
|
2967
|
+
constructor() {
|
|
2968
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2969
|
+
}
|
|
2745
2970
|
/**
|
|
2746
|
-
*
|
|
2747
|
-
* Returns `
|
|
2748
|
-
* @param
|
|
2971
|
+
* Replaces all occurrences of `regexValue` in `value` with `replaceValue`.
|
|
2972
|
+
* Returns `undefined` when `value` is empty or `undefined`.
|
|
2973
|
+
* @param value - The source string to process.
|
|
2974
|
+
* @param regexValue - The regex pattern string to match (applied with the global flag).
|
|
2975
|
+
* @param replaceValue - The replacement string. Defaults to `'<br>'` when `regexValue` is `'\n'` and this is falsy.
|
|
2976
|
+
* @returns A `SafeHtml` value with all matches replaced, or `undefined` when the input is empty.
|
|
2749
2977
|
*/
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
return
|
|
2978
|
+
transform(value, regexValue, replaceValue) {
|
|
2979
|
+
if (!value)
|
|
2980
|
+
return undefined;
|
|
2981
|
+
const replacement = (regexValue === '\n' && !replaceValue) ? '<br>' : (replaceValue ?? '');
|
|
2982
|
+
return this.sanitizer.bypassSecurityTrustHtml(value.replace(new RegExp(regexValue, 'g'), replacement));
|
|
2755
2983
|
}
|
|
2756
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2757
|
-
static { this.ɵ
|
|
2758
|
-
{
|
|
2759
|
-
provide: NG_VALIDATORS,
|
|
2760
|
-
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2761
|
-
multi: true,
|
|
2762
|
-
},
|
|
2763
|
-
], ngImport: i0 }); }
|
|
2984
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2985
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" }); }
|
|
2764
2986
|
}
|
|
2765
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2766
|
-
type:
|
|
2987
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, decorators: [{
|
|
2988
|
+
type: Pipe,
|
|
2767
2989
|
args: [{
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
{
|
|
2771
|
-
provide: NG_VALIDATORS,
|
|
2772
|
-
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2773
|
-
multi: true,
|
|
2774
|
-
},
|
|
2775
|
-
],
|
|
2776
|
-
standalone: true,
|
|
2990
|
+
name: 'replace',
|
|
2991
|
+
standalone: true
|
|
2777
2992
|
}]
|
|
2778
2993
|
}] });
|
|
2779
2994
|
|
|
2780
2995
|
/**
|
|
2781
|
-
*
|
|
2782
|
-
*
|
|
2996
|
+
* Pipe that marks an HTML string as trusted so Angular does not escape it when
|
|
2997
|
+
* bound via `[innerHTML]`.
|
|
2998
|
+
*
|
|
2999
|
+
* Usage: `<div [innerHTML]="html | safeHtml"></div>`
|
|
2783
3000
|
*/
|
|
2784
|
-
class
|
|
3001
|
+
class SafeHtmlPipe {
|
|
2785
3002
|
constructor() {
|
|
2786
|
-
|
|
2787
|
-
this.validIf = input(false, /* @ts-ignore */
|
|
2788
|
-
...(ngDevMode ? [{ debugName: "validIf" }] : /* istanbul ignore next */ []));
|
|
3003
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2789
3004
|
}
|
|
2790
3005
|
/**
|
|
2791
|
-
*
|
|
2792
|
-
* @param
|
|
3006
|
+
* Bypasses Angular's HTML sanitization and returns a `SafeHtml` instance.
|
|
3007
|
+
* @param value - The raw HTML string to trust. Treated as an empty string when `undefined`.
|
|
3008
|
+
* @returns A `SafeHtml` value that can be bound to `[innerHTML]` without escaping.
|
|
2793
3009
|
*/
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const c = control.value ? control.value : null;
|
|
2797
|
-
if (!c) {
|
|
2798
|
-
isValid = this.validIf() === true;
|
|
2799
|
-
}
|
|
2800
|
-
else {
|
|
2801
|
-
try {
|
|
2802
|
-
isValid = c.isValid();
|
|
2803
|
-
}
|
|
2804
|
-
catch { }
|
|
2805
|
-
}
|
|
2806
|
-
return isValid ? null : { validIf: "Non valido." };
|
|
3010
|
+
transform(value) {
|
|
3011
|
+
return this.sanitizer.bypassSecurityTrustHtml(value ?? '');
|
|
2807
3012
|
}
|
|
2808
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2809
|
-
static { this.ɵ
|
|
2810
|
-
{
|
|
2811
|
-
provide: NG_VALIDATORS,
|
|
2812
|
-
useExisting: forwardRef(() => ValidIfDirective),
|
|
2813
|
-
multi: true,
|
|
2814
|
-
},
|
|
2815
|
-
], ngImport: i0 }); }
|
|
3013
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3014
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, isStandalone: true, name: "safeHtml" }); }
|
|
2816
3015
|
}
|
|
2817
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2818
|
-
type:
|
|
3016
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, decorators: [{
|
|
3017
|
+
type: Pipe,
|
|
2819
3018
|
args: [{
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
{
|
|
2823
|
-
provide: NG_VALIDATORS,
|
|
2824
|
-
useExisting: forwardRef(() => ValidIfDirective),
|
|
2825
|
-
multi: true,
|
|
2826
|
-
},
|
|
2827
|
-
],
|
|
2828
|
-
standalone: true,
|
|
3019
|
+
name: 'safeHtml',
|
|
3020
|
+
standalone: true
|
|
2829
3021
|
}]
|
|
2830
|
-
}]
|
|
3022
|
+
}] });
|
|
2831
3023
|
|
|
2832
3024
|
/**
|
|
2833
|
-
*
|
|
2834
|
-
*
|
|
3025
|
+
* Pipe that marks a URL string as a trusted resource URL so Angular does not block it
|
|
3026
|
+
* when bound to attributes such as `[src]` or `[href]` on iframes, objects, or embeds.
|
|
3027
|
+
*
|
|
3028
|
+
* Usage: `<iframe [src]="url | safeUrl"></iframe>`
|
|
2835
3029
|
*/
|
|
2836
|
-
class
|
|
3030
|
+
class SafeUrlPipe {
|
|
2837
3031
|
constructor() {
|
|
2838
|
-
|
|
2839
|
-
this.validator = input(undefined, /* @ts-ignore */
|
|
2840
|
-
...(ngDevMode ? [{ debugName: "validator" }] : /* istanbul ignore next */ []));
|
|
3032
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2841
3033
|
}
|
|
2842
3034
|
/**
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2845
|
-
* @
|
|
3035
|
+
* Bypasses Angular's resource-URL sanitization and returns a `SafeResourceUrl` instance.
|
|
3036
|
+
* @param value - The URL string to trust. Treated as an empty string when `undefined`.
|
|
3037
|
+
* @returns A `SafeResourceUrl` that can be bound to resource URL attributes without blocking.
|
|
2846
3038
|
*/
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
return fn ? fn(control) : null;
|
|
3039
|
+
transform(value) {
|
|
3040
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(value ?? '');
|
|
2850
3041
|
}
|
|
2851
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2852
|
-
static { this.ɵ
|
|
3042
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3043
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, isStandalone: true, name: "safeUrl" }); }
|
|
2853
3044
|
}
|
|
2854
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2855
|
-
type:
|
|
3045
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, decorators: [{
|
|
3046
|
+
type: Pipe,
|
|
2856
3047
|
args: [{
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
standalone: true,
|
|
3048
|
+
name: 'safeUrl',
|
|
3049
|
+
standalone: true
|
|
2860
3050
|
}]
|
|
2861
|
-
}]
|
|
3051
|
+
}] });
|
|
2862
3052
|
|
|
2863
3053
|
/**
|
|
2864
|
-
*
|
|
3054
|
+
* Impure pipe that filters an array using a caller-provided predicate function.
|
|
3055
|
+
* Because the pipe is impure it re-evaluates on every change-detection cycle,
|
|
3056
|
+
* which is necessary when the predicate's captured state changes.
|
|
2865
3057
|
*
|
|
2866
|
-
* Usage:
|
|
3058
|
+
* Usage: `*ngFor="let item of items | callback:myFilter"`
|
|
2867
3059
|
*/
|
|
2868
|
-
class
|
|
2869
|
-
constructor() {
|
|
2870
|
-
this.sanitizer = inject(DomSanitizer);
|
|
2871
|
-
}
|
|
3060
|
+
class SearchCallbackPipe {
|
|
2872
3061
|
/**
|
|
2873
|
-
*
|
|
2874
|
-
*
|
|
2875
|
-
* @
|
|
3062
|
+
* Filters `items` by applying `callback` to each element.
|
|
3063
|
+
* Returns the original array unchanged when either argument is falsy.
|
|
3064
|
+
* @param items - The source array to filter. May be `undefined`.
|
|
3065
|
+
* @param callback - A predicate function that returns `true` for items to keep.
|
|
3066
|
+
* @returns A new filtered array, the original array when no callback is provided,
|
|
3067
|
+
* or `undefined` when `items` is `undefined`.
|
|
2876
3068
|
*/
|
|
2877
|
-
transform(
|
|
2878
|
-
|
|
3069
|
+
transform(items, callback) {
|
|
3070
|
+
if (!items || !callback)
|
|
3071
|
+
return items;
|
|
3072
|
+
return items.filter(item => callback(item));
|
|
2879
3073
|
}
|
|
2880
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2881
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type:
|
|
3074
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3075
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, isStandalone: true, name: "callback", pure: false }); }
|
|
2882
3076
|
}
|
|
2883
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
3077
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, decorators: [{
|
|
2884
3078
|
type: Pipe,
|
|
2885
3079
|
args: [{
|
|
2886
|
-
name: '
|
|
3080
|
+
name: 'callback',
|
|
3081
|
+
pure: false,
|
|
2887
3082
|
standalone: true
|
|
2888
3083
|
}]
|
|
2889
3084
|
}] });
|
|
2890
3085
|
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3086
|
+
/**
|
|
3087
|
+
* Pure pipe that filters an array of searchable items against a text query.
|
|
3088
|
+
* Note: being pure, it re-runs only when the array REFERENCE or the query
|
|
3089
|
+
* changes; mutate-in-place updates of the array are not detected.
|
|
3090
|
+
*
|
|
3091
|
+
* Each item is matched either via its `searchBag.name` property (when present)
|
|
3092
|
+
* or by converting the item itself to a lowercase string. The optional `metadata`
|
|
3093
|
+
* argument is updated in-place with the total item count and the filtered count,
|
|
3094
|
+
* making it usable in the template alongside `*ngFor`.
|
|
3095
|
+
*
|
|
3096
|
+
* Usage:
|
|
3097
|
+
* ```html
|
|
3098
|
+
* <div *ngFor="let item of items | search:filterText:meta">...</div>
|
|
3099
|
+
* <div>Showing {{ meta.count }} of {{ meta.total }}</div>
|
|
3100
|
+
* ```
|
|
3101
|
+
*/
|
|
3102
|
+
class SearchFilterPipe {
|
|
3103
|
+
/**
|
|
3104
|
+
* Filters `items` by performing a case-insensitive substring match against `value`.
|
|
3105
|
+
* When `items` or `value` is falsy the original array is returned unfiltered.
|
|
3106
|
+
* @param items - The source array to filter. May be `undefined`.
|
|
3107
|
+
* @param value - The search text to match against each item. May be `undefined`.
|
|
3108
|
+
* @param metadata - Optional object that is updated with `total` and `count` after filtering.
|
|
3109
|
+
* @returns The filtered array, the original array when no filter text is given,
|
|
3110
|
+
* or `undefined` when `items` is `undefined`.
|
|
3111
|
+
*/
|
|
3112
|
+
transform(items, value, metadata) {
|
|
3113
|
+
metadata ??= { total: 0, count: 0 };
|
|
3114
|
+
if (!items || !value) {
|
|
3115
|
+
metadata.total = items?.length ?? 0;
|
|
3116
|
+
metadata.count = metadata.total;
|
|
3117
|
+
return items;
|
|
3118
|
+
}
|
|
3119
|
+
const query = value.toLowerCase();
|
|
3120
|
+
const result = items.filter(item => {
|
|
3121
|
+
if (!item)
|
|
3122
|
+
return false;
|
|
3123
|
+
const text = item.searchBag?.name ?? (typeof item === 'string' ? item : undefined);
|
|
3124
|
+
return text?.toLowerCase().includes(query) ?? false;
|
|
3125
|
+
});
|
|
3126
|
+
metadata.total = items.length;
|
|
3127
|
+
metadata.count = result.length;
|
|
3128
|
+
return result;
|
|
3129
|
+
}
|
|
3130
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3131
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, isStandalone: true, name: "search" }); }
|
|
3132
|
+
}
|
|
3133
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, decorators: [{
|
|
3134
|
+
type: Pipe,
|
|
3135
|
+
args: [{
|
|
3136
|
+
name: 'search',
|
|
3137
|
+
standalone: true
|
|
3138
|
+
}]
|
|
3139
|
+
}] });
|
|
2900
3140
|
|
|
2901
3141
|
const UtilsMessages = {
|
|
2902
3142
|
/**
|
|
@@ -2927,20 +3167,32 @@ class SelectableModel {
|
|
|
2927
3167
|
}
|
|
2928
3168
|
/**
|
|
2929
3169
|
* @param allowMultiSelect - When `true` (default), multiple items can be selected simultaneously.
|
|
2930
|
-
* @param lookupFieldName - Name of the
|
|
3170
|
+
* @param lookupFieldName - Name of the field used as the unique key when searching the internal list
|
|
3171
|
+
* (default: `'id'`). Keys of `T` are autocompleted; plain strings remain accepted.
|
|
2931
3172
|
*/
|
|
2932
3173
|
constructor(allowMultiSelect = true, lookupFieldName = 'id') {
|
|
2933
3174
|
/**
|
|
2934
3175
|
* Emits whenever the selection changes.
|
|
2935
3176
|
* Carries the affected item, or `undefined` when the whole selection is cleared.
|
|
3177
|
+
*
|
|
3178
|
+
* Note: kept as `EventEmitter` (instead of a plain RxJS `Subject`) for
|
|
3179
|
+
* backward compatibility with existing consumers; `EventEmitter` extends
|
|
3180
|
+
* `Subject`, so `.subscribe()`, `pipe(...)` and `takeUntilDestroyed` all
|
|
3181
|
+
* work as expected.
|
|
2936
3182
|
*/
|
|
2937
3183
|
this.changed = new EventEmitter();
|
|
2938
3184
|
this._all = signal([], /* @ts-ignore */
|
|
2939
3185
|
...(ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []));
|
|
2940
|
-
/** Signal that is `true` when at least one item is
|
|
3186
|
+
/** Signal that is `true` when at least one item is tracked in the internal list. */
|
|
2941
3187
|
this.hasValue = computed(() => this._all().length > 0, /* @ts-ignore */
|
|
2942
3188
|
...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
|
|
2943
|
-
|
|
3189
|
+
/** Signal with the number of items currently tracked in the internal list. */
|
|
3190
|
+
this.count = computed(() => this._all().length, /* @ts-ignore */
|
|
3191
|
+
...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
|
|
3192
|
+
this._current = new SelectionModel(allowMultiSelect, [], true,
|
|
3193
|
+
// Compare by lookup key so different instances of the same logical item
|
|
3194
|
+
// (e.g. after a data reload) are treated as the same selection entry.
|
|
3195
|
+
(a, b) => a[lookupFieldName] === b[lookupFieldName]);
|
|
2944
3196
|
this._lookupFieldName = lookupFieldName;
|
|
2945
3197
|
}
|
|
2946
3198
|
/**
|
|
@@ -3071,6 +3323,27 @@ class BroadcastChannelManager {
|
|
|
3071
3323
|
get currentBus() {
|
|
3072
3324
|
return this.channel?.name;
|
|
3073
3325
|
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Observable that emits EVERY message bag received on the channel,
|
|
3328
|
+
* regardless of `messageId`. Completed by {@link dispose}.
|
|
3329
|
+
* Prefer {@link observe} to listen for a single message type.
|
|
3330
|
+
*/
|
|
3331
|
+
get messages() {
|
|
3332
|
+
return this.subject.asObservable();
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Returns a typed Observable that emits only the messages with the given `messageId`.
|
|
3336
|
+
*
|
|
3337
|
+
* Unlike {@link subscribe} (one callback per id, replaced on re-registration),
|
|
3338
|
+
* this supports any number of concurrent subscribers and integrates with the
|
|
3339
|
+
* RxJS lifecycle (`takeUntilDestroyed`, `async` pipe, ...). Completed by {@link dispose}.
|
|
3340
|
+
*
|
|
3341
|
+
* @param messageId - The message type identifier to listen for.
|
|
3342
|
+
* @returns An Observable of the matching typed message bags.
|
|
3343
|
+
*/
|
|
3344
|
+
observe(messageId) {
|
|
3345
|
+
return this.subject.pipe(filter(bag => bag.messageId === messageId), map(bag => bag));
|
|
3346
|
+
}
|
|
3074
3347
|
/**
|
|
3075
3348
|
* Opens a `BroadcastChannel` with the given name (when the API is available).
|
|
3076
3349
|
* @param bus - The channel name to open (default: `'ARS-CHANNEL'`).
|
|
@@ -3163,6 +3436,7 @@ class BroadcastChannelManager {
|
|
|
3163
3436
|
}
|
|
3164
3437
|
}
|
|
3165
3438
|
|
|
3439
|
+
const CHANNEL_NAME = 'D2693418-B18F-41BB-BAE9-41BB5407957A-CHANNEL';
|
|
3166
3440
|
/**
|
|
3167
3441
|
* Application-level messaging service that combines two transports:
|
|
3168
3442
|
* - An in-process RxJS `Subject` for same-tab communication (`sendMessage` / `getMessage`).
|
|
@@ -3171,15 +3445,15 @@ class BroadcastChannelManager {
|
|
|
3171
3445
|
class BroadcastService {
|
|
3172
3446
|
constructor() {
|
|
3173
3447
|
this.subject = new Subject();
|
|
3174
|
-
this.channel = new BroadcastChannelManager(
|
|
3448
|
+
this.channel = new BroadcastChannelManager(CHANNEL_NAME);
|
|
3175
3449
|
}
|
|
3176
3450
|
/**
|
|
3177
3451
|
* Creates a new standalone `BroadcastChannelManager` instance bound to the shared ARS channel.
|
|
3178
3452
|
* Useful when a caller needs direct channel access outside the service.
|
|
3179
|
-
* @returns A new `BroadcastChannelManager` connected to `'
|
|
3453
|
+
* @returns A new `BroadcastChannelManager` connected to `'D2693418-B18F-41BB-BAE9-41BB5407957A-CHANNEL'`.
|
|
3180
3454
|
*/
|
|
3181
3455
|
static createChannel() {
|
|
3182
|
-
return new BroadcastChannelManager(
|
|
3456
|
+
return new BroadcastChannelManager(CHANNEL_NAME);
|
|
3183
3457
|
}
|
|
3184
3458
|
ngOnDestroy() {
|
|
3185
3459
|
this.channel?.dispose();
|
|
@@ -3224,6 +3498,16 @@ class BroadcastService {
|
|
|
3224
3498
|
unsubscribeChannelMessage(id) {
|
|
3225
3499
|
this.channel.unsubscribe(id);
|
|
3226
3500
|
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Returns a typed Observable of the cross-tab channel messages with the given `id`.
|
|
3503
|
+
* Supports multiple concurrent subscribers and the RxJS lifecycle operators,
|
|
3504
|
+
* unlike `subscribeChannelMessage` (single callback per id).
|
|
3505
|
+
* @param id - The message type identifier to listen for.
|
|
3506
|
+
* @returns An Observable of the matching typed message bags.
|
|
3507
|
+
*/
|
|
3508
|
+
observeChannelMessage(id) {
|
|
3509
|
+
return this.channel.observe(id);
|
|
3510
|
+
}
|
|
3227
3511
|
/**
|
|
3228
3512
|
* Returns an `Observable` that emits every in-process message published via `sendMessage`.
|
|
3229
3513
|
* @returns An observable stream of `BroadcastMessageInfo` objects.
|
|
@@ -3231,6 +3515,14 @@ class BroadcastService {
|
|
|
3231
3515
|
getMessage() {
|
|
3232
3516
|
return this.subject.asObservable();
|
|
3233
3517
|
}
|
|
3518
|
+
/**
|
|
3519
|
+
* Returns a typed `Observable` of the in-process messages with the given `id`.
|
|
3520
|
+
* @param id - The message type identifier to listen for.
|
|
3521
|
+
* @returns An Observable emitting the typed `data` payload of each matching message.
|
|
3522
|
+
*/
|
|
3523
|
+
observeMessage(id) {
|
|
3524
|
+
return this.subject.pipe(filter$1(info => info.id === id), map$1(info => info.data));
|
|
3525
|
+
}
|
|
3234
3526
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BroadcastService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
3235
3527
|
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: BroadcastService }); }
|
|
3236
3528
|
}
|
|
@@ -3324,7 +3616,7 @@ class ScreenService {
|
|
|
3324
3616
|
* Modern Chromium-based Edge reports a different user-agent and will return `false`.
|
|
3325
3617
|
*/
|
|
3326
3618
|
get isIEOrEdge() {
|
|
3327
|
-
return /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
|
|
3619
|
+
return SystemUtils.isBrowser() && /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
|
|
3328
3620
|
}
|
|
3329
3621
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ScreenService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
3330
3622
|
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: ScreenService }); }
|
|
@@ -3379,7 +3671,9 @@ class ThemeService {
|
|
|
3379
3671
|
constructor() {
|
|
3380
3672
|
this.broadcastChannel = new BroadcastChannelManager('THEME-SERVICE');
|
|
3381
3673
|
this.broadcastMessage = '$theme-changed';
|
|
3382
|
-
this.prefersColorSchemeMediaQueryList = window.matchMedia
|
|
3674
|
+
this.prefersColorSchemeMediaQueryList = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
3675
|
+
? window.matchMedia('(prefers-color-scheme: dark)')
|
|
3676
|
+
: undefined;
|
|
3383
3677
|
this.themeChanged = new BehaviorSubject('auto');
|
|
3384
3678
|
this.renderFactory2 = inject(RendererFactory2);
|
|
3385
3679
|
this.renderer = this.renderFactory2.createRenderer(null, null);
|
|
@@ -3412,10 +3706,12 @@ class ThemeService {
|
|
|
3412
3706
|
* @param theme - The initial theme to apply (defaults to the value stored in `localStorage`).
|
|
3413
3707
|
*/
|
|
3414
3708
|
initialize(theme = this.getPreferredTheme()) {
|
|
3415
|
-
this.prefersColorSchemeMediaQueryList
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3709
|
+
if (this.prefersColorSchemeMediaQueryList) {
|
|
3710
|
+
this.prefersColorSchemeMediaQueryList.onchange = () => {
|
|
3711
|
+
if (this.auto())
|
|
3712
|
+
this.setTheme(this.theme());
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3419
3715
|
this.broadcastChannel.subscribe({
|
|
3420
3716
|
messageId: this.broadcastMessage,
|
|
3421
3717
|
action: (bag) => {
|
|
@@ -3441,10 +3737,11 @@ class ThemeService {
|
|
|
3441
3737
|
* @returns `'light'` or `'dark'` — never `'auto'`.
|
|
3442
3738
|
*/
|
|
3443
3739
|
getTheme() {
|
|
3444
|
-
|
|
3445
|
-
|
|
3740
|
+
const current = this.theme();
|
|
3741
|
+
if (current === 'auto') {
|
|
3742
|
+
return this.prefersColorSchemeMediaQueryList?.matches ? 'dark' : 'light';
|
|
3446
3743
|
}
|
|
3447
|
-
return
|
|
3744
|
+
return current;
|
|
3448
3745
|
}
|
|
3449
3746
|
/**
|
|
3450
3747
|
* Reads the theme stored in `localStorage` and validates it.
|
|
@@ -3452,6 +3749,8 @@ class ThemeService {
|
|
|
3452
3749
|
* @returns The persisted `ThemeType`, or `'auto'` as default.
|
|
3453
3750
|
*/
|
|
3454
3751
|
getPreferredTheme() {
|
|
3752
|
+
if (typeof localStorage === 'undefined')
|
|
3753
|
+
return 'auto';
|
|
3455
3754
|
const stored = localStorage.getItem('preferred-theme');
|
|
3456
3755
|
return stored && this.isTheme(stored) ? stored : 'auto';
|
|
3457
3756
|
}
|
|
@@ -3472,9 +3771,11 @@ class ThemeService {
|
|
|
3472
3771
|
this.renderer.removeClass(document.body, 'light');
|
|
3473
3772
|
this.renderer.removeClass(document.body, 'dark');
|
|
3474
3773
|
this.theme.set(theme);
|
|
3475
|
-
localStorage
|
|
3774
|
+
if (typeof localStorage !== 'undefined') {
|
|
3775
|
+
localStorage.setItem('preferred-theme', theme);
|
|
3776
|
+
}
|
|
3476
3777
|
const effectiveClass = this.auto()
|
|
3477
|
-
? (this.prefersColorSchemeMediaQueryList
|
|
3778
|
+
? (this.prefersColorSchemeMediaQueryList?.matches ? 'dark' : 'light')
|
|
3478
3779
|
: theme;
|
|
3479
3780
|
this.renderer.addClass(document.body, effectiveClass);
|
|
3480
3781
|
this.themeChanged.next(this.getTheme());
|
|
@@ -3488,12 +3789,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
3488
3789
|
}], ctorParameters: () => [] });
|
|
3489
3790
|
|
|
3490
3791
|
/*
|
|
3491
|
-
* Public API Surface of
|
|
3792
|
+
* Public API Surface of scm-utils
|
|
3492
3793
|
*/
|
|
3493
3794
|
|
|
3494
3795
|
/**
|
|
3495
3796
|
* Generated bundle index. Do not edit.
|
|
3496
3797
|
*/
|
|
3497
3798
|
|
|
3498
|
-
export { AutoFocusDirective,
|
|
3799
|
+
export { AutoFocusDirective, BroadcastChannelManager, BroadcastService, CHANNEL_NAME, CopyClipboardDirective, DateFnsAdapter, DateFormat, DateInterval, DateIntervalChangeDirective, DeleteModel, EmailsValidatorDirective, EnvironmentService, EqualsValidatorDirective, FileInfo, FileSizeValidatorDirective, FormatHtmlPipe, FormatMarkdownPipe, FormatPipe, GroupModel, GuidValidatorDirective, IDModel, ImportModel, MAT_DATE_FNS_FORMATS, MaxTermsValidatorDirective, NotEmptyValidatorDirective, NotEqualValidatorDirective, NotFutureValidatorDirective, PasswordValidatorDirective, QueryModel, RelationModel, RemoveFocusDirective, ReplacePipe, SafeHtmlPipe, SafeUrlPipe, ScreenService, SearchCallbackPipe, SearchFilterPipe, SelectableModel, SplashService, SqlDateValidatorDirective, SystemUtils, ThemeService, TimeValidatorDirective, UpdateRelationsModel, UrlValidatorDirective, UtilsMessages, ValidIfDirective, ValidatorDirective, ValueModel, provideArsDateFns };
|
|
3499
3800
|
//# sourceMappingURL=arsedizioni-ars-utils-core.mjs.map
|