diff --git a/backend/app/export/markdown.py b/backend/app/export/markdown.py index 436ebda..7f83986 100644 --- a/backend/app/export/markdown.py +++ b/backend/app/export/markdown.py @@ -178,6 +178,8 @@ class _AstMapper: type="link", node_id=self.next_id(), attributes=attributes, children=self.map_inline(token.get("children", [])), ) + if kind == "inline_html": + return DocumentNode(type="text", node_id=self.next_id(), text=token.get("raw", ""), attributes={"raw_html": True}) if kind == "codespan": return DocumentNode(type="codespan", node_id=self.next_id(), text=token.get("raw", "")) if kind == "image": diff --git a/backend/app/export/service.py b/backend/app/export/service.py index e001bcc..70c351a 100644 --- a/backend/app/export/service.py +++ b/backend/app/export/service.py @@ -411,12 +411,23 @@ async def preview_resources(request: ExportRequest): from app.export.assets import enrich_document from app.plot.parser import parse_source from app.plot.render import render_svg - from app.export.document import Document + from app.export.document import Document, DocumentNode + from html.parser import HTMLParser markdown, _, metadata = await _resolve_source(request.source, True) def prepare(): document = parse_document(markdown) images, plots = [], [] + class HtmlImages(HTMLParser): + def handle_starttag(self, tag, attrs): + if tag == 'img': + src = dict(attrs).get('src') + if src: + visit(DocumentNode(type='image', node_id='html-image', attributes={'src':src})) def visit(node): + if node.type == 'html_block' or node.attributes.get('raw_html'): + parser = HtmlImages(convert_charrefs=True) + parser.feed(node.text) + parser.close() if node.type == 'image': warnings = enrich_document(Document(node_id='pdf-resources', children=[node]), (metadata or {}).get('file_path'), True, request.options, preserve_alpha=True) raw = node.attributes.get('static_png') diff --git a/backend/tests/test_pdf_browser.py b/backend/tests/test_pdf_browser.py index 1a42030..531a8fb 100644 --- a/backend/tests/test_pdf_browser.py +++ b/backend/tests/test_pdf_browser.py @@ -45,3 +45,25 @@ def test_browser_prints_css_without_executing_document_scripts(tmp_path): text=subprocess.check_output(['pdftotext',str(pdf),'-']).decode('utf-8') assert 'Theme Snapshot' in text assert 'EXECUTED' not in text + + +@pytest.mark.parametrize('source', ['
', 'inline image']) +def test_preview_embeds_html_images(source): + from app.config import get_settings + from PIL import Image + import base64 + folder=get_settings().vault_path/'notes'/'assets' + folder.mkdir(parents=True) + Image.new('RGBA',(2,2),(10,20,30,128)).save(folder/'a&b.png') + resources=asyncio.run(service.preview_resources(ExportRequest(format='pdf',source={'type':'markdown','markdown':source,'file_path':'notes/test.md'}))) + image=resources['images'][0] + assert image['source']=='assets/a&b.png' + assert base64.b64decode(image['data'].split(',')[1]).startswith(b'\x89PNG') + assert image['warnings']==[] + + +def test_html_images_keep_path_validation_and_code_is_not_an_image(): + source='\n\ninline \n\n``\n\n```html\n\n```' + resources=asyncio.run(service.preview_resources(ExportRequest(format='pdf',source={'type':'markdown','markdown':source}))) + assert [image['source'] for image in resources['images']]==['../../private.png','https://example.com/a.png'] + assert all(image['data'] is None and image['warnings'] for image in resources['images']) diff --git a/frontend/src/services/pdfSnapshotService.spec.ts b/frontend/src/services/pdfSnapshotService.spec.ts index a454943..2025e3b 100644 --- a/frontend/src/services/pdfSnapshotService.spec.ts +++ b/frontend/src/services/pdfSnapshotService.spec.ts @@ -58,3 +58,19 @@ it('does not invent a metadata bar for plain notes or unsupported frontmatter',a expect(renderMarkdown).toHaveBeenCalledWith(source,expect.anything()) } }) + +it('exports metadata-only notes without submitting an empty resource request',async()=>{ + for(const tail of ['', '\n \n']) { + const html=await preparePdfSnapshot('---\ntitle: Metadata only\ntags: [draft]\n---\n'+tail,'note',{theme_id:'light',include_title:false,page_size:'A4'}) + const doc=new DOMParser().parseFromString(html,'text/html') + expect(doc.querySelector('.note-metadata h1')?.textContent).toBe('Metadata only') + expect(doc.querySelector('.metadata-tag')?.textContent).toBe('draft') + } + expect(apiClient.post).not.toHaveBeenCalled() +}) +it('embeds prepared HTML images without retaining local URLs',async()=>{ + vi.mocked(renderMarkdown).mockResolvedValueOnce('

') + vi.mocked(apiClient.post).mockResolvedValueOnce({images:[{source:'assets/a&b.png',data:'data:image/png;base64,aGVsbG8=',warnings:[]}],plots:[]}) + const html=await preparePdfSnapshot('','note',{theme_id:'light',include_title:false,page_size:'A4'}) + expect(new DOMParser().parseFromString(html,'text/html').querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,aGVsbG8=') +}) diff --git a/frontend/src/services/pdfSnapshotService.ts b/frontend/src/services/pdfSnapshotService.ts index 61213f6..e7a1284 100644 --- a/frontend/src/services/pdfSnapshotService.ts +++ b/frontend/src/services/pdfSnapshotService.ts @@ -78,7 +78,7 @@ export async function preparePdfSnapshot(markdown:string,title:string,options:Op const scope=scopeAttributes(VisualMarkdownEditor) // Match the editor DOM and scoped styles, with read-only metadata controls. const metadataHtml=metadata ? `
${escape(t('笔记属性','Note properties'))}${metadata.title ? `${escape(metadata.title)}` : ''}
` : '' - const resources=await apiClient.post('/api/exports/preview-resources',{format:'pdf',source:{type:'markdown',markdown:body,file_path:filePath},options}) + const resources:Resources=body.trim() ? await apiClient.post('/api/exports/preview-resources',{format:'pdf',source:{type:'markdown',markdown:body,file_path:filePath},options}) : {images:[],plots:[]} signal?.throwIfAborted() const rendered=await renderMarkdown(body,{themeId:options.theme_id,theme:dark?'dark':'light',preferences,pdf:{mermaidVariables:diagramVariables,plot:async source=>{ const plot=resources.plots.find(p=>p.source.trim()===source.trim()); if(!plot?.svg)throw Error(plot?.warnings.join('; ')||'函数图像无法导出');return plot