@zeropress/build-core 0.6.10 → 0.6.12
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/package.json +2 -2
- package/src/api/build-site.js +22 -0
- package/src/render/content-renderer.js +279 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "Shared ZeroPress build core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@zeropress/slug-policy": "0.1.0",
|
|
37
|
-
"@zeropress/preview-data-validator": "0.6.
|
|
37
|
+
"@zeropress/preview-data-validator": "0.6.12",
|
|
38
38
|
"@zeropress/theme-validator": "0.6.5",
|
|
39
39
|
"highlight.js": "11.11.1",
|
|
40
40
|
"markdown-it": "14.2.0",
|
package/src/api/build-site.js
CHANGED
|
@@ -449,6 +449,7 @@ function normalizePreviewData(previewData, options = {}) {
|
|
|
449
449
|
? normalizeSiteFavicon(previewData.site.favicon, media_base_url)
|
|
450
450
|
: normalizeSiteFavicon(options.favicon, ''),
|
|
451
451
|
logo: normalizeSiteLogo(previewData.site.logo, media_base_url),
|
|
452
|
+
newsletter: normalizeSiteNewsletter(previewData.site.newsletter),
|
|
452
453
|
posts_per_page: Number.isInteger(previewData.site.posts_per_page) && previewData.site.posts_per_page > 0
|
|
453
454
|
? previewData.site.posts_per_page
|
|
454
455
|
: DEFAULT_POSTS_PER_PAGE,
|
|
@@ -773,6 +774,27 @@ function normalizeSiteLogo(logo, media_base_url) {
|
|
|
773
774
|
};
|
|
774
775
|
}
|
|
775
776
|
|
|
777
|
+
function normalizeSiteNewsletter(newsletter) {
|
|
778
|
+
if (!newsletter || typeof newsletter !== 'object' || Array.isArray(newsletter)) {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const title = normalizeOptionalString(newsletter.title);
|
|
783
|
+
const description = normalizeOptionalString(newsletter.description);
|
|
784
|
+
const buttonLabel = normalizeOptionalString(newsletter.button_label);
|
|
785
|
+
const signupUrl = normalizeOptionalString(newsletter.signup_url);
|
|
786
|
+
const embedUrl = normalizeOptionalString(newsletter.embed_url);
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
enabled: newsletter.enabled === true,
|
|
790
|
+
...(title ? { title } : {}),
|
|
791
|
+
...(description ? { description } : {}),
|
|
792
|
+
...(buttonLabel ? { button_label: buttonLabel } : {}),
|
|
793
|
+
...(signupUrl ? { signup_url: signupUrl } : {}),
|
|
794
|
+
...(embedUrl ? { embed_url: embedUrl } : {}),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
776
798
|
function normalizePermalinks(permalinks) {
|
|
777
799
|
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
778
800
|
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
@@ -10,6 +10,15 @@ const ALERT_TYPES = new Map([
|
|
|
10
10
|
['WARNING', { className: 'warning', title: 'Warning' }],
|
|
11
11
|
['CAUTION', { className: 'caution', title: 'Caution' }],
|
|
12
12
|
]);
|
|
13
|
+
const ADMONITION_CONTAINER_TYPES = new Map([
|
|
14
|
+
['NOTE', { className: 'note', title: 'Note' }],
|
|
15
|
+
['INFO', { className: 'note', title: 'Info' }],
|
|
16
|
+
['TIP', { className: 'tip', title: 'Tip' }],
|
|
17
|
+
['IMPORTANT', { className: 'important', title: 'Important' }],
|
|
18
|
+
['WARNING', { className: 'warning', title: 'Warning' }],
|
|
19
|
+
['CAUTION', { className: 'caution', title: 'Caution' }],
|
|
20
|
+
['DANGER', { className: 'caution', title: 'Danger' }],
|
|
21
|
+
]);
|
|
13
22
|
|
|
14
23
|
export function renderDocumentContent(content, documentType = 'markdown') {
|
|
15
24
|
return renderDocument(content, documentType).html;
|
|
@@ -51,7 +60,7 @@ function createMarkdownRenderer(toc) {
|
|
|
51
60
|
html: true,
|
|
52
61
|
linkify: false,
|
|
53
62
|
typographer: true,
|
|
54
|
-
breaks:
|
|
63
|
+
breaks: false,
|
|
55
64
|
highlight(value, language) {
|
|
56
65
|
if (language && hljs.getLanguage(language)) {
|
|
57
66
|
try {
|
|
@@ -69,6 +78,7 @@ function createMarkdownRenderer(toc) {
|
|
|
69
78
|
},
|
|
70
79
|
});
|
|
71
80
|
|
|
81
|
+
markdown.use(markdownAdmonitionContainers);
|
|
72
82
|
markdown.use(markdownTaskLists);
|
|
73
83
|
markdown.use(markdownAlerts);
|
|
74
84
|
markdown.use(markdownTableAlignmentClasses);
|
|
@@ -92,6 +102,107 @@ function createMarkdownRenderer(toc) {
|
|
|
92
102
|
return markdown;
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
function markdownAdmonitionContainers(markdown) {
|
|
106
|
+
markdown.block.ruler.before('fence', 'zeropress_admonition_containers', (state, startLine, endLine, silent) => {
|
|
107
|
+
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const opener = parseAdmonitionContainerOpener(getLineText(state, startLine));
|
|
112
|
+
if (!opener) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const alert = ADMONITION_CONTAINER_TYPES.get(opener.type.toUpperCase());
|
|
117
|
+
if (!alert) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const closeLine = findAdmonitionContainerClose(state, startLine + 1, endLine, opener.markerLength);
|
|
122
|
+
if (closeLine === -1) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (silent) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const openToken = state.push('admonition_container_open', 'aside', 1);
|
|
131
|
+
openToken.block = true;
|
|
132
|
+
openToken.markup = opener.markup;
|
|
133
|
+
openToken.map = [startLine, closeLine + 1];
|
|
134
|
+
openToken.attrSet('class', `zp-alert zp-alert--${alert.className}`);
|
|
135
|
+
openToken.attrSet('role', 'note');
|
|
136
|
+
|
|
137
|
+
state.tokens.push(...createAlertTitleTokens(state, alert.title));
|
|
138
|
+
state.md.block.tokenize(state, startLine + 1, closeLine);
|
|
139
|
+
|
|
140
|
+
const closeToken = state.push('admonition_container_close', 'aside', -1);
|
|
141
|
+
closeToken.block = true;
|
|
142
|
+
closeToken.markup = opener.markup;
|
|
143
|
+
|
|
144
|
+
state.line = closeLine + 1;
|
|
145
|
+
return true;
|
|
146
|
+
}, {
|
|
147
|
+
alt: ['paragraph', 'reference', 'blockquote', 'list'],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLineText(state, line) {
|
|
152
|
+
const start = state.bMarks[line] + state.tShift[line];
|
|
153
|
+
const end = state.eMarks[line];
|
|
154
|
+
return state.src.slice(start, end);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseAdmonitionContainerOpener(lineText) {
|
|
158
|
+
const match = /^(:{3,})[ \t]*([A-Za-z][A-Za-z0-9_-]*)(?=$|[ \t[\{])/.exec(lineText);
|
|
159
|
+
if (!match) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
markup: match[1],
|
|
165
|
+
markerLength: match[1].length,
|
|
166
|
+
type: match[2],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function findAdmonitionContainerClose(state, startLine, endLine, markerLength) {
|
|
171
|
+
let fenceMarker = '';
|
|
172
|
+
let fenceLength = 0;
|
|
173
|
+
|
|
174
|
+
for (let line = startLine; line < endLine; line += 1) {
|
|
175
|
+
const lineText = getLineText(state, line);
|
|
176
|
+
|
|
177
|
+
if (fenceMarker) {
|
|
178
|
+
const fenceClose = new RegExp(`^${escapeRegExp(fenceMarker)}{${fenceLength},}[ \\t]*$`).exec(lineText);
|
|
179
|
+
if (fenceClose) {
|
|
180
|
+
fenceMarker = '';
|
|
181
|
+
fenceLength = 0;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fenceOpen = /^(`{3,}|~{3,})/.exec(lineText);
|
|
187
|
+
if (fenceOpen) {
|
|
188
|
+
fenceMarker = fenceOpen[1][0];
|
|
189
|
+
fenceLength = fenceOpen[1].length;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const closeMatch = /^(:{3,})[ \t]*$/.exec(lineText);
|
|
194
|
+
if (closeMatch && closeMatch[1].length >= markerLength) {
|
|
195
|
+
return line;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return -1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function escapeRegExp(value) {
|
|
203
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
204
|
+
}
|
|
205
|
+
|
|
95
206
|
function markdownTableAlignmentClasses(markdown) {
|
|
96
207
|
for (const tokenName of ['th_open', 'td_open']) {
|
|
97
208
|
markdown.renderer.rules[tokenName] = (tokens, index, options, env, self) => {
|
|
@@ -209,8 +320,9 @@ function markdownAlerts(markdown) {
|
|
|
209
320
|
state.tokens.splice(firstParagraph.openIndex, 3);
|
|
210
321
|
}
|
|
211
322
|
|
|
212
|
-
|
|
213
|
-
index
|
|
323
|
+
const titleTokens = createAlertTitleTokens(state, alert.title);
|
|
324
|
+
state.tokens.splice(index + 1, 0, ...titleTokens);
|
|
325
|
+
index += titleTokens.length;
|
|
214
326
|
}
|
|
215
327
|
});
|
|
216
328
|
}
|
|
@@ -272,18 +384,10 @@ function stripAlertMarker(inlineToken, state) {
|
|
|
272
384
|
}
|
|
273
385
|
|
|
274
386
|
function createAlertTitleTokens(state, title) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
inlineToken.content = title;
|
|
280
|
-
const textToken = new state.Token('text', '', 0);
|
|
281
|
-
textToken.content = title;
|
|
282
|
-
inlineToken.children = [textToken];
|
|
283
|
-
|
|
284
|
-
const closeToken = new state.Token('paragraph_close', 'p', -1);
|
|
285
|
-
|
|
286
|
-
return [openToken, inlineToken, closeToken];
|
|
387
|
+
const token = new state.Token('html_block', '', 0);
|
|
388
|
+
token.block = true;
|
|
389
|
+
token.content = `<p class="zp-alert__title">${escapeHtml(title)}</p>`;
|
|
390
|
+
return [token];
|
|
287
391
|
}
|
|
288
392
|
|
|
289
393
|
function addTokenClass(token, className) {
|
|
@@ -301,12 +405,7 @@ function normalizeDocumentType(value) {
|
|
|
301
405
|
}
|
|
302
406
|
|
|
303
407
|
function transformPlaintext(content) {
|
|
304
|
-
const escaped = content
|
|
305
|
-
.replace(/&/g, '&')
|
|
306
|
-
.replace(/</g, '<')
|
|
307
|
-
.replace(/>/g, '>')
|
|
308
|
-
.replace(/"/g, '"')
|
|
309
|
-
.replace(/'/g, ''');
|
|
408
|
+
const escaped = escapeHtml(content);
|
|
310
409
|
|
|
311
410
|
const paragraphs = escaped
|
|
312
411
|
.split(/\n\s*\n/)
|
|
@@ -320,11 +419,20 @@ function transformPlaintext(content) {
|
|
|
320
419
|
return paragraphs.map((entry) => `<p>${entry}</p>`).join('\n');
|
|
321
420
|
}
|
|
322
421
|
|
|
422
|
+
function escapeHtml(value) {
|
|
423
|
+
return String(value)
|
|
424
|
+
.replace(/&/g, '&')
|
|
425
|
+
.replace(/</g, '<')
|
|
426
|
+
.replace(/>/g, '>')
|
|
427
|
+
.replace(/"/g, '"')
|
|
428
|
+
.replace(/'/g, ''');
|
|
429
|
+
}
|
|
430
|
+
|
|
323
431
|
function sanitizeHtml(html) {
|
|
324
432
|
const allowedTags = new Set([
|
|
325
433
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
326
434
|
'p', 'br', 'hr',
|
|
327
|
-
'strong', 'em', 'u', 's', 'code', 'pre',
|
|
435
|
+
'strong', 'b', 'em', 'u', 's', 'sup', 'sub', 'code', 'pre',
|
|
328
436
|
'a', 'img',
|
|
329
437
|
'ul', 'ol', 'li',
|
|
330
438
|
'blockquote', 'aside',
|
|
@@ -336,14 +444,16 @@ function sanitizeHtml(html) {
|
|
|
336
444
|
]);
|
|
337
445
|
|
|
338
446
|
const allowedAttributes = {
|
|
339
|
-
a: new Set(['href', 'title', 'class', 'id']),
|
|
447
|
+
a: new Set(['href', 'title', 'class', 'id', 'target', 'rel']),
|
|
340
448
|
aside: new Set(['role', 'class', 'id']),
|
|
341
449
|
img: new Set(['src', 'srcset', 'sizes', 'alt', 'title', 'class', 'id', 'width', 'height', 'loading', 'decoding']),
|
|
342
450
|
iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class', 'title']),
|
|
343
451
|
input: new Set(['type', 'checked', 'disabled', 'class', 'id', 'aria-label']),
|
|
344
452
|
source: new Set(['src', 'srcset', 'sizes', 'type', 'media', 'width', 'height', 'class', 'id']),
|
|
345
|
-
|
|
346
|
-
|
|
453
|
+
th: new Set(['rowspan', 'colspan', 'align', 'class', 'id']),
|
|
454
|
+
td: new Set(['rowspan', 'colspan', 'align', 'class', 'id']),
|
|
455
|
+
video: new Set(['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'playsinline', 'poster', 'preload', 'width', 'height', 'class', 'id', 'title']),
|
|
456
|
+
audio: new Set(['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'preload', 'class', 'id', 'title']),
|
|
347
457
|
track: new Set(['src', 'kind', 'srclang', 'label', 'default', 'class', 'id']),
|
|
348
458
|
'*': new Set(['class', 'id']),
|
|
349
459
|
};
|
|
@@ -368,6 +478,9 @@ function sanitizeHtml(html) {
|
|
|
368
478
|
const tagAllowedAttributes = allowedAttributes[normalizedTag] || new Set();
|
|
369
479
|
const globalAllowedAttributes = allowedAttributes['*'];
|
|
370
480
|
const filteredAttributes = [];
|
|
481
|
+
let anchorTarget = '';
|
|
482
|
+
let anchorRel = '';
|
|
483
|
+
let tableCellAlignClass = '';
|
|
371
484
|
|
|
372
485
|
if (attributeString) {
|
|
373
486
|
const attributePattern = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
@@ -385,10 +498,55 @@ function sanitizeHtml(html) {
|
|
|
385
498
|
continue;
|
|
386
499
|
}
|
|
387
500
|
|
|
501
|
+
if (normalizedTag === 'a' && attributeName === 'target') {
|
|
502
|
+
const target = sanitizeAnchorTarget(attributeValue);
|
|
503
|
+
if (!target) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
anchorTarget = target;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (normalizedTag === 'a' && attributeName === 'rel') {
|
|
511
|
+
const rel = sanitizeRelList(attributeValue);
|
|
512
|
+
if (!rel) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
anchorRel = rel;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
388
519
|
if (attributeName === 'srcset' && !isSafeSrcset(attributeValue, safeUriPattern)) {
|
|
389
520
|
continue;
|
|
390
521
|
}
|
|
391
522
|
|
|
523
|
+
if (attributeName === 'controlslist') {
|
|
524
|
+
const controlsList = sanitizeControlsList(attributeValue);
|
|
525
|
+
if (!controlsList) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
filteredAttributes.push(`${attributeName}="${controlsList}"`);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if ((normalizedTag === 'th' || normalizedTag === 'td') && (attributeName === 'rowspan' || attributeName === 'colspan')) {
|
|
533
|
+
const span = sanitizePositiveIntegerAttribute(attributeValue);
|
|
534
|
+
if (!span) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
filteredAttributes.push(`${attributeName}="${span}"`);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if ((normalizedTag === 'th' || normalizedTag === 'td') && attributeName === 'align') {
|
|
542
|
+
const alignment = sanitizeTableAlignment(attributeValue);
|
|
543
|
+
if (!alignment) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
tableCellAlignClass = `zp-align-${alignment}`;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
392
550
|
if (normalizedTag === 'input' && attributeName === 'type' && attributeValue !== 'checkbox') {
|
|
393
551
|
continue;
|
|
394
552
|
}
|
|
@@ -397,6 +555,22 @@ function sanitizeHtml(html) {
|
|
|
397
555
|
}
|
|
398
556
|
}
|
|
399
557
|
|
|
558
|
+
if (tableCellAlignClass) {
|
|
559
|
+
appendClassAttribute(filteredAttributes, tableCellAlignClass);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (normalizedTag === 'a') {
|
|
563
|
+
if (anchorTarget) {
|
|
564
|
+
filteredAttributes.push(`target="${anchorTarget}"`);
|
|
565
|
+
}
|
|
566
|
+
if (anchorTarget === '_blank') {
|
|
567
|
+
anchorRel = ensureBlankTargetRel(anchorRel);
|
|
568
|
+
}
|
|
569
|
+
if (anchorRel) {
|
|
570
|
+
filteredAttributes.push(`rel="${anchorRel}"`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
400
574
|
const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img' || normalizedTag === 'input' || normalizedTag === 'source' || normalizedTag === 'track';
|
|
401
575
|
const attributeSuffix = filteredAttributes.length > 0 ? ` ${filteredAttributes.join(' ')}` : '';
|
|
402
576
|
return isSelfClosing ? `<${normalizedTag}${attributeSuffix} />` : `<${normalizedTag}${attributeSuffix}>`;
|
|
@@ -414,6 +588,86 @@ function sanitizeHtml(html) {
|
|
|
414
588
|
.join('');
|
|
415
589
|
}
|
|
416
590
|
|
|
591
|
+
function appendClassAttribute(attributes, className) {
|
|
592
|
+
const classIndex = attributes.findIndex((attribute) => attribute.startsWith('class="'));
|
|
593
|
+
if (classIndex === -1) {
|
|
594
|
+
attributes.push(`class="${className}"`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const existingValue = attributes[classIndex].slice('class="'.length, -1);
|
|
599
|
+
const classes = existingValue.split(/\s+/).filter(Boolean);
|
|
600
|
+
if (!classes.includes(className)) {
|
|
601
|
+
classes.push(className);
|
|
602
|
+
}
|
|
603
|
+
attributes[classIndex] = `class="${classes.join(' ')}"`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function sanitizePositiveIntegerAttribute(value) {
|
|
607
|
+
const normalized = String(value).trim();
|
|
608
|
+
if (!/^\d+$/.test(normalized)) {
|
|
609
|
+
return '';
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const numericValue = Number(normalized);
|
|
613
|
+
if (!Number.isSafeInteger(numericValue) || numericValue < 1) {
|
|
614
|
+
return '';
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return String(numericValue);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function sanitizeTableAlignment(value) {
|
|
621
|
+
const normalized = String(value).trim().toLowerCase();
|
|
622
|
+
return ['left', 'center', 'right'].includes(normalized) ? normalized : '';
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function sanitizeAnchorTarget(value) {
|
|
626
|
+
const normalized = String(value).trim().toLowerCase();
|
|
627
|
+
return normalized === '_blank' ? '_blank' : '';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function sanitizeRelList(value) {
|
|
631
|
+
const allowedTokens = new Set(['noopener', 'noreferrer', 'nofollow', 'ugc', 'sponsored', 'external']);
|
|
632
|
+
const tokens = String(value)
|
|
633
|
+
.toLowerCase()
|
|
634
|
+
.split(/\s+/)
|
|
635
|
+
.filter((token, index, allTokens) => (
|
|
636
|
+
allowedTokens.has(token)
|
|
637
|
+
&& allTokens.indexOf(token) === index
|
|
638
|
+
));
|
|
639
|
+
|
|
640
|
+
return tokens.join(' ');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function ensureBlankTargetRel(value) {
|
|
644
|
+
const tokens = String(value)
|
|
645
|
+
.toLowerCase()
|
|
646
|
+
.split(/\s+/)
|
|
647
|
+
.filter(Boolean);
|
|
648
|
+
|
|
649
|
+
for (const requiredToken of ['noopener', 'noreferrer']) {
|
|
650
|
+
if (!tokens.includes(requiredToken)) {
|
|
651
|
+
tokens.push(requiredToken);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return tokens.join(' ');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function sanitizeControlsList(value) {
|
|
659
|
+
const allowedTokens = new Set(['nodownload', 'nofullscreen', 'noremoteplayback']);
|
|
660
|
+
const tokens = String(value)
|
|
661
|
+
.toLowerCase()
|
|
662
|
+
.split(/\s+/)
|
|
663
|
+
.filter((token, index, allTokens) => (
|
|
664
|
+
allowedTokens.has(token)
|
|
665
|
+
&& allTokens.indexOf(token) === index
|
|
666
|
+
));
|
|
667
|
+
|
|
668
|
+
return tokens.join(' ');
|
|
669
|
+
}
|
|
670
|
+
|
|
417
671
|
function isSafeSrcset(value, safeUriPattern) {
|
|
418
672
|
const candidates = String(value)
|
|
419
673
|
.split(',')
|