//#!/usr/env/swift import Foundation import ArgumentParser struct HtmlElement { let id:String let index:Int let type:String } struct StandardError: TextOutputStream { mutating func write(_ string: String) { for byte in string.utf8 { putc(numericCast(byte), stderr) } } } nonisolated(unsafe) var standardError: StandardError = StandardError() extension String { public func swiftSafe() -> 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_ } } */