| 📦 Turbo MoonBit cc1f595 k33g 11h ago | 1 | package moonbitlang |
| 2 | |
| 3 | // The literals of MoonBit: five quoted forms, and one rule that governs all of |
| 4 | // them — none may reach the next line. |
| 5 | |
| 6 | import "rickub.com/turbo-editors/turbo-core/syntax" |
| 7 | |
| 8 | // literalPrefixes are the letters that may come before a quote, and what |
| 9 | // opening quote each of them expects. |
| 10 | // |
| 11 | // b takes either quote — b"bytes" and b'x' are both byte literals — while re |
| 12 | // takes only the double one. A prefix is only a prefix when the quote touches |
| 13 | // it, which is what keeps the variable b in `b + 1` from starting a literal. |
| 14 | var literalPrefixes = []struct { |
| 15 | prefix string |
| 16 | quotes string |
| 17 | }{ |
| 18 | {"b", `"'`}, |
| 19 | {"re", `"`}, |
| 20 | } |
| 21 | |
| 22 | // isLiteralStart reports whether a quoted literal opens at the scanner's |
| 23 | // position, prefix included. |
| 24 | func isLiteralStart(s *syntax.LineScanner) bool { |
| 25 | _, opens := literalPrefixLength(s) |
| 26 | return opens |
| 27 | } |
| 28 | |
| 29 | // literalPrefixLength returns how many prefix runes come before the quote, and |
| 30 | // whether a literal opens here at all. |
| 31 | func literalPrefixLength(s *syntax.LineScanner) (int, bool) { |
| 32 | if isQuote(s.Peek(0)) { |
| 33 | return 0, true |
| 34 | } |
| 35 | for _, candidate := range literalPrefixes { |
| 36 | length := len(candidate.prefix) |
| 37 | if s.HasPrefix(0, candidate.prefix) && containsRune(candidate.quotes, s.Peek(length)) { |
| 38 | return length, true |
| 39 | } |
| 40 | } |
| 41 | return 0, false |
| 42 | } |
| 43 | |
| 44 | // isQuote reports whether a rune opens a literal on its own. |
| 45 | func isQuote(r rune) bool { return r == '"' || r == '\'' } |
| 46 | |
| 47 | // containsRune reports whether a set of runes, written as a string, holds one. |
| 48 | func containsRune(set string, r rune) bool { |
| 49 | for _, member := range set { |
| 50 | if member == r { |
| 51 | return true |
| 52 | } |
| 53 | } |
| 54 | return false |
| 55 | } |
| 56 | |
| 57 | // takeLiteral colours a quoted literal from its prefix to its closing quote, or |
| 58 | // to the end of the line when it has none. |
| 59 | // |
| 60 | // An unterminated literal is coloured to the end of the line and left there. In |
| 61 | // most languages that is a tolerance; in MoonBit it is the rule, because "a |
| 62 | // newline before the closing quote reports an unterminated string literal" — so |
| 63 | // a line ending inside a literal is broken source, and carrying the colour onto |
| 64 | // the next line would paint the rest of the file for one stray quote. |
| 65 | // |
| 66 | // An interpolated \{expression} is *not* scanned as code. The grammar matches |
| 67 | // it to "the matching }", with braces inside nested literals not counting, so |
| 68 | // finding where one ends needs the parser rather than the scanner; and a brace |
| 69 | // counter that got it wrong would end the string early, which is the loudest |
| 70 | // way a highlighter can be broken. One flat run is the honest answer, and it is |
| 71 | // the one Turbo Python gives an f-string for the same reason. |
| 72 | func takeLiteral(s *syntax.LineScanner) { |
| 73 | start := s.Pos() |
| 74 | |
| 75 | prefix, _ := literalPrefixLength(s) |
| 76 | s.Advance(prefix) |
| 77 | |
| 78 | quote := s.Peek(0) |
| 79 | s.Advance(1) |
| 80 | consumeLiteral(s, quote) |
| 81 | |
| 82 | s.Emit(start, s.Pos(), classOfQuote(quote)) |
| 83 | } |
| 84 | |
| 85 | // classOfQuote says which class a literal gets from the quote that opened it. |
| 86 | // |
| 87 | // The apostrophe forms — 'c' and b'x' — are characters; the double-quoted |
| 88 | // forms — "s", b"s" and re"s" — are strings. A regex literal is a string |
| 89 | // rather than a class of its own: the closed Class set has no regex, and a |
| 90 | // regex is a string with a second reader. |
| 91 | func classOfQuote(quote rune) syntax.Class { |
| 92 | if quote == '\'' { |
| 93 | return syntax.ClassChar |
| 94 | } |
| 95 | return syntax.ClassString |
| 96 | } |
| 97 | |
| 98 | // consumeLiteral runs to the closing quote, or to the end of the line. |
| 99 | // |
| 100 | // A backslash takes the rune after it out of consideration. That one rule |
| 101 | // covers every escape the language has — \n, \u{1F600}, \xFF and the \{ that |
| 102 | // opens an interpolation — because all any of them need from this scanner is |
| 103 | // that the rune after the backslash cannot close the literal. |
| 104 | func consumeLiteral(s *syntax.LineScanner, quote rune) { |
| 105 | for !s.AtEnd() { |
| 106 | switch s.Peek(0) { |
| 107 | case '\\': |
| 108 | s.Advance(2) |
| 109 | case quote: |
| 110 | s.Advance(1) |
| 111 | return |
| 112 | default: |
| 113 | s.Advance(1) |
| 114 | } |
| 115 | } |
| 116 | } |