diff --git a/Package.swift b/Package.swift index 365be613..2350e9d4 100644 --- a/Package.swift +++ b/Package.swift @@ -26,13 +26,43 @@ let package = Package( .product(name: "Collections", package: "swift-collections") ] ), + .target( + name: "Converter", + dependencies: [ + .target(name: "HTMLKit") + ] + ), .testTarget( name: "HTMLKitTests", - dependencies: ["HTMLKit"], + dependencies: [ + .target(name: "HTMLKit") + ], + resources: [ + .process("Localization") + ] + ), + .testTarget( + name: "ConverterTests", + dependencies: [ + .target(name: "Converter") + ], resources: [ - .process("Localization"), .process("Conversion") ] + ), + .executableTarget( + name: "ConvertCommand", + dependencies: [ + .target(name: "Converter") + ], + path: "Sources/Commands" + ), + .plugin( + name: "ConverterPlugin", + capability: .command(intent: .custom(verb: "convert", description: "Convert html content"), permissions: [.writeToPackageDirectory(reason: "The command needs the permission to create the converted file.")]), + dependencies: [ + .target(name: "ConvertCommand") + ] ) ] ) diff --git a/Plugins/ConverterPlugin/plugin.swift b/Plugins/ConverterPlugin/plugin.swift new file mode 100644 index 00000000..6472d7c2 --- /dev/null +++ b/Plugins/ConverterPlugin/plugin.swift @@ -0,0 +1,74 @@ +import PackagePlugin +import Foundation + +@main +struct ConverterPlugin: CommandPlugin { + + func performCommand(context: PluginContext, arguments: [String]) async throws { + + let outputOptions = ["debug", "file"] + + let tool = try context.tool(named: "ConvertCommand") + + var extractor = ArgumentExtractor(arguments) + + let usageArgument = extractor.extractFlag(named: "command-usage") + + if usageArgument > 0 { + + let explanation = """ + USAGE: convert --output-option <option> --source-path <path> --target-path <path> + + ARGUMENTS: + <output option> - file or debug + <source path> - The path, where the html files are located. + <target path> - The path, where the converted files should be saved into. + """ + + print(explanation) + + } else { + + let outputArgument = extractor.extractOption(named: "output-option") + let sourceArgument = extractor.extractOption(named: "source-path") + let targetArgument = extractor.extractOption(named: "target-path") + + var processArguments = [String]() + + if let output = outputArgument.first { + + if !outputOptions.contains(output) { + Diagnostics.error("Invalid output option. Choose 'file' or 'debug' instead.") + } + + processArguments.insert(output, at: 0) + + } else { + Diagnostics.error("Missing argument --output-option.") + } + + if let source = sourceArgument.first { + processArguments.insert(source, at: 1) + + } else { + Diagnostics.error("Missing argument --source-path.") + } + + if let target = targetArgument.first { + processArguments.insert(target, at: 2) + } + + print("The conversion starts...") + + let process = try Process.run(URL(fileURLWithPath: tool.path.string), arguments: processArguments) + process.waitUntilExit() + + if process.terminationReason == .exit && process.terminationStatus == 0 { + print("The conversion has finished.") + + } else { + Diagnostics.error("The conversion has failed: \(process.terminationReason)") + } + } + } +} diff --git a/Sources/Commands/Converter/ConvertCommand.swift b/Sources/Commands/Converter/ConvertCommand.swift new file mode 100644 index 00000000..eb8b0efa --- /dev/null +++ b/Sources/Commands/Converter/ConvertCommand.swift @@ -0,0 +1,69 @@ +import Foundation +import Converter + +@main +@available(macOS 11.0, *) +internal struct ConvertCommand { + + private static var outputOption: String { + return CommandLine.arguments[1] + } + + private static var sourcePath: String { + return CommandLine.arguments[2] + } + + private static var targetPath: String? { + + if CommandLine.arguments.count < 4 { + return nil + } + + return CommandLine.arguments[3] + } + + internal static func main() throws { + + if !FileManager.default.fileExists(atPath: sourcePath) { + print("No valid source path.") + + exit(1) + + } else { + + let url = URL(fileURLWithPath: sourcePath) + + switch outputOption { + case "file": + + if let targetPath = self.targetPath { + try Converter.default.convert(source: url, target: URL(fileURLWithPath: targetPath)) + + exit(0) + + } else { + print("Unkown target path.") + + exit(1) + } + + case "debug": + + if targetPath != nil { + print("Wrong output option.") + + exit(1) + + } else { + try Converter.default.convert(source: url) + + exit(0) + } + + default: + break + + } + } + } +} diff --git a/Sources/Converter/Converter.swift b/Sources/Converter/Converter.swift new file mode 100644 index 00000000..632b7b04 --- /dev/null +++ b/Sources/Converter/Converter.swift @@ -0,0 +1,117 @@ +import Foundation + +#if canImport(FoundationXML) + import FoundationXML +#endif + +@available(macOS 11.0, *) +public class Converter { + + public enum ConverterError: Error { + case rootNotFound + case emptyFile + } + + public static let `default` = Converter() + + private init() {} + + private func convert(content: String) throws -> String { + + let document = try XMLDocument(xmlString: content, options: [.documentIncludeContentTypeDeclaration]) + + guard let root = document.rootElement() else { + throw ConverterError.rootNotFound + } + + return try Parser.shared.parse(node: root) + } + + public func convert(source path: URL) throws { + + if !path.hasDirectoryPath { + + let content = try String(contentsOf: path) + + if content.count > 1 { + print(try convert(content: content)) + + } else { + throw ConverterError.emptyFile + } + + } else { + + if let enumerator = FileManager.default.enumerator(at: path, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { + + for case let path as URL in enumerator { + + if !path.hasDirectoryPath { + + if !path.isFileURL { + enumerator.skipDescendants() + + } else { + + let content = try String(contentsOf: path) + + if content.count > 1 { + print(try convert(content: content)) + + } else { + throw ConverterError.emptyFile + } + } + } + } + } + } + } + + public func convert(source path: URL, target: URL) throws { + + if !path.hasDirectoryPath { + + let content = try String(contentsOf: path) + + if content.count > 1 { + + let result = try convert(content: content) + + try result.write(to: target, atomically: true, encoding: .utf8) + + } else { + throw ConverterError.emptyFile + } + + } else { + + if let enumerator = FileManager.default.enumerator(at: path, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { + + for case let path as URL in enumerator { + + if !path.hasDirectoryPath { + + if !path.isFileURL { + enumerator.skipDescendants() + + } else { + + let content = try String(contentsOf: path) + + if content.count > 1 { + + let result = try convert(content: content) + + try result.write(to: target, atomically: true, encoding: .utf8) + + } else { + throw ConverterError.emptyFile + } + } + } + } + } + } + } +} diff --git a/Sources/Converter/InitRepresentable.swift b/Sources/Converter/InitRepresentable.swift new file mode 100644 index 00000000..45b01311 --- /dev/null +++ b/Sources/Converter/InitRepresentable.swift @@ -0,0 +1,39 @@ +public protocol InitRepresentable { + + init?(value: String) +} + +extension String: InitRepresentable { + + public init?(value: String) { + self.init(value) + } +} + +extension Float: InitRepresentable { + + public init?(value: String) { + self.init(value) + } +} + +extension Int: InitRepresentable { + + public init?(value: String) { + self.init(value) + } +} + +extension Double: InitRepresentable { + + public init?(value: String) { + self.init(value) + } +} + +extension Bool: InitRepresentable { + + public init?(value: String) { + self.init(value) + } +} diff --git a/Sources/Converter/Parser.swift b/Sources/Converter/Parser.swift new file mode 100644 index 00000000..21a81b95 --- /dev/null +++ b/Sources/Converter/Parser.swift @@ -0,0 +1,1772 @@ +import HTMLKit +import Foundation + +#if canImport(FoundationXML) +import FoundationXML +#endif + +internal class Parser { + + internal enum ParserError: Error { + + case noLocalName + case unkownElement(String) + case unkownAttribute(String) + case unknownTag(String) + + var description: String { + + switch self { + case .noLocalName: + return "No local name." + + case .unkownElement(let element): + return "Element '\(element) not found." + + case .unkownAttribute(let attribute): + return "Attribute '\(attribute) not found." + + case .unknownTag(let tag): + return "Tag '\(tag)' not found." + } + } + } + + internal static let shared = Parser() + + private init() {} + + internal func parse(node: XMLNode, indent: Int? = nil) throws -> String { + + switch node.kind { + case .text: + return TextElement(node: node).build() + + case .comment: + return CommentElement(node: node).build() + + case .element: + + if let element = node as? XMLElement { + + guard let localName = element.localName else { + throw ParserError.noLocalName + } + + switch localName { + case "html": + return try ContentElement(element: element).build() + + case "head": + return try ContentElement(element: element).build(verbatim: "Head") + + case "body": + return try ContentElement(element: element).build() + + case "nav": + return try ContentElement(element: element).build(verbatim: "Navigation") + + case "link": + return try EmptyElement(element: element).build() + + case "aside": + return try ContentElement(element: element).build() + + case "section": + return try ContentElement(element: element).build() + + case "h1": + return try ContentElement(element: element).build(verbatim: "Heading1") + + case "h2": + return try ContentElement(element: element).build(verbatim: "Heading2") + + case "h3": + return try ContentElement(element: element).build(verbatim: "Heading3") + + case "h4": + return try ContentElement(element: element).build(verbatim: "Heading4") + + case "h5": + return try ContentElement(element: element).build(verbatim: "Heading5") + + case "h6": + return try ContentElement(element: element).build(verbatim: "Heading6") + + case "hgroup": + return try ContentElement(element: element).build(verbatim: "HeadingGroup") + + case "header": + return try ContentElement(element: element).build() + + case "footer": + return try ContentElement(element: element).build() + + case "address": + return try ContentElement(element: element).build() + + case "p": + return try ContentElement(element: element).build(verbatim: "Paragraph") + + case "hr": + return try EmptyElement(element: element).build(verbatim: "HorizontalRule") + + case "pre": + return try ContentElement(element: element).build(verbatim: "PreformattedText") + + case "blockquote": + return try ContentElement(element: element).build() + + case "ol": + return try ContentElement(element: element).build(verbatim: "OrderedList") + + case "ul": + return try ContentElement(element: element).build(verbatim: "UnorderedList") + + case "dl": + return try ContentElement(element: element).build(verbatim: "DescriptionList") + + case "figure": + return try ContentElement(element: element).build() + + case "a": + return try ContentElement(element: element).build(verbatim: "Anchor") + + case "em": + return try ContentElement(element: element).build(verbatim: "Emphasize") + + case "small": + return try ContentElement(element: element).build() + + case "s": + return try ContentElement(element: element).build(verbatim: "StrikeThrough") + + case "main": + return try ContentElement(element: element).build() + + case "div": + return try ContentElement(element: element).build(verbatim: "Division") + + case "dfn": + return try ContentElement(element: element).build(verbatim: "Definition") + + case "cite": + return try ContentElement(element: element).build() + + case "q": + return try ContentElement(element: element).build(verbatim: "ShortQuote") + + case "rt": + return try ContentElement(element: element).build(verbatim: "RubyText") + + case "rp": + return try ContentElement(element: element).build(verbatim: "RubyPronunciation") + + case "abbr": + return try ContentElement(element: element).build(verbatim: "Abbreviation") + + case "data": + return try ContentElement(element: element).build() + + case "time": + return try ContentElement(element: element).build() + + case "code": + return try ContentElement(element: element).build() + + case "v": + return try ContentElement(element: element).build(verbatim: "Variable") + + case "samp": + return try ContentElement(element: element).build(verbatim: "SampleOutput") + + case "kbd": + return try ContentElement(element: element).build(verbatim: "KeyboardOutput") + + case "sub": + return try ContentElement(element: element).build(verbatim: "Subscript") + + case "sup": + return try ContentElement(element: element).build(verbatim: "Superscript") + + case "i": + return try ContentElement(element: element).build(verbatim: "Italic") + + case "b": + return try ContentElement(element: element).build(verbatim: "Bold") + + case "strong": + return try ContentElement(element: element).build(verbatim: "Strong") + + case "u": + return try ContentElement(element: element).build(verbatim: "SampleOutput") + + case "mark": + return try ContentElement(element: element).build() + + case "bdi": + return try ContentElement(element: element).build() + + case "bdo": + return try EmptyElement(element: element).build() + + case "span": + return try ContentElement(element: element).build() + + case "br": + return try EmptyElement(element: element).build(verbatim: "LineBreak") + + case "wbr": + return try EmptyElement(element: element).build(verbatim: "WordBreak") + + case "ins": + return try ContentElement(element: element).build(verbatim: "InsertedText") + + case "del": + return try ContentElement(element: element).build(verbatim: "DeletedText") + + case "img": + return try EmptyElement(element: element).build(verbatim: "Image") + + case "embed": + return try ContentElement(element: element).build() + + case "iframe": + return try ContentElement(element: element).build(verbatim: "InlineFrame") + + case "param": + return try EmptyElement(element: element).build(verbatim: "Parameter") + + case "dt": + return try ContentElement(element: element).build(verbatim: "TermName") + + case "dd": + return try ContentElement(element: element).build(verbatim: "TermDefinition") + + case "figcaption": + return try ContentElement(element: element).build(verbatim: "FigureCaption") + + case "optgroup": + return try ContentElement(element: element).build(verbatim: "OptionGroup") + + case "option": + return try ContentElement(element: element).build() + + case "legend": + return try ContentElement(element: element).build() + + case "summary": + return try ContentElement(element: element).build() + + case "li": + return try ContentElement(element: element).build(verbatim: "ListItem") + + case "colgroup": + return try ContentElement(element: element).build(verbatim: "ColumnGroup") + + case "col": + return try ContentElement(element: element).build(verbatim: "Column") + + case "tbody": + return try ContentElement(element: element).build(verbatim: "TableBody") + + case "thead": + return try ContentElement(element: element).build(verbatim: "TableHead") + + case "tfoot": + return try ContentElement(element: element).build(verbatim: "TableFoot") + + case "tr": + return try ContentElement(element: element).build(verbatim: "TableRow") + + case "td": + return try ContentElement(element: element).build(verbatim: "DataCell") + + case "th": + return try ContentElement(element: element).build(verbatim: "HeaderCell") + + case "textarea": + return try ContentElement(element: element).build(verbatim: "TextArea") + + case "input": + return try EmptyElement(element: element).build() + + case "video": + return try ContentElement(element: element).build() + + case "audio": + return try ContentElement(element: element).build() + + case "map": + return try ContentElement(element: element).build() + + case "area": + return try ContentElement(element: element).build() + + case "form": + return try ContentElement(element: element).build() + + case "datalist": + return try ContentElement(element: element).build() + + case "output": + return try ContentElement(element: element).build() + + case "meter": + return try ContentElement(element: element).build() + + case "details": + return try ContentElement(element: element).build() + + case "dialog": + return try ContentElement(element: element).build() + + case "script": + return try ContentElement(element: element).build() + + case "noscript": + return try ContentElement(element: element).build() + + case "template": + return try ContentElement(element: element).build() + + case "canvas": + return try ContentElement(element: element).build() + + case "table": + return try ContentElement(element: element).build() + + case "fieldset": + return try ContentElement(element: element).build() + + case "button": + return try ContentElement(element: element).build() + + case "select": + return try ContentElement(element: element).build() + + case "label": + return try ContentElement(element: element).build() + + case "title": + return try ContentElement(element: element).build() + + case "base": + return try EmptyElement(element: element).build() + + case "meta": + return try EmptyElement(element: element).build() + + case "style": + return try ContentElement(element: element).build() + + case "source": + return try EmptyElement(element: element).build() + + case "track": + return try EmptyElement(element: element).build() + + case "article": + return try ContentElement(element: element).build() + + case "progress": + return try ContentElement(element: element).build() + + case "circle": + return try ContentElement(element: element).build() + + case "rect": + return try ContentElement(element: element).build(verbatim: "Rectangle") + + case "ellipse": + return try ContentElement(element: element).build() + + case "line": + return try ContentElement(element: element).build() + + case "polygon": + return try ContentElement(element: element).build() + + case "path": + return try ContentElement(element: element).build() + + case "use": + return try ContentElement(element: element).build() + + case "g": + return try ContentElement(element: element).build(verbatim: "Group") + + default: + throw ParserError.unkownElement(localName) + } + } + + case .attribute: + + guard let localName = node.localName else { + throw ParserError.noLocalName + } + + switch localName { + case "accesskey": + return try ValueAttribute<String>(node: node).build() + + case "accept": + return try ValueAttribute<String>(node: node).build() + + case "action": + return try ValueAttribute<String>(node: node).build() + + case "alt": + return try ValueAttribute<String>(node: node).build(verbatim: "alternate") + + case "async": + return try EmptyAttribute(node: node).build(verbatim: "asynchronously") + + case "autocapitalize": + return try TypeAttribute<Capitalization>(node: node).build() + + case "autocomplete": + return try ValueAttribute<Bool>(node: node).build(verbatim: "hasCompletion") + + case "autofocus": + return try EmptyAttribute(node: node).build() + + case "autoplay": + return try EmptyAttribute(node: node).build() + + case "checked": + return try EmptyAttribute(node: node).build() + + case "cite": + return try ValueAttribute<String>(node: node).build() + + case "class": + return try ValueAttribute<String>(node: node).build() + + case "cols": + return try ValueAttribute<Int>(node: node).build(verbatim: "columns") + + case "colspan": + return try ValueAttribute<Int>(node: node).build(verbatim: "columnSpan") + + case "content": + return try ValueAttribute<String>(node: node).build() + + case "contenteditable": + return try ValueAttribute<Bool>(node: node).build(verbatim: "isEditable") + + case "controls": + return try EmptyAttribute(node: node).build() + + case "coords": + return try ValueAttribute<String>(node: node).build(verbatim: "coordinates") + + case "data": + return try ValueAttribute<String>(node: node).build() + + case "datetime": + return try ValueAttribute<String>(node: node).build(verbatim: "dateTime") + + case "default": + return try EmptyAttribute(node: node).build() + + case "defer": + return try EmptyAttribute(node: node).build() + + case "dir": + return try TypeAttribute<Direction>(node: node).build(verbatim: "direction") + + case "disabled": + return try EmptyAttribute(node: node).build() + + case "download": + return try EmptyAttribute(node: node).build() + + case "draggable": + return try ValueAttribute<String>(node: node).build(verbatim: "isDraggable") + + case "enctype": + return try TypeAttribute<Encoding>(node: node).build(verbatim: "encoding") + + case "enterkeyhint": + return try TypeAttribute<Hint>(node: node).build(verbatim: "enterKeyHint") + + case "for": + return try ValueAttribute<String>(node: node).build() + + case "form": + return try ValueAttribute<String>(node: node).build() + + case "formaction": + return try ValueAttribute<String>(node: node).build(verbatim: "formAction") + + case "headers": + return try ValueAttribute<String>(node: node).build() + + case "height": + return try ValueAttribute<Int>(node: node).build() + + case "hidden": + return try EmptyAttribute(node: node).build() + + case "high": + return try ValueAttribute<Float>(node: node).build() + + case "href": + return try ValueAttribute<String>(node: node).build(verbatim: "reference") + + case "hreflang": + return try TypeAttribute<Language>(node: node).build(verbatim: "referenceLanguage") + + case "id": + return try ValueAttribute<String>(node: node).build() + + case "ismap": + return try EmptyAttribute(node: node).build(verbatim: "isMap") + + case "inputmode": + return try ValueAttribute<String>(node: node).build(verbatim: "inputMode") + + case "is": + return try ValueAttribute<String>(node: node).build() + + case "itemid": + return try ValueAttribute<String>(node: node).build(verbatim: "itemId") + + case "itemproperty": + return try ValueAttribute<String>(node: node).build(verbatim: "itemProperty") + + case "itemref": + return try ValueAttribute<String>(node: node).build(verbatim: "itemReference") + + case "itemscope": + return try ValueAttribute<String>(node: node).build(verbatim: "itemScope") + + case "itemtype": + return try ValueAttribute<String>(node: node).build(verbatim: "itemType") + + case "kind": + return try TypeAttribute<Kinds>(node: node).build() + + case "label": + return try ValueAttribute<String>(node: node).build() + + case "lang": + return try TypeAttribute<Language>(node: node).build(verbatim: "language") + + case "list": + return try ValueAttribute<String>(node: node).build() + + case "loop": + return try EmptyAttribute(node: node).build() + + case "low": + return try ValueAttribute<Float>(node: node).build() + + case "max": + + if let parent = node.parent { + + switch parent.localName { + case "progress", "meter": + return try ValueAttribute<Float>(node: node).build(verbatim: "maximum") + + default: + return try ValueAttribute<String>(node: node).build(verbatim: "maximum") + } + } + + case "media": + return try ValueAttribute<String>(node: node).build() + + case "method": + return try TypeAttribute<HTMLKit.Method>(node: node).build() + + case "min": + return try ValueAttribute<String>(node: node).build(verbatim: "minimum") + + case "multiple": + return try EmptyAttribute(node: node).build() + + case "muted": + return try EmptyAttribute(node: node).build() + + case "name": + + if let parent = node.parent { + + switch parent.localName { + case "meta": + return try TypeAttribute<Names>(node: node).build() + + default: + return try ValueAttribute<String>(node: node).build() + } + } + + case "nonce": + return try ValueAttribute<String>(node: node).build() + + case "novalidate": + return try EmptyAttribute(node: node).build() + + case "open": + return try ValueAttribute<Bool>(node: node).build(verbatim: "isOpen") + + case "optimum": + return try ValueAttribute<Float>(node: node).build() + + case "pattern": + return try ValueAttribute<String>(node: node).build() + + case "part": + return try ValueAttribute<String>(node: node).build() + + case "ping": + return try ValueAttribute<String>(node: node).build() + + case "placeholder": + return try ValueAttribute<String>(node: node).build() + + case "poster": + return try ValueAttribute<String>(node: node).build() + + case "preload": + return try TypeAttribute<Preload>(node: node).build() + + case "readonly": + return try EmptyAttribute(node: node).build() + + case "referrerpolicy": + return try TypeAttribute<Policy>(node: node).build(verbatim: "referrerPolicy") + + case "rel": + return try TypeAttribute<Relation>(node: node).build(verbatim: "relationship") + + case "required": + return try EmptyAttribute(node: node).build() + + case "reversed": + return try EmptyAttribute(node: node).build() + + case "role": + return try TypeAttribute<Roles>(node: node).build() + + case "rows": + return try ValueAttribute<String>(node: node).build() + + case "rowspan": + return try ValueAttribute<Int>(node: node).build(verbatim: "rowSpan") + + case "sandbox": + return try EmptyAttribute(node: node).build() + + case "scope": + return try ValueAttribute<String>(node: node).build() + + case "shape": + return try TypeAttribute<Shape>(node: node).build() + + case "size": + return try ValueAttribute<String>(node: node).build() + + case "sizes": + return try ValueAttribute<Int>(node: node).build() + + case "slot": + return try ValueAttribute<String>(node: node).build() + + case "span": + return try ValueAttribute<Int>(node: node).build() + + case "spellcheck": + return try ValueAttribute<Bool>(node: node).build(verbatim: "hasSpellCheck") + + case "src": + return try ValueAttribute<String>(node: node).build(verbatim: "source") + + case "start": + return try ValueAttribute<Int>(node: node).build() + + case "step": + return try ValueAttribute<Int>(node: node).build() + + case "style": + return try ValueAttribute<String>(node: node).build() + + case "tabindex": + return try ValueAttribute<Int>(node: node).build(verbatim: "tabIndex") + + case "target": + return try TypeAttribute<Target>(node: node).build() + + case "title": + return try ValueAttribute<String>(node: node).build() + + case "translate": + return try TypeAttribute<Decision>(node: node).build() + + case "type": + + if let parent = node.parent { + + switch parent.localName { + + case "input": + return try TypeAttribute<Inputs>(node: node).build() + + case "button": + return try TypeAttribute<Buttons>(node: node).build() + + case "link", "script", "audio": + return try TypeAttribute<Medias>(node: node).build() + + default: + return try ValueAttribute<String>(node: node).build() + } + } + + case "value": + return try ValueAttribute<String>(node: node).build() + + case "width": + return try ValueAttribute<Int>(node: node).build() + + case "wrap": + return try TypeAttribute<Wrapping>(node: node).build() + + case "property": + return try TypeAttribute<Graphs>(node: node).build() + + case "charset": + return try TypeAttribute<Charset>(node: node).build() + + case "http-equiv": + return try TypeAttribute<Equivalent>(node: node).build() + + case "selected": + return try EmptyAttribute(node: node).build() + + case "maxlength": + return try ValueAttribute<String>(node: node).build(verbatim: "maximum") + + case "minlength": + return try ValueAttribute<String>(node: node).build(verbatim: "minimum") + + case "d": + return try ValueAttribute<String>(node: node).build(verbatim: "draw") + + case "fill": + return try ValueAttribute<String>(node: node).build() + + case "fill-opacity": + return try ValueAttribute<Double>(node: node).build(verbatim: "fillOpacity") + + case "stroke": + return try ValueAttribute<String>(node: node).build() + + case "stroke-width": + return try ValueAttribute<Int>(node: node).build(verbatim: "strokeWidth") + + case "stroke-opacity": + return try ValueAttribute<Double>(node: node).build(verbatim: "strokeOpacity") + + case "stroke-linecap": + return try TypeAttribute<Linecap>(node: node).build(verbatim: "strokeLineCap") + + case "stroke-linejoin": + return try TypeAttribute<Linejoin>(node: node).build(verbatim: "strokeLineJoin") + + case "r": + return try ValueAttribute<Int>(node: node).build(verbatim: "radius") + + case "viewbox": + return try ValueAttribute<String>(node: node).build(verbatim: "viewBox") + + case "onafterprint": + return try EventAttribute<Events.Window>(node: node).build() + + case "onbeforeprint": + return try EventAttribute<Events.Window>(node: node).build() + + case "onbeforeunload": + return try EventAttribute<Events.Window>(node: node).build() + + case "onhashchange": + return try EventAttribute<Events.Window>(node: node).build() + + case "onlanguagechange": + return try EventAttribute<Events.Window>(node: node).build() + + case "onmessage": + return try EventAttribute<Events.Window>(node: node).build() + + case "onmessageerror": + return try EventAttribute<Events.Window>(node: node).build() + + case "onoffline": + return try EventAttribute<Events.Window>(node: node).build() + + case "ononline": + return try EventAttribute<Events.Window>(node: node).build() + + case "onpagehide": + return try EventAttribute<Events.Window>(node: node).build() + + case "onpageshow": + return try EventAttribute<Events.Window>(node: node).build() + + case "onpopstate": + return try EventAttribute<Events.Window>(node: node).build() + + case "onrejectionhandled": + return try EventAttribute<Events.Window>(node: node).build() + + case "onstorage": + return try EventAttribute<Events.Window>(node: node).build() + + case "onunhandledrejection": + return try EventAttribute<Events.Window>(node: node).build() + + case "onunload": + return try EventAttribute<Events.Window>(node: node).build() + + case "onerror": + return try EventAttribute<Events.Window>(node: node).build() + + case "onblur": + return try EventAttribute<Events.Focus>(node: node).build() + + case "onfocus": + return try EventAttribute<Events.Focus>(node: node).build() + + case "onpointercancel": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onpointerdown": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onpointerenter": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onpointerleave": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onpointermove": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onpointerout": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onpointerover": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onpointerup": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onlostpointercapture": + return try EventAttribute<Events.Pointer>(node: node).build() + + case "onclick": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "oncontextmenu": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "ondblclick": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onmousedown": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onmouseenter": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onmouseleave": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onmousemove": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onmouseout": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onmouseover": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onmouseup": + return try EventAttribute<Events.Mouse>(node: node).build() + + case "onwheel": + return try EventAttribute<Events.Wheel>(node: node).build() + + case "onbeforeinput": + return try EventAttribute<Events.Input>(node: node).build() + + case "oninput": + return try EventAttribute<Events.Input>(node: node).build() + + case "onselect": + return try EventAttribute<Events.Input>(node: node).build() + + case "onkeydown": + return try EventAttribute<Events.Keyboard>(node: node).build() + + case "onkeyup": + return try EventAttribute<Events.Keyboard>(node: node).build() + + case "ondrag": + return try EventAttribute<Events.Drag>(node: node).build() + + case "ondragend": + return try EventAttribute<Events.Drag>(node: node).build() + + case "ondragenter": + return try EventAttribute<Events.Drag>(node: node).build() + + case "ondragleave": + return try EventAttribute<Events.Drag>(node: node).build() + + case "ondragover": + return try EventAttribute<Events.Drag>(node: node).build() + + case "ondragstart": + return try EventAttribute<Events.Drag>(node: node).build() + + case "ondrop": + return try EventAttribute<Events.Drag>(node: node).build() + + case "oncopy": + return try EventAttribute<Events.Clipboard>(node: node).build() + + case "oncut": + return try EventAttribute<Events.Clipboard>(node: node).build() + + case "onpaste": + return try EventAttribute<Events.Clipboard>(node: node).build() + + case "onselectionchange": + return try EventAttribute<Events.Selection>(node: node).build() + + case "onselectstart": + return try EventAttribute<Events.Selection>(node: node).build() + + case "onabort": + return try EventAttribute<Events.Media>(node: node).build() + + case "oncanplay": + return try EventAttribute<Events.Media>(node: node).build() + + case "oncanplaythrough": + return try EventAttribute<Events.Media>(node: node).build() + + case "ondurationchange": + return try EventAttribute<Events.Media>(node: node).build() + + case "onemptied": + return try EventAttribute<Events.Media>(node: node).build() + + case "onended": + return try EventAttribute<Events.Media>(node: node).build() + + case "onplay": + return try EventAttribute<Events.Media>(node: node).build() + + case "onplaying": + return try EventAttribute<Events.Media>(node: node).build() + + case "onpause": + return try EventAttribute<Events.Media>(node: node).build() + + case "onratechange": + return try EventAttribute<Events.Media>(node: node).build() + + case "onseeked": + return try EventAttribute<Events.Media>(node: node).build() + + case "onseeking": + return try EventAttribute<Events.Media>(node: node).build() + + case "onstalled": + return try EventAttribute<Events.Media>(node: node).build() + + case "onsuspend": + return try EventAttribute<Events.Media>(node: node).build() + + case "ontimeupdate": + return try EventAttribute<Events.Media>(node: node).build() + + case "onvolumechange": + return try EventAttribute<Events.Media>(node: node).build() + + case "onwaiting": + return try EventAttribute<Events.Media>(node: node).build() + + case "onreset": + return try EventAttribute<Events.Form>(node: node).build() + + case "onsubmit": + return try EventAttribute<Events.Form>(node: node).build() + + case "ontoggle": + return try EventAttribute<Events.Detail>(node: node).build() + + case "aria-activedescendant": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "activeDescendant") + + case "aria-atomic": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "atomic") + + case "aria-autocomplete": + return try TypeAttribute<Accessibility.Complete>(node: node, kind: .aria).build(verbatim: "autoComplete") + + case "aria-busy": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "busy") + + case "aria-checked": + return try TypeAttribute<Accessibility.Check>(node: node, kind: .aria).build(verbatim: "checked") + + case "aria-colcount": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "columnCount") + + case "aria-colindex": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "columnIndex") + + case "aria-colspan": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "columnSpan") + + case "aria-controls": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "controls") + + case "aria-current": + return try TypeAttribute<Accessibility.Current>(node: node, kind: .aria).build(verbatim: "current") + + case "aria-describedby": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "describedBy") + + case "aria-details": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "details") + + case "aria-disabled": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "disabled") + + case "aria-errormessage": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "errorMessage") + + case "aria-expanded": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "expanded") + + case "aria-flowto": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "flowTo") + + case "aria-haspopup": + return try TypeAttribute<Accessibility.Popup>(node: node, kind: .aria).build(verbatim: "hasPopup") + + case "aria-hidden": + return try ValueAttribute<Bool>(node: node).build(verbatim: "hidden") + + case "aria-invalid": + return try TypeAttribute<Accessibility.Invalid>(node: node, kind: .aria).build(verbatim: "invalid") + + case "aria-keyshortcuts": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "keyShortcuts") + + case "aria-label": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "label") + + case "aria-labeledby": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "labeledBy") + + case "aria-level": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "level") + + case "aria-live": + return try TypeAttribute<Accessibility.Live>(node: node, kind: .aria).build(verbatim: "live") + + case "aria-modal": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "modal") + + case "aria-multiline": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "multiline") + + case "aria-multiselectable": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "multiselectable") + + case "aria-orientation": + return try TypeAttribute<Accessibility.Orientation>(node: node, kind: .aria).build(verbatim: "orientation") + + case "aria-owns": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "owns") + + case "aria-placeholder": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "placeholder") + + case "aria-posinset": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "positionIndex") + + case "aria-pressed": + return try TypeAttribute<Accessibility.Pressed>(node: node, kind: .aria).build(verbatim: "pressed") + + case "aria-readonly": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "readonly") + + case "aria-relevant": + return try TypeAttribute<Accessibility.Relevant>(node: node, kind: .aria).build(verbatim: "relevant") + + case "aria-required": + return try ValueAttribute<Bool>(node: node, kind: .aria).build(verbatim: "required") + + case "aria-roledescription": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "roleDescription") + + case "aria-rowcount": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "rowCount") + + case "aria-rowindex": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "rowIndex") + + case "aria-rowspan": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "rowSpan") + + case "aria-selected": + return try TypeAttribute<Accessibility.Selected>(node: node, kind: .aria).build(verbatim: "selected") + + case "aria-setsize": + return try ValueAttribute<Int>(node: node, kind: .aria).build(verbatim: "setSize") + + case "aria-sort": + return try TypeAttribute<Accessibility.Sort>(node: node, kind: .aria).build(verbatim: "sort") + + case "aria-valuemax": + return try ValueAttribute<Float>(node: node, kind: .aria).build(verbatim: "valueMaximum") + + case "aria-valuemin": + return try ValueAttribute<Float>(node: node, kind: .aria).build(verbatim: "valueMinimum") + + case "aria-valuenow": + return try ValueAttribute<Float>(node: node, kind: .aria).build(verbatim: "valueNow") + + case "aria-valuetext": + return try ValueAttribute<String>(node: node, kind: .aria).build(verbatim: "valueText") + + default: + throw ParserError.unkownAttribute(localName) + } + + default: + break + } + + return "" + } + + internal struct PageLayout<T: RawRepresentable> { + + private let name: String + + private var content: String { + + get throws { + return try Parser.shared.parse(node: element) + } + } + + private var type: String { + + if let name = doctype.name, let publicId = doctype.publicID, let systemId = doctype.systemID { + + if let type = T(rawValue: "\(name) PUBLIC \"\(publicId)\" \"\(systemId)\"" as! T.RawValue) { + return ".\(type)" + } + } + + return ".html5" + } + + private let doctype: XMLDTD + + private let element: XMLElement + + internal init(name: String, doctype: XMLDTD, element: XMLElement) { + self.name = name.capitalized + self.doctype = doctype + self.element = element + } + + internal func build() throws -> String { + + return """ + import HTMLKit + + struct \(name)Page: Page { + + public var body: AnyContent { + Document(type: \(type)) + \(try content) + } + } + """ + } + } + + internal struct ViewLayout { + + private let name: String + + private var content: String { + + get throws { + return try Parser.shared.parse(node: element) + } + } + + private let element: XMLElement + + internal init(name: String, element: XMLElement) { + self.name = name.capitalized + self.element = element + } + + internal func build() throws -> String { + + return """ + import HTMLKit + + struct \(name)View: View { + + @TemplateValue(String.self) var context + + public var body: AnyContent { + \(try content) + } + } + """ + } + } + + internal struct CommentElement { + + private var value: String? { + + guard let value = node.stringValue else { + return nil + } + + return value + } + + private var level: Int { + return node.level - 1 + } + + private let node: XMLNode + + internal init(node: XMLNode) { + self.node = node + } + + internal func build(preindent: Int = 0) -> String { + + let indent = String(repeating: "\t", count: (level + preindent)) + + if let value = value { + return "\(indent)Comment(\"\(value)\")\n" + } + + return "\(indent)Comment(\"\")\n" + } + } + + internal struct TextElement { + + private var value: String? { + + guard let value = node.stringValue else { + return nil + } + + return value + } + + private var level: Int { + return node.level - 1 + } + + private let node: XMLNode + + internal init(node: XMLNode) { + self.node = node + } + + internal func build(preindent: Int = 0) -> String { + + let indent = String(repeating: "\t", count: (level + preindent)) + + if let value = value { + return "\(indent)\"\(value)\"\n" + } + + return "\(indent)\"\"\n" + } + } + + internal struct ContentElement { + + private var name: String { + + get throws { + + guard let name = element.name else { + throw ParserError.noLocalName + } + + return name.capitalized + } + } + + private var attributes: [String]? { + + get throws { + + guard let attributes = element.attributes else { + return nil + } + + return try attributes.map { attribute in + return try Parser.shared.parse(node: attribute) + } + } + } + + private var content: [String]? { + + get throws { + + guard let children = element.children else { + return nil + } + + return try children.compactMap { child in + return try Parser.shared.parse(node: child) + } + } + } + + private var level: Int { + return element.level - 1 + } + + private let element: XMLElement + + internal init(element: XMLElement) { + self.element = element + } + + internal func build(preindent: Int = 0) throws -> String { + + let indent = String(repeating: "\t", count: (level + preindent)) + + var yield: String = "" + + yield += "\(indent)\(try name) {\n" + + if let content = try content { + yield += content.joined() + } + + yield += "\(indent)}\n" + + if let attributes = try attributes { + yield += "\(indent)\(attributes.joined(separator: "\(indent)"))" + } + + return yield + } + + internal func build(verbatim: String, preindent: Int = 0) throws -> String { + + let indent = String(repeating: "\t", count: (level + preindent)) + + var yield: String = "" + + yield += "\(indent)\(verbatim) {\n" + + if let content = try content { + yield += content.joined() + } + + yield += "\(indent)}\n" + + if let attributes = try attributes { + yield += "\(indent)\(attributes.joined(separator: "\(indent)"))" + } + + return yield + } + } + + internal struct EmptyElement { + + private var name: String { + + get throws { + + guard let name = element.name else { + throw ParserError.noLocalName + } + + return name.capitalized + } + } + + private var attributes: [String]? { + + get throws { + + guard let attributes = element.attributes else { + return nil + } + + return try attributes.map { attribute in + return try Parser.shared.parse(node: attribute) + } + } + } + + private var level: Int { + return element.level - 1 + } + + private let element: XMLElement + + internal init(element: XMLElement) { + self.element = element + } + + internal func build(preindent: Int = 0) throws -> String { + + let indent = String(repeating: "\t", count: (level + preindent)) + + var yield: String = "" + + yield += "\(indent)\(try name)()\n" + + if let attributes = try attributes { + yield += "\(indent)\t\(attributes.joined(separator: "\t\(indent)"))" + } + + return yield + } + + internal func build(verbatim: String, preindent: Int = 0) throws -> String { + + let indent = String(repeating: "\t", count: (level + preindent)) + + var yield: String = "" + + yield += "\(indent)\(verbatim)()\n" + + if let attributes = try attributes { + yield += "\(indent)\t\(attributes.joined(separator: "\t\(indent)"))" + } + + return yield + } + } + + internal struct ValueAttribute<T: InitRepresentable> { + + internal enum AttributeKind { + case normal + case aria + } + + private var kind: AttributeKind + + private var name: String { + + get throws { + + guard let name = node.name else { + throw ParserError.noLocalName + } + + return name + } + } + + private var value: T? { + + guard let value = node.stringValue else { + return nil + } + + return T(value: value) + } + + private let node: XMLNode + + internal init(node: XMLNode, kind: AttributeKind = .normal) { + self.node = node + self.kind = kind + } + + internal func build() throws -> String { + + switch kind { + case .normal: + + if let value = self.value { + + switch value { + case is Float, is Int, is Double, is Bool: + return ".\(try name)(\(value))\n" + + default: + return ".\(try name)(\"\(value)\")\n" + } + } + + return ".\(try name)()\n" + + case .aria: + + if let value = self.value { + + switch value { + case is Float, is Int, is Double, is Bool: + return ".aria(\(try name): \(value))\n" + + default: + return ".aria(\(try name): \"\(value)\")\n" + } + } + + return ".aria(\(try name): \"\")\n" + } + } + + internal func build(verbatim: String) throws -> String { + + switch kind { + case .normal: + + if let value = value { + + switch value { + case is Float, is Int, is Double, is Bool: + return ".\(verbatim)(\(value))\n" + + default: + return ".\(verbatim)(\"\(value)\")\n" + } + } + + return ".\(verbatim)()\n" + + case .aria: + + if let value = value { + + switch value { + case is Float, is Int, is Double, is Bool: + return ".aria(\(verbatim): \(value))\n" + + default: + return ".aria(\(verbatim): \"\(value)\")\n" + } + } + + return ".aria(\(verbatim): \"\")\n" + } + } + } + + internal struct EmptyAttribute { + + private var name: String { + + get throws { + + guard let name = node.name else { + throw ParserError.noLocalName + } + + return name + } + } + + private let node: XMLNode + + internal init(node: XMLNode) { + self.node = node + } + + internal func build() throws -> String { + return ".\(try name)()\n" + } + + internal func build(verbatim: String) throws -> String { + return ".\(verbatim)()\n" + } + } + + internal struct TypeAttribute<T: RawRepresentable> { + + internal enum AttributeKind { + case normal + case aria + } + + private var kind: AttributeKind + + private var name: String { + + get throws { + + guard let name = node.name else { + throw ParserError.noLocalName + } + + return name + } + } + + private var value: T? { + + guard let value = node.stringValue else { + return nil + } + + return T(rawValue: value.lowercased() as! T.RawValue) + } + + private let node: XMLNode + + internal init(node: XMLNode, kind: AttributeKind = .normal) { + self.node = node + self.kind = kind + } + + internal func build() throws -> String { + + switch kind { + case .normal: + + if let value = value { + return ".\(try name)(.\(value))\n" + } + + return ".\(try name)()\n" + + case .aria: + + if let value = value { + return ".aria(\(try name): .\(value))\n" + } + + return ".aria(\(try name): \"\")\n" + } + } + + internal func build(verbatim: String) throws -> String { + + switch kind { + case .normal: + + if let value = value { + return ".\(verbatim)(.\(value))\n" + } + + return ".\(verbatim)()\n" + + case .aria: + + if let value = value { + return ".aria(\(verbatim): .\(value))\n" + } + + return ".aria(\(verbatim): \"\")\n" + } + } + } + + internal struct CustomAttribute { + + private var name: String { + + get throws { + + guard let name = node.name else { + throw ParserError.noLocalName + } + + return name + } + } + + private var value: String? { + + guard let value = node.stringValue else { + return nil + } + + return value + } + + private let node: XMLNode + + internal init(node: XMLNode) { + self.node = node + } + + internal func build() throws -> String { + + if let value = value { + return ".custom(key: \"\(try name)\", value: \"\(value)\")\n" + } + + return ".custom(key: \"\(try name)\", value: \"\")\n" + } + } + + internal struct EventAttribute<T: RawRepresentable> { + + private var name: T { + + get throws { + + guard let name = T(rawValue: node.localName?.lowercased() as! T.RawValue) else { + throw ParserError.noLocalName + } + + return name + } + } + + private var value: String? { + + guard let value = node.stringValue else { + return nil + } + + return value + } + + private let node: XMLNode + + internal init(node: XMLNode) { + self.node = node + } + + internal func build() throws -> String { + + if let value = value { + return ".on(event: .\(try name), \"\(value)\")\n" + } + + return ".on(event: .\(try name), \"\")\n" + } + } +} diff --git a/Sources/HTMLKit/Internal/Features/Conversion/Converter.swift b/Sources/HTMLKit/Internal/Features/Conversion/Converter.swift deleted file mode 100644 index 31e1f913..00000000 --- a/Sources/HTMLKit/Internal/Features/Conversion/Converter.swift +++ /dev/null @@ -1,1506 +0,0 @@ -/* - Abstract: - The file contains the converter. - - Authors: - - Mats Moll (https://github.com/matsmoll) - - Contributors: - - Mattes Mohr (https://github.com/mattesmohr) - - Note: - If you about to add something to the file, stick to the official documentation to keep the code consistent. - */ - -import Foundation - #if canImport(FoundationXML) - import FoundationXML - #endif - -@available(macOS 11.0, *) -public class Converter { - - public enum Extension: String { - case html - case leaf - } - - public enum Output: String { - case print - case file - } - - public enum Errors: Error { - case rootNotFound - } - - public static let `default` = Converter() - - private init() {} - - public func convert(directory: URL, fileExtension: Extension = .html, option: Output) throws { - - if let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { - - for case let path as URL in enumerator { - - if !path.hasDirectoryPath { - - if path.pathExtension != fileExtension.rawValue { - enumerator.skipDescendants() - } else { - try convert(file: path, option: option) - } - - } - } - } - } - - public func convert(file: URL, option: Output) throws { - - let fileName = file.deletingPathExtension().lastPathComponent - - let document = try XMLDocument(contentsOf: file, options: [.documentIncludeContentTypeDeclaration]) - - guard let root = document.rootElement() else { - throw Errors.rootNotFound - } - - switch option { - case .print: - - if let dtd = document.dtd { - - let layout = PageLayout<Doctypes>(name: fileName, doctype: dtd, root: root).build() - - print(layout) - - } else { - - let layout = ViewLayout(name: fileName, root: root).build() - - print(layout) - } - - case .file: - - if let dtd = document.dtd { - - let layout = PageLayout<Doctypes>(name: fileName, doctype: dtd, root: root).build() - - try layout.write(to: file.deletingPathExtension().appendingPathExtension("swift"), - atomically: true, - encoding: .utf8) - - } else { - - let layout = ViewLayout(name: fileName, root: root).build() - - try layout.write(to: file.deletingPathExtension().appendingPathExtension("swift"), - atomically: true, - encoding: .utf8) - } - } - } - - /// Converts an HTML component to be put into an existing Page or View layout. - /// - /// The whole html string needs to be inside a tag. - /// For example, multiple `div`s will give an error. They need to be inside another `div`. - public func convert(html: String) throws -> String { - - let document = try XMLDocument(xmlString: html, options: [.documentIncludeContentTypeDeclaration]) - - guard let root = document.rootElement() else { - throw Errors.rootNotFound - } - - let content = Converter.default.decode(element: root) - - // The user would put this to a line that's already indented. So, remove the extra indentation: - return content.replacingOccurrences(of: "\t\t\t", with: "\t") - } - - @StringBuilder private func decode(attribute: XMLNode) -> String { - - switch attribute.localName { - case "accesskey": - ValueProperty<String>(node: attribute).build() - case "accept": - ValueProperty<String>(node: attribute).build() - case "action": - ValueProperty<String>(node: attribute).build() - case "alt": - ValueProperty<String>(node: attribute).build(verbatim: "alternate") - case "async": - EmptyProperty(node: attribute).build(verbatim: "asynchronously") - case "autocapitalize": - TypeProperty<Capitalization>(node: attribute).build() - case "autocomplete": - ValueProperty<Bool>(node: attribute).build(verbatim: "hasCompletion") - case "autofocus": - EmptyProperty(node: attribute).build() - case "autoplay": - EmptyProperty(node: attribute).build() - case "checked": - EmptyProperty(node: attribute).build() - case "cite": - ValueProperty<String>(node: attribute).build() - case "class": - ValueProperty<String>(node: attribute).build() - case "cols": - ValueProperty<Int>(node: attribute).build(verbatim: "columns") - case "colspan": - ValueProperty<Int>(node: attribute).build(verbatim: "columnSpan") - case "content": - ValueProperty<String>(node: attribute).build() - case "contenteditable": - ValueProperty<Bool>(node: attribute).build(verbatim: "isEditable") - case "controls": - EmptyProperty(node: attribute).build() - case "coords": - ValueProperty<String>(node: attribute).build(verbatim: "coordinates") - case "data": - ValueProperty<String>(node: attribute).build() - case "datetime": - ValueProperty<String>(node: attribute).build(verbatim: "dateTime") - case "default": - EmptyProperty(node: attribute).build() - case "defer": - EmptyProperty(node: attribute).build() - case "dir": - TypeProperty<Direction>(node: attribute).build(verbatim: "direction") - case "disabled": - EmptyProperty(node: attribute).build() - case "download": - EmptyProperty(node: attribute).build() - case "draggable": - ValueProperty<String>(node: attribute).build(verbatim: "isDraggable") - case "enctype": - TypeProperty<Encoding>(node: attribute).build(verbatim: "encoding") - case "enterkeyhint": - TypeProperty<Hint>(node: attribute).build(verbatim: "enterKeyHint") - case "for": - ValueProperty<String>(node: attribute).build() - case "form": - ValueProperty<String>(node: attribute).build() - case "formaction": - ValueProperty<String>(node: attribute).build(verbatim: "formAction") - case "headers": - ValueProperty<String>(node: attribute).build() - case "height": - ValueProperty<Int>(node: attribute).build() - case "hidden": - EmptyProperty(node: attribute).build() - case "high": - ValueProperty<Float>(node: attribute).build() - case "href": - ValueProperty<String>(node: attribute).build(verbatim: "reference") - case "hreflang": - TypeProperty<Language>(node: attribute).build(verbatim: "referenceLanguage") - case "id": - ValueProperty<String>(node: attribute).build() - case "ismap": - EmptyProperty(node: attribute).build(verbatim: "isMap") - case "inputmode": - ValueProperty<String>(node: attribute).build(verbatim: "inputMode") - case "is": - ValueProperty<String>(node: attribute).build() - case "itemid": - ValueProperty<String>(node: attribute).build(verbatim: "itemId") - case "itemproperty": - ValueProperty<String>(node: attribute).build(verbatim: "itemProperty") - case "itemref": - ValueProperty<String>(node: attribute).build(verbatim: "itemReference") - case "itemscope": - ValueProperty<String>(node: attribute).build(verbatim: "itemScope") - case "itemtype": - ValueProperty<String>(node: attribute).build(verbatim: "itemType") - case "kind": - TypeProperty<Kinds>(node: attribute).build() - case "label": - ValueProperty<String>(node: attribute).build() - case "lang": - TypeProperty<Language>(node: attribute).build(verbatim: "language") - case "list": - ValueProperty<String>(node: attribute).build() - case "loop": - EmptyProperty(node: attribute).build() - case "low": - ValueProperty<Float>(node: attribute).build() - case "max": - - if let parent = attribute.parent { - - switch parent.localName { - case "progress", "meter": - ValueProperty<Float>(node: attribute).build(verbatim: "maximum") - default: - ValueProperty<String>(node: attribute).build(verbatim: "maximum") - } - } - - case "media": - ValueProperty<String>(node: attribute).build() - case "method": - TypeProperty<Method>(node: attribute).build() - case "min": - ValueProperty<String>(node: attribute).build(verbatim: "minimum") - case "multiple": - EmptyProperty(node: attribute).build() - case "muted": - EmptyProperty(node: attribute).build() - case "name": - - if let parent = attribute.parent { - - switch parent.localName { - case "meta": - TypeProperty<Names>(node: attribute).build() - default: - ValueProperty<String>(node: attribute).build() - } - } - - case "nonce": - ValueProperty<String>(node: attribute).build() - case "novalidate": - EmptyProperty(node: attribute).build() - case "open": - ValueProperty<Bool>(node: attribute).build(verbatim: "isOpen") - case "optimum": - ValueProperty<Float>(node: attribute).build() - case "pattern": - ValueProperty<String>(node: attribute).build() - case "part": - ValueProperty<String>(node: attribute).build() - case "ping": - ValueProperty<String>(node: attribute).build() - case "placeholder": - ValueProperty<String>(node: attribute).build() - case "poster": - ValueProperty<String>(node: attribute).build() - case "preload": - TypeProperty<Preload>(node: attribute).build() - case "readonly": - EmptyProperty(node: attribute).build() - case "referrerpolicy": - TypeProperty<Policy>(node: attribute).build(verbatim: "referrerPolicy") - case "rel": - TypeProperty<Relation>(node: attribute).build(verbatim: "relationship") - case "required": - EmptyProperty(node: attribute).build() - case "reversed": - EmptyProperty(node: attribute).build() - case "role": - TypeProperty<Roles>(node: attribute).build() - case "rows": - ValueProperty<String>(node: attribute).build() - case "rowspan": - ValueProperty<Int>(node: attribute).build(verbatim: "rowSpan") - case "sandbox": - EmptyProperty(node: attribute).build() - case "scope": - ValueProperty<String>(node: attribute).build() - case "shape": - TypeProperty<Shape>(node: attribute).build() - case "size": - ValueProperty<String>(node: attribute).build() - case "sizes": - ValueProperty<Int>(node: attribute).build() - case "slot": - ValueProperty<String>(node: attribute).build() - case "span": - ValueProperty<Int>(node: attribute).build() - case "spellcheck": - ValueProperty<Bool>(node: attribute).build(verbatim: "hasSpellCheck") - case "src": - ValueProperty<String>(node: attribute).build(verbatim: "source") - case "start": - ValueProperty<Int>(node: attribute).build() - case "step": - ValueProperty<Int>(node: attribute).build() - case "style": - ValueProperty<String>(node: attribute).build() - case "tabindex": - ValueProperty<Int>(node: attribute).build(verbatim: "tabIndex") - case "target": - TypeProperty<Target>(node: attribute).build() - case "title": - ValueProperty<String>(node: attribute).build() - case "translate": - TypeProperty<Decision>(node: attribute).build() - case "type": - - if let parent = attribute.parent { - - switch parent.localName { - case "input": - TypeProperty<Inputs>(node: attribute).build() - case "button": - TypeProperty<Buttons>(node: attribute).build() - case "link", "script", "audio": - TypeProperty<Medias>(node: attribute).build() - default: - ValueProperty<String>(node: attribute).build() - } - } - - case "value": - ValueProperty<String>(node: attribute).build() - case "width": - ValueProperty<Int>(node: attribute).build() - case "wrap": - TypeProperty<Wrapping>(node: attribute).build() - case "property": - TypeProperty<Graphs>(node: attribute).build() - case "charset": - TypeProperty<Charset>(node: attribute).build() - case "http-equiv": - TypeProperty<Equivalent>(node: attribute).build() - case "selected": - EmptyProperty(node: attribute).build() - case "maxlength": - ValueProperty<String>(node: attribute).build(verbatim: "maximum") - case "minlength": - ValueProperty<String>(node: attribute).build(verbatim: "minimum") - case "d": - ValueProperty<String>(node: attribute).build(verbatim: "draw") - case "fill": - ValueProperty<String>(node: attribute).build() - case "fill-opacity": - ValueProperty<Double>(node: attribute).build(verbatim: "fillOpacity") - case "stroke": - ValueProperty<String>(node: attribute).build() - case "stroke-width": - ValueProperty<Int>(node: attribute).build(verbatim: "strokeWidth") - case "stroke-opacity": - ValueProperty<Double>(node: attribute).build(verbatim: "strokeOpacity") - case "stroke-linecap": - TypeProperty<Linecap>(node: attribute).build(verbatim: "strokeLineCap") - case "stroke-linejoin": - TypeProperty<Linejoin>(node: attribute).build(verbatim: "strokeLineJoin") - case "r": - ValueProperty<Int>(node: attribute).build(verbatim: "radius") - case "viewbox": - ValueProperty<String>(node: attribute).build(verbatim: "viewBox") - case "onafterprint": - EventProperty<Events.Window>(node: attribute).build() - case "onbeforeprint": - EventProperty<Events.Window>(node: attribute).build() - case "onbeforeunload": - EventProperty<Events.Window>(node: attribute).build() - case "onhashchange": - EventProperty<Events.Window>(node: attribute).build() - case "onlanguagechange": - EventProperty<Events.Window>(node: attribute).build() - case "onmessage": - EventProperty<Events.Window>(node: attribute).build() - case "onmessageerror": - EventProperty<Events.Window>(node: attribute).build() - case "onoffline": - EventProperty<Events.Window>(node: attribute).build() - case "ononline": - EventProperty<Events.Window>(node: attribute).build() - case "onpagehide": - EventProperty<Events.Window>(node: attribute).build() - case "onpageshow": - EventProperty<Events.Window>(node: attribute).build() - case "onpopstate": - EventProperty<Events.Window>(node: attribute).build() - case "onrejectionhandled": - EventProperty<Events.Window>(node: attribute).build() - case "onstorage": - EventProperty<Events.Window>(node: attribute).build() - case "onunhandledrejection": - EventProperty<Events.Window>(node: attribute).build() - case "onunload": - EventProperty<Events.Window>(node: attribute).build() - case "onerror": - EventProperty<Events.Window>(node: attribute).build() - case "onblur": - EventProperty<Events.Focus>(node: attribute).build() - case "onfocus": - EventProperty<Events.Focus>(node: attribute).build() - case "onpointercancel": - EventProperty<Events.Pointer>(node: attribute).build() - case "onpointerdown": - EventProperty<Events.Pointer>(node: attribute).build() - case "onpointerenter": - EventProperty<Events.Pointer>(node: attribute).build() - case "onpointerleave": - EventProperty<Events.Pointer>(node: attribute).build() - case "onpointermove": - EventProperty<Events.Pointer>(node: attribute).build() - case "onpointerout": - EventProperty<Events.Pointer>(node: attribute).build() - case "onpointerover": - EventProperty<Events.Pointer>(node: attribute).build() - case "onpointerup": - EventProperty<Events.Pointer>(node: attribute).build() - case "onlostpointercapture": - EventProperty<Events.Pointer>(node: attribute).build() - case "onclick": - EventProperty<Events.Mouse>(node: attribute).build() - case "oncontextmenu": - EventProperty<Events.Mouse>(node: attribute).build() - case "ondblclick": - EventProperty<Events.Mouse>(node: attribute).build() - case "onmousedown": - EventProperty<Events.Mouse>(node: attribute).build() - case "onmouseenter": - EventProperty<Events.Mouse>(node: attribute).build() - case "onmouseleave": - EventProperty<Events.Mouse>(node: attribute).build() - case "onmousemove": - EventProperty<Events.Mouse>(node: attribute).build() - case "onmouseout": - EventProperty<Events.Mouse>(node: attribute).build() - case "onmouseover": - EventProperty<Events.Mouse>(node: attribute).build() - case "onmouseup": - EventProperty<Events.Mouse>(node: attribute).build() - case "onwheel": - EventProperty<Events.Wheel>(node: attribute).build() - case "onbeforeinput": - EventProperty<Events.Input>(node: attribute).build() - case "oninput": - EventProperty<Events.Input>(node: attribute).build() - case "onselect": - EventProperty<Events.Input>(node: attribute).build() - case "onkeydown": - EventProperty<Events.Keyboard>(node: attribute).build() - case "onkeyup": - EventProperty<Events.Keyboard>(node: attribute).build() - case "ondrag": - EventProperty<Events.Drag>(node: attribute).build() - case "ondragend": - EventProperty<Events.Drag>(node: attribute).build() - case "ondragenter": - EventProperty<Events.Drag>(node: attribute).build() - case "ondragleave": - EventProperty<Events.Drag>(node: attribute).build() - case "ondragover": - EventProperty<Events.Drag>(node: attribute).build() - case "ondragstart": - EventProperty<Events.Drag>(node: attribute).build() - case "ondrop": - EventProperty<Events.Drag>(node: attribute).build() - case "oncopy": - EventProperty<Events.Clipboard>(node: attribute).build() - case "oncut": - EventProperty<Events.Clipboard>(node: attribute).build() - case "onpaste": - EventProperty<Events.Clipboard>(node: attribute).build() - case "onselectionchange": - EventProperty<Events.Selection>(node: attribute).build() - case "onselectstart": - EventProperty<Events.Selection>(node: attribute).build() - case "onabort": - EventProperty<Events.Media>(node: attribute).build() - case "oncanplay": - EventProperty<Events.Media>(node: attribute).build() - case "oncanplaythrough": - EventProperty<Events.Media>(node: attribute).build() - case "ondurationchange": - EventProperty<Events.Media>(node: attribute).build() - case "onemptied": - EventProperty<Events.Media>(node: attribute).build() - case "onended": - EventProperty<Events.Media>(node: attribute).build() - case "onplay": - EventProperty<Events.Media>(node: attribute).build() - case "onplaying": - EventProperty<Events.Media>(node: attribute).build() - case "onpause": - EventProperty<Events.Media>(node: attribute).build() - case "onratechange": - EventProperty<Events.Media>(node: attribute).build() - case "onseeked": - EventProperty<Events.Media>(node: attribute).build() - case "onseeking": - EventProperty<Events.Media>(node: attribute).build() - case "onstalled": - EventProperty<Events.Media>(node: attribute).build() - case "onsuspend": - EventProperty<Events.Media>(node: attribute).build() - case "ontimeupdate": - EventProperty<Events.Media>(node: attribute).build() - case "onvolumechange": - EventProperty<Events.Media>(node: attribute).build() - case "onwaiting": - EventProperty<Events.Media>(node: attribute).build() - case "onreset": - EventProperty<Events.Form>(node: attribute).build() - case "onsubmit": - EventProperty<Events.Form>(node: attribute).build() - case "ontoggle": - EventProperty<Events.Detail>(node: attribute).build() - case "aria-activedescendant": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "activeDescendant") - case "aria-atomic": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "atomic") - case "aria-autocomplete": - TypeProperty<Accessibility.Complete>(node: attribute, kind: .aria).build(verbatim: "autoComplete") - case "aria-busy": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "busy") - case "aria-checked": - TypeProperty<Accessibility.Check>(node: attribute, kind: .aria).build(verbatim: "checked") - case "aria-colcount": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "columnCount") - case "aria-colindex": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "columnIndex") - case "aria-colspan": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "columnSpan") - case "aria-controls": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "controls") - case "aria-current": - TypeProperty<Accessibility.Current>(node: attribute, kind: .aria).build(verbatim: "current") - case "aria-describedby": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "describedBy") - case "aria-details": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "details") - case "aria-disabled": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "disabled") - case "aria-errormessage": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "errorMessage") - case "aria-expanded": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "expanded") - case "aria-flowto": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "flowTo") - case "aria-haspopup": - TypeProperty<Accessibility.Popup>(node: attribute, kind: .aria).build(verbatim: "hasPopup") - case "aria-hidden": - ValueProperty<Bool>(node: attribute).build(verbatim: "hidden") - case "aria-invalid": - TypeProperty<Accessibility.Invalid>(node: attribute, kind: .aria).build(verbatim: "invalid") - case "aria-keyshortcuts": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "keyShortcuts") - case "aria-label": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "label") - case "aria-labeledby": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "labeledBy") - case "aria-level": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "level") - case "aria-live": - TypeProperty<Accessibility.Live>(node: attribute, kind: .aria).build(verbatim: "live") - case "aria-modal": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "modal") - case "aria-multiline": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "multiline") - case "aria-multiselectable": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "multiselectable") - case "aria-orientation": - TypeProperty<Accessibility.Orientation>(node: attribute, kind: .aria).build(verbatim: "orientation") - case "aria-owns": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "owns") - case "aria-placeholder": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "placeholder") - case "aria-posinset": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "positionIndex") - case "aria-pressed": - TypeProperty<Accessibility.Pressed>(node: attribute, kind: .aria).build(verbatim: "pressed") - case "aria-readonly": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "readonly") - case "aria-relevant": - TypeProperty<Accessibility.Relevant>(node: attribute, kind: .aria).build(verbatim: "relevant") - case "aria-required": - ValueProperty<Bool>(node: attribute, kind: .aria).build(verbatim: "required") - case "aria-roledescription": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "roleDescription") - case "aria-rowcount": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "rowCount") - case "aria-rowindex": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "rowIndex") - case "aria-rowspan": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "rowSpan") - case "aria-selected": - TypeProperty<Accessibility.Selected>(node: attribute, kind: .aria).build(verbatim: "selected") - case "aria-setsize": - ValueProperty<Int>(node: attribute, kind: .aria).build(verbatim: "setSize") - case "aria-sort": - TypeProperty<Accessibility.Sort>(node: attribute, kind: .aria).build(verbatim: "sort") - case "aria-valuemax": - ValueProperty<Float>(node: attribute, kind: .aria).build(verbatim: "valueMaximum") - case "aria-valuemin": - ValueProperty<Float>(node: attribute, kind: .aria).build(verbatim: "valueMinimum") - case "aria-valuenow": - ValueProperty<Float>(node: attribute, kind: .aria).build(verbatim: "valueNow") - case "aria-valuetext": - ValueProperty<String>(node: attribute, kind: .aria).build(verbatim: "valueText") - default: - CustomProperty(node: attribute).build() - } - } - - @StringBuilder private func decode(element: XMLNode, indent: Int? = nil) -> String { - - switch element.kind { - case .text: - - TextElement(node: element).build(preindent: indent) - - case .comment: - - CommentElement(node: element).build(preindent: indent) - - default: - - if let element = element as? XMLElement { - - switch element.localName { - case "html": - ContentElement(element: element).build(preindent: indent) - case "head": - ContentElement(element: element).build(verbatim: "Head", preindent: indent) - case "body": - ContentElement(element: element).build(preindent: indent) - case "nav": - ContentElement(element: element).build(verbatim: "Navigation", preindent: indent) - case "link": - EmptyElement(element: element).build(preindent: indent) - case "aside": - ContentElement(element: element).build(preindent: indent) - case "section": - ContentElement(element: element).build(preindent: indent) - case "h1": - ContentElement(element: element).build(verbatim: "Heading1", preindent: indent) - case "h2": - ContentElement(element: element).build(verbatim: "Heading2", preindent: indent) - case "h3": - ContentElement(element: element).build(verbatim: "Heading3", preindent: indent) - case "h4": - ContentElement(element: element).build(verbatim: "Heading4", preindent: indent) - case "h5": - ContentElement(element: element).build(verbatim: "Heading5", preindent: indent) - case "h6": - ContentElement(element: element).build(verbatim: "Heading6", preindent: indent) - case "hgroup": - ContentElement(element: element).build(verbatim: "HeadingGroup", preindent: indent) - case "header": - ContentElement(element: element).build(preindent: indent) - case "footer": - ContentElement(element: element).build(preindent: indent) - case "address": - ContentElement(element: element).build(preindent: indent) - case "p": - ContentElement(element: element).build(verbatim: "Paragraph", preindent: indent) - case "hr": - EmptyElement(element: element).build(verbatim: "HorizontalRule", preindent: indent) - case "pre": - ContentElement(element: element).build(verbatim: "PreformattedText", preindent: indent) - case "blockquote": - ContentElement(element: element).build(preindent: indent) - case "ol": - ContentElement(element: element).build(verbatim: "OrderedList", preindent: indent) - case "ul": - ContentElement(element: element).build(verbatim: "UnorderedList", preindent: indent) - case "dl": - ContentElement(element: element).build(verbatim: "DescriptionList", preindent: indent) - case "figure": - ContentElement(element: element).build(preindent: indent) - case "a": - ContentElement(element: element).build(verbatim: "Anchor", preindent: indent) - case "em": - ContentElement(element: element).build(verbatim: "Emphasize", preindent: indent) - case "small": - ContentElement(element: element).build(preindent: indent) - case "s": - ContentElement(element: element).build(verbatim: "StrikeThrough", preindent: indent) - case "main": - ContentElement(element: element).build(preindent: indent) - case "div": - ContentElement(element: element).build(verbatim: "Division", preindent: indent) - case "dfn": - ContentElement(element: element).build(verbatim: "Definition", preindent: indent) - case "cite": - ContentElement(element: element).build(preindent: indent) - case "q": - ContentElement(element: element).build(verbatim: "ShortQuote", preindent: indent) - case "rt": - ContentElement(element: element).build(verbatim: "RubyText", preindent: indent) - case "rp": - ContentElement(element: element).build(verbatim: "RubyPronunciation", preindent: indent) - case "abbr": - ContentElement(element: element).build(verbatim: "Abbreviation", preindent: indent) - case "data": - ContentElement(element: element).build(preindent: indent) - case "time": - ContentElement(element: element).build(preindent: indent) - case "code": - ContentElement(element: element).build(preindent: indent) - case "v": - ContentElement(element: element).build(verbatim: "Variable", preindent: indent) - case "samp": - ContentElement(element: element).build(verbatim: "SampleOutput", preindent: indent) - case "kbd": - ContentElement(element: element).build(verbatim: "KeyboardOutput", preindent: indent) - case "sub": - ContentElement(element: element).build(verbatim: "Subscript", preindent: indent) - case "sup": - ContentElement(element: element).build(verbatim: "Superscript", preindent: indent) - case "i": - ContentElement(element: element).build(verbatim: "Italic", preindent: indent) - case "b": - ContentElement(element: element).build(verbatim: "Bold", preindent: indent) - case "strong": - ContentElement(element: element).build(verbatim: "Strong", preindent: indent) - case "u": - ContentElement(element: element).build(verbatim: "SampleOutput", preindent: indent) - case "mark": - ContentElement(element: element).build(preindent: indent) - case "bdi": - ContentElement(element: element).build(preindent: indent) - case "bdo": - EmptyElement(element: element).build(preindent: indent) - case "span": - ContentElement(element: element).build(preindent: indent) - case "br": - EmptyElement(element: element).build(verbatim: "LineBreak", preindent: indent) - case "wbr": - EmptyElement(element: element).build(verbatim: "WordBreak", preindent: indent) - case "ins": - ContentElement(element: element).build(verbatim: "InsertedText", preindent: indent) - case "del": - ContentElement(element: element).build(verbatim: "DeletedText", preindent: indent) - case "img": - EmptyElement(element: element).build(verbatim: "Image", preindent: indent) - case "embed": - ContentElement(element: element).build(preindent: indent) - case "iframe": - ContentElement(element: element).build(verbatim: "InlineFrame", preindent: indent) - case "param": - EmptyElement(element: element).build(verbatim: "Parameter", preindent: indent) - case "dt": - ContentElement(element: element).build(verbatim: "TermName", preindent: indent) - case "dd": - ContentElement(element: element).build(verbatim: "TermDefinition", preindent: indent) - case "figcaption": - ContentElement(element: element).build(verbatim: "FigureCaption", preindent: indent) - case "optgroup": - ContentElement(element: element).build(verbatim: "OptionGroup", preindent: indent) - case "option": - ContentElement(element: element).build(preindent: indent) - case "legend": - ContentElement(element: element).build(preindent: indent) - case "summary": - ContentElement(element: element).build(preindent: indent) - case "li": - ContentElement(element: element).build(verbatim: "ListItem", preindent: indent) - case "colgroup": - ContentElement(element: element).build(verbatim: "ColumnGroup", preindent: indent) - case "col": - ContentElement(element: element).build(verbatim: "Column", preindent: indent) - case "tbody": - ContentElement(element: element).build(verbatim: "TableBody", preindent: indent) - case "thead": - ContentElement(element: element).build(verbatim: "TableHead", preindent: indent) - case "tfoot": - ContentElement(element: element).build(verbatim: "TableFoot", preindent: indent) - case "tr": - ContentElement(element: element).build(verbatim: "TableRow", preindent: indent) - case "td": - ContentElement(element: element).build(verbatim: "DataCell", preindent: indent) - case "th": - ContentElement(element: element).build(verbatim: "HeaderCell", preindent: indent) - case "textarea": - ContentElement(element: element).build(verbatim: "TextArea", preindent: indent) - case "input": - EmptyElement(element: element).build(preindent: indent) - case "video": - ContentElement(element: element).build(preindent: indent) - case "audio": - ContentElement(element: element).build(preindent: indent) - case "map": - ContentElement(element: element).build(preindent: indent) - case "area": - ContentElement(element: element).build(preindent: indent) - case "form": - ContentElement(element: element).build(preindent: indent) - case "datalist": - ContentElement(element: element).build(preindent: indent) - case "output": - ContentElement(element: element).build(preindent: indent) - case "meter": - ContentElement(element: element).build(preindent: indent) - case "details": - ContentElement(element: element).build(preindent: indent) - case "dialog": - ContentElement(element: element).build(preindent: indent) - case "script": - ContentElement(element: element).build(preindent: indent) - case "noscript": - ContentElement(element: element).build(preindent: indent) - case "template": - ContentElement(element: element).build(preindent: indent) - case "canvas": - ContentElement(element: element).build(preindent: indent) - case "table": - ContentElement(element: element).build(preindent: indent) - case "fieldset": - ContentElement(element: element).build(preindent: indent) - case "button": - ContentElement(element: element).build(preindent: indent) - case "select": - ContentElement(element: element).build(preindent: indent) - case "label": - ContentElement(element: element).build(preindent: indent) - case "title": - ContentElement(element: element).build(preindent: indent) - case "base": - EmptyElement(element: element).build(preindent: indent) - case "meta": - EmptyElement(element: element).build(preindent: indent) - case "style": - ContentElement(element: element).build(preindent: indent) - case "source": - EmptyElement(element: element).build(preindent: indent) - case "track": - EmptyElement(element: element).build(preindent: indent) - case "article": - ContentElement(element: element).build(preindent: indent) - case "progress": - ContentElement(element: element).build(preindent: indent) - case "circle": - ContentElement(element: element).build(preindent: indent) - case "rect": - ContentElement(element: element).build(verbatim: "Rectangle", preindent: indent) - case "ellipse": - ContentElement(element: element).build(preindent: indent) - case "line": - ContentElement(element: element).build(preindent: indent) - case "polygon": - ContentElement(element: element).build(preindent: indent) - case "path": - ContentElement(element: element).build(preindent: indent) - case "use": - ContentElement(element: element).build(preindent: indent) - case "g": - ContentElement(element: element).build(verbatim: "Group", preindent: indent) - default: - "element is not listed. contact the author" - } - } - } - } -} - -@available(macOS 11.0, *) -extension Converter { - - private struct CommentElement { - - private var comment: String? { - - guard let comment = node.stringValue else { - return nil - } - - return comment - } - - private let node: XMLNode - - internal init(node: XMLNode) { - self.node = node - } - - @StringBuilder internal func build(preindent: Int? = nil) -> String { - - let indent = String(repeating: "\t", count: (node.level - 1) + (preindent ?? 0)) - - if let comment = comment - { - "\(indent)Comment(\"\(comment)\")\n" - } - } - } - - private struct TextElement { - - private var text: String? { - - guard let text = node.stringValue else { - return nil - } - - return text - } - - private let node: XMLNode - - internal init(node: XMLNode) { - self.node = node - } - - @StringBuilder internal func build(preindent: Int? = nil) -> String { - - let indent = String(repeating: "\t", count: (node.level - 1) + (preindent ?? 0)) - - if let text = text { - - if node.parent?.localName == "pre" { - - "\(indent)\"\"\"\n\(text)\"\"\"\n" - - } else { - - "\(indent)\"\(text.trimmingCharacters(in: .whitespacesAndNewlines))\"\n" - } - } - } - } - - private struct ContentElement { - - private var name: String? { - - guard let name = element.name else { - return nil - } - - return name.capitalized - } - - private var attributes: [String]? { - - guard let attributes = element.attributes else { - return nil - } - - return attributes.map { attribute in - return Converter.default.decode(attribute: attribute) - } - } - - private var content: [String]? { - - guard let children = element.children else { - return nil - } - - return children.map { child in - return Converter.default.decode(element: child, indent: 2) - } - } - - private var level: Int { - return element.level - } - - private let element: XMLElement - - internal init(element: XMLElement) { - self.element = element - } - - @StringBuilder internal func build(preindent: Int? = nil) -> String { - - let indent = String(repeating: "\t", count: (level - 1) + (preindent ?? 0)) - - if let name = name { - - "\(indent)\(name) {\n" - - if let content = content { - content.joined() - } - - "\(indent)}\n" - - if let attributes = attributes { - "\(indent)\(attributes.joined(separator: "\(indent)"))" - } - } - } - - @StringBuilder internal func build(verbatim: String? = nil, preindent: Int? = nil) -> String { - - let indent = String(repeating: "\t", count: (level - 1) + (preindent ?? 0)) - - if let verbatim = verbatim { - - "\(indent)\(verbatim) {\n" - - if let content = content { - content.joined() - } - - "\(indent)}\n" - - if let attributes = attributes { - "\(indent)\(attributes.joined(separator: "\(indent)"))" - } - } - } - } - - private struct EmptyElement { - - private var name: String? { - - guard let name = element.name else { - return nil - } - - return name.capitalized - } - - private var attributes: [String]? { - - guard let attributes = element.attributes else { - return nil - } - - return attributes.map { attribute in - return Converter.default.decode(attribute: attribute) - } - } - - private var level: Int { - return element.level - } - - private let element: XMLElement - - internal init(element: XMLElement) { - self.element = element - } - - @StringBuilder internal func build(preindent: Int? = nil) -> String { - - let indent = String(repeating: "\t", count: (level - 1) + (preindent ?? 0)) - - if let name = name { - - "\(indent)\(name)()\n" - - if let attributes = attributes { - "\(indent)\t\(attributes.joined(separator: "\t\(indent)"))" - } - } - } - - @StringBuilder internal func build(verbatim: String? = nil, preindent: Int? = nil) -> String { - - let indent = String(repeating: "\t", count: (level - 1) + (preindent ?? 0)) - - if let verbatim = verbatim { - - "\(indent)\(verbatim)()\n" - - if let attributes = attributes { - "\(indent)\t\(attributes.joined(separator: "\t\(indent)"))" - } - - } - } - } - - private struct PageLayout<T: RawRepresentable> { - - private var name: String - - private var content: String { - return Converter.default.decode(element: root, indent: 2) - } - - private var type: String { - - if let name = doctype.name, let publicId = doctype.publicID, let systemId = doctype.systemID { - - if let type = T(rawValue: "\(name) PUBLIC \"\(publicId)\" \"\(systemId)\"" as! T.RawValue) { - return ".\(type)" - } - } - - return ".html5" - } - - private var doctype: XMLDTD - - private var root: XMLElement - - internal init(name: String, doctype: XMLDTD, root: XMLElement) { - self.name = name.capitalized - self.doctype = doctype - self.root = root - } - - internal func build() -> String { - - """ - import HTMLKit - - struct \(name)Page: Page { - - public var body: AnyContent { - Document(type: \(type)) - \(content) - } - } - """ - } - } - - private struct ViewLayout { - - private var name: String - - private var content: String { - return Converter.default.decode(element: root, indent: 2) - } - - private var root: XMLElement - - internal init(name: String, root: XMLElement) { - self.name = name.capitalized - self.root = root - } - - internal func build() -> String { - - """ - import HTMLKit - - struct \(name)View: View { - - @TemplateValue(String.self) var context - - public var body: AnyContent { - \(content) - } - } - """ - } - } - - private struct ValueProperty<T: InitRepresentable> { - - internal enum PropertyKind { - case normal - case aria - } - - private var kind: PropertyKind - - private var name: String? { - - guard let name = node.name else { - return nil - } - - return name - } - - private var value: T? { - - guard let value = node.stringValue else { - return nil - } - - return T(value: value) - } - - private let node: XMLNode - - internal init(node: XMLNode, kind: PropertyKind = .normal) { - self.node = node - self.kind = kind - } - - @StringBuilder internal func build() -> String { - - switch kind { - case .normal: - - if let name = self.name, let value = self.value { - - switch value { - case is Float, is Int, is Double, is Bool: - - ".\(name)(\(value))\n" - - default: - - ".\(name)(\"\(value)\")\n" - } - - } else if let name = name { - - ".\(name)()\n" - } - - case .aria: - - if let name = self.name, let value = self.value { - - switch value { - case is Float, is Int, is Double, is Bool: - ".aria(\(name): \(value))\n" - - default: - ".aria(\(name): \"\(value)\")\n" - } - } - } - } - - @StringBuilder internal func build(verbatim: String? = nil) -> String { - - switch kind { - case .normal: - - if let verbatim = verbatim, let value = value { - - switch value { - case is Float, is Int, is Double, is Bool: - - ".\(verbatim)(\(value))\n" - - default: - - ".\(verbatim)(\"\(value)\")\n" - } - - } else if let verbatim = verbatim { - - ".\(verbatim)()\n" - } - - case .aria: - - if let verbatim = verbatim, let value = value { - - switch value { - case is Float, is Int, is Double, is Bool: - ".aria(\(verbatim): \(value))\n" - - default: - ".aria(\(verbatim): \"\(value)\")\n" - } - } - } - } - } - - private struct EmptyProperty { - - private var name: String? { - - guard let name = node.name else { - return nil - } - - return name - } - - private let node: XMLNode - - internal init(node: XMLNode) { - self.node = node - } - - @StringBuilder internal func build() -> String { - - if let name = name { - ".\(name)()\n" - } - } - - @StringBuilder internal func build(verbatim: String? = nil) -> String { - - if let verbatim = verbatim { - ".\(verbatim)()\n" - } - } - } - - private struct TypeProperty<T: RawRepresentable>{ - - internal enum PropertyKind { - case normal - case aria - } - - private var kind: PropertyKind - - private var name: String? { - - guard let name = node.name else { - return nil - } - - return name - } - - private var value: T? { - - guard let value = node.stringValue else { - return nil - } - - return T(rawValue: value.lowercased() as! T.RawValue) - } - - private let node: XMLNode - - internal init(node: XMLNode, kind: PropertyKind = .normal) { - self.node = node - self.kind = kind - } - - @StringBuilder internal func build() -> String { - - switch kind { - case .normal: - - if let name = name, let value = value { - - ".\(name)(.\(value))\n" - - } else if let name = name{ - - ".\(name)()\n" - } - - case .aria: - - if let name = name, let value = value { - ".aria(\(name): .\(value))\n" - } - } - } - - @StringBuilder internal func build(verbatim: String? = nil) -> String { - - switch kind { - case .normal: - - if let verbatim = verbatim, let value = value { - - ".\(verbatim)(.\(value))\n" - - } else if let verbatim = verbatim { - - ".\(verbatim)()\n" - } - - case .aria: - - if let verbatim = verbatim, let value = value { - ".aria(\(verbatim): .\(value))\n" - } - } - } - } - - private struct CustomProperty { - - private var name: String? { - - guard let name = node.name else { - return nil - } - - return name - } - - private var value: String? { - - guard let value = node.stringValue else { - return nil - } - - return value - } - - private let node: XMLNode - - internal init(node: XMLNode) { - self.node = node - } - - @StringBuilder internal func build() -> String { - - if let name = name { - ".custom(key: \"\(name)\", value: \"\(value ?? "")\")\n" - } - } - } - - private struct EventProperty<T: RawRepresentable> { - - private var name: T? { - - guard let name = node.localName else { - return nil - } - - return T(rawValue: name.lowercased() as! T.RawValue) - } - - private var value: String? { - - guard let value = node.stringValue else { - return nil - } - - return value - } - - private let node: XMLNode - - internal init(node: XMLNode) { - self.node = node - } - - @StringBuilder internal func build() -> String { - - if let name = name { - ".on(event: .\(name), \"\(value ?? "")\")\n" - } - } - } -} - -public protocol InitRepresentable { - - init?(value: String) -} - -extension String: InitRepresentable { - - public init?(value: String) { - self.init(value) - } -} - -extension Float: InitRepresentable { - - public init?(value: String) { - self.init(value) - } -} - -extension Int: InitRepresentable { - - public init?(value: String) { - self.init(value) - } -} - -extension Double: InitRepresentable { - - public init?(value: String) { - self.init(value) - } -} - -extension Bool: InitRepresentable { - - public init?(value: String) { - self.init(value) - } -} diff --git a/Tests/HTMLKitTests/Conversion/articles/article.html b/Tests/ConverterTests/Conversion/articles/article.html similarity index 100% rename from Tests/HTMLKitTests/Conversion/articles/article.html rename to Tests/ConverterTests/Conversion/articles/article.html diff --git a/Tests/HTMLKitTests/Conversion/component.html b/Tests/ConverterTests/Conversion/component.html similarity index 100% rename from Tests/HTMLKitTests/Conversion/component.html rename to Tests/ConverterTests/Conversion/component.html diff --git a/Tests/HTMLKitTests/Conversion/index.html b/Tests/ConverterTests/Conversion/index.html similarity index 100% rename from Tests/HTMLKitTests/Conversion/index.html rename to Tests/ConverterTests/Conversion/index.html diff --git a/Tests/HTMLKitTests/ConversionTests.swift b/Tests/ConverterTests/ConversionTests.swift similarity index 53% rename from Tests/HTMLKitTests/ConversionTests.swift rename to Tests/ConverterTests/ConversionTests.swift index 98c48b02..c10e36fe 100644 --- a/Tests/HTMLKitTests/ConversionTests.swift +++ b/Tests/ConverterTests/ConversionTests.swift @@ -1,4 +1,4 @@ -import HTMLKit +import Converter import XCTest final class ConversionTests: XCTestCase { @@ -20,24 +20,7 @@ final class ConversionTests: XCTestCase { return XCTFail("No directory.") } - XCTAssertNoThrow(try Converter.default.convert(directory: directory, option: .print)) -#endif - } - - func testStringConversion() throws { - -#if os(Linux) - throw XCTSkip("Requires macOS >= 11.0") -#else - guard let directory = directory else { - return XCTFail("No directory.") - } - - guard let content = try? String(contentsOf: directory.appendingPathComponent("component.html")) else { - return XCTFail("No file.") - } - - XCTAssertNoThrow(try Converter.default.convert(html: content)) + XCTAssertNoThrow(try Converter.default.convert(source: directory)) #endif } }