refactor(typlite): use EcoString instead (#1815)
Some checks are pending
tinymist::ci / build-vsc-assets (push) Blocked by required conditions
tinymist::ci / build-vscode-others (push) Blocked by required conditions
tinymist::ci / publish-vscode (push) Blocked by required conditions
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / E2E Tests (win32-x64 on windows-latest) (push) Blocked by required conditions
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / E2E Tests (darwin-arm64 on macos-latest) (push) Blocked by required conditions
tinymist::ci / E2E Tests (linux-x64 on ubuntu-22.04) (push) Blocked by required conditions
tinymist::ci / E2E Tests (linux-x64 on ubuntu-latest) (push) Blocked by required conditions
tinymist::ci / E2E Tests (win32-x64 on windows-2019) (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run
tinymist::ci / build-binary (push) Blocked by required conditions
tinymist::ci / build-vscode (push) Blocked by required conditions

* feat: update cmark-writer to version 0.7.6 and refactor string handling to use EcoString

* dev: make use of eco string

---------

Co-authored-by: Myriad-Dreamin <camiyoru@gmail.com>
This commit is contained in:
Hong Jiarong 2025-06-16 06:20:09 +08:00 committed by GitHub
parent 8fb118f3ff
commit c0d5f0c800
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 89 additions and 85 deletions

11
Cargo.lock generated
View file

@ -581,11 +581,12 @@ dependencies = [
[[package]]
name = "cmark-writer"
version = "0.7.5"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a132e7fc9fddc446c3930910894eab669241332af90981e53eb1b782bd70b77a"
checksum = "7f10f0b392cc21023a462ef0972118edbf4616f5eb8546f3434c53353ba12f9a"
dependencies = [
"cmark-writer-macros",
"ecow",
"env_logger",
"html-escape",
"log",
@ -593,9 +594,9 @@ dependencies = [
[[package]]
name = "cmark-writer-macros"
version = "0.7.5"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45366269a70c39928d4fd7d95045250757be31ac93b5e85e3a8469b2690d93a"
checksum = "be7dbe651b0dee1bfd8a1a93efe76a43d6bbdee978b802102a9c14d070e8221c"
dependencies = [
"proc-macro2",
"quote",
@ -5593,7 +5594,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]

View file

@ -97,7 +97,7 @@ rpds = "1"
# Data/Text Format and Processing
biblatex = "0.10"
cmark-writer = { version = "0.7.5", features = ["gfm"] }
cmark-writer = { version = "0.7.6", features = ["gfm"] }
docx-rs = { version = "0.4.18-rc19", git = "https://github.com/Myriad-Dreamin/docx-rs", default-features = false, rev = "db49a729f68dbdb9e8e91857fbb1c3d414209871" }
hayagriva = "0.8"
hex = "0.4.3"

View file

@ -10,6 +10,7 @@ use cmark_writer::HtmlWriter;
use cmark_writer::HtmlWriterOptions;
use cmark_writer::WriteResult;
use cmark_writer::WriterOptions;
use ecow::eco_format;
use ecow::EcoString;
use std::path::PathBuf;
@ -56,10 +57,10 @@ impl FigureNode {
fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
let body = self.body.clone();
let node = Node::HtmlElement(HtmlElement {
tag: "figure".to_string(),
tag: EcoString::inline("figure"),
attributes: vec![HtmlAttribute {
name: "class".to_string(),
value: "figure".to_string(),
name: EcoString::inline("class"),
value: EcoString::inline("figure"),
}],
children: vec![*body],
self_closing: false,
@ -76,7 +77,7 @@ pub struct ExternalFrameNode {
/// The path to the external file containing the frame
pub file_path: PathBuf,
/// Alternative text for the frame
pub alt_text: String,
pub alt_text: EcoString,
/// Original SVG data (needed for DOCX that still embeds images)
pub svg: String,
}
@ -94,14 +95,14 @@ impl ExternalFrameNode {
fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
let node = Node::HtmlElement(HtmlElement {
tag: "img".to_string(),
tag: EcoString::inline("img"),
attributes: vec![
HtmlAttribute {
name: "src".to_string(),
value: self.file_path.display().to_string(),
name: EcoString::inline("src"),
value: self.file_path.display().to_string().into(),
},
HtmlAttribute {
name: "alt".to_string(),
name: EcoString::inline("alt"),
value: self.alt_text.clone(),
},
],
@ -134,7 +135,7 @@ impl HighlightNode {
fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
let node = Node::HtmlElement(HtmlElement {
tag: "mark".to_string(),
tag: EcoString::inline("mark"),
attributes: vec![],
children: self.content.clone(),
self_closing: false,
@ -156,10 +157,10 @@ impl CenterNode {
pub fn new(children: Vec<Node>) -> Self {
CenterNode {
node: Node::HtmlElement(cmark_writer::ast::HtmlElement {
tag: "p".to_string(),
tag: EcoString::inline("p"),
attributes: vec![cmark_writer::ast::HtmlAttribute {
name: "align".to_string(),
value: "center".to_string(),
name: EcoString::inline("align"),
value: EcoString::inline("center"),
}],
children,
self_closing: false,
@ -225,10 +226,11 @@ pub struct AlertNode {
impl AlertNode {
fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
let quote = Node::BlockQuote(vec![
Node::Paragraph(vec![Node::Text(
"[!".to_string() + &self.class.clone().to_string().to_ascii_uppercase() + "]",
)]),
Node::Paragraph(vec![Node::Text("".to_string())]),
Node::Paragraph(vec![Node::Text(eco_format!(
"[!{}]",
self.class.to_ascii_uppercase()
))]),
Node::Paragraph(vec![Node::Text("".into())]),
]);
let mut tmp_writer = CommonMarkWriter::with_options(WriterOptions {
escape_special_chars: false,
@ -239,7 +241,7 @@ impl AlertNode {
let quote = Node::BlockQuote(self.content.clone());
let mut tmp_writer = CommonMarkWriter::with_options(writer.options.clone());
tmp_writer.write(&quote)?;
content += &tmp_writer.into_string();
content += tmp_writer.into_string();
writer.write_str(&content)?;
Ok(())
}

View file

@ -4,6 +4,7 @@ use std::sync::Arc;
use cmark_writer::ast::{CustomNode, HtmlAttribute, HtmlElement as CmarkHtmlElement, Node};
use cmark_writer::{CommonMarkWriter, WriteResult};
use ecow::EcoString;
use tinymist_project::LspWorld;
use typst::html::{tag, HtmlElement, HtmlNode};
@ -98,9 +99,9 @@ impl HtmlToAstParser {
if attrs.block {
self.flush_inline_buffer();
self.blocks
.push(Node::code_block(Some(attrs.lang.into()), attrs.text.into()));
.push(Node::code_block(Some(attrs.lang), attrs.text));
} else {
self.inline_buffer.push(Node::InlineCode(attrs.text.into()));
self.inline_buffer.push(Node::InlineCode(attrs.text));
}
Ok(())
}
@ -201,8 +202,8 @@ impl HtmlToAstParser {
.0
.iter()
.map(|(name, value)| HtmlAttribute {
name: name.to_string(),
value: value.to_string(),
name: name.resolve().to_string().into(),
value: value.clone(),
})
.collect();
@ -210,7 +211,7 @@ impl HtmlToAstParser {
self.convert_children_into(&mut children, element)?;
Ok(Node::HtmlElement(CmarkHtmlElement {
tag: element.tag.resolve().to_string(),
tag: element.tag.resolve().to_string().into(),
attributes,
children,
self_closing: element.children.is_empty(),
@ -235,8 +236,7 @@ impl HtmlToAstParser {
for child in &element.children {
match child {
HtmlNode::Text(text, _) => {
self.inline_buffer
.push(Node::Text(text.as_str().to_string()));
self.inline_buffer.push(Node::Text(text.clone()));
}
HtmlNode::Element(element) => {
self.convert_element(element)?;
@ -265,7 +265,7 @@ impl HtmlToAstParser {
}
#[derive(Debug, Clone)]
pub(crate) struct Comment(pub String);
pub(crate) struct Comment(pub EcoString);
impl CustomNode for Comment {
fn as_any(&self) -> &dyn std::any::Any {

View file

@ -49,7 +49,7 @@ impl HtmlToAstParser {
let mut content = Vec::new();
self.convert_children_into(&mut content, element)?;
self.inline_buffer.push(Node::Link {
url: attrs.dest.into(),
url: attrs.dest,
title: None,
content,
});
@ -59,11 +59,10 @@ impl HtmlToAstParser {
/// Convert image element
pub fn convert_image(&mut self, element: &HtmlElement) -> Result<()> {
let attrs = ImageAttr::parse(&element.attrs)?;
let src = attrs.src.as_str();
self.inline_buffer.push(Node::Image {
url: src.to_string(),
url: attrs.src,
title: None,
alt: vec![Node::Text(attrs.alt.into())],
alt: vec![Node::Text(attrs.alt)],
});
Ok(())
}

View file

@ -1,6 +1,7 @@
//! HTML list parsing module, handling conversion of ordered and unordered lists
use cmark_writer::ast::{ListItem, Node};
use ecow::eco_format;
use typst::html::{tag, HtmlElement, HtmlNode};
use crate::attributes::{ListItemAttr, TypliteAttrsParser};
@ -32,7 +33,7 @@ impl ListParser {
let mut li_buffer = Vec::new();
if parser.feat.annotate_elem {
li_buffer.push(Node::Custom(Box::new(super::core::Comment(format!(
li_buffer.push(Node::Custom(Box::new(super::core::Comment(eco_format!(
"typlite:begin:list-item {}",
parser.list_level - 1
)))));
@ -41,7 +42,7 @@ impl ListParser {
for li_child in &li.children {
match li_child {
HtmlNode::Text(text, _) => {
li_buffer.push(Node::Text(text.as_str().to_string()));
li_buffer.push(Node::Text(text.clone()));
}
HtmlNode::Element(child_elem) => {
let element_content =
@ -56,7 +57,7 @@ impl ListParser {
}
if parser.feat.annotate_elem {
li_buffer.push(Node::Custom(Box::new(super::core::Comment(format!(
li_buffer.push(Node::Custom(Box::new(super::core::Comment(eco_format!(
"typlite:end:list-item {}",
parser.list_level - 1
)))));

View file

@ -6,7 +6,7 @@ use std::sync::{Arc, LazyLock};
use base64::Engine;
use cmark_writer::ast::{HtmlAttribute, HtmlElement as CmarkHtmlElement, Node};
use ecow::eco_format;
use ecow::{eco_format, EcoString};
use tinymist_project::{base::ShadowApi, EntryReader, TaskInputs, MEMORY_MAIN_ENTRY};
use typst::{
foundations::{Bytes, Dict, IntoValue},
@ -47,12 +47,12 @@ impl HtmlToAstParser {
if element.children.len() != 1 {
// Construct error node
return Node::HtmlElement(CmarkHtmlElement {
tag: "div".to_string(),
tag: EcoString::inline("div"),
attributes: vec![HtmlAttribute {
name: "class".to_string(),
value: "error".to_string(),
name: EcoString::inline("class"),
value: EcoString::inline("error"),
}],
children: vec![Node::Text(format!(
children: vec![Node::Text(eco_format!(
"source contains not only one child: {}, whose attrs: {:?}",
element.children.len(),
element.attrs
@ -64,12 +64,12 @@ impl HtmlToAstParser {
let Some(HtmlNode::Frame(frame)) = element.children.first() else {
// todo: utils to remove duplicated error construction
return Node::HtmlElement(CmarkHtmlElement {
tag: "div".to_string(),
tag: EcoString::inline("div"),
attributes: vec![HtmlAttribute {
name: "class".to_string(),
value: "error".to_string(),
name: EcoString::inline("class"),
value: EcoString::inline("error"),
}],
children: vec![Node::Text(format!(
children: vec![Node::Text(eco_format!(
"source contains not a frame, but: {:?}",
element.children
))],
@ -83,12 +83,12 @@ impl HtmlToAstParser {
Err(e) => {
// Construct error node
return Node::HtmlElement(CmarkHtmlElement {
tag: "div".to_string(),
tag: EcoString::inline("div"),
attributes: vec![HtmlAttribute {
name: "class".to_string(),
value: "error".to_string(),
name: EcoString::inline("class"),
value: EcoString::inline("error"),
}],
children: vec![Node::Text(format!("Error creating source URL: {e}"))],
children: vec![Node::Text(eco_format!("Error creating source URL: {e}"))],
self_closing: false,
});
}
@ -103,17 +103,15 @@ impl HtmlToAstParser {
});
Node::HtmlElement(CmarkHtmlElement {
tag: "source".to_string(),
tag: EcoString::inline("source"),
attributes: vec![
HtmlAttribute {
name: "media".to_string(),
value: media
.map(|m| m.to_string())
.unwrap_or_else(|| "all".to_string()),
name: EcoString::inline("media"),
value: media.unwrap_or_else(|| "all".into()),
},
HtmlAttribute {
name: "srcset".to_string(),
value: frame_url.to_string(),
name: EcoString::inline("srcset"),
value: frame_url.to_string().into(),
},
],
children: vec![],
@ -125,7 +123,7 @@ impl HtmlToAstParser {
pub fn convert_frame(&mut self, frame: &Frame) -> Node {
if self.feat.remove_html {
// todo: make error silent is not good.
return Node::Text(String::new());
return Node::Text(EcoString::new());
}
let svg = typst_svg::svg_frame(frame);
@ -139,7 +137,7 @@ impl HtmlToAstParser {
Ok(url @ AssetUrl::Embedded(..)) => Self::create_embedded_frame(&url),
Ok(AssetUrl::External(file_path)) => Node::Custom(Box::new(ExternalFrameNode {
file_path,
alt_text: "typst-frame".to_string(),
alt_text: EcoString::inline("typst-frame"),
svg,
})),
Err(e) => {
@ -149,12 +147,12 @@ impl HtmlToAstParser {
} else {
// Construct error node
Node::HtmlElement(CmarkHtmlElement {
tag: "div".to_string(),
tag: EcoString::inline("div"),
attributes: vec![HtmlAttribute {
name: "class".to_string(),
value: "error".to_string(),
name: EcoString::inline("class"),
value: EcoString::inline("error"),
}],
children: vec![Node::Text(format!("Error creating frame URL: {}", e))],
children: vec![Node::Text(eco_format!("Error creating frame URL: {e}"))],
self_closing: false,
})
}
@ -165,15 +163,15 @@ impl HtmlToAstParser {
/// Create embedded frame node
fn create_embedded_frame(url: &AssetUrl) -> Node {
Node::HtmlElement(CmarkHtmlElement {
tag: "img".to_string(),
tag: EcoString::inline("img"),
attributes: vec![
HtmlAttribute {
name: "alt".to_string(),
value: "typst-block".to_string(),
name: EcoString::inline("alt"),
value: EcoString::inline("typst-block"),
},
HtmlAttribute {
name: "src".to_string(),
value: url.to_string(),
name: EcoString::inline("src"),
value: url.to_string().into(),
},
],
children: vec![],
@ -213,22 +211,24 @@ impl HtmlToAstParser {
if self.feat.remove_html {
eprintln!("Removing idoc element due to remove_html feature");
// todo: make error silent is not good.
return Node::Text(String::new());
return Node::Text(EcoString::new());
}
let attrs = match IdocAttr::parse(&element.attrs) {
Ok(attrs) => attrs,
Err(e) => {
if self.feat.soft_error {
return Node::Text(format!("Error parsing idoc attributes: {e}"));
return Node::Text(eco_format!("Error parsing idoc attributes: {e}"));
} else {
// Construct error node
return Node::HtmlElement(CmarkHtmlElement {
tag: "div".to_string(),
tag: EcoString::inline("div"),
attributes: vec![HtmlAttribute {
name: "class".to_string(),
value: "error".to_string(),
name: EcoString::inline("class"),
value: EcoString::inline("error"),
}],
children: vec![Node::Text(format!("Error parsing idoc attributes: {e}"))],
children: vec![Node::Text(eco_format!(
"Error parsing idoc attributes: {e}"
))],
self_closing: false,
});
}
@ -275,16 +275,16 @@ impl HtmlToAstParser {
Ok(doc) => doc,
Err(e) => {
if self.feat.soft_error {
return Node::Text(format!("Error compiling idoc: {e:?}"));
return Node::Text(eco_format!("Error compiling idoc: {e:?}"));
} else {
// Construct error node
return Node::HtmlElement(CmarkHtmlElement {
tag: "div".to_string(),
tag: EcoString::inline("div"),
attributes: vec![HtmlAttribute {
name: "class".to_string(),
value: "error".to_string(),
name: EcoString::inline("class"),
value: EcoString::inline("error"),
}],
children: vec![Node::Text(format!("Error compiling idoc: {e:?}"))],
children: vec![Node::Text(eco_format!("Error compiling idoc: {e:?}"))],
self_closing: false,
});
}

View file

@ -2,6 +2,7 @@
use cmark_writer::ast::Node;
use cmark_writer::gfm::TableAlignment;
use ecow::EcoString;
use typst::html::{tag, HtmlElement, HtmlNode};
use typst::utils::PicoStr;
@ -181,7 +182,7 @@ impl TableParser {
/// Merge cell content nodes into a single node
fn merge_cell_content(content: Vec<Node>) -> Node {
match content.len() {
0 => Node::Text("".to_string()),
0 => Node::Text(EcoString::new()),
1 => content.into_iter().next().unwrap(),
_ => Node::Custom(Box::new(InlineNode { content })),
}

View file

@ -81,7 +81,7 @@ impl DocxWriter {
} = node
{
// Process the image
if let Ok(img_data) = fs::read(url) {
if let Ok(img_data) = fs::read(url.as_str()) {
let alt_text = figure_node.caption.clone();
// Add the image with caption
docx = self.image_processor.process_image_data(
@ -191,7 +191,7 @@ impl DocxWriter {
title: _,
alt: _,
} => {
if let Ok(img_data) = fs::read(url) {
if let Ok(img_data) = fs::read(url.as_str()) {
run = self.image_processor.process_inline_image(run, &img_data)?;
} else {
run = run.add_text(format!("[Image not found: {}]", url));

View file

@ -248,7 +248,7 @@ impl LaTeXWriter {
} = node
{
// Path to the image file
let path = unix_slash(Path::new(url));
let path = unix_slash(Path::new(url.as_str()));
// Write includegraphics command
output.push_str("\\includegraphics[width=0.8\\textwidth]{");
@ -352,7 +352,7 @@ impl LaTeXWriter {
"".into()
};
let path = unix_slash(Path::new(url));
let path = unix_slash(Path::new(&url.as_str()));
output.push_str("\\begin{figure}\n");
output.push_str("\\centering\n");