diff --git a/packages/remark-lint-fenced-code-flag-case/LICENSE b/packages/remark-lint-fenced-code-flag-case/LICENSE new file mode 100644 index 0000000..9580002 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Bernard Dickens + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/remark-lint-fenced-code-flag-case/README.md b/packages/remark-lint-fenced-code-flag-case/README.md new file mode 100644 index 0000000..84d2ea3 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/README.md @@ -0,0 +1,298 @@ + + +[![Black Lives Matter!][badge-blm]][link-blm] +[![Maintenance status][badge-maintenance]][link-repo] +[![Last commit timestamp][badge-last-commit]][link-repo] +[![Open issues][badge-issues]][link-issues] +[![Pull requests][badge-pulls]][link-pulls] +[![Codecov][badge-codecov]][link-codecov] +[![Source license][badge-license]][link-license] +[![NPM version][badge-npm]][link-npm] +[![Uses Semantic Release!][badge-semantic-release]][link-semantic-release] + + + +# remark-lint-fenced-code-flag-case + +This is a [remark-lint][1] rule to warn when fenced code blocks have +inconsistent or improperly cased language flags. Also comes with full unicode +support. + +This check is useful when using [a tool like prettier that formats fenced code +blocks][2], since such tools do not consistently recognize uppercase or +mixed-case code flags. That is: code fenced with the flag `typescript` or +`markdown` will be formatted while code fenced with the flag `TypeScript` or +`MARKDOWN` may be _silently ignored_, even as syntax highlighting still works, +which results in a false sense of correctness. + +--- + + + + + +- [Install](#install) +- [Usage](#usage) + - [Via API](#via-api) + - [Via remark-cli](#via-remark-cli) + - [Via unified configuration](#via-unified-configuration) +- [API](#api) + - [Options](#options) +- [Examples](#examples) + - [`ok-missing.md`](#ok-missingmd) + - [`ok-lower.md`](#ok-lowermd) + - [`not-ok-mixed.md`](#not-ok-mixedmd) + - [`not-ok-upper.md`](#not-ok-uppermd) +- [Related](#related) +- [Contributing and Support](#contributing-and-support) + - [Contributors](#contributors) + + + + +## Install + +> Due to the nature of the unified ecosystem, this package is ESM only and +> cannot be `require`'d. + +```bash +npm install --save-dev remark-lint-fenced-code-flag-case +``` + +## Usage + +### Via API + +```typescript +import { read } from 'to-vfile'; +import { remark } from 'remark'; +import lintFencedCodeFlagCase from 'remark-lint-fenced-code-flag-case'; + +const file = await remark() + .use(lintFencedCodeFlagCase) + .process(await read('example.md')); + +console.log(String(file)); +``` + + + +### Via [remark-cli](https://xunn.at/docs-remark-cli) + +```shell +remark --use remark-lint --use lint-fenced-code-flag-case README.md +``` + + + +### Via [unified configuration](https://xunn.at/docs-unified-configuration) + +In `package.json`: + +```javascript + /* … */ + "remarkConfig": { + "plugins": [ + "remark-lint-fenced-code-flag-case" + /* … */ + ] + }, + /* … */ +``` + +In `.remarkrc.js`: + +```javascript +module.exports = { + plugins: [ + // … + 'lint-fenced-code-flag-case' + ] +}; +``` + +In `.remarkrc.mjs`: + +```javascript +import lintFencedCodeFlagCase from 'remark-lint-fenced-code-flag-case'; + +export default { + plugins: [ + // … + lintFencedCodeFlagCase + ] +}; +``` + +## API + +Detailed interface information can be found under [`docs/`][docs]. + +### Options + +This rule supports [standard configuration][3] that all remark lint rules accept +(such as `false` to turn it off or `[1, options]` to configure it). + +Additionally, this plugin recognizes the following options: + +#### `case` + +Valid values: `"lower"` | `"upper"` | `"capitalize"`\ +Default: `"lower"` + +All code fence flags must be of the specified case. Code fences without flags +are ignored. + +## Examples + +### `ok-missing.md` + +#### In + +````markdown +# Document + +``` +Text. +``` +```` + +#### Out + +No messages. + +### `ok-lower.md` + +#### In + +````markdown +# Document + +```js +const str = 'string'; +``` + +```javascript +const str = 'string'; +``` +```` + +#### Out + +No messages. + +### `not-ok-mixed.md` + +#### In + +````markdown +# Document + +```Js +const str = 'string'; +``` + +```JavaScript +const str = 'string'; +``` +```` + +#### Out + +```text +3:1-5:4: Code fence flag "Js" should be "js" +7:1-10:5: Code fence flag "JavaScript" should be "javascript" +``` + +### `not-ok-upper.md` + +#### In + +````markdown +# Document + +```JS +const str = 'string'; +``` + +```JAVASCRIPT +const str = 'string'; +``` +```` + +#### Out + +```text +3:1-5:4: Code fence flag "JS" should be "js" +7:1-10:5: Code fence flag "JAVASCRIPT" should be "javascript" +``` + +## Related + +- [remark-lint-fenced-code-flag][4] — warn when fenced code blocks occur without + language flag +- [remark-lint-fenced-code-marker][5] — warn when fenced code markers violate + the given style + +## Contributing and Support + +**[New issues][choose-new-issue] and [pull requests][pr-compare] are always +welcome and greatly appreciated! 🤩** Just as well, you can [star 🌟 this +project][link-repo] to let me know you found it useful! ✊🏿 Thank you! + +See [CONTRIBUTING.md][contributing] and [SUPPORT.md][support] for more +information. + +### Contributors + +See the [table of contributors][6]. + +[badge-blm]: https://xunn.at/badge-blm 'Join the movement!' +[badge-codecov]: + https://codecov.io/gh/Xunnamius/unified-utils/branch/main/graph/badge.svg?token=HWRIOBAAPW + 'Is this package well-tested?' +[badge-issues]: + https://img.shields.io/github/issues/Xunnamius/unified-utils + 'Open issues' +[badge-last-commit]: + https://img.shields.io/github/last-commit/xunnamius/unified-utils + 'Latest commit timestamp' +[badge-license]: + https://img.shields.io/npm/l/remark-lint-fenced-code-flag-case + "This package's source license" +[badge-maintenance]: + https://img.shields.io/maintenance/active/2022 + 'Is this package maintained?' +[badge-npm]: + https://api.ergodark.com/badges/npm-pkg-version/remark-lint-fenced-code-flag-case + 'Install this package using npm or yarn!' +[badge-pulls]: + https://img.shields.io/github/issues-pr/xunnamius/unified-utils + 'Open pull requests' +[badge-semantic-release]: + https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg + 'This repo practices continuous integration and deployment!' +[choose-new-issue]: https://github.com/xunnamius/unified-utils/issues/new/choose +[contributing]: /CONTRIBUTING.md +[docs]: docs +[link-blm]: https://xunn.at/donate-blm +[link-codecov]: https://codecov.io/gh/Xunnamius/unified-utils +[link-issues]: https://github.com/Xunnamius/unified-utils/issues?q= +[link-license]: + https://github.com/Xunnamius/unified-utils/blob/main/packages/remark-lint-fenced-code-flag-case/LICENSE +[link-npm]: https://www.npmjs.com/package/remark-lint-fenced-code-flag-case +[link-pulls]: https://github.com/xunnamius/unified-utils/pulls +[link-repo]: + https://github.com/xunnamius/unified-utils/blob/main/packages/remark-lint-fenced-code-flag-case +[link-semantic-release]: https://github.com/semantic-release/semantic-release +[pr-compare]: https://github.com/xunnamius/unified-utils/compare +[support]: /.github/SUPPORT.md +[1]: https://github.com/remarkjs/remark-lint +[2]: + https://prettier.io/blog/2017/11/07/1.8.0.html#support-markdown-2943httpsgithubcomprettierprettierpull2943-by-ikatyanghttpsgithubcomikatyang +[3]: https://github.com/remarkjs/remark-lint#configure +[4]: + https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-fenced-code-flag +[5]: + https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-fenced-code-marker +[6]: /README.md#contributors diff --git a/packages/remark-lint-fenced-code-flag-case/docs/.nojekyll b/packages/remark-lint-fenced-code-flag-case/docs/.nojekyll new file mode 100644 index 0000000..e2ac661 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/docs/.nojekyll @@ -0,0 +1 @@ +TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/packages/remark-lint-fenced-code-flag-case/docs/README.md b/packages/remark-lint-fenced-code-flag-case/docs/README.md new file mode 100644 index 0000000..be13366 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/docs/README.md @@ -0,0 +1,67 @@ +## Table of contents + +### Type Aliases + +- [Options](README.md#options) + +### Variables + +- [optionsCases](README.md#optionscases) + +### Functions + +- [default](README.md#default) + +## Type Aliases + +### Options + +Ƭ **Options**: `Object` + +Options type for the remark-remove-url-trailing-slash plugin. + +#### Type declaration + +| Name | Type | Description | +| :------ | :------ | :------ | +| `case?` | typeof [`optionsCases`](README.md#optionscases)[`number`] | All code fence flags must be of the specified case. Code fences without flags are ignored. **`Default`** 'lower' | + +#### Defined in + +packages/remark-lint-fenced-code-flag-case/src/index.ts:17 + +## Variables + +### optionsCases + +• `Const` **optionsCases**: readonly [``"lower"``, ``"upper"``, ``"capitalize"``] + +Valid values for the Options.case property. + +#### Defined in + +packages/remark-lint-fenced-code-flag-case/src/index.ts:12 + +## Functions + +### default + +▸ **default**(`this`, ...`settings`): `void` \| `Transformer`<`Node`<`Data`\>, `Node`<`Data`\>\> + +A remark-lint rule that takes a Root node as input and attaches any error +messages to the resulting virtual file pertaining to fenced code flag case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `this` | `Processor`<`void`, `Node`<`Data`\>, `void`, `void`\> | +| `...settings` | `void`[] \| [`unknown`] \| [`boolean` \| `Label` \| `Severity`, `unknown`] | + +#### Returns + +`void` \| `Transformer`<`Node`<`Data`\>, `Node`<`Data`\>\> + +#### Defined in + +node_modules/unified/index.d.ts:531 diff --git a/packages/remark-lint-fenced-code-flag-case/package.json b/packages/remark-lint-fenced-code-flag-case/package.json new file mode 100644 index 0000000..abdc629 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/package.json @@ -0,0 +1,84 @@ +{ + "name": "remark-lint-fenced-code-flag-case", + "version": "0.0.0-semantic", + "description": "remark-lint rule to warn when fenced code blocks have an inconsistently-cased language flag", + "keywords": [ + "remark", + "remark-plugin", + "plugin", + "markdown", + "remark-lint", + "lint", + "rule", + "remark-lint-rule", + "fenced", + "code", + "flag", + "infostring", + "case", + "lower", + "lowercase" + ], + "homepage": "https://github.com/Xunnamius/unified-utils/blob/main/packages/remark-lint-fenced-code-flag-case", + "repository": { + "type": "git", + "url": "https://github.com/Xunnamius/unified-utils" + }, + "license": "MIT", + "author": "Xunnamius", + "sideEffects": false, + "type": "commonjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./dist/index.mjs", + "default": "./dist/index.mjs" + }, + "./package": "./package.json", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "*": [ + "./dist/index.d.ts" + ] + } + }, + "files": [ + "/dist", + "/LICENSE", + "/package.json", + "/README.md" + ], + "scripts": { + "build": "npm run build:dist --", + "build:changelog": "conventional-changelog --outfile CHANGELOG.md --config ../../conventional.config.js --release-count 0 --skip-unstable && (if [ \"$CHANGELOG_SKIP_TITLE\" != 'true' ]; then { node -e 'console.log(require(\"../../conventional.config.js\").changelogTitle)'; cat CHANGELOG.md; } > CHANGELOG.md.ignore && mv CHANGELOG.md.ignore CHANGELOG.md; fi) && NODE_ENV=format remark --output --frail CHANGELOG.md && prettier --write CHANGELOG.md", + "build:dist": "NODE_ENV=production tsc --project tsconfig.types.json --incremental false && tsconfig-replace-paths --project tsconfig.types.json && NODE_ENV=production-esm babel src --extensions .ts --out-dir dist --out-file-extension .mjs --root-mode upward", + "build:docs": "if [ -r ./next.config.js ]; then typedoc --plugin typedoc-plugin-markdown --tsconfig tsconfig.docs.json --out docs --readme none lib src test types external-scripts --exclude '**/*.test.*' --exclude external-scripts/bin; else ENTRY=`node -e 'const entry = require(\"./package.json\").config?.[\"plugin-build\"]?.docs?.entry; if(!entry) throw new Error(\"\\\"config['\"'\"'plugin-build'\"'\"'].docs.entry\\\" field is not defined in package.json\"); console.log(entry)'` && echo 'Entry file:' \"$ENTRY\" && typedoc --plugin typedoc-plugin-markdown --tsconfig tsconfig.docs.json --out docs --readme none $(echo $ENTRY) && find docs -name '*.md' -exec sed -i -e 's/Project: //g' {} + && sed -i -e 1,4d docs/README.md; fi && find docs -name '*.md' -exec sed -i -e 's/`__namedParameters`/`\\(destructured\\)`/g' {} + && find docs -name '*.md' -exec sed -i -E 's/`__namedParameters\\.([^`]+)`/`\\({ \\1 }\\)`/g' {} +", + "clean": "git ls-files --exclude-standard --ignored --others --directory | grep -vE '^((\\.(env|vscode|husky))|next-env\\.d\\.ts|node_modules)($|\\/)' | xargs -p rm -rf", + "format": "cd ../.. && npm run format", + "lint": "echo 'IMPLEMENT ME'", + "list-tasks": "node -e 'console.log(Object.keys(require(\"./package.json\").scripts).join(\"\\n\"))'", + "test": "npm run test:unit --", + "test:integration": "echo 'IMPLEMENT ME'", + "test:unit": "echo 'IMPLEMENT ME'" + }, + "config": { + "plugin-build": { + "docs": { + "entry": "./src/*" + } + } + }, + "dependencies": { + "unified-lint-rule": "^2.1.1", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.1.1" + }, + "engines": { + "node": "^14.19.0 || ^16.13.0 || >=17.4.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/remark-lint-fenced-code-flag-case/src/index.ts b/packages/remark-lint-fenced-code-flag-case/src/index.ts new file mode 100644 index 0000000..dfa504f --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/src/index.ts @@ -0,0 +1,73 @@ +import { visit } from 'unist-util-visit'; +import { generated as isGenerated } from 'unist-util-generated'; +import { lintRule as createLintRule } from 'unified-lint-rule'; + +import type { Code } from 'mdast'; + +const origin = 'unified-utils:fenced-code-flag-case'; + +/** + * Valid values for the Options.case property. + */ +export const optionsCases = ['lower', 'upper', 'capitalize'] as const; + +/** + * Options type for the remark-remove-url-trailing-slash plugin. + */ +export type Options = { + /** + * All code fence flags must be of the specified case. Code fences without + * flags are ignored. + * + * @default 'lower' + */ + case?: typeof optionsCases[number]; +}; + +/** + * A remark-lint rule that takes a Root node as input and attaches any error + * messages to the resulting virtual file pertaining to fenced code flag case. + */ +const remarkLintFencedCodeFlagCase = createLintRule( + { + origin, + url: 'https://github.com/Xunnamius/unified-utils/tree/main/packages/remark-lint-fenced-code-flag-case#readme' + }, + function (tree, file, options = {}) { + if (!options || (typeof options == 'object' && !Array.isArray(options))) { + const expectedCase = + // @ts-expect-error: works in typescript@4.9 + ('case' in options ? options.case : 'lower') as NonNullable; + + if (optionsCases.includes(expectedCase)) { + // The visit function's types are not very optimal and cause problems... + visit(tree, (node) => { + if (node.type == 'code') { + const { lang: langActual } = node as Code; + if (!isGenerated(node) && langActual) { + const langExpected = + expectedCase == 'lower' + ? langActual.toLowerCase() + : expectedCase == 'upper' + ? langActual.toUpperCase() + : langActual[0].toUpperCase() + langActual.slice(1).toLowerCase(); + + if (langActual != langExpected) { + file.message( + `Code fence flag "${langActual}" should be "${langExpected}"`, + node + ); + } + } + } + }); + } else { + file.fail(`Error: Bad configuration case value "${expectedCase}"`); + } + } else { + file.fail('Error: Bad configuration'); + } + } +); + +export default remarkLintFencedCodeFlagCase; diff --git a/packages/remark-lint-fenced-code-flag-case/test/fixtures/not-ok-mixed.md b/packages/remark-lint-fenced-code-flag-case/test/fixtures/not-ok-mixed.md new file mode 100644 index 0000000..56d5b15 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/test/fixtures/not-ok-mixed.md @@ -0,0 +1,9 @@ +# Document + +```Js +const str = 'string'; +``` + +```JavaScript +const str = 'string'; +``` diff --git a/packages/remark-lint-fenced-code-flag-case/test/fixtures/not-ok-upper.md b/packages/remark-lint-fenced-code-flag-case/test/fixtures/not-ok-upper.md new file mode 100644 index 0000000..ceaf1d4 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/test/fixtures/not-ok-upper.md @@ -0,0 +1,9 @@ +# Document + +```JS +const str = 'string'; +``` + +```JAVASCRIPT +const str = 'string'; +``` diff --git a/packages/remark-lint-fenced-code-flag-case/test/fixtures/ok-lower.md b/packages/remark-lint-fenced-code-flag-case/test/fixtures/ok-lower.md new file mode 100644 index 0000000..78f1375 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/test/fixtures/ok-lower.md @@ -0,0 +1,9 @@ +# Document + +```js +const str = 'string'; +``` + +```javascript +const str = 'string'; +``` diff --git a/packages/remark-lint-fenced-code-flag-case/test/fixtures/ok-missing.md b/packages/remark-lint-fenced-code-flag-case/test/fixtures/ok-missing.md new file mode 100644 index 0000000..a05e839 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/test/fixtures/ok-missing.md @@ -0,0 +1,5 @@ +# Document + +``` +Text. +``` diff --git a/packages/remark-lint-fenced-code-flag-case/test/helpers.ts b/packages/remark-lint-fenced-code-flag-case/test/helpers.ts new file mode 100644 index 0000000..cf4cea0 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/test/helpers.ts @@ -0,0 +1,12 @@ +import { readFileSync as readToString } from 'node:fs'; +import { read as readToVFile } from 'to-vfile'; + +export function getFixtureString(fixture: string, { trim = false } = {}) { + return readToString(`${__dirname}/fixtures/${fixture}.md`, 'utf8')[ + trim ? 'trim' : 'toString' + ](); +} + +export function getFixtureVFile(fixture: string) { + return readToVFile(`${__dirname}/fixtures/${fixture}.md`); +} diff --git a/packages/remark-lint-fenced-code-flag-case/test/integration-node.test.ts b/packages/remark-lint-fenced-code-flag-case/test/integration-node.test.ts new file mode 100644 index 0000000..8f77e16 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/test/integration-node.test.ts @@ -0,0 +1,274 @@ +import assert from 'node:assert'; +import { debugFactory } from 'multiverse/debug-extended'; +import { run } from 'multiverse/run'; +import { getFixtureString } from 'pkgverse/remark-lint-fenced-code-flag-case/test/helpers'; +import { name as pkgName, exports as pkgExports } from '../package.json'; + +import { + mockFixtureFactory, + dummyFilesFixture, + dummyNpmPackageFixture, + npmCopySelfFixture, + nodeImportTestFixture, + runTestFixture +} from 'testverse/setup'; + +// TODO: note that we've made some modifications to the setup.ts file that +// TODO: should be propagated! + +const TEST_IDENTIFIER = 'integration-node'; +const debug = debugFactory(`${pkgName}:${TEST_IDENTIFIER}`); + +const pkgMainPaths = Object.values(pkgExports) + .map((xport) => + typeof xport == 'string' ? null : `${__dirname}/../${xport.node || xport.default}` + ) + .filter(Boolean) as string[]; + +const withMockedFixture = mockFixtureFactory(TEST_IDENTIFIER, { + performCleanup: true, + pkgRoot: `${__dirname}/..`, + pkgName, + use: [ + dummyNpmPackageFixture(), + dummyFilesFixture(), + npmCopySelfFixture(), + runTestFixture() + ], + npmInstall: ['remark', 'remark-cli', 'remark-lint'] +}); + +beforeAll(async () => { + debug('pkgMainPaths: %O', pkgMainPaths); + + await Promise.all( + pkgMainPaths.map(async (pkgMainPath) => { + if ((await run('test', ['-e', pkgMainPath])).code != 0) { + debug(`unable to find main distributable: ${pkgMainPath}`); + throw new Error('must build distributables first (try `npm run build:dist`)'); + } + }) + ); +}); + +describe('via api', () => { + it('works as an ESM import', async () => { + expect.hasAssertions(); + + await withMockedFixture( + async (context) => { + assert(context.testResult, 'must use node-import-test fixture'); + expect(context.testResult?.stderr).toBeEmpty(); + + expect(context.testResult?.stdout).toMatch( + /.*?3:1-5:4.*?Code fence flag "JS" should be "js"\s/ + ); + + expect(context.testResult?.stdout).toMatch( + /.*?7:1-9:4.*?Code fence flag "JAVASCRIPT" should be "javascript"$/ + ); + + expect(context.testResult?.code).toBe(0); + }, + { + initialFileContents: { + 'src/index.mjs': ` + import { remark } from 'remark'; + import remarkLintFencedCodeFlagCase from 'remark-lint-fenced-code-flag-case'; + + const file = await remark() + .use(remarkLintFencedCodeFlagCase) + .process(${JSON.stringify(getFixtureString('not-ok-upper'))}); + + console.log(file.messages.map(String).join('\\n')); + ` + }, + use: [ + dummyNpmPackageFixture(), + dummyFilesFixture(), + npmCopySelfFixture(), + nodeImportTestFixture() + ] + } + ); + }); +}); + +describe('via remark-cli inline configuration', () => { + it('works with --use option', async () => { + expect.hasAssertions(); + + await withMockedFixture( + async (context) => { + assert(context.testResult, 'must use run-test-test fixture'); + expect(context.testResult?.stdout).toBeEmpty(); + + expect(context.testResult?.stderr).toMatch( + /.*? 3:1-5:4 .*? Code fence flag "JS" should be "js" .*/ + ); + + expect(context.testResult?.stderr).toMatch( + /.*? 7:1-9:4 .*? Code fence flag "JAVASCRIPT" should be "javascript" .*/ + ); + + expect(context.testResult?.code).toBe(0); + }, + { + initialFileContents: { 'README.md': getFixtureString('not-ok-upper') }, + runWith: { + binary: 'npx', + args: [ + '--no-install', + 'remark', + '--no-stdout', + '--use', + 'remark-lint', + '--use', + 'remark-lint-fenced-code-flag-case', + 'README.md' + ] + } + } + ); + }); +}); + +describe('via remark-cli unified configuration', () => { + it('works with package.json (short-string)', async () => { + expect.hasAssertions(); + + await withMockedFixture( + async (context) => { + assert(context.testResult, 'must use run-test-test fixture'); + expect(context.testResult?.stdout).toBeEmpty(); + + expect(context.testResult?.stderr).toMatch( + /.*? 3:1-5:4 .*? Code fence flag "JS" should be "js" .*/ + ); + + expect(context.testResult?.stderr).toMatch( + /.*? 7:1-9:4 .*? Code fence flag "JAVASCRIPT" should be "javascript" .*/ + ); + + expect(context.testResult?.code).toBe(0); + }, + { + initialFileContents: { + 'README.md': getFixtureString('not-ok-upper'), + 'package.json': JSON.stringify({ + name: 'dummy-pkg', + remarkConfig: { plugins: ['lint-fenced-code-flag-case'] } + }) + }, + runWith: { + binary: 'npx', + args: ['--no-install', 'remark', '--no-stdout', 'README.md'] + } + } + ); + }); + + it('works with package.json (string)', async () => { + expect.hasAssertions(); + + await withMockedFixture( + async (context) => { + assert(context.testResult, 'must use run-test-test fixture'); + expect(context.testResult?.stdout).toBeEmpty(); + expect(context.testResult?.stderr).toMatch( + /.*? 3:1-5:4 .*? Code fence flag "JS" should be "js" .*/ + ); + + expect(context.testResult?.stderr).toMatch( + /.*? 7:1-9:4 .*? Code fence flag "JAVASCRIPT" should be "javascript" .*/ + ); + + expect(context.testResult?.code).toBe(0); + }, + { + initialFileContents: { + 'README.md': getFixtureString('not-ok-upper'), + 'package.json': JSON.stringify({ + name: 'dummy-pkg', + remarkConfig: { + plugins: ['remark-lint', 'remark-lint-fenced-code-flag-case'] + } + }) + }, + runWith: { + binary: 'npx', + args: ['--no-install', 'remark', '--no-stdout', 'README.md'] + } + } + ); + }); + + it('works with .remarkrc.js (string)', async () => { + expect.hasAssertions(); + + await withMockedFixture( + async (context) => { + assert(context.testResult, 'must use run-test-test fixture'); + expect(context.testResult?.stdout).toBeEmpty(); + + expect(context.testResult?.stderr).toMatch( + /.*? 3:1-5:4 .*? Code fence flag "JS" should be "js" .*/ + ); + + expect(context.testResult?.stderr).toMatch( + /.*? 7:1-9:4 .*? Code fence flag "JAVASCRIPT" should be "javascript" .*/ + ); + + expect(context.testResult?.code).toBe(0); + }, + { + initialFileContents: { + 'README.md': getFixtureString('not-ok-upper'), + '.remarkrc.js': ` + module.exports = { + plugins: ['remark-lint', 'remark-lint-fenced-code-flag-case'] + }; + ` + }, + runWith: { + binary: 'npx', + args: ['--no-install', 'remark', '--no-stdout', 'README.md'] + } + } + ); + }); + + it('works with .remarkrc.mjs (function)', async () => { + expect.hasAssertions(); + + await withMockedFixture( + async (context) => { + assert(context.testResult, 'must use run-test-test fixture'); + expect(context.testResult?.stdout).toBeEmpty(); + + expect(context.testResult?.stderr).toMatch( + /.*? 3:1-5:4 .*? Code fence flag "JS" should be "js" .*/ + ); + + expect(context.testResult?.stderr).toMatch( + /.*? 7:1-9:4 .*? Code fence flag "JAVASCRIPT" should be "javascript" .*/ + ); + + expect(context.testResult?.code).toBe(0); + }, + { + initialFileContents: { + 'README.md': getFixtureString('not-ok-upper'), + '.remarkrc.mjs': ` + import remarkLintFencedCodeFlagCase from 'remark-lint-fenced-code-flag-case'; + export default { plugins: [ remarkLintFencedCodeFlagCase] }; + ` + }, + runWith: { + binary: 'npx', + args: ['--no-install', 'remark', '--no-stdout', 'README.md'] + } + } + ); + }); +}); diff --git a/packages/remark-lint-fenced-code-flag-case/test/unit-index.test.ts b/packages/remark-lint-fenced-code-flag-case/test/unit-index.test.ts new file mode 100644 index 0000000..3d95bf8 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/test/unit-index.test.ts @@ -0,0 +1,120 @@ +import { remark } from 'remark'; +import remarkGfm from 'remark-gfm'; +import remarkRemoveUnusedDefs from 'pkgverse/remark-lint-fenced-code-flag-case/src/index'; +import { getFixtureVFile } from 'pkgverse/remark-lint-fenced-code-flag-case/test/helpers'; + +describe('::default', () => { + it('warns when fenced code flags are not lowercase by default', async () => { + expect.hasAssertions(); + + const runner = remark().use(remarkGfm).use(remarkRemoveUnusedDefs); + const okMissing = await runner.process(await getFixtureVFile('ok-missing')); + const okLower = await runner.process(await getFixtureVFile('ok-lower')); + const noOkMixed = await runner.process(await getFixtureVFile('not-ok-mixed')); + const notOkUpper = await runner.process(await getFixtureVFile('not-ok-upper')); + + expect(okMissing.messages).toStrictEqual([]); + expect(okLower.messages).toStrictEqual([]); + + expect(noOkMixed.messages).toStrictEqual([ + expect.objectContaining({ message: 'Code fence flag "Js" should be "js"' }), + expect.objectContaining({ + message: 'Code fence flag "JavaScript" should be "javascript"' + }) + ]); + + expect(notOkUpper.messages).toStrictEqual([ + expect.objectContaining({ message: 'Code fence flag "JS" should be "js"' }), + expect.objectContaining({ + message: 'Code fence flag "JAVASCRIPT" should be "javascript"' + }) + ]); + }); + + it('warns about fenced code flags with respect to "upper" case option', async () => { + expect.hasAssertions(); + + const runner = remark().use(remarkGfm).use(remarkRemoveUnusedDefs, { case: 'upper' }); + + const okMissing = await runner.process(await getFixtureVFile('ok-missing')); + const okLower = await runner.process(await getFixtureVFile('ok-lower')); + const noOkMixed = await runner.process(await getFixtureVFile('not-ok-mixed')); + const notOkUpper = await runner.process(await getFixtureVFile('not-ok-upper')); + + expect(okMissing.messages).toStrictEqual([]); + expect(notOkUpper.messages).toStrictEqual([]); + + expect(noOkMixed.messages).toStrictEqual([ + expect.objectContaining({ + message: 'Code fence flag "Js" should be "JS"' + }), + expect.objectContaining({ + message: 'Code fence flag "JavaScript" should be "JAVASCRIPT"' + }) + ]); + + expect(okLower.messages).toStrictEqual([ + expect.objectContaining({ message: 'Code fence flag "js" should be "JS"' }), + expect.objectContaining({ + message: 'Code fence flag "javascript" should be "JAVASCRIPT"' + }) + ]); + }); + + it('warns about fenced code flags with respect to "capitalize" case option', async () => { + expect.hasAssertions(); + + const runner = remark() + .use(remarkGfm) + .use(remarkRemoveUnusedDefs, { case: 'capitalize' }); + + const okMissing = await runner.process(await getFixtureVFile('ok-missing')); + const okLower = await runner.process(await getFixtureVFile('ok-lower')); + const noOkMixed = await runner.process(await getFixtureVFile('not-ok-mixed')); + const notOkUpper = await runner.process(await getFixtureVFile('not-ok-upper')); + + expect(okMissing.messages).toStrictEqual([]); + + expect(noOkMixed.messages).toStrictEqual([ + expect.objectContaining({ + message: 'Code fence flag "JavaScript" should be "Javascript"' + }) + ]); + + expect(okLower.messages).toStrictEqual([ + expect.objectContaining({ message: 'Code fence flag "js" should be "Js"' }), + expect.objectContaining({ + message: 'Code fence flag "javascript" should be "Javascript"' + }) + ]); + + expect(notOkUpper.messages).toStrictEqual([ + expect.objectContaining({ message: 'Code fence flag "JS" should be "Js"' }), + expect.objectContaining({ + message: 'Code fence flag "JAVASCRIPT" should be "Javascript"' + }) + ]); + }); + + it('error over bad configuration', async () => { + expect.hasAssertions(); + + const runner = remark().use(remarkGfm).use(remarkRemoveUnusedDefs, { case: 'bad' }); + + await expect( + runner.process(await getFixtureVFile('ok-lower')) + ).resolves.toHaveProperty( + 'messages.0.message', + expect.stringContaining('Bad configuration case value "bad"') + ); + + runner().use(remarkRemoveUnusedDefs, 'upper'); + + await expect( + runner.process(await getFixtureVFile('ok-lower')) + ).resolves.toHaveProperty( + 'messages.0.message', + expect.stringContaining('Bad configuration') + ); + }); +}); diff --git a/packages/remark-lint-fenced-code-flag-case/tsconfig.docs.json b/packages/remark-lint-fenced-code-flag-case/tsconfig.docs.json new file mode 100644 index 0000000..07dd331 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/tsconfig.docs.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["../../types/**/*", "src/**/*"] +} diff --git a/packages/remark-lint-fenced-code-flag-case/tsconfig.lint.json b/packages/remark-lint-fenced-code-flag-case/tsconfig.lint.json new file mode 100644 index 0000000..07dd331 --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/tsconfig.lint.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["../../types/**/*", "src/**/*"] +} diff --git a/packages/remark-lint-fenced-code-flag-case/tsconfig.types.json b/packages/remark-lint-fenced-code-flag-case/tsconfig.types.json new file mode 100644 index 0000000..c5793eb --- /dev/null +++ b/packages/remark-lint-fenced-code-flag-case/tsconfig.types.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "allowJs": false, + "declaration": true, + "emitDeclarationOnly": true, + "isolatedModules": false, + "noEmit": false, + "outDir": "dist", + "rootDir": "./src" + }, + "extends": "../../tsconfig.json", + "include": ["../../types/**/*", "src/**/*"] +}