1#![allow(missing_docs)]
5
6pub mod attributes;
7pub mod common;
8mod error;
9pub mod parser;
10pub mod tags;
11pub mod writer;
12
13use std::path::PathBuf;
14use std::str::FromStr;
15use std::sync::Arc;
16
17pub use error::*;
18
19use cmark_writer::ast::Node;
20use tinymist_project::base::ShadowApi;
21use tinymist_project::vfs::WorkspaceResolver;
22use tinymist_project::{EntryReader, LspWorld, TaskInputs};
23use tinymist_std::error::prelude::*;
24use typst::World;
25use typst::foundations::Bytes;
26use typst::html::HtmlDocument;
27use typst_syntax::VirtualPath;
28
29pub use crate::common::Format;
30use crate::parser::HtmlToAstParser;
31use crate::writer::WriterFactory;
32use typst_syntax::FileId;
33
34use crate::tinymist_std::typst::foundations::Value::Str;
35use crate::tinymist_std::typst::{LazyHash, TypstDict};
36
37pub type Result<T, Err = Error> = std::result::Result<T, Err>;
39
40pub use cmark_writer::ast;
41pub use tinymist_project::CompileOnceArgs;
42pub use tinymist_std;
43
44#[derive(Clone)]
45pub struct MarkdownDocument {
46 pub base: HtmlDocument,
47 world: Arc<LspWorld>,
48 feat: TypliteFeat,
49 ast: Option<Node>,
50}
51
52impl MarkdownDocument {
53 pub fn new(base: HtmlDocument, world: Arc<LspWorld>, feat: TypliteFeat) -> Self {
55 Self {
56 base,
57 world,
58 feat,
59 ast: None,
60 }
61 }
62
63 pub fn with_ast(
65 base: HtmlDocument,
66 world: Arc<LspWorld>,
67 feat: TypliteFeat,
68 ast: Node,
69 ) -> Self {
70 Self {
71 base,
72 world,
73 feat,
74 ast: Some(ast),
75 }
76 }
77
78 pub fn parse(&self) -> tinymist_std::Result<Node> {
80 if let Some(ast) = &self.ast {
81 return Ok(ast.clone());
82 }
83 let parser = HtmlToAstParser::new(self.feat.clone(), &self.world);
84 parser.parse(&self.base.root).context_ut("failed to parse")
85 }
86
87 pub fn to_md_string(&self) -> tinymist_std::Result<ecow::EcoString> {
89 let mut output = ecow::EcoString::new();
90 let ast = self.parse()?;
91
92 let mut writer = WriterFactory::create(Format::Md);
93 writer
94 .write_eco(&ast, &mut output)
95 .context_ut("failed to write")?;
96
97 Ok(output)
98 }
99
100 pub fn to_text_string(&self) -> tinymist_std::Result<ecow::EcoString> {
102 let mut output = ecow::EcoString::new();
103 let ast = self.parse()?;
104
105 let mut writer = WriterFactory::create(Format::Text);
106 writer
107 .write_eco(&ast, &mut output)
108 .context_ut("failed to write")?;
109
110 Ok(output)
111 }
112
113 pub fn to_tex_string(&self) -> tinymist_std::Result<ecow::EcoString> {
115 let mut output = ecow::EcoString::new();
116 let ast = self.parse()?;
117
118 let mut writer = WriterFactory::create(Format::LaTeX);
119 writer
120 .write_eco(&ast, &mut output)
121 .context_ut("failed to write")?;
122
123 Ok(output)
124 }
125
126 #[cfg(feature = "docx")]
128 pub fn to_docx(&self) -> tinymist_std::Result<Vec<u8>> {
129 let ast = self.parse()?;
130
131 let mut writer = WriterFactory::create(Format::Docx);
132 writer.write_vec(&ast).context_ut("failed to write")
133 }
134}
135
136#[derive(Debug, Default, Clone, Copy)]
138pub enum ColorTheme {
139 #[default]
140 Light,
141 Dark,
142}
143
144#[derive(Debug, Default, Clone)]
145pub struct TypliteFeat {
146 pub color_theme: Option<ColorTheme>,
148 pub assets_path: Option<PathBuf>,
150 pub gfm: bool,
152 pub annotate_elem: bool,
154 pub soft_error: bool,
156 pub remove_html: bool,
158 pub target: Format,
160 pub import_context: Option<String>,
163 pub processor: Option<String>,
181}
182
183impl TypliteFeat {
184 pub fn prepare_world(
185 &self,
186 world: &LspWorld,
187 format: Format,
188 ) -> tinymist_std::Result<LspWorld> {
189 let entry = world.entry_state();
190 let main = entry.main();
191 let current = main.context("no main file in workspace")?;
192
193 if WorkspaceResolver::is_package_file(current) {
194 bail!("package file is not supported");
195 }
196
197 let wrap_main_id = current.join("__wrap_md_main.typ");
198
199 let (main_id, main_content) = match self.processor.as_ref() {
200 None => (wrap_main_id, None),
201 Some(processor) => {
202 let main_id = current.join("__md_main.typ");
203 let content = format!(
204 r#"#import {processor:?}: article
205#article(include "__wrap_md_main.typ")"#
206 );
207
208 (main_id, Some(Bytes::from_string(content)))
209 }
210 };
211
212 let mut dict = TypstDict::new();
213 dict.insert("x-target".into(), Str("md".into()));
214 if format == Format::Text || self.remove_html {
215 dict.insert("x-remove-html".into(), Str("true".into()));
216 }
217
218 let task_inputs = TaskInputs {
219 entry: Some(entry.select_in_workspace(main_id.vpath().as_rooted_path())),
220 inputs: Some(Arc::new(LazyHash::new(dict))),
221 };
222
223 let mut world = world.task(task_inputs).html_task().into_owned();
224
225 let markdown_id = FileId::new(
226 Some(
227 typst_syntax::package::PackageSpec::from_str("@local/_markdown:0.1.0")
228 .context_ut("failed to import markdown package")?,
229 ),
230 VirtualPath::new("lib.typ"),
231 );
232
233 world
234 .map_shadow_by_id(
235 markdown_id.join("typst.toml"),
236 Bytes::from_string(include_str!("markdown-typst.toml")),
237 )
238 .context_ut("cannot map markdown-typst.toml")?;
239 world
240 .map_shadow_by_id(
241 markdown_id,
242 Bytes::from_string(include_str!("markdown.typ")),
243 )
244 .context_ut("cannot map markdown.typ")?;
245
246 world
247 .map_shadow_by_id(
248 wrap_main_id,
249 Bytes::from_string(format!(
250 r#"#import "@local/_markdown:0.1.0": md-doc, example; #show: md-doc
251{}"#,
252 world
253 .source(current)
254 .context_ut("failed to get main file content")?
255 .text()
256 )),
257 )
258 .context_ut("cannot map source for main file")?;
259
260 if let Some(main_content) = main_content {
261 world
262 .map_shadow_by_id(main_id, main_content)
263 .context_ut("cannot map source for main file")?;
264 }
265
266 Ok(world)
267 }
268}
269
270pub struct Typlite {
272 world: Arc<LspWorld>,
274 feat: TypliteFeat,
276 format: Format,
278}
279
280impl Typlite {
281 pub fn new(world: Arc<LspWorld>) -> Self {
283 Self {
284 world,
285 feat: Default::default(),
286 format: Format::Md,
287 }
288 }
289
290 pub fn with_feature(mut self, feat: TypliteFeat) -> Self {
292 self.feat = feat;
293 self
294 }
295
296 pub fn with_format(mut self, format: Format) -> Self {
297 self.format = format;
298 self
299 }
300
301 pub fn convert(self) -> tinymist_std::Result<ecow::EcoString> {
303 match self.format {
304 Format::Md => self.convert_doc(Format::Md)?.to_md_string(),
305 Format::LaTeX => self.convert_doc(Format::LaTeX)?.to_tex_string(),
306 Format::Text => self.convert_doc(Format::Text)?.to_text_string(),
307 #[cfg(feature = "docx")]
308 Format::Docx => bail!("docx format is not supported"),
309 }
310 }
311
312 #[cfg(feature = "docx")]
314 pub fn to_docx(self) -> tinymist_std::Result<Vec<u8>> {
315 if self.format != Format::Docx {
316 bail!("format is not DOCX");
317 }
318 self.convert_doc(Format::Docx)?.to_docx()
319 }
320
321 pub fn convert_doc(self, format: Format) -> tinymist_std::Result<MarkdownDocument> {
323 let world = Arc::new(self.feat.prepare_world(&self.world, format)?);
324 let feat = self.feat.clone();
325 Self::convert_doc_prepared(feat, format, world)
326 }
327
328 pub fn convert_doc_prepared(
330 feat: TypliteFeat,
331 format: Format,
332 world: Arc<LspWorld>,
333 ) -> tinymist_std::Result<MarkdownDocument> {
334 let base = typst::compile(&world).output?;
336 let mut feat = feat;
337 feat.target = format;
338 Ok(MarkdownDocument::new(base, world.clone(), feat))
339 }
340}
341
342#[cfg(test)]
343mod tests;