From 4a0edcbda37cd44f2d17fde451d47c3f4d26a614 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Tue, 18 Nov 2025 11:43:29 +0800 Subject: [PATCH] feat: add BlockVerbatimNode for block-level raw text output and update related handling in parser and writers --- crates/typlite/src/common.rs | 23 +++++++++++++++++-- .../snaps/convert@issue-1844.typ.snap | 5 ++-- .../docx_generation_hash@issue-1844.typ.snap | 2 +- crates/typlite/src/parser/core.rs | 20 ++++++---------- crates/typlite/src/writer/docx/writer.rs | 19 ++++++++++----- crates/typlite/src/writer/latex.rs | 9 ++++++-- crates/typlite/src/writer/text.rs | 13 ++++++++++- 7 files changed, 63 insertions(+), 28 deletions(-) diff --git a/crates/typlite/src/common.rs b/crates/typlite/src/common.rs index ff02fe77..4f34a7dd 100644 --- a/crates/typlite/src/common.rs +++ b/crates/typlite/src/common.rs @@ -208,11 +208,11 @@ impl InlineNode { } } -/// Verbatim node for raw text output +/// Inline verbatim node for raw text output #[derive(Debug, PartialEq, Clone)] #[custom_node(block = false, html_impl = true)] pub struct VerbatimNode { - /// The content to directly output + /// The content to directly output inline pub content: EcoString, } @@ -226,6 +226,25 @@ impl VerbatimNode { } } +/// Block verbatim node for raw text output without paragraph wrapping +#[derive(Debug, PartialEq, Clone)] +#[custom_node(block = true, html_impl = true)] +pub struct BlockVerbatimNode { + /// The content to directly output as a block + pub content: EcoString, +} + +impl BlockVerbatimNode { + fn write_custom(&self, writer: &mut BlockWriterProxy) -> WriteResult<()> { + writer.write_str(&self.content)?; + Ok(()) + } + + fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> { + writer.write_trusted_html(&self.content) + } +} + /// Alert node for alert messages #[derive(Debug, PartialEq, Clone)] #[custom_node(block = true, html_impl = false)] diff --git a/crates/typlite/src/fixtures/integration/snaps/convert@issue-1844.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert@issue-1844.typ.snap index 58bea71f..28ba5000 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert@issue-1844.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert@issue-1844.typ.snap @@ -64,7 +64,7 @@ separately rendered SVG -

```` +```` == Under the Greenwood Tree by Shakespeare. @@ -74,8 +74,7 @@ Under the greenwood tree #emoji.tree Who loves to lie with me, ... ```) -````

