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
|
package pythonlang
// The strings of Python: eight prefixes, two quotes, and each of those in a
// single and a triple form — sixteen spellings of one construct, plus the two
// different ways one of them reaches the next line.
import (
"strings"
"rickub.com/turbo-editors/turbo-core/syntax"
)
// maxStringPrefix is how many letters may come before the quote: rb, fr and
// their case variants are two, and nothing in the language is three.
const maxStringPrefix = 2
// isStringStart reports whether a string literal opens at the scanner's
// position, prefix included.
func isStringStart(s *syntax.LineScanner) bool {
_, opens := stringPrefixLength(s)
return opens
}
// stringPrefixLength returns how many prefix letters come before the quote, and
// whether a string opens here at all.
//
// It accepts any one or two of f, r, b and u in any order and in any case,
// which is a little more generous than the language: `bu"…"` is not a Python
// string and is coloured as one. Being generous is the right way to be wrong
// here — the alternative is a table of the twenty-four accepted spellings, and
// a half-typed prefix under the cursor colouring as an identifier and a string
// that are not there.
func stringPrefixLength(s *syntax.LineScanner) (int, bool) {
for length := 0; length <= maxStringPrefix; length++ {
switch r := s.Peek(length); {
case r == '"' || r == '\'':
return length, true
case !isStringPrefixRune(r):
return 0, false
}
}
return 0, false
}
// isStringPrefixRune reports whether a rune may appear in a string's prefix.
func isStringPrefixRune(r rune) bool {
switch r {
case 'f', 'F', 'r', 'R', 'b', 'B', 'u', 'U':
return true
}
return false
}
// takeString colours a string from its prefix, remembering what would close it.
//
// An f-string's {expression} is *not* scanned as code. Since Python 3.12 it may
// hold anything, nested quotes and comments included, so colouring it properly
// means running the whole scanner inside itself; colouring it half-properly
// means a brace in a format spec — "{n:{width}}" — ending the string early. One
// flat run is the honest answer, and it is the one this scanner gives.
func takeString(s *syntax.LineScanner, open *carry) {
start := s.Pos()
prefix, _ := stringPrefixLength(s)
s.Advance(prefix)
quote := s.Peek(0)
triple := s.Peek(1) == quote && s.Peek(2) == quote
if triple {
s.Advance(3)
} else {
s.Advance(1)
}
open.open, open.quote, open.triple = true, quote, triple
consumeString(s, open)
s.Emit(start, s.Pos(), syntax.ClassString)
}
// continueString colours the rest of a string opened on an earlier line, and
// reports whether the line has code after it.
func continueString(s *syntax.LineScanner, open *carry) bool {
consumeString(s, open)
s.Emit(0, s.Pos(), syntax.ClassString)
return !open.open && !s.AtEnd()
}
// consumeString runs to whatever closes the string, or to the end of the line,
// and decides there whether the string carries on to the next one.
//
// A backslash takes the rune after it out of consideration, in a raw string as
// much as in an ordinary one — that is what makes r"\"" one string rather than
// two. A backslash that is itself the last rune on the line escapes the newline
// instead, which is the only way a single-quoted string reaches the next line.
func consumeString(s *syntax.LineScanner, open *carry) {
closer := stringCloser(*open)
continued := false
for !s.AtEnd() {
switch {
case s.Peek(0) == '\\':
continued = s.Pos()+2 > s.Len()
s.Advance(2)
case s.HasPrefix(0, closer):
s.Advance(len([]rune(closer)))
open.open = false
return
default:
continued = false
s.Advance(1)
}
}
// The line ended with the string still open. A triple-quoted one is meant
// to do that; a single-quoted one only does it when the newline was
// escaped. Anything else is source in the middle of being typed, and is
// left behind rather than carried into the rest of the file.
if !open.triple && !continued {
open.open = false
}
}
// stringCloser returns the text that ends the string being scanned.
func stringCloser(open carry) string {
if open.triple {
return strings.Repeat(string(open.quote), 3)
}
return string(open.quote)
}
|