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', ['

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('
','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 ? `` : ''
- const resources=await apiClient.post