UNPKG

@entro314labs/starlight-document-converter

Version:

A comprehensive document converter for Astro Starlight that transforms various document formats into Starlight-compatible Markdown with proper frontmatter

347 lines (346 loc) 11.7 kB
// src/plugins/built-in/link-image-processor.ts import { copyFile, mkdir, stat } from "fs/promises"; import { basename, dirname, extname, join, relative, resolve } from "path"; var LinkImageProcessor = class { baseDir; outputDir; assetsDir; logger; constructor(baseDir, outputDir, assetsDir = "assets", logger) { this.baseDir = baseDir; this.outputDir = outputDir; this.assetsDir = assetsDir; this.logger = logger; } /** * Process all links and images in markdown content */ async processContent(content, sourceFilePath, targetFilePath) { const links = []; const images = []; let processedContent = await this.processImages(content, sourceFilePath, targetFilePath, images); processedContent = await this.processLinks( processedContent, sourceFilePath, targetFilePath, links ); return { content: processedContent, links, images }; } /** * Process and fix internal links */ async processLinks(content, sourceFilePath, targetFilePath, links) { const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; let processedContent = content; const replacements = []; let match; while ((match = linkRegex.exec(content)) !== null) { const [fullMatch, linkText, linkUrl] = match; if (linkUrl.startsWith("http") || linkUrl.startsWith("mailto:") || linkUrl.startsWith("#")) { links.push({ original: linkUrl, resolved: linkUrl, isInternal: false, exists: true, // Assume external links exist needsRepair: false }); continue; } const linkInfo = await this.processInternalLink(linkUrl, sourceFilePath, targetFilePath); links.push(linkInfo); if (linkInfo.needsRepair && linkInfo.resolved !== linkInfo.original) { replacements.push({ original: fullMatch, replacement: `[${linkText}](${linkInfo.resolved})` }); } } for (const replacement of replacements) { processedContent = processedContent.replace(replacement.original, replacement.replacement); } return processedContent; } /** * Process and copy images */ async processImages(content, sourceFilePath, targetFilePath, images) { const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; let processedContent = content; const replacements = []; let match; while ((match = imageRegex.exec(content)) !== null) { const [fullMatch, altText, imagePath] = match; if (imagePath.startsWith("http")) { images.push({ original: imagePath, resolved: imagePath, copied: false, alt: altText }); continue; } const imageInfo = await this.processImage(imagePath, altText, sourceFilePath, targetFilePath); images.push(imageInfo); if (imageInfo.copied && imageInfo.outputPath) { const newImagePath = this.getRelativeImagePath(imageInfo.outputPath, targetFilePath); replacements.push({ original: fullMatch, replacement: `![${altText || basename(imageInfo.original, extname(imageInfo.original))}](${newImagePath})` }); } } for (const replacement of replacements) { processedContent = processedContent.replace(replacement.original, replacement.replacement); } return processedContent; } /** * Process a single internal link */ async processInternalLink(linkUrl, sourceFilePath, targetFilePath) { const sourceDir = dirname(sourceFilePath); let resolvedPath = linkUrl; let exists = false; let needsRepair = false; try { if (linkUrl.startsWith("./") || linkUrl.startsWith("../") || !linkUrl.startsWith("/")) { resolvedPath = resolve(sourceDir, linkUrl); } else { resolvedPath = resolve(this.baseDir, linkUrl.substring(1)); } const possiblePaths = [ resolvedPath, `${resolvedPath}.md`, `${resolvedPath}.mdx`, join(resolvedPath, "index.md"), join(resolvedPath, "index.mdx") ]; for (const path of possiblePaths) { try { await stat(path); resolvedPath = path; exists = true; break; } catch { } } if (exists) { const targetDir = dirname(targetFilePath); const relativePath = relative(targetDir, resolvedPath); let starlightPath = relativePath.replace(/\.mdx?$/, "").replace(/\/index$/, "").replace(/\\/g, "/"); if (!(starlightPath.startsWith("./") || starlightPath.startsWith("../"))) { starlightPath = `./${starlightPath}`; } resolvedPath = starlightPath; needsRepair = starlightPath !== linkUrl; } else { needsRepair = true; } } catch (error) { needsRepair = true; } return { original: linkUrl, resolved: resolvedPath, isInternal: true, exists, needsRepair }; } /** * Process and copy a single image */ async processImage(imagePath, altText, sourceFilePath, targetFilePath) { const sourceDir = dirname(sourceFilePath); let resolvedPath = imagePath; let copied = false; let outputPath; const possiblePaths = []; try { if (imagePath.startsWith("./") || imagePath.startsWith("../") || !imagePath.startsWith("/")) { possiblePaths.push(resolve(sourceDir, imagePath)); possiblePaths.push(resolve(sourceDir, "images", basename(imagePath))); possiblePaths.push(resolve(sourceDir, "assets", basename(imagePath))); possiblePaths.push(resolve(this.baseDir, "images", basename(imagePath))); possiblePaths.push(resolve(this.baseDir, "assets", basename(imagePath))); } else { possiblePaths.push(resolve(this.baseDir, imagePath.substring(1))); } if (!imagePath.includes("/")) { possiblePaths.push(resolve(sourceDir, "images", imagePath)); possiblePaths.push(resolve(sourceDir, "assets", imagePath)); possiblePaths.push(resolve(this.baseDir, "images", imagePath)); possiblePaths.push(resolve(this.baseDir, "assets", imagePath)); possiblePaths.push(resolve(this.baseDir, "src", "assets", imagePath)); possiblePaths.push(resolve(this.baseDir, "public", imagePath)); } let foundPath = null; for (const path of possiblePaths) { try { await stat(path); foundPath = path; resolvedPath = path; break; } catch { } } if (foundPath) { const imageName = basename(foundPath); const imageExt = extname(imageName); const imageBase = basename(imageName, imageExt); const assetsPath = join(dirname(this.outputDir), this.assetsDir); await mkdir(assetsPath, { recursive: true }); let finalImageName = imageName; let counter = 1; while (true) { const candidatePath = join(assetsPath, finalImageName); try { await stat(candidatePath); finalImageName = `${imageBase}-${counter}${imageExt}`; counter++; } catch { outputPath = candidatePath; break; } } await copyFile(foundPath, outputPath); copied = true; } else { resolvedPath = imagePath; if (this.logger) { this.logger.warn(`Image not found: ${imagePath}`); this.logger.warn(`Searched in: ${possiblePaths.join(", ")}`); this.logger.warn("Consider running with --process-images flag or ensuring images are in the correct location"); } } } catch (error) { resolvedPath = imagePath; if (this.logger) { this.logger.error(`Error processing image ${imagePath}:`, error); } } return { original: imagePath, resolved: resolvedPath, copied, outputPath, alt: altText }; } /** * Get relative path for image in markdown */ getRelativeImagePath(imagePath, targetFilePath) { const targetDir = dirname(targetFilePath); let relativePath = relative(targetDir, imagePath); relativePath = relativePath.replace(/\\/g, "/"); if (relativePath.includes("../assets/")) { const imageName = basename(imagePath); return relativePath; } return relativePath; } /** * Generate an image report */ generateImageReport(images) { const external = images.filter((img) => img.original.startsWith("http")).length; const copied = images.filter((img) => img.copied).length; const missing = images.filter((img) => !img.copied && !img.original.startsWith("http")).length; const missingImages = images.filter((img) => !img.copied && !img.original.startsWith("http")).map((img) => img.original); return { total: images.length, copied, external, missing, missingImages }; } /** * Generate suggestions for missing images */ generateImageSuggestions(missingImages) { const suggestions = []; if (missingImages.length > 0) { suggestions.push("\u{1F5BC}\uFE0F Missing Images Found"); suggestions.push(""); suggestions.push("The following images could not be found during conversion:"); suggestions.push(""); missingImages.forEach((img, index) => { suggestions.push(`${index + 1}. ${img}`); }); suggestions.push(""); suggestions.push("\u{1F4A1} To fix this:"); suggestions.push("1. Ensure images exist in one of these locations:"); suggestions.push(" \u2022 Same directory as the source file"); suggestions.push(" \u2022 ./images/ subdirectory"); suggestions.push(" \u2022 ./assets/ subdirectory"); suggestions.push(" \u2022 Project root /images/ or /assets/"); suggestions.push(" \u2022 /src/assets/ directory"); suggestions.push(" \u2022 /public/ directory"); suggestions.push(""); suggestions.push("2. Or run the conversion with --process-images flag"); suggestions.push(""); suggestions.push("3. For Astro projects, consider placing images in:"); suggestions.push(" \u2022 src/assets/ for processed images"); suggestions.push(" \u2022 public/ for static images"); } return suggestions; } /** * Generate a link report */ generateLinkReport(links) { const internal = links.filter((l) => l.isInternal); const external = links.filter((l) => !l.isInternal); const broken = links.filter((l) => l.isInternal && !l.exists); const repaired = links.filter((l) => l.needsRepair && l.exists); return { total: links.length, internal: internal.length, external: external.length, broken: broken.length, repaired: repaired.length }; } /** * Extract all images from content for batch processing */ static extractImages(content) { const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; const images = []; let match; while ((match = imageRegex.exec(content)) !== null) { images.push({ alt: match[1] || "", src: match[2] }); } return images; } /** * Extract all links from content for batch processing */ static extractLinks(content) { const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; const links = []; let match; while ((match = linkRegex.exec(content)) !== null) { links.push({ text: match[1], url: match[2] }); } return links; } }; export { LinkImageProcessor }; //# sourceMappingURL=chunk-HLRP77HC.js.map