From cee5bfa4e6082c7a89cb7641af3492d2adfa33ae Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Thu, 16 Oct 2025 18:18:57 +0800 Subject: [PATCH] feat: warning collector and logging for diagnostics in typlite (#2180) This PR depends on #2173 and should be merged after it. --- Cargo.lock | 2 + crates/tinymist-query/src/docs/convert.rs | 6 +- crates/typlite/Cargo.toml | 4 +- crates/typlite/src/diagnostics.rs | 49 ++++++ .../snaps/convert_docs@nest_list.typ.snap | 2 +- .../docs/snaps/convert_docs@tidy.typ.snap | 3 +- .../snaps/convert@figure_raw.typ.snap | 2 +- .../snaps/convert@issue-1845.typ.snap | 19 ++- .../snaps/convert@outline.typ.snap | 4 +- .../snaps/convert@raw_inline.typ.snap | 4 +- .../snaps/convert_tex@figure_raw.typ.snap | 2 +- .../snaps/convert_tex@issue-1845.typ.snap | 2 +- .../snaps/convert_tex@outline.typ.snap | 4 +- .../snaps/convert_tex@raw_inline.typ.snap | 2 +- crates/typlite/src/lib.rs | 151 +++++++++++++++--- crates/typlite/src/main.rs | 8 + crates/typlite/src/markdown.typ | 6 +- crates/typlite/src/parser/core.rs | 35 +++- crates/typlite/src/parser/media.rs | 11 +- crates/typlite/src/parser/table.rs | 19 ++- crates/typlite/src/writer/docx/writer.rs | 8 +- 21 files changed, 288 insertions(+), 55 deletions(-) create mode 100644 crates/typlite/src/diagnostics.rs diff --git a/Cargo.lock b/Cargo.lock index 0b3a92fd..a534b54a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4982,8 +4982,10 @@ dependencies = [ "comemo", "docx-rs", "ecow", + "env_logger", "image", "insta", + "log", "regex", "resvg", "tinymist-derive", diff --git a/crates/tinymist-query/src/docs/convert.rs b/crates/tinymist-query/src/docs/convert.rs index 898999b4..d1403a8b 100644 --- a/crates/tinymist-query/src/docs/convert.rs +++ b/crates/tinymist-query/src/docs/convert.rs @@ -47,7 +47,7 @@ pub(crate) fn convert_docs( )); imports.join("; ") }); - let feat = TypliteFeat { + let mut feat = TypliteFeat { color_theme: Some(ctx.analysis.color_theme), annotate_elem: true, soft_error: true, @@ -67,10 +67,12 @@ pub(crate) fn convert_docs( w.map_shadow_by_id(w.main(), Bytes::from_string(content.to_owned()))?; // todo: bad performance w.take_db(); - let w = feat + let (w, wrap_info) = feat .prepare_world(&w, Format::Md) .map_err(|e| eco_format!("failed to prepare world: {e}"))?; + feat.wrap_info = wrap_info; + let w = Arc::new(w); let res = typlite::Typlite::new(w.clone()) .with_feature(feat) diff --git a/crates/typlite/Cargo.toml b/crates/typlite/Cargo.toml index 92f31414..242e35c3 100644 --- a/crates/typlite/Cargo.toml +++ b/crates/typlite/Cargo.toml @@ -34,6 +34,8 @@ typst.workspace = true typst-svg.workspace = true typst-syntax.workspace = true typst-html.workspace = true +log.workspace = true +env_logger = { workspace = true, optional = true } # Feature: docx docx-rs = { workspace = true, optional = true } @@ -51,7 +53,7 @@ clap = ["dep:clap"] # Note: this is the feature for typlite as a CLI, not for others. # `docx` is enabled in CLI mode, but not in library mode. # `fonts` is enabled in CLI mode. -cli = ["clap", "clap/wrap_help", "docx", "fonts", "system"] +cli = ["clap", "clap/wrap_help", "docx", "env_logger", "fonts", "system"] no-content-hint = ["tinymist-project/no-content-hint"] docx = ["docx-rs", "image", "resvg"] diff --git a/crates/typlite/src/diagnostics.rs b/crates/typlite/src/diagnostics.rs new file mode 100644 index 00000000..7d71e789 --- /dev/null +++ b/crates/typlite/src/diagnostics.rs @@ -0,0 +1,49 @@ +use std::sync::{Arc, Mutex}; + +use log::warn; +use tinymist_project::diag::print_diagnostics_to_string; +use tinymist_project::{DiagnosticFormat, SourceWorld}; +use typst::diag::SourceDiagnostic; + +/// Shared collector for Typst warnings emitted during conversion. +#[derive(Clone, Default)] +pub(crate) struct WarningCollector { + inner: Arc>>, +} + +impl WarningCollector { + /// Extend the collector with multiple warnings. + pub fn extend(&self, warnings: I) + where + I: IntoIterator, + { + let mut guard = self.inner.lock().expect("warning collector poisoned"); + guard.extend(warnings); + } + + /// Clone all collected warnings into a standalone vector. + pub fn snapshot(&self) -> Vec { + let guard = self.inner.lock().expect("warning collector poisoned"); + guard.clone() + } +} + +/// Render warnings into a human-readable string for the CLI. +#[allow(dead_code)] +pub(crate) fn render_warnings<'a>( + world: &dyn SourceWorld, + warnings: impl IntoIterator, +) -> Option { + let warnings: Vec<&SourceDiagnostic> = warnings.into_iter().collect(); + if warnings.is_empty() { + return None; + } + + match print_diagnostics_to_string(world, warnings.into_iter(), DiagnosticFormat::Human) { + Ok(message) => Some(message.to_string()), + Err(err) => { + warn!("failed to render Typst warnings: {err}"); + None + } + } +} diff --git a/crates/typlite/src/fixtures/docs/snaps/convert_docs@nest_list.typ.snap b/crates/typlite/src/fixtures/docs/snaps/convert_docs@nest_list.typ.snap index 56b0b99e..16d139fb 100644 --- a/crates/typlite/src/fixtures/docs/snaps/convert_docs@nest_list.typ.snap +++ b/crates/typlite/src/fixtures/docs/snaps/convert_docs@nest_list.typ.snap @@ -9,7 +9,7 @@ input_file: crates/typlite/src/fixtures/docs/nest_list.typ -

These again are dictionaries with the keys

  • (optional): The description for the argument.

See show-module() for outputting the results of this function.

  • name (string): The name for the module.
  • label-prefix (auto, string): The label-prefix for internal function references. If , the label-prefix name will be the module name.

    • nested something
    • nested something 2

-> string

+

These again are dictionaries with the keys

  • description (optional): The description for the argument.

See show-module() for outputting the results of this function.

  • name (string): The name for the module.
  • label-prefix (auto, string): The label-prefix for internal function references. If auto, the label-prefix name will be the module name.

    • nested something
    • nested something 2

-> string

===== diff --git a/crates/typlite/src/fixtures/docs/snaps/convert_docs@tidy.typ.snap b/crates/typlite/src/fixtures/docs/snaps/convert_docs@tidy.typ.snap index 70172c72..7f013b6c 100644 --- a/crates/typlite/src/fixtures/docs/snaps/convert_docs@tidy.typ.snap +++ b/crates/typlite/src/fixtures/docs/snaps/convert_docs@tidy.typ.snap @@ -2,7 +2,6 @@ source: crates/typlite/src/tests.rs expression: "conv(world, ConvKind::Md { for_docs: true })" input_file: crates/typlite/src/fixtures/docs/tidy.typ -snapshot_kind: text --- @@ -10,7 +9,7 @@ snapshot_kind: text -

These again are dictionaries with the keys

  • (optional): The description for the argument.
  • (optional): A list of accepted argument types.
  • (optional): Default value for this argument.

See show-module() for outputting the results of this function.

  • content (string): Content of file to analyze for docstrings.
  • name (string): The name for the module.
  • label-prefix (auto, string): The label-prefix for internal function references. If , the label-prefix name will be the module name.
  • require-all-parameters (boolean): Require that all parameters of a functions are documented and fail if some are not.
  • scope (dictionary): A dictionary of definitions that are then available in all function and parameter descriptions.
  • preamble (string): Code to prepend to all code snippets shown with . This can for instance be used to import something from the scope.

-> string

+

These again are dictionaries with the keys

  • description (optional): The description for the argument.
  • types (optional): A list of accepted argument types.
  • default (optional): Default value for this argument.

See show-module() for outputting the results of this function.

  • content (string): Content of .typ file to analyze for docstrings.
  • name (string): The name for the module.
  • label-prefix (auto, string): The label-prefix for internal function references. If auto, the label-prefix name will be the module name.
  • require-all-parameters (boolean): Require that all parameters of a functions are documented and fail if some are not.
  • scope (dictionary): A dictionary of definitions that are then available in all function and parameter descriptions.
  • preamble (string): Code to prepend to all code snippets shown with #example(). This can for instance be used to import something from the scope.

-> string

===== diff --git a/crates/typlite/src/fixtures/integration/snaps/convert@figure_raw.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert@figure_raw.typ.snap index e76ffef9..b23c77c9 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert@figure_raw.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert@figure_raw.typ.snap @@ -9,7 +9,7 @@ input_file: crates/typlite/src/fixtures/integration/figure_raw.typ - +
markdown
===== diff --git a/crates/typlite/src/fixtures/integration/snaps/convert@issue-1845.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert@issue-1845.typ.snap index 7b535da5..98148d60 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert@issue-1845.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert@issue-1845.typ.snap @@ -10,11 +10,10 @@ input_file: crates/typlite/src/fixtures/integration/issue-1845.typ
HeaderRow
Regular text
+Code line 2">

Code line 1

Code line 2

Regular text ===== - @@ -36,7 +35,23 @@ Row +
 
+

+ +Code line 1 + +

+ + + +

+ +Code line 2 + +

+ +
diff --git a/crates/typlite/src/fixtures/integration/snaps/convert@outline.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert@outline.typ.snap index 7fef4603..f2f16923 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert@outline.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert@outline.typ.snap @@ -9,7 +9,7 @@ input_file: crates/typlite/src/fixtures/integration/outline.typ - ContentsHeading 1Heading 2Heading 1Heading 2

This is a link to example.com

Inline has it.

ContentsHeading 1Heading 2Heading 1Heading 2

This is a link to example.com

Inline code has back-ticks around it.

Math inline:

redacted-frame

and block:

redacted-frame
  • First item
  • Second item

    1. First sub-item
    2. Second sub-item

      • First sub-sub-item
First term
First definition
012345678910111213141516171819
+}">

