diff --git a/README.md b/README.md index d3e94be..122e53b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A remark-lint rule to check language syntax in a code block. - JavaScript - JSON - YAML +- CSS ## Install diff --git a/index.js b/index.js index 570708d..a828fc7 100644 --- a/index.js +++ b/index.js @@ -2,36 +2,50 @@ import { lintRule } from "unified-lint-rule"; import { visit } from "unist-util-visit"; import { default as swc } from "@swc/core"; import { default as yaml } from "js-yaml"; +import { default as postcss } from "postcss"; const remarkLintCodeBlockSyntax = lintRule("remark-lint:code-block-syntax", codeSyntax); export default remarkLintCodeBlockSyntax; function codeSyntax(tree, file) { - const supportedLangs = ["js", "json", "yaml"]; + const supportedLangs = ["js", "json", "yaml", "css"]; const test = supportedLangs.map((lang) => ({ type: "code", lang: lang })); visit(tree, test, visitor); function visitor(node) { - switch (node.lang) { + const report = (reason, language) => { + file.message(`Invalid ${language}: ${reason}`, node); + }; + + const { lang, value } = node; + + switch (lang) { case "js": { - const reason = checkJs(node.value); + const reason = checkJs(value); if (reason) { - file.message(`Invalid JavaScript: ${reason}`, node); + report(reason, "JavaScript"); } break; } case "json": { - const reason = checkJson(node.value); + const reason = checkJson(value); if (reason) { - file.message(`Invalid JSON: ${reason}`, node); + report(reason, "JSON"); } break; } case "yaml": { - const reason = checkYaml(node.value); + const reason = checkYaml(value); if (reason) { - file.message(`Invalid YAML: ${reason}`, node); + report(reason, "YAML"); + } + break; + } + case "css": { + const reason = checkCss(value); + if (reason) { + report(reason, "CSS"); } break; } @@ -70,4 +84,13 @@ function codeSyntax(tree, file) { return e.message.split(/\r?\n/)[0]; } } + + function checkCss(code) { + try { + postcss.parse(code); + return null; + } catch (e) { + return e.message; + } + } } diff --git a/index.test.js b/index.test.js index 2b734d4..619c02b 100644 --- a/index.test.js +++ b/index.test.js @@ -69,8 +69,26 @@ describe("YAML", () => { }); }); +describe("CSS", () => { + test("valid", () => { + expect(run("css", "a{}")).toEqual([]); + }); + + test("invalid", () => { + expect(run("css", "a{\n}}")).toEqual([ + { + column: 1, + line: 1, + message: "Invalid CSS: :2:2: Unexpected }", + ruleId: "code-block-syntax", + source: "remark-lint", + }, + ]); + }); +}); + describe("Unsupported languages", () => { - test("CSS", () => { - expect(run("css", "a{")).toEqual([]); + test("Ruby", () => { + expect(run("ruby", "a=")).toEqual([]); }); }); diff --git a/package-lock.json b/package-lock.json index cf7ee63..ad7dd94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@swc/core": "^1.2.171", "js-yaml": "^4.0.0", + "postcss": "^8.4.12", "unified-lint-rule": "^2.0.0", "unist-util-visit": "^4.0.0" }, @@ -3922,6 +3923,17 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4122,8 +4134,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -4158,6 +4169,29 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.4.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", + "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.1", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -4428,6 +4462,14 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -7885,6 +7927,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8039,8 +8086,7 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "picomatch": { "version": "2.3.1", @@ -8063,6 +8109,16 @@ "find-up": "^4.0.0" } }, + "postcss": { + "version": "8.4.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", + "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==", + "requires": { + "nanoid": "^3.3.1", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -8263,6 +8319,11 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", diff --git a/package.json b/package.json index 5a03d90..6c67b0f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ ], "scripts": { "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test:watch": "npm run test -- --watch", "lint": "npm run eslint && npm run prettier:check", "lint:fix": "npm run eslint:fix && npm run prettier:write", "eslint": "npx eslint --ignore-path=.gitignore .", @@ -35,6 +36,7 @@ "dependencies": { "@swc/core": "^1.2.171", "js-yaml": "^4.0.0", + "postcss": "^8.4.12", "unified-lint-rule": "^2.0.0", "unist-util-visit": "^4.0.0" },