1use super::*;
7use crate::Span;
8use crate::escape::is_escaped;
9use crate::source::SourceText;
10
11pub(crate) fn classify_lines(source: &SourceText) -> LineMap {
12 let mut builder = LineMapBuilder::new(source);
13 builder.classify();
14 builder.finish()
15}
16
17struct LineMapBuilder<'source> {
18 source: &'source SourceText,
19 lines: Vec<ClassifiedLine>,
20 blocks: Vec<SourceBlock>,
21 tunes: Vec<TuneBlock>,
22 continuation_edges: Vec<ContinuationEdge>,
23 non_note_items: Vec<NonNoteItem>,
24 state: DocumentState,
25 next_tune_index: usize,
26 previous_field_line: Option<usize>,
27 pending_music_continuation: Option<PendingMusicContinuation>,
28 typeset_text_block_start: Option<usize>,
29 in_begintext_block: bool,
30}
31
32impl<'source> LineMapBuilder<'source> {
33 fn new(source: &'source SourceText) -> Self {
34 Self {
35 source,
36 lines: Vec::new(),
37 blocks: Vec::new(),
38 tunes: Vec::new(),
39 continuation_edges: Vec::new(),
40 non_note_items: Vec::new(),
41 state: DocumentState::Start,
42 next_tune_index: 0,
43 previous_field_line: None,
44 pending_music_continuation: None,
45 typeset_text_block_start: None,
46 in_begintext_block: false,
47 }
48 }
49
50 fn classify(&mut self) {
51 for index in 0..self.source.line_count() {
52 let Some(source_line) = self.source.line(index) else {
53 continue;
54 };
55 let Some(text) = self.source.line_text(index) else {
56 continue;
57 };
58
59 let raw_line = classify_raw_line(
60 index,
61 source_line.full_span(),
62 source_line.text_span(),
63 text,
64 self.in_begintext_block,
65 );
66
67 let line = self.apply_structure(raw_line);
68 self.track_typeset_block(&line);
69 self.track_non_note_items(&line);
70 self.track_field_continuation(&line);
71 self.track_music_continuation(&line);
72 self.update_begintext_state(&line, text);
73 self.lines.push(line);
74 }
75 }
76
77 fn apply_structure(&mut self, mut line: ClassifiedLine) -> ClassifiedLine {
78 line.context = match self.state.clone() {
79 DocumentState::Start => self.apply_start_structure(&mut line),
80 DocumentState::FileHeader { start_line } => {
81 self.apply_file_header_structure(&mut line, start_line)
82 }
83 DocumentState::BetweenBlocks => self.apply_between_blocks_structure(&mut line),
84 DocumentState::FreeText { start_line } => {
85 self.apply_free_text_structure(&mut line, start_line)
86 }
87 DocumentState::TuneHeader { tune } => self.apply_tune_header_structure(&mut line, tune),
88 DocumentState::TuneBody { tune } => self.apply_tune_body_structure(&mut line, tune),
89 };
90
91 if line.kind != LineKind::MusicCode {
92 line.score_line_break = ScoreLineBreak::NotApplicable;
93 }
94
95 line
96 }
97
98 fn apply_start_structure(&mut self, line: &mut ClassifiedLine) -> LineContext {
99 match line.kind {
100 LineKind::VersionLine | LineKind::EmptyLine | LineKind::Comment => {
101 LineContext::Preamble
102 }
103 LineKind::InformationField if line.field_code() == Some('X') => self.start_tune(line),
104 LineKind::InformationField
105 | LineKind::FieldContinuation
106 | LineKind::StylesheetDirective => {
107 self.state = DocumentState::FileHeader {
108 start_line: line.index,
109 };
110 LineContext::FileHeader
111 }
112 LineKind::TypesetTextDirective => {
113 self.state = DocumentState::BetweenBlocks;
114 LineContext::TypesetText
115 }
116 LineKind::FreeText | LineKind::MusicCode => {
117 line.kind = LineKind::FreeText;
118 self.state = DocumentState::FreeText {
119 start_line: line.index,
120 };
121 LineContext::FreeText
122 }
123 }
124 }
125
126 fn apply_file_header_structure(
127 &mut self,
128 line: &mut ClassifiedLine,
129 start_line: usize,
130 ) -> LineContext {
131 if line.kind == LineKind::EmptyLine && self.is_synthetic_final_empty_line(line) {
132 self.close_file_header(start_line, line.index);
133 self.state = DocumentState::BetweenBlocks;
134 return LineContext::BetweenBlocks;
135 }
136
137 match line.kind {
138 LineKind::EmptyLine => {
139 self.close_file_header(start_line, line.index);
140 self.state = DocumentState::BetweenBlocks;
141 LineContext::BetweenBlocks
142 }
143 LineKind::InformationField if line.field_code() == Some('X') => {
144 self.close_file_header(start_line, line.index);
145 self.start_tune(line)
146 }
147 LineKind::InformationField
148 | LineKind::FieldContinuation
149 | LineKind::StylesheetDirective
150 | LineKind::Comment => LineContext::FileHeader,
151 LineKind::TypesetTextDirective => {
152 self.close_file_header(start_line, line.index);
153 self.state = DocumentState::BetweenBlocks;
154 LineContext::TypesetText
155 }
156 LineKind::VersionLine => LineContext::FileHeader,
157 LineKind::FreeText | LineKind::MusicCode => {
158 self.close_file_header(start_line, line.index);
159 line.kind = LineKind::FreeText;
160 self.state = DocumentState::FreeText {
161 start_line: line.index,
162 };
163 LineContext::FreeText
164 }
165 }
166 }
167
168 fn apply_between_blocks_structure(&mut self, line: &mut ClassifiedLine) -> LineContext {
169 match line.kind {
170 LineKind::InformationField if line.field_code() == Some('X') => self.start_tune(line),
171 LineKind::TypesetTextDirective => LineContext::TypesetText,
172 LineKind::FreeText | LineKind::MusicCode => {
173 line.kind = LineKind::FreeText;
174 self.state = DocumentState::FreeText {
175 start_line: line.index,
176 };
177 LineContext::FreeText
178 }
179 LineKind::VersionLine
180 | LineKind::EmptyLine
181 | LineKind::Comment
182 | LineKind::StylesheetDirective
183 | LineKind::InformationField
184 | LineKind::FieldContinuation => LineContext::BetweenBlocks,
185 }
186 }
187
188 fn apply_free_text_structure(
189 &mut self,
190 line: &mut ClassifiedLine,
191 start_line: usize,
192 ) -> LineContext {
193 if line.kind == LineKind::EmptyLine && self.is_synthetic_final_empty_line(line) {
194 self.close_free_text_block(start_line, line.index);
195 self.state = DocumentState::BetweenBlocks;
196 return LineContext::BetweenBlocks;
197 }
198
199 match line.kind {
200 LineKind::EmptyLine => {
201 self.close_free_text_block(start_line, line.index);
202 self.state = DocumentState::BetweenBlocks;
203 LineContext::FreeText
204 }
205 LineKind::InformationField if line.field_code() == Some('X') => {
206 self.close_free_text_block(start_line, line.index);
207 self.start_tune(line)
208 }
209 LineKind::TypesetTextDirective => {
210 self.close_free_text_block(start_line, line.index);
211 self.state = DocumentState::BetweenBlocks;
212 LineContext::TypesetText
213 }
214 LineKind::FreeText | LineKind::MusicCode => {
215 line.kind = LineKind::FreeText;
216 LineContext::FreeText
217 }
218 LineKind::VersionLine
219 | LineKind::Comment
220 | LineKind::StylesheetDirective
221 | LineKind::InformationField
222 | LineKind::FieldContinuation => LineContext::FreeText,
223 }
224 }
225
226 fn apply_tune_header_structure(
227 &mut self,
228 line: &mut ClassifiedLine,
229 mut tune: OpenTune,
230 ) -> LineContext {
231 if line.kind == LineKind::EmptyLine && self.is_synthetic_final_empty_line(line) {
232 self.close_tune(tune, line.index, None);
233 self.state = DocumentState::BetweenBlocks;
234 return LineContext::BetweenBlocks;
235 }
236
237 match line.kind {
238 LineKind::EmptyLine => {
239 let tune_index = tune.index;
240 self.close_tune(tune, line.index, Some(line.index));
241 self.state = DocumentState::BetweenBlocks;
242 LineContext::TuneTerminator { tune_index }
243 }
244 LineKind::InformationField
245 if line.field_code() == Some('X') && line.index != tune.start_line =>
246 {
247 self.close_tune(tune, line.index, None);
248 self.start_tune(line)
249 }
250 LineKind::InformationField if line.field_code() == Some('K') => {
251 let tune_index = tune.index;
252 tune.body_start_line = Some(line.index + 1);
253 self.state = DocumentState::TuneBody { tune };
254 LineContext::TuneHeader { tune_index }
255 }
256 LineKind::FreeText | LineKind::MusicCode => {
257 line.kind = LineKind::FreeText;
258 let tune_index = tune.index;
259 self.state = DocumentState::TuneHeader { tune };
260 LineContext::TuneHeader { tune_index }
261 }
262 LineKind::VersionLine
263 | LineKind::Comment
264 | LineKind::StylesheetDirective
265 | LineKind::InformationField
266 | LineKind::FieldContinuation
267 | LineKind::TypesetTextDirective => {
268 let tune_index = tune.index;
269 self.state = DocumentState::TuneHeader { tune };
270 LineContext::TuneHeader { tune_index }
271 }
272 }
273 }
274
275 fn apply_tune_body_structure(
276 &mut self,
277 line: &mut ClassifiedLine,
278 tune: OpenTune,
279 ) -> LineContext {
280 if line.kind == LineKind::EmptyLine && self.is_synthetic_final_empty_line(line) {
281 self.close_tune(tune, line.index, None);
282 self.pending_music_continuation = None;
283 self.state = DocumentState::BetweenBlocks;
284 return LineContext::BetweenBlocks;
285 }
286
287 match line.kind {
288 LineKind::EmptyLine => {
289 let tune_index = tune.index;
290 self.close_tune(tune, line.index, Some(line.index));
291 self.pending_music_continuation = None;
292 self.state = DocumentState::BetweenBlocks;
293 LineContext::TuneTerminator { tune_index }
294 }
295 LineKind::FreeText | LineKind::MusicCode => {
296 line.kind = LineKind::MusicCode;
297 let tune_index = tune.index;
298 self.state = DocumentState::TuneBody { tune };
299 LineContext::TuneBody { tune_index }
300 }
301 LineKind::VersionLine
302 | LineKind::Comment
303 | LineKind::StylesheetDirective
304 | LineKind::InformationField
305 | LineKind::FieldContinuation
306 | LineKind::TypesetTextDirective => {
307 let tune_index = tune.index;
308 self.state = DocumentState::TuneBody { tune };
309 LineContext::TuneBody { tune_index }
310 }
311 }
312 }
313
314 fn start_tune(&mut self, line: &ClassifiedLine) -> LineContext {
315 let tune_index = self.next_tune_index;
316 self.next_tune_index += 1;
317 self.pending_music_continuation = None;
318 self.state = DocumentState::TuneHeader {
319 tune: OpenTune {
320 index: tune_index,
321 start_line: line.index,
322 body_start_line: None,
323 },
324 };
325 LineContext::TuneHeader { tune_index }
326 }
327
328 fn close_file_header(&mut self, start_line: usize, end_line: usize) {
329 if start_line >= end_line {
330 return;
331 }
332
333 self.blocks.push(SourceBlock {
334 kind: SourceBlockKind::FileHeader,
335 span: self.line_range_span(start_line, end_line),
336 line_start: start_line,
337 line_end: end_line,
338 tune_index: None,
339 });
340 }
341
342 fn close_free_text_block(&mut self, start_line: usize, end_line: usize) {
343 if start_line >= end_line {
344 return;
345 }
346
347 self.blocks.push(SourceBlock {
348 kind: SourceBlockKind::FreeText,
349 span: self.line_range_span(start_line, end_line),
350 line_start: start_line,
351 line_end: end_line,
352 tune_index: None,
353 });
354 }
355
356 fn close_tune(&mut self, tune: OpenTune, end_line: usize, terminator_line: Option<usize>) {
357 if tune.start_line >= end_line {
358 return;
359 }
360
361 let body_start_line = tune.body_start_line.unwrap_or(end_line);
362 let header_line_end = body_start_line.min(end_line);
363 let body_line_start = body_start_line.min(end_line);
364 let span = self.line_range_span(tune.start_line, end_line);
365 let header_span = self.line_range_span(tune.start_line, header_line_end);
366 let body_span = self.line_range_span(body_line_start, end_line);
367
368 self.blocks.push(SourceBlock {
369 kind: SourceBlockKind::Tune,
370 span,
371 line_start: tune.start_line,
372 line_end: end_line,
373 tune_index: Some(tune.index),
374 });
375 self.tunes.push(TuneBlock {
376 index: tune.index,
377 span,
378 header_span,
379 body_span,
380 line_start: tune.start_line,
381 line_end: end_line,
382 header_line_start: tune.start_line,
383 header_line_end,
384 body_line_start,
385 body_line_end: end_line,
386 terminator_line,
387 });
388 }
389
390 fn track_typeset_block(&mut self, line: &ClassifiedLine) {
391 if line.kind == LineKind::TypesetTextDirective {
392 if self.typeset_text_block_start.is_none() {
393 self.typeset_text_block_start = Some(line.index);
394 }
395 } else if let Some(start_line) = self.typeset_text_block_start.take() {
396 self.close_typeset_text_block(start_line, line.index);
397 }
398 }
399
400 fn close_typeset_text_block(&mut self, start_line: usize, end_line: usize) {
401 if start_line >= end_line {
402 return;
403 }
404
405 self.blocks.push(SourceBlock {
406 kind: SourceBlockKind::TypesetText,
407 span: self.line_range_span(start_line, end_line),
408 line_start: start_line,
409 line_end: end_line,
410 tune_index: tune_index_for_context(self.lines[start_line].context),
411 });
412 }
413
414 fn track_non_note_items(&mut self, line: &ClassifiedLine) {
415 if let Some(kind) = non_note_kind_for_line(line.kind) {
416 self.non_note_items.push(NonNoteItem {
417 kind,
418 line_index: line.index,
419 span: non_note_span(line),
420 });
421 }
422
423 if let Some(span) = line.trailing_comment {
424 self.non_note_items.push(NonNoteItem {
425 kind: NonNoteKind::InlineComment,
426 line_index: line.index,
427 span,
428 });
429 }
430 }
431
432 fn track_field_continuation(&mut self, line: &ClassifiedLine) {
433 if line.kind == LineKind::FieldContinuation {
434 if let (Some(from_line), Some(marker_span)) =
435 (self.previous_field_line, line.marker_span)
436 {
437 let from_end = self
438 .lines
439 .get(from_line)
440 .map(|line| line.text_span.end)
441 .unwrap_or(marker_span.start);
442 self.continuation_edges.push(ContinuationEdge {
443 kind: ContinuationKind::FieldContinuation,
444 from_line,
445 to_line: line.index,
446 span: Span::new(from_end, marker_span.end),
447 marker_span,
448 });
449 }
450 self.previous_field_line = Some(line.index);
451 return;
452 }
453
454 if matches!(line.kind, LineKind::InformationField) {
455 self.previous_field_line = Some(line.index);
456 } else if !matches!(
457 line.kind,
458 LineKind::Comment | LineKind::StylesheetDirective | LineKind::TypesetTextDirective
459 ) {
460 self.previous_field_line = None;
461 }
462 }
463
464 fn track_music_continuation(&mut self, line: &ClassifiedLine) {
465 if line.kind == LineKind::MusicCode {
466 if let Some(pending) = self.pending_music_continuation.take() {
467 self.continuation_edges.push(ContinuationEdge {
468 kind: ContinuationKind::MusicBackslash,
469 from_line: pending.from_line,
470 to_line: line.index,
471 span: Span::new(pending.marker_span.start, line.content_span.start),
472 marker_span: pending.marker_span,
473 });
474 }
475
476 self.pending_music_continuation = match line.score_line_break {
477 ScoreLineBreak::Suppressed { marker_span } => Some(PendingMusicContinuation {
478 from_line: line.index,
479 marker_span,
480 }),
481 ScoreLineBreak::NotApplicable | ScoreLineBreak::Physical => None,
482 };
483 }
484 }
485
486 fn update_begintext_state(&mut self, line: &ClassifiedLine, text: &str) {
487 if line.kind != LineKind::TypesetTextDirective {
488 return;
489 }
490
491 let Some(name) = directive_name(text) else {
492 return;
493 };
494
495 if name.eq_ignore_ascii_case("begintext") {
496 self.in_begintext_block = true;
497 } else if name.eq_ignore_ascii_case("endtext") {
498 self.in_begintext_block = false;
499 }
500 }
501
502 fn finish(mut self) -> LineMap {
503 match self.state.clone() {
504 DocumentState::FileHeader { start_line } => {
505 self.close_file_header(start_line, self.source.line_count());
506 }
507 DocumentState::FreeText { start_line } => {
508 self.close_free_text_block(start_line, self.source.line_count());
509 }
510 DocumentState::TuneHeader { tune } | DocumentState::TuneBody { tune } => {
511 self.close_tune(tune, self.source.line_count(), None);
512 }
513 DocumentState::Start | DocumentState::BetweenBlocks => {}
514 }
515
516 if let Some(start_line) = self.typeset_text_block_start.take() {
517 self.close_typeset_text_block(start_line, self.source.line_count());
518 }
519
520 LineMap {
521 lines: self.lines,
522 blocks: self.blocks,
523 tunes: self.tunes,
524 continuation_edges: self.continuation_edges,
525 non_note_items: self.non_note_items,
526 }
527 }
528
529 fn line_range_span(&self, start_line: usize, end_line: usize) -> Span {
530 if start_line >= end_line {
531 let offset = self
532 .source
533 .line(start_line)
534 .map(|line| line.start())
535 .unwrap_or_else(|| self.source.len());
536 return Span::new(offset, offset);
537 }
538
539 let start = self
540 .source
541 .line(start_line)
542 .map(|line| line.start())
543 .unwrap_or_else(|| self.source.len());
544 let end = self
545 .source
546 .line(end_line - 1)
547 .map(|line| line.end())
548 .unwrap_or(start);
549 Span::new(start, end)
550 }
551
552 fn is_synthetic_final_empty_line(&self, line: &ClassifiedLine) -> bool {
553 line.index + 1 == self.source.line_count()
554 && line.text_span.is_empty()
555 && line.span.is_empty()
556 && line.text_span.start == self.source.len()
557 && self.source.len() > self.source.content_start()
558 }
559}
560
561#[derive(Debug, Clone, PartialEq, Eq)]
562enum DocumentState {
563 Start,
564 FileHeader { start_line: usize },
565 BetweenBlocks,
566 FreeText { start_line: usize },
567 TuneHeader { tune: OpenTune },
568 TuneBody { tune: OpenTune },
569}
570
571#[derive(Debug, Clone, PartialEq, Eq)]
572struct OpenTune {
573 index: usize,
574 start_line: usize,
575 body_start_line: Option<usize>,
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579struct PendingMusicContinuation {
580 from_line: usize,
581 marker_span: Span,
582}
583
584impl ClassifiedLine {
585 pub fn field_code(&self) -> Option<char> {
586 self.field.map(|field| field.code)
587 }
588}
589
590fn classify_raw_line(
591 index: usize,
592 span: Span,
593 text_span: Span,
594 text: &str,
595 in_begintext_block: bool,
596) -> ClassifiedLine {
597 let content_start = trimmed_start_offset(text);
598 let content = &text[content_start..];
599 let content_span = Span::new(text_span.start + content_start, text_span.end);
600 let trailing_comment = trailing_comment_span(text, text_span.start);
601
602 let mut line = ClassifiedLine {
603 index,
604 kind: LineKind::FreeText,
605 context: LineContext::BetweenBlocks,
606 span,
607 text_span,
608 content_span,
609 marker_span: None,
610 field: None,
611 trailing_comment,
612 score_line_break: ScoreLineBreak::NotApplicable,
613 };
614
615 if content.trim_end().is_empty() {
616 line.kind = LineKind::EmptyLine;
617 line.content_span = Span::new(text_span.end, text_span.end);
618 line.trailing_comment = None;
619 return line;
620 }
621
622 if in_begintext_block {
623 line.kind = LineKind::TypesetTextDirective;
624 line.marker_span = content
625 .strip_prefix("%%")
626 .map(|_| Span::new(line.content_span.start, line.content_span.start + 2));
627 return line;
628 }
629
630 if is_version_line(content) {
631 line.kind = LineKind::VersionLine;
632 line.marker_span = Some(Span::new(
633 line.content_span.start,
634 line.content_span.start + 4,
635 ));
636 line.trailing_comment = None;
637 return line;
638 }
639
640 if content.starts_with("%%") {
641 line.marker_span = Some(Span::new(
642 line.content_span.start,
643 line.content_span.start + 2,
644 ));
645 line.kind = if is_typeset_text_directive(content) {
646 LineKind::TypesetTextDirective
647 } else {
648 LineKind::StylesheetDirective
649 };
650 line.trailing_comment = None;
651 return line;
652 }
653
654 if content.starts_with('%') {
655 line.kind = LineKind::Comment;
656 line.marker_span = Some(Span::new(
657 line.content_span.start,
658 line.content_span.start + 1,
659 ));
660 line.trailing_comment = None;
661 return line;
662 }
663
664 if content.starts_with("+:") {
665 line.kind = LineKind::FieldContinuation;
666 line.marker_span = Some(Span::new(
667 line.content_span.start,
668 line.content_span.start + 2,
669 ));
670 return line;
671 }
672
673 if let Some(field) = parse_field_header(content, line.content_span.start, text_span.end) {
674 line.kind = LineKind::InformationField;
675 line.marker_span = Some(field.marker_span);
676 line.field = Some(field);
677 return line;
678 }
679
680 line.score_line_break = music_score_line_break(text, text_span.start);
681 line
682}
683
684fn parse_field_header(
685 content: &str,
686 content_offset: usize,
687 text_end: usize,
688) -> Option<FieldHeader> {
689 let mut chars = content.chars();
690 let code = chars.next()?;
691 if !code.is_ascii_alphabetic() || chars.next()? != ':' {
692 return None;
693 }
694
695 let marker_end = content_offset + code.len_utf8() + 1;
696 Some(FieldHeader {
697 code,
698 marker_span: Span::new(content_offset, marker_end),
699 value_span: Span::new(marker_end, text_end),
700 })
701}
702
703fn trimmed_start_offset(text: &str) -> usize {
704 text.len() - text.trim_start().len()
705}
706
707fn trailing_comment_span(text: &str, line_offset: usize) -> Option<Span> {
708 let comment_start = text.char_indices().find_map(|(offset, ch)| {
709 if ch == '%' && !is_escaped(text, offset) {
710 Some(offset)
711 } else {
712 None
713 }
714 })?;
715
716 Some(Span::new(
717 line_offset + comment_start,
718 line_offset + text.len(),
719 ))
720}
721
722fn music_score_line_break(text: &str, line_offset: usize) -> ScoreLineBreak {
723 let code_end = trailing_comment_span(text, 0)
724 .map(|span| span.start)
725 .unwrap_or(text.len());
726 let code = text[..code_end].trim_end();
727 let Some(last) = code.strip_suffix('\\') else {
728 return ScoreLineBreak::Physical;
729 };
730 let marker_start = line_offset + last.len();
731 ScoreLineBreak::Suppressed {
732 marker_span: Span::new(marker_start, marker_start + 1),
733 }
734}
735
736fn directive_name(text: &str) -> Option<&str> {
737 let content = text.trim_start();
738 let rest = content.strip_prefix("%%")?;
739 let name_end = rest
740 .find(|ch: char| ch.is_whitespace())
741 .unwrap_or(rest.len());
742 Some(&rest[..name_end])
743}
744
745fn is_typeset_text_directive(content: &str) -> bool {
746 let Some(name) = directive_name(content) else {
747 return false;
748 };
749
750 matches!(
751 name.to_ascii_lowercase().as_str(),
752 "text" | "center" | "begintext" | "endtext"
753 )
754}
755
756fn is_version_line(content: &str) -> bool {
757 let Some(rest) = content.strip_prefix("%abc") else {
758 return false;
759 };
760 rest.is_empty()
761 || rest
762 .chars()
763 .next()
764 .map(|ch| ch == '-' || ch.is_whitespace())
765 .unwrap_or(true)
766}
767
768fn non_note_kind_for_line(kind: LineKind) -> Option<NonNoteKind> {
769 match kind {
770 LineKind::VersionLine => Some(NonNoteKind::VersionLine),
771 LineKind::EmptyLine => Some(NonNoteKind::EmptyLine),
772 LineKind::Comment => Some(NonNoteKind::Comment),
773 LineKind::StylesheetDirective => Some(NonNoteKind::StylesheetDirective),
774 LineKind::InformationField => Some(NonNoteKind::InformationField),
775 LineKind::FieldContinuation => Some(NonNoteKind::FieldContinuation),
776 LineKind::FreeText => Some(NonNoteKind::FreeText),
777 LineKind::TypesetTextDirective => Some(NonNoteKind::TypesetTextDirective),
778 LineKind::MusicCode => None,
779 }
780}
781
782fn non_note_span(line: &ClassifiedLine) -> Span {
783 if line.kind == LineKind::EmptyLine {
784 line.text_span
785 } else {
786 line.content_span
787 }
788}
789
790fn tune_index_for_context(context: LineContext) -> Option<usize> {
791 match context {
792 LineContext::TuneHeader { tune_index }
793 | LineContext::TuneBody { tune_index }
794 | LineContext::TuneTerminator { tune_index } => Some(tune_index),
795 LineContext::Preamble
796 | LineContext::FileHeader
797 | LineContext::BetweenBlocks
798 | LineContext::FreeText
799 | LineContext::TypesetText => None,
800 }
801}
802
803pub(crate) fn tokenize_surface(source: &SourceText, line_map: &LineMap) -> Vec<SurfaceToken> {
804 let mut tokens = Vec::new();
805
806 for line in &line_map.lines {
807 match line.kind {
808 LineKind::InformationField => {
809 if let Some(field) = line.field {
810 tokens.push(SurfaceToken {
811 kind: SurfaceKind::Field,
812 span: field.marker_span,
813 });
814 }
815 }
816 LineKind::FieldContinuation => {
817 if let Some(marker_span) = line.marker_span {
818 tokens.push(SurfaceToken {
819 kind: SurfaceKind::FieldContinuation,
820 span: marker_span,
821 });
822 }
823 }
824 LineKind::Comment => tokens.push(SurfaceToken {
825 kind: SurfaceKind::Comment,
826 span: line.content_span,
827 }),
828 LineKind::StylesheetDirective | LineKind::TypesetTextDirective => {
829 tokens.push(SurfaceToken {
830 kind: SurfaceKind::Directive,
831 span: line.content_span,
832 });
833 }
834 LineKind::FreeText => tokens.push(SurfaceToken {
835 kind: SurfaceKind::FreeText,
836 span: line.content_span,
837 }),
838 LineKind::MusicCode => {
839 if let Some(text) = source.slice(line.text_span) {
840 tokenize_music_line(text, line.text_span.start, &mut tokens);
841 }
842 }
843 LineKind::VersionLine | LineKind::EmptyLine => {}
844 }
845
846 if line.kind != LineKind::MusicCode
847 && let Some(span) = line.trailing_comment
848 {
849 tokens.push(SurfaceToken {
850 kind: SurfaceKind::Comment,
851 span,
852 });
853 }
854 }
855
856 tokens
857}
858
859fn tokenize_music_line(line: &str, line_offset: usize, tokens: &mut Vec<SurfaceToken>) {
860 for (column, ch) in line.char_indices() {
861 let kind = match ch {
862 '%' if !is_escaped(line, column) => {
863 tokens.push(SurfaceToken {
864 kind: SurfaceKind::Comment,
865 span: Span::new(line_offset + column, line_offset + line.len()),
866 });
867 break;
868 }
869 '|' => SurfaceKind::Barline,
870 'A'..='G' | 'a'..='g' => SurfaceKind::Note,
871 'z' | 'x' => SurfaceKind::Rest,
872 _ => SurfaceKind::Other,
873 };
874 if kind != SurfaceKind::Other {
875 tokens.push(SurfaceToken {
876 kind,
877 span: Span::new(line_offset + column, line_offset + column + ch.len_utf8()),
878 });
879 }
880 }
881}