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) }