diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 868a8fdb8..eb86c1f23 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -6,3 +6,5 @@ deck-config-used-by-decks = *[other] { $decks } decks } deck-config-default-name = Default +deck-config-description-markdown = Enable markdown+clean HTML +deck-config-description-markdown-hint = Will appear as text on Anki 2.1.40 and below. diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 1d7c442e7..d909832a7 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -867,6 +867,10 @@ table.review-log {{ {revlog_style} }} def set_preferences(self, prefs: Preferences) -> None: self._backend.set_preferences(prefs) + def render_markdown(self, text: str, sanitize: bool = True) -> str: + "Not intended for public consumption at this time." + return self._backend.render_markdown(markdown=text, sanitize=sanitize) + # legacy name _Collection = Collection diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index 4b9e59c03..1441b68c0 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -236,6 +236,7 @@ class DeckConf(QDialog): f.autoplaySounds.setChecked(c["autoplay"]) f.replayQuestion.setChecked(c.get("replayq", True)) # description + f.enable_markdown.setChecked(self.deck.get("md", False)) f.desc.setPlainText(self.deck["desc"]) gui_hooks.deck_conf_did_load_config(self, self.deck, self.conf) @@ -320,6 +321,7 @@ class DeckConf(QDialog): c["autoplay"] = f.autoplaySounds.isChecked() c["replayq"] = f.replayQuestion.isChecked() # description + self.deck["md"] = f.enable_markdown.isChecked() self.deck["desc"] = f.desc.toPlainText() gui_hooks.deck_conf_will_save_config(self, self.deck, self.conf) self.mw.col.decks.save(self.deck) diff --git a/qt/aqt/forms/dconf.ui b/qt/aqt/forms/dconf.ui index b2ce0f719..78480251f 100644 --- a/qt/aqt/forms/dconf.ui +++ b/qt/aqt/forms/dconf.ui @@ -6,7 +6,7 @@ 0 0 - 623 + 638 514 @@ -670,6 +670,16 @@ + + + + DECK_CONFIG_DESCRIPTION_MARKDOWN_HINT + + + DECK_CONFIG_DESCRIPTION_MARKDOWN + + + diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 7bb8b57e6..eb6c4925a 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -185,6 +185,8 @@ class Overview: desc += " " + tr(TR.STUDYING_DELETING_THIS_DECK_FROM_THE_DECK) else: desc = deck.get("desc", "") + if deck.get("md", False): + desc = self.mw.col.render_markdown(desc) if not desc: return "

" if deck["dyn"]: diff --git a/rslib/backend.proto b/rslib/backend.proto index 245d94adc..cdb8b27cd 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -207,11 +207,12 @@ service BackendService { rpc FullDownload(SyncAuth) returns (Empty); rpc SyncServerMethod(SyncServerMethodIn) returns (Json); - // translation/messages + // translation/messages/text manipulation rpc TranslateString(TranslateStringIn) returns (String); rpc FormatTimespan(FormatTimespanIn) returns (String); rpc I18nResources(Empty) returns (Json); + rpc RenderMarkdown(RenderMarkdownIn) returns (String); // tags @@ -303,6 +304,8 @@ message DeckCommon { // but not currently used for anything int32 learning_studied = 6; + reserved 8 to 13; + bytes other = 255; } @@ -318,6 +321,9 @@ message NormalDeck { uint32 extend_new = 2; uint32 extend_review = 3; string description = 4; + bool markdown_description = 5; + + reserved 6 to 11; } message FilteredDeck { @@ -1245,3 +1251,8 @@ message SetConfigStringIn { Config.String.Key key = 1; string value = 2; } + +message RenderMarkdownIn { + string markdown = 1; + bool sanitize = 2; +} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 265675f24..b80b16868 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -23,6 +23,7 @@ use crate::{ latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}, log, log::default_logger, + markdown::render_markdown, media::check::MediaChecker, media::sync::MediaSyncProgress, media::MediaManager, @@ -48,7 +49,7 @@ use crate::{ SyncStage, }, template::RenderedNode, - text::{escape_anki_wildcards, extract_av_tags, strip_av_tags, AVTag}, + text::{escape_anki_wildcards, extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, timestamp::TimestampSecs, types::Usn, }; @@ -1402,6 +1403,15 @@ impl BackendService for Backend { .map_err(Into::into) } + fn render_markdown(&self, input: pb::RenderMarkdownIn) -> BackendResult { + let mut text = render_markdown(&input.markdown); + if input.sanitize { + // currently no images + text = sanitize_html_no_images(&text); + } + Ok(text.into()) + } + // tags //------------------------------------------------------------------- diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index fee9160c7..06bf15378 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -39,6 +39,8 @@ impl Deck { pub fn new_normal() -> Deck { let mut norm = NormalDeck::default(); norm.config_id = 1; + // enable in the future + // norm.markdown_description = true; Deck { id: DeckID(0), @@ -95,10 +97,14 @@ impl Deck { 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) + if normal.markdown_description { + 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() + } } else { String::new() } diff --git a/rslib/src/decks/schema11.rs b/rslib/src/decks/schema11.rs index 8f89b4c96..45879dc6b 100644 --- a/rslib/src/decks/schema11.rs +++ b/rslib/src/decks/schema11.rs @@ -75,6 +75,10 @@ mod dynfix { } } +fn is_false(b: &bool) -> bool { + !b +} + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub struct DeckCommonSchema11 { #[serde(deserialize_with = "deserialize_number_from_string")] @@ -95,6 +99,8 @@ pub struct DeckCommonSchema11 { browser_collapsed: bool, #[serde(default)] desc: String, + #[serde(default, rename = "md", skip_serializing_if = "is_false")] + markdown_description: bool, #[serde(rename = "dyn")] dynamic: u8, #[serde(flatten)] @@ -218,6 +224,7 @@ impl Default for NormalDeckSchema11 { today: Default::default(), other: Default::default(), dynamic: 0, + markdown_description: false, }, conf: 1, extend_new: 0, @@ -292,6 +299,7 @@ impl From for NormalDeck { config_id: deck.conf, extend_new: deck.extend_new.max(0) as u32, extend_review: deck.extend_rev.max(0) as u32, + markdown_description: deck.common.markdown_description, description: deck.common.desc, } } @@ -365,6 +373,10 @@ impl From for DeckCommonSchema11 { } else { 0 }, + markdown_description: match &deck.kind { + DeckKind::Normal(n) => n.markdown_description, + DeckKind::Filtered(_) => false, + }, desc: match deck.kind { DeckKind::Normal(n) => n.description, DeckKind::Filtered(_) => String::new(),