1use std::{num::NonZero, str::FromStr};
2
3use comemo::Track;
4use ecow::EcoVec;
5use pulldown_cmark::{Alignment, CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
6use pulldown_cmark_ast::{Ast, Tree};
7use serde::{Deserialize, Serialize};
8use syntect::{html::ClassStyle, parsing::SyntaxSet, util::LinesWithEndings};
9use typst::{
10 diag::{EcoString, SourceDiagnostic},
11 foundations::{Content, Packed, Scope, Smart, Value},
12 layout::{Celled, Length, Ratio, Sizing, TrackSizings},
13 model::{
14 EnumElem, EnumItem, FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ListItem,
15 ParbreakElem, TableCell, TableChild, TableElem, TableHeader, TableItem, Url,
16 },
17 syntax::Span,
18 text::{LinebreakElem, RawContent, RawElem, SpaceElem, StrikeElem, TextElem},
19 visualize::LineElem,
20 World,
21};
22
23use crate::render::typst::TypstWrapperWorld;
24
25#[derive(thiserror::Error, Debug, PartialEq, Eq)]
26pub enum RenderError {
27 #[error("Error while processing typst: {0:?}")]
28 TypstError(Vec<SourceDiagnostic>),
29 #[error("HTML tags are unsupported in Markdown")]
30 UnsupportedHtml,
31}
32
33type RenderResult<T> = Result<T, RenderError>;
34
35impl From<EcoVec<SourceDiagnostic>> for RenderError {
36 fn from(value: EcoVec<SourceDiagnostic>) -> Self {
37 Self::TypstError(value.to_vec())
38 }
39}
40
41impl From<RenderError> for std::io::Error {
42 fn from(val: RenderError) -> Self {
43 std::io::Error::other(format!("{}", val))
44 }
45}
46
47const CMARK_OPTIONS: Options = Options::from_bits_truncate(
49 (1 << 1) | (1 << 5) | (1 << 3) | (1 << 10), );
54
55#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
56#[repr(transparent)]
57#[serde(transparent)]
58pub struct MarkdownRenderable(String);
59
60impl From<String> for MarkdownRenderable {
61 fn from(value: String) -> Self {
62 Self(value)
63 }
64}
65
66impl From<&str> for MarkdownRenderable {
67 fn from(value: &str) -> Self {
68 Self(value.into())
69 }
70}
71
72impl FromStr for MarkdownRenderable {
73 type Err = std::convert::Infallible;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 Ok(Self::from(s))
77 }
78}
79
80impl MarkdownRenderable {
81 pub fn from_raw(raw: impl Into<String>) -> Self {
82 Self(raw.into())
83 }
84
85 pub fn raw(&self) -> &str {
86 &self.0
87 }
88
89 pub fn html(&self) -> RenderResult<String> {
93 let parser = Parser::new_ext(self.raw(), CMARK_OPTIONS);
94 let mut errors = Vec::new();
95 let mut current_code = None;
96 let syntax_set = SyntaxSet::load_defaults_newlines();
97 let parser = parser.flat_map(|event| -> Box<dyn Iterator<Item = Event>> {
98 match event {
99 pulldown_cmark::Event::InlineMath(cow_str) => {
100 let f = format!(
103 "#set page(width: auto, height: auto, margin: 0em)
104 ${}$",
105 cow_str
106 );
107 let world = TypstWrapperWorld::new(f);
108 match typst::compile(&world).output {
109 Ok(doc) => {
110 let svg = typst_svg::svg(&doc.pages[0]);
111 Box::new(std::iter::once(Event::InlineHtml(svg.into())))
112 }
113 Err(err) => {
114 errors.extend(err);
115 Box::new(std::iter::once(Event::Text("".into())))
116 }
117 }
118 }
119 pulldown_cmark::Event::DisplayMath(cow_str) => {
120 let f = format!(
123 "
124 #set page(width: auto, height: auto, margin: 0em)
125 $ {} $
126 ",
127 cow_str
128 );
129 let world = TypstWrapperWorld::new(f);
130 match typst::compile(&world).output {
131 Ok(doc) => {
132 let svg = typst_svg::svg(&doc.pages[0]);
133 Box::new(std::iter::once(Event::Html(svg.into())))
134 }
135 Err(err) => {
136 errors.extend(err);
137
138 Box::new(std::iter::once(Event::Text("".into())))
139 }
140 }
141 }
142 pulldown_cmark::Event::Start(Tag::CodeBlock(kind)) => {
143 let lang = match kind {
144 CodeBlockKind::Indented => String::new(),
145 CodeBlockKind::Fenced(cow_str) => cow_str.to_string(),
146 };
147
148 let syntax = syntax_set
149 .find_syntax_by_name(&lang)
150 .or_else(|| syntax_set.find_syntax_by_extension(&lang))
151 .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
152 current_code = Some(syntect::html::ClassedHTMLGenerator::new_with_class_style(
153 syntax,
154 &syntax_set,
155 ClassStyle::Spaced,
156 ));
157 Box::new(std::iter::empty())
158 }
159 pulldown_cmark::Event::Text(t) => {
160 if let Some(ref mut code) = current_code {
161 for line in LinesWithEndings::from(&t) {
162 code.parse_html_for_line_which_includes_newline(line)
163 .unwrap();
164 }
165 Box::new(std::iter::empty())
166 } else {
167 Box::new(std::iter::once(Event::Text(t)))
168 }
169 }
170 pulldown_cmark::Event::End(TagEnd::CodeBlock) => {
171 let code = current_code.take().expect("Can't have end without start");
172 let out = code.finalize();
173 Box::new(std::iter::once(Event::Html(
174 format!("<pre>{}</pre>", out).into(),
175 )))
176 }
177 e => Box::new(std::iter::once(e)),
178 }
179 });
180 let mut s = String::new();
181 pulldown_cmark::html::push_html(&mut s, parser);
182 if !errors.is_empty() {
183 Err(RenderError::TypstError(errors))?
184 } else {
185 Ok(s)
186 }
187 }
188
189 pub fn content(&self, world: &impl World) -> RenderResult<Content> {
191 render_markdown(self.raw(), world)
192 }
193}
194
195fn map_align(a: &Alignment) -> Smart<typst::layout::Alignment> {
196 match a {
197 Alignment::None => Smart::Auto,
198 Alignment::Left => {
199 Smart::Custom(typst::layout::Alignment::H(typst::layout::HAlignment::Left))
200 }
201 Alignment::Center => Smart::Custom(typst::layout::Alignment::H(
202 typst::layout::HAlignment::Center,
203 )),
204 Alignment::Right => Smart::Custom(typst::layout::Alignment::H(
205 typst::layout::HAlignment::Right,
206 )),
207 }
208}
209
210struct TypstMarkdownRenderer<'a> {
211 world: &'a dyn World,
212}
213
214impl<'a> TypstMarkdownRenderer<'a> {
215 fn new(world: &'a dyn World) -> Self {
216 Self { world }
217 }
218
219 fn render_tree(&self, tree: Tree) -> RenderResult<Content> {
220 match tree {
221 Tree::Group(g) => match g.tag.item {
222 Tag::Paragraph => Ok(Content::sequence(
223 std::iter::once(Ok(Content::new(ParbreakElem::new())))
224 .chain(g.stream.0.into_iter().map(|t| self.render_tree(t)))
225 .chain(std::iter::once(Ok(Content::new(ParbreakElem::new()))))
226 .collect::<RenderResult<Vec<_>>>()?,
227 )),
228 Tag::Heading { level, .. } => Ok(Content::new(
229 HeadingElem::new(self.render_ast(g.stream)?).with_level(
230 typst::foundations::Smart::Custom(
231 NonZero::new(level as usize).expect("1 <= level <= 6"),
232 ),
233 ),
234 )),
235 Tag::BlockQuote(_) => {
236 let content = Content::sequence(
239 std::iter::once(Ok(Content::new(ParbreakElem::new())))
240 .chain(g.stream.0.into_iter().map(|t| self.render_tree(t)))
241 .chain(std::iter::once(Ok(Content::new(ParbreakElem::new()))))
242 .collect::<RenderResult<Vec<_>>>()?,
243 );
244 Ok(Content::new(FigureElem::new(content.aligned(
245 typst::layout::Alignment::H(typst::layout::HAlignment::Left),
246 ))))
247 }
248 Tag::CodeBlock(code_block_kind) => {
249 let content = self.render_ast_to_text(g.stream);
250 let elem = RawElem::new(RawContent::Text(content)).with_block(true);
251 let elem = match code_block_kind {
252 CodeBlockKind::Indented => elem,
253 CodeBlockKind::Fenced(s) => {
254 if s.is_empty() {
255 elem
256 } else {
257 elem.with_lang(Some(s.as_ref().into()))
258 }
259 }
260 };
261 Ok(Content::new(FigureElem::new(Content::new(elem))))
262 }
263 Tag::HtmlBlock => Err(RenderError::UnsupportedHtml),
264 Tag::List(ord) => {
265 if let Some(ord) = ord {
266 let packed = g
267 .stream
268 .0
269 .into_iter()
270 .enumerate()
271 .map(|(i, t)| -> RenderResult<_> {
272 match t {
273 Tree::Group(group) => match group.tag.item {
274 Tag::Item => Ok(Packed::new(
275 EnumItem::new(self.render_ast(group.stream)?)
276 .with_number(Some(ord as usize + i)),
277 )),
278 _ => unreachable!(),
279 },
280 _ => unreachable!(),
281 }
282 })
283 .collect::<RenderResult<Vec<_>>>()?;
284 Ok(Content::new(EnumElem::new(packed)))
285 } else {
286 let packed = g
287 .stream
288 .0
289 .into_iter()
290 .map(|t| self.render_tree(t).map(|c| c.into_packed().unwrap()))
291 .collect::<RenderResult<_>>()?;
292 Ok(Content::new(ListElem::new(packed)))
293 }
294 }
295 Tag::Item => Ok(Content::new(ListItem::new(self.render_ast(g.stream)?))),
296 Tag::FootnoteDefinition(_) => unreachable!("Feature is disabled"),
297 Tag::Table(align) => {
298 let mut things = g.stream.0;
299 let mut children = Vec::new();
300 let header = match things.remove(0) {
301 Tree::Group(hg) => match hg.tag.item {
302 Tag::TableHead => hg.stream,
303 _ => unreachable!(),
304 },
305 _ => unreachable!(),
306 };
307
308 let cols = header.0.len();
309
310 children.push(TableChild::Header(Packed::new(TableHeader::new(
311 header
312 .0
313 .into_iter()
314 .map(|t| {
315 self.render_tree(t)
316 .map(|c| c.into_packed().unwrap())
317 .map(TableItem::Cell)
318 })
319 .collect::<RenderResult<_>>()?,
320 ))));
321
322 for thing in things {
323 let row = match thing {
324 Tree::Group(hg) => match hg.tag.item {
325 Tag::TableRow => hg.stream.0,
326 _ => unreachable!(),
327 },
328 _ => unreachable!(),
329 };
330 children.extend_from_slice(
331 &row.into_iter()
332 .map(|t| {
333 self.render_tree(t)
334 .map(|c| c.into_packed().unwrap())
335 .map(TableItem::Cell)
336 .map(TableChild::Item)
337 })
338 .collect::<RenderResult<Vec<_>>>()?,
339 );
340 }
341
342 let columns = (0..cols).map(|_| Sizing::Auto).collect::<Vec<_>>();
343
344 Ok(Content::new(FigureElem::new(Content::new(
345 TableElem::new(children)
346 .with_columns(TrackSizings(columns.into()))
347 .with_align(Celled::Array(align.iter().map(map_align).collect())),
348 ))))
349 }
350 Tag::TableHead => {
351 let items = g
352 .stream
353 .0
354 .into_iter()
355 .map(|t| {
356 self.render_tree(t)
357 .map(|c| c.into_packed().unwrap())
358 .map(TableItem::Cell)
359 })
360 .collect::<RenderResult<_>>()?;
361 Ok(Content::new(TableHeader::new(items)))
362 }
363 Tag::TableRow => g
364 .stream
365 .0
366 .into_iter()
367 .map(|t| {
368 self.render_tree(t)
369 .map(|c| c.into_packed().unwrap())
370 .map(TableItem::Cell)
371 })
372 .collect::<RenderResult<_>>()
373 .map(TableHeader::new)
374 .map(Content::new),
375 Tag::TableCell => self
376 .render_ast(g.stream)
377 .map(TableCell::new)
378 .map(Content::new),
379 Tag::Emphasis => self.render_ast(g.stream).map(Content::emph),
380 Tag::Strong => self.render_ast(g.stream).map(Content::strong),
381 Tag::Strikethrough => self
382 .render_ast(g.stream)
383 .map(StrikeElem::new)
384 .map(Content::new),
385 Tag::Link { dest_url, .. } => Ok(Content::new(LinkElem::new(
386 LinkTarget::Dest(typst::model::Destination::Url(
387 Url::new(&*dest_url).unwrap(),
388 )),
389 self.render_ast(g.stream)?,
390 ))),
391 Tag::Image { .. } => todo!(),
392 Tag::MetadataBlock(_) => unreachable!("Feature is disabled"),
393 },
394 Tree::Text(spanned) => Ok(Content::new(TextElem::new(spanned.item.as_ref().into()))),
395 Tree::Code(spanned) => Ok(Content::new(RawElem::new(RawContent::Text(
396 spanned.item.as_ref().into(),
397 )))),
398 Tree::Html(_) => Err(RenderError::UnsupportedHtml),
399 Tree::InlineHtml(_) => Err(RenderError::UnsupportedHtml),
400 Tree::FootnoteReference(_) => unreachable!("Feature is disabled"),
401 Tree::SoftBreak(_) => Ok(Content::new(SpaceElem::new())),
402 Tree::HardBreak(_) => Ok(Content::new(LinebreakElem::new())),
403 Tree::Rule(_) => Ok(Content::new(LineElem::new().with_length(
404 typst::layout::Rel {
405 rel: Ratio::new(1.),
406 abs: Length::zero(),
407 },
408 ))),
409 Tree::TaskListMarker(_) => unreachable!("Feature is disabled"),
410 Tree::InlineMath(spanned) => {
411 let content = spanned.item;
412
413 let val = typst::eval::eval_string(
414 self.world.track(),
415 &content,
416 Span::detached(),
417 typst::eval::EvalMode::Math,
418 Scope::new(),
419 )?;
420
421 match val {
422 Value::Content(content) => Ok(content),
423 _ => unreachable!(),
424 }
425 }
426 Tree::DisplayMath(spanned) => {
427 let content = spanned.item.trim();
428
429 let val = typst::eval::eval_string(
430 self.world.track(),
431 &format!("$ {} $", content),
432 Span::detached(),
433 typst::eval::EvalMode::Markup,
434 self.world.library().math.scope().clone(),
435 )?;
436
437 match val {
438 Value::Content(content) => Ok(content),
439 _ => unreachable!(),
440 }
441 }
442 }
443 }
444
445 fn render_ast(&self, ast: Ast) -> RenderResult<Content> {
446 Ok(Content::sequence(
447 ast.0
448 .into_iter()
449 .map(|t| self.render_tree(t))
450 .collect::<RenderResult<Vec<_>>>()?,
451 ))
452 }
453
454 fn render_ast_to_text(&self, ast: Ast) -> EcoString {
455 let mut s = EcoString::new();
456 for t in ast.0 {
457 match t {
458 Tree::Text(spanned) => {
459 s.push_str(&spanned.item);
460 }
461 s => unreachable!("need to impl {:?}", s),
462 }
463 }
464 s
465 }
466
467 fn render(&self, markdown: impl AsRef<str>) -> RenderResult<Content> {
468 let markdown = markdown.as_ref();
469 let ast = Ast::new_ext(markdown, CMARK_OPTIONS);
470 self.render_ast(ast)
471 }
472}
473
474pub fn render_markdown(markdown: impl AsRef<str>, world: &impl World) -> RenderResult<Content> {
475 TypstMarkdownRenderer::new(world).render(markdown)
476}