Skip to content

Commit 696c280

Browse files
feat: support html themes and components (#105)
1 parent aae8be0 commit 696c280

File tree

16 files changed

+948
-121
lines changed

16 files changed

+948
-121
lines changed

assets/artifacts

Submodule artifacts updated 1 file

cli/src/project.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,12 @@ impl Project {
446446
})
447447
};
448448

449+
let rel_data_path = file_name
450+
.to_str()
451+
.ok_or_else(|| error_once!("path_to_root is not a valid utf-8 string"))?
452+
// windows
453+
.replace('\\', "/");
454+
449455
let (description, content) = match self.render_mode.clone() {
450456
RenderMode::StaticHtml => {
451457
let doc = doc
@@ -456,21 +462,34 @@ impl Project {
456462
_ => bail!("doc is not Html"),
457463
};
458464

465+
let content = self
466+
.hr
467+
.handlebars
468+
.render(
469+
"typst_load_html_trampoline",
470+
&json!({
471+
"rel_data_path": rel_data_path,
472+
}),
473+
)
474+
.map_err(map_string_err(
475+
"render typst_load_html_trampoline for compile_chapter",
476+
))?;
477+
459478
let res = self
460479
.tr
461480
.report(static_html(html_doc))
462481
.expect("failed to render static html");
463482

464483
let description: Option<Result<String>> = res.description().map(From::from).map(Ok);
465-
(description.unwrap_or_else(auto_description)?, res.body)
484+
(
485+
description.unwrap_or_else(auto_description)?,
486+
format!(
487+
r#"{content}<div class="typst-preload-content" style="display: none">{}</div>"#,
488+
res.body
489+
),
490+
)
466491
}
467492
RenderMode::DynPaged | RenderMode::StaticHtmlDynPaged => {
468-
let rel_data_path = file_name
469-
.to_str()
470-
.ok_or_else(|| error_once!("path_to_root is not a valid utf-8 string"))?
471-
// windows
472-
.replace('\\', "/");
473-
474493
let content = self
475494
.hr
476495
.handlebars

cli/src/render/html.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ impl HtmlRenderer {
2020
("head", &theme.head),
2121
("header", &theme.header),
2222
("typst_load_trampoline", &theme.typst_load_trampoline),
23+
(
24+
"typst_load_html_trampoline",
25+
&theme.typst_load_html_trampoline,
26+
),
2327
] {
2428
handlebars
2529
.register_template_string(name, String::from_utf8(partial.clone()).unwrap())

cli/src/render/typst.rs

Lines changed: 107 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use reflexo_typst::{
1818
config::CompileOpts,
1919
escape::{escape_str, AttributeEscapes},
2020
path::PathClean,
21+
static_html,
2122
system::SystemWorldComputeGraph,
2223
vector::{
2324
ir::{LayoutRegionNode, Module, Page, PageMetadata},
@@ -134,10 +135,18 @@ impl TypstRenderer {
134135
format!("{prefix}-{theme}")
135136
});
136137

137-
self.extension = if theme.is_empty() {
138-
"multi.sir.in".into()
138+
if self.static_html {
139+
self.extension = if theme.is_empty() {
140+
"html".into()
141+
} else {
142+
format!("{theme}.html").into()
143+
};
139144
} else {
140-
format!("{theme}.multi.sir.in").into()
145+
self.extension = if theme.is_empty() {
146+
"multi.sir.in".into()
147+
} else {
148+
format!("{theme}.multi.sir.in").into()
149+
};
141150
};
142151
}
143152

@@ -417,19 +426,13 @@ impl TypstRenderer {
417426
items: Vec<OutlineItemRef>,
418427
) {
419428
if items.len() != chapters.len() {
420-
panic!(
421-
"cannot merge outline with different chapter
422-
count"
423-
);
429+
panic!("cannot merge outline with different chapter count");
424430
}
425431
for (idx, item) in items.into_iter().enumerate() {
426432
let chapter = &mut chapters[idx];
427433

428434
if chapter.item != item.item {
429-
panic!(
430-
"cannot merge outline with different
431-
chapter"
432-
);
435+
panic!("cannot merge outline with different chapter");
433436
}
434437

435438
Self::intern_pages(
@@ -663,74 +666,109 @@ impl TypstRenderer {
663666
TypstDocument::Paged(self.pure_compile::<TypstPagedDocument>(&graph)?)
664667
};
665668

666-
if self.static_html {
667-
return Ok(doc);
669+
for theme in THEME_LIST {
670+
if self.static_html {
671+
self.compile_html_page_with(theme)?;
672+
} else {
673+
self.compile_paged_page_with(theme, &graph, settings.clone())?;
674+
}
668675
}
669676

670-
for theme in THEME_LIST {
671-
self.set_theme_target(theme);
677+
Ok(doc)
678+
}
679+
680+
pub fn compile_html_page_with(&mut self, theme: &str) -> Result<TypstDocument> {
681+
let inputs = TaskInputs {
682+
entry: None,
683+
inputs: Some({
684+
let mut dict = TypstDict::new();
685+
self.set_theme_target(theme);
686+
dict.insert("x-target".into(), self.compiler.target.clone().into_value());
672687

673-
// let path = path.clone().to_owned();
674-
self.compiler
675-
.set_post_process_layout(move |_m, doc, layout| {
676-
// println!("post process {}", path.display());
688+
Arc::new(LazyHash::new(dict))
689+
}),
690+
};
691+
let graph = self.verse.computation_with(inputs);
692+
let doc = self.pure_compile::<TypstHtmlDocument>(&graph)?;
677693

678-
let LayoutRegionNode::Pages(pages) = layout else {
679-
unreachable!();
680-
};
694+
let html_doc = doc.as_ref();
681695

682-
let (mut meta, pages) = pages.take();
683-
684-
let introspector = &doc.introspector();
685-
let labels = doc
686-
.introspector()
687-
.all()
688-
.flat_map(|elem| elem.label().zip(elem.location()))
689-
.map(|(label, elem)| {
690-
(label.resolve().to_owned(), introspector.position(elem))
691-
})
692-
.map(|(label, pos)| {
693-
(
694-
label,
695-
format!(
696-
"p{}x{:.2}y{:.2}",
697-
pos.page,
698-
pos.point.x.to_pt(),
699-
pos.point.y.to_pt()
700-
),
701-
)
702-
})
703-
.collect::<Vec<_>>();
704-
// println!("{:#?}", labels);
705-
706-
let labels = serde_json::to_vec(&labels).unwrap_or_exit();
707-
let sema_label_meta = ("sema-label".into(), labels.into());
708-
709-
let mut custom = vec![sema_label_meta];
710-
711-
if settings.with_outline {
712-
let mut spans = SpanInternerImpl::default();
713-
714-
let outline = crate::outline::outline(&mut spans, &doc);
715-
let outline = serde_json::to_vec(&outline).unwrap_or_exit();
716-
let outline_meta = ("outline".into(), outline.into());
717-
custom.push(outline_meta);
718-
}
696+
let res = self
697+
.report(static_html(html_doc))
698+
.expect("failed to render static html");
719699

720-
meta.push(PageMetadata::Custom(custom));
700+
let dest = self.module_dest_path();
701+
std::fs::write(&dest, res.body).unwrap_or_exit();
721702

722-
LayoutRegionNode::Pages(Arc::new((meta, pages)))
723-
});
703+
Ok(TypstDocument::Html(doc.clone()))
704+
}
724705

725-
let res = DynSvgModuleExport::run(&graph, &self.compiler)?;
726-
if let Some(doc) = res {
727-
let content = doc.to_bytes();
728-
let dest = self.module_dest_path();
729-
std::fs::write(&dest, content).unwrap_or_exit();
730-
}
706+
pub fn compile_paged_page_with(
707+
&mut self,
708+
theme: &str,
709+
graph: &Arc<SystemWorldComputeGraph>,
710+
settings: CompilePageSetting,
711+
) -> Result<()> {
712+
self.set_theme_target(theme);
713+
714+
// let path = path.clone().to_owned();
715+
self.compiler
716+
.set_post_process_layout(move |_m, doc, layout| {
717+
// println!("post process {}", path.display());
718+
719+
let LayoutRegionNode::Pages(pages) = layout else {
720+
unreachable!();
721+
};
722+
723+
let (mut meta, pages) = pages.take();
724+
725+
let introspector = &doc.introspector();
726+
let labels = doc
727+
.introspector()
728+
.all()
729+
.flat_map(|elem| elem.label().zip(elem.location()))
730+
.map(|(label, elem)| (label.resolve().to_owned(), introspector.position(elem)))
731+
.map(|(label, pos)| {
732+
(
733+
label,
734+
format!(
735+
"p{}x{:.2}y{:.2}",
736+
pos.page,
737+
pos.point.x.to_pt(),
738+
pos.point.y.to_pt()
739+
),
740+
)
741+
})
742+
.collect::<Vec<_>>();
743+
// println!("{:#?}", labels);
744+
745+
let labels = serde_json::to_vec(&labels).unwrap_or_exit();
746+
let sema_label_meta = ("sema-label".into(), labels.into());
747+
748+
let mut custom = vec![sema_label_meta];
749+
750+
if settings.with_outline {
751+
let mut spans = SpanInternerImpl::default();
752+
753+
let outline = crate::outline::outline(&mut spans, &doc);
754+
let outline = serde_json::to_vec(&outline).unwrap_or_exit();
755+
let outline_meta = ("outline".into(), outline.into());
756+
custom.push(outline_meta);
757+
}
758+
759+
meta.push(PageMetadata::Custom(custom));
760+
761+
LayoutRegionNode::Pages(Arc::new((meta, pages)))
762+
});
763+
764+
let res = DynSvgModuleExport::run(graph, &self.compiler)?;
765+
if let Some(doc) = res {
766+
let content = doc.to_bytes();
767+
let dest = self.module_dest_path();
768+
std::fs::write(&dest, content).unwrap_or_exit();
731769
}
732770

733-
Ok(doc)
771+
Ok(())
734772
}
735773

736774
// todo: we should use same snapshot as that compiled documents

cli/src/theme.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct Theme {
2929
pub head: Vec<u8>,
3030
pub header: Vec<u8>,
3131
pub typst_load_trampoline: Vec<u8>,
32+
pub typst_load_html_trampoline: Vec<u8>,
3233

3334
asset: ThemeAsset,
3435
}
@@ -46,6 +47,7 @@ impl Default for Theme {
4647
head: default_theme_file!("head.hbs"),
4748
header: default_theme_file!("header.hbs"),
4849
typst_load_trampoline: default_theme_file!("typst-load-trampoline.hbs"),
50+
typst_load_html_trampoline: default_theme_file!("typst-load-html-trampoline.hbs"),
4951
asset: ThemeAsset::Static(EmbeddedThemeAsset::MdBook),
5052
}
5153
}
@@ -79,6 +81,10 @@ impl Theme {
7981
"typst-load-trampoline.hbs",
8082
&mut theme.typst_load_trampoline,
8183
),
84+
(
85+
"typst-load-html-trampoline.hbs",
86+
&mut theme.typst_load_html_trampoline,
87+
),
8288
];
8389

8490
let load_with_warn = |filename: &str, dest: &mut Vec<u8>| {

0 commit comments

Comments
 (0)