From ded626f0b9685355e9856c352daf21c09382999e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 6 Feb 2021 15:02:40 +1000 Subject: [PATCH] render deck description with markdown; strip images To support images on that screen, we'll first need to adjust the base url for each platform, or rewrite the local image URLs, as otherwise they are resolved to _anki/pages/... --- Cargo.lock | 28 ++++ cargo/crates.bzl | 30 ++++ cargo/licenses.json | 27 ++++ cargo/remote/BUILD.getopts-0.2.21.bazel | 56 +++++++ cargo/remote/BUILD.pulldown-cmark-0.8.0.bazel | 138 ++++++++++++++++++ cargo/remote/BUILD.unicode-width-0.1.8.bazel | 54 +++++++ rslib/BUILD.bazel | 1 + rslib/Cargo.toml | 1 + rslib/cargo/BUILD.bazel | 9 ++ rslib/src/decks/mod.rs | 13 +- rslib/src/lib.rs | 1 + rslib/src/markdown.rs | 11 ++ rslib/src/sched/congrats.rs | 7 +- rslib/src/text.rs | 12 ++ ts/congrats/CongratsPage.svelte | 11 +- 15 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 cargo/remote/BUILD.getopts-0.2.21.bazel create mode 100644 cargo/remote/BUILD.pulldown-cmark-0.8.0.bazel create mode 100644 cargo/remote/BUILD.unicode-width-0.1.8.bazel create mode 100644 rslib/src/markdown.rs diff --git a/Cargo.lock b/Cargo.lock index caf06b11f..a1a6adf9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,7 @@ dependencies = [ "proc-macro-nested", "prost", "prost-build", + "pulldown-cmark", "rand 0.7.3", "regex", "reqwest", @@ -780,6 +781,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1717,6 +1727,18 @@ dependencies = [ "prost", ] +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + [[package]] name = "pyo3" version = "0.13.1" @@ -2704,6 +2726,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.1" diff --git a/cargo/crates.bzl b/cargo/crates.bzl index f034293ef..7f60f12a8 100644 --- a/cargo/crates.bzl +++ b/cargo/crates.bzl @@ -801,6 +801,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.generic-array-0.14.4.bazel"), ) + maybe( + http_archive, + name = "raze__getopts__0_2_21", + url = "https://crates.io/api/v1/crates/getopts/0.2.21/download", + type = "tar.gz", + sha256 = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5", + strip_prefix = "getopts-0.2.21", + build_file = Label("//cargo/remote:BUILD.getopts-0.2.21.bazel"), + ) + maybe( http_archive, name = "raze__getrandom__0_1_16", @@ -1781,6 +1791,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.prost-types-0.7.0.bazel"), ) + maybe( + http_archive, + name = "raze__pulldown_cmark__0_8_0", + url = "https://crates.io/api/v1/crates/pulldown-cmark/0.8.0/download", + type = "tar.gz", + sha256 = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8", + strip_prefix = "pulldown-cmark-0.8.0", + build_file = Label("//cargo/remote:BUILD.pulldown-cmark-0.8.0.bazel"), + ) + maybe( http_archive, name = "raze__pyo3__0_13_1", @@ -2751,6 +2771,16 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.unicode-segmentation-1.7.1.bazel"), ) + maybe( + http_archive, + name = "raze__unicode_width__0_1_8", + url = "https://crates.io/api/v1/crates/unicode-width/0.1.8/download", + type = "tar.gz", + sha256 = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3", + strip_prefix = "unicode-width-0.1.8", + build_file = Label("//cargo/remote:BUILD.unicode-width-0.1.8.bazel"), + ) + maybe( http_archive, name = "raze__unicode_xid__0_2_1", diff --git a/cargo/licenses.json b/cargo/licenses.json index 9e8f32aef..fcb3e2e07 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -728,6 +728,15 @@ "license_file": null, "description": "Generic types implementing functionality of arrays" }, + { + "name": "getopts", + "version": "0.2.21", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-lang/getopts", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "getopts-like option parsing." + }, { "name": "getrandom", "version": "0.1.16", @@ -1610,6 +1619,15 @@ "license_file": null, "description": "A Protocol Buffers implementation for the Rust Language." }, + { + "name": "pulldown-cmark", + "version": "0.8.0", + "authors": "Raph Levien |Marcus Klaas de Vries ", + "repository": "https://github.com/raphlinus/pulldown-cmark", + "license": "MIT", + "license_file": null, + "description": "A pull parser for CommonMark" + }, { "name": "pyo3", "version": "0.13.1", @@ -2492,6 +2510,15 @@ "license_file": null, "description": "This crate provides Grapheme Cluster, Word and Sentence boundaries according to Unicode Standard Annex #29 rules." }, + { + "name": "unicode-width", + "version": "0.1.8", + "authors": "kwantam |Manish Goregaokar ", + "repository": "https://github.com/unicode-rs/unicode-width", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Determine displayed width of `char` and `str` types according to Unicode Standard Annex #11 rules." + }, { "name": "unicode-xid", "version": "0.2.1", diff --git a/cargo/remote/BUILD.getopts-0.2.21.bazel b/cargo/remote/BUILD.getopts-0.2.21.bazel new file mode 100644 index 000000000..cbdf45e23 --- /dev/null +++ b/cargo/remote/BUILD.getopts-0.2.21.bazel @@ -0,0 +1,56 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_library( + name = "getopts", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + data = [], + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.2.21", + # buildifier: leave-alone + deps = [ + "@raze__unicode_width__0_1_8//:unicode_width", + ], +) + +# Unsupported target "smoke" with type "test" omitted diff --git a/cargo/remote/BUILD.pulldown-cmark-0.8.0.bazel b/cargo/remote/BUILD.pulldown-cmark-0.8.0.bazel new file mode 100644 index 000000000..05231842a --- /dev/null +++ b/cargo/remote/BUILD.pulldown-cmark-0.8.0.bazel @@ -0,0 +1,138 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT" +]) + +# Generated Targets +# buildifier: disable=load-on-top +load( + "@io_bazel_rules_rust//cargo:cargo_build_script.bzl", + "cargo_build_script", +) + +cargo_build_script( + name = "pulldown_cmark_build_script", + srcs = glob(["**/*.rs"]), + build_script_env = { + }, + crate_features = [ + "default", + "getopts", + ], + crate_root = "build.rs", + data = glob(["**"]), + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.8.0", + visibility = ["//visibility:private"], + deps = [ + ], +) + +# Unsupported target "html_rendering" with type "bench" omitted + +# Unsupported target "lib" with type "bench" omitted + +rust_binary( + # Prefix bin name to disambiguate from (probable) collision with lib name + # N.B.: The exact form of this is subject to change. + name = "cargo_bin_pulldown_cmark", + srcs = glob(["**/*.rs"]), + crate_features = [ + "default", + "getopts", + ], + crate_root = "src/main.rs", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.8.0", + # buildifier: leave-alone + deps = [ + # Binaries get an implicit dependency on their crate's lib + ":pulldown_cmark", + ":pulldown_cmark_build_script", + "@raze__bitflags__1_2_1//:bitflags", + "@raze__getopts__0_2_21//:getopts", + "@raze__memchr__2_3_4//:memchr", + "@raze__unicase__2_6_0//:unicase", + ], +) + +# Unsupported target "broken-link-callbacks" with type "example" omitted + +# Unsupported target "event-filter" with type "example" omitted + +# Unsupported target "string-to-string" with type "example" omitted + +rust_library( + name = "pulldown_cmark", + srcs = glob(["**/*.rs"]), + crate_features = [ + "default", + "getopts", + ], + crate_root = "src/lib.rs", + crate_type = "lib", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.8.0", + # buildifier: leave-alone + deps = [ + ":pulldown_cmark_build_script", + "@raze__bitflags__1_2_1//:bitflags", + "@raze__getopts__0_2_21//:getopts", + "@raze__memchr__2_3_4//:memchr", + "@raze__unicase__2_6_0//:unicase", + ], +) + +# Unsupported target "errors" with type "test" omitted + +# Unsupported target "html" with type "test" omitted + +# Unsupported target "lib" with type "test" omitted diff --git a/cargo/remote/BUILD.unicode-width-0.1.8.bazel b/cargo/remote/BUILD.unicode-width-0.1.8.bazel new file mode 100644 index 000000000..1c5020538 --- /dev/null +++ b/cargo/remote/BUILD.unicode-width-0.1.8.bazel @@ -0,0 +1,54 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_library( + name = "unicode_width", + srcs = glob(["**/*.rs"]), + crate_features = [ + "default", + ], + crate_root = "src/lib.rs", + crate_type = "lib", + data = [], + edition = "2015", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.1.8", + # buildifier: leave-alone + deps = [ + ], +) diff --git a/rslib/BUILD.bazel b/rslib/BUILD.bazel index a219f621c..52a886621 100644 --- a/rslib/BUILD.bazel +++ b/rslib/BUILD.bazel @@ -97,6 +97,7 @@ rust_library( "//rslib/cargo:once_cell", "//rslib/cargo:pin_project", "//rslib/cargo:prost", + "//rslib/cargo:pulldown_cmark", "//rslib/cargo:rand", "//rslib/cargo:regex", "//rslib/cargo:reqwest", diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index eb024ce9b..657cb372c 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -80,3 +80,4 @@ async-trait = "0.1.42" # only in Bazel) proc-macro-nested = "=0.1.6" ammonia = "3.1.0" +pulldown-cmark = "0.8.0" diff --git a/rslib/cargo/BUILD.bazel b/rslib/cargo/BUILD.bazel index f5cf3fb2d..3e0b3dae5 100644 --- a/rslib/cargo/BUILD.bazel +++ b/rslib/cargo/BUILD.bazel @@ -264,6 +264,15 @@ alias( ], ) +alias( + name = "pulldown_cmark", + actual = "@raze__pulldown_cmark__0_8_0//:pulldown_cmark", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "rand", actual = "@raze__rand__0_7_3//:rand", diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index e85061654..fee9160c7 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -1,11 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::backend_proto as pb; pub use crate::backend_proto::{ deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto, DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, }; +use crate::{backend_proto as pb, markdown::render_markdown, text::sanitize_html_no_images}; use crate::{ collection::Collection, deckconf::DeckConfID, @@ -92,6 +92,17 @@ impl Deck { (0, 0) } } + + pub fn rendered_description(&self) -> String { + if let DeckKind::Normal(normal) = &self.kind { + let description = render_markdown(&normal.description); + // before allowing images, we'll need to handle relative image + // links on the various platforms + sanitize_html_no_images(&description) + } else { + String::new() + } + } } fn invalid_char_for_deck_component(c: char) -> bool { diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index 209d00233..ca91003ab 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -19,6 +19,7 @@ mod fluent_proto; pub mod i18n; pub mod latex; pub mod log; +mod markdown; pub mod media; pub mod notes; pub mod notetype; diff --git a/rslib/src/markdown.rs b/rslib/src/markdown.rs new file mode 100644 index 000000000..34de6dc01 --- /dev/null +++ b/rslib/src/markdown.rs @@ -0,0 +1,11 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use pulldown_cmark::{html, Parser}; + +pub(crate) fn render_markdown(markdown: &str) -> String { + let mut buf = String::with_capacity(markdown.len()); + let parser = Parser::new(markdown); + html::push_html(&mut buf, parser); + buf +} diff --git a/rslib/src/sched/congrats.rs b/rslib/src/sched/congrats.rs index b77d6ec57..543337316 100644 --- a/rslib/src/sched/congrats.rs +++ b/rslib/src/sched/congrats.rs @@ -2,7 +2,6 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::backend_proto as pb; -use crate::decks::DeckKind; use crate::prelude::*; #[derive(Debug)] @@ -22,11 +21,7 @@ impl Collection { let today = self.timing_today()?.days_elapsed; let info = self.storage.congrats_info(&deck, today)?; let is_filtered_deck = deck.is_filtered(); - let deck_description = if let DeckKind::Normal(normal) = &deck.kind { - ammonia::clean(&normal.description) - } else { - String::new() - }; + let deck_description = deck.rendered_description(); let secs_until_next_learn = ((info.next_learn_due as i64) - self.learn_ahead_secs() as i64 - TimestampSecs::now().0) diff --git a/rslib/src/text.rs b/rslib/src/text.rs index 40be9342b..be985cbd0 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -226,6 +226,18 @@ pub fn strip_html_preserving_media_filenames(html: &str) -> Cow { without_html.into_owned().into() } +#[allow(dead_code)] +pub(crate) fn sanitize_html(html: &str) -> String { + ammonia::clean(html) +} + +pub(crate) fn sanitize_html_no_images(html: &str) -> String { + ammonia::Builder::default() + .rm_tags(&["img"]) + .clean(html) + .to_string() +} + pub(crate) fn normalize_to_nfc(s: &str) -> Cow { if !is_nfc(s) { s.chars().nfc().collect::().into() diff --git a/ts/congrats/CongratsPage.svelte b/ts/congrats/CongratsPage.svelte index 73d9b52a0..b061de04a 100644 --- a/ts/congrats/CongratsPage.svelte +++ b/ts/congrats/CongratsPage.svelte @@ -32,6 +32,11 @@ .congrats-inner { max-width: 30em; } + + .description { + border: 1px solid var(--border); + padding: 1em; + }
@@ -62,6 +67,10 @@ {/if} {/if} - {@html info.deckDescription} + {#if info.deckDescription} +
+ {@html info.deckDescription} +
+ {/if}