npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lint-md/parser

v0.3.1

Published

lint-md 的解析器,基于 remark 生态,将 Markdown 字符串转换成 AST

Readme

@lint-md/parser

lint-md 的解析器,基于 remark 生态,将 Markdown 字符串转换成 AST。

单独拆包封装一层的意义是当前 remark 的稳定版本只支持 ESModule,但是 lint-md 主模块依赖了很多 CommonJS 的库,故无法直接迁移到 ESModule。

故将使用 remark 的代码抽离到单独模块,通过 esbuild 同时提供 CommonJS 和 ESModule 产物。

快速开始

import { parseMd } from '@lint-md/parser';

// 将 markdown 转换成 ast
const ast = parseMd('你的 Markdown 文本');

序列化说明

revertMdAstNode 会将 AST 序列化为 Markdown,但 不保证恢复原始文本。 revertMdAstNode(parseMd(md)) 会对 Markdown 做规范化:

  • 合法的 autolink(www.…、http://…、[email protected])会被序列化为标准 GFM 格式
  • 波浪线删除线 ~text~ 会规范化为 ~~text~~
  • 部分特殊字符会被转义(如 "www.google.com" 在序列化时变为 "www\.google.com")

如需语义更明确的名字,可使用别名 stringifyMdAst,功能与 revertMdAstNode 完全相同。

位置契约

parseMd 返回的 AST 节点总是带 position,且 position.start 与 position.end 的 line / column / offset 字段都是 number(而非 undefined)。 这个契约通过 PositionedMarkdownRoot / PositionedMarkdownNode 在类型层表达, 因此直接解析和遍历 AST 时不需要额外判空:

import { parseMd, type PositionedMarkdownNode } from '@lint-md/parser';

const ast = parseMd('# title');
ast.position.start.offset; // number

const firstNode: PositionedMarkdownNode = ast.children[0];
firstNode.position.end.offset; // number

注意:MarkdownRoot / MarkdownNode 仍透传 mdast 原生类型(即 position 可选), 因为外部构造或修改后的 AST 未必带 position。如果需要“必有 position”约束,使用 PositionedMarkdownRoot / PositionedMarkdownNode。

如果要向解析结果插入自行构造的无 position 节点,请先将其类型放宽为 MarkdownRoot, 或为新节点补齐 position。

源码映射(source map)

parseMd 只保证每个节点在原始 Markdown 中的整体范围(node.position)。但对于 text 节点,node.value 已经过归一化(如 \( → (、& → &),而 node.position 仍指向原始文本。要回答“node.value 中的某一段对应原文哪一段”, 请使用 parseMdWithSourceMap:

import { parseMdWithSourceMap } from '@lint-md/parser';

const { ast, sourceMap } = parseMdWithSourceMap('A&B');

const textNode = ast.children[0].children[0]; // text value "A&B"
// 把 value 中第 1 个字符(解码后的 '&')映射回原始 Markdown 的 '&' 区间
const range = sourceMap.getSourceRange(textNode, 1, 2);
// range.start.offset === 1, range.end.offset === 6

// 为顺序扫描建立轻量索引。结果是原始 Markdown 的绝对 offset。
const sourceIndex = sourceMap.getValueSourceIndex(textNode);
sourceIndex.sourceOffsetAt(1); // 1

// 取回该 text 节点对应的原始 Markdown 子串
sourceMap.getRaw(textNode); // 'A&B'

为什么由 parser 提供

只有 tokenizer / AST 编译器掌握完整上下文,才能正确处理:

  • \( 是否被解析为转义字符
  • &amp; 是否被解析为字符引用,还是(如在 autolink <https://…?a&amp;b> 中)保持字面量
  • 数值字符引用被归一化成什么字符,非法码点是否变为替换字符(U+FFFD)

下游若自行重新解析或对齐,会与 parser / 实体库的语义产生漂移。parseMdWithSourceMap 在同一次 micromark → mdast 编译过程中直接记录映射,复用与 parseMd 完全相同的 解码决策路径。

契约

  • getSourceRange(node, valueStart, valueEnd) 的索引与 JavaScript 字符串下标一致,范围均为半开区间 [start, end);当前支持 text.value、inlineCode.value 与 block code.value。getFieldSourceRange(node, 'url', valueStart, valueEnd) 当前支持 inline resource link 与 definition 的 destination;autolink 和 GFM autolink literal 暂不包含。
  • getValueSourceIndex(node).sourceOffsetAt(valueIndex) 返回原始 Markdown 的绝对 offset。该接口为顺序边界查询保留游标。反向和随机查询使用二分查找。
  • 映射覆盖受支持节点的整个 value,segment 之间无空洞、无重叠。
  • 当 value 对应的原始源码连续时,getSourceRange(node, 0, node.value.length) 覆盖该节点 value 的完整原始来源范围。当 blockquote marker、list indentation 等容器语法将来源分隔开时,单个连续的 ParsedPosition 无法准确表达该范围,getSourceRange() 会抛出 RangeError。
  • 错误分为三条路径,专属错误均继承 RangeError(现有 catch (RangeError) 不受影响),并带稳定的 code 字段;当跨边界传递时(如跨 CJS/ESM 实例、重复安装、worker 边界),只要错误被显式序列化且 code 字段被保留,即可用 code 而非 instanceof 判断(类本身无法保证任意序列化机制一定保留自定义属性):
    • SourceMapConsistencyError(ERR_SOURCE_MAP_CONSISTENCY):已建立映射的 text、inlineCode、code 节点的 value,或 link、definition 节点的 url 在解析后被修改——映射只对原始解析字段有效,重新赋入相同内容不受影响;
    • SourceMapUnavailableError(ERR_SOURCE_MAP_UNAVAILABLE):节点属于其他文档、由插件生成或在解析后加入、或不是受支持的节点或字段——不会伪造位置;
    • 普通 RangeError:valueStart / valueEnd 非法(非有限整数、越界、倒置)、范围跨越非连续的源码片段,或空区间落在原子构造内部。
  • 当前版本覆盖 text.value、inlineCode.value、code.value、inline resource link 的 url 与 definition 的 url;其余字段后续版本补充。

开发验证

pnpm run build
pnpm run test:package

test:package 会打包并安装实际 tarball,然后验证 CommonJS、ESModule 和 TypeScript 类型入口。

维护说明

环境要求

  • Node.js >= 20
  • pnpm 11(参见 packageManager 字段)
pnpm install --frozen-lockfile   # 首次安装使用锁文件

公开 API 管理

本项目使用 API Extractor 管理公开 API。

  • 所有公开导出必须添加 /** @public */ 标签
  • JSDoc 注释须遵循 TSDoc 格式:参数写 @param name - 说明(不写类型),返回值写 @returns 说明
  • 修改公开 API 后运行 pnpm run build,API Extractor 会自动更新 etc/parser.api.md
  • 将更新后的 API report 文件一并提交

CI 检查

  • etc/parser.api.md 过期会导致 CI 构建失败
  • API Extractor 报告的 compiler / extractor / TSDoc warning 均视为错误

License

MIT