388 lines
14 KiB
Swift
388 lines
14 KiB
Swift
//#!/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..<extIndex])
|
|
let srcParent = eachUrl.deletingLastPathComponent()
|
|
let relativePath = try iPathDir.getRelativePathComponents(to: srcParent)
|
|
let relativePathStr = relativePath.toStringList("/") //TODO: Seperator might be different on windows
|
|
|
|
let fileStr = try String.init(readFromUrl: eachUrl)
|
|
guard let xmlReader = XMLParser(str: fileStr) else { throw EmptyStringError() }
|
|
let rootNodes:[HTMLNode] = try xmlReader.readObjects()
|
|
var somePrimaryNode:HTMLNode? = nil
|
|
if (rootNodes.count == 1) {
|
|
somePrimaryNode = rootNodes.first
|
|
} else {
|
|
for eachNode in rootNodes {
|
|
if let targetNode = eachNode as? Html {
|
|
somePrimaryNode = targetNode
|
|
break
|
|
}
|
|
}
|
|
if somePrimaryNode == nil {
|
|
somePrimaryNode = rootNodes.first
|
|
}
|
|
}
|
|
guard let htmlNode = somePrimaryNode else { throw AppError("Did not find html root node")}
|
|
|
|
print("Parsed into node tree")
|
|
let outputStr = generateFileWithHtmlNode(htmlNode, fileNameNoExt: withoutExt, relativeDirPath: relativePathStr)
|
|
print("Generated Binding")
|
|
|
|
//Figure out output directory and filename
|
|
//write data to file
|
|
let targetFileName = "\(withoutExt.uppercaseFirstLetter()).swift"
|
|
let userOutputDir:URL = try outputFileDirectory()
|
|
let targetOutputDir:URL
|
|
if (flatten) {
|
|
targetOutputDir = userOutputDir
|
|
} else {
|
|
targetOutputDir = try userOutputDir.appendRelativePath(from: srcParent, base: iPathDir)
|
|
}
|
|
let targetPath = targetOutputDir.appending(path: targetFileName)
|
|
let fm = FileManager.default
|
|
if (fm.fileExists(atPath: targetOutputDir.path()) == false) {
|
|
print("Creating Directory: \(targetOutputDir.path())")
|
|
try fm.createDirectory(atPath: targetOutputDir.path(), withIntermediateDirectories: true)
|
|
}
|
|
print("Writing binding to file: \(targetPath.path)")
|
|
try outputStr.write(toFile: targetPath.path, atomically: true, encoding: .utf8)
|
|
} catch {
|
|
print("\(eachUrl.path()):0:0: error: \(error.localizedDescription)", to: &standardError)
|
|
throw AppError("\(eachUrl.path()):0:0: error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
/*
|
|
let outElementsDir = outputDir.appending(path: "Elements")
|
|
let fm = FileManager.default
|
|
if (fm.fileExists(atPath: outElementsDir.path()) == false) {
|
|
print("Creating Directory: \(outElementsDir.path())")
|
|
try fm.createDirectory(atPath: outElementsDir.path(), withIntermediateDirectories: true)
|
|
try fileBody.write(toFile: strOutput, atomically: true, encoding: .utf8)
|
|
}*/
|
|
|
|
}
|
|
|
|
func generateFileWithHtmlNode(_ rootNode:HTMLNode, fileNameNoExt:String, relativeDirPath:String) -> String {
|
|
|
|
var indexedElements:[HtmlElement] = []
|
|
var usedIds:Set<String> = 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_
|
|
}
|
|
|
|
}
|
|
*/
|