diff --git a/Cargo.lock b/Cargo.lock index 74240f2..9a7c098 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -694,6 +694,33 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf", + "proc-macro2", + "quote", + "smallvec", + "syn", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "derive_more" version = "0.99.11" @@ -735,6 +762,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dtoa-short" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +dependencies = [ + "dtoa", +] + [[package]] name = "either" version = "1.6.1" @@ -825,6 +867,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "futf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.13" @@ -927,11 +979,15 @@ dependencies = [ "chrono", "clap", "env_logger", + "html5ever", + "kuchiki", "linkify", "log", + "markup5ever", "percent-encoding", "pulldown-cmark", "regex", + "slug", "tera", ] @@ -1070,6 +1126,20 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "html5ever" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "http" version = "0.2.3" @@ -1184,6 +1254,18 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kuchiki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" +dependencies = [ + "cssparser", + "html5ever", + "matches", + "selectors", +] + [[package]] name = "language-tags" version = "0.2.2" @@ -1244,12 +1326,32 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -1376,6 +1478,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "4.2.3" @@ -1531,6 +1645,60 @@ dependencies = [ "sha-1 0.8.2", ] +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "0.4.27" @@ -1595,6 +1763,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1654,6 +1828,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc 0.2.0", + "rand_pcg", ] [[package]] @@ -1724,6 +1899,15 @@ dependencies = [ "rand_core 0.6.2", ] +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "redox_syscall" version = "0.2.5" @@ -1797,6 +1981,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + [[package]] name = "semver" version = "0.9.0" @@ -1855,6 +2059,16 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -1895,6 +2109,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" + [[package]] name = "slab" version = "0.4.2" @@ -1927,6 +2147,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "standback" version = "0.2.15" @@ -1985,6 +2211,31 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.8.0" @@ -2002,6 +2253,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tendril" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "tera" version = "1.6.1" @@ -2042,6 +2304,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + [[package]] name = "thiserror" version = "1.0.24" @@ -2385,6 +2653,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "v_escape" version = "0.15.0" diff --git a/Cargo.toml b/Cargo.toml index 3f091b0..6aa1ec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,12 @@ pulldown-cmark = "0.8.0" tera = "1" +kuchiki = "0.8.1" +html5ever = "*" +markup5ever = "*" + chrono = "0.4" regex = "1" linkify = "0.5" +slug = "0.1.4" percent-encoding = "2.1.0" diff --git a/src/main.rs b/src/main.rs index 57b4f87..e1abd5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ +use crate::markup5ever::tendril::TendrilSink; use actix_files::NamedFile; use actix_web::error::ErrorInternalServerError; use actix_web::{error, get, http, middleware, web, App, Error, HttpResponse, HttpServer}; use anyhow::anyhow; use chrono::{DateTime, Local}; use clap::{App as ClapApp, Arg}; +use html5ever::serialize::{serialize, SerializeOpts}; +use kuchiki::{parse_fragment, Attribute, ExpandedName, NodeRef}; use linkify::LinkFinder; use log::{info, trace}; +use markup5ever::QualName; use percent_encoding::{percent_decode_str, utf8_percent_encode}; use pulldown_cmark::{html, Event, Options, Parser, Tag}; use regex::{Captures, Regex}; +use slug::slugify; use std::collections::HashMap; use std::fs::File; use std::io::Read; @@ -19,6 +24,9 @@ use std::time::SystemTime; use std::{env, fs}; use tera::{Context, Tera}; +#[macro_use] +extern crate markup5ever; + #[derive(Clone)] struct State { garden_dir: PathBuf, @@ -323,7 +331,7 @@ fn update_garden>( let mut file_string = String::new(); file.read_to_string(&mut file_string)?; let markdown_source = preprocess_markdown(file_string); - let result = GardenParser::parse(&markdown_source); + let result = GardenParser::parse(&markdown_source)?; pages.insert( String::from(path.to_str().unwrap()), @@ -366,7 +374,7 @@ struct ParseResult { } impl<'a> GardenParser<'a> { - fn parse>(text: &'a S) -> ParseResult { + fn parse>(text: &'a S) -> anyhow::Result { let mut title: Option = None; let mut links: Vec = vec![]; @@ -380,8 +388,9 @@ impl<'a> GardenParser<'a> { let mut html = String::new(); html::push_html(&mut html, parser); + html = postprocess_html(html)?; - ParseResult { html, title, links } + Ok(ParseResult { html, title, links }) } } @@ -449,6 +458,45 @@ fn preprocess_markdown(string: String) -> String { result_string } +fn postprocess_html>(document: T) -> anyhow::Result { + let document_bytes = String::from(document.as_ref()); + let frag = parse_fragment(QualName::new(None, ns!(html), local_name!("body")), vec![]) + .from_utf8() + .read_from(&mut document_bytes.as_bytes()) + .unwrap(); + + frag.select("h1,h2,h3,h4,h5") + .unwrap() + .into_iter() + .for_each(|el| { + let id = slugify(el.text_contents()); + el.attributes.borrow_mut().insert("id", id.clone()); + el.as_node().prepend(NodeRef::new_element( + QualName::new(None, ns!(html), local_name!("a")), + vec![ + ( + ExpandedName::new(ns!(), local_name!("class")), + Attribute { + prefix: None, + value: "anchor".to_string(), + }, + ), + ( + ExpandedName::new(ns!(), local_name!("href")), + Attribute { + prefix: None, + value: format!("#{}", id), + }, + ), + ], + )); + }); + + let mut bytes = vec![]; + serialize(&mut bytes, &frag, SerializeOpts::default())?; + Ok(String::from_utf8(bytes)?) +} + fn normalize_name(filename: &str) -> String { let decoded = percent_decode_str(filename).decode_utf8_lossy(); let result = decoded.strip_suffix(".md"); diff --git a/templates/main.css b/templates/main.css index 0f369e0..02d78d9 100644 --- a/templates/main.css +++ b/templates/main.css @@ -49,6 +49,21 @@ li p { margin: 0; } +.anchor { + text-decoration: none; + opacity: .5; + color: lightgray; +} + +.anchor:after { + content: "#"; + margin-right: .15em; +} + +.anchor:visited { + color: lightgray; +} + aside h1 { font-size: 16pt; text-decoration: underline;