using System.IO.Compression;

#pragma warning disable 414, 3021

namespace MyApplication

{

[Obsolete("...")]

class Program : IInterface

{

public static List<int> JustDoIt(int count)

{

Console.WriteLine($"Hello {Name}!");

return new List<int>(new int[] { 1, 2, 3 })

}

}

}

Math inline:

redacted-frame

and block:

redacted-frame
  • First item
  • Second item

    1. First sub-item
    2. Second sub-item

      • First sub-sub-item
First term
First definition
012345678910111213141516171819
===== diff --git a/crates/typlite/src/fixtures/integration/snaps/convert@raw_inline.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert@raw_inline.typ.snap index b2ae43d8..8b77c20f 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert@raw_inline.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert@raw_inline.typ.snap @@ -1,6 +1,6 @@ --- source: crates/typlite/src/tests.rs -expression: "conv(world, false)" +expression: "conv(world, ConvKind::Md { for_docs: false })" input_file: crates/typlite/src/fixtures/integration/raw_inline.typ --- @@ -9,7 +9,7 @@ input_file: crates/typlite/src/fixtures/integration/raw_inline.typ - Some inlined raw , + Some inlined raw a, b ===== diff --git a/crates/typlite/src/fixtures/integration/snaps/convert_tex@figure_raw.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert_tex@figure_raw.typ.snap index ed03f4e6..d0529d17 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert_tex@figure_raw.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert_tex@figure_raw.typ.snap @@ -9,7 +9,7 @@ input_file: crates/typlite/src/fixtures/integration/figure_raw.typ - +
markdown
===== diff --git a/crates/typlite/src/fixtures/integration/snaps/convert_tex@issue-1845.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert_tex@issue-1845.typ.snap index e4a67bf8..3e85a221 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert_tex@issue-1845.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert_tex@issue-1845.typ.snap @@ -10,7 +10,7 @@ input_file: crates/typlite/src/fixtures/integration/issue-1845.typ
HeaderRow
Regular text
+Code line 2">

