@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.6.10",
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.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",
@@ -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: true,
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
- state.tokens.splice(index + 1, 0, ...createAlertTitleTokens(state, alert.title));
213
- index += 3;
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 openToken = new state.Token('paragraph_open', 'p', 1);
276
- openToken.attrSet('class', 'zp-alert__title');
277
-
278
- const inlineToken = new state.Token('inline', '', 0);
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, '&amp;')
306
- .replace(/</g, '&lt;')
307
- .replace(/>/g, '&gt;')
308
- .replace(/"/g, '&quot;')
309
- .replace(/'/g, '&#39;');
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, '&amp;')
425
+ .replace(/</g, '&lt;')
426
+ .replace(/>/g, '&gt;')
427
+ .replace(/"/g, '&quot;')
428
+ .replace(/'/g, '&#39;');
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
- video: new Set(['src', 'controls', 'autoplay', 'loop', 'muted', 'playsinline', 'poster', 'preload', 'width', 'height', 'class', 'id', 'title']),
346
- audio: new Set(['src', 'controls', 'autoplay', 'loop', 'muted', 'preload', 'class', 'id', 'title']),
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(',')