209 lines
7.5 KiB
TypeScript
209 lines
7.5 KiB
TypeScript
/**
|
||
* 文件:content-processor.ts
|
||
* 作用:内容处理器,负责处理markdown内容的各种转换
|
||
*/
|
||
|
||
import { TFile, App } from 'obsidian';
|
||
import { ErrorHandler } from './error-handler';
|
||
|
||
export interface ProcessorOptions {
|
||
enableImageToBase64?: boolean;
|
||
enableLinkProcessing?: boolean;
|
||
enableCodeHighlight?: boolean;
|
||
enableMathProcessing?: boolean;
|
||
platform?: string;
|
||
}
|
||
|
||
export class ContentProcessor {
|
||
private app: App;
|
||
|
||
constructor(app: App) {
|
||
this.app = app;
|
||
}
|
||
|
||
/**
|
||
* 处理图片链接,转换为base64或平台URL
|
||
*/
|
||
async processImages(
|
||
content: string,
|
||
file: TFile,
|
||
options: ProcessorOptions = {}
|
||
): Promise<string> {
|
||
return await ErrorHandler.withErrorHandling(async () => {
|
||
const { enableImageToBase64 = true } = options;
|
||
|
||
if (!enableImageToBase64) {
|
||
return content;
|
||
}
|
||
|
||
// WikiLink 图片处理: ![[image.png]]
|
||
content = await this.processWikiLinkImages(content, file);
|
||
|
||
// Markdown 图片处理: 
|
||
content = await this.processMarkdownImages(content, file);
|
||
|
||
return content;
|
||
}, 'ContentProcessor.processImages', content) || content;
|
||
}
|
||
|
||
/**
|
||
* 处理链接
|
||
*/
|
||
processLinks(content: string, linkStyle: 'inline' | 'footnote' = 'inline'): string {
|
||
return ErrorHandler.withErrorHandlingSync(() => {
|
||
if (linkStyle === 'footnote') {
|
||
return this.convertLinksToFootnotes(content);
|
||
}
|
||
return this.processInlineLinks(content);
|
||
}, 'ContentProcessor.processLinks', content) || content;
|
||
}
|
||
|
||
/**
|
||
* 处理代码块高亮
|
||
*/
|
||
processCodeBlocks(content: string, highlightTheme: string = 'default'): string {
|
||
return ErrorHandler.withErrorHandlingSync(() => {
|
||
// 为代码块添加语法高亮类
|
||
return content.replace(
|
||
/```(\w+)?\n([\s\S]*?)```/g,
|
||
(match, lang, code) => {
|
||
const language = lang || 'text';
|
||
return `<div class="code-section">
|
||
<pre><code class="language-${language}">${this.escapeHtml(code.trim())}</code></pre>
|
||
</div>`;
|
||
}
|
||
);
|
||
}, 'ContentProcessor.processCodeBlocks', content) || content;
|
||
}
|
||
|
||
/**
|
||
* 处理数学公式
|
||
*/
|
||
processMath(content: string, mathEngine: 'latex' | 'asciimath' = 'latex'): string {
|
||
return ErrorHandler.withErrorHandlingSync(() => {
|
||
// 行内公式: $...$
|
||
content = content.replace(/\$([^$]+)\$/g, (match, formula) => {
|
||
return `<span class="math-inline" data-engine="${mathEngine}">${formula}</span>`;
|
||
});
|
||
|
||
// 块级公式: $$...$$
|
||
content = content.replace(/\$\$([^$]+)\$\$/g, (match, formula) => {
|
||
return `<div class="math-block" data-engine="${mathEngine}">${formula}</div>`;
|
||
});
|
||
|
||
return content;
|
||
}, 'ContentProcessor.processMath', content) || content;
|
||
}
|
||
|
||
/**
|
||
* 处理Gallery短代码
|
||
*/
|
||
async processGalleryShortcode(content: string, galleryPath: string, numPics: number = 2): Promise<string> {
|
||
return await ErrorHandler.withErrorHandling(async () => {
|
||
const galleryRegex = /{{<gallery\s+dir="([^"]+)"(?:\s+figcaption="([^"]*)")?(?:\s+mppickall=(?:"(1|0)"|'(1|0)'|(1|0)))?\s*\/?>}}\s*{{<load-photoswipe>}}/g;
|
||
|
||
return content.replace(galleryRegex, (match, dir, figcaption, q1, q2, unquoted) => {
|
||
const pickAll = q1 === '1' || q2 === '1' || unquoted === '1';
|
||
const maxPics = pickAll ? 999 : numPics;
|
||
|
||
// 这里应该调用实际的图片列表获取逻辑
|
||
// 为了简化,返回占位符
|
||
return `<!-- Gallery: ${dir}, max: ${maxPics}, caption: ${figcaption || ''} -->`;
|
||
});
|
||
}, 'ContentProcessor.processGalleryShortcode', content) || content;
|
||
}
|
||
|
||
/**
|
||
* 清理HTML标签
|
||
*/
|
||
sanitizeHtml(content: string, allowedTags: string[] = []): string {
|
||
return ErrorHandler.withErrorHandlingSync(() => {
|
||
const allowedTagsSet = new Set(allowedTags);
|
||
|
||
return content.replace(/<[^>]*>/g, (tag) => {
|
||
const tagName = tag.match(/<\/?(\w+)/)?.[1]?.toLowerCase();
|
||
if (tagName && allowedTagsSet.has(tagName)) {
|
||
return tag;
|
||
}
|
||
return '';
|
||
});
|
||
}, 'ContentProcessor.sanitizeHtml', content) || content;
|
||
}
|
||
|
||
/**
|
||
* 处理自定义语法扩展
|
||
*/
|
||
processCustomSyntax(content: string): string {
|
||
return ErrorHandler.withErrorHandlingSync(() => {
|
||
// 斜体标注: [fig 一段说明 /]
|
||
content = content.replace(/\[fig\s+([^/]+)\s+\/\]/g,
|
||
'<span style="font-style:italic;color:#666;font-size:0.9em;">$1</span>');
|
||
|
||
// 彩色提示块
|
||
content = content.replace(/^\|\|([rgby]?)\s+(.+)$/gm, (match, color, text) => {
|
||
const colorMap: Record<string, string> = {
|
||
'r': 'background:#8B4513;color:white',
|
||
'g': 'background:#9ACD32;color:black',
|
||
'b': 'background:#D3D3D3;color:black',
|
||
'y': 'background:#FFFF99;color:black',
|
||
'': 'background:#F5F5F5;color:black'
|
||
};
|
||
const style = colorMap[color] || colorMap[''];
|
||
return `<div style="padding:8px;margin:4px 0;border-radius:4px;${style}">${text}</div>`;
|
||
});
|
||
|
||
return content;
|
||
}, 'ContentProcessor.processCustomSyntax', content) || content;
|
||
}
|
||
|
||
// 私有辅助方法
|
||
|
||
private async processWikiLinkImages(content: string, file: TFile): Promise<string> {
|
||
const wikiImageRegex = /!\[\[([^\]]+)\]\]/g;
|
||
|
||
return content.replace(wikiImageRegex, (match, imagePath) => {
|
||
// 这里应该实现实际的图片处理逻辑
|
||
return `<img src="data:image/png;base64,placeholder" alt="${imagePath}">`;
|
||
});
|
||
}
|
||
|
||
private async processMarkdownImages(content: string, file: TFile): Promise<string> {
|
||
const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||
|
||
return content.replace(markdownImageRegex, (match, alt, src) => {
|
||
// 这里应该实现实际的图片处理逻辑
|
||
return `<img src="data:image/png;base64,placeholder" alt="${alt}">`;
|
||
});
|
||
}
|
||
|
||
private convertLinksToFootnotes(content: string): string {
|
||
const links: string[] = [];
|
||
|
||
// 提取所有链接
|
||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
||
links.push(url);
|
||
return `${text}[${links.length}]`;
|
||
});
|
||
|
||
// 添加脚注
|
||
if (links.length > 0) {
|
||
content += '\n\n---\n\n';
|
||
links.forEach((url, index) => {
|
||
content += `[${index + 1}]: ${url}\n`;
|
||
});
|
||
}
|
||
|
||
return content;
|
||
}
|
||
|
||
private processInlineLinks(content: string): string {
|
||
return content.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
||
'<a href="$2" style="color:#1e6bb8;text-decoration:none;">$1</a>');
|
||
}
|
||
|
||
private escapeHtml(text: string): string {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
} |