From 69980a6bdbbe5131e719d5331062d75c26cfab38 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:51:49 +0000 Subject: [PATCH 01/28] Add util function to get lint errors from a file --- markdownlint/test_utils/lint.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 markdownlint/test_utils/lint.js diff --git a/markdownlint/test_utils/lint.js b/markdownlint/test_utils/lint.js new file mode 100644 index 00000000000..c7f35fa9a39 --- /dev/null +++ b/markdownlint/test_utils/lint.js @@ -0,0 +1,28 @@ +const { join } = require("node:path"); +const { access } = require("node:fs/promises"); +const { promisify } = require("node:util"); +const exec = promisify(require("node:child_process").exec); + +/** + * @param {string} dirname - Pass in the __dirname global for a function relative to the current test file + */ +module.exports = (dirname) => { + /** + * @param {string} filePath - Path to markdown file relative to current test file + * @returns {Promise} Array of markdownlint error strings, each error being its own element + * @throws {Error} with .code === 'ENOENT' if no file at given path + */ + return async (filePath) => { + const markdownFileFullPath = join(dirname, filePath); + + // don't catch - we want this to throw and halt the tests if the file does not exist + await access(markdownFileFullPath); + + try { + await exec(`npm run lint -- "${markdownFileFullPath}"`); + return []; + } catch (error) { + return error.stderr.trim().split("\n"); + } + }; +}; From 50249a2a8814da4d235db3c53f7edb1737a4947e Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:51:53 +0000 Subject: [PATCH 02/28] Add util function to get fixed file contents (dry run) --- markdownlint/test_utils/fix.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 markdownlint/test_utils/fix.js diff --git a/markdownlint/test_utils/fix.js b/markdownlint/test_utils/fix.js new file mode 100644 index 00000000000..4384babcdf9 --- /dev/null +++ b/markdownlint/test_utils/fix.js @@ -0,0 +1,33 @@ +const { join } = require("node:path"); +const { readFile } = require("node:fs/promises"); +const { spawnSync } = require("node:child_process"); + +/** + * @param {string} dirname - Pass in the __dirname global for a function relative to the current test file + */ +module.exports = (dirname) => { + /** + * @param {string} filePath - Path to markdown file relative to current test file + * @returns {Promise} File contents after lint fixes applied + * @throws {Error} with .code === 'ENOENT' if no file at given path + */ + return async (filePath) => { + const markdownFileFullPath = join(dirname, filePath); + + // don't catch - we want this to throw and halt the tests if the file does not exist + const fileContents = await readFile(markdownFileFullPath); + const childProcess = spawnSync(`npm`, ["run", "lint", "--", "--format"], { + input: fileContents.toString(), + }); + + return ( + childProcess.stdout + .toString() + // get rid of the markdownlint-cli2 output noise + .replace( + "\n> curriculum@1.0.0 lint\n> markdownlint-cli2 --format\n\n", + "", + ) + ); + }; +}; From 80cc34d31e71025d72efb8c56b4d20fce5272c00 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:52:34 +0000 Subject: [PATCH 03/28] Add test script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ec51c2af89..12db79e608f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "[The Odin Project](https://www.theodinproject.com/) (TOP) is an open-source curriculum for learning full-stack web development. Our curriculum is divided into distinct courses, each covering the subject language in depth. Each course contains a listing of lessons interspersed with multiple projects. These projects give users the opportunity to practice what they are learning, thereby reinforcing and solidifying the theoretical knowledge learned in the lessons. Completed projects may then be included in the user's portfolio.", "scripts": { "lint": "markdownlint-cli2", - "fix": "markdownlint-cli2 --fix" + "fix": "markdownlint-cli2 --fix", + "test": "node --test" }, "license": "CC BY-NC-SA 4.0", "devDependencies": { From 9e8e3e36e0b704b39ca1b80efd66efb30e1c48dd Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:53:54 +0000 Subject: [PATCH 04/28] Update markdownlint-cli2 minimum version required --format CLI option released in v0.19.0 and required in custom rule tests --- .markdownlint-cli2.jsonc | 5 +- package-lock.json | 1037 ++++++++++++++++++++++++++++++++++---- package.json | 2 +- 3 files changed, 949 insertions(+), 95 deletions(-) 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/package-lock.json b/package-lock.json index 719714f3290..c401eee16d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "CC BY-NC-SA 4.0", "devDependencies": { - "markdownlint-cli2": "^0.12.1" + "markdownlint-cli2": "^0.20.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -17,6 +17,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -30,6 +31,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -39,6 +41,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -48,10 +51,11 @@ } }, "node_modules/@sindresorhus/merge-streams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", - "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -59,11 +63,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/braces": { "version": "3.0.3", @@ -78,11 +127,111 @@ "node": ">=8" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -91,26 +240,28 @@ } }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -128,11 +279,25 @@ "node": ">=8" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -141,39 +306,79 @@ } }, "node_modules/globby": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", - "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-15.0.0.tgz", + "integrity": "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==", "dev": true, + "license": "MIT", "dependencies": { - "@sindresorhus/merge-streams": "^1.0.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "path-type": "^6.0.0", "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -183,6 +388,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -190,6 +396,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -200,133 +417,735 @@ "node": ">=0.12.0" } }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/katex": { + "version": "0.16.27", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", + "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, + "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } }, "node_modules/markdown-it": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", - "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.0.0" + "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdownlint": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.33.0.tgz", - "integrity": "sha512-4lbtT14A3m0LPX1WS/3d1m7Blg+ZwiLq36WvjQqFGsX3Gik99NV+VXp/PW3n+Q62xyPdbvGOCfjPqjW+/SKMig==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", + "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", "dev": true, + "license": "MIT", "dependencies": { - "markdown-it": "14.0.0", - "markdownlint-micromark": "0.1.8" + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.2", + "string-width": "8.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" } }, "node_modules/markdownlint-cli2": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.12.1.tgz", - "integrity": "sha512-RcK+l5FjJEyrU3REhrThiEUXNK89dLYNJCYbvOUKypxqIGfkcgpz8g08EKqhrmUbYfYoLC5nEYQy53NhJSEtfQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.20.0.tgz", + "integrity": "sha512-esPk+8Qvx/f0bzI7YelUeZp+jCtFOk3KjZ7s9iBQZ6HlymSXoTtWGiIRZP05/9Oy2ehIoIjenVwndxGtxOIJYQ==", "dev": true, + "license": "MIT", "dependencies": { - "globby": "14.0.0", - "jsonc-parser": "3.2.0", - "markdownlint": "0.33.0", - "markdownlint-cli2-formatter-default": "0.0.4", - "micromatch": "4.0.5", - "yaml": "2.3.4" + "globby": "15.0.0", + "js-yaml": "4.1.1", + "jsonc-parser": "3.3.1", + "markdown-it": "14.1.0", + "markdownlint": "0.40.0", + "markdownlint-cli2-formatter-default": "0.0.6", + "micromatch": "4.0.8" }, "bin": { - "markdownlint-cli2": "markdownlint-cli2.js" + "markdownlint-cli2": "markdownlint-cli2-bin.mjs" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" } }, "node_modules/markdownlint-cli2-formatter-default": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.4.tgz", - "integrity": "sha512-xm2rM0E+sWgjpPn1EesPXx5hIyrN2ddUnUwnbCsD/ONxYtw3PX6LydvdH6dciWAoFDpwzbHM1TO7uHfcMd6IYg==", - "dev": true, - "peerDependencies": { - "markdownlint-cli2": ">=0.0.4" - } - }, - "node_modules/markdownlint-micromark": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.8.tgz", - "integrity": "sha512-1ouYkMRo9/6gou9gObuMDnvZM8jC/ly3QCFQyoSPCS2XV1ZClU0xpKbL1Ar3bWWRT1RnBZkWUEiNKrI2CwiBQA==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", + "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", "dev": true, - "engines": { - "node": ">=16" - }, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/DavidAnson" + }, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" } }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -337,6 +1156,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -349,6 +1169,7 @@ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -371,13 +1192,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -402,6 +1225,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -411,6 +1235,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -418,6 +1243,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -432,31 +1290,24 @@ } }, "node_modules/uc.micro": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz", - "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" }, "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } } } } diff --git a/package.json b/package.json index 12db79e608f..197407ef080 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,6 @@ }, "license": "CC BY-NC-SA 4.0", "devDependencies": { - "markdownlint-cli2": "^0.12.1" + "markdownlint-cli2": "^0.20.0" } } From 8de0ae6e94a3e0fe61a5d9d3e9ee0634f823e51a Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:56:11 +0000 Subject: [PATCH 05/28] Add GH workflow for markdownlint custom rule testing Normal markdownlint workflow now should only apply to lessons/projects --- .github/workflows/markdownlint.yml | 4 ++-- .github/workflows/markdownlint_testing.yml | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/markdownlint_testing.yml 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 From c42c410e613b4ca68aec675f2cec66797a9e13b6 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:58:21 +0000 Subject: [PATCH 06/28] Split different TOP001 violation types to different test files --- .../tests/blacklisted_label_text.md | 52 +++++++++++++++++++ .../tests/{TOP001_test.md => this_or_here.md} | 12 +---- 2 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 markdownlint/TOP001_descriptiveLinkTextLabels/tests/blacklisted_label_text.md rename markdownlint/TOP001_descriptiveLinkTextLabels/tests/{TOP001_test.md => this_or_here.md} (83%) 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 + +
+ +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/TOP001_descriptiveLinkTextLabels/tests/TOP001_test.md b/markdownlint/TOP001_descriptiveLinkTextLabels/tests/this_or_here.md similarity index 83% rename from markdownlint/TOP001_descriptiveLinkTextLabels/tests/TOP001_test.md rename to markdownlint/TOP001_descriptiveLinkTextLabels/tests/this_or_here.md index a7dcec4a5f0..a35f00fa882 100644 --- a/markdownlint/TOP001_descriptiveLinkTextLabels/tests/TOP001_test.md +++ b/markdownlint/TOP001_descriptiveLinkTextLabels/tests/this_or_here.md @@ -18,20 +18,10 @@ This section contains a general overview of topics that you will learn in this l [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 +### The following links SHOULD be flagged for containing "this" or "here" [this](someURL) [This video](someURL) -[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) [here](someURL) [Click here](someURL) [This other thing](someURL) From 2e147b8bc99a2c6654d9056e0370a31a8acc4d03 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:59:11 +0000 Subject: [PATCH 07/28] Add tests for TOP001 custom rule --- .../TOP001_descriptiveLinkTextLabels.js | 2 +- .../tests/TOP001.test.js | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 markdownlint/TOP001_descriptiveLinkTextLabels/tests/TOP001.test.js 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)"]`, + ]); + }); +}); From 9e42366098548141fbc6ff59070c03facafd8dce Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:01:16 +0000 Subject: [PATCH 08/28] Add tests for TOP002 custom rule --- .../tests/TOP002.test.js | 45 +++++++++++++++++++ .../tests/fixed_test.md | 39 ++++++++++++++++ .../tests/{TOP002_test.md => test.md} | 0 3 files changed, 84 insertions(+) create mode 100644 markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js create mode 100644 markdownlint/TOP002_noCodeInHeadings/tests/fixed_test.md rename markdownlint/TOP002_noCodeInHeadings/tests/{TOP002_test.md => test.md} (100%) diff --git a/markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js b/markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js new file mode 100644 index 00000000000..5a7f08f3721 --- /dev/null +++ b/markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js @@ -0,0 +1,45 @@ +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("../TOP002_noCodeInHeadings"); + +const pathInRepo = "markdownlint/TOP002_noCodeInHeadings/tests"; +const expected = { + name: "TOP002/no-code-headings", + description: "No inline code in headings", + information: new URL( + "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP002.md", + ), +}; + +describe("TOP002", () => { + describe("Lint", () => { + it("Links to the TOP002 docs", () => { + assert.deepEqual(rule.information, expected.information); + }); + + it("Flags inline code in headings", async () => { + const filePath = "test.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:15 error ${expected.name} ${expected.description} [Headings should not contain inline code.] [Context: "\`heading\`"]`, + `${errorPath}:19 error ${expected.name} ${expected.description} [Headings should not contain inline code.] [Context: "\`other heading\`"]`, + `${errorPath}:19 error ${expected.name} ${expected.description} [Headings should not contain inline code.] [Context: "\`flagged\`"]`, + ]); + }); + }); + + describe("Fix", () => { + it("Strips inline code blocks in headings", 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/TOP002_noCodeInHeadings/tests/fixed_test.md b/markdownlint/TOP002_noCodeInHeadings/tests/fixed_test.md new file mode 100644 index 00000000000..5908673cbf1 --- /dev/null +++ b/markdownlint/TOP002_noCodeInHeadings/tests/fixed_test.md @@ -0,0 +1,39 @@ +### Introduction + +Text content. + +### Lesson overview + +This section contains a general overview of topics that you will learn in this lesson. + +- LO item. + +### This heading should NOT be flagged + +Some more content. + +### This heading SHOULD be flagged + +Some content. + +### This other heading will get flagged twice + +### 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/TOP002_noCodeInHeadings/tests/TOP002_test.md b/markdownlint/TOP002_noCodeInHeadings/tests/test.md similarity index 100% rename from markdownlint/TOP002_noCodeInHeadings/tests/TOP002_test.md rename to markdownlint/TOP002_noCodeInHeadings/tests/test.md From 9284065a3bff6411fbf522fbfd05d4a81e65efe5 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:02:54 +0000 Subject: [PATCH 09/28] Rename TOP003 test md files --- ...{TOP003_test_content-around-list.md => content_around_list.md} | 0 .../tests/{TOP003_test_empty-section.md => empty_section.md} | 0 .../{TOP003_test_incorrect-content.md => incorrect_content.md} | 0 .../tests/{TOP003_test_missing-list.md => missing_list.md} | 0 .../tests/{TOP003_test_missing-wrapper.md => missing_wrapper.md} | 0 ...test_nested-and-ordered-list.md => nested_and_ordered_list.md} | 0 .../tests/{TOP003_test_valid.md => valid.md} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename markdownlint/TOP003_defaultSectionContent/tests/{TOP003_test_content-around-list.md => content_around_list.md} (100%) rename markdownlint/TOP003_defaultSectionContent/tests/{TOP003_test_empty-section.md => empty_section.md} (100%) rename markdownlint/TOP003_defaultSectionContent/tests/{TOP003_test_incorrect-content.md => incorrect_content.md} (100%) rename markdownlint/TOP003_defaultSectionContent/tests/{TOP003_test_missing-list.md => missing_list.md} (100%) rename markdownlint/TOP003_defaultSectionContent/tests/{TOP003_test_missing-wrapper.md => missing_wrapper.md} (100%) rename markdownlint/TOP003_defaultSectionContent/tests/{TOP003_test_nested-and-ordered-list.md => nested_and_ordered_list.md} (100%) rename markdownlint/TOP003_defaultSectionContent/tests/{TOP003_test_valid.md => valid.md} (100%) diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_content-around-list.md b/markdownlint/TOP003_defaultSectionContent/tests/content_around_list.md similarity index 100% rename from markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_content-around-list.md rename to markdownlint/TOP003_defaultSectionContent/tests/content_around_list.md diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_empty-section.md b/markdownlint/TOP003_defaultSectionContent/tests/empty_section.md similarity index 100% rename from markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_empty-section.md rename to markdownlint/TOP003_defaultSectionContent/tests/empty_section.md diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_incorrect-content.md b/markdownlint/TOP003_defaultSectionContent/tests/incorrect_content.md similarity index 100% rename from markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_incorrect-content.md rename to markdownlint/TOP003_defaultSectionContent/tests/incorrect_content.md diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_missing-list.md b/markdownlint/TOP003_defaultSectionContent/tests/missing_list.md similarity index 100% rename from markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_missing-list.md rename to markdownlint/TOP003_defaultSectionContent/tests/missing_list.md diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_missing-wrapper.md b/markdownlint/TOP003_defaultSectionContent/tests/missing_wrapper.md similarity index 100% rename from markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_missing-wrapper.md rename to markdownlint/TOP003_defaultSectionContent/tests/missing_wrapper.md diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_nested-and-ordered-list.md b/markdownlint/TOP003_defaultSectionContent/tests/nested_and_ordered_list.md similarity index 100% rename from markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_nested-and-ordered-list.md rename to markdownlint/TOP003_defaultSectionContent/tests/nested_and_ordered_list.md diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_valid.md b/markdownlint/TOP003_defaultSectionContent/tests/valid.md similarity index 100% rename from markdownlint/TOP003_defaultSectionContent/tests/TOP003_test_valid.md rename to markdownlint/TOP003_defaultSectionContent/tests/valid.md From 55df4d2c42f85953819d5e28517dd5d6a4ce4f14 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:04:02 +0000 Subject: [PATCH 10/28] Add tests for TOP003 custom rule --- .../TOP003_defaultSectionContent.js | 2 +- .../tests/TOP003.test.js | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js diff --git a/markdownlint/TOP003_defaultSectionContent/TOP003_defaultSectionContent.js b/markdownlint/TOP003_defaultSectionContent/TOP003_defaultSectionContent.js index b19abf1606a..a817bafd424 100644 --- a/markdownlint/TOP003_defaultSectionContent/TOP003_defaultSectionContent.js +++ b/markdownlint/TOP003_defaultSectionContent/TOP003_defaultSectionContent.js @@ -85,7 +85,7 @@ function getListSectionErrors(sectionTokens, section) { const sectionStartsWithList = tokensAfterHeading[0].line.startsWith("- "); const errorDetail = sectionStartsWithList ? `Expect default content to precede unordered list of ${listItemsName}: "${listSectionsDefaultContent[section]}"` - : `Expected: "${listSectionsDefaultContent[section]}"; Actual: "${tokensAfterHeading[0].line}",`; + : `Expected: "${listSectionsDefaultContent[section]}"; Actual: "${tokensAfterHeading[0].line}"`; let replacementText = listSectionsDefaultContent[section]; if (sectionStartsWithList) { diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js b/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js new file mode 100644 index 00000000000..bae07cd120d --- /dev/null +++ b/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js @@ -0,0 +1,96 @@ +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("../TOP003_defaultSectionContent"); + +const pathInRepo = "markdownlint/TOP003_defaultSectionContent/tests"; +const expected = { + name: "TOP003/default-section-content", + description: "Sections have default content", + information: new URL( + "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP003.md", + ), +}; + +describe("TOP003", () => { + it("Links to the TOP003 docs", () => { + assert.deepEqual(rule.information, expected.information); + }); + + it("Flags when extra content surrounds default section lists", async () => { + const filePath = "./content_around_list.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:9 error ${expected.name} ${expected.description} [Only an unordered list of lesson overviews can follow the default content.]`, + `${errorPath}:13 error ${expected.name} ${expected.description} [There should be no additional content after the unordered list of lesson overviews]`, + ]); + }); + + it("Flags when sections with default content are empty", async () => { + const filePath = "./empty_section.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:5 error ${expected.name} ${expected.description} [The lesson overview section cannot be empty]`, + `${errorPath}:19 error ${expected.name} ${expected.description} [The knowledge check section cannot be empty]`, + `${errorPath}:21 error ${expected.name} ${expected.description} [The additional resources section cannot be empty]`, + ]); + }); + + it("Flags when sections have incorrect default content", async () => { + const filePath = "./incorrect_content.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:7 error ${expected.name} ${expected.description} [Expected: "This section contains a general overview of topics that you will learn in this lesson."; Actual: "This section has the wrong text following the heading that should flag an error."]`, + `${errorPath}:25 error ${expected.name} ${expected.description} [Expect default content to precede unordered list of knowledge checks: "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."]`, + ]); + }); + + it("Flags when a section does not have a required unordered list", async () => { + const filePath = "./missing_list.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:7 error ${expected.name} ${expected.description} [Must include an unordered list of lesson overviews in the "lesson overview" section]`, + `${errorPath}:23 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, + `${errorPath}:27 error ${expected.name} ${expected.description} [Must include an unordered list of additional resources in the "additional resources" section]`, + ]); + }); + + it("Flags when the assignment section is missing the necessary div wrapper", async () => { + const filePath = "./missing_wrapper.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:15 error ${expected.name} ${expected.description} [Assignment sections must include an HTML div element with class="lesson-content__panel" and markdown="1" attributes]`, + ]); + }); + + it("Flags when ordered or nested lists are used for unordered list sections", async () => { + const filePath = "./nested_and_ordered_list.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:10 error ${expected.name} ${expected.description} [The lesson overview section must not contain nested lists.]`, + `${errorPath}:29 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, + `${errorPath}:29 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, + `${errorPath}:30 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, + ]); + }); + + it("Does not flag any errors if no violations", async () => { + const filePath = "./valid.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); +}); From b3553bb28c55f3580c86af775328ffc42f04ae0b Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:06:00 +0000 Subject: [PATCH 11/28] Add tests for TOP003 fixes Not all parts of TOP003 are fixable --- .../tests/TOP003.test.js | 187 +++++++++++------- .../tests/fixed_content_around_list.md | 35 ++++ .../tests/fixed_incorrect_content.md | 33 ++++ .../tests/fixed_nested_and_ordered_list.md | 36 ++++ 4 files changed, 215 insertions(+), 76 deletions(-) create mode 100644 markdownlint/TOP003_defaultSectionContent/tests/fixed_content_around_list.md create mode 100644 markdownlint/TOP003_defaultSectionContent/tests/fixed_incorrect_content.md create mode 100644 markdownlint/TOP003_defaultSectionContent/tests/fixed_nested_and_ordered_list.md diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js b/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js index bae07cd120d..f8299859b4c 100644 --- a/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js +++ b/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js @@ -1,7 +1,9 @@ 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("../TOP003_defaultSectionContent"); const pathInRepo = "markdownlint/TOP003_defaultSectionContent/tests"; @@ -14,83 +16,116 @@ const expected = { }; describe("TOP003", () => { - it("Links to the TOP003 docs", () => { - assert.deepEqual(rule.information, expected.information); + describe("Lint", () => { + it("Links to the TOP003 docs", () => { + assert.deepEqual(rule.information, expected.information); + }); + + it("Flags when extra content surrounds default section lists", async () => { + const filePath = "./content_around_list.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:9 error ${expected.name} ${expected.description} [Only an unordered list of lesson overviews can follow the default content.]`, + `${errorPath}:13 error ${expected.name} ${expected.description} [There should be no additional content after the unordered list of lesson overviews]`, + ]); + }); + + it("Flags when sections with default content are empty", async () => { + const filePath = "./empty_section.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:5 error ${expected.name} ${expected.description} [The lesson overview section cannot be empty]`, + `${errorPath}:19 error ${expected.name} ${expected.description} [The knowledge check section cannot be empty]`, + `${errorPath}:21 error ${expected.name} ${expected.description} [The additional resources section cannot be empty]`, + ]); + }); + + it("Flags when sections have incorrect default content", async () => { + const filePath = "./incorrect_content.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:7 error ${expected.name} ${expected.description} [Expected: "This section contains a general overview of topics that you will learn in this lesson."; Actual: "This section has the wrong text following the heading that should flag an error."]`, + `${errorPath}:25 error ${expected.name} ${expected.description} [Expect default content to precede unordered list of knowledge checks: "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."]`, + ]); + }); + + it("Flags when a section does not have a required unordered list", async () => { + const filePath = "./missing_list.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:7 error ${expected.name} ${expected.description} [Must include an unordered list of lesson overviews in the "lesson overview" section]`, + `${errorPath}:23 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, + `${errorPath}:27 error ${expected.name} ${expected.description} [Must include an unordered list of additional resources in the "additional resources" section]`, + ]); + }); + + it("Flags when the assignment section is missing the necessary div wrapper", async () => { + const filePath = "./missing_wrapper.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:15 error ${expected.name} ${expected.description} [Assignment sections must include an HTML div element with class="lesson-content__panel" and markdown="1" attributes]`, + ]); + }); + + it("Flags when ordered or nested lists are used for unordered list sections", async () => { + const filePath = "./nested_and_ordered_list.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:10 error ${expected.name} ${expected.description} [The lesson overview section must not contain nested lists.]`, + `${errorPath}:29 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, + `${errorPath}:29 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, + `${errorPath}:30 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, + ]); + }); + + it("Does not flag any errors if no violations", async () => { + const filePath = "./valid.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); }); - it("Flags when extra content surrounds default section lists", async () => { - const filePath = "./content_around_list.md"; - const errorPath = join(pathInRepo, filePath); - const lintErrors = await getLintErrors(filePath); - - assert.deepEqual(lintErrors, [ - `${errorPath}:9 error ${expected.name} ${expected.description} [Only an unordered list of lesson overviews can follow the default content.]`, - `${errorPath}:13 error ${expected.name} ${expected.description} [There should be no additional content after the unordered list of lesson overviews]`, - ]); - }); - - it("Flags when sections with default content are empty", async () => { - const filePath = "./empty_section.md"; - const errorPath = join(pathInRepo, filePath); - const lintErrors = await getLintErrors(filePath); - - assert.deepEqual(lintErrors, [ - `${errorPath}:5 error ${expected.name} ${expected.description} [The lesson overview section cannot be empty]`, - `${errorPath}:19 error ${expected.name} ${expected.description} [The knowledge check section cannot be empty]`, - `${errorPath}:21 error ${expected.name} ${expected.description} [The additional resources section cannot be empty]`, - ]); - }); - - it("Flags when sections have incorrect default content", async () => { - const filePath = "./incorrect_content.md"; - const errorPath = join(pathInRepo, filePath); - const lintErrors = await getLintErrors(filePath); - - assert.deepEqual(lintErrors, [ - `${errorPath}:7 error ${expected.name} ${expected.description} [Expected: "This section contains a general overview of topics that you will learn in this lesson."; Actual: "This section has the wrong text following the heading that should flag an error."]`, - `${errorPath}:25 error ${expected.name} ${expected.description} [Expect default content to precede unordered list of knowledge checks: "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."]`, - ]); - }); - - it("Flags when a section does not have a required unordered list", async () => { - const filePath = "./missing_list.md"; - const errorPath = join(pathInRepo, filePath); - const lintErrors = await getLintErrors(filePath); - - assert.deepEqual(lintErrors, [ - `${errorPath}:7 error ${expected.name} ${expected.description} [Must include an unordered list of lesson overviews in the "lesson overview" section]`, - `${errorPath}:23 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, - `${errorPath}:27 error ${expected.name} ${expected.description} [Must include an unordered list of additional resources in the "additional resources" section]`, - ]); - }); - - it("Flags when the assignment section is missing the necessary div wrapper", async () => { - const filePath = "./missing_wrapper.md"; - const errorPath = join(pathInRepo, filePath); - const lintErrors = await getLintErrors(filePath); - - assert.deepEqual(lintErrors, [ - `${errorPath}:15 error ${expected.name} ${expected.description} [Assignment sections must include an HTML div element with class="lesson-content__panel" and markdown="1" attributes]`, - ]); - }); - - it("Flags when ordered or nested lists are used for unordered list sections", async () => { - const filePath = "./nested_and_ordered_list.md"; - const errorPath = join(pathInRepo, filePath); - const lintErrors = await getLintErrors(filePath); - - assert.deepEqual(lintErrors, [ - `${errorPath}:10 error ${expected.name} ${expected.description} [The lesson overview section must not contain nested lists.]`, - `${errorPath}:29 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, - `${errorPath}:29 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, - `${errorPath}:30 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, - ]); - }); - - it("Does not flag any errors if no violations", async () => { - const filePath = "./valid.md"; - const lintErrors = await getLintErrors(filePath); - - assert.deepEqual(lintErrors, []); + describe("Fix", () => { + it("Converts ordered lists to unordered lists", async () => { + const fixedFileContents = await fixLintErrors( + "./nested_and_ordered_list.md", + ); + const correctFile = await readFile( + join(__dirname, "./fixed_nested_and_ordered_list.md"), + ); + + assert.equal(fixedFileContents, correctFile.toString()); + }); + + it("Inserts/replaces missing or incorrect default section content", async () => { + const fixedFileContents = await fixLintErrors("./incorrect_content.md"); + const correctFile = await readFile( + join(__dirname, "./fixed_incorrect_content.md"), + ); + + assert.equal(fixedFileContents, correctFile.toString()); + }); + + it("Removes flagged content around default section lists", async () => { + const fixedFileContents = await fixLintErrors("./content_around_list.md"); + const correctFile = await readFile( + join(__dirname, "./fixed_content_around_list.md"), + ); + + assert.equal(fixedFileContents, correctFile.toString()); + }); }); }); diff --git a/markdownlint/TOP003_defaultSectionContent/tests/fixed_content_around_list.md b/markdownlint/TOP003_defaultSectionContent/tests/fixed_content_around_list.md new file mode 100644 index 00000000000..536f51dc867 --- /dev/null +++ b/markdownlint/TOP003_defaultSectionContent/tests/fixed_content_around_list.md @@ -0,0 +1,35 @@ +### Introduction + +Text content + +### Lesson overview + +This section contains a general overview of topics that you will learn in this lesson. + + +- LO item. + + +### Custom section + +Text content + +### 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/TOP003_defaultSectionContent/tests/fixed_incorrect_content.md b/markdownlint/TOP003_defaultSectionContent/tests/fixed_incorrect_content.md new file mode 100644 index 00000000000..daff977f7b9 --- /dev/null +++ b/markdownlint/TOP003_defaultSectionContent/tests/fixed_incorrect_content.md @@ -0,0 +1,33 @@ +### Introduction + +Text content + +### Lesson overview + +This section contains a general overview of topics that you will learn in this lesson. + +- LO item. + +### Custom section + +Text content + +### 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 that should flag an error + +### Additional resources + +This section contains helpful links to related content. It isn't required, so consider it supplemental. + +- AR item diff --git a/markdownlint/TOP003_defaultSectionContent/tests/fixed_nested_and_ordered_list.md b/markdownlint/TOP003_defaultSectionContent/tests/fixed_nested_and_ordered_list.md new file mode 100644 index 00000000000..94f117b6148 --- /dev/null +++ b/markdownlint/TOP003_defaultSectionContent/tests/fixed_nested_and_ordered_list.md @@ -0,0 +1,36 @@ +### Introduction + +Text content + +### Lesson overview + +This section contains a general overview of topics that you will learn in this lesson. + +- An item. + - A nested item that should flag an error. +- Unnested list item. + +### Custom section + +Text content + +### 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 that should flag an error +- Another KC item that should flag an error + +### Additional resources + +This section contains helpful links to related content. It isn't required, so consider it supplemental. + +- AR item From 00997c6922a76795e4caf031ebf1a4af9e8578a2 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:08:29 +0000 Subject: [PATCH 12/28] Add tests for TOP004 custom rule --- .../tests/TOP004.test.js | 108 ++++++++++++++++++ ...OP004_test_invalid.md => guide_invalid.md} | 0 ...de_TOP004_test_valid.md => guide_valid.md} | 0 ..._missing_heading.md => missing_heading.md} | 0 ...004_test_invalid.md => project_invalid.md} | 2 +- ..._TOP004_test_valid.md => project_valid.md} | 0 ...es.md => valid_no_additional_resources.md} | 0 ....md => valid_with_additional_resources.md} | 0 ...dcard_level.md => wrong_wildcard_level.md} | 0 9 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 markdownlint/TOP004_lessonHeadings/tests/TOP004.test.js rename markdownlint/TOP004_lessonHeadings/tests/_guides/{guide_TOP004_test_invalid.md => guide_invalid.md} (100%) rename markdownlint/TOP004_lessonHeadings/tests/_guides/{guide_TOP004_test_valid.md => guide_valid.md} (100%) rename markdownlint/TOP004_lessonHeadings/tests/{TOP004_test_missing_heading.md => missing_heading.md} (100%) rename markdownlint/TOP004_lessonHeadings/tests/{project_TOP004_test_invalid.md => project_invalid.md} (55%) rename markdownlint/TOP004_lessonHeadings/tests/{project_TOP004_test_valid.md => project_valid.md} (100%) rename markdownlint/TOP004_lessonHeadings/tests/{TOP004_test_valid_no_additional_resources.md => valid_no_additional_resources.md} (100%) rename markdownlint/TOP004_lessonHeadings/tests/{TOP004_test_valid_with_additional_resources.md => valid_with_additional_resources.md} (100%) rename markdownlint/TOP004_lessonHeadings/tests/{TOP004_test_wrong_wildcard_level.md => wrong_wildcard_level.md} (100%) diff --git a/markdownlint/TOP004_lessonHeadings/tests/TOP004.test.js b/markdownlint/TOP004_lessonHeadings/tests/TOP004.test.js new file mode 100644 index 00000000000..c2a4a7273e1 --- /dev/null +++ b/markdownlint/TOP004_lessonHeadings/tests/TOP004.test.js @@ -0,0 +1,108 @@ +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("../TOP004_lessonHeadings"); + +const pathInRepo = "markdownlint/TOP004_lessonHeadings/tests"; +const expected = { + name: "TOP004/lesson-headings", + description: "Required heading structure", + information: new URL( + "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP004.md", + ), +}; + +describe("TOP004", () => { + it("Links to the TOP004 docs", () => { + assert.deepEqual(rule.information, expected.information); + }); + + it("Flags when a required heading is missing", async () => { + const filePath = "./missing_heading.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:5 error ${expected.name} ${expected.description} [Expected: ### Lesson overview; Actual: ### Custom section]`, + ]); + }); + + it("Flags when a level-specific wildcard heading uses the wrong level", async () => { + const filePath = "./wrong_wildcard_level.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:21 error ${expected.name} ${expected.description} [Expected: h4 heading; Actual: h3 heading] [Context: "### An invalid wildcard heading"]`, + ]); + }); + + it("Does not flag any errors if no heading structure violations", async () => { + const filePath = "./valid_with_additional_resources.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); + + it("Does not flag in a valid lesson file without additional resources", async () => { + const filePath = "./valid_no_additional_resources.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); + + describe("Projects", () => { + it("Flags incorrect heading structure in a project", async () => { + const filePath = "./project_invalid.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:12 error ${expected.name} ${expected.description} [Missing heading (case sensitive): ### Assignment] [Context: "### Assignment"]`, + ]); + }); + + it("Does not flag a project if no heading structure violations", async () => { + const filePath = "./project_valid.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); + }); + + describe("Lesson exceptions", () => { + it("Does not flag in a Conclusion lesson", async () => { + const filePath = "./conclusion.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); + + it("Does not flag in a How This Course Will Work lesson", async () => { + const filePath = "./how_this_course_will_work.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); + }); + + describe("Guides", () => { + it("Flags when first heading does not start correctly", async () => { + const filePath = "./_guides/guide_invalid.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:1 error ${expected.name} ${expected.description} [Expected: heading starting with "### Guide: "; Actual: ### Installation Guide]`, + ]); + }); + + it("Does not flag any errors if no violations", async () => { + const filePath = "./_guides/guide_valid.md"; + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, []); + }); + }); +}); diff --git a/markdownlint/TOP004_lessonHeadings/tests/_guides/guide_TOP004_test_invalid.md b/markdownlint/TOP004_lessonHeadings/tests/_guides/guide_invalid.md similarity index 100% rename from markdownlint/TOP004_lessonHeadings/tests/_guides/guide_TOP004_test_invalid.md rename to markdownlint/TOP004_lessonHeadings/tests/_guides/guide_invalid.md diff --git a/markdownlint/TOP004_lessonHeadings/tests/_guides/guide_TOP004_test_valid.md b/markdownlint/TOP004_lessonHeadings/tests/_guides/guide_valid.md similarity index 100% rename from markdownlint/TOP004_lessonHeadings/tests/_guides/guide_TOP004_test_valid.md rename to markdownlint/TOP004_lessonHeadings/tests/_guides/guide_valid.md diff --git a/markdownlint/TOP004_lessonHeadings/tests/TOP004_test_missing_heading.md b/markdownlint/TOP004_lessonHeadings/tests/missing_heading.md similarity index 100% rename from markdownlint/TOP004_lessonHeadings/tests/TOP004_test_missing_heading.md rename to markdownlint/TOP004_lessonHeadings/tests/missing_heading.md diff --git a/markdownlint/TOP004_lessonHeadings/tests/project_TOP004_test_invalid.md b/markdownlint/TOP004_lessonHeadings/tests/project_invalid.md similarity index 55% rename from markdownlint/TOP004_lessonHeadings/tests/project_TOP004_test_invalid.md rename to markdownlint/TOP004_lessonHeadings/tests/project_invalid.md index e6b35c8fd44..a87cacf843d 100644 --- a/markdownlint/TOP004_lessonHeadings/tests/project_TOP004_test_invalid.md +++ b/markdownlint/TOP004_lessonHeadings/tests/project_invalid.md @@ -1,6 +1,6 @@ ### Introduction -This file should not be flagged with any errors. +This file should flag a TOP004 error due to a missing assignment assignment section. ### Custom section diff --git a/markdownlint/TOP004_lessonHeadings/tests/project_TOP004_test_valid.md b/markdownlint/TOP004_lessonHeadings/tests/project_valid.md similarity index 100% rename from markdownlint/TOP004_lessonHeadings/tests/project_TOP004_test_valid.md rename to markdownlint/TOP004_lessonHeadings/tests/project_valid.md diff --git a/markdownlint/TOP004_lessonHeadings/tests/TOP004_test_valid_no_additional_resources.md b/markdownlint/TOP004_lessonHeadings/tests/valid_no_additional_resources.md similarity index 100% rename from markdownlint/TOP004_lessonHeadings/tests/TOP004_test_valid_no_additional_resources.md rename to markdownlint/TOP004_lessonHeadings/tests/valid_no_additional_resources.md diff --git a/markdownlint/TOP004_lessonHeadings/tests/TOP004_test_valid_with_additional_resources.md b/markdownlint/TOP004_lessonHeadings/tests/valid_with_additional_resources.md similarity index 100% rename from markdownlint/TOP004_lessonHeadings/tests/TOP004_test_valid_with_additional_resources.md rename to markdownlint/TOP004_lessonHeadings/tests/valid_with_additional_resources.md diff --git a/markdownlint/TOP004_lessonHeadings/tests/TOP004_test_wrong_wildcard_level.md b/markdownlint/TOP004_lessonHeadings/tests/wrong_wildcard_level.md similarity index 100% rename from markdownlint/TOP004_lessonHeadings/tests/TOP004_test_wrong_wildcard_level.md rename to markdownlint/TOP004_lessonHeadings/tests/wrong_wildcard_level.md From c8c546fa0107300888f05dbe85634246f35a75e7 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:09:40 +0000 Subject: [PATCH 13/28] Split TOP005 test .md files to valid/invalid cases --- .../tests/{TOP005_test.md => flagged_tags.md} | 59 +------------ .../tests/ignored_tags.md | 86 +++++++++++++++++++ 2 files changed, 87 insertions(+), 58 deletions(-) rename markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/{TOP005_test.md => flagged_tags.md} (62%) create mode 100644 markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/ignored_tags.md diff --git a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005_test.md b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/flagged_tags.md similarity index 62% rename from markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005_test.md rename to markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/flagged_tags.md index b121539cffe..ae4bff78ac6 100644 --- a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005_test.md +++ b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/flagged_tags.md @@ -12,16 +12,10 @@ 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. @@ -48,61 +42,13 @@ Also invalidates when HTML blocks are chained without blank lines between them. ```markdown
-The only exception to blank lines is a code block delimiter. - -
-``` - -```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 -

- 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.

-`; -``` - ```markdown

- But does not like it if done in a non-HTML/JSX code block + Flags such tags if not in an ignored code block (like HTML/JS/JSX etc.)

@@ -112,9 +58,6 @@ const htmlString = `
``` - -### `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/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 From 84d181e50a8811112bdafd1f47b6c8130d0ba109 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:10:37 +0000 Subject: [PATCH 14/28] Add tests for TOP005 custom rule --- .../tests/TOP005.test.js | 65 ++++++++++++++ .../tests/fixed_flagged_tags.md | 85 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js create mode 100644 markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/fixed_flagged_tags.md diff --git a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js new file mode 100644 index 00000000000..008ecd189cb --- /dev/null +++ b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js @@ -0,0 +1,65 @@ +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("../TOP005_blanksAroundMultilineHtmlTags"); + +const pathInRepo = "markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests"; +const expected = { + name: "TOP005/blanks-around-multiline-html-tags", + description: + "Multiline HTML tags should be surrounded by blank lines or code block delimiters", + information: new URL( + "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP005.md", + ), +}; + +describe("TOP005", () => { + describe("Lint", () => { + it("Links to the TOP005 docs", () => { + assert.deepEqual(rule.information, expected.information); + }); + + it("Flags when markdown multiline HTML tags are not surrounded by blank lines or code block delimiters", async () => { + const filePath = "./flagged_tags.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:19 error ${expected.name} ${expected.description} [Expected a blank line or a code block delimiter (\`\`\`) after the tag] [Context: "
"]`, + `${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("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/fixed_flagged_tags.md b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/fixed_flagged_tags.md new file mode 100644 index 00000000000..1e56868baa4 --- /dev/null +++ b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/fixed_flagged_tags.md @@ -0,0 +1,85 @@ +### 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 From 72c5ec9f64298515e676d2a4b5cd8d07cdfacc46 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:12:12 +0000 Subject: [PATCH 15/28] Add tests for TOP006 custom rule --- .../TOP006_fullFencedCodeLanguage.js | 2 +- .../tests/TOP006.test.js | 0 .../tests/fixed_test.md | 110 ++++++++++++++++++ .../tests/{TOP006_test.md => test.md} | 2 +- 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js create mode 100644 markdownlint/TOP006_fullFencedCodeLanguage/tests/fixed_test.md rename markdownlint/TOP006_fullFencedCodeLanguage/tests/{TOP006_test.md => test.md} (97%) 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..e69de29bb2d 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
From 3b6f24c1bf1cd06e95809ef3c68b4f1de78fd550 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:13:52 +0000 Subject: [PATCH 16/28] Add tests for TOP007 custom rule --- .../tests/TOP006.test.js | 52 +++++++++++++++++ .../tests/TOP007.test.js | 57 +++++++++++++++++++ .../tests/anchors_in_markdown.md | 40 +++++++++++++ .../tests/fixed_anchors_in_markdown.md | 40 +++++++++++++ .../{TOP007_test.md => valid_anchors.md} | 19 +------ 5 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js create mode 100644 markdownlint/TOP007_useMarkdownLinks/tests/anchors_in_markdown.md create mode 100644 markdownlint/TOP007_useMarkdownLinks/tests/fixed_anchors_in_markdown.md rename markdownlint/TOP007_useMarkdownLinks/tests/{TOP007_test.md => valid_anchors.md} (67%) diff --git a/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js b/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js index e69de29bb2d..21ba3b2ea32 100644 --- a/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js +++ b/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js @@ -0,0 +1,52 @@ +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("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/TOP007_useMarkdownLinks/tests/TOP007.test.js b/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js new file mode 100644 index 00000000000..62196f73f30 --- /dev/null +++ b/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js @@ -0,0 +1,57 @@ +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("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 From 8aa683128410ba05b3fae1284b9838121602f547 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:14:58 +0000 Subject: [PATCH 17/28] Add tests for TOP008 custom rule --- .../tests/TOP008.test.js | 50 ++++++++++++++++ .../tests/fixed_test.md | 57 +++++++++++++++++++ .../tests/{TOP008_test.md => test.md} | 0 3 files changed, 107 insertions(+) create mode 100644 markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js create mode 100644 markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/fixed_test.md rename markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/{TOP008_test.md => test.md} (100%) diff --git a/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js new file mode 100644 index 00000000000..ee0c67bc8d4 --- /dev/null +++ b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js @@ -0,0 +1,50 @@ +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("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 From 08d8270bc87d29d24d6117258704dbd131dbdd0a Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:16:11 +0000 Subject: [PATCH 18/28] Add tests for TOP009 custom rule --- .../tests/TOP009.test.js | 50 +++++++++++++++++++ ...09_capital_letter.md => capital_letter.md} | 0 ..._punctuation.md => invalid_punctuation.md} | 0 .../tests/{TOP009_test_valid.md => valid.md} | 0 4 files changed, 50 insertions(+) create mode 100644 markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/TOP009.test.js rename markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/{TOP009_capital_letter.md => capital_letter.md} (100%) rename markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/{TOP009_invalid_punctuation.md => invalid_punctuation.md} (100%) rename markdownlint/TOP009_lessonOverviewItemsSentenceStructure/tests/{TOP009_test_valid.md => valid.md} (100%) 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 From 49798eab192ab313012d044a78e6e1419cc67a62 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:17:08 +0000 Subject: [PATCH 19/28] Add tests for TOP010 custom rule --- .../tests/TOP010.test.js | 46 ++++++++++++++++ .../tests/fixed_test.md | 55 +++++++++++++++++++ .../tests/{TOP010_test.md => test.md} | 0 3 files changed, 101 insertions(+) create mode 100644 markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js create mode 100644 markdownlint/TOP010_useLazyNumbering/tests/fixed_test.md rename markdownlint/TOP010_useLazyNumbering/tests/{TOP010_test.md => test.md} (100%) diff --git a/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js b/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js new file mode 100644 index 00000000000..e17085023fb --- /dev/null +++ b/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js @@ -0,0 +1,46 @@ +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("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 From 30447ffba46bdb7bee933f54466c47ac903c1743 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:19:09 +0000 Subject: [PATCH 20/28] Add tests for TOP011 custom rule --- .../TOP011_headingIndentation.js | 2 + .../tests/TOP011.test.js | 45 +++++++++++ .../tests/fixed_test.md | 75 +++++++++++++++++++ .../tests/{TOP011_test.md => test.md} | 0 4 files changed, 122 insertions(+) create mode 100644 markdownlint/TOP011_headingIndentation/tests/TOP011.test.js create mode 100644 markdownlint/TOP011_headingIndentation/tests/fixed_test.md rename markdownlint/TOP011_headingIndentation/tests/{TOP011_test.md => test.md} (100%) 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..3012a81fd7f --- /dev/null +++ b/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js @@ -0,0 +1,45 @@ +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("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 From 78b6c5a19dbbddd9a8aca96acf1ceb6d05ff1d18 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:20:20 +0000 Subject: [PATCH 21/28] Add tests for TOP012 custom rule --- .../TOP012_noteBoxHeadings.js | 1 + .../tests/TOP012.test.js | 45 +++++++++++++ .../tests/fixed_test.md | 63 +++++++++++++++++++ .../tests/{TOP012_test.md => test.md} | 0 4 files changed, 109 insertions(+) create mode 100644 markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js create mode 100644 markdownlint/TOP012_noteBoxHeadings/tests/fixed_test.md rename markdownlint/TOP012_noteBoxHeadings/tests/{TOP012_test.md => test.md} (100%) 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..e3f67b9b7dc --- /dev/null +++ b/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js @@ -0,0 +1,45 @@ +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 or incorrectly levelled headings", async () => { + const filePath = "./test.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"]`, + `${errorPath}:41 error ${expected.name} ${expected.description} [Note box is missing a heading. Note boxes must start with a level 4 heading (####).]`, + ]); + }); + }); + + describe("Fix", () => { + it("Converts incorrectly levelled note box headings to level 4", 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/TOP012_noteBoxHeadings/tests/fixed_test.md b/markdownlint/TOP012_noteBoxHeadings/tests/fixed_test.md new file mode 100644 index 00000000000..d06609e8ad1 --- /dev/null +++ b/markdownlint/TOP012_noteBoxHeadings/tests/fixed_test.md @@ -0,0 +1,63 @@ +### 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. + +
+ +
+ +Note boxes without a heading will flag a missing heading error + +
+ +### 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/test.md similarity index 100% rename from markdownlint/TOP012_noteBoxHeadings/tests/TOP012_test.md rename to markdownlint/TOP012_noteBoxHeadings/tests/test.md From 1c7754794550749e9591999b6eb2b6b3b46f1ef5 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:21:14 +0000 Subject: [PATCH 22/28] Add tests for TOP013 custom rule --- .../tests/TOP013.test.js | 33 +++++++++++++++++++ .../tests/{TOP013.md => test.md} | 0 2 files changed, 33 insertions(+) create mode 100644 markdownlint/TOP013_descriptiveHeadings/tests/TOP013.test.js rename markdownlint/TOP013_descriptiveHeadings/tests/{TOP013.md => test.md} (100%) diff --git a/markdownlint/TOP013_descriptiveHeadings/tests/TOP013.test.js b/markdownlint/TOP013_descriptiveHeadings/tests/TOP013.test.js new file mode 100644 index 00000000000..8774fe8ae96 --- /dev/null +++ b/markdownlint/TOP013_descriptiveHeadings/tests/TOP013.test.js @@ -0,0 +1,33 @@ +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("../TOP013_descriptiveHeadings"); + +const pathInRepo = "markdownlint/TOP013_descriptiveHeadings/tests"; +const expected = { + name: "TOP013/descriptive-headings", + description: "Headings must have descriptive text", + information: new URL( + "https://github.com/TheOdinProject/curriculum/blob/main/markdownlint/docs/TOP013.md", + ), +}; + +describe("TOP013", () => { + it("Links to the TOP013 docs", () => { + assert.deepEqual(rule.information, expected.information); + }); + + it("Flags blacklisted headings", async () => { + const filePath = "./test.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:15 error ${expected.name} ${expected.description} ["Important" is not sufficiently descriptive by itself. Use a more descriptive heading that briefly but clearly summarizes the content of the section.] [Context: "### Important"]`, + `${errorPath}:19 error ${expected.name} ${expected.description} ["A warning" is not sufficiently descriptive by itself. Use a more descriptive heading that briefly but clearly summarizes the content of the section.] [Context: "#### A warning"]`, + `${errorPath}:25 error ${expected.name} ${expected.description} ["Remember" is not sufficiently descriptive by itself. Use a more descriptive heading that briefly but clearly summarizes the content of the section.] [Context: "#### Remember!"]`, + `${errorPath}:35 error ${expected.name} ${expected.description} ["Important note" is not sufficiently descriptive by itself. Use a more descriptive heading that briefly but clearly summarizes the content of the section.] [Context: " #### Important note"]`, + ]); + }); +}); diff --git a/markdownlint/TOP013_descriptiveHeadings/tests/TOP013.md b/markdownlint/TOP013_descriptiveHeadings/tests/test.md similarity index 100% rename from markdownlint/TOP013_descriptiveHeadings/tests/TOP013.md rename to markdownlint/TOP013_descriptiveHeadings/tests/test.md From da37a38694ca07a95907d3edc8ba26aeb7e52415 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:29:35 +0000 Subject: [PATCH 23/28] Ignore package{-lock}.json in codespell action --- .github/workflows/codespell.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' From 9c1c2eb7656fed2623dd07f3f279f19b529b3c13 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 14:26:15 +0000 Subject: [PATCH 24/28] Test that fixable rule violations are fully resolved by a fix run Original fix tests only assert that running the fix script matches the respective `fixed_*` md file, but do not actually test that the applied fixes are sensible and fully resolve the relevant errors. --- .../TOP002_noCodeInHeadings/tests/TOP002.test.js | 10 ++++++++++ .../tests/TOP005.test.js | 10 ++++++++++ .../TOP006_fullFencedCodeLanguage/tests/TOP006.test.js | 10 ++++++++++ .../TOP007_useMarkdownLinks/tests/TOP007.test.js | 10 ++++++++++ .../tests/TOP008.test.js | 10 ++++++++++ .../TOP010_useLazyNumbering/tests/TOP010.test.js | 10 ++++++++++ .../TOP011_headingIndentation/tests/TOP011.test.js | 10 ++++++++++ 7 files changed, 70 insertions(+) diff --git a/markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js b/markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js index 5a7f08f3721..c8d4d2d0803 100644 --- a/markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js +++ b/markdownlint/TOP002_noCodeInHeadings/tests/TOP002.test.js @@ -35,6 +35,16 @@ describe("TOP002", () => { }); describe("Fix", () => { + it("Does not flag TOP002 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 TOP002 errors`, + ); + }); + it("Strips inline code blocks in headings", async () => { const fixedFileContents = await fixLintErrors("./test.md"); const correctFile = await readFile(join(__dirname, "./fixed_test.md")); diff --git a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js index 008ecd189cb..25f867d1471 100644 --- a/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js +++ b/markdownlint/TOP005_blanksAroundMultilineHtmlTags/tests/TOP005.test.js @@ -53,6 +53,16 @@ describe("TOP005", () => { }); 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( diff --git a/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js b/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js index 21ba3b2ea32..3f983efdbea 100644 --- a/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js +++ b/markdownlint/TOP006_fullFencedCodeLanguage/tests/TOP006.test.js @@ -42,6 +42,16 @@ describe("TOP006", () => { }); 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")); diff --git a/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js b/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js index 62196f73f30..fba76312666 100644 --- a/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js +++ b/markdownlint/TOP007_useMarkdownLinks/tests/TOP007.test.js @@ -45,6 +45,16 @@ describe("TOP007", () => { }); 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( diff --git a/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js index ee0c67bc8d4..4f4cfed114b 100644 --- a/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js +++ b/markdownlint/TOP008_useBackticksForFencedCodeBlocks/tests/TOP008.test.js @@ -40,6 +40,16 @@ describe("TOP008", () => { }); 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")); diff --git a/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js b/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js index e17085023fb..e21861f66f2 100644 --- a/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js +++ b/markdownlint/TOP010_useLazyNumbering/tests/TOP010.test.js @@ -36,6 +36,16 @@ describe("TOP010", () => { }); 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")); diff --git a/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js b/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js index 3012a81fd7f..4cd5ab6a374 100644 --- a/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js +++ b/markdownlint/TOP011_headingIndentation/tests/TOP011.test.js @@ -35,6 +35,16 @@ describe("TOP011", () => { }); 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")); From b1738a79f609f291c80f774ac02f4f4816431b2b Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 14:14:45 +0000 Subject: [PATCH 25/28] Ensure fixable TOP003 errors are fully resolved after fix Nested/ordered list test md files need splitting because of this (nested can't be fixed but ordered can). --- .../tests/TOP003.test.js | 43 +++++++++++++++---- ..._ordered_list.md => fixed_ordered_list.md} | 2 - .../tests/nested_list.md | 36 ++++++++++++++++ ...ed_and_ordered_list.md => ordered_list.md} | 2 - 4 files changed, 70 insertions(+), 13 deletions(-) rename markdownlint/TOP003_defaultSectionContent/tests/{fixed_nested_and_ordered_list.md => fixed_ordered_list.md} (91%) create mode 100644 markdownlint/TOP003_defaultSectionContent/tests/nested_list.md rename markdownlint/TOP003_defaultSectionContent/tests/{nested_and_ordered_list.md => ordered_list.md} (91%) diff --git a/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js b/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js index f8299859b4c..048f1f55da5 100644 --- a/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js +++ b/markdownlint/TOP003_defaultSectionContent/tests/TOP003.test.js @@ -77,16 +77,26 @@ describe("TOP003", () => { ]); }); - it("Flags when ordered or nested lists are used for unordered list sections", async () => { - const filePath = "./nested_and_ordered_list.md"; + it("Flags when ordered list used instead of unordered list", async () => { + const filePath = "./ordered_list.md"; + const errorPath = join(pathInRepo, filePath); + const lintErrors = await getLintErrors(filePath); + + assert.deepEqual(lintErrors, [ + `${errorPath}:27 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, + `${errorPath}:27 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, + `${errorPath}:28 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, + ]); + }); + + it("Flags when list section contains a nested list", async () => { + const filePath = "./nested_list.md"; const errorPath = join(pathInRepo, filePath); const lintErrors = await getLintErrors(filePath); assert.deepEqual(lintErrors, [ `${errorPath}:10 error ${expected.name} ${expected.description} [The lesson overview section must not contain nested lists.]`, - `${errorPath}:29 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, - `${errorPath}:29 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, - `${errorPath}:30 error ${expected.name} ${expected.description} [The knowledge check section must not include any ordered lists.]`, + `${errorPath}:36 error ${expected.name} ${expected.description} [The additional resources section must not contain nested lists.]`, ]); }); @@ -99,12 +109,27 @@ describe("TOP003", () => { }); describe("Fix", () => { + it("Does not flag any TOP003 errors in any fixed test md file", async () => { + const fixedTestMarkdownFiles = [ + "./fixed_ordered_list.md", + "./fixed_incorrect_content.md", + "./fixed_content_around_list.md", + ]; + + for (const file of fixedTestMarkdownFiles) { + const lintErrors = await getLintErrors(file); + + assert( + lintErrors.every((error) => !error.includes(expected.name)), + `"${file}" contains TOP003 errors`, + ); + } + }); + it("Converts ordered lists to unordered lists", async () => { - const fixedFileContents = await fixLintErrors( - "./nested_and_ordered_list.md", - ); + const fixedFileContents = await fixLintErrors("./ordered_list.md"); const correctFile = await readFile( - join(__dirname, "./fixed_nested_and_ordered_list.md"), + join(__dirname, "./fixed_ordered_list.md"), ); assert.equal(fixedFileContents, correctFile.toString()); diff --git a/markdownlint/TOP003_defaultSectionContent/tests/fixed_nested_and_ordered_list.md b/markdownlint/TOP003_defaultSectionContent/tests/fixed_ordered_list.md similarity index 91% rename from markdownlint/TOP003_defaultSectionContent/tests/fixed_nested_and_ordered_list.md rename to markdownlint/TOP003_defaultSectionContent/tests/fixed_ordered_list.md index 94f117b6148..ceba6c3a7f1 100644 --- a/markdownlint/TOP003_defaultSectionContent/tests/fixed_nested_and_ordered_list.md +++ b/markdownlint/TOP003_defaultSectionContent/tests/fixed_ordered_list.md @@ -7,8 +7,6 @@ Text content This section contains a general overview of topics that you will learn in this lesson. - An item. - - A nested item that should flag an error. -- Unnested list item. ### Custom section diff --git a/markdownlint/TOP003_defaultSectionContent/tests/nested_list.md b/markdownlint/TOP003_defaultSectionContent/tests/nested_list.md new file mode 100644 index 00000000000..a82c4b932f6 --- /dev/null +++ b/markdownlint/TOP003_defaultSectionContent/tests/nested_list.md @@ -0,0 +1,36 @@ +### Introduction + +Text content + +### Lesson overview + +This section contains a general overview of topics that you will learn in this lesson. + +- An item. + - A nested item that should flag an error. +- Unnested list item. + +### Custom section + +Text content + +### 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 + - Nested AR item that should flag an error. diff --git a/markdownlint/TOP003_defaultSectionContent/tests/nested_and_ordered_list.md b/markdownlint/TOP003_defaultSectionContent/tests/ordered_list.md similarity index 91% rename from markdownlint/TOP003_defaultSectionContent/tests/nested_and_ordered_list.md rename to markdownlint/TOP003_defaultSectionContent/tests/ordered_list.md index 5fbe3ff29b5..4780ccbec35 100644 --- a/markdownlint/TOP003_defaultSectionContent/tests/nested_and_ordered_list.md +++ b/markdownlint/TOP003_defaultSectionContent/tests/ordered_list.md @@ -7,8 +7,6 @@ Text content This section contains a general overview of topics that you will learn in this lesson. - An item. - - A nested item that should flag an error. -- Unnested list item. ### Custom section From 94fc9866a4f49e29d046739914e0119f9e2624df Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 30 Dec 2025 14:25:33 +0000 Subject: [PATCH 26/28] Ensure fixable TOP012 errors are fully resolved after fix Missing headings not fixable but incorrect heading levels are, so the test md files should be split. --- .../tests/TOP012.test.js | 33 ++++++++++++++--- ...st.md => fixed_incorrect_heading_level.md} | 6 ---- .../{test.md => incorrect_heading_level.md} | 6 ---- .../tests/missing_heading.md | 35 +++++++++++++++++++ 4 files changed, 63 insertions(+), 17 deletions(-) rename markdownlint/TOP012_noteBoxHeadings/tests/{fixed_test.md => fixed_incorrect_heading_level.md} (92%) rename markdownlint/TOP012_noteBoxHeadings/tests/{test.md => incorrect_heading_level.md} (92%) create mode 100644 markdownlint/TOP012_noteBoxHeadings/tests/missing_heading.md diff --git a/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js b/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js index e3f67b9b7dc..6b491d023ee 100644 --- a/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js +++ b/markdownlint/TOP012_noteBoxHeadings/tests/TOP012.test.js @@ -21,23 +21,46 @@ describe("TOP012", () => { assert.deepEqual(rule.information, expected.information); }); - it("Flags note boxes with missing or incorrectly levelled headings", async () => { - const filePath = "./test.md"; + 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"]`, - `${errorPath}:41 error ${expected.name} ${expected.description} [Note box is missing a heading. Note boxes must start with a level 4 heading (####).]`, ]); }); }); 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("./test.md"); - const correctFile = await readFile(join(__dirname, "./fixed_test.md")); + 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_test.md b/markdownlint/TOP012_noteBoxHeadings/tests/fixed_incorrect_heading_level.md similarity index 92% rename from markdownlint/TOP012_noteBoxHeadings/tests/fixed_test.md rename to markdownlint/TOP012_noteBoxHeadings/tests/fixed_incorrect_heading_level.md index d06609e8ad1..16ed62aada0 100644 --- a/markdownlint/TOP012_noteBoxHeadings/tests/fixed_test.md +++ b/markdownlint/TOP012_noteBoxHeadings/tests/fixed_incorrect_heading_level.md @@ -38,12 +38,6 @@ Note box contents.
-
- -Note boxes without a heading will flag a missing heading error - -
- ### Assignment
diff --git a/markdownlint/TOP012_noteBoxHeadings/tests/test.md b/markdownlint/TOP012_noteBoxHeadings/tests/incorrect_heading_level.md similarity index 92% rename from markdownlint/TOP012_noteBoxHeadings/tests/test.md rename to markdownlint/TOP012_noteBoxHeadings/tests/incorrect_heading_level.md index e4dbe2cd142..b80d66ab2ec 100644 --- a/markdownlint/TOP012_noteBoxHeadings/tests/test.md +++ b/markdownlint/TOP012_noteBoxHeadings/tests/incorrect_heading_level.md @@ -38,12 +38,6 @@ Note box contents.
-
- -Note boxes without a heading will flag a missing heading error - -
- ### Assignment
diff --git a/markdownlint/TOP012_noteBoxHeadings/tests/missing_heading.md b/markdownlint/TOP012_noteBoxHeadings/tests/missing_heading.md new file mode 100644 index 00000000000..34ebeb57f01 --- /dev/null +++ b/markdownlint/TOP012_noteBoxHeadings/tests/missing_heading.md @@ -0,0 +1,35 @@ +### 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 + +
+ +Note boxes without a heading will flag a missing heading error + +
+ +### 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. From 36770e5fccc3207a1c9529bd3f8a3cb8f17958db Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:09:22 +0000 Subject: [PATCH 27/28] Document custom rule contribution protocol --- CONTRIBUTING.md | 3 ++ markdownlint/docs/README.md | 74 ++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) 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/docs/README.md b/markdownlint/docs/README.md index cdec4e49c1d..b9c12b16654 100644 --- a/markdownlint/docs/README.md +++ b/markdownlint/docs/README.md @@ -1,3 +1,75 @@ # TOP Custom Markdownlint Rules -This directory contains documentation for our custom rules for linting Markdown files using the markdownlint tool. These rules supplement the [default rules provided by markdownlint](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md) to enforce our [curriculum's layout style](https://github.com/TheOdinProject/curriculum/blob/main/LAYOUT_STYLE_GUIDE.md). +This directory contains documentation for our custom rules for linting Markdown files using the [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) tool. These rules supplement the [default rules provided by markdownlint](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md) to enforce our [curriculum's layout style](https://github.com/TheOdinProject/curriculum/blob/main/LAYOUT_STYLE_GUIDE.md). + +## Table of Contents + +- [How to Contribute](#how-to-contribute) +- [Custom Rules](#custom-rules) + - [File Structure](#file-structure) + - [Rule Code](#rule-code) + - [Tests](#tests) + - [Test Utilities](#test-utilities) + +## How to Contribute + +Make sure you have read our [general contributing guide](https://github.com/TheOdinProject/.github/blob/main/CONTRIBUTING.md), as it contains information that is important for all of our repos. + +This contributing guide assumes you have followed the instructions in the general contributing guide to fork and clone this curriculum repo. + +If you have a suggestion for a new linting rule, please **do not** open a pull request (PR) directly. Instead, open an issue with the full proposal so the team can discuss it first. + +## Custom Rules + +### File Structure + +Every custom rule should have its own directory inside `/markdownlint` (parent directory to this file), in the following format: + +```text +TOPXXX_ruleName/ +├── TOPXXX_ruleName.js +└── tests/ + ├── TOPXXX.test.js + └── ....md +``` + +It must also have a corresponding `TOPXXX.md` documentation file inside `/markdownlint/docs` (same directory as this file) that explains how the rule works, whether it includes auto-fix behaviour (and in what way), and the rationale behind the rule. + +### Rule Code + +Refer to [markdonwlint custom rule documentation](https://github.com/DavidAnson/markdownlint/blob/main/doc/CustomRules.md) for general information about how to write a custom rule. + +Our custom rules must also ensure the following properties contain the following values: + +- `names`: An array with two elements: + - The rule code (e.g. `TOP001`) + - The rule name in kebab-case (e.g. `descriptive-link-text-labels`) +- `parser`: Must be set to `markdownit` +- `information`: A `new URL()` object that links to the rule's documentation file + +### Tests + +Tests are run via [Node's built-in test runner](https://nodejs.org/api/test.html). All rules require the following: + +- Any number of markdown files containing only violations of the respective rule. **These files must not include violations of other rules.** +- A markdown file containing no rule violations. +- A `.test.js` file containing tests that: + - Show the rule links to the correct documentation file + - Demonstrate the correct error output when linting markdown files with rule violations (will indirectly test for the correct rule name/description as they are included in the error output) + - Demonstrate the file with no rule violations has no error output + +For rules that contain auto-fix behavior, the following things are also required: + +- Any number of markdown files that contain the intended "fixed" contents of other test markdown files (the intended result of running `npm run fix` on them) +- Tests that: + - Show no error output from any "fixed" markdown files + - Show fixing a test markdown file results in the same contents as the respective "fixed" markdown file + +Tests can be run by running `npm run test`. + +### Test Utilities + +Two utility functions are provided in `/markdownlint/test_utils` for testing both linting and fixing: `getLintErrors` and `fixLintErrors`. For both of these, `require` them into the test file and call them with `__dirname` as an argument. These will each return an async function that's relative to the test file, so you only need to pass them paths relative to the test file. + +- `getLintErrors` takes a string containing the relative path to a test markdown file, and returns a Promise that resolves to an array containing each violation's error output as a separate string. +- `fixLintErrors` takes a string containing the relative path to a test markdown file, and returns a Promise that resolves to a string containing the contents of the file after fixing all fixable violations. From 015d14ac7897d3e94c3cc0bce0e0f7dc483539d9 Mon Sep 17 00:00:00 2001 From: mao-sz <122839503+mao-sz@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:37:05 +0000 Subject: [PATCH 28/28] Add instruction for config file custom rule path --- markdownlint/docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/markdownlint/docs/README.md b/markdownlint/docs/README.md index b9c12b16654..dd1e55a7a48 100644 --- a/markdownlint/docs/README.md +++ b/markdownlint/docs/README.md @@ -35,6 +35,8 @@ TOPXXX_ruleName/ It must also have a corresponding `TOPXXX.md` documentation file inside `/markdownlint/docs` (same directory as this file) that explains how the rule works, whether it includes auto-fix behaviour (and in what way), and the rationale behind the rule. +A path to the `TOPXXX_ruleName.js` file must also be added to the `customRules` array in the `.markdownlint-cli2.jsonc` configuration file in the root of this repo. + ### Rule Code Refer to [markdonwlint custom rule documentation](https://github.com/DavidAnson/markdownlint/blob/main/doc/CustomRules.md) for general information about how to write a custom rule.