Parses each `range` of `source` as an independent module and concatenates the results into a single [`Parsed `] whose nodes keep their offsets into `source`. This validates sources such as Jupyter notebooks, where each cell must be syntactically valid on its own while later cells can still reference earlier definitions. The `ranges` must be ordered and non-overlapping. Consecutive rang
(
source: &str,
ranges: impl IntoIterator<Item = TextRange>,
options: &ParseOptions,
)
| 311 | /// anchored at a cell's trailing offset then lands on that separator, the cell's own last line, so |
| 312 | /// it is attributed to that cell rather than to the following one. |
| 313 | pub fn parse_cells_unchecked( |
| 314 | source: &str, |
| 315 | ranges: impl IntoIterator<Item = TextRange>, |
| 316 | options: &ParseOptions, |
| 317 | ) -> Parsed<ModModule> { |
| 318 | let mut ranges = ranges.into_iter().peekable(); |
| 319 | let mut body = Suite::new(); |
| 320 | let mut tokens = Vec::new(); |
| 321 | let mut errors = Vec::new(); |
| 322 | let mut unsupported_syntax_errors = Vec::new(); |
| 323 | let mut module_range: Option<TextRange> = None; |
| 324 | |
| 325 | while let Some(range) = ranges.next() { |
| 326 | if let Some(previous) = module_range { |
| 327 | assert!(previous.end() <= range.start()); |
| 328 | } |
| 329 | |
| 330 | // The cell is lexed from `range.start()`, so the slice must keep the leading text to |
| 331 | // preserve absolute offsets into the concatenated source. |
| 332 | let cell_source = &source[TextRange::up_to(range.end())]; |
| 333 | let Parsed { |
| 334 | syntax, |
| 335 | tokens: cell_tokens, |
| 336 | errors: cell_errors, |
| 337 | unsupported_syntax_errors: cell_unsupported_syntax_errors, |
| 338 | } = Parser::new_starts_at(cell_source, range.start(), options.clone()) |
| 339 | .parse() |
| 340 | .try_into_module() |
| 341 | .expect("module options should parse into a module"); |
| 342 | |
| 343 | body.extend(syntax.body); |
| 344 | tokens.extend(cell_tokens); |
| 345 | errors.extend(cell_errors); |
| 346 | unsupported_syntax_errors.extend(cell_unsupported_syntax_errors); |
| 347 | |
| 348 | // Each range excludes its trailing `\n` separator (see the doc comment above), leaving a |
| 349 | // one-byte gap in the token stream. Cover it with a `NonLogicalNewline` so token-based |
| 350 | // checks don't treat the separator as another logical line terminator. The final cell's |
| 351 | // separator is the file-final newline and is deliberately left uncovered. |
| 352 | if let Some(next) = ranges.peek() { |
| 353 | let separator = TextRange::new(range.end(), next.start()); |
| 354 | assert_eq!(&source[separator], "\n"); |
| 355 | tokens.push(Token::new( |
| 356 | TokenKind::NonLogicalNewline, |
| 357 | separator, |
| 358 | TokenFlags::empty(), |
| 359 | )); |
| 360 | } |
| 361 | |
| 362 | module_range = Some(match module_range { |
| 363 | Some(previous) => TextRange::new(previous.start(), range.end()), |
| 364 | None => range, |
| 365 | }); |
| 366 | } |
| 367 | |
| 368 | body.shrink_to_fit(); |
| 369 | tokens.shrink_to_fit(); |
| 370 | errors.shrink_to_fit(); |