Skip to content

Commit ab2ee77

Browse files
committed
update
1 parent ffd153b commit ab2ee77

4 files changed

Lines changed: 186 additions & 61 deletions

File tree

app/src/cards-metadata.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const cardsLastModified = '2025-10-22T01:00:14Z';
1+
export const cardsLastModified = '2025-10-22T01:27:35Z';

app/src/cards.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ensureMarkedReady, parseMarkdown } from './markdown.js';
2+
13
let cardsCache = null;
24

35
const cardsRequestUrl = (() => {
@@ -49,6 +51,38 @@ function generateCardId(seed = 0) {
4951
return `card-${timestamp}-${seed}-${randomSegment}`;
5052
}
5153

54+
function buildDetailsMarkdown(detailItems, rawDetails, rawSummary) {
55+
if (Array.isArray(detailItems) && detailItems.length > 0) {
56+
return detailItems
57+
.map((item) => {
58+
if (/^\s*(?:[-*+]\s+|\d+\.\s+)/.test(item)) {
59+
return item;
60+
}
61+
return `- ${item}`;
62+
})
63+
.join('\n');
64+
}
65+
66+
if (rawDetails) {
67+
return rawDetails;
68+
}
69+
70+
if (rawSummary) {
71+
return rawSummary;
72+
}
73+
74+
return '';
75+
}
76+
77+
function renderDetailsHtml(markdownSource) {
78+
if (!markdownSource) {
79+
return '';
80+
}
81+
82+
const html = parseMarkdown(markdownSource);
83+
return typeof html === 'string' ? html.trim() : '';
84+
}
85+
5286
function normalizeCards(raw) {
5387
if (!Array.isArray(raw)) {
5488
return [];
@@ -83,6 +117,9 @@ function normalizeCards(raw) {
83117
? detailItems
84118
: rawDetails || rawSummary || '';
85119

120+
const detailsMarkdown = buildDetailsMarkdown(detailItems, rawDetails, rawSummary);
121+
const detailsHtml = renderDetailsHtml(detailsMarkdown);
122+
86123
const tags = Array.isArray(card.tags) ? card.tags.filter((tag) => typeof tag === 'string') : [];
87124
const image = card.image && typeof card.image === 'object' ? {
88125
src: typeof card.image.src === 'string' ? card.image.src.trim() : '',
@@ -95,6 +132,7 @@ function normalizeCards(raw) {
95132
fullTitle,
96133
summary,
97134
details,
135+
detailsHtml,
98136
tags,
99137
image,
100138
};
@@ -164,6 +202,12 @@ export async function fetchCards() {
164202
return cardsCache;
165203
}
166204

205+
try {
206+
await ensureMarkedReady();
207+
} catch (error) {
208+
console.warn('Unable to preload markdown parser for cards', error);
209+
}
210+
167211
const bundled = loadFromBundle();
168212
if (bundled && bundled.length > 0) {
169213
cardsCache = prepareCards(bundled, { shuffle: true });

app/src/markdown.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
let markedInstance = null;
2+
let markedPromise = null;
3+
4+
const CDN_FALLBACK = 'https://cdn.jsdelivr.net/npm/marked@5.1.2/lib/marked.esm.js';
5+
const MARKED_OPTIONS = {
6+
breaks: true,
7+
gfm: true,
8+
mangle: false,
9+
};
10+
11+
function configureMarked(candidate) {
12+
if (!candidate || typeof candidate.parse !== 'function') {
13+
throw new Error('Unable to resolve marked parser');
14+
}
15+
16+
if (typeof candidate.use === 'function') {
17+
candidate.use(MARKED_OPTIONS);
18+
} else if (typeof candidate.setOptions === 'function') {
19+
candidate.setOptions(MARKED_OPTIONS);
20+
}
21+
22+
return candidate;
23+
}
24+
25+
async function importMarked() {
26+
const existing = globalThis.marked;
27+
if (existing && typeof existing.parse === 'function') {
28+
return existing;
29+
}
30+
31+
try {
32+
const module = await import('marked');
33+
return module.marked ?? module.default ?? module;
34+
} catch (error) {
35+
if (typeof window !== 'undefined') {
36+
const module = await import(/* @vite-ignore */ CDN_FALLBACK);
37+
return module.marked ?? module.default ?? module;
38+
}
39+
throw error;
40+
}
41+
}
42+
43+
export function ensureMarkedReady() {
44+
if (markedInstance) {
45+
return Promise.resolve(markedInstance);
46+
}
47+
48+
if (markedPromise) {
49+
return markedPromise;
50+
}
51+
52+
markedPromise = importMarked()
53+
.then((module) => {
54+
markedInstance = configureMarked(module);
55+
return markedInstance;
56+
})
57+
.catch((error) => {
58+
markedPromise = null;
59+
throw error;
60+
});
61+
62+
return markedPromise;
63+
}
64+
65+
export function getMarked() {
66+
if (!markedInstance) {
67+
throw new Error('Marked has not finished loading');
68+
}
69+
return markedInstance;
70+
}
71+
72+
export function parseMarkdown(markdownSource) {
73+
const source = typeof markdownSource === 'string' ? markdownSource : '';
74+
if (!source) {
75+
return '';
76+
}
77+
78+
if (!markedInstance) {
79+
console.warn('Markdown parser requested before it was ready');
80+
return '';
81+
}
82+
83+
try {
84+
return markedInstance.parse(source);
85+
} catch (error) {
86+
console.warn('Failed to parse markdown', error);
87+
return '';
88+
}
89+
}
90+
91+
if (typeof window !== 'undefined') {
92+
ensureMarkedReady().catch((error) => {
93+
console.warn('Unable to preload markdown parser', error);
94+
});
95+
}

app/src/modal.js

Lines changed: 46 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,7 @@
1-
let markedPromise = null;
1+
import { ensureMarkedReady, parseMarkdown } from './markdown.js';
22

33
function resolveMarkedInstance() {
4-
if (markedPromise) {
5-
return markedPromise;
6-
}
7-
8-
markedPromise = (async () => {
9-
const existing = globalThis.marked;
10-
if (existing && typeof existing.parse === 'function') {
11-
return existing;
12-
}
13-
14-
const module = await import('marked');
15-
const candidate = module.marked ?? module.default ?? module;
16-
if (!candidate || typeof candidate.parse !== 'function') {
17-
throw new Error('Unable to load marked parser');
18-
}
19-
20-
return candidate;
21-
})()
22-
.then((instance) => {
23-
if (typeof instance.setOptions === 'function') {
24-
instance.setOptions({
25-
breaks: true,
26-
gfm: true,
27-
});
28-
}
29-
return instance;
30-
})
31-
.catch((error) => {
32-
markedPromise = null;
33-
throw error;
34-
});
35-
36-
return markedPromise;
4+
return ensureMarkedReady();
375
}
386

397
const FOCUSABLE_SELECTOR = [
@@ -71,6 +39,16 @@ function normalizeToText(value) {
7139
return '';
7240
}
7341

42+
function decorateModalContent(target) {
43+
if (!target) {
44+
return;
45+
}
46+
47+
target.querySelectorAll('ul').forEach((list) => {
48+
list.classList.add('modal__list');
49+
});
50+
}
51+
7452
function renderMarkdown(target, markdown) {
7553
if (!target) {
7654
return Promise.resolve(false);
@@ -82,17 +60,14 @@ function renderMarkdown(target, markdown) {
8260
}
8361

8462
return resolveMarkedInstance()
85-
.then((instance) => {
86-
const parser = typeof instance.parse === 'function' ? instance.parse.bind(instance) : null;
87-
const html = parser ? parser(text) : '';
63+
.then(() => {
64+
const html = parseMarkdown(text);
8865
if (!html) {
8966
return false;
9067
}
9168

9269
target.innerHTML = html;
93-
target.querySelectorAll('ul').forEach((list) => {
94-
list.classList.add('modal__list');
95-
});
70+
decorateModalContent(target);
9671
return true;
9772
})
9873
.catch((error) => {
@@ -264,6 +239,8 @@ export function createModalController(root) {
264239
content.innerHTML = '';
265240
content.hidden = true;
266241

242+
const resolvedDetailsHtml = typeof card.detailsHtml === 'string' ? card.detailsHtml.trim() : '';
243+
267244
const listItems = Array.isArray(card.details)
268245
? card.details
269246
.map((item) => {
@@ -294,26 +271,35 @@ export function createModalController(root) {
294271
|| '';
295272
}
296273

297-
const fallbackText = markdownSource || detailsTextForFallback || summaryTextCandidate || '';
298-
299-
renderMarkdown(content, markdownSource)
300-
.then((rendered) => {
301-
if (rendered) {
302-
content.hidden = false;
303-
return;
304-
}
305-
306-
if (fallbackText) {
307-
content.textContent = fallbackText;
308-
content.hidden = false;
309-
}
310-
})
311-
.catch(() => {
312-
if (fallbackText) {
313-
content.textContent = fallbackText;
314-
content.hidden = false;
315-
}
316-
});
274+
const fallbackText = markdownSource
275+
|| detailsTextForFallback
276+
|| summaryTextCandidate
277+
|| '';
278+
279+
if (resolvedDetailsHtml) {
280+
content.innerHTML = resolvedDetailsHtml;
281+
decorateModalContent(content);
282+
content.hidden = false;
283+
} else {
284+
renderMarkdown(content, markdownSource)
285+
.then((rendered) => {
286+
if (rendered) {
287+
content.hidden = false;
288+
return;
289+
}
290+
291+
if (fallbackText) {
292+
content.textContent = fallbackText;
293+
content.hidden = false;
294+
}
295+
})
296+
.catch(() => {
297+
if (fallbackText) {
298+
content.textContent = fallbackText;
299+
content.hidden = false;
300+
}
301+
});
302+
}
317303

318304
if (card.backgroundColor) {
319305
dialog.style.background = card.backgroundColor;

0 commit comments

Comments
 (0)