// // JavaScriptHighlighter.swift // MongoView // // Created by Shadowfacts on 4/1/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import AppKit fileprivate let identifiers: CharacterSet = { var set = CharacterSet.alphanumerics set.insert(charactersIn: "$_") return set }() fileprivate let identifierStarts: CharacterSet = { var set = identifiers set.subtract(.decimalDigits) return set }() fileprivate let operators = CharacterSet(charactersIn: "+-*/<>=") fileprivate let expressionEnds = CharacterSet(charactersIn: ",]});") class JavaScriptHighlighter { private let text: String private var attributed: NSMutableAttributedString private var currentIndex: String.Index! private var indent = "" private(set) var tokens = [(token: TokenType, range: NSRange)]() var debug = false init(text: String) { self.text = text self.attributed = NSMutableAttributedString(attributedString: NSAttributedString(string: text)) } init(mutableAttributed: NSMutableAttributedString) { self.text = mutableAttributed.string self.attributed = mutableAttributed } private func print(_ str: @autoclosure () -> String) { if debug { Swift.print("\(indent)\(str())") } } private func range(from: String.Index, to: String.Index) -> NSRange { return NSRange(from.. NSRange { return range(from: text.index(before: currentIndex), to: currentIndex) } private func peek() -> Unicode.Scalar? { guard currentIndex < text.endIndex else { return nil } return text.unicodeScalars[currentIndex] } private func peek(length: Int) -> Substring { let realLength = min(length, text.distance(from: currentIndex, to: text.endIndex)) return text[currentIndex.. Unicode.Scalar? { let c = peek() currentIndex = text.index(after: currentIndex) return c } func highlight() { attributed.beginEditing() let fullRange = NSRange(location: 0, length: attributed.length) attributed.setAttributes([ .foregroundColor: NSColor.textColor, .font: NSFont.monospacedSystemFont(ofSize: 13, weight: .regular) ], range: fullRange) currentIndex = text.startIndex while let char = peek(), !expressionEnds.contains(char) { consumeExpression() } attributed.endEditing() } private func emit(token: TokenType, range: NSRange) { let color: NSColor switch token { case .string: color = .systemRed case .number: color = .systemBlue case .punctuation: color = .systemTeal case .identifier: return } attributed.addAttribute(.foregroundColor, value: color, range: range) tokens.append((token, range)) } private func consumeExpression() { consumeWhitespace() guard let char = peek() else { return } if identifierStarts.contains(char) { consumeIdentifier() } else if char == "'" || char == "\"" { consumeString() } else if char == "`" { consumeTemplateString() } else if CharacterSet.decimalDigits.contains(char) { consumeNumber() } else if operators.contains(char) { consumeOperator() } else if char == "(" { consumeFunctionCallOrGrouping() } else if char == "{" { consumeObject() } else if char == "[" { consumeArray() } else if char == "." { consumeDotLookup() } else if char == "?" { consumeTernaryExpression() } else { consume() } consumeWhitespace() } private func consumeWhitespace(newlines: Bool = true) { let charSet = newlines ? CharacterSet.whitespacesAndNewlines : .whitespaces while let char = peek(), charSet.contains(char) { consume() } } private func consumeIdentifier() { let identifierStart = currentIndex! while let char = peek(), identifiers.contains(char) { consume() } print("Identifier: '\(text[identifierStart..