summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock96
-rw-r--r--Cargo.toml1
-rw-r--r--res/style.css171
-rw-r--r--res/template.html72
-rw-r--r--src/build.rs411
-rw-r--r--src/filters.rs23
-rw-r--r--src/utils.rs18
7 files changed, 519 insertions, 273 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5a17968..81f5135 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -186,6 +186,17 @@ dependencies = [
186] 186]
187 187
188[[package]] 188[[package]]
189name = "getrandom"
190version = "0.2.3"
191source = "registry+https://github.com/rust-lang/crates.io-index"
192checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
193dependencies = [
194 "cfg-if",
195 "libc",
196 "wasi",
197]
198
199[[package]]
189name = "gimli" 200name = "gimli"
190version = "0.26.1" 201version = "0.26.1"
191source = "registry+https://github.com/rust-lang/crates.io-index" 202source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -390,6 +401,7 @@ dependencies = [
390 "pandoc", 401 "pandoc",
391 "pandoc_ast", 402 "pandoc_ast",
392 "serde 1.0.130", 403 "serde 1.0.130",
404 "tempfile",
393] 405]
394 406
395[[package]] 407[[package]]
@@ -409,6 +421,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
409checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" 421checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
410 422
411[[package]] 423[[package]]
424name = "ppv-lite86"
425version = "0.2.15"
426source = "registry+https://github.com/rust-lang/crates.io-index"
427checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
428
429[[package]]
412name = "proc-macro-error" 430name = "proc-macro-error"
413version = "1.0.4" 431version = "1.0.4"
414source = "registry+https://github.com/rust-lang/crates.io-index" 432source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -451,6 +469,55 @@ dependencies = [
451] 469]
452 470
453[[package]] 471[[package]]
472name = "rand"
473version = "0.8.4"
474source = "registry+https://github.com/rust-lang/crates.io-index"
475checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
476dependencies = [
477 "libc",
478 "rand_chacha",
479 "rand_core",
480 "rand_hc",
481]
482
483[[package]]
484name = "rand_chacha"
485version = "0.3.1"
486source = "registry+https://github.com/rust-lang/crates.io-index"
487checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
488dependencies = [
489 "ppv-lite86",
490 "rand_core",
491]
492
493[[package]]
494name = "rand_core"
495version = "0.6.3"
496source = "registry+https://github.com/rust-lang/crates.io-index"
497checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
498dependencies = [
499 "getrandom",
500]
501
502[[package]]
503name = "rand_hc"
504version = "0.3.1"
505source = "registry+https://github.com/rust-lang/crates.io-index"
506checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
507dependencies = [
508 "rand_core",
509]
510
511[[package]]
512name = "redox_syscall"
513version = "0.2.10"
514source = "registry+https://github.com/rust-lang/crates.io-index"
515checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
516dependencies = [
517 "bitflags",
518]
519
520[[package]]
454name = "regex" 521name = "regex"
455version = "1.5.4" 522version = "1.5.4"
456source = "registry+https://github.com/rust-lang/crates.io-index" 523source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -468,6 +535,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
468checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 535checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
469 536
470[[package]] 537[[package]]
538name = "remove_dir_all"
539version = "0.5.3"
540source = "registry+https://github.com/rust-lang/crates.io-index"
541checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
542dependencies = [
543 "winapi",
544]
545
546[[package]]
471name = "rust-ini" 547name = "rust-ini"
472version = "0.13.0" 548version = "0.13.0"
473source = "registry+https://github.com/rust-lang/crates.io-index" 549source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -567,6 +643,20 @@ dependencies = [
567] 643]
568 644
569[[package]] 645[[package]]
646name = "tempfile"
647version = "3.2.0"
648source = "registry+https://github.com/rust-lang/crates.io-index"
649checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
650dependencies = [
651 "cfg-if",
652 "libc",
653 "rand",
654 "redox_syscall",
655 "remove_dir_all",
656 "winapi",
657]
658
659[[package]]
570name = "termcolor" 660name = "termcolor"
571version = "1.1.2" 661version = "1.1.2"
572source = "registry+https://github.com/rust-lang/crates.io-index" 662source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -689,6 +779,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
689checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 779checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
690 780
691[[package]] 781[[package]]
782name = "wasi"
783version = "0.10.2+wasi-snapshot-preview1"
784source = "registry+https://github.com/rust-lang/crates.io-index"
785checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
786
787[[package]]
692name = "winapi" 788name = "winapi"
693version = "0.3.9" 789version = "0.3.9"
694source = "registry+https://github.com/rust-lang/crates.io-index" 790source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 365b07b..e8783c8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,3 +20,4 @@ pandoc = "0.8"
20# TODO: change this when new version is released 20# TODO: change this when new version is released
21pandoc_ast = { git = "https://github.com/oli-obk/pandoc-ast", rev = "d73a7d0a065f568d60bcee6cff2340f75065273e" } 21pandoc_ast = { git = "https://github.com/oli-obk/pandoc-ast", rev = "d73a7d0a065f568d60bcee6cff2340f75065273e" }
22serde = { version = "1", features = [ "derive" ] } 22serde = { version = "1", features = [ "derive" ] }
23tempfile = "3"
diff --git a/res/style.css b/res/style.css
new file mode 100644
index 0000000..54ee84e
--- /dev/null
+++ b/res/style.css
@@ -0,0 +1,171 @@
1/* from here */
2
3.summary {
4 position: fixed;
5 top: 0;
6 bottom: 0;
7 left: 0;
8 border-right: 1px solid #1a1a1a;
9 /* TODO: ugly */
10 background-color: hsl(0, 0%, 80%);
11 transition: transform .2s;
12}
13
14.summary.hidden {
15 transform: translateX(-100%);
16}
17
18.summary::before {
19 content: "x";
20 position: absolute;
21 right: 1em;
22 top: 0.5em;
23}
24
25.summary > ul {
26 padding-right: 1em;
27}
28
29.summary .generated {
30 color: #5a5a5a;
31}
32
33/* from pandoc */
34html {
35 line-height: 1.5;
36 font-family: Georgia, serif;
37 font-size: 20px;
38 color: #1a1a1a;
39 background-color: #fdfdfd;
40}
41body {
42 margin: 0 auto;
43 max-width: 36em;
44 padding-left: 50px;
45 padding-right: 50px;
46 padding-top: 50px;
47 padding-bottom: 50px;
48 hyphens: auto;
49 word-wrap: break-word;
50 text-rendering: optimizeLegibility;
51 font-kerning: normal;
52}
53@media (max-width: 600px) {
54 body {
55 font-size: 0.9em;
56 padding: 1em;
57 }
58}
59@media print {
60 body {
61 background-color: transparent;
62 color: black;
63 font-size: 12pt;
64 }
65 p, h2, h3 {
66 orphans: 3;
67 widows: 3;
68 }
69 h2, h3, h4 {
70 page-break-after: avoid;
71 }
72}
73p {
74 margin: 1em 0;
75}
76a {
77 color: #1a1a1a;
78}
79a:visited {
80 color: #1a1a1a;
81}
82img {
83 max-width: 100%;
84}
85h1, h2, h3, h4, h5, h6 {
86 margin-top: 1.4em;
87}
88h5, h6 {
89 font-size: 1em;
90 font-style: italic;
91}
92h6 {
93 font-weight: normal;
94}
95ol, ul {
96 padding-left: 1.7em;
97 margin-top: 1em;
98}
99li > ol, li > ul {
100 margin-top: 0;
101}
102blockquote {
103 margin: 1em 0 1em 1.7em;
104 padding-left: 1em;
105 border-left: 2px solid #e6e6e6;
106 color: #606060;
107}
108code {
109 font-family: Menlo, Monaco, 'Lucida Console', Consolas, monospace;
110 font-size: 85%;
111 margin: 0;
112}
113pre {
114 margin: 1em 0;
115 overflow: auto;
116}
117pre code {
118 padding: 0;
119 overflow: visible;
120}
121.sourceCode {
122 background-color: transparent;
123 overflow: visible;
124}
125hr {
126 background-color: #1a1a1a;
127 border: none;
128 height: 1px;
129 margin: 1em 0;
130}
131table {
132 margin: 1em 0;
133 border-collapse: collapse;
134 width: 100%;
135 overflow-x: auto;
136 display: block;
137 font-variant-numeric: lining-nums tabular-nums;
138}
139table caption {
140 margin-bottom: 0.75em;
141}
142tbody {
143 margin-top: 0.5em;
144 border-top: 1px solid #1a1a1a;
145 border-bottom: 1px solid #1a1a1a;
146}
147th {
148 border-top: 1px solid #1a1a1a;
149 padding: 0.25em 0.5em 0.25em 0.5em;
150}
151td {
152 padding: 0.125em 0.5em 0.25em 0.5em;
153}
154header {
155 margin-bottom: 4em;
156 text-align: center;
157}
158#TOC li {
159 list-style: none;
160}
161#TOC a:not(:hover) {
162 text-decoration: none;
163}
164code{white-space: pre-wrap;}
165span.smallcaps{font-variant: small-caps;}
166span.underline{text-decoration: underline;}
167div.column{display: inline-block; vertical-align: top; width: 50%;}
168div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
169ul.task-list{list-style: none;}
170q { quotes: "“" "”" "‘" "’"; }
171.display.math{display: block; text-align: center; margin: 0.5rem auto;}
diff --git a/res/template.html b/res/template.html
new file mode 100644
index 0000000..bacb376
--- /dev/null
+++ b/res/template.html
@@ -0,0 +1,72 @@
1<!DOCTYPE html>
2<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
3<head>
4 <meta charset="utf-8" />
5 <meta name="generator" content="pandoc" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
7$for(author-meta)$
8 <meta name="author" content="$author-meta$" />
9$endfor$
10$if(date-meta)$
11 <meta name="dcterms.date" content="$date-meta$" />
12$endif$
13$if(keywords)$
14 <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" />
15$endif$
16$if(description-meta)$
17 <meta name="description" content="$description-meta$" />
18$endif$
19 <title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
20 <style>
21 $styles.html()$
22 </style>
23$for(css)$
24 <link rel="stylesheet" href="$css$" />
25$endfor$
26$if(math)$
27 $math$
28$endif$
29 <!--[if lt IE 9]>
30 <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
31 <![endif]-->
32$for(header-includes)$
33 $header-includes$
34$endfor$
35</head>
36<body>
37<nav class="summary">
38$summary$
39</nav>
40<main>
41$for(include-before)$
42$include-before$
43$endfor$
44$if(title)$
45<header id="title-block-header">
46<h1 class="title">$title$</h1>
47$if(subtitle)$
48<p class="subtitle">$subtitle$</p>
49$endif$
50$for(author)$
51<p class="author">$author$</p>
52$endfor$
53$if(date)$
54<p class="date">$date$</p>
55$endif$
56</header>
57$endif$
58$if(toc)$
59<nav id="$idprefix$TOC" role="doc-toc">
60$if(toc-title)$
61<h2 id="$idprefix$toc-title">$toc-title$</h2>
62$endif$
63$table-of-contents$
64</nav>
65$endif$
66$body$
67$for(include-after)$
68$include-after$
69$endfor$
70</main>
71</body>
72</html>
diff --git a/src/build.rs b/src/build.rs
index 0b0c646..1c12476 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1,20 +1,29 @@
1use std::path::{Path, PathBuf}; 1use std::path::{Path, PathBuf};
2 2
3use eyre::{eyre, ContextCompat, Result, WrapErr}; 3use eyre::{eyre, Result, WrapErr};
4use log::{debug, error, log_enabled, trace, warn}; 4use log::{debug, error, log_enabled, trace, warn};
5use pandoc_ast::MutVisitor; 5use pandoc_ast::MutVisitor;
6 6
7use crate::{ 7use crate::{
8 filters, 8 filters::{self, relativize_summary},
9 utils::{AutoIdentifier, PandocMeta, PandocOutputExt}, 9 utils::{AutoIdentifier, PandocMeta, PandocOutputExt, PathExt},
10}; 10};
11 11
12const CSS: &str = include_str!("../res/style.css");
13const HTML_TEMPLATE: &str = include_str!("../res/template.html");
14
12pub fn do_build(config: &crate::config::Config) -> Result<()> { 15pub fn do_build(config: &crate::config::Config) -> Result<()> {
13 let summary = Summary::try_from_file(&config.book.summary)?; 16 let tmpdir = tempfile::tempdir().wrap_err("Could not create temporary directory")?;
17 debug!("Created temporary directory at: '{}'", tmpdir.path().display());
18 let template_path = tmpdir.path().join("template.html");
19 trace!("Writing HTML template to: '{}'", template_path.display());
20 std::fs::write(&template_path, HTML_TEMPLATE)
21 .wrap_err("Could not save HTML template in temporary directory")?;
22
14 let source_root = Path::new(&config.book.summary) 23 let source_root = Path::new(&config.book.summary)
15 .parent() 24 .parent()
16 .expect("Summary has no parent"); 25 .expect("Summary has no parent");
17 let files = summary.collect_source_files(source_root)?; 26 let (summary, files) = process_summary(&config.book.summary, source_root)?;
18 27
19 let build_dir = Path::new(&config.build.build_dir); 28 let build_dir = Path::new(&config.build.build_dir);
20 trace!("Creating build directory: '{}'", build_dir.display()); 29 trace!("Creating build directory: '{}'", build_dir.display());
@@ -25,6 +34,10 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
25 ) 34 )
26 })?; 35 })?;
27 36
37 let style_path = build_dir.join("style.css");
38 debug!("Generating file: '{}'", style_path.display());
39 std::fs::write(&style_path, CSS).wrap_err("Could not create style.css")?;
40
28 // Pre-create files so that we know which links to relativize 41 // Pre-create files so that we know which links to relativize
29 for SourceFile { path, .. } in &files { 42 for SourceFile { path, .. } in &files {
30 let output_file = build_dir.join(path.with_extension("html")); 43 let output_file = build_dir.join(path.with_extension("html"));
@@ -47,7 +60,7 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
47 })?; 60 })?;
48 } 61 }
49 62
50 for SourceFile { path, source } in &files { 63 for SourceFile { path, source, .. } in &files {
51 let mut pandoc_command = pandoc::new(); 64 let mut pandoc_command = pandoc::new();
52 65
53 let output_file = build_dir.join(path.with_extension("html")); 66 let output_file = build_dir.join(path.with_extension("html"));
@@ -61,25 +74,36 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
61 .expect("Source file has no parent") 74 .expect("Source file has no parent")
62 .to_path_buf(); 75 .to_path_buf();
63 let build_dir_clone = build_dir.to_path_buf(); 76 let build_dir_clone = build_dir.to_path_buf();
64 let summary_clone = summary.source.clone(); 77
78 let level = source_dir
79 .components()
80 .skip_while(|c| matches!(c, std::path::Component::CurDir))
81 .count();
82
83 let summary = relativize_summary(&summary.source, level);
84 let mut source = source.clone();
85 source.meta.insert(
86 "summary".to_string(),
87 pandoc_ast::MetaValue::MetaBlocks(summary.blocks),
88 );
89
90 let style_path = std::iter::repeat(std::path::Component::ParentDir)
91 .take(level)
92 .collect::<PathBuf>()
93 .join("style.css");
65 94
66 pandoc_command 95 pandoc_command
67 .set_input(pandoc::InputKind::Pipe(source.to_json())) 96 .set_input(pandoc::InputKind::Pipe(source.to_json()))
68 .set_input_format(pandoc::InputFormat::Json, vec![]) 97 .set_input_format(pandoc::InputFormat::Json, vec![])
69 .set_output(pandoc::OutputKind::File(output_file)) 98 .set_output(pandoc::OutputKind::File(output_file))
70 .set_output_format(pandoc::OutputFormat::Html5, vec![]) 99 .set_output_format(pandoc::OutputFormat::Html5, vec![])
71 .add_options(&[pandoc::PandocOption::SelfContained]) 100 .add_options(&[
101 pandoc::PandocOption::Css(style_path.to_string()),
102 pandoc::PandocOption::SectionDivs,
103 pandoc::PandocOption::Standalone,
104 pandoc::PandocOption::Template(template_path.clone()),
105 ])
72 .add_filter(move |source| { 106 .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 { 107 let mut relativize_urls_filter = filters::RelativizeUrls {
84 config: &config_clone, 108 config: &config_clone,
85 // TODO: other output formats 109 // TODO: other output formats
@@ -89,7 +113,6 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
89 }; 113 };
90 114
91 let mut source = pandoc_ast::Pandoc::from_json(&source); 115 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); 116 relativize_urls_filter.walk_pandoc(&mut source);
94 source.to_json() 117 source.to_json()
95 }); 118 });
@@ -109,7 +132,7 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
109// TODO: move that into generated.rs 132// TODO: move that into generated.rs
110fn generate_source( 133fn generate_source(
111 title: Vec<pandoc_ast::Inline>, 134 title: Vec<pandoc_ast::Inline>,
112 children: Vec<(PandocMeta, PathBuf)>, 135 children: &[(&PandocMeta, &Path)],
113 level: usize, 136 level: usize,
114) -> Result<pandoc_ast::Pandoc> { 137) -> Result<pandoc_ast::Pandoc> {
115 // TODO: make that text configurable 138 // TODO: make that text configurable
@@ -117,8 +140,8 @@ fn generate_source(
117 "Here are the articles in this section:".to_string(), 140 "Here are the articles in this section:".to_string(),
118 )])]; 141 )])];
119 142
120 for (mut child, file) in children { 143 for (child, file) in children {
121 let title = match child.remove("title") { 144 let title = match child.get("title").cloned() {
122 None => { 145 None => {
123 warn!("Missing title for file: '{}'", file.display()); 146 warn!("Missing title for file: '{}'", file.display());
124 vec![pandoc_ast::Inline::Str("Untitled page".to_string())] 147 vec![pandoc_ast::Inline::Str("Untitled page".to_string())]
@@ -174,17 +197,13 @@ fn list_content(block: &mut pandoc_ast::Block) -> Result<&mut Vec<Vec<pandoc_ast
174 } 197 }
175} 198}
176 199
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: 200// TODO: support separators like these:
182// --------- 201// ---------
183 202
184#[derive(Debug)] 203#[derive(Debug)]
185pub struct Summary { 204pub struct Summary {
186 source: pandoc_ast::Pandoc, 205 source: pandoc_ast::Pandoc,
187 nodes: Vec<Node>, 206 //nodes: Vec<Node>,
188} 207}
189 208
190#[derive(Debug)] 209#[derive(Debug)]
@@ -194,258 +213,150 @@ struct SourceFile {
194} 213}
195 214
196// TODO: move that into summary.rs 215// TODO: move that into summary.rs
197impl Summary { 216fn process_summary(file: &str, source_root: &Path) -> Result<(Summary, Vec<SourceFile>)> {
198 fn try_from_file(file: &str) -> Result<Self> { 217 debug!("Parsing summary");
199 debug!("Parsing summary"); 218 let mut pandoc_command = pandoc::new();
200 let mut pandoc_command = pandoc::new(); 219 pandoc_command
201 pandoc_command 220 .add_input(file)
202 .add_input(file) 221 .set_output_format(pandoc::OutputFormat::Json, vec![])
203 .set_output_format(pandoc::OutputFormat::Json, vec![]) 222 .set_output(pandoc::OutputKind::Pipe);
204 .set_output(pandoc::OutputKind::Pipe); 223
224 if log_enabled!(log::Level::Trace) {
225 pandoc_command.set_show_cmdline(true);
226 }
205 227
206 trace!("Launching pandoc command"); 228 let output = pandoc_command
229 .execute()
230 .wrap_err("Could not execute pandoc")?
231 .buffer();
207 232
208 if log_enabled!(log::Level::Trace) { 233 let mut document = pandoc_ast::Pandoc::from_json(&output);
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 234
228 Ok(summary) 235 if document.blocks.len() != 1 {
236 return Err(eyre!("Summary does not contain a single list"));
229 } 237 }
230 238
231 fn has_files_missing(&self, root: &Path) -> bool { 239 let root = &mut document.blocks[0];
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 240
239 /// Get a list of source files. 241 let list = list_content(root)?;
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 242
245 for node in &self.nodes { 243 let mut source_files = Vec::new();
246 node.collect_source_files(&mut result, root, Path::new("."), 0)?;
247 }
248 244
249 Ok(result) 245 for element in list.iter_mut() {
246 process_summary_element(element, Path::new("./"), source_root, &mut source_files)?;
250 } 247 }
251}
252
253impl TryFrom<pandoc_ast::Pandoc> for Summary {
254 type Error = eyre::Error;
255 248
256 fn try_from(mut document: pandoc_ast::Pandoc) -> Result<Self, Self::Error> { 249 Ok((Summary { source: document }, source_files))
257 if document.blocks.len() != 1 { 250}
258 return Err(eyre!("Summary does not contain a single list"));
259 }
260 251
261 let root = &mut document.blocks[0]; 252fn process_summary_element(
253 element: &mut Vec<pandoc_ast::Block>,
254 parent: &Path,
255 source_root: &Path,
256 source_files: &mut Vec<SourceFile>,
257) -> Result<()> {
258 if element.len() != 1 && element.len() != 2 {
259 // TODO: better error message?
260 return Err(eyre!("Summary element does not contain a single list"));
261 }
262 262
263 let list = list_content(root)?; 263 trace!("Parsing summary element");
264 let mut value = element.iter_mut();
264 265
265 let nodes = list 266 let item = match value.next().unwrap() {
266 .iter_mut() 267 pandoc_ast::Block::Plain(inlines) => inlines,
267 .map(Node::try_from_vec_block) 268 pandoc_ast::Block::Para(inlines) => inlines,
268 .collect::<Result<_>>()?; 269 _ => return Err(eyre!("List item is not a link or plain text")),
270 };
269 271
270 Ok(Summary { 272 if item.is_empty() {
271 source: document, 273 return Err(eyre!("Summary list items cannot be empty"));
272 nodes,
273 })
274 } 274 }
275}
276 275
277#[derive(Debug)] 276 let child_parent = match &item[0] {
278pub enum Node { 277 pandoc_ast::Inline::Link(_, _, target) => {
279 Provided { 278 let file = &target.0;
280 file: String, 279 Path::new(&file).with_extension("")
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 } 280 }
296 } 281 _ => {
297 282 let title = item.clone();
298 fn has_files_missing(&self, root: &Path) -> bool { 283 let id = AutoIdentifier::from(title.as_slice());
299 if let Node::Provided { file, .. } = self { 284 parent.join(&id.0)
300 if !root.join(file).exists() {
301 error!("File '{}' specified in summary does not exists", file);
302 return true;
303 }
304 } 285 }
286 };
287 trace!("Summary element is {:?}", child_parent);
305 288
306 // Do not use `.any()` to prevent short-circuiting, we want to report all missing files 289 let previous_source_len = source_files.len();
307 self.children().iter().fold(false, |acc, node| { 290 if let Some(children) = value.next() {
308 let missing = node.has_files_missing(root); 291 for child in list_content(children)? {
309 acc || missing 292 process_summary_element(child, &child_parent, source_root, source_files)?;
310 }) 293 }
311 } 294 }
312 295
313 fn collect_source_files( 296 match &item[0] {
314 &self, 297 pandoc_ast::Inline::Link(_, _, target) => {
315 result: &mut Vec<SourceFile>, 298 if item.len() != 1 {
316 root: &Path, 299 return Err(eyre!("Summary list item not a single link or plain text"));
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 } 300 }
356 301
357 Self::Generated { 302 let file = target.0.clone();
358 file, 303 source_files.push(parse_file(&file, source_root)?);
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 } 304 }
381 305 _ => {
382 result.push(SourceFile { 306 let title = item.clone();
383 path, 307
384 source: source(direct_children)?, 308 let id = AutoIdentifier::from(title.as_slice());
385 }); 309
386 310 *item = vec![pandoc_ast::Inline::Link(
387 Ok(()) 311 (String::new(), vec!["generated".to_string()], vec![]),
388 } 312 item.clone(),
389 313 (
390 // Wil also modify the block to linkify generated pages 314 parent.join(&id.0).with_extension("html").to_string(),
391 fn try_from_vec_block(value: &mut Vec<pandoc_ast::Block>) -> Result<Self> { 315 String::new(),
392 if value.len() != 1 && value.len() != 2 { 316 ),
393 // TODO: better error message? 317 )];
394 return Err(eyre!("Summary does not contain a single list")); 318
395 } 319 // TODO: this shows children recursively (and has a bug when in a subdirectory)
396 320 let children_metadata = source_files[previous_source_len..source_files.len()]
397 let mut value = value.iter_mut(); 321 .iter()
398 322 .map(|source| (&source.source.meta, source.path.as_ref()))
399 let item = match value.next().unwrap() { 323 .collect::<Vec<_>>();
400 pandoc_ast::Block::Plain(inlines) => inlines, 324
401 pandoc_ast::Block::Para(inlines) => inlines, 325 let source = generate_source(title, &children_metadata, 0)?;
402 _ => return Err(eyre!("List item is not a link or plain text")), 326
403 }; 327 source_files.push(SourceFile {
404 328 path: child_parent.with_extension("html"),
405 if item.is_empty() { 329 source,
406 return Err(eyre!("Summary list items cannot be empty")); 330 });
407 } 331 }
332 }
408 333
409 let children = if let Some(children) = value.next() { 334 Ok(())
410 try_into_node_vec(list_content(children)?)? 335}
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 336
428 let id = AutoIdentifier::from(title.as_slice()); 337fn parse_file(file: &str, source_root: &Path) -> Result<SourceFile> {
338 trace!("Parsing file: '{}'", file);
429 339
430 // TODO: missing parent 340 // TODO: some filters here? not all filters, since we may want to filter generated
341 // files too
342 let mut pandoc_command = pandoc::new();
343 pandoc_command
344 .add_input(&source_root.join(file))
345 .set_output(pandoc::OutputKind::Pipe)
346 .set_output_format(pandoc::OutputFormat::Json, vec![]);
431 347
432 // Move generate page into this pass 348 if log_enabled!(log::Level::Trace) {
433 //let mut file = parent.join(&*id); 349 pandoc_command.set_show_cmdline(true);
434 //file.set_extension("md"); 350 }
435 351
436 // TODO: Attribute to style them differently 352 let raw_source = pandoc_command
437 *item = vec![pandoc_ast::Inline::Link( 353 .execute()
438 (String::new(), vec!["generated".to_string()], vec![]), 354 .wrap_err_with(|| format!("Failed to parse '{}'", file))?
439 item.clone(), 355 .buffer();
440 (id.0.clone(), String::new()), 356 let source = pandoc_ast::Pandoc::from_json(&raw_source);
441 )];
442 357
443 Ok(Node::Generated { 358 Ok(SourceFile {
444 file: id.0, 359 path: file.into(),
445 title, 360 source,
446 children, 361 })
447 })
448 }
449 }
450 }
451} 362}
diff --git a/src/filters.rs b/src/filters.rs
index 1b06920..8dbe578 100644
--- a/src/filters.rs
+++ b/src/filters.rs
@@ -40,15 +40,13 @@ impl<'a> pandoc_ast::MutVisitor for RelativizeUrls<'a> {
40 if output_path.exists() { 40 if output_path.exists() {
41 // TODO: relativize from URL root 41 // TODO: relativize from URL root
42 42
43 trace!("Relativizing link '{}'", link_path.display());
44
45 *link = Path::new(link) 43 *link = Path::new(link)
46 .with_extension(&self.extension) 44 .with_extension(&self.extension)
47 .to_str() 45 .to_str()
48 .expect("Path constructed from UTF-8 valid strings in not UTF-8 valid") 46 .expect("Path constructed from UTF-8 valid strings in not UTF-8 valid")
49 .to_string(); 47 .to_string();
50 48
51 trace!("-> into '{}'", link); 49 trace!("Relativizing link '{}' -> into '{}'", link_path.display(), link);
52 } 50 }
53 } 51 }
54} 52}
@@ -94,22 +92,3 @@ pub fn relativize_summary(summary: &pandoc_ast::Pandoc, level: usize) -> pandoc_
94 }) 92 })
95 .clone() 93 .clone()
96} 94}
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/utils.rs b/src/utils.rs
index 8928cfb..828ae46 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,4 +1,4 @@
1use std::path::PathBuf; 1use std::path::{Path, PathBuf};
2 2
3pub fn pandoc_stringify(inlines: &[pandoc_ast::Inline]) -> String { 3pub fn pandoc_stringify(inlines: &[pandoc_ast::Inline]) -> String {
4 fn pandoc_stringify_(result: &mut String, inlines: &[pandoc_ast::Inline]) { 4 fn pandoc_stringify_(result: &mut String, inlines: &[pandoc_ast::Inline]) {
@@ -114,3 +114,19 @@ impl PandocOutputExt for pandoc::PandocOutput {
114} 114}
115 115
116pub type PandocMeta = pandoc_ast::Map<String, pandoc_ast::MetaValue>; 116pub type PandocMeta = pandoc_ast::Map<String, pandoc_ast::MetaValue>;
117
118pub trait PathExt {
119 fn to_string(&self) -> String;
120}
121
122impl PathExt for Path {
123 fn to_string(&self) -> String {
124 self.to_str().expect("Path is not valid UTF-8").to_string()
125 }
126}
127
128impl PathExt for PathBuf {
129 fn to_string(&self) -> String {
130 self.to_str().expect("Path is not valid UTF-8").to_string()
131 }
132}