/
Parser.purs
1415 lines (1228 loc) · 36.5 KB
/
Parser.purs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- | An ABC Parser.
module Data.Abc.Parser
( parse
, parseKeySignature
) where
import Data.Abc
import Data.Abc.Meter as Meter
import Control.Alt ((<|>))
import Data.Array as Array
import Data.Either (Either(..))
import Data.Foldable (foldMap)
import Data.Functor (map)
import Data.Int (fromString, pow)
import Data.List (List(..), (:))
import Data.List (length) as L
import Data.List.NonEmpty as Nel
import Data.Map (Map, empty)
import Data.Map (fromFoldable) as Map
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Rational (Rational, fromInt, (%))
import Data.String (drop, toUpper, singleton)
import Data.String.CodePoints (codePointFromChar, length)
import Data.String.CodeUnits (charAt, fromCharArray, toCharArray)
import Data.String.Utils (startsWith, includes)
import Data.Tuple (Tuple(..))
import Data.Unfoldable1 (replicate1A)
import Prelude (bind, flip, join, max, pure, ($), (*>), (+), (-), (<$), (<$>), (<*), (<*>), (<<<), (<>), (==))
import StringParser (Parser, ParseError, runParser, try)
import StringParser.CodePoints (satisfy, string, alphaNum, char, eof, regex)
import StringParser.Combinators (between, choice, many, many1, manyTill, option, optional, optionMaybe, sepBy, sepBy1, (<?>))
{- transient data type just used for parsing the awkward Tempo syntax
a list of time signatures expressed as rationals and a bpm expressed as an Int
-}
data TempoDesignation = TempoDesignation (Nel.NonEmptyList Rational) Int
{- use for debug like this:
traceParse "manySlashes" <$>
(
parseRule
)
-}
{-
traceParse :: forall a. String -> a -> a
traceParse s p =
trace s (\_ -> p)
-}
abc :: Parser AbcTune
abc =
{ headers:_, body:_ }
<$> headers <*> body
body :: Parser (List BodyPart)
body =
(:)
<$> score
<*> manyTill
{- there is unfortunately ambiguity between a score item and
an in-score header. For example 'M' may introduce both a
decoration or a meter. We probably don't pay too much for
trying the header because they are only short sequences
-}
(try tuneBodyHeader <|> score)
eof
score :: Parser BodyPart
score =
Score <$>
{- there is potential ambiguity here betweeb 'fullyBarredLine' and
'inline' both of which commence with '[' making this order in the
pairing necessary. To do it in the other order would involve:
(try fullyBarredLine <|> introLine)
-}
(introLine <|> fullyBarredLine)
<?> "score"
bar :: Parser Bar
bar =
{ decorations:_, startLine:_, music:_ }
<$> decorations
<*> barline
<*> (many scoreItem)
<?> "bar"
-- | an intro bar is a bar at the beginning of a line which has no starting bar line
introBar :: Parser Bar
introBar =
{ decorations: Nil, startLine: invisibleBarType, music:_ }
<$> many scoreItem
<?> "intro bar"
where
-- | a bar type for an introductory 'bar' where there is no opening bar line
invisibleBarType :: BarLine
invisibleBarType =
{ endRepeats: 0
, thickness: Invisible
, startRepeats: 0
, iteration: Nothing
}
-- | an intro line as a full line of bars thus introduced
introLine :: Parser (List Bar)
introLine =
(:) <$> introBar <*> manyTill bar eol
<?> "intro line"
-- | a fully barred line has bar lines both at begin and end
fullyBarredLine :: Parser (List Bar)
fullyBarredLine =
manyTill bar eol
<?> "fully barred line"
scoreItem :: Parser Music
scoreItem =
choice
[ try chord -- potential ambiguity with (inline) in-score headers and slur brackets
, try inline
, continuation
, try decoratedSpace -- potential ambiguity with a decorated note
, ignore
, spacer
, try annotation -- potential ambiguity with chordSymbol
, chordSymbol
, try tuplet -- potential ambiguity with slurs inside a note
, rest
, try brokenRhythmPair -- potential ambiguity with note
, try note -- potential ambiguity with decorations on bars
]
<?> "score item"
chord :: Parser Music
chord =
Chord
<$> abcChord
<?> "chord"
abcChord :: Parser AbcChord
abcChord =
buildChord
<$> leftSlurBrackets
<*> decorations
<*> (between (char '[') (char ']') (many1 (abcNote <* whiteSpace)))
<*> optionMaybe noteDur
<*> rightSlurBrackets
<?> "ABC chord"
where
buildChord :: Int -> List String -> Nel.NonEmptyList AbcNote -> Maybe Rational -> Int -> AbcChord
buildChord leftSlurs decs ns ml rightSlurs =
let
l =
fromMaybe (fromInt 1) ml
in
{ leftSlurs, decorations: decs, notes: ns, duration: l, rightSlurs }
inline :: Parser Music
inline =
Inline
<$> between (char '[') (char ']') (tuneBodyInfo true)
<?> "inline header"
barline :: Parser BarLine
barline =
choice
[ try normalBarline -- ambiguity of :: caused by degenerateDoubleColon
, degenerateDoubleColon
, degenerateBarVolta
]
{- a normal bar line (plus optional repeat iteration marker)
see comments in 4.8 Repeat/bar symbols:
Abc parsers should be quite liberal in recognizing bar lines. In the wild, bar lines
may have any shape, using a sequence of | (thin bar line), [| or |] (thick bar line),
and : (dots), e.g. |[| or [|:::
-}
normalBarline :: Parser BarLine
normalBarline =
{ endRepeats:_, thickness:_, startRepeats:_, iteration:_ }
<$> repeatMarkers
<*> barlineThickness
<*> repeatMarkers
<*> optionMaybe repeatSection
<?> "bartype"
{- sometimes in the wild we get a degenerate volta marker at the start of a line
of music like this:
[1 .....
or
_[1 ....
again we have to be careful about ambiguity between this and inline headers by
making sure we parse '[' immediately followed by '1' etc.
We treat this as a bar on its own. If you try to treat it as a free-standing volta
then you are beset by the ambiguity issues.
-}
degenerateBarVolta :: Parser BarLine
degenerateBarVolta =
{ endRepeats: 0, thickness: Thin, startRepeats: 0, iteration:_}
<$> (Just <$> (whiteSpace *> char '[' *> repeatSection))
{- Parse a degenerate barline with no bar line! Just :: on its own -}
degenerateDoubleColon :: Parser BarLine
degenerateDoubleColon =
{ endRepeats: 1, thickness: Thin, startRepeats: 1, iteration: Nothing }
<$ char ':'
<* char ':'
{- a repeat section at the start of a bar. We have just parsed a bar marker (say |) and so the combination of this and the repeat may be:
|1
|[1
| [1
but is not allowed to be
| 1
repeats of the form 1,2,3 are also accepted
associating the digit with the bracket bar number should remove ambiguity with respect to other productions that use the bracket
(in particular, inline headers and chords).
-}
repeatSection :: Parser (Nel.NonEmptyList Volta)
repeatSection =
sepBy1 volta (char ',')
volta :: Parser Volta
volta =
try voltaRange
<|> simpleVolta
voltaRange :: Parser Volta
voltaRange =
VoltaRange <$> digit <*> (char '-' *> digit)
<?> "volta range"
simpleVolta :: Parser Volta
simpleVolta =
Volta <$> digit
<?> "simple volta"
barlineThickness :: Parser Thickness
barlineThickness =
choice
[ ThickThin <$ string "[|"
, ThinThick <$ string "|]"
, ThickThin <$ string "]|"
, ThinThin <$ string "||"
, Thin <$ string "|"
]
repeatMarkers :: Parser Int
repeatMarkers =
L.length <$> many (char ':')
-- spec is unclear if spaces are allowed after a broken rhythm operator but it's easy to support, is more permissive and doesn't break anything
brokenRhythmTie :: Parser Broken
brokenRhythmTie =
buildBrokenOperator <$> degenerateBrokenRhythmOperator <* whiteSpace
-- | In the wild, we can see slurs encompassing the operator. For example,
-- | instead of A>(BC) we can see A(>BC)
-- | instead of (AB)>C we can see (AB>)C
-- | Be lenient but throw the slur away
degenerateBrokenRhythmOperator :: Parser String
degenerateBrokenRhythmOperator =
optional leftBracket *> brokenRhythmOperator <* optional rightBracket
brokenRhythmPair :: Parser Music
brokenRhythmPair =
BrokenRhythmPair
<$> graceableNote
<*> brokenRhythmTie
<*> graceableNote
<?> "broken rhythm pair"
note :: Parser Music
note =
Note <$> graceableNote
abcNote :: Parser AbcNote
abcNote =
buildNote
<$> maybeAccidental
<*> pitch
<*> moveOctave
<*> optionMaybe noteDur
<*> maybeTie
<?> "ABC note"
where
buildNote :: Maybe Accidental -> String -> Int -> Maybe Rational -> Maybe Char -> AbcNote
buildNote macc pitchStr octave ml mt =
let
l =
fromMaybe (1 % 1) ml
pc =
lookupPitch (toUpper pitchStr)
spn =
scientificPitchNotation pitchStr octave
tied =
case mt of
Just _ ->
true
_ ->
false
acc =
case macc of
Nothing -> Implicit
Just a -> a
in
{ pitchClass: pc, accidental: acc, octave: spn, duration: l, tied: tied }
graceableNote :: Parser GraceableNote
graceableNote =
{ maybeGrace:_, leftSlurs:_, decorations:_, abcNote:_, rightSlurs:_ }
<$> optionMaybe graceBracket
<*> leftSlurBrackets
<*> decorations
<*> abcNote
<*> rightSlurBrackets
<?> "graceable note"
{- maybe an accidental defining a note's pitch -}
maybeAccidental :: Parser (Maybe Accidental)
maybeAccidental =
optionMaybe accidental
accidental :: Parser Accidental
accidental =
buildAccidental
<$>
( choice
[ string "^^"
, string "__"
, string "^"
, string "_"
, string "="
]
)
where
buildAccidental :: String -> Accidental
buildAccidental s =
case s of
"^^" ->
DoubleSharp
"__" ->
DoubleFlat
"^" ->
Sharp
"_" ->
Flat
_ ->
Natural
{- an upper or lower case note ([A-Ga-g]) -}
pitch :: Parser String
pitch =
regex "[A-Ga-g]"
moveOctave :: Parser Int
moveOctave =
octaveShift <$> regex "[',]*"
{- count the number of apostrophe (up) or comma (down) characters in the string
and give the result a value of (up-down)
-}
octaveShift :: String -> Int
octaveShift s =
let
up = Array.length $ Array.filter ((==) '\'') (toCharArray s)
down = Array.length $ Array.filter ((==) ',') (toCharArray s)
in
up - down
{- the duration of a note in the body
order of choices here is important to remove ambiguity
-}
noteDur :: Parser Rational
noteDur =
choice
[ try manySlashes
, try anyRat
, integralAsRational
]
{-| this matches:
1/2
/2
1
/
i.e. there has to be at least a single slash
-}
anyRat :: Parser Rational
anyRat =
(%) <$> option 1 int <* char '/' <*> option 2 int
{-| this matches // or /// etc. -}
manySlashes :: Parser Rational
manySlashes =
buildRationalFromSlashList
<$> (Nel.cons <$> char '/' <*> many1 (char '/'))
integralAsRational :: Parser Rational
integralAsRational =
fromInt <$> int
{-}
-- | this implements the spec - a tie attaches to the end of the note and is
-- | not free standing
maybeTie :: Parser (Maybe Char)
maybeTie =
(optionMaybe (char '-'))
<?> "tie"
-}
-- | but here we relax the spec. The tie still attaches to the previous note
-- | bur can now be separated from it with spaces. It can thus appear to attach
-- | to the next note syntactically. This helps a good deal of 'bad' ABC
-- | examples in the wild.
maybeTie :: Parser (Maybe Char)
maybeTie =
map (\_ -> '-') <$>
(optionMaybe (regex " *-"))
<?> "tie"
rest :: Parser Music
rest =
Rest
<$> abcRest
<?> "rest"
abcRest :: Parser AbcRest
abcRest =
{ duration:_}
<$> (fromMaybe (fromInt 1) <$> (regex "[XxZz]" *> optionMaybe noteDur))
<?> "abcRest"
tuplet :: Parser Music
tuplet = do
maybeGrace <- optionMaybe graceBracket
leftBracketCount <- tupletBrackets
-- calculate the number of slurs by subtracting the tuplet bracket
let
leftSlurs = max 0 (leftBracketCount - 1)
signature <- tupletSignature
-- ensure that the contents match the signature count
restsOrNotes <- counted signature.r restOrNote
pure $ Tuplet { maybeGrace, leftSlurs, signature, restsOrNotes }
-- | tuplets may now contain either a (Left) rest or a (Right) Note
restOrNote :: Parser RestOrNote
restOrNote =
(Left <$> abcRest) <|> (Right <$> graceableNote)
<* whiteSpace
{- possible tuplet signatures
(3 --> {3,2,3}
(3:2 --> {3,2,3}
(3:: --> {3,2,3}
(3:2:4 --> {3,2,4}
(3::2 --> {3,2,2}
note, space is allowed after the tuplet signature but before the notes in the tuplet
-}
tupletSignature :: Parser TupletSignature
tupletSignature =
buildTupletSignature
<$> tupletLength
<*> tup
<*> tup
<* whiteSpace
tup :: Parser (Maybe String)
tup =
join
<$> optionMaybe
(char ':' *> optionMaybe tupletLength)
-- | left and right slurs. We now attach the (optional) slur brackets to thr
-- | actual target note which respectively starts or ends the slurred sequence
-- | this may be prefaced by an optional grace note - e.g.(fg)(abc)
-- | here the openinhg slur is attached to note a and the final slur to c
leftSlurBrackets :: Parser Int
leftSlurBrackets =
L.length
<$> many leftBracket
<?> "left slurs"
-- | ditto for tuplets - a mandatory left bracket possibly preceded by slurs
tupletBrackets :: Parser Int
tupletBrackets =
Nel.length
<$> many1 leftBracket
<?> "tuplet + slurs"
leftBracket :: Parser Char
leftBracket =
char '('
rightSlurBrackets :: Parser Int
rightSlurBrackets =
L.length
<$> many rightBracket
<?> "right slurs"
rightBracket :: Parser Char
rightBracket =
char ')'
graceBracket :: Parser Grace
graceBracket =
between (char '{') (char '}') grace <* whiteSpace
<?> "grace bracket"
grace :: Parser Grace
grace =
{ isAcciaccatura:_, notes:_ }
<$> acciaccatura <*> (many1 abcNote)
{- acciaccaturas are indicated with an optional forward slash
was
acciaccatura = withDefault false <$> ( (\_ -> true) <$> maybe (char '/'))
-}
acciaccatura :: Parser Boolean
acciaccatura =
(\_ -> true) <$> optionMaybe (char '/')
{- an annotation to the score
4.19 Annotations
General text annotations can be added above, below or on the staff in a similar way to chord symbols. In this case, the string within double quotes
is preceded by one of five symbols ^, _, <, > or @ which controls where the annotation is to be placed; above, below, to the left or right respectively
of the following note, rest or bar line. Using the @ symbol leaves the exact placing of the string to the discretion of the interpreting program.
These placement specifiers distinguish annotations from chord symbols, and should prevent programs from attempting to play or transpose them.
All text that follows the placement specifier is treated as a text string.
Example:
"<(" ">)" C
-}
annotation :: Parser Music
annotation =
buildAnnotation
<$> annotationString
<?> "annotation"
where
buildAnnotation :: String -> Music
buildAnnotation s =
let
placement =
case (charAt 0 s) of
Just '^' ->
AboveNextSymbol
Just '_' ->
BelowNextSymbol
Just '<' ->
LeftOfNextSymbol
Just '>' ->
RightOfNextSymbol
_ ->
Discretional
in
Annotation placement (drop 1 s)
annotationString :: Parser String
annotationString =
-- (\s -> "\"" <> s <> "\"") <$>
string "\""
*> regex "[\\^\\>\\<-@](\\\\\"|[^\"\n])*"
<* string "\""
<?> "annotation"
-- | a free - format chord symbol - see 4.18 Chord symbols. Drop the quotes round the string.
chordSymbol :: Parser Music
chordSymbol =
(ChordSymbol <<< { name:_, duration: Nothing })
<$> literalQuotedString false
<?> "chord symbol"
decorations :: Parser (List String)
decorations =
many decoration
decoration :: Parser String
decoration =
(shortDecoration <|> longDecoration)
<* whiteSpace
<?> "decoration"
shortDecoration :: Parser String
shortDecoration =
regex "[\\.~HLMOPSTuv]"
<?> "short decoration"
longDecoration :: Parser String
longDecoration =
between (char '!') (char '!') (regex "[^\x0D\n!]+")
<?> "long decoration"
-- | our whiteSpace differs from that of the string parser we do NOT want to
-- |consume carriage returns or newlines
whiteSpace :: Parser String
whiteSpace =
foldMap (singleton <<< codePointFromChar) <$>
many scoreSpace
-- at least one (intended) space somewhere inside the music body
spacer :: Parser Music
spacer =
Spacer
<$> (Nel.length <$> (many1 scoreSpace))
<?> "space"
-- | see section 6.1.2 Typesetting extra space
-- | y can be used to add extra space between the surrounding notes; moreover,
-- | chord symbols and decorations can be attached to it, to separate them from notes.
decoratedSpace :: Parser Music
decoratedSpace =
DecoratedSpace <$> decorations <* (char 'y')
-- normal space within a line of the tune's score
scoreSpace :: Parser Char
scoreSpace =
-- tab <|> space
(char '\t') <|> space
space :: Parser Char --
space = char ' '
{- characters to ignore
Section 8.1 Tune Body:
The following characters are currently reserved: # * ; ? @
In future standards they may be used to extend the abc syntax. To ensure forward compatibility,
current software should ignore these characters when they appear inside or between note groups.
section 4.7 Beams:
Back quotes ` may be used freely between notes to be beamed, to increase legibility.
They are ignored by computer programs. For example, A2``B``C is equivalent to A2BC.
-}
ignore :: Parser Music
ignore =
Ignore <$
(regex "[#@;`\\*\\?]+")
<?> "ignored character"
{- This is an area where the spec is uncertain. See 6.1.1 Typesetting line-breaks
The forward slash is used to indicate 'continuation of input lines' often because
users may need to avoid long lines if, for example, they would otherwise extend
beyond the limit of an old email system. All very out of date, but nevertheless
still prevalent in the wild. We take the view that we must do our best to recognise
them and then throw them away (along with any other later stuff in the line)
Any text between the forward slash and eol is treated as comment.
Return (Continuation comment) if we have a continuation. Now we consume the
eol character at the end of the continuation so that the parser will continue
to accumulate the following line into the ADT as a continuation of this line.
-}
continuation :: Parser Music
continuation =
Continuation
<$ char '\\'
<*> regex "[^\x0D\n]*"
<* eol
<?> "continuation"
-- tune headers
headers :: Parser TuneHeaders
headers =
many header <?> "headers"
header :: Parser Header
header =
informationField false <* eol
{- headers that may appear in the tune body -}
tuneBodyHeader :: Parser BodyPart
tuneBodyHeader =
BodyInfo
<$> tuneBodyInfo true
<* eol
<?> "tune body header"
tuneBodyInfo :: Boolean -> Parser Header
tuneBodyInfo isInline =
choice
[ tuneBodyOnlyInfo isInline
, anywhereInfo isInline
]
<?> "tune body info"
tuneBodyOnlyInfo :: Boolean -> Parser Header
tuneBodyOnlyInfo isInline =
choice
[ symbolLine isInline
, wordsAligned isInline
]
<?> "tune body only info"
{- Headers/Information fields. These can be used in three different ways:
1) As a normal tune header
2) As an 'inline' header inside the tune body on a separate line
3) Embedded inside a tune score between '[' and ']'
Only a named subset of headers can be used inline in this way.
One subtlety is therefore that header information that accepts simple text content
should not be allowed to incorporate '[' or ']' because of the potential ambiguity.
Thus, headers functions are given a parameter 'inline' which is the inline context
simply allowing 'normal' headers to accept these values in text content but to allow
inline headers to reject them.
-}
{- whereas information fields can be used inline
isInline - is this information field being used in an in-line context
(as opposed to being used in a conventional header)
-}
informationField :: Boolean -> Parser Header
informationField isInline =
choice
[ anywhereInfo isInline
, tuneInfo
]
<?> "header"
anywhereInfo :: Boolean -> Parser Header
anywhereInfo isInline =
choice
[ instruction isInline
, key
, unitNoteLength
, meter
, macro isInline
, notes isInline
, parts isInline
, tempo
, rhythm isInline
, remark isInline
, title isInline
, userDefined isInline
, voice
, wordsAfter isInline
, fieldContinuation
, commentLine
]
<?> "anywhere info"
tuneInfo :: Parser Header
tuneInfo =
choice
[ area
, book
, composer
, discography
, fileUrl
, group
, history
, origin
, source
, referenceNumber
, transcription
, unsupportedHeader -- headers that are currently unsupported but must be recognized and ignored
]
<?> "tune info"
headerCode :: Char -> Parser String
headerCode c =
let
pattern =
fromCharArray [ c, ':' ]
in
string pattern <* whiteSpace
unsupportedHeaderCode :: Parser String
unsupportedHeaderCode =
regex "[a-qt-vx-zEJ]:" <* whiteSpace
{- Full comment lines. Comments are introduced with '%' and can occur anywhere
and carry on thill the end of the line. We'll treat single line comments
as Headers so as not to pollute the parse tree overmuch.
The stylesheet directive '%%' is not recognized here and will simply be
treated as a comment.
-}
commentLine :: Parser Header
commentLine =
Comment
<$> comment
<?> "comment line"
{- parse an information item String - note that, because these can be used inline
(bracketed by '[' and ']') it behoves us not to use the framing characters in the string
when the header is used inline (but not when used in a normal header)
not that the spec has anything to say about it as far as I can see
-}
inlineInfo :: Boolean -> Parser String
inlineInfo isInline =
let
pattern =
if isInline then
"[^\x0D\n\\[\\]]*"
else
"[^\x0D\n]*"
in
regex pattern
area :: Parser Header
area =
Area
<$> ((headerCode 'A') *> strToEol)
<?> "A header"
book :: Parser Header
book =
Book
<$> ((headerCode 'B') *> strToEol)
<?> "B Header"
composer :: Parser Header
composer =
Composer
<$> ((headerCode 'C') *> strToEol)
<?> "C header"
discography :: Parser Header
discography =
Discography
<$> ((headerCode 'D') *> strToEol)
<?> "D header"
fileUrl :: Parser Header
fileUrl =
FileUrl
<$> ((headerCode 'F') *> strToEol)
<?> "F header"
group :: Parser Header
group =
Group
<$> ((headerCode 'G') *> strToEol)
<?> "G header"
history :: Parser Header
history =
History
<$> ((headerCode 'H') *> strToEol)
<?> "H header"
instruction :: Boolean -> Parser Header
instruction isInline =
Instruction
<$> ((headerCode 'I') *> (inlineInfo isInline))
<?> "I header"
key :: Parser Header
key =
Key <$>
({ keySignature:_, modifications:_, properties:_ }
<$ (headerCode 'K')
<*> keySignature
<*> keyAccidentals
<*> amorphousProperties
<?> "K header"
)
unitNoteLength :: Parser Header
unitNoteLength =
UnitNoteLength
<$> ((headerCode 'L') *> noteDuration)
<?> "L header"
meter :: Parser Header
meter =
Meter
<$> ((headerCode 'M') *> meterDefinition)
<?> "M header"
macro :: Boolean -> Parser Header
macro isInline =
Macro
<$> ((headerCode 'm') *> (inlineInfo isInline))
<?> "m header"
notes :: Boolean -> Parser Header
notes isInline =
Notes
<$> ((headerCode 'N') *> (inlineInfo isInline))
<?> "N header"
origin :: Parser Header
origin =
Origin
<$> ((headerCode 'O') *> strToEol)
<?> "O header"
parts :: Boolean -> Parser Header
parts isInline =
Parts
<$> ((headerCode 'P') *> (inlineInfo isInline))
<?> "P header"
tempo :: Parser Header
tempo =
Tempo
<$> ((headerCode 'Q') *> tempoSignature)
<?> "Q header"
rhythm :: Boolean -> Parser Header
rhythm isInline =
Rhythm
<$> ((headerCode 'R') *> (inlineInfo isInline))
<?> "R header"
remark :: Boolean -> Parser Header
remark isInline =
Remark
<$> ((headerCode 'r') *> (inlineInfo isInline))
<?> "r header"
source :: Parser Header
source =
Source
<$> ((headerCode 'S') *> strToEol)
<?> "S header"
symbolLine :: Boolean -> Parser Header
symbolLine isInline =
SymbolLine
<$> ((headerCode 's') *> (inlineInfo isInline))
<?> "s header"
title :: Boolean -> Parser Header
title isInline =
Title
<$> ((headerCode 'T') *> (inlineInfo isInline))
<?> "T header"
userDefined :: Boolean -> Parser Header
userDefined isInline =
UserDefined
<$> ((headerCode 'U') *> (inlineInfo isInline))
<?> "U header"
voice :: Parser Header
voice =
Voice <$>
( { id:_, properties:_ }
<$ (headerCode 'V')
<*> alphaNumPlusString
<*> amorphousProperties
<?> "V header"
)
wordsAfter :: Boolean -> Parser Header
wordsAfter isInline =
WordsAfter
<$> ((headerCode 'W') *> (inlineInfo isInline))
<?> "W header"
wordsAligned :: Boolean -> Parser Header
wordsAligned isInline =
WordsAligned
<$> ((headerCode 'w') *> (inlineInfo isInline))
<?> "w header"
referenceNumber :: Parser Header
referenceNumber =
ReferenceNumber
<$> ((headerCode 'X') *> (optionMaybe int))
<* whiteSpace
<?> "x header"
transcription :: Parser Header
transcription =
Transcription
<$> ((headerCode 'Z') *> strToEol)
<?> "Z header"
fieldContinuation :: Parser Header
fieldContinuation =
FieldContinuation
<$> ((headerCode '+') *> strToEol)
<?> "field continuation"
{- unsupported header reserved for future use -}
unsupportedHeader :: Parser Header
unsupportedHeader =
UnsupportedHeader
<$ unsupportedHeaderCode
<* strToEol
<?> "unsupported header"
-- HEADER ATTRIBUTES
{- normal Rational e.g 3/4 -}
rational :: Parser Rational
rational =
(%) <$> int <* char '/' <*> int
-- rational with trailing optional spaces
headerRational :: Parser Rational
headerRational =
rational <* whiteSpace
noteDuration :: Parser NoteDuration
noteDuration =
rational <* whiteSpace