Code line 1

Code line 2

Regular text ===== diff --git a/crates/typlite/src/fixtures/integration/snaps/convert_tex@outline.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert_tex@outline.typ.snap index 4b28c6b3..1a30317a 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert_tex@outline.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert_tex@outline.typ.snap @@ -9,7 +9,7 @@ input_file: crates/typlite/src/fixtures/integration/outline.typ - ContentsHeading 1Heading 2Heading 1Heading 2

This is a link to example.com

Inline has it.

ContentsHeading 1Heading 2Heading 1Heading 2

This is a link to example.com

Inline code has back-ticks around it.

Math inline:

redacted-frame

and block:

redacted-frame
  • First item
  • Second item

    1. First sub-item
    2. Second sub-item

      • First sub-sub-item
First term
First definition
012345678910111213141516171819
+}">

using System.IO.Compression;

#pragma warning disable 414, 3021

namespace MyApplication

{

[Obsolete("...")]

class Program : IInterface

{

public static List<int> JustDoIt(int count)

{

Console.WriteLine($"Hello {Name}!");

return new List<int>(new int[] { 1, 2, 3 })

}

}

}

Math inline:

redacted-frame

and block:

redacted-frame
  • First item
  • Second item

    1. First sub-item
    2. Second sub-item

      • First sub-sub-item
