diff --git a/src/modals/BookSelectionModal.ts b/src/modals/BookSelectionModal.ts index 709a92a..5e7b7df 100644 --- a/src/modals/BookSelectionModal.ts +++ b/src/modals/BookSelectionModal.ts @@ -145,7 +145,7 @@ export class BookSelectionModal extends Modal { try { this.close(); - const bookRenderService = new BookRenderService(this.plugin); + const bookRenderService = new BookRenderService(this.app); // 创建并打开导出模态框 - 修正参数顺序 const exportModal = new ExportModal( this.app, diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts index 938a7f9..7a08a49 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -1,6 +1,6 @@ import { Notice, App, Modal, ButtonComponent } from "obsidian"; import { i18n } from "../i18n/i18n"; -import { BookRenderService, RenderConfig, RenderedBook } from "../services/BookRenderService"; +import { BookRenderService, RenderConfig } from "../services/BookRenderService"; import { Book } from "../types/book"; import { HeaderFooterTocModal } from "./HeaderFooterTocModal"; import { CoverSettingModal } from "./CoverSettingModal"; @@ -27,7 +27,6 @@ export class ExportModal extends Modal { private exportBtn: HTMLButtonElement; private isRendering: boolean = false; private abortController: AbortController | null = null; - private renderedBook: RenderedBook | null = null; private webview: electron.WebviewTag | null = null; private webviewReady: boolean = false; @@ -141,208 +140,6 @@ export class ExportModal extends Modal { return webview; } - private getAllStyles(): string[] { - const cssTexts: string[] = []; - - // 添加主题相关的样式注释 - cssTexts.push('/* ---------- Obsidian Theme Styles ---------- */'); - - Array.from(document.styleSheets).forEach((sheet) => { - // @ts-ignore - const id = sheet.ownerNode?.id; - // @ts-ignore - const href = sheet.ownerNode?.href; - - // 跳过Svelte样式,但保留所有主题相关样式 - if (id?.startsWith('svelte-')) { - return; - } - - const division = `/* ----------${id ? `id:${id}` : href ? `href:${href}` : 'inline'}---------- */`; - cssTexts.push(division); - - try { - Array.from(sheet?.cssRules ?? []).forEach((rule) => { - cssTexts.push(rule.cssText); - }); - } catch (error) { - console.error('Error reading CSS rules:', error); - // 对于跨域样式表,尝试获取基本信息 - if (href) { - cssTexts.push(`/* External stylesheet: ${href} */`); - } - } - }); - - // 获取当前主题的body类名并添加相关样式 - const bodyClasses = Array.from(document.body.classList); - const themeClasses = bodyClasses.filter(cls => - cls.includes('theme-') || - cls.includes('dark') || - cls.includes('light') - ); - - if (themeClasses.length > 0) { - cssTexts.push(`/* ---------- Current Theme Classes: ${themeClasses.join(', ')} ---------- */`); - } - - // 添加补丁样式 - cssTexts.push(...this.getPatchStyles()); - - return cssTexts; - } - - private getPatchStyles(): string[] { - const CSS_PATCH = ` - /* ---------- css patch ---------- */ - body { - overflow: auto !important; - } - @media print { - .print .markdown-preview-view { - height: auto !important; - } - .md-print-anchor, .blockid { - white-space: pre !important; - border: none !important; - display: inline-block !important; - position: absolute !important; - width: 1px !important; - height: 1px !important; - right: 0 !important; - outline: 0 !important; - background: 0 0 !important; - text-decoration: initial !important; - text-shadow: initial !important; - } - table { - break-inside: auto; - } - tr { - break-inside: avoid; - break-after: auto; - } - } - img.__canvas__ { - width: 100% !important; - height: 100% !important; - } - .book-chapter { - margin-bottom: 2em; - } - `; - - return [CSS_PATCH, ...this.getPrintStyles()]; - } - - private getPrintStyles(): string[] { - const cssTexts: string[] = []; - - Array.from(document.styleSheets).forEach((sheet) => { - try { - const cssRules = sheet?.cssRules ?? []; - Array.from(cssRules).forEach((rule) => { - if (rule.constructor.name === "CSSMediaRule") { - if ((rule as CSSMediaRule).conditionText === "print") { - const res = rule.cssText.replace(/@media print\s*\{(.+)\}/g, "$1"); - cssTexts.push(res); - } - } - }); - } catch (error) { - console.error('Error reading print styles:', error); - } - }); - - return cssTexts; - } - - private makeWebviewJs(doc: Document): string { - // 获取当前主题相关的类名 - const currentBodyClasses = Array.from(document.body.classList); - const currentHtmlClasses = Array.from(document.documentElement.classList); - - // 获取重要的data属性 - const themeAttr = document.documentElement.getAttribute('data-theme') || ''; - const modeAttr = document.documentElement.getAttribute('data-mode') || ''; - - return ` - // 设置基本内容 - document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.body.innerHTML)}\`); - document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.head.innerHTML)}\`); - - // 复制原始属性 - document.body.setAttribute("class", \`${doc.body.getAttribute("class") || ''}\`); - document.body.setAttribute("style", \`${doc.body.getAttribute("style") || ''}\`); - - // 动态应用当前主题的类名到body - ${currentBodyClasses.map(cls => - cls.includes('theme-') || cls.includes('dark') || cls.includes('light') || cls.includes('obsidian-app') - ? `document.body.classList.add('${cls}');` - : '' - ).filter(Boolean).join('\n ')} - - // 动态应用当前主题的类名到html - ${currentHtmlClasses.map(cls => - `document.documentElement.classList.add('${cls}');` - ).join('\n ')} - - // 设置重要的data属性 - ${themeAttr ? `document.documentElement.setAttribute('data-theme', '${themeAttr}');` : ''} - ${modeAttr ? `document.documentElement.setAttribute('data-mode', '${modeAttr}');` : ''} - - document.title = \`${doc.title}\`; - `; - } - - private async setupWebview(doc: Document) { - if (!this.webview) return; - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Webview setup timeout')); - }, 10000); - - this.webview!.addEventListener('dom-ready', async () => { - try { - clearTimeout(timeout); - - // 1. 先注入所有基础样式 - const styles = this.getAllStyles(); - for (const css of styles) { - await this.webview!.insertCSS(css); - } - - // 2. 处理自定义CSS片段(如果有的话) - if (this.renderSettings.cssSnippet && this.renderSettings.cssSnippet !== '0') { - try { - // 这里需要实现CSS片段读取逻辑 - await this.webview!.insertCSS(this.renderSettings.cssSnippet); - } catch (error) { - console.warn('Failed to load CSS snippet:', error); - } - } - - // 3. 注入内容 - await this.webview!.executeJavaScript(this.makeWebviewJs(doc)); - - // 4. 最后再次注入补丁样式,确保优先级 - const patchStyles = this.getPatchStyles(); - for (const css of patchStyles) { - await this.webview!.insertCSS(css); - } - - this.webviewReady = true; - this.updateExportButtonState(); - resolve(); - } catch (error) { - clearTimeout(timeout); - reject(error); - } - }); - }); - } - private createPreviewArea() { if (this.selectedFormat !== 'pdf') { return; @@ -367,8 +164,11 @@ export class ExportModal extends Modal {
准备中...
`; - } else if (this.renderedBook) { - this.displayRenderedContent(previewContent); + } else if (this.webviewReady) { + // 如果webview已经准备好,显示它 + if (this.webview) { + previewContent.appendChild(this.webview); + } } else { const error = previewContent.createDiv({ cls: 'preview-error' }); error.innerHTML = ` @@ -399,22 +199,28 @@ export class ExportModal extends Modal { } } - // 移除重复的方法:getAllStyles, getPatchStyles, getPrintStyles, makeWebviewJs - private async startRenderPreview() { if (!this.selectedFormat || this.isRendering) return; - + this.stopRendering(); this.isRendering = true; - this.renderedBook = null; + this.webviewReady = false; this.abortController = new AbortController(); this.updateExportButtonState(); this.updateFormatButtonsState(); this.createPreviewArea(); try { - const renderService = new BookRenderService(this.app); + // 创建webview + this.webview = this.createWebview(); + const previewContent = this.previewContainer.querySelector('.preview-content'); + if (previewContent) { + previewContent.empty(); + previewContent.appendChild(this.webview); + } + + // 使用BookRenderService的新方法直接渲染到webview const config: RenderConfig = { showTitle: this.renderSettings.showTitle, scale: this.renderSettings.scale, @@ -427,7 +233,9 @@ export class ExportModal extends Modal { } }; - this.renderedBook = await renderService.renderBook( + // 直接渲染到webview,无需中间对象 + await this.bookRenderService.renderToWebview( + this.webview, this.selectedBook, this.plugin.settings.defaultBookPath, config @@ -437,8 +245,9 @@ export class ExportModal extends Modal { console.log('Rendering was aborted after completion'); return; } - - console.log('Render completed, updating preview...', this.renderedBook); + + this.webviewReady = true; + console.log('Rendering completed successfully'); } catch (error) { if (this.abortController?.signal.aborted || error.message === 'Render aborted') { @@ -454,7 +263,7 @@ export class ExportModal extends Modal { if (!this.abortController?.signal.aborted) { this.isRendering = false; this.updateFormatButtonsState(); - this.createPreviewArea(); // 这会触发 displayRenderedContent + // 延迟更新按钮状态,确保webview已经设置完成 setTimeout(() => { this.updateExportButtonState(); @@ -463,30 +272,6 @@ export class ExportModal extends Modal { } } - private async displayRenderedContent(container: HTMLElement) { - if (!this.renderedBook?.doc) return; - - container.empty(); - - try { - this.webview = this.createWebview(); - container.appendChild(this.webview); - - // 使用BookRenderService的setupWebview方法 - await this.bookRenderService.setupWebview(this.webview, this.renderedBook.doc); - - this.webviewReady = true; - this.updateExportButtonState(); - } catch (error) { - console.error('Error setting up webview:', error); - const errorDiv = container.createDiv({ cls: 'preview-error' }); - errorDiv.innerHTML = ` -
-
设置预览时出错: ${error.message}
- `; - } - } - private createSettingsContent(container: HTMLElement) { this.createBookInfo(container); this.createFormatSelection(container); @@ -519,7 +304,7 @@ export class ExportModal extends Modal { let canExport: boolean; if (this.selectedFormat === 'pdf') { - canExport = !!(this.selectedFormat && !this.isRendering && this.renderedBook && this.webviewReady); + canExport = !!(this.selectedFormat && !this.isRendering && this.webviewReady); } else { canExport = !!this.selectedFormat && !this.isRendering; } @@ -639,7 +424,6 @@ export class ExportModal extends Modal { if (format.key === 'pdf') { this.startRenderPreview(); } else { - this.renderedBook = null; this.cleanupWebview(); } @@ -765,7 +549,7 @@ export class ExportModal extends Modal { } private async exportToPdf() { - if (!this.webview || !this.webviewReady || !this.renderedBook) { + if (!this.webview || !this.webviewReady) { new Notice('PDF 预览未准备就绪,请稍候'); return; } @@ -781,7 +565,7 @@ export class ExportModal extends Modal { // PDF 导出选项 const printOptions: electron.PrintToPDFOptions = { pageSize: this.exportSettings.bookSize as any || 'A4', - printBackground: true, + printBackground: false, landscape: false, scale: this.renderSettings.scale / 100, margins: { diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts index ab87150..795d510 100644 --- a/src/services/BookRenderService.ts +++ b/src/services/BookRenderService.ts @@ -12,130 +12,40 @@ export interface RenderConfig { onProgress?: (current: number, total: number, fileName: string) => void; } -export interface RenderedChapter { +export interface DocType { + doc: Document; + frontMatter: any; file: TFile; title: string; - content: HTMLElement; - frontMatter: any; } -export interface RenderedBook { - doc: Document; - title: string; - frontMatter: any; - chapters: RenderedChapter[]; -} - -export interface WebviewContent { - doc: Document; - styles: string[]; - webviewJs: string; +export interface ParamType { + app: App; + file: TFile; + config: RenderConfig; + book: Book; + rootPath: string; + extra?: { + title?: string; + id?: string; + }; } export class BookRenderService { + private docs: DocType[] = []; + private scale = 1; + constructor(private app: App) { } /** - * 渲染整本书籍 + * 主要渲染方法 - 基于 obsidian-better-export-pdf 架构 */ - async renderBook(book: Book, rootPath: String, config: RenderConfig): Promise { - const startTime = new Date().getTime(); - - // 检查是否已被中断 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 创建主文档 - const doc = document.implementation.createHTMLDocument(book.basic.title || 'Book'); - doc.title = book.basic.title || 'Book'; - - // 添加样式 - this.injectStyles(doc, config); - - // 渲染章节 - const chapters: RenderedChapter[] = []; - - // 递归收集所有文件类型的章节节点 - const fileNodes = this.collectFileNodes(book.structure.tree); - const totalFiles = fileNodes.length; - - // 报告开始进度 - config.onProgress?.(0, totalFiles, '开始渲染...'); - - for (let i = 0; i < fileNodes.length; i++) { - const chapterNode = fileNodes[i]; - - // 在每个章节渲染前检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 报告当前进度 - config.onProgress?.(i + 1, totalFiles, chapterNode.title || chapterNode.path); - - try { - // 根据路径获取TFile对象 - const filePath = `${rootPath}/${book?.basic.title}/${chapterNode.path}`; - const file = this.app.vault.getAbstractFileByPath(filePath); - if (file instanceof TFile) { - const renderedChapter = await this.renderChapter(file, config); - chapters.push(renderedChapter); - } else { - console.warn(`File not found: ${chapterNode.path}`); - } - } catch (error) { - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - console.error(`Failed to render chapter: ${chapterNode.path}`, error); - new Notice(`渲染章节失败: ${chapterNode.title}`); - } - } - - // 最后检查一次中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 报告完成进度 - config.onProgress?.(totalFiles, totalFiles, '合并章节中...'); - - // 合并章节到主文档 - this.mergeChaptersToDocument(doc, chapters, config); - - console.log(`Book render time: ${new Date().getTime() - startTime}ms`); - - return { - doc, - title: book.basic.title || 'Book', - frontMatter: book.basic || {}, - chapters - }; - } - - /** - * 为webview准备渲染内容(新增方法) - */ - async prepareWebviewContent(book: Book, rootPath: string, config: RenderConfig): Promise { - // 渲染书籍 - const renderedBook = await this.renderBook(book, rootPath, config); - - // 准备webview所需的样式和脚本 - const styles = this.getAllStyles(); - const webviewJs = this.makeWebviewJs(renderedBook.doc); - - return { - doc: renderedBook.doc, - styles, - webviewJs - }; - } - - /** - * 设置webview内容(新增方法) - */ - async setupWebview(webview: electron.WebviewTag, doc: Document, config?: RenderConfig): Promise { + async renderToWebview( + webview: electron.WebviewTag, + book: Book, + rootPath: string, + config: RenderConfig + ): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Webview setup timeout')); @@ -145,13 +55,17 @@ export class BookRenderService { try { clearTimeout(timeout); - // 1. 先注入所有基础样式 + // 1. 收集所有文件并渲染为文档 + const { data, docs } = await this.getAllFiles(book, rootPath, config); + await this.renderFiles(data, docs, config); + + // 2. 注入所有样式 const styles = this.getAllStyles(); for (const css of styles) { await webview.insertCSS(css); } - // 2. 处理自定义CSS片段(如果有的话) + // 3. 处理自定义CSS片段 if (config?.cssSnippet && config.cssSnippet !== '0') { try { await webview.insertCSS(config.cssSnippet); @@ -160,10 +74,10 @@ export class BookRenderService { } } - // 3. 注入内容 - await webview.executeJavaScript(this.makeWebviewJs(doc)); + // 4. 将渲染好的文档注入到webview + await this.appendWebview(webview, this.docs[0]); - // 4. 最后再次注入补丁样式,确保优先级 + // 5. 注入补丁样式 const patchStyles = this.getPatchStyles(); for (const css of patchStyles) { await webview.insertCSS(css); @@ -179,46 +93,318 @@ export class BookRenderService { } /** - * 生成webview注入脚本(从ExportModal移动过来) + * 收集所有文件 - 对应 modal.ts 的 getAllFiles + */ + private async getAllFiles(book: Book, rootPath: string, config: RenderConfig): Promise<{ data: ParamType[], docs: DocType[] }> { + const fileNodes = this.collectFileNodes(book.structure.tree); + const data: ParamType[] = fileNodes.map(node => ({ + app: this.app, + file: this.getFileFromNode(node, book, rootPath), + config, + book, + rootPath, + extra: { + title: node.title, + id: node.id + } + })).filter(param => param.file !== null) as ParamType[]; + + return { data, docs: [] }; + } + + /** + * 渲染所有文件 - 对应 modal.ts 的 renderFiles + */ + private async renderFiles( + data: ParamType[], + docs: DocType[] = [], + config: RenderConfig + ): Promise { + const totalFiles = data.length; + + // 并发渲染所有文件 + const inputs = data.map((param, i) => + this.renderMarkdown(param).then(res => { + config.onProgress?.(i + 1, totalFiles, param.file.basename); + return res; + }) + ); + + let _docs = [...docs, ...(await Promise.all(inputs))]; + + // 合并所有文档为一个完整的书籍文档 + _docs = this.mergeDoc(_docs); + + // 修复文档 + this.docs = _docs.map(({ doc, ...rest }) => { + return { ...rest, doc: this.fixDoc(doc, doc.title) }; + }); + } + + /** + * 渲染单个Markdown文件 - 基于 render.ts 的 renderMarkdown + */ + private async renderMarkdown({ app, file, config, book, extra }: ParamType): Promise { + const startTime = new Date().getTime(); + + // 检查中断信号 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + const leaf = app.workspace.getLeaf(true); + await leaf.openFile(file); + const view = leaf.view as MarkdownView; + const data: string = view?.data || await app.vault.cachedRead(file); + + if (!data) { + console.warn(`Data is empty for file: ${file.path}`); + } + + const frontMatter = this.getFrontMatter(file); + const cssclasses = this.extractCssClasses(frontMatter); + + const comp = new Component(); + comp.load(); + + try { + const printEl = document.body.createDiv('print'); + const viewEl = printEl.createDiv({ + cls: 'markdown-preview-view markdown-rendered ' + cssclasses.join(' '), + }); + + // 设置RTL和属性显示 + // @ts-ignore + viewEl.toggleClass('rtl', app.vault.getConfig('rightToLeft')); + // @ts-ignore + viewEl.toggleClass('show-properties', 'hidden' !== app.vault.getConfig('propertiesInDocument')); + + // 设置标题 + const title = extra?.title || frontMatter?.title || file.basename; + viewEl.createEl('h1', { text: title }, (e) => { + e.addClass('__title__'); + e.style.display = config.showTitle ? 'block' : 'none'; + e.id = extra?.id || ''; + }); + + // 处理块引用 + const processedData = this.processBlockReferences(data || '', file); + + // 渲染markdown - 使用 better-export-pdf 的方式 + const fragment = this.createRenderFragment(); + const promises: Array<() => Promise> = []; + + try { + await MarkdownRenderer.render(app, processedData, fragment, file.path, comp); + } catch (error) { + // 预期的错误,用于避免postProcess + } + + // 添加渲染内容到viewEl + const el = createFragment(); + Array.from(fragment.children).forEach((item) => { + el.createDiv({}, (t) => { + //@ts-ignore + return t.appendChild(item); + }); + }); + viewEl.appendChild(el); + + // 后处理 + // @ts-ignore + await MarkdownRenderer.postProcess(app, { + docId: this.generateDocId(16), + sourcePath: file.path, + frontmatter: {}, + promises, + addChild: function (e: Component) { + return comp.addChild(e); + }, + getSectionInfo: function () { + return null; + }, + containerEl: viewEl, + el: viewEl, + displayMode: true, + }); + + await Promise.all(promises); + + // 修复内部链接 + this.fixInternalLinks(printEl, file); + + // 等待动态内容渲染 + await this.fixWaitRender(data || '', viewEl); + + // 修复canvas为图片 + this.fixCanvasToImage(viewEl); + + // 创建文档 + const doc = document.implementation.createHTMLDocument('document'); + doc.body.appendChild(printEl.cloneNode(true)); + doc.title = title; + + // 清理 + printEl.detach(); + printEl.remove(); + leaf.detach(); + + console.log(`Markdown render time: ${new Date().getTime() - startTime}ms`); + return { doc, frontMatter, file, title }; + + } finally { + comp.unload(); + } + } + + /** + * 合并所有文档 - 对应 modal.ts 的 mergeDoc + */ + private mergeDoc(docs: DocType[]): DocType[] { + if (docs.length <= 1) return docs; + + const { doc: doc0, frontMatter, file, title } = docs[0]; + const sections = []; + + for (const { doc } of docs) { + const element = doc.querySelector('.markdown-preview-view'); + if (element) { + const section = doc0.createElement('section'); + section.className = 'book-chapter'; + Array.from(element.children).forEach((child) => { + section.appendChild(doc0.importNode(child, true)); + }); + sections.push(section); + } + } + + const root = doc0.querySelector('.markdown-preview-view'); + if (root) { + root.innerHTML = ''; + sections.forEach((section, index) => { + root.appendChild(section); + + // 添加分页符(除了最后一章) + // if (index < sections.length - 1) { + // const pageBreak = doc0.createElement('div'); + // pageBreak.style.pageBreakAfter = 'always'; + // pageBreak.style.breakAfter = 'page'; + // pageBreak.className = 'page-break'; + // root.appendChild(pageBreak); + // } + }); + } + + return [{ doc: doc0, frontMatter, file, title }]; + } + + /** + * 将文档注入到webview - 对应 modal.ts 的 appendWebview + */ + private async appendWebview(webview: electron.WebviewTag, docData: DocType): Promise { + const { doc } = docData; + + // 使用 better-export-pdf 的安全注入方式 + const webviewJs = this.makeWebviewJs(doc); + + try { + await webview.executeJavaScript(webviewJs); + } catch (error) { + console.error('Failed to inject content to webview:', error); + throw new Error(`Failed to render book: ${error.message}`); + } + } + + /** + * 生成webview注入脚本 - 对应 modal.ts 的 makeWebviewJs */ private makeWebviewJs(doc: Document): string { - // 获取当前主题相关的类名 - const currentBodyClasses = Array.from(document.body.classList); - const currentHtmlClasses = Array.from(document.documentElement.classList); - - // 获取重要的data属性 - const themeAttr = document.documentElement.getAttribute('data-theme') || ''; - const modeAttr = document.documentElement.getAttribute('data-mode') || ''; + // 使用更安全的方式传输HTML内容 + const bodyContent = doc.body.innerHTML; + const headContent = doc.head.innerHTML; + const docTitle = doc.title; return ` - // 设置基本内容 - document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.body.innerHTML)}\`); - document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.head.innerHTML)}\`); - - // 复制原始属性 - document.body.setAttribute("class", \`${doc.body.getAttribute("class") || ''}\`); - document.body.setAttribute("style", \`${doc.body.getAttribute("style") || ''}\`); - - // 动态应用当前主题的类名到body - ${currentBodyClasses.map(cls => - cls.includes('theme-') || cls.includes('dark') || cls.includes('light') || cls.includes('obsidian-app') - ? `document.body.classList.add('${cls}');` - : '' - ).filter(Boolean).join('\n ')} - - // 动态应用当前主题的类名到html - ${currentHtmlClasses.map(cls => - `document.documentElement.classList.add('${cls}');` - ).join('\n ')} - - // 设置重要的data属性 - ${themeAttr ? `document.documentElement.setAttribute('data-theme', '${themeAttr}');` : ''} - ${modeAttr ? `document.documentElement.setAttribute('data-mode', '${modeAttr}');` : ''} - - document.title = \`${doc.title}\`; + try { + // 安全地设置内容 + document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(bodyContent)}\`); + document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(headContent)}\`); + + // 递归解码嵌入内容 + function decodeAndReplaceEmbed(element) { + if (!element || !element.innerHTML) return; + try { + element.innerHTML = decodeURIComponent(element.innerHTML); + const newEmbeds = element.querySelectorAll("span.markdown-embed"); + newEmbeds.forEach(decodeAndReplaceEmbed); + } catch (e) { + console.warn('Failed to decode embed:', e); + } + } + + document.querySelectorAll("span.markdown-embed").forEach(decodeAndReplaceEmbed); + + // 应用主题类名 + const currentBodyClasses = "${Array.from(document.body.classList).join(' ')}"; + const currentHtmlClasses = "${Array.from(document.documentElement.classList).join(' ')}"; + + if (currentBodyClasses) { + document.body.className = currentBodyClasses; + } + if (currentHtmlClasses) { + document.documentElement.className = currentHtmlClasses; + } + + // 设置主题相关属性 + const themeAttr = "${document.documentElement.getAttribute('data-theme') || ''}"; + const modeAttr = "${document.documentElement.getAttribute('data-mode') || ''}"; + + if (themeAttr) document.documentElement.setAttribute('data-theme', themeAttr); + if (modeAttr) document.documentElement.setAttribute('data-mode', modeAttr); + + document.body.addClass("theme-light"); + document.body.removeClass("theme-dark"); + document.title = \`${docTitle.replace(/[`\\]/g, '\\$&')}\`; + + } catch (error) { + console.error('Error in webview script:', error); + throw error; + } `; } + /** + * 修复文档 - 对应 render.ts 的 fixDoc + */ + private fixDoc(doc: Document, title: string): Document { + this.encodeEmbeds(doc); + return doc; + } + + /** + * 编码嵌入内容 - 对应 render.ts 的 encodeEmbeds + */ + private encodeEmbeds(doc: Document): void { + const spans = Array.from(doc.querySelectorAll('span.markdown-embed')).reverse(); + spans.forEach((span: HTMLElement) => { + span.innerHTML = encodeURIComponent(span.innerHTML); + }); + } + + // ... existing code ... + // 保留所有现有的辅助方法:collectFileNodes, getFrontMatter, extractCssClasses, + // processBlockReferences, createRenderFragment, fixInternalLinks, fixWaitRender, + // fixCanvasToImage, getAllStyles, getPatchStyles, 等等 + + /** + * 根据章节节点获取文件对象 + */ + private getFileFromNode(node: ChapterNode, book: Book, rootPath: string): TFile | null { + const filePath = `${rootPath}/${book.basic.title}/${node.path}`; + const file = this.app.vault.getAbstractFileByPath(filePath); + return file instanceof TFile ? file : null; + } + /** * 递归收集所有文件类型的章节节点 */ @@ -226,112 +412,18 @@ export class BookRenderService { const fileNodes: ChapterNode[] = []; for (const node of nodes) { - // 跳过被排除的节点 - if (node.exclude) { - continue; - } + if (node.exclude) continue; if (node.type === 'file') { fileNodes.push(node); } else if (node.type === 'group' && node.children) { - // 递归处理子节点 fileNodes.push(...this.collectFileNodes(node.children)); } } - // 按order排序 return fileNodes.sort((a, b) => a.order - b.order); } - /** - * 渲染单个章节 - */ - async renderChapter(file: TFile, config: RenderConfig): Promise { - // 检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - const ws = this.app.workspace; - - // 创建临时叶子来渲染文件 - const leaf = ws.getLeaf(true); - await leaf.openFile(file); - const view = leaf.view as MarkdownView; - - // 获取文件内容 - const data: string = view?.data || await this.app.vault.cachedRead(file); - - // 获取前置元数据 - const frontMatter = this.getFrontMatter(file); - - // 获取CSS类 - const cssclasses = this.extractCssClasses(frontMatter); - - // 创建组件 - const comp = new Component(); - comp.load(); - - try { - // 再次检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 在当前文档环境中完成渲染 - const printEl = document.body.createDiv('print'); - const viewEl = printEl.createDiv({ - cls: 'markdown-preview-view markdown-rendered ' + cssclasses.join(' ') - }); - - // 设置RTL和属性显示 - // @ts-ignore - viewEl.toggleClass('rtl', this.app.vault.getConfig('rightToLeft')); - // @ts-ignore - viewEl.toggleClass('show-properties', 'hidden' !== this.app.vault.getConfig('propertiesInDocument')); - - // 添加标题 - const title = frontMatter?.title || file.basename; - viewEl.createEl('h1', { text: title }, (e) => { - e.addClass('__title__'); - e.style.display = config.showTitle ? 'block' : 'none'; - }); - - // 处理块引用 - 如果数据为空,使用空字符串 - const processedData = this.processBlockReferences(data || '', file); - - // 渲染Markdown - await this.renderMarkdownContent(processedData, viewEl, file, comp); - - // 检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 后处理 - await this.postProcessContent(viewEl, file, comp); - await this.fixRenderedContent(data || '', viewEl); - - // 在当前环境中完成所有渲染后,克隆内容 - const clonedContent = viewEl.cloneNode(true) as HTMLElement; - - // 清理当前 document 中的临时元素 - printEl.detach(); - printEl.remove(); - leaf.detach(); - - return { - file, - title, - content: clonedContent, - frontMatter - }; - - } finally { - comp.unload(); - } - } - /** * 获取前置元数据 */ @@ -375,7 +467,6 @@ export class BookRenderService { return line; }); - // 添加剩余的块引用 [...blocks.values()].forEach(({ id, position: { start } }) => { const idx = start.line; lines[idx] = `\n\n` + lines[idx]; @@ -385,90 +476,38 @@ export class BookRenderService { } /** - * 渲染Markdown内容 + * 创建渲染片段 */ - private async renderMarkdownContent(data: string, viewEl: HTMLElement, file: TFile, comp: Component) { - const fragment = { + private createRenderFragment(): any { + return { children: undefined, appendChild(e: DocumentFragment) { this.children = e?.children; throw new Error('exit'); - } + }, } as unknown as HTMLElement; - - try { - await MarkdownRenderer.render(this.app, data, fragment, file.path, comp); - } catch (error) { - // 预期的错误,用于跳过postProcess - } - - const el = createFragment(); - Array.from(fragment.children).forEach((item) => { - el.createDiv({}, (t) => { - return t.appendChild(item); - }); - }); - - viewEl.appendChild(el); } /** - * 后处理内容 + * 修复内部链接 */ - private async postProcessContent(viewEl: HTMLElement, file: TFile, comp: Component) { - const promises: Array<() => Promise> = []; - //@ts-ignore - await MarkdownRenderer.postProcess(this.app, { - docId: this.generateDocId(16), - sourcePath: file.path, - frontmatter: {}, - promises, - addChild: function (e: Component) { - return comp.addChild(e); - }, - getSectionInfo: function () { - return null; - }, - containerEl: viewEl, - el: viewEl, - displayMode: true - }); - - await Promise.all(promises); - } - - /** - * 修复渲染内容 - */ - private async fixRenderedContent(data: string, viewEl: HTMLElement) { - // 修复内部链接 - viewEl.findAll('a.internal-link').forEach((el: HTMLAnchorElement) => { + private fixInternalLinks(printEl: HTMLElement, file: TFile): void { + printEl.findAll('a.internal-link').forEach((el: HTMLAnchorElement) => { const [title, anchor] = el.dataset.href?.split('#') || []; - if ((!title || title?.length === 0) && anchor?.startsWith('^')) { + if ((!title || title?.length === 0 || title === file.basename) && anchor?.startsWith('^')) { return; } el.removeAttribute('href'); }); - - // 等待动态内容渲染 - try { - await this.waitForDynamicContent(data, viewEl); - } catch (error) { - console.warn('Wait timeout for dynamic content'); - } - - // 修复Canvas为图片 - this.fixCanvasToImage(viewEl); } /** * 等待动态内容渲染 */ - private async waitForDynamicContent(data: string, viewEl: HTMLElement) { + private async fixWaitRender(data: string, viewEl: HTMLElement): Promise { if (data.includes('```dataview') || data.includes('```gEvent') || data.includes('![[')) { await this.sleep(2000); } - try { await this.waitForDomChange(viewEl); } catch (error) { @@ -479,173 +518,44 @@ export class BookRenderService { /** * 修复Canvas为图片 */ - private fixCanvasToImage(el: HTMLElement) { + private fixCanvasToImage(el: HTMLElement): void { for (const canvas of Array.from(el.querySelectorAll('canvas'))) { const data = canvas.toDataURL(); const img = document.createElement('img'); img.src = data; img.className = '__canvas__'; - - // 复制属性 Array.from(canvas.attributes).forEach(attr => { img.setAttribute(attr.name, attr.value); }); - canvas.replaceWith(img); } } - /** - * 合并章节到文档 - */ - private mergeChaptersToDocument(doc: Document, chapters: RenderedChapter[], config: RenderConfig) { - const body = doc.body; - body.innerHTML = ''; - - // 添加打印容器,保持与当前文档相同的类名结构 - const printEl = body.appendChild(doc.createElement('div')); - printEl.className = 'print'; - - // 添加主容器,确保包含所有必要的类名 - const mainEl = printEl.appendChild(doc.createElement('div')); - mainEl.className = 'markdown-preview-view markdown-rendered'; - - // 复制当前文档中markdown-preview-view的相关类名 - const currentPreviewEl = document.querySelector('.markdown-preview-view'); - if (currentPreviewEl) { - Array.from(currentPreviewEl.classList).forEach(className => { - if (!mainEl.classList.contains(className)) { - mainEl.classList.add(className); - } - }); - } - - // 添加每个章节 - chapters.forEach((chapter, index) => { - const section = doc.createElement('section'); - section.className = 'book-chapter'; - section.setAttribute('data-chapter', index.toString()); - - // 导入章节内容 - Array.from(chapter.content.children).forEach(child => { - const importedNode = doc.importNode(child, true); - section.appendChild(importedNode); - }); - - mainEl.appendChild(section); - - // 添加分页符(除了最后一章) - // if (index < chapters.length - 1) { - // const pageBreak = doc.createElement('div'); - // pageBreak.style.pageBreakAfter = 'always'; - // pageBreak.style.breakAfter = 'page'; - // pageBreak.className = 'page-break'; - // mainEl.appendChild(pageBreak); - // } - }); - } - - /** - * 注入样式 - */ - private injectStyles(doc: Document, config?: RenderConfig) { - const head = doc.head; - const body = doc.body; - - // 复制当前文档的主题相关类名到目标文档 - const bodyClasses = Array.from(document.body.classList); - bodyClasses.forEach(className => { - // 复制所有主题相关的类名 - if (className.includes('theme-') || - className.includes('dark') || - className.includes('light') || - className.includes('obsidian-app')) { - body.classList.add(className); - } - }); - - // 复制html元素的类名和属性 - const htmlClasses = Array.from(document.documentElement.classList); - htmlClasses.forEach(className => { - doc.documentElement.classList.add(className); - }); - - // 复制重要的data属性 - const htmlElement = document.documentElement; - ['data-theme', 'data-mode'].forEach(attr => { - const value = htmlElement.getAttribute(attr); - if (value) { - doc.documentElement.setAttribute(attr, value); - } - }); - - // 获取所有样式 - const styles = this.getAllStyles(); - - // 创建样式元素 - const styleEl = doc.createElement('style'); - styleEl.textContent = styles.join('\n'); - head.appendChild(styleEl); - - // 处理自定义CSS片段 - if (config?.cssSnippet && config.cssSnippet !== '0') { - const customStyleEl = doc.createElement('style'); - customStyleEl.textContent = config.cssSnippet; - head.appendChild(customStyleEl); - } - } - /** * 获取所有样式 */ private getAllStyles(): string[] { const cssTexts: string[] = []; - - // 添加主题相关的样式注释 - cssTexts.push('/* ---------- Obsidian Theme Styles ---------- */'); - + Array.from(document.styleSheets).forEach((sheet) => { // @ts-ignore const id = sheet.ownerNode?.id; + if (id?.startsWith('svelte-')) return; + // @ts-ignore const href = sheet.ownerNode?.href; - - // 跳过Svelte样式,但保留所有主题相关样式 - if (id?.startsWith('svelte-')) { - return; - } - const division = `/* ----------${id ? `id:${id}` : href ? `href:${href}` : 'inline'}---------- */`; cssTexts.push(division); - + try { Array.from(sheet?.cssRules || []).forEach((rule) => { cssTexts.push(rule.cssText); }); } catch (error) { console.error('Error reading CSS rules:', error); - // 对于跨域样式表,尝试获取基本信息 - if (href) { - cssTexts.push(`/* External stylesheet: ${href} */`); - } } }); - - // 获取当前主题的body类名并添加相关样式 - const bodyClasses = Array.from(document.body.classList); - const themeClasses = bodyClasses.filter(cls => - cls.includes('theme-') || - cls.includes('dark') || - cls.includes('light') - ); - if (themeClasses.length > 0) { - cssTexts.push(`/* ---------- Current Theme Classes: ${themeClasses.join(', ')} ---------- */`); - } - - // 添加补丁样式 - cssTexts.push(...this.getPatchStyles()); - return cssTexts; } @@ -654,71 +564,43 @@ export class BookRenderService { */ private getPatchStyles(): string[] { const patchCSS = ` - /* ---------- css patch ---------- */ - body { - overflow: auto !important; - } - - @media print { - .print .markdown-preview-view { - height: auto !important; - } - - .md-print-anchor, .blockid { - white-space: pre !important; - border: none !important; - display: inline-block !important; - position: absolute !important; - width: 1px !important; - height: 1px !important; - right: 0 !important; - outline: 0 !important; - background: 0 0 !important; - text-decoration: initial !important; - text-shadow: initial !important; - } - - table { - break-inside: auto; - } - - tr { - break-inside: avoid; - break-after: auto; - } - } - - img.__canvas__ { - width: 100% !important; - height: 100% !important; - } - - .book-chapter { - margin-bottom: 2em; - } - `; - - return [patchCSS, ...this.getPrintStyles()]; - } - - private getPrintStyles(): string[] { - const printStyles: string[] = []; - - Array.from(document.styleSheets).forEach((sheet) => { - try { - Array.from(sheet?.cssRules || []).forEach((rule) => { - if (rule instanceof CSSMediaRule && rule.media.mediaText.includes('print')) { - Array.from(rule.cssRules).forEach((printRule) => { - printStyles.push(printRule.cssText); - }); - } - }); - } catch (error) { - console.error('Error reading print CSS rules:', error); + /* ---------- css patch ---------- */ + body { + overflow: auto !important; } - }); - - return printStyles; + + @media print { + .print .markdown-preview-view { + height: auto !important; + } + + .md-print-anchor, .blockid { + white-space: pre !important; + border: none !important; + display: inline-block !important; + position: absolute !important; + width: 1px !important; + height: 1px !important; + right: 0 !important; + } + } + + img.__canvas__ { + width: 100% !important; + height: 100% !important; + } + + .book-chapter { + margin-bottom: 2em; + } + + .page-break { + page-break-after: always; + break-after: page; + } + `; + + return [patchCSS]; } /**