From 85b072cc7ff4d11d84d293a90cd330cbe18e8ff8 Mon Sep 17 00:00:00 2001 From: prampey Date: Sun, 17 Sep 2023 22:19:18 -0400 Subject: [PATCH 1/8] Add HeadingsSpace lint - Finds and reports texts starting with '#' --- eipw-lint/src/lib.rs | 4 + eipw-lint/src/lints/known_lints.rs | 4 + eipw-lint/src/lints/markdown.rs | 2 + .../src/lints/markdown/headings_space.rs | 73 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 eipw-lint/src/lints/markdown/headings_space.rs diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index 8b24d368..6ec5fc59 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -457,6 +457,10 @@ pub fn default_lints_enum() -> impl Iterator { MarkdownSectionRequired { sections: markdown::SectionRequired, }, + MarkdownHeadingsSpace(markdown::HeadingsSpace), } impl DefaultLint @@ -110,6 +111,7 @@ where Self::MarkdownRelativeLinks(l) => Box::new(l), Self::MarkdownSectionOrder { sections } => Box::new(sections), Self::MarkdownSectionRequired { sections } => Box::new(sections), + Self::MarkdownHeadingsSpace(l) => Box::new(l), } } } @@ -148,6 +150,7 @@ where Self::MarkdownRelativeLinks(l) => l, Self::MarkdownSectionOrder { sections } => sections, Self::MarkdownSectionRequired { sections } => sections, + Self::MarkdownHeadingsSpace(l) => l, } } } @@ -278,6 +281,7 @@ where Self::MarkdownSectionRequired { sections } => DefaultLint::MarkdownSectionRequired { sections: markdown::SectionRequired(sections.0.iter().map(AsRef::as_ref).collect()), }, + Self::MarkdownHeadingsSpace(l) => DefaultLint::MarkdownHeadingsSpace(l.clone()), } } } diff --git a/eipw-lint/src/lints/markdown.rs b/eipw-lint/src/lints/markdown.rs index d9a91117..7f1ca018 100644 --- a/eipw-lint/src/lints/markdown.rs +++ b/eipw-lint/src/lints/markdown.rs @@ -4,6 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +pub mod headings_space; pub mod html_comments; pub mod json_schema; pub mod link_first; @@ -14,6 +15,7 @@ pub mod relative_links; pub mod section_order; pub mod section_required; +pub use self::headings_space::HeadingsSpace; pub use self::html_comments::HtmlComments; pub use self::json_schema::JsonSchema; pub use self::link_first::LinkFirst; diff --git a/eipw-lint/src/lints/markdown/headings_space.rs b/eipw-lint/src/lints/markdown/headings_space.rs new file mode 100644 index 00000000..bff108a2 --- /dev/null +++ b/eipw-lint/src/lints/markdown/headings_space.rs @@ -0,0 +1,73 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use annotate_snippets::snippet::{Annotation, Slice, Snippet}; + +use comrak::nodes::Ast; +use comrak::nodes::NodeValue; + +use crate::lints::{Context, Error, Lint}; +use crate::reporters::text; +use crate::tree::{self, Next, TraverseExt}; + +use regex::{Regex, RegexSet}; + +use scraper::node::Node as HtmlNode; +use scraper::Html; + +use serde::{Deserialize, Serialize}; + +use snafu::Snafu; + +use std::fmt::{Debug, Display}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct HeadingsSpace; + +impl Lint for HeadingsSpace { + fn lint<'a, 'b>(&self, slug: &'a str, ctx: &Context<'a, 'b>) -> Result<(), Error> { + // Collect all headings. + let false_headings: Vec<_> = ctx + .body() + .descendants() + .filter_map(|node| match &*node.data.borrow() { + Ast { value: NodeValue::Text(text), .. } => { + if text.starts_with("#") { + Some((text.clone(), node.data.borrow().sourcepos.start.line)) + } + else { + None + } + }, + _ => None, + }).collect(); + + let slices = false_headings.iter() + .map(|(text, line_start)| { + Slice{ + line_start: line_start.clone(), + origin: ctx.origin(), + source: text, + fold: false, + annotations: vec![], + } + }) + .collect(); + + // Print all false headings + ctx.report(Snippet { + title: Some(Annotation { + id: Some(slug), + annotation_type: ctx.annotation_type(), + label: Some("Space missing in header"), + }), + footer: vec![], + slices, + opt: Default::default(), + })?; + Ok(()) + } +} From 8d51feb6db57f7f3be0efa1183e9857aef32b2d0 Mon Sep 17 00:00:00 2001 From: prampey Date: Sun, 17 Sep 2023 22:46:46 -0400 Subject: [PATCH 2/8] Add annotation --- .../src/lints/markdown/headings_space.rs | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/eipw-lint/src/lints/markdown/headings_space.rs b/eipw-lint/src/lints/markdown/headings_space.rs index bff108a2..92bdf054 100644 --- a/eipw-lint/src/lints/markdown/headings_space.rs +++ b/eipw-lint/src/lints/markdown/headings_space.rs @@ -6,68 +6,68 @@ use annotate_snippets::snippet::{Annotation, Slice, Snippet}; +use annotate_snippets::snippet::SourceAnnotation; use comrak::nodes::Ast; use comrak::nodes::NodeValue; use crate::lints::{Context, Error, Lint}; -use crate::reporters::text; -use crate::tree::{self, Next, TraverseExt}; - -use regex::{Regex, RegexSet}; - -use scraper::node::Node as HtmlNode; -use scraper::Html; use serde::{Deserialize, Serialize}; -use snafu::Snafu; - -use std::fmt::{Debug, Display}; +use std::fmt::Debug; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct HeadingsSpace; impl Lint for HeadingsSpace { fn lint<'a, 'b>(&self, slug: &'a str, ctx: &Context<'a, 'b>) -> Result<(), Error> { - // Collect all headings. let false_headings: Vec<_> = ctx - .body() - .descendants() - .filter_map(|node| match &*node.data.borrow() { - Ast { value: NodeValue::Text(text), .. } => { - if text.starts_with("#") { - Some((text.clone(), node.data.borrow().sourcepos.start.line)) + .body() + .descendants() + .filter_map(|node| match &*node.data.borrow() { + // Collect all Text nodes as Markdown does not recognise headings without space + Ast { + value: NodeValue::Text(text), + .. + } => { + if text.starts_with("#") { + Some((text.clone(), node.data.borrow().sourcepos.start.line)) + } else { + None + } } - else { - None - } - }, - _ => None, - }).collect(); + _ => None, + }) + .collect(); - let slices = false_headings.iter() - .map(|(text, line_start)| { - Slice{ - line_start: line_start.clone(), - origin: ctx.origin(), - source: text, - fold: false, - annotations: vec![], - } - }) - .collect(); + let slices = false_headings + .iter() + .map(|(text, line_start)| { + let error_idx = text.rfind("#").unwrap(); + Slice { + line_start: line_start.clone(), + origin: ctx.origin(), + source: text, + fold: false, + annotations: vec![SourceAnnotation { + annotation_type: ctx.annotation_type(), + label: "space required here", + range: (error_idx, error_idx + 1), + }], + } + }) + .collect(); - // Print all false headings ctx.report(Snippet { title: Some(Annotation { - id: Some(slug), - annotation_type: ctx.annotation_type(), - label: Some("Space missing in header"), - }), - footer: vec![], - slices, - opt: Default::default(), - })?; + id: Some(slug), + annotation_type: ctx.annotation_type(), + label: Some("Space missing in header"), + }), + footer: vec![], + slices, + opt: Default::default(), + })?; Ok(()) } } From 0ba00ceceb107dccd4d9d16ca346268cf8c8c7f6 Mon Sep 17 00:00:00 2001 From: prampey Date: Tue, 19 Sep 2023 18:00:20 -0400 Subject: [PATCH 3/8] Use regex to match false headings --- eipw-lint/src/lints/markdown/headings_space.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/eipw-lint/src/lints/markdown/headings_space.rs b/eipw-lint/src/lints/markdown/headings_space.rs index 92bdf054..cebb4a0f 100644 --- a/eipw-lint/src/lints/markdown/headings_space.rs +++ b/eipw-lint/src/lints/markdown/headings_space.rs @@ -9,6 +9,7 @@ use annotate_snippets::snippet::{Annotation, Slice, Snippet}; use annotate_snippets::snippet::SourceAnnotation; use comrak::nodes::Ast; use comrak::nodes::NodeValue; +use regex::Regex; use crate::lints::{Context, Error, Lint}; @@ -21,17 +22,21 @@ pub struct HeadingsSpace; impl Lint for HeadingsSpace { fn lint<'a, 'b>(&self, slug: &'a str, ctx: &Context<'a, 'b>) -> Result<(), Error> { + // Match for text nodes starting with with 1 to 6 '#' chars + // as Markdown does not recognise headings without space + let heading_pattern = Regex::new("^#{1,6}").unwrap(); let false_headings: Vec<_> = ctx .body() .descendants() .filter_map(|node| match &*node.data.borrow() { - // Collect all Text nodes as Markdown does not recognise headings without space + // Collect all matching Text nodes Ast { value: NodeValue::Text(text), .. } => { - if text.starts_with("#") { - Some((text.clone(), node.data.borrow().sourcepos.start.line)) + if let Some(matched_text) = heading_pattern.find(text) { + let heading_level = matched_text.len(); + Some((text.clone(), node.data.borrow().sourcepos.start.line, heading_level)) } else { None } @@ -42,8 +47,7 @@ impl Lint for HeadingsSpace { let slices = false_headings .iter() - .map(|(text, line_start)| { - let error_idx = text.rfind("#").unwrap(); + .map(|(text, line_start, heading_level)| { Slice { line_start: line_start.clone(), origin: ctx.origin(), @@ -52,7 +56,7 @@ impl Lint for HeadingsSpace { annotations: vec![SourceAnnotation { annotation_type: ctx.annotation_type(), label: "space required here", - range: (error_idx, error_idx + 1), + range: (*heading_level - 1, *heading_level), }], } }) From ace9daa5032af662fb9f7eec7f9acc43168cfd7b Mon Sep 17 00:00:00 2001 From: prampey Date: Tue, 19 Sep 2023 18:44:23 -0400 Subject: [PATCH 4/8] Add tests --- .../src/lints/markdown/headings_space.rs | 40 ++++++----- .../tests/lint_markdown_headings_space.rs | 71 +++++++++++++++++++ 2 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 eipw-lint/tests/lint_markdown_headings_space.rs diff --git a/eipw-lint/src/lints/markdown/headings_space.rs b/eipw-lint/src/lints/markdown/headings_space.rs index cebb4a0f..28b3b2b4 100644 --- a/eipw-lint/src/lints/markdown/headings_space.rs +++ b/eipw-lint/src/lints/markdown/headings_space.rs @@ -22,21 +22,25 @@ pub struct HeadingsSpace; impl Lint for HeadingsSpace { fn lint<'a, 'b>(&self, slug: &'a str, ctx: &Context<'a, 'b>) -> Result<(), Error> { - // Match for text nodes starting with with 1 to 6 '#' chars - // as Markdown does not recognise headings without space + // Match for text nodes starting with leading '#' chars (upto 6) + // Markdown does not recognise these nodes as valid Headings without the space let heading_pattern = Regex::new("^#{1,6}").unwrap(); - let false_headings: Vec<_> = ctx + let invalid_headings: Vec<_> = ctx .body() .descendants() .filter_map(|node| match &*node.data.borrow() { - // Collect all matching Text nodes + // Collect all matching Text nodes Ast { value: NodeValue::Text(text), .. } => { if let Some(matched_text) = heading_pattern.find(text) { - let heading_level = matched_text.len(); - Some((text.clone(), node.data.borrow().sourcepos.start.line, heading_level)) + let heading_level = matched_text.end(); + Some(( + text.clone(), + node.data.borrow().sourcepos.start.line, + heading_level, + )) } else { None } @@ -45,20 +49,18 @@ impl Lint for HeadingsSpace { }) .collect(); - let slices = false_headings + let slices = invalid_headings .iter() - .map(|(text, line_start, heading_level)| { - Slice { - line_start: line_start.clone(), - origin: ctx.origin(), - source: text, - fold: false, - annotations: vec![SourceAnnotation { - annotation_type: ctx.annotation_type(), - label: "space required here", - range: (*heading_level - 1, *heading_level), - }], - } + .map(|(text, line_start, heading_level)| Slice { + line_start: line_start.clone(), + origin: ctx.origin(), + source: text, + fold: false, + annotations: vec![SourceAnnotation { + annotation_type: ctx.annotation_type(), + label: "space required here", + range: (*heading_level - 1, *heading_level), + }], }) .collect(); diff --git a/eipw-lint/tests/lint_markdown_headings_space.rs b/eipw-lint/tests/lint_markdown_headings_space.rs new file mode 100644 index 00000000..61454bc6 --- /dev/null +++ b/eipw-lint/tests/lint_markdown_headings_space.rs @@ -0,0 +1,71 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use eipw_lint::lints::markdown::HeadingsSpace; +use eipw_lint::reporters::Text; +use eipw_lint::Linter; + +#[tokio::test] +async fn normal_headings() { + let src = r#"--- +header: value1 +--- + +##Banana +####Mango +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-headings-space", HeadingsSpace{}) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-headings-space]: Space missing in header + | +5 | ##Banana + | ^ space required here + | +6 | ####Mango + | ^ space required here + | +"# + ); +} + +#[tokio::test] +async fn abnormal_heading() { + let src = r#"--- +header: value1 +--- + +##B#an#ana +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-headings-space", HeadingsSpace{}) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-headings-space]: Space missing in header + | +5 | ##B#an#ana + | ^ space required here + | +"# + ); +} \ No newline at end of file From e89a6bd2bc2dc9c1aafc25621cde188524f7605b Mon Sep 17 00:00:00 2001 From: prampey Date: Tue, 19 Sep 2023 18:52:35 -0400 Subject: [PATCH 5/8] edit README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4c8a144b..89143606 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ error[preamble-order]: preamble header `description` must come after `title` | `markdown-refs` | ERCs are referenced using ERC-X, while other proposals use EIP-X. | | `markdown-rel-links` | All URLs in the page are relative. | | `markdown-req-section` | Required sections are present in the body of the proposal. | +| `markdown-headings-space` | Headers have a space after the leading '#' characters | | `preamble-author` | The author header is correctly formatted, and there is at least one GitHub user listed. | | `preamble-date-created` | The `created` header is a date. | | `preamble-date-last-call-deadline` | The `last-call-deadline` header is a date. | From aa0865fae2d803bed2e8b377218674c91606e223 Mon Sep 17 00:00:00 2001 From: prampey Date: Tue, 19 Sep 2023 19:02:37 -0400 Subject: [PATCH 6/8] Run cargo fmt --- .../src/lints/markdown/headings_space.rs | 39 ++++++++++--------- .../tests/lint_markdown_headings_space.rs | 6 +-- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/eipw-lint/src/lints/markdown/headings_space.rs b/eipw-lint/src/lints/markdown/headings_space.rs index 28b3b2b4..5bfef0f3 100644 --- a/eipw-lint/src/lints/markdown/headings_space.rs +++ b/eipw-lint/src/lints/markdown/headings_space.rs @@ -21,7 +21,7 @@ use std::fmt::Debug; pub struct HeadingsSpace; impl Lint for HeadingsSpace { - fn lint<'a, 'b>(&self, slug: &'a str, ctx: &Context<'a, 'b>) -> Result<(), Error> { + fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { // Match for text nodes starting with leading '#' chars (upto 6) // Markdown does not recognise these nodes as valid Headings without the space let heading_pattern = Regex::new("^#{1,6}").unwrap(); @@ -36,11 +36,11 @@ impl Lint for HeadingsSpace { } => { if let Some(matched_text) = heading_pattern.find(text) { let heading_level = matched_text.end(); - Some(( - text.clone(), - node.data.borrow().sourcepos.start.line, - heading_level, - )) + Some(( + text.clone(), + node.data.borrow().sourcepos.start.line, + heading_level, + )) } else { None } @@ -49,10 +49,10 @@ impl Lint for HeadingsSpace { }) .collect(); - let slices = invalid_headings + let slices: Vec<_> = invalid_headings .iter() .map(|(text, line_start, heading_level)| Slice { - line_start: line_start.clone(), + line_start: *line_start, origin: ctx.origin(), source: text, fold: false, @@ -64,16 +64,19 @@ impl Lint for HeadingsSpace { }) .collect(); - ctx.report(Snippet { - title: Some(Annotation { - id: Some(slug), - annotation_type: ctx.annotation_type(), - label: Some("Space missing in header"), - }), - footer: vec![], - slices, - opt: Default::default(), - })?; + if !slices.is_empty() { + ctx.report(Snippet { + title: Some(Annotation { + id: Some(slug), + annotation_type: ctx.annotation_type(), + label: Some("Space missing in header"), + }), + footer: vec![], + slices, + opt: Default::default(), + })?; + } + Ok(()) } } diff --git a/eipw-lint/tests/lint_markdown_headings_space.rs b/eipw-lint/tests/lint_markdown_headings_space.rs index 61454bc6..3c970463 100644 --- a/eipw-lint/tests/lint_markdown_headings_space.rs +++ b/eipw-lint/tests/lint_markdown_headings_space.rs @@ -20,7 +20,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-headings-space", HeadingsSpace{}) + .deny("markdown-headings-space", HeadingsSpace {}) .check_slice(None, src) .run() .await @@ -52,7 +52,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-headings-space", HeadingsSpace{}) + .deny("markdown-headings-space", HeadingsSpace {}) .check_slice(None, src) .run() .await @@ -68,4 +68,4 @@ header: value1 | "# ); -} \ No newline at end of file +} From 51ab61232f3b550f2d88172b33fec0d695ba5ea4 Mon Sep 17 00:00:00 2001 From: prampey Date: Sun, 21 Jan 2024 13:23:23 -0500 Subject: [PATCH 7/8] Match nodes at start of line - Add docs and test --- docs/markdown-headings-space/index.html | 43 +++++++++++++++++++ .../src/lints/markdown/headings_space.rs | 8 +++- .../tests/lint_markdown_headings_space.rs | 24 +++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 docs/markdown-headings-space/index.html diff --git a/docs/markdown-headings-space/index.html b/docs/markdown-headings-space/index.html new file mode 100644 index 00000000..78377616 --- /dev/null +++ b/docs/markdown-headings-space/index.html @@ -0,0 +1,43 @@ + + + + + markdown-headings-space + + + + +
+

markdown-headings-space

+

+ Markdown headers must have a space after the hash characters. +

+ +
+

Examples

+ +
error[markdown-headings-space]: Space missing in header
+  |
+5 | ##Banana
+  |  ^ space required here
+  |
+6 | ####Mango
+  |    ^ space required here
+  |
+			
+
+
+

Explanation

+ +

+ markdown-headings-space ensures that all headers + have a space after the hash characters so that they are valid. +

+ +

+ The lack of a space makes headers invalid, and they will not be rendered as such. +

+
+
+ + diff --git a/eipw-lint/src/lints/markdown/headings_space.rs b/eipw-lint/src/lints/markdown/headings_space.rs index 5bfef0f3..b72bc12b 100644 --- a/eipw-lint/src/lints/markdown/headings_space.rs +++ b/eipw-lint/src/lints/markdown/headings_space.rs @@ -7,8 +7,7 @@ use annotate_snippets::snippet::{Annotation, Slice, Snippet}; use annotate_snippets::snippet::SourceAnnotation; -use comrak::nodes::Ast; -use comrak::nodes::NodeValue; +use comrak::nodes::{Ast, LineColumn, NodeValue, Sourcepos}; use regex::Regex; use crate::lints::{Context, Error, Lint}; @@ -32,6 +31,11 @@ impl Lint for HeadingsSpace { // Collect all matching Text nodes Ast { value: NodeValue::Text(text), + sourcepos: + Sourcepos { + start: LineColumn { column: 1, .. }, // Only match text nodes at the start of the line + .. + }, .. } => { if let Some(matched_text) = heading_pattern.find(text) { diff --git a/eipw-lint/tests/lint_markdown_headings_space.rs b/eipw-lint/tests/lint_markdown_headings_space.rs index 3c970463..c60c00fb 100644 --- a/eipw-lint/tests/lint_markdown_headings_space.rs +++ b/eipw-lint/tests/lint_markdown_headings_space.rs @@ -69,3 +69,27 @@ header: value1 "# ); } + +#[tokio::test] +async fn not_headings() { + let src_str = r#"--- +header: value1 +--- + +*Hello*#world +`#world` +"#; + + println!("{}", src_str); + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-headings-space", HeadingsSpace {}) + .check_slice(None, src_str) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!(reports, ""); +} From 938c3caf31677d6fa364f6ad7ec6283ac83f4d5d Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Sun, 21 Jan 2024 14:19:27 -0500 Subject: [PATCH 8/8] Update lint_markdown_headings_space.rs --- eipw-lint/tests/lint_markdown_headings_space.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/eipw-lint/tests/lint_markdown_headings_space.rs b/eipw-lint/tests/lint_markdown_headings_space.rs index c60c00fb..37e7618e 100644 --- a/eipw-lint/tests/lint_markdown_headings_space.rs +++ b/eipw-lint/tests/lint_markdown_headings_space.rs @@ -80,8 +80,6 @@ header: value1 `#world` "#; - println!("{}", src_str); - let reports = Linter::>::default() .clear_lints() .deny("markdown-headings-space", HeadingsSpace {})