1#![deny(clippy::correctness)]
2#![doc = include_str!("../README.md")]
3
4use serde::{Deserialize, Serialize};
5use std::num::NonZeroU8;
6use tsify_next::Tsify;
7use wasm_bindgen::prelude::*;
8
9pub(crate) mod arrangement;
10pub(crate) mod error;
11pub(crate) mod guitar;
12pub(crate) mod parser;
13pub(crate) mod pitch;
14pub(crate) mod renderer;
15pub(crate) mod string_number;
16
17pub use arrangement::{Arrangement, BeatVec, Line, create_arrangements};
21pub use error::{ParseError, TabError, UnplayablePitch};
22pub use guitar::{Guitar, PitchFingering, create_string_tuning};
23pub use parser::{TuningName, get_tuning_names, parse_lines};
24pub use pitch::Pitch;
25pub use renderer::render_tab;
26pub use string_number::StringNumber;
27
28#[doc(hidden)]
41pub mod __bench_internals {
42 pub use crate::arrangement::memoized_original_create_arrangements;
43 pub use crate::parser::{
44 create_string_tuning_offset, memoized_original_parse_lines, parse_tuning,
45 };
46}
47
48#[derive(Debug, Clone, Deserialize, Tsify)]
54#[tsify(from_wasm_abi)]
55#[serde(rename_all = "camelCase")]
56#[non_exhaustive]
57pub struct TabInput {
58 pub input: String,
59 pub tuning_name: String,
64 pub guitar_num_frets: u8,
65 pub guitar_capo: u8,
66 pub num_arrangements: u8,
67 #[tsify(optional)]
70 pub max_fret_span_filter: Option<u8>,
71}
72
73impl TabInput {
74 #[must_use]
79 pub fn new(
80 input: impl Into<String>,
81 tuning_name: impl Into<String>,
82 guitar_num_frets: u8,
83 guitar_capo: u8,
84 num_arrangements: u8,
85 ) -> Self {
86 Self {
87 input: input.into(),
88 tuning_name: tuning_name.into(),
89 guitar_num_frets,
90 guitar_capo,
91 num_arrangements,
92 max_fret_span_filter: None,
93 }
94 }
95
96 #[must_use]
98 pub fn with_max_fret_span_filter(mut self, filter: u8) -> Self {
99 self.max_fret_span_filter = Some(filter);
100 self
101 }
102}
103
104#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
110pub struct NumArrangements(NonZeroU8);
111
112impl NumArrangements {
113 pub const MAX: u8 = 20;
115
116 pub fn try_new(n: u8) -> Result<Self, TabError> {
122 match NonZeroU8::new(n) {
123 Some(nz) if n <= Self::MAX => Ok(Self(nz)),
124 _ => Err(TabError::NumArrangementsOutOfRange {
125 value: n,
126 max: Self::MAX,
127 }),
128 }
129 }
130
131 #[inline]
133 #[must_use]
134 pub fn get(self) -> u8 {
135 self.0.get()
136 }
137}
138
139impl From<NumArrangements> for u8 {
140 fn from(n: NumArrangements) -> Self {
141 n.get()
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Tsify)]
150#[tsify(into_wasm_abi)]
151#[serde(tag = "kind", rename_all = "camelCase")]
152pub enum NormalizedBeat {
153 Playable { pitches: Vec<String> },
154 Rest,
155 MeasureBreak,
156}
157
158#[derive(Debug)]
164#[wasm_bindgen]
165pub struct ArrangementSet {
166 arrangements: Vec<arrangement::Arrangement>,
167 guitar: Guitar,
168 normalized_input: Vec<NormalizedBeat>,
169}
170
171#[wasm_bindgen]
177impl ArrangementSet {
178 #[wasm_bindgen(getter)]
181 #[must_use]
182 pub fn len(&self) -> usize {
183 self.arrangements.len()
184 }
185
186 #[wasm_bindgen(getter, js_name = "isEmpty")]
188 #[must_use]
189 pub fn is_empty(&self) -> bool {
190 self.arrangements.is_empty()
191 }
192
193 #[wasm_bindgen(getter, js_name = "normalizedInput")]
200 #[must_use]
201 pub fn normalized_input(&self) -> Vec<NormalizedBeat> {
202 self.normalized_input.clone()
203 }
204
205 #[wasm_bindgen(js_name = "maxFretSpan")]
211 pub fn max_fret_span(&self, index: usize) -> Result<u8, TabError> {
212 self.arrangements
213 .get(index)
214 .map(|a| a.max_fret_span())
215 .ok_or(TabError::IndexOutOfBounds {
216 index,
217 len: self.arrangements.len(),
218 })
219 }
220
221 pub fn difficulty(&self, index: usize) -> Result<i32, TabError> {
227 self.arrangements
228 .get(index)
229 .map(|a| a.difficulty())
230 .ok_or(TabError::IndexOutOfBounds {
231 index,
232 len: self.arrangements.len(),
233 })
234 }
235
236 pub fn render(
246 &self,
247 index: usize,
248 width: u16,
249 padding: u8,
250 playback: Option<u16>,
251 ) -> Result<String, TabError> {
252 let arrangement = self
253 .arrangements
254 .get(index)
255 .ok_or(TabError::IndexOutOfBounds {
256 index,
257 len: self.arrangements.len(),
258 })?;
259 let min = renderer::min_render_width(padding);
260 if width < min {
261 return Err(TabError::RenderWidthTooSmall { width, min });
262 }
263 Ok(renderer::render_tab(
264 &arrangement.lines,
265 &self.guitar,
266 width,
267 padding,
268 playback,
269 ))
270 }
271}
272
273#[wasm_bindgen(js_name = "generateArrangements")]
315pub fn generate_arrangements(tab_input: TabInput) -> Result<ArrangementSet, TabError> {
316 let num_arrangements = NumArrangements::try_new(tab_input.num_arrangements)?;
317
318 let input_lines = parser::parse_lines(tab_input.input.clone())?;
319
320 let tuning = parser::create_string_tuning_offset(parser::parse_tuning(&tab_input.tuning_name)?);
325 let guitar = Guitar::new(tuning, tab_input.guitar_num_frets, tab_input.guitar_capo)?;
326
327 let first_playable_index = arrangement::first_playable_index(&input_lines);
328
329 let normalized_input: Vec<NormalizedBeat> = input_lines
330 .iter()
331 .skip(first_playable_index)
332 .map(|line| match line {
333 arrangement::Line::Playable(pitches) => NormalizedBeat::Playable {
334 pitches: pitches.iter().map(|p| p.plain_text().to_owned()).collect(),
335 },
336 arrangement::Line::Rest => NormalizedBeat::Rest,
337 arrangement::Line::MeasureBreak => NormalizedBeat::MeasureBreak,
338 })
339 .collect();
340
341 let arrangements = arrangement::create_arrangements(
342 guitar.clone(),
343 input_lines,
344 num_arrangements,
345 tab_input.max_fret_span_filter,
346 )?;
347
348 Ok(ArrangementSet {
349 arrangements,
350 guitar,
351 normalized_input,
352 })
353}
354
355#[cfg(test)]
356mod test_generate_arrangements_and_render {
357 use super::*;
358
359 #[test]
360 fn valid_input() {
361 let tab_input = TabInput {
362 input: "E2\nA2\nD3\n\nG3\nB3\n---\nE4".to_owned(),
363 tuning_name: "standard".to_owned(),
364 guitar_num_frets: 20,
365 guitar_capo: 0,
366 num_arrangements: 1,
367 max_fret_span_filter: None,
368 };
369 let set = generate_arrangements(tab_input).unwrap();
370
371 assert_eq!(set.len(), 1);
372 assert_eq!(set.max_fret_span(0).unwrap(), 0);
373
374 let tab = set.render(0, 30, 2, Some(3)).unwrap();
375 assert_eq!(
376 tab,
377 " \u{25bc}\n--------------------|--0------\n-----------------0--|---------\n--------------0-----|---------\n--------0-----------|---------\n-----0--------------|---------\n--0-----------------|---------\n \u{25b2}\n"
378 );
379
380 let beats = set.normalized_input();
381 assert_eq!(beats.len(), 8);
382 assert!(
383 matches!(beats[0], NormalizedBeat::Playable { ref pitches } if pitches == &["E2".to_owned()])
384 );
385 assert!(matches!(beats[3], NormalizedBeat::Rest));
386 assert!(matches!(beats[6], NormalizedBeat::MeasureBreak));
387 }
388
389 #[test]
390 fn empty_input_returns_set_with_requested_count() {
391 let tab_input = TabInput {
392 input: "\n\n\n---\n \n".to_owned(),
393 tuning_name: "standard".to_owned(),
394 guitar_num_frets: 20,
395 guitar_capo: 0,
396 num_arrangements: 2,
397 max_fret_span_filter: None,
398 };
399 let set = generate_arrangements(tab_input).unwrap();
400 assert_eq!(set.len(), 2);
401 assert_eq!(set.render(0, 30, 2, Some(3)).unwrap(), "");
402 assert_eq!(set.render(1, 30, 2, Some(3)).unwrap(), "");
403
404 let beats = set.normalized_input();
410 assert!(
411 beats
412 .iter()
413 .all(|b| matches!(b, NormalizedBeat::Rest | NormalizedBeat::MeasureBreak))
414 );
415 assert!(
416 beats
417 .iter()
418 .any(|b| matches!(b, NormalizedBeat::MeasureBreak))
419 );
420 }
421
422 #[test]
423 fn invalid_input_returns_parse_error() {
424 let tab_input = TabInput {
425 input: "E2\nA2\nD3\n???\nG3\nB3\nE4".to_owned(),
426 tuning_name: "standard".to_owned(),
427 guitar_num_frets: 20,
428 guitar_capo: 0,
429 num_arrangements: 1,
430 max_fret_span_filter: None,
431 };
432 let err = generate_arrangements(tab_input).unwrap_err();
433 match err {
434 TabError::Parse { errors } => {
435 assert_eq!(errors.len(), 1);
436 assert_eq!(errors[0].line, 4);
437 assert_eq!(errors[0].text, "???");
438 }
439 other => panic!("expected Parse, got {other:?}"),
440 }
441 }
442
443 #[test]
444 fn num_arrangements_zero_is_invalid() {
445 let tab_input = TabInput {
446 input: "E2".to_owned(),
447 tuning_name: "standard".to_owned(),
448 guitar_num_frets: 20,
449 guitar_capo: 0,
450 num_arrangements: 0,
451 max_fret_span_filter: None,
452 };
453 let err = generate_arrangements(tab_input).unwrap_err();
454 match err {
455 TabError::NumArrangementsOutOfRange { value, max } => {
456 assert_eq!(value, 0);
457 assert_eq!(max, 20);
458 }
459 other => panic!("expected NumArrangementsOutOfRange, got {other:?}"),
460 }
461 }
462
463 #[test]
464 fn num_arrangements_above_cap_is_invalid() {
465 let tab_input = TabInput {
466 input: "E2".to_owned(),
467 tuning_name: "standard".to_owned(),
468 guitar_num_frets: 20,
469 guitar_capo: 0,
470 num_arrangements: 21,
471 max_fret_span_filter: None,
472 };
473 let err = generate_arrangements(tab_input).unwrap_err();
474 match err {
475 TabError::NumArrangementsOutOfRange { value, max } => {
476 assert_eq!(value, 21);
477 assert_eq!(max, 20);
478 }
479 other => panic!("expected NumArrangementsOutOfRange, got {other:?}"),
480 }
481 }
482
483 #[test]
484 fn invalid_guitar_config_returns_num_frets_too_high() {
485 let tab_input = TabInput {
486 input: "E2".to_owned(),
487 tuning_name: "standard".to_owned(),
488 guitar_num_frets: 31, guitar_capo: 0,
490 num_arrangements: 1,
491 max_fret_span_filter: None,
492 };
493 let err = generate_arrangements(tab_input).unwrap_err();
494 match err {
495 TabError::NumFretsTooHigh { num_frets, max } => {
496 assert_eq!(num_frets, 31);
497 assert_eq!(max, 30);
498 }
499 other => panic!("expected NumFretsTooHigh, got {other:?}"),
500 }
501 }
502
503 #[test]
504 fn unreachable_pitch_returns_unplayable_pitches() {
505 let tab_input = TabInput {
506 input: "A1".to_owned(), tuning_name: "standard".to_owned(),
508 guitar_num_frets: 20,
509 guitar_capo: 0,
510 num_arrangements: 1,
511 max_fret_span_filter: None,
512 };
513 let err = generate_arrangements(tab_input).unwrap_err();
514 match err {
515 TabError::UnplayablePitches { pitches } => {
516 assert_eq!(pitches.len(), 1);
517 assert_eq!(pitches[0].value, "A1");
518 assert_eq!(pitches[0].line, 1);
519 }
520 other => panic!("expected UnplayablePitches, got {other:?}"),
521 }
522 }
523
524 #[test]
525 fn unplayable_pitch_line_accounts_for_leading_rests() {
526 let tab_input = TabInput {
530 input: "\n\nA1".to_owned(),
531 tuning_name: "standard".to_owned(),
532 guitar_num_frets: 20,
533 guitar_capo: 0,
534 num_arrangements: 1,
535 max_fret_span_filter: None,
536 };
537 let err = generate_arrangements(tab_input).unwrap_err();
538 match err {
539 TabError::UnplayablePitches { pitches } => {
540 assert_eq!(pitches.len(), 1);
541 assert_eq!(pitches[0].value, "A1");
542 assert_eq!(pitches[0].line, 3);
543 }
544 other => panic!("expected UnplayablePitches, got {other:?}"),
545 }
546 }
547
548 #[test]
549 fn render_at_two_widths_produces_different_outputs() {
550 let tab_input = TabInput {
551 input: "E2\nA2\nD3".to_owned(),
552 tuning_name: "standard".to_owned(),
553 guitar_num_frets: 20,
554 guitar_capo: 0,
555 num_arrangements: 1,
556 max_fret_span_filter: None,
557 };
558 let set = generate_arrangements(tab_input).unwrap();
559 let narrow = set.render(0, 12, 1, None).unwrap();
560 let wide = set.render(0, 100, 1, None).unwrap();
561 assert_ne!(narrow, wide);
562 }
563}
564
565#[cfg(test)]
566mod test_boundary_types {
567 use super::*;
568
569 #[test]
570 fn arrangement_set_len_matches_num_arrangements() {
571 let set = arrangement_set_fixture(2);
572 assert_eq!(set.len(), 2);
573 }
574
575 #[test]
576 fn arrangement_set_normalized_input_is_tagged_variants() {
577 let set = arrangement_set_fixture(1);
578 let beats = set.normalized_input();
579 assert!(matches!(beats[0], NormalizedBeat::Playable { .. }));
580 }
581
582 #[test]
583 fn arrangement_set_render_returns_string_for_in_bounds_index() {
584 let set = arrangement_set_fixture(1);
585 let tab = set.render(0, 30, 2, None).unwrap();
586 assert!(!tab.is_empty());
587 }
588
589 #[test]
590 fn arrangement_set_render_rejects_out_of_bounds_index() {
591 let set = arrangement_set_fixture(1);
592 let err = set.render(99, 30, 2, None).unwrap_err();
593 match err {
594 TabError::IndexOutOfBounds { .. } => {}
595 other => panic!("expected IndexOutOfBounds, got {other:?}"),
596 }
597 }
598
599 #[test]
600 fn arrangement_set_render_rejects_width_below_minimum() {
601 let set = arrangement_set_fixture(1);
602 let err = set.render(0, 3, 1, None).unwrap_err();
604 assert_eq!(err, TabError::RenderWidthTooSmall { width: 3, min: 5 });
605 }
606
607 #[test]
608 fn arrangement_set_max_fret_span_returns_value_for_in_bounds_index() {
609 let set = arrangement_set_fixture(1);
610 let span = set.max_fret_span(0).unwrap();
611 assert_eq!(span, 0);
612 }
613
614 #[test]
615 fn arrangement_set_max_fret_span_rejects_out_of_bounds_index() {
616 let set = arrangement_set_fixture(1);
617 let err = set.max_fret_span(99).unwrap_err();
618 match err {
619 TabError::IndexOutOfBounds { .. } => {}
620 other => panic!("expected IndexOutOfBounds, got {other:?}"),
621 }
622 }
623
624 #[test]
625 fn arrangement_set_difficulty_returns_value_for_in_bounds_index() {
626 let set = arrangement_set_fixture(1);
627 assert_eq!(set.difficulty(0).unwrap(), 0);
630 }
631
632 #[test]
633 fn arrangement_set_difficulty_rejects_out_of_bounds_index() {
634 let set = arrangement_set_fixture(1);
635 let err = set.difficulty(99).unwrap_err();
636 match err {
637 TabError::IndexOutOfBounds { .. } => {}
638 other => panic!("expected IndexOutOfBounds, got {other:?}"),
639 }
640 }
641
642 #[test]
643 fn arrangement_set_is_empty_returns_false_for_non_empty_set() {
644 let set = arrangement_set_fixture(1);
645 assert!(!set.is_empty());
646 assert_eq!(set.len(), 1);
647 }
648
649 #[test]
650 fn arrangement_set_is_empty_returns_true_when_filter_drops_every_candidate() {
651 let tab_input = TabInput {
656 input: "C3E3".to_owned(),
657 tuning_name: "standard".to_owned(),
658 guitar_num_frets: 20,
659 guitar_capo: 0,
660 num_arrangements: 5,
661 max_fret_span_filter: Some(0),
662 };
663 let set = generate_arrangements(tab_input).unwrap();
664 assert!(set.is_empty());
665 assert_eq!(set.len(), 0);
666 }
667
668 #[test]
669 fn arrangement_set_render_rejects_index_when_filter_empties_the_set() {
670 let tab_input = TabInput {
674 input: "C3E3".to_owned(),
675 tuning_name: "standard".to_owned(),
676 guitar_num_frets: 20,
677 guitar_capo: 0,
678 num_arrangements: 5,
679 max_fret_span_filter: Some(0),
680 };
681 let set = generate_arrangements(tab_input).unwrap();
682 assert!(set.is_empty());
683 let err = set.render(0, 30, 2, None).unwrap_err();
684 assert!(
685 matches!(err, TabError::IndexOutOfBounds { index: 0, len: 0 }),
686 "got {err:?}"
687 );
688 }
689
690 fn arrangement_set_fixture(num_arrangements: u8) -> ArrangementSet {
691 let tab_input = TabInput {
692 input: "E2\nA2\nD3".to_owned(),
693 tuning_name: "standard".to_owned(),
694 guitar_num_frets: 20,
695 guitar_capo: 0,
696 num_arrangements,
697 max_fret_span_filter: None,
698 };
699 generate_arrangements(tab_input).unwrap()
700 }
701
702 #[test]
703 fn tab_input_deserializes_from_camelcase_json() {
704 let json = r#"{
705 "input": "E2\nA2",
706 "tuningName": "standard",
707 "guitarNumFrets": 18,
708 "guitarCapo": 0,
709 "numArrangements": 1,
710 "maxFretSpanFilter": null
711 }"#;
712 let input: TabInput = serde_json::from_str(json).unwrap();
713 assert_eq!(input.input, "E2\nA2");
714 assert_eq!(input.tuning_name, "standard");
715 assert_eq!(input.guitar_num_frets, 18);
716 assert_eq!(input.num_arrangements, 1);
717 assert!(input.max_fret_span_filter.is_none());
718 }
719
720 #[test]
721 fn normalized_beat_serializes_with_kind_discriminant() {
722 let playable = NormalizedBeat::Playable {
723 pitches: vec!["E2".to_owned()],
724 };
725 let json = serde_json::to_string(&playable).unwrap();
726 assert_eq!(json, r#"{"kind":"playable","pitches":["E2"]}"#);
727
728 let rest = NormalizedBeat::Rest;
729 let json = serde_json::to_string(&rest).unwrap();
730 assert_eq!(json, r#"{"kind":"rest"}"#);
731
732 let mb = NormalizedBeat::MeasureBreak;
733 let json = serde_json::to_string(&mb).unwrap();
734 assert_eq!(json, r#"{"kind":"measureBreak"}"#);
735 }
736}
737
738#[cfg(test)]
739mod test_num_arrangements {
740 use super::*;
741
742 #[test]
743 fn try_new_accepts_one_through_max() {
744 for n in 1u8..=NumArrangements::MAX {
745 assert!(NumArrangements::try_new(n).is_ok(), "n={n} must be Ok");
746 }
747 }
748
749 #[test]
750 fn try_new_rejects_zero_with_typed_variant() {
751 let err = NumArrangements::try_new(0).unwrap_err();
752 match err {
753 TabError::NumArrangementsOutOfRange { value, max } => {
754 assert_eq!(value, 0);
755 assert_eq!(max, 20);
756 }
757 other => panic!("expected NumArrangementsOutOfRange, got {other:?}"),
758 }
759 }
760
761 #[test]
762 fn try_new_rejects_above_max_with_typed_variant() {
763 let err = NumArrangements::try_new(NumArrangements::MAX + 1).unwrap_err();
764 match err {
765 TabError::NumArrangementsOutOfRange { value, max } => {
766 assert_eq!(value, 21);
767 assert_eq!(max, 20);
768 }
769 other => panic!("expected NumArrangementsOutOfRange, got {other:?}"),
770 }
771 }
772
773 #[test]
774 fn get_returns_inner_value() {
775 let n = NumArrangements::try_new(7).unwrap();
776 assert_eq!(n.get(), 7);
777 }
778}
779
780#[cfg(test)]
781mod test_tab_input {
782 use super::*;
783
784 #[test]
785 fn new_defaults_max_fret_span_filter_to_none() {
786 let input = TabInput::new("E2\nA2", "standard", 18, 0, 1);
787 assert_eq!(input.input, "E2\nA2");
788 assert_eq!(input.tuning_name, "standard");
789 assert_eq!(input.guitar_num_frets, 18);
790 assert_eq!(input.guitar_capo, 0);
791 assert_eq!(input.num_arrangements, 1);
792 assert_eq!(input.max_fret_span_filter, None);
793 }
794
795 #[test]
796 fn with_max_fret_span_filter_sets_some() {
797 let input = TabInput::new("E2", "standard", 18, 0, 1).with_max_fret_span_filter(5);
798 assert_eq!(input.max_fret_span_filter, Some(5));
799 }
800}