First term
First definition
012345678910111213141516171819
===== diff --git a/crates/typlite/src/fixtures/integration/snaps/convert_tex@raw_inline.typ.snap b/crates/typlite/src/fixtures/integration/snaps/convert_tex@raw_inline.typ.snap index 4c7b6756..7d22aa40 100644 --- a/crates/typlite/src/fixtures/integration/snaps/convert_tex@raw_inline.typ.snap +++ b/crates/typlite/src/fixtures/integration/snaps/convert_tex@raw_inline.typ.snap @@ -9,7 +9,7 @@ input_file: crates/typlite/src/fixtures/integration/raw_inline.typ - Some inlined raw , + Some inlined raw a, b ===== diff --git a/crates/typlite/src/lib.rs b/crates/typlite/src/lib.rs index f9ac13d0..3a2fc928 100644 --- a/crates/typlite/src/lib.rs +++ b/crates/typlite/src/lib.rs @@ -5,6 +5,7 @@ pub mod attributes; pub mod common; +mod diagnostics; mod error; pub mod parser; pub mod tags; @@ -22,11 +23,15 @@ use tinymist_project::vfs::WorkspaceResolver; use tinymist_project::{EntryReader, LspWorld, TaskInputs}; use tinymist_std::error::prelude::*; use typst::World; +use typst::WorldExt; +use typst::diag::SourceDiagnostic; use typst::foundations::Bytes; use typst::html::HtmlDocument; +use typst_syntax::Span; use typst_syntax::VirtualPath; pub use crate::common::Format; +use crate::diagnostics::WarningCollector; use crate::parser::HtmlToAstParser; use crate::writer::WriterFactory; use typst_syntax::FileId; @@ -47,6 +52,7 @@ pub struct MarkdownDocument { world: Arc, feat: TypliteFeat, ast: Option, + warnings: WarningCollector, } impl MarkdownDocument { @@ -57,6 +63,7 @@ impl MarkdownDocument { world, feat, ast: None, + warnings: WarningCollector::default(), } } @@ -72,15 +79,69 @@ impl MarkdownDocument { world, feat, ast: Some(ast), + warnings: WarningCollector::default(), } } + /// Replace the backing warning collector, preserving shared state with + /// other components of the pipeline. + pub(crate) fn with_warning_collector(mut self, collector: WarningCollector) -> Self { + self.warnings = collector; + self + } + + /// Get a snapshot of all collected warnings so far. + pub fn warnings(&self) -> Vec { + let warnings = self.warnings.snapshot(); + if let Some(info) = &self.feat.wrap_info { + warnings + .into_iter() + .filter_map(|diag| self.remap_diagnostic(diag, info)) + .collect() + } else { + warnings + } + } + + /// Internal accessor for sharing the collector with the parser. + fn warning_collector(&self) -> WarningCollector { + self.warnings.clone() + } + + fn remap_diagnostic( + &self, + mut diagnostic: SourceDiagnostic, + info: &WrapInfo, + ) -> Option { + if let Some(span) = info.remap_span(self.world.as_ref(), diagnostic.span) { + diagnostic.span = span; + } else { + return None; + } + + diagnostic.trace = diagnostic + .trace + .into_iter() + .filter_map( + |mut spanned| match info.remap_span(self.world.as_ref(), spanned.span) { + Some(span) => { + spanned.span = span; + Some(spanned) + } + None => None, + }, + ) + .collect(); + + Some(diagnostic) + } + /// Parse HTML document to AST pub fn parse(&self) -> tinymist_std::Result { if let Some(ast) = &self.ast { return Ok(ast.clone()); } - let parser = HtmlToAstParser::new(self.feat.clone(), &self.world); + let parser = HtmlToAstParser::new(self.feat.clone(), &self.world, self.warning_collector()); parser.parse(&self.base.root).context_ut("failed to parse") } @@ -141,6 +202,38 @@ pub enum ColorTheme { Dark, } +#[derive(Debug, Clone)] +pub struct WrapInfo { + /// The synthetic wrapper file that hosts the original Typst source. + pub wrap_file_id: FileId, + /// The user's actual Typst source file. + pub original_file_id: FileId, + /// Number of UTF-8 bytes injected ahead of the original source. + pub prefix_len_bytes: usize, +} + +impl WrapInfo { + /// Translate a span from the wrapper file back into the original file. + pub fn remap_span(&self, world: &dyn typst::World, span: Span) -> Option { + if span.id() != Some(self.wrap_file_id) { + return Some(span); + } + + let range = world.range(span)?; + let start = range.start.checked_sub(self.prefix_len_bytes)?; + let end = range.end.checked_sub(self.prefix_len_bytes)?; + + let original_source = world.source(self.original_file_id).ok()?; + let original_len = original_source.len_bytes(); + + if start >= original_len || end > original_len { + return None; + } + + Some(Span::from_range(self.original_file_id, start..end)) + } +} + #[derive(Debug, Default, Clone)] pub struct TypliteFeat { /// The preferred color theme. @@ -178,6 +271,8 @@ pub struct TypliteFeat { /// It resembles the regular typst show rule function, like `#show: /// article`. pub processor: Option, + /// Optional mapping from the wrapper file back to the original source. + pub wrap_info: Option, } impl TypliteFeat { @@ -185,7 +280,7 @@ impl TypliteFeat { &self, world: &LspWorld, format: Format, - ) -> tinymist_std::Result { + ) -> tinymist_std::Result<(LspWorld, Option)> { let entry = world.entry_state(); let main = entry.main(); let current = main.context("no main file in workspace")?; @@ -242,19 +337,18 @@ impl TypliteFeat { Bytes::from_string(include_str!("markdown.typ")), ) .context_ut("cannot map markdown.typ")?; + let original_source = world + .source(current) + .context_ut("cannot fetch main source")? + .text() + .to_owned(); + + const WRAP_PREFIX: &str = + "#import \"@local/_markdown:0.1.0\": md-doc, example; #show: md-doc\n"; + let wrap_content = format!("{WRAP_PREFIX}{original_source}"); world - .map_shadow_by_id( - wrap_main_id, - Bytes::from_string(format!( - r#"#import "@local/_markdown:0.1.0": md-doc, example; #show: md-doc -{}"#, - world - .source(current) - .context_ut("failed to get main file content")? - .text() - )), - ) + .map_shadow_by_id(wrap_main_id, Bytes::from_string(wrap_content)) .context_ut("cannot map source for main file")?; if let Some(main_content) = main_content { @@ -263,7 +357,13 @@ impl TypliteFeat { .context_ut("cannot map source for main file")?; } - Ok(world) + let wrap_info = Some(WrapInfo { + wrap_file_id: wrap_main_id, + original_file_id: current, + prefix_len_bytes: WRAP_PREFIX.len(), + }); + + Ok((world, wrap_info)) } } @@ -319,9 +419,11 @@ impl Typlite { } /// Convert the content to a markdown document. - pub fn convert_doc(self, format: Format) -> tinymist_std::Result { - let world = Arc::new(self.feat.prepare_world(&self.world, format)?); + pub fn convert_doc(mut self, format: Format) -> tinymist_std::Result { + let (prepared_world, wrap_info) = self.feat.prepare_world(&self.world, format)?; + self.feat.wrap_info = wrap_info; let feat = self.feat.clone(); + let world = Arc::new(prepared_world); Self::convert_doc_prepared(feat, format, world) } @@ -331,11 +433,22 @@ impl Typlite { format: Format, world: Arc, ) -> tinymist_std::Result { - // todo: ignoring warnings - let base = typst::compile(&world).output?; + let compiled = typst::compile(&world); + let collector = WarningCollector::default(); + collector.extend( + compiled + .warnings + .iter() + .filter(|&diag| { + diag.message.as_str() + != "html export is under active development and incomplete" + }) + .cloned(), + ); + let base = compiled.output?; let mut feat = feat; feat.target = format; - Ok(MarkdownDocument::new(base, world.clone(), feat)) + Ok(MarkdownDocument::new(base, world.clone(), feat).with_warning_collector(collector)) } } diff --git a/crates/typlite/src/main.rs b/crates/typlite/src/main.rs index 2d8ca13f..f935427c 100644 --- a/crates/typlite/src/main.rs +++ b/crates/typlite/src/main.rs @@ -52,6 +52,7 @@ pub struct CompileArgs { } fn main() -> Result<()> { + let _ = env_logger::try_init(); // Parse command line arguments let args = CompileArgs::parse(); @@ -103,6 +104,8 @@ fn run(args: CompileArgs, world: Arc) -> Result<()> { Format::Docx => Bytes::new(doc.to_docx()?), }; + let warnings = doc.warnings(); + if is_stdout { std::io::stdout() .write_all(result.as_slice()) @@ -111,6 +114,11 @@ fn run(args: CompileArgs, world: Arc) -> Result<()> { bail!("failed to write file {output_path:?}: {err}"); } + if !warnings.is_empty() { + print_diagnostics(world.as_ref(), warnings.iter(), DiagnosticFormat::Human) + .context_ut("print warnings")?; + } + Ok(()) } diff --git a/crates/typlite/src/markdown.typ b/crates/typlite/src/markdown.typ index 3c2a0c55..3d7b75f2 100644 --- a/crates/typlite/src/markdown.typ +++ b/crates/typlite/src/markdown.typ @@ -13,7 +13,7 @@ #let md-emph(body) = html.elem("span", html.elem("m1emph", body)) #let md-highlight(body) = html.elem("span", html.elem("m1highlight", body)) #let md-strike(body) = html.elem("span", html.elem("m1strike", body)) -#let md-raw(lang: none, block: false, text) = { +#let md-raw(lang: none, block: false, text: "", body) = { let body = html.elem( "m1raw", attrs: ( @@ -25,7 +25,7 @@ block: bool-str(block), text: text, ), - "", + body, ) if block { @@ -190,7 +190,7 @@ // todo: icc? show image: it => if-not-paged(it, md-image(src: it.source, alt: it.alt)) - show raw: it => if-not-paged(it, md-raw(lang: it.lang, block: it.block, it.text)) + show raw: it => if-not-paged(it, md-raw(lang: it.lang, block: it.block, text: it.text, it)) show link: it => if-not-paged(it, md-link(dest: it.dest, it.body)) show ref: it => if-not-paged(it, md-ref(it)) diff --git a/crates/typlite/src/parser/core.rs b/crates/typlite/src/parser/core.rs index 0a5f1ca6..cb8c7dac 100644 --- a/crates/typlite/src/parser/core.rs +++ b/crates/typlite/src/parser/core.rs @@ -2,6 +2,9 @@ use std::sync::Arc; +use typst::diag::SourceDiagnostic; +use typst_syntax::Span; + use cmark_writer::WriteResult; use cmark_writer::ast::{CustomNode, HtmlAttribute, HtmlElement as CmarkHtmlElement, Node}; use cmark_writer::writer::InlineWriterProxy; @@ -13,6 +16,7 @@ use crate::Result; use crate::TypliteFeat; use crate::attributes::{AlertsAttr, HeadingAttr, RawAttr, TypliteAttrsParser, md_attr}; use crate::common::{AlertNode, CenterNode, VerbatimNode}; +use crate::diagnostics::WarningCollector; use crate::tags::md_tag; use super::{list::ListParser, table::TableParser}; @@ -25,10 +29,15 @@ pub struct HtmlToAstParser { pub list_level: usize, pub blocks: Vec, pub inline_buffer: Vec, + pub(crate) warnings: WarningCollector, } impl HtmlToAstParser { - pub fn new(feat: TypliteFeat, world: &Arc) -> Self { + pub(crate) fn new( + feat: TypliteFeat, + world: &Arc, + warnings: WarningCollector, + ) -> Self { Self { feat, world: world.clone(), @@ -36,6 +45,7 @@ impl HtmlToAstParser { list_level: 0, blocks: Vec::new(), inline_buffer: Vec::new(), + warnings, } } @@ -197,6 +207,12 @@ impl HtmlToAstParser { let tag_name = element.tag.resolve().to_string(); if !tag_name.starts_with("m1") { + // self.warn_at( + // Some(element.span), + // eco_format!( + // "unsupported HTML element `<{tag_name}>`; exported as raw HTML" + // ), + // ); let html_element = self.create_html_element(element)?; self.inline_buffer.push(html_element); } else { @@ -290,6 +306,23 @@ impl HtmlToAstParser { Ok((inline, blocks)) } + + pub(crate) fn warn_at(&mut self, span: Option, message: EcoString) { + let span = span.unwrap_or_else(Span::detached); + let span = self + .feat + .wrap_info + .as_ref() + .and_then(|info| self.remap_span_from_wrapper(span, info)) + .unwrap_or(span); + + let diag = SourceDiagnostic::warning(span, message); + self.warnings.extend(std::iter::once(diag)); + } + + fn remap_span_from_wrapper(&self, span: Span, info: &crate::WrapInfo) -> Option { + info.remap_span(self.world.as_ref(), span) + } } #[derive(Debug, Clone)] diff --git a/crates/typlite/src/parser/media.rs b/crates/typlite/src/parser/media.rs index b3723451..d7308f7c 100644 --- a/crates/typlite/src/parser/media.rs +++ b/crates/typlite/src/parser/media.rs @@ -7,6 +7,7 @@ use std::sync::{Arc, LazyLock}; use base64::Engine; use cmark_writer::ast::{HtmlAttribute, HtmlElement as CmarkHtmlElement, Node}; use ecow::{EcoString, eco_format}; +use log::debug; use tinymist_project::diag::print_diagnostics_to_string; use tinymist_project::{EntryReader, MEMORY_MAIN_ENTRY, TaskInputs, base::ShadowApi}; use typst::{ @@ -210,7 +211,7 @@ impl HtmlToAstParser { }); if self.feat.remove_html { - eprintln!("Removing idoc element due to remove_html feature"); + debug!("remove_html feature active, dropping inline document element"); // todo: make error silent is not good. return Node::Text(EcoString::new()); } @@ -278,12 +279,12 @@ impl HtmlToAstParser { ) .unwrap(); - //todo: ignoring warnings - let doc = typst::compile(&world); - let doc = match doc.output { + let compiled = typst::compile(&world); + self.warnings.extend(compiled.warnings.iter().cloned()); + let doc = match compiled.output { Ok(doc) => doc, Err(e) => { - let diag = doc.warnings.iter().chain(e.iter()); + let diag = compiled.warnings.iter().chain(e.iter()); let e = print_diagnostics_to_string( &world, diff --git a/crates/typlite/src/parser/table.rs b/crates/typlite/src/parser/table.rs index 4b46194f..1a22697e 100644 --- a/crates/typlite/src/parser/table.rs +++ b/crates/typlite/src/parser/table.rs @@ -30,6 +30,12 @@ impl TableParser { // Check if the table contains rowspan or colspan attributes // If it does, fall back to using HtmlElement if Self::table_has_complex_cells(table) { + parser.warn_at( + Some(table.span), + eco_format!( + "table contains rowspan or colspan attributes; exported original HTML table" + ), + ); return parser.create_html_element(table).map(Some); } @@ -48,15 +54,8 @@ impl TableParser { )?; if fallback_to_html { - eprintln!( - "[typlite] warning: block content detected inside table cell; exporting original HTML table" - ); let html = Self::serialize_html_element(parser, table).map_err(|e| e.to_string())?; - let html = eco_format!( - "\n{}", - html - ); return Ok(Some(Node::HtmlBlock(html))); } @@ -226,6 +225,12 @@ impl TableParser { let (cell_content, block_content) = parser.capture_children(cell)?; if !block_content.is_empty() { + parser.warn_at( + Some(cell.span), + eco_format!( + "block content detected inside table cell; exported original HTML table" + ), + ); *fallback_to_html = true; return Ok(Vec::new()); } diff --git a/crates/typlite/src/writer/docx/writer.rs b/crates/typlite/src/writer/docx/writer.rs index 0d29f8c1..f37c7c9a 100644 --- a/crates/typlite/src/writer/docx/writer.rs +++ b/crates/typlite/src/writer/docx/writer.rs @@ -4,6 +4,7 @@ use base64::Engine; use cmark_writer::ast::{ListItem, Node}; use docx_rs::*; use ecow::EcoString; +use log::{debug, warn}; use std::fs; use std::io::Cursor; @@ -252,11 +253,14 @@ impl DocxWriter { } node if node.is_custom_type::() => { let node = node.as_custom_type::().unwrap(); - eprintln!("Warning: `m1verbatim` is ignored {:?}.", node.content); + warn!( + "ignoring `m1verbatim` content in DOCX export: {:?}", + node.content + ); } // Other inline element types _ => { - eprintln!("other inline element: {node:?}"); + debug!("unhandled inline node in DOCX export: {node:?}"); } }