diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e7048..9b8be89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## [Unreleased] +## [2.2.4](https://github.com/lint-md/cli/compare/v2.2.3...v2.2.4) (2026-07-30) + +### Dependencies + +- upgrade `@lint-md/core` from `^2.2.1` to `^2.3.0` + +### Documentation + +- document Core 2.3 opt-in rules in both README files + +### Tests + +- add a CLI regression test for the Core 2.3 opt-in rules + +### Refactoring + +- use `fixMarkdown()` for CLI fix paths +- require an explicit fix mode for lint workers + ## [2.2.3](https://github.com/lint-md/cli/compare/v2.2.2...v2.2.3) (2026-07-29) ### Tests diff --git a/README.en-US.md b/README.en-US.md index e41a66e..1ff5c11 100644 --- a/README.en-US.md +++ b/README.en-US.md @@ -103,6 +103,59 @@ The image runs as a non-root user by default. If the mounted directory has stric | `extensions` | `string[]` | `[".md", ".markdown", ".mdx"]` | File extensions to lint | | `rules` | `object` | `{}` | Rule configuration. See the `@lint-md/core` documentation for details | +### Core 2.3 Rules + +The following rules are disabled by default. +Enable them in `./.lintmdrc` at the project root. + +```json +{ + "rules": { + "require-trailing-spaces": 2, + "space-around-link": 2, + "no-multiple-blank-lines": 2 + } +} +``` + +The CLI reads `./.lintmdrc` by default. +Use `lint-md --config ` to select another file. + +For direct Core API use, configure rules in `fixMarkdown()`. + +```ts +import { fixMarkdown, RULE_SEVERITY } from "@lint-md/core"; + +const markdown = "First line\nSecond line"; +const result = fixMarkdown(markdown, { + rules: { + "require-trailing-spaces": RULE_SEVERITY.ERROR, + "space-around-link": RULE_SEVERITY.ERROR, + "no-multiple-blank-lines": RULE_SEVERITY.ERROR, + }, +}); + +console.log(result.fixedResult.result); +``` + +`RULE_SEVERITY.ERROR` equals rule level `2`. +`fixMarkdown()` always applies fixes. +Use `lintMarkdown(markdown, rules, false)` for lint-only checks. + +`space-around-link` handles normal, automatic, and reference links. +It does not handle standalone images. +Full-width punctuation needs no added spaces. +Other Unicode punctuation needs no added spaces. +Existing whitespace needs no added spaces. +Block boundaries need no added spaces. +Adjacent links receive only one space. + +`no-multiple-blank-lines` reduces consecutive blank lines to one blank line. +It removes blank lines at the document start. +It keeps one final newline at the document end. +Lines that contain only spaces or tabs are blank lines. +Content inside code blocks remains unchanged. + ## Exit Codes - `0`: no errors were found, or only warnings were found while `--suppress-warnings` is enabled diff --git a/README.md b/README.md index 29cdcb2..9198d8a 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,55 @@ docker run --rm \ | `extensions` | `string[]` | `[".md", ".markdown", ".mdx"]` | 要 lint 的文件扩展名 | | `rules` | `object` | `{}` | 规则配置,详见 `@lint-md/core` 文档 | +### Core 2.3 新规则 + +以下规则默认关闭。请在项目根目录的 `./.lintmdrc` 中启用它们。 + +```json +{ + "rules": { + "require-trailing-spaces": 2, + "space-around-link": 2, + "no-multiple-blank-lines": 2 + } +} +``` + +CLI 默认读取 `./.lintmdrc`。 +也可以使用 `lint-md --config <文件路径>` 指定配置文件。 + +直接使用 Core API 时,请在 `fixMarkdown()` 的 `rules` 中配置规则。 + +```ts +import { fixMarkdown, RULE_SEVERITY } from "@lint-md/core"; + +const markdown = "第一行\n第二行"; +const result = fixMarkdown(markdown, { + rules: { + "require-trailing-spaces": RULE_SEVERITY.ERROR, + "space-around-link": RULE_SEVERITY.ERROR, + "no-multiple-blank-lines": RULE_SEVERITY.ERROR, + }, +}); + +console.log(result.fixedResult.result); +``` + +`RULE_SEVERITY.ERROR` 等同于规则级别 `2`。 +`fixMarkdown()` 始终执行自动修复。 +只检查时,请使用 `lintMarkdown(markdown, rules, false)`。 + +`space-around-link` 处理普通链接、自动链接和引用链接。 +它不处理独立图片。 +全角标点、其他 Unicode 标点、已有空白和块边界不需要空格。 +连续链接之间只添加一个空格。 + +`no-multiple-blank-lines` 将连续空白行修复为一个空白行。 +它删除文档开头的空白行。 +它保留文档末尾的一个换行。 +空格和 Tab 组成的行也算空白行。 +代码块内部内容不受影响。 + ## 退出码约定 - `0`:无错误(或仅 warning 且启用了 `--suppress-warnings`) diff --git a/__tests__/core-2-3-cli.spec.ts b/__tests__/core-2-3-cli.spec.ts new file mode 100644 index 0000000..68fd097 --- /dev/null +++ b/__tests__/core-2-3-cli.spec.ts @@ -0,0 +1,48 @@ +import { execFileSync } from "child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import * as path from "path"; + +const TSX = path.resolve(__dirname, "../node_modules/tsx/dist/cli.mjs"); +const CLI = path.resolve(__dirname, "../src/lint-md.ts"); + +describe("core 2.3 rules", () => { + let tmpDir: string; + let configPath: string; + + beforeEach(() => { + tmpDir = mkdtempSync(path.join(tmpdir(), "lint-md-core-2-3-")); + configPath = path.join(tmpDir, ".lintmdrc"); + writeFileSync( + configPath, + JSON.stringify({ + rules: { + "require-trailing-spaces": 2, + "space-around-link": 2, + "no-multiple-blank-lines": 2, + }, + }), + "utf8" + ); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + test("applies all opt-in rules through stdin fix mode", () => { + const stdout = execFileSync( + process.execPath, + [TSX, CLI, "--stdin", "--fix", "--config", configPath], + { + input: "甲[链接](https://example.com)乙\n第二行\n\n\n末行\n", + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"], + } + ); + + expect(stdout).toBe( + "甲 [链接](https://example.com) 乙 \n第二行\n\n末行\n" + ); + }); +}); diff --git a/__tests__/lint-worker.spec.ts b/__tests__/lint-worker.spec.ts index d3601bd..6878384 100644 --- a/__tests__/lint-worker.spec.ts +++ b/__tests__/lint-worker.spec.ts @@ -1,10 +1,11 @@ import { jest } from "@jest/globals"; jest.mock("@lint-md/core", () => ({ + fixMarkdown: jest.fn(), lintMarkdown: jest.fn(), })); -import { lintMarkdown } from "@lint-md/core"; +import { fixMarkdown, lintMarkdown } from "@lint-md/core"; import lintWorker from "../src/utils/lint-worker"; import { writeFile, mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -13,12 +14,16 @@ import * as path from "path"; const mockedLintMarkdown = lintMarkdown as jest.MockedFunction< typeof lintMarkdown >; +const mockedFixMarkdown = fixMarkdown as jest.MockedFunction< + typeof fixMarkdown +>; describe("lintWorker executionErrors passthrough", () => { let tmpDir: string; beforeEach(async () => { tmpDir = await mkdtemp(path.join(tmpdir(), "lint-worker-exec-")); + mockedFixMarkdown.mockReset(); mockedLintMarkdown.mockReset(); }); @@ -76,4 +81,26 @@ describe("lintWorker executionErrors passthrough", () => { expect(result.executionErrors).toBeUndefined(); }); + + test("uses fixMarkdown for fix mode", async () => { + const file = path.join(tmpDir, "fix.md"); + const rules = { "space-around-link": 2 }; + await writeFile(file, "甲[链接](https://example.com)乙\n", "utf8"); + + mockedFixMarkdown.mockReturnValue({ + lintResult: [], + fixedResult: { result: "甲 [链接](https://example.com) 乙\n" }, + fixableErrorCount: 0, + fixableWarningCount: 0, + executionErrors: [], + } as any); + + await lintWorker({ filePath: file, rules, isFixMode: true }); + + expect(mockedFixMarkdown).toHaveBeenCalledWith( + "甲[链接](https://example.com)乙\n", + { rules } + ); + expect(mockedLintMarkdown).not.toHaveBeenCalled(); + }); }); diff --git a/package.json b/package.json index ff16362..bea96bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lint-md/cli", - "version": "2.2.3", + "version": "2.2.4", "description": "CLI tool to lint your markdown file for Chinese.", "main": "lib/index.js", "module": "esm/index.js", @@ -60,7 +60,7 @@ ] }, "dependencies": { - "@lint-md/core": "^2.2.1", + "@lint-md/core": "^2.3.0", "chalk": "^4", "commander": "^9.4.1", "glob": "^13.0.6", diff --git a/src/lint-md.ts b/src/lint-md.ts index 897cd29..7405a22 100644 --- a/src/lint-md.ts +++ b/src/lint-md.ts @@ -8,7 +8,7 @@ const setExitCode = (code: number): void => { import { readFileSync } from "fs"; import { availableParallelism } from "os"; import { program } from "commander"; -import { lintMarkdown } from "@lint-md/core"; +import { fixMarkdown, lintMarkdown } from "@lint-md/core"; import { version } from "../package.json"; import { safeWriteFile } from "./utils/safe-write-file"; import { @@ -100,7 +100,7 @@ program } try { - const result = lintMarkdown(content, rules, true); + const result = fixMarkdown(content, { rules }); process.stdout.write(result.fixedResult?.result ?? content); const stdinItem = { path: "(stdin)", diff --git a/src/types.ts b/src/types.ts index b982514..8921225 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,7 +40,7 @@ export interface CliErrorCount { export interface LintWorkerOptions { filePath: string; rules?: LintMdRulesConfig; - isFixMode?: boolean; + isFixMode: boolean; } /** batchLint 单个文件的 lint 结果 */ diff --git a/src/utils/lint-worker.ts b/src/utils/lint-worker.ts index d214e3f..a573afa 100644 --- a/src/utils/lint-worker.ts +++ b/src/utils/lint-worker.ts @@ -1,12 +1,14 @@ import { readFile } from "fs/promises"; -import { lintMarkdown } from "@lint-md/core"; +import { fixMarkdown, lintMarkdown } from "@lint-md/core"; import type { LintWorkerOptions } from "../types"; const lintWorker = async (options: LintWorkerOptions) => { const { filePath, rules, isFixMode } = options; const content = await readFile(filePath, "utf8"); - const result = lintMarkdown(content, rules, isFixMode); + const result = isFixMode + ? fixMarkdown(content, { rules }) + : lintMarkdown(content, rules, false); return { path: filePath,