diff --git a/Images/DFD.jpg b/Images/DFD.jpg new file mode 100644 index 0000000..8ee9ac2 Binary files /dev/null and b/Images/DFD.jpg differ diff --git a/Images/DFD.png b/Images/DFD.png deleted file mode 100644 index 6c4e4d4..0000000 Binary files a/Images/DFD.png and /dev/null differ diff --git a/default.png b/Images/default.png similarity index 100% rename from default.png rename to Images/default.png diff --git a/JSONPreview.podspec b/JSONPreview.podspec index 4213a4f..f5a2623 100755 --- a/JSONPreview.podspec +++ b/JSONPreview.podspec @@ -29,7 +29,7 @@ Pod::Spec.new do |s| s.module_name = 'JSONPreview' - s.source_files = 'JSONPreview/Core/*' + s.source_files = 'JSONPreview/Core/*/*' s.resource_bundle = { 'JSONPreviewBundle' => [ 'JSONPreview/Other/*.xcassets' ] diff --git a/JSONPreview.xcodeproj/project.pbxproj b/JSONPreview.xcodeproj/project.pbxproj index edde2b7..d27e7d7 100644 --- a/JSONPreview.xcodeproj/project.pbxproj +++ b/JSONPreview.xcodeproj/project.pbxproj @@ -16,15 +16,18 @@ 3A1F06C02508D1DD00C16862 /* JSONPreviewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1F06BF2508D1DD00C16862 /* JSONPreviewUITests.swift */; }; 3A1F06D02508D74A00C16862 /* JSONPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1F06CF2508D74A00C16862 /* JSONPreview.swift */; }; 3A1F06D22508D78B00C16862 /* JSONDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1F06D12508D78B00C16862 /* JSONDecorator.swift */; }; - 3A67B14E250B3E6F000903EB /* JSONLexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A67B14D250B3E6F000903EB /* JSONLexer.swift */; }; 3A69AC7425427648001092F4 /* LineNumberCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A69AC7325427648001092F4 /* LineNumberCell.swift */; }; 3A8F1AB42509C50C003BAC09 /* JSONSlice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */; }; 3A97FCBC250CA0670017352A /* JSONTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A97FCBB250CA0670017352A /* JSONTextView.swift */; }; 3A9DB22D2509D7F4002E7B15 /* HighlightColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9DB22C2509D7F4002E7B15 /* HighlightColor.swift */; }; 3A9DB22F2509DA7A002E7B15 /* HighlightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9DB22E2509DA7A002E7B15 /* HighlightStyle.swift */; }; 3A9DB33D250A0DB4002E7B15 /* LineNumberTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9DB33C250A0DB4002E7B15 /* LineNumberTableView.swift */; }; - AE5465DE27F7DDEA00201BD0 /* default.png in Resources */ = {isa = PBXBuildFile; fileRef = AE5465DD27F7DDEA00201BD0 /* default.png */; }; AE71889A26FADA8300A16878 /* String+ValidURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE71889926FADA8300A16878 /* String+ValidURL.swift */; }; + AE92DFD6283F13AD002A7DAF /* JSONParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE92DFD5283F13AD002A7DAF /* JSONParser.swift */; }; + AE92DFD8283F13C7002A7DAF /* JSONValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE92DFD7283F13C7002A7DAF /* JSONValue.swift */; }; + AE92DFDA283F14B0002A7DAF /* JSONError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE92DFD9283F14B0002A7DAF /* JSONError.swift */; }; + AEA693AA2844894C006BAF10 /* Dictionary+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA693A92844894C006BAF10 /* Dictionary+Sort.swift */; }; + AEF7BC2F28596D60009F4B3B /* JSONObjectKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF7BC2E28596D60009F4B3B /* JSONObjectKey.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,7 +65,6 @@ 3A1F06C12508D1DD00C16862 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3A1F06CF2508D74A00C16862 /* JSONPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONPreview.swift; sourceTree = ""; }; 3A1F06D12508D78B00C16862 /* JSONDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDecorator.swift; sourceTree = ""; }; - 3A67B14D250B3E6F000903EB /* JSONLexer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONLexer.swift; sourceTree = ""; }; 3A69AC7325427648001092F4 /* LineNumberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineNumberCell.swift; sourceTree = ""; }; 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONSlice.swift; sourceTree = ""; }; 3A97FCBB250CA0670017352A /* JSONTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTextView.swift; sourceTree = ""; }; @@ -71,8 +73,12 @@ 3A9DB33C250A0DB4002E7B15 /* LineNumberTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineNumberTableView.swift; sourceTree = ""; }; AE5465D927F7DDC800201BD0 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; AE5465DA27F7DDC800201BD0 /* README_CN.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README_CN.md; sourceTree = SOURCE_ROOT; }; - AE5465DD27F7DDEA00201BD0 /* default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = default.png; sourceTree = SOURCE_ROOT; }; AE71889926FADA8300A16878 /* String+ValidURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ValidURL.swift"; sourceTree = ""; }; + AE92DFD5283F13AD002A7DAF /* JSONParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONParser.swift; sourceTree = ""; }; + AE92DFD7283F13C7002A7DAF /* JSONValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONValue.swift; sourceTree = ""; }; + AE92DFD9283F14B0002A7DAF /* JSONError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONError.swift; sourceTree = ""; }; + AEA693A92844894C006BAF10 /* Dictionary+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Sort.swift"; sourceTree = ""; }; + AEF7BC2E28596D60009F4B3B /* JSONObjectKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONObjectKey.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -127,7 +133,6 @@ AE5465D927F7DDC800201BD0 /* README.md */, 3A134BCC251486B3002AAFA6 /* push.sh */, 3A134BCB251486B3002AAFA6 /* JSONPreview.podspec */, - AE5465DD27F7DDEA00201BD0 /* default.png */, 3A1F06CD2508D20700C16862 /* Core */, 3A1F06CE2508D20C00C16862 /* Other */, ); @@ -155,16 +160,10 @@ 3A1F06CD2508D20700C16862 /* Core */ = { isa = PBXGroup; children = ( - 3A9DB22C2509D7F4002E7B15 /* HighlightColor.swift */, - 3A9DB22E2509DA7A002E7B15 /* HighlightStyle.swift */, - AE71889926FADA8300A16878 /* String+ValidURL.swift */, - 3A67B14D250B3E6F000903EB /* JSONLexer.swift */, - 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */, - 3A1F06D12508D78B00C16862 /* JSONDecorator.swift */, - 3A1F06CF2508D74A00C16862 /* JSONPreview.swift */, - 3A9DB33C250A0DB4002E7B15 /* LineNumberTableView.swift */, - 3A69AC7325427648001092F4 /* LineNumberCell.swift */, - 3A97FCBB250CA0670017352A /* JSONTextView.swift */, + AE6C038328406F7000E544FD /* Tools */, + AE6C038228406F5D00E544FD /* Entity */, + AE6C038528406FE100E544FD /* Model */, + AE6C038428406F7A00E544FD /* View */, ); path = Core; sourceTree = ""; @@ -182,6 +181,48 @@ path = Other; sourceTree = ""; }; + AE6C038228406F5D00E544FD /* Entity */ = { + isa = PBXGroup; + children = ( + 3A9DB22C2509D7F4002E7B15 /* HighlightColor.swift */, + 3A9DB22E2509DA7A002E7B15 /* HighlightStyle.swift */, + AE92DFD9283F14B0002A7DAF /* JSONError.swift */, + AE92DFD7283F13C7002A7DAF /* JSONValue.swift */, + 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */, + AEF7BC2E28596D60009F4B3B /* JSONObjectKey.swift */, + ); + path = Entity; + sourceTree = ""; + }; + AE6C038328406F7000E544FD /* Tools */ = { + isa = PBXGroup; + children = ( + AE71889926FADA8300A16878 /* String+ValidURL.swift */, + AEA693A92844894C006BAF10 /* Dictionary+Sort.swift */, + ); + path = Tools; + sourceTree = ""; + }; + AE6C038428406F7A00E544FD /* View */ = { + isa = PBXGroup; + children = ( + 3A1F06CF2508D74A00C16862 /* JSONPreview.swift */, + 3A9DB33C250A0DB4002E7B15 /* LineNumberTableView.swift */, + 3A69AC7325427648001092F4 /* LineNumberCell.swift */, + 3A97FCBB250CA0670017352A /* JSONTextView.swift */, + ); + path = View; + sourceTree = ""; + }; + AE6C038528406FE100E544FD /* Model */ = { + isa = PBXGroup; + children = ( + AE92DFD5283F13AD002A7DAF /* JSONParser.swift */, + 3A1F06D12508D78B00C16862 /* JSONDecorator.swift */, + ); + path = Model; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -289,7 +330,6 @@ 3A1F06AA2508D1DC00C16862 /* LaunchScreen.storyboard in Resources */, 3A1F06A72508D1DC00C16862 /* Assets.xcassets in Resources */, 3A1F06A52508D1D800C16862 /* Main.storyboard in Resources */, - AE5465DE27F7DDEA00201BD0 /* default.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -314,13 +354,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AE92DFDA283F14B0002A7DAF /* JSONError.swift in Sources */, 3A69AC7425427648001092F4 /* LineNumberCell.swift in Sources */, + AE92DFD8283F13C7002A7DAF /* JSONValue.swift in Sources */, AE71889A26FADA8300A16878 /* String+ValidURL.swift in Sources */, 3A9DB33D250A0DB4002E7B15 /* LineNumberTableView.swift in Sources */, 3A1F06D02508D74A00C16862 /* JSONPreview.swift in Sources */, + AE92DFD6283F13AD002A7DAF /* JSONParser.swift in Sources */, 3A97FCBC250CA0670017352A /* JSONTextView.swift in Sources */, + AEF7BC2F28596D60009F4B3B /* JSONObjectKey.swift in Sources */, + AEA693AA2844894C006BAF10 /* Dictionary+Sort.swift in Sources */, 3A1F06A22508D1D800C16862 /* ViewController.swift in Sources */, - 3A67B14E250B3E6F000903EB /* JSONLexer.swift in Sources */, 3A9DB22F2509DA7A002E7B15 /* HighlightStyle.swift in Sources */, 3A9DB22D2509D7F4002E7B15 /* HighlightColor.swift in Sources */, 3A1F069E2508D1D800C16862 /* AppDelegate.swift in Sources */, diff --git a/JSONPreview/Core/HighlightColor.swift b/JSONPreview/Core/Entity/HighlightColor.swift similarity index 100% rename from JSONPreview/Core/HighlightColor.swift rename to JSONPreview/Core/Entity/HighlightColor.swift diff --git a/JSONPreview/Core/HighlightStyle.swift b/JSONPreview/Core/Entity/HighlightStyle.swift similarity index 94% rename from JSONPreview/Core/HighlightStyle.swift rename to JSONPreview/Core/Entity/HighlightStyle.swift index d239b34..35b5d32 100644 --- a/JSONPreview/Core/HighlightStyle.swift +++ b/JSONPreview/Core/Entity/HighlightStyle.swift @@ -8,6 +8,10 @@ import UIKit +public typealias AttributedString = NSMutableAttributedString +public typealias AttributedKey = AttributedString.Key +public typealias StyleInfos = [AttributedKey : Any] + /// Highlight style configuration public struct HighlightStyle { /// Initialization method @@ -79,7 +83,6 @@ public extension HighlightStyle { fileprivate extension UIImage { convenience init?(name: String) { - if let resourcePath = Bundle(for: JSONPreview.self).resourcePath, let bundle = Bundle(path: resourcePath + "JSONPreview.bundle") { diff --git a/JSONPreview/Core/Entity/JSONError.swift b/JSONPreview/Core/Entity/JSONError.swift new file mode 100644 index 0000000..36b72de --- /dev/null +++ b/JSONPreview/Core/Entity/JSONError.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +public enum JSONError: Swift.Error, Equatable { + case cannotConvertInputDataToUTF8 + case unexpectedCharacter(jsonValue: JSONValue?, ascii: UInt8, characterIndex: Int) + case unexpectedEndOfFile + case tooManyNestedArraysOrDictionaries(characterIndex: Int) + case invalidHexDigitSequence(String, index: Int) + case unexpectedEscapedCharacter(ascii: UInt8, in: String, index: Int) + case unescapedControlCharacterInString(ascii: UInt8, in: String, index: Int) + case expectedLowSurrogateUTF8SequenceAfterHighSurrogate(in: String, index: Int) + case couldNotCreateUnicodeScalarFromUInt32(in: String, index: Int, unicodeScalarValue: UInt32) + case numberWithLeadingZero(index: Int) + case numberIsNotRepresentableInSwift(parsed: String) + case singleFragmentFoundButNotAllowed +} diff --git a/JSONPreview/Core/Entity/JSONObjectKey.swift b/JSONPreview/Core/Entity/JSONObjectKey.swift new file mode 100644 index 0000000..9f979b3 --- /dev/null +++ b/JSONPreview/Core/Entity/JSONObjectKey.swift @@ -0,0 +1,46 @@ +import Foundation + +/// Used to enrich the information of the key of the object in json. +public struct JSONObjectKey { + /// The object key. + /// + /// If this key is wrong, then the value is `""`. + public let key: String + + /// Is the key wrong. + public let isWrong: Bool + + /// Used to mark an incorrect key. + public static let wrong: Self = .init(key: "", isWrong: true) + + fileprivate init(key: String, isWrong: Bool) { + self.key = key + self.isWrong = isWrong + } + + public init(_ key: String) { + self.init(key: key, isWrong: false) + } +} + +// MARK: - Hashable + +extension JSONObjectKey: Hashable { } + +// MARK: - Comparable + +extension JSONObjectKey: Comparable { + public static func < (lhs: JSONObjectKey, rhs: JSONObjectKey) -> Bool { + if lhs.isWrong { return false } + if rhs.isWrong { return true } + return lhs.key < rhs.key + } +} + +// MARK: - ExpressibleByStringLiteral + +extension JSONObjectKey: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self.init(value) + } +} diff --git a/JSONPreview/Core/JSONSlice.swift b/JSONPreview/Core/Entity/JSONSlice.swift similarity index 82% rename from JSONPreview/Core/JSONSlice.swift rename to JSONPreview/Core/Entity/JSONSlice.swift index 55bf509..696d58e 100644 --- a/JSONPreview/Core/JSONSlice.swift +++ b/JSONPreview/Core/Entity/JSONSlice.swift @@ -25,8 +25,8 @@ public struct JSONSlice { public init( level: Int, lineNumber: Int, - expand: NSAttributedString, - folded: NSAttributedString? = nil + expand: AttributedString, + folded: AttributedString? = nil ) { self.level = level self.lineNumber = lineNumber @@ -44,16 +44,16 @@ public struct JSONSlice { public init( level: Int, lineNumber: Int, - expand: (String, [NSAttributedString.Key : Any]), - folded: (String, [NSAttributedString.Key : Any])? = nil + expand: (String, StyleInfos), + folded: (String, StyleInfos)? = nil ) { self.level = level self.lineNumber = lineNumber - self.expand = NSAttributedString(string: expand.0, attributes: expand.1) + self.expand = AttributedString(string: expand.0, attributes: expand.1) if let folded = folded { - self.folded = NSAttributedString(string: folded.0, attributes: folded.1) + self.folded = AttributedString(string: folded.0, attributes: folded.1) } else { self.folded = nil } @@ -75,15 +75,15 @@ public struct JSONSlice { public let level: Int /// The complete content of the JSON slice in the expanded state. - public var expand: NSAttributedString + public var expand: AttributedString /// The summary content of the JSON slice in the folded state. - public var folded: NSAttributedString? + public var folded: AttributedString? } public extension JSONSlice { /// According to different status, return the content that should be displayed currently. - var showContent: NSAttributedString? { + var showContent: AttributedString? { switch state { case .expand: return expand case .folded: return folded diff --git a/JSONPreview/Core/Entity/JSONValue.swift b/JSONPreview/Core/Entity/JSONValue.swift new file mode 100644 index 0000000..29561bd --- /dev/null +++ b/JSONPreview/Core/Entity/JSONValue.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +public enum JSONValue: Equatable { + case string(String, wrong: String? = nil) + case number(String, wrong: String? = nil) + case bool(Bool, wrong: String? = nil) + case null(wrong: String? = nil) + + case array([JSONValue]) + case object([JSONObjectKey: JSONValue]) + + case unknown(String) +} + +extension JSONValue { + public enum ValueType { + case wrong + case right(isContainer: Bool) + } + + public var isRight: ValueType { + switch self { + case .unknown: + return .wrong + + case .string(_, let wrong): + return wrong == nil ? .right(isContainer: false) : .wrong + + case .number(_, let wrong): + return wrong == nil ? .right(isContainer: false) : .wrong + + case .bool(_, let wrong): + return wrong == nil ? .right(isContainer: false) : .wrong + + case .null(let wrong): + return wrong == nil ? .right(isContainer: false) : .wrong + + case .array(let array): + guard let last = array.last else { + return .right(isContainer: true) + } + + switch last.isRight { + case .right: + return .right(isContainer: true) + case .wrong: + return .wrong + } + + case .object(let object): + for value in object.values { + guard case .wrong = value.isRight else { continue } + return .wrong + } + return .right(isContainer: true) + } + } + + public func appendWrong(_ wrong: String) -> Self { + switch self { + case .bool(let value, _): + return .bool(value, wrong: wrong) + + case .number(let value, _): + return .number(value, wrong: wrong) + + case .string(let value, _): + return .string(value, wrong: wrong) + + case .null: + return .null(wrong: wrong) + + default: + return self + } + } +} + +extension JSONValue { + public var debugDataTypeDescription: String { + switch self { + case .array: + return "an array" + case .bool: + return "bool" + case .number: + return "a number" + case .string: + return "a string" + case .object: + return "a dictionary" + case .null: + return "null" + case .unknown: + return "unknown json value" + } + } +} diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift deleted file mode 100644 index d4031fb..0000000 --- a/JSONPreview/Core/JSONDecorator.swift +++ /dev/null @@ -1,478 +0,0 @@ -// -// JSONDecorator.swift -// JSONPreview -// -// Created by Rakuyo on 2020/9/9. -// Copyright © 2020 Rakuyo. All rights reserved. -// - -import UIKit - -/// Responsible for beautifying JSON -public class JSONDecorator { - // Do not want the caller to directly initialize the object - private init(json: String, style: HighlightStyle) { - self.json = json - self.style = style - } - - /// The JSON string to be highlighted. - private let json: String - - /// Style of highlight. See `HighlightStyle` for details. - private let style: HighlightStyle - - /// JSON slice. See `JSONSlice` for details. - public var slices: [JSONSlice] = [] - - /// The string used to hold the icon of the expand button - private lazy var expandIconString = createIconAttributedString(with: style.expandIcon) - - /// The string used to hold the icon of the fold button - private lazy var foldIconString = createIconAttributedString(with: style.foldIcon) - - /// A newline string with the same style as JSON. - /// Can be used to splice slices into a complete string. - private(set) lazy var wrapString = NSAttributedString(string: "\n", attributes: createStyle(foregroundColor: nil)) - - private lazy var startStyle = createStyle(foregroundColor: style.color.keyWord) - private lazy var keyWordStyle = createStyle(foregroundColor: style.color.keyWord) - private lazy var keyStyle = createStyle(foregroundColor: style.color.key) - private lazy var linkStyle = createStyle(foregroundColor: style.color.link) - private lazy var stringStyle = createStyle(foregroundColor: style.color.string) - private lazy var numberStyle = createStyle(foregroundColor: style.color.number) - private lazy var boolStyle = createStyle(foregroundColor: style.color.boolean) - private lazy var nullStyle = createStyle(foregroundColor: style.color.null) - - private lazy var placeholderStyle = createStyle( - foregroundColor: style.color.lineText, - other: [.backgroundColor : style.color.lineBackground] - ) - - private lazy var unknownStyle = createStyle( - foregroundColor: style.color.unknownText, - other: [.backgroundColor : style.color.unknownBackground] - ) -} - -public extension JSONDecorator { - /// Highlight the incoming JSON string. - /// - /// Serve for `JSONPreview`. Will split JSON into arrays that meet the requirements of `JSONPreview` display. - /// - /// - Parameters: - /// - json: The JSON string to be highlighted. - /// - judgmentValid: Whether to check the validity of JSON. - /// - style: style of highlight. See `HighlightStyle` for details. - /// - Returns: Return `nil` when JSON is invalid. See `JSONDecorator` for details. - static func highlight(_ json: String, judgmentValid: Bool, style: HighlightStyle = .default) -> JSONDecorator? { - if judgmentValid { - guard let data = json.data(using: .utf8), - let _ = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { - return nil - } - } - - return highlight(json, style: style) - } - - /// Highlight the incoming JSON string. - /// - /// Serve for `JSONPreview`. Will split JSON into arrays that meet the requirements of `JSONPreview` display. - /// - /// - Parameters: - /// - json: The JSON string to be highlighted. - /// - style: style of highlight. See `HighlightStyle` for details. - /// - Returns: See `JSONDecorator` for details. - static func highlight(_ json: String, style: HighlightStyle = .default) -> JSONDecorator { - let decorator = JSONDecorator(json: json, style: style) - decorator.slices = decorator._slices - return decorator - } -} - -private extension JSONDecorator { - var _slices: [JSONSlice] { - var _slices: [JSONSlice] = [] - - // Record indentation level - var level = 0 - - var lastToken: JSONLexer.Token? = nil - - JSONLexer.getTokens(of: json).forEach { (token) in - defer { lastToken = token } - - let lineNumber = _slices.count + 1 - - switch token { - - // MARK: objectBegin - case .objectBegin: - // There is a previous slice, and the previous slice is a colon. - // Need to splice the current slice to the previous slice. - if let _lastToken = lastToken, case .colon = _lastToken, let lastSlices = _slices.last { - - let expandString = NSMutableAttributedString( - string: " {", - attributes: startStyle - ) - - let foldString = NSMutableAttributedString( - string: "{Object...}", - attributes: placeholderStyle - ) - - foldString.insert(NSAttributedString(string: " ", attributes: keyWordStyle), at: 0) - - expandString.insert(foldIconString, at: 0) - foldString.insert(expandIconString, at: 0) - - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - lastExpand.append(expandString) - - let lastFolded = NSMutableAttributedString(attributedString: lastSlices.folded!) - lastFolded.append(foldString) - - _slices[_slices.count - 1].expand = lastExpand - _slices[_slices.count - 1].folded = lastFolded - } - - // When the conditions are not met, create a new slice - else { - let indentation = createIndentedString(level: level) - - let expandString = NSMutableAttributedString( - string: indentation + " {", - attributes: startStyle - ) - - let foldString = NSMutableAttributedString( - string: "{Object...}", - attributes: placeholderStyle - ) - - foldString.insert(NSAttributedString(string: indentation, attributes: keyWordStyle), at: 0) - foldString.insert(NSAttributedString(string: " ", attributes: keyWordStyle), at: indentation.count) - - foldString.insert(expandIconString, at: indentation.count) - expandString.insert(foldIconString, at: indentation.count) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: expandString, folded: foldString)) - } - - level += 1 - - // MARK: objectEnd - case .objectEnd: - level -= 1 - - let indentation = createIndentedString(level: level) - - let expandString = NSMutableAttributedString( - string: indentation + "}", - attributes: startStyle - ) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: expandString)) - - // MARK: objectKey - case .objectKey(let key): - let indentation = createIndentedString(level: level) - - let expandString = NSAttributedString( - string: indentation + "\"\(key)\"", - attributes: keyStyle - ) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: expandString)) - - // MARK: arrayBegin - case .arrayBegin: - if let _lastToken = lastToken, case .colon = _lastToken, let lastSlices = _slices.last { - - let expandString = NSMutableAttributedString( - string: " [", - attributes: startStyle - ) - - let foldString = NSMutableAttributedString( - string: "[Array...]", - attributes: placeholderStyle - ) - - foldString.insert(NSAttributedString(string: " ", attributes: keyWordStyle), at: 0) - - foldString.insert(expandIconString, at: 0) - expandString.insert(foldIconString, at: 0) - - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - lastExpand.append(expandString) - - let lastFolded = NSMutableAttributedString(attributedString: lastSlices.folded!) - lastFolded.append(foldString) - - _slices[_slices.count - 1].expand = lastExpand - _slices[_slices.count - 1].folded = lastFolded - - } else { - let indentation = createIndentedString(level: level) - - let expandString = NSMutableAttributedString( - string: indentation + " [", - attributes: startStyle - ) - - let foldString = NSMutableAttributedString( - string: "[Array...]", - attributes: placeholderStyle - ) - - foldString.insert(NSAttributedString(string: indentation, attributes: keyWordStyle), at: 0) - foldString.insert(NSAttributedString(string: " ", attributes: keyWordStyle), at: indentation.count) - - foldString.insert(expandIconString, at: indentation.count) - expandString.insert(foldIconString, at: indentation.count) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: expandString, folded: foldString)) - } - - level += 1 - - // MARK: arrayEnd - case .arrayEnd: - level -= 1 - - let indentation = createIndentedString(level: level) - - let expandString = NSMutableAttributedString( - string: indentation + "]", - attributes: startStyle - ) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: expandString)) - - // MARK: colon - case .colon: - guard let lastSlices = _slices.last else { break } - - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - - lastExpand.append(NSAttributedString( - string: " : ", - attributes: keyWordStyle - )) - - _slices[_slices.count - 1].expand = lastExpand - _slices[_slices.count - 1].folded = lastExpand - - // MARK: comma - case .comma: - guard let lastSlices = _slices.last else { break } - - let commaString = NSAttributedString(string: ",", attributes: keyWordStyle) - - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - lastExpand.append(commaString) - - _slices[_slices.count - 1].expand = lastExpand - - // Add a comma to the beginning of the token - guard lastToken == .objectEnd || lastToken == .arrayEnd else { break } - - for i in (0 ..< _slices.count).reversed() { - let _slice = _slices[i] - - guard let _folded = _slice.folded, _slice.level == lastSlices.level else { - continue - } - - let lastFolded = NSMutableAttributedString(attributedString: _folded) - lastFolded.append(commaString) - _slices[i].folded = lastFolded - - break - } - - // MARK: Link - case .link(let value): - let addExtraLinkStyle: (NSMutableAttributedString, Int) -> Void = { - let range = NSRange(location: $1, length: value.count) - - $0.addAttribute(.link, value: value, range: range) - $0.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range) - } - - if let _lastToken = lastToken, case .colon = _lastToken, let lastSlices = _slices.last { - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - - let attString = NSMutableAttributedString(string: "\"\(value)\"", attributes: linkStyle) - addExtraLinkStyle(attString, 1) - - lastExpand.append(attString) - - _slices[_slices.count - 1] = JSONSlice( - level: lastSlices.level, - lineNumber: lastSlices.lineNumber, - expand: lastExpand, - folded: nil - ) - - } else { - let indentation = createIndentedString(level: level) - - let attString = NSMutableAttributedString(string: indentation + "\"\(value)\"", attributes: linkStyle) - addExtraLinkStyle(attString, indentation.count + 1) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: attString)) - } - - // MARK: string - case .string(let value): - if let _lastToken = lastToken, case .colon = _lastToken, let lastSlices = _slices.last { - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - - lastExpand.append(NSAttributedString(string: "\"\(value)\"", attributes: stringStyle)) - - _slices[_slices.count - 1] = JSONSlice( - level: lastSlices.level, - lineNumber: lastSlices.lineNumber, - expand: lastExpand, - folded: nil - ) - - } else { - let indentation = createIndentedString(level: level) - let string = NSAttributedString(string: indentation + "\"\(value)\"", attributes: stringStyle) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: string)) - } - - // MARK: number - case .number(let number): - if let _lastToken = lastToken, case .colon = _lastToken, let lastSlices = _slices.last { - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - - lastExpand.append(NSAttributedString(string: "\(number)", attributes: numberStyle)) - - _slices[_slices.count - 1].expand = lastExpand - - } else { - let indentation = createIndentedString(level: level) - let numberString = NSAttributedString(string: indentation + "\(number)", attributes: numberStyle) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: numberString)) - } - - // MARK: boolean - case .boolean(let bool): - let value = bool ? "true" : "false" - - if let _lastToken = lastToken, case .colon = _lastToken, let lastSlices = _slices.last { - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - - lastExpand.append(NSAttributedString(string: value, attributes: boolStyle)) - - _slices[_slices.count - 1].expand = lastExpand - - } else { - let indentation = createIndentedString(level: level) - let boolString = NSAttributedString(string: indentation + value, attributes: boolStyle) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: boolString)) - } - - // MARK: null - case .null: - if let _lastToken = lastToken, case .colon = _lastToken, let lastSlices = _slices.last { - let lastExpand = NSMutableAttributedString(attributedString: lastSlices.expand) - - lastExpand.append(NSAttributedString(string: "null", attributes: nullStyle)) - - _slices[_slices.count - 1].expand = lastExpand - - } else { - let indentation = createIndentedString(level: level) - let nullString = NSAttributedString(string: indentation + "null", attributes: nullStyle) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: nullString)) - } - - // MARK: unknown - case .unknown(let string): - let newString = string.replacingOccurrences(of: "\n", with: "") - let indentation = createIndentedString(level: level) - - let attributedString = NSMutableAttributedString(string: indentation) - attributedString.append(NSAttributedString(string: newString, attributes: unknownStyle)) - - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: attributedString)) - } - } - - return _slices - } -} - -// MARK: - Tools - -private extension JSONDecorator { - - /// Create a string to represent indentation. - /// - /// - Parameter level: Current level. - /// - Returns: Use the indentation indicated by `"\t"`. - func createIndentedString(level: Int) -> String { - return (0 ..< level).map{ _ in "\t" }.joined() - } - - /// Create an `NSAttributedString` object for displaying image. - /// - /// - Parameter image: The image to be displayed. - /// - Returns: `NSAttributedString` object. - func createIconAttributedString(with image: UIImage) -> NSAttributedString { - let expandAttach = NSTextAttachment() - - expandAttach.image = image - - let font = style.jsonFont - - let y = (style.lineHeight - font.lineHeight + 1) + font.descender - - expandAttach.bounds = CGRect(x: 0, y: y, width: font.ascender, height: font.ascender) - - return NSAttributedString(attachment: expandAttach) - } - - func createStyle( - foregroundColor: UIColor?, - other: [NSAttributedString.Key : Any]? = nil - ) -> [NSAttributedString.Key : Any] { - var newStyle: [NSAttributedString.Key : Any] = [.font : style.jsonFont] - - if let color = foregroundColor { - newStyle[.foregroundColor] = color - } - - let lineHeightMultiple: CGFloat = 1 - let paragraphStyle = NSMutableParagraphStyle() - - if #available(iOS 15.0, *) { - paragraphStyle.usesDefaultHyphenation = false - } - - paragraphStyle.lineHeightMultiple = lineHeightMultiple - paragraphStyle.maximumLineHeight = style.lineHeight - paragraphStyle.minimumLineHeight = style.lineHeight - paragraphStyle.lineSpacing = 0 - - newStyle[.paragraphStyle] = paragraphStyle - newStyle[.baselineOffset] = (style.lineHeight - style.jsonFont.lineHeight) + 1 - - if let other = other { - other.forEach { newStyle[$0] = $1 } - } - - return newStyle - } -} diff --git a/JSONPreview/Core/JSONLexer.swift b/JSONPreview/Core/JSONLexer.swift deleted file mode 100644 index 26c1059..0000000 --- a/JSONPreview/Core/JSONLexer.swift +++ /dev/null @@ -1,346 +0,0 @@ -// -// JSONLexer.swift -// JSONPreview -// -// Created by Rakuyo on 2020/9/11. -// Copyright © 2020 Rakuyo. All rights reserved. -// - -import Foundation - -/// JSON lexical analyzer -public class JSONLexer { - private init() { - // Do not want the caller to directly initialize the object - } - - private lazy var tokens: [Token] = [] - - /// Storage hierarchy, used to analyze the type of data structure of the current token, and used for syntax analysis - private lazy var beginNodes: [BeginNode] = [] -} - -extension JSONLexer { - public enum Token: Equatable { - /// { - case objectBegin - - /// } - case objectEnd - - case objectKey(String) - - /// [ - case arrayBegin - - /// ] - case arrayEnd - - /// : - case colon - - /// , - case comma - - case link(String) - - case string(String) - - case number(String) - - /// true or false - case boolean(Bool) - - case null - - case unknown(String) - } - - fileprivate enum BeginNode: Equatable { - /// { - case object - - /// [ - case array - } -} - -public extension JSONLexer { - /// Parse JSON string into `JSONLexer.Token` array. - /// - /// When `json` meets the following conditions, an empty array will be returned: - /// - /// 1. `json` is empty. - /// - /// - Parameter json: JSON to be processed. - /// - Returns: Parsed Token array. See `JSONLexer.Token` for details. - static func getTokens(of json: String) -> [Token] { - guard !json.isEmpty else { return [] } - - let lexer = JSONLexer() - - var _json = json - - // Processing initial data - switch _json.removeFirst() { - case "{": - lexer.beginNodes.append(.object) - lexer.tokens.append(.objectBegin) - - case "[": - lexer.beginNodes.append(.array) - lexer.tokens.append(.arrayBegin) - - default: - lexer.tokens.append(.unknown(json)) - return lexer.tokens - } - - // Parse the remaining content - lexer.getremainingTokens(in: _json) - - return lexer.tokens - } -} - -private extension JSONLexer { - /// Get the remaining token. - /// - /// - Parameter json: Target json. - func getremainingTokens(in json: String) { - guard !json.isEmpty else { return } - - var tmpJSON = json - - // Due to some restrictions, it cannot be implemented with recursion, - // so it can only use the loop algorithm - for _ in 0 ..< json.count { - guard !tmpJSON.isEmpty else { break } - - let last = tokens.last - - switch tmpJSON.removeFirst() { - // Filter spaces and newlines. - // This type of character will not cause format errors. - case " ", "\t", "\n": break - - case ":": - switch last { - case .objectKey: - tokens.append(.colon) - - default: - tokens.append(.unknown(":" + tmpJSON)) - return - } - - case ",": - switch last { - case .null, .link, .string, .number, .boolean, .objectEnd, .arrayEnd: - tokens.append(.comma) - - default: - tokens.append(.unknown("," + tmpJSON)) - return - } - - case "{": - if last == nil || last! != .objectBegin { - beginNodes.append(.object) - tokens.append(.objectBegin) - - } else { - tokens.append(.unknown("{" + tmpJSON)) - return - } - - case "}": - - if beginNodes.last == .object { - beginNodes.removeLast() - tokens.append(.objectEnd) - - } else { - tokens.append(.unknown("}" + tmpJSON)) - return - } - - case "[": - if last == nil || last! != .arrayBegin { - beginNodes.append(.array) - tokens.append(.arrayBegin) - - } else { - tokens.append(.unknown("[" + tmpJSON)) - return - } - - case "]": - if beginNodes.last == .array { - beginNodes.removeLast() - tokens.append(.arrayEnd) - - } else { - tokens.append(.unknown("]" + tmpJSON)) - return - } - - case "\"": - // Determine whether the string is a complete string node. - guard let index = tmpJSON.firstEndOfString() else { - tokens.append(.unknown(tmpJSON)) - return - } - - let startIndex = tmpJSON.startIndex - let string = String(tmpJSON[startIndex ..< index]) - - switch last { - case .objectBegin: - tokens.append(.objectKey(string)) - - case .arrayBegin, .colon: - if let url = string.validURL { - tokens.append(.link(url.urlString)) - } else { - tokens.append(.string(string)) - } - - case .comma: - // Need to determine whether it is currently in the object or in the array. - if beginNodes.last == .object { - tokens.append(.objectKey(string)) - - } else if let url = string.validURL { - tokens.append(.link(url.urlString)) - - } else { - tokens.append(.string(string)) - } - - default: - tokens.append(.unknown("\"" + tmpJSON)) - return - } - - tmpJSON.removeSubrange(startIndex ... index) - - case "n": - let startIndex = tmpJSON.startIndex - let bounds = startIndex ..< tmpJSON.index(startIndex, offsetBy: 3) - - // The character `n` should be processed in `case "\""`. - // If it is not `null` and it hits the `case`, it should be classified as a syntax error. - guard tmpJSON[bounds] == "ull" else { - tokens.append(.unknown("n" + tmpJSON)) - return - } - - tokens.append(.null) - tmpJSON.removeSubrange(bounds) - - case "t": - let startIndex = tmpJSON.startIndex - let bounds = startIndex ..< tmpJSON.index(startIndex, offsetBy: 3) - - // The character `t` should be processed in `case "\""`. - // If it is not `true` and it hits the `case`, it should be classified as a syntax error. - guard tmpJSON[bounds] == "rue" else { - tokens.append(.unknown("t" + tmpJSON)) - return - } - - tokens.append(.boolean(true)) - tmpJSON.removeSubrange(bounds) - - case "f": - let startIndex = tmpJSON.startIndex - let bounds = startIndex ..< tmpJSON.index(startIndex, offsetBy: 4) - - // The character `f` should be processed in `case "\""`. - // If it is not `false` and it hits the `case`, it should be classified as a syntax error. - guard tmpJSON[bounds] == "alse" else { - tokens.append(.unknown("f" + tmpJSON)) - return - } - - tokens.append(.boolean(false)) - tmpJSON.removeSubrange(bounds) - - case let value: - guard !tmpJSON.isEmpty && (value == "-" || value.isNumber) else { - tokens.append(.unknown(String(value) + tmpJSON)) - return - } - - var number = String(value) - var _first: Character? = tmpJSON.removeFirst() - - while let first = _first { - if first.isNumber || first == "." { - number += String(first) - _first = tmpJSON.removeFirst() - } - - // Scientific counting support - else if first.lowercased() == "e", - let next = tmpJSON.first, - (next == "+" || next == "-" || next.isNumber) { - - number += (String(first) + String(tmpJSON.removeFirst())) - _first = tmpJSON.removeFirst() - - } else { - tmpJSON = String(first) + tmpJSON - _first = nil - } - } - - guard !number.hasSuffix(".") else { - tokens.append(.unknown(String(value) + tmpJSON)) - return - } - - tokens.append(.number(number)) - } - } - } -} - -// MARK: - Tools - - -fileprivate extension String { - /// Search for the fist 'end of string' element. - /// (in case a `"` is preceded by `\` it will be ignored) - func firstEndOfString() -> String.Index? { - guard var _index = firstIndex(of: "\"") else { return nil } - - /// Search for the first index of the specified character, - /// starting from the specified start index. - /// - /// - Parameters: - /// - element: The element to search for. - /// - start: The start index to start seaching. - /// - Returns: The first index of the element after the start index, or `nil` when not found. - func _firstIndex( - of element: Character, - startingAt start: String.Index - ) -> String.Index? { - guard count > 0 && start < endIndex else { return nil } - return self[start...].firstIndex(of: element) - } - - // If this isn't the first character of the string, check if it is proceeded - // by a `\`. If so, search again, starting with the next index. - while _index > startIndex && self[index(before: _index)] == "\\" { - if let next = _firstIndex(of: "\"", startingAt: index(after: _index)) { - _index = next - } else { - return nil - } - } - - return _index - } -} diff --git a/JSONPreview/Core/Model/JSONDecorator.swift b/JSONPreview/Core/Model/JSONDecorator.swift new file mode 100644 index 0000000..dba65e8 --- /dev/null +++ b/JSONPreview/Core/Model/JSONDecorator.swift @@ -0,0 +1,563 @@ +// +// JSONDecorator.swift +// JSONPreview +// +// Created by Rakuyo on 2020/9/9. +// Copyright © 2020 Rakuyo. All rights reserved. +// + +import UIKit + +/// Responsible for beautifying JSON +public class JSONDecorator { + // Do not want the caller to directly initialize the object + private init(style: HighlightStyle) { + self.style = style + } + + /// Style of highlight. See `HighlightStyle` for details. + private let style: HighlightStyle + + /// Current number of indent + private var indent = 0 + + /// JSON slice. See `JSONSlice` for details. + public var slices: [JSONSlice] = [] + + // MARK: - Style + + /// The string used to hold the icon of the expand button + private lazy var expandIconString = createIconAttributedString(with: style.expandIcon) + + /// The string used to hold the icon of the fold button + private lazy var foldIconString = createIconAttributedString(with: style.foldIcon) + + /// A newline string with the same style as JSON. + /// Can be used to splice slices into a complete string. + private(set) lazy var wrapString = AttributedString(string: "\n", attributes: createStyle(foregroundColor: nil)) + + private lazy var startStyle = createStyle(foregroundColor: style.color.keyWord) + private lazy var keyWordStyle = createStyle(foregroundColor: style.color.keyWord) + private lazy var keyStyle = createStyle(foregroundColor: style.color.key) + private lazy var linkStyle = createStyle(foregroundColor: style.color.link) + private lazy var stringStyle = createStyle(foregroundColor: style.color.string) + private lazy var numberStyle = createStyle(foregroundColor: style.color.number) + private lazy var boolStyle = createStyle(foregroundColor: style.color.boolean) + private lazy var nullStyle = createStyle(foregroundColor: style.color.null) + + private lazy var placeholderStyle = createStyle( + foregroundColor: style.color.lineText, + other: [.backgroundColor : style.color.lineBackground] + ) + + private lazy var unknownStyle = createStyle( + foregroundColor: style.color.unknownText, + other: [.backgroundColor : style.color.unknownBackground] + ) +} + +// MARK: - Public + +public extension JSONDecorator { + /// Highlight the incoming JSON string. + /// + /// Serve for `JSONPreview`. Will split JSON into arrays that meet the requirements of `JSONPreview` display. + /// + /// - Parameters: + /// - json: The JSON string to be highlighted. + /// - judgmentValid: Whether to check the validity of JSON. + /// - style: style of highlight. See `HighlightStyle` for details. + /// - Returns: Return `nil` when JSON is invalid. See `JSONDecorator` for details. + static func highlight( + _ json: String, + judgmentValid: Bool = false, + style: HighlightStyle = .default + ) -> JSONDecorator? { + guard let data = json.data(using: .utf8) else { return nil } + + if judgmentValid { + guard let _ = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { + return nil + } + } + + let decorator = JSONDecorator(style: style) + decorator.slices = decorator.createSlices(from: data) + return decorator + } +} + +// MARK: - Main Logic + +private extension JSONDecorator { + func createSlices(from data: Data) -> [JSONSlice] { + guard let jsonValue = createJSONValue(from: data) else { return [] } + return createJSONSlices(from: jsonValue) + } + + func createJSONValue(from data: Data) -> JSONValue? { + return try? data.withUnsafeBytes { + // we got utf8... happy path + var parser = JSONParser(bytes: Array($0[0 ..< $0.count])) + return try parser.parse() + } + } + + func createJSONSlices(from jsonValue: JSONValue) -> [JSONSlice] { + return processJSONValueRecursively( + jsonValue, + currentSlicesCount: 0, + isNeedIndent: false, + isNeedComma: false) + } + + func processJSONValueRecursively( + _ jsonValue: JSONValue, + currentSlicesCount: Int, + isNeedIndent: Bool, + isNeedComma: Bool + ) -> [JSONSlice] { + var result: [JSONSlice] = [] + + /// Simplify the initialization of `JSONSlice` + func _append(expand: AttributedString, fold: AttributedString?) { + let slice = JSONSlice( + level: indent, + lineNumber: currentSlicesCount + result.count + 1, + expand: expand, + folded: fold) + + result.append(slice) + } + + func createUnknownAttributedString(with string: String) -> AttributedString { + let newString = string.replacingOccurrences(of: "\n", with: "") + return .init(string: newString, attributes: unknownStyle) + } + + // Process each json value + switch jsonValue { + // MARK: array + case .array(let values): + let (startExpand, startFold) = createArrayStartAttribute( + isNeedIndent: isNeedIndent, + isNeedComma: isNeedComma) + + _append(expand: startExpand, fold: startFold) + + func _appendArrayEnd() { + let endExpand = createArrayEndAttribute(isNeedComma: isNeedComma) + _append(expand: endExpand, fold: nil) + } + + guard !values.isEmpty else { + // If the array is empty, add the end flag directly. + _appendArrayEnd() + return result + } + + incIndent() + + // Process each value + for (i, value) in values.enumerated() { + let _isNeedComma = i != (values.count - 1) + + let slices = processJSONValueRecursively( + value, + currentSlicesCount: currentSlicesCount + result.count, + isNeedIndent: true, + isNeedComma: _isNeedComma) + result.append(contentsOf: slices) + } + + decIndent() + + // The end node is added only if the array is correct. + if case .wrong = values.last?.isRight { } else { + _appendArrayEnd() + } + + return result + + // MARK: object + case .object(let object): + let (startExpand, startFold) = createObjectStartAttribute( + isNeedIndent: isNeedIndent, + isNeedComma: isNeedComma) + + _append(expand: startExpand, fold: startFold) + + func _appendObjectEnd() { + let endExpand = createObjectEndAttribute(isNeedComma: isNeedComma) + _append(expand: endExpand, fold: nil) + } + + guard !object.isEmpty else { + // If the object is empty, add the end flag directly. + _appendObjectEnd() + return result + } + + // Sorting the key. + // The order of displaying each time the bail is taken is consistent. + let sortKeys = object.rankingUnknownKeyLast() + + incIndent() + + // Process each value + for (i, key) in sortKeys.enumerated() { + guard let value = object[key] else { continue } + + func createKeyAttribute(_ key: String, isNeedColon: Bool = true) -> AttributedString { + let keyAttribute = AttributedString(string: key, attributes: keyStyle) + if isNeedColon { + keyAttribute.append(colonAttributeString) + } + return keyAttribute + } + + // Different treatment according to different situations + switch value.isRight { + case .wrong: + let slices = processJSONValueRecursively( + value, + currentSlicesCount: currentSlicesCount + result.count, + isNeedIndent: true, + isNeedComma: false) + + if key.isWrong { + result.append(contentsOf: slices) + + } else { + let string = writeIndent() + "\"\(key.key)\"" + let expand = createKeyAttribute(string, isNeedColon: false) + + if let slice = slices.first { + expand.append(slice.expand) + } + + _append(expand: expand, fold: nil) + } + + case .right(let isContainer): + let string = writeIndent() + "\"\(key.key)\"" + + let _isNeedComma = (i != (object.count - 1)) && { + guard sortKeys.indices.contains(i + 1) else { return false } + switch object[sortKeys[i + 1]]?.isRight { + case .right: return true + default: return false + } + }() + + let expand = createKeyAttribute(string) + + if isContainer { + let fold = createKeyAttribute(string) + + // Get the content of the subvalue + var slices = processJSONValueRecursively( + value, + currentSlicesCount: currentSlicesCount + result.count, + isNeedIndent: false, + isNeedComma: _isNeedComma) + + if !slices.isEmpty { + let startSlice = slices.removeFirst() + expand.append(startSlice.expand) + + if let valueFold = startSlice.folded { + fold.append(valueFold) + } + + _append(expand: expand, fold: fold) + } + + result.append(contentsOf: slices) + + } else { + var fold: AttributedString? = nil + + // Get the content of the subvalue + let slices = processJSONValueRecursively( + value, + currentSlicesCount: 0, + isNeedIndent: false, + isNeedComma: _isNeedComma) + + // Usually there is only one value for `slices` in this case, + // so only the first value is taken + if let slice = slices.first { + expand.append(slice.expand) + + if let valueFold = slice.folded { + fold = createKeyAttribute(string) + fold?.append(valueFold) + } + + _append(expand: expand, fold: fold) + } + } + } + } + + decIndent() + + // The end node is added only if the object is correct. + if let lastKey = sortKeys.last, + case .wrong = object[lastKey]?.isRight { } + else { + _appendObjectEnd() + } + + return result + + case let .string(value, wrong): + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + "\"\(value)\"" + + let expand: AttributedString + + if let url = value.validURL { + // MARK: link + expand = AttributedString(string: string, attributes: linkStyle) + + let urlString = url.urlString + let range = NSRange(location: indent.count + 1, length: urlString.count) + + expand.addAttribute(.link, value: urlString, range: range) + expand.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range) + } else { + + // MARK: string + expand = AttributedString(string: string, attributes: stringStyle) + } + + if isNeedComma { + expand.append(commaAttributeString) + } + + if let wrong = wrong { + expand.append(createUnknownAttributedString(with: wrong)) + } + + _append(expand: expand, fold: nil) + return result + + // MARK: number + case let .number(value, wrong): + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + "\(value)" + let expand = AttributedString(string: string, attributes: numberStyle) + + if isNeedComma { + expand.append(commaAttributeString) + } + + if let wrong = wrong { + expand.append(createUnknownAttributedString(with: wrong)) + } + + _append(expand: expand, fold: nil) + return result + + // MARK: bool + case let .bool(value, wrong): + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + (value ? "true" : "false") + let expand = AttributedString(string: string, attributes: boolStyle) + + if isNeedComma { + expand.append(commaAttributeString) + } + + if let wrong = wrong { + expand.append(createUnknownAttributedString(with: wrong)) + } + + _append(expand: expand, fold: nil) + return result + + // MARK: null + case .null(let wrong): + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + "null" + let expand = AttributedString(string: string, attributes: nullStyle) + + if isNeedComma { + expand.append(commaAttributeString) + } + + if let wrong = wrong { + expand.append(createUnknownAttributedString(with: wrong)) + } + + _append(expand: expand, fold: nil) + return result + + // MARK: unknown + case .unknown(let string): + let indent = isNeedIndent ? writeIndent() : "" + + let expand = AttributedString(string: indent) + expand.append(createUnknownAttributedString(with: string)) + + _append(expand: expand, fold: nil) + return result + } + } +} + +// MARK: - Indent + +private extension JSONDecorator { + /// Fixed value of the number of contractions per increase or decrease + static let indentAmount = 1 + + func incIndent() { + indent += Self.indentAmount + } + + func decIndent() { + indent -= Self.indentAmount + } + + func writeIndent() -> String { + return (0 ..< indent).map { _ in "\t" }.joined() + } +} + +// MARK: - Attributed String + +private extension JSONDecorator { + + /// An attribute string of ":" + var colonAttributeString: AttributedString { + createKeywordAttribute(key: " : ") + } + + /// An attribute string of "," + var commaAttributeString: AttributedString { + createKeywordAttribute(key: ",") + } + + /// Create an attribute string of "array - start node" + func createArrayStartAttribute(isNeedIndent: Bool, isNeedComma: Bool) -> (AttributedString, AttributedString) { + return createStartAttribute(expand: "[", fold: "[Array...]", isNeedIndent: isNeedIndent, isNeedComma: isNeedComma) + } + + /// Create an attribute string of "Array - End Node" + func createArrayEndAttribute(isNeedComma: Bool) -> AttributedString { + return createEndAttribute(key: "]", isNeedComma: isNeedComma) + } + + /// Create an attribute string of "Object - Start Node" + func createObjectStartAttribute(isNeedIndent: Bool, isNeedComma: Bool) -> (AttributedString, AttributedString) { + return createStartAttribute(expand: "{", fold: "{Object...}", isNeedIndent: isNeedIndent, isNeedComma: isNeedComma) + } + + /// Create an attribute string of "object - end node" + func createObjectEndAttribute(isNeedComma: Bool) -> AttributedString { + return createEndAttribute(key: "}", isNeedComma: isNeedComma) + } + + /// Create an attribute string of "keyword". + /// + /// - Parameter key: keyword + /// - Returns: `AttributedString` object. + func createKeywordAttribute(key: String) -> AttributedString { + return .init(string: key, attributes: keyWordStyle) + } + + /// Create an attribute string of "begin node". + /// + /// - Parameters: + /// - expand: String when expand. + /// - fold: String when folded. + /// - isNeedIndent: Indentation required. + /// - isNeedComma: Comma required. + /// - Returns: `AttributedString` object. + func createStartAttribute( + expand: String, + fold: String, + isNeedIndent: Bool, + isNeedComma: Bool + ) -> (AttributedString, AttributedString) { + let indent = isNeedIndent ? writeIndent() : "" + + let expandString = AttributedString( + string: indent + " " + expand, + attributes: startStyle + ) + + let foldString = AttributedString( + string: fold + (isNeedComma ? "," : ""), + attributes: placeholderStyle + ) + + foldString.insert(createKeywordAttribute(key: indent + " "), at: 0) + + expandString.insert(foldIconString, at: indent.count) + foldString.insert(expandIconString, at: indent.count) + + return (expandString, foldString) + } + + /// Create an attribute string of "end node". + /// + /// - Parameters: + /// - key: Node characters, such as `}` or `]`. + /// - isNeedComma: Comma required. + /// - Returns: `AttributedString` object. + func createEndAttribute(key: String, isNeedComma: Bool) -> AttributedString { + let indent = writeIndent() + let string = key + (isNeedComma ? "," : "") + return .init(string: indent + string, attributes: startStyle) + } + + /// Create an `AttributedString` object for displaying image. + /// + /// - Parameter image: The image to be displayed. + /// - Returns: `AttributedString` object. + func createIconAttributedString(with image: UIImage) -> AttributedString { + let expandAttach = NSTextAttachment() + expandAttach.image = image + + let font = style.jsonFont + + let y = (style.lineHeight - font.lineHeight + 1) + font.descender + + expandAttach.bounds = CGRect(x: 0, y: y, width: font.ascender, height: font.ascender) + + return .init(attachment: expandAttach) + } + + func createStyle(foregroundColor: UIColor?, other: StyleInfos? = nil) -> StyleInfos { + var newStyle: StyleInfos = [.font : style.jsonFont] + + if let color = foregroundColor { + newStyle[.foregroundColor] = color + } + + let lineHeightMultiple: CGFloat = 1 + let paragraphStyle = NSMutableParagraphStyle() + + if #available(iOS 15.0, *) { + paragraphStyle.usesDefaultHyphenation = false + } + + paragraphStyle.lineHeightMultiple = lineHeightMultiple + paragraphStyle.maximumLineHeight = style.lineHeight + paragraphStyle.minimumLineHeight = style.lineHeight + paragraphStyle.lineSpacing = 0 + + newStyle[.paragraphStyle] = paragraphStyle + newStyle[.baselineOffset] = (style.lineHeight - style.jsonFont.lineHeight) + 1 + + if let other = other { + other.forEach { newStyle[$0] = $1 } + } + + return newStyle + } +} diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift new file mode 100644 index 0000000..58ccc81 --- /dev/null +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -0,0 +1,757 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + + +public struct JSONParser { + public var reader: DocumentReader + public var depth: Int = 0 + + public init(bytes: [UInt8]) { + self.reader = DocumentReader(array: bytes) + } + + public mutating func parse() throws -> JSONValue { + do { + try reader.consumeWhitespace() + let value = try self.parseValue() + + #if DEBUG + defer { + guard self.depth == 0 else { + preconditionFailure("Expected to end parsing with a depth of 0") + } + } + #endif + + // ensure only white space is remaining + var whitespace = 0 + while let next = reader.peek(offset: whitespace) { + switch next { + case ._space, ._tab, ._return, ._newline: + whitespace += 1 + continue + default: + throw JSONError.unexpectedCharacter( + jsonValue: value, + ascii: next, + characterIndex: self.reader.readerIndex + whitespace) + } + } + + return value + + } catch let JSONError.unexpectedCharacter(jsonValue, _, characterIndex) { + guard let jsonValue = jsonValue else { + return .unknown(self.reader.readUnknown(start: characterIndex)) + } + + switch jsonValue { + case .null, .number, .string, .bool: + let wrongString = self.reader.readUnknown(start: characterIndex) + return jsonValue.appendWrong(wrongString) + + case .array(var array): + guard let last = array.popLast() else { + // Theoretically empty arrays do not execute here. + return jsonValue + } + + let wrongString = self.reader.readUnknown(start: characterIndex) + array.append(last.appendWrong(wrongString)) + return .array(array) + + case .object, .unknown: + return jsonValue + } + + } catch let error { + return .unknown("\(error)") + } + } + + // MARK: Generic Value Parsing + + mutating func parseValue() throws -> JSONValue { + var whitespace = 0 + while let byte = reader.peek(offset: whitespace) { + switch byte { + case UInt8(ascii: "\""): + reader.moveReaderIndex(forwardBy: whitespace) + return .string(try reader.readString()) + case ._openbrace: + reader.moveReaderIndex(forwardBy: whitespace) + let object = try parseObject() + return .object(object) + case ._openbracket: + reader.moveReaderIndex(forwardBy: whitespace) + let array = try parseArray() + return .array(array) + case UInt8(ascii: "f"), UInt8(ascii: "t"): + reader.moveReaderIndex(forwardBy: whitespace) + let bool = try reader.readBool() + return .bool(bool) + case UInt8(ascii: "n"): + reader.moveReaderIndex(forwardBy: whitespace) + try reader.readNull() + return .null() + case UInt8(ascii: "-"), UInt8(ascii: "0") ... UInt8(ascii: "9"): + reader.moveReaderIndex(forwardBy: whitespace) + let number = try self.reader.readNumber() + return .number(number) + case ._space, ._return, ._newline, ._tab: + whitespace += 1 + continue + default: + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: self.reader.readerIndex) + } + } + + throw JSONError.unexpectedEndOfFile + } + + + // MARK: - Parse Array - + + mutating func parseArray() throws -> [JSONValue] { + precondition(self.reader.read() == ._openbracket) + guard self.depth < 512 else { + throw JSONError.tooManyNestedArraysOrDictionaries(characterIndex: self.reader.readerIndex - 1) + } + self.depth += 1 + defer { depth -= 1 } + + // parse first value or end immediatly + switch try reader.consumeWhitespace() { + case ._space, ._return, ._newline, ._tab: + preconditionFailure("Expected that all white space is consumed") + case ._closebracket: + // if the first char after whitespace is a closing bracket, we found an empty array + self.reader.moveReaderIndex(forwardBy: 1) + return [] + default: + break + } + + var array = [JSONValue]() + array.reserveCapacity(10) + + // parse values + while true { + let value = try parseValue() + array.append(value) + + // consume the whitespace after the value before the comma + let ascii = try reader.consumeWhitespace() + switch ascii { + case ._space, ._return, ._newline, ._tab: + preconditionFailure("Expected that all white space is consumed") + case ._closebracket: + reader.moveReaderIndex(forwardBy: 1) + return array + case ._comma: + // consume the comma + reader.moveReaderIndex(forwardBy: 1) + // consume the whitespace before the next value + if try reader.consumeWhitespace() == ._closebracket { + // the foundation json implementation does support trailing commas + reader.moveReaderIndex(forwardBy: 1) + return array + } + continue + default: + throw JSONError.unexpectedCharacter( + jsonValue: .array(array), + ascii: ascii, + characterIndex: reader.readerIndex) + } + } + } + + // MARK: - Object parsing - + + mutating func parseObject() throws -> [JSONObjectKey: JSONValue] { + precondition(self.reader.read() == ._openbrace) + guard self.depth < 512 else { + throw JSONError.tooManyNestedArraysOrDictionaries(characterIndex: self.reader.readerIndex - 1) + } + self.depth += 1 + defer { depth -= 1 } + + // parse first value or end immediatly + switch try reader.consumeWhitespace() { + case ._space, ._return, ._newline, ._tab: + preconditionFailure("Expected that all white space is consumed") + case ._closebrace: + // if the first char after whitespace is a closing bracket, we found an empty array + self.reader.moveReaderIndex(forwardBy: 1) + return [:] + default: + break + } + + var object = [JSONObjectKey: JSONValue]() + object.reserveCapacity(20) + + while true { + let key: JSONObjectKey + do { + key = .init(try reader.readString()) + + } catch JSONError.unexpectedCharacter(_, _, let characterIndex) { + object[.wrong] = .unknown(reader.readUnknown(start: characterIndex)) + return object + + } catch let error { + throw error + } + + let colon = try reader.consumeWhitespace() + guard colon == ._colon else { + let characterIndex = reader.readerIndex + object[key] = .unknown(reader.readUnknown(start: characterIndex)) + + throw JSONError.unexpectedCharacter( + jsonValue: .object(object), + ascii: colon, + characterIndex: characterIndex) + } + reader.moveReaderIndex(forwardBy: 1) + try reader.consumeWhitespace() + + do { + object[key] = try self.parseValue() + + let commaOrBrace = try reader.consumeWhitespace() + switch commaOrBrace { + case ._closebrace: + reader.moveReaderIndex(forwardBy: 1) + return object + case ._comma: + reader.moveReaderIndex(forwardBy: 1) + if try reader.consumeWhitespace() == ._closebrace { + // the foundation json implementation does support trailing commas + reader.moveReaderIndex(forwardBy: 1) + return object + } + continue + default: + let characterIndex = reader.readerIndex + object[.wrong] = .unknown(reader.readUnknown(start: characterIndex)) + + throw JSONError.unexpectedCharacter( + jsonValue: .object(object), + ascii: commaOrBrace, + characterIndex: characterIndex) + } + + } catch let JSONError.unexpectedCharacter(jsonValue, ascii, characterIndex) { + /// In the scenario of nested container elements, if the value itself is + /// incorrectly formatted, it should be handled as an "error rendering". + /// + /// However, this kind of processing is currently missing, + /// and a good way to implement this logic has not been thought of yet. + /// + /// For example, if the value of an object is an array or an object, + /// and there is a missing `]` (array) or `}` (object), then this problem will occur. + /// + /// ```json + /// { + /// "key": [ + /// "123" + /// // Missing `]`, which will cause some json content to be missing when rendering. + /// } + /// ``` + #warning("TODO Handling nested scenarios with wrong value") + + var _jsonValue = jsonValue + if _jsonValue == nil { + // There is only one possibility to have no value + object[key] = .unknown(reader.readUnknown(start: characterIndex - 2)) + _jsonValue = .object(object) + } + + throw JSONError.unexpectedCharacter( + jsonValue: _jsonValue, + ascii: ascii, + characterIndex: characterIndex) + + } catch let error { + throw error + } + } + } +} + +extension JSONParser { + + public struct DocumentReader { + public let array: [UInt8] + + private(set) var readerIndex: Int = 0 + + private var readableBytes: Int { + self.array.endIndex - self.readerIndex + } + + public var isEOF: Bool { + self.readerIndex >= self.array.endIndex + } + + + public init(array: [UInt8]) { + self.array = array + } + + public subscript(bounds: Range) -> ArraySlice { + self.array[bounds] + } + + public mutating func read() -> UInt8? { + guard self.readerIndex < self.array.endIndex else { + self.readerIndex = self.array.endIndex + return nil + } + + defer { self.readerIndex += 1 } + + return self.array[self.readerIndex] + } + + public func peek(offset: Int = 0) -> UInt8? { + guard self.readerIndex + offset < self.array.endIndex else { + return nil + } + + return self.array[self.readerIndex + offset] + } + + public mutating func moveReaderIndex(forwardBy offset: Int) { + self.readerIndex += offset + } + + @discardableResult + public mutating func consumeWhitespace() throws -> UInt8 { + var whitespace = 0 + while let ascii = self.peek(offset: whitespace) { + switch ascii { + case ._space, ._return, ._newline, ._tab: + whitespace += 1 + continue + default: + self.moveReaderIndex(forwardBy: whitespace) + return ascii + } + } + + throw JSONError.unexpectedEndOfFile + } + + public mutating func readString() throws -> String { + try self.readUTF8StringTillNextUnescapedQuote() + } + + public mutating func readNumber() throws -> String { + try self.parseNumber() + } + + public mutating func readBool() throws -> Bool { + switch self.read() { + case UInt8(ascii: "t"): + try readGenericValue([UInt8]._trueSub) + return true + + case UInt8(ascii: "f"): + try readGenericValue([UInt8]._falseSub) + return false + + default: + preconditionFailure("Expected to have `t` or `f` as first character") + } + } + + public mutating func readNull() throws { + try readGenericValue([UInt8]._null) + } + + public func readUnknown(start index: Int) -> String { + let start = min(index, max(array.count, 1) - 1) + return String(decoding: self[start ..< array.count], as: Unicode.UTF8.self) + } + + // MARK: - Private Methods - + + mutating func readGenericValue(_ value: [UInt8]) throws { + for (i, ascii) in value.enumerated() { + if self.read() == ascii { continue } + + let offset = min(2 + i, self.readerIndex) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: self.peek(offset: -offset)!, + characterIndex: self.readerIndex - offset) + } + } + + // MARK: String + + public enum EscapedSequenceError: Swift.Error { + case expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: Int) + case unexpectedEscapedCharacter(ascii: UInt8, index: Int) + case couldNotCreateUnicodeScalarFromUInt32(index: Int, unicodeScalarValue: UInt32) + } + + private mutating func readUTF8StringTillNextUnescapedQuote() throws -> String { + guard self.read() == ._quote else { + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: self.peek(offset: -1)!, + characterIndex: self.readerIndex - 1) + } + var stringStartIndex = self.readerIndex + var copy = 0 + var output: String? + + while let byte = peek(offset: copy) { + switch byte { + case UInt8(ascii: "\""): + self.moveReaderIndex(forwardBy: copy + 1) + guard var result = output else { + // if we don't have an output string we create a new string + return String(decoding: self[stringStartIndex ..< stringStartIndex + copy], as: Unicode.UTF8.self) + } + // if we have an output string we append + result += String(decoding: self[stringStartIndex ..< stringStartIndex + copy], as: Unicode.UTF8.self) + return result + + case 0 ... 31: + // All Unicode characters may be placed within the + // quotation marks, except for the characters that must be escaped: + // quotation mark, reverse solidus, and the control characters (U+0000 + // through U+001F). + var string = output ?? "" + let errorIndex = self.readerIndex + copy + string += self.makeStringFast(self.array[stringStartIndex ... errorIndex]) + throw JSONError.unescapedControlCharacterInString(ascii: byte, in: string, index: errorIndex) + + case UInt8(ascii: "\\"): + self.moveReaderIndex(forwardBy: copy) + if output != nil { + output! += self.makeStringFast(self.array[stringStartIndex ..< stringStartIndex + copy]) + } else { + output = self.makeStringFast(self.array[stringStartIndex ..< stringStartIndex + copy]) + } + + let escapedStartIndex = self.readerIndex + + do { + let escaped = try parseEscapeSequence() + output! += escaped + stringStartIndex = self.readerIndex + copy = 0 + } catch EscapedSequenceError.unexpectedEscapedCharacter(let ascii, let failureIndex) { + output! += makeStringFast(array[escapedStartIndex ..< self.readerIndex]) + throw JSONError.unexpectedEscapedCharacter(ascii: ascii, in: output!, index: failureIndex) + } catch EscapedSequenceError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(let failureIndex) { + output! += makeStringFast(array[escapedStartIndex ..< self.readerIndex]) + throw JSONError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(in: output!, index: failureIndex) + } catch EscapedSequenceError.couldNotCreateUnicodeScalarFromUInt32(let failureIndex, let unicodeScalarValue) { + output! += makeStringFast(array[escapedStartIndex ..< self.readerIndex]) + throw JSONError.couldNotCreateUnicodeScalarFromUInt32( + in: output!, index: failureIndex, unicodeScalarValue: unicodeScalarValue + ) + } + + default: + copy += 1 + continue + } + } + + throw JSONError.unexpectedEndOfFile + } + + // can be removed as soon https://bugs.swift.org/browse/SR-12126 and + // https://bugs.swift.org/browse/SR-12125 has landed. + // Thanks @weissi for making my code fast! + private func makeStringFast(_ bytes: Bytes) -> String where Bytes.Element == UInt8 { + if let string = bytes.withContiguousStorageIfAvailable({ String(decoding: $0, as: Unicode.UTF8.self) }) { + return string + } else { + return String(decoding: bytes, as: Unicode.UTF8.self) + } + } + + private mutating func parseEscapeSequence() throws -> String { + precondition(self.read() == ._backslash, "Expected to have an backslash first") + guard let ascii = self.read() else { + throw JSONError.unexpectedEndOfFile + } + + switch ascii { + case 0x22: return "\"" + case 0x5C: return "\\" + case 0x2F: return "/" + case 0x62: return "\u{08}" // \b + case 0x66: return "\u{0C}" // \f + case 0x6E: return "\u{0A}" // \n + case 0x72: return "\u{0D}" // \r + case 0x74: return "\u{09}" // \t + case 0x75: + let character = try parseUnicodeSequence() + return String(character) + default: + throw EscapedSequenceError.unexpectedEscapedCharacter(ascii: ascii, index: self.readerIndex - 1) + } + } + + private mutating func parseUnicodeSequence() throws -> Unicode.Scalar { + // we build this for utf8 only for now. + let bitPattern = try parseUnicodeHexSequence() + + // check if high surrogate + let isFirstByteHighSurrogate = bitPattern & 0xFC00 // nil everything except first six bits + if isFirstByteHighSurrogate == 0xD800 { + // if we have a high surrogate we expect a low surrogate next + let highSurrogateBitPattern = bitPattern + guard let (escapeChar) = self.read(), + let (uChar) = self.read() + else { + throw JSONError.unexpectedEndOfFile + } + + guard escapeChar == UInt8(ascii: #"\"#), uChar == UInt8(ascii: "u") else { + throw EscapedSequenceError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: self.readerIndex - 1) + } + + let lowSurrogateBitBattern = try parseUnicodeHexSequence() + let isSecondByteLowSurrogate = lowSurrogateBitBattern & 0xFC00 // nil everything except first six bits + guard isSecondByteLowSurrogate == 0xDC00 else { + // we are in an escaped sequence. for this reason an output string must have + // been initialized + throw EscapedSequenceError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: self.readerIndex - 1) + } + + let highValue = UInt32(highSurrogateBitPattern - 0xD800) * 0x400 + let lowValue = UInt32(lowSurrogateBitBattern - 0xDC00) + let unicodeValue = highValue + lowValue + 0x10000 + guard let unicode = Unicode.Scalar(unicodeValue) else { + throw EscapedSequenceError.couldNotCreateUnicodeScalarFromUInt32( + index: self.readerIndex, unicodeScalarValue: unicodeValue + ) + } + return unicode + } + + guard let unicode = Unicode.Scalar(bitPattern) else { + throw EscapedSequenceError.couldNotCreateUnicodeScalarFromUInt32( + index: self.readerIndex, unicodeScalarValue: UInt32(bitPattern) + ) + } + return unicode + } + + private mutating func parseUnicodeHexSequence() throws -> UInt16 { + // As stated in RFC-8259 an escaped unicode character is 4 HEXDIGITs long + // https://tools.ietf.org/html/rfc8259#section-7 + let startIndex = self.readerIndex + guard let firstHex = self.read(), + let secondHex = self.read(), + let thirdHex = self.read(), + let forthHex = self.read() + else { + throw JSONError.unexpectedEndOfFile + } + + guard let first = DocumentReader.hexAsciiTo4Bits(firstHex), + let second = DocumentReader.hexAsciiTo4Bits(secondHex), + let third = DocumentReader.hexAsciiTo4Bits(thirdHex), + let forth = DocumentReader.hexAsciiTo4Bits(forthHex) + else { + let hexString = String(decoding: [firstHex, secondHex, thirdHex, forthHex], as: Unicode.UTF8.self) + throw JSONError.invalidHexDigitSequence(hexString, index: startIndex) + } + let firstByte = UInt16(first) << 4 | UInt16(second) + let secondByte = UInt16(third) << 4 | UInt16(forth) + + let bitPattern = UInt16(firstByte) << 8 | UInt16(secondByte) + + return bitPattern + } + + private static func hexAsciiTo4Bits(_ ascii: UInt8) -> UInt8? { + switch ascii { + case 48 ... 57: + return ascii - 48 + case 65 ... 70: + // uppercase letters + return ascii - 55 + case 97 ... 102: + // lowercase letters + return ascii - 87 + default: + return nil + } + } + + // MARK: Numbers + + private enum ControlCharacter { + case operand + case decimalPoint + case exp + case expOperator + } + + private mutating func parseNumber() throws -> String { + var pastControlChar: ControlCharacter = .operand + var numbersSinceControlChar: UInt = 0 + var hasLeadingZero = false + + // parse first character + + guard let ascii = self.peek() else { + preconditionFailure("Why was this function called, if there is no 0...9 or -") + } + switch ascii { + case UInt8(ascii: "0"): + numbersSinceControlChar = 1 + pastControlChar = .operand + hasLeadingZero = true + case UInt8(ascii: "1") ... UInt8(ascii: "9"): + numbersSinceControlChar = 1 + pastControlChar = .operand + case UInt8(ascii: "-"): + numbersSinceControlChar = 0 + pastControlChar = .operand + default: + preconditionFailure("Why was this function called, if there is no 0...9 or -") + } + + var numberchars = 1 + + // parse everything else + while let byte = self.peek(offset: numberchars) { + switch byte { + case UInt8(ascii: "0"): + if hasLeadingZero { + throw JSONError.numberWithLeadingZero(index: readerIndex + numberchars) + } + if numbersSinceControlChar == 0, pastControlChar == .operand { + // the number started with a minus. this is the leading zero. + hasLeadingZero = true + } + numberchars += 1 + numbersSinceControlChar += 1 + case UInt8(ascii: "1") ... UInt8(ascii: "9"): + if hasLeadingZero { + throw JSONError.numberWithLeadingZero(index: readerIndex + numberchars) + } + numberchars += 1 + numbersSinceControlChar += 1 + case UInt8(ascii: "."): + guard numbersSinceControlChar > 0, pastControlChar == .operand else { + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) + } + + numberchars += 1 + hasLeadingZero = false + pastControlChar = .decimalPoint + numbersSinceControlChar = 0 + + case UInt8(ascii: "e"), UInt8(ascii: "E"): + guard numbersSinceControlChar > 0, + pastControlChar == .operand || pastControlChar == .decimalPoint + else { + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) + } + + numberchars += 1 + hasLeadingZero = false + pastControlChar = .exp + numbersSinceControlChar = 0 + case UInt8(ascii: "+"), UInt8(ascii: "-"): + guard numbersSinceControlChar == 0, pastControlChar == .exp else { + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) + } + + numberchars += 1 + pastControlChar = .expOperator + numbersSinceControlChar = 0 + case ._space, ._return, ._newline, ._tab, ._comma, ._closebracket, ._closebrace: + guard numbersSinceControlChar > 0 else { + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) + } + let numberStartIndex = self.readerIndex + self.moveReaderIndex(forwardBy: numberchars) + + return self.makeStringFast(self[numberStartIndex ..< self.readerIndex]) + default: + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) + } + } + + guard numbersSinceControlChar > 0 else { + throw JSONError.unexpectedEndOfFile + } + + defer { self.readerIndex = self.array.endIndex } + return String(decoding: self.array.suffix(from: readerIndex), as: Unicode.UTF8.self) + } + } +} + +extension UInt8 { + fileprivate static let _space = UInt8(ascii: " ") + fileprivate static let _return = UInt8(ascii: "\r") + fileprivate static let _newline = UInt8(ascii: "\n") + fileprivate static let _tab = UInt8(ascii: "\t") + + fileprivate static let _colon = UInt8(ascii: ":") + fileprivate static let _comma = UInt8(ascii: ",") + + fileprivate static let _openbrace = UInt8(ascii: "{") + fileprivate static let _closebrace = UInt8(ascii: "}") + + fileprivate static let _openbracket = UInt8(ascii: "[") + fileprivate static let _closebracket = UInt8(ascii: "]") + + fileprivate static let _quote = UInt8(ascii: "\"") + fileprivate static let _backslash = UInt8(ascii: "\\") +} + +extension Array where Element == UInt8 { + fileprivate static let _true = [UInt8(ascii: "t")] + _trueSub + fileprivate static let _trueSub = [UInt8(ascii: "r"), UInt8(ascii: "u"), UInt8(ascii: "e")] + + fileprivate static let _false = [UInt8(ascii: "f")] + _falseSub + fileprivate static let _falseSub = [UInt8(ascii: "a"), UInt8(ascii: "l"), UInt8(ascii: "s"), UInt8(ascii: "e")] + + fileprivate static let _null = [UInt8(ascii: "n"), UInt8(ascii: "u"), UInt8(ascii: "l"), UInt8(ascii: "l")] +} diff --git a/JSONPreview/Core/Tools/Dictionary+Sort.swift b/JSONPreview/Core/Tools/Dictionary+Sort.swift new file mode 100644 index 0000000..677855f --- /dev/null +++ b/JSONPreview/Core/Tools/Dictionary+Sort.swift @@ -0,0 +1,38 @@ +// +// Dictionary+Sort.swift +// JSONPreview +// +// Created by Rakuyo on 2022/5/30. +// Copyright © 2022 Rakuyo. All rights reserved. +// + +import Foundation + +extension Dictionary where Key == JSONObjectKey, Value == JSONValue { + /// Put the `.unknown` value in last place. + /// + /// Returns a sorted array of keys, + /// conforming to the following rules: + /// + /// 1. If `JSONValue.unknown` exists, then it will be the last one in the array. + /// 2. The remaining elements will be sorted by `<`. + func rankingUnknownKeyLast() -> [Key] { + guard !isEmpty else { return [] } + + var unknownKey: Key? = nil + + let otherKeys = keys.drop { key in + guard case .unknown = self[key] else { return false } + unknownKey = key + return true + } + + var result = Array(otherKeys).sorted(by: <) + + if let unknownKey = unknownKey { + result.append(unknownKey) + } + + return result + } +} diff --git a/JSONPreview/Core/String+ValidURL.swift b/JSONPreview/Core/Tools/String+ValidURL.swift similarity index 100% rename from JSONPreview/Core/String+ValidURL.swift rename to JSONPreview/Core/Tools/String+ValidURL.swift diff --git a/JSONPreview/Core/JSONPreview.swift b/JSONPreview/Core/View/JSONPreview.swift similarity index 93% rename from JSONPreview/Core/JSONPreview.swift rename to JSONPreview/Core/View/JSONPreview.swift index 499aee3..848dc0a 100644 --- a/JSONPreview/Core/JSONPreview.swift +++ b/JSONPreview/Core/View/JSONPreview.swift @@ -103,7 +103,7 @@ open class JSONPreview: UIView { private var decorator: JSONDecorator! { didSet { // Combine the slice result into a string - let tmp = NSMutableAttributedString(string: "") + let tmp = AttributedString(string: "") decorator.slices.forEach { tmp.append($0.expand) @@ -115,6 +115,8 @@ open class JSONPreview: UIView { guard let this = self else { return } this.jsonTextView.attributedText = tmp + + guard !this.decorator.slices.isEmpty else { return } this.lineDataSource = (1 ... this.decorator.slices.count).map { $0 } } } @@ -179,9 +181,9 @@ private extension JSONPreview { lineNumberTableView.bottomAnchor.constraint(equalTo: bottomAnchor), ] - constraints.append(lineNumberTableView.leftAnchor.constraint(equalTo: { - guard #available(iOS 11.0, *) else { return leftAnchor } - return safeAreaLayoutGuide.leftAnchor + constraints.append(lineNumberTableView.leadingAnchor.constraint(equalTo: { + guard #available(iOS 11.0, *) else { return leadingAnchor } + return safeAreaLayoutGuide.leadingAnchor }())) NSLayoutConstraint.activate(constraints) @@ -189,14 +191,14 @@ private extension JSONPreview { func addJSONTextViewLayout() { var constraints = [ - jsonTextView.leftAnchor.constraint(equalTo: lineNumberTableView.rightAnchor/*, constant: -1*/), + jsonTextView.leadingAnchor.constraint(equalTo: lineNumberTableView.trailingAnchor/*, constant: -1*/), jsonTextView.topAnchor.constraint(equalTo: lineNumberTableView.topAnchor), jsonTextView.bottomAnchor.constraint(equalTo: lineNumberTableView.bottomAnchor), ] - constraints.append(jsonTextView.rightAnchor.constraint(equalTo: { - guard #available(iOS 11.0, *) else { return rightAnchor } - return safeAreaLayoutGuide.rightAnchor + constraints.append(jsonTextView.trailingAnchor.constraint(equalTo: { + guard #available(iOS 11.0, *) else { return trailingAnchor } + return safeAreaLayoutGuide.trailingAnchor }())) NSLayoutConstraint.activate(constraints) @@ -398,28 +400,28 @@ extension JSONPreview: JSONTextViewDelegate { var lines: [Int] = [] var length = clickSlice.expand.length - for i in realRow + 1 ..< slices.count { + for i in (realRow + 1) ..< slices.count { guard isExecution else { break } - let _slices = slices[i] + let _slice = slices[i] - guard _slices.level >= clickSlice.level else { continue } + guard _slice.level >= clickSlice.level else { continue } - if _slices.level == clickSlice.level { isExecution = false } + if _slice.level == clickSlice.level { isExecution = false } // Increase the number of times being folded decorator.slices[i].foldedTimes += 1 - guard _slices.foldedTimes == 0 else { continue } + guard _slice.foldedTimes == 0 else { continue } // Record the line number to be hidden - lines.append(_slices.lineNumber) + lines.append(_slice.lineNumber) // Accumulate the length of the string to be hidden length = length + 1 /* Wrap */ + { - switch _slices.state { - case .expand: return _slices.expand.length - case .folded: return _slices.folded?.length ?? 0 + switch _slice.state { + case .expand: return _slice.expand.length + case .folded: return _slice.folded?.length ?? 0 } }() } @@ -449,7 +451,7 @@ extension JSONPreview: JSONTextViewDelegate { var isExecution = true var lines: [Int] = [] - let replaceString = NSMutableAttributedString(string: "") + let replaceString = AttributedString(string: "") for i in realRow + 1 ..< slices.count { guard isExecution else { break } diff --git a/JSONPreview/Core/JSONTextView.swift b/JSONPreview/Core/View/JSONTextView.swift similarity index 100% rename from JSONPreview/Core/JSONTextView.swift rename to JSONPreview/Core/View/JSONTextView.swift diff --git a/JSONPreview/Core/LineNumberCell.swift b/JSONPreview/Core/View/LineNumberCell.swift similarity index 100% rename from JSONPreview/Core/LineNumberCell.swift rename to JSONPreview/Core/View/LineNumberCell.swift diff --git a/JSONPreview/Core/LineNumberTableView.swift b/JSONPreview/Core/View/LineNumberTableView.swift similarity index 100% rename from JSONPreview/Core/LineNumberTableView.swift rename to JSONPreview/Core/View/LineNumberTableView.swift diff --git a/JSONPreview/Other/Base.lproj/Main.storyboard b/JSONPreview/Other/Base.lproj/Main.storyboard index 25a7638..2fac4de 100644 --- a/JSONPreview/Other/Base.lproj/Main.storyboard +++ b/JSONPreview/Other/Base.lproj/Main.storyboard @@ -1,24 +1,44 @@ - + + - + + - + - + - + - + + + + + + + + + + + + + + + + + + + diff --git a/JSONPreview/Other/ViewController.swift b/JSONPreview/Other/ViewController.swift index 34c0105..05bc42f 100644 --- a/JSONPreview/Other/ViewController.swift +++ b/JSONPreview/Other/ViewController.swift @@ -19,33 +19,51 @@ class ViewController: UIViewController { previewView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(previewView) - view.translatesAutoresizingMaskIntoConstraints = false - var constraints = [ - previewView.heightAnchor.constraint(equalToConstant: 350), - previewView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + var constraints: [NSLayoutConstraint] = [ +// previewView.heightAnchor.constraint(equalTo: view.heightAnchor), +// previewView.centerYAnchor.constraint(equalTo: view.centerYAnchor), ] - constraints.append(previewView.leftAnchor.constraint(equalTo: { + constraints.append(previewView.topAnchor.constraint(equalTo: { if #available(iOS 11.0, *) { - return view.safeAreaLayoutGuide.leftAnchor + return view.safeAreaLayoutGuide.topAnchor } else { - return view.leftAnchor + return view.topAnchor } }())) - constraints.append(previewView.rightAnchor.constraint(equalTo: { + constraints.append(previewView.bottomAnchor.constraint(equalTo: { if #available(iOS 11.0, *) { - return view.safeAreaLayoutGuide.rightAnchor + return view.safeAreaLayoutGuide.bottomAnchor } else { - return view.rightAnchor + return view.bottomAnchor + } + }())) + + constraints.append(previewView.leadingAnchor.constraint(equalTo: { + if #available(iOS 11.0, *) { + return view.safeAreaLayoutGuide.leadingAnchor + } else { + return view.leadingAnchor + } + }())) + + constraints.append(previewView.trailingAnchor.constraint(equalTo: { + if #available(iOS 11.0, *) { + return view.safeAreaLayoutGuide.trailingAnchor + } else { + return view.trailingAnchor } }())) NSLayoutConstraint.activate(constraints) - let json = """ + let json = + """ [ + [], + [], { "string" : "string", "int" : 1024, @@ -110,12 +128,11 @@ class ViewController: UIViewController { } } }, - { - {123456} - } + {123456} ] """ + let start = Date().timeIntervalSince1970 print("will display json") diff --git a/README.md b/README.md index 957e65d..9d26110 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ All of `JSONPreview`'s features are written using **native methods**, which mean Here is a gif of about 25 seconds (**about 2.5M**) that shows the effect when using this library to preview JSON. -![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/screenshot.gif) +![screenshot](Images/screenshot.gif) ## Prerequisites @@ -40,13 +40,13 @@ pod 'JSONPreview' - File > Swift Packages > Add Package Dependency - Add https://github.com/rakuyoMo/JSONPreview.git -- Select "Up to Next Major" with "1.3.5" +- Select "Up to Next Major" with "1.3.6" Or add the following to your `Package.swift` file: ```swift dependencies: [ - .package(url: "https://github.com/rakuyoMo/JSONPreview.git", from: "1.3.5") + .package(url: "https://github.com/rakuyoMo/JSONPreview.git", from: "1.3.6") ] ``` @@ -64,7 +64,7 @@ dependencies: [ ## Usage -> After downloading the project, [`ViewController.swift`](https://github.com/rakuyoMo/JSONPreview/blob/master/JSONPreview/JSONPreview/Other/ViewController.swift) file contains part of the test code, just run the project Check the corresponding effect. +> After downloading the project, [`ViewController.swift`](JSONPreview/Other/ViewController.swift#L47) file contains part of the test code, just run the project Check the corresponding effect. 1. Create the `JSONPreview` object and add it to the interface. @@ -84,7 +84,7 @@ previewView.preview(json) 3. If you want to customize the highlight style, you can set it through the `HighlightStyle` and `HighlightColor` types: -> Among them, [`ConvertibleToColor`](https://github.com/rakuyoMo/JSONPreview/blob/master/JSONPreview/JSONPreview/Core/HighlightColor.swift#L119) is a protocol for providing colors. Through this protocol, you can directly use the `UIColor` object, or easily convert such objects as `0xffffff`, `#FF7F20` and `[0.72, 0.18, 0.13]` to `UIColor` objects. +> Among them, [`ConvertibleToColor`](JSONPreview/Core/Entity/HighlightColor.swift#L117) is a protocol for providing colors. Through this protocol, you can directly use the `UIColor` object, or easily convert such objects as `0xffffff`, `#FF7F20` and `[0.72, 0.18, 0.13]` to `UIColor` objects. ```swift let highlightColor = HighlightColor( @@ -118,27 +118,27 @@ previewView.preview(json, style: style) ### Rendering -For rendering, `JSONPreview` performs only **limited** formatting checks, including. +For rendering, `JSONPreview` only performs **limited** formatting checks. -> None of the `previous nodes` mentioned below include `spaces`, `\t`, and `\n`. +The conditions that are known to trigger `Error Rendering` include -- The JSON to be previewed must start with `{` or `[`. -- The previous node of `:` must be `.string`. -- The previous node of `,` can only be one of `.null`, `.link`, `.string`, `.number`, `.boolean`, `}`, and `]`. -- `{` must have a preceding node, and the preceding node must not be `{`. -- `}` must appear in pairs with `{`. -- `[` must exist for the previous node, and the previous node cannot be `]`. -- `]` must occur in pairs with `[`. -- `"` must occur in pairs. -- The previous node of `"` can only be one of `{`, `[`, `,` and `:`. -- Spell checking for `null`, `true`, and `false`. +- Value Unconventional JSON types. Supported types include `object`, `array`, `number`, `bool`, `string`, and `null`. +- Checks for the `number` format, such as scientific notation and decimals. +- Spell checking for `true`, `false` and `null`. - For scientific notation, the next node of `{E/e}` must be `+`, `-` or a number. +- `array` elements are not separated by `,`. +- `object` elements are not separated by `,`. +- No `:` after the key of `object`. +- `object` has `:` after the key but is missing the value. +- The key of `object` is not a string. -Any other syntax errors will not trigger a rendering error. +In addition to the conditions explicitly mentioned above, other errors may also trigger an "error rendering". There may also be errors outside the scope of the formatting check that do not trigger "error rendering". However, they may **lead to missing json content**. + +It is recommended that you do not rely too much on `JSONPreview` format checking, and use it to preview correctly formatted json whenever possible. ### Link -The *1.2.0* version adds rendering of links (`.link`). While rendering, `JSONPreview` performs a limited **de-escaping** operation. +The *1.2.0* version adds rendering of links. While rendering, `JSONPreview` performs a limited **de-escaping** operation. The de-escaping operations supported by different versions are as follows: @@ -148,7 +148,7 @@ The de-escaping operations supported by different versions are as follows: ## Data Flow Diagram -![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/DFD.png) +![DFD](Images/DFD.jpg) ## TODO @@ -161,4 +161,4 @@ Thanks to [Awhisper](https://github.com/Awhisper) for his valuable input during ## License -`JSONPreview` is available under the **MIT** license. For more information, see [LICENSE](https://github.com/rakuyoMo/JSONPreview/blob/master/LICENSE). +`JSONPreview` is available under the **MIT** license. For more information, see [LICENSE](LICENSE). diff --git a/README_CN.md b/README_CN.md index e27a391..8ff8c9e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -18,7 +18,7 @@ 下面是一个大约25秒的gif(**大约2.5M**),它展示了使用本库预览 JSON 时的效果。 -![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/screenshot.gif) +![screenshot](Images/screenshot.gif) ## 基本要求 @@ -38,13 +38,13 @@ pod 'JSONPreview' - 依次选择 File > Swift Packages > Add Package Dependency - 输入 https://github.com/rakuyoMo/JSONPreview.git -- 选择 "Up to Next Major" 并填入 "1.3.5" +- 选择 "Up to Next Major" 并填入 "1.3.6" 或者将下面的内容添加到 `Package.swift` 文件中: ```swift dependencies: [ - .package(url: "https://github.com/rakuyoMo/JSONPreview.git", from: "1.3.5") + .package(url: "https://github.com/rakuyoMo/JSONPreview.git", from: "1.3.6") ] ``` @@ -62,7 +62,7 @@ dependencies: [ ## 使用 -> 下载项目后,[`ViewController.swift`](https://github.com/rakuyoMo/JSONPreview/blob/master/JSONPreview/JSONPreview/Other/ViewController.swift) 文件中包含部分测试代码,运行项目即可查看对应的效果。 +> 下载项目后,[`ViewController.swift`](JSONPreview/Other/ViewController.swift#L47) 文件中包含部分测试代码,运行项目即可查看对应的效果。 1. 首先创建 `JSONPreview` 对象,并添加到界面上: @@ -82,7 +82,7 @@ previewView.preview(json) 3. 如果您想要自定义高亮样式,可通过 `HighlightStyle` 与 `HighlightColor` 类型进行设置: -> 其中,[`ConvertibleToColor`](https://github.com/rakuyoMo/JSONPreview/blob/master/JSONPreview/JSONPreview/Core/HighlightColor.swift#L119) 是一个用于提供颜色的协议。通过该协议,您可以直接使用 `UIColor` 对象,或轻松的将诸如 `0xffffff`、`#FF7F20` 以及 `[0.72, 0.18, 0.13]` 转换为 `UIColor` 对象。 +> 其中,[`ConvertibleToColor`](JSONPreview/Core/Entity/HighlightColor.swift#L117) 是一个用于提供颜色的协议。通过该协议,您可以直接使用 `UIColor` 对象,或轻松的将诸如 `0xffffff`、`#FF7F20` 以及 `[0.72, 0.18, 0.13]` 转换为 `UIColor` 对象。 ```swift let highlightColor = HighlightColor( @@ -116,27 +116,27 @@ previewView.preview(json, style: style) ### 渲染 -对于渲染,`JSONPreview` 只进行**有限**的格式检查,包括: +对于渲染,`JSONPreview` 只进行**有限**的格式检查。 -> 以下所提到的 “上一个节点” 均不包括 `空格`、`\t` 以及 `\n`。 +目前已知的会触发 “错误渲染” 的条件,包括: -- 所要预览的JSON必须以 `{` 或 `[` 开头。 -- `:` 的上一个节点必须是 `.string`。 -- `,` 的上一个节点只能是 `.null`、`.link`、`.string`、`.number`、`.boolean`、`}` 以及 `]` 中的一个。 -- `{` 必须存在上一个节点,同时上一个节点不能为 `{`。 -- `}` 必须与 `{` 成对出现。 -- `[` 必须存在上一个节点,同时上一个节点不能为 `]`。 -- `]` 必须与 `[` 成对出现。 -- `"` 必须成对出现。 -- `"` 的上一个节点只能是 `{`、`[`、`,` 以及 `:` 中的一个。 -- 针对 `null`、`true` 以及 `false` 的拼写检查。 +- Value 非常规 JSON 类型。支持的类型包括 `object`、`array`、`number`、`bool`、`string` 以及 `null`。 +- 对于 `number` 格式的检查,例如科学计数法以及小数。 +- 针对于 `true`、`false` 以及 `null` 的拼写检查。 - 针对科学计数法,`{E/e}` 的下一个节点必须是 `+`、`-` 或数字。 +- `array` 元素之间没有使用 `,` 进行分隔。 +- `object` 元素之间没有使用 `,` 进行分隔。 +- `object` 的 key 后没有`:`。 +- `object` 的 key 后有`:`,但是缺失 value。 +- `object` 的 key 不是字符串。 -除此之外的语法错误均不会触发渲染错误。 +除了上述明确提到的条件,其他错误也可能触发“错误渲染”。另外还有可能有一些错误在格式检查的范围之外,这些错误不会触发“错误渲染”。但是可能**会导致 json 内容的缺失**。 + +建议您不要过于依赖 `JSONPreview` 的格式检查功能,尽可能用于预览格式正确的 json。 ### 链接 -*1.2.0* 版本增加了对链接(`.link`)的渲染功能。在渲染的同时,`JSONPreview` 会进行有限的**去转义**操作。 +*1.2.0* 版本增加了对链接的渲染功能。在渲染的同时,`JSONPreview` 会进行有限的**去转义**操作。 不同版本支持的去转义操作如下: @@ -146,7 +146,7 @@ previewView.preview(json, style: style) ## DFD -![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/DFD.png) +![DFD](Images/DFD.jpg) ## TODO @@ -159,4 +159,4 @@ previewView.preview(json, style: style) ## License -`License` 在 **MIT** 许可下可用。 有关更多信息,请参见 [LICENSE](https://github.com/rakuyoMo/License/blob/master/LICENSE) 文件。 +`License` 在 **MIT** 许可下可用。 有关更多信息,请参见 [LICENSE](LICENSE) 文件。