From 5fe15abc72ccd3fd0db67faf23e0ee42cf97d493 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 10:36:13 +0800 Subject: [PATCH 01/35] Improve code readability with typealias --- JSONPreview/Core/HighlightStyle.swift | 3 +++ JSONPreview/Core/JSONDecorator.swift | 7 ++----- JSONPreview/Core/JSONSlice.swift | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/JSONPreview/Core/HighlightStyle.swift b/JSONPreview/Core/HighlightStyle.swift index d239b34..6dd5755 100644 --- a/JSONPreview/Core/HighlightStyle.swift +++ b/JSONPreview/Core/HighlightStyle.swift @@ -8,6 +8,9 @@ import UIKit +public typealias AttributedKey = NSAttributedString.Key +public typealias StyleInfos = [AttributedKey : Any] + /// Highlight style configuration public struct HighlightStyle { /// Initialization method diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index d4031fb..1cdd801 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -444,11 +444,8 @@ private extension JSONDecorator { return NSAttributedString(attachment: expandAttach) } - func createStyle( - foregroundColor: UIColor?, - other: [NSAttributedString.Key : Any]? = nil - ) -> [NSAttributedString.Key : Any] { - var newStyle: [NSAttributedString.Key : Any] = [.font : style.jsonFont] + func createStyle(foregroundColor: UIColor?, other: StyleInfos? = nil) -> StyleInfos { + var newStyle: StyleInfos = [.font : style.jsonFont] if let color = foregroundColor { newStyle[.foregroundColor] = color diff --git a/JSONPreview/Core/JSONSlice.swift b/JSONPreview/Core/JSONSlice.swift index 55bf509..6f660d7 100644 --- a/JSONPreview/Core/JSONSlice.swift +++ b/JSONPreview/Core/JSONSlice.swift @@ -44,8 +44,8 @@ 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 From 2ec312937dac80ac9ac93b37f98e8632bb607da2 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 10:38:47 +0800 Subject: [PATCH 02/35] Keep only one `highlight` method JSONDecorator no longer holds json strings --- JSONPreview/Core/JSONDecorator.swift | 35 ++++++++++------------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index 1cdd801..ed75e4d 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -11,14 +11,10 @@ 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 + private init(style: HighlightStyle) { self.style = style } - /// The JSON string to be highlighted. - private let json: String - /// Style of highlight. See `HighlightStyle` for details. private let style: HighlightStyle @@ -65,34 +61,27 @@ public extension JSONDecorator { /// - 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? { + 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 data = json.data(using: .utf8), - let _ = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { + guard 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 + let decorator = JSONDecorator(style: style) + decorator.slices = decorator.createSlices(from: json) return decorator } } private extension JSONDecorator { - var _slices: [JSONSlice] { + func createSlices(from json: String) -> [JSONSlice] { var _slices: [JSONSlice] = [] // Record indentation level From 6f3a7fd2f10403686616117edb97ee48054ff0cf Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 10:39:23 +0800 Subject: [PATCH 03/35] update --- JSONPreview/Core/HighlightStyle.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/JSONPreview/Core/HighlightStyle.swift b/JSONPreview/Core/HighlightStyle.swift index 6dd5755..e8cdf7e 100644 --- a/JSONPreview/Core/HighlightStyle.swift +++ b/JSONPreview/Core/HighlightStyle.swift @@ -82,7 +82,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") { From f631c903e7e9666f67616e32b2a170bbc5acd3cf Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 10:40:40 +0800 Subject: [PATCH 04/35] Refer to Apple's format implementation Referenced from: https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/JSONSerialization%2BParser.swift#L527 --- JSONPreview.xcodeproj/project.pbxproj | 12 + JSONPreview/Core/JSONDecorator.swift | 68 +++ JSONPreview/Core/JSONError.swift | 28 ++ JSONPreview/Core/JSONParser.swift | 648 ++++++++++++++++++++++++++ JSONPreview/Core/JSONValue.swift | 62 +++ 5 files changed, 818 insertions(+) create mode 100644 JSONPreview/Core/JSONError.swift create mode 100644 JSONPreview/Core/JSONParser.swift create mode 100644 JSONPreview/Core/JSONValue.swift diff --git a/JSONPreview.xcodeproj/project.pbxproj b/JSONPreview.xcodeproj/project.pbxproj index 11912b4..32961b0 100644 --- a/JSONPreview.xcodeproj/project.pbxproj +++ b/JSONPreview.xcodeproj/project.pbxproj @@ -25,6 +25,9 @@ 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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -73,6 +76,9 @@ 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -158,6 +164,9 @@ 3A9DB22C2509D7F4002E7B15 /* HighlightColor.swift */, 3A9DB22E2509DA7A002E7B15 /* HighlightStyle.swift */, AE71889926FADA8300A16878 /* String+ValidURL.swift */, + AE92DFD9283F14B0002A7DAF /* JSONError.swift */, + AE92DFD7283F13C7002A7DAF /* JSONValue.swift */, + AE92DFD5283F13AD002A7DAF /* JSONParser.swift */, 3A67B14D250B3E6F000903EB /* JSONLexer.swift */, 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */, 3A1F06D12508D78B00C16862 /* JSONDecorator.swift */, @@ -314,10 +323,13 @@ 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 */, 3A1F06A22508D1D800C16862 /* ViewController.swift in Sources */, 3A67B14E250B3E6F000903EB /* JSONLexer.swift in Sources */, diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index ed75e4d..4ec3d5c 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -403,6 +403,74 @@ private extension JSONDecorator { } } +// MARK: - Encoding Detection + +private extension JSONDecorator { + + /// Detect the encoding format of the NSData contents + func detectEncoding(_ bytes: UnsafeRawBufferPointer) -> (String.Encoding, Int) { + // According to RFC8259, the text encoding in JSON must be UTF8 in nonclosed systems + // https://tools.ietf.org/html/rfc8259#section-8.1 + // However, since Darwin Foundation supports utf16 and utf32, so should Swift Foundation. + + // First let's check if we can determine the encoding based on a leading Byte Ordering Mark + // (BOM). + if bytes.count >= 4 { + if bytes.starts(with: Self.utf8BOM) { + return (.utf8, 3) + } + if bytes.starts(with: Self.utf32BigEndianBOM) { + return (.utf32BigEndian, 4) + } + if bytes.starts(with: Self.utf32LittleEndianBOM) { + return (.utf32LittleEndian, 4) + } + if bytes.starts(with: [0xFF, 0xFE]) { + return (.utf16LittleEndian, 2) + } + if bytes.starts(with: [0xFE, 0xFF]) { + return (.utf16BigEndian, 2) + } + } + + // If there is no BOM present, we might be able to determine the encoding based on + // occurences of null bytes. + if bytes.count >= 4 { + switch (bytes[0], bytes[1], bytes[2], bytes[3]) { + case (0, 0, 0, _): + return (.utf32BigEndian, 0) + case (_, 0, 0, 0): + return (.utf32LittleEndian, 0) + case (0, _, 0, _): + return (.utf16BigEndian, 0) + case (_, 0, _, 0): + return (.utf16LittleEndian, 0) + default: + break + } + } + else if bytes.count >= 2 { + switch (bytes[0], bytes[1]) { + case (0, _): + return (.utf16BigEndian, 0) + case (_, 0): + return (.utf16LittleEndian, 0) + default: + break + } + } + return (.utf8, 0) + } + + // These static properties don't look very nice, but we need them to + // workaround: https://bugs.swift.org/browse/SR-14102 + private static let utf8BOM: [UInt8] = [0xEF, 0xBB, 0xBF] + private static let utf32BigEndianBOM: [UInt8] = [0x00, 0x00, 0xFE, 0xFF] + private static let utf32LittleEndianBOM: [UInt8] = [0xFF, 0xFE, 0x00, 0x00] + private static let utf16BigEndianBOM: [UInt8] = [0xFF, 0xFE] + private static let utf16LittleEndianBOM: [UInt8] = [0xFE, 0xFF] +} + // MARK: - Tools private extension JSONDecorator { diff --git a/JSONPreview/Core/JSONError.swift b/JSONPreview/Core/JSONError.swift new file mode 100644 index 0000000..2f1264a --- /dev/null +++ b/JSONPreview/Core/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(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/JSONParser.swift b/JSONPreview/Core/JSONParser.swift new file mode 100644 index 0000000..cc69d74 --- /dev/null +++ b/JSONPreview/Core/JSONParser.swift @@ -0,0 +1,648 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + 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(ascii: next, characterIndex: reader.readerIndex + whitespace) + } + } + + return value + } + + // 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(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(ascii: ascii, characterIndex: reader.readerIndex) + } + } + } + + // MARK: - Object parsing - + + mutating func parseObject() throws -> [String: 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 = [String: JSONValue]() + object.reserveCapacity(20) + + while true { + let key = try reader.readString() + let colon = try reader.consumeWhitespace() + guard colon == ._colon else { + throw JSONError.unexpectedCharacter(ascii: colon, characterIndex: reader.readerIndex) + } + reader.moveReaderIndex(forwardBy: 1) + try reader.consumeWhitespace() + 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: + throw JSONError.unexpectedCharacter(ascii: commaOrBrace, characterIndex: reader.readerIndex) + } + } + } +} + +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"): + guard self.read() == UInt8(ascii: "r"), + self.read() == UInt8(ascii: "u"), + self.read() == UInt8(ascii: "e") + else { + guard !self.isEOF else { + throw JSONError.unexpectedEndOfFile + } + + throw JSONError.unexpectedCharacter(ascii: self.peek(offset: -1)!, characterIndex: self.readerIndex - 1) + } + + return true + case UInt8(ascii: "f"): + guard self.read() == UInt8(ascii: "a"), + self.read() == UInt8(ascii: "l"), + self.read() == UInt8(ascii: "s"), + self.read() == UInt8(ascii: "e") + else { + guard !self.isEOF else { + throw JSONError.unexpectedEndOfFile + } + + throw JSONError.unexpectedCharacter(ascii: self.peek(offset: -1)!, characterIndex: self.readerIndex - 1) + } + + return false + default: + preconditionFailure("Expected to have `t` or `f` as first character") + } + } + + public mutating func readNull() throws { + guard self.read() == UInt8(ascii: "n"), + self.read() == UInt8(ascii: "u"), + self.read() == UInt8(ascii: "l"), + self.read() == UInt8(ascii: "l") + else { + guard !self.isEOF else { + throw JSONError.unexpectedEndOfFile + } + + throw JSONError.unexpectedCharacter(ascii: self.peek(offset: -1)!, characterIndex: self.readerIndex - 1) + } + } + + // MARK: - Private Methods - + + // 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(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(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(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(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(ascii: byte, characterIndex: readerIndex + numberchars) + } + let numberStartIndex = self.readerIndex + self.moveReaderIndex(forwardBy: numberchars) + + return self.makeStringFast(self[numberStartIndex ..< self.readerIndex]) + default: + throw JSONError.unexpectedCharacter(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"), UInt8(ascii: "r"), UInt8(ascii: "u"), UInt8(ascii: "e")] + fileprivate static let _false = [UInt8(ascii: "f"), 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/JSONValue.swift b/JSONPreview/Core/JSONValue.swift new file mode 100644 index 0000000..2b9a4cf --- /dev/null +++ b/JSONPreview/Core/JSONValue.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// 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) + case number(String) + case bool(Bool) + case null + + case array([JSONValue]) + case object([String: JSONValue]) +} + +extension JSONValue { + public var isValue: Bool { + switch self { + case .array, .object: + return false + case .null, .number, .string, .bool: + return true + } + } + + public var isContainer: Bool { + switch self { + case .array, .object: + return true + case .null, .number, .string, .bool: + return false + } + } +} + +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" + } + } +} From 5e164f24c9bfffca31bbc0814b8801ed86205e14 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 11:27:37 +0800 Subject: [PATCH 05/35] Add boundary condition judgment --- JSONPreview/Core/JSONPreview.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/JSONPreview/Core/JSONPreview.swift b/JSONPreview/Core/JSONPreview.swift index 499aee3..1f898a5 100644 --- a/JSONPreview/Core/JSONPreview.swift +++ b/JSONPreview/Core/JSONPreview.swift @@ -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 } } } From 7e2d94c68365b7a3319ec12c48d4c60c9c28393e Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 11:30:09 +0800 Subject: [PATCH 06/35] Known issue: Apple's format implementation does not support returning incorrectly formatted json Changes need to be made to the error (JSONError.unexpectedCharacter) handling in JSONParser.swift. Prioritize the parsing of correctly formatted json. --- JSONPreview/Other/ViewController.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/JSONPreview/Other/ViewController.swift b/JSONPreview/Other/ViewController.swift index 34c0105..03f5c7a 100644 --- a/JSONPreview/Other/ViewController.swift +++ b/JSONPreview/Other/ViewController.swift @@ -110,12 +110,13 @@ class ViewController: UIViewController { } } }, - { - {123456} - } ] """ +// { +// {123456} +// } + let start = Date().timeIntervalSince1970 print("will display json") From aacc889ae624a6fa04c79f8d88cba136266d9112 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 16:32:34 +0800 Subject: [PATCH 07/35] update test --- JSONPreview/Other/ViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/JSONPreview/Other/ViewController.swift b/JSONPreview/Other/ViewController.swift index 03f5c7a..2817b88 100644 --- a/JSONPreview/Other/ViewController.swift +++ b/JSONPreview/Other/ViewController.swift @@ -46,6 +46,8 @@ class ViewController: UIViewController { let json = """ [ + [], + [], { "string" : "string", "int" : 1024, From 7b139d388c2f2e6eb1e09207c0304ffad9e839b4 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 17:11:34 +0800 Subject: [PATCH 08/35] Refactoring the rendering logic in JSONDecorator --- JSONPreview/Core/JSONDecorator.swift | 674 ++++++++++++--------------- 1 file changed, 300 insertions(+), 374 deletions(-) diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index 4ec3d5c..2c221fa 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -18,9 +18,14 @@ public class JSONDecorator { /// 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) @@ -51,6 +56,8 @@ public class JSONDecorator { ) } +// MARK: - Public + public extension JSONDecorator { /// Highlight the incoming JSON string. /// @@ -75,421 +82,340 @@ public extension JSONDecorator { } let decorator = JSONDecorator(style: style) - decorator.slices = decorator.createSlices(from: json) + decorator.slices = decorator.createSlices(from: data) return decorator } } +// MARK: - Main Logic + private extension JSONDecorator { - func createSlices(from json: String) -> [JSONSlice] { - var _slices: [JSONSlice] = [] - - // Record indentation level - var level = 0 + 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, isNeedIndent: false, isNeedComma: false) + } + + func processJSONValueRecursively( + _ jsonValue: JSONValue, + isNeedIndent: Bool, + isNeedComma: Bool + ) -> [JSONSlice] { + var result: [JSONSlice] = [] - var lastToken: JSONLexer.Token? = nil + // 简化 JSONSlice 的初始化 + func _append(expand: AttributedString, fold: AttributedString?) { + let slice = JSONSlice(level: indent, lineNumber: result.count + 1, expand: expand, folded: fold) + result.append(slice) + } - JSONLexer.getTokens(of: json).forEach { (token) in - defer { lastToken = token } - - let lineNumber = _slices.count + 1 + // 处理每个 json 节点 + switch jsonValue { + // MARK: array + case .array(let values): + // 处理开头节点 + let (startExpand, startFold) = createArrayStartAttribute( + isNeedIndent: isNeedIndent, + isNeedComma: isNeedComma) - switch token { + // 将开头节点添加到结果数组中 + _append(expand: startExpand, fold: startFold) - // 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 - } + if !values.isEmpty { + // 增加缩进 + incIndent() - // When the conditions are not met, create a new slice - else { - let indentation = createIndentedString(level: level) + // 处理里面每一个 value + for (i, value) in values.enumerated() { + let _isNeedComma = i != (values.count - 1) - 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)) + let slices = processJSONValueRecursively(value, isNeedIndent: true, isNeedComma: _isNeedComma) + result.append(contentsOf: slices) } - 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 + // 减少缩进 + decIndent() + } + + // 处理结束节点 + let endExpand = createArrayEndAttribute(isNeedComma: isNeedComma) + + // 将结尾节点添加到结果数组中 + _append(expand: endExpand, fold: nil) + + // MARK: object + case .object(let object): + // 处理开头节点 + let (startExpand, startFold) = createObjectStartAttribute( + isNeedIndent: isNeedIndent, + isNeedComma: isNeedComma) + + // 将开头节点添加到结果数组中 + _append(expand: startExpand, fold: startFold) + + if !object.isEmpty { + // 增加缩进 + incIndent() + + // 处理里面每一个 value + for (i, (key, value)) in object.enumerated() { + let _isNeedComma = i != (object.count - 1) + let string = writeIndent() + "\"\(key)\"" + + let createKeyAttribute: () -> AttributedString = { [weak self] in + guard let this = self else { return .init(string: "") } + + let keyAttribute = AttributedString(string: string, attributes: this.keyStyle) + keyAttribute.append(this.colonAttributeString) + return keyAttribute } - 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)) + let expand = createKeyAttribute() + + // 根据不同的情况进行不同的处理 + if value.isContainer { + let fold = createKeyAttribute() + + // 获取子值的内容 + var slices = processJSONValueRecursively(value, 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 + + // 获取子值的内容 + let slices = processJSONValueRecursively(value, isNeedIndent: false, isNeedComma: _isNeedComma) + + // 一般这种时候`slices`只会有一个值,所以只取第一个值 + if let slice = slices.first { + expand.append(slice.expand) + + if let valueFold = slice.folded { + fold = createKeyAttribute() + fold?.append(valueFold) + } + + _append(expand: expand, fold: fold) + } + } } - // 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)) - } + // 减少缩进 + decIndent() + } + + // 处理结束节点 + let endExpand = createObjectEndAttribute(isNeedComma: isNeedComma) + + // 将结尾节点添加到结果数组中 + _append(expand: endExpand, fold: nil) + + case .string(let value): + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + "\"\(value)\"" + + let expand: AttributedString + + if let url = value.validURL { + // MARK: link + expand = AttributedString(string: string, attributes: linkStyle) - // MARK: unknown - case .unknown(let string): - let newString = string.replacingOccurrences(of: "\n", with: "") - let indentation = createIndentedString(level: level) + let urlString = url.urlString + let range = NSRange(location: indent.count + 1, length: urlString.count) - let attributedString = NSMutableAttributedString(string: indentation) - attributedString.append(NSAttributedString(string: newString, attributes: unknownStyle)) + expand.addAttribute(.link, value: urlString, range: range) + expand.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range) + } else { - _slices.append(JSONSlice(level: level, lineNumber: lineNumber, expand: attributedString)) + // MARK: string + expand = AttributedString(string: string, attributes: stringStyle) } + + if isNeedComma { + expand.append(commaAttributeString) + } + + _append(expand: expand, fold: nil) + + // MARK: number + case .number(let value): + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + "\(value)" + let expand = AttributedString(string: string, attributes: numberStyle) + + if isNeedComma { + expand.append(commaAttributeString) + } + + _append(expand: expand, fold: nil) + + // MARK: bool + case .bool(let value): + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + (value ? "true" : "false") + let expand = AttributedString(string: string, attributes: boolStyle) + + if isNeedComma { + expand.append(commaAttributeString) + } + + _append(expand: expand, fold: nil) + + // MARK: null + case .null: + let indent = isNeedIndent ? writeIndent() : "" + let string = indent + "null" + let expand = AttributedString(string: string, attributes: nullStyle) + + if isNeedComma { + expand.append(commaAttributeString) + } + + _append(expand: expand, fold: nil) } - return _slices + return result } } -// MARK: - Encoding Detection +// MARK: - Indent private extension JSONDecorator { + /// Fixed value of the number of contractions per increase or decrease + static let indentAmount = 1 - /// Detect the encoding format of the NSData contents - func detectEncoding(_ bytes: UnsafeRawBufferPointer) -> (String.Encoding, Int) { - // According to RFC8259, the text encoding in JSON must be UTF8 in nonclosed systems - // https://tools.ietf.org/html/rfc8259#section-8.1 - // However, since Darwin Foundation supports utf16 and utf32, so should Swift Foundation. - - // First let's check if we can determine the encoding based on a leading Byte Ordering Mark - // (BOM). - if bytes.count >= 4 { - if bytes.starts(with: Self.utf8BOM) { - return (.utf8, 3) - } - if bytes.starts(with: Self.utf32BigEndianBOM) { - return (.utf32BigEndian, 4) - } - if bytes.starts(with: Self.utf32LittleEndianBOM) { - return (.utf32LittleEndian, 4) - } - if bytes.starts(with: [0xFF, 0xFE]) { - return (.utf16LittleEndian, 2) - } - if bytes.starts(with: [0xFE, 0xFF]) { - return (.utf16BigEndian, 2) - } - } - - // If there is no BOM present, we might be able to determine the encoding based on - // occurences of null bytes. - if bytes.count >= 4 { - switch (bytes[0], bytes[1], bytes[2], bytes[3]) { - case (0, 0, 0, _): - return (.utf32BigEndian, 0) - case (_, 0, 0, 0): - return (.utf32LittleEndian, 0) - case (0, _, 0, _): - return (.utf16BigEndian, 0) - case (_, 0, _, 0): - return (.utf16LittleEndian, 0) - default: - break - } - } - else if bytes.count >= 2 { - switch (bytes[0], bytes[1]) { - case (0, _): - return (.utf16BigEndian, 0) - case (_, 0): - return (.utf16LittleEndian, 0) - default: - break - } - } - return (.utf8, 0) + func incIndent() { + indent += Self.indentAmount + } + + func decIndent() { + indent -= Self.indentAmount } - // These static properties don't look very nice, but we need them to - // workaround: https://bugs.swift.org/browse/SR-14102 - private static let utf8BOM: [UInt8] = [0xEF, 0xBB, 0xBF] - private static let utf32BigEndianBOM: [UInt8] = [0x00, 0x00, 0xFE, 0xFF] - private static let utf32LittleEndianBOM: [UInt8] = [0xFF, 0xFE, 0x00, 0x00] - private static let utf16BigEndianBOM: [UInt8] = [0xFF, 0xFE] - private static let utf16LittleEndianBOM: [UInt8] = [0xFE, 0xFF] + func writeIndent() -> String { + return (0 ..< indent).map { _ in "\t" }.joined() + } } -// MARK: - Tools +// MARK: - Attributed String private extension JSONDecorator { - /// Create a string to represent indentation. + typealias AttributedString = NSMutableAttributedString + + /// 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 level: Current level. - /// - Returns: Use the indentation indicated by `"\t"`. - func createIndentedString(level: Int) -> String { - return (0 ..< level).map{ _ in "\t" }.joined() + /// - Parameter key: keyword + /// - Returns: `AttributedString` object. + func createKeywordAttribute(key: String) -> AttributedString { + return .init(string: key, attributes: keyWordStyle) } - /// Create an `NSAttributedString` object for displaying image. + /// Create an attribute string of "begin node". + /// + /// - Parameters: + /// - expand: String when expand. + /// - fold: String when folded. + /// - isNeedIndent: + /// - isNeedComma: Did need to append a comma at the end. + /// - 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: Did need to append a comma at the end. + /// - 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: `NSAttributedString` object. - func createIconAttributedString(with image: UIImage) -> NSAttributedString { + /// - Returns: `AttributedString` object. + func createIconAttributedString(with image: UIImage) -> AttributedString { let expandAttach = NSTextAttachment() - expandAttach.image = image let font = style.jsonFont @@ -498,7 +424,7 @@ private extension JSONDecorator { expandAttach.bounds = CGRect(x: 0, y: y, width: font.ascender, height: font.ascender) - return NSAttributedString(attachment: expandAttach) + return .init(attachment: expandAttach) } func createStyle(foregroundColor: UIColor?, other: StyleInfos? = nil) -> StyleInfos { From d5fa09d30444f01a910fca42c4725b8e7b8fc90f Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 17:12:37 +0800 Subject: [PATCH 09/35] Remove JSONLexer Use JSONValue instead --- JSONPreview.xcodeproj/project.pbxproj | 4 - JSONPreview/Core/JSONLexer.swift | 346 -------------------------- 2 files changed, 350 deletions(-) delete mode 100644 JSONPreview/Core/JSONLexer.swift diff --git a/JSONPreview.xcodeproj/project.pbxproj b/JSONPreview.xcodeproj/project.pbxproj index 32961b0..8b139c3 100644 --- a/JSONPreview.xcodeproj/project.pbxproj +++ b/JSONPreview.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 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 */; }; @@ -65,7 +64,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 = ""; }; @@ -167,7 +165,6 @@ AE92DFD9283F14B0002A7DAF /* JSONError.swift */, AE92DFD7283F13C7002A7DAF /* JSONValue.swift */, AE92DFD5283F13AD002A7DAF /* JSONParser.swift */, - 3A67B14D250B3E6F000903EB /* JSONLexer.swift */, 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */, 3A1F06D12508D78B00C16862 /* JSONDecorator.swift */, 3A1F06CF2508D74A00C16862 /* JSONPreview.swift */, @@ -332,7 +329,6 @@ AE92DFD6283F13AD002A7DAF /* JSONParser.swift in Sources */, 3A97FCBC250CA0670017352A /* JSONTextView.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/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 - } -} From b18fb8970a9c452d7cb10c777dcec3b27c2c0e8b Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 17:19:39 +0800 Subject: [PATCH 10/35] rename --- JSONPreview/Core/JSONPreview.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/JSONPreview/Core/JSONPreview.swift b/JSONPreview/Core/JSONPreview.swift index 1f898a5..9a08e91 100644 --- a/JSONPreview/Core/JSONPreview.swift +++ b/JSONPreview/Core/JSONPreview.swift @@ -403,25 +403,25 @@ extension JSONPreview: JSONTextViewDelegate { 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 } }() } From 1aa302687e6e059ea4761e3434affa8796524a5a Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 17:47:22 +0800 Subject: [PATCH 11/35] Unify NSAttributedString and NSMutableAttributedString into AttributedString (NSMutableAttributedString) --- JSONPreview/Core/HighlightStyle.swift | 3 ++- JSONPreview/Core/JSONDecorator.swift | 4 +--- JSONPreview/Core/JSONPreview.swift | 6 +++--- JSONPreview/Core/JSONSlice.swift | 14 +++++++------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/JSONPreview/Core/HighlightStyle.swift b/JSONPreview/Core/HighlightStyle.swift index e8cdf7e..35b5d32 100644 --- a/JSONPreview/Core/HighlightStyle.swift +++ b/JSONPreview/Core/HighlightStyle.swift @@ -8,7 +8,8 @@ import UIKit -public typealias AttributedKey = NSAttributedString.Key +public typealias AttributedString = NSMutableAttributedString +public typealias AttributedKey = AttributedString.Key public typealias StyleInfos = [AttributedKey : Any] /// Highlight style configuration diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index 2c221fa..e51548d 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -34,7 +34,7 @@ public class JSONDecorator { /// 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(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) @@ -324,8 +324,6 @@ private extension JSONDecorator { private extension JSONDecorator { - typealias AttributedString = NSMutableAttributedString - /// An attribute string of ":" var colonAttributeString: AttributedString { createKeywordAttribute(key: " : ") diff --git a/JSONPreview/Core/JSONPreview.swift b/JSONPreview/Core/JSONPreview.swift index 9a08e91..d2746f8 100644 --- a/JSONPreview/Core/JSONPreview.swift +++ b/JSONPreview/Core/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) @@ -400,7 +400,7 @@ 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 _slice = slices[i] @@ -451,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/JSONSlice.swift b/JSONPreview/Core/JSONSlice.swift index 6f660d7..696d58e 100644 --- a/JSONPreview/Core/JSONSlice.swift +++ b/JSONPreview/Core/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 @@ -50,10 +50,10 @@ public struct JSONSlice { 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 From b64687f4b6ba303a96f1bf34710b3c1d4994674c Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Thu, 26 May 2022 17:52:27 +0800 Subject: [PATCH 12/35] Fix lineNumber calculation error. --- JSONPreview/Core/JSONDecorator.swift | 32 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index e51548d..6d988be 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -104,11 +104,16 @@ private extension JSONDecorator { } func createJSONSlices(from jsonValue: JSONValue) -> [JSONSlice] { - return processJSONValueRecursively(jsonValue, isNeedIndent: false, isNeedComma: false) + return processJSONValueRecursively( + jsonValue, + currentSlicesCount: 0, + isNeedIndent: false, + isNeedComma: false) } func processJSONValueRecursively( _ jsonValue: JSONValue, + currentSlicesCount: Int, isNeedIndent: Bool, isNeedComma: Bool ) -> [JSONSlice] { @@ -116,7 +121,12 @@ private extension JSONDecorator { // 简化 JSONSlice 的初始化 func _append(expand: AttributedString, fold: AttributedString?) { - let slice = JSONSlice(level: indent, lineNumber: result.count + 1, expand: expand, folded: fold) + let slice = JSONSlice( + level: indent, + lineNumber: currentSlicesCount + result.count + 1, + expand: expand, + folded: fold) + result.append(slice) } @@ -140,7 +150,11 @@ private extension JSONDecorator { for (i, value) in values.enumerated() { let _isNeedComma = i != (values.count - 1) - let slices = processJSONValueRecursively(value, isNeedIndent: true, isNeedComma: _isNeedComma) + let slices = processJSONValueRecursively( + value, + currentSlicesCount: currentSlicesCount + result.count, + isNeedIndent: true, + isNeedComma: _isNeedComma) result.append(contentsOf: slices) } @@ -188,7 +202,11 @@ private extension JSONDecorator { let fold = createKeyAttribute() // 获取子值的内容 - var slices = processJSONValueRecursively(value, isNeedIndent: false, isNeedComma: _isNeedComma) + var slices = processJSONValueRecursively( + value, + currentSlicesCount: currentSlicesCount + result.count, + isNeedIndent: false, + isNeedComma: _isNeedComma) if !slices.isEmpty { let startSlice = slices.removeFirst() @@ -207,7 +225,11 @@ private extension JSONDecorator { var fold: AttributedString? = nil // 获取子值的内容 - let slices = processJSONValueRecursively(value, isNeedIndent: false, isNeedComma: _isNeedComma) + let slices = processJSONValueRecursively( + value, + currentSlicesCount: 0, + isNeedIndent: false, + isNeedComma: _isNeedComma) // 一般这种时候`slices`只会有一个值,所以只取第一个值 if let slice = slices.first { From b075ce8e9a2ef098a585097d1105d371be8d9335 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 09:53:25 +0800 Subject: [PATCH 13/35] `JSONValue` add `.unknown` case --- JSONPreview/Core/JSONDecorator.swift | 10 ++++++++++ JSONPreview/Core/JSONValue.swift | 8 ++++++-- JSONPreview/Other/ViewController.swift | 7 ++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index 6d988be..ef4ac12 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -316,6 +316,16 @@ private extension JSONDecorator { expand.append(commaAttributeString) } + _append(expand: expand, fold: nil) + + // MARK: unknown + case .unknown(let string): + let indent = isNeedIndent ? writeIndent() : "" + let newString = string.replacingOccurrences(of: "\n", with: "") + + let expand = AttributedString(string: indent) + expand.append(AttributedString(string: newString, attributes: unknownStyle)) + _append(expand: expand, fold: nil) } diff --git a/JSONPreview/Core/JSONValue.swift b/JSONPreview/Core/JSONValue.swift index 2b9a4cf..4428ed7 100644 --- a/JSONPreview/Core/JSONValue.swift +++ b/JSONPreview/Core/JSONValue.swift @@ -20,6 +20,8 @@ public enum JSONValue: Equatable { case array([JSONValue]) case object([String: JSONValue]) + + case unknown(String) } extension JSONValue { @@ -27,7 +29,7 @@ extension JSONValue { switch self { case .array, .object: return false - case .null, .number, .string, .bool: + case .null, .number, .string, .bool, .unknown: return true } } @@ -36,7 +38,7 @@ extension JSONValue { switch self { case .array, .object: return true - case .null, .number, .string, .bool: + case .null, .number, .string, .bool, .unknown: return false } } @@ -57,6 +59,8 @@ extension JSONValue { return "a dictionary" case .null: return "null" + case .unknown: + return "unknown json value" } } } diff --git a/JSONPreview/Other/ViewController.swift b/JSONPreview/Other/ViewController.swift index 2817b88..c2b387e 100644 --- a/JSONPreview/Other/ViewController.swift +++ b/JSONPreview/Other/ViewController.swift @@ -112,12 +112,13 @@ class ViewController: UIViewController { } } }, + { + {123456} + } ] """ -// { -// {123456} -// } + let start = Date().timeIntervalSince1970 print("will display json") From 65e111df42674801df827b366807f51c0d108a53 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 10:26:18 +0800 Subject: [PATCH 14/35] Improve annotation --- JSONPreview/Core/JSONDecorator.swift | 37 +++++++++------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/JSONDecorator.swift index ef4ac12..0e4853c 100644 --- a/JSONPreview/Core/JSONDecorator.swift +++ b/JSONPreview/Core/JSONDecorator.swift @@ -119,7 +119,7 @@ private extension JSONDecorator { ) -> [JSONSlice] { var result: [JSONSlice] = [] - // 简化 JSONSlice 的初始化 + /// Simplify the initialization of `JSONSlice` func _append(expand: AttributedString, fold: AttributedString?) { let slice = JSONSlice( level: indent, @@ -130,23 +130,20 @@ private extension JSONDecorator { result.append(slice) } - // 处理每个 json 节点 + // 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) if !values.isEmpty { - // 增加缩进 incIndent() - // 处理里面每一个 value + // Process each value for (i, value) in values.enumerated() { let _isNeedComma = i != (values.count - 1) @@ -158,31 +155,24 @@ private extension JSONDecorator { result.append(contentsOf: slices) } - // 减少缩进 decIndent() } - // 处理结束节点 let endExpand = createArrayEndAttribute(isNeedComma: isNeedComma) - - // 将结尾节点添加到结果数组中 _append(expand: endExpand, fold: nil) // MARK: object case .object(let object): - // 处理开头节点 let (startExpand, startFold) = createObjectStartAttribute( isNeedIndent: isNeedIndent, isNeedComma: isNeedComma) - // 将开头节点添加到结果数组中 _append(expand: startExpand, fold: startFold) if !object.isEmpty { - // 增加缩进 incIndent() - // 处理里面每一个 value + // Process each value for (i, (key, value)) in object.enumerated() { let _isNeedComma = i != (object.count - 1) let string = writeIndent() + "\"\(key)\"" @@ -197,11 +187,11 @@ private extension JSONDecorator { let expand = createKeyAttribute() - // 根据不同的情况进行不同的处理 + // Different treatment according to different situations if value.isContainer { let fold = createKeyAttribute() - // 获取子值的内容 + // Get the content of the subvalue var slices = processJSONValueRecursively( value, currentSlicesCount: currentSlicesCount + result.count, @@ -224,14 +214,15 @@ private extension JSONDecorator { } else { var fold: AttributedString? = nil - // 获取子值的内容 + // Get the content of the subvalue let slices = processJSONValueRecursively( value, currentSlicesCount: 0, isNeedIndent: false, isNeedComma: _isNeedComma) - // 一般这种时候`slices`只会有一个值,所以只取第一个值 + // 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) @@ -245,14 +236,10 @@ private extension JSONDecorator { } } - // 减少缩进 decIndent() } - // 处理结束节点 let endExpand = createObjectEndAttribute(isNeedComma: isNeedComma) - - // 将结尾节点添加到结果数组中 _append(expand: endExpand, fold: nil) case .string(let value): @@ -399,8 +386,8 @@ private extension JSONDecorator { /// - Parameters: /// - expand: String when expand. /// - fold: String when folded. - /// - isNeedIndent: - /// - isNeedComma: Did need to append a comma at the end. + /// - isNeedIndent: Indentation required. + /// - isNeedComma: Comma required. /// - Returns: `AttributedString` object. func createStartAttribute( expand: String, @@ -432,7 +419,7 @@ private extension JSONDecorator { /// /// - Parameters: /// - key: Node characters, such as `}` or `]`. - /// - isNeedComma: Did need to append a comma at the end. + /// - isNeedComma: Comma required. /// - Returns: `AttributedString` object. func createEndAttribute(key: String, isNeedComma: Bool) -> AttributedString { let indent = writeIndent() From d1e0dccd5195d7140a7e5c349db98c444270254f Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 10:38:17 +0800 Subject: [PATCH 15/35] Restructuring the directory --- JSONPreview.podspec | 2 +- JSONPreview.xcodeproj/project.pbxproj | 56 +++++++++++++++---- .../Core/{ => Entity}/HighlightColor.swift | 0 .../Core/{ => Entity}/HighlightStyle.swift | 0 JSONPreview/Core/{ => Entity}/JSONError.swift | 0 JSONPreview/Core/{ => Entity}/JSONSlice.swift | 0 JSONPreview/Core/{ => Entity}/JSONValue.swift | 0 .../Core/{ => Model}/JSONDecorator.swift | 0 JSONPreview/Core/{ => Model}/JSONParser.swift | 0 .../Core/{ => Tools}/String+ValidURL.swift | 0 JSONPreview/Core/{ => View}/JSONPreview.swift | 0 .../Core/{ => View}/JSONTextView.swift | 0 .../Core/{ => View}/LineNumberCell.swift | 0 .../Core/{ => View}/LineNumberTableView.swift | 0 README.md | 2 +- README_CN.md | 2 +- 16 files changed, 47 insertions(+), 15 deletions(-) rename JSONPreview/Core/{ => Entity}/HighlightColor.swift (100%) rename JSONPreview/Core/{ => Entity}/HighlightStyle.swift (100%) rename JSONPreview/Core/{ => Entity}/JSONError.swift (100%) rename JSONPreview/Core/{ => Entity}/JSONSlice.swift (100%) rename JSONPreview/Core/{ => Entity}/JSONValue.swift (100%) rename JSONPreview/Core/{ => Model}/JSONDecorator.swift (100%) rename JSONPreview/Core/{ => Model}/JSONParser.swift (100%) rename JSONPreview/Core/{ => Tools}/String+ValidURL.swift (100%) rename JSONPreview/Core/{ => View}/JSONPreview.swift (100%) rename JSONPreview/Core/{ => View}/JSONTextView.swift (100%) rename JSONPreview/Core/{ => View}/LineNumberCell.swift (100%) rename JSONPreview/Core/{ => View}/LineNumberTableView.swift (100%) diff --git a/JSONPreview.podspec b/JSONPreview.podspec index 003bb5f..da6b69c 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 8b139c3..dfcd3d6 100644 --- a/JSONPreview.xcodeproj/project.pbxproj +++ b/JSONPreview.xcodeproj/project.pbxproj @@ -159,18 +159,10 @@ 3A1F06CD2508D20700C16862 /* Core */ = { isa = PBXGroup; children = ( - 3A9DB22C2509D7F4002E7B15 /* HighlightColor.swift */, - 3A9DB22E2509DA7A002E7B15 /* HighlightStyle.swift */, - AE71889926FADA8300A16878 /* String+ValidURL.swift */, - AE92DFD9283F14B0002A7DAF /* JSONError.swift */, - AE92DFD7283F13C7002A7DAF /* JSONValue.swift */, - AE92DFD5283F13AD002A7DAF /* JSONParser.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 = ""; @@ -188,6 +180,46 @@ path = Other; sourceTree = ""; }; + AE6C038228406F5D00E544FD /* Entity */ = { + isa = PBXGroup; + children = ( + 3A9DB22C2509D7F4002E7B15 /* HighlightColor.swift */, + 3A9DB22E2509DA7A002E7B15 /* HighlightStyle.swift */, + AE92DFD9283F14B0002A7DAF /* JSONError.swift */, + AE92DFD7283F13C7002A7DAF /* JSONValue.swift */, + 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */, + ); + path = Entity; + sourceTree = ""; + }; + AE6C038328406F7000E544FD /* Tools */ = { + isa = PBXGroup; + children = ( + AE71889926FADA8300A16878 /* String+ValidURL.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 */ 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 100% rename from JSONPreview/Core/HighlightStyle.swift rename to JSONPreview/Core/Entity/HighlightStyle.swift diff --git a/JSONPreview/Core/JSONError.swift b/JSONPreview/Core/Entity/JSONError.swift similarity index 100% rename from JSONPreview/Core/JSONError.swift rename to JSONPreview/Core/Entity/JSONError.swift diff --git a/JSONPreview/Core/JSONSlice.swift b/JSONPreview/Core/Entity/JSONSlice.swift similarity index 100% rename from JSONPreview/Core/JSONSlice.swift rename to JSONPreview/Core/Entity/JSONSlice.swift diff --git a/JSONPreview/Core/JSONValue.swift b/JSONPreview/Core/Entity/JSONValue.swift similarity index 100% rename from JSONPreview/Core/JSONValue.swift rename to JSONPreview/Core/Entity/JSONValue.swift diff --git a/JSONPreview/Core/JSONDecorator.swift b/JSONPreview/Core/Model/JSONDecorator.swift similarity index 100% rename from JSONPreview/Core/JSONDecorator.swift rename to JSONPreview/Core/Model/JSONDecorator.swift diff --git a/JSONPreview/Core/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift similarity index 100% rename from JSONPreview/Core/JSONParser.swift rename to JSONPreview/Core/Model/JSONParser.swift 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 100% rename from JSONPreview/Core/JSONPreview.swift rename to JSONPreview/Core/View/JSONPreview.swift 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/README.md b/README.md index 957e65d..0cf25d8 100644 --- a/README.md +++ b/README.md @@ -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`](https://github.com/rakuyoMo/JSONPreview/blob/master/JSONPreview/JSONPreview/Core/Entity/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. ```swift let highlightColor = HighlightColor( diff --git a/README_CN.md b/README_CN.md index e27a391..a7eb12f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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`](https://github.com/rakuyoMo/JSONPreview/blob/master/JSONPreview/JSONPreview/Core/Entity/HighlightColor.swift#L119) 是一个用于提供颜色的协议。通过该协议,您可以直接使用 `UIColor` 对象,或轻松的将诸如 `0xffffff`、`#FF7F20` 以及 `[0.72, 0.18, 0.13]` 转换为 `UIColor` 对象。 ```swift let highlightColor = HighlightColor( From 04065349f2b2f63a690bad839065a627a4d69eb3 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 10:50:29 +0800 Subject: [PATCH 16/35] update --- default.png => Images/default.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename default.png => Images/default.png (100%) diff --git a/default.png b/Images/default.png similarity index 100% rename from default.png rename to Images/default.png From 83cadf0a3bf4ba60b93ef8b9afc6da1319f8a7ad Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 10:55:25 +0800 Subject: [PATCH 17/35] update --- JSONPreview.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/JSONPreview.xcodeproj/project.pbxproj b/JSONPreview.xcodeproj/project.pbxproj index dfcd3d6..d9a65d6 100644 --- a/JSONPreview.xcodeproj/project.pbxproj +++ b/JSONPreview.xcodeproj/project.pbxproj @@ -22,7 +22,6 @@ 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 */; }; @@ -72,7 +71,6 @@ 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 = ""; }; @@ -131,7 +129,6 @@ AE5465D927F7DDC800201BD0 /* README.md */, 3A134BCC251486B3002AAFA6 /* push.sh */, 3A134BCB251486B3002AAFA6 /* JSONPreview.podspec */, - AE5465DD27F7DDEA00201BD0 /* default.png */, 3A1F06CD2508D20700C16862 /* Core */, 3A1F06CE2508D20C00C16862 /* Other */, ); @@ -327,7 +324,6 @@ 3A1F06AA2508D1DC00C16862 /* LaunchScreen.storyboard in Resources */, 3A1F06A72508D1DC00C16862 /* Assets.xcassets in Resources */, 3A1F06A52508D1D800C16862 /* Main.storyboard in Resources */, - AE5465DE27F7DDEA00201BD0 /* default.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From f4081449efc194465af43f96c38c831051accde7 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 11:11:30 +0800 Subject: [PATCH 18/35] update DFD image --- Images/DFD.jpg | Bin 0 -> 64658 bytes Images/DFD.png | Bin 47741 -> 0 bytes README.md | 2 +- README_CN.md | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 Images/DFD.jpg delete mode 100644 Images/DFD.png diff --git a/Images/DFD.jpg b/Images/DFD.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ee9ac293d685e8211979c60df609b2f679c1652 GIT binary patch literal 64658 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

