流式、行分帧和隐藏标记解析器
这个阶段是幕后工具箱,专管“文字还没收完就要先看懂”的情况。数据常一小块一小块来,utf8_stream先把字节安全拼成文字,line_buffer再按换行凑整行。stream_text定好边显示边抽取隐藏内容的规矩;inline_hidden_tag抓内联暗号,tagged_line_parser切出带标签的块。table_detect认 Markdown 表格,mention_codec保住提及背后的真实指向,citations整理记忆引用。lib.rs像柜台,把工具统一交给别处用。
解析器基础
这些文件定义共享解析器契约和 crate 级导出,供流式解析器工具构建使用。
utils/stream-parser/src/stream_text.rs源码 ↗
很多文字不是一次性来的,而是一小段一小段流进来,比如聊天回复、网络数据、模型输出。这里解决的问题是:有些内容可以马上显示,有些内容其实是隐藏标记或特殊载荷,比如引用、标签、控制信息,不能直接原样展示。StreamTextChunk 就像一次解析后的“收货单”:visible_text 是可以马上给人看的文字,extracted 是从这段文字里拆出来的隐藏内容。StreamTextParser 是一份约定,也就是“实现这个接口的解析器都必须会两件事”:push_str 接收新来的文字片段,finish 在全部结束时把缓存里剩下的东西清出来。这样不同解析器可以用同一种方式接入上层流程,上层不用关心具体怎么拆,只管拿到可显示文字和提取结果。
StreamTextChunk::default11–16 ↗
fn default() -> Self
作用:创建一个空的解析结果,表示这次既没有可显示文字,也没有提取出隐藏内容。有人在开始解析、清空状态、或者没有产出时会用它当“空篮子”。
数据流:进去不需要任何输入 → 它新建一个空字符串作为 visible_text,又新建一个空列表作为 extracted → 出来一个 StreamTextChunk,里面什么内容都没有,也不会改动外部状态。
调用关系:它是很多解析流程的基础零件。像 collect_chunks、push_str、finish、map_segments、push_bytes 这些步骤在需要先准备一个空结果、或在没有东西可交付时,会调用它。它内部只是把活儿交给字符串和列表的 new 方法,生成两个空容器。
调用图:被 9 处调用(collect_chunks, finish, push_str, collect_chunks, map_segments, collect_chunks, finish, push_bytes, collect_bytes);外部调用 2 个(new, new)。
StreamTextChunk::is_empty21–23 ↗
fn is_empty(&self) -> bool
作用:检查这次解析结果是不是真的没有产出。也就是既没有能显示的文字,也没有拆出来的隐藏内容。
数据流:进去的是当前这个 StreamTextChunk 本身 → 它分别查看 visible_text 是否为空、extracted 是否为空 → 如果两边都空,就返回 true;只要有一边有内容,就返回 false。它不修改任何数据。
调用关系:它通常会被上层流程用来判断“这次结果要不要继续处理或发送”。它不调用别的复杂逻辑,只是做一个简单检查,像看篮子里有没有东西一样。
utils/stream-parser/src/lib.rs源码 ↗
这个文件本身不做具体解析工作,更像一个服务台或目录牌。这个库里有很多小解析器:有的处理连续传来的文字流,有的保证字节按 UTF-8(常见的文字编码)正确拼成文字,有的识别引用标记,有的抽取隐藏标签,有的提取或删除“拟议计划”块。lib.rs 先声明这些内部模块存在,然后用 pub use 把常用类型和函数重新导出。这样外部使用者可以直接写“从 stream-parser 里拿 AssistantTextStreamParser”,不用关心它实际藏在哪个子文件里。它的重要性在于稳定入口:内部代码以后可以调整文件结构,但只要这里公开的名字不变,外部调用者就不容易被影响。
流式输入适配器
这些工具通过处理 UTF-8 分块边界和增量行分帧,将原始流式输入适配为可解析单元。
utils/stream-parser/src/utf8_stream.rs源码 ↗
流式数据常常不是一次性到齐,而是一小块一小块来。问题是 UTF-8(一种把文字存成字节的通用编码)里,一个字符可能占多个字节,比如“é”可能被拆成前半截和后半截。这个文件里的 Utf8StreamParser 就像一个门卫:它先收字节,检查哪些字节已经能组成完整文字;完整的部分才交给里面包着的 StreamTextParser(真正读文本内容的解析器)。如果只收到半个字符,它会先存起来,等下一块字节补齐。如果发现字节根本不是合法 UTF-8,它会报 Utf8StreamParserError,并且会把刚刚这块输入整体回滚,避免里面的解析器吃到半截脏数据。finish 用来在流结束时做最后检查:如果还剩半个字符,就明确报错。文件后半部分是测试,专门验证拆开的字符、坏字节、结束时残缺字符,以及取回内部解析器这些边界情况。
Utf8StreamParserError::fmt22–35 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:把 UTF-8 解析错误变成给人看的文字说明。这样日志、报错信息或调试输出里,不只是一个冷冰冰的错误类型,而能看出是哪里坏了。
数据流:进去的是一个错误值和一个格式化输出位置 → 它根据错误种类写出不同句子:非法字节会带上出错偏移和长度,流结束时半个字符会说明字符没收完整 → 出来的是格式化是否成功的结果,错误本身不被改动。
调用关系:当外部代码把 Utf8StreamParserError 当作可显示错误来打印时,会走到这里;它把底层错误信息交给 Rust 的 write! 宏写成字符串。
调用图:外部调用 1 个(write!)。
Utf8StreamParser::new54–59 ↗
fn new(inner: P) -> Self
作用:创建一个新的字节流解析外壳,把已有的文本解析器包起来。之后外部就可以喂原始字节,而不是必须先自己处理 UTF-8。
数据流:进去的是一个已经准备好的内部文本解析器 → 它把这个解析器保存起来,并准备一个空的 pending_utf8 缓冲区,用来存暂时还拼不成完整字符的字节 → 出来的是一个可接收字节块的 Utf8StreamParser。
调用关系:这是使用这个工具的第一步。测试里多个场景都会先调用它创建解析器,然后再喂字节、结束流,或取回内部解析器。
调用图:被 6 处调用(utf8_stream_parser_errors_on_incomplete_code_point_at_eof, utf8_stream_parser_handles_split_code_points_across_chunks, utf8_stream_parser_into_inner_errors_when_partial_code_point_is_buffered, utf8_stream_parser_into_inner_lossy_drops_buffered_partial_code_point, utf8_stream_parser_rolls_back_entire_chunk_when_invalid_byte_follows_valid_prefix, utf8_stream_parser_rolls_back_on_invalid_utf8_chunk);外部调用 1 个(new)。
Utf8StreamParser::push_bytes66–109 ↗
fn push_bytes(
&mut self,
chunk: &[u8],
) -> Result<StreamTextChunk<P::Extracted>, Utf8StreamParserError>
作用:接收一块新来的原始字节,尽量把能组成完整 UTF-8 文本的部分交给内部解析器。它解决的是“一个字符被拆在两块数据里”的问题。
数据流:进去的是一段字节,以及之前可能暂存的半个字符 → 它先把新字节接到缓冲区后面,再用 UTF-8 检查:如果全都合法,就转成文本交给内部 parser 的 push_str;如果结尾只是少了几个字节,就只把前面完整的文本交进去,把尾巴留下;如果中间出现非法字节,就撤回这次新加的整块字节并返回错误 → 出来的是内部解析器吐出的可见文本和提取内容,或者一个 UTF-8 错误。
调用关系:这是正常流式处理时最常用的入口。tests::collect_bytes 会反复调用它收集多块输入;它自己把真正的文本工作交给内部解析器的 push_str,把字节是否合法的判断交给标准库的 from_utf8。
调用图:调用 1 个内部函数(default);被 1 处调用(collect_bytes);外部调用 2 个(push_str, from_utf8)。
Utf8StreamParser::finish111–149 ↗
fn finish(&mut self) -> Result<StreamTextChunk<P::Extracted>, Utf8StreamParserError>
作用:告诉解析器“字节流结束了”,让它把最后剩下的内容吐出来,并检查有没有没补齐的半个 UTF-8 字符。
数据流:进去的是解析器当前状态,里面可能有暂存字节和内部文本解析器的未完成状态 → 它先检查暂存字节:合法就转成文本送进内部解析器,不完整就报 IncompleteUtf8AtEof;然后调用内部解析器的 finish 拿到最后一批结果;最后把两部分结果合在一起 → 出来的是最终的文本块和提取结果,或者明确的 UTF-8 错误。
调用关系:它通常在所有 push_bytes 都完成后调用。tests::collect_bytes 用它收尾;它也负责把收尾工作继续交给内部解析器的 finish。
调用图:调用 1 个内部函数(default);被 1 处调用(collect_bytes);外部调用 3 个(finish, push_str, from_utf8)。
Utf8StreamParser::into_inner154–170 ↗
fn into_inner(self) -> Result<P, Utf8StreamParserError>
作用:把包在外面的 Utf8StreamParser 拆掉,取回里面原来的文本解析器,但前提是没有危险的残留半个字符。
数据流:进去的是整个 Utf8StreamParser 本身 → 它检查 pending_utf8 是否为空;如果为空,就直接交还内部解析器;如果不为空,就验证这些残留字节是不是完整合法的 UTF-8,非法或不完整就返回错误 → 出来的是内部解析器,或者一个说明残留字节有问题的错误。
调用关系:这个函数适合调用方不想再用字节外壳、想拿回内部解析器时使用。它不会像 finish 那样把残留文本喂给内部解析器,所以注释提醒:想刷新内容要先调用 finish。
调用图:外部调用 1 个(from_utf8)。
Utf8StreamParser::into_inner_lossy175–177 ↗
fn into_inner_lossy(self) -> P
作用:不做检查,直接取回里面的文本解析器。名字里的 lossy 表示“可能丢东西”:如果外壳里还存着半个字符,会被直接丢掉。
数据流:进去的是整个 Utf8StreamParser → 它不看 pending_utf8,也不验证 UTF-8,更不把残留字节送进内部解析器 → 出来的是内部文本解析器,外壳里没处理完的字节被放弃。
调用关系:这是一个有风险但简单的逃生口。测试 tests::utf8_stream_parser_into_inner_lossy_drops_buffered_partial_code_point 专门确认它确实会丢掉残留的半个字符。
tests::collect_bytes190–204 ↗
fn collect_bytes(
parser: &mut Utf8StreamParser<CitationStreamParser>,
chunks: &[&[u8]],
) -> Result<StreamTextChunk<String>, Utf8StreamParserError>
作用:这是测试用的小帮手,把多块字节依次喂给 Utf8StreamParser,并把每次输出合并成一个总结果。
数据流:进去的是一个可变的 UTF-8 流解析器和一组字节块 → 它先建一个空结果,然后循环调用 push_bytes,把每次得到的可见文本和提取内容接到总结果里;最后调用 finish 收尾并继续合并 → 出来的是完整的 StreamTextChunk,或者中途遇到的 UTF-8 错误。
调用关系:它服务于测试用例,特别是拆分字符跨 chunk 的场景。它把测试里重复的“喂多块数据、收集输出、最后收尾”流程集中在一个地方。
调用图:调用 3 个内部函数(default, finish, push_bytes)。
tests::utf8_stream_parser_handles_split_code_points_across_chunks207–222 ↗
fn utf8_stream_parser_handles_split_code_points_across_chunks()
作用:验证一个 UTF-8 字符被拆到不同字节块里时,解析器仍然能拼好它,并且里面的引用文本解析器也能正常工作。
数据流:进去的是测试写死的三段字节,其中包含被拆开的“é”和中文“中”,还包含引用标签 → 它创建 CitationStreamParser,再用 Utf8StreamParser 包起来,通过 collect_bytes 喂入所有字节 → 出来的是断言结果:可见文本应是“AéZ”,被提取的内容应是“中”。
调用关系:这是最核心的成功路径测试。测试框架运行它;它先调用两个 new 创建内外两层解析器,再借助 tests::collect_bytes 覆盖 push_bytes 和 finish 的配合。
调用图:调用 2 个内部函数(new, new);外部调用 3 个(assert_eq!, panic!, collect_bytes)。
tests::utf8_stream_parser_rolls_back_on_invalid_utf8_chunk225–258 ↗
fn utf8_stream_parser_rolls_back_on_invalid_utf8_chunk()
作用:验证当第二块字节不是合法的 UTF-8 后续字节时,解析器会报错,而且不会把之前暂存的半个字符弄坏。
数据流:进去的是先给一个可能组成“é”的开头字节 0xC3,再给一个错误的后续字节 0x28,之后再给正确后续字节 0xA9 和字母 x → 它检查第一次只缓存不输出,第二次返回 InvalidUtf8,第三次还能恢复并输出“éx”,最后 finish 没有多余内容 → 出来的是一组断言通过或失败。
调用关系:这个测试说明 push_bytes 的“回滚整块输入”很重要:坏 chunk 不应该污染解析器状态。它通过新建解析器并连续喂不同字节来模拟网络数据出错后恢复的情况。
调用图:调用 2 个内部函数(new, new);外部调用 3 个(assert!, assert_eq!, panic!)。
tests::utf8_stream_parser_rolls_back_entire_chunk_when_invalid_byte_follows_valid_prefix261–283 ↗
fn utf8_stream_parser_rolls_back_entire_chunk_when_invalid_byte_follows_valid_prefix()
作用:验证如果一整块输入里前面是好文本、后面突然有坏字节,解析器会把这整块都撤回,而不是先吃掉前面的好文本。
数据流:进去的是字节串“ok”加上非法字节 0xFF → 它期望 push_bytes 返回 InvalidUtf8,错误位置在“ok”之后;然后再喂入“!” → 出来的是断言:下一次只输出“!”,说明之前那块“ok\xFF”没有被内部解析器部分接收。
调用关系:这个测试专门保护 push_bytes 的一个重要承诺:遇到非法 UTF-8 时,内部文本解析器不会看到这个 chunk 的任何前缀内容。这样调用方之后才有机会自己决定怎么恢复。
调用图:调用 2 个内部函数(new, new);外部调用 3 个(assert!, assert_eq!, panic!)。
tests::utf8_stream_parser_errors_on_incomplete_code_point_at_eof286–300 ↗
fn utf8_stream_parser_errors_on_incomplete_code_point_at_eof()
作用:验证流结束时如果还剩半个 UTF-8 字符,解析器会明确报错,而不是偷偷忽略或猜一个字符。
数据流:进去的是两个字节 0xE2、0x82,它们像是某个三字节字符的前两段,但缺最后一段 → push_bytes 会先缓存并不输出;finish 发现流已经结束却还不完整 → 出来的是 IncompleteUtf8AtEof 错误断言。
调用关系:这个测试覆盖 finish 的收尾检查。它说明 finish 不只是拿最后结果,还负责判断“是不是还有没收完整的字符”。
调用图:调用 2 个内部函数(new, new);外部调用 3 个(assert!, assert_eq!, panic!)。
tests::utf8_stream_parser_into_inner_errors_when_partial_code_point_is_buffered303–317 ↗
fn utf8_stream_parser_into_inner_errors_when_partial_code_point_is_buffered()
作用:验证在还缓存着半个 UTF-8 字符时,安全取回内部解析器会失败,避免调用方误以为所有输入都处理完了。
数据流:进去的是只包含 0xC3 的半个字符开头 → push_bytes 缓存它并返回空输出;随后 into_inner 检查到残留字节不完整 → 出来的是 IncompleteUtf8AtEof 错误断言。
调用关系:这个测试针对 Utf8StreamParser::into_inner 的安全行为。它和 lossy 版本形成对照:安全版本不会默默丢掉未完成字节。
调用图:调用 2 个内部函数(new, new);外部调用 3 个(assert!, assert_eq!, panic!)。
tests::utf8_stream_parser_into_inner_lossy_drops_buffered_partial_code_point320–332 ↗
fn utf8_stream_parser_into_inner_lossy_drops_buffered_partial_code_point()
作用:验证不安全但方便的 into_inner_lossy 会直接丢掉缓存里的半个字符,并把内部解析器交还出来。
数据流:进去的是只喂了 0xC3 的解析器状态 → push_bytes 先把这个半个字符缓存起来;into_inner_lossy 不检查也不刷新,直接返回内部 CitationStreamParser;随后内部 finish 没有任何输出 → 出来的是断言:尾部结果为空。
调用关系:这个测试说明 into_inner_lossy 的名字不是摆设:它确实可能造成数据丢失。它帮助读者理解什么时候该用安全的 into_inner,什么时候才可能接受 lossy。
ollama/src/line_buffer.rs源码 ↗
这个文件定义了 LineBuffer,可以把它想成一个临时记事本:外面不断塞进新收到的字节,它先存着;等看到换行符“\n”时,就把前面这一整行拿出来。这里处理的是字节,不直接处理文字,因为进程输出、网络数据这类东西在底层都是一串字节。它还有一个 scanned_len,意思是“前面这些内容我已经检查过了,里面没有换行”,这样下次不用从头再找,省时间。关键行为是:extend_from_slice 只负责追加新数据;take_line 负责寻找下一行,如果没找到换行,就记住已检查到哪里并返回空;如果找到了,就把这一行从缓冲区里切走并返回。这样上层代码就能放心地一行一行处理输出。
LineBuffer::extend_from_slice13–15 ↗
fn extend_from_slice(&mut self, bytes: &[u8])
作用:把新收到的一小段字节追加到缓冲区后面。有人会在读取进程输出时用它,因为一次读取可能只读到半行,需要先攒起来。
数据流:进去的是一段字节切片,也就是新来的原始数据;它把这段数据原样接到 LineBuffer 里面已有的 bytes 后面;出来没有返回值,但缓冲区变长了,等待之后被 take_line 按行取出。
调用关系:当 push_process_output 或 push_stderr 收到外部进程的新输出时,会调用它先把数据放进缓冲区。它只做“收货入库”,不判断是否已经凑成一行;后续是否能取出完整行,要交给 LineBuffer::take_line。
调用图:被 2 处调用(push_process_output, push_stderr);外部调用 1 个(extend_from_slice)。
LineBuffer::take_line17–27 ↗
fn take_line(&mut self) -> Option<BytesMut>
作用:尝试从缓冲区里取出一整行,判断标准是有没有换行符“\n”。如果还没凑够一行,它会返回空,让调用者下次有更多数据时再试。
数据流:进去的是当前 LineBuffer 里已经攒下的字节,以及 scanned_len 记录的“已检查区域”;它从还没检查过的位置开始找换行符。找不到时,把 scanned_len 更新到当前末尾并返回 None;找到时,把从开头到换行符这一段从缓冲区切下来返回,并把 scanned_len 重置为 0,因为剩下的数据要重新从头检查。
调用关系:push_stderr 和 take_stdout_message 会在需要按行处理输出时调用它。它内部借助 memchr 快速找换行符,并用 split_to 把完整的一行从缓冲区中分离出来;这样上层代码拿到的总是完整行,而不是零散碎片。
调用图:被 2 处调用(push_stderr, take_stdout_message);外部调用 3 个(len, split_to, memchr)。
通用标记解析器
这些可复用解析器可从流式文本中增量识别隐藏的内联标签和按行分隔的标签块。
utils/stream-parser/src/tagged_line_parser.rs源码 ↗
这个解析器像一个边读边分拣的传送带。输入文字不是一次性给全的,而是可能一点点到达,比如先来“<t”,下一次才来“ag>”。为了不把半截标签当普通文字,它会先把当前这一行暂存起来,等到能确认“这是不是一整行标签”再输出结果。文件里用 TagSpec 说明一种标签的开始文字、结束文字,以及它代表哪类标签;TaggedLineSegment 是解析后的片段,可能是普通文字、标签开始、标签里的内容、标签结束。TaggedLineParser 保存当前状态:现在是否在标签块里、是否正在尝试识别标签、当前行暂存了什么。它特别注意一个规则:开始和结束标签必须单独占一行,多出来字就算普通文本。最后的 push_segment 会把相邻的普通文字或同一标签里的连续内容合并,避免输出太碎。
TaggedLineParser::new37–44 ↗
fn new(specs: Vec<TagSpec<T>>) -> Self
作用:创建一个新的按行标签解析器。调用者把“哪些字符串算开始标签、哪些算结束标签”交给它,它之后就按这些规则分拣流式文字。
数据流:进去的是一组 TagSpec,也就是标签规则列表;函数把这些规则存起来,并把当前标签、行缓存、检测开关都设成初始状态;出来的是一个空白但已知道规则的 TaggedLineParser。
调用关系:这是整个解析流程的起点。测试里的 tests::parser 会用它做一个带有 <tag> 和 </tag> 规则的解析器,真实使用时外层代码也会先调用它,再把文字交给 TaggedLineParser::parse。
TaggedLineParser::parse46–82 ↗
fn parse(&mut self, delta: &str) -> Vec<TaggedLineSegment<T>>
作用:接收新到的一小段文字,并尽量把它切成普通文字、标签开始、标签内容或标签结束。它适合处理“文字像水流一样一段段进来”的场景。
数据流:进去的是一段新文本 delta,以及解析器之前保存的状态;它逐个字符查看,遇到可能是标签的行就先放进 line_buffer,直到换行或确认不可能是标签;能确认一行结束时交给 TaggedLineParser::finish_line 判断;普通文字则通过 TaggedLineParser::push_text 输出;出来的是本次能确定的一批 TaggedLineSegment,同时解析器内部状态会继续保留给下一次输入用。
调用关系:外层追加流式文本时会调用它。它是日常工作主入口:自己负责走字符,遇到完整行就交给 TaggedLineParser::finish_line,遇到文本就交给 TaggedLineParser::push_text,还会用 TaggedLineParser::is_tag_prefix 判断当前半行有没有可能继续变成标签。
调用图:调用 3 个内部函数(finish_line, is_tag_prefix, push_text);被 1 处调用(push_str);外部调用 3 个(new, new, take)。
TaggedLineParser::finish84–110 ↗
fn finish(&mut self) -> Vec<TaggedLineSegment<T>>
作用:告诉解析器“输入已经结束了”,让它把还没吐出来的半行和未关闭的标签收尾。没有它,最后一段没有换行的文字或打开但没关上的标签可能会卡在内部缓存里。
数据流:进去的是解析器当前保存的 line_buffer、active_tag 等状态;它先检查剩下的半行是不是完整的开始标签或结束标签,不是就当文本输出;如果还有正在打开的标签,它会补一个 TagEnd;出来的是最后一批片段,并把解析器恢复到可重新检测标签的状态。
调用关系:它在整段流结束时使用,和 TaggedLineParser::parse 配套。它会调用 TaggedLineParser::match_open、TaggedLineParser::match_close 来识别最后一行,也会通过 TaggedLineParser::push_text 和 push_segment 生成最终片段。
调用图:调用 4 个内部函数(match_close, match_open, push_text, push_segment);被 1 处调用(finish);外部调用 4 个(new, take, TagEnd, TagStart)。
TaggedLineParser::finish_line112–137 ↗
fn finish_line(&mut self, segments: &mut Vec<TaggedLineSegment<T>>)
作用:处理一整行已经读完的内容,判断这一行到底是开始标签、结束标签,还是普通文字。它是“标签必须单独占一行”这个规则真正落地的地方。
数据流:进去的是当前暂存在 line_buffer 里的完整一行,以及要追加结果的 segments;它去掉行尾换行和两侧空白后尝试匹配开始标签或结束标签;匹配成功就输出 TagStart 或 TagEnd 并更新 active_tag,匹配失败就把整行原样当文本输出;出来是更新后的片段列表和解析器状态。
调用关系:TaggedLineParser::parse 在读到换行时会调用它。它自己不负责读字符,只负责给整行下结论;判断标签时交给 TaggedLineParser::match_open 和 TaggedLineParser::match_close,输出文本时交给 TaggedLineParser::push_text,输出标签边界时交给 push_segment。
调用图:调用 4 个内部函数(match_close, match_open, push_text, push_segment);被 1 处调用(parse);外部调用 3 个(take, TagEnd, TagStart)。
TaggedLineParser::push_text139–145 ↗
fn push_text(&self, text: String, segments: &mut Vec<TaggedLineSegment<T>>)
作用:把一段确定是文字的内容放进结果里,但会根据当前是否处在标签块内,决定它是普通文字还是标签内部文字。
数据流:进去的是一段 text、当前 active_tag 状态和结果列表 segments;如果现在在某个标签里,它会把 text 包成 TagDelta;如果不在标签里,就包成 Normal;出来是追加或合并后的结果列表。
调用关系:TaggedLineParser::parse、TaggedLineParser::finish_line 和 TaggedLineParser::finish 都会在需要输出文字时调用它。它把“这段文字属于哪里”的判断做好后,再把真正追加和合并片段的工作交给 push_segment。
调用图:调用 1 个内部函数(push_segment);被 3 处调用(finish, finish_line, parse);外部调用 2 个(Normal, TagDelta)。
TaggedLineParser::is_tag_prefix147–152 ↗
fn is_tag_prefix(&self, slug: &str) -> bool
作用:判断当前行里已经看到的文字,是否还有可能是某个标签的开头。这样解析器就能在只看到半截标签时先等等,而不是过早当普通文字输出。
数据流:进去的是当前行去掉左侧空白后的 slug,以及解析器保存的所有标签规则;它把 slug 和每个开始标签、结束标签的前缀比较;出来是一个真假值,表示“还有没有可能继续组成标签”。
调用关系:TaggedLineParser::parse 在逐字符读取时会用它。只要它说“还有可能”,parse 就继续缓存;一旦它说“不可能”,parse 就把缓存内容转成普通文本交给 TaggedLineParser::push_text。
调用图:被 1 处调用(parse)。
TaggedLineParser::match_open154–159 ↗
fn match_open(&self, slug: &str) -> Option<T>
作用:检查一整行内容是否正好等于某个开始标签。它只认完整匹配,不认带额外文字的行。
数据流:进去的是已经修剪过空白的 slug,以及标签规则列表;它逐个查看 spec.open 是否和 slug 完全相同;找到就返回这个标签对应的标识,找不到就返回空。
调用关系:TaggedLineParser::finish_line 在一行结束时会用它判断是否进入标签块,TaggedLineParser::finish 在收尾时也会用它判断最后半行。它只负责识别,不负责修改解析状态。
调用图:被 2 处调用(finish, finish_line)。
TaggedLineParser::match_close161–166 ↗
fn match_close(&self, slug: &str) -> Option<T>
作用:检查一整行内容是否正好等于某个结束标签。它帮助解析器知道什么时候离开当前标签块。
数据流:进去的是已经修剪过空白的 slug,以及标签规则列表;它逐个查看 spec.close 是否和 slug 完全相同;找到就返回这个标签对应的标识,找不到就返回空。
调用关系:TaggedLineParser::finish_line 和 TaggedLineParser::finish 会调用它。调用方还会额外检查这个结束标签是不是和当前打开的标签一致,避免乱关标签。
调用图:被 2 处调用(finish, finish_line)。
push_segment169–199 ↗
fn push_segment(segments: &mut Vec<TaggedLineSegment<T>>, segment: TaggedLineSegment<T>)
作用:把一个解析结果片段放进列表里,并顺手把相邻的同类文字合并起来。这样输出不会被切成很多没必要的小碎片。
数据流:进去的是结果列表 segments 和一个新片段 segment;如果新片段是空文字,它直接丢掉;如果它和列表最后一个片段都是普通文字,或者都是同一个标签里的内容,就把字符串接到已有片段后面;否则就新增一个片段;出来是更整洁的结果列表。
调用关系:它是这个文件里的统一出口整理员。TaggedLineParser::push_text、TaggedLineParser::finish_line 和 TaggedLineParser::finish 都会通过它追加 Normal、TagDelta、TagStart、TagEnd,保证输出格式稳定。
调用图:被 3 处调用(finish, finish_line, push_text);外部调用 4 个(Normal, TagDelta, TagEnd, TagStart)。
tests::parser213–219 ↗
fn parser() -> TaggedLineParser<Tag>
作用:给测试快速创建一个固定规则的解析器。它规定 <tag> 是开始,</tag> 是结束,标签类型叫 Block。
数据流:进去没有外部输入;它构造一个 TagSpec 列表并传给 TaggedLineParser::new;出来是测试可直接使用的 TaggedLineParser<Tag>。
调用关系:这是测试辅助函数。后面的 tests::buffers_prefix_until_tag_is_decided 和 tests::rejects_tag_lines_with_extra_text 都靠它拿到同样配置的解析器,避免每个测试重复写规则。
tests::buffers_prefix_until_tag_is_decided222–236 ↗
fn buffers_prefix_until_tag_is_decided()
作用:验证解析器遇到被拆开的标签时会耐心等待,不会把半截 <t 误当普通文字。这个测试保护的是流式输入中很关键的行为。
数据流:进去的是测试里手写的两段输入:先给 <t,再给 ag>\nline\n</tag>\n;它调用解析器多次 parse,最后 finish 收尾;出来的实际片段会和期望片段比较,期望是一个标签开始、一段标签内容、一个标签结束。
调用关系:它通过 tests::parser 创建解析器,然后模拟分批输入。这个测试覆盖 TaggedLineParser::parse 的前缀缓存能力,也间接覆盖 finish_line、push_text 和 push_segment 的配合。
调用图:外部调用 2 个(assert_eq!, parser)。
tests::rejects_tag_lines_with_extra_text239–248 ↗
fn rejects_tag_lines_with_extra_text()
作用:验证带额外文字的标签行不会被当成真正标签。比如 <tag> extra 不是一行纯标签,所以应该原样作为普通文字。
数据流:进去的是测试输入 <tag> extra\n;它调用 parse,再调用 finish;出来的实际结果应只有一个 Normal 片段,内容就是原来的整行文字。
调用关系:它通过 tests::parser 创建解析器,专门检查 TaggedLineParser::finish_line 对“标签必须单独占一行”的判断。这个测试能防止解析器过于宽松,把普通文本误切成标签块。
调用图:外部调用 2 个(assert_eq!, parser)。
Markdown 结构辅助工具
这些辅助工具检测 markdown 特定结构,并编码或解码高级文本处理使用的提及标记。
tui/src/table_detect.rs源码 ↗
这个文件像一个“交通警察”,帮其他模块判断一行 Markdown 到底该按表格看,还是该当普通文字或代码跳过。它先把可能的表格行按没有被反斜杠转义的 | 切成格子,再判断表头是不是有内容、分隔线是不是由 ---、:---: 这类 Markdown 表格标记组成。另一半是 FenceTracker,用来跟踪围栏代码块,也就是用三个以上反引号或波浪线包起来的代码。它会区分普通代码块和 Markdown 代码块:普通代码块里的 | 不该被当成表格,Markdown 代码块里则仍可能需要继续识别。这个文件的关键价值是把这些判断集中在一个地方,流式显示和 Markdown 拆包都用同一套规则,避免一边认表格、一边不认表格的怪问题。
parse_table_segments38–54 ↗
fn parse_table_segments(line: &str) -> Option<Vec<&str>>
作用:把一行可能的 Markdown 表格拆成一个个单元格。它只关心“结构像不像表格”,不负责最终怎么显示文字。
数据流:进去的是一整行文字;它先去掉首尾空白和最外侧可选的竖线,再交给 split_unescaped_pipe 按真正的分隔竖线切开,最后把每段两边空白去掉;出来的是单元格列表,或者在空行、完全不像表格时返回“没有”。
调用关系:它是表格识别的基础零件。is_table_header_line 和 is_table_delimiter_line 会用它先拆格子,table_candidate_text 也会用它判断候选表格文本。
调用图:调用 1 个内部函数(split_unescaped_pipe);被 3 处调用(table_candidate_text, is_table_delimiter_line, is_table_header_line)。
split_unescaped_pipe61–80 ↗
fn split_unescaped_pipe(content: &str) -> Vec<&str>
作用:按没有被反斜杠保护的 | 切开文本。这样 A \| B 这种内容不会被误拆成两列。
数据流:进去的是已经去掉外侧竖线的内容;它从左到右扫描,遇到反斜杠就跳过后面一个字符,遇到真正的竖线才切段;出来的是原文本中的若干切片,不会额外改写内容。
调用关系:它只给 parse_table_segments 干活,是更底层的小工具。parse_table_segments 负责决定这一行是否值得切,它负责具体怎么切。
调用图:被 1 处调用(parse_table_segments);外部调用 1 个(with_capacity)。
is_table_header_line88–90 ↗
fn is_table_header_line(line: &str) -> bool
作用:判断一行能不能当 Markdown 表格的表头。标准很简单:能拆成表格段,并且至少有一个格子不是空的。
数据流:进去的是一行文字;它用 parse_table_segments 拆成格子,再检查有没有非空格子;出来的是 true 或 false,不改动任何状态。
调用关系:流式扫描表格时,table_holdback_state 会用它判断当前行是不是可能的表头。它只做单行判断,是否真的成表还要看下一行是不是分隔线。
调用图:调用 1 个内部函数(parse_table_segments);被 1 处调用(table_holdback_state)。
is_table_delimiter_segment95–103 ↗
fn is_table_delimiter_segment(segment: &str) -> bool
作用:判断表格分隔线里的一个格子是否合法,比如 ---、:---、---:、:---:。
数据流:进去的是一个分隔线片段;它去掉空白,再允许前后有冒号,中间必须至少三个横线且全是横线;出来的是 true 或 false。
调用关系:它是 is_table_delimiter_line 的内部判断标准。整行分隔线要每个片段都通过它才算合法。
is_table_delimiter_line108–111 ↗
fn is_table_delimiter_line(line: &str) -> bool
作用:判断一整行是不是 Markdown 表格的分隔线,也就是表头下面那行 | --- | --- |。
数据流:进去的是一行文字;它先用 parse_table_segments 拆成多个片段,再要求每个片段都符合 is_table_delimiter_segment 的规则;出来的是 true 或 false。
调用关系:它会被 table_holdback_state 调用,通常和 is_table_header_line 配合:上一行像表头,这一行像分隔线,才确认是表格。
调用图:调用 1 个内部函数(parse_table_segments);被 1 处调用(table_holdback_state)。
FenceTracker::new149–151 ↗
fn new() -> Self
作用:创建一个新的代码块跟踪器,初始状态是“不在任何围栏代码块里”。
数据流:进去不需要参数;它生成一个内部状态为空的 FenceTracker;出来的是可继续喂入 Markdown 行的跟踪器。
调用关系:外部解析流程在开始逐行处理前会创建它。测试也会创建它来验证各种开关代码块的场景。
调用图:被 12 处调用(new, parse_lines_with_fence_state, fence_tracker_blockquote_prefix_stripped, fence_tracker_close_with_trailing_content_does_not_close, fence_tracker_indented_4_spaces_ignored, fence_tracker_markdown_case_insensitive, fence_tracker_markdown_fence, fence_tracker_mismatched_char_does_not_close, fence_tracker_nested_shorter_marker_does_not_close, fence_tracker_opens_and_closes_backtick_fence (+2 more))。
FenceTracker::advance157–188 ↗
fn advance(&mut self, raw_line: &str)
作用:把一行原始 Markdown 喂给跟踪器,让它更新“现在是否在代码块里”的状态。
数据流:进去的是一行原文;它先忽略缩进超过 3 个空格的行,再去掉引用块的 > 前缀,用 parse_fence_marker 找围栏标记,如果是开头就记录代码块类型,如果是匹配的结尾就退出代码块;结果是更新内部状态,不直接返回值。
调用关系:push_line 在逐行处理时会调用它。它把识别围栏标记交给 parse_fence_marker,把 Markdown 类型判断交给 is_markdown_fence_info,把引用块前缀清理交给 strip_blockquote_prefix。
调用图:调用 3 个内部函数(is_markdown_fence_info, parse_fence_marker, strip_blockquote_prefix);被 1 处调用(push_line)。
FenceTracker::kind192–194 ↗
fn kind(&self) -> FenceKind
作用:告诉调用者当前处在什么环境:不在代码块里、在 Markdown 代码块里,还是在其他语言代码块里。
数据流:进去不需要额外数据;它读取跟踪器内部状态;出来的是一个 FenceKind 分类,不改动状态。
调用关系:push_line 会在处理每行时查询它,从而决定这一行里的 | 是否还能当表格符号看。
调用图:被 1 处调用(push_line)。
parse_fence_marker203–213 ↗
fn parse_fence_marker(line: &str) -> Option<(char, usize)>
作用:识别一行开头有没有三个以上反引号或波浪线,也就是 Markdown 围栏代码块的标记。
数据流:进去的是已经去掉前导空白和引用符号的行;它检查第一个字符是不是 ` 或 ~,并数连续出现了几个;如果至少 3 个,就返回标记字符和长度,否则返回“没有”。
调用关系:FenceTracker::advance 用它来判断当前行是否可能打开或关闭代码块。它只认标记本身,不决定代码块语言。
调用图:被 1 处调用(advance)。
is_markdown_fence_info219–225 ↗
fn is_markdown_fence_info(trimmed_line: &str, marker_len: usize) -> bool
作用:判断代码块开头标记后面的语言说明是不是 md 或 markdown。
数据流:进去的是整行围栏开头和标记长度;它取标记后第一个单词,忽略大小写比较 md 和 markdown;出来的是 true 或 false。
调用关系:FenceTracker::advance 在发现新代码块打开时调用它,用来决定这个围栏算 Markdown 内容还是其他代码内容。
调用图:被 1 处调用(advance)。
strip_blockquote_prefix232–240 ↗
fn strip_blockquote_prefix(line: &str) -> &str
作用:去掉行首的 Markdown 引用标记 >,包括多层引用。这样引用里的表格或代码围栏也能被正常识别。
数据流:进去的是一行文字;它不断去掉开头空白、> 和后面的一个空格;出来的是去掉引用前缀后的剩余文本,不修改原字符串。
调用关系:FenceTracker::advance 用它识别引用块里的代码围栏,table_candidate_text 用它识别引用块里的表格。
调用图:被 2 处调用(table_candidate_text, advance)。
tests::parse_table_segments_basic247–252 ↗
fn parse_table_segments_basic()
作用:验证标准写法 | A | B | C | 能被拆成三个表格单元。
数据流:进去的是固定测试行;测试调用解析函数并和期望的 A、B、C 比较;结果是通过或失败,不影响运行时状态。
调用关系:它用断言保护 parse_table_segments 的基本行为,防止以后改代码时把最常见表格拆坏。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_table_segments_no_outer_pipes255–257 ↗
fn parse_table_segments_no_outer_pipes()
作用:验证没有首尾竖线的 A | B | C 也能被当成表格行。
数据流:进去的是没有外侧竖线的测试行;解析后应得到三个单元格;测试只产生通过或失败。
调用关系:它补充检查 parse_table_segments 对 Markdown 允许的简写表格形式是否支持。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_table_segments_no_leading_pipe260–265 ↗
fn parse_table_segments_no_leading_pipe()
作用:验证缺少开头竖线、但末尾有竖线的表格行也能正确拆分。
数据流:进去的是 A | B | C |;解析器应去掉末尾竖线并拆出三个单元格;测试结果是断言通过或失败。
调用关系:它确保 parse_table_segments 不把开头竖线当成硬性要求。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_table_segments_no_trailing_pipe268–273 ↗
fn parse_table_segments_no_trailing_pipe()
作用:验证有开头竖线、但没有末尾竖线的表格行也能正确拆分。
数据流:进去的是 | A | B | C;解析器应去掉开头竖线并拆出三个单元格;测试结果是断言通过或失败。
调用关系:它确保 parse_table_segments 不把末尾竖线当成硬性要求。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_table_segments_single_segment_is_allowed276–278 ↗
fn parse_table_segments_single_segment_is_allowed()
作用:验证 | only | 这种只有一个格子的写法仍会被接受。
数据流:进去的是单格表格样式文本;解析后应得到一个 only;测试只检查结果是否一致。
调用关系:它保护 parse_table_segments 对带外侧竖线的单列结构的支持。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_table_segments_without_pipe_returns_none281–283 ↗
fn parse_table_segments_without_pipe_returns_none()
作用:验证普通文字 just text 不会被误认为表格。
数据流:进去的是没有竖线的普通文本;解析器应返回“没有表格段”;测试检查这个结果。
调用关系:它防止 parse_table_segments 过度宽松,避免普通句子进入表格流程。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_table_segments_empty_returns_none286–289 ↗
fn parse_table_segments_empty_returns_none()
作用:验证空行和全是空格的行不会被当成表格。
数据流:进去的是空字符串和空白字符串;解析器应都返回“没有”;测试用断言确认。
调用关系:它保护 parse_table_segments 的基础过滤规则,避免空行干扰表格识别。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_table_segments_escaped_pipe292–298 ↗
fn parse_table_segments_escaped_pipe()
作用:验证被反斜杠保护的 \| 不会被当成列分隔符。
数据流:进去的是包含 A \| B 的测试表格行;解析器应把它留在同一个单元格里,只在真正的竖线处分列;测试检查输出。
调用关系:它直接覆盖 split_unescaped_pipe 通过 parse_table_segments 表现出来的关键规则。
调用图:外部调用 1 个(assert_eq!)。
tests::is_table_delimiter_segment_valid301–307 ↗
fn is_table_delimiter_segment_valid()
作用:验证合法的表格分隔片段都会被接受,包括左右对齐用的冒号写法。
数据流:进去的是多种合法片段;函数应全部返回 true;测试用断言确认。
调用关系:它保护 is_table_delimiter_segment 的正向规则,确保常见 Markdown 表格对齐语法可用。
调用图:外部调用 1 个(assert!)。
tests::is_table_delimiter_segment_invalid310–315 ↗
fn is_table_delimiter_segment_invalid()
作用:验证空片段、横线太少、普通字母等内容不会被当成表格分隔片段。
数据流:进去的是多种非法片段;函数应全部返回 false;测试用断言确认。
调用关系:它保护 is_table_delimiter_segment 的反向规则,避免误把普通内容识别成分隔线。
调用图:外部调用 1 个(assert!)。
tests::is_table_delimiter_line_valid318–322 ↗
fn is_table_delimiter_line_valid()
作用:验证几种合法的整行表格分隔线都能被识别出来。
数据流:进去的是带竖线或不带外侧竖线的分隔线;函数应返回 true;测试检查结果。
调用关系:它测试 is_table_delimiter_line 和底层分段逻辑能否配合识别完整分隔行。
调用图:外部调用 1 个(assert!)。
tests::is_table_delimiter_line_invalid325–328 ↗
fn is_table_delimiter_line_invalid()
作用:验证普通表格内容行和横线太短的行不会被误认为分隔线。
数据流:进去的是非法分隔线示例;函数应返回 false;测试用断言确认。
调用关系:它防止 is_table_delimiter_line 太宽松,从而误启动表格识别。
调用图:外部调用 1 个(assert!)。
tests::is_table_header_line_valid331–334 ↗
fn is_table_header_line_valid()
作用:验证有内容的竖线分隔行会被看作可能的表头。
数据流:进去的是两种表头样式;函数应返回 true;测试只判断真假。
调用关系:它覆盖 is_table_header_line 的正常使用场景,也间接检查 parse_table_segments。
调用图:外部调用 1 个(assert!)。
tests::is_table_header_line_all_empty_segments337–339 ↗
fn is_table_header_line_all_empty_segments()
作用:验证 | | | 这种全空格子的行不会被当成表头。
数据流:进去的是全空单元格行;函数应返回 false;测试确认这个结果。
调用关系:它保护 is_table_header_line 的“至少一个格子有内容”规则。
调用图:外部调用 1 个(assert!)。
tests::fence_tracker_outside_by_default346–349 ↗
fn fence_tracker_outside_by_default()
作用:验证新建的代码块跟踪器默认不在任何代码块里。
数据流:进去不需要真实文本;测试创建 FenceTracker 后读取状态;结果应是 Outside。
调用关系:它检查 FenceTracker::new 和 FenceTracker::kind 的初始配合。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_opens_and_closes_backtick_fence352–362 ↗
fn fence_tracker_opens_and_closes_backtick_fence()
作用:验证反引号代码块可以正常打开、保持、关闭。
数据流:进去的是三行:开头 ``rust、中间代码、结尾 `;跟踪器状态应从 Other 保持到关闭后的 Outside`。
调用关系:它测试 FenceTracker::advance 处理最常见反引号围栏的完整流程。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_opens_and_closes_tilde_fence365–371 ↗
fn fence_tracker_opens_and_closes_tilde_fence()
作用:验证波浪线 ~~~ 形式的代码块也能正常打开和关闭。
数据流:进去的是 ~~~python 和 ~~~;跟踪器应先进入 Other,再回到 Outside。
调用关系:它确保 parse_fence_marker 和 FenceTracker::advance 不只支持反引号,也支持波浪线。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_markdown_fence374–382 ↗
fn fence_tracker_markdown_fence()
作用:验证标成 md 的围栏会被识别为 Markdown 代码块。
数据流:进去的是 ``md、一个表格样式行、以及关闭标记;状态应在中间保持 Markdown,关闭后变回 Outside`。
调用关系:它检查 FenceTracker::advance 和 is_markdown_fence_info 的配合,保证 Markdown 围栏不会被当成普通代码。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_markdown_case_insensitive385–391 ↗
fn fence_tracker_markdown_case_insensitive()
作用:验证 Markdown 这种大小写写法也能被认作 Markdown 围栏。
数据流:进去的是 ``Markdown 和关闭标记;状态应先是 Markdown,再是 Outside`。
调用关系:它保护 is_markdown_fence_info 的忽略大小写规则。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_nested_shorter_marker_does_not_close394–404 ↗
fn fence_tracker_nested_shorter_marker_does_not_close()
作用:验证用四个反引号打开的代码块,三个反引号不能把它关掉。
数据流:进去的是 ```sh、`、``;跟踪器应在较短标记时仍保持 Other`,直到长度匹配才关闭。
调用关系:它测试 FenceTracker::advance 对围栏长度匹配的规则,避免代码内容里的短标记误关块。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_mismatched_char_does_not_close407–416 ↗
fn fence_tracker_mismatched_char_does_not_close()
作用:验证反引号打开的代码块不能被波浪线关闭。
数据流:进去的是 ``sh、~~~、``;中间的波浪线不应改变状态,最后反引号才关闭。
调用关系:它保护 FenceTracker::advance 的标记字符匹配规则。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_indented_4_spaces_ignored419–423 ↗
fn fence_tracker_indented_4_spaces_ignored()
作用:验证前面有 4 个空格的围栏标记不会被当成真正围栏。
数据流:进去的是缩进 4 个空格的 ``sh;跟踪器应仍然是 Outside`。
调用关系:它检查 FenceTracker::advance 对 Markdown 缩进代码块规则的处理。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_blockquote_prefix_stripped426–432 ↗
fn fence_tracker_blockquote_prefix_stripped()
作用:验证引用块里的围栏代码块也能被识别和关闭。
数据流:进去的是 > `sh 和 > `` 这类带引用符号的行;跟踪器应先进入 Other,再回到 Outside`。
调用关系:它覆盖 strip_blockquote_prefix 在 FenceTracker::advance 中的作用。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fence_tracker_close_with_trailing_content_does_not_close435–444 ↗
fn fence_tracker_close_with_trailing_content_does_not_close()
作用:验证关闭围栏那一行如果后面还有额外文字,就不能算真正关闭。
数据流:进去的是开头 ``sh、伪关闭 ` extra、真正关闭 `;状态应在伪关闭后仍是 Other,最后才变成 Outside`。
调用关系:它保护 FenceTracker::advance 的关闭规则,防止带尾巴的行误关代码块。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::parse_fence_marker_backtick451–454 ↗
fn parse_fence_marker_backtick()
作用:验证反引号围栏标记能被正确识别,并能数出长度。
数据流:进去的是 ``rust 和 ```;函数应返回反引号字符以及 3 或 4 的长度;测试检查结果。
调用关系:它直接测试 parse_fence_marker 的反引号分支。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_fence_marker_tilde457–459 ↗
fn parse_fence_marker_tilde()
作用:验证波浪线围栏标记能被正确识别。
数据流:进去的是 ~~~python;函数应返回波浪线字符和长度 3;测试检查结果。
调用关系:它直接测试 parse_fence_marker 的波浪线分支。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_fence_marker_too_short462–465 ↗
fn parse_fence_marker_too_short()
作用:验证少于三个的反引号或波浪线不算围栏。
数据流:进去的是 `` 和 ~~;函数应返回“没有标记”;测试检查结果。
调用关系:它保护 parse_fence_marker 的最低长度规则。
调用图:外部调用 1 个(assert_eq!)。
tests::parse_fence_marker_not_fence468–471 ↗
fn parse_fence_marker_not_fence()
作用:验证普通文字和空行不会被误识别成围栏标记。
数据流:进去的是 hello 和空字符串;函数应返回“没有”;测试确认。
调用关系:它防止 parse_fence_marker 对非围栏文本过度匹配。
调用图:外部调用 1 个(assert_eq!)。
tests::is_markdown_fence_info_basic474–480 ↗
fn is_markdown_fence_info_basic()
作用:验证围栏后的语言说明只有 md 或 markdown 才算 Markdown,且大小写不敏感。
数据流:进去的是多种围栏开头文本和标记长度;函数对 md、markdown、MD 返回 true,对 rust 或空说明返回 false;测试检查真假。
调用关系:它直接保护 is_markdown_fence_info,也间接保证 FenceTracker::advance 能正确分类代码块。
调用图:外部调用 1 个(assert!)。
tests::strip_blockquote_prefix_basic483–487 ↗
fn strip_blockquote_prefix_basic()
作用:验证行首引用符号 >,包括嵌套引用,会被剥掉。
数据流:进去的是单层引用、多层引用和无引用文本;函数应返回去掉引用前缀后的内容,普通文本保持不变;测试检查结果。
调用关系:它直接测试 strip_blockquote_prefix,保证表格和代码围栏在引用块里也能被后续识别。
调用图:外部调用 1 个(assert_eq!)。
tui/src/mention_codec.rs源码 ↗
这份代码像一个“书签翻译器”。人在界面里看到的是短短的 $figma、@sample,但保存到历史记录时,需要把它变成类似 [$figma](app://figma-1) 这样的链接,才能记住它到底指向哪个工具或插件。读历史时又要反过来:把链接还原成用户看得懂的短名字,同时把隐藏的路径收集起来,方便之后继续使用。文件里最重要的两个数据是 LinkedMention,表示一个提及符号、名字和真实路径;以及 DecodedHistoryText,表示还原后的正文和提及列表。代码还特别小心:它不会把 $PATH 这类常见环境变量误认成工具,也不会把邮箱里的 @sample 当成提及。
encode_history_mentions21–89 ↗
fn encode_history_mentions(text: &str, mentions: &[LinkedMention]) -> String
作用:把普通正文里的可见提及,比如 $figma 或 @sample,替换成带真实路径的历史链接。这样保存聊天历史时,不只是保存名字,还能保存它背后指向的工具、插件或技能。
数据流:进去的是一段用户可见文字和一组已经知道真实路径的提及记录。它先按“符号加名字”把路径排好队,然后从左到右扫描文字;遇到合法的提及时,就用对应路径改写成 [$名字](路径) 这种链接。出来的是一段适合写进历史记录的字符串,原文字不会被直接修改。
调用关系:它通常在保存历史前使用。测试里的多个用例会调用它,确认同名提及按出现顺序绑定、@ 和 $ 不互相抢路径、句号和括号旁边也能正确识别。它把边界判断交给 starts_plaintext_mention 和 ends_plaintext_mention,把名字字符判断交给 is_mention_name_char。
调用图:调用 3 个内部函数(ends_plaintext_mention, is_mention_name_char, starts_plaintext_mention);被 9 处调用(encode_history_mentions_does_not_let_at_token_steal_later_tool_binding, encode_history_mentions_links_at_mentions_after_unicode_whitespace, encode_history_mentions_links_both_sigils_for_same_name, encode_history_mentions_links_bound_mentions_in_order, encode_history_mentions_links_dollar_mentions_after_punctuation, encode_history_mentions_links_parenthesized_at_mentions, encode_history_mentions_links_sentence_ending_at_mentions, encode_history_mentions_preserves_at_sigils, encode_history_mentions_skips_embedded_at_substrings);外部调用 4 个(new, with_capacity, matches!, is_empty)。
decode_history_mentions_with_at_mentions91–127 ↗
fn decode_history_mentions_with_at_mentions(
text: &str,
at_mentions_enabled: bool,
) -> DecodedHistoryText
作用:把历史记录里的链接提及还原成界面上可见的短提及,同时把链接里藏着的真实路径提取出来。它还会根据设置决定是否保留 @ 提及。
数据流:进去的是历史文本,以及一个开关,说明是否启用 @ 提及。它逐字扫描文本;遇到像 [$figma](app://...) 或 [@sample](plugin://...) 的片段,就请 parse_history_linked_mention 判断是否真的是有效提及。出来的是 DecodedHistoryText:一份干净正文,加上一份提及和路径的清单。
调用关系:它通常在读取历史记录时使用,也会被外部的 new_with_at_mentions 之类初始化流程调用。它自己不判断所有细节,而是把“这个链接算不算提及”的问题交给 parse_history_linked_mention。相关测试覆盖了保留 $、保留 @、关闭 @ 时的旧格式兼容等情况。
调用图:调用 1 个内部函数(parse_history_linked_mention);被 6 处调用(new_with_at_mentions, decode_history_mentions_restores_at_sigil_for_tool_paths, decode_history_mentions_restores_plugin_links_with_at_sigil, decode_history_mentions_restores_visible_tokens, decode_history_mentions_without_at_mentions_ignores_at_non_plugin_paths, decode_history_mentions_without_at_mentions_uses_legacy_plugin_fallback);外部调用 2 个(with_capacity, new)。
parse_history_linked_mention129–162 ↗
fn parse_history_linked_mention(
text: &'a str,
text_bytes: &[u8],
start: usize,
at_mentions_enabled: bool,
) -> Option<(char, &'a str, &'a str, usize)>
作用:判断历史文本当前位置的链接,是否是一个可信的工具或插件提及。它是解码过程里的“验票员”。
数据流:进去的是整段文本、文本字节、当前起点,以及 @ 提及是否启用。它先尝试按 $ 链接解析,再根据开关尝试按 @ 链接解析;解析后还会排除常见环境变量,并检查路径是不是工具、插件、技能这类路径。出来的是提及符号、名字、路径和片段结束位置;如果不合格,就返回空。
调用关系:它只被 decode_history_mentions_with_at_mentions 调用。它把具体的链接格式拆解交给 parse_linked_tool_mention,再用 is_common_env_var 和 is_tool_path 做安全筛选,避免把普通 Markdown 链接或环境变量误当成工具提及。
调用图:调用 3 个内部函数(is_common_env_var, is_tool_path, parse_linked_tool_mention);被 1 处调用(decode_history_mentions_with_at_mentions)。
parse_linked_tool_mention164–219 ↗
fn parse_linked_tool_mention(
text: &'a str,
text_bytes: &[u8],
start: usize,
sigil: char,
) -> Option<(&'a str, &'a str, usize)>
作用:拆开一个形如 [$name](path) 或 [@name](path) 的链接,取出名字和路径。它只负责格式是否长得对,不负责判断这个路径是不是真工具。
数据流:进去的是文本、字节数组、起始位置,以及期望的符号 $ 或 @。它检查开头符号、名字字符、右中括号、括号里的路径,并去掉路径两边空白。出来的是名字、路径和整个链接结束后的下标;格式不对或路径为空就返回空。
调用关系:它被 parse_history_linked_mention 调用,是链接解析的基础零件。它把“名字里能有哪些字符”的判断交给 is_mention_name_char,但不关心路径属于哪类工具。
调用图:调用 1 个内部函数(is_mention_name_char);被 1 处调用(parse_history_linked_mention)。
is_mention_name_char221–223 ↗
fn is_mention_name_char(byte: u8) -> bool
作用:判断一个字节能不能出现在提及名字里。允许英文字母、数字、下划线和短横线。
数据流:进去的是一个字节。它检查这个字节是否落在允许范围内。出来的是真假值:能做名字字符就是真,否则是假。
调用关系:它被 encode_history_mentions 和 parse_linked_tool_mention 使用。前者用它找出正文里的提及名字有多长,后者用它确认历史链接里的名字格式是否合法。
调用图:被 2 处调用(encode_history_mentions, parse_linked_tool_mention);外部调用 1 个(matches!)。
starts_plaintext_mention225–233 ↗
fn starts_plaintext_mention(text: &str, index: usize) -> bool
作用:判断某个 @ 位置是不是一个真正提及的开头,而不是邮箱、包名或普通单词中间的一部分。
数据流:进去的是整段文本和 @ 的位置。它看这个位置前面有没有字符;如果在开头,或前一个字符是空白、标点这类边界,就认为可以作为提及开头。出来的是真假值。
调用关系:它被 encode_history_mentions 用来保护 @ 提及。这样 foo@sample.com 不会被错改成链接,而 请问 @sample 这种正常提及可以被编码。
调用图:被 1 处调用(encode_history_mentions)。
ends_plaintext_mention235–248 ↗
fn ends_plaintext_mention(text_bytes: &[u8], index: usize) -> bool
作用:判断一个提及名字后面是不是已经结束了。它避免把路径、文件名或更长的单词误切成提及。
数据流:进去的是文本字节和名字结束的位置。它查看后一个字符:空白、合适的标点、文本结束通常算结束;斜杠、反斜杠、单词字符等则可能说明后面还是路径或名字的一部分。出来的是真假值。
调用关系:它被 encode_history_mentions 调用。这个判断让 $figma/docs 能只链接 $figma,同时避免在明显不该断开的地方乱插历史链接。
调用图:被 1 处调用(encode_history_mentions)。
is_mention_name_char_char250–252 ↗
fn is_mention_name_char_char(ch: char) -> bool
作用:用字符而不是字节来判断某个字符能不能算提及名字的一部分。它主要服务于边界判断。
数据流:进去的是一个字符。它检查这个字符是不是 ASCII 字母数字,或者下划线、短横线。出来的是真假值。
调用关系:它和 starts_plaintext_mention 的判断配套,用来区分“前面还是名字的一部分”还是“前面是边界”。这样提及识别不会太贪心。
调用图:外部调用 1 个(matches!)。
is_common_env_var254–270 ↗
fn is_common_env_var(name: &str) -> bool
作用:判断一个名字是不是常见环境变量,比如 PATH、HOME、USER。环境变量就是操作系统和程序常用的一些配置名字。
数据流:进去的是提及名字。它先转成大写,再和一小组常见环境变量名称比较。出来的是真假值:如果像环境变量,就返回真。
调用关系:它被 parse_history_linked_mention 调用,用来防止把 [$PATH](...) 这类内容误当成工具提及。这个小过滤能减少历史记录解码时的误判。
调用图:被 1 处调用(parse_history_linked_mention);外部调用 1 个(matches!)。
is_tool_path272–281 ↗
fn is_tool_path(path: &str) -> bool
作用:判断一个路径看起来是不是工具、插件或技能的路径。它是确认“这个链接真的值得当提及恢复”的关键筛子。
数据流:进去的是链接里的路径字符串。它检查路径是否以 app://、mcp://、plugin://、skill:// 开头,或者文件名是不是 SKILL.md。出来的是真假值。
调用关系:它被 parse_history_linked_mention 调用。前面的解析只说明链接格式对,这个函数进一步说明它像不像系统认识的工具路径。
调用图:被 1 处调用(parse_history_linked_mention)。
tests::decode_history_mentions_restores_visible_tokens289–315 ↗
fn decode_history_mentions_restores_visible_tokens()
作用:测试历史链接能不能还原成用户看得见的 $name 文本,并保留每个链接背后的路径。
数据流:进去的是一段包含多个 $ 链接的历史文本。测试调用 decode_history_mentions_with_at_mentions,再比较还原后的正文和提及列表。结果必须是正文变回 $figma、$sample,路径一条不丢。
调用关系:它是解码主流程的基础验收用例。它直接检查 decode_history_mentions_with_at_mentions,间接覆盖链接解析和工具路径判断。
调用图:调用 1 个内部函数(decode_history_mentions_with_at_mentions);外部调用 1 个(assert_eq!)。
tests::decode_history_mentions_restores_plugin_links_with_at_sigil318–339 ↗
fn decode_history_mentions_restores_plugin_links_with_at_sigil()
作用:测试启用 @ 提及时,插件链接里的 @sample 会按 @ 原样恢复。
数据流:进去的是同时包含 @ 插件链接和 $ 工具链接的历史文本。测试解码后检查正文是否变成 @sample 和 $figma,并检查提及列表里的符号和路径是否正确。
调用关系:它验证 decode_history_mentions_with_at_mentions 在 at_mentions_enabled 为真时的行为,确保新格式不会被错误降级成旧的 $ 格式。
调用图:调用 1 个内部函数(decode_history_mentions_with_at_mentions);外部调用 1 个(assert_eq!)。
tests::decode_history_mentions_without_at_mentions_uses_legacy_plugin_fallback342–363 ↗
fn decode_history_mentions_without_at_mentions_uses_legacy_plugin_fallback()
作用:测试关闭 @ 提及时,旧系统仍能读懂 @ 插件链接,并把它当成 $ 提及恢复。
数据流:进去的是含有 [@sample](plugin://...) 的历史文本,但开关设置为不启用 @。测试解码后要求正文里显示 $sample,提及记录也用 $ 符号。出来的结果证明旧模式仍兼容插件历史。
调用关系:它检查 decode_history_mentions_with_at_mentions 的兼容分支。这个用例保护老历史记录或老配置,不会因为新提及符号而失效。
调用图:调用 1 个内部函数(decode_history_mentions_with_at_mentions);外部调用 1 个(assert_eq!)。
tests::decode_history_mentions_without_at_mentions_ignores_at_non_plugin_paths366–374 ↗
fn decode_history_mentions_without_at_mentions_ignores_at_non_plugin_paths()
作用:测试关闭 @ 提及时,非插件路径的 @ 链接不会被偷偷当成提及。
数据流:进去的是 [@figma](app://...),但 @ 提及开关关闭。测试解码后要求原文保持不变,提及列表为空。出来的结果说明代码没有过度兼容。
调用关系:它验证 parse_history_linked_mention 的限制条件。只有 plugin:// 在旧模式下走回退逻辑,普通 app 路径不会被误处理。
调用图:调用 1 个内部函数(decode_history_mentions_with_at_mentions);外部调用 1 个(assert_eq!)。
tests::decode_history_mentions_restores_at_sigil_for_tool_paths377–392 ↗
fn decode_history_mentions_restores_at_sigil_for_tool_paths()
作用:测试启用 @ 提及时,@ 指向工具路径也能被正确还原。
数据流:进去的是 [@figma](app://figma-1),并打开 @ 提及。测试解码后要求正文是 @figma,提及列表保存 @、figma 和 app 路径。
调用关系:它验证 decode_history_mentions_with_at_mentions 配合 is_tool_path 的新行为,确保 @ 不只适用于插件,也能表示统一的工具提及。
调用图:调用 1 个内部函数(decode_history_mentions_with_at_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_links_bound_mentions_in_order395–421 ↗
fn encode_history_mentions_links_bound_mentions_in_order()
作用:测试同一个名字出现多次时,会按出现顺序绑定不同路径。
数据流:进去的是含有两个 $figma 和一个 $sample 的正文,以及三条提及路径。测试编码后要求第一个 $figma 用第一个 figma 路径,第二个 $figma 用第二个 figma 路径,没绑定的 $other 保持原样。
调用关系:它直接测试 encode_history_mentions 的排队绑定逻辑。这个用例保证同名工具不会全都错误指向同一个路径。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_links_dollar_mentions_after_punctuation424–434 ↗
fn encode_history_mentions_links_dollar_mentions_after_punctuation()
作用:测试 $ 提及放在括号这类标点后面时,也能被识别并编码。
数据流:进去的是 ($figma) 和一条 figma 路径。测试编码后要求括号还在,但中间的 $figma 变成带路径的链接。结果说明标点不会挡住正常提及。
调用关系:它调用 encode_history_mentions,重点覆盖提及开头边界的判断,防止只在空格后才能识别提及。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_links_dollar_mentions_with_path_like_suffixes437–456 ↗
fn encode_history_mentions_links_dollar_mentions_with_path_like_suffixes()
作用:测试 $figma 后面接 /docs、.suffix、反斜杠路径时,编码只链接 $figma 本身。
数据流:进去的是几段看起来像路径或后缀的文本,以及同一条 figma 提及。测试期望输出把 $figma 变成链接,但后面的 /docs、.suffix、\docs 仍作为普通文字跟在后面。
调用关系:它验证 encode_history_mentions 和 ends_plaintext_mention 的配合。这个用例很重要,因为用户常会写“工具名加路径”,代码不能把整串都吞进提及名字里。
调用图:外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_preserves_at_sigils459–480 ↗
fn encode_history_mentions_preserves_at_sigils()
作用:测试 @ 提及在保存历史时仍然保存成 @,不会被改成 $。
数据流:进去的是含有 @figma、@sample 和一个无绑定 $other 的文本。测试编码后要求两个 @ 提及变成 [@name](path),而 $other 保持普通文本。
调用关系:它调用 encode_history_mentions,保护新提及符号的保存行为。这样用户看到和历史里记录的符号能保持一致。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_links_both_sigils_for_same_name483–504 ↗
fn encode_history_mentions_links_both_sigils_for_same_name()
作用:测试同一个名字同时用 @figma 和 $figma 出现时,两者能各自绑定自己的路径。
数据流:进去的是 @figma then $figma,以及两条同名但符号不同的提及记录。测试编码后要求 @figma 链到插件路径,$figma 链到 app 路径。
调用关系:它验证 encode_history_mentions 按“符号加名字”区分绑定,而不是只看名字。这样两个不同来源不会互相串线。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_does_not_let_at_token_steal_later_tool_binding507–518 ↗
fn encode_history_mentions_does_not_let_at_token_steal_later_tool_binding()
作用:测试前面的 @figma 不会抢走后面 $figma 的 $ 路径绑定。
数据流:进去的是 @figma then $figma,但只提供一条 $figma 的绑定。测试编码后要求 @figma 保持原样,只有 $figma 被链接到 app 路径。
调用关系:它直接调用 encode_history_mentions,保护符号隔离逻辑。这个用例防止编码器看到同名就乱用路径。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_links_at_mentions_after_unicode_whitespace521–533 ↗
fn encode_history_mentions_links_at_mentions_after_unicode_whitespace()
作用:测试 @ 提及出现在全角空格后面时也能识别。全角空格是中文环境里常见的宽空格。
数据流:进去的是 foo @sample 和一条插件路径。测试编码后要求 @sample 被改成链接,全角空格保持不变。结果说明边界判断不只认普通英文空格。
调用关系:它调用 encode_history_mentions,重点保护 starts_plaintext_mention 对 Unicode 空白的支持。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_links_sentence_ending_at_mentions536–547 ↗
fn encode_history_mentions_links_sentence_ending_at_mentions()
作用:测试句子末尾的 @figma. 能正确把 @figma 链接起来,并把句号留在外面。
数据流:进去的是 Please ask @figma. 和一条技能路径。测试编码后要求输出为 Please ask [@figma](路径).。也就是说,提及被保存,句号仍是普通标点。
调用关系:它调用 encode_history_mentions,验证 ends_plaintext_mention 对句末标点的处理,避免把句号错误算进名字。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_links_parenthesized_at_mentions550–561 ↗
fn encode_history_mentions_links_parenthesized_at_mentions()
作用:测试括号里的 @figma 也能被编码成链接。
数据流:进去的是 Please ask (@figma) 和一条插件路径。测试编码后要求括号保留,里面的 @figma 变成带路径链接。
调用关系:它调用 encode_history_mentions,覆盖 @ 提及在标点包围时的情况,保证自然语言写法不会影响保存。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
tests::encode_history_mentions_skips_embedded_at_substrings564–578 ↗
fn encode_history_mentions_skips_embedded_at_substrings()
作用:测试邮箱、包名、路径片段里的 @sample 不会被误认成提及,只有真正独立的 @sample 会被链接。
数据流:进去的是 foo@sample.com npx @sample/pkg then @sample 和一条插件路径。测试编码后要求前两个 @sample 保持原样,最后一个独立 @sample 才变成链接。
调用关系:它调用 encode_history_mentions,集中验证 starts_plaintext_mention 和 ends_plaintext_mention 的防误判能力。这个测试能避免历史保存把邮箱或包名改坏。
调用图:调用 1 个内部函数(encode_history_mentions);外部调用 1 个(assert_eq!)。
引用提取
这个专用解析器将隐藏的引用标记转换为结构化的内存引用数据,供下游使用。
memories/read/src/citations.rs源码 ↗
这个文件像一个“引用信息清洗器”。上游可能给它一段文字,里面夹着类似 <citation_entries> 和 <rollout_ids> 这样的隐藏标签。普通人可以把这些标签理解成表格的边框:边框里面才是真正要读的数据。parse_memory_citation 会逐段检查这些文字,找出引用条目和历史线程 ID,并去掉重复的线程 ID。引用条目会被拆成文件路径、行号范围和备注;线程 ID 会先保留成字符串,后面需要时再转成 ThreadId(线程编号,一次对话或运行的身份标识)。如果什么有效信息都没找到,它会返回空,表示“这里没有可用引用”。文件里还兼容两种 ID 标签:rollout_ids 和 thread_ids,这说明它要照顾新旧格式,避免因为标签名字变化就读不到历史引用。
parse_memory_citation6–43 ↗
fn parse_memory_citation(citations: Vec<String>) -> Option<MemoryCitation>
作用:把多段可能含有隐藏引用标签的字符串,解析成一个 MemoryCitation(记忆引用对象)。如果里面没有任何有效引用或线程 ID,就明确返回 None,表示不用继续处理。
数据流:进去的是 Vec<String>,也就是多段原始引用文字。它逐段查找 <citation_entries> 里的引用行,交给 parse_memory_citation_entry 拆成结构化条目;同时查找 rollout_ids 或 thread_ids 里的 ID,去掉空行和重复项。出来的是 Some(MemoryCitation),里面包含引用条目和去重后的 ID;如果两边都为空,就出来 None。
调用关系:它是这个文件的主入口之一。record_stage1_output_usage_and_detect_memory_citation 和 strip_hidden_assistant_markup_and_parse_memory_citation 会在需要从助手输出里识别隐藏记忆引用时调用它。它自己把找标签的工作交给 extract_block 和 extract_ids_block,把单行引用的细拆工作交给 parse_memory_citation_entry。
调用图:调用 2 个内部函数(extract_block, extract_ids_block);被 2 处调用(record_stage1_output_usage_and_detect_memory_citation, strip_hidden_assistant_markup_and_parse_memory_citation);外部调用 2 个(new, new)。
thread_ids_from_memory_citation45–51 ↗
fn thread_ids_from_memory_citation(memory_citation: &MemoryCitation) -> Vec<ThreadId>
作用:从 MemoryCitation 里取出 rollout_ids,并尝试把这些字符串变成真正的 ThreadId。它用来筛掉格式不对的 ID,只留下系统认可的线程编号。
数据流:进去的是一个 MemoryCitation 的引用。它读取其中的 rollout_ids,逐个尝试转换成 ThreadId;转换失败的会被跳过。出来的是 Vec<ThreadId>,也就是一组可安全使用的线程编号;它不修改原来的 MemoryCitation。
调用关系:record_stage1_output_usage_for_memory_citation 会在记录引用使用情况时调用它。它位于“已经解析出记忆引用”之后,是把普通字符串 ID 变成正式线程 ID 的小关卡。
调用图:被 1 处调用(record_stage1_output_usage_for_memory_citation)。
parse_memory_citation_entry53–70 ↗
fn parse_memory_citation_entry(line: &str) -> Option<MemoryCitationEntry>
作用:解析一行具体的引用条目,把它拆成文件路径、起止行号和备注。它保证只有格式完整的行才会变成 MemoryCitationEntry。
数据流:进去的是一行文本,例如包含“路径:起始行-结束行|note=[备注]”这种格式。它先去掉首尾空白,再拆出备注、文件路径和行号范围,并把行号从文字转成数字。出来的是 Some(MemoryCitationEntry);如果少了备注、行号格式不对、数字解析失败等,就出来 None。
调用关系:它由 parse_memory_citation 在读取 <citation_entries> 块时逐行使用。它不再调用本文件里的其他函数,像一个严格的检票员:每一行引用必须票面完整,才能进入最终的 MemoryCitation。
extract_block72–76 ↗
fn extract_block(text: &'a str, open: &str, close: &str) -> Option<&'a str>
作用:从一段文字里取出两个指定标签之间的内容。它是最基础的“夹心提取”工具,比如从开始标签和结束标签中间拿出正文。
数据流:进去的是完整文本,以及 open 和 close 两个边界字符串。它先找到 open 后面的部分,再找到 close 前面的部分。出来的是中间那段文本的引用;如果任意一个边界找不到,就出来 None。它不复制内容,也不修改原文。
调用关系:parse_memory_citation 用它直接提取 <citation_entries> 内容;extract_ids_block 也用它尝试提取 ID 区块。它是这个文件里其他解析动作的基础小工具。
调用图:被 2 处调用(extract_ids_block, parse_memory_citation)。
extract_ids_block78–81 ↗
fn extract_ids_block(text: &str) -> Option<&str>
作用:专门从文字里找线程 ID 列表。它同时支持 <rollout_ids> 和 <thread_ids> 两种标签,避免新旧格式不一致导致解析失败。
数据流:进去的是一段原始文本。它先尝试用 extract_block 读取 <rollout_ids> 中间的内容;如果没有,再尝试读取 <thread_ids> 中间的内容。出来的是找到的 ID 文本块;两种标签都没有时,出来 None。
调用关系:parse_memory_citation 在收集历史线程 ID 时会调用它。它把具体“找哪种 ID 标签”的判断包起来,让主解析函数不用关心兼容细节。
调用图:调用 1 个内部函数(extract_block);被 1 处调用(parse_memory_citation)。