| 📦 Turbo Python 6fc62ea k33g 9h ago | 1 | package pythonlang |
| 2 | |
| 3 | // The strings of Python: eight prefixes, two quotes, and each of those in a |
| 4 | // single and a triple form — sixteen spellings of one construct, plus the two |
| 5 | // different ways one of them reaches the next line. |
| 6 | |
| 7 | import ( |
| 8 | "strings" |
| 9 | |
| 10 | "rickub.com/turbo-editors/turbo-core/syntax" |
| 11 | ) |
| 12 | |
| 13 | // maxStringPrefix is how many letters may come before the quote: rb, fr and |
| 14 | // their case variants are two, and nothing in the language is three. |
| 15 | const maxStringPrefix = 2 |
| 16 | |
| 17 | // isStringStart reports whether a string literal opens at the scanner's |
| 18 | // position, prefix included. |
| 19 | func isStringStart(s *syntax.LineScanner) bool { |
| 20 | _, opens := stringPrefixLength(s) |
| 21 | return opens |
| 22 | } |
| 23 | |
| 24 | // stringPrefixLength returns how many prefix letters come before the quote, and |
| 25 | // whether a string opens here at all. |
| 26 | // |
| 27 | // It accepts any one or two of f, r, b and u in any order and in any case, |
| 28 | // which is a little more generous than the language: `bu"…"` is not a Python |
| 29 | // string and is coloured as one. Being generous is the right way to be wrong |
| 30 | // here — the alternative is a table of the twenty-four accepted spellings, and |
| 31 | // a half-typed prefix under the cursor colouring as an identifier and a string |
| 32 | // that are not there. |
| 33 | func stringPrefixLength(s *syntax.LineScanner) (int, bool) { |
| 34 | for length := 0; length <= maxStringPrefix; length++ { |
| 35 | switch r := s.Peek(length); { |
| 36 | case r == '"' || r == '\'': |
| 37 | return length, true |
| 38 | case !isStringPrefixRune(r): |
| 39 | return 0, false |
| 40 | } |
| 41 | } |
| 42 | return 0, false |
| 43 | } |
| 44 | |
| 45 | // isStringPrefixRune reports whether a rune may appear in a string's prefix. |
| 46 | func isStringPrefixRune(r rune) bool { |
| 47 | switch r { |
| 48 | case 'f', 'F', 'r', 'R', 'b', 'B', 'u', 'U': |
| 49 | return true |
| 50 | } |
| 51 | return false |
| 52 | } |
| 53 | |
| 54 | // takeString colours a string from its prefix, remembering what would close it. |
| 55 | // |
| 56 | // An f-string's {expression} is *not* scanned as code. Since Python 3.12 it may |
| 57 | // hold anything, nested quotes and comments included, so colouring it properly |
| 58 | // means running the whole scanner inside itself; colouring it half-properly |
| 59 | // means a brace in a format spec — "{n:{width}}" — ending the string early. One |
| 60 | // flat run is the honest answer, and it is the one this scanner gives. |
| 61 | func takeString(s *syntax.LineScanner, open *carry) { |
| 62 | start := s.Pos() |
| 63 | |
| 64 | prefix, _ := stringPrefixLength(s) |
| 65 | s.Advance(prefix) |
| 66 | |
| 67 | quote := s.Peek(0) |
| 68 | triple := s.Peek(1) == quote && s.Peek(2) == quote |
| 69 | if triple { |
| 70 | s.Advance(3) |
| 71 | } else { |
| 72 | s.Advance(1) |
| 73 | } |
| 74 | open.open, open.quote, open.triple = true, quote, triple |
| 75 | |
| 76 | consumeString(s, open) |
| 77 | s.Emit(start, s.Pos(), syntax.ClassString) |
| 78 | } |
| 79 | |
| 80 | // continueString colours the rest of a string opened on an earlier line, and |
| 81 | // reports whether the line has code after it. |
| 82 | func continueString(s *syntax.LineScanner, open *carry) bool { |
| 83 | consumeString(s, open) |
| 84 | s.Emit(0, s.Pos(), syntax.ClassString) |
| 85 | return !open.open && !s.AtEnd() |
| 86 | } |
| 87 | |
| 88 | // consumeString runs to whatever closes the string, or to the end of the line, |
| 89 | // and decides there whether the string carries on to the next one. |
| 90 | // |
| 91 | // A backslash takes the rune after it out of consideration, in a raw string as |
| 92 | // much as in an ordinary one — that is what makes r"\"" one string rather than |
| 93 | // two. A backslash that is itself the last rune on the line escapes the newline |
| 94 | // instead, which is the only way a single-quoted string reaches the next line. |
| 95 | func consumeString(s *syntax.LineScanner, open *carry) { |
| 96 | closer := stringCloser(*open) |
| 97 | continued := false |
| 98 | |
| 99 | for !s.AtEnd() { |
| 100 | switch { |
| 101 | case s.Peek(0) == '\\': |
| 102 | continued = s.Pos()+2 > s.Len() |
| 103 | s.Advance(2) |
| 104 | case s.HasPrefix(0, closer): |
| 105 | s.Advance(len([]rune(closer))) |
| 106 | open.open = false |
| 107 | return |
| 108 | default: |
| 109 | continued = false |
| 110 | s.Advance(1) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | // The line ended with the string still open. A triple-quoted one is meant |
| 115 | // to do that; a single-quoted one only does it when the newline was |
| 116 | // escaped. Anything else is source in the middle of being typed, and is |
| 117 | // left behind rather than carried into the rest of the file. |
| 118 | if !open.triple && !continued { |
| 119 | open.open = false |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // stringCloser returns the text that ends the string being scanned. |
| 124 | func stringCloser(open carry) string { |
| 125 | if open.triple { |
| 126 | return strings.Repeat(string(open.quote), 3) |
| 127 | } |
| 128 | return string(open.quote) |
| 129 | } |