feat: 公开精确物理行范围接口
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# 精确物理行范围
|
||||
|
||||
本文记录 `mdpolish.text_ranges` 当前稳定的公共查询口径。实现和运行校验以
|
||||
`src/mdpolish/text_ranges.py` 与测试为准;设计理由、方案取舍和施工边界见
|
||||
[`0013-public-physical-line-ranges.md`](../design/0013-public-physical-line-ranges.md)。
|
||||
|
||||
## 1. 公共入口
|
||||
|
||||
```python
|
||||
from mdpolish.text_ranges import (
|
||||
PhysicalLine,
|
||||
iter_physical_lines,
|
||||
line_ending_styles,
|
||||
physical_lines,
|
||||
)
|
||||
```
|
||||
|
||||
这些名称只从 `mdpolish.text_ranges` 导出,不在包根重复导出。`mdpolish._text_ranges` 不是兼容路径。
|
||||
|
||||
模块只接收内存中的 `str`。它不读取文件、不改写文本、不建立 Markdown AST,也不会在 `Pipeline` 运行前自动执行。
|
||||
|
||||
## 2. 什么是物理行
|
||||
|
||||
物理行由以下任一边界结束:
|
||||
|
||||
| 原文边界 | 行尾字符串 | code point 长度 |
|
||||
| --- | --- | ---: |
|
||||
| LF | `"\n"` | 1 |
|
||||
| CR | `"\r"` | 1 |
|
||||
| CRLF | `"\r\n"` | 2 |
|
||||
| 文档末尾 | `""` | 0 |
|
||||
|
||||
CRLF 是一个物理行尾。U+2028、U+2029、vertical tab、form feed、NUL 以及其他字符都属于行内容,不会被当成物理行尾。
|
||||
扫描不做 Unicode 或换行规范化。
|
||||
|
||||
空文档产生零条物理行。原文以行尾结束时,不会在末尾虚构额外空行;连续行尾之间确实存在的零长度行仍会返回。
|
||||
|
||||
## 3. `PhysicalLine`
|
||||
|
||||
`PhysicalLine` 是 `dataclass(frozen=True, slots=True)`:
|
||||
|
||||
```python
|
||||
PhysicalLine(
|
||||
content_start=0,
|
||||
content_end=4,
|
||||
full_end=6,
|
||||
)
|
||||
```
|
||||
|
||||
三个 offset 都是相对于同一个 Python `str` 的 0-based Unicode code point index:
|
||||
|
||||
- 内容范围:`[content_start, content_end)`;
|
||||
- 行尾范围:`[content_end, full_end)`;
|
||||
- 完整物理行范围:`[content_start, full_end)`。
|
||||
|
||||
它们不是 UTF-8 byte、UTF-16 code unit、终端显示列或人类使用的 1-based 行列。
|
||||
|
||||
公共成员:
|
||||
|
||||
| 成员 | 结果 |
|
||||
| --- | --- |
|
||||
| `content(source)` | 返回内容范围的精确切片 |
|
||||
| `line_ending(source)` | 返回 `""`、LF、CR 或 CRLF |
|
||||
| `is_empty` | 内容范围长度是否为零 |
|
||||
|
||||
`is_empty` 不等于 Markdown blank line。`" \n"` 和 `"\t\n"` 的内容不是零长度,因此均为 `False`。项目若要把 space、Tab
|
||||
或其他 Unicode whitespace 当作空白,必须在自己的 Modifier 中显式定义。
|
||||
|
||||
手工构造时,offset 必须是非负的严格 `int`,不接受 `bool`,并满足:
|
||||
|
||||
```text
|
||||
content_start <= content_end <= full_end
|
||||
full_end - content_end <= 2
|
||||
```
|
||||
|
||||
访问器还会拒绝非字符串来源、超出来源长度的范围,以及不是 `""`、LF、CR、CRLF 的行尾切片。错误信息不包含原文。
|
||||
|
||||
## 4. 扫描函数
|
||||
|
||||
`iter_physical_lines(source)` 返回惰性迭代器,但会在函数调用当下检查 `source` 类型。它单次从左到右扫描,不预先复制原文。
|
||||
|
||||
`physical_lines(source)` 返回同一扫描结果的不可变 tuple,适合需要查看前后行或重复遍历的 Modifier。
|
||||
|
||||
`line_ending_styles(source)` 返回原文实际出现过的非空行尾集合,只可能包含 `"\n"`、`"\r"`、`"\r\n"`。空文档或
|
||||
没有换行的文档返回空 `frozenset`。
|
||||
|
||||
## 5. 精确示例
|
||||
|
||||
下表的结果写作 `(content_start, content_end, full_end)`:
|
||||
|
||||
| source | 结果 |
|
||||
| --- | --- |
|
||||
| `""` | `()` |
|
||||
| `"a"` | `((0, 1, 1),)` |
|
||||
| `"a\n"` | `((0, 1, 2),)` |
|
||||
| `"\n"` | `((0, 0, 1),)` |
|
||||
| `"\r\n"` | `((0, 0, 2),)` |
|
||||
| `"a\n\n"` | `((0, 1, 2), (2, 2, 3))` |
|
||||
| `"\ntext"` | `((0, 0, 1), (1, 5, 5))` |
|
||||
| `"a\r\nb\rc\n"` | `((0, 1, 3), (3, 4, 5), (5, 6, 7))` |
|
||||
| `"a\u2028b"` | `((0, 3, 3),)` |
|
||||
|
||||
按顺序拼接所有 `source[line.content_start:line.full_end]` 必须逐 code point 还原原文。
|
||||
|
||||
## 6. 在项目 Modifier 中使用
|
||||
|
||||
物理行范围只是定位工具,不会自动成为修改:
|
||||
|
||||
```python
|
||||
from mdpolish import DocumentSnapshot, ProposedChange, TextEdit, TextSpan
|
||||
from mdpolish.text_ranges import physical_lines
|
||||
|
||||
|
||||
def propose(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
|
||||
lines = physical_lines(snapshot.markdown)
|
||||
if not lines or lines[0].content(snapshot.markdown) != "[REMOVE-ME]":
|
||||
return ()
|
||||
|
||||
line = lines[0]
|
||||
expected = snapshot.markdown[line.content_start : line.full_end]
|
||||
return (
|
||||
ProposedChange(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
reason="删除项目确认的独占行标记",
|
||||
edits=(
|
||||
TextEdit(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
span=TextSpan(line.content_start, line.full_end),
|
||||
expected_text=expected,
|
||||
replacement="",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
项目仍负责判断标题、列表、引用、代码、表格和业务语义。公共编辑执行器仍负责验证快照哈希、范围、预期原文与冲突。
|
||||
Reference in New Issue
Block a user