summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/build.rs451
-rw-r--r--src/cli.rs21
-rw-r--r--src/config.rs59
-rw-r--r--src/filters.rs115
-rw-r--r--src/main.rs93
-rw-r--r--src/utils.rs116
6 files changed, 855 insertions, 0 deletions
diff --git a/src/build.rs b/src/build.rs
new file mode 100644
index 0000000..0b0c646
--- /dev/null
+++ b/src/build.rs
@@ -0,0 +1,451 @@
1use std::path::{Path, PathBuf};
2
3use eyre::{eyre, ContextCompat, Result, WrapErr};
4use log::{debug, error, log_enabled, trace, warn};
5use pandoc_ast::MutVisitor;
6
7use crate::{
8 filters,
9 utils::{AutoIdentifier, PandocMeta, PandocOutputExt},
10};
11
12pub fn do_build(config: &crate::config::Config) -> Result<()> {
13 let summary = Summary::try_from_file(&config.book.summary)?;
14 let source_root = Path::new(&config.book.summary)
15 .parent()
16 .expect("Summary has no parent");
17 let files = summary.collect_source_files(source_root)?;
18
19 let build_dir = Path::new(&config.build.build_dir);
20 trace!("Creating build directory: '{}'", build_dir.display());
21 std::fs::create_dir_all(build_dir).wrap_err_with(|| {
22 format!(
23 "Could not create build directory: '{}'",
24 build_dir.display()
25 )
26 })?;
27
28 // Pre-create files so that we know which links to relativize
29 for SourceFile { path, .. } in &files {
30 let output_file = build_dir.join(path.with_extension("html"));
31
32 let product_dir = build_dir.join(path.parent().expect("Source file has no parent"));
33 trace!("Creating product directory: '{}'", product_dir.display());
34 std::fs::create_dir_all(&product_dir).wrap_err_with(|| {
35 format!(
36 "Could not create build output directory: '{}'",
37 product_dir.display()
38 )
39 })?;
40
41 std::fs::OpenOptions::new()
42 .write(true)
43 .create(true)
44 .open(&output_file)
45 .wrap_err_with(|| {
46 format!("Failed to create output file: '{}'", output_file.display())
47 })?;
48 }
49
50 for SourceFile { path, source } in &files {
51 let mut pandoc_command = pandoc::new();
52
53 let output_file = build_dir.join(path.with_extension("html"));
54
55 debug!("Generating file: '{}'", output_file.display());
56
57 // To be captured in the filter
58 let config_clone = config.clone();
59 let source_dir = path
60 .parent()
61 .expect("Source file has no parent")
62 .to_path_buf();
63 let build_dir_clone = build_dir.to_path_buf();
64 let summary_clone = summary.source.clone();
65
66 pandoc_command
67 .set_input(pandoc::InputKind::Pipe(source.to_json()))
68 .set_input_format(pandoc::InputFormat::Json, vec![])
69 .set_output(pandoc::OutputKind::File(output_file))
70 .set_output_format(pandoc::OutputFormat::Html5, vec![])
71 .add_options(&[pandoc::PandocOption::SelfContained])
72 .add_filter(move |source| {
73 let level = source_dir
74 .components()
75 .skip_while(|c| matches!(c, std::path::Component::CurDir))
76 .count();
77
78 let mut insert_summary_filter = filters::InsertSummary {
79 level,
80 summary: &summary_clone,
81 };
82
83 let mut relativize_urls_filter = filters::RelativizeUrls {
84 config: &config_clone,
85 // TODO: other output formats
86 extension: "html",
87 build_dir: &build_dir_clone,
88 source_dir: &source_dir,
89 };
90
91 let mut source = pandoc_ast::Pandoc::from_json(&source);
92 insert_summary_filter.walk_pandoc(&mut source);
93 relativize_urls_filter.walk_pandoc(&mut source);
94 source.to_json()
95 });
96
97 if log_enabled!(log::Level::Trace) {
98 pandoc_command.set_show_cmdline(true);
99 }
100
101 pandoc_command
102 .execute()
103 .wrap_err_with(|| format!("Failed to generate output of: '{}'", path.display()))?;
104 }
105
106 Ok(())
107}
108
109// TODO: move that into generated.rs
110fn generate_source(
111 title: Vec<pandoc_ast::Inline>,
112 children: Vec<(PandocMeta, PathBuf)>,
113 level: usize,
114) -> Result<pandoc_ast::Pandoc> {
115 // TODO: make that text configurable
116 let mut content = vec![pandoc_ast::Block::Para(vec![pandoc_ast::Inline::Str(
117 "Here are the articles in this section:".to_string(),
118 )])];
119
120 for (mut child, file) in children {
121 let title = match child.remove("title") {
122 None => {
123 warn!("Missing title for file: '{}'", file.display());
124 vec![pandoc_ast::Inline::Str("Untitled page".to_string())]
125 }
126 Some(pandoc_ast::MetaValue::MetaInlines(inlines)) => inlines,
127 Some(pandoc_ast::MetaValue::MetaString(s)) => {
128 vec![pandoc_ast::Inline::Str(s)]
129 }
130 // TODO: check that other values are actually invalid
131 _ => {
132 error!("Invalid value for title");
133 vec![pandoc_ast::Inline::Str("Untitled page".to_string())]
134 }
135 };
136
137 let link_target = std::iter::repeat(std::path::Component::ParentDir)
138 .take(level)
139 .collect::<PathBuf>()
140 .join(file);
141
142 content.push(pandoc_ast::Block::Para(vec![pandoc_ast::Inline::Link(
143 // TODO: attribute to recognize big links?
144 (String::new(), vec![], vec![]),
145 title,
146 (
147 link_target
148 .to_str()
149 .expect("Filename contains invalid unicode")
150 .to_string(),
151 String::new(),
152 ),
153 )]));
154 }
155
156 let mut meta = PandocMeta::new();
157 meta.insert(
158 "title".to_string(),
159 pandoc_ast::MetaValue::MetaInlines(title),
160 );
161
162 Ok(pandoc_ast::Pandoc {
163 meta,
164 blocks: content,
165 pandoc_api_version: vec![1, 22],
166 })
167}
168
169fn list_content(block: &mut pandoc_ast::Block) -> Result<&mut Vec<Vec<pandoc_ast::Block>>> {
170 match block {
171 pandoc_ast::Block::OrderedList(_, list) => Ok(list),
172 pandoc_ast::Block::BulletList(list) => Ok(list),
173 _ => Err(eyre!("Expected list in summary, found something else")),
174 }
175}
176
177fn try_into_node_vec(vec: &mut Vec<Vec<pandoc_ast::Block>>) -> Result<Vec<Node>> {
178 vec.iter_mut().map(Node::try_from_vec_block).collect()
179}
180
181// TODO: support separators like these:
182// ---------
183
184#[derive(Debug)]
185pub struct Summary {
186 source: pandoc_ast::Pandoc,
187 nodes: Vec<Node>,
188}
189
190#[derive(Debug)]
191struct SourceFile {
192 path: PathBuf,
193 source: pandoc_ast::Pandoc,
194}
195
196// TODO: move that into summary.rs
197impl Summary {
198 fn try_from_file(file: &str) -> Result<Self> {
199 debug!("Parsing summary");
200 let mut pandoc_command = pandoc::new();
201 pandoc_command
202 .add_input(file)
203 .set_output_format(pandoc::OutputFormat::Json, vec![])
204 .set_output(pandoc::OutputKind::Pipe);
205
206 trace!("Launching pandoc command");
207
208 if log_enabled!(log::Level::Trace) {
209 pandoc_command.set_show_cmdline(true);
210 }
211
212 let output = pandoc_command
213 .execute()
214 .wrap_err("Could not execute pandoc")?
215 .buffer();
216
217 let document = pandoc_ast::Pandoc::from_json(&output);
218
219 let summary: Self = document.try_into()?;
220 if summary.has_files_missing(
221 Path::new(file)
222 .parent()
223 .expect("Summary file has no parent"),
224 ) {
225 return Err(eyre!("Files from the summary are missing, aborting"));
226 }
227
228 Ok(summary)
229 }
230
231 fn has_files_missing(&self, root: &Path) -> bool {
232 // Do not use `.any()` to prevent short-circuiting, we want to report all missing files
233 self.nodes.iter().fold(false, |acc, node| {
234 let missing = node.has_files_missing(root);
235 acc || missing
236 })
237 }
238
239 /// Get a list of source files.
240 ///
241 /// If a file is a generated file, generate it and store it in memory.
242 fn collect_source_files(&self, root: &Path) -> Result<Vec<SourceFile>> {
243 let mut result = Vec::new();
244
245 for node in &self.nodes {
246 node.collect_source_files(&mut result, root, Path::new("."), 0)?;
247 }
248
249 Ok(result)
250 }
251}
252
253impl TryFrom<pandoc_ast::Pandoc> for Summary {
254 type Error = eyre::Error;
255
256 fn try_from(mut document: pandoc_ast::Pandoc) -> Result<Self, Self::Error> {
257 if document.blocks.len() != 1 {
258 return Err(eyre!("Summary does not contain a single list"));
259 }
260
261 let root = &mut document.blocks[0];
262
263 let list = list_content(root)?;
264
265 let nodes = list
266 .iter_mut()
267 .map(Node::try_from_vec_block)
268 .collect::<Result<_>>()?;
269
270 Ok(Summary {
271 source: document,
272 nodes,
273 })
274 }
275}
276
277#[derive(Debug)]
278pub enum Node {
279 Provided {
280 file: String,
281 children: Vec<Node>,
282 },
283 Generated {
284 file: String,
285 title: Vec<pandoc_ast::Inline>,
286 children: Vec<Node>,
287 },
288}
289
290impl Node {
291 fn children(&self) -> &[Node] {
292 match self {
293 Node::Provided { children, .. } => children,
294 Node::Generated { children, .. } => children,
295 }
296 }
297
298 fn has_files_missing(&self, root: &Path) -> bool {
299 if let Node::Provided { file, .. } = self {
300 if !root.join(file).exists() {
301 error!("File '{}' specified in summary does not exists", file);
302 return true;
303 }
304 }
305
306 // Do not use `.any()` to prevent short-circuiting, we want to report all missing files
307 self.children().iter().fold(false, |acc, node| {
308 let missing = node.has_files_missing(root);
309 acc || missing
310 })
311 }
312
313 fn collect_source_files(
314 &self,
315 result: &mut Vec<SourceFile>,
316 root: &Path,
317 parent: &Path,
318 level: usize,
319 ) -> Result<()> {
320 let new_parent;
321 let children_;
322 let path;
323 let source: Box<dyn FnOnce(_) -> _>;
324
325 match self {
326 Node::Provided { file, children } => {
327 trace!("Parsing file: '{}'", file);
328
329 // TODO: some filters here? not all filters, since we may want to filter generated
330 // files too
331 let mut pandoc_command = pandoc::new();
332 pandoc_command
333 .add_input(&root.join(file))
334 .set_output(pandoc::OutputKind::Pipe)
335 .set_output_format(pandoc::OutputFormat::Json, vec![]);
336
337 if log_enabled!(log::Level::Trace) {
338 pandoc_command.set_show_cmdline(true);
339 }
340
341 let raw_source = pandoc_command
342 .execute()
343 .wrap_err_with(|| format!("Failed to parse '{}'", file))?
344 .buffer();
345 source = Box::new(move |_| Ok(pandoc_ast::Pandoc::from_json(&raw_source)));
346
347 let file = Path::new(&file);
348 let stem = file.file_stem().expect("No file name");
349 let id =
350 AutoIdentifier::from(stem.to_str().wrap_err("Invalid unicode in file name")?);
351
352 path = file.into();
353 new_parent = file.parent().expect("Source file has no parent").join(&*id);
354 children_ = children;
355 }
356
357 Self::Generated {
358 file,
359 title,
360 children,
361 } => {
362 trace!("Found file to generate: '{}'", file);
363
364 path = file.into();
365
366 source = Box::new(move |direct_children| {
367 generate_source(title.clone(), direct_children, level)
368 });
369 new_parent = Path::new(file).with_extension("");
370 children_ = children;
371 }
372 };
373
374 let mut direct_children = Vec::with_capacity(children_.len());
375
376 for child in children_ {
377 child.collect_source_files(result, root, &new_parent, level + 1)?;
378 let direct_child = result.last().unwrap();
379 direct_children.push((direct_child.source.meta.clone(), direct_child.path.clone()));
380 }
381
382 result.push(SourceFile {
383 path,
384 source: source(direct_children)?,
385 });
386
387 Ok(())
388 }
389
390 // Wil also modify the block to linkify generated pages
391 fn try_from_vec_block(value: &mut Vec<pandoc_ast::Block>) -> Result<Self> {
392 if value.len() != 1 && value.len() != 2 {
393 // TODO: better error message?
394 return Err(eyre!("Summary does not contain a single list"));
395 }
396
397 let mut value = value.iter_mut();
398
399 let item = match value.next().unwrap() {
400 pandoc_ast::Block::Plain(inlines) => inlines,
401 pandoc_ast::Block::Para(inlines) => inlines,
402 _ => return Err(eyre!("List item is not a link or plain text")),
403 };
404
405 if item.is_empty() {
406 return Err(eyre!("Summary list items cannot be empty"));
407 }
408
409 let children = if let Some(children) = value.next() {
410 try_into_node_vec(list_content(children)?)?
411 } else {
412 vec![]
413 };
414
415 match &item[0] {
416 pandoc_ast::Inline::Link(_, _, target) => {
417 if item.len() != 1 {
418 return Err(eyre!("Summary list item not a single link or plain text"));
419 }
420
421 let file = target.0.clone();
422
423 Ok(Node::Provided { file, children })
424 }
425 _ => {
426 let title = item.clone();
427
428 let id = AutoIdentifier::from(title.as_slice());
429
430 // TODO: missing parent
431
432 // Move generate page into this pass
433 //let mut file = parent.join(&*id);
434 //file.set_extension("md");
435
436 // TODO: Attribute to style them differently
437 *item = vec![pandoc_ast::Inline::Link(
438 (String::new(), vec!["generated".to_string()], vec![]),
439 item.clone(),
440 (id.0.clone(), String::new()),
441 )];
442
443 Ok(Node::Generated {
444 file: id.0,
445 title,
446 children,
447 })
448 }
449 }
450 }
451}
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..30e771a
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,21 @@
1use clap::{AppSettings, Parser};
2
3// TODO: document
4
5#[derive(Debug, Parser)]
6#[clap(setting = AppSettings::InferSubcommands)]
7pub struct Cli {
8 #[clap(short, long, default_value = "pdbook.toml")]
9 pub config: String,
10 #[clap(short, long)]
11 pub quiet: bool,
12 #[clap(short, long, parse(from_occurrences))]
13 pub verbose: u8,
14 #[clap(subcommand)]
15 pub subcommand: SubCommand,
16}
17
18#[derive(Debug, Parser)]
19pub enum SubCommand {
20 Build,
21}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..53922b0
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,59 @@
1use log::debug;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Deserialize, Serialize)]
5pub struct Config {
6 #[serde(default)]
7 pub book: BookConfig,
8 #[serde(default)]
9 pub build: BuildConfig,
10}
11
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct BookConfig {
14 #[serde(default = "default_summary")]
15 pub summary: String,
16}
17
18impl Default for BookConfig {
19 fn default() -> Self {
20 Self {
21 summary: default_summary(),
22 }
23 }
24}
25
26fn default_summary() -> String {
27 "src/_summary.md".to_string()
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
31pub struct BuildConfig {
32 #[serde(default = "default_build_dir")]
33 pub build_dir: String,
34}
35
36impl Default for BuildConfig {
37 fn default() -> Self {
38 Self {
39 build_dir: default_build_dir(),
40 }
41 }
42}
43
44fn default_build_dir() -> String {
45 "pdbook".to_string()
46}
47
48impl Config {
49 pub fn new(config_file: &str) -> Result<Self, config::ConfigError> {
50 let mut s = config::Config::default();
51
52 debug!("Parsing config file: {}", config_file);
53 s.merge(config::File::with_name(config_file))?;
54 debug!("Parsing config from environment");
55 s.merge(config::Environment::with_prefix("PANDOC_DOCBOOK").separator("_"))?;
56
57 s.try_into()
58 }
59}
diff --git a/src/filters.rs b/src/filters.rs
new file mode 100644
index 0000000..1b06920
--- /dev/null
+++ b/src/filters.rs
@@ -0,0 +1,115 @@
1use std::{collections::HashMap, path::Path};
2
3use log::trace;
4use pandoc_ast::MutVisitor;
5
6// If a link points to `./a/b/c.ext`, and a file in the output directory `pdbook/./a/b/c.html`
7// exists, rewrite that link.
8pub struct RelativizeUrls<'a> {
9 pub config: &'a crate::config::Config,
10 pub extension: &'a str,
11 pub build_dir: &'a Path,
12 pub source_dir: &'a Path,
13}
14
15impl<'a> pandoc_ast::MutVisitor for RelativizeUrls<'a> {
16 fn walk_inline(&mut self, inline: &mut pandoc_ast::Inline) {
17 let link = match inline {
18 pandoc_ast::Inline::Link(_, _, target) => &mut target.0,
19 _ => return,
20 };
21
22 if link.starts_with('#') || link.contains("://") {
23 return;
24 }
25
26 let link_path = self.source_dir.join(&link);
27
28 if link_path.is_absolute() {
29 return;
30 }
31
32 let mut output_path = self.build_dir.join(&link_path);
33 if !output_path.set_extension(self.extension) {
34 return;
35 }
36
37 trace!("Checking output_path: {:?}", output_path);
38
39 // TODO: warn when referencing a "markdown or other" file not specified in the summary
40 if output_path.exists() {
41 // TODO: relativize from URL root
42
43 trace!("Relativizing link '{}'", link_path.display());
44
45 *link = Path::new(link)
46 .with_extension(&self.extension)
47 .to_str()
48 .expect("Path constructed from UTF-8 valid strings in not UTF-8 valid")
49 .to_string();
50
51 trace!("-> into '{}'", link);
52 }
53 }
54}
55
56// Applied just to the summary
57pub struct RelativizeSummary {
58 level: usize,
59}
60
61impl pandoc_ast::MutVisitor for RelativizeSummary {
62 fn walk_inline(&mut self, inline: &mut pandoc_ast::Inline) {
63 if self.level == 0 {
64 return;
65 }
66
67 let link = match inline {
68 pandoc_ast::Inline::Link(_, _, target) => &mut target.0,
69 _ => return,
70 };
71
72 // Assume link is to a managed file
73 for _ in 0..self.level {
74 link.insert_str(0, "../");
75 }
76 }
77}
78
79pub fn relativize_summary(summary: &pandoc_ast::Pandoc, level: usize) -> pandoc_ast::Pandoc {
80 use std::sync::RwLock;
81
82 lazy_static::lazy_static! {
83 static ref CACHE: RwLock<HashMap<usize, pandoc_ast::Pandoc>> = RwLock::new(HashMap::new());
84 }
85
86 CACHE
87 .write()
88 .expect("Relativized summary cache poison")
89 .entry(level)
90 .or_insert_with(|| {
91 let mut summary = summary.clone();
92 RelativizeSummary { level }.walk_pandoc(&mut summary);
93 summary
94 })
95 .clone()
96}
97
98pub struct InsertSummary<'a> {
99 pub summary: &'a pandoc_ast::Pandoc,
100 pub level: usize,
101}
102
103impl<'a> pandoc_ast::MutVisitor for InsertSummary<'a> {
104 fn walk_pandoc(&mut self, pandoc: &mut pandoc_ast::Pandoc) {
105 let summary = relativize_summary(self.summary, self.level);
106
107 pandoc.blocks.insert(
108 0,
109 pandoc_ast::Block::Div(
110 (String::new(), vec!["summary".to_string()], vec![]),
111 summary.blocks,
112 ),
113 );
114 }
115}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..b5de3bf
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,93 @@
1mod build;
2mod cli;
3mod config;
4mod utils;
5mod filters;
6
7use std::path::PathBuf;
8
9use cli::Cli;
10
11use clap::Parser;
12use eyre::{Result, WrapErr};
13use log::trace;
14
15fn init_log(quiet: bool, verbosity: u8) {
16 use log::LevelFilter;
17
18 let verbosity = match verbosity {
19 0 => LevelFilter::Info,
20 1 => LevelFilter::Debug,
21 _ => LevelFilter::Trace,
22 };
23
24 let env = env_logger::Env::default().default_filter_or(if quiet {
25 "off".to_string()
26 } else {
27 format!("pandoc_docbook={}", verbosity)
28 });
29
30 let mut builder = env_logger::Builder::from_env(env);
31
32 // Shamelessly taken from pretty_env_logger
33 builder.format(move |f, record| {
34 use std::io::Write;
35
36 let target = record.target();
37
38 let mut style = f.style();
39 let level = colored_level(&mut style, record.level());
40
41 let mut style = f.style();
42 let target = style.set_bold(true).value(target);
43
44 if verbosity >= LevelFilter::Debug {
45 writeln!(f, " {} {} > {}", level, target, record.args())
46 } else {
47 writeln!(f, " {} > {}", level, record.args())
48 }
49 });
50
51 builder.init();
52}
53
54fn colored_level<'a>(
55 style: &'a mut env_logger::fmt::Style,
56 level: log::Level,
57) -> env_logger::fmt::StyledValue<'a, &'static str> {
58 use env_logger::fmt::Color;
59 use log::Level;
60
61 match level {
62 Level::Trace => style.set_color(Color::Magenta).value("TRACE"),
63 Level::Debug => style.set_color(Color::Blue).value("DEBUG"),
64 Level::Info => style.set_color(Color::Green).value("INFO "),
65 Level::Warn => style.set_color(Color::Yellow).value("WARN "),
66 Level::Error => style.set_color(Color::Red).value("ERROR"),
67 }
68}
69
70fn main() -> Result<()> {
71 color_eyre::install()?;
72
73 let cli = Cli::parse();
74 init_log(cli.quiet, cli.verbose);
75
76 let config = config::Config::new(&cli.config)?;
77 trace!("Parsed configuration: {:?}", config);
78
79 std::env::set_current_dir(
80 PathBuf::from(cli.config)
81 .parent()
82 .expect("Configuration file has no parent"),
83 )
84 .wrap_err("Could not change current directory to the configuration file's directory")?;
85
86 match cli.subcommand {
87 cli::SubCommand::Build => {
88 build::do_build(&config)?
89 }
90 }
91
92 Ok(())
93}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..8928cfb
--- /dev/null
+++ b/src/utils.rs
@@ -0,0 +1,116 @@
1use std::path::PathBuf;
2
3pub fn pandoc_stringify(inlines: &[pandoc_ast::Inline]) -> String {
4 fn pandoc_stringify_(result: &mut String, inlines: &[pandoc_ast::Inline]) {
5 for inline in inlines {
6 match inline {
7 pandoc_ast::Inline::Str(s)
8 | pandoc_ast::Inline::Code(_, s)
9 | pandoc_ast::Inline::Math(_, s) => result.push_str(s),
10 pandoc_ast::Inline::Emph(inner)
11 | pandoc_ast::Inline::Underline(inner)
12 | pandoc_ast::Inline::Strong(inner)
13 | pandoc_ast::Inline::Strikeout(inner)
14 | pandoc_ast::Inline::Superscript(inner)
15 | pandoc_ast::Inline::Subscript(inner)
16 | pandoc_ast::Inline::SmallCaps(inner)
17 | pandoc_ast::Inline::Quoted(_, inner)
18 | pandoc_ast::Inline::Cite(_, inner)
19 | pandoc_ast::Inline::Link(_, inner, _)
20 | pandoc_ast::Inline::Image(_, inner, _)
21 | pandoc_ast::Inline::Span(_, inner) => pandoc_stringify_(result, inner),
22 pandoc_ast::Inline::Space => result.push(' '),
23 pandoc_ast::Inline::SoftBreak => todo!(),
24 pandoc_ast::Inline::LineBreak => todo!(),
25 pandoc_ast::Inline::RawInline(_, _) => todo!(),
26 pandoc_ast::Inline::Note(_) => todo!(),
27 }
28 }
29 }
30
31 let mut result = String::new();
32 pandoc_stringify_(&mut result, inlines);
33 result
34}
35
36/// Follows the algorithm specified in the Pandoc manual[1]
37///
38/// [1]: <https://pandoc.org/MANUAL.html#extension-auto_identifiers>
39#[derive(Debug)]
40pub struct AutoIdentifier(pub String);
41
42impl std::ops::Deref for AutoIdentifier {
43 type Target = String;
44
45 fn deref(&self) -> &Self::Target {
46 &self.0
47 }
48}
49
50impl From<AutoIdentifier> for String {
51 fn from(id: AutoIdentifier) -> Self {
52 id.0
53 }
54}
55
56impl From<&[pandoc_ast::Inline]> for AutoIdentifier {
57 fn from(inlines: &[pandoc_ast::Inline]) -> Self {
58 let text = pandoc_stringify(inlines);
59 AutoIdentifier::from(text.as_str())
60 }
61}
62
63impl From<&str> for AutoIdentifier {
64 fn from(text: &str) -> Self {
65 let id = text
66 .chars()
67 .skip_while(|ch| !ch.is_alphabetic())
68 .filter_map(|ch| {
69 if !ch.is_ascii_alphanumeric()
70 && !ch.is_whitespace()
71 && ch != '_'
72 && ch != '-'
73 && ch != '.'
74 {
75 return None;
76 }
77
78 if ch.is_whitespace() {
79 return Some('-');
80 }
81
82 Some(ch.to_ascii_lowercase())
83 })
84 .collect();
85
86 AutoIdentifier(id)
87 }
88}
89
90pub trait PandocOutputExt {
91 fn buffer(self) -> String;
92 fn file(self) -> PathBuf;
93}
94
95impl PandocOutputExt for pandoc::PandocOutput {
96 fn buffer(self) -> String {
97 match self {
98 pandoc::PandocOutput::ToBuffer(buffer) => buffer,
99 pandoc::PandocOutput::ToBufferRaw(_) => {
100 panic!("Expected text pandoc output, found binary format")
101 }
102 pandoc::PandocOutput::ToFile(_) => {
103 panic!("Expected buffered pandoc output, found file")
104 }
105 }
106 }
107
108 fn file(self) -> PathBuf {
109 match self {
110 pandoc::PandocOutput::ToFile(file) => file,
111 _ => panic!("Expected file pandoc output, found buffer"),
112 }
113 }
114}
115
116pub type PandocMeta = pandoc_ast::Map<String, pandoc_ast::MetaValue>;