| 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 raw = []; |
| 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) { |
| raw.push(current); |
| current = ''; |
| } else { |
| current += ch; |
| } |
| } |
| raw.push(current); |
| |
| |
| |
| let start = 0; |
| let end = raw.length - 1; |
| while (start <= end && !raw[start].trim()) start++; |
| while (end >= start && !raw[end].trim()) end--; |
| |
| return raw.slice(start, end + 1).map(c => c.trim()); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function parseTableRow(row, expectedCols) { |
| let cells = splitTableRow(row); |
| |
| 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 !== '') |
| .map(cell => markdownToHtml(cell)); |
| |
| const expectedCols = headers.length; |
| |
| |
| |
| |
| const dataRows = rows.slice(2).map(row => { |
| return parseTableRow(row, expectedCols) |
| .map(cell => markdownToHtml(cell)); |
| }); |
| |
| 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 !== '') |
| .map(cell => markdownToHtml(cell)); |
| |
| const expectedCols = headers.length; |
| |
| const dataRows = rows.slice(2).map(row => { |
| return parseTableRow(row, expectedCols) |
| .map(cell => markdownToHtml(cell)); |
| }); |
| |
| 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; |
| } |
|
|