add headline anchors

master
Tomáš Mládek 2021-04-21 22:16:43 +02:00
parent b1591eebae
commit 1b81973a56
4 changed files with 345 additions and 3 deletions

274
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<P: AsRef<Path>>(
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<S: AsRef<str>>(text: &'a S) -> ParseResult {
fn parse<S: AsRef<str>>(text: &'a S) -> anyhow::Result<ParseResult> {
let mut title: Option<String> = None;
let mut links: Vec<String> = 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<T: AsRef<str>>(document: T) -> anyhow::Result<String> {
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");

View File

@ -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;