create-portfoliable 1.2.15 → 1.2.17
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/cli/portfoliable.mjs
CHANGED
|
@@ -456,6 +456,37 @@ function runThumbnailOptions(flags) {
|
|
|
456
456
|
return 0;
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
// Emits case media files so authored src/content URLs survive production builds.
|
|
460
|
+
function createCaseContentAssetPlugin() {
|
|
461
|
+
return {
|
|
462
|
+
name: 'portfoliable-case-content-assets',
|
|
463
|
+
generateBundle() {
|
|
464
|
+
const casesRoot = path.resolve(process.cwd(), 'src', 'content', 'cases');
|
|
465
|
+
if (!fs.existsSync(casesRoot)) return;
|
|
466
|
+
|
|
467
|
+
const emitDirectory = (directory) => {
|
|
468
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
469
|
+
const entryPath = path.join(directory, entry.name);
|
|
470
|
+
if (entry.isDirectory()) {
|
|
471
|
+
emitDirectory(entryPath);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (entry.name.toLowerCase().endsWith('.md')) continue;
|
|
475
|
+
|
|
476
|
+
const relativePath = path.relative(process.cwd(), entryPath).replace(/\\/g, '/');
|
|
477
|
+
this.emitFile({
|
|
478
|
+
type: 'asset',
|
|
479
|
+
fileName: relativePath,
|
|
480
|
+
source: fs.readFileSync(entryPath)
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
emitDirectory(casesRoot);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
459
490
|
// Prints startup summary box for dev server sessions.
|
|
460
491
|
function printStartupBox({ localUrl, networkUrl, localVersion, latestVersion }) {
|
|
461
492
|
const lines = [
|
|
@@ -649,9 +680,10 @@ async function runBuild() {
|
|
|
649
680
|
printRailSegment({ dot: true, label: `${cyan}${bold}Starting build...${reset}` });
|
|
650
681
|
console.log(`${cyan}${bold}Building Portfoliable...${reset}`);
|
|
651
682
|
const runtimeAliases = resolveConsumerRuntimeAliases(process.cwd());
|
|
652
|
-
await viteBuild(
|
|
653
|
-
? { resolve: { alias: runtimeAliases } }
|
|
654
|
-
:
|
|
683
|
+
await viteBuild({
|
|
684
|
+
...(runtimeAliases.length > 0 ? { resolve: { alias: runtimeAliases } } : {}),
|
|
685
|
+
plugins: [createCaseContentAssetPlugin()]
|
|
686
|
+
});
|
|
655
687
|
console.log(`${green}Build complete.${reset}`);
|
|
656
688
|
printRailSegment({ dot: true, label: `${green}${bold}Build complete.${reset}` });
|
|
657
689
|
}
|
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
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
A11Y_THEME_TOKEN_KEYS,
|
|
36
36
|
Header,
|
|
37
37
|
HomeView,
|
|
38
|
+
MermaidDiagram,
|
|
38
39
|
Summary,
|
|
39
40
|
Thumbnail,
|
|
40
41
|
Toast,
|
|
@@ -49,6 +50,7 @@ void Summary;
|
|
|
49
50
|
void Thumbnail;
|
|
50
51
|
void Toast;
|
|
51
52
|
void VideoPlayer;
|
|
53
|
+
void MermaidDiagram;
|
|
52
54
|
|
|
53
55
|
// MARK: HEADER DEFAULTS
|
|
54
56
|
// Resolves runtime defaults and persistence keys used across navigation and resume flows.
|
|
@@ -4662,7 +4664,7 @@ class AppShell extends HTMLElement {
|
|
|
4662
4664
|
].join('::');
|
|
4663
4665
|
|
|
4664
4666
|
if (this._caseViewRenderSignature !== caseRenderSignature) {
|
|
4665
|
-
caseView.innerHTML = caseMarkup;
|
|
4667
|
+
caseView.innerHTML = this._normalizeArticleMediaUrls(caseMarkup);
|
|
4666
4668
|
this._caseViewRenderSignature = caseRenderSignature;
|
|
4667
4669
|
}
|
|
4668
4670
|
|
|
@@ -5365,7 +5367,9 @@ class AppShell extends HTMLElement {
|
|
|
5365
5367
|
const thumbBrand = activeCase.thumbBrand || 'apple';
|
|
5366
5368
|
const thumbModel = activeCase.thumbModel || 'Apple iPhone 12';
|
|
5367
5369
|
const thumbColor = activeCase.thumbColor || 'Black';
|
|
5368
|
-
const subtitleSrc =
|
|
5370
|
+
const subtitleSrc = this._resolveRuntimeMediaUrl(
|
|
5371
|
+
this.getLangCurrentLocaleOnly(this._resolveCasePrimaryVttProp(activeCase))
|
|
5372
|
+
);
|
|
5369
5373
|
const rawSubtitleConfig = this._resolveCasePrimaryVttProp(activeCase);
|
|
5370
5374
|
const {
|
|
5371
5375
|
breadcrumbMenuLabels,
|
|
@@ -5547,7 +5551,7 @@ class AppShell extends HTMLElement {
|
|
|
5547
5551
|
const composedHtml = this.getLang(aboutPayload.bodyHtml) || '';
|
|
5548
5552
|
|
|
5549
5553
|
if (aboutArticle.dataset.aboutHtml !== composedHtml) {
|
|
5550
|
-
aboutArticle.innerHTML = composedHtml;
|
|
5554
|
+
aboutArticle.innerHTML = this._normalizeArticleMediaUrls(composedHtml);
|
|
5551
5555
|
aboutArticle.dataset.aboutHtml = composedHtml;
|
|
5552
5556
|
}
|
|
5553
5557
|
|
|
@@ -5648,6 +5652,34 @@ class AppShell extends HTMLElement {
|
|
|
5648
5652
|
});
|
|
5649
5653
|
}
|
|
5650
5654
|
|
|
5655
|
+
_normalizeArticleMediaUrls(html) {
|
|
5656
|
+
if (typeof document === 'undefined') return String(html || '');
|
|
5657
|
+
|
|
5658
|
+
const template = document.createElement('template');
|
|
5659
|
+
template.innerHTML = String(html || '');
|
|
5660
|
+
template.content.querySelectorAll('[src]').forEach((element) => {
|
|
5661
|
+
const source = element.getAttribute('src');
|
|
5662
|
+
if (source) element.setAttribute('src', this._resolveRuntimeMediaUrl(source));
|
|
5663
|
+
});
|
|
5664
|
+
template.content.querySelectorAll('[poster]').forEach((element) => {
|
|
5665
|
+
const poster = element.getAttribute('poster');
|
|
5666
|
+
if (poster) element.setAttribute('poster', this._resolveRuntimeMediaUrl(poster));
|
|
5667
|
+
});
|
|
5668
|
+
template.content.querySelectorAll('[srcset]').forEach((element) => {
|
|
5669
|
+
const srcset = element.getAttribute('srcset');
|
|
5670
|
+
if (!srcset) return;
|
|
5671
|
+
const normalizedSrcset = srcset.split(',').map((candidate) => {
|
|
5672
|
+
const parts = candidate.trim().split(/\s+/);
|
|
5673
|
+
if (!parts[0]) return candidate;
|
|
5674
|
+
parts[0] = this._resolveRuntimeMediaUrl(parts[0]);
|
|
5675
|
+
return parts.join(' ');
|
|
5676
|
+
}).join(', ');
|
|
5677
|
+
element.setAttribute('srcset', normalizedSrcset);
|
|
5678
|
+
});
|
|
5679
|
+
|
|
5680
|
+
return template.innerHTML;
|
|
5681
|
+
}
|
|
5682
|
+
|
|
5651
5683
|
_resolveLocalizedConfigText(value) {
|
|
5652
5684
|
if (typeof value === 'string') return value;
|
|
5653
5685
|
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) {
|