Sign in

    mbt-typeset

    中文排版校对工具:检查并自动修复中英文间距、全角标点、术语统一等问题,提供命令行工具与可复用库。

    chinese
    typography
    linter
    markdown
    cli
    proofreading
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    24 days ago
    Downloads
    12

    Dependencies

    #mbt-typeset

    English | 简体中文

    中文排版校对工具(MoonBit 实现) —— 像 ESLint 一样检查中文排版,支持一键自动修复与 CI 集成。

    mbt-typeset 用纯 MoonBit 编写,把「中英文之间有没有空格」「标点是不是全角」「术语大小写是否统一」这类 中文写作中反复出现的排版问题,变成可检查、可自动修复、可放进 CI 的工程化流程。

    $ mbt-typeset --markdown 文档.md 文档.md:3:5: warning [cjk-latin-space] 中文与英文或数字之间应插入一个空格 (可自动修复) 文档.md:3:14: warning [fullwidth-punct] 中文语境中应使用全角标点「,」 (可自动修复) 文档.md:3:24: warning [ellipsis] 省略号应使用中文省略号「……」 (可自动修复)

    $ mbt-typeset --markdown --fix 文档.md 文档.md: 已应用 34 处修复,剩余 0 处问题

    修复前:本文使用MoonBit编写,演示常见的排版问题... 修复后:本文使用 MoonBit 编写,演示常见的排版问题……

    完整的前后对比见 examples/before.md 与 examples/after.md。


    #为什么做这个项目

    在动手之前,我们对 mooncakes.io 的官方索引(728 个用户 / 2396 个模块)做了全量排查:

    方向生态现状
    Markdown 解析 / 渲染已有 mizchi/markdown、moon-ssg 等
    JSON Schema / 校验已有 mizchi/jsonschema、Betterlol/moon_zod
    CSV / SQL / 数据库已有 maria/csv_parser、uiwcvb/moonsql、sqlparser
    序列化 / 网络协议已有 mizchi/cbor、hackwaly/msgpack、http11、moonbit-mqtt
    拼音 / 分词 / emoji已有 walkzzz/pinyin、colmugx/jieba、fundon/emoji
    中文排版校对索引中不存在任何排版检查 / 校对类包

    于是我们把目标定在了一个生态空白、边界清晰、且能客观验收的题目上: 把成熟生态里已有的工具(如 textlint、pangu.js、中文文案排版指北)所覆盖的能力,在 MoonBit 里从零实现一遍, 并提供命令行工具与可复用的库。

    #特性

    • 30 条排版规则,覆盖间距、标点、引号、术语、Markdown 结构五大类
    • 一键自动修复,满足幂等性:修完再修不会产生新改动
    • Markdown 感知:自动跳过代码围栏、行内代码、链接地址、URL、前置元数据(front matter)
    • 三种输出格式:text(人读)、json(机器读)、github(Actions 注解)
    • 可配置:JSON 配置文件 + 命令行覆盖,规则可逐条开关
    • 零外部依赖:纯 MoonBit 实现,只依赖标准库与 moonbitlang/x 的文件 API

    #规则一览

    规则 id默认可修复说明
    cjk-latin-space开是中文与英文、数字之间插入空格
    fullwidth-punct开是中文语境中的半角标点改为全角
    fullwidth-alnum开是中文语境中的全角字母数字改为半角
    curly-quote开是中文语境中的直引号改为弯引号
    quote-pairing开否引号必须成对出现
    duplicate-punct开是重复标点只保留一个
    ellipsis开是省略号统一为「……」
    dash开是中文破折号统一为「——」
    number-range开是数字区间号统一为半角 ~
    space-around-punct开是全角标点周围不应有多余空格
    number-unit-space开是数字与单位之间插入空格
    percent-sign开是百分号与数字之间不应有空格
    terminology开是术语大小写与写法统一(如 github → GitHub)
    trailing-whitespace开是行尾不应有多余空白
    blank-line-collapse开是连续空行压缩为 1 个
    heading-space开是Markdown 标题的 # 之后应有空格
    heading-level-jump开否Markdown 标题层级不应跳跃
    heading-trailing-punct开是标题末尾不应有句读标点
    list-marker-space开是Markdown 列表标记之后应有空格
    list-marker-consistency开是同文档内列表标记保持一致
    blockquote-space开是Markdown 引用块的 > 之后应有空格
    table-separator开否表格分隔行列数与表头一致
    link-bare开是链接文字与地址相同时可简写为 <地址>
    image-alt开否Markdown 图片应提供 alt 文本
    inline-code-space开是行内代码与中文之间加空格
    emoji-space开是emoji 与中文之间加空格
    unclosed-code-fence开否Markdown 代码围栏未闭合(error 级)
    halfwidth-punct-in-latin关是英文语境中的全角标点改为半角
    consecutive-spaces关是正文中不应出现连续空格
    line-length关否行宽上限检查(需设置 maxLineLength)

    用 mbt-typeset --list-rules 可在终端查看同样的一张表。

    #安装与构建

    前置条件:已安装 MoonBit 工具链 与 git。

    Windows(PowerShell):

    Set-ExecutionPolicy RemoteSigned -Scope CurrentUser irm https://cli.moonbitlang.cn/install/powershell.ps1 | iex

    Linux / macOS:

    curl -fsSL https://cli.moonbitlang.cn/install/unix.sh | bash

    获取源码并构建:

    git clone <this-repo> cd mbt-typeset moon check # 类型检查 moon build # 构建

    #命令行用法

    用法: mbt-typeset [选项] <文件...> 选项: --fix 就地修复可自动修复的问题 --format <text|json|github> 输出格式(默认 text) --config <路径> 读取 JSON 配置 --markdown 按 Markdown 文档处理 --max-line-length <n> 行宽上限(0 表示不检查) --rule <id=on|off> 启用/关闭某条规则(可重复) --terminology <源=标准> 追加术语(可重复) --list-rules 列出全部规则 -v, --version 显示版本 -h, --help 显示帮助 退出码: 0 未发现问题 1 发现 warning 及以上问题 2 用法错误或读写失败

    在仓库内直接运行:

    moon run cmd/main -- --markdown examples/before.md # 检查 moon run cmd/main -- --markdown --fix examples/after.md # 修复 moon run cmd/main -- --format json examples/before.md # JSON 输出 moon run cmd/main -- --list-rules # 规则表

    #只检查某几条规则

    # 关闭术语规则,并开启「英文语境用半角标点」 moon run cmd/main -- --markdown \ --rule terminology=off \ --rule halfwidth-punct-in-latin=on \ examples/before.md

    #配置文件

    --config 指向一个 JSON 文件,命令行参数优先级更高:

    { "markdown": true, "maxLineLength": 120, "rules": { "halfwidth-punct-in-latin": true, "consecutive-spaces": false }, "terminology": { "moonbit": "MoonBit", "javascript": "JavaScript" } }

    完整示例见 examples/config.json。

    #作为库使用

    库包为 Mr-Houjie/mbt-typeset,核心 API 只有三个:

    // 1. 检查:返回按位置排序的诊断列表
    let diags = @lib.lint(source, @lib.Config::markdown())

    // 2. 修复:返回修复结果、应用的编辑数与剩余问题
    let outcome = @lib.fix(source, @lib.Config::markdown())
    println(outcome.output) // 修复后的文本
    println(outcome.applied) // 应用了多少处修复
    println(outcome.remaining) // 仍然存在、但无法自动修复的问题

    // 3. 渲染:text / json / github 三种输出
    let report = @lib.render_text(diags, "文档.md")

    #诊断结构

    pub struct Diagnostic {
    rule : String // 规则 id
    severity : Severity // Error | Warning | Info
    message : String // 说明
    line : Int // 1 起始行号
    column : Int // 1 起始列号(按字符计)
    start : Int // 全文字符偏移(闭端,用于修复)
    end : Int // 全文字符偏移(开端)
    replacement : String? // 可自动修复时的替换文本
    }

    #测试与验收

    项目提供两条相互独立的验证路径:

    #1. 标准单元测试

    moon test

    测试分两层:

    • 一致性语料(conformance.mbt):69 条「输入 → 期望输出」用例,是全部规则的单一事实来源; 每条用例都会校验 lint 报告了期望的规则、fix 的结果等于期望文本、并对结果再跑一次 fix 确认幂等。
    • 黑盒单元测试(mbt-typeset_test.mbt):只通过公开 API 覆盖位置计算、保护掩码、渲染输出、规则表、配置等细节。

    #2. 自检程序(无需 moon test 也能跑)

    moon run cmd/verify # mbt-typeset 自检:共 69 条用例,失败 0 条 # 全部通过

    cmd/verify 直接遍历同一份一致性语料,适合在没有 moon test 支持的环境或 CI 之外做快速回归。

    #设计要点

    mbt-typeset/ ├── charclass.mbt # Unicode 字符分类:CJK 文字 / CJK 标点 / 全角 / ASCII ├── types.mbt # Diagnostic、Severity、RuleInfo、FixOutcome ├── config.mbt # 运行配置与内置术语表 ├── segment.mbt # 扫描:切行、识别代码区域、构造逐字符「保护掩码」 ├── blocks.mbt # 块级分析:标题 / 列表 / 表格 / 缩进代码块 ├── quotes.mbt # 引号配对状态机与中文弯引号转换 ├── rules.mbt # 30 条规则 + 规则注册表 ├── engine.mbt # lint / fix(迭代到不动点)+ 位置计算 ├── render.mbt # text / json / github 渲染 + 规则表 ├── conformance.mbt # 一致性语料 + 自检入口 ├── mbt-typeset_test.mbt # 黑盒测试(只走公开 API) ├── cmd/main # 命令行工具 ├── cmd/verify # 自检程序 ├── examples/ # 前后对比示例、配置示例 └── PROJECT_PROPOSAL.md # 大赛项目申报书

    #为什么修复是幂等的

    fix 不是「一条规则改一遍」,而是反复执行「检查 → 应用互不重叠的可修复项」,直到没有任何可修复项 (最多 8 轮)。因此:

    • 规则之间即使相互影响(例如先把半角逗号改成全角,再去掉标点旁的空格),最终也会收敛到稳定结果;
    • 对同一份输入重复调用 fix,第二次不会再产生任何改动,这一点由一致性语料对每条用例强制校验。

    #如何避免误伤代码

    扫描阶段会为每一行构造逐字符的保护掩码:行内代码( `code` )、Markdown 链接目标、URL、邮箱、 尖括号包裹的内容都会被标记为「受保护」,所有内容类规则都会跳过这些位置。代码围栏与 front matter 则整块标记为 code,完全不参与检查。

    #已知边界

    • 只做「确定性强」的排版规则:不做标点语义判断、不做语法检查、不做引号弯直转换(避免误伤)。
    • 数字与单位规则采用保守的单位白名单(不包含单字母单位,避免把 5G 误判为 5 G)。
    • halfwidth-punct-in-latin、consecutive-spaces、line-length 默认关闭,因为它们依赖具体写作约定。

    #许可证

    Block

    pub struct Block {
    kind : BlockKind
    start : Int
    end : Int
    line : Int
    indent : Int
    level : Int
    marker : ListMarker?
    columns : Int
    } derive(Eq,
    Debug
    )

    一个块级元素(以行为单位)。

    Block::equal

    fn Block::equal(Block, Block) -> Bool

    Block::not_equal

    fn Block::not_equal(x : Block, y : Block) -> Bool

    Block::to_repr

    BlockKind

    pub enum BlockKind {
    Heading
    ListItem
    BlockQuote
    TableRow
    TableSeparator
    FencedCode
    IndentedCode
    ThematicBreak
    Blank
    Paragraph
    } derive(Eq,
    Debug
    )

    块级元素类型。

    BlockKind::equal

    fn BlockKind::equal(BlockKind, BlockKind) -> Bool

    BlockKind::label

    fn BlockKind::label(self : BlockKind) -> String

    块级元素类型的显示名。

    BlockKind::not_equal

    fn BlockKind::not_equal(x : BlockKind, y : BlockKind) -> Bool

    Case

    pub struct Case {
    name : String
    input : String
    expected : String
    rules : Array[String]
    markdown : Bool
    enable : Array[String]
    disable : Array[String]
    max_line_length : Int
    } derive(Eq,
    Debug
    )

    一条排版一致性用例。

    Case::equal

    fn Case::equal(Case, Case) -> Bool

    Case::not_equal

    fn Case::not_equal(x : Case, y : Case) -> Bool

    Case::to_repr

    Config

    pub struct Config {
    rules : Map[String, Bool]
    terminology : Map[String, String]
    markdown : Bool
    max_line_length : Int
    } derive(
    Debug
    )

    运行配置。

    默认配置面向「纯文本 + 中文排版」,Config::markdown() 会额外打开 与 Markdown 结构相关的规则(标题、列表、引用块),并跳过代码块内容。

    Config::is_enabled

    fn Config::is_enabled(self : Config, id : String, default : Bool) -> Bool

    查询某条规则是否启用;未显式配置时回落到该规则的默认值。

    Config::markdown

    fn Config::markdown() -> Config

    Markdown 默认配置。

    Config::new

    fn Config::new() -> Config

    纯文本默认配置。

    Config::set_markdown

    fn Config::set_markdown(self : Config, value : Bool) -> Unit

    设置是否按 Markdown 文档处理。

    Config::set_max_line_length

    fn Config::set_max_line_length(self : Config, value : Int) -> Unit

    设置行宽上限(0 表示不检查)。

    Config::to_repr

    Config::with_max_line_length

    fn Config::with_max_line_length(self : Config, n : Int) -> Config

    设置行宽上限(0 表示不检查)。

    Config::with_rule

    fn Config::with_rule(self : Config, id : String, enabled : Bool) -> Config

    打开 / 关闭某条规则。

    Config::with_terminology

    fn Config::with_terminology(self : Config, from : String, to : String) -> Config

    追加 / 覆盖一条术语。

    Diagnostic

    pub struct Diagnostic {
    rule : String
    severity : Severity
    message : String
    line : Int
    column : Int
    start : Int
    end : Int
    replacement : String?
    } derive(Eq,
    Debug
    )

    一条排版诊断。

    位置同时提供两种表达:
    • start / end:全文 字符 偏移(end 为开区间),用于应用修复;
    • line / column:1 起始的行列号(按字符计),用于展示。

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    Diagnostic::to_json

    fn Diagnostic::to_json(self : Diagnostic) -> Json

    单条诊断转 JSON。

    FixOutcome

    pub struct FixOutcome {
    output : String
    applied : Int
    remaining : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    一次自动修复的结果。

    FixOutcome::equal

    fn FixOutcome::equal(FixOutcome, FixOutcome) -> Bool

    FixOutcome::not_equal

    fn FixOutcome::not_equal(x : FixOutcome, y : FixOutcome) -> Bool

    ListMarker

    pub enum ListMarker {
    Bullet(Char)
    Ordered(Char)
    } derive(Eq,
    Debug
    )

    列表标记。

    ListMarker::equal

    fn ListMarker::equal(ListMarker, ListMarker) -> Bool

    ListMarker::not_equal

    fn ListMarker::not_equal(x : ListMarker, y : ListMarker) -> Bool

    QuoteEdit

    pub struct QuoteEdit {
    start : Int
    end : Int
    text : String
    opening : Bool
    } derive(Eq,
    Debug
    )

    一次引号替换编辑。

    QuoteEdit::equal

    fn QuoteEdit::equal(QuoteEdit, QuoteEdit) -> Bool

    QuoteEdit::not_equal

    fn QuoteEdit::not_equal(x : QuoteEdit, y : QuoteEdit) -> Bool

    QuoteKind

    pub enum QuoteKind {
    DoubleQuote
    SingleQuote
    } derive(Eq,
    Debug
    )

    引号类型。

    QuoteKind::equal

    fn QuoteKind::equal(QuoteKind, QuoteKind) -> Bool

    QuoteKind::label

    fn QuoteKind::label(self : QuoteKind) -> String

    单引号 / 双引号的显示名,用于诊断与展示。

    QuoteKind::not_equal

    fn QuoteKind::not_equal(x : QuoteKind, y : QuoteKind) -> Bool

    QuoteScan

    pub struct QuoteScan {
    edits : Array[QuoteEdit]
    unpaired : Array[UnpairedQuote]
    } derive(
    Debug
    )

    一行的引号扫描结果。

    RuleInfo

    pub struct RuleInfo {
    id : String
    description : String
    severity : Severity
    enabled_by_default : Bool
    fixable : Bool
    } derive(Eq,
    Debug
    )

    规则元信息,用于 --list-rules 与配置校验。

    RuleInfo::equal

    fn RuleInfo::equal(RuleInfo, RuleInfo) -> Bool

    RuleInfo::not_equal

    fn RuleInfo::not_equal(x : RuleInfo, y : RuleInfo) -> Bool

    RuleInfo::to_repr

    Severity

    pub enum Severity {
    Error
    Warning
    Info
    } derive(Eq,
    Debug
    )

    诊断的严重级别。

    Severity::equal

    fn Severity::equal(Severity, Severity) -> Bool

    Severity::label

    fn Severity::label(self : Severity) -> String

    严重级别的短标签。

    Severity::not_equal

    fn Severity::not_equal(x : Severity, y : Severity) -> Bool

    Severity::to_repr

    UnpairedQuote

    pub struct UnpairedQuote {
    start : Int
    end : Int
    kind : QuoteKind
    opening : Bool
    } derive(Eq,
    Debug
    )

    未配对的引号。

    UnpairedQuote::equal

    UnpairedQuote::not_equal

    fn UnpairedQuote::not_equal(x : UnpairedQuote, y : UnpairedQuote) -> Bool

    analyze_blocks

    fn analyze_blocks(source : String, markdown : Bool) -> Array[Block]

    分析文本的块级结构(每行一个块)。

    库级入口,供外部工具直接获取块结构;内部规则复用同一实现。

    conformance_cases

    fn conformance_cases() -> Array[Case]

    内置用例集。

    convert_straight_quotes

    fn convert_straight_quotes(text : String) -> String

    把中文语境中的直引号转换为弯引号。

    这是纯文本级 API(不做 Markdown 区域识别);库内规则使用掩码版本, 会自动跳过行内代码、URL 等区域。

    default_terminology

    fn default_terminology() -> Array[(String, String)]

    内置术语表:小写形式 -> 权威写法。

    只收录「几乎不会误伤」的专有名词,避免把普通英文单词也一起改了。

    find_unpaired_quotes

    fn find_unpaired_quotes(text : String) -> Array[UnpairedQuote]

    找出文本中未配对的引号(同时覆盖直引号与弯引号场景)。

    fix

    fn fix(source : String, config : Config) -> FixOutcome

    自动修复文本。

    反复「检查 -> 应用可修复项」直到收敛(最多 8 轮),因此结果是幂等的: 对同一份输入重复调用 fix,第二次不会再产生任何改动。

    fullwidth_alnum_to_ascii

    fn fullwidth_alnum_to_ascii(c : Char) -> Char?

    全角字母 / 数字 -> 半角 ASCII(A → A、0 → 0)。

    全角形式与对应的 ASCII 码点相差固定值 0xFEE0。

    has_problems

    fn has_problems(diags : Array[Diagnostic]) -> Bool

    是否存在严重级别 >= Warning 的诊断(用于 CLI 退出码)。

    heading_level_of

    fn heading_level_of(chars : Array[Char]) -> Int

    标题层级:# 开头返回 1-6,否则 0。

    indent_width

    fn indent_width(chars : Array[Char]) -> Int

    计算行首缩进宽度,tab 记为 4 列。

    is_ascii_alnum

    fn is_ascii_alnum(c : Char) -> Bool

    判断是否为 ASCII 字母或数字。

    is_ascii_punct

    fn is_ascii_punct(c : Char) -> Bool

    判断是否为 ASCII 标点。

    is_blank_line

    fn is_blank_line(chars : Array[Char]) -> Bool

    是否为空白行(只含空格与制表符)。

    is_cjk_ideograph

    fn is_cjk_ideograph(c : Char) -> Bool

    判断是否为 CJK 文字(汉字 / 假名 / 谚文)。

    注意:这里刻意 不 包含 CJK 标点与全角符号, 标点需要单独判断(见 is_cjk_punct),否则「中文,英文」之间的逗号会被误判为文字。

    is_cjk_punct

    fn is_cjk_punct(c : Char) -> Bool

    判断是否为 CJK 标点(、。,;:?!「」『』【】《》()— … · 等)。

    is_emoji

    fn is_emoji(c : Char) -> Bool

    判断是否为 emoji 字符。

    覆盖常见表情符号区段;不追求完整 Unicode 覆盖,只用于排版间距提示。

    is_fullwidth_form

    fn is_fullwidth_form(c : Char) -> Bool

    判断是否为全角 ASCII 变体(A-Z a-z 0-9 等,U+FF01..U+FF5E)。

    is_fullwidth_punct

    fn is_fullwidth_punct(c : Char) -> Bool

    判断是否为全角标点(中文语境中使用的标点)。

    is_indented_code_line

    fn is_indented_code_line(chars : Array[Char]) -> Bool

    是否为缩进代码块行(缩进 >= 4 且行内有内容)。

    Markdown 中缩进 4 空格(或 1 个制表符)的连续行构成代码块, 其中的内容不应参与排版检查。

    is_known_rule

    fn is_known_rule(id : String) -> Bool

    判断规则 id 是否已知。

    is_straight_quote

    fn is_straight_quote(c : Char) -> Bool

    判断是否为直引号(" 或 ')。

    is_table_separator

    fn is_table_separator(chars : Array[Char]) -> Bool

    是否为 Markdown 表格的分隔行(|---|---|、| :--- | ---: | 等)。

    is_thematic_break

    fn is_thematic_break(chars : Array[Char]) -> Bool

    是否为分割线(--- / *** / ___,至少 3 个)。

    is_variation_selector

    fn is_variation_selector(c : Char) -> Bool

    判断是否为变体选择符(U+FE0E / U+FE0F),常紧跟 emoji。

    lint

    fn lint(source : String, config : Config) -> Array[Diagnostic]

    对文本做排版检查。

    返回值按位置排序,line / column 已填充。

    lint_with_default_config

    fn lint_with_default_config(source : String) -> Array[Diagnostic]

    对文本执行排版检查。

    list_marker_of

    fn list_marker_of(chars : Array[Char]) -> ListMarker?

    列表标记:返回标记种类,非列表行为 None。

    render_github

    fn render_github(diags : Array[Diagnostic], file : String) -> String

    GitHub Actions 注解格式:::warning file=...,line=...,col=...::消息。

    render_json

    fn render_json(diags : Array[Diagnostic], file : String) -> String

    JSON 格式输出(便于其它工具消费)。

    render_rule_list

    fn render_rule_list() -> String

    规则列表的文本表格(--list-rules)。

    render_text

    fn render_text(diags : Array[Diagnostic], file : String) -> String

    文本格式输出:文件:行:列: 级别 [规则] 说明。

    file 为空时省略文件前缀。

    rule_infos

    fn rule_infos() -> Array[RuleInfo]

    全部规则的元信息。

    rules

    fn rules() -> Array[RuleInfo]

    全部规则。数组顺序即为诊断的默认排序优先级(同位置时)。

    run_conformance

    fn run_conformance() -> Array[String]

    运行全部内置用例。

    返回失败描述列表;返回空数组表示全部通过。 每条用例都会验证三件事:
    1. lint 报告了期望的规则;
    2. fix 的结果等于期望文本;
    3. 对结果再跑一次 fix 不再产生改动(幂等)。

    table_cells

    fn table_cells(chars : Array[Char]) -> Array[String]

    统计一行中表格单元格的文本内容(去掉首尾空白);非表格行返回空数组。

    table_column_count

    fn table_column_count(chars : Array[Char]) -> Int

    统计表格列数;非表格行返回 0。

    以不在受保护区域内的 | 作为分隔符统计,首尾的 | 不计入列数。