- +```` diff --git a/crates/typlite/src/fixtures/integration/snaps/docx_generation_hash@issue-1844.typ.snap b/crates/typlite/src/fixtures/integration/snaps/docx_generation_hash@issue-1844.typ.snap index a8c37f80..1da63d1a 100644 --- a/crates/typlite/src/fixtures/integration/snaps/docx_generation_hash@issue-1844.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/docx_generation_hash@issue-1844.typ.snap @@ -3,4 +3,4 @@ source: crates/typlite/src/tests.rs expression: hash input_file: crates/typlite/src/fixtures/integration/issue-1844.typ --- -siphash128_13:5680ee2c48f93da5b40c162e391ef15 +siphash128_13:b700ae0ff0daf675a40354416190674e diff --git a/crates/typlite/src/parser/core.rs b/crates/typlite/src/parser/core.rs index d8a3c686..c91eb4da 100644 --- a/crates/typlite/src/parser/core.rs +++ b/crates/typlite/src/parser/core.rs @@ -14,10 +14,8 @@ use typst_html::{HtmlElement, HtmlNode, tag}; use crate::Result; use crate::TypliteFeat; -use crate::attributes::{ - AlertsAttr, HeadingAttr, RawAttr, TypliteAttrsParser, VerbatimAttr, md_attr, -}; -use crate::common::{AlertNode, CenterNode, VerbatimNode}; +use crate::attributes::{AlertsAttr, HeadingAttr, RawAttr, TypliteAttrsParser, VerbatimAttr}; +use crate::common::{AlertNode, BlockVerbatimNode, CenterNode, VerbatimNode}; use crate::diagnostics::WarningCollector; use crate::tags::md_tag; @@ -196,9 +194,9 @@ impl HtmlToAstParser { let attrs = VerbatimAttr::parse(&element.attrs)?; if attrs.block { self.flush_inline_buffer(); - self.blocks.push(Node::Paragraph(vec![Node::Custom(Box::new( - VerbatimNode { content: attrs.src }, - ))])); + self.blocks.push(Node::Custom(Box::new(BlockVerbatimNode { + content: attrs.src, + }))); } else { self.inline_buffer .push(Node::Custom(Box::new(VerbatimNode { content: attrs.src }))); @@ -405,12 +403,8 @@ impl HtmlToAstParser { } fn is_verbatim_block(element: &HtmlElement) -> bool { - element - .attrs - .0 - .iter() - .find(|(name, _)| *name == md_attr::block) - .and_then(|(_, value)| value.parse::().ok()) + VerbatimAttr::parse(&element.attrs) + .map(|attrs| attrs.block) .unwrap_or(false) } diff --git a/crates/typlite/src/writer/docx/writer.rs b/crates/typlite/src/writer/docx/writer.rs index f37c7c9a..42c58ddc 100644 --- a/crates/typlite/src/writer/docx/writer.rs +++ b/crates/typlite/src/writer/docx/writer.rs @@ -4,13 +4,14 @@ use base64::Engine; use cmark_writer::ast::{ListItem, Node}; use docx_rs::*; use ecow::EcoString; -use log::{debug, warn}; +use log::debug; use std::fs; use std::io::Cursor; use crate::Result; use crate::common::{ - CenterNode, FigureNode, FormatWriter, HighlightNode, InlineNode, VerbatimNode, + BlockVerbatimNode, CenterNode, FigureNode, FormatWriter, HighlightNode, InlineNode, + VerbatimNode, }; use super::image_processor::DocxImageProcessor; @@ -253,10 +254,7 @@ impl DocxWriter { } node if node.is_custom_type::() => { let node = node.as_custom_type::().unwrap(); - warn!( - "ignoring `m1verbatim` content in DOCX export: {:?}", - node.content - ); + run = run.style("CodeInline").add_text(&node.content); } // Other inline element types _ => { @@ -504,6 +502,15 @@ impl DocxWriter { docx = docx.add_paragraph(para); } } + node if node.is_custom_type::() => { + let block_node = node.as_custom_type::().unwrap(); + for line in block_node.content.split('\n') { + let para = Paragraph::new() + .style("CodeBlock") + .add_run(Run::new().add_text(line)); + docx = docx.add_paragraph(para); + } + } node if node.is_custom_type::() => { let inline_node = node.as_custom_type::().unwrap(); // Handle InlineNode at block level (convert to paragraph) diff --git a/crates/typlite/src/writer/latex.rs b/crates/typlite/src/writer/latex.rs index b3ef3698..27617164 100644 --- a/crates/typlite/src/writer/latex.rs +++ b/crates/typlite/src/writer/latex.rs @@ -8,8 +8,8 @@ use tinymist_std::path::unix_slash; use crate::Result; use crate::common::{ - CenterNode, ExternalFrameNode, FigureNode, FormatWriter, HighlightNode, InlineNode, ListState, - VerbatimNode, + BlockVerbatimNode, CenterNode, ExternalFrameNode, FigureNode, FormatWriter, HighlightNode, + InlineNode, ListState, VerbatimNode, }; /// LaTeX writer implementation @@ -289,6 +289,11 @@ impl LaTeXWriter { self.write_node(child, output)?; } } + node if node.is_custom_type::() => { + let block_node = node.as_custom_type::().unwrap(); + output.push_str(&block_node.content); + output.push_str("\n\n"); + } node if node.is_custom_type::() => { let inline_node = node.as_custom_type::().unwrap(); output.push_str(&inline_node.content); diff --git a/crates/typlite/src/writer/text.rs b/crates/typlite/src/writer/text.rs index 76f45cef..d8153c55 100644 --- a/crates/typlite/src/writer/text.rs +++ b/crates/typlite/src/writer/text.rs @@ -4,7 +4,7 @@ use cmark_writer::ast::Node; use ecow::EcoString; use crate::Result; -use crate::common::{ExternalFrameNode, FigureNode, FormatWriter}; +use crate::common::{BlockVerbatimNode, ExternalFrameNode, FigureNode, FormatWriter, VerbatimNode}; /// Text writer implementation #[derive(Default)] @@ -161,6 +161,12 @@ impl TextWriter { output.push_str(&external_frame.alt_text); } } + node if node.is_custom_type::() => { + if let Some(block_node) = node.as_custom_type::() { + output.push_str(&block_node.content); + output.push_str("\n\n"); + } + } node if node.is_custom_type::() => { if let Some(highlight) = node.as_custom_type::() { for child in &highlight.content { @@ -168,6 +174,11 @@ impl TextWriter { } } } + node if node.is_custom_type::() => { + if let Some(inline_node) = node.as_custom_type::() { + output.push_str(&inline_node.content); + } + } _ => {} } Ok(())