diff --git a/README.md b/README.md
index 24823932..0e89eda6 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,8 @@ 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 |
+| `markdown-heading-first` | No content appears between preamble and first heading. |
+| `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. |
diff --git a/docs/markdown-heading-first/index.html b/docs/markdown-heading-first/index.html
new file mode 100644
index 00000000..d0928567
--- /dev/null
+++ b/docs/markdown-heading-first/index.html
@@ -0,0 +1,42 @@
+
+
+
+
+ markdown-heading-first
+
+
+
+
+
+
markdown-heading-first
+
+ No content appears between preamble and first heading.
+
+
+
+
Examples
+
+
+error[markdown-heading-first]: Nothing is permitted between the preamble and the first heading
+ --> input.md
+ |
+12 | This proposal describes the introduction in clients of a controlled gas limit increase strategy to determine the gas limit of a spec...
+ |
+
+
+
Explanation
+
+
+ markdown-heading-first ensures that no content
+ appears before the first heading.
+
+
+
+ It is improper form to put text/markdown outside of a section.
+ Such text cannot be referred to in a URL (eg. #Section-Title),
+ nor does it appear in the table of contents.
+
+
+
+
+
diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs
index 7e9a2784..8f445fab 100644
--- a/eipw-lint/src/lib.rs
+++ b/eipw-lint/src/lib.rs
@@ -471,6 +471,10 @@ pub fn default_lints_enum() -> impl Iterator {
sections: markdown::SectionRequired,
},
MarkdownHeadingsSpace(markdown::HeadingsSpace),
+ MarkdownHeadingFirst(markdown::HeadingFirst),
}
impl DefaultLint
@@ -114,6 +115,7 @@ where
Self::MarkdownSectionOrder { sections } => Box::new(sections),
Self::MarkdownSectionRequired { sections } => Box::new(sections),
Self::MarkdownHeadingsSpace(l) => Box::new(l),
+ Self::MarkdownHeadingFirst(l) => Box::new(l),
}
}
}
@@ -154,6 +156,7 @@ where
Self::MarkdownSectionOrder { sections } => sections,
Self::MarkdownSectionRequired { sections } => sections,
Self::MarkdownHeadingsSpace(l) => l,
+ Self::MarkdownHeadingFirst(l) => l,
}
}
}
@@ -290,6 +293,7 @@ where
sections: markdown::SectionRequired(sections.0.iter().map(AsRef::as_ref).collect()),
},
Self::MarkdownHeadingsSpace(l) => DefaultLint::MarkdownHeadingsSpace(l.clone()),
+ Self::MarkdownHeadingFirst(l) => DefaultLint::MarkdownHeadingFirst(l.clone()),
}
}
}
diff --git a/eipw-lint/src/lints/markdown.rs b/eipw-lint/src/lints/markdown.rs
index 264329e1..41d07591 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 heading_first;
pub mod headings_space;
pub mod html_comments;
pub mod json_schema;
@@ -16,6 +17,7 @@ pub mod relative_links;
pub mod section_order;
pub mod section_required;
+pub use self::heading_first::HeadingFirst;
pub use self::headings_space::HeadingsSpace;
pub use self::html_comments::HtmlComments;
pub use self::json_schema::JsonSchema;
diff --git a/eipw-lint/src/lints/markdown/heading_first.rs b/eipw-lint/src/lints/markdown/heading_first.rs
new file mode 100644
index 00000000..52adcc94
--- /dev/null
+++ b/eipw-lint/src/lints/markdown/heading_first.rs
@@ -0,0 +1,49 @@
+/*
+ * 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 comrak::nodes::{Ast, NodeValue};
+
+use crate::lints::{Error, Lint};
+use crate::SnippetExt;
+
+use eipw_snippets::Snippet;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HeadingFirst;
+
+impl Lint for HeadingFirst {
+ fn lint<'a>(&self, slug: &'a str, ctx: &crate::lints::Context<'a, '_>) -> Result<(), Error> {
+ let second = match ctx.body().descendants().nth(1) {
+ Some(el) => el.data.borrow().to_owned(),
+ None => return Ok(()),
+ };
+
+ let ast = match second {
+ Ast {
+ value: NodeValue::Heading(_),
+ ..
+ } => return Ok(()),
+ other => other,
+ };
+
+ let source = ctx.line(ast.sourcepos.start.line);
+ ctx.report(
+ ctx.annotation_level()
+ .title("Nothing is permitted between the preamble and the first heading")
+ .id(slug)
+ .snippet(
+ Snippet::source(source)
+ .origin_opt(ctx.origin())
+ .line_start(ast.sourcepos.start.line)
+ .fold(false),
+ ),
+ )?;
+
+ Ok(())
+ }
+}
diff --git a/eipw-lint/tests/lint_markdown_heading_first.rs b/eipw-lint/tests/lint_markdown_heading_first.rs
new file mode 100644
index 00000000..b6d422f3
--- /dev/null
+++ b/eipw-lint/tests/lint_markdown_heading_first.rs
@@ -0,0 +1,69 @@
+/*
+ * 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::HeadingFirst, reporters::Text, Linter};
+
+#[tokio::test]
+async fn invalid_eip() {
+ let src = r#"---
+eip: 1234
+---
+
+This is some text that appears before the first heading. Authors sometimes try
+to write an introduction or preface to their proposal here. We don't want to allow
+this.
+
+## Abstract
+
+After the "Abstract" heading is the first place we want to allow text."#;
+
+ let reports = Linter::>::default()
+ .clear_lints()
+ .deny("markdown-heading-first", HeadingFirst {})
+ .check_slice(None, src)
+ .run()
+ .await
+ .unwrap()
+ .into_inner();
+
+ assert_eq!(
+ reports,
+ r#"error[markdown-heading-first]: Nothing is permitted between the preamble and the first heading
+ |
+5 | This is some text that appears before the first heading. Authors sometimes try
+ |
+"#
+ );
+}
+
+#[tokio::test]
+async fn valid_eip() {
+ let src = r#"---
+eip: 100
+title: Change difficulty adjustment to target mean block time including uncles
+author: Vitalik Buterin (@vbuterin)
+type: Standards Track
+category: Core
+status: Final
+created: 2016-04-28
+---
+
+### Specification
+
+Currently, the formula to compute the difficulty of a block includes the following logic:
+"#;
+
+ let reports = Linter::>::default()
+ .clear_lints()
+ .deny("markdown-heading-first", HeadingFirst {})
+ .check_slice(None, src)
+ .run()
+ .await
+ .unwrap()
+ .into_inner();
+
+ assert_eq!(reports, "");
+}