diff --git a/README.md b/README.md index 5d5d73e..4a164f6 100644 --- a/README.md +++ b/README.md @@ -247,4 +247,5 @@ You can configure mdbook-quiz by adding options to the `[preprocessor.quiz]` sec * `fullscreen` (boolean): If true, then a quiz will take up the web page's full screen during use. * `cache-answers` (boolean): If true, then the user's answers will be saved in their browser's `localStorage`. Then the quiz will show the user's answers even after they reload the page. -* `more-words` (path): An optional path to a `.dic` file that adds valid words to the spellchecker. +* `spellcheck` (boolean): If true, then run a spellchecker on all Markdown strings. +* `more-words` (path): An optional path to a `.dic` file that adds valid words to the spellchecker. You can find a base dictionary for each language in [wooorm/dictionaries](https://github.com/wooorm/dictionaries/tree/main/dictionaries). You can find documentation about how to write a `.dic` file in [this blog post](https://typethinker.blogspot.com/2008/02/fun-with-aspell-word-lists.html). diff --git a/crates/mdbook-quiz-validate/src/impls/markdown.rs b/crates/mdbook-quiz-validate/src/impls/markdown.rs index f5cd1fa..abe97a3 100644 --- a/crates/mdbook-quiz-validate/src/impls/markdown.rs +++ b/crates/mdbook-quiz-validate/src/impls/markdown.rs @@ -29,36 +29,38 @@ impl Validate for Markdown { nodes } - let nodes = collect_nodes(&root); - let dict = crate::spellcheck::dictionary(); - let open_quote = &cx.contents()[value.start()..]; - let quote_size = if let Some(next) = open_quote.strip_prefix(r#"""""#) { - if next.starts_with('\n') { - 4 + if cx.spellcheck { + let nodes = collect_nodes(&root); + let dict = crate::spellcheck::dictionary(); + let open_quote = &cx.contents()[value.start()..]; + let quote_size = if let Some(next) = open_quote.strip_prefix(r#"""""#) { + if next.starts_with('\n') { + 4 + } else { + 3 + } } else { - 3 - } - } else { - 1 - }; - let base = value.start() + quote_size; - for node in nodes { - if let (Node::Text(text), Some(pos)) = (node, node.position()) { - let errors = dict - .check_indices(&text.value) - .filter(|(_, s)| *s != "-") - .filter(|(_, s)| s.parse::().is_err()) - .filter(|(_, s)| s.parse::().is_err()); - for (idx, substr) in errors { - // base: location of string literal in TOML file - // pos.start.offset: location of markdown text node in the string - // idx: location of error in text node - let span = (base + pos.start.offset + idx, substr.len()); - let error = SpellingError { - word: substr.to_string(), - span: span.into(), - }; - cx.warning(error); + 1 + }; + let base = value.start() + quote_size; + for node in nodes { + if let (Node::Text(text), Some(pos)) = (node, node.position()) { + let errors = dict + .check_indices(&text.value) + .filter(|(_, s)| *s != "-") + .filter(|(_, s)| s.parse::().is_err()) + .filter(|(_, s)| s.parse::().is_err()); + for (idx, substr) in errors { + // base: location of string literal in TOML file + // pos.start.offset: location of markdown text node in the string + // idx: location of error in text node + let span = (base + pos.start.offset + idx, substr.len()); + let error = SpellingError { + word: substr.to_string(), + span: span.into(), + }; + cx.warning(error); + } } } } diff --git a/crates/mdbook-quiz-validate/src/lib.rs b/crates/mdbook-quiz-validate/src/lib.rs index 4653421..9dfef88 100644 --- a/crates/mdbook-quiz-validate/src/lib.rs +++ b/crates/mdbook-quiz-validate/src/lib.rs @@ -35,15 +35,17 @@ pub(crate) struct ValidationContext { path: PathBuf, contents: String, ids: IdSet, + spellcheck: bool, } impl ValidationContext { - pub fn new(path: &Path, contents: &str, ids: IdSet) -> Self { + pub fn new(path: &Path, contents: &str, ids: IdSet, spellcheck: bool) -> Self { ValidationContext { diagnostics: Default::default(), path: path.to_owned(), contents: contents.to_owned(), ids, + spellcheck, } } @@ -147,8 +149,8 @@ struct ParseError { } /// Runs validation on a quiz with TOML-format `contents` at `path` under the ID set `ids`. -pub fn validate(path: &Path, contents: &str, ids: &IdSet) -> anyhow::Result<()> { - let mut cx = ValidationContext::new(path, contents, Arc::clone(ids)); +pub fn validate(path: &Path, contents: &str, ids: &IdSet, spellcheck: bool) -> anyhow::Result<()> { + let mut cx = ValidationContext::new(path, contents, Arc::clone(ids), spellcheck); let parse_result = toml::from_str::(contents); match parse_result { @@ -179,7 +181,7 @@ pub fn validate(path: &Path, contents: &str, ids: &IdSet) -> anyhow::Result<()> #[cfg(test)] pub(crate) fn harness(contents: &str) -> anyhow::Result<()> { - validate(Path::new("dummy.rs"), contents, &IdSet::default()) + validate(Path::new("dummy.rs"), contents, &IdSet::default(), true) } #[cfg(test)] diff --git a/crates/mdbook-quiz/src/main.rs b/crates/mdbook-quiz/src/main.rs index 4f41924..1642ccb 100644 --- a/crates/mdbook-quiz/src/main.rs +++ b/crates/mdbook-quiz/src/main.rs @@ -45,6 +45,11 @@ struct QuizConfig { /// Sets the default language for syntax highlighting. default_language: Option, + /// If true, then run a spellchecker on all Markdown strings. + /// + /// You can add a custom dictionary via the `more-words` key. + spellcheck: Option, + /// Path to a .dic file containing words to include in the spellcheck dictionary. more_words: Option, @@ -121,7 +126,12 @@ impl QuizPreprocessor { let mut content_toml = fs::read_to_string(&quiz_path_abs) .with_context(|| format!("Failed to read quiz file: {}", quiz_path_abs.display()))?; - mdbook_quiz_validate::validate(&quiz_path_abs, &content_toml, &self.question_ids)?; + mdbook_quiz_validate::validate( + &quiz_path_abs, + &content_toml, + &self.question_ids, + self.config.spellcheck.unwrap_or(false), + )?; let changed = self.auto_id(&quiz_path_abs, &content_toml)?; if changed { @@ -186,6 +196,7 @@ impl SimplePreprocessor for QuizPreprocessor { more_words: config_toml .get("more-words") .map(|value| value.as_str().unwrap().into()), + spellcheck: parse_bool("spellcheck"), dev_mode, };