xo0| 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 literal 0 HcmV?d00001 diff --git a/Images/DFD.png b/Images/DFD.png deleted file mode 100644 index 6c4e4d4d192a7764c4992269a2bde7604941a196..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47741 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 DTBPAY diff --git a/README.md b/README.md index 0cf25d8..57bb7e4 100644 --- a/README.md +++ b/README.md @@ -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) +![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/DFD.jpg) ## TODO diff --git a/README_CN.md b/README_CN.md index a7eb12f..9360e0e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -146,7 +146,7 @@ previewView.preview(json, style: style) ## DFD -![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/DFD.png) +![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/DFD.jpg) ## TODO From 3c6187303919c6b387d21ca8f0f47ae5407ead74 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 11:19:07 +0800 Subject: [PATCH 19/35] Try to use relative paths in README. --- README.md | 14 +++++++------- README_CN.md | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 57bb7e4..2ac3074 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) 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/Entity/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#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. ```swift let highlightColor = HighlightColor( @@ -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.jpg) +![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 9360e0e..2f887ea 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) 文件中包含部分测试代码,运行项目即可查看对应的效果。 1. 首先创建 `JSONPreview` 对象,并添加到界面上: @@ -82,7 +82,7 @@ previewView.preview(json) 3. 如果您想要自定义高亮样式,可通过 `HighlightStyle` 与 `HighlightColor` 类型进行设置: -> 其中,[`ConvertibleToColor`](https://github.com/rakuyoMo/JSONPreview/blob/master/JSONPreview/JSONPreview/Core/Entity/HighlightColor.swift#L119) 是一个用于提供颜色的协议。通过该协议,您可以直接使用 `UIColor` 对象,或轻松的将诸如 `0xffffff`、`#FF7F20` 以及 `[0.72, 0.18, 0.13]` 转换为 `UIColor` 对象。 +> 其中,[`ConvertibleToColor`](JSONPreview/Core/Entity/HighlightColor.swift#L119) 是一个用于提供颜色的协议。通过该协议,您可以直接使用 `UIColor` 对象,或轻松的将诸如 `0xffffff`、`#FF7F20` 以及 `[0.72, 0.18, 0.13]` 转换为 `UIColor` 对象。 ```swift let highlightColor = HighlightColor( @@ -146,7 +146,7 @@ previewView.preview(json, style: style) ## DFD -![image](https://github.com/rakuyoMo/JSONPreview/blob/master/Images/DFD.jpg) +![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) 文件。 From 8acdd8c85d3ea312ac6d6879a2a3bfa32541e919 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 11:20:36 +0800 Subject: [PATCH 20/35] Update the README with the line of ConvertibleToColor --- README.md | 2 +- README_CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ac3074..3136cc4 100644 --- a/README.md +++ b/README.md @@ -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`](JSONPreview/Core/Entity/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( diff --git a/README_CN.md b/README_CN.md index 2f887ea..1ad41e0 100644 --- a/README_CN.md +++ b/README_CN.md @@ -82,7 +82,7 @@ previewView.preview(json) 3. 如果您想要自定义高亮样式,可通过 `HighlightStyle` 与 `HighlightColor` 类型进行设置: -> 其中,[`ConvertibleToColor`](JSONPreview/Core/Entity/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( From 143059bcee5d3e2372f713769704e571fa6430b1 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 11:21:38 +0800 Subject: [PATCH 21/35] Adding test code for line --- README.md | 2 +- README_CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3136cc4..1fc0db3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ dependencies: [ ## Usage -> After downloading the project, [`ViewController.swift`](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. diff --git a/README_CN.md b/README_CN.md index 1ad41e0..c76b433 100644 --- a/README_CN.md +++ b/README_CN.md @@ -62,7 +62,7 @@ dependencies: [ ## 使用 -> 下载项目后,[`ViewController.swift`](JSONPreview/Other/ViewController.swift) 文件中包含部分测试代码,运行项目即可查看对应的效果。 +> 下载项目后,[`ViewController.swift`](JSONPreview/Other/ViewController.swift#L47) 文件中包含部分测试代码,运行项目即可查看对应的效果。 1. 首先创建 `JSONPreview` 对象,并添加到界面上: From 6b79f8618eb13be9d6528df4e02a5f914aab679a Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 15:55:33 +0800 Subject: [PATCH 22/35] update test --- JSONPreview/Other/Base.lproj/Main.storyboard | 32 +++++++++++++---- JSONPreview/Other/ViewController.swift | 38 +++++++++++++------- 2 files changed, 51 insertions(+), 19 deletions(-) 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 c2b387e..15c5004 100644 --- a/JSONPreview/Other/ViewController.swift +++ b/JSONPreview/Other/ViewController.swift @@ -19,26 +19,41 @@ 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 } }())) @@ -112,9 +127,6 @@ class ViewController: UIViewController { } } }, - { - {123456} - } ] """ From 84bb8035d58be2083f0b6a7648071599765beae4 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Fri, 27 May 2022 15:57:02 +0800 Subject: [PATCH 23/35] update layout --- JSONPreview/Core/View/JSONPreview.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/JSONPreview/Core/View/JSONPreview.swift b/JSONPreview/Core/View/JSONPreview.swift index d2746f8..848dc0a 100644 --- a/JSONPreview/Core/View/JSONPreview.swift +++ b/JSONPreview/Core/View/JSONPreview.swift @@ -181,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) @@ -191,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) From 4bc0c0399b1263fecc435da2ba9645be3329228d Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Sun, 29 May 2022 18:10:56 +0800 Subject: [PATCH 24/35] Sorting the key. The order of displaying each time the bail is taken is consistent. --- JSONPreview/Core/Model/JSONDecorator.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/JSONPreview/Core/Model/JSONDecorator.swift b/JSONPreview/Core/Model/JSONDecorator.swift index 0e4853c..c1663a6 100644 --- a/JSONPreview/Core/Model/JSONDecorator.swift +++ b/JSONPreview/Core/Model/JSONDecorator.swift @@ -169,11 +169,16 @@ private extension JSONDecorator { _append(expand: startExpand, fold: startFold) - if !object.isEmpty { + // Sorting the key. + // The order of displaying each time the bail is taken is consistent. + let sortKeys = object.keys.sorted(by: <) + + if !sortKeys.isEmpty { incIndent() // Process each value - for (i, (key, value)) in object.enumerated() { + for (i, key) in sortKeys.enumerated() { + guard let value = object[key] else { continue } let _isNeedComma = i != (object.count - 1) let string = writeIndent() + "\"\(key)\"" From a65f9e634a56e2c1c2e93f8a0b0f96cbbbc8aa0b Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Mon, 30 May 2022 13:41:55 +0800 Subject: [PATCH 25/35] Adjusting the sorting algorithm --- JSONPreview/Core/Model/JSONDecorator.swift | 22 ++++++++---- JSONPreview/Core/Tools/Dictionary+Sort.swift | 38 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 JSONPreview/Core/Tools/Dictionary+Sort.swift diff --git a/JSONPreview/Core/Model/JSONDecorator.swift b/JSONPreview/Core/Model/JSONDecorator.swift index c1663a6..e4aed3e 100644 --- a/JSONPreview/Core/Model/JSONDecorator.swift +++ b/JSONPreview/Core/Model/JSONDecorator.swift @@ -169,11 +169,20 @@ private extension JSONDecorator { _append(expand: startExpand, fold: startFold) - // Sorting the key. - // The order of displaying each time the bail is taken is consistent. - let sortKeys = object.keys.sorted(by: <) + func _appendObjectEnd() { + let endExpand = createObjectEndAttribute(isNeedComma: isNeedComma) + _append(expand: endExpand, fold: nil) + } - if !sortKeys.isEmpty { + if object.isEmpty { + // If the object is empty, add the end flag directly. + _appendObjectEnd() + + } else { + // Sorting the key. + // The order of displaying each time the bail is taken is consistent. + let sortKeys = object.rankingUnknownKeyLast() + incIndent() // Process each value @@ -242,11 +251,10 @@ private extension JSONDecorator { } decIndent() + + _appendObjectEnd() } - let endExpand = createObjectEndAttribute(isNeedComma: isNeedComma) - _append(expand: endExpand, fold: nil) - case .string(let value): let indent = isNeedIndent ? writeIndent() : "" let string = indent + "\"\(value)\"" diff --git a/JSONPreview/Core/Tools/Dictionary+Sort.swift b/JSONPreview/Core/Tools/Dictionary+Sort.swift new file mode 100644 index 0000000..5e7dfa3 --- /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 == String, 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: String? = 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 + } +} From 7937b64d504a7014b984bc730e2d3c5888cbcdd3 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Mon, 13 Jun 2022 17:53:05 +0800 Subject: [PATCH 26/35] fix --- JSONPreview.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/JSONPreview.xcodeproj/project.pbxproj b/JSONPreview.xcodeproj/project.pbxproj index d9a65d6..0048f02 100644 --- a/JSONPreview.xcodeproj/project.pbxproj +++ b/JSONPreview.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -75,6 +76,7 @@ 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -193,6 +195,7 @@ isa = PBXGroup; children = ( AE71889926FADA8300A16878 /* String+ValidURL.swift */, + AEA693A92844894C006BAF10 /* Dictionary+Sort.swift */, ); path = Tools; sourceTree = ""; @@ -356,6 +359,7 @@ 3A1F06D02508D74A00C16862 /* JSONPreview.swift in Sources */, AE92DFD6283F13AD002A7DAF /* JSONParser.swift in Sources */, 3A97FCBC250CA0670017352A /* JSONTextView.swift in Sources */, + AEA693AA2844894C006BAF10 /* Dictionary+Sort.swift in Sources */, 3A1F06A22508D1D800C16862 /* ViewController.swift in Sources */, 3A9DB22F2509DA7A002E7B15 /* HighlightStyle.swift in Sources */, 3A9DB22D2509D7F4002E7B15 /* HighlightColor.swift in Sources */, From 5bf665cf9534daa575c7a9200a317dea76d1f117 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 11:46:00 +0800 Subject: [PATCH 27/35] Add handling of erroneous json and rendering. --- JSONPreview.xcodeproj/project.pbxproj | 8 +- JSONPreview/Core/Entity/JSONError.swift | 2 +- JSONPreview/Core/Entity/JSONObjectKey.swift | 46 ++++ JSONPreview/Core/Entity/JSONValue.swift | 76 +++++-- JSONPreview/Core/Model/JSONDecorator.swift | 175 ++++++++++----- JSONPreview/Core/Model/JSONParser.swift | 211 ++++++++++++++----- JSONPreview/Core/Tools/Dictionary+Sort.swift | 4 +- JSONPreview/Other/ViewController.swift | 1 + 8 files changed, 403 insertions(+), 120 deletions(-) create mode 100644 JSONPreview/Core/Entity/JSONObjectKey.swift diff --git a/JSONPreview.xcodeproj/project.pbxproj b/JSONPreview.xcodeproj/project.pbxproj index 0048f02..ac9f8cd 100644 --- a/JSONPreview.xcodeproj/project.pbxproj +++ b/JSONPreview.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 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 */ @@ -77,6 +78,7 @@ 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 */ @@ -187,6 +189,7 @@ AE92DFD9283F14B0002A7DAF /* JSONError.swift */, AE92DFD7283F13C7002A7DAF /* JSONValue.swift */, 3A8F1AB32509C50C003BAC09 /* JSONSlice.swift */, + AEF7BC2E28596D60009F4B3B /* JSONObjectKey.swift */, ); path = Entity; sourceTree = ""; @@ -359,6 +362,7 @@ 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 */, 3A9DB22F2509DA7A002E7B15 /* HighlightStyle.swift in Sources */, @@ -542,7 +546,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 14; - DEVELOPMENT_TEAM = 5C9JW4S9DE; + DEVELOPMENT_TEAM = W9J48S5PC5; INFOPLIST_FILE = "$(SRCROOT)/JSONPreview/Other/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -564,7 +568,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 14; - DEVELOPMENT_TEAM = 5C9JW4S9DE; + DEVELOPMENT_TEAM = W9J48S5PC5; INFOPLIST_FILE = "$(SRCROOT)/JSONPreview/Other/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/JSONPreview/Core/Entity/JSONError.swift b/JSONPreview/Core/Entity/JSONError.swift index 2f1264a..36b72de 100644 --- a/JSONPreview/Core/Entity/JSONError.swift +++ b/JSONPreview/Core/Entity/JSONError.swift @@ -14,7 +14,7 @@ import Foundation public enum JSONError: Swift.Error, Equatable { case cannotConvertInputDataToUTF8 - case unexpectedCharacter(ascii: UInt8, characterIndex: Int) + case unexpectedCharacter(jsonValue: JSONValue?, ascii: UInt8, characterIndex: Int) case unexpectedEndOfFile case tooManyNestedArraysOrDictionaries(characterIndex: Int) case invalidHexDigitSequence(String, index: Int) 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/Entity/JSONValue.swift b/JSONPreview/Core/Entity/JSONValue.swift index 4428ed7..29561bd 100644 --- a/JSONPreview/Core/Entity/JSONValue.swift +++ b/JSONPreview/Core/Entity/JSONValue.swift @@ -13,33 +13,77 @@ import Foundation public enum JSONValue: Equatable { - case string(String) - case number(String) - case bool(Bool) - case null - + 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([String: JSONValue]) + case object([JSONObjectKey: JSONValue]) case unknown(String) } extension JSONValue { - public var isValue: Bool { + public enum ValueType { + case wrong + case right(isContainer: Bool) + } + + public var isRight: ValueType { switch self { - case .array, .object: - return false - case .null, .number, .string, .bool, .unknown: - return true + 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 var isContainer: Bool { + public func appendWrong(_ wrong: String) -> Self { switch self { - case .array, .object: - return true - case .null, .number, .string, .bool, .unknown: - return false + 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 } } } diff --git a/JSONPreview/Core/Model/JSONDecorator.swift b/JSONPreview/Core/Model/JSONDecorator.swift index e4aed3e..dba65e8 100644 --- a/JSONPreview/Core/Model/JSONDecorator.swift +++ b/JSONPreview/Core/Model/JSONDecorator.swift @@ -130,6 +130,11 @@ private extension JSONDecorator { 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 @@ -140,26 +145,39 @@ private extension JSONDecorator { _append(expand: startExpand, fold: startFold) - if !values.isEmpty { - 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) - } + 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) - decIndent() + 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() } - let endExpand = createArrayEndAttribute(isNeedComma: isNeedComma) - _append(expand: endExpand, fold: nil) + return result // MARK: object case .object(let object): @@ -174,36 +192,68 @@ private extension JSONDecorator { _append(expand: endExpand, fold: nil) } - if object.isEmpty { + 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 } - } else { - // Sorting the key. - // The order of displaying each time the bail is taken is consistent. - let sortKeys = object.rankingUnknownKeyLast() - - incIndent() + func createKeyAttribute(_ key: String, isNeedColon: Bool = true) -> AttributedString { + let keyAttribute = AttributedString(string: key, attributes: keyStyle) + if isNeedColon { + keyAttribute.append(colonAttributeString) + } + return keyAttribute + } - // Process each value - for (i, key) in sortKeys.enumerated() { - guard let value = object[key] else { continue } - let _isNeedComma = i != (object.count - 1) - let string = writeIndent() + "\"\(key)\"" + // Different treatment according to different situations + switch value.isRight { + case .wrong: + let slices = processJSONValueRecursively( + value, + currentSlicesCount: currentSlicesCount + result.count, + isNeedIndent: true, + isNeedComma: false) - let createKeyAttribute: () -> AttributedString = { [weak self] in - guard let this = self else { return .init(string: "") } + 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) + } - let keyAttribute = AttributedString(string: string, attributes: this.keyStyle) - keyAttribute.append(this.colonAttributeString) - return keyAttribute + _append(expand: expand, fold: nil) } - let expand = createKeyAttribute() + 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 + } + }() - // Different treatment according to different situations - if value.isContainer { - let fold = createKeyAttribute() + let expand = createKeyAttribute(string) + + if isContainer { + let fold = createKeyAttribute(string) // Get the content of the subvalue var slices = processJSONValueRecursively( @@ -241,7 +291,7 @@ private extension JSONDecorator { expand.append(slice.expand) if let valueFold = slice.folded { - fold = createKeyAttribute() + fold = createKeyAttribute(string) fold?.append(valueFold) } @@ -249,13 +299,20 @@ private extension JSONDecorator { } } } - - decIndent() - + } + + decIndent() + + // The end node is added only if the object is correct. + if let lastKey = sortKeys.last, + case .wrong = object[lastKey]?.isRight { } + else { _appendObjectEnd() } - case .string(let value): + return result + + case let .string(value, wrong): let indent = isNeedIndent ? writeIndent() : "" let string = indent + "\"\(value)\"" @@ -280,10 +337,15 @@ private extension JSONDecorator { expand.append(commaAttributeString) } + if let wrong = wrong { + expand.append(createUnknownAttributedString(with: wrong)) + } + _append(expand: expand, fold: nil) + return result // MARK: number - case .number(let value): + case let .number(value, wrong): let indent = isNeedIndent ? writeIndent() : "" let string = indent + "\(value)" let expand = AttributedString(string: string, attributes: numberStyle) @@ -292,10 +354,15 @@ private extension JSONDecorator { expand.append(commaAttributeString) } + if let wrong = wrong { + expand.append(createUnknownAttributedString(with: wrong)) + } + _append(expand: expand, fold: nil) + return result // MARK: bool - case .bool(let value): + case let .bool(value, wrong): let indent = isNeedIndent ? writeIndent() : "" let string = indent + (value ? "true" : "false") let expand = AttributedString(string: string, attributes: boolStyle) @@ -304,10 +371,15 @@ private extension JSONDecorator { expand.append(commaAttributeString) } + if let wrong = wrong { + expand.append(createUnknownAttributedString(with: wrong)) + } + _append(expand: expand, fold: nil) + return result // MARK: null - case .null: + case .null(let wrong): let indent = isNeedIndent ? writeIndent() : "" let string = indent + "null" let expand = AttributedString(string: string, attributes: nullStyle) @@ -316,20 +388,23 @@ private extension JSONDecorator { 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 newString = string.replacingOccurrences(of: "\n", with: "") let expand = AttributedString(string: indent) - expand.append(AttributedString(string: newString, attributes: unknownStyle)) + expand.append(createUnknownAttributedString(with: string)) _append(expand: expand, fold: nil) + return result } - - return result } } diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift index cc69d74..f0896ba 100644 --- a/JSONPreview/Core/Model/JSONParser.swift +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -20,29 +20,62 @@ public struct JSONParser { } public mutating func parse() throws -> JSONValue { - 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") + 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(ascii: next, characterIndex: reader.readerIndex + whitespace) + #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)") } - - return value } // MARK: Generic Value Parsing @@ -69,7 +102,7 @@ public struct JSONParser { case UInt8(ascii: "n"): reader.moveReaderIndex(forwardBy: whitespace) try reader.readNull() - return .null + return .null() case UInt8(ascii: "-"), UInt8(ascii: "0") ... UInt8(ascii: "9"): reader.moveReaderIndex(forwardBy: whitespace) let number = try self.reader.readNumber() @@ -78,7 +111,10 @@ public struct JSONParser { whitespace += 1 continue default: - throw JSONError.unexpectedCharacter(ascii: byte, characterIndex: self.reader.readerIndex) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: self.reader.readerIndex) } } @@ -135,14 +171,17 @@ public struct JSONParser { } continue default: - throw JSONError.unexpectedCharacter(ascii: ascii, characterIndex: reader.readerIndex) + throw JSONError.unexpectedCharacter( + jsonValue: .array(array), + ascii: ascii, + characterIndex: reader.readerIndex) } } } // MARK: - Object parsing - - mutating func parseObject() throws -> [String: JSONValue] { + mutating func parseObject() throws -> [JSONObjectKey: JSONValue] { precondition(self.reader.read() == ._openbrace) guard self.depth < 512 else { throw JSONError.tooManyNestedArraysOrDictionaries(characterIndex: self.reader.readerIndex - 1) @@ -162,34 +201,76 @@ public struct JSONParser { break } - var object = [String: JSONValue]() + var object = [JSONObjectKey: JSONValue]() object.reserveCapacity(20) while true { - let key = try reader.readString() + 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 { - throw JSONError.unexpectedCharacter(ascii: colon, characterIndex: reader.readerIndex) + 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() - 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 + 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) } - continue - default: - throw JSONError.unexpectedCharacter(ascii: commaOrBrace, characterIndex: reader.readerIndex) + + } catch let JSONError.unexpectedCharacter(jsonValue, ascii, characterIndex) { + 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 } } } @@ -278,7 +359,10 @@ extension JSONParser { throw JSONError.unexpectedEndOfFile } - throw JSONError.unexpectedCharacter(ascii: self.peek(offset: -1)!, characterIndex: self.readerIndex - 1) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: self.peek(offset: -1)!, + characterIndex: self.readerIndex - 1) } return true @@ -292,7 +376,10 @@ extension JSONParser { throw JSONError.unexpectedEndOfFile } - throw JSONError.unexpectedCharacter(ascii: self.peek(offset: -1)!, characterIndex: self.readerIndex - 1) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: self.peek(offset: -1)!, + characterIndex: self.readerIndex - 1) } return false @@ -311,10 +398,18 @@ extension JSONParser { throw JSONError.unexpectedEndOfFile } - throw JSONError.unexpectedCharacter(ascii: self.peek(offset: -1)!, characterIndex: self.readerIndex - 1) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: self.peek(offset: -1)!, + characterIndex: self.readerIndex - 1) } } + public func readUnknown(start index: Int, leftCount: Int = 0) -> String { + let start = min(index, max(readerIndex, 0)) - leftCount + return String(decoding: self[start ..< array.count], as: Unicode.UTF8.self) + } + // MARK: - Private Methods - // MARK: String @@ -327,7 +422,10 @@ extension JSONParser { private mutating func readUTF8StringTillNextUnescapedQuote() throws -> String { guard self.read() == ._quote else { - throw JSONError.unexpectedCharacter(ascii: self.peek(offset: -1)!, characterIndex: self.readerIndex - 1) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: self.peek(offset: -1)!, + characterIndex: self.readerIndex - 1) } var stringStartIndex = self.readerIndex var copy = 0 @@ -572,7 +670,10 @@ extension JSONParser { numbersSinceControlChar += 1 case UInt8(ascii: "."): guard numbersSinceControlChar > 0, pastControlChar == .operand else { - throw JSONError.unexpectedCharacter(ascii: byte, characterIndex: readerIndex + numberchars) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) } numberchars += 1 @@ -584,7 +685,10 @@ extension JSONParser { guard numbersSinceControlChar > 0, pastControlChar == .operand || pastControlChar == .decimalPoint else { - throw JSONError.unexpectedCharacter(ascii: byte, characterIndex: readerIndex + numberchars) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) } numberchars += 1 @@ -593,7 +697,10 @@ extension JSONParser { numbersSinceControlChar = 0 case UInt8(ascii: "+"), UInt8(ascii: "-"): guard numbersSinceControlChar == 0, pastControlChar == .exp else { - throw JSONError.unexpectedCharacter(ascii: byte, characterIndex: readerIndex + numberchars) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) } numberchars += 1 @@ -601,14 +708,20 @@ extension JSONParser { numbersSinceControlChar = 0 case ._space, ._return, ._newline, ._tab, ._comma, ._closebracket, ._closebrace: guard numbersSinceControlChar > 0 else { - throw JSONError.unexpectedCharacter(ascii: byte, characterIndex: readerIndex + numberchars) + 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(ascii: byte, characterIndex: readerIndex + numberchars) + throw JSONError.unexpectedCharacter( + jsonValue: nil, + ascii: byte, + characterIndex: readerIndex + numberchars) } } diff --git a/JSONPreview/Core/Tools/Dictionary+Sort.swift b/JSONPreview/Core/Tools/Dictionary+Sort.swift index 5e7dfa3..677855f 100644 --- a/JSONPreview/Core/Tools/Dictionary+Sort.swift +++ b/JSONPreview/Core/Tools/Dictionary+Sort.swift @@ -8,7 +8,7 @@ import Foundation -extension Dictionary where Key == String, Value == JSONValue { +extension Dictionary where Key == JSONObjectKey, Value == JSONValue { /// Put the `.unknown` value in last place. /// /// Returns a sorted array of keys, @@ -19,7 +19,7 @@ extension Dictionary where Key == String, Value == JSONValue { func rankingUnknownKeyLast() -> [Key] { guard !isEmpty else { return [] } - var unknownKey: String? = nil + var unknownKey: Key? = nil let otherKeys = keys.drop { key in guard case .unknown = self[key] else { return false } diff --git a/JSONPreview/Other/ViewController.swift b/JSONPreview/Other/ViewController.swift index 15c5004..888a70e 100644 --- a/JSONPreview/Other/ViewController.swift +++ b/JSONPreview/Other/ViewController.swift @@ -127,6 +127,7 @@ class ViewController: UIViewController { } } }, + {123456} ] """ From 901c1f84a936a4fd849a6bd7ad54203952aa5b4d Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 15:04:29 +0800 Subject: [PATCH 28/35] Adjusting the `readUnknown` method --- JSONPreview/Core/Model/JSONParser.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift index f0896ba..d90f015 100644 --- a/JSONPreview/Core/Model/JSONParser.swift +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -405,8 +405,8 @@ extension JSONParser { } } - public func readUnknown(start index: Int, leftCount: Int = 0) -> String { - let start = min(index, max(readerIndex, 0)) - leftCount + 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) } From 3bc23ac65997b76c817ea41fa375eae9eca15fdf Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 15:05:07 +0800 Subject: [PATCH 29/35] Adjust the parsing logic for null and bool values --- JSONPreview/Core/Model/JSONParser.swift | 44 ++++++++++++------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift index d90f015..88d784e 100644 --- a/JSONPreview/Core/Model/JSONParser.swift +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -351,35 +351,34 @@ extension JSONParser { public mutating func readBool() throws -> Bool { switch self.read() { case UInt8(ascii: "t"): - guard self.read() == UInt8(ascii: "r"), - self.read() == UInt8(ascii: "u"), - self.read() == UInt8(ascii: "e") - else { + for (i, ascii) in [UInt8]._true.enumerated() { + if self.read() == ascii { continue } + guard !self.isEOF else { throw JSONError.unexpectedEndOfFile } - + + let offset = min(2 + i, self.readerIndex) throw JSONError.unexpectedCharacter( jsonValue: nil, - ascii: self.peek(offset: -1)!, - characterIndex: self.readerIndex - 1) + ascii: self.peek(offset: -offset)!, + characterIndex: self.readerIndex - offset) } return true case UInt8(ascii: "f"): - guard self.read() == UInt8(ascii: "a"), - self.read() == UInt8(ascii: "l"), - self.read() == UInt8(ascii: "s"), - self.read() == UInt8(ascii: "e") - else { + for (i, ascii) in [UInt8]._false.enumerated() { + if self.read() == ascii { continue } + guard !self.isEOF else { throw JSONError.unexpectedEndOfFile } - + + let offset = min(2 + i, self.readerIndex) throw JSONError.unexpectedCharacter( jsonValue: nil, - ascii: self.peek(offset: -1)!, - characterIndex: self.readerIndex - 1) + ascii: self.peek(offset: -offset)!, + characterIndex: self.readerIndex - offset) } return false @@ -389,19 +388,18 @@ extension JSONParser { } public mutating func readNull() throws { - guard self.read() == UInt8(ascii: "n"), - self.read() == UInt8(ascii: "u"), - self.read() == UInt8(ascii: "l"), - self.read() == UInt8(ascii: "l") - else { + for (i, ascii) in [UInt8]._null.enumerated() { + if self.read() == ascii { continue } + guard !self.isEOF else { throw JSONError.unexpectedEndOfFile } - + + let offset = min(2 + i, self.readerIndex) throw JSONError.unexpectedCharacter( jsonValue: nil, - ascii: self.peek(offset: -1)!, - characterIndex: self.readerIndex - 1) + ascii: self.peek(offset: -offset)!, + characterIndex: self.readerIndex - offset) } } From 6c65260f463bc26d01367a0e71740dd6b3dca0e2 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 15:16:06 +0800 Subject: [PATCH 30/35] Encapsulating the processing logic of regular volumes --- JSONPreview/Core/Model/JSONParser.swift | 52 ++++++++----------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift index 88d784e..28a0610 100644 --- a/JSONPreview/Core/Model/JSONParser.swift +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -351,44 +351,31 @@ extension JSONParser { public mutating func readBool() throws -> Bool { switch self.read() { case UInt8(ascii: "t"): - for (i, ascii) in [UInt8]._true.enumerated() { - if self.read() == ascii { continue } - - guard !self.isEOF else { - throw JSONError.unexpectedEndOfFile - } - - let offset = min(2 + i, self.readerIndex) - throw JSONError.unexpectedCharacter( - jsonValue: nil, - ascii: self.peek(offset: -offset)!, - characterIndex: self.readerIndex - offset) - } - + try readGenericValue([UInt8]._true) return true + case UInt8(ascii: "f"): - for (i, ascii) in [UInt8]._false.enumerated() { - if self.read() == ascii { continue } - - guard !self.isEOF else { - throw JSONError.unexpectedEndOfFile - } - - let offset = min(2 + i, self.readerIndex) - throw JSONError.unexpectedCharacter( - jsonValue: nil, - ascii: self.peek(offset: -offset)!, - characterIndex: self.readerIndex - offset) - } - + try readGenericValue([UInt8]._false) return false + default: preconditionFailure("Expected to have `t` or `f` as first character") } } public mutating func readNull() throws { - for (i, ascii) in [UInt8]._null.enumerated() { + 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 } guard !self.isEOF else { @@ -403,13 +390,6 @@ extension JSONParser { } } - 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 - - // MARK: String public enum EscapedSequenceError: Swift.Error { From 95beb0639d00db8615a07dbaf80b80e1a5b17ea0 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 15:17:00 +0800 Subject: [PATCH 31/35] Reducing `unexpectedEndOfFile` errors --- JSONPreview/Core/Model/JSONParser.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift index 28a0610..8d10521 100644 --- a/JSONPreview/Core/Model/JSONParser.swift +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -378,10 +378,6 @@ extension JSONParser { for (i, ascii) in value.enumerated() { if self.read() == ascii { continue } - guard !self.isEOF else { - throw JSONError.unexpectedEndOfFile - } - let offset = min(2 + i, self.readerIndex) throw JSONError.unexpectedCharacter( jsonValue: nil, From df49536ddc2e4ed42cf671c2d67825d4c7361ce3 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 15:28:24 +0800 Subject: [PATCH 32/35] Fix boolean value parsing error --- JSONPreview/Core/Model/JSONParser.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift index 8d10521..1489bbf 100644 --- a/JSONPreview/Core/Model/JSONParser.swift +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -351,11 +351,11 @@ extension JSONParser { public mutating func readBool() throws -> Bool { switch self.read() { case UInt8(ascii: "t"): - try readGenericValue([UInt8]._true) + try readGenericValue([UInt8]._trueSub) return true case UInt8(ascii: "f"): - try readGenericValue([UInt8]._false) + try readGenericValue([UInt8]._falseSub) return false default: @@ -729,7 +729,11 @@ extension UInt8 { } extension Array where Element == UInt8 { - fileprivate static let _true = [UInt8(ascii: "t"), UInt8(ascii: "r"), UInt8(ascii: "u"), UInt8(ascii: "e")] - fileprivate static let _false = [UInt8(ascii: "f"), UInt8(ascii: "a"), UInt8(ascii: "l"), UInt8(ascii: "s"), UInt8(ascii: "e")] + 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")] } From 35091de548ed2e8b9cb0ca49cad6d4cf2e469819 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 15:28:41 +0800 Subject: [PATCH 33/35] update --- JSONPreview/Other/ViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONPreview/Other/ViewController.swift b/JSONPreview/Other/ViewController.swift index 888a70e..05bc42f 100644 --- a/JSONPreview/Other/ViewController.swift +++ b/JSONPreview/Other/ViewController.swift @@ -59,7 +59,8 @@ class ViewController: UIViewController { NSLayoutConstraint.activate(constraints) - let json = """ + let json = + """ [ [], [], @@ -132,7 +133,6 @@ class ViewController: UIViewController { """ - let start = Date().timeIntervalSince1970 print("will display json") From d427e9b2375327a2b5348a2dfaaa745192ae8296 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 17:08:41 +0800 Subject: [PATCH 34/35] TODO Handling nested scenarios with wrong value --- JSONPreview/Core/Model/JSONParser.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/JSONPreview/Core/Model/JSONParser.swift b/JSONPreview/Core/Model/JSONParser.swift index 1489bbf..58ccc81 100644 --- a/JSONPreview/Core/Model/JSONParser.swift +++ b/JSONPreview/Core/Model/JSONParser.swift @@ -257,6 +257,24 @@ public struct JSONParser { } } 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 From 952da41ca92ca4c3658639c10a9183d541e7e907 Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Wed, 15 Jun 2022 17:11:03 +0800 Subject: [PATCH 35/35] update README: Format Check --- README.md | 32 ++++++++++++++++---------------- README_CN.md | 32 ++++++++++++++++---------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1fc0db3..9d26110 100644 --- a/README.md +++ b/README.md @@ -118,27 +118,27 @@ previewView.preview(json, style: style) ### Rendering -For rendering, `JSONPreview` performs only **limited** formatting checks, including. - -> None of the `previous nodes` mentioned below include `spaces`, `\t`, and `\n`. - -- 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`. +For rendering, `JSONPreview` only performs **limited** formatting checks. + +The conditions that are known to trigger `Error Rendering` include + +- 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. + +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**. -Any other syntax errors will not trigger a rendering error. +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: diff --git a/README_CN.md b/README_CN.md index c76b433..8ff8c9e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -116,27 +116,27 @@ previewView.preview(json, style: style) ### 渲染 -对于渲染,`JSONPreview` 只进行**有限**的格式检查,包括: - -> 以下所提到的 “上一个节点” 均不包括 `空格`、`\t` 以及 `\n`。 - -- 所要预览的JSON必须以 `{` 或 `[` 开头。 -- `:` 的上一个节点必须是 `.string`。 -- `,` 的上一个节点只能是 `.null`、`.link`、`.string`、`.number`、`.boolean`、`}` 以及 `]` 中的一个。 -- `{` 必须存在上一个节点,同时上一个节点不能为 `{`。 -- `}` 必须与 `{` 成对出现。 -- `[` 必须存在上一个节点,同时上一个节点不能为 `]`。 -- `]` 必须与 `[` 成对出现。 -- `"` 必须成对出现。 -- `"` 的上一个节点只能是 `{`、`[`、`,` 以及 `:` 中的一个。 -- 针对 `null`、`true` 以及 `false` 的拼写检查。 +对于渲染,`JSONPreview` 只进行**有限**的格式检查。 + +目前已知的会触发 “错误渲染” 的条件,包括: + +- 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` 会进行有限的**去转义**操作。 不同版本支持的去转义操作如下: