diff --git a/Package.swift b/Package.swift index 78baf41..affb82d 100644 --- a/Package.swift +++ b/Package.swift @@ -12,26 +12,38 @@ import PackageDescription let package = Package( name: "HRW", + platforms: [ + .macOS(.v13), //v10_15 + .iOS(.v13)], products: [ .library( name: "HRW", targets: ["HRW"] ), + .plugin(name: "BindingPlugin", targets: ["BindingPlugin"]) ], dependencies: [ - .package(path: "/Users/isaacpaul/Projects/swift-projects/GenHTML5") + .package(path: "/Users/isaacpaul/Projects/swift-projects/GenHTML5"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), ], targets: [ .target( name: "HRW", dependencies: [ ] ), + .executableTarget(name: "BindingGenerator", dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser") + ]), .testTarget( name: "HRWTests", dependencies: [ "HRW"], - sources: ["example.html"], - plugins: [.plugin(name: "BindingPlugin", package: "genhtml5")] + plugins: [.plugin(name: "BindingPlugin")] + ), + .plugin( + name: "BindingPlugin", + capability: .buildTool(), + dependencies: [.target(name: "BindingGenerator")] ) ] ) diff --git a/Plugins/BindingPlugin/source.swift b/Plugins/BindingPlugin/source.swift new file mode 100644 index 0000000..5ac8163 --- /dev/null +++ b/Plugins/BindingPlugin/source.swift @@ -0,0 +1,173 @@ +// +// source.swift +// gen_html +// +// Created by Isaac Paul on 9/30/25. +// + +import Foundation +import PackagePlugin + +final class AppError: LocalizedError, Sendable { + + let message: String + + init(_ message: String) { + self.message = message + } + init(_ message: String, _ error:Error) { + self.message = "\(message) : \(error.localizedDescription)" + } + + static func failure(_ message: String) -> Result { + return .failure(AppError(message)) + } + + var errorDescription: String? { + get { + return message + } + } + + var failureReason: String? { get { return message } } +} + +let toolName = "BindingGenerator" + +extension BindingPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + guard let swiftTarget = target as? SwiftSourceModuleTarget else { + print("Unexpected target: \(type(of: target)). Needs SwiftSourceModuleTarget") + throw AppError("Unexpected target: \(type(of: target)). Needs SwiftSourceModuleTarget") + } + let toolUrl = try context.tool(named: toolName).url + + return try createBuildCommands( + outputDir: context.pluginWorkDirectoryURL, + inputDir: swiftTarget.directoryURL, + inputFiles: swiftTarget.sourceFiles, + toolExe: toolUrl + ) + } +} + +#if canImport(XcodeProjectPlugin) + + import XcodeProjectPlugin +import System + + // The entry point for Xcode project builds. + extension BindingPlugin: XcodeBuildToolPlugin { + func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { + let toolUrl = try context.tool(named: toolName).url + return try createBuildCommands( + outputDir: context.pluginWorkDirectoryURL, + inputDir: context.xcodeProject.directoryURL, + inputFiles: target.inputFiles, + toolExe: toolUrl + ) + } + } + +#endif + +struct Config: Codable { + var root_path: String? +} + +extension URL { + func isHtml() -> Bool { + return self.pathExtension == "html" || self.pathExtension == "htm" + } +} + +extension String { + func uppercaseFirstLetter() -> String { + guard let firstLetter = self.first else { return self } + return firstLetter.uppercased() + self.dropFirst() + } +} + +@main +struct BindingPlugin { + + private static let configFile = "binding_generator_plugin.json" + + private func createBuildCommands( + outputDir: URL, + inputDir: URL, + inputFiles: FileList, + toolExe: URL + ) throws -> [Command] { + + let configFilePath = inputFiles.first(where: { $0.url.lastPathComponent == BindingPlugin.configFile }) + let configFileURL = configFilePath?.url + + //let listSearchDir: [String] + let htmlFiles = inputFiles.filter { $0.url.isHtml() }.map { $0.url } + let rootPath:URL + + if let configFileURL = configFileURL { + + let configData = try Data(contentsOf: configFileURL) + let config = try JSONDecoder().decode(Config.self, from: configData) + + let baseDirectory:URL = configFileURL.deletingLastPathComponent() + if let configRootPath = config.root_path { + rootPath = baseDirectory.appending(path: configRootPath) + } else { + rootPath = inputDir + } + /* + for source in config.sources ?? [] { + let sourceFileOrDirectory = baseDirectory.appendingPathComponent(source) + if sourceFileOrDirectory.isHtml() { + htmlFiles.append(sourceFileOrDirectory) + } else { + let files = try FileManager.default.contentsOfDirectory( + at: sourceFileOrDirectory, + includingPropertiesForKeys: nil + ) + let filteredFiles = files.filter { $0.isHtml() } + htmlFiles.append(contentsOf: filteredFiles) + } + }*/ + + } else { + rootPath = inputDir + } +/* + /Users/isaacpaul/Projects/swift-projects/HRW/Plugins/BindingPlugin/source.swift:146:13: error: 'let' cannot appear nested inside another 'var' or 'let' pattern + :0: error: error opening input file '/Users/isaacpaul/Library/Developer/Xcode/DerivedData/HRW-bicwrilrmihgqogjvxyqtcosfaro/Build/Intermediates.noindex/BuildToolPluginIntermediates/hrw.output/HRWTests/BindingPlugin/Example.swift' (No such file or directory) + // + Showing All Messages + Error opening input file '/Users/isaacpaul/Library/Developer/Xcode/DerivedData/HRW-bicwrilrmihgqogjvxyqtcosfaro/Build/Intermediates.noindex/BuildToolPluginIntermediates/hrw.output/HRWTests/BindingPlugin/ExampleBinding.swift' (No such file or directory) + */ + return htmlFiles.map { eachFile in + let fileName = eachFile.lastPathComponent + let ext = eachFile.pathExtension + let extIndex = fileName.index(fileName.startIndex, offsetBy: fileName.count - (ext.count + 1)) + let withoutExt = String(fileName[fileName.startIndex.. { get set } + var globalEvents:Dictionary { get set } + var dataAttributes:Dictionary { 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.count >= 5) { + if key[.. = [:] + public var globalEvents:Dictionary = [:] + public var dataAttributes:Dictionary = [:] + + public init(globalAttributes: Dictionary, globalEvents: Dictionary, dataAttributes: Dictionary) { + 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 + } + continue + } + } +} + +// 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: HTMLNode) { + self._currentNode = src + } + + public typealias Element = HTMLNode + + private var _currentNode:HTMLNode? + + public mutating func next() -> HTMLNode? { + let (index, result) = _src.renderableAtIndex(_index) + _index += 1 + return result + } +}*/ + +public class HTMLNode : XMLNode, IGlobalContainer { + + public var globalAttributes:Dictionary = [:] + public var globalEvents:Dictionary = [:] + + 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[.., globalEvents:Dictionary, dataAttributes:Dictionary) { + 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 var id: String? { globalAttributes[.id] } + + 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 + } + + public func addTo(array:inout UnsafeMutableBufferPointer, 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 = [:] + 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 = [:] + 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[.. 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[.. String { + if (self == "for") { + return "for_" + } + if (self == "defer") { + return "defer_" + } + if (self == "class") { + return "class_" + } + if (self == "is") { + return "is_" + } + if (self == "as") { + return "as_" + } + if (self == "subscript") { + return "subscript_" + } + if (self == "default") { + return "default_" + } + if (self == "type") { + return "type_" + } + let final = self.replacingOccurrences(of: "-", with: "_") + return final + } +} + +@available(macOS 10.15.4, *) +struct Process: AsyncParsableCommand { + + static let configuration = CommandConfiguration( + abstract: "A utility to generate Swift Bindings for HTML IDs", + version: "0.0.1") + + @Flag(name:[.customShort("r"), .long], help: "Recursively searches for html to create bindings for.") + var recursive: Bool = false + + @Flag(name:[.customShort("f"), .long], help: "When specifying an output dir, do not recreate the folder structure.") + var flatten: Bool = false + + @Option(name:[.customShort("i"), .customLong("input")], help: "Path to file or folder. Defaults to working directory.", completion: .file(), transform: URL.init(fileURLWithPath:)) + var inputFilePath: URL? = nil + + @Option(name:[.customShort("o"), .customLong("output")], help: "Destination to write output. Defaults to same directory as source file.", completion: .file(), transform: URL.init(fileURLWithPath:)) + var outputFilePath: URL? = nil + + @Option(name:[.customShort("b"), .customLong("basePath")], help: "", completion: .file(), transform: URL.init(fileURLWithPath:)) + var baseFilePath: URL? = nil + + var workingDirectory: String = "" + + func inputPath() throws -> URL { + if let inputFilePath = inputFilePath { + return inputFilePath + } + let workingDirUrl = URL(filePath: workingDirectory) + /* TODO: Verify valid path + guard let workingDirUrl = URL(filePath: CLI.workingDirectory) else { + throw AppError("Error: could not convert \(CLI.workingDirectory) to a url.") + }*/ + let parentDirectory = workingDirUrl.deletingLastPathComponent() + return parentDirectory + } + + func outputFileDirectory() throws -> URL { + if let outputFilePath = outputFilePath { + return outputFilePath + } + guard let workingDirUrl = URL(string: workingDirectory) else { + throw AppError("Error: could not convert \(workingDirectory) to a url.") + } + let parentDirectory = workingDirUrl.deletingLastPathComponent() + return parentDirectory + } + + mutating func run() async throws { + self.workingDirectory = await CLI.workingDirectory + let iPath = try inputPath() + let fileType = try iPath.fetchFileType() + let fileList:[URL] + + let iPathDir:URL + + switch (fileType) { + case .directory: + print("Checking directory for html files: \(iPath.absoluteString)") + let fm:FileManager = FileManager.default + fileList = try fm.listFiles(iPath, withLowercaseExtensions: ["htm", "html"]) + if let baseFilePath = baseFilePath { + iPathDir = baseFilePath + } else { + iPathDir = iPath + } + break; + + case .regularFile: + fileList = [iPath] + if let baseFilePath = baseFilePath { + iPathDir = baseFilePath + } else { + iPathDir = iPath.deletingLastPathComponent() + } + break; + + case .other: + throw AppError("path \(iPath.absoluteString) is not a file or directory") + } + for eachUrl in fileList { + do { + print("Generating binding for: \(eachUrl.absoluteString)") + //Read file into html tree + let fileName = eachUrl.lastPathComponent + let ext = eachUrl.pathExtension + let extIndex = fileName.index(fileName.startIndex, offsetBy: fileName.count - (ext.count + 1)) + let withoutExt = String(fileName[fileName.startIndex.. String { + + var indexedElements:[HtmlElement] = [] + var usedIds:Set = Set() + /* It's possible to do everything on the stack. But the generated code is larger and more complex + let _ = rootNode.iterate(0, true) { (eachNode:HTMLNode, i:Int) in + if let nodeId = eachNode.id { + if (usedIds.contains(nodeId)) { return } + let dynamicType = type(of: eachNode) + indexedElements.append(HtmlElement(id: nodeId, index: i, type: "\(dynamicType)")) + usedIds.insert(nodeId) + } + }*/ + + let allNodes = rootNode.flatten() + let rootNode = allNodes.first! + let rootNodeType = "\(type(of: rootNode))" + for (i, eachNode) in allNodes.enumerated() { + if let nodeId = eachNode.id { + if (usedIds.contains(nodeId)) { continue } + let dynamicType = type(of: eachNode) + indexedElements.append(HtmlElement(id: nodeId, index: i, type: "\(dynamicType)")) + usedIds.insert(nodeId) + } + } + var variables:String = "" + var initCode:String = "" + for eachElement in indexedElements { + let swiftSafeId = eachElement.id.swiftSafe() + variables += " public var \(swiftSafeId): \(eachElement.type)\n" + initCode += " \(swiftSafeId) = allNodes[\(eachElement.index)] as! \(eachElement.type)\n" + //initCode += " case \(eachElement.index):\n" + //initCode += " \(swiftSafeId) = eachNode as! \(eachElement.type); break\n" + } + + let className = "\(fileNameNoExt.uppercaseFirstLetter())" + let fileName = "\(className).swift" + let htmlFileName = "\(fileNameNoExt).html" + let relativeFilePath:String + if (relativeDirPath.isEmpty) { + relativeFilePath = " static let relativeFilePath = \"\(htmlFileName)\"" + } else { + relativeFilePath = " static let relativeFilePath = \"\(relativeDirPath)/\(htmlFileName)\"" + } + + let header = Process.genHeader(fileName) + return """ +\(header) + +import HRW + +public class \(className) : IHtmlNodeContainer { + +\(relativeFilePath) + +\(variables) + public let rootNode: \(rootNodeType) + public var rootNodeGeneric: HTMLNode { get { rootNode } } + + public init(rootNode: \(rootNodeType)) throws { + self.rootNode = rootNode + let allNodes = rootNode.flatten() +\(initCode) + } + + public init(_ rootDirectory:String? = nil) throws { + let nodes = try IHtmlNodeContainerUtility.readHtmlFromFile(rootDirectory, \(className).relativeFilePath) + let rootNode = nodes.first as! \(rootNodeType) + self.rootNode = rootNode + + let allNodes = rootNode.flatten() +\(initCode) + } +} +""" + } + + /* + + struct HomePage { + let binding:HomePageBinding + let rootNode:Html + + public init(_ app:Application, isAdmin:Bool) throws { + let nodes = try app.readHtmlFromFile("HomePage.html") + let rootNode = nodes.first as! Html + binding = try HomePageBinding(rootNode: rootNode) + self.rootNode = rootNode + + binding.nav_bar.addChild(try NavBar(app, isAdmin: isAdmin).rootNode) + } + } + + */ + + static func genHeader(_ fileName:String) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MM/dd/yyyy" + let date = dateFormatter.string(from: Date()) + + return """ +// +// \(fileName) +// +// Generated on \(date). +// THIS FILE IS GENERATED. DO NOT EDIT. +// +""" + } +} + +@main +struct CLI { + @MainActor static var workingDirectory:String = "" + static func main() async throws { + if #available(macOS 13, *) { + + if (CommandLine.arguments.count == 0) { + print("Error: Expected at least 1 command line argument (the program directory)") + exit(EXIT_FAILURE) + } + + #if DEBUG + workingDirectory = "/Users/isaacpaul/Downloads/test/dummy.ece"//CommandLine.arguments[0] + #else + workingDirectory = CommandLine.arguments[0] + #endif + + let cmdLinArgs = CommandLine.arguments.dropFirst() + + #if DEBUG + var args:[String] = Array(cmdLinArgs) + args.append(contentsOf: ["-r"]) + #else + #endif + + await Process.main(args) + } else { + print("This only works on mac 13 and newer.") + exit(EXIT_FAILURE) + } + + } +} +/* + let _ = rootNode.interate(0, true) { (eachNode:HTMLNode, i:Int) in + switch (i) { +\(initCode) + default: + return + } + } +public class ExampleBinding { + + public var top: Html + public var theme_style: Style + public var page: Main + public var page_content: Div + + + public init(rootNode: HTMLNode) throws { + var top_: Html + var theme_style_: Style + var page_: Main + var page_content_: Div + let _ = rootNode.iterate(0, true) { (eachNode:HTMLNode, i:Int) in + switch (i) { + case 0: + top_ = eachNode as! Html; break + case 16: + theme_style_ = eachNode as! Style; break + case 58: + page_ = eachNode as! Main; break + case 60: + page_content_ = eachNode as! Div; break + + default: + return + } + } + top = top_ + theme_style = theme_style_ + page = page_ + page_content = page_content_ + } + +} +*/ diff --git a/Sources/BindingGenerator/Extensions/FileManager+Ext.swift b/Sources/BindingGenerator/Extensions/FileManager+Ext.swift new file mode 100644 index 0000000..543a02c --- /dev/null +++ b/Sources/BindingGenerator/Extensions/FileManager+Ext.swift @@ -0,0 +1,33 @@ +// +// FileManager+Ext.swift +// gen_html +// +// Created by Isaac Paul on 4/30/25. +// + +import Foundation + +extension FileManager { + //TODO: should also check if url is file + func listFiles(_ directory:URL, withLowercaseExtensions:[String] = [], recursive:Bool = false) throws -> [URL] { + let options:FileManager.DirectoryEnumerationOptions = recursive ? [] : [.skipsSubdirectoryDescendants] + let fileIterator = try self.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.isDirectoryKey], options: options) + let filesOnly = fileIterator.filter { (url) -> Bool in + do { + let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey]) + let isDirectory = resourceValues.isDirectory ?? true + if (isDirectory) { + return false + } + } catch { return false } + + if withLowercaseExtensions.count > 0 { + let pathExt = url.pathExtension.lowercased() + let contains = withLowercaseExtensions.firstIndex(of: pathExt) + return contains != nil + } + return true + } + return filesOnly + } +} diff --git a/Sources/BindingGenerator/Extensions/String+Ext.swift b/Sources/BindingGenerator/Extensions/String+Ext.swift new file mode 100644 index 0000000..28cca65 --- /dev/null +++ b/Sources/BindingGenerator/Extensions/String+Ext.swift @@ -0,0 +1,43 @@ +// +// String+Ext.swift +// gen_html +// +// Created by Isaac Paul on 9/25/25. +// + +import Foundation + +extension String { + init(readFromUrl:URL) throws { + let inputHandle = try FileHandle(forReadingFrom: readFromUrl) + guard let data = try inputHandle.readToEnd() else { + throw AppError("Empty input.") + } + guard let fileStr = String(data: data, encoding: .utf8) else { + throw AppError("Unable to decode data as utf-8 string") + } + self = fileStr + } + + func uppercaseFirstLetter() -> String { + guard let firstLetter = self.first else { return self } + return firstLetter.uppercased() + self.dropFirst() + } + + func fixPoorCharactersForVariables() -> String { + let firstPass = self.camelCaseBy("-") + let secondPass = firstPass.replacingOccurrences(of: "/", with: "_") + + return secondPass + } + + func camelCaseBy(_ c: Character) -> String { + let components = self.split(separator: c) + guard let first = components.first?.lowercased() else { return "" } + + let rest = components.dropFirst().map { $0.capitalized } + let camelCase = ([first] + rest).joined() + + return camelCase + } +} diff --git a/Sources/BindingGenerator/Extensions/Url+Ext.swift b/Sources/BindingGenerator/Extensions/Url+Ext.swift new file mode 100644 index 0000000..25a60e1 --- /dev/null +++ b/Sources/BindingGenerator/Extensions/Url+Ext.swift @@ -0,0 +1,68 @@ +// +// Url+Ext.swift +// gen_html +// +// Created by Isaac Paul on 9/25/25. +// + +import Foundation + +extension URL { + + enum FileType { + case directory + case regularFile + case other + } + + func fetchFileType() throws -> FileType { + let values = try self.resourceValues(forKeys: [.isDirectoryKey, .isRegularFileKey]) + + if values.isDirectory == true { + return .directory + } + if values.isRegularFile == true { + return .regularFile + } + if self.hasDirectoryPath { + return .directory + } + return .other + } + + func getRelativePathComponents( + to toFolder: URL, // URL A + ) throws -> [String] { + // 1) Get path components of A’s directory + let aDirComponents = toFolder.pathComponents + // 2) Get path components of B + let bComponents = self.pathComponents + + // Ensure B is actually a prefix of A’s directory + guard bComponents.count <= aDirComponents.count, + aDirComponents[0.. URL { + let relativeComponents = try baseFolder.getRelativePathComponents(to: fromFolder) + + // 4) Append them one by one onto C + var result = self + for comp in relativeComponents { + result.appendPathComponent(comp) + } + + return result + } +} diff --git a/Sources/BindingGenerator/Generated/Elements/A.swift b/Sources/BindingGenerator/Generated/Elements/A.swift new file mode 100644 index 0000000..9a654cc --- /dev/null +++ b/Sources/BindingGenerator/Generated/Elements/A.swift @@ -0,0 +1,116 @@ +// +// A.swift +// HTMLStandard +// +// Generated on 09/23/2025. +// THIS FILE IS GENERATED. DO NOT EDIT. +// +import Foundation + +/// Hyperlink +public class A : HTMLNode, IFlow, IInteractive, IPalpable, IPhrasing { + + /// Whether to download the resource instead of navigating to it, and its file name if so. + public var download:String? = nil + + /// Address of the hyperlink. Valid URL potentially surrounded by spaces. + public var href:URL? = nil + + /// Language of the linked resource. Valid BCP 47 language tag. + public var hreflang:String? = nil + + /// URLs to ping. Set of space-separated tokens consisting of valid non-empty URLs. + public var ping:[URL] = [] + + /// Referrer policy for fetches initiated by the element. Referrer policy. + public var referrerpolicy:ReferrerPolicy? = nil + + /// Relationship between the location in the document containing the hyperlink and the destination resource. Unordered set of unique space-separated tokens. The actual rules are more complicated than indicated. + public var rel:[String] = [] + + /// Browsing context for hyperlink navigation. Valid browsing context name or keyword. + public var target:String? = nil + + /// Hint for the type of the referenced resource. Valid MIME type string. + public var type:String? = nil + + + public init(_ attributes:[String:String], _ parser:XMLParser? = nil) throws { + var globalAttr = GlobalAttributesBuilder() + for (key, attValue) in attributes { + switch (key) { + case "download": + download = attValue + continue + case "href": + href = try URL(expect: attValue) + continue + case "hreflang": + hreflang = attValue + continue + case "ping": + ping = try URL.parseList(attValue, " ") + continue + case "referrerpolicy": + referrerpolicy = try ReferrerPolicy(expect: attValue) + continue + case "rel": + rel = try String.parseList(attValue, " ") + continue + case "target": + target = attValue + continue + case "type": + type = attValue + continue + + default: break + } + if globalAttr.trySetGlobalAttribute(key, attValue) { + continue + } + continue + } + var allItems:[HTMLNode] = [] + while let obj = try parser?.readObject(endTag: "a", xmlToHtmlMapper) { + allItems.append(obj) + } + super.init(globalAttr, allItems) + } + + + public override func renderAttributes() -> String { + var result = super.renderAttributes() + if let download = download { + result += " download='\(download)'" + } + if let href = href { + result += " href='\(href.absoluteString)'" + } + if let hreflang = hreflang { + result += " hreflang='\(hreflang)'" + } + result += " ping='\(ping.toStringList(" "))'" + if let referrerpolicy = referrerpolicy { + result += " referrerpolicy='\(referrerpolicy.rawValue)'" + } + result += " rel='\(rel.toStringList(" "))'" + if let target = target { + result += " target='\(target)'" + } + if let type = type { + result += " type='\(type)'" + } + + + return result + } + + override var nodeName: String { + return "a" + } + + override var isVoidElement: Bool { + return false + } +} diff --git a/Sources/BindingGenerator/Generated/Elements/Abbr.swift b/Sources/BindingGenerator/Generated/Elements/Abbr.swift new file mode 100644 index 0000000..bd86b7c --- /dev/null +++ b/Sources/BindingGenerator/Generated/Elements/Abbr.swift @@ -0,0 +1,56 @@ +// +// Abbr.swift +// HTMLStandard +// +// Generated on 09/23/2025. +// THIS FILE IS GENERATED. DO NOT EDIT. +// +import Foundation + +/// Abbreviation +public class Abbr : HTMLNode, IFlow, IPalpable, IPhrasing { + + + /// Full term or expansion of abbreviation. + public var title:String? { + get { return globalAttributes[.title] } + set { globalAttributes[.title] = newValue } + } + + + public init(_ attributes:[String:String], _ parser:XMLParser? = nil) throws { + var globalAttr = GlobalAttributesBuilder() + for (key, attValue) in attributes { + + if globalAttr.trySetGlobalAttribute(key, attValue) { + continue + } + continue + } + var allItems:[HTMLNode] = [] + while let obj = try parser?.readObject(endTag: "abbr", xmlToHtmlMapper) { + allItems.append(obj) + } + super.init(globalAttr, allItems) + } + + + public func addChild(_ someElement:IPhrasing) { + children.append(someElement) + } + + public override func renderAttributes() -> String { + var result = super.renderAttributes() + + + return result + } + + override var nodeName: String { + return "abbr" + } + + override var isVoidElement: Bool { + return false + } +} diff --git a/Sources/BindingGenerator/Generated/Elements/Address.swift b/Sources/BindingGenerator/Generated/Elements/Address.swift new file mode 100644 index 0000000..1f5d6d4 --- /dev/null +++ b/Sources/BindingGenerator/Generated/Elements/Address.swift @@ -0,0 +1,49 @@ +// +// Address.swift +// HTMLStandard +// +// Generated on 09/23/2025. +// THIS FILE IS GENERATED. DO NOT EDIT. +// +import Foundation + +///
Contact information for a page or article element +public class Address : HTMLNode, IFlow, IPalpable { + + + public init(_ attributes:[String:String], _ parser:XMLParser? = nil) throws { + var globalAttr = GlobalAttributesBuilder() + for (key, attValue) in attributes { + + if globalAttr.trySetGlobalAttribute(key, attValue) { + continue + } + continue + } + var allItems:[HTMLNode] = [] + while let obj = try parser?.readObject(endTag: "address", xmlToHtmlMapper) { + allItems.append(obj) + } + super.init(globalAttr, allItems) + } + + + public func addChild(_ someElement:IFlow) { + children.append(someElement) + } + + public override func renderAttributes() -> String { + var result = super.renderAttributes() + + + return result + } + + override var nodeName: String { + return "address" + } + + override var isVoidElement: Bool { + return false + } +} diff --git a/Sources/BindingGenerator/Generated/Elements/Area.swift b/Sources/BindingGenerator/Generated/Elements/Area.swift new file mode 100644 index 0000000..0660a8b --- /dev/null +++ b/Sources/BindingGenerator/Generated/Elements/Area.swift @@ -0,0 +1,161 @@ +// +// Area.swift +// HTMLStandard +// +// Generated on 09/23/2025. +// THIS FILE IS GENERATED. DO NOT EDIT. +// +import Foundation + +/// Hyperlink or dead area on an image map +public class Area : HTMLNode, IFlow, IPhrasing { + + public enum Shape : String, CaseIterable { + + case circle + case default_ = "default" + case poly + case rect + + public init?(rawValue: Substring) { + guard + let value = Self.allCases.first(where: { $0.rawValue == rawValue }) + else + { return nil } + + self = value + } + + public init(expect: Substring) throws { + guard + let value = Self.allCases.first(where: { $0.rawValue == expect }) + else + { throw AppError("Unexpected value for Shape: \(expect)") } + + self = value + } + + public init(expect: String) throws { + guard let result = Shape(rawValue: expect) else { + throw AppError("Unexpected value for Shape: \(expect)") + } + self = result + } + + static func parseList(_ value:String?, _ separator:String = " ") throws -> [Shape] { + guard let value = value else { return [] } + var iterator = value.componentsIterator(separatedBy: separator) + let result = try iterator.map { input in + return try expect(Shape(rawValue: input), "unexpected value for Shape: \(input)") + } + return result + } + } + + /// Replacement text for use when images are not available. Text. The actual rules are more complicated than indicated. + public var alt:String? = nil + + /// Coordinates for the shape to be created in an image map. Valid list of floating-point numbers. The actual rules are more complicated than indicated. + public var coords:[Float] = [] + + /// Whether to download the resource instead of navigating to it, and its file name if so. + public var download:String? = nil + + /// Address of the hyperlink. Valid URL potentially surrounded by spaces. + public var href:URL? = nil + + /// URLs to ping. Set of space-separated tokens consisting of valid non-empty URLs. + public var ping:[URL] = [] + + /// Referrer policy for fetches initiated by the element. Referrer policy. + public var referrerpolicy:ReferrerPolicy? = nil + + /// Relationship between the location in the document containing the hyperlink and the destination resource. Unordered set of unique space-separated tokens. The actual rules are more complicated than indicated. + public var rel:[String] = [] + + /// The kind of shape to be created in an image map. + public var shape:Shape? = nil + + /// Browsing context for hyperlink navigation. Valid browsing context name or keyword. + public var target:String? = nil + + + public init(_ attributes:[String:String], _ parser:XMLParser? = nil) throws { + var globalAttr = GlobalAttributesBuilder() + for (key, attValue) in attributes { + switch (key) { + case "alt": + alt = attValue + continue + case "coords": + coords = try Float.parseList(attValue, ",") + continue + case "download": + download = attValue + continue + case "href": + href = try URL(expect: attValue) + continue + case "ping": + ping = try URL.parseList(attValue, " ") + continue + case "referrerpolicy": + referrerpolicy = try ReferrerPolicy(expect: attValue) + continue + case "rel": + rel = try String.parseList(attValue, " ") + continue + case "shape": + shape = try Shape(expect: attValue) + continue + case "target": + target = attValue + continue + + default: break + } + if globalAttr.trySetGlobalAttribute(key, attValue) { + continue + } + continue + } + super.init(globalAttr) + } + + + public override func renderAttributes() -> String { + var result = super.renderAttributes() + if let alt = alt { + result += " alt='\(alt)'" + } + result += " coords='\(coords.toStringList(","))'" + if let download = download { + result += " download='\(download)'" + } + if let href = href { + result += " href='\(href.absoluteString)'" + } + result += " ping='\(ping.toStringList(" "))'" + if let referrerpolicy = referrerpolicy { + result += " referrerpolicy='\(referrerpolicy.rawValue)'" + } + result += " rel='\(rel.toStringList(" "))'" + if let shape = shape { + result += " shape='\(shape.rawValue)'" + } + if let target = target { + result += " target='\(target)'" + } + + + return result + } + + override var nodeName: String { + return "area" + } + + override var isVoidElement: Bool { + return true + } +} diff --git a/Sources/BindingGenerator/Generated/Elements/Article.swift b/Sources/BindingGenerator/Generated/Elements/Article.swift new file mode 100644 index 0000000..0c0c4ed --- /dev/null +++ b/Sources/BindingGenerator/Generated/Elements/Article.swift @@ -0,0 +1,49 @@ +// +// Article.swift +// HTMLStandard +// +// Generated on 09/23/2025. +// THIS FILE IS GENERATED. DO NOT EDIT. +// +import Foundation + +///
Self-contained syndicatable or reusable composition +public class Article : HTMLNode, IFlow, IPalpable, ISectioning { + + + public init(_ attributes:[String:String], _ parser:XMLParser? = nil) throws { + var globalAttr = GlobalAttributesBuilder() + for (key, attValue) in attributes { + + if globalAttr.trySetGlobalAttribute(key, attValue) { + continue + } + continue + } + var allItems:[HTMLNode] = [] + while let obj = try parser?.readObject(endTag: "article", xmlToHtmlMapper) { + allItems.append(obj) + } + super.init(globalAttr, allItems) + } + + + public func addChild(_ someElement:IFlow) { + children.append(someElement) + } + + public override func renderAttributes() -> String { + var result = super.renderAttributes() + + + return result + } + + override var nodeName: String { + return "article" + } + + override var isVoidElement: Bool { + return false + } +} diff --git a/Sources/BindingGenerator/Generated/Elements/Aside.swift b/Sources/BindingGenerator/Generated/Elements/Aside.swift new file mode 100644 index 0000000..ea1fafe --- /dev/null +++ b/Sources/BindingGenerator/Generated/Elements/Aside.swift @@ -0,0 +1,49 @@ +// +// Aside.swift +// HTMLStandard +// +// Generated on 09/23/2025. +// THIS FILE IS GENERATED. DO NOT EDIT. +// +import Foundation + +///