Sign in

    MoonRTF

    Pure MoonBit RTF parsing, structured extraction, diagnostics, and text conversion.

    Download zip
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    25 days ago
    Downloads
    13

    Dependencies

    #MoonRTF

    CI

    纯 MoonBit 的 RTF 文档解析与文本提取库,附原生 CLI。面向文档导入、搜索索引和格式转换。支持中文 Unicode、基础样式、简单表格、超链接、元数据、错误诊断和来源定位。

    Pure MoonBit RTF parsing and extraction with bounded inputs, source ranges, diagnostics, and a native command-line interface. The parser does not invoke Word, Python, LibreOffice or an OS rich-text control.

    #快速运行

    本项目的本地验证基线是 2026-09-15 发布的 MoonBit 工具链,组件版本由 moon version --all 核实:

    组件完整版本构建日期
    构建工具 moon0.1.20260915 (2e1a46d)2026-09-15
    编译器 mooncv0.10.13+cbb11c36f2026-09-15
    执行器 moonrun0.1.20260915 (2e1a46d)2026-09-15

    工具链安装入口见 MoonBit 下载页。安装后执行 moon version --all 核对版本;下载页的 stable 渠道会更新,并不固定为上述版本。其他工具链版本需重新运行本文的验证命令,不将“更新版本”视为已验证兼容。

    原生 CLI 需要 C 编译环境;本地 Windows 构建使用 MSVC。库支持 wasm、wasm-gc、js、native 四个后端。Python 3 用于仓库内的行数统计与 CLI 检查,不是库或 CLI 的运行时依赖。

    git clone https://github.com/tlhuecdkoyg/MoonRTF.git cd MoonRTF moon update moon run examples/basic moon run --target native cmd/main -- text fixtures/basic.rtf moon run --target native cmd/main -- json fixtures/basic.rtf --pretty moon run --target native cmd/main -- markdown fixtures/basic.rtf moon run --target native cmd/main -- html fixtures/basic.rtf -o fragment.html moon run --target native cmd/main -- csv fixtures/table.rtf moon run --target native cmd/main -- validate fixtures/malformed.rtf

    最后一个命令故意返回 1,展示截断文件的诊断。普通正文输出包含 中文 😀。命令的所有选项见 moon run --target native cmd/main -- --help。

    #库接口

    模块发布名为 tlhuecdkoyg/MoonRTF。版本源码见 GitHub 标签,已发布版本及 API 文档见 Mooncakes。安装指定版本:

    moon add tlhuecdkoyg/MoonRTF@0.2.0

    已有依赖的消费项目需使用 moon add --upgrade tlhuecdkoyg/MoonRTF@0.2.0 显式升级;普通 moon add 不替换已有版本。

    在消费包的 moon.pkg 导入模块并指定别名 @rtf。以下是直接针对本包运行的文档测试(测试使用本包默认别名 @MoonRTF):

    ///|
    test "README extraction" {
    let result = @MoonRTF.parse(
    b"{\\rtf1 Hello {\\b MoonRTF}! \\u20013?\\u25991?}",
    )
    assert_true(result.acceptable())
    assert_eq(result.document.to_text(), "Hello MoonRTF! 中文")
    assert_eq(result.document.find_text("MoonRTF").length(), 1)
    }

    推荐使用 parse 并检查 complete / diagnostics。extract_text 是便捷接口,不返回诊断,不能用于判断输入是否有效。validate 可额外检查未定义字体、颜色和链接策略。

    主要 API:

    • parse / validate / scan:结构解析、语义校验、词法检查。
    • Document::to_text / to_markdown / to_html:可配置正文导出。
    • ParseResult::to_json_text:moonrtf.document.v1 结构化输出。
    • Document::tables / Table::to_csv:简单表格提取。
    • Document::mapped_text / find_text:定位到原始字节范围。
    • MappedText::source_range(start, end):将非空 UTF-16 半开选区映射到相关 run 的输入字节范围及段落编号。越界、空选区、反向选区返回 None;仅包含生成分隔符的合法选区返回空来源列表。
    • ParseResult::convert:返回转换结果、格式损失诊断及输出限额状态。
    • Document::statistics / created_time / revised_time:统计与时间元数据。

    #支持边界

    正确处理 \uN / \ucN、代理对、Windows-1252,以及显式 \ansicpg65001 UTF-8。GBK/CP936 等其他传统编码尚不支持;受影响字符会有诊断,结果标记为不完整。Unicode 转义形式的中文不受此限制。

    支持单层表格、基础字符/段落样式和字段显示文本。图片与对象只记录元数据,不渲染。默认正文导出排除隐藏文字、页眉、页脚和注释,可显式启用。JSON 保留已解析的隐藏文字,因此分享结构化输出前应考虑这一点。

    不承诺完整 Word 排版、RTF 写回、嵌套/合并表格、自动列表编号、压缩 RTF、宏执行或完整 RTF 1.9.1 一致性。Markdown 将首行视为表头,复杂格式会降级;HTML 为语义片段,不是像素级渲染。详细矩阵见 support-matrix。

    #CLI 与限额

    命令:text、json、markdown、html、inspect、validate、tokens、source-map、csv。省略文件名或使用 - 从 stdin 读取。-o FILE 写文件,默认拒绝覆盖,--force 才允许覆盖已有输出;输入文件受到保护。

    html 输出语义 HTML 片段,保留基础样式和单层表格,转义正文及链接属性,并限制可激活的链接协议。它不生成完整 HTML 页面,不加载嵌入对象或外部资源;区域和隐藏文本选项与库接口一致。

    退出码:0 成功;1 文档错误、资源限额或所选校验策略失败;2 参数/文件 I/O 错误。诊断与正常输出分流。--recover 保留可恢复内容,但错误仍返回 1;--strict 使警告也影响成功判定。

    默认输入上限 32 MiB、嵌套 256 层、文档节点 200000、解码文本 33554432 个 UTF-16 单元、诊断 1000 条。CLI 在读取时检查输入上限。输出上限在序列化后检查,不是序列化期间的峰值内存保证。

    #验证与代码规模

    moon check --target all --deny-warn moon test --target all --deny-warn moon fmt --check moon info python scripts/count_lines.py --minimum 4000 moon build --target native --release cmd/main python scripts/smoke_cli.py python scripts/stress_cli.py

    行数脚本统计生产 .mbt 中含代码的物理行,排除注释、空行、帮助文字块、测试、示例、生成接口和依赖;此指标描述源码规模,不单独代表功能完整性或工程质量。测试覆盖随机字节、逐字节截断、Unicode、编码错误、分组作用域、表格、导出、来源选区映射与 CLI。

    CI 在 Linux/Windows 上安装运行当时 stable 渠道的工具链,并在日志中记录完整版本;这是持续兼容性检查,与上面的固定本地验证基线有区别。版本验证记录见 0.2.0 验证报告。

    工程设计见 design,发布步骤见 release,许可证与参考资料见 THIRD_PARTY。

    #许可证

    Apache-2.0。依据公开规范独立实现。样例为项目自制;AI 辅助代码经过编译、测试与审查,具体限制在上述文档中公开。

    Alignment

    pub(all) enum Alignment {
    Left
    Right
    Center
    Justify
    } derive(Eq, ToJson,
    Debug
    )

    Attachment

    pub(all) struct Attachment {
    kind : String
    span : Span
    declared_width : Int?
    declared_height : Int?
    } derive(Eq, ToJson,
    Debug
    )

    Color

    pub(all) struct Color {
    red : Int
    green : Int
    blue : Int
    automatic : Bool
    } derive(Eq, ToJson,
    Debug
    )

    Conversion

    pub(all) struct Conversion {
    content : String
    diagnostics : Array[Diagnostic]
    exceeded_limit : Bool
    } derive(Eq, ToJson,
    Debug
    )

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    severity : Severity
    message : String
    span : Span
    } derive(Eq, ToJson,
    Debug
    )

    Diagnostic::hint

    fn Diagnostic::hint(self : Diagnostic) -> String

    Diagnostic::is_error

    fn Diagnostic::is_error(self : Diagnostic) -> Bool

    DiagnosticSummary

    pub(all) struct DiagnosticSummary {
    errors : Int
    warnings : Int
    distinct_codes : Int
    first_error : Int?
    } derive(Eq, ToJson,
    Debug
    )

    Document

    pub(all) struct Document {
    paragraphs : Array[Paragraph]
    fonts : Array[Font]
    colors : Array[Color]
    metadata : Array[Metadata]
    attachments : Array[Attachment]
    input_bytes : Int
    } derive(Eq, ToJson,
    Debug
    )

    Document::body_paragraphs

    fn Document::body_paragraphs(self : Document) -> Array[Paragraph]

    Document::character_count

    fn Document::character_count(self : Document) -> Int

    Document::conversion_diagnostics

    fn Document::conversion_diagnostics(self : Document, format : OutputFormat, include_hidden? : Bool) -> Array[Diagnostic]

    Identify representation losses before selecting an output format.

    Document::created_time

    fn Document::created_time(self : Document) -> DocumentTime?

    Document::find_text

    fn Document::find_text(self : Document, needle : String, include_hidden? : Bool, region? : Region, max_matches? : Int) -> Array[TextMatch]

    Document::font

    fn Document::font(self : Document, id : Int) -> Font?

    fn Document::links(self : Document) -> Array[String]

    Document::mapped_text

    fn Document::mapped_text(self : Document, include_hidden? : Bool, region? : Region) -> MappedText

    UTF-16 output ranges map to encompassing byte ranges, not one-to-one bytes.

    Document::metadata_value

    fn Document::metadata_value(self : Document, key : String) -> String?

    Document::revised_time

    fn Document::revised_time(self : Document) -> DocumentTime?

    Document::runs_at_source

    fn Document::runs_at_source(self : Document, byte_offset : Int) -> Array[Run]

    Document::statistics

    fn Document::statistics(self : Document) -> Statistics

    Words are whitespace-delimited segments, not language-specific segmentation.

    Document::tables

    fn Document::tables(self : Document) -> Array[Table]

    Group contiguous table paragraphs. No inference of merged cells is made.

    Document::to_html

    fn Document::to_html(self : Document, options? : TextOptions) -> String

    Semantic HTML fragment. No scripts, embedded resources, or raw RTF HTML.

    Document::to_markdown

    fn Document::to_markdown(self : Document, options? : TextOptions) -> String

    Document::to_text

    fn Document::to_text(self : Document, options? : TextOptions) -> String

    Document::validate

    fn Document::validate(self : Document, max_diagnostics? : Int) -> Array[Diagnostic]

    Check references and structural invariants in the extracted document.

    DocumentTime

    pub(all) struct DocumentTime {
    year : Int
    month : Int
    day : Int
    hour : Int
    minute : Int
    second : Int
    } derive(Eq, ToJson,
    Debug
    )

    DocumentTime::from_iso

    fn DocumentTime::from_iso(text : String) -> DocumentTime?

    DocumentTime::to_iso

    fn DocumentTime::to_iso(self : DocumentTime) -> String

    RTF timestamps do not declare a timezone; no UTC suffix is fabricated.

    DocumentTime::valid

    fn DocumentTime::valid(self : DocumentTime) -> Bool

    Font

    pub(all) struct Font {
    id : Int
    name : String
    charset : Int
    codepage : Int?
    family : String
    } derive(Eq, ToJson,
    Debug
    )

    Limits

    pub(all) struct Limits {
    input_bytes : Int
    depth : Int
    nodes : Int
    output_units : Int
    diagnostics : Int
    } derive(Eq, ToJson,
    Debug
    )

    Limits::default

    fn Limits::default() -> Limits

    Limits::valid

    fn Limits::valid(self : Limits) -> Bool

    Location

    pub(all) struct Location {
    line : Int
    column : Int
    offset : Int
    } derive(Eq, ToJson,
    Debug
    )

    MappedText

    pub(all) struct MappedText {
    text : String
    mappings : Array[TextMapping]
    } derive(Eq, ToJson,
    Debug
    )

    MappedText::find_all

    fn MappedText::find_all(self : MappedText, needle : String, ascii_insensitive? : Bool, max_matches? : Int, overlapping? : Bool) -> Array[TextMatch]

    Linear-time exact search across style runs. Ranges use UTF-16 units. ASCII-insensitive mode deliberately avoids length-changing Unicode folding.

    MappedText::source_at

    fn MappedText::source_at(self : MappedText, output_offset : Int) -> Span?

    MappedText::source_range

    fn MappedText::source_range(self : MappedText, start : Int, end : Int) -> TextMatch?

    Map a nonempty, half-open UTF-16 selection to its contributing source runs. Returns None for out-of-bounds, empty or reversed ranges. A selection of generated separators is valid but has no sources. Source spans encompass whole runs, even when only part of a run is selected. As with source_at, offsets may address either UTF-16 unit of a supplementary character.

    MappedText::to_json_text

    fn MappedText::to_json_text(self : MappedText) -> String

    Metadata

    pub(all) struct Metadata {
    key : String
    value : String
    span : Span
    } derive(Eq, ToJson,
    Debug
    )

    OutputFormat

    pub(all) enum OutputFormat {
    PlainText
    Markdown
    Html
    StructuredJson
    } derive(Eq,
    Debug
    )

    Paragraph

    pub(all) struct Paragraph {
    runs : Array[Run]
    style : ParagraphStyle
    region : Region
    table_row : Int?
    table_cell : Int?
    span : Span
    } derive(Eq, ToJson,
    Debug
    )

    Paragraph::text

    fn Paragraph::text(self : Paragraph, include_hidden? : Bool) -> String

    ParagraphStyle

    pub(all) struct ParagraphStyle {
    alignment : Alignment
    left_indent : Int
    right_indent : Int
    first_indent : Int
    space_before : Int
    space_after : Int
    line_spacing : Int
    keep_together : Bool
    keep_next : Bool
    } derive(Eq, ToJson,
    Debug
    )

    ParagraphStyle::default

    ParseResult

    pub(all) struct ParseResult {
    document : Document
    diagnostics : Array[Diagnostic]
    complete : Bool
    } derive(Eq, ToJson,
    Debug
    )

    ParseResult::acceptable

    fn ParseResult::acceptable(self : ParseResult, fail_on_warning? : Bool) -> Bool

    ParseResult::convert

    fn ParseResult::convert(self : ParseResult, format : OutputFormat, options? : TextOptions, max_output_units? : Int) -> Conversion

    Convert a parsed document with an explicit output limit and loss report. Structured JSON keeps parse diagnostics in the interchange representation.

    ParseResult::has_errors

    fn ParseResult::has_errors(self : ParseResult) -> Bool

    ParseResult::has_warnings

    fn ParseResult::has_warnings(self : ParseResult) -> Bool

    ParseResult::inspect

    fn ParseResult::inspect(self : ParseResult) -> String

    ParseResult::to_interchange

    fn ParseResult::to_interchange(self : ParseResult) -> Json

    Stable interchange schema; independent of MoonBit enum serialization.

    ParseResult::to_json_text

    fn ParseResult::to_json_text(self : ParseResult, pretty? : Bool) -> String

    Region

    pub(all) enum Region {
    Body
    Header
    Footer
    Footnote
    Annotation
    } derive(Eq, ToJson,
    Debug
    )

    Run

    pub(all) struct Run {
    text : String
    style : Style
    span : Span
    link : String?
    } derive(Eq, ToJson,
    Debug
    )

    ScanReport

    pub(all) struct ScanReport {
    tokens : Array[TokenInfo]
    diagnostics : Array[Diagnostic]
    truncated : Bool
    consumed_bytes : Int
    maximum_depth : Int
    } derive(Eq, ToJson,
    Debug
    )

    ScanReport::to_json_text

    fn ScanReport::to_json_text(self : ScanReport) -> String

    ScanReport::to_text

    fn ScanReport::to_text(self : ScanReport) -> String

    Severity

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

    Span

    pub(all) struct Span {
    start : Int
    end : Int
    } derive(Eq, ToJson,
    Debug
    )

    Span::contains

    fn Span::contains(self : Span, offset : Int) -> Bool

    Span::length

    fn Span::length(self : Span) -> Int

    Statistics

    pub(all) struct Statistics {
    input_bytes : Int
    paragraphs : Int
    runs : Int
    characters : Int
    hidden_characters : Int
    words : Int
    tables : Int
    rows : Int
    cells : Int
    links : Int
    fonts : Int
    colors : Int
    attachments : Int
    body_paragraphs : Int
    header_paragraphs : Int
    footer_paragraphs : Int
    note_paragraphs : Int
    } derive(Eq, ToJson,
    Debug
    )

    Statistics::to_text

    fn Statistics::to_text(self : Statistics) -> String

    Style

    pub(all) struct Style {
    bold : Bool
    italic : Bool
    underline : Bool
    strike : Bool
    hidden : Bool
    superscript : Int
    font : Int
    size_half_points : Int
    foreground : Int
    background : Int
    } derive(Eq, ToJson,
    Debug
    )

    Style::default

    fn Style::default() -> Style

    Table

    pub(all) struct Table {
    region : Region
    rows : Array[TableRow]
    } derive(Eq, ToJson,
    Debug
    )

    Table::cell

    fn Table::cell(self : Table, row : Int, column : Int) -> TableCell?

    Table::column_count

    fn Table::column_count(self : Table) -> Int

    Table::is_rectangular

    fn Table::is_rectangular(self : Table) -> Bool

    Table::to_csv

    fn Table::to_csv(self : Table, delimiter? : Char, include_hidden? : Bool) -> String

    TableCell

    pub(all) struct TableCell {
    index : Int
    paragraphs : Array[Paragraph]
    } derive(Eq, ToJson,
    Debug
    )

    TableCell::text

    fn TableCell::text(self : TableCell, include_hidden? : Bool) -> String

    TableRow

    pub(all) struct TableRow {
    index : Int
    cells : Array[TableCell]
    } derive(Eq, ToJson,
    Debug
    )

    TextMapping

    pub(all) struct TextMapping {
    paragraph : Int
    run : Int
    output_start : Int
    output_end : Int
    source : Span
    } derive(Eq, ToJson,
    Debug
    )

    TextMatch

    pub(all) struct TextMatch {
    output : Span
    sources : Array[Span]
    paragraphs : Array[Int]
    } derive(Eq, ToJson,
    Debug
    )

    TextOptions

    pub(all) struct TextOptions {
    include_hidden : Bool
    include_headers : Bool
    include_footers : Bool
    include_notes : Bool
    paragraph_separator : String
    cell_separator : String
    trim_trailing_space : Bool
    } derive(Eq,
    Debug
    )

    TextOptions::default

    fn TextOptions::default() -> TextOptions

    TokenInfo

    pub(all) struct TokenInfo {
    kind : String
    span : Span
    control : String?
    parameter : Int?
    depth : Int
    } derive(Eq, ToJson,
    Debug
    )

    ValidationReport

    pub(all) struct ValidationReport {
    valid : Bool
    complete : Bool
    errors : Int
    warnings : Int
    diagnostics : Array[Diagnostic]
    } derive(Eq, ToJson,
    Debug
    )

    ValidationReport::to_json_text

    fn ValidationReport::to_json_text(self : ValidationReport) -> String

    ValidationReport::to_text

    fn ValidationReport::to_text(self : ValidationReport, input : Bytes) -> String

    alignment_name

    fn alignment_name(alignment : Alignment) -> String

    explain_diagnostic

    fn explain_diagnostic(input : Bytes, diagnostic : Diagnostic, context_bytes? : Int) -> String

    Terminal-safe context; input control bytes are escaped before display.

    explain_diagnostics

    fn explain_diagnostics(input : Bytes, diagnostics : Array[Diagnostic], max_messages? : Int) -> String

    extract_text

    fn extract_text(input : Bytes, recover? : Bool, include_hidden? : Bool) -> String

    format_diagnostic

    fn format_diagnostic(input : Bytes, d : Diagnostic) -> String

    locate

    fn locate(input : Bytes, offset : Int) -> Location

    Byte columns, not Unicode display columns. CRLF counts as one newline.

    parse

    fn parse(input : Bytes, recover? : Bool, limits? : Limits) -> ParseResult

    Parse a bounded RTF byte buffer. Recovery never bypasses resource limits.

    region_name

    fn region_name(region : Region) -> String

    scan

    fn scan(input : Bytes, max_tokens? : Int, max_input? : Int) -> ScanReport

    Bounded syntax inspection for editor integrations and troubleshooting.

    source_excerpt

    fn source_excerpt(input : Bytes, span : Span, radius? : Int) -> String

    Show an escaped source excerpt with a bounded byte radius.

    summarize_diagnostics

    fn summarize_diagnostics(diagnostics : Array[Diagnostic]) -> DiagnosticSummary

    validate

    fn validate(input : Bytes, fail_on_warning? : Bool, limits? : Limits) -> ValidationReport