sebastienrousseau/metadata-gen
GitHub: sebastienrousseau/metadata-gen
Rust 生态中集 YAML/TOML/JSON frontmatter 解析与 SEO meta 标签生成于一体的强类型、供应链审计库。
Stars: 2 | Forks: 0
metadata-gen
一个为 Rust 设计的强类型、经过审计的 frontmatter 解析器 —— 支持 YAML、TOML 和 JSON 提取,并可为 SEO、Open Graph、Twitter Cards 和 Apple Web Apps 生成 HTML meta 标签。
## 目录
- [功能简介](#what-it-does)
- [适用场景](#when-to-use-it)
- [安装说明](#install)
- [快速开始](#quick-start)
- [示例](#examples)
- [YAML frontmatter](#yaml-frontmatter)
- [TOML frontmatter](#toml-frontmatter)
- [JSON frontmatter](#json-frontmatter)
- [HTML meta 标签生成](#html-meta-tag-generation)
- [异步文件提取](#asynchronous-file-extraction)
- [对比](#comparisons)
- [性能](#performance)
- [供应链](#supply-chain)
- [MSRV 策略](#msrv-policy)
- [路线图](#roadmap)
- [常见问题](#faq)
- [贡献指南](#contributing)
- [安全性](#security)
- [许可证](#license)
## 功能简介
`metadata-gen` 解析内容文件的 *frontmatter* —— 即 Markdown/HTML 文档顶部的结构化块 ——
并将其转化为可用的 Rust 值,以及一组 SEO meta 标签分组
(`primary`、`og`、`twitter`、`apple`、`ms`)。
它开箱即用地支持三种 frontmatter 格式:
| 格式 | 分隔符 | 示例头部 |
|--------|------------------|------------------------------|
| YAML | `---` / `---` | `title: Hello` |
| TOML | `+++` / `+++` | `title = "Hello"` |
| JSON | `{` / `}` | `{"title": "Hello"}` |
格式检测是自动进行的。检测顺序为 YAML → TOML → JSON;将使用首个起始分隔符匹配成功的格式。嵌套值会通过以点分隔的键(例如 `author.name`)进行扁平化处理,而在当前基于 map 的 API 中,序列会被序列化为 `[a, b, c]` 字符串。
## 适用场景
在以下需求时,请选择 `metadata-gen`:
- 一个 **单一依赖项**,可同时处理 YAML、TOML *和* JSON frontmatter,
而无需你提前选择序列化器。
- 内置 **HTML meta 标签生成器**(支持 Open Graph、Twitter Cards、Apple
Mobile、Microsoft Tiles、基础 SEO),并连接到同一个元数据 map。
- 一个具有 **明确的供应链安全姿态** 的库 —— 强制执行 `cargo-deny`、
`cargo-audit`、SBOM 生成和 `#![forbid(unsafe_code)]`。
- 一个正积极向 **强类型提取**、**零拷贝值**
和 **WASI 0.2 组件** 迈进的库(参见[路线图](#roadmap))。
在仅需解析原始 YAML(直接使用 `serde_yaml_ng`
或 `noyalib`),或*现在*就需要强类型提取(请关注
v0.0.6 的 issue [#42](https://github.com/sebastienrousseau/metadata-gen/issues/42))的情况下,请考虑其他选择。
## 安装说明
```
cargo add metadata-gen
```
或者添加到 `Cargo.toml` 中:
```
[dependencies]
metadata-gen = "0.0.6"
```
最低支持的 Rust 版本:**1.88.0** —— 请参阅
[MSRV 策略](#msrv-policy)。已在 x86_64
和 ARM64 架构的 Linux、macOS 和 Windows 上经过测试。
## 快速开始
```
use metadata_gen::extract_and_prepare_metadata;
let content = "---\n\
title: Hello, world!\n\
description: A short greeting\n\
keywords: rust, frontmatter, seo\n\
---\n\
# 正文从这里开始";
let (metadata, keywords, tags) =
extract_and_prepare_metadata(content).expect("valid frontmatter");
assert_eq!(metadata.get("title"), Some(&"Hello, world!".to_string()));
assert_eq!(keywords, vec!["rust", "frontmatter", "seo"]);
assert!(tags.primary.contains("description"));
```
## 示例
使用 `cargo run --example
` 运行任意示例:
| 示例 | 演示内容 |
|------------------------|-------------------------------------------------------------|
| `lib_example` | 高级 `extract_and_prepare_metadata` + meta 标签流程 |
| `metadata_example` | 按格式提取 (YAML, TOML, JSON) + 嵌套映射 |
| `metatags_example` | 生成 + 提取 HTML `` 标签 |
| `utils_example` | HTML 转义/反转义,异步文件提取 |
| `error_example` | 每一个 `MetadataError` 变体 + 恢复模式 |
### YAML frontmatter
```
use metadata_gen::metadata::extract_metadata;
let content = "---\n\
title: My Post\n\
date: 2026-06-28\n\
author:\n name: Ada\n handle: ada@example.com\n\
tags:\n - rust\n - parsing\n---\n";
let meta = extract_metadata(content).unwrap();
assert_eq!(meta.get("title"), Some(&"My Post".to_string()));
assert_eq!(meta.get("author.name"), Some(&"Ada".to_string()));
assert_eq!(meta.get("tags"), Some(&"[rust, parsing]".to_string()));
```
### TOML frontmatter
```
use metadata_gen::metadata::extract_metadata;
let content = "+++\n\
title = \"My Post\"\n\
date = \"2026-06-28\"\n\
\n\
[author]\n\
name = \"Ada\"\n\
+++\n";
let meta = extract_metadata(content).unwrap();
assert_eq!(meta.get("author.name"), Some(&"Ada".to_string()));
```
### JSON frontmatter
```
use metadata_gen::metadata::extract_metadata;
let content = "{\
\"title\":\"My Post\",\
\"description\":\"Inline JSON header\"\
}\n# Body";
let meta = extract_metadata(content).unwrap();
assert_eq!(meta.get("title"), Some(&"My Post".to_string()));
```
### HTML meta 标签生成
```
use std::collections::HashMap;
use metadata_gen::metatags::generate_metatags;
let mut map = HashMap::new();
map.insert("description".to_string(), "About the page".to_string());
map.insert("og:title".to_string(), "Page Title".to_string());
map.insert("twitter:card".to_string(),"summary_large_image".to_string());
let groups = generate_metatags(&map);
assert!(groups.primary.contains("description"));
assert!(groups.og.contains("og:title"));
assert!(groups.twitter.contains("twitter:card"));
```
### 异步文件提取
```
use metadata_gen::utils::async_extract_metadata_from_file;
#[tokio::main]
async fn main() -> Result<(), Box> {
let (metadata, keywords, tags) =
async_extract_metadata_from_file("post.md").await?;
println!("title = {:?}", metadata.get("title"));
println!("keywords = {:?}", keywords);
println!("og tags =\n{}", tags.og);
Ok(())
}
```
## 对比
| Crate | YAML | TOML | JSON | 强类型提取 | Meta 标签生成 | no_std (计划中) |
|-----------------------|:----:|:----:|:----:|:----------------:|:-------------:|:----------------:|
| **`metadata-gen`** | ✅ | ✅ | ✅ | v0.0.6 路线图 | ✅ | v0.0.9 路线图 |
| `gray_matter` | ✅ | ✅ | ✅ | ✅ | — | — |
| `yaml-front-matter` | ✅ | — | — | ✅ | — | — |
| `matter` | ✅ | — | — | — | — | ✅ |
`gray_matter` 是最相近的现有库。`metadata-gen` 的差异在于
内置的 meta 标签生成器、供应链安全姿态以及 WASI/no_std
路线图。请参阅 [审计演示文稿](docs/AUDIT-2026.md) 了解战略背景。
## 性能
在 2024 年参考笔记本电脑(M-class CPU,单线程)上的单次调用延迟:
| 目标 | 输入大小 | 延迟 |
|-----------------------------------|-----------:|---------:|
| `extract_metadata` (YAML) | ~200 B | ~10 µs |
| `process_metadata` | ~200 B | ~1 µs |
| `generate_metatags` | ~200 B | ~1 µs |
| `escape_html` | ~80 B | ~0.3 µs |
自行运行测试套件:
```
cargo bench --bench metadata_benchmark
```
计划在 v0.0.7 中通过 `Cow<'a, str>`
值、`LazyLock` 静态变量、单次遍历 HTML 转义以及
`memchr::memmem` 分隔符扫描,实现 10–100 倍的吞吐量提升。详情请参见
[v0.0.7](https://github.com/sebastienrousseau/metadata-gen/milestones)。
## 供应链
`metadata-gen` 执行明确的供应链安全姿态:
- **`cargo-deny`** 在每个 PR 上运行(`advisories`、`licenses`、`bans`、
`sources`);任何违规都会导致 CI 失败。
- **`cargo-audit`** 在每个 PR 上按计划每日运行,对 RUSTSEC 数据库进行比对。
- **`#![forbid(unsafe_code)]`** 在整个 crate 范围内强制执行。
- **第一方 0.0.x 依赖项**(`noyalib`、`dtt`)在 `Cargo.toml` 中被严格锁定,
因此上游补件无法在没有经过严格的 `metadata-gen` 发版的情况下
破坏下游使用者。
- **SBOM 生成**(CycloneDX)和 cosign 签名将在 v0.0.5 中落地 —— 请参阅
[路线图](#roadmap)。
已记录的豁免清单位于
[`audit.toml`](audit.toml) 中;每个条目都带有引用
上游追踪 issue 的理由说明。
## MSRV 策略
最低支持的 Rust 版本:**1.88.0**。
我们将 MSRV 视为公共 API 的一部分:版本提升会被打包到
次要(`0.x.0`)发布中,并在 `CHANGELOG.md` 中注明。当前的
1.88.0 基线是由 `dtt 0.0.10 → time 0.3.47 →
time-core =0.1.8` (edition2024) 传递性锁定的。降低基线将在
`time` 中重新引入一个中等严重程度的栈耗尽安全公告,因此我们坚持这一底线。
如果您需要旧的工具链,请提交一个描述您限制的
issue —— 我们很乐意讨论 MSRV 分段分支。
## 路线图
v0.0.4 之后的路线图分为六个主题发布。每个里程碑都在
[GitHub Milestones](https://github.com/sebastienrousseau/metadata-gen/milestones)
上进行跟踪,每个 issue 都包含完整的用户故事和验收标准。
| 版本 | 主题 | 亮点 |
|----------|----------------------------------|---------------------------------------------------------------------------|
| v0.0.5 | **基础强化** | 移除 `tokio = "full"`,`LazyLock` 静态变量,修复 JSON 嵌套花括号 bug,`cargo-deny`/`cargo-audit` 门控,SBOM 生成,rustdoc Actions 部署,README/FAQ 大修。 |
| v0.0.6 | **强类型 API 与人机工程学** | `extract_typed::`,`(Metadata, body: &str)` 返回,构建者模式,按格式划分的 Cargo feature,schema 验证。 |
| v0.0.7 | **零拷贝与性能** | `Cow<'a, str>` 值 API,单次遍历 HTML 转义,`memchr::memmem` 扫描,在 1 KB → 10 MB 下的吞吐量基准测试,Codspeed CI 门控。 |
| v0.0.8 | **正确性与验证** | `proptest` 测试套件,`cargo-fuzz` 目标,夜间 CI 中的 Miri,`cargo-mutants` ≥ 85 % 杀伤率,Kani 证明,≥ 98 % 覆盖率门控。 |
| v0.0.9 | **可移植性** | `no_std + alloc` 核心,与异步运行时无关的 IO,可选的 Tokio/smol/Embassy 适配器,嵌入式 CI 矩阵。 |
| v0.0.10 | **WASI / 蓝海 / 1.0 RC** | 带有 WIT 接口的 `wasm32-wasip2` Component,Cloudflare Workers / Spin / wasmCloud 指南,PQC 签名元数据,MCP 服务器示例,ADR 系列。 |
## 常见问题
### 1. 为什么有三种 frontmatter 格式,而不是仅使用 YAML?
现实世界中的内容流水线并非同质化的。Jekyll/Hugo 使用 YAML 和
TOML;基于 `serde_json` 构建的静态站点生成器首选 JSON;文档
工具链通常会同时遇到这三种情况。`metadata-gen` 接受这三种格式,因此
您的下游代码只需依赖一个 crate。
### 2. 这与 `gray_matter` 相比如何?
`gray_matter` 是 Rust 生态中主流的 frontmatter 解析器,自
2020 年起便占据主导地位。它目前已实现强类型提取(通过其 `Pod`),而这
正是 `metadata-gen` 将在 v0.0.6 中达成的目标。`metadata-gen` 的区别在于
捆绑的 HTML meta 标签生成器、记录在案的供应链安全姿态、
WASI/no_std 路线图,以及对第一方传递依赖项的严格锁定。如果您*现在*就需要强类型提取,请使用 `gray_matter`。如果
您想要 v0.0.10 的 WASI Component,请关注此 crate。
### 3. 使用此库需要异步运行时吗?
不需要。同步入口点(`extract_metadata`、`process_metadata`、
`extract_and_prepare_metadata`、`generate_metatags`、`escape_html`)不需要
Tokio。异步辅助函数 `async_extract_metadata_from_file` 是为
已经使用 Tokio 的调用者提供的便利;我们将 Tokio 精简为其 `fs` +
`io-util` 特性,因此它不会使您的构建变得臃肿。运行时无关的
`AsyncRead` 边界将在 v0.0.9 中落地。
### 4. 我可以在 `no_std` / WASM 中使用 `metadata-gen` 吗?
在 v0.0.5 中不可以 —— `regex`、`scraper` 和 `tokio` 都是
被无条件引入。`no_std + alloc` 支持是 v0.0.9 的里程碑,而完整的
`wasm32-wasip2` Component 将在 v0.0.10 中落地。请追踪里程碑
[v0.0.9](https://github.com/sebastienrousseau/metadata-gen/milestones)
和 [v0.0.10](https://github.com/sebastienrousseau/metadata-gen/milestones)
以获取状态。
### 5. MSRV 策略是什么?
MSRV 是公共 API 的一部分。版本提升会在 `0.x.0` 边界发生,并且
会在 `CHANGELOG.md` 中记录。当前基线 (1.88.0) 被 `time` 中的
安全公告传递性锁定;降低它将重新引入该漏洞。
### 6. 日期是如何解析的?
`process_metadata` 会按顺序尝试:
1. ISO-8601 / RFC 3339 (`2026-06-28`, `2026-06-28T15:30:00Z`)。
2. `YYYY-MM-DD` 显式格式。
3. `MM/DD/YYYY` 美国格式。
4. `DD/MM/YYYY` 欧洲格式(通过长度 + 斜杠模式识别)。
输出始终标准化为 `YYYY-MM-DD`。范围超出或模棱两可的
输入将返回 `MetadataError::DateParseError`。
### 7. 如何添加自定义必填字段?
在 v0.0.5 中,必填字段被硬编码为 `title` 和 `date`。可配置的
`MetadataProcessor` 构建器将在 v0.0.6 中落地
(issue [#47](https://github.com/sebastienrousseau/metadata-gen/issues/47))。
在此之前,请在 `extract_metadata` 之后使用
`metadata.contains_key("…")` 验证您自己的必填字段。
### 8. HTML 转义是如何处理的?
`escape_html` 将 `& < > " '` 映射为它们对应的实体字符。`unescape_html`
将它们映射回来(并将 `/` / `/` 映射为 `/`)。这对组合在
所有 ASCII 输入上都是往返安全的 —— 这一
属性的属性测试语料库和 Kani 证明将在 v0.0.8 中落地。目前的实现是一个五次遍历的
`str::replace` 链;单次遍历重写(可选通过
`v_htmlescape` 启用 SIMD)将在 v0.0.7 中交付
([#52](https://github.com/sebastienrousseau/metadata-gen/issues/52))。
### 9. 如何提取类型化的结构体(而不是 `HashMap`)?
在 v0.0.5 中不支持。v0.0.6 里程碑添加了
`metadata_gen::extract_typed::(content)`,它
会保留类型化信息(日期作为 `time::Date`,整数作为整数,
嵌套对象作为嵌套结构体)。请追踪
[issue #45](https://github.com/sebastienrousseau/metadata-gen/issues/45)。
### 10. 我应该在哪里报告漏洞?
请**不要**公开 GitHub issue。请根据
[SECURITY.md](.github/SECURITY.md) 策略给维护者发送电子邮件。我们将在 48 小时内确认,并
在最新的稳定线上发布修复程序。新的漏洞类别将
触发 `cargo-fuzz` 目标,从而防止相同特征的漏洞再次出现。
### 11. 添加此 crate 会让我的依赖树变庞大吗?
比以前少了。v0.0.5 淘汰了 `scraper` → `html5ever` → `selectors` →
`fxhash` / `phf_generator` 链,转而采用由 `quick-xml` 支持的
`` 提取器 —— 减少了约 30 个传递依赖 crate,并消除了
RUSTSEC-2025-0057 (`fxhash`) 和 RUSTSEC-2026-0097 (`rand 0.8` 通过
`phf_generator`)。剩余的运行时 crate:`tokio`(精简为
`fs`+`io-util`)、`regex`、`serde`、`serde_json`、`noyalib`、`toml`、
`yaml-rust2`、`thiserror`、`quick-xml`、`time`、`dtt`。按格式划分的 Cargo
特性门控将在 v0.0.6 中落地
([#41](https://github.com/sebastienrousseau/metadata-gen/issues/41)),因此
您可以选择不使用不需要的格式。
### 12. 有 CLI 吗?
没有。`metadata-gen` 是一个库 crate。我们在 v0.0.5 中从 `Cargo.toml`
移除了 `command-line-utilities` 类别,因为没有附带
`[[bin]]` 目标。如果您想要一个 CLI 包装器,请发起讨论 ——
对于 `metadata-gen-cli` 的伴随 crate 存在合理的理由。
## 安全性
- 根据 [`.github/SECURITY.md`](.github/SECURITY.md) 报告漏洞。
- 此 crate 强制执行 `#![forbid(unsafe_code)]`。
- 供应链控制(`cargo-deny`、`cargo-audit`、SBOM、`cargo-vet`
审计)已在[供应链](#supply-chain)部分记录。
## 许可证
根据您的选择,受 [Apache 2.0](LICENSE-APACHE) 或 [MIT](LICENSE-MIT) 双重许可。
回到顶部
标签:Frontmatter解析, Rust, SEO, WebAssembly, 元数据生成, 可视化界面, 瑞士军刀, 网络流量审计, 通知系统