Rust in Everything
基于 pulldown-cmark 0.13.5

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 &amp; 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 &amp; 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

原因

两边的类型对不上,编译器说得很准:

写出器用不上范围,所以签名里不收;库里也没有一个「同时接受两种元素」的 trait。范围要不要、在哪一步丢掉,由调用方决定。

📝NOTE
冷知识:范围最初是两个 usize

into_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 &amp; 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 &amp; b。</p>
<a data-line="5"></a>
<h2>第二节</h2>

几点要注意:

  • 顺序是「先 into_offset_iter,再 TextMergeWithOffset,再自己的变换,最后丢范围」。先 .map(|(ev, _)| ev) 再想要范围就拿不回来了。
  • TextMergeWithOffset 合并后的范围从第一段的 start 到最后一段的 end,中间实体和转义的源文也算在里面(改写事件流)。
  • 插进去的 Event::Html 原样写出,里面的内容要自己转义;这里只拼了一个数字。
  • 这里每个事件都从文档开头数一遍换行,总开销随文档长度平方增长;事件多时先建一张行首偏移表再二分。

怎么检查

这个坑编译器替你查,不会漏到运行时。容易混淆的是这几个名字各收什么、产什么:

名字收产
Parser&strEvent
Parser::into_offset_iterParser(Event, Range<usize>)
TextMergeStream::newIterator<Item = Event>Event
TextMergeWithOffset::newIterator<Item = (Event, Range<usize>)>(Event, Range<usize>)
push_html / write_html_io / write_html_fmtIterator<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)起就是这个二元组,写出器从来不收范围。

💬 关联讨论

发起讨论
加载中...