Files
HtmlRW/Sources/HRW/BaseComponents.swift
2025-10-25 00:04:37 -04:00

396 lines
11 KiB
Swift

//
// BaseComponents.swift
// HRW
//
// Created by Isaac Paul on 10/15/24.
// Non-commercial license, see LICENSE.MD in the project root for details
//
public protocol IFlowContent : HTMLNode {}
public protocol IGlobalContainer {
var globalAttributes:Dictionary<GlobalAttributeKey, String> { get set }
var globalEvents:Dictionary<GlobalEventKey, String> { get set }
var dataAttributes:Dictionary<String, String> { get set }
}
extension IGlobalContainer {
mutating func trySetGlobalAttribute(_ key:String, _ value:String) -> Bool {
if let attr = GlobalAttributeKey(rawValue: key) {
globalAttributes.updateValue(value, forKey: attr)
return true
}
if let attr = GlobalEventKey(rawValue: key.asSubstring()) {
globalEvents[attr] = value
return true
}
if key[..<key.index(key.startIndex, offsetBy: 5)] == "data-" {
dataAttributes[key] = value
return true
}
return false
}
}
public class NParent : NRenderable {
public var children:[NRenderable] = []
public override init() {
super.init()
children = []
}
}
public class NRenderable {
public var parent:NParent? = nil
}
public class NNone: NRenderable { }
public struct GlobalAttributesBuilder : IGlobalContainer{
public var globalAttributes:Dictionary<GlobalAttributeKey, String> = [:]
public var globalEvents:Dictionary<GlobalEventKey, String> = [:]
public var dataAttributes:Dictionary<String, String> = [:]
public init(globalAttributes: Dictionary<GlobalAttributeKey, String>, globalEvents: Dictionary<GlobalEventKey, String>, dataAttributes: Dictionary<String, String>) {
self.globalAttributes = globalAttributes
self.globalEvents = globalEvents
self.dataAttributes = dataAttributes
}
public init() {
self.globalAttributes = [:]
self.globalEvents = [:]
self.dataAttributes = [:]
}
public init(_ attributes: [String: String]) throws {
self.globalAttributes = [:]
self.globalEvents = [:]
self.dataAttributes = [:]
for (key, value) in attributes {
if self.trySetGlobalAttribute(key, value) {
continue
}
throw AppError("Unexpected attribute: \(key)")
}
}
}
// The problem is pausing and resuming so this is a n-squared search, but is it faster than make allocations? do we even need this?
/*
public struct HtmlIterator : IteratorProtocol {
public init(src: NHTMLRenderable) {
self._src = src
}
public typealias Element = NHTMLRenderable
private let _src:NHTMLRenderable
private var _index:Int = 0
public mutating func next() -> NHTMLRenderable? {
let (index, result) = _src.renderableAtIndex(_index)
_index += 1
return result
}
}*/
public class HTMLNode : XMLNode, IGlobalContainer {
public var globalAttributes:Dictionary<GlobalAttributeKey, String> = [:]
public var globalEvents:Dictionary<GlobalEventKey, String> = [:]
public var children:[HTMLNode] = []
public init(expectedAttributes:[String:String]) throws {
super.init()
for (key, value) in expectedAttributes {
if let attr = GlobalAttributeKey(rawValue: key) {
globalAttributes.updateValue(value, forKey: attr)
continue
}
if let attr = GlobalEventKey(rawValue: key.asSubstring()) {
globalEvents[attr] = value
continue
}
if key[..<key.index(key.startIndex, offsetBy: 5)] == "data-" {
dataAttributes[key] = value
continue
}
throw AppError("Unexpected attribute: \(key)")
}
}
public init(globalAttributes:Dictionary<GlobalAttributeKey, String>, globalEvents:Dictionary<GlobalEventKey, String>, dataAttributes:Dictionary<String, String>) {
self.globalAttributes = globalAttributes
self.globalEvents = globalEvents
super.init(dataAttributes: dataAttributes)
}
public init(_ builder:GlobalAttributesBuilder, _ children:[HTMLNode] = []) {
self.globalAttributes = builder.globalAttributes
self.globalEvents = builder.globalEvents
self.children = children
super.init(dataAttributes: builder.dataAttributes)
}
public func findById(_ id:String) -> HTMLNode? {
if (globalAttributes[.id] == id) {
return self
}
for eachChild in children {
if let result = eachChild.findById(id) {
return result
}
}
return nil
}
public func iterate(_ index:Int, _ skipTextNodes:Bool, _ cb:(HTMLNode, Int)->()) -> Int {
if (skipTextNodes && self is HTMLText) {
return index
}
cb(self, index)
var newIndex = index + 1
for eachChild in children {
newIndex = eachChild.iterate(newIndex, skipTextNodes, cb)
}
return newIndex
}
public func flatten() -> [HTMLNode] {
let count = countElements()
let result:[HTMLNode] = Array(unsafeUninitializedCapacity: count+1) { buffer, initializedCount in
initializedCount = self.addTo(array: &buffer, index: 0)
}
return result
}
public func countElements() -> Int {
var result = 1
for eachChild in children {
result += eachChild.countElements()
}
return result
}
private func addTo(array:inout UnsafeMutableBufferPointer<HTMLNode>, index:Int) -> Int {
array.initializeElement(at: index, to: self)
var newIndex = index + 1
for eachChild in children {
newIndex = eachChild.addTo(array: &array, index: newIndex)
}
return newIndex
}
public override func renderAttributes() -> String {
var result = super.renderAttributes()
var first = result.count == 0
for eachAttr in globalAttributes {
if (!first) {
result += " "
}
first = false
if (eachAttr.value.count > 0) {
result += "\(eachAttr.key)='\(eachAttr.value)'"
} else {
result += "\(eachAttr.key)"
}
}
for eachAttr in globalEvents {
if (!first) {
result += " "
}
first = false
if (eachAttr.value.count > 0) {
result += "\(eachAttr.key) = \(eachAttr.value)"
} else {
result += "\(eachAttr.key)"
}
}
return result
}
override var nodeName: String {
return "HTML"
}
override var isVoidElement: Bool {
return false
}
override public func toString(_ depth:Int = 0, spacingStrat:SpacingStrat = .tabs) -> (Int, String) {
var newDepth = depth
var result = renderTag()
if (!isVoidElement) {
for eachChild in children {
let (nextDepth, renderedChild) = eachChild.toString(newDepth, spacingStrat: spacingStrat)
newDepth = nextDepth
result += renderedChild
}
result += "<\(nodeName)/>"
}
return (newDepth, result)
}
}
public enum SpacingStrat {
case tabs
case spaces(num:Int)
}
public class GenericXMLNode : XMLNode {
public var attributes:Dictionary<String, String> = [:]
public var children:[XMLNode] = []
public var name:String
public init(_ name:String, _ attributes:[String:String], _ children:[XMLNode] = []) {
self.attributes = attributes
self.children = children
self.name = name
super.init(attributes)
}
public init(_ name:String, _ attributes:[String:String], _ parser:XMLParser? = nil) throws {
self.attributes = attributes
self.name = name
super.init(attributes)
var allItems:[XMLNode] = []
while let obj = try parser?.readObjectXml(endTag: "a") {
allItems.append(obj)
}
children = allItems
}
}
public class XMLNode { //Not sure if should be a parent class or just protocol
public var dataAttributes:Dictionary<String, String> = [:]
public var parent:XMLNode? = nil
var nodeName: String {
return ""
}
var isVoidElement: Bool {
return true
}
/*
public func it() -> HtmlIterator {
return HtmlIterator(src: self)
}*/
public init() {
}
public init(_ attributes:[String:String]) {
for (key, value) in attributes {
if key[..<key.index(key.startIndex, offsetBy: 5)] == "data-" {
dataAttributes[key] = value
continue
}
}
}
public init(dataAttributes:[String:String]) {
self.dataAttributes = dataAttributes
}
public func renderAttributes() -> String {
var result = ""
var first = true
for eachAttr in dataAttributes {
if (!first) {
result += " "
}
first = false
if (eachAttr.value.count > 0) {
result += "\(eachAttr.key) = \(eachAttr.value)"
} else {
result += "\(eachAttr.key)"
}
}
return result
}
/*
public func renderableAtIndex(_ index:Int) -> (Int, NHTMLRenderable?) {
if (index == 0) {
return (-1, self)
}
var nextIndex = index - 1
for eachChild in children {
let (index, result) = eachChild.renderableAtIndex(nextIndex)
if let result = result {
return (-1, result)
}
nextIndex = index
}
return (nextIndex, nil)
}*/
public func toString(_ depth:Int = 0, spacingStrat:SpacingStrat = .tabs) -> (Int, String) {
var newDepth = depth
var result = "<\(nodeName) \(renderAttributes())"
if (isVoidElement) {
result += "/>"
} else {
result += ">"
}
return (newDepth, result)
}
public func renderTag() -> String {
let closing = isVoidElement ? "/" : ""
let attributes = renderAttributes()
if (attributes.isEmpty) {
let result = "<\(nodeName)\(closing)>"
return result
} else {
let result = "<\(nodeName) \(renderAttributes()) \(closing)>"
return result
}
}
}
func isGlobalHTMLAttribute(_ key:String) -> Bool {
if let _ = GlobalAttributeKey(rawValue: key) {
return true
}
if let _ = GlobalEventKey(rawValue: key.asSubstring()) {
return true
}
if key[..<key.index(key.startIndex, offsetBy: 5)] == "data-" {
return true
}
return false
}
public protocol IHTMLParent : HTMLNode {
var childrenAny:[HTMLNode] { get set }
}
/*
public class NHTMLParent : NHTMLRenderable {
public var children:[NHTMLRenderable] { get {} set {} }
override open func findById(_ id:String) -> NHTMLRenderable? {
if let result = super.findById(id) {
return result
}
for eachChild in children {
if let result = eachChild.findById(id) {
return result
}
}
return nil
}
}*/