toolkit for mdBook [mirror of my GitHub repo] docs.tonywu.dev/mdbookkit/
permalinks rust-analyzer mdbook
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: fix postprocessor

Tony Wu 4e0335fd d137d4f1

+290 -297
+6 -17
Cargo.lock
··· 1567 1567 "anyhow", 1568 1568 "clap", 1569 1569 "gix-url", 1570 + "glob", 1571 + "image", 1572 + "lol_html", 1570 1573 "mdbook-markdown", 1571 1574 "mdbookkit", 1572 1575 "miette", 1576 + "minijinja", 1573 1577 "serde", 1574 1578 "serde_json", 1575 1579 "shlex", 1576 1580 "tap", 1577 1581 "tokio", 1582 + "toml 0.5.11", 1578 1583 "tracing", 1579 1584 "tracing-subscriber", 1585 + "url", 1580 1586 ] 1581 1587 1582 1588 [[package]] ··· 3097 3103 version = "0.2.2" 3098 3104 source = "registry+https://github.com/rust-lang/crates.io-index" 3099 3105 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 3100 - 3101 - [[package]] 3102 - name = "util-mdbook-socials" 3103 - version = "0.1.0" 3104 - dependencies = [ 3105 - "anyhow", 3106 - "clap", 3107 - "glob", 3108 - "image", 3109 - "lol_html", 3110 - "minijinja", 3111 - "serde", 3112 - "serde_json", 3113 - "tap", 3114 - "toml 0.5.11", 3115 - "url", 3116 - ] 3117 3106 3118 3107 [[package]] 3119 3108 name = "util-rust-analyzer"
+8 -7
TODO.md
··· 21 21 - [x] ra-version 22 22 - [x] describe 23 23 24 - - [ ] postprocess 25 - - [ ] anchors 26 - - [ ] external links 27 - - [ ] socials 24 + - [x] postprocess 25 + - [ ] ~~anchors~~ 26 + - [x] external links 27 + - [x] socials 28 28 29 29 - [ ] upgrade deps 30 30 - [ ] msrv 31 31 32 - - [ ] changelog 33 - - [ ] binstall 32 + - [ ] CI 33 + - [ ] changelog 34 + - [ ] binstall 34 35 35 - - [ ] CI 36 36 - [ ] cloudflare 37 37 - [ ] URLs 38 + - [ ] socials
+9
docs/Cargo.toml
··· 12 12 anyhow = { workspace = true } 13 13 clap = { workspace = true, features = ["unstable-doc"] } 14 14 gix-url = { version = "0.30.0" } 15 + glob = "0.3.2" 16 + image = { version = "0.25.6", features = [ 17 + "png", 18 + "webp", 19 + ], default-features = false } 20 + lol_html = "2.2.0" 15 21 mdbook-markdown = { workspace = true } 16 22 mdbookkit = { workspace = true } 17 23 miette = { workspace = true } 24 + minijinja = { workspace = true } 18 25 serde = { workspace = true } 19 26 serde_json = { workspace = true } 20 27 shlex = { workspace = true } 21 28 tap = { workspace = true } 22 29 tokio = { workspace = true } 30 + toml = { workspace = true } 23 31 tracing = { workspace = true } 24 32 tracing-subscriber = { workspace = true, features = ["env-filter"] } 33 + url = { workspace = true, features = ["serde"] } 25 34 26 35 [[bin]] 27 36 name = "mdbookkit-docs"
+11 -150
docs/bin/main.rs
··· 1 - use std::{ops::Range, process::Stdio, sync::LazyLock}; 1 + use std::{env::current_dir, path::PathBuf}; 2 2 3 - use anyhow::{Context, Result}; 3 + use anyhow::Result; 4 4 5 - use mdbook_markdown::pulldown_cmark::{CodeBlockKind, CowStr, Event, Parser, Tag, TagEnd}; 6 - use tap::Pipe; 7 5 use tracing::info_span; 8 6 9 - use mdbookkit::{ 10 - book::{BookHelper, book_from_stdin}, 11 - emit_error, 12 - error::ExitProcess, 13 - logging::Logging, 14 - markdown::PatchStream, 15 - }; 7 + use mdbookkit::logging::Logging; 16 8 17 - fn preprocess() -> Result<()> { 18 - let (ctx, mut book) = book_from_stdin().context("Failed to read from mdBook")?; 19 - 20 - #[derive(Default)] 21 - struct State { 22 - mermaid: Option<Range<usize>>, 23 - ra_version: Option<Range<usize>>, 24 - describe: Option<Range<usize>>, 25 - } 26 - 27 - enum Replace<'a> { 28 - Mermaid { 29 - text: CowStr<'a>, 30 - span: Range<usize>, 31 - }, 32 - RustAnalyzerVersion { 33 - span: Range<usize>, 34 - }, 35 - Describe { 36 - package: &'static str, 37 - span: Range<usize>, 38 - }, 39 - } 40 - 41 - book.for_each_text_mut(|_, content| { 42 - let stream = Parser::new(content) 43 - .into_offset_iter() 44 - .scan(State::default(), |state, (event, span)| { 45 - match event { 46 - Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) 47 - if &*tag == "mermaid" => 48 - { 49 - state.mermaid = Some(span); 50 - } 51 - Event::Text(text) => { 52 - if let Some(ref span) = state.mermaid { 53 - let span = span.clone(); 54 - return Some(Some(Replace::Mermaid { text, span })); 55 - } 56 - } 57 - Event::End(TagEnd::CodeBlock) => { 58 - state.mermaid = None; 59 - } 60 - Event::InlineHtml(tag) => match &*tag { 61 - "<ra-version>" => state.ra_version = Some(span), 62 - "</ra-version>" => { 63 - if let Some(start) = state.ra_version.take() { 64 - let span = start.start..span.end; 65 - return Some(Some(Replace::RustAnalyzerVersion { span })); 66 - } 67 - } 68 - "<rustdoc-links-options>" => state.describe = Some(span), 69 - "</rustdoc-links-options>" => { 70 - if let Some(start) = state.describe.take() { 71 - let span = start.start..span.end; 72 - let package = "mdbook-rustdoc-links"; 73 - return Some(Some(Replace::Describe { package, span })); 74 - } 75 - } 76 - "<permalinks-options>" => state.describe = Some(span), 77 - "</permalinks-options>" => { 78 - if let Some(start) = state.describe.take() { 79 - let span = start.start..span.end; 80 - let package = "mdbook-permalinks"; 81 - return Some(Some(Replace::Describe { package, span })); 82 - } 83 - } 84 - _ => {} 85 - }, 86 - _ => {} 87 - } 88 - Some(None) 89 - }) 90 - .flat_map(|chunk| match chunk? { 91 - Replace::Mermaid { text, span } => { 92 - let repl = vec![ 93 - Event::Start(Tag::HtmlBlock), 94 - Event::Html(CowStr::Borrowed("<pre class=\"mermaid\">")), 95 - Event::Html(text), 96 - Event::Html(CowStr::Borrowed("</pre>")), 97 - Event::End(TagEnd::HtmlBlock), 98 - ] 99 - .into_iter(); 100 - Some((repl, span)) 101 - } 102 - Replace::RustAnalyzerVersion { span } => { 103 - static RA_VERSION: LazyLock<String> = LazyLock::new(|| { 104 - std::process::Command::new(env!("CARGO")) 105 - .args(["run", "--package", "util-rust-analyzer", "--", "version"]) 106 - .stdout(Stdio::piped()) 107 - .output() 108 - .context("failed to run util-rust-analyzer") 109 - .exit(emit_error!()) 110 - .stdout 111 - .pipe(String::from_utf8) 112 - .context("failed to parse version") 113 - .exit(emit_error!()) 114 - }); 115 - let repl = vec![Event::Code(RA_VERSION.clone().into())].into_iter(); 116 - Some((repl, span)) 117 - } 118 - Replace::Describe { package, span } => { 119 - let described = std::process::Command::new(env!("CARGO")) 120 - .args([ 121 - "run", 122 - "--package", 123 - package, 124 - "--features", 125 - "_testing", 126 - "--", 127 - "describe", 128 - ]) 129 - .stdout(Stdio::piped()) 130 - .stderr(Stdio::inherit()) 131 - .output() 132 - .with_context(|| format!("failed to describe {package}")) 133 - .exit(emit_error!()) 134 - .stdout 135 - .pipe(String::from_utf8) 136 - .context("failed to parse version") 137 - .exit(emit_error!()); 138 - let repl = vec![Event::Html(described.into())].into_iter(); 139 - Some((repl, span)) 140 - } 141 - }) 142 - .collect::<Vec<_>>(); 143 - 144 - if !stream.is_empty() { 145 - *content = PatchStream::new(content, stream.into_iter()) 146 - .into_string() 147 - .unwrap(); 148 - } 149 - }); 150 - 151 - book.to_stdout(&ctx) 152 - } 9 + mod postprocess; 10 + mod preprocess; 153 11 154 12 fn main() -> Result<()> { 155 13 Logging::default().init(); 156 14 let _span = info_span!({ env!("CARGO_PKG_NAME") }).entered(); 157 15 let Program { command } = clap::Parser::parse(); 158 16 match command { 159 - Command::Preprocess { command: None } => preprocess(), 17 + Command::Postprocess { root_dir } => postprocess::run(root_dir.unwrap_or(current_dir()?)), 18 + Command::Preprocess { command: None } => preprocess::run(), 160 19 Command::Preprocess { 161 20 command: Some(Preprocess::Supports { .. }), 162 21 } => Ok(()), 163 - Command::Postprocess => Ok(()), 164 22 } 165 23 } 166 24 ··· 176 34 #[command(subcommand)] 177 35 command: Option<Preprocess>, 178 36 }, 179 - Postprocess, 37 + Postprocess { 38 + #[arg(long)] 39 + root_dir: Option<PathBuf>, 40 + }, 180 41 } 181 42 182 43 #[derive(clap::Subcommand, Debug, Clone)]
+150
docs/bin/preprocess.rs
··· 1 + use std::{ops::Range, process::Stdio, sync::LazyLock}; 2 + 3 + use anyhow::{Context, Result}; 4 + 5 + use mdbook_markdown::pulldown_cmark::{CodeBlockKind, CowStr, Event, Parser, Tag, TagEnd}; 6 + use tap::Pipe; 7 + 8 + use mdbookkit::{ 9 + book::{BookHelper, book_from_stdin}, 10 + emit_error, 11 + error::ExitProcess, 12 + markdown::PatchStream, 13 + }; 14 + 15 + pub fn run() -> Result<()> { 16 + let (ctx, mut book) = book_from_stdin().context("Failed to read from mdBook")?; 17 + 18 + #[derive(Default)] 19 + struct State { 20 + mermaid: Option<Range<usize>>, 21 + ra_version: Option<Range<usize>>, 22 + describe: Option<Range<usize>>, 23 + } 24 + 25 + enum Replace<'a> { 26 + Mermaid { 27 + text: CowStr<'a>, 28 + span: Range<usize>, 29 + }, 30 + RustAnalyzerVersion { 31 + span: Range<usize>, 32 + }, 33 + Describe { 34 + package: &'static str, 35 + span: Range<usize>, 36 + }, 37 + } 38 + 39 + book.for_each_text_mut(|_, content| { 40 + let stream = Parser::new(content) 41 + .into_offset_iter() 42 + .scan(State::default(), |state, (event, span)| { 43 + match event { 44 + Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) 45 + if &*tag == "mermaid" => 46 + { 47 + state.mermaid = Some(span); 48 + } 49 + Event::Text(text) => { 50 + if let Some(ref span) = state.mermaid { 51 + let span = span.clone(); 52 + return Some(Some(Replace::Mermaid { text, span })); 53 + } 54 + } 55 + Event::End(TagEnd::CodeBlock) => { 56 + state.mermaid = None; 57 + } 58 + Event::InlineHtml(tag) => match &*tag { 59 + "<ra-version>" => state.ra_version = Some(span), 60 + "</ra-version>" => { 61 + if let Some(start) = state.ra_version.take() { 62 + let span = start.start..span.end; 63 + return Some(Some(Replace::RustAnalyzerVersion { span })); 64 + } 65 + } 66 + "<rustdoc-links-options>" => state.describe = Some(span), 67 + "</rustdoc-links-options>" => { 68 + if let Some(start) = state.describe.take() { 69 + let span = start.start..span.end; 70 + let package = "mdbook-rustdoc-links"; 71 + return Some(Some(Replace::Describe { package, span })); 72 + } 73 + } 74 + "<permalinks-options>" => state.describe = Some(span), 75 + "</permalinks-options>" => { 76 + if let Some(start) = state.describe.take() { 77 + let span = start.start..span.end; 78 + let package = "mdbook-permalinks"; 79 + return Some(Some(Replace::Describe { package, span })); 80 + } 81 + } 82 + _ => {} 83 + }, 84 + _ => {} 85 + } 86 + Some(None) 87 + }) 88 + .flat_map(|chunk| match chunk? { 89 + Replace::Mermaid { text, span } => { 90 + let repl = vec![ 91 + Event::Start(Tag::HtmlBlock), 92 + Event::Html(CowStr::Borrowed("<pre class=\"mermaid\">")), 93 + Event::Html(text), 94 + Event::Html(CowStr::Borrowed("</pre>")), 95 + Event::End(TagEnd::HtmlBlock), 96 + ] 97 + .into_iter(); 98 + Some((repl, span)) 99 + } 100 + Replace::RustAnalyzerVersion { span } => { 101 + static RA_VERSION: LazyLock<String> = LazyLock::new(|| { 102 + std::process::Command::new(env!("CARGO")) 103 + .args(["run", "--package", "util-rust-analyzer", "--", "version"]) 104 + .stdout(Stdio::piped()) 105 + .output() 106 + .context("failed to run util-rust-analyzer") 107 + .exit(emit_error!()) 108 + .stdout 109 + .pipe(String::from_utf8) 110 + .context("failed to parse version") 111 + .exit(emit_error!()) 112 + }); 113 + let repl = vec![Event::Code(RA_VERSION.clone().into())].into_iter(); 114 + Some((repl, span)) 115 + } 116 + Replace::Describe { package, span } => { 117 + let described = std::process::Command::new(env!("CARGO")) 118 + .args([ 119 + "run", 120 + "--package", 121 + package, 122 + "--features", 123 + "_testing", 124 + "--", 125 + "describe", 126 + ]) 127 + .stdout(Stdio::piped()) 128 + .stderr(Stdio::inherit()) 129 + .output() 130 + .with_context(|| format!("failed to describe {package}")) 131 + .exit(emit_error!()) 132 + .stdout 133 + .pipe(String::from_utf8) 134 + .context("failed to parse version") 135 + .exit(emit_error!()); 136 + let repl = vec![Event::Html(described.into())].into_iter(); 137 + Some((repl, span)) 138 + } 139 + }) 140 + .collect::<Vec<_>>(); 141 + 142 + if !stream.is_empty() { 143 + *content = PatchStream::new(content, stream.into_iter()) 144 + .into_string() 145 + .unwrap(); 146 + } 147 + }); 148 + 149 + book.to_stdout(&ctx) 150 + }
+10 -10
docs/book.toml
··· 38 38 [preprocessor.app] 39 39 command = "deno run --allow-all app/build/preprocessor.ts" 40 40 41 - [preprocessor.preprocess] 41 + [preprocessor.doc] 42 42 after = ["links"] 43 43 command = "cargo run -- preprocess" 44 44 45 - # [_metadata.socials."/"] 46 - # image = "src/media/social.webp" 47 - # title = "mdbookkit" 45 + [preprocessor.doc.socials."/"] 46 + image = "src/media/social.webp" 47 + title = "mdbookkit" 48 48 49 - # [_metadata.socials."/rustdoc-link"] 50 - # image = "src/rustdoc-link/media/social.webp" 51 - # title = "mdbook-rustdoc-link" 49 + [preprocessor.doc.socials."/rustdoc-links/"] 50 + image = "src/rustdoc-links/media/social.webp" 51 + title = "mdbook-rustdoc-links" 52 52 53 - # [_metadata.socials."/link-forever"] 54 - # image = "src/link-forever/media/social.webp" 55 - # title = "mdbook-link-forever" 53 + [preprocessor.doc.socials."/permalinks/"] 54 + image = "src/permalinks/media/social.webp" 55 + title = "mdbook-permalinks"
+2 -6
docs/src/permalinks/continuous-integration.md
··· 5 5 6 6 ## Detecting CI 7 7 8 - {{#include ../snippets/detecting-ci.md}} 8 + {{#include ../snippets/ci/detecting-ci.md}} 9 9 10 10 ## Linking to Git tags 11 11 ··· 26 26 fetch-depth: 0 # https://github.com/actions/checkout/issues/1471#issuecomment-1771231294 27 27 ``` 28 28 29 - ## Logging 30 - 31 - {{#include ../snippets/logging.md}} 32 - 33 29 ## Error handling 34 30 35 - {{#include ../snippets/error-handling.md}} 31 + {{#include ../snippets/ci/error-handling.md}} 36 32 37 33 <!-- prettier-ignore-start --> 38 34
+2 -6
docs/src/rustdoc-links/continuous-integration.md
··· 5 5 6 6 ## Detecting CI 7 7 8 - {{#include ../snippets/detecting-ci.md}} 8 + {{#include ../snippets/ci/detecting-ci.md}} 9 9 10 10 ## Installing rust-analyzer 11 11 ··· 26 26 > rust-analyzer from rustup follows Rust's release schedule, which may lag behind the 27 27 > version bundled with the VS Code extension. 28 28 29 - ## Logging 30 - 31 - {{#include ../snippets/logging.md}} 32 - 33 29 ## Error handling 34 30 35 - {{#include ../snippets/error-handling.md}} 31 + {{#include ../snippets/ci/error-handling.md}} 36 32 37 33 [^ra-on-path]: 38 34 You may alternatively specify a command to use for rust-analyzer via the
docs/src/snippets/detecting-ci.md docs/src/snippets/ci/detecting-ci.md
docs/src/snippets/error-handling.md docs/src/snippets/ci/error-handling.md
-25
utils/mdbook-socials/Cargo.toml
··· 1 - [package] 2 - name = "util-mdbook-socials" 3 - version = "0.1.0" 4 - 5 - authors.workspace = true 6 - edition.workspace = true 7 - license.workspace = true 8 - publish.workspace = true 9 - repository.workspace = true 10 - 11 - [dependencies] 12 - anyhow = { workspace = true } 13 - clap = { workspace = true } 14 - glob = "0.3.2" 15 - image = { version = "0.25.6", features = [ 16 - "png", 17 - "webp", 18 - ], default-features = false } 19 - lol_html = "2.2.0" 20 - minijinja = { workspace = true } 21 - serde = { workspace = true } 22 - serde_json = { workspace = true } 23 - tap = { workspace = true } 24 - toml = { workspace = true } 25 - url = { workspace = true, features = ["serde"] }
+92 -76
utils/mdbook-socials/src/main.rs docs/bin/postprocess.rs
··· 1 - //! Postprocess mdBook HTML output. 2 - //! 3 - //! Currently does the following: 4 - //! 5 - //! - Add OpenGraph metadata and link to images for social. 6 - //! - Add explicit widths and heights to images: <https://web.dev/articles/optimize-cls#images-without-dimensions> 7 - //! 8 - //! mdBook doesn't support frontmatters yet, so this cannot be a preprocessor. 9 - 10 - use std::{collections::HashMap, path::PathBuf}; 1 + use std::{collections::HashMap, fmt::Write, path::PathBuf}; 11 2 12 3 use anyhow::{Context, Result}; 13 - use clap::Parser; 14 4 use glob::glob; 15 5 use lol_html::{ 16 6 HtmlRewriter, RewriteStrSettings, Settings, element, html_content::ContentType, rewrite_str, 17 7 text, 18 8 }; 9 + use mdbookkit::url::UrlFromPath; 19 10 use minijinja::Environment; 20 11 use serde::Deserialize; 21 12 use serde_json::json; 22 13 use tap::{Pipe, Tap}; 14 + use tracing::{debug, info, info_span}; 23 15 use url::Url; 24 16 25 - fn main() -> Result<()> { 26 - let Program { root_dir } = Program::parse(); 27 - 17 + pub fn run(root_dir: PathBuf) -> Result<()> { 28 18 let jinja = 29 19 Environment::new().tap_mut(|env| env.add_template("index.html", OPEN_GRAPH).unwrap()); 30 20 31 - let root_dir = std::fs::canonicalize(root_dir)? 32 - .pipe(Url::from_directory_path) 33 - .unwrap(); 21 + let root_dir = std::fs::canonicalize(root_dir)?.to_directory_url(); 34 22 35 23 let book_toml_path = root_dir.join("book.toml")?; 36 24 ··· 38 26 .path() 39 27 .pipe(std::fs::read_to_string)? 40 28 .pipe_deref(toml::from_str::<BookToml>)?; 29 + 30 + debug!("{book_toml:#?}"); 41 31 42 32 let src_dir = book_toml.book.src.as_deref().unwrap_or("src"); 43 33 let src_dir = root_dir.join(&format!("{src_dir}/"))?; ··· 45 35 let out_dir = book_toml.build.build_dir.as_deref().unwrap_or("book"); 46 36 let out_dir = root_dir.join(&format!("{out_dir}/"))?; 47 37 48 - let metadata = book_toml 49 - .metadata 50 - .socials 51 - .0 38 + let BookToml { 39 + preprocessor: 40 + PreprocessorConfig { 41 + permalinks: PermalinksConfig { book_url }, 42 + .. 43 + }, 44 + .. 45 + } = book_toml; 46 + 47 + let metadata = (book_toml.preprocessor.doc.socials.0) 52 48 .into_iter() 53 49 .map(|(prefix, metadata)| -> Result<(_, PageMetadata)> { 54 50 let image = match metadata.image { ··· 62 58 let image = src_dir 63 59 .make_relative(&image) 64 60 .context("Failed to make relative path to image")?; 65 - book_toml.preprocessor.link_forever.book_url.join(&image)? 61 + book_url.join(&image)? 66 62 }; 67 63 let metadata = PageMetadata { 68 64 title: metadata.title, ··· 73 69 .collect::<Result<Vec<_>>>()? 74 70 .tap_mut(|metadata| metadata.sort_by(|(p1, _), (p2, _)| p1.cmp(p2))); 75 71 76 - for path in glob(out_dir.join("**/*.html")?.path())? { 77 - let url = Url::from_file_path(path?).unwrap(); 72 + debug!("{metadata:#?}"); 78 73 74 + for path in glob(out_dir.join("**/*.html")?.path())? { 75 + let url = path?.to_file_url(); 79 76 let html = std::fs::read_to_string(url.path())?; 77 + 78 + let _span = info_span!("html").entered(); 79 + 80 + info!(%url); 80 81 81 82 let (og_title, og_description) = { 82 83 let mut title = String::new(); ··· 109 110 .replace(".html", "") 110 111 .pipe(|p| format!("/{p}")); 111 112 112 - let title = metadata 113 + let suffix = metadata 113 114 .iter() 114 115 .filter_map(|(prefix, metadata)| { 115 116 let title = metadata.title.as_ref()?; 116 - if pathname.starts_with(prefix) && &pathname != prefix 117 117 // pathname != prefix because subroute index page 118 118 // should already have a sensible title 119 - { 119 + if pathname.starts_with(prefix) && &pathname != prefix { 120 120 Some(title.as_str()) 121 121 } else { 122 122 None 123 123 } 124 124 }) 125 - .chain(std::iter::once(og_title.as_str())) 126 125 .rev() 127 - .collect::<Vec<_>>() 128 - .join(" | "); 126 + .collect::<Vec<_>>(); 129 127 130 128 let og_image = metadata.iter().rev().find_map(|(prefix, metadata)| { 131 129 if !pathname.starts_with(prefix) { ··· 135 133 } 136 134 }); 137 135 138 - let og_url = book_toml 139 - .preprocessor 140 - .link_forever 141 - .book_url 142 - .join(&pathname[1..])?; 136 + let og_url = book_url.join(&pathname[1..])?; 143 137 144 138 let og_site_name = book_toml.book.title.as_deref(); 145 139 ··· 151 145 "og_site_name": og_site_name, 152 146 }); 153 147 148 + debug!(?ctx); 149 + 154 150 let html = RewriteStrSettings { 155 151 element_content_handlers: vec![ 156 152 element!("title", |elem| { 153 + let title = suffix.iter().fold(og_title.clone(), |mut out, suffix| { 154 + write!(&mut out, " | {suffix}").and(Ok(out)).unwrap() 155 + }); 156 + debug!(title); 157 157 elem.set_inner_content(&title, ContentType::Text); 158 - Ok(()) 159 - }), 160 - element!(r#"meta[property^="og:"]"#, |elem| { 161 - elem.remove(); 162 158 Ok(()) 163 159 }), 164 160 element!(r#"img[src]"#, |elem| { ··· 175 171 let img = image::open(src)?; 176 172 elem.set_attribute("width", &img.width().to_string())?; 177 173 elem.set_attribute("height", &img.height().to_string())?; 174 + debug!(?elem); 178 175 Ok(()) 179 176 }), 180 177 element!(r#"img[src^="https://img.shields.io/"]"#, |elem| { 181 178 elem.set_attribute("height", "20")?; 182 179 elem.set_attribute("fetchpriority", "low")?; 180 + debug!(?elem); 181 + Ok(()) 182 + }), 183 + element!(r#"meta[property^="og:"]"#, |elem| { 184 + elem.remove(); 183 185 Ok(()) 184 186 }), 185 187 element!(r#"meta[name="description"]"#, |elem| { ··· 188 190 elem.before(&meta, ContentType::Html); 189 191 Ok(()) 190 192 }), 193 + element!(r#"h1.menu-title"#, |elem| { 194 + if let Some(suffix) = suffix.iter().nth_back(1) { 195 + elem.set_inner_content(suffix, ContentType::Text); 196 + } 197 + Ok(()) 198 + }), 199 + element!(r#"a"#, |elem| { 200 + let Some(href) = elem 201 + .get_attribute("href") 202 + .and_then(|u| u.parse::<Url>().ok()) 203 + else { 204 + return Ok(()); 205 + }; 206 + if href.origin() != book_url.origin() { 207 + elem.set_attribute("target", "_blank").unwrap(); 208 + elem.set_attribute("rel", "noreferrer").unwrap(); 209 + } 210 + debug!(?elem); 211 + Ok(()) 212 + }), 191 213 ], 192 214 ..Default::default() 193 215 } ··· 199 221 Ok(()) 200 222 } 201 223 202 - static OPEN_GRAPH: &str = r##" 203 - <meta property="og:type" content="article"> 204 - <meta property="og:title" content="{{ og_title }}"> 205 - <meta property="og:url" content="{{ og_url }}"> 206 - <meta property="og:image" content="{{ og_image }}"> 207 - <meta property="og:description" content="{{ og_description }}"> 208 - <meta property="og:site_name" content="{{ og_site_name }}"> 209 - <meta name="twitter:card" content="summary_large_image"> 210 - <meta name="twitter:title" content="{{ og_title }}"> 211 - <meta name="twitter:image" content="{{ og_image }}"> 212 - <meta name="twitter:image:alt" content="toolkit for mdBook"> 213 - <meta name="twitter:description" content="{{ og_description }}"> 214 - <meta name="theme-color" content="#d2a6ff"> 215 - "##; 216 - 217 - #[derive(Parser)] 218 - struct Program { 219 - root_dir: PathBuf, 220 - } 221 - 222 - #[derive(Deserialize)] 224 + #[derive(Deserialize, Debug)] 223 225 #[serde(rename_all = "kebab-case")] 224 226 struct BookToml { 225 227 book: BookConfig, 226 228 build: BuildConfig, 227 - #[serde(rename = "_metadata")] 228 - metadata: MetadataConfig, 229 229 preprocessor: PreprocessorConfig, 230 230 } 231 231 232 - #[derive(Deserialize)] 232 + #[derive(Deserialize, Debug)] 233 233 #[serde(rename_all = "kebab-case")] 234 234 struct BookConfig { 235 235 #[serde(default)] ··· 238 238 src: Option<String>, 239 239 } 240 240 241 - #[derive(Deserialize)] 241 + #[derive(Deserialize, Debug)] 242 242 #[serde(rename_all = "kebab-case")] 243 - struct PreprocessorConfig { 244 - link_forever: LinkConfig, 243 + struct BuildConfig { 244 + #[serde(default)] 245 + build_dir: Option<String>, 245 246 } 246 247 247 - #[derive(Deserialize)] 248 + #[derive(Deserialize, Debug)] 248 249 #[serde(rename_all = "kebab-case")] 249 - struct LinkConfig { 250 - book_url: Url, 250 + struct PreprocessorConfig { 251 + permalinks: PermalinksConfig, 252 + doc: MetadataConfig, 251 253 } 252 254 253 - #[derive(Deserialize)] 255 + #[derive(Deserialize, Debug)] 254 256 #[serde(rename_all = "kebab-case")] 255 - struct BuildConfig { 256 - #[serde(default)] 257 - build_dir: Option<String>, 257 + struct PermalinksConfig { 258 + book_url: Url, 258 259 } 259 260 260 - #[derive(Deserialize)] 261 + #[derive(Deserialize, Debug)] 261 262 #[serde(rename_all = "kebab-case")] 262 263 struct MetadataConfig { 263 264 #[serde(default)] 264 265 socials: Socials, 265 266 } 266 267 267 - #[derive(Deserialize, Default)] 268 + #[derive(Deserialize, Default, Debug)] 268 269 struct Socials(HashMap<String, PageMetadata>); 269 270 270 - #[derive(Deserialize)] 271 + #[derive(Deserialize, Debug)] 271 272 #[serde(rename_all = "kebab-case")] 272 273 struct PageMetadata { 273 274 title: Option<String>, 274 275 image: Option<String>, 275 276 } 277 + 278 + static OPEN_GRAPH: &str = r##" 279 + <meta property="og:type" content="article"> 280 + <meta property="og:title" content="{{ og_title }}"> 281 + <meta property="og:url" content="{{ og_url }}"> 282 + <meta property="og:image" content="{{ og_image }}"> 283 + <meta property="og:description" content="{{ og_description }}"> 284 + <meta property="og:site_name" content="{{ og_site_name }}"> 285 + <meta name="twitter:card" content="summary_large_image"> 286 + <meta name="twitter:title" content="{{ og_title }}"> 287 + <meta name="twitter:image" content="{{ og_image }}"> 288 + <meta name="twitter:image:alt" content="toolkit for mdBook"> 289 + <meta name="twitter:description" content="{{ og_description }}"> 290 + <meta name="theme-color" content="#d2a6ff"> 291 + "##; 276 292 277 293 fn collapse_whitespace(src: String) -> String { 278 294 src.chars()