@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
JavaScript
// 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: ``
});
}
}
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