Skip to content

Commit 5d3f52f

Browse files
committed
Match only ASCII digits in the lexer number patterns
In Python 3 \d matches every Unicode decimal digit, so the lexers read full-width digits as numbers. \tempo 4 = 60 lexed 60 as an IntegerValue and #(display 42) lexed 42 as a Number, though neither is legal LilyPond. Replaced \d with [0-9] in the 12 patterns that match a digit, across ly/lex/scheme.py, ly/lex/lilypond.py and ly/lex/html.py. Six other \d uses are left as they are. They sit inside negated character classes or negative lookaheads, where \d correctly excludes every digit. Rewriting [^\W\d_] as [^\W0-9_] would start accepting 4 as an identifier character, which is the bug this issue reports rather than a fix for it. Fixes #166
1 parent ac3db74 commit 5d3f52f

4 files changed

Lines changed: 74 additions & 13 deletions

File tree

‎ly/lex/html.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class StringSQEnd(String, _token.StringEnd, _token.Leaver):
9696

9797

9898
class EntityRef(_token.Character):
99-
rx = r"\&(#\d+|#[xX][0-9A-Fa-f]+|[A-Za-z_:][\w.:_-]*);"
99+
rx = r"\&(#[0-9]+|#[xX][0-9A-Fa-f]+|[A-Za-z_:][\w.:_-]*);"
100100

101101

102102
class LilyPondTag(Tag):

‎ly/lex/lilypond.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
re_duration = rf"(\\(maxima|longa|breve){re_identifier_end}|(1|2|4|8|16|32|64|128|256|512|1024|2048)(?!\d))"
4646

4747
re_dot = r"\."
48-
re_scaling = r"\*[\t ]*\d+(/\d+)?"
48+
re_scaling = r"\*[\t ]*[0-9]+(/[0-9]+)?"
4949

5050

5151

@@ -72,15 +72,15 @@ class Value(_token.Item, _token.Numeric):
7272

7373

7474
class DecimalValue(Value):
75-
rx = r"-?\d+(\.\d+)?"
75+
rx = r"-?[0-9]+(\.[0-9]+)?"
7676

7777

7878
class IntegerValue(DecimalValue):
79-
rx = r"\d+"
79+
rx = r"[0-9]+"
8080

8181

8282
class Fraction(Value):
83-
rx = r"\d+/\d+"
83+
rx = r"[0-9]+/[0-9]+"
8484

8585

8686
class Delimiter(_token.Token):
@@ -289,11 +289,11 @@ class ScriptAbbreviation(Articulation, _token.Leaver):
289289

290290

291291
class Fingering(Articulation, _token.Leaver):
292-
rx = r"\d+"
292+
rx = r"[0-9]+"
293293

294294

295295
class StringNumber(Articulation):
296-
rx = r"\\\d+"
296+
rx = r"\\[0-9]+"
297297

298298

299299
class Slur(_token.Token):
@@ -379,7 +379,7 @@ class ChordSeparator(ChordItem):
379379

380380

381381
class ChordStepNumber(ChordItem):
382-
rx = r"\d+[-+]?"
382+
rx = r"[0-9]+[-+]?"
383383

384384

385385
class DotChord(ChordItem):
@@ -571,7 +571,7 @@ def update_state(self, state):
571571

572572

573573
class TempoSeparator(Delimiter):
574-
rx = r"[-~](?=\s*\d)"
574+
rx = r"[-~](?=\s*[0-9])"
575575

576576

577577
class Partial(Command):
@@ -740,7 +740,7 @@ class FigureBracket(Figure):
740740

741741
class FigureStep(Figure):
742742
"""A step figure number or the underscore."""
743-
rx = r"_|\d+"
743+
rx = r"_|[0-9]+"
744744

745745

746746
class FigureAccidental(Figure):

‎ly/lex/scheme.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,18 @@ def test_match(cls, match):
140140

141141
class Number(_token.Item, _token.Numeric):
142142
rx = (r"("
143-
r"-?\d+|"
143+
r"-?[0-9]+|"
144144
r"#(b[0-1]+|o[0-7]+|x[0-9a-fA-F]+)|"
145145
r"[-+]inf.0|[-+]?nan.0"
146146
r")(?=$|[)\s])")
147147

148148

149149
class Fraction(Number):
150-
rx = r"-?\d+/\d+(?=$|[)\s])"
150+
rx = r"-?[0-9]+/[0-9]+(?=$|[)\s])"
151151

152152

153153
class Float(Number):
154-
rx = r"-?((\d+(\.\d*)|\.\d+)(E\d+)?)(?=$|[)\s])"
154+
rx = r"-?(([0-9]+(\.[0-9]*)|\.[0-9]+)(E[0-9]+)?)(?=$|[)\s])"
155155

156156

157157
class VectorStart(OpenParen):

‎tests/test_lex.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Tests for the lexers."""
2+
import ly.document
3+
import ly.lex._token
4+
import ly.lex.html
5+
import ly.lex.lilypond
6+
7+
8+
DIGIT_TOKENS = (ly.lex._token.Numeric,
9+
ly.lex.lilypond.Fingering,
10+
ly.lex.lilypond.StringNumber,
11+
ly.lex.lilypond.ChordStepNumber,
12+
ly.lex.lilypond.FigureStep,
13+
ly.lex.lilypond.TempoSeparator,
14+
ly.lex.lilypond.Scaling)
15+
16+
TO_FULLWIDTH = str.maketrans('0123456789', '0123456789')
17+
18+
SOURCES = [
19+
'#(display 42)',
20+
'#(x 1/2)',
21+
'#(x 1.5)',
22+
'\\tempo 4 = 60 - 80',
23+
'\\time 3/4',
24+
'c4',
25+
'c-4',
26+
'\\override NoteHead.font-size = 2',
27+
'\\chords { c1:5 }',
28+
'\\figures { <1> }',
29+
'\\score { { c1*2 } }',
30+
'\\score { \\new TabStaff { c4\\4 } }',
31+
]
32+
33+
34+
def digit_tokens(text, mode=None):
35+
"""Return the text of every token the lexer only reads as such because of
36+
the digits in it."""
37+
doc = ly.document.Document(text, mode)
38+
return [str(t) for block in doc
39+
for t in doc.tokens(block) if isinstance(t, DIGIT_TOKENS)]
40+
41+
42+
def test_ascii_digits_are_read():
43+
for source in SOURCES:
44+
assert digit_tokens(source), source
45+
46+
47+
def test_fullwidth_digits_are_not_read():
48+
for source in SOURCES:
49+
fullwidth = source.translate(TO_FULLWIDTH)
50+
assert digit_tokens(fullwidth) == [], fullwidth
51+
52+
53+
def entity_refs(text):
54+
doc = ly.document.Document(text, 'html')
55+
return [str(t) for block in doc
56+
for t in doc.tokens(block) if isinstance(t, ly.lex.html.EntityRef)]
57+
58+
59+
def test_only_ascii_digits_form_a_numeric_entity():
60+
assert entity_refs('&#65;') == ['&#65;']
61+
assert entity_refs('&#65;'.translate(TO_FULLWIDTH)) == []

0 commit comments

Comments
 (0)