| | import { readFileSync, readdirSync, statSync } from 'fs'; |
| | import { join, relative, dirname } from 'path'; |
| | import { fileURLToPath } from 'url'; |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | export function extractHtmlEmbeds(content) { |
| | const embeds = []; |
| |
|
| | |
| | |
| | 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 = []) { |
| | const entries = readdirSync(dir); |
| |
|
| | for (const entry of entries) { |
| | const fullPath = join(dir, entry); |
| | const stat = statSync(fullPath); |
| |
|
| | if (stat.isDirectory()) { |
| | findMdxFiles(fullPath, baseDir, files); |
| | } 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; |
| | } |
| |
|
| | |
| | |
| | |
| | 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); |
| | 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); |
| | 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 uniqueEmbeds = Array.from( |
| | new Map(allEmbeds.map(e => [e.src, e])).values() |
| | ); |
| |
|
| | return uniqueEmbeds; |
| | } |
| |
|
| |
|