Files
HtmlRW/Sources/BindingGenerator/CLI.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_
}
}
*/