parent
1c461041c1
commit
29a065049e
|
@ -24,8 +24,8 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
|
||||||
private var tokenizer: Tokenizer<String.Iterator>!
|
private var tokenizer: Tokenizer<String.Iterator>!
|
||||||
private var str: NSMutableAttributedString!
|
private var str: NSMutableAttributedString!
|
||||||
|
|
||||||
private var actionStack: InlineArray3<ElementAction> = []
|
private var actionStack: [ElementAction] = []
|
||||||
private var styleStack: InlineArray3<Style> = []
|
private var styleStack: [Style] = []
|
||||||
// The current run of text w/o styles changing
|
// The current run of text w/o styles changing
|
||||||
private var currentRun: String = ""
|
private var currentRun: String = ""
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func handleStartTag(_ name: String, attributes: InlineArray3<HTMLStreamer.Attribute>) {
|
private mutating func handleStartTag(_ name: String, attributes: [HTMLStreamer.Attribute]) {
|
||||||
switch name {
|
switch name {
|
||||||
case "br":
|
case "br":
|
||||||
currentRun.append("\n")
|
currentRun.append("\n")
|
||||||
|
@ -287,7 +287,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
|
||||||
|
|
||||||
public protocol AttributedStringCallbacks {
|
public protocol AttributedStringCallbacks {
|
||||||
static func makeURL(string: String) -> URL?
|
static func makeURL(string: String) -> URL?
|
||||||
static func elementAction(name: String, attributes: InlineArray3<Attribute>) -> ElementAction
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ElementAction: Equatable {
|
public enum ElementAction: Equatable {
|
||||||
|
@ -308,7 +308,7 @@ public extension AttributedStringCallbacks {
|
||||||
static func makeURL(string: String) -> URL? {
|
static func makeURL(string: String) -> URL? {
|
||||||
URL(string: string)
|
URL(string: string)
|
||||||
}
|
}
|
||||||
static func elementAction(name: String, attributes: InlineArray3<Attribute>) -> ElementAction {
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
.default
|
.default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,220 +0,0 @@
|
||||||
//
|
|
||||||
// InlineArray3.swift
|
|
||||||
// HTMLStreamer
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/19/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// An array with inline space for up to 3 elements.
|
|
||||||
///
|
|
||||||
/// If the array grows beyond 3 elements, it will be stored out-of-line.
|
|
||||||
/// Once that happens, the array will never return to being stored inline,
|
|
||||||
/// since the allocation cost has already been paid.
|
|
||||||
public struct InlineArray3<E> {
|
|
||||||
private var storage: Storage
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
self.storage = .inline(nil, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3 {
|
|
||||||
fileprivate enum Storage {
|
|
||||||
case inline(Element?, Element?, Element?)
|
|
||||||
case array(ContiguousArray<Element>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3: ExpressibleByArrayLiteral {
|
|
||||||
public init(arrayLiteral elements: Element...) {
|
|
||||||
switch elements.count {
|
|
||||||
case 0:
|
|
||||||
self.storage = .inline(nil, nil, nil)
|
|
||||||
case 1:
|
|
||||||
self.storage = .inline(elements[0], nil, nil)
|
|
||||||
case 2:
|
|
||||||
self.storage = .inline(elements[0], elements[1], nil)
|
|
||||||
case 3:
|
|
||||||
self.storage = .inline(elements[0], elements[1], elements[2])
|
|
||||||
default:
|
|
||||||
self.storage = .array(.init(elements))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3: MutableCollection {
|
|
||||||
public typealias Element = E
|
|
||||||
public typealias Index = Int
|
|
||||||
public typealias Indices = Range<Int>
|
|
||||||
|
|
||||||
public subscript(position: Int) -> Element {
|
|
||||||
_read {
|
|
||||||
precondition(position < endIndex)
|
|
||||||
switch storage {
|
|
||||||
case .inline(let a, let b, let c):
|
|
||||||
switch position {
|
|
||||||
case 0:
|
|
||||||
yield a.unsafelyUnwrapped
|
|
||||||
case 1:
|
|
||||||
yield b.unsafelyUnwrapped
|
|
||||||
case 2:
|
|
||||||
yield c.unsafelyUnwrapped
|
|
||||||
default:
|
|
||||||
fatalError("unreachable")
|
|
||||||
}
|
|
||||||
case .array(let arr):
|
|
||||||
yield arr[position]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_modify {
|
|
||||||
precondition(position < endIndex)
|
|
||||||
switch storage {
|
|
||||||
case .inline(let a, let b, let c):
|
|
||||||
switch position {
|
|
||||||
case 0:
|
|
||||||
var newValue = a.unsafelyUnwrapped
|
|
||||||
yield &newValue
|
|
||||||
storage = .inline(newValue, b, c)
|
|
||||||
case 1:
|
|
||||||
var newValue = b.unsafelyUnwrapped
|
|
||||||
yield &newValue
|
|
||||||
storage = .inline(a, newValue, c)
|
|
||||||
case 2:
|
|
||||||
var newValue = c.unsafelyUnwrapped
|
|
||||||
yield &newValue
|
|
||||||
storage = .inline(a, b, newValue)
|
|
||||||
default:
|
|
||||||
fatalError("unreachable")
|
|
||||||
}
|
|
||||||
case .array(var arr):
|
|
||||||
yield &arr[position]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var startIndex: Int {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
public var endIndex: Int {
|
|
||||||
switch storage {
|
|
||||||
case .inline(let a, let b, let c):
|
|
||||||
a == nil ? 0 : b == nil ? 1 : c == nil ? 2 : 3
|
|
||||||
case .array(let arr):
|
|
||||||
arr.endIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3: BidirectionalCollection {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3: RandomAccessCollection {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3: RangeReplaceableCollection {
|
|
||||||
public mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C: Collection, Element == C.Element {
|
|
||||||
switch storage {
|
|
||||||
case .array(var arr):
|
|
||||||
arr.replaceSubrange(subrange, with: newElements)
|
|
||||||
storage = .array(arr)
|
|
||||||
case .inline(var a, var b, var c):
|
|
||||||
if count - subrange.count + newElements.count <= 3 {
|
|
||||||
// remove elements at subrange indices
|
|
||||||
if subrange.contains(2) {
|
|
||||||
c = nil
|
|
||||||
}
|
|
||||||
if subrange.contains(1) {
|
|
||||||
b = c
|
|
||||||
c = nil
|
|
||||||
}
|
|
||||||
if subrange.contains(0) {
|
|
||||||
a = b
|
|
||||||
b = c
|
|
||||||
c = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert newElements starting at subrange.lowerBound
|
|
||||||
for (offset, el) in newElements.enumerated() {
|
|
||||||
// assert that we have space to insert
|
|
||||||
assert(c == nil)
|
|
||||||
let newIndex = subrange.lowerBound + offset
|
|
||||||
switch newIndex {
|
|
||||||
case 2:
|
|
||||||
c = el
|
|
||||||
case 1:
|
|
||||||
c = b
|
|
||||||
b = el
|
|
||||||
case 0:
|
|
||||||
c = b
|
|
||||||
b = a
|
|
||||||
a = el
|
|
||||||
default:
|
|
||||||
fatalError("unreachable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
storage = .inline(a, b, c)
|
|
||||||
} else {
|
|
||||||
var arr: ContiguousArray = if let a {
|
|
||||||
if let b {
|
|
||||||
if let c {
|
|
||||||
[a, b, c]
|
|
||||||
} else {
|
|
||||||
[a, b]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
[a]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
arr.replaceSubrange(subrange, with: newElements)
|
|
||||||
storage = .array(arr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Collection {
|
|
||||||
func safeIndex(_ index: Index, offsetBy: Int) -> Index? {
|
|
||||||
var index = index
|
|
||||||
var offsetBy = offsetBy
|
|
||||||
while offsetBy > 0 {
|
|
||||||
if index < endIndex {
|
|
||||||
formIndex(after: &index)
|
|
||||||
offsetBy -= 1
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return index >= endIndex ? nil : index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3.Storage: Equatable where E: Equatable {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3: Equatable where E: Equatable {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InlineArray3: CustomStringConvertible {
|
|
||||||
public var description: String {
|
|
||||||
switch storage {
|
|
||||||
case .inline(nil, nil, nil):
|
|
||||||
return "[]"
|
|
||||||
case .inline(.some(let a), nil, nil):
|
|
||||||
return "[\(a)]"
|
|
||||||
case .inline(.some(let a), .some(let b), nil):
|
|
||||||
return "[\(a), \(b)]"
|
|
||||||
case .inline(.some(let a), .some(let b), .some(let c)):
|
|
||||||
return "[\(a), \(b), \(c)]"
|
|
||||||
case .inline(_, _, _):
|
|
||||||
fatalError("InlineArray3 invariant violated")
|
|
||||||
case .array(let arr):
|
|
||||||
return arr.description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ struct Tokenizer<Chars: IteratorProtocol<Character>>: IteratorProtocol {
|
||||||
typealias Element = Token
|
typealias Element = Token
|
||||||
|
|
||||||
private var chars: Chars
|
private var chars: Chars
|
||||||
private var reconsumeStack: InlineArray3<Character> = []
|
private var reconsumeStack: [Character] = []
|
||||||
private var state = State.data
|
private var state = State.data
|
||||||
private var returnState: State?
|
private var returnState: State?
|
||||||
private var temporaryBuffer: String?
|
private var temporaryBuffer: String?
|
||||||
|
@ -195,7 +195,7 @@ struct Tokenizer<Chars: IteratorProtocol<Character>>: IteratorProtocol {
|
||||||
enum Token: Equatable {
|
enum Token: Equatable {
|
||||||
case character(Character)
|
case character(Character)
|
||||||
case comment(String)
|
case comment(String)
|
||||||
case startTag(String, selfClosing: Bool, attributes: InlineArray3<Attribute>)
|
case startTag(String, selfClosing: Bool, attributes: [Attribute])
|
||||||
case endTag(String)
|
case endTag(String)
|
||||||
case doctype(String, forceQuirks: Bool, publicIdentifier: String?, systemIdentifier: String?)
|
case doctype(String, forceQuirks: Bool, publicIdentifier: String?, systemIdentifier: String?)
|
||||||
}
|
}
|
||||||
|
@ -561,7 +561,6 @@ private extension Tokenizer {
|
||||||
switch c {
|
switch c {
|
||||||
case .some("0"..."9"), .some("a"..."z"), .some("A"..."Z"):
|
case .some("0"..."9"), .some("a"..."z"), .some("A"..."Z"):
|
||||||
if case .attributeValue(_) = returnState {
|
if case .attributeValue(_) = returnState {
|
||||||
// TODO: append the current input character to the current attribute's value
|
|
||||||
if case .startTag(let s, let selfClosing, var attributes) = currentToken {
|
if case .startTag(let s, let selfClosing, var attributes) = currentToken {
|
||||||
attributes[attributes.count - 1].value.append(c!)
|
attributes[attributes.count - 1].value.append(c!)
|
||||||
currentToken = .startTag(s, selfClosing: selfClosing, attributes: attributes)
|
currentToken = .startTag(s, selfClosing: selfClosing, attributes: attributes)
|
||||||
|
|
|
@ -175,7 +175,7 @@ final class AttributedStringConverterTests: XCTestCase {
|
||||||
|
|
||||||
func testElementActionCallback() {
|
func testElementActionCallback() {
|
||||||
struct Callbacks: AttributedStringCallbacks {
|
struct Callbacks: AttributedStringCallbacks {
|
||||||
static func elementAction(name: String, attributes: InlineArray3<Attribute>) -> ElementAction {
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
let clazz = attributes.attributeValue(for: "class")
|
let clazz = attributes.attributeValue(for: "class")
|
||||||
if clazz == "invisible" {
|
if clazz == "invisible" {
|
||||||
return .skip
|
return .skip
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
//
|
|
||||||
// InlineArray3Tests.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/19/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
@testable import HTMLStreamer
|
|
||||||
|
|
||||||
final class InlineArray3Tests: XCTestCase {
|
|
||||||
|
|
||||||
func testReplaceSubrange() {
|
|
||||||
// same size
|
|
||||||
var a: InlineArray3 = [0, 1, 2]
|
|
||||||
a.replaceSubrange(0..<2, with: [3, 4])
|
|
||||||
XCTAssertEqual(a, [3, 4, 2])
|
|
||||||
|
|
||||||
// grow
|
|
||||||
a = [0, 1]
|
|
||||||
a.replaceSubrange(1..<2, with: [2, 3])
|
|
||||||
XCTAssertEqual(a, [0, 2, 3])
|
|
||||||
|
|
||||||
// shrink
|
|
||||||
a = [0, 1, 2]
|
|
||||||
a.replaceSubrange(0..<2, with: [])
|
|
||||||
XCTAssertEqual(a, [2])
|
|
||||||
a.removeFirst()
|
|
||||||
XCTAssertEqual(a, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRemoveLast() {
|
|
||||||
var a: InlineArray3 = [0, 1, 2]
|
|
||||||
a.removeLast(2)
|
|
||||||
XCTAssertEqual(a, [0])
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue