create-portfoliable 1.2.15 → 1.2.16

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": "create-portfoliable",
3
- "version": "1.2.15",
3
+ "version": "1.2.16",
4
4
  "description": "Unified Portfoliable package with initializer and runtime CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -12,6 +12,7 @@ import { pathToFileURL } from "node:url";
12
12
  const REPO_URL = "https://github.com/portfoliablejs/portfoliable";
13
13
  // Enables dry-run behavior when --dry-run is provided on process args.
14
14
  const isDryRun = process.argv.includes("--dry-run");
15
+ const isPreparePr = process.argv.includes("--prepare-pr");
15
16
  // Enables signing release commit and tag objects when RELEASE_SIGN=true.
16
17
  const shouldSign = process.env.RELEASE_SIGN === "true";
17
18
 
@@ -410,6 +411,11 @@ function run() {
410
411
  }
411
412
  git(commitArgs);
412
413
 
414
+ if (isPreparePr) {
415
+ console.log("Release PR preparation enabled; skipping tag creation until merge.");
416
+ return;
417
+ }
418
+
413
419
  // Creates annotated or signed tags for each release if tag is not already present.
414
420
  for (const plan of releases) {
415
421
  // Checks whether target tag already exists in repository.
package/src/App.js CHANGED
@@ -4662,7 +4662,7 @@ class AppShell extends HTMLElement {
4662
4662
  ].join('::');
4663
4663
 
4664
4664
  if (this._caseViewRenderSignature !== caseRenderSignature) {
4665
- caseView.innerHTML = caseMarkup;
4665
+ caseView.innerHTML = this._normalizeArticleMediaUrls(caseMarkup);
4666
4666
  this._caseViewRenderSignature = caseRenderSignature;
4667
4667
  }
4668
4668
 
@@ -5365,7 +5365,9 @@ class AppShell extends HTMLElement {
5365
5365
  const thumbBrand = activeCase.thumbBrand || 'apple';
5366
5366
  const thumbModel = activeCase.thumbModel || 'Apple iPhone 12';
5367
5367
  const thumbColor = activeCase.thumbColor || 'Black';
5368
- const subtitleSrc = String(this.getLangCurrentLocaleOnly(this._resolveCasePrimaryVttProp(activeCase)) || '').trim();
5368
+ const subtitleSrc = this._resolveRuntimeMediaUrl(
5369
+ this.getLangCurrentLocaleOnly(this._resolveCasePrimaryVttProp(activeCase))
5370
+ );
5369
5371
  const rawSubtitleConfig = this._resolveCasePrimaryVttProp(activeCase);
5370
5372
  const {
5371
5373
  breadcrumbMenuLabels,
@@ -5547,7 +5549,7 @@ class AppShell extends HTMLElement {
5547
5549
  const composedHtml = this.getLang(aboutPayload.bodyHtml) || '';
5548
5550
 
5549
5551
  if (aboutArticle.dataset.aboutHtml !== composedHtml) {
5550
- aboutArticle.innerHTML = composedHtml;
5552
+ aboutArticle.innerHTML = this._normalizeArticleMediaUrls(composedHtml);
5551
5553
  aboutArticle.dataset.aboutHtml = composedHtml;
5552
5554
  }
5553
5555
 
@@ -5648,6 +5650,34 @@ class AppShell extends HTMLElement {
5648
5650
  });
5649
5651
  }
5650
5652
 
5653
+ _normalizeArticleMediaUrls(html) {
5654
+ if (typeof document === 'undefined') return String(html || '');
5655
+
5656
+ const template = document.createElement('template');
5657
+ template.innerHTML = String(html || '');
5658
+ template.content.querySelectorAll('[src]').forEach((element) => {
5659
+ const source = element.getAttribute('src');
5660
+ if (source) element.setAttribute('src', this._resolveRuntimeMediaUrl(source));
5661
+ });
5662
+ template.content.querySelectorAll('[poster]').forEach((element) => {
5663
+ const poster = element.getAttribute('poster');
5664
+ if (poster) element.setAttribute('poster', this._resolveRuntimeMediaUrl(poster));
5665
+ });
5666
+ template.content.querySelectorAll('[srcset]').forEach((element) => {
5667
+ const srcset = element.getAttribute('srcset');
5668
+ if (!srcset) return;
5669
+ const normalizedSrcset = srcset.split(',').map((candidate) => {
5670
+ const parts = candidate.trim().split(/\s+/);
5671
+ if (!parts[0]) return candidate;
5672
+ parts[0] = this._resolveRuntimeMediaUrl(parts[0]);
5673
+ return parts.join(' ');
5674
+ }).join(', ');
5675
+ element.setAttribute('srcset', normalizedSrcset);
5676
+ });
5677
+
5678
+ return template.innerHTML;
5679
+ }
5680
+
5651
5681
  _resolveLocalizedConfigText(value) {
5652
5682
  if (typeof value === 'string') return value;
5653
5683
  if (!value || typeof value !== 'object') return '';
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { renderLocalizedMarkdownHtml } from './markdown.js';
4
+
5
+ describe('case markdown rendering', () => {
6
+ it('preserves authored HTML image sizing attributes', () => {
7
+ const html = renderLocalizedMarkdownHtml(
8
+ '<img src="src/content/cases/example/image.avif" alt="Example" width="300">',
9
+ ['en']
10
+ ).htmlByLocale.en;
11
+
12
+ expect(html).toContain('<img src="src/content/cases/example/image.avif" alt="Example" width="300">');
13
+ });
14
+
15
+ it('renders Mermaid fences as diagram elements', () => {
16
+ const html = renderLocalizedMarkdownHtml('```mermaid\ngraph TD\n A --> B\n```', ['en'])
17
+ .htmlByLocale.en;
18
+
19
+ expect(html).toContain('<mermaid-diagram>');
20
+ expect(html).toContain('graph TD');
21
+ expect(html).not.toContain('<pre><code>');
22
+ });
23
+
24
+ it('keeps ordinary fenced code blocks as code', () => {
25
+ const html = renderLocalizedMarkdownHtml('```javascript\nconst answer = 42;\n```', ['en'])
26
+ .htmlByLocale.en;
27
+
28
+ expect(html).toContain('<pre><code>');
29
+ expect(html).toContain('const answer = 42;');
30
+ });
31
+ });
@@ -10,7 +10,7 @@ import markdownItTaskLists from 'markdown-it-task-lists';
10
10
  // MARK: MARKDOWN RENDERER
11
11
  // Uses markdown-it + plugins to support broad markdown syntax for case content.
12
12
  const markdownRenderer = new MarkdownIt({
13
- html: false,
13
+ html: true,
14
14
  linkify: true,
15
15
  typographer: true,
16
16
  breaks: false
@@ -56,6 +56,16 @@ markdownRenderer.renderer.rules.link_open = (tokens, idx, options, env, self) =>
56
56
  return self.renderToken(tokens, idx, options);
57
57
  };
58
58
 
59
+ // Emits Mermaid fences as the Valence custom element instead of a code block.
60
+ markdownRenderer.renderer.rules.fence = (tokens, idx) => {
61
+ const token = tokens[idx];
62
+ if (String(token.info || '').trim().split(/\s+/)[0].toLowerCase() !== 'mermaid') {
63
+ return `<pre><code>${markdownRenderer.utils.escapeHtml(token.content)}</code></pre>\n`;
64
+ }
65
+
66
+ return `<mermaid-diagram>${markdownRenderer.utils.escapeHtml(token.content)}</mermaid-diagram>\n`;
67
+ };
68
+
59
69
  // MARK: VALIDATION CONTRACTS
60
70
  // Lists required scalar config fields that every case must define.
61
71
  const REQUIRED_SCALAR_FIELDS = ['id', 'thumbCategory', 'thumbBrand', 'thumbModel', 'thumbColor'];
@@ -10,7 +10,7 @@ import markdownItTaskLists from 'markdown-it-task-lists';
10
10
  // MARK: MARKDOWN RENDERER
11
11
  // Uses markdown-it + plugins to support broad markdown syntax for case content.
12
12
  const markdownRenderer = new MarkdownIt({
13
- html: false,
13
+ html: true,
14
14
  linkify: true,
15
15
  typographer: true,
16
16
  breaks: false
@@ -56,6 +56,16 @@ markdownRenderer.renderer.rules.link_open = (tokens, idx, options, env, self) =>
56
56
  return self.renderToken(tokens, idx, options);
57
57
  };
58
58
 
59
+ // Emits Mermaid fences as the Valence custom element instead of a code block.
60
+ markdownRenderer.renderer.rules.fence = (tokens, idx) => {
61
+ const token = tokens[idx];
62
+ if (String(token.info || '').trim().split(/\s+/)[0].toLowerCase() !== 'mermaid') {
63
+ return `<pre><code>${markdownRenderer.utils.escapeHtml(token.content)}</code></pre>\n`;
64
+ }
65
+
66
+ return `<mermaid-diagram>${markdownRenderer.utils.escapeHtml(token.content)}</mermaid-diagram>\n`;
67
+ };
68
+
59
69
  // MARK: VALIDATION CONTRACTS
60
70
  // Lists required scalar config fields that every case must define.
61
71
  const REQUIRED_SCALAR_FIELDS = ['id', 'thumbCategory', 'thumbBrand', 'thumbModel', 'thumbColor'];
@@ -107,6 +117,56 @@ function markdownToHtml(markdown) {
107
117
  return markdownRenderer.render(source);
108
118
  }
109
119
 
120
+ // Renders localized markdown sections into HTML using the shared markdown-it pipeline.
121
+ export function renderLocalizedMarkdownHtml(bodyText, localeCodes = DEFAULT_LOCALE_CODES) {
122
+ const normalizedBodyText = String(bodyText || '').replace(/<!--\s*config\s*[\s\S]*?-->/gi, '').trim();
123
+ const targetLocales = normalizeLocaleCodes(localeCodes);
124
+ const sections = Object.fromEntries(targetLocales.map((localeCode) => [localeCode, '']));
125
+ const markerLocales = new Set();
126
+ const langRegex = /<!--\s*lang:([a-z0-9-]+)\s*-->/gi;
127
+ let activeLang = null;
128
+ let lastIndex = 0;
129
+ let match;
130
+
131
+ while ((match = langRegex.exec(normalizedBodyText)) !== null) {
132
+ const markerLocale = String(match[1] || '').trim().toLowerCase();
133
+ if (!markerLocale) continue;
134
+ markerLocales.add(markerLocale);
135
+ if (!(markerLocale in sections)) {
136
+ sections[markerLocale] = '';
137
+ }
138
+
139
+ if (activeLang) {
140
+ sections[activeLang] += normalizedBodyText.slice(lastIndex, match.index);
141
+ }
142
+ activeLang = markerLocale;
143
+ lastIndex = langRegex.lastIndex;
144
+ }
145
+
146
+ if (activeLang) {
147
+ sections[activeLang] += normalizedBodyText.slice(lastIndex);
148
+ } else {
149
+ Object.keys(sections).forEach((localeCode) => {
150
+ sections[localeCode] = normalizedBodyText;
151
+ });
152
+ }
153
+
154
+ const resolvedLocales = normalizeLocaleCodes([...targetLocales, ...Object.keys(sections)]);
155
+ const htmlByLocale = Object.fromEntries(resolvedLocales.map((localeCode) => {
156
+ const sectionRaw = String(sections[localeCode] || '').trim();
157
+ return [localeCode, markdownToHtml(sectionRaw)];
158
+ }));
159
+
160
+ return {
161
+ htmlByLocale,
162
+ meta: {
163
+ localeCodes: resolvedLocales,
164
+ hasAnyLangMarker: markerLocales.size > 0,
165
+ hasLangMarkerByLocale: Object.fromEntries(resolvedLocales.map((localeCode) => [localeCode, markerLocales.has(localeCode)]))
166
+ }
167
+ };
168
+ }
169
+
110
170
  // MARK: CONFIG BLOCK PARSING
111
171
  // Parses a top-level config JSON comment block and returns metadata + remaining body text.
112
172
  function parseConfigMetadata(rawText, contextLabel) {