diff --git a/Cargo.lock b/Cargo.lock index 96acc8455..95af5047f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ dependencies = [ "intl-memoizer", "itertools 0.10.1", "lazy_static", + "linkcheck", "nom 7.0.0-alpha1", "num-integer", "num_enum", @@ -84,7 +85,7 @@ dependencies = [ "pulldown-cmark", "rand 0.8.4", "regex", - "reqwest", + "reqwest 0.11.3", "rusqlite", "scopeguard", "serde", @@ -337,12 +338,39 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "codespan" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3362992a0d9f1dd7c3d0e89e0ab2bb540b7a95fea8cd798090e758fda2899b5e" +dependencies = [ + "codespan-reporting", + "serde", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation" version = "0.9.1" @@ -398,6 +426,33 @@ dependencies = [ "subtle", ] +[[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 0.8.0", + "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 = "ctor" version = "0.1.20" @@ -419,6 +474,19 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "0.99.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "digest" version = "0.9.0" @@ -449,6 +517,27 @@ dependencies = [ "winapi", ] +[[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 = "dunce" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" + [[package]] name = "either" version = "1.6.1" @@ -692,6 +781,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.4" @@ -1080,6 +1178,18 @@ dependencies = [ "wasm-bindgen", ] +[[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 = "lazy_static" version = "1.4.0" @@ -1116,6 +1226,37 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linkcheck" +version = "0.4.1-alpha.0" +source = "git+https://github.com/ankitects/linkcheck.git?rev=2f20798ce521cc594d510d4e417e76d5eac04d4b#2f20798ce521cc594d510d4e417e76d5eac04d4b" +dependencies = [ + "bytes", + "codespan", + "dunce", + "futures", + "http", + "kuchiki", + "lazy_static", + "linkify", + "log", + "pulldown-cmark", + "regex", + "reqwest 0.11.4", + "serde", + "thiserror", + "url", +] + +[[package]] +name = "linkify" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d59d732ba6d7eeefc418aab8057dc8e3da4374bd5802ffa95bebc04b4d1dfb" +dependencies = [ + "memchr", +] + [[package]] name = "lock_api" version = "0.4.4" @@ -1482,6 +1623,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + [[package]] name = "petgraph" version = "0.5.1" @@ -1498,7 +1648,9 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ + "phf_macros 0.8.0", "phf_shared 0.8.0", + "proc-macro-hack", ] [[package]] @@ -1507,7 +1659,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" dependencies = [ - "phf_macros", + "phf_macros 0.9.0", "phf_shared 0.9.0", "proc-macro-hack", ] @@ -1542,6 +1694,20 @@ dependencies = [ "rand 0.8.4", ] +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_macros" version = "0.9.0" @@ -1973,6 +2139,40 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -2017,6 +2217,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.19.1" @@ -2097,6 +2306,44 @@ dependencies = [ "libc", ] +[[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 0.8.0", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.126" @@ -2183,6 +2430,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 = "sha1" version = "0.6.0" @@ -2422,6 +2679,12 @@ dependencies = [ "winapi-util", ] +[[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.25" @@ -2496,6 +2759,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", + "tokio-macros", "winapi", ] @@ -2508,6 +2772,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.0" @@ -2611,6 +2886,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "unic-char-property" version = "0.9.0" @@ -2763,6 +3044,7 @@ dependencies = [ "idna", "matches", "percent-encoding", + "serde", ] [[package]] diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index 09b86a38d..068efb38f 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -18,6 +18,8 @@ prost-build = "0.7.0" [dev-dependencies] env_logger = "0.8.4" +linkcheck = { git = "https://github.com/ankitects/linkcheck.git", rev = "2f20798ce521cc594d510d4e417e76d5eac04d4b" } +tokio = { version = "*", features = ["macros"] } [dependencies] # pinned as any changes could invalidate sqlite indexes diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index bdc34c752..e3ae998c8 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -18,6 +18,7 @@ pub mod error; pub mod findreplace; pub mod i18n; pub mod latex; +pub mod links; pub mod log; mod markdown; pub mod media; diff --git a/rslib/src/links.rs b/rslib/src/links.rs new file mode 100644 index 000000000..872b7f885 --- /dev/null +++ b/rslib/src/links.rs @@ -0,0 +1,122 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use strum::{Display, EnumIter, EnumString}; + +static HELP_SITE: &'static str = "https://docs.ankiweb.net/"; + +#[derive(Debug, PartialEq, Clone, Copy, Display, EnumIter, EnumString)] +pub enum HelpPage { + #[strum(serialize = "getting-started#note-types")] + Notetype, + #[strum(serialize = "browsing")] + Browsing, + #[strum(serialize = "browsing#find-and-replace")] + BrowsingFindAndReplace, + #[strum(serialize = "browsing#notes")] + BrowsingNotesMenu, + #[strum(serialize = "studying#keyboard-shortcuts")] + KeyboardShortcuts, + #[strum(serialize = "editing")] + Editing, + #[strum(serialize = "editing#adding-cards-and-notes")] + AddingCardAndNote, + #[strum(serialize = "editing#adding-a-note-type")] + AddingNotetype, + #[strum(serialize = "math#latex")] + Latex, + #[strum(serialize = "preferences")] + Preferences, + #[strum(serialize = "")] + Index, + #[strum(serialize = "templates/intro")] + Templates, + #[strum(serialize = "filtered-decks")] + FilteredDeck, + #[strum(serialize = "importing")] + Importing, + #[strum(serialize = "editing#customizing-fields")] + CustomizingFields, + #[strum(serialize = "deck-options")] + DeckOptions, + #[strum(serialize = "editing#features")] + EditingFeatures, +} + +pub fn help_page_link(page: HelpPage) -> String { + format!("{}{}", HELP_SITE, page) +} + +pub fn help_page_link_from_str(page: &str) -> String { + format!("{}{}", HELP_SITE, page) +} + +#[cfg(test)] +mod test { + use super::*; + + use futures::StreamExt; + use itertools::Itertools; + use linkcheck::{ + validation::{check_web, Context, Reason}, + BasicContext, + }; + use reqwest::Url; + use std::iter; + use strum::IntoEnumIterator; + + /// Aggregates [`Outcome`]s by collecting the error messages of the invalid ones. + #[derive(Default)] + struct Outcomes(Vec); + + enum Outcome { + Valid, + Invalid(String), + } + + #[tokio::test] + async fn check_links() { + let ctx = BasicContext::default(); + let result = futures::stream::iter(HelpPage::iter()) + .map(|page| check_page(page, &ctx)) + .buffer_unordered(ctx.concurrency()) + .collect::() + .await; + if result.0.len() > 0 { + panic!(result.message()); + } + } + + async fn check_page(page: HelpPage, ctx: &BasicContext) -> Outcome { + match Url::parse(&help_page_link(page)) { + Ok(url) => match check_web(&url, ctx).await { + Ok(()) => Outcome::Valid, + Err(Reason::Dom) => { + Outcome::Invalid(format!("'{}' not found on '{}'", page, HELP_SITE)) + } + Err(Reason::Web(err)) => Outcome::Invalid(err.to_string()), + _ => unreachable!(), + }, + Err(err) => Outcome::Invalid(err.to_string()), + } + } + + impl Extend for Outcomes { + fn extend>(&mut self, items: T) { + for outcome in items { + match outcome { + Outcome::Valid => (), + Outcome::Invalid(err) => self.0.push(err), + } + } + } + } + + impl Outcomes { + fn message(&self) -> String { + iter::once(&format!("{} links could not be validated:", self.0.len())) + .chain(self.0.iter()) + .join("\n - ") + } + } +}