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
|
@@ -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 =
|
|
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
|
+
});
|
package/src/parser/markdown.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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) {
|