wiki_ghostguild/app/server/plugins/wikilink-transform.ts

91 lines
3 KiB
TypeScript

/**
* Wikilink Transformation Plugin
*
* Transforms Obsidian wikilink syntax to standard markdown links at build time.
* Handles:
* - [[Page Title]] → [Page Title](/articles/page-title)
* - [[Page|Display Text]] → [Display Text](/articles/page)
* - ![[image.jpg]] → ![image.jpg](/img/image.jpg)
* - ![[image.jpg|alt text]] → ![alt text](/img/image.jpg)
*
* Runs during content:file:beforeParse hook for early transformation
*/
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('content:file:beforeParse' as any, (file: any) => {
// Only process markdown files in articles collection
if (file._id && file._id.endsWith('.md') && file._id.includes('/articles/')) {
if (file.body) {
file.body = transformWikilinks(file.body);
file.body = transformImageEmbeds(file.body);
}
}
});
});
/**
* Transform Obsidian wikilinks to markdown links
* Patterns:
* - [[P0 - Page Title]]
* - [[Page Title|Display Text]]
* - [[P1 - Long Title|Short]]
*/
function transformWikilinks(content: string): string {
// Pattern: [[P0/P1/P2 - Page Title]] or [[Page Title]] with optional |Display Text
const wikilinkPattern = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
return content.replace(wikilinkPattern, (match, pageName, displayText) => {
// Clean the page name: strip P0/P1/P2 prefix
const cleanName = pageName.replace(/^P[012]\s*-\s*/i, '').trim();
// Generate slug from clean name
const slug = titleToSlug(cleanName);
// Use display text if provided, otherwise use clean name
const linkText = displayText ? displayText.trim() : cleanName;
// Generate markdown link with /articles/ prefix
return `[${linkText}](/articles/${slug})`;
});
}
/**
* Transform Obsidian image embeds to markdown image syntax
* Patterns:
* - ![[image.jpg]]
* - ![[image.jpg|alt text]]
*/
function transformImageEmbeds(content: string): string {
// Pattern: ![[filename]] or ![[filename|alt text]]
const imagePattern = /!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
return content.replace(imagePattern, (match, filename, altText) => {
// Use alt text if provided, otherwise use filename (without extension)
const alt = altText?.trim() || filename.replace(/\.(jpg|jpeg|png|gif|svg|webp)$/i, '');
// Generate markdown image syntax with /img/ prefix
return `![${alt}](/img/${filename})`;
});
}
/**
* Convert a page title to a URL slug
* Rules:
* 1. Convert to lowercase
* 2. Remove quotation marks
* 3. Replace & with "and"
* 4. Remove other special characters (keep alphanumeric and hyphens)
* 5. Replace spaces with hyphens
* 6. Collapse multiple hyphens to single hyphen
* 7. Trim hyphens from start/end
*/
function titleToSlug(title: string): string {
return title
.toLowerCase()
.replace(/['"]/g, '') // Remove quotes
.replace(/&/g, 'and') // & → and
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Spaces → hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.replace(/^-|-$/g, ''); // Trim hyphens from edges
}