add: render_direct fn in lib
This commit is contained in:
parent
63a7b9d666
commit
95a8a5f814
3 changed files with 112 additions and 96 deletions
185
src/lib.rs
185
src/lib.rs
|
|
@ -6,11 +6,11 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids};
|
pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
use typst::ecow::EcoString;
|
use typst::ecow::EcoString;
|
||||||
use typst::syntax::Source;
|
use typst::syntax::Source;
|
||||||
use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstAsLibError, TypstEngine};
|
use typst_as_lib::{TypstAsLibError, TypstEngine, typst_kit_options::TypstKitFontOptions};
|
||||||
use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode};
|
use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode};
|
||||||
use log::info;
|
|
||||||
|
|
||||||
fn format_typst_compile_error(
|
fn format_typst_compile_error(
|
||||||
err: TypstAsLibError,
|
err: TypstAsLibError,
|
||||||
|
|
@ -31,13 +31,8 @@ fn format_typst_compile_error(
|
||||||
if byte >= index_byte_start && byte < index_end {
|
if byte >= index_byte_start && byte < index_end {
|
||||||
let rel = byte - index_byte_start;
|
let rel = byte - index_byte_start;
|
||||||
if let Some((line, col)) = index_only.lines().byte_to_line_column(rel) {
|
if let Some((line, col)) = index_only.lines().byte_to_line_column(rel) {
|
||||||
let _ = writeln!(
|
let _ =
|
||||||
&mut out,
|
writeln!(&mut out, " index.typ:{}:{}: {}", line + 1, col + 1, msg);
|
||||||
" index.typ:{}:{}: {}",
|
|
||||||
line + 1,
|
|
||||||
col + 1,
|
|
||||||
msg
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
let _ = writeln!(&mut out, " {msg}");
|
let _ = writeln!(&mut out, " {msg}");
|
||||||
}
|
}
|
||||||
|
|
@ -66,64 +61,30 @@ fn format_typst_compile_error(
|
||||||
std::io::Error::new(std::io::ErrorKind::Other, report)
|
std::io::Error::new(std::io::ErrorKind::Other, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Internal compilation kernel. `index_byte_start` is the byte offset within
|
||||||
pub fn compile_article(
|
/// `full_source` where the user's source begins (plugins/prepend precede it).
|
||||||
article_dir: &PathBuf,
|
fn compile_source(
|
||||||
prepend: &Option<PathBuf>,
|
full_source: &str,
|
||||||
plugins: &[impl AsRef<str>],
|
index_byte_start: usize,
|
||||||
|
base_dir: &PathBuf,
|
||||||
include_title: bool,
|
include_title: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
info!("compiling {} ...", article_dir.display());
|
let index_source = &full_source[index_byte_start..];
|
||||||
|
|
||||||
let template_file = article_dir.join("index.typ");
|
|
||||||
let output = article_dir.join("index.html");
|
|
||||||
let outline_file = article_dir.join("outline.html");
|
|
||||||
|
|
||||||
let plugin_block = concat_plugin_sources(plugins)?;
|
|
||||||
|
|
||||||
let user_prepend = if let Some(prepend_file) = prepend {
|
|
||||||
fs::read_to_string(&prepend_file).map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"could not read prepend file {}: {e}",
|
|
||||||
prepend_file.display()
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
fs::read_to_string(article_dir.join("prepend.typ")).unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let index_source = fs::read_to_string(&template_file).map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"could not read template {}: {e}",
|
|
||||||
template_file.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut template: EcoString = EcoString::new();
|
|
||||||
template.push_str(&plugin_block);
|
|
||||||
if !plugin_block.is_empty() && !user_prepend.is_empty() {
|
|
||||||
template.push('\n');
|
|
||||||
}
|
|
||||||
template.push_str(&user_prepend);
|
|
||||||
let index_byte_start = template.len();
|
|
||||||
template.push_str(&index_source);
|
|
||||||
|
|
||||||
let full_source_str = template.to_string();
|
|
||||||
|
|
||||||
let engine = TypstEngine::builder()
|
let engine = TypstEngine::builder()
|
||||||
.main_file(full_source_str.clone())
|
.main_file(full_source.to_string())
|
||||||
.search_fonts_with(
|
.search_fonts_with(
|
||||||
TypstKitFontOptions::default()
|
TypstKitFontOptions::default()
|
||||||
.include_system_fonts(false)
|
.include_system_fonts(false)
|
||||||
.include_embedded_fonts(true),
|
.include_embedded_fonts(true),
|
||||||
)
|
)
|
||||||
.with_file_system_resolver(article_dir)
|
.with_file_system_resolver(base_dir)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mut doc: HtmlDocument = engine
|
let mut doc: HtmlDocument = engine
|
||||||
.compile()
|
.compile()
|
||||||
.output
|
.output
|
||||||
.map_err(|e| format_typst_compile_error(e, &full_source_str, index_byte_start, &index_source))?;
|
.map_err(|e| format_typst_compile_error(e, full_source, index_byte_start, index_source))?;
|
||||||
|
|
||||||
let mut outline = EcoString::new();
|
let mut outline = EcoString::new();
|
||||||
let mut curr_level = 1u32;
|
let mut curr_level = 1u32;
|
||||||
|
|
@ -139,19 +100,12 @@ pub fn compile_article(
|
||||||
&mut title_h2_pending,
|
&mut title_h2_pending,
|
||||||
&mut first_outline_heading,
|
&mut first_outline_heading,
|
||||||
);
|
);
|
||||||
// `ul_depth` counts open `<ul>` tags; it can diverge from `curr_level - 1` when the first
|
|
||||||
// outline heading uses lazy depth (skip title). Always drain by `ul_depth`, not `curr_level`.
|
|
||||||
while ul_depth > 0 {
|
while ul_depth > 0 {
|
||||||
ul_depth -= 1;
|
ul_depth -= 1;
|
||||||
outline.push_str(" ".repeat(ul_depth as usize).as_str());
|
outline.push_str(" ".repeat(ul_depth as usize).as_str());
|
||||||
outline.push_str("</ul>\n");
|
outline.push_str("</ul>\n");
|
||||||
}
|
}
|
||||||
fs::write(&outline_file, outline.as_bytes()).map_err(|e| {
|
let outline_str = outline.to_string();
|
||||||
format!(
|
|
||||||
"could not write outline {}: {e}",
|
|
||||||
outline_file.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut body: Option<HtmlElement> = None;
|
let mut body: Option<HtmlElement> = None;
|
||||||
for child in &doc.root.children {
|
for child in &doc.root.children {
|
||||||
|
|
@ -165,16 +119,11 @@ pub fn compile_article(
|
||||||
let body = body.ok_or("compiled HTML has no <body> element")?;
|
let body = body.ok_or("compiled HTML has no <body> element")?;
|
||||||
doc.root = body;
|
doc.root = body;
|
||||||
|
|
||||||
let mut html: String = typst_html::html(&doc).map_err(|e| format!("html generation failed: {e:?}"))?;
|
let mut html: String =
|
||||||
|
typst_html::html(&doc).map_err(|e| format!("html generation failed: {e:?}"))?;
|
||||||
let lines = html
|
let lines = html
|
||||||
.lines()
|
.lines()
|
||||||
.map(|line| {
|
.map(|line| if line.len() >= 2 { &line[2..] } else { "" })
|
||||||
if line.len() >= 2 {
|
|
||||||
&line[2..]
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<&str>>();
|
.collect::<Vec<&str>>();
|
||||||
|
|
||||||
if lines.len() > 2 {
|
if lines.len() > 2 {
|
||||||
|
|
@ -183,12 +132,88 @@ pub fn compile_article(
|
||||||
html = String::new();
|
html = String::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write(&output, html).map_err(|e| {
|
Ok((html, outline_str))
|
||||||
format!(
|
}
|
||||||
"could not write output {}: {e}",
|
|
||||||
output.display()
|
/// Render Typst source directly from a string. Plugins are prepended
|
||||||
)
|
/// automatically. `base_dir` is used by the Typst engine for file-system
|
||||||
})?;
|
/// resolution (images, includes, etc.). Returns `(index_html, outline_html)`.
|
||||||
|
pub fn render_direct(
|
||||||
|
source: &str,
|
||||||
|
plugins: &[impl AsRef<str>],
|
||||||
|
include_title: bool,
|
||||||
|
base_dir: &PathBuf,
|
||||||
|
) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let plugin_block = concat_plugin_sources(plugins)?;
|
||||||
|
|
||||||
|
let mut full_source = EcoString::new();
|
||||||
|
full_source.push_str(&plugin_block);
|
||||||
|
if !plugin_block.is_empty() {
|
||||||
|
full_source.push('\n');
|
||||||
|
}
|
||||||
|
let index_byte_start = full_source.len();
|
||||||
|
full_source.push_str(source);
|
||||||
|
|
||||||
|
compile_source(
|
||||||
|
full_source.as_str(),
|
||||||
|
index_byte_start,
|
||||||
|
base_dir,
|
||||||
|
include_title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render Typst source read from `article_dir/index.typ`. Supports an
|
||||||
|
/// optional `prepend` file and writes the output to `index.html` and
|
||||||
|
/// `outline.html` in the same directory.
|
||||||
|
pub fn compile_article(
|
||||||
|
article_dir: &PathBuf,
|
||||||
|
prepend: &Option<PathBuf>,
|
||||||
|
plugins: &[impl AsRef<str>],
|
||||||
|
include_title: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
info!("compiling {} ...", article_dir.display());
|
||||||
|
|
||||||
|
let output = article_dir.join("index.html");
|
||||||
|
let outline_file = article_dir.join("outline.html");
|
||||||
|
|
||||||
|
let plugin_block = concat_plugin_sources(plugins)?;
|
||||||
|
|
||||||
|
let user_prepend = if let Some(prepend_file) = prepend {
|
||||||
|
fs::read_to_string(prepend_file).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"could not read prepend file {}: {e}",
|
||||||
|
prepend_file.display()
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
fs::read_to_string(article_dir.join("prepend.typ")).unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let template_file = article_dir.join("index.typ");
|
||||||
|
let index_source = fs::read_to_string(&template_file)
|
||||||
|
.map_err(|e| format!("could not read template {}: {e}", template_file.display()))?;
|
||||||
|
|
||||||
|
let mut full_source: EcoString = EcoString::new();
|
||||||
|
full_source.push_str(&plugin_block);
|
||||||
|
if !plugin_block.is_empty() && !user_prepend.is_empty() {
|
||||||
|
full_source.push('\n');
|
||||||
|
}
|
||||||
|
full_source.push_str(&user_prepend);
|
||||||
|
let index_byte_start = full_source.len();
|
||||||
|
full_source.push_str(&index_source);
|
||||||
|
|
||||||
|
let (index_html, outline_html) = compile_source(
|
||||||
|
full_source.as_str(),
|
||||||
|
index_byte_start,
|
||||||
|
article_dir,
|
||||||
|
include_title,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
fs::write(&output, index_html)
|
||||||
|
.map_err(|e| format!("could not write output {}: {e}", output.display()))?;
|
||||||
|
|
||||||
|
fs::write(&outline_file, outline_html.as_bytes())
|
||||||
|
.map_err(|e| format!("could not write outline {}: {e}", outline_file.display()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -283,13 +308,7 @@ fn parse_outline(
|
||||||
*curr_level = level;
|
*curr_level = level;
|
||||||
|
|
||||||
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
||||||
outline.push_str(
|
outline.push_str(format!("<li><a href=\"#{}\">{}</a></li>\n", slug, header_text).as_str());
|
||||||
format!(
|
|
||||||
"<li><a href=\"#{}\">{}</a></li>\n",
|
|
||||||
slug, header_text
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
elem.attrs.push(HtmlAttr::intern("id").unwrap(), slug);
|
elem.attrs.push(HtmlAttr::intern("id").unwrap(), slug);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use log::{error, info};
|
||||||
use typssg::{compile_all, compile_article};
|
use typssg::{compile_all, compile_article};
|
||||||
use log::{info, error};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use include_dir::{include_dir, Dir};
|
use include_dir::{Dir, include_dir};
|
||||||
|
|
||||||
static PREPENDS_DIR: Dir<'static> = include_dir!("prepends");
|
static PREPENDS_DIR: Dir<'static> = include_dir!("prepends");
|
||||||
|
|
||||||
|
|
@ -40,17 +40,14 @@ pub fn embedded_prepend_source(id: &str) -> Result<String, String> {
|
||||||
if id.is_empty() {
|
if id.is_empty() {
|
||||||
return Err("empty plugin id".into());
|
return Err("empty plugin id".into());
|
||||||
}
|
}
|
||||||
prepends_table()
|
prepends_table().get(&id).cloned().ok_or_else(|| {
|
||||||
.get(&id)
|
let known = list_embedded_plugin_ids().join(", ");
|
||||||
.cloned()
|
if known.is_empty() {
|
||||||
.ok_or_else(|| {
|
format!("unknown plugin '{id}' (no embedded prepends in this build)")
|
||||||
let known = list_embedded_plugin_ids().join(", ");
|
} else {
|
||||||
if known.is_empty() {
|
format!("unknown plugin '{id}' (embedded: {known})")
|
||||||
format!("unknown plugin '{id}' (no embedded prepends in this build)")
|
}
|
||||||
} else {
|
})
|
||||||
format!("unknown plugin '{id}' (embedded: {known})")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn concat_plugin_sources(plugin_ids: &[impl AsRef<str>]) -> Result<String, String> {
|
pub fn concat_plugin_sources(plugin_ids: &[impl AsRef<str>]) -> Result<String, String> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue