@zeropress/build-core 0.6.11 → 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.11",
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',
@@ -342,6 +450,8 @@ function sanitizeHtml(html) {
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']),
453
+ th: new Set(['rowspan', 'colspan', 'align', 'class', 'id']),
454
+ td: new Set(['rowspan', 'colspan', 'align', 'class', 'id']),
345
455
  video: new Set(['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'playsinline', 'poster', 'preload', 'width', 'height', 'class', 'id', 'title']),
346
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']),
@@ -370,6 +480,7 @@ function sanitizeHtml(html) {
370
480
  const filteredAttributes = [];
371
481
  let anchorTarget = '';
372
482
  let anchorRel = '';
483
+ let tableCellAlignClass = '';
373
484
 
374
485
  if (attributeString) {
375
486
  const attributePattern = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
@@ -418,6 +529,24 @@ function sanitizeHtml(html) {
418
529
  continue;
419
530
  }
420
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
+
421
550
  if (normalizedTag === 'input' && attributeName === 'type' && attributeValue !== 'checkbox') {
422
551
  continue;
423
552
  }
@@ -426,6 +555,10 @@ function sanitizeHtml(html) {
426
555
  }
427
556
  }
428
557
 
558
+ if (tableCellAlignClass) {
559
+ appendClassAttribute(filteredAttributes, tableCellAlignClass);
560
+ }
561
+
429
562
  if (normalizedTag === 'a') {
430
563
  if (anchorTarget) {
431
564
  filteredAttributes.push(`target="${anchorTarget}"`);
@@ -455,6 +588,40 @@ function sanitizeHtml(html) {
455
588
  .join('');
456
589
  }
457
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
+
458
625
  function sanitizeAnchorTarget(value) {
459
626
  const normalized = String(value).trim().toLowerCase();
460
627
  return normalized === '_blank' ? '_blank' : '';