Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions README.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file-path>` 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
Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
48 changes: 48 additions & 0 deletions __tests__/core-2-3-cli.spec.ts
Original file line number Diff line number Diff line change
@@ -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"
);
});
});
29 changes: 28 additions & 1 deletion __tests__/lint-worker.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();
});

Expand Down Expand Up @@ -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();
});
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/lint-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)",
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface CliErrorCount {
export interface LintWorkerOptions {
filePath: string;
rules?: LintMdRulesConfig;
isFixMode?: boolean;
isFixMode: boolean;
}

/** batchLint 单个文件的 lint 结果 */
Expand Down
6 changes: 4 additions & 2 deletions src/utils/lint-worker.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down