dcf5951d10
This patch makes Splash correctly highlight strings in which a value is interpolated next to a delimiter character. The fix is to enable each `Grammar` implementation to decide whether two tokens should be *merged*, which in turn enables `SwiftGrammar` to veto that string interpolation delimiters should be merged with their predecesor.
232 lines
6.8 KiB
Swift
232 lines
6.8 KiB
Swift
/**
|
|
* Splash
|
|
* Copyright (c) John Sundell 2018
|
|
* MIT license - see LICENSE.md
|
|
*/
|
|
|
|
import Foundation
|
|
|
|
internal struct Tokenizer {
|
|
func segmentsByTokenizing(_ code: String,
|
|
using grammar: Grammar) -> AnySequence<Segment> {
|
|
return AnySequence<Segment> {
|
|
Buffer(iterator: Iterator(code: code, grammar: grammar))
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Tokenizer {
|
|
struct Buffer: IteratorProtocol {
|
|
private var iterator: Iterator
|
|
private var nextSegment: Segment?
|
|
|
|
init(iterator: Iterator) {
|
|
self.iterator = iterator
|
|
}
|
|
|
|
mutating func next() -> Segment? {
|
|
var segment = nextSegment ?? iterator.next()
|
|
nextSegment = iterator.next()
|
|
segment?.tokens.next = nextSegment?.tokens.current
|
|
return segment
|
|
}
|
|
}
|
|
|
|
struct Iterator: IteratorProtocol {
|
|
struct Component {
|
|
enum Kind {
|
|
case token
|
|
case delimiter
|
|
case whitespace
|
|
case newline
|
|
}
|
|
|
|
let character: Character
|
|
let kind: Kind
|
|
}
|
|
|
|
private let code: String
|
|
private let grammar: Grammar
|
|
private var index: String.Index?
|
|
private var tokenCounts = [String: Int]()
|
|
private var allTokens = [String]()
|
|
private var lineTokens = [String]()
|
|
private var segments: (current: Segment?, previous: Segment?)
|
|
|
|
init(code: String, grammar: Grammar) {
|
|
self.code = code
|
|
self.grammar = grammar
|
|
segments = (nil, nil)
|
|
}
|
|
|
|
mutating func next() -> Segment? {
|
|
let nextIndex = makeNextIndex()
|
|
|
|
guard nextIndex != code.endIndex else {
|
|
let segment = segments.current
|
|
segments.current = nil
|
|
return segment
|
|
}
|
|
|
|
index = nextIndex
|
|
let component = makeComponent(at: nextIndex)
|
|
|
|
switch component.kind {
|
|
case .token, .delimiter:
|
|
guard var segment = segments.current else {
|
|
segments.current = makeSegment(with: component, at: nextIndex)
|
|
return next()
|
|
}
|
|
|
|
guard segment.trailingWhitespace == nil,
|
|
component.isDelimiter == segment.currentTokenIsDelimiter else {
|
|
return finish(segment, with: component, at: nextIndex)
|
|
}
|
|
|
|
if component.isDelimiter {
|
|
let previousCharacter = segment.tokens.current.last!
|
|
let shouldMerge = grammar.isDelimiter(previousCharacter,
|
|
mergableWith: component.character)
|
|
|
|
guard shouldMerge else {
|
|
return finish(segment, with: component, at: nextIndex)
|
|
}
|
|
}
|
|
|
|
segment.tokens.current.append(component.character)
|
|
segments.current = segment
|
|
return next()
|
|
case .whitespace, .newline:
|
|
guard var segment = segments.current else {
|
|
var segment = makeSegment(with: component, at: nextIndex)
|
|
segment.trailingWhitespace = component.token
|
|
segment.isLastOnLine = component.isNewline
|
|
segments.current = segment
|
|
return next()
|
|
}
|
|
|
|
if var existingWhitespace = segment.trailingWhitespace {
|
|
existingWhitespace.append(component.character)
|
|
segment.trailingWhitespace = existingWhitespace
|
|
} else {
|
|
segment.trailingWhitespace = component.token
|
|
}
|
|
|
|
if component.isNewline {
|
|
segment.isLastOnLine = true
|
|
}
|
|
|
|
segments.current = segment
|
|
return next()
|
|
}
|
|
}
|
|
|
|
private func makeNextIndex() -> String.Index {
|
|
guard let index = index else {
|
|
return code.startIndex
|
|
}
|
|
|
|
return code.index(after: index)
|
|
}
|
|
|
|
private func makeComponent(at index: String.Index) -> Component {
|
|
func kind(for character: Character) -> Component.Kind {
|
|
if character.isWhitespace {
|
|
return .whitespace
|
|
}
|
|
|
|
if character.isNewline {
|
|
return .newline
|
|
}
|
|
|
|
if grammar.delimiters.contains(character) {
|
|
return .delimiter
|
|
}
|
|
|
|
return .token
|
|
}
|
|
|
|
let character = code[index]
|
|
|
|
return Component(
|
|
character: character,
|
|
kind: kind(for: character)
|
|
)
|
|
}
|
|
|
|
private func makeSegment(with component: Component, at index: String.Index) -> Segment {
|
|
let tokens = Segment.Tokens(
|
|
all: allTokens,
|
|
counts: tokenCounts,
|
|
onSameLine: lineTokens,
|
|
previous: segments.current?.tokens.current,
|
|
current: component.token,
|
|
next: nil
|
|
)
|
|
|
|
return Segment(
|
|
prefix: code[..<index],
|
|
tokens: tokens,
|
|
trailingWhitespace: nil,
|
|
currentTokenIsDelimiter: component.isDelimiter,
|
|
isLastOnLine: false
|
|
)
|
|
}
|
|
|
|
private mutating func finish(_ segment: Segment,
|
|
with component: Component,
|
|
at index: String.Index) -> Segment {
|
|
var count = tokenCounts[segment.tokens.current] ?? 0
|
|
count += 1
|
|
tokenCounts[segment.tokens.current] = count
|
|
|
|
allTokens.append(segment.tokens.current)
|
|
|
|
if segment.isLastOnLine {
|
|
lineTokens = []
|
|
} else {
|
|
lineTokens.append(segment.tokens.current)
|
|
}
|
|
|
|
segments.previous = segment
|
|
segments.current = makeSegment(with: component, at: index)
|
|
|
|
return segment
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Tokenizer.Iterator.Component {
|
|
var token: String {
|
|
return String(character)
|
|
}
|
|
|
|
var isDelimiter: Bool {
|
|
switch kind {
|
|
case .token, .whitespace, .newline:
|
|
return false
|
|
case .delimiter:
|
|
return true
|
|
}
|
|
}
|
|
|
|
var isNewline: Bool {
|
|
switch kind {
|
|
case .token, .whitespace, .delimiter:
|
|
return false
|
|
case .newline:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Character {
|
|
var isWhitespace: Bool {
|
|
return CharacterSet.whitespaces.contains(self)
|
|
}
|
|
|
|
var isNewline: Bool {
|
|
return CharacterSet.newlines.contains(self)
|
|
}
|
|
}
|