diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index 9289a3e13c3..9acffaed0a2 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -13,5 +13,5 @@ jobs:
with:
check_filenames: true
check_hidden: true
- skip: ./.git,*.png,*.csv,./archive,./legacy_submissions
+ skip: ./.git,./package.json,./package-lock.json,*.png,*.csv,./archive,./legacy_submissions
ignore_words_file: './.codespellignore'
diff --git a/.github/workflows/markdownlint.yml b/.github/workflows/markdownlint.yml
index 2e0b3deb895..76fe0052bce 100644
--- a/.github/workflows/markdownlint.yml
+++ b/.github/workflows/markdownlint.yml
@@ -6,7 +6,7 @@ on:
- '!*'
- '!archive/**'
- '!templates/**'
- - '!markdownlint/docs/**'
+ - '!markdownlint/**'
- '!.github/**'
jobs:
@@ -23,7 +23,7 @@ jobs:
!*
!archive/**
!templates/**
- !markdownlint/docs/**
+ !markdownlint/**
!.github/**
separator: ','
- uses: DavidAnson/markdownlint-cli2-action@v14
diff --git a/.github/workflows/markdownlint_testing.yml b/.github/workflows/markdownlint_testing.yml
new file mode 100644
index 00000000000..e3322423ca5
--- /dev/null
+++ b/.github/workflows/markdownlint_testing.yml
@@ -0,0 +1,15 @@
+name: MarkdownLint
+on:
+ pull_request:
+ paths:
+ - 'markdownlint/**'
+ - '!markdownlint/docs/**'
+
+jobs:
+ test_custom_rules:
+ name: Test custom rules
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - run: npm install
+ - run: npm run test
diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc
index e3b03c6969e..efdcdd66191 100644
--- a/.markdownlint-cli2.jsonc
+++ b/.markdownlint-cli2.jsonc
@@ -82,7 +82,10 @@
},
// link-fragments
// Disabled as it uses a different heading conversion algo to the TOP website
- "MD051": false
+ "MD051": false,
+ // descriptive-link-text
+ // Disabled and overridden by TOP001 custom rule
+ "MD059": false
},
// Custom rules specific to the project
// Docs for each rule can be found in `./markdownlint/docs`
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index feaff20bee9..00f30433d7b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,6 +26,9 @@ Before submitting a PR for any lesson, you must also use our [Lesson Preview Too
## Curriculum Linting
+> [!NOTE]
+> For information about contributing to our custom linting rules, please read the [custom markdown linting contributing guide](./markdownlint/docs/README.md).
+
To help enforce the layout specified in our layout style guide, we use [markdownlint](https://github.com/DavidAnson/markdownlint). Whenever a PR is opened or has updates made to it, a workflow will run to check any files changed in the PR against common rules as well as custom rules specific to TOP. To make the workflow easier, we also strongly suggest that users who have a local clone run this linter locally before committing any changes. There are 2 ways you can do so:
1. Install the [Markdownlint VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint). This plugin will automatically pick up our markdownlint configuration and flag issues with a squiggly underline.
diff --git a/markdownlint/TOP001_descriptiveLinkTextLabels/TOP001_descriptiveLinkTextLabels.js b/markdownlint/TOP001_descriptiveLinkTextLabels/TOP001_descriptiveLinkTextLabels.js
index dcff6252390..6677b3b10ba 100644
--- a/markdownlint/TOP001_descriptiveLinkTextLabels/TOP001_descriptiveLinkTextLabels.js
+++ b/markdownlint/TOP001_descriptiveLinkTextLabels/TOP001_descriptiveLinkTextLabels.js
@@ -60,7 +60,7 @@ module.exports = {
onError({
lineNumber: tokensAfterLinkOpen[0].lineNumber,
detail: containsThisOrHere
- ? `Expected text to not include the words "this" or "here". Use a more descriptive text that clearly conveys the purpose or content of the link.`
+ ? `Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.`
: `"${linkContentString}" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.`,
context: `[${linkContentString}](${linkUrl})`,
});
diff --git a/markdownlint/TOP001_descriptiveLinkTextLabels/tests/TOP001.test.js b/markdownlint/TOP001_descriptiveLinkTextLabels/tests/TOP001.test.js
new file mode 100644
index 00000000000..311121e6e31
--- /dev/null
+++ b/markdownlint/TOP001_descriptiveLinkTextLabels/tests/TOP001.test.js
@@ -0,0 +1,56 @@
+const { join } = require("node:path");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const rule = require("../TOP001_descriptiveLinkTextLabels");
+
+const pathInRepo = "markdownlint/TOP001_descriptiveLinkTextLabels/tests";
+const expected = {
+ name: "TOP001/descriptive-link-text-labels",
+ description: "Links must have descriptive text labels",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP001.md",
+ ),
+};
+
+describe("TOP001", () => {
+ it("Links to the TOP001 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it('Flags link text labels containing "this" or "here"', async () => {
+ const filePath = "./this_or_here.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:23 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[this](someURL)"]`,
+ `${errorPath}:24 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[This video](someURL)"]`,
+ `${errorPath}:25 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[here](someURL)"]`,
+ `${errorPath}:26 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[Click here](someURL)"]`,
+ `${errorPath}:27 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[This other thing](someURL)"]`,
+ `${errorPath}:28 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[This blog post about flex-grow will be flagged as a false positive, but could still be updated](someURL)"]`,
+ `${errorPath}:29 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[This will get caught](someURL)"]`,
+ `${errorPath}:29 error ${expected.name} ${expected.description} [Expected text to not include the words "this" or "here". Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[this as separate matches](someURL)"]`,
+ ]);
+ });
+
+ it("Flags blacklisted link text labels", async () => {
+ const filePath = "./blacklisted_label_text.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:23 error ${expected.name} ${expected.description} ["video" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[video](someURL)"]`,
+ `${errorPath}:24 error ${expected.name} ${expected.description} ["videos" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[videos](someURL)"]`,
+ `${errorPath}:25 error ${expected.name} ${expected.description} ["a video" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[a video](someURL)"]`,
+ `${errorPath}:26 error ${expected.name} ${expected.description} ["docs" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[docs](someURL)"]`,
+ `${errorPath}:27 error ${expected.name} ${expected.description} ["the documentation" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[the documentation](someURL)"]`,
+ `${errorPath}:28 error ${expected.name} ${expected.description} ["their documentation" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[their documentation](someURL)"]`,
+ `${errorPath}:29 error ${expected.name} ${expected.description} ["page" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[page](someURL)"]`,
+ `${errorPath}:30 error ${expected.name} ${expected.description} ["their homepage" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[their homepage](someURL)"]`,
+ `${errorPath}:31 error ${expected.name} ${expected.description} ["playlist" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[playlist](someURL)"]`,
+ `${errorPath}:32 error ${expected.name} ${expected.description} ["a playlist" is not sufficiently descriptive by itself. Use a more descriptive label that clearly conveys the purpose or content of the link.] [Context: "[a playlist](someURL)"]`,
+ ]);
+ });
+});
diff --git a/markdownlint/TOP001_descriptiveLinkTextLabels/tests/blacklisted_label_text.md b/markdownlint/TOP001_descriptiveLinkTextLabels/tests/blacklisted_label_text.md
new file mode 100644
index 00000000000..44ee828725f
--- /dev/null
+++ b/markdownlint/TOP001_descriptiveLinkTextLabels/tests/blacklisted_label_text.md
@@ -0,0 +1,52 @@
+### Introduction
+
+Text content
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### The following links should NOT be flagged
+
+[Some descriptive link text](someURL)
+[He replied](someURL)
+[With is](someURL)
+[Where](someURL)
+[Heresy](someURL)
+[Some text with the word video](someURL)
+[Some text with the words documentation, an article, docs, the docs, page, a video, articles, resource, their docs](someURL)
+
+### The following links SHOULD be flagged as blacklisted text labels
+
+[video](someURL)
+[videos](someURL)
+[a video](someURL)
+[docs](someURL)
+[the documentation](someURL)
+[their documentation](someURL)
+[page](someURL)
+[their homepage](someURL)
+[playlist](someURL)
+[a playlist](someURL)
+
+### Assignment
+
+
"]`,
+ `${errorPath}:28 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag] [Context: "
"]`,
+ `${errorPath}:35 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) after the tag] [Context: "
"]`,
+ `${errorPath}:37 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag and after the tag] [Context: "
"]`,
+ `${errorPath}:38 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag and after the tag] [Context: "
"]`,
+ `${errorPath}:40 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag] [Context: "
"]`,
+ `${errorPath}:46 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag] [Context: "
"]`,
+ `${errorPath}:50 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) after the tag] [Context: "
"]`,
+ `${errorPath}:52 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag] [Context: "
"]`,
+ `${errorPath}:54 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) after the tag] [Context: "
"]`,
+ `${errorPath}:55 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag and after the tag] [Context: "
"]`,
+ `${errorPath}:57 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag and after the tag] [Context: "
"]`,
+ `${errorPath}:58 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) before the tag] [Context: "
"]`,
+ ]);
+ });
+
+ it("Does not flag when no rule violations", async () => {
+ const filePath = "./ignored_tags.md";
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, []);
+ });
+ });
+
+ describe("Fix", () => {
+ it("Does not flag TOP005 in the fixed test md file", async () => {
+ const file = "./fixed_flagged_tags.md";
+ const lintErrors = await getLintErrors(file);
+
+ assert(
+ lintErrors.every((error) => !error.includes(expected.name)),
+ `"${file}" contains TOP005 errors`,
+ );
+ });
+
+ it("Wraps flagged multiline HTML tags with missing blank lines", async () => {
+ const fixedFileContents = await fixLintErrors("./flagged_tags.md");
+ const correctFile = await readFile(
+ join(__dirname, "./fixed_flagged_tags.md"),
+ );
+
+ assert.equal(fixedFileContents, correctFile.toString());
+ });
+ });
+});
diff --git a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005_test.md b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/fixed_flagged_tags.md
similarity index 63%
rename from markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005_test.md
rename to markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/fixed_flagged_tags.md
index b121539cffe..1e56868baa4 100644
--- a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005_test.md
+++ b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/fixed_flagged_tags.md
@@ -12,17 +12,12 @@ This section contains a general overview of topics that you will learn in this l
-Valid div due to each tag being surrounded by blank lines.
-
#### Custom section
-
Valid single-line div
-
-
Valid single-line div
Might even have other
paragraph content with it.
-
+
The opening tag is invalid due to not being surrounded by blank lines.
Until a blank line is encountered, if there are any unrelated linting errors, the vast majority of them will not be caught due to how `markdown-it` parses `html_block` tokens.
@@ -31,6 +26,7 @@ The closing tag is valid as it is surrounded by blank lines.
Non-empty/codeblock line
+
The opening tag is invalid due to not being surrounded by blank lines or codeblock delimiters.
@@ -39,82 +35,43 @@ The blank line after it does allow the linter to correctly flag and unrelated li
+
Also invalidates when HTML blocks are chained without blank lines between them.
-
-
-Also invalidates when HTML blocks are chained without blank lines between them.
+
-```markdown
-The only exception to blank lines is a code block delimiter.
+Also invalidates when HTML blocks are chained without blank lines between them.
-```
```markdown
This line above the closing tag is not a blank line nor a code block delimiter, so the closing tag errors.
-
-```
-```html
-
-
- Does not flag when used in an HTML example
-
```
-```jsx
+```markdown
- Also does not flag when used in JSX code blocks
-
-```
-```erb
-<%= if language.isErb? %>
-
Also does not flag when used in erb code blocks
-<% end %>
-```
+ Flags such tags if not in an ignored code block (like HTML/JS/JSX etc.)
-```ejs
-<% if (isEjs) { %>
-
Also does not flag when used in ejs code blocks
-<% } %>
-```
-
-```ruby
-if ruby?
- html_fragment = <<~HTML
-
Does not flag when used in ruby code blocks
- HTML
-end
-```
-
-```javascript
-const htmlString = `
-
Does not flag when used in JavaScript code blocks, e.g. template literals.
-`;
-```
-
-```markdown
-
- But does not like it if done in a non-HTML/JSX code block
+
+
TOP005 doesn't care it the tag is indented or not.
+
+
```
-
-### `Will not flag ignore comments which require being directly followed by the line to ignore`
-
### Knowledge check
The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
diff --git a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/flagged_tags.md b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/flagged_tags.md
new file mode 100644
index 00000000000..ae4bff78ac6
--- /dev/null
+++ b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/flagged_tags.md
@@ -0,0 +1,71 @@
+### Introduction
+
+This file should flag with TOP005 errors, and no other linting errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### Assignment
+
+
+
+
+
+#### Custom section
+
+
+The opening tag is invalid due to not being surrounded by blank lines.
+Until a blank line is encountered, if there are any unrelated linting errors, the vast majority of them will not be caught due to how `markdown-it` parses `html_block` tokens.
+
+The closing tag is valid as it is surrounded by blank lines.
+
+
+
+Non-empty/codeblock line
+
+
+The opening tag is invalid due to not being surrounded by blank lines or codeblock delimiters.
+The blank line after it does allow the linter to correctly flag and unrelated linting errors in these lines if there are any.
+
+
+
+
+Also invalidates when HTML blocks are chained without blank lines between them.
+
+
+Also invalidates when HTML blocks are chained without blank lines between them.
+
+
+```markdown
+
+
+This line above the closing tag is not a blank line nor a code block delimiter, so the closing tag errors.
+
+```
+
+```markdown
+
+ Flags such tags if not in an ignored code block (like HTML/JS/JSX etc.)
+
+
+
+
+ TOP005 doesn't care it the tag is indented or not.
+
+
+```
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- KC item
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- AR item
diff --git a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/ignored_tags.md b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/ignored_tags.md
new file mode 100644
index 00000000000..e342c037857
--- /dev/null
+++ b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/ignored_tags.md
@@ -0,0 +1,86 @@
+### Introduction
+
+This file should not flag any errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### Assignment
+
+
+
+Valid div due to each tag being surrounded by blank lines.
+
+
+
+#### Custom section
+
+
Valid single-line div
+
+
Valid single-line div
Might even have other
paragraph content with it.
+
+```markdown
+
+
+The only exception to blank lines is a code block delimiter.
+
+
+```
+
+```html
+
+
+ Does not flag when used in an HTML example
+
+
+```
+
+```jsx
+
+ Also does not flag when used in JSX code blocks
+
+```
+
+```erb
+<%= if language.isErb? %>
+
Also does not flag when used in erb code blocks
+<% end %>
+```
+
+```ejs
+<% if (isEjs) { %>
+
Also does not flag when used in ejs code blocks
+<% } %>
+```
+
+```ruby
+if ruby?
+ html_fragment = <<~HTML
+
Does not flag when used in ruby code blocks
+ HTML
+end
+```
+
+```javascript
+const htmlString = `
+
Does not flag when used in JavaScript code blocks, e.g. template literals.
+`;
+```
+
+
+### `Will not flag ignore comments which require being directly followed by the line to ignore`
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- KC item
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- AR item
diff --git a/markdownlint/TOP006_fullFencedCodeLanguage/TOP006_fullFencedCodeLanguage.js b/markdownlint/TOP006_fullFencedCodeLanguage/TOP006_fullFencedCodeLanguage.js
index a8c93d01ef9..7bb930e56c3 100644
--- a/markdownlint/TOP006_fullFencedCodeLanguage/TOP006_fullFencedCodeLanguage.js
+++ b/markdownlint/TOP006_fullFencedCodeLanguage/TOP006_fullFencedCodeLanguage.js
@@ -42,7 +42,7 @@ module.exports = {
fencesWithAbbreviatedName.forEach((fence) => {
onError({
lineNumber: fence.lineNumber,
- detail: `Expected: ${fence.fullName}; Actual: ${fence.abbreviatedName} `,
+ detail: `Expected: ${fence.fullName}; Actual: ${fence.abbreviatedName}`,
context: fence.text,
fixInfo: {
editColumn: fence.languageStartingColumn,
diff --git a/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js b/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js
new file mode 100644
index 00000000000..3f983efdbea
--- /dev/null
+++ b/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js
@@ -0,0 +1,62 @@
+const { join } = require("node:path");
+const { readFile } = require("node:fs/promises");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const fixLintErrors = require("../../test_utils/fix")(__dirname);
+const rule = require("../TOP006_fullFencedCodeLanguage");
+
+const pathInRepo = "markdownlint/TOP006_fullFencedCodeLanguage/tests";
+const expected = {
+ name: "TOP006/full-fenced-code-language",
+ description:
+ "Fenced code blocks must use the full name for a language if both full and abbreviated options are valid.",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP006.md",
+ ),
+};
+
+describe("TOP006", () => {
+ describe("Lint", () => {
+ it("Links to the TOP006 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it("Flags abbreviated code block languages", async () => {
+ const filePath = "./test.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:21 error ${expected.name} ${expected.description} [Expected: javascript; Actual: js] [Context: "\`\`\`js"]`,
+ `${errorPath}:26 error ${expected.name} ${expected.description} [Expected: javascript; Actual: js] [Context: "~~~js"]`,
+ `${errorPath}:45 error ${expected.name} ${expected.description} [Expected: ruby; Actual: rb] [Context: "\`\`\`rb"]`,
+ `${errorPath}:53 error ${expected.name} ${expected.description} [Expected: text; Actual: txt] [Context: "\`\`\`txt"]`,
+ `${errorPath}:57 error ${expected.name} ${expected.description} [Expected: yaml; Actual: yml] [Context: "\`\`\`yml"]`,
+ `${errorPath}:65 error ${expected.name} ${expected.description} [Expected: bash; Actual: sh] [Context: "\`\`\`sh"]`,
+ `${errorPath}:88 error ${expected.name} ${expected.description} [Expected: markdown; Actual: md] [Context: "\`\`\`\`md"]`,
+ `${errorPath}:89 error ${expected.name} ${expected.description} [Expected: javascript; Actual: js] [Context: "\`\`\`js"]`,
+ `${errorPath}:96 error ${expected.name} ${expected.description} [Expected: javascript; Actual: js] [Context: " \`\`\`js"]`,
+ ]);
+ });
+ });
+
+ describe("Fix", () => {
+ it("Does not flag TOP006 in the fixed test md file", async () => {
+ const file = "./fixed_test.md";
+ const lintErrors = await getLintErrors(file);
+
+ assert(
+ lintErrors.every((error) => !error.includes(expected.name)),
+ `"${file}" contains TOP006 errors`,
+ );
+ });
+
+ it("Replaces abbreviated code block languages with full name", async () => {
+ const fixedFileContents = await fixLintErrors("./test.md");
+ const correctFile = await readFile(join(__dirname, "./fixed_test.md"));
+
+ assert.equal(fixedFileContents, correctFile.toString());
+ });
+ });
+});
diff --git a/markdownlint/TOP006_fullFencedCodeLanguage/tests/fixed_test.md b/markdownlint/TOP006_fullFencedCodeLanguage/tests/fixed_test.md
new file mode 100644
index 00000000000..24a566e33a0
--- /dev/null
+++ b/markdownlint/TOP006_fullFencedCodeLanguage/tests/fixed_test.md
@@ -0,0 +1,110 @@
+### Introduction
+
+This file should flag with TOP006 errors, and no other linting errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### Assignment
+
+
+
+Assignment
+
+
+
+#### Custom section
+
+```javascript
+console.log("This code block should flag an error as it uses "js" instead of "javascript".");
+```
+
+
+~~~javascript
+console.log("The rule will still flag even if tilde delimiters are used");
+~~~
+
+
+```javascript
+console.log("This code block is valid as it uses the appropriate full name.");
+```
+
+```markdown
+The rule catches the following languages, as they are they ones expected to be seen in this repo's files
+md => markdown
+rb => ruby
+js => javascript
+txt => text
+sh => bash
+yml => yaml
+```
+
+```ruby
+puts "Example of rb flagging."
+```
+
+```ruby
+puts "Use the full name!"
+```
+
+```text
+As does txt.
+```
+
+```yaml
+description: This will flag
+```
+
+```yaml
+description: Unless you use the full name
+```
+
+```bash
+prefer --bash-over-sh
+```
+
+```bash
+like --this
+```
+
+```html
+
HTML is not considered as only the abbreviated name is a valid option.
+
The same applies to similar languages like CSS and JSX.
+```
+
+```css
+.error {
+ display: none;
+}
+```
+
+```jsx
+{isExempt &&
No error here!
}
+```
+
+````markdown
+```javascript
+console.log("Flags abbreviated names even with nested code blocks.");
+```
+````
+
+1. List item
+
+ ```javascript
+ console.log("Flags abbreviated names even with indented code blocks.");
+ ```
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- KC item
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- AR item
diff --git a/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006_test.md b/markdownlint/TOP006_fullFencedCodeLanguage/tests/test.md
similarity index 97%
rename from markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006_test.md
rename to markdownlint/TOP006_fullFencedCodeLanguage/tests/test.md
index ecc95f04431..eba4a0b5493 100644
--- a/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006_test.md
+++ b/markdownlint/TOP006_fullFencedCodeLanguage/tests/test.md
@@ -12,7 +12,7 @@ This section contains a general overview of topics that you will learn in this l
-Valid div due to each tag being surrounded by blank lines.
+Assignment
diff --git a/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js b/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js
new file mode 100644
index 00000000000..fba76312666
--- /dev/null
+++ b/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js
@@ -0,0 +1,67 @@
+const { join } = require("node:path");
+const { readFile } = require("node:fs/promises");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const fixLintErrors = require("../../test_utils/fix")(__dirname);
+const rule = require("../TOP007_useMarkdownLinks");
+
+const pathInRepo = "markdownlint/TOP007_useMarkdownLinks/tests";
+const expected = {
+ name: "TOP007/use-markdown-links",
+ description:
+ "Links used to navigate to external content or other landmarks in the page should use markdown links instead of HTML anchor tags.",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP007.md",
+ ),
+};
+
+describe("TOP007", () => {
+ describe("Lint", () => {
+ it("Links to the TOP007 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it("Flags non-markdown links used for markdown purposes", async () => {
+ const filePath = "./anchors_in_markdown.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:21 error ${expected.name} ${expected.description} [ Expected: "[Link should flag as we should be using a markdown link instead](#custom-section)" Actual: "
Link should flag as we should be using a markdown link instead" ]`,
+ `${errorPath}:23 error ${expected.name} ${expected.description} [ Expected: "[Will flag](#custom-section)" Actual: "
Will flag" ]`,
+ `${errorPath}:23 error ${expected.name} ${expected.description} [ Expected: "[multiple anchors](#assignment)" Actual: "
multiple anchors" ]`,
+ `${errorPath}:26 error ${expected.name} ${expected.description} [ Expected: "[@TheOdinProjectExamples](https://codepen.io/TheOdinProjectExamples)" Actual: "
@TheOdinProjectExamples" ]`,
+ `${errorPath}:34 error ${expected.name} ${expected.description} [ Expected: "[Flags knowledge check anchors](#knowledge-check)" Actual: "
Flags knowledge check anchors" ]`,
+ ]);
+ });
+
+ it("Does not flag any errors if no rule violations", async () => {
+ const filePath = "./valid_anchors.md";
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, []);
+ });
+ });
+
+ describe("Fix", () => {
+ it("Does not flag TOP007 in the fixed test md file", async () => {
+ const file = "./fixed_anchors_in_markdown.md";
+ const lintErrors = await getLintErrors(file);
+
+ assert(
+ lintErrors.every((error) => !error.includes(expected.name)),
+ `"${file}" contains TOP007 errors`,
+ );
+ });
+
+ it("Converts flagged anchors to markdown links", async () => {
+ const fixedFileContents = await fixLintErrors("./anchors_in_markdown.md");
+ const correctFile = await readFile(
+ join(__dirname, "./fixed_anchors_in_markdown.md"),
+ );
+
+ assert.equal(fixedFileContents, correctFile.toString());
+ });
+ });
+});
diff --git a/markdownlint/TOP007_useMarkdownLinks/tests/anchors_in_markdown.md b/markdownlint/TOP007_useMarkdownLinks/tests/anchors_in_markdown.md
new file mode 100644
index 00000000000..ec0551ff9ea
--- /dev/null
+++ b/markdownlint/TOP007_useMarkdownLinks/tests/anchors_in_markdown.md
@@ -0,0 +1,40 @@
+### Introduction
+
+This file should flag with TOP007 errors, and no other linting errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### Assignment
+
+
+
+Assignment section
+
+
+
+#### Custom section
+
+
Link should flag as we should be using a markdown link instead.
+
+
Will flag if
multiple anchors in same line.
+
+
+
@TheOdinProjectExamples
+
+
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+-
Flags knowledge check anchors
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- AR item
diff --git a/markdownlint/TOP007_useMarkdownLinks/tests/fixed_anchors_in_markdown.md b/markdownlint/TOP007_useMarkdownLinks/tests/fixed_anchors_in_markdown.md
new file mode 100644
index 00000000000..1cd79963c9f
--- /dev/null
+++ b/markdownlint/TOP007_useMarkdownLinks/tests/fixed_anchors_in_markdown.md
@@ -0,0 +1,40 @@
+### Introduction
+
+This file should flag with TOP007 errors, and no other linting errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### Assignment
+
+
+
+Assignment section
+
+
+
+#### Custom section
+
+[Link should flag as we should be using a markdown link instead](#custom-section).
+
+[Will flag](#custom-section) if [multiple anchors](#assignment) in same line.
+
+
+[@TheOdinProjectExamples](https://codepen.io/TheOdinProjectExamples)
+
+
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- [Flags knowledge check anchors](#knowledge-check)
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- AR item
diff --git a/markdownlint/TOP007_useMarkdownLinks/tests/TOP007_test.md b/markdownlint/TOP007_useMarkdownLinks/tests/valid_anchors.md
similarity index 67%
rename from markdownlint/TOP007_useMarkdownLinks/tests/TOP007_test.md
rename to markdownlint/TOP007_useMarkdownLinks/tests/valid_anchors.md
index 47c225cac6c..9f45d6d0e30 100644
--- a/markdownlint/TOP007_useMarkdownLinks/tests/TOP007_test.md
+++ b/markdownlint/TOP007_useMarkdownLinks/tests/valid_anchors.md
@@ -1,6 +1,6 @@
### Introduction
-This file should flag with TOP007 errors, and no other linting errors.
+This file should not flag any linting errors.
### Lesson overview
@@ -18,18 +18,8 @@ Assignment section
#### Custom section
-```html
-
- The following </p> should be ignored by this rule as it does not belong to a codepen embed.
-
-```
-
[Markdown links are desired in most cases](#custom-section)
-
Link should flag as we should be using a markdown link instead.
-
-
Will flag if
multiple anchors in same line.
-
`
Anchors inside an inline code block are ignored`
```html
@@ -45,16 +35,11 @@ on
CodePen.
-
-
@TheOdinProjectExamples
-
-
-
### Knowledge check
The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
--
Flags with and omits non-href attributes
+- [Does not flag markdown links](#href)
### Additional resources
diff --git a/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js
new file mode 100644
index 00000000000..4f4cfed114b
--- /dev/null
+++ b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js
@@ -0,0 +1,60 @@
+const { join } = require("node:path");
+const { readFile } = require("node:fs/promises");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const fixLintErrors = require("../../test_utils/fix")(__dirname);
+const rule = require("../TOP008_useBackticksForFencedCodeBlocks");
+
+const pathInRepo = "markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests";
+const expected = {
+ name: "TOP008/use-backticks-for-fenced-code-blocks",
+ description: "Fenced code blocks should use backticks instead of tildes",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP008.md",
+ ),
+};
+
+describe("TOP008", () => {
+ describe("Lint", () => {
+ it("Links to the TOP008 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it("Flags code blocks with tilde delimiters", async () => {
+ const filePath = "./test.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:21 error ${expected.name} ${expected.description} [Expected: "\`\`\`"; Actual: "~~~"] [Context: "~~~text"]`,
+ `${errorPath}:23 error ${expected.name} ${expected.description} [Expected: "\`\`\`"; Actual: "~~~"] [Context: "~~~"]`,
+ `${errorPath}:25 error ${expected.name} ${expected.description} [Expected: "\`\`\`\`"; Actual: "~~~~"] [Context: "~~~~markdown"]`,
+ `${errorPath}:26 error ${expected.name} ${expected.description} [Expected: "\`\`\`"; Actual: "~~~"] [Context: "~~~text"]`,
+ `${errorPath}:28 error ${expected.name} ${expected.description} [Expected: "\`\`\`"; Actual: "~~~"] [Context: "~~~"]`,
+ `${errorPath}:29 error ${expected.name} ${expected.description} [Expected: "\`\`\`\`"; Actual: "~~~~"] [Context: "~~~~"]`,
+ `${errorPath}:33 error ${expected.name} ${expected.description} [Expected: "\`\`\`"; Actual: "~~~"] [Context: " ~~~text"]`,
+ `${errorPath}:35 error ${expected.name} ${expected.description} [Expected: "\`\`\`"; Actual: "~~~"] [Context: " ~~~"]`,
+ ]);
+ });
+ });
+
+ describe("Fix", () => {
+ it("Does not flag TOP008 in the fixed test md file", async () => {
+ const file = "./fixed_test.md";
+ const lintErrors = await getLintErrors(file);
+
+ assert(
+ lintErrors.every((error) => !error.includes(expected.name)),
+ `"${file}" contains TOP008 errors`,
+ );
+ });
+
+ it("Replaces tilde delimiters with backticks", async () => {
+ const fixedFileContents = await fixLintErrors("./test.md");
+ const correctFile = await readFile(join(__dirname, "./fixed_test.md"));
+
+ assert.equal(fixedFileContents, correctFile.toString());
+ });
+ });
+});
diff --git a/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/fixed_test.md b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/fixed_test.md
new file mode 100644
index 00000000000..a5b889b8c7b
--- /dev/null
+++ b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/fixed_test.md
@@ -0,0 +1,57 @@
+### Introduction
+
+This file should flag with TOP008 errors, and no other linting errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### Assignment
+
+
+
+Assignment section
+
+
+
+#### Custom section
+
+```text
+This codeblock should flag an error as it uses tildes instead of backticks.
+```
+
+````markdown
+```text
+Parent and nested code blocks should both individually flag if tildes are used instead of backticks.
+```
+````
+
+1. List item
+
+ ```text
+ Indented code blocks are treated all the same.
+ ```
+
+```text
+Backticks are valid and will not flag errors.
+```
+
+````markdown
+```text
+As will backticked parent and nested code blocks.
+```
+````
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- KC item
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- AR item
diff --git a/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008_test.md b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/test.md
similarity index 100%
rename from markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008_test.md
rename to markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/test.md
diff --git a/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009.test.js b/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009.test.js
new file mode 100644
index 00000000000..2dd1aa39a01
--- /dev/null
+++ b/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009.test.js
@@ -0,0 +1,50 @@
+const { join } = require("node:path");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const rule = require("../TOP009_lessonOverviewItemsSentenceStructure");
+
+const pathInRepo =
+ "markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests";
+const expected = {
+ name: "TOP009/lesson-overview-items-sentence-structure",
+ description:
+ "Lesson overview items must be statements, not questions, and must begin with a capital letter and end with a period.",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP009.md",
+ ),
+};
+
+describe("TOP009", () => {
+ it("Links to the TOP009 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it("Flags when lesson overview item does not begin with a capital letter", async () => {
+ const filePath = "./capital_letter.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:10 error ${expected.name} ${expected.description} [Lesson overview items must be statements, not questions, and must begin with a capital letter and end with a period.] [Context: "lesson overview item 2."]`,
+ ]);
+ });
+
+ it("Flags when lesson overview item does not end with a period", async () => {
+ const filePath = "./invalid_punctuation.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:11 error ${expected.name} ${expected.description} [Lesson overview items must be statements, not questions, and must begin with a capital letter and end with a period.] [Context: "Lesson overview item 3?"]`,
+ `${errorPath}:13 error ${expected.name} ${expected.description} [Lesson overview items must be statements, not questions, and must begin with a capital letter and end with a period.] [Context: "Lesson overview item 6"]`,
+ ]);
+ });
+
+ it("Does not flag any errors if no rule violations", async () => {
+ const filePath = "./valid.md";
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, []);
+ });
+});
diff --git a/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009_capital_letter.md b/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/capital_letter.md
similarity index 100%
rename from markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009_capital_letter.md
rename to markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/capital_letter.md
diff --git a/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009_invalid_punctuation.md b/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/invalid_punctuation.md
similarity index 100%
rename from markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009_invalid_punctuation.md
rename to markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/invalid_punctuation.md
diff --git a/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009_test_valid.md b/markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/valid.md
similarity index 100%
rename from markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009_test_valid.md
rename to markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/valid.md
diff --git a/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js b/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js
new file mode 100644
index 00000000000..e21861f66f2
--- /dev/null
+++ b/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js
@@ -0,0 +1,56 @@
+const { join } = require("node:path");
+const { readFile } = require("node:fs/promises");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const fixLintErrors = require("../../test_utils/fix")(__dirname);
+const rule = require("../TOP010_useLazyNumbering");
+
+const pathInRepo = "markdownlint/TOP010_useLazyNumbering/tests";
+const expected = {
+ name: "TOP010/lazy-numbering-for-ordered-lists",
+ description: "Ordered lists must always use 1. as a prefix (lazy numbering)",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP010.md",
+ ),
+};
+
+describe("TOP010", () => {
+ describe("Lint", () => {
+ it("Links to the TOP010 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it("Flags non-1 ordered list prefixes", async () => {
+ const filePath = "./test.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:26 error ${expected.name} ${expected.description} [ Expected: "1" Actual: "2" ]`,
+ `${errorPath}:28 error ${expected.name} ${expected.description} [ Expected: " 1" Actual: " 2" ]`,
+ `${errorPath}:29 error ${expected.name} ${expected.description} [ Expected: "1" Actual: "3" ]`,
+ `${errorPath}:38 error ${expected.name} ${expected.description} [ Expected: "1" Actual: "2" ]`,
+ ]);
+ });
+ });
+
+ describe("Fix", () => {
+ it("Does not flag TOP010 in the fixed test md file", async () => {
+ const file = "./fixed_test.md";
+ const lintErrors = await getLintErrors(file);
+
+ assert(
+ lintErrors.every((error) => !error.includes(expected.name)),
+ `"${file}" contains TOP010 errors`,
+ );
+ });
+
+ it("Replaces non-1 ordered list prefixes with 1", async () => {
+ const fixedFileContents = await fixLintErrors("./test.md");
+ const correctFile = await readFile(join(__dirname, "./fixed_test.md"));
+
+ assert.equal(fixedFileContents, correctFile.toString());
+ });
+ });
+});
diff --git a/markdownlint/TOP010_useLazyNumbering/tests/fixed_test.md b/markdownlint/TOP010_useLazyNumbering/tests/fixed_test.md
new file mode 100644
index 00000000000..08856cbbd36
--- /dev/null
+++ b/markdownlint/TOP010_useLazyNumbering/tests/fixed_test.md
@@ -0,0 +1,55 @@
+### Introduction
+
+This file should flag with TOP010 errors, and no other linting errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- A LESSON OVERVIEW ITEM.
+
+### CUSTOM SECTION HEADING
+
+CUSTOM SECTION CONTENT.
+
+### Assignment
+
+
+
+#### OPTIONAL CUSTOM ASSIGNMENT HEADING
+
+1. A RESOURCE OR EXERCISE ITEM
+
+ - AN INSTRUCTION ITEM
+
+1. Item One
+1. Item Two
+ 1. Child of Item Two
+ 1. Child of Item Two
+1. Item Three
+
+1. Item One
+1. Item Two
+ 1. Child of Item Two
+ 1. Child of Item Two
+1. Item Three
+
+1. *foo*
+1. *Bar*
+
+- This is an unordered list item to test TOP010
+- This is another unordered list item
+
+
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- [A KNOWLEDGE CHECK QUESTION](A-KNOWLEDGE-CHECK-URL)
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- It looks like this lesson doesn't have any additional resources yet. Help us expand this section by contributing to our curriculum.
diff --git a/markdownlint/TOP010_useLazyNumbering/tests/TOP010_test.md b/markdownlint/TOP010_useLazyNumbering/tests/test.md
similarity index 100%
rename from markdownlint/TOP010_useLazyNumbering/tests/TOP010_test.md
rename to markdownlint/TOP010_useLazyNumbering/tests/test.md
diff --git a/markdownlint/TOP011_headingIndentation/TOP011_headingIndentation.js b/markdownlint/TOP011_headingIndentation/TOP011_headingIndentation.js
index c10346e4ae0..e53d2709f6e 100644
--- a/markdownlint/TOP011_headingIndentation/TOP011_headingIndentation.js
+++ b/markdownlint/TOP011_headingIndentation/TOP011_headingIndentation.js
@@ -47,6 +47,7 @@ module.exports = {
onError({
lineNumber: heading.lineNumber,
detail: `Note box heading indented ${headingIndentation} spaces but should be indented ${noteBoxIndentation} spaces instead to match the containing note box.`,
+ context: heading.line,
fixInfo: {
lineNumber: heading.lineNumber,
deleteCount: headingIndentation,
@@ -62,6 +63,7 @@ module.exports = {
onError({
lineNumber: heading.lineNumber,
detail: `Normal headings must not be indented.`,
+ context: heading.line,
fixInfo: {
lineNumber: heading.lineNumber,
deleteCount: headingIndentation,
diff --git a/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js b/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js
new file mode 100644
index 00000000000..4cd5ab6a374
--- /dev/null
+++ b/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js
@@ -0,0 +1,55 @@
+const { join } = require("node:path");
+const { readFile } = require("node:fs/promises");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const fixLintErrors = require("../../test_utils/fix")(__dirname);
+const rule = require("../TOP011_headingIndentation");
+
+const pathInRepo = "markdownlint/TOP011_headingIndentation/tests";
+const expected = {
+ name: "TOP011/heading-indentation",
+ description: "Headings must not be indented unless they are for a note box.",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP011.md",
+ ),
+};
+
+describe("TOP011", () => {
+ describe("Lint", () => {
+ it("Links to the TOP011 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it("Flags incorrectly indented headings", async () => {
+ const filePath = "./test.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:19 error ${expected.name} ${expected.description} [Normal headings must not be indented.] [Context: " ### This heading is indented so will flag an error"]`,
+ `${errorPath}:47 error ${expected.name} ${expected.description} [Note box heading indented 9 spaces but should be indented 7 spaces instead to match the containing note box.] [Context: " #### The note box and heading do not match indentation levels (7v9) so this should flag"]`,
+ `${errorPath}:53 error ${expected.name} ${expected.description} [Note box heading indented 2 spaces but should be indented 0 spaces instead to match the containing note box.] [Context: " #### The note box and heading do not match indentation levels (0v2) so this should flag"]`,
+ ]);
+ });
+ });
+
+ describe("Fix", () => {
+ it("Does not flag TOP011 in the fixed test md file", async () => {
+ const file = "./fixed_test.md";
+ const lintErrors = await getLintErrors(file);
+
+ assert(
+ lintErrors.every((error) => !error.includes(expected.name)),
+ `"${file}" contains TOP011 errors`,
+ );
+ });
+
+ it("Fixes incorrect heading indentation", async () => {
+ const fixedFileContents = await fixLintErrors("./test.md");
+ const correctFile = await readFile(join(__dirname, "./fixed_test.md"));
+
+ assert.equal(fixedFileContents, correctFile.toString());
+ });
+ });
+});
diff --git a/markdownlint/TOP011_headingIndentation/tests/fixed_test.md b/markdownlint/TOP011_headingIndentation/tests/fixed_test.md
new file mode 100644
index 00000000000..fc7cb7b8155
--- /dev/null
+++ b/markdownlint/TOP011_headingIndentation/tests/fixed_test.md
@@ -0,0 +1,75 @@
+### Introduction
+
+Text content.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- LO item.
+
+### This heading is not indented so should NOT be flagged
+
+Some more content.
+
+#### Heading level makes no difference to indent expectations
+
+Some content.
+
+### This heading is indented so will flag an error
+
+
+
+#### The note box is not indented so this heading must also not be indented
+
+
+
+1. List item
+ 1. List item child.
+
+
+
+ #### The note box and heading are both indented 3 spaces so should NOT be flagged
+
+
+
+ - List item child UL.
+ - UL item child.
+
+
+
+ #### The note box and heading are both indented 5 spaces so should NOT be flagged
+
+
+
+
+
+ #### The note box and heading do not match indentation levels (7v9) so this should flag
+
+
+
+
+
+#### The note box and heading do not match indentation levels (0v2) so this should flag
+
+
+
+### Assignment
+
+
+
+Assignment content
+
+
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- KC item
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- AR item
diff --git a/markdownlint/TOP011_headingIndentation/tests/TOP011_test.md b/markdownlint/TOP011_headingIndentation/tests/test.md
similarity index 100%
rename from markdownlint/TOP011_headingIndentation/tests/TOP011_test.md
rename to markdownlint/TOP011_headingIndentation/tests/test.md
diff --git a/markdownlint/TOP012_noteBoxHeadings/TOP012_noteBoxHeadings.js b/markdownlint/TOP012_noteBoxHeadings/TOP012_noteBoxHeadings.js
index 7b004316025..9227f07d1bc 100644
--- a/markdownlint/TOP012_noteBoxHeadings/TOP012_noteBoxHeadings.js
+++ b/markdownlint/TOP012_noteBoxHeadings/TOP012_noteBoxHeadings.js
@@ -54,6 +54,7 @@ module.exports = {
onError({
lineNumber: heading.lineNumber,
detail: `Expected a level 4 heading (####) but got a level ${heading.hashes.length} heading (${heading.hashes}) instead.`,
+ context: heading.text,
fixInfo: {
editColumn: hashesStartColumn,
deleteCount: heading.hashes.length,
diff --git a/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js b/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js
new file mode 100644
index 00000000000..6b491d023ee
--- /dev/null
+++ b/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js
@@ -0,0 +1,68 @@
+const { join } = require("node:path");
+const { readFile } = require("node:fs/promises");
+const { describe, it } = require("node:test");
+const assert = require("node:assert/strict");
+const getLintErrors = require("../../test_utils/lint")(__dirname);
+const fixLintErrors = require("../../test_utils/fix")(__dirname);
+const rule = require("../TOP012_noteBoxHeadings");
+
+const pathInRepo = "markdownlint/TOP012_noteBoxHeadings/tests";
+const expected = {
+ name: "TOP012/note-box-headings",
+ description: "Note boxes have appropriate headings",
+ information: new URL(
+ "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP012.md",
+ ),
+};
+
+describe("TOP012", () => {
+ describe("Lint", () => {
+ it("Links to the TOP012 docs", () => {
+ assert.deepEqual(rule.information, expected.information);
+ });
+
+ it("Flags note boxes with missing headings", async () => {
+ const filePath = "./missing_heading.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:13 error ${expected.name} ${expected.description} [Note box is missing a heading. Note boxes must start with a level 4 heading (####).]`,
+ ]);
+ });
+
+ it("Flags note boxes with incorrectly levelled headings", async () => {
+ const filePath = "./incorrect_heading_level.md";
+ const errorPath = join(pathInRepo, filePath);
+ const lintErrors = await getLintErrors(filePath);
+
+ assert.deepEqual(lintErrors, [
+ `${errorPath}:27 error ${expected.name} ${expected.description} [Expected a level 4 heading (####) but got a level 3 heading (###) instead.] [Context: "### Level 3 note box heading: Will flag error as it should be level 4"]`,
+ `${errorPath}:35 error ${expected.name} ${expected.description} [Expected a level 4 heading (####) but got a level 2 heading (##) instead.] [Context: "## Level 2 note box heading: Will flag error as it should be level 4"]`,
+ ]);
+ });
+ });
+
+ describe("Fix", () => {
+ it("Does not flag TOP012 in the fixed test md file", async () => {
+ const file = "./fixed_incorrect_heading_level.md";
+ const lintErrors = await getLintErrors(file);
+
+ assert(
+ lintErrors.every((error) => !error.includes(expected.name)),
+ `"${file}" contains TOP012 errors`,
+ );
+ });
+
+ it("Converts incorrectly levelled note box headings to level 4", async () => {
+ const fixedFileContents = await fixLintErrors(
+ "./incorrect_heading_level.md",
+ );
+ const correctFile = await readFile(
+ join(__dirname, "./fixed_incorrect_heading_level.md"),
+ );
+
+ assert.equal(fixedFileContents, correctFile.toString());
+ });
+ });
+});
diff --git a/markdownlint/TOP012_noteBoxHeadings/tests/fixed_incorrect_heading_level.md b/markdownlint/TOP012_noteBoxHeadings/tests/fixed_incorrect_heading_level.md
new file mode 100644
index 00000000000..16ed62aada0
--- /dev/null
+++ b/markdownlint/TOP012_noteBoxHeadings/tests/fixed_incorrect_heading_level.md
@@ -0,0 +1,57 @@
+### Introduction
+
+This file should flag with TOP012 errors, and no other linting errors.
+
+### Lesson overview
+
+This section contains a general overview of topics that you will learn in this lesson.
+
+- A LESSON OVERVIEW ITEM.
+
+### Custom section
+
+#### Non-note box level 4 headings will not flag this error
+
+Custom subsection contents.
+
+
+
+#### Level 4 note box heading: Correct and will not flag error
+
+Note box contents.
+
+
+
+
+
+#### Level 3 note box heading: Will flag error as it should be level 4
+
+Note box contents.
+
+
+
+
+
+#### Level 2 note box heading: Will flag error as it should be level 4
+
+Note box contents.
+
+
+
+### Assignment
+
+
+
+
+
+### Knowledge check
+
+The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
+
+- [A KNOWLEDGE CHECK QUESTION](A-KNOWLEDGE-CHECK-URL)
+
+### Additional resources
+
+This section contains helpful links to related content. It isn't required, so consider it supplemental.
+
+- It looks like this lesson doesn't have any additional resources yet. Help us expand this section by contributing to our curriculum.
diff --git a/markdownlint/TOP012_noteBoxHeadings/tests/TOP012_test.md b/markdownlint/TOP012_noteBoxHeadings/tests/incorrect_heading_level.md
similarity index 92%
rename from markdownlint/TOP012_noteBoxHeadings/tests/TOP012_test.md
rename to markdownlint/TOP012_noteBoxHeadings/tests/incorrect_heading_level.md
index e4dbe2cd142..b80d66ab2ec 100644
--- a/markdownlint/TOP012_noteBoxHeadings/tests/TOP012_test.md
+++ b/markdownlint/TOP012_noteBoxHeadings/tests/incorrect_heading_level.md
@@ -38,12 +38,6 @@ Note box contents.
-