E0271: OffsetIter yields (Event, Range)
主线位置:输出主线的前半段,解析器和写出器之间的适配器层(改写事件流)。
into_offset_iter()给每个事件附上源文字节范围,范围的含义见事件模型;写出器和TextMergeStream只收不带范围的Event。
flowchart TB p["Parser"] --> off["into_offset_iter"] off --> pair["元素 Event 和 Range"] pair --> ph["push_html"] ph --> err["E0271"] pair --> tms["TextMergeStream"] tms --> err pair --> tmo["TextMergeWithOffset"] tmo --> work["按范围做变换"] work --> strip["最后丢掉范围"] strip --> ok["push_html"]
实验 crate:exp-pulldown-cmark/pitb-e0271,src/bin/direct.rs、merged.rs 是两种错法,mapped.rs 和 main.rs 是修法。M2 Max、macOS 27、rustc 1.99.0。
现象
想给输出加上「这一段在源文第几行」,于是把解析器换成 into_offset_iter(),其余照旧:
html::push_html(&mut out, Parser::new("*a*").into_offset_iter());
error[E0271]: expected `OffsetIter<'_>` to be an iterator that yields `Event<'_>`, but it yields `(Event<'_>, Range<usize>)`
--> src/bin/direct.rs:5:29
|
5 | html::push_html(&mut out, Parser::new("*a*").into_offset_iter());
| --------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Event<'_>`, found `(Event<'_>, Range<usize>)`
| |
| required by a bound introduced by this call
|
= note: expected enum `Event<'_>`
found tuple `(Event<'_>, std::ops::Range<usize>)`
note: required by a bound in `push_html`
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pulldown-cmark-0.13.5/src/html.rs:552:17
|
550 | pub fn push_html<'a, I>(s: &mut String, iter: I)
| --------- required by a bound in this function
551 | where
552 | I: Iterator<Item = Event<'a>>,
| ^^^^^^^^^^^^^^^^ required by this bound in `push_html`
中间如果还套了一层 TextMergeStream,错误在它的构造函数上就出现,push_html 那里再报一次:
let events = TextMergeStream::new(Parser::new("a & b").into_offset_iter());
html::push_html(&mut out, events);
error[E0271]: expected `OffsetIter<'_>` to be an iterator that yields `Event<'_>`, but it yields `(Event<'_>, Range<usize>)`
--> src/bin/merged.rs:5:37
|
5 | let events = TextMergeStream::new(Parser::new("a & b").into_offset_iter());
| -------------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Event<'_>`, found `(Event<'_>, Range<usize>)`
...
note: required by a bound in `TextMergeStream::<'a, I>::new`
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pulldown-cmark-0.13.5/src/utils.rs:23:17
...
error[E0271]: expected `OffsetIter<'_>` to be an iterator that yields `Event<'_>`, but it yields `(Event<'_>, Range<usize>)`
--> src/bin/merged.rs:6:29
...
= note: required for `TextMergeStream<'_, OffsetIter<'_>>` to implement `Iterator`
...
error: could not compile `pitb-e0271` (bin "merged") due to 2 previous errors
原因
两边的类型对不上,编译器说得很准:
Parser::into_offset_iter返回OffsetIter,它的Iterator实现是type Item = (Event<'a>, Range<usize>)。push_html(以及write_html_io、write_html_fmt)要求I: Iterator<Item = Event<'a>>。TextMergeStream::new也要求Item = Event<'a>;收(Event, Range)的是另一个类型TextMergeWithOffset。
写出器用不上范围,所以签名里不收;库里也没有一个「同时接受两种元素」的 trait。范围要不要、在哪一步丢掉,由调用方决定。
usizeinto_offset_iter 来自 PR #222「Implement an event iterator that includes source offsets」,首次发布在 0.4.0。PR 的第一个提交 86eb0c7 给 Parser 加了一个 next_event,返回 Option<(Event<'a>, usize, usize)>,OffsetIter 的元素也是这个三元组;原来的 next 改成调用它再把两个数扔掉。第二个提交 85b17bd「Use Range in offset_iter API」把元素改成 (Event<'a>, Range<usize>),PR 合并时就是这样。从那以后元素类型没再变过,push_html 也一直只收 Event,今天报错里那句 found tuple `(Event<'_>, std::ops::Range<usize>)` 就是这次改动的样子。
修法
只是想用范围做点事,最后交给 push_html:在最后一步丢掉范围。
html::push_html(&mut out, Parser::new("*a*").into_offset_iter().map(|(ev, _)| ev));
<p><em>a</em></p>
要合并文本,又要范围:用 TextMergeWithOffset,变换时一并丢掉范围。 下面在每个标题前插一个带行号的锚点,用 flat_map 一变多,输出的元素已经是 Event:
let src = "# 标题\n\n正文 a & b。\n\n## 第二节\n";
let events = TextMergeWithOffset::new(Parser::new(src).into_offset_iter()).flat_map(|(ev, r)| {
let line = src[..r.start].matches('\n').count() + 1;
match ev {
Event::Start(Tag::Heading { .. }) => vec![Event::Html(format!("<a data-line=\"{line}\"></a>").into()), ev],
_ => vec![ev],
}
});
let mut out = String::new();
html::push_html(&mut out, events);
<a data-line="1"></a>
<h1>标题</h1>
<p>正文 a & b。</p>
<a data-line="5"></a>
<h2>第二节</h2>
几点要注意:
- 顺序是「先
into_offset_iter,再TextMergeWithOffset,再自己的变换,最后丢范围」。先.map(|(ev, _)| ev)再想要范围就拿不回来了。 TextMergeWithOffset合并后的范围从第一段的start到最后一段的end,中间实体和转义的源文也算在里面(改写事件流)。- 插进去的
Event::Html原样写出,里面的内容要自己转义;这里只拼了一个数字。 - 这里每个事件都从文档开头数一遍换行,总开销随文档长度平方增长;事件多时先建一张行首偏移表再二分。
怎么检查
这个坑编译器替你查,不会漏到运行时。容易混淆的是这几个名字各收什么、产什么:
| 名字 | 收 | 产 |
|---|---|---|
Parser | &str | Event |
Parser::into_offset_iter | Parser | (Event, Range<usize>) |
TextMergeStream::new | Iterator<Item = Event> | Event |
TextMergeWithOffset::new | Iterator<Item = (Event, Range<usize>)> | (Event, Range<usize>) |
push_html / write_html_io / write_html_fmt | Iterator<Item = Event> | 写出 HTML |
看到 E0271 里出现 (Event<'_>, Range<usize>),就是带范围的流接到了只收 Event 的地方;顺着链往回找第一个 into_offset_iter(),决定范围在哪一步丢。
小结
into_offset_iter()的元素是(Event, Range<usize>);push_html、write_html_*和TextMergeStream::new都只收Event,直接接上就是 E0271。- 修法:带着范围做完所有变换,最后
.map(|(ev, _)| ev)(或在flat_map里只产出事件);合并文本用TextMergeWithOffset,不要用TextMergeStream。 - 范围一旦丢掉就拿不回来,所以丢范围放在链的最后。
- 元素类型从 0.4.0(PR #222)起就是这个二元组,写出器从来不收范围。