|
|
import { readFileSync, readdirSync, statSync } from 'fs'; |
|
|
import { join, relative, dirname } from 'path'; |
|
|
import { fileURLToPath } from 'url'; |
|
|
import { createHash } from 'crypto'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseImageImports(content) { |
|
|
const importMap = new Map(); |
|
|
const importPattern = /import\s+(\w+)\s+from\s+["']([^"']+)["']/g; |
|
|
let match; |
|
|
while ((match = importPattern.exec(content)) !== null) { |
|
|
const varName = match[1]; |
|
|
const importPath = match[2]; |
|
|
|
|
|
const filename = importPath.split('/').pop(); |
|
|
if (filename && /\.(png|jpe?g|gif|webp|svg)$/i.test(filename)) { |
|
|
importMap.set(varName, filename); |
|
|
} |
|
|
} |
|
|
return importMap; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function stripCodeBlocks(content) { |
|
|
return content.replace(/```[\s\S]*?```/g, (match) => ' '.repeat(match.length)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function markdownToHtml(md) { |
|
|
if (!md) return ''; |
|
|
|
|
|
let html = md; |
|
|
|
|
|
|
|
|
|
|
|
if (!html.includes('<a ') && !html.includes('<strong>')) { |
|
|
html = html |
|
|
.replace(/&/g, '&') |
|
|
.replace(/</g, '<') |
|
|
.replace(/>/g, '>'); |
|
|
} |
|
|
|
|
|
|
|
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); |
|
|
|
|
|
|
|
|
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); |
|
|
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>'); |
|
|
|
|
|
|
|
|
html = html.replace(/(?<![*_])\*([^*]+)\*(?![*_])/g, '<em>$1</em>'); |
|
|
html = html.replace(/(?<![*_])_([^_]+)_(?![*_])/g, '<em>$1</em>'); |
|
|
|
|
|
|
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); |
|
|
|
|
|
|
|
|
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>'); |
|
|
|
|
|
|
|
|
html = html.replace(/\[x\]/gi, '✅'); |
|
|
html = html.replace(/\[ \]/g, '❌'); |
|
|
|
|
|
return html; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function extractImages(content) { |
|
|
const images = []; |
|
|
|
|
|
|
|
|
const imagePattern = /<Image[^>]*\/>/gi; |
|
|
let match; |
|
|
|
|
|
while ((match = imagePattern.exec(content)) !== null) { |
|
|
const tag = match[0]; |
|
|
|
|
|
|
|
|
const srcMatch = tag.match(/src\s*=\s*\{([^}]+)\}/i); |
|
|
const src = srcMatch ? srcMatch[1].trim() : null; |
|
|
|
|
|
|
|
|
const altMatch = tag.match(/alt\s*=\s*["']([^"']+)["']/i); |
|
|
const alt = altMatch ? altMatch[1] : 'Image'; |
|
|
|
|
|
|
|
|
const captionMatch = tag.match(/caption\s*=\s*["']([^"']+)["']/i) || |
|
|
tag.match(/caption\s*=\s*\{`([^`]+)`\}/i); |
|
|
const caption = captionMatch ? captionMatch[1] : null; |
|
|
|
|
|
|
|
|
const idMatch = tag.match(/id\s*=\s*["']([^"']+)["']/i); |
|
|
const id = idMatch ? idMatch[1] : null; |
|
|
|
|
|
|
|
|
const skipGallery = /\bskipGallery\b/i.test(tag); |
|
|
|
|
|
if (src) { |
|
|
images.push({ |
|
|
type: 'image', |
|
|
src, |
|
|
alt, |
|
|
caption, |
|
|
id, |
|
|
skipGallery |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
return images; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function splitTableRow(row) { |
|
|
const cells = []; |
|
|
let current = ''; |
|
|
let inBacktick = false; |
|
|
|
|
|
for (let i = 0; i < row.length; i++) { |
|
|
const ch = row[i]; |
|
|
if (ch === '`') { |
|
|
inBacktick = !inBacktick; |
|
|
current += ch; |
|
|
} else if (ch === '|' && !inBacktick) { |
|
|
cells.push(current); |
|
|
current = ''; |
|
|
} else { |
|
|
current += ch; |
|
|
} |
|
|
} |
|
|
cells.push(current); |
|
|
return cells; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseTableRow(row, expectedCols) { |
|
|
let cells = splitTableRow(row).filter(c => c.trim()); |
|
|
|
|
|
if (cells.length <= expectedCols) return cells; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const head = cells.slice(0, expectedCols - 1); |
|
|
const tail = cells.slice(expectedCols - 1); |
|
|
|
|
|
if (tail.length > 1) { |
|
|
const lastCell = tail.pop(); |
|
|
const merged = tail.join(' | '); |
|
|
return [...head, merged, lastCell].slice(0, expectedCols); |
|
|
} |
|
|
|
|
|
return cells.slice(0, expectedCols); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function extractTables(content) { |
|
|
const tables = []; |
|
|
|
|
|
|
|
|
|
|
|
const tablePattern = /(\|[^\n]+\|\n\|[-:\s|]+\|\n(?:\|[^\n]+\|\n?)+)/g; |
|
|
let match; |
|
|
let tableIndex = 0; |
|
|
|
|
|
while ((match = tablePattern.exec(content)) !== null) { |
|
|
const tableContent = match[1].trim(); |
|
|
const rows = tableContent.split('\n').filter(row => row.trim()); |
|
|
|
|
|
if (rows.length >= 3) { |
|
|
|
|
|
const headerRow = rows[0]; |
|
|
const headers = splitTableRow(headerRow) |
|
|
.filter(cell => cell.trim()) |
|
|
.map(cell => markdownToHtml(cell.trim())); |
|
|
|
|
|
const expectedCols = headers.length; |
|
|
|
|
|
|
|
|
|
|
|
const dataRows = rows.slice(2).map(row => { |
|
|
return parseTableRow(row, expectedCols) |
|
|
.map(cell => markdownToHtml(cell.trim())); |
|
|
}); |
|
|
|
|
|
tables.push({ |
|
|
type: 'table', |
|
|
id: `table-${tableIndex++}`, |
|
|
headers, |
|
|
rows: dataRows, |
|
|
raw: tableContent |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
return tables; |
|
|
} |
|
|
|
|
|
export function extractHtmlEmbeds(rawContent) { |
|
|
const embeds = []; |
|
|
|
|
|
|
|
|
const content = stripCodeBlocks(rawContent); |
|
|
|
|
|
|
|
|
|
|
|
const widePattern = /<Wide[\s\S]*?>([\s\S]*?)<\/Wide>/gi; |
|
|
const wideBlocks = []; |
|
|
let wideMatch; |
|
|
while ((wideMatch = widePattern.exec(content)) !== null) { |
|
|
wideBlocks.push({ |
|
|
start: wideMatch.index, |
|
|
end: wideMatch.index + wideMatch[0].length, |
|
|
content: wideMatch[0] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const isInsideWide = (embedStartIndex) => { |
|
|
return wideBlocks.some(block => |
|
|
embedStartIndex >= block.start && embedStartIndex < block.end |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
const embedPattern = /<HtmlEmbed/gi; |
|
|
let embedMatch; |
|
|
|
|
|
while ((embedMatch = embedPattern.exec(content)) !== null) { |
|
|
const matchIndex = embedMatch.index; |
|
|
|
|
|
|
|
|
let pos = matchIndex + 10; |
|
|
let match = '<HtmlEmbed'; |
|
|
let inString = false; |
|
|
let stringDelim = null; |
|
|
let inJSXBraces = 0; |
|
|
|
|
|
while (pos < content.length) { |
|
|
const char = content[pos]; |
|
|
const prevChar = pos > 0 ? content[pos - 1] : ''; |
|
|
|
|
|
match += char; |
|
|
|
|
|
|
|
|
if (!inString) { |
|
|
if ((char === '`' || char === '"' || char === "'") && prevChar !== '\\') { |
|
|
inString = true; |
|
|
stringDelim = char; |
|
|
} |
|
|
} else { |
|
|
if (char === stringDelim && prevChar !== '\\') { |
|
|
inString = false; |
|
|
stringDelim = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!inString) { |
|
|
if (char === '{') { |
|
|
inJSXBraces++; |
|
|
} else if (char === '}') { |
|
|
inJSXBraces--; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!inString && inJSXBraces === 0 && char === '/' && pos + 1 < content.length && content[pos + 1] === '>') { |
|
|
match += '>'; |
|
|
break; |
|
|
} |
|
|
|
|
|
pos++; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (match.includes('config={{') && !match.includes('}}')) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const configStart = match.indexOf('config={{'); |
|
|
if (configStart >= 0) { |
|
|
|
|
|
let braceCount = 2; |
|
|
let pos = matchIndex + configStart + 9; |
|
|
let foundEnd = false; |
|
|
|
|
|
while (pos < content.length) { |
|
|
const char = content[pos]; |
|
|
const prevChar = pos > 0 ? content[pos - 1] : ''; |
|
|
|
|
|
|
|
|
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') { |
|
|
|
|
|
const stringDelim = char; |
|
|
pos++; |
|
|
while (pos < content.length) { |
|
|
if (content[pos] === stringDelim && content[pos - 1] !== '\\') { |
|
|
break; |
|
|
} |
|
|
|
|
|
if (stringDelim === '`' && content[pos] === '$' && pos + 1 < content.length && content[pos + 1] === '{') { |
|
|
|
|
|
pos += 2; |
|
|
let innerBraces = 1; |
|
|
while (pos < content.length && innerBraces > 0) { |
|
|
if (content[pos] === '{') innerBraces++; |
|
|
if (content[pos] === '}') innerBraces--; |
|
|
pos++; |
|
|
} |
|
|
continue; |
|
|
} |
|
|
pos++; |
|
|
} |
|
|
pos++; |
|
|
continue; |
|
|
} |
|
|
|
|
|
if (char === '{') braceCount++; |
|
|
if (char === '}') { |
|
|
braceCount--; |
|
|
if (braceCount === 0) { |
|
|
|
|
|
|
|
|
pos++; |
|
|
while (pos < content.length && /\s/.test(content[pos])) { |
|
|
pos++; |
|
|
} |
|
|
if (pos < content.length && content[pos] === '/' && pos + 1 < content.length && content[pos + 1] === '>') { |
|
|
|
|
|
match = content.substring(matchIndex, pos + 2); |
|
|
foundEnd = true; |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
pos++; |
|
|
} |
|
|
|
|
|
if (!foundEnd) { |
|
|
|
|
|
const after = content.substring(matchIndex + match.length); |
|
|
const endPattern = after.match(/\}\}\s*\/>/); |
|
|
if (endPattern) { |
|
|
match = content.substring(matchIndex, matchIndex + match.length + endPattern.index + endPattern[0].length); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const extractAttr = (attrName, content) => { |
|
|
|
|
|
const templateMatch = content.match(new RegExp(`${attrName}\\s*=\\s*\\{\`([\\s\\S]*?)\`\\}`, 'i')); |
|
|
if (templateMatch) return templateMatch[1].trim(); |
|
|
|
|
|
|
|
|
const singleQuoteMatch = content.match(new RegExp(`${attrName}\\s*=\\s*'([\\s\\S]*?)'`, 'i')); |
|
|
if (singleQuoteMatch) return singleQuoteMatch[1].trim(); |
|
|
|
|
|
|
|
|
const doubleQuoteMatch = content.match(new RegExp(`${attrName}\\s*=\\s*"([\\s\\S]*?)"`, 'i')); |
|
|
if (doubleQuoteMatch) return doubleQuoteMatch[1].trim(); |
|
|
|
|
|
return undefined; |
|
|
}; |
|
|
|
|
|
|
|
|
const src = extractAttr('src', match); |
|
|
if (!src) continue; |
|
|
|
|
|
|
|
|
const title = extractAttr('title', match); |
|
|
const desc = extractAttr('desc', match); |
|
|
const id = extractAttr('id', match); |
|
|
const data = extractAttr('data', match); |
|
|
const frameless = /\bframeless\b/i.test(match); |
|
|
const wideAttr = /\bwide\b/i.test(match); |
|
|
const skipGallery = /\bskipGallery\b/i.test(match); |
|
|
|
|
|
|
|
|
let config = null; |
|
|
|
|
|
|
|
|
const jsxConfigRegex = /config\s*=\s*\{\{/i; |
|
|
const jsxConfigMatch = match.match(jsxConfigRegex); |
|
|
|
|
|
if (jsxConfigMatch) { |
|
|
try { |
|
|
|
|
|
const configStart = jsxConfigMatch.index; |
|
|
const startPos = match.indexOf('{{', configStart) + 2; |
|
|
|
|
|
|
|
|
let braceCount = 1; |
|
|
let inString = false; |
|
|
let stringChar = null; |
|
|
let pos = startPos; |
|
|
|
|
|
for (; pos < match.length; pos++) { |
|
|
const char = match[pos]; |
|
|
const prevChar = pos > 0 ? match[pos - 1] : ''; |
|
|
const nextChar = pos < match.length - 1 ? match[pos + 1] : ''; |
|
|
|
|
|
|
|
|
if (!inString) { |
|
|
if (char === '`') { |
|
|
inString = true; |
|
|
stringChar = '`'; |
|
|
} else if (char === '"' && prevChar !== '\\') { |
|
|
inString = true; |
|
|
stringChar = '"'; |
|
|
} else if (char === "'" && prevChar !== '\\') { |
|
|
inString = true; |
|
|
stringChar = "'"; |
|
|
} |
|
|
} else { |
|
|
|
|
|
if (char === stringChar && prevChar !== '\\') { |
|
|
inString = false; |
|
|
stringChar = null; |
|
|
} |
|
|
|
|
|
if (stringChar === '`' && char === '$' && nextChar === '{') { |
|
|
|
|
|
pos++; |
|
|
braceCount++; |
|
|
continue; |
|
|
} |
|
|
} |
|
|
|
|
|
if (!inString) { |
|
|
if (char === '{') { |
|
|
braceCount++; |
|
|
} else if (char === '}') { |
|
|
braceCount--; |
|
|
if (braceCount === 0) { |
|
|
|
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (braceCount !== 0) { |
|
|
throw new Error(`Unbalanced braces: braceCount=${braceCount}`); |
|
|
} |
|
|
|
|
|
|
|
|
let jsxContent = match.substring(startPos, pos).trim(); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const jsCode = `({${jsxContent}})`; |
|
|
|
|
|
|
|
|
|
|
|
config = new Function('return ' + jsCode)(); |
|
|
} catch (evalError) { |
|
|
|
|
|
|
|
|
let jsonStr = jsxContent; |
|
|
|
|
|
|
|
|
jsonStr = '{' + jsonStr + '}'; |
|
|
|
|
|
|
|
|
for (let pass = 0; pass < 5; pass++) { |
|
|
jsonStr = jsonStr.replace(/([{,\[\s])([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":'); |
|
|
jsonStr = jsonStr.replace(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/gm, '"$1":'); |
|
|
} |
|
|
|
|
|
|
|
|
jsonStr = jsonStr.replace(/'/g, '"'); |
|
|
|
|
|
|
|
|
jsonStr = jsonStr.replace(/,\s*([}\]])/g, '$1'); |
|
|
|
|
|
try { |
|
|
config = JSON.parse(jsonStr); |
|
|
} catch (jsonError) { |
|
|
|
|
|
console.warn('[extract-embeds] Config parsing failed:', jsonError.message); |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!config) { |
|
|
const configAttr = extractAttr('config', match); |
|
|
if (configAttr) { |
|
|
try { |
|
|
config = JSON.parse(configAttr); |
|
|
} catch (e) { |
|
|
|
|
|
config = configAttr; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const isWide = isInsideWide(matchIndex) || wideAttr; |
|
|
|
|
|
embeds.push({ |
|
|
src, |
|
|
title, |
|
|
desc, |
|
|
id, |
|
|
frameless, |
|
|
data, |
|
|
config, |
|
|
wide: isWide, |
|
|
skipGallery |
|
|
}); |
|
|
} |
|
|
|
|
|
return embeds; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function findMdxFiles(dir, baseDir = dir, files = [], skipDemo = true) { |
|
|
const entries = readdirSync(dir); |
|
|
|
|
|
for (const entry of entries) { |
|
|
const fullPath = join(dir, entry); |
|
|
const stat = statSync(fullPath); |
|
|
|
|
|
if (stat.isDirectory()) { |
|
|
|
|
|
if (skipDemo && entry === 'demo') { |
|
|
continue; |
|
|
} |
|
|
findMdxFiles(fullPath, baseDir, files, skipDemo); |
|
|
} else if (entry.endsWith('.mdx')) { |
|
|
files.push(fullPath); |
|
|
} |
|
|
} |
|
|
|
|
|
return files; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseArticleChapters(articleContent, contentDir) { |
|
|
const chapterMap = new Map(); |
|
|
const chapterOrder = []; |
|
|
|
|
|
|
|
|
const importPattern = /import\s+(\w+)\s+from\s+["'](.\/chapters\/[^"']+)["']/g; |
|
|
let match; |
|
|
while ((match = importPattern.exec(articleContent)) !== null) { |
|
|
const [, componentName, importPath] = match; |
|
|
const fullPath = join(contentDir, importPath); |
|
|
chapterMap.set(componentName, fullPath); |
|
|
} |
|
|
|
|
|
|
|
|
const usagePattern = /<(\w+)\s*\/>/g; |
|
|
while ((match = usagePattern.exec(articleContent)) !== null) { |
|
|
const componentName = match[1]; |
|
|
if (chapterMap.has(componentName)) { |
|
|
const chapterPath = chapterMap.get(componentName); |
|
|
if (!chapterOrder.includes(chapterPath)) { |
|
|
chapterOrder.push(chapterPath); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return chapterOrder; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function embedKey(embed) { |
|
|
if (embed.id) return `id:${embed.id}`; |
|
|
|
|
|
const hasConfig = embed.config != null; |
|
|
const hasData = embed.data != null; |
|
|
|
|
|
if (!hasConfig && !hasData) return `src:${embed.src}`; |
|
|
|
|
|
|
|
|
const payload = JSON.stringify({ config: embed.config ?? null, data: embed.data ?? null }); |
|
|
const hash = createHash('sha1').update(payload).digest('hex').slice(0, 10); |
|
|
return `src:${embed.src}#${hash}`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function loadEmbedsFromMDX() { |
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url); |
|
|
const __dirname = dirname(__filename); |
|
|
|
|
|
|
|
|
let contentDir = join(__dirname, '../content'); |
|
|
|
|
|
if (!statSync(contentDir, { throwIfNoEntry: false })) { |
|
|
contentDir = join(__dirname, '../../src/content'); |
|
|
} |
|
|
|
|
|
if (!statSync(contentDir, { throwIfNoEntry: false })) { |
|
|
contentDir = join(__dirname, '../../../src/content'); |
|
|
} |
|
|
|
|
|
const allEmbeds = []; |
|
|
const articleFile = join(contentDir, 'article.mdx'); |
|
|
|
|
|
try { |
|
|
|
|
|
const articleContent = readFileSync(articleFile, 'utf-8'); |
|
|
|
|
|
|
|
|
const articleEmbeds = extractHtmlEmbeds(articleContent); |
|
|
articleEmbeds.forEach(embed => { |
|
|
embed.sourceFile = 'content/article.mdx'; |
|
|
}); |
|
|
allEmbeds.push(...articleEmbeds); |
|
|
|
|
|
|
|
|
const chapterOrder = parseArticleChapters(articleContent, contentDir); |
|
|
|
|
|
|
|
|
for (const chapterPath of chapterOrder) { |
|
|
try { |
|
|
const chapterContent = readFileSync(chapterPath, 'utf-8'); |
|
|
const embeds = extractHtmlEmbeds(chapterContent); |
|
|
|
|
|
|
|
|
const relativePath = relative(contentDir, chapterPath); |
|
|
embeds.forEach(embed => { |
|
|
embed.sourceFile = `content/${relativePath}`; |
|
|
}); |
|
|
|
|
|
allEmbeds.push(...embeds); |
|
|
} catch (error) { |
|
|
console.error(`Error reading chapter ${chapterPath}:`, error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const allMdxFiles = findMdxFiles(contentDir, contentDir, [], false); |
|
|
const processedFiles = new Set([articleFile, ...chapterOrder]); |
|
|
|
|
|
for (const filePath of allMdxFiles) { |
|
|
if (!processedFiles.has(filePath)) { |
|
|
try { |
|
|
const rawContent = readFileSync(filePath, 'utf-8'); |
|
|
const embeds = extractHtmlEmbeds(rawContent); |
|
|
const relativePath = relative(contentDir, filePath); |
|
|
embeds.forEach(embed => { |
|
|
embed.sourceFile = `content/${relativePath}`; |
|
|
}); |
|
|
allEmbeds.push(...embeds); |
|
|
} catch (error) { |
|
|
console.error(`Error reading ${filePath}:`, error); |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error processing article:', error); |
|
|
|
|
|
const mdxFiles = findMdxFiles(contentDir, contentDir, [], false); |
|
|
for (const filePath of mdxFiles) { |
|
|
try { |
|
|
const rawContent = readFileSync(filePath, 'utf-8'); |
|
|
const embeds = extractHtmlEmbeds(rawContent); |
|
|
const relativePath = relative(contentDir, filePath); |
|
|
embeds.forEach(embed => { |
|
|
embed.sourceFile = `content/${relativePath}`; |
|
|
}); |
|
|
allEmbeds.push(...embeds); |
|
|
} catch (err) { |
|
|
console.error(`Error reading ${filePath}:`, err); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seen = new Map(); |
|
|
const uniqueEmbeds = []; |
|
|
for (const embed of allEmbeds) { |
|
|
const key = embedKey(embed); |
|
|
if (!seen.has(key)) { |
|
|
seen.set(key, true); |
|
|
uniqueEmbeds.push(embed); |
|
|
} |
|
|
} |
|
|
|
|
|
return uniqueEmbeds; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function extractAttrFromTag(attrName, tagContent) { |
|
|
|
|
|
const templateMatch = tagContent.match(new RegExp(`${attrName}\\s*=\\s*\\{\`([\\s\\S]*?)\`\\}`, 'i')); |
|
|
if (templateMatch) return templateMatch[1].trim(); |
|
|
|
|
|
|
|
|
const singleQuoteMatch = tagContent.match(new RegExp(`${attrName}\\s*=\\s*'([\\s\\S]*?)'`, 'i')); |
|
|
if (singleQuoteMatch) return singleQuoteMatch[1].trim(); |
|
|
|
|
|
|
|
|
const doubleQuoteMatch = tagContent.match(new RegExp(`${attrName}\\s*=\\s*"([\\s\\S]*?)"`, 'i')); |
|
|
if (doubleQuoteMatch) return doubleQuoteMatch[1].trim(); |
|
|
|
|
|
return undefined; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isPositionInsideWide(content, position) { |
|
|
const widePattern = /<Wide[\s\S]*?>([\s\S]*?)<\/Wide>/gi; |
|
|
let match; |
|
|
while ((match = widePattern.exec(content)) !== null) { |
|
|
if (position >= match.index && position < match.index + match[0].length) { |
|
|
return true; |
|
|
} |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function extractAllVisualsWithPosition(rawContent) { |
|
|
const visuals = []; |
|
|
|
|
|
|
|
|
const imageImports = parseImageImports(rawContent); |
|
|
|
|
|
|
|
|
const content = stripCodeBlocks(rawContent); |
|
|
|
|
|
|
|
|
const embedPattern = /<HtmlEmbed/gi; |
|
|
let match; |
|
|
while ((match = embedPattern.exec(content)) !== null) { |
|
|
const position = match.index; |
|
|
|
|
|
let pos = position + 10; |
|
|
let tagContent = '<HtmlEmbed'; |
|
|
let inString = false; |
|
|
let stringDelim = null; |
|
|
let inJSXBraces = 0; |
|
|
|
|
|
while (pos < content.length) { |
|
|
const char = content[pos]; |
|
|
const prevChar = pos > 0 ? content[pos - 1] : ''; |
|
|
tagContent += char; |
|
|
|
|
|
if (!inString) { |
|
|
if ((char === '`' || char === '"' || char === "'") && prevChar !== '\\') { |
|
|
inString = true; |
|
|
stringDelim = char; |
|
|
} |
|
|
} else { |
|
|
if (char === stringDelim && prevChar !== '\\') { |
|
|
inString = false; |
|
|
stringDelim = null; |
|
|
} |
|
|
} |
|
|
|
|
|
if (!inString) { |
|
|
if (char === '{') inJSXBraces++; |
|
|
else if (char === '}') inJSXBraces--; |
|
|
} |
|
|
|
|
|
if (!inString && inJSXBraces === 0 && char === '/' && pos + 1 < content.length && content[pos + 1] === '>') { |
|
|
tagContent += '>'; |
|
|
break; |
|
|
} |
|
|
pos++; |
|
|
} |
|
|
|
|
|
|
|
|
const src = extractAttrFromTag('src', tagContent); |
|
|
if (src) { |
|
|
const title = extractAttrFromTag('title', tagContent); |
|
|
const desc = extractAttrFromTag('desc', tagContent); |
|
|
const id = extractAttrFromTag('id', tagContent); |
|
|
const data = extractAttrFromTag('data', tagContent); |
|
|
const frameless = /\bframeless\b/i.test(tagContent); |
|
|
const wideAttr = /\bwide\b/i.test(tagContent); |
|
|
const skipGallery = /\bskipGallery\b/i.test(tagContent); |
|
|
|
|
|
|
|
|
let config = null; |
|
|
const jsxConfigMatch = tagContent.match(/config\s*=\s*\{\{/i); |
|
|
if (jsxConfigMatch) { |
|
|
try { |
|
|
const configStart = tagContent.indexOf('{{', jsxConfigMatch.index) + 2; |
|
|
let braceCount = 1; |
|
|
let configEnd = configStart; |
|
|
for (let i = configStart; i < tagContent.length && braceCount > 0; i++) { |
|
|
if (tagContent[i] === '{') braceCount++; |
|
|
if (tagContent[i] === '}') braceCount--; |
|
|
if (braceCount === 0) configEnd = i; |
|
|
} |
|
|
const jsxContent = tagContent.substring(configStart, configEnd).trim(); |
|
|
config = new Function('return ({' + jsxContent + '})')(); |
|
|
} catch (e) { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
const isWide = isPositionInsideWide(content, position) || wideAttr; |
|
|
|
|
|
visuals.push({ |
|
|
type: 'embed', |
|
|
position, |
|
|
src, |
|
|
title, |
|
|
desc, |
|
|
id, |
|
|
data, |
|
|
frameless, |
|
|
config, |
|
|
wide: isWide, |
|
|
skipGallery |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const stackBlocks = []; |
|
|
const stackPattern = /<Stack([\s\S]*?)>([\s\S]*?)<\/Stack>/gi; |
|
|
while ((match = stackPattern.exec(content)) !== null) { |
|
|
const stackAttrs = match[1]; |
|
|
const stackContent = match[2]; |
|
|
const stackStart = match.index; |
|
|
const stackEnd = stackStart + match[0].length; |
|
|
|
|
|
|
|
|
const innerImages = []; |
|
|
const innerImagePattern = /<Image([^>]*)\/?>/gi; |
|
|
let imgMatch; |
|
|
while ((imgMatch = innerImagePattern.exec(stackContent)) !== null) { |
|
|
const tag = imgMatch[0]; |
|
|
const srcM = tag.match(/src\s*=\s*\{([^}]+)\}/i); |
|
|
if (srcM) { |
|
|
const varName = srcM[1].trim(); |
|
|
const altM = tag.match(/alt\s*=\s*["']([^"']+)["']/i); |
|
|
const captionM = tag.match(/caption\s*=\s*["']([^"']+)["']/i); |
|
|
const imgSkipGallery = /\bskipGallery\b/i.test(tag); |
|
|
innerImages.push({ |
|
|
src: varName, |
|
|
resolvedFilename: imageImports.get(varName) || null, |
|
|
alt: altM ? altM[1] : 'Image', |
|
|
caption: captionM ? captionM[1] : null, |
|
|
skipGallery: imgSkipGallery, |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
if (innerImages.length > 0) { |
|
|
|
|
|
const layoutM = stackAttrs.match(/layout\s*=\s*["']([^"']+)["']/i); |
|
|
const gapM = stackAttrs.match(/gap\s*=\s*["']([^"']+)["']/i); |
|
|
|
|
|
|
|
|
const allSkipped = innerImages.every(img => img.skipGallery); |
|
|
|
|
|
stackBlocks.push({ start: stackStart, end: stackEnd }); |
|
|
visuals.push({ |
|
|
type: 'stack', |
|
|
position: stackStart, |
|
|
images: innerImages, |
|
|
layout: layoutM ? layoutM[1] : '2-column', |
|
|
gap: gapM ? gapM[1] : 'medium', |
|
|
skipGallery: allSkipped, |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const isInsideStack = (pos) => { |
|
|
return stackBlocks.some(b => pos >= b.start && pos < b.end); |
|
|
}; |
|
|
|
|
|
|
|
|
const imagePattern = /<Image[^>]*\/>/gi; |
|
|
while ((match = imagePattern.exec(content)) !== null) { |
|
|
|
|
|
if (isInsideStack(match.index)) continue; |
|
|
|
|
|
const srcMatch = match[0].match(/src\s*=\s*\{([^}]+)\}/i); |
|
|
if (srcMatch) { |
|
|
const varName = srcMatch[1].trim(); |
|
|
const altMatch = match[0].match(/alt\s*=\s*["']([^"']+)["']/i); |
|
|
const captionMatch = match[0].match(/caption\s*=\s*["']([^"']+)["']/i); |
|
|
const skipGallery = /\bskipGallery\b/i.test(match[0]); |
|
|
const resolvedFilename = imageImports.get(varName) || null; |
|
|
visuals.push({ |
|
|
type: 'image', |
|
|
position: match.index, |
|
|
src: varName, |
|
|
resolvedFilename, |
|
|
alt: altMatch ? altMatch[1] : 'Image', |
|
|
caption: captionMatch ? captionMatch[1] : null, |
|
|
skipGallery, |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const tablePattern = /(\|[^\n]+\|\n\|[-:\s|]+\|\n(?:\|[^\n]+\|\n?)+)/g; |
|
|
let tableIndex = 0; |
|
|
while ((match = tablePattern.exec(content)) !== null) { |
|
|
const tableContent = match[1].trim(); |
|
|
const rows = tableContent.split('\n').filter(row => row.trim()); |
|
|
|
|
|
if (rows.length >= 3) { |
|
|
const headerRow = rows[0]; |
|
|
const headers = splitTableRow(headerRow) |
|
|
.filter(cell => cell.trim()) |
|
|
.map(cell => markdownToHtml(cell.trim())); |
|
|
|
|
|
const expectedCols = headers.length; |
|
|
|
|
|
const dataRows = rows.slice(2).map(row => { |
|
|
return parseTableRow(row, expectedCols) |
|
|
.map(cell => markdownToHtml(cell.trim())); |
|
|
}); |
|
|
|
|
|
visuals.push({ |
|
|
type: 'table', |
|
|
position: match.index, |
|
|
id: `table-${tableIndex++}`, |
|
|
headers, |
|
|
rows: dataRows, |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
visuals.sort((a, b) => a.position - b.position); |
|
|
|
|
|
return visuals; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function loadAllVisualsFromMDX() { |
|
|
const __filename = fileURLToPath(import.meta.url); |
|
|
const __dirname = dirname(__filename); |
|
|
|
|
|
let contentDir = join(__dirname, '../content'); |
|
|
if (!statSync(contentDir, { throwIfNoEntry: false })) { |
|
|
contentDir = join(__dirname, '../../src/content'); |
|
|
} |
|
|
if (!statSync(contentDir, { throwIfNoEntry: false })) { |
|
|
contentDir = join(__dirname, '../../../src/content'); |
|
|
} |
|
|
|
|
|
const allVisuals = []; |
|
|
const articleFile = join(contentDir, 'article.mdx'); |
|
|
|
|
|
try { |
|
|
const articleContent = readFileSync(articleFile, 'utf-8'); |
|
|
|
|
|
|
|
|
const articleVisuals = extractAllVisualsWithPosition(articleContent); |
|
|
articleVisuals.forEach(item => { |
|
|
item.sourceFile = 'content/article.mdx'; |
|
|
}); |
|
|
allVisuals.push(...articleVisuals); |
|
|
|
|
|
|
|
|
const chapterOrder = parseArticleChapters(articleContent, contentDir); |
|
|
|
|
|
for (const chapterPath of chapterOrder) { |
|
|
try { |
|
|
const chapterContent = readFileSync(chapterPath, 'utf-8'); |
|
|
|
|
|
|
|
|
const chapterVisuals = extractAllVisualsWithPosition(chapterContent); |
|
|
const relativePath = relative(contentDir, chapterPath); |
|
|
chapterVisuals.forEach(item => { |
|
|
item.sourceFile = `content/${relativePath}`; |
|
|
}); |
|
|
allVisuals.push(...chapterVisuals); |
|
|
} catch (error) { |
|
|
console.error(`Error reading chapter ${chapterPath}:`, error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const allMdxFiles = findMdxFiles(contentDir, contentDir, [], false); |
|
|
const processedFiles = new Set([articleFile, ...chapterOrder]); |
|
|
|
|
|
for (const filePath of allMdxFiles) { |
|
|
if (!processedFiles.has(filePath)) { |
|
|
try { |
|
|
const rawContent = readFileSync(filePath, 'utf-8'); |
|
|
const fileVisuals = extractAllVisualsWithPosition(rawContent); |
|
|
const relativePath = relative(contentDir, filePath); |
|
|
fileVisuals.forEach(item => { |
|
|
item.sourceFile = `content/${relativePath}`; |
|
|
}); |
|
|
allVisuals.push(...fileVisuals); |
|
|
} catch (error) { |
|
|
console.error(`Error reading ${filePath}:`, error); |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error processing article:', error); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return allVisuals; |
|
|
} |
|
|
|