├── Sources └── Highlight │ ├── Syntax.swift │ ├── JsonTokenizerBehaviour.swift │ ├── Tokenizer.swift │ ├── Font.swift │ ├── Array+CompactAppend.swift │ ├── JsonTokenizerError.swift │ ├── Terminator.swift │ ├── JsonToken.swift │ ├── Color.swift │ ├── SyntaxHightlightProvider.swift │ ├── DefaultJsonSyntaxHighlightingTheme.swift │ ├── StringScanner.swift │ ├── JsonSyntaxHighlightingTheme.swift │ ├── Color+Json.swift │ ├── JsonSyntaxHighlightProvider.swift │ ├── StringParser.swift │ └── JsonTokenizer.swift ├── Tests ├── LinuxMain.swift └── HighlightTests │ ├── XCTestManifests.swift │ └── HighlightTests.swift ├── .github ├── workflows │ └── ci.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── Package.swift ├── LICENSE ├── README.md ├── PULL_REQUEST_TEMPLATE.md └── .gitignore /Sources/Highlight/Syntax.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Syntax { 4 | case json 5 | case other(identifier: String) 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Highlight/JsonTokenizerBehaviour.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum JsonTokenizerBehaviour { 4 | case strict 5 | case lenient 6 | } 7 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import HighlightTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += HighlightTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Sources/Highlight/Tokenizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Tokenizer { 4 | associatedtype TToken 5 | 6 | func tokenize(_ text: String) -> [TToken] 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Highlight/Font.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | public typealias Font = UIFont 5 | 6 | #elseif canImport(AppKit) 7 | 8 | import AppKit 9 | public typealias Font = NSFont 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Tests/HighlightTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(HighlightTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Sources/Highlight/Array+CompactAppend.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal extension Array { 4 | mutating func compactAppend(_ element: Element?) { 5 | if let element = element { 6 | append(element) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: macOS-latest 6 | steps: 7 | 8 | - name: Checkout 9 | uses: actions/checkout@v1 10 | 11 | - name: Build 12 | run: swift build -v 13 | 14 | - name: Test 15 | run: swift test -v 16 | -------------------------------------------------------------------------------- /Sources/Highlight/JsonTokenizerError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum JsonTokenizerError: Error { 4 | case invalidSymbol(expected: Character?, actual: Character?) 5 | case expectedSymbol 6 | case unexpectedSymbol(description: String) 7 | case unenclosedQuotationMarks 8 | case invalidProperty 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Highlight/Terminator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal struct Terminator { 4 | public var endingCharacter: Character? 5 | } 6 | 7 | internal extension Terminator { 8 | static var end: Terminator { 9 | return Terminator(endingCharacter: nil) 10 | } 11 | } 12 | 13 | extension Terminator: ExpressibleByUnicodeScalarLiteral { 14 | typealias UnicodeScalarLiteralType = Character 15 | 16 | init(unicodeScalarLiteral value: Character) { 17 | self.init(endingCharacter: value) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? 11 | 12 | 13 | ## Describe the solution you'd like 14 | 15 | 16 | ## Describe alternatives you've considered 17 | 18 | 19 | ## Additional context 20 | 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Highlight", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15) 11 | ], 12 | products: [ 13 | .library( 14 | name: "Highlight", 15 | targets: ["Highlight"]), 16 | ], 17 | targets: [ 18 | .target( 19 | name: "Highlight", 20 | dependencies: []), 21 | .testTarget( 22 | name: "HighlightTests", 23 | dependencies: ["Highlight"]), 24 | ], 25 | swiftLanguageVersions: [ 26 | .v5 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Sources/Highlight/JsonToken.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A specific part of JSON data with the associated value and location in the original data 4 | public enum JsonToken { 5 | /// Keys 6 | case memberKey(NSRange) 7 | 8 | /// Whitespace 9 | case whitespace(NSRange) 10 | 11 | /// Operator like '{', '[' or ':' 12 | case `operator`(NSRange) 13 | 14 | /// String 15 | case stringValue(NSRange) 16 | 17 | /// Number 18 | case numericValue(NSRange) 19 | 20 | /// Literal like 'true', 'false' or 'null' 21 | case literal(NSRange) 22 | 23 | /// This will contain the range after the parsing failed with the associated error 24 | case unknown(NSRange, JsonTokenizerError) 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior 21 | 22 | 23 | ## Screenshots 24 | 25 | 26 | ## Platform 27 | 28 | - Device: 29 | - OS: 30 | - Version of this library: 31 | 32 | ## Additional context 33 | 34 | -------------------------------------------------------------------------------- /Sources/Highlight/Color.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | public typealias Color = UIColor 5 | 6 | internal extension UIColor { 7 | convenience init(dynamicProvider: @escaping (UserInterfaceStyle) -> Color) { 8 | self.init { traitCollection -> Color in 9 | dynamicProvider(traitCollection.userInterfaceStyle == .dark ? .dark : .light) 10 | } 11 | } 12 | } 13 | 14 | #elseif canImport(AppKit) 15 | 16 | import AppKit 17 | public typealias Color = NSColor 18 | 19 | fileprivate extension NSAppearance { 20 | var isDark: Bool { 21 | bestMatch(from: [.darkAqua, .aqua]) == .darkAqua 22 | } 23 | } 24 | 25 | internal extension Color { 26 | convenience init(dynamicProvider: @escaping (UserInterfaceStyle) -> Color) { 27 | self.init(name: nil) { appearance -> Color in 28 | dynamicProvider(appearance.isDark ? .dark : .light) 29 | } 30 | } 31 | } 32 | 33 | #endif 34 | 35 | internal enum UserInterfaceStyle { 36 | case light 37 | case dark 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Niclas Kristek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Highlight 2 | 3 | [![CI Status](https://github.com/nkristek/Highlight/workflows/CI/badge.svg)](https://github.com/nkristek/Highlight/actions) 4 | 5 | This library provides a syntax highlighter which currently supports highlighting JSON data. It is fully written in Swift and there are no additional dependencies. 6 | It uses a compatibility layer for colors and fonts for `UIKit` and `AppKit` and the default theme supports dark mode on both platforms. 7 | 8 | ## Features 9 | 10 | This library provides a `JsonSyntaxHighlightProvider` which can either be instanciated or accessed through a static instance `JsonSyntaxHighlightProvider.shared`. 11 | 12 | It parses the given `String` and highlights the given `NSMutableAttributedString` by setting the color and font on specific areas. 13 | 14 | ## Installation 15 | 16 | Currently supported methods of importing it: 17 | - SwiftPM: https://github.com/nkristek/Highlight.git 18 | - Manually copying all files in the [Sources](https://github.com/nkristek/Highlight/tree/master/Sources) folder 19 | 20 | ## Contribution 21 | 22 | If you find a bug feel free to open an issue. Contributions are also appreciated. 23 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | ## Motivation and Context 5 | 6 | 7 | 8 | ## Details 9 | 10 | 11 | ## Types of changes 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist: 18 | 19 | 20 | - [ ] My code follows the code style of this project. 21 | - [ ] My change requires a change to the documentation. 22 | - [ ] I have updated the documentation accordingly. 23 | - [ ] I have written/altered unit tests for the changes. 24 | - [ ] Only necessary files have been added/altered. 25 | -------------------------------------------------------------------------------- /Sources/Highlight/SyntaxHightlightProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A provider for coloring/formatting text according to a specific syntax 4 | public protocol SyntaxHighlightProvider { 5 | 6 | /// Create an attributed string from the given `String` which contains the highlighted text. 7 | /// 8 | /// - parameters: 9 | /// - text: The `String` that should be highlighted. 10 | /// - syntaxIdentifier: The syntax that should be used to highlight the given `String` 11 | func highlight(_ text: String, as syntax: Syntax) -> NSAttributedString 12 | 13 | /// Modify the given `NSMutableAttributedString` to be highlighted according to the syntax. 14 | /// 15 | /// - parameters: 16 | /// - attributedText: The `NSMutableAttributedString` that should be highlighted. 17 | /// - syntaxIdentifier: The syntax that should be used to highlight the given `String` 18 | func highlight(_ attributedText: NSMutableAttributedString, as syntax: Syntax) 19 | } 20 | 21 | public extension SyntaxHighlightProvider { 22 | func highlight(_ text: String, as syntax: Syntax) -> NSAttributedString { 23 | let result = NSMutableAttributedString(string: text) 24 | highlight(result, as: syntax) 25 | return result 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Highlight/DefaultJsonSyntaxHighlightingTheme.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | 4 | public struct DefaultJsonSyntaxHighlightingTheme: JsonSyntaxHighlightingTheme { 5 | 6 | public init(fontSize size: CGFloat = 13) { 7 | whitespaceFont = .monospacedSystemFont(ofSize: size, weight: .medium) 8 | operatorFont = .monospacedSystemFont(ofSize: size, weight: .medium) 9 | numericValueFont = .monospacedSystemFont(ofSize: size, weight: .medium) 10 | stringValueFont = .monospacedSystemFont(ofSize: size, weight: .medium) 11 | literalFont = .monospacedSystemFont(ofSize: size, weight: .bold) 12 | unknownFont = .monospacedSystemFont(ofSize: size, weight: .medium) 13 | } 14 | 15 | public var memberKeyColor: Color = .jsonMemberKeyColor 16 | 17 | public var whitespaceColor: Color = .jsonOperatorColor 18 | 19 | public var whitespaceFont: Font 20 | 21 | public var operatorColor: Color = .jsonOperatorColor 22 | 23 | public var operatorFont: Font 24 | 25 | public var numericValueColor: Color = .jsonNumberColor 26 | 27 | public var numericValueFont: Font 28 | 29 | public var stringValueColor: Color = .jsonStringColor 30 | 31 | public var stringValueFont: Font 32 | 33 | public var literalColor: Color = .jsonLiteralColor 34 | 35 | public var literalFont: Font 36 | 37 | public var unknownColor: Color = .jsonOperatorColor 38 | 39 | public var unknownFont: Font 40 | } 41 | -------------------------------------------------------------------------------- /Tests/HighlightTests/HighlightTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Highlight 3 | 4 | final class HighlightTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(Highlight().text, "Hello, World!") 10 | 11 | // let jsonData = 12 | //#""" 13 | //{ 14 | // "widget": { 15 | // "debug": "on", 16 | // "window": { 17 | // "title": "Sample Konfabulator Widget", 18 | // "name": "main_window", 19 | // "width": 500, 20 | // "height": 500 21 | // }, 22 | // "image": { 23 | // "src": "Images/Sun.png", 24 | // "name": "sun1", 25 | // "hOffset": 250, 26 | // "vOffset": 250, 27 | // "alignment": "center" 28 | // }, 29 | // "text": { 30 | // "data": "Click Here", 31 | // "size": 36, 32 | // "style": "bold", 33 | // "name": "text1", 34 | // "hOffset": 250, 35 | // "vOffset": 100, 36 | // "alignment": "center", 37 | // "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" 38 | // } 39 | // } 40 | //} 41 | //"""# 42 | // let highlighter = JsonSyntaxHighlightProvider() 43 | // let highlightedText = highlighter.highlight(jsonData, as: "json") 44 | } 45 | 46 | static var allTests = [ 47 | ("testExample", testExample), 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Highlight/StringScanner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal protocol StringScanner { 4 | var currentIndex: Int { get } 5 | 6 | var isAtEnd: Bool { get } 7 | 8 | var string: NSString { get } 9 | 10 | @discardableResult 11 | mutating func scanCharacter() -> (Character, NSRange)? 12 | 13 | @discardableResult 14 | mutating func scanCharacter(_ character: Character) -> NSRange? 15 | 16 | @discardableResult 17 | mutating func scanCharacter(from set: CharacterSet) -> (Character, NSRange)? 18 | 19 | @discardableResult 20 | mutating func scanCharacters(from set: CharacterSet) -> (String, NSRange)? 21 | 22 | @discardableResult 23 | mutating func scanCharacters(until terminator: Terminator) -> (String, NSRange)? 24 | 25 | @discardableResult 26 | mutating func scanUpToCharacters(from set: CharacterSet) -> (String, NSRange)? 27 | 28 | @discardableResult 29 | mutating func scanString(_ string: String) -> NSRange? 30 | 31 | func peekCharacter() -> (Character, NSRange)? 32 | 33 | func peekCharacter(_ character: Character) -> NSRange? 34 | 35 | func peekCharacter(from set: CharacterSet) -> (Character, NSRange)? 36 | 37 | func peekCharacters(from set: CharacterSet) -> (String, NSRange)? 38 | 39 | func peekCharacters(until terminator: Terminator) -> (String, NSRange)? 40 | 41 | func peekUpToCharacters(from set: CharacterSet) -> (String, NSRange)? 42 | 43 | func peekString(_ string: String) -> NSRange? 44 | 45 | func peekCharacter(afterCharactersFrom characterSet: CharacterSet) -> (Character, NSRange)? 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Highlight/JsonSyntaxHighlightingTheme.swift: -------------------------------------------------------------------------------- 1 | /// Provides colors and fonts for displaying highlighted JSON data 2 | public protocol JsonSyntaxHighlightingTheme { 3 | 4 | // MARK: - Whitespace 5 | 6 | /// The color for whitespace 7 | var whitespaceColor: Color { get } 8 | 9 | /// The font for whitespace 10 | var whitespaceFont: Font { get } 11 | 12 | // MARK: - Members 13 | 14 | /// The color for keys 15 | var memberKeyColor: Color { get } 16 | 17 | // MARK: - Operators 18 | 19 | /// The color for operators like '{', '[' or ':' 20 | var operatorColor: Color { get } 21 | 22 | /// The font for operators like '{', '[' or ':' 23 | var operatorFont: Font { get } 24 | 25 | // MARK: - Numeric values 26 | 27 | /// The color for numbers 28 | var numericValueColor: Color { get } 29 | 30 | /// The color for numbers 31 | var numericValueFont: Font { get } 32 | 33 | // MARK: - String values 34 | 35 | /// The color for string values 36 | var stringValueColor: Color { get } 37 | 38 | /// The font for string values 39 | var stringValueFont: Font { get } 40 | 41 | // MARK: - Literals 42 | 43 | /// The color for literals like 'true', 'false' or 'null' 44 | var literalColor: Color { get } 45 | 46 | /// The font for literals like 'true', 'false' or 'null' 47 | var literalFont: Font { get } 48 | 49 | /// The color for text which could not be parsed correctly 50 | var unknownColor: Color { get } 51 | 52 | /// The font for text which could not be parsed correctly 53 | var unknownFont: Font { get } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Highlight/Color+Json.swift: -------------------------------------------------------------------------------- 1 | internal extension Color { 2 | class var jsonOperatorColor: Color { 3 | Color { (style: UserInterfaceStyle) -> Color in 4 | switch style { 5 | case .dark: 6 | return Color(hue: 0, saturation: 0, brightness: 1, alpha: 0.85) 7 | default: 8 | return Color(hue: 0, saturation: 0, brightness: 0, alpha: 0.85) 9 | } 10 | } 11 | } 12 | 13 | class var jsonNumberColor: Color { 14 | Color { (style: UserInterfaceStyle) -> Color in 15 | switch style { 16 | case .dark: 17 | return Color(hue: 50/360, saturation: 0.49, brightness: 0.81, alpha: 1) 18 | default: 19 | return Color(hue: 248/360, saturation: 1, brightness: 0.81, alpha: 1) 20 | } 21 | } 22 | } 23 | 24 | class var jsonStringColor: Color { 25 | Color { (style: UserInterfaceStyle) -> Color in 26 | switch style { 27 | case .dark: 28 | return Color(hue: 5/360, saturation: 0.63, brightness: 0.99, alpha: 1) 29 | default: 30 | return Color(hue: 1/360, saturation: 0.89, brightness: 0.77, alpha: 1) 31 | } 32 | } 33 | } 34 | 35 | class var jsonLiteralColor: Color { 36 | Color { (style: UserInterfaceStyle) -> Color in 37 | switch style { 38 | case .dark: 39 | return Color(hue: 334/360, saturation: 0.62, brightness: 0.99, alpha: 1) 40 | default: 41 | return Color(hue: 304/360, saturation: 0.77, brightness: 0.61, alpha: 1) 42 | } 43 | } 44 | } 45 | 46 | class var jsonMemberKeyColor: Color { 47 | Color { (style: UserInterfaceStyle) -> Color in 48 | switch style { 49 | case .dark: 50 | return Color(hue: 234/360, saturation: 0.62, brightness: 0.99, alpha: 1) 51 | default: 52 | return Color(hue: 204/360, saturation: 0.77, brightness: 0.61, alpha: 1) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Sources/Highlight/JsonSyntaxHighlightProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A syntax highlighter which supports highlighting JSON data 4 | open class JsonSyntaxHighlightProvider: SyntaxHighlightProvider { 5 | 6 | /// A static instance of this provider 7 | public static let shared: JsonSyntaxHighlightProvider = JsonSyntaxHighlightProvider() 8 | 9 | /// Initialize an instance of the `JsonSyntaxHighlightProvider` class with a theme 10 | public init(theme: JsonSyntaxHighlightingTheme? = nil) { 11 | self.theme = theme ?? DefaultJsonSyntaxHighlightingTheme() 12 | } 13 | 14 | /// The theme which will be used to highlight the JSON data 15 | open var theme: JsonSyntaxHighlightingTheme 16 | 17 | /// Modify the given `NSMutableAttributedString` to be highlighted according to the syntax. It will be parsed using the `JsonTokenizerBehaviour.lenient` setting. 18 | /// 19 | /// - parameters: 20 | /// - attributedText: The `NSMutableAttributedString` that should be highlighted. 21 | /// - syntax: The syntax that should be used to highlight the given `String`. 22 | open func highlight(_ attributedText: NSMutableAttributedString, as syntax: Syntax) { 23 | guard case .json = syntax else { 24 | debugPrint("Highlighting '\(syntax)' is not supported. Supported ones are: 'json'") 25 | return 26 | } 27 | 28 | let tokenizer = JsonTokenizer(behaviour: .lenient) 29 | let tokens = tokenizer.tokenize(attributedText.string) 30 | highlightJson(attributedText, tokens: tokens) 31 | } 32 | 33 | /// Modify the given `NSMutableAttributedString` to be highlighted according to the syntax. 34 | /// 35 | /// - parameters: 36 | /// - attributedText: The `NSMutableAttributedString` that should be highlighted. 37 | /// - syntax: The syntax that should be used to highlight the given `String`. 38 | /// - behaviour: The behaviour when parsing the JSON data. 39 | open func highlight(_ attributedText: NSMutableAttributedString, as syntax: Syntax, behaviour: JsonTokenizerBehaviour) { 40 | guard case .json = syntax else { 41 | debugPrint("Highlighting '\(syntax)' is not supported. Supported ones are: 'json'") 42 | return 43 | } 44 | 45 | let tokenizer = JsonTokenizer(behaviour: behaviour) 46 | let tokens = tokenizer.tokenize(attributedText.string) 47 | highlightJson(attributedText, tokens: tokens) 48 | } 49 | 50 | /// Modify the given `NSMutableAttributedString` to be highlighted according to the given `tokens`. 51 | /// 52 | /// - parameters: 53 | /// - attributedText: The `NSMutableAttributedString` that should be highlighted. 54 | /// - tokens: The tokens that should be used to highlight the given `String`. 55 | open func highlightJson(_ attributedText: NSMutableAttributedString, tokens: [JsonToken]) { 56 | attributedText.addAttributes([ .foregroundColor : theme.unknownColor, .font : theme.unknownFont ], range: NSRange(location: 0, length: attributedText.length)) 57 | 58 | for token in tokens { 59 | switch token { 60 | case .whitespace(let range): 61 | attributedText.setAttributes([ .foregroundColor : theme.whitespaceColor, .font : theme.whitespaceFont], range: range) 62 | case .operator(let range): 63 | attributedText.setAttributes([ .foregroundColor : theme.operatorColor, .font : theme.operatorFont ], range: range) 64 | case .stringValue(let range): 65 | attributedText.setAttributes([ .foregroundColor : theme.stringValueColor, .font : theme.stringValueFont ], range: range) 66 | case .numericValue(let range): 67 | attributedText.setAttributes([ .foregroundColor : theme.numericValueColor, .font : theme.numericValueFont ], range: range) 68 | case .literal(let range): 69 | attributedText.setAttributes([ .foregroundColor : theme.literalColor, .font : theme.literalFont ], range: range) 70 | case .memberKey(let range): 71 | attributedText.setAttributes([ .foregroundColor : theme.memberKeyColor, .font : theme.literalFont ], range: range) 72 | case .unknown(_, _): 73 | return 74 | } 75 | } 76 | } 77 | } 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Sources/Highlight/StringParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal struct StringParser: StringScanner { 4 | 5 | internal var currentIndex: Int 6 | 7 | internal var isAtEnd: Bool { 8 | currentIndex >= string.length 9 | } 10 | 11 | internal private(set) var string: NSString 12 | 13 | internal init(string: NSString) { 14 | self.string = string 15 | self.currentIndex = 0 16 | } 17 | 18 | @discardableResult 19 | internal mutating func scanCharacter() -> (Character, NSRange)? { 20 | guard let (nextCharacterPreview, nextCharacterRange) = peekCharacter() else { return nil } 21 | currentIndex += nextCharacterRange.length 22 | return (nextCharacterPreview, nextCharacterRange) 23 | } 24 | 25 | @discardableResult 26 | internal mutating func scanCharacter(_ character: Character) -> NSRange? { 27 | guard let characterRange = peekCharacter(character) else { return nil } 28 | currentIndex += characterRange.length 29 | return characterRange 30 | } 31 | 32 | @discardableResult 33 | internal mutating func scanCharacter(from set: CharacterSet) -> (Character, NSRange)? { 34 | guard let (characterPreview, characterRange) = peekCharacter(from: set) else { return nil } 35 | currentIndex += characterRange.length 36 | return (characterPreview, characterRange) 37 | } 38 | 39 | @discardableResult 40 | internal mutating func scanCharacters(from set: CharacterSet) -> (String, NSRange)? { 41 | guard let (charactersPreview, charactersRange) = peekCharacters(from: set) else { return nil } 42 | currentIndex += charactersRange.length 43 | return (charactersPreview, charactersRange) 44 | } 45 | 46 | @discardableResult 47 | internal mutating func scanCharacters(until terminator: Terminator) -> (String, NSRange)? { 48 | guard let (charactersPreview, charactersRange) = peekCharacters(until: terminator) else { return nil } 49 | currentIndex += charactersRange.length 50 | return (charactersPreview, charactersRange) 51 | } 52 | 53 | @discardableResult 54 | internal mutating func scanUpToCharacters(from set: CharacterSet) -> (String, NSRange)? { 55 | guard let (charactersPreview, charactersRange) = peekUpToCharacters(from: set) else { return nil } 56 | currentIndex += charactersRange.length 57 | return (charactersPreview, charactersRange) 58 | } 59 | 60 | @discardableResult 61 | internal mutating func scanString(_ searchString: String) -> NSRange? { 62 | guard let stringRange = peekString(searchString) else { return nil } 63 | currentIndex += stringRange.length 64 | return stringRange 65 | } 66 | 67 | internal func peekCharacter() -> (Character, NSRange)? { 68 | guard !isAtEnd else { return nil } 69 | let rangeOfNextCharacter = string.rangeOfComposedCharacterSequence(at: currentIndex) 70 | let charString = string.substring(with: rangeOfNextCharacter) 71 | guard 72 | charString.count == 1, 73 | let char = charString.first 74 | else { return nil } 75 | return (char, rangeOfNextCharacter) 76 | } 77 | 78 | internal func peekCharacter(_ character: Character) -> NSRange? { 79 | guard !isAtEnd else { return nil } 80 | let rangeOfNextCharacter = string.rangeOfComposedCharacterSequence(at: currentIndex) 81 | let charString = string.substring(with: rangeOfNextCharacter) 82 | guard 83 | charString.count == 1, 84 | let char = charString.first, 85 | char == character 86 | else { return nil } 87 | return rangeOfNextCharacter 88 | } 89 | 90 | internal func peekCharacter(from set: CharacterSet) -> (Character, NSRange)? { 91 | guard !isAtEnd else { return nil } 92 | let rangeOfNextCharacter = string.rangeOfComposedCharacterSequence(at: currentIndex) 93 | let charString = string.substring(with: rangeOfNextCharacter) 94 | guard 95 | charString.count == 1, 96 | let char = charString.first, 97 | char.unicodeScalars.allSatisfy({ set.contains($0) }) 98 | else { return nil } 99 | return (char, rangeOfNextCharacter) 100 | } 101 | 102 | internal func peekCharacters(from set: CharacterSet) -> (String, NSRange)? { 103 | guard !isAtEnd else { return nil } 104 | let indexBefore = currentIndex 105 | var result = "" 106 | var index = currentIndex 107 | 108 | while index < string.length { 109 | let rangeOfNextCharacter = string.rangeOfComposedCharacterSequence(at: index) 110 | let charString = string.substring(with: rangeOfNextCharacter) 111 | guard 112 | charString.count == 1, 113 | let char = charString.first, 114 | char.unicodeScalars.allSatisfy({ set.contains($0) }) 115 | else { break } 116 | index += rangeOfNextCharacter.length 117 | result.append(char) 118 | } 119 | 120 | return index > indexBefore ? (result, NSRange(location: indexBefore, length: index - indexBefore)) : nil 121 | } 122 | 123 | func peekCharacters(until terminator: Terminator) -> (String, NSRange)? { 124 | guard !isAtEnd else { return nil } 125 | let indexBefore = currentIndex 126 | var result = "" 127 | var index = currentIndex 128 | 129 | while index < string.length { 130 | let rangeOfNextCharacter = string.rangeOfComposedCharacterSequence(at: index) 131 | let charString = string.substring(with: rangeOfNextCharacter) 132 | guard 133 | charString.count == 1, 134 | let char = charString.first, 135 | char != terminator.endingCharacter 136 | else { break } 137 | index += rangeOfNextCharacter.length 138 | result.append(char) 139 | } 140 | 141 | return index > indexBefore ? (result, NSRange(location: indexBefore, length: index - indexBefore)) : nil 142 | } 143 | 144 | internal func peekUpToCharacters(from set: CharacterSet) -> (String, NSRange)? { 145 | guard !isAtEnd else { return nil } 146 | let indexBefore = currentIndex 147 | var result = "" 148 | var index = currentIndex 149 | 150 | while index < string.length { 151 | let rangeOfNextCharacter = string.rangeOfComposedCharacterSequence(at: index) 152 | let charString = string.substring(with: rangeOfNextCharacter) 153 | guard 154 | charString.count == 1, 155 | let char = charString.first, 156 | !char.unicodeScalars.allSatisfy({ set.contains($0) }) 157 | else { break } 158 | index += rangeOfNextCharacter.length 159 | result.append(char) 160 | } 161 | 162 | return index > indexBefore ? (result, NSRange(location: indexBefore, length: index - indexBefore)) : nil 163 | } 164 | 165 | internal func peekString(_ searchString: String) -> NSRange? { 166 | let nsSearchStringLength = (searchString as NSString).length 167 | guard currentIndex + nsSearchStringLength <= string.length else { return nil } 168 | let searchRange = NSRange(location: currentIndex, length: nsSearchStringLength) 169 | let scannedString = string.substring(with: searchRange) 170 | guard scannedString == searchString else { return nil } 171 | return searchRange 172 | } 173 | 174 | internal func peekCharacter(afterCharactersFrom set: CharacterSet) -> (Character, NSRange)? { 175 | guard let (_, rangeOfExcludedCharacters) = peekCharacters(from: set) else { 176 | return peekCharacter() 177 | } 178 | guard rangeOfExcludedCharacters.location + rangeOfExcludedCharacters.length < string.length else { return nil } 179 | let rangeOfNextCharacter = string.rangeOfComposedCharacterSequence(at: rangeOfExcludedCharacters.location + rangeOfExcludedCharacters.length) 180 | let charString = string.substring(with: rangeOfNextCharacter) 181 | guard 182 | charString.count == 1, 183 | let char = charString.first 184 | else { return nil } 185 | return (char, rangeOfNextCharacter) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Sources/Highlight/JsonTokenizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct JsonTokenizer: Tokenizer { 4 | public typealias TToken = JsonToken 5 | 6 | public init(behaviour: JsonTokenizerBehaviour) { 7 | self.behaviour = behaviour 8 | } 9 | 10 | public var behaviour: JsonTokenizerBehaviour 11 | 12 | public func tokenize(_ text: String) -> [JsonToken] { 13 | var scanner: StringScanner = StringParser(string: text as NSString) 14 | return parseElement(&scanner) 15 | } 16 | 17 | private func parseElement(_ scanner: inout StringScanner) -> [JsonToken] { 18 | var tokens = [JsonToken]() 19 | 20 | tokens.compactAppend(parseWhitespace(&scanner)) 21 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 22 | return tokens 23 | } 24 | 25 | tokens += parseValue(&scanner) 26 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 27 | return tokens 28 | } 29 | 30 | tokens.compactAppend(parseWhitespace(&scanner)) 31 | return tokens 32 | } 33 | 34 | private func parseWhitespace(_ scanner: inout StringScanner) -> JsonToken? { 35 | if let (_, range) = scanner.scanCharacters(from: .whitespacesAndNewlines) { 36 | return .whitespace(range) 37 | } 38 | return nil 39 | } 40 | 41 | private func parseValue(_ scanner: inout StringScanner) -> [JsonToken] { 42 | var range = NSRange(location: scanner.currentIndex, length: 0) 43 | 44 | guard let (nextCharacterPreview, _) = scanner.peekCharacter() else { 45 | return [ .unknown(range, .expectedSymbol) ] 46 | } 47 | 48 | switch nextCharacterPreview { 49 | case "{": 50 | return parseObject(&scanner) 51 | case "[": 52 | return parseArray(&scanner) 53 | case "\"": 54 | return [ parseString(&scanner) ] 55 | case "0"..."9", "-": 56 | return [ parseNumber(&scanner) ] 57 | case "t", "f", "n": 58 | return [ parseLiteral(&scanner) ] 59 | default: 60 | range.length = 1 61 | return [ .unknown(range, .unexpectedSymbol(description: "Expected the start of a value, got '\(nextCharacterPreview)'")) ] 62 | } 63 | } 64 | 65 | private func parseObject(_ scanner: inout StringScanner) -> [JsonToken] { 66 | var tokens = [JsonToken]() 67 | 68 | let openingCurlyBraceToken = parseOperator(&scanner, operator: "{") 69 | tokens.append(openingCurlyBraceToken) 70 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 71 | return tokens 72 | } 73 | 74 | if let (characterAfterWhitespacePreview, _) = scanner.peekCharacter(afterCharactersFrom: .whitespacesAndNewlines), characterAfterWhitespacePreview != "}" { 75 | tokens += parseMembers(&scanner, until: "}") 76 | } else { 77 | tokens.compactAppend(parseWhitespace(&scanner)) 78 | } 79 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 80 | return tokens 81 | } 82 | 83 | let closingCurlyBraceToken = parseOperator(&scanner, operator: "}") 84 | tokens.append(closingCurlyBraceToken) 85 | return tokens 86 | } 87 | 88 | private func parseMembers(_ scanner: inout StringScanner, until terminator: Terminator) -> [JsonToken] { 89 | var tokens = [JsonToken]() 90 | 91 | tokens += parseMember(&scanner) 92 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 93 | return tokens 94 | } 95 | 96 | while let (nextCharacterPreview, _) = scanner.peekCharacter(), nextCharacterPreview != terminator.endingCharacter { 97 | tokens.append(parseOperator(&scanner, operator: ",")) 98 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 99 | return tokens 100 | } 101 | 102 | tokens += parseMember(&scanner) 103 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 104 | return tokens 105 | } 106 | } 107 | 108 | return tokens 109 | } 110 | 111 | private func parseMember(_ scanner: inout StringScanner) -> [JsonToken] { 112 | var tokens = [JsonToken]() 113 | 114 | tokens.compactAppend(parseWhitespace(&scanner)) 115 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 116 | return tokens 117 | } 118 | 119 | tokens.append(parseMemberKey(&scanner)) 120 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 121 | return tokens 122 | } 123 | 124 | tokens.compactAppend(parseWhitespace(&scanner)) 125 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 126 | return tokens 127 | } 128 | 129 | tokens.append(parseOperator(&scanner, operator: ":")) 130 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 131 | return tokens 132 | } 133 | 134 | tokens += parseElement(&scanner) 135 | return tokens 136 | } 137 | 138 | private func parseArray(_ scanner: inout StringScanner) -> [JsonToken] { 139 | var tokens = [JsonToken]() 140 | 141 | tokens.append(parseOperator(&scanner, operator: "[")) 142 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 143 | return tokens 144 | } 145 | 146 | if let (nextCharacterPreview, _) = scanner.peekCharacter(afterCharactersFrom: .whitespacesAndNewlines), nextCharacterPreview != "]" { 147 | tokens += parseElements(&scanner, until: "]") 148 | } else { 149 | tokens.compactAppend(parseWhitespace(&scanner)) 150 | } 151 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 152 | return tokens 153 | } 154 | 155 | tokens.append(parseOperator(&scanner, operator: "]")) 156 | return tokens 157 | } 158 | 159 | private func parseElements(_ scanner: inout StringScanner, until terminator: Terminator) -> [JsonToken] { 160 | var tokens = [JsonToken]() 161 | 162 | tokens += parseElement(&scanner) 163 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 164 | return tokens 165 | } 166 | 167 | while let (nextCharacterPreview, _) = scanner.peekCharacter(), nextCharacterPreview != terminator.endingCharacter { 168 | tokens.append(parseOperator(&scanner, operator: ",")) 169 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 170 | return tokens 171 | } 172 | 173 | tokens += parseElement(&scanner) 174 | if let lastToken = tokens.last, case JsonToken.unknown(_, _) = lastToken { 175 | return tokens 176 | } 177 | } 178 | return tokens 179 | } 180 | 181 | private func parseMemberKey(_ scanner: inout StringScanner) -> JsonToken { 182 | let token = parseString(&scanner) 183 | if case .stringValue(let range) = token { 184 | return .memberKey(range) 185 | } 186 | return token 187 | } 188 | 189 | private func parseString(_ scanner: inout StringScanner) -> JsonToken { 190 | var range = NSRange(location: scanner.currentIndex, length: 0) 191 | 192 | guard scanner.scanCharacter("\"") != nil else { 193 | range.length = 1 194 | if let (nextCharacter, _) = scanner.peekCharacter() { 195 | return .unknown(range, .invalidSymbol(expected: "\"", actual: nextCharacter)) 196 | } 197 | return .unknown(range, .invalidSymbol(expected: "\"", actual: nil)) 198 | } 199 | 200 | while !scanner.isAtEnd { 201 | scanner.scanUpToCharacters(from: CharacterSet(charactersIn: "\"\\")) 202 | 203 | guard let (nextCharacter, _) = scanner.scanCharacter() else { 204 | range.length = scanner.currentIndex - range.location 205 | return .unknown(range, .unenclosedQuotationMarks) 206 | } 207 | if nextCharacter == "\"" { 208 | break 209 | } else if nextCharacter == "\\" { 210 | // next character should be escaped 211 | guard let (escapedCharacter, _) = scanner.scanCharacter() else { 212 | range.length = scanner.currentIndex - range.location 213 | return .unknown(range, .expectedSymbol) 214 | } 215 | switch escapedCharacter { 216 | case "\"", "\\", "/", "b", "f", "n", "r", "t": 217 | break 218 | case "u": 219 | let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdefABCDEF") 220 | guard 221 | scanner.scanCharacter(from: hexCharacterSet) != nil, 222 | scanner.scanCharacter(from: hexCharacterSet) != nil, 223 | scanner.scanCharacter(from: hexCharacterSet) != nil, 224 | scanner.scanCharacter(from: hexCharacterSet) != nil 225 | else { 226 | range.length = scanner.currentIndex - range.location 227 | return .unknown(range, .unexpectedSymbol(description: "Error while parsing an escaped unicode symbol")) 228 | } 229 | default: 230 | range.length = scanner.currentIndex - range.location 231 | return .unknown(range, .unexpectedSymbol(description: "Escaped character can only be \" \\ / b f n r t u, got: \(String(escapedCharacter))")) 232 | } 233 | } else { 234 | range.length = scanner.currentIndex - range.location 235 | return .unknown(range, .unenclosedQuotationMarks) 236 | } 237 | } 238 | 239 | range.length = scanner.currentIndex - range.location 240 | return .stringValue(range) 241 | } 242 | 243 | private func parseNumber(_ scanner: inout StringScanner) -> JsonToken { 244 | let indexBefore = scanner.currentIndex 245 | do { 246 | try parseInteger(&scanner) 247 | try parseFraction(&scanner) 248 | try parseExponent(&scanner) 249 | } catch { 250 | let range = NSRange(location: indexBefore, length: scanner.currentIndex - indexBefore) 251 | return .unknown(range, error as? JsonTokenizerError ?? .unexpectedSymbol(description: "Error while parsing a number")) 252 | } 253 | return .numericValue(NSRange(location: indexBefore, length: scanner.currentIndex - indexBefore)) 254 | } 255 | 256 | @discardableResult 257 | private func parseInteger(_ scanner: inout StringScanner) throws -> String { 258 | var integer = "" 259 | 260 | while true { 261 | guard let (characterPreview, _) = scanner.peekCharacter() else { 262 | throw JsonTokenizerError.expectedSymbol 263 | } 264 | switch characterPreview { 265 | case "0": 266 | integer.append(characterPreview) 267 | scanner.scanCharacter() 268 | if integer == "0" || integer == "-0" { 269 | return integer 270 | } 271 | case "-": 272 | if !integer.isEmpty { 273 | return integer 274 | } 275 | integer.append(characterPreview) 276 | scanner.scanCharacter() 277 | case "1"..."9": 278 | integer.append(characterPreview) 279 | scanner.scanCharacter() 280 | default: 281 | if integer.isEmpty { 282 | throw JsonTokenizerError.unexpectedSymbol(description: "Expected digit, got: \(String(characterPreview))") 283 | } 284 | return integer 285 | } 286 | } 287 | } 288 | 289 | @discardableResult 290 | private func parseFraction(_ scanner: inout StringScanner) throws -> String { 291 | var fraction = "" 292 | 293 | guard scanner.scanCharacter(".") != nil else { 294 | return fraction 295 | } 296 | fraction.append(".") 297 | 298 | while true { 299 | guard let (characterPreview, _) = scanner.peekCharacter() else { 300 | throw JsonTokenizerError.expectedSymbol 301 | } 302 | switch characterPreview { 303 | case "0"..."9": 304 | fraction.append(characterPreview) 305 | scanner.scanCharacter() 306 | default: 307 | return fraction 308 | } 309 | } 310 | } 311 | 312 | @discardableResult 313 | private func parseExponent(_ scanner: inout StringScanner) throws -> String { 314 | var exponent = "" 315 | 316 | guard let (eCharacter, _) = scanner.scanCharacter(from: CharacterSet(charactersIn: "eE")) else { 317 | return exponent 318 | } 319 | exponent.append(eCharacter) 320 | 321 | if let (signCharacter, _) = scanner.scanCharacter(from: CharacterSet(charactersIn: "+-")) { 322 | exponent.append(signCharacter) 323 | } 324 | 325 | while true { 326 | guard let (characterPreview, _) = scanner.peekCharacter() else { 327 | throw JsonTokenizerError.expectedSymbol 328 | } 329 | switch characterPreview { 330 | case "0"..."9": 331 | exponent.append(characterPreview) 332 | scanner.scanCharacter() 333 | default: 334 | return exponent 335 | } 336 | } 337 | } 338 | 339 | private func parseLiteral(_ scanner: inout StringScanner) -> JsonToken { 340 | if let range = scanner.scanString("true") { 341 | return .literal(range) 342 | } else if let range = scanner.scanString("false") { 343 | return .literal(range) 344 | } else if let range = scanner.scanString("null") { 345 | return .literal(range) 346 | } else { 347 | return .unknown(NSRange(location: scanner.currentIndex, length: 0), .unexpectedSymbol(description: "Expected literal like 'true', 'false' or 'null'")) 348 | } 349 | } 350 | 351 | private func parseOperator(_ scanner: inout StringScanner, operator: Character) -> JsonToken { 352 | guard let range = scanner.scanCharacter(`operator`) else { 353 | return .unknown(NSRange(location: scanner.currentIndex, length: 0), .invalidSymbol(expected: `operator`, actual: nil)) 354 | } 355 | return .operator(range) 356 | } 357 | } 358 | --------------------------------------------------------------------------------