diff options
-rw-r--r-- | Cargo.lock | 96 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | res/style.css | 171 | ||||
-rw-r--r-- | res/template.html | 72 | ||||
-rw-r--r-- | src/build.rs | 411 | ||||
-rw-r--r-- | src/filters.rs | 23 | ||||
-rw-r--r-- | src/utils.rs | 18 |
7 files changed, 519 insertions, 273 deletions
@@ -186,6 +186,17 @@ dependencies = [ | |||
186 | ] | 186 | ] |
187 | 187 | ||
188 | [[package]] | 188 | [[package]] |
189 | name = "getrandom" | ||
190 | version = "0.2.3" | ||
191 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
192 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" | ||
193 | dependencies = [ | ||
194 | "cfg-if", | ||
195 | "libc", | ||
196 | "wasi", | ||
197 | ] | ||
198 | |||
199 | [[package]] | ||
189 | name = "gimli" | 200 | name = "gimli" |
190 | version = "0.26.1" | 201 | version = "0.26.1" |
191 | source = "registry+https://github.com/rust-lang/crates.io-index" | 202 | source = "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" | |||
409 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" | 421 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" |
410 | 422 | ||
411 | [[package]] | 423 | [[package]] |
424 | name = "ppv-lite86" | ||
425 | version = "0.2.15" | ||
426 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
427 | checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" | ||
428 | |||
429 | [[package]] | ||
412 | name = "proc-macro-error" | 430 | name = "proc-macro-error" |
413 | version = "1.0.4" | 431 | version = "1.0.4" |
414 | source = "registry+https://github.com/rust-lang/crates.io-index" | 432 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -451,6 +469,55 @@ dependencies = [ | |||
451 | ] | 469 | ] |
452 | 470 | ||
453 | [[package]] | 471 | [[package]] |
472 | name = "rand" | ||
473 | version = "0.8.4" | ||
474 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
475 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" | ||
476 | dependencies = [ | ||
477 | "libc", | ||
478 | "rand_chacha", | ||
479 | "rand_core", | ||
480 | "rand_hc", | ||
481 | ] | ||
482 | |||
483 | [[package]] | ||
484 | name = "rand_chacha" | ||
485 | version = "0.3.1" | ||
486 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
487 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | ||
488 | dependencies = [ | ||
489 | "ppv-lite86", | ||
490 | "rand_core", | ||
491 | ] | ||
492 | |||
493 | [[package]] | ||
494 | name = "rand_core" | ||
495 | version = "0.6.3" | ||
496 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
497 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" | ||
498 | dependencies = [ | ||
499 | "getrandom", | ||
500 | ] | ||
501 | |||
502 | [[package]] | ||
503 | name = "rand_hc" | ||
504 | version = "0.3.1" | ||
505 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
506 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" | ||
507 | dependencies = [ | ||
508 | "rand_core", | ||
509 | ] | ||
510 | |||
511 | [[package]] | ||
512 | name = "redox_syscall" | ||
513 | version = "0.2.10" | ||
514 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
515 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" | ||
516 | dependencies = [ | ||
517 | "bitflags", | ||
518 | ] | ||
519 | |||
520 | [[package]] | ||
454 | name = "regex" | 521 | name = "regex" |
455 | version = "1.5.4" | 522 | version = "1.5.4" |
456 | source = "registry+https://github.com/rust-lang/crates.io-index" | 523 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -468,6 +535,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
468 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" | 535 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" |
469 | 536 | ||
470 | [[package]] | 537 | [[package]] |
538 | name = "remove_dir_all" | ||
539 | version = "0.5.3" | ||
540 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
541 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" | ||
542 | dependencies = [ | ||
543 | "winapi", | ||
544 | ] | ||
545 | |||
546 | [[package]] | ||
471 | name = "rust-ini" | 547 | name = "rust-ini" |
472 | version = "0.13.0" | 548 | version = "0.13.0" |
473 | source = "registry+https://github.com/rust-lang/crates.io-index" | 549 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -567,6 +643,20 @@ dependencies = [ | |||
567 | ] | 643 | ] |
568 | 644 | ||
569 | [[package]] | 645 | [[package]] |
646 | name = "tempfile" | ||
647 | version = "3.2.0" | ||
648 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
649 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" | ||
650 | dependencies = [ | ||
651 | "cfg-if", | ||
652 | "libc", | ||
653 | "rand", | ||
654 | "redox_syscall", | ||
655 | "remove_dir_all", | ||
656 | "winapi", | ||
657 | ] | ||
658 | |||
659 | [[package]] | ||
570 | name = "termcolor" | 660 | name = "termcolor" |
571 | version = "1.1.2" | 661 | version = "1.1.2" |
572 | source = "registry+https://github.com/rust-lang/crates.io-index" | 662 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -689,6 +779,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
689 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" | 779 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" |
690 | 780 | ||
691 | [[package]] | 781 | [[package]] |
782 | name = "wasi" | ||
783 | version = "0.10.2+wasi-snapshot-preview1" | ||
784 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
785 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" | ||
786 | |||
787 | [[package]] | ||
692 | name = "winapi" | 788 | name = "winapi" |
693 | version = "0.3.9" | 789 | version = "0.3.9" |
694 | source = "registry+https://github.com/rust-lang/crates.io-index" | 790 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -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 |
21 | pandoc_ast = { git = "https://github.com/oli-obk/pandoc-ast", rev = "d73a7d0a065f568d60bcee6cff2340f75065273e" } | 21 | pandoc_ast = { git = "https://github.com/oli-obk/pandoc-ast", rev = "d73a7d0a065f568d60bcee6cff2340f75065273e" } |
22 | serde = { version = "1", features = [ "derive" ] } | 22 | serde = { version = "1", features = [ "derive" ] } |
23 | tempfile = "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 */ | ||
34 | html { | ||
35 | line-height: 1.5; | ||
36 | font-family: Georgia, serif; | ||
37 | font-size: 20px; | ||
38 | color: #1a1a1a; | ||
39 | background-color: #fdfdfd; | ||
40 | } | ||
41 | body { | ||
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 | } | ||
73 | p { | ||
74 | margin: 1em 0; | ||
75 | } | ||
76 | a { | ||
77 | color: #1a1a1a; | ||
78 | } | ||
79 | a:visited { | ||
80 | color: #1a1a1a; | ||
81 | } | ||
82 | img { | ||
83 | max-width: 100%; | ||
84 | } | ||
85 | h1, h2, h3, h4, h5, h6 { | ||
86 | margin-top: 1.4em; | ||
87 | } | ||
88 | h5, h6 { | ||
89 | font-size: 1em; | ||
90 | font-style: italic; | ||
91 | } | ||
92 | h6 { | ||
93 | font-weight: normal; | ||
94 | } | ||
95 | ol, ul { | ||
96 | padding-left: 1.7em; | ||
97 | margin-top: 1em; | ||
98 | } | ||
99 | li > ol, li > ul { | ||
100 | margin-top: 0; | ||
101 | } | ||
102 | blockquote { | ||
103 | margin: 1em 0 1em 1.7em; | ||
104 | padding-left: 1em; | ||
105 | border-left: 2px solid #e6e6e6; | ||
106 | color: #606060; | ||
107 | } | ||
108 | code { | ||
109 | font-family: Menlo, Monaco, 'Lucida Console', Consolas, monospace; | ||
110 | font-size: 85%; | ||
111 | margin: 0; | ||
112 | } | ||
113 | pre { | ||
114 | margin: 1em 0; | ||
115 | overflow: auto; | ||
116 | } | ||
117 | pre code { | ||
118 | padding: 0; | ||
119 | overflow: visible; | ||
120 | } | ||
121 | .sourceCode { | ||
122 | background-color: transparent; | ||
123 | overflow: visible; | ||
124 | } | ||
125 | hr { | ||
126 | background-color: #1a1a1a; | ||
127 | border: none; | ||
128 | height: 1px; | ||
129 | margin: 1em 0; | ||
130 | } | ||
131 | table { | ||
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 | } | ||
139 | table caption { | ||
140 | margin-bottom: 0.75em; | ||
141 | } | ||
142 | tbody { | ||
143 | margin-top: 0.5em; | ||
144 | border-top: 1px solid #1a1a1a; | ||
145 | border-bottom: 1px solid #1a1a1a; | ||
146 | } | ||
147 | th { | ||
148 | border-top: 1px solid #1a1a1a; | ||
149 | padding: 0.25em 0.5em 0.25em 0.5em; | ||
150 | } | ||
151 | td { | ||
152 | padding: 0.125em 0.5em 0.25em 0.5em; | ||
153 | } | ||
154 | header { | ||
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 | } | ||
164 | code{white-space: pre-wrap;} | ||
165 | span.smallcaps{font-variant: small-caps;} | ||
166 | span.underline{text-decoration: underline;} | ||
167 | div.column{display: inline-block; vertical-align: top; width: 50%;} | ||
168 | div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} | ||
169 | ul.task-list{list-style: none;} | ||
170 | q { 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 @@ | |||
1 | use std::path::{Path, PathBuf}; | 1 | use std::path::{Path, PathBuf}; |
2 | 2 | ||
3 | use eyre::{eyre, ContextCompat, Result, WrapErr}; | 3 | use eyre::{eyre, Result, WrapErr}; |
4 | use log::{debug, error, log_enabled, trace, warn}; | 4 | use log::{debug, error, log_enabled, trace, warn}; |
5 | use pandoc_ast::MutVisitor; | 5 | use pandoc_ast::MutVisitor; |
6 | 6 | ||
7 | use crate::{ | 7 | use crate::{ |
8 | filters, | 8 | filters::{self, relativize_summary}, |
9 | utils::{AutoIdentifier, PandocMeta, PandocOutputExt}, | 9 | utils::{AutoIdentifier, PandocMeta, PandocOutputExt, PathExt}, |
10 | }; | 10 | }; |
11 | 11 | ||
12 | const CSS: &str = include_str!("../res/style.css"); | ||
13 | const HTML_TEMPLATE: &str = include_str!("../res/template.html"); | ||
14 | |||
12 | pub fn do_build(config: &crate::config::Config) -> Result<()> { | 15 | pub 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 |
110 | fn generate_source( | 133 | fn 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 | ||
177 | fn 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)] |
185 | pub struct Summary { | 204 | pub 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 |
197 | impl Summary { | 216 | fn 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 | |||
253 | impl 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]; | 252 | fn 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] { |
278 | pub 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 | |||
290 | impl 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()); | 337 | fn 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 | |||
98 | pub struct InsertSummary<'a> { | ||
99 | pub summary: &'a pandoc_ast::Pandoc, | ||
100 | pub level: usize, | ||
101 | } | ||
102 | |||
103 | impl<'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 @@ | |||
1 | use std::path::PathBuf; | 1 | use std::path::{Path, PathBuf}; |
2 | 2 | ||
3 | pub fn pandoc_stringify(inlines: &[pandoc_ast::Inline]) -> String { | 3 | pub 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 | ||
116 | pub type PandocMeta = pandoc_ast::Map<String, pandoc_ast::MetaValue>; | 116 | pub type PandocMeta = pandoc_ast::Map<String, pandoc_ast::MetaValue>; |
117 | |||
118 | pub trait PathExt { | ||
119 | fn to_string(&self) -> String; | ||
120 | } | ||
121 | |||
122 | impl 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 | |||
128 | impl PathExt for PathBuf { | ||
129 | fn to_string(&self) -> String { | ||
130 | self.to_str().expect("Path is not valid UTF-8").to_string() | ||
131 | } | ||
132 | } | ||