diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml new file mode 100644 index 0000000..02d6daa --- /dev/null +++ b/.github/workflows/actions.yml @@ -0,0 +1,37 @@ +name: Actions + +on: + pull_request: + branches: + - main + +jobs: + + bb_checks: + name: BB Checks + uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main + with: + local_swift_dependencies_check_enabled : true + + swiftlang_checks: + name: Swiftlang Checks + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "Toucan" + format_check_enabled : true + broken_symlink_check_enabled : true + unacceptable_language_check_enabled : true + api_breakage_check_enabled : false + docs_check_enabled : false + license_header_check_enabled : false + shell_check_enabled : false + yamllint_check_enabled : false + python_lint_check_enabled : false + + swiftlang_tests: + name: Swiftlang Tests + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_windows_checks : false + linux_build_command: "swift test --parallel --enable-code-coverage" + linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"nightly\"}, {\"swift_version\": \"nightly-main\"}, {\"swift_version\": \"nightly-6.0\"}, {\"swift_version\": \"nightly-6.1\"}]" \ No newline at end of file diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..58d21f0 --- /dev/null +++ b/.swift-format @@ -0,0 +1,64 @@ +{ + "version": 1, + "lineLength": 80, + "maximumBlankLines": 1, + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "tabWidth": 4, + "indentation": { + "spaces": 4 + }, + "indentConditionalCompilationBlocks": false, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": true, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "spacesAroundRangeFormationOperators": false, + "multiElementCollectionTrailingCommas": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": true, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true + } +} diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 0000000..c73cf0c --- /dev/null +++ b/.swiftformatignore @@ -0,0 +1 @@ +Package.swift \ No newline at end of file diff --git a/LICENSE b/LICENSE index ca9fb45..705dd77 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2018-2022 Tibor Bödecs +Copyright (c) 2022-2025 Binary Birds Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/Makefile b/Makefile index fd99366..65b960e 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,52 @@ +SHELL=/bin/bash + +.PHONY: docker + +baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts + +check: symlinks language deps lint + +symlinks: + curl -s $(baseUrl)/check-broken-symlinks.sh | bash + +language: + curl -s $(baseUrl)/check-unacceptable-language.sh | bash + +deps: + curl -s $(baseUrl)/check-local-swift-dependencies.sh | bash + +lint: + curl -s $(baseUrl)/run-swift-format.sh | bash + +fmt: + swiftformat . + +format: + curl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix + +headers: + curl -s $(baseUrl)/check-swift-headers.sh | bash + +fix-headers: + curl -s $(baseUrl)/check-swift-headers.sh | bash -s -- --fix + +build: + swift build + +release: + swift build -c release + test: - swift test --enable-test-discovery --parallel - -docs: - jazzy \ - --clean \ - --author "Tibor Bödecs" \ - --author_url https://twitter.com/tiborbodecs/ \ - --module-version 1.6.0 \ - --module SwiftHtml \ - --output docs/ + swift test --parallel + +test-with-coverage: + swift test --parallel --enable-code-coverage + +clean: + rm -rf .build + +docker-run: + docker run --rm -v $(pwd):/app -it swift:6.0 + +docker-tests: + docker build -t swift-html-tests . -f ./docker/Dockerfile.testing && docker run --rm swift-html-tests diff --git a/Package.swift b/Package.swift index a7c00a6..d7cd6ab 100644 --- a/Package.swift +++ b/Package.swift @@ -1,51 +1,104 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 import PackageDescription +let defaultSwiftSettings: [SwiftSetting] = [ + .swiftLanguageMode(.v6), + .enableExperimentalFeature("AvailabilityMacro=SwiftHTML 1.0:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), + + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md + .enableUpcomingFeature("MemberImportVisibility"), +] + let package = Package( name: "swift-html", - platforms: [ - .macOS(.v10_15) - ], products: [ - .library(name: "SwiftSgml", targets: ["SwiftSgml"]), - .library(name: "SwiftHtml", targets: ["SwiftHtml"]), - .library(name: "SwiftSvg", targets: ["SwiftSvg"]), + .library(name: "DOM", targets: ["DOM"]), + .library(name: "SGML", targets: ["SGML"]), + .library(name: "SwiftHTML", targets: ["SwiftHTML"]), + .library(name: "SwiftRSS", targets: ["SwiftRSS"]), .library(name: "SwiftSitemap", targets: ["SwiftSitemap"]), - .library(name: "SwiftRss", targets: ["SwiftRss"]), + .library(name: "SwiftSVG", targets: ["SwiftSVG"]), ], dependencies: [ - +// .package(url: "https://github.com/apple/swift-collections", .upToNextMinor(from: "1.3.0")), ], targets: [ - .target(name: "SwiftSgml", dependencies: []), - .target(name: "SwiftHtml", dependencies: [ - .target(name: "SwiftSgml") - ]), - .target(name: "SwiftSvg", dependencies: [ - .target(name: "SwiftSgml") - ]), - .target(name: "SwiftSitemap", dependencies: [ - .target(name: "SwiftSgml") - ]), - .target(name: "SwiftRss", dependencies: [ - .target(name: "SwiftSgml") - ]), - .testTarget(name: "SwiftSgmlTests", dependencies: [ - .target(name: "SwiftSgml"), - ]), - .testTarget(name: "SwiftHtmlTests", dependencies: [ - .target(name: "SwiftHtml"), - ]), - .testTarget(name: "SwiftSvgTests", dependencies: [ - .target(name: "SwiftSvg"), - ]), - .testTarget(name: "SwiftSitemapTests", dependencies: [ - .target(name: "SwiftSitemap"), - ]), - .testTarget(name: "SwiftRssTests", dependencies: [ - .target(name: "SwiftRss"), - ]), + .target( + name: "DOM", + swiftSettings: defaultSwiftSettings + ), + .target( + name: "SGML", + dependencies: [ +// .product(name: "Collections", package: "swift-collections"), + .target(name: "DOM"), + ], + swiftSettings: defaultSwiftSettings + ), + .target( + name: "SwiftHTML", + dependencies: [ + .target(name: "SGML"), + ], + swiftSettings: defaultSwiftSettings + ), + .target( + name: "SwiftRSS", + dependencies: [ + .target(name: "SGML"), + ], + swiftSettings: defaultSwiftSettings + ), + .target( + name: "SwiftSitemap", + dependencies: [ + .target(name: "SGML"), + ], + swiftSettings: defaultSwiftSettings + ), + .target( + name: "SwiftSVG", + dependencies: [ + .target(name: "SGML"), + ], + swiftSettings: defaultSwiftSettings + ), + // MARK: - test + .testTarget( + name: "DOMTests", + dependencies: [ + .target(name: "DOM"), + ] + ), + .testTarget( + name: "SGMLTests", + dependencies: [ + .target(name: "SGML"), + ] + ), + .testTarget( + name: "SwiftHTMLTests", + dependencies: [ + .target(name: "SwiftHTML"), + ] + ), + .testTarget( + name: "SwiftRSSTests", + dependencies: [ + .target(name: "SwiftRSS"), + ] + ), + .testTarget( + name: "SwiftSitemapTests", + dependencies: [ + .target(name: "SwiftSitemap"), + ] + ), + .testTarget( + name: "SwiftSVGTests", + dependencies: [ + .target(name: "SwiftSVG"), + ] + ), ] ) - - diff --git a/README.md b/README.md index 7acb9ee..39aae18 100644 --- a/README.md +++ b/README.md @@ -1,250 +1,240 @@ -# SwiftHtml +# SwiftHTML -An awesome Swift HTML DSL library using result builders. +An awesome Swift HTML DSL library using result builders that closely follows the W3C standards. ```swift -import SwiftHtml - -let doc = Document(.html) { - Html { - Head { - Title("Hello Swift HTML DSL") - - Meta().charset("utf-8") - Meta().name(.viewport).content("width=device-width, initial-scale=1") - - Link(rel: .stylesheet).href("./css/style.css") - } - Body { - Main { - Div { - Section { - Img(src: "./images/swift.png", alt: "Swift Logo") - .title("Picture of the Swift Logo") - H1("Lorem ipsum") - .class("red") - P("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") - .class(["green", "blue"]) - .spellcheck(false) - } - - A("Download SwiftHtml now!") - .href("https://github.com/binarybirds/swift-html/") - .target(.blank) - .download() - - Abbr("WTFPL") - .title("Do What The Fuck You Want To Public License") - } - } - .class("container") - - Script().src("./js/main.js").async() +import SwiftHTML + +let html = Html { + Head { + Title("Hello, SwiftHTML!") + Meta().charset("utf-8") + Meta().name(.viewport).content("width=device-width, initial-scale=1") + Link(rel: .stylesheet).href("./css/style.css") + } + Body { + H1("Hello, SwiftHTML!") + Ul { + Li("Type-safe HTML DSL for Swift 6+") + Li("Concurrency-safety; sendable support") + Li("Contains all the HTML tag definitions") + Li("RSS, Sitemap, SVG support as well") } + + Script(#"console.log("Hello, SwiftHTML!")"#) + Script().src("./js/main.js").async() } } -let html = DocumentRenderer(minify: false, indent: 2).render(doc) -print(html) +let result = Document(type: .html, root: html).render(indent: 4) +print(result) // HTML output ``` +## Installation -## Install +`SwiftHTML` is distributed through **Swift Package Manager**. -You can simply use `SwiftHtml` as a dependency via the Swift Package Manager: +Add the package to your `Package.swift`: ```swift -.package(url: "https://github.com/binarybirds/swift-html", from: "1.6.0"), +.package(url: "https://github.com/binarybirds/swift-html", from: "2.0.0"), ``` -Add the `SwiftHtml` product from the `swift-html` package as a dependency to your target: +Then include the `SwiftHTML` product as a dependency for your target: ```swift -.product(name: "SwiftHtml", package: "swift-html"), +.product(name: "SwiftHTML", package: "swift-html"), ``` -Import the framework: +Import the module in your source files: ```swift -import SwiftHtml +import SwiftHTML ``` -That's it. +The package is now ready to use. -## Creating custom tags +## DOM vs. SGML -You can define your own custom tags by subclassing the `Tag` or `EmptyTag` class. +The **DOM** library provides the foundational data structures used to construct and render a `Node`-based object tree. +This tree is composed of the following node types: -You can follow the same pattern if you take a look at the core tags. +- **`CommentNode`** — represents an HTML/XML-style comment (``) +- **`ListNode`** — a container node used to group child nodes +- **`ShortNode`** — a void (self-closing) element representation (``) +- **`StandardNode`** — a normal element with opening and closing tags (``) +- **`TextNode`** — raw textual content within the tree -```swift -open class Div: Tag { +These node types form the low-level DOM representation used by the renderer. -} +--- -//
- standard tag +## SGML Elements -open class Br: EmptyTag { - -} -//
- no closing tag +The **SGML** library provides a higher-level API for defining and constructing markup languages. +It is designed to support the creation of any XML-based format—including **HTML**, **RSS**, **SVG**, and custom schemas. -``` +You can define your own elements by conforming to one of the following protocols: -By default the name of the tag is automatically derived from the class name (lowercased), but you can also create your own tag type & name by overriding the `createNode()` class function. +- **`Element`** — the base protocol representing a generic element backed by a `Node` +- **`Tag`** — a named element; inherits from `Element` +- **`ShortTag`** — a named, void (self-closing) tag; inherits from `Tag` +- **`StandardTag`** — a named element with both opening and closing tags; inherits from `Tag` + +### Examples + +Here is a minimal example of defining a custom short tag: ```swift -open class LastBuildDate: Tag { +public struct Br: ShortTag { + + public var attributes: AttributeStore - open override class func createNode() -> Node { - Node(type: .standard, name: "lastBuildDate") + public init() { + attributes = .init() } } - -// - standard tag with custom name ``` -It is also possible to create tags with altered content or default attributes. +A standard tag can be represented as follows, including result-builder support provided by the `@ElementBuilder` attribute: ```swift -open class Description: Tag { - - public init(_ contents: String) { - super.init() - setContents("") +public struct P: StandardTag { + + public var attributes: AttributeStore + public var children: [Element] + + public init( + _ contents: String + ) { + self.attributes = .init() + self.children = [ + Text(contents) + ] } -} -// - content wrapped in CDATA -open class Rss: Tag { - - public init(@TagBuilder _ builder: () -> Tag) { - super.init(builder()) - setAttributes([ - .init(key: "version", value: "2.0"), - ]) + public init( + children: [Element] + ) { + self.attributes = .init() + self.children = children + } + + public init( + @ElementBuilder _ block: () -> [Element] + ) { + self.init(children: block()) } } -// ... - tag with a default attribute ``` -## Attribute management +### Custom tag names -You can set, add or delete the attributes of a given tag. +By default, the tag name is automatically derived from the type name (converted to lowercase). +It is also possible to override the static `name` property manually: ```swift -Leaf("example") - // set (override) the current attributes - .setAttributes([ - .init(key: "a", value: "foo"), - .init(key: "b", value: "bar"), - .init(key: "c", value: "baz"), - ]) - // add a new attribute using a key & value - .attribute("foo", "example") - // add a new flag attribute (without a value) - .flagAttribute("bar") - // delete an attribute by using a key - .deleteAttribute("b") +struct LastBuildDate: StandardTag { + + static let name = "lastBuildDate" -// + // ... +} ``` -You can also manage the class atrribute through helper methods. - -```swift -Span("foo") - // set (override) class values - .class("a", "b", "c") - // add new class values - .class(add: ["d", "e", "f"]) - // add new class value if the condition is true - .class(add: "b", true) - /// remove multiple class values - .class(remove: ["b", "c", "d"]) - /// remove a class value if the condition is true - .class(remove: "e", true) - -// -``` +### Attributes -You can create your own attribute modifier via an extension. +You can define custom element attributes by conforming to the `Attribute` protocol. +By default, the attribute name is automatically derived from the type name, but this behavior can be overridden when needed: ```swift -public extension Guid { +// very simple attribute +struct Class: Attribute { + var value: String? - func isPermalink(_ value: Bool = true) -> Self { - attribute("isPermalink", String(value)) + init(_ value: Value) { + self.value = value } } -``` - -There are other built-in type-safe attribute modifiers available on tags. - -## Composing tags +// custom name and value type +struct Alignment: Attribute { -You can come up with your own `Tag` composition system by introducing a new protocol. + enum Value: String { + case left + case right + } -```swift -protocol TagRepresentable { + static let name = "align" + var value: String? - @TagBuilder - func build() -> Tag + init(_ value: Value) { + self.value = value.rawValue + } } +``` -struct ListComponent: TagRepresentable { +You can set, add, or remove attributes—or even modify individual attribute values—on any tag that supports attributes: - let items: [String] - - init(_ items: [String]) { - self.items = items - } +```swift +P("Lorem ipsum") + // set (override) the current attributes + .setAttribute(Class("note")) + .setAttributeValueBy(name: "style", value: "color: white;") + .setAttributes([ + Alignment(.left) + ]) + // add attribute or value(s) + .addAttributeValue(Class("important")) + .addAttributeValueBy(name: "style", value: "background: black;") + .addAttributeValues([ + Class("large") + ]) + // remove attribute or value(s) + .removeAttributeBy(Class.self) + .removeAttributeBy(name: "style") + .removeAttributeValueBy( + Alignment(.left) + ) +``` - func build() -> Tag { - Ul { - for item in items { - Li(item) - } - } - } -} +There are built-in, type-safe attributes and helper modifiers available for the standard tags. -let tag = ListComponent(["a", "b", "c"]).build() -``` +### Conditions -This way it is also possible to extend the `TagBuilder` to support the new protocol. +Use the `check` modifier to evaluate a condition and update an element when the condition is met: ```swift -extension TagBuilder { +let condition = false - static func buildExpression(_ expression: Tag) -> Tag { - expression +H1("Lorem ipsum") + .check(condition) { + $0.class("foo") + } else: { + $0.class("bar") } - - static func buildExpression(_ expression: TagRepresentable) -> Tag { - expression.build() - } -} ``` -Sometimes you'll need extra parameters for the build function, so you have to call the build method by hand. +### Container elements -In those cases it is recommended to introduce a `render` function instead of using build. +It is also possible to define tags that contain child elements; these are referred to as *container elements*. +All standard tags support child elements by default. ```swift - -let tag = WebIndexTemplate(ctx) { - ListComponent(["a", "b", "c"]) - .render(req) -} -.render(req) +// TODO ``` -If you want to create a lightweight template engine for the [Vapor](https://vapor.codes/) web framework using SwiftHtml, you can see a working example inside the [Feather CMS core](https://github.com/FeatherCMS/feather-core) repository. +## Future improvements + +- [ ] Finish attributes (global, event) +- [ ] Finish content models (use `assert` & types) +- [ ] Get rid of public enums +- [ ] Get rid of `@_exported SGML` +- [ ] Add `OrderedDictionary` support? ## Credits & references +- [HTML Standard](https://html.spec.whatwg.org/multipage/) - [HTML Reference](https://www.w3schools.com/tags/default.asp) diff --git a/Sources/DOM/Node.swift b/Sources/DOM/Node.swift new file mode 100644 index 0000000..fc41096 --- /dev/null +++ b/Sources/DOM/Node.swift @@ -0,0 +1,13 @@ +public protocol Node: Sendable { + // You should never implement this protocol +} + +extension Node { + + public func render( + indent: UInt8 = 0 + ) -> String { + let renderer = Renderer(indent: indent) + return renderer.render(node: self) + } +} diff --git a/Sources/DOM/Nodes/CommentNode.swift b/Sources/DOM/Nodes/CommentNode.swift new file mode 100644 index 0000000..b0489f9 --- /dev/null +++ b/Sources/DOM/Nodes/CommentNode.swift @@ -0,0 +1,10 @@ +public struct CommentNode: Node { + + public var value: String + + public init( + value: String + ) { + self.value = value + } +} diff --git a/Sources/DOM/Nodes/ListNode.swift b/Sources/DOM/Nodes/ListNode.swift new file mode 100644 index 0000000..d253f26 --- /dev/null +++ b/Sources/DOM/Nodes/ListNode.swift @@ -0,0 +1,10 @@ +public struct ListNode: Node { + + public var items: [Node] + + public init( + items: [Node] + ) { + self.items = items + } +} diff --git a/Sources/DOM/Nodes/ShortNode.swift b/Sources/DOM/Nodes/ShortNode.swift new file mode 100644 index 0000000..0f11b21 --- /dev/null +++ b/Sources/DOM/Nodes/ShortNode.swift @@ -0,0 +1,13 @@ +public struct ShortNode: Node { + + public var name: String + public var properties: [Property] + + public init( + name: String, + properties: [Property] = [] + ) { + self.name = name + self.properties = properties + } +} diff --git a/Sources/DOM/Nodes/StandardNode.swift b/Sources/DOM/Nodes/StandardNode.swift new file mode 100644 index 0000000..d274949 --- /dev/null +++ b/Sources/DOM/Nodes/StandardNode.swift @@ -0,0 +1,18 @@ +public struct StandardNode: Node { + + public var name: String + public var properties: [Property] + public var children: [Node] { list.items } + + private var list: ListNode + + public init( + name: String, + properties: [Property] = [], + children: [Node] = [] + ) { + self.name = name + self.properties = properties + self.list = .init(items: children) + } +} diff --git a/Sources/DOM/Nodes/TextNode.swift b/Sources/DOM/Nodes/TextNode.swift new file mode 100644 index 0000000..f3d15c3 --- /dev/null +++ b/Sources/DOM/Nodes/TextNode.swift @@ -0,0 +1,13 @@ +public struct TextNode: Node { + + public var value: String + public var ignoreRenderIndentation: Bool + + public init( + value: String, + ignoreRenderIndentation: Bool = false + ) { + self.value = value + self.ignoreRenderIndentation = ignoreRenderIndentation + } +} diff --git a/Sources/DOM/Property.swift b/Sources/DOM/Property.swift new file mode 100644 index 0000000..1094c34 --- /dev/null +++ b/Sources/DOM/Property.swift @@ -0,0 +1,13 @@ +public struct Property: Sendable { + + public var name: String + public var value: String? + + public init( + name: String, + value: String? = nil + ) { + self.name = name + self.value = value + } +} diff --git a/Sources/DOM/Renderer.swift b/Sources/DOM/Renderer.swift new file mode 100644 index 0000000..32b8985 --- /dev/null +++ b/Sources/DOM/Renderer.swift @@ -0,0 +1,208 @@ +public struct Renderer { + + public var indent: UInt8 + + public init( + indent: UInt8 = 0 + ) { + self.indent = indent + } + + public func render( + node: Node + ) -> String { + if indent == 0 { + return renderInline(node) + } + return renderWithIndentation(node) + } + + // MARK: - internal + + func indentation( + for level: UInt8 + ) -> String { + .init(repeating: " ", count: Int(indent) * Int(level)) + } + + func renderAttributeList( + _ attributes: [Property] + ) -> String { + let attributesList = + attributes + .map { renderAttribute($0) } + .joined(separator: " ") + if !attributesList.isEmpty { + return " " + attributesList + } + return attributesList + } + + func renderAttribute( + _ attribute: Property + ) -> String { + if let value = attribute.value { + return #"\#(attribute.name)="\#(value)""# + } + return attribute.name + } + + func renderStandardOpening( + _ node: StandardNode + ) -> String { + let attributesList = renderAttributeList(node.properties) + return "<\(node.name)\(attributesList)>" + } + + func renderStandardClosing( + _ node: StandardNode + ) -> String { + "" + } + + func renderShort( + _ node: ShortNode + ) -> String { + let attributesList = renderAttributeList(node.properties) + return "<\(node.name)\(attributesList)>" + } + + func renderComment( + _ node: CommentNode + ) -> String { + "" + } + + // MARK: - rendering + + func renderInline( + _ node: Node + ) -> String { + switch node { + case let node as ListNode: + return node.items + .map { renderInline($0) } + .joined() + case let node as StandardNode: + let openingTag = renderStandardOpening(node) + let closingTag = renderStandardClosing(node) + let childrenInline = node.children + .map { renderInline($0) } + .joined() + return openingTag + childrenInline + closingTag + case let node as ShortNode: + return renderShort(node) + case let node as CommentNode: + return renderComment(node) + case let node as TextNode: + return node.value + default: + fatalError("Unknown node type `\(String(describing: node))`.") + } + } + + func renderWithIndentation( + _ node: Node, + level: UInt8 = 0, + isInsideList: Bool = false + ) -> String { + let (spaces, newline) = + indent > 0 ? (indentation(for: level), "\n") : ("", "") + var result: String = "" + switch node { + case let node as ListNode: + for item in node.items { + result += renderWithIndentation( + item, + level: level, + isInsideList: true + ) + } + case let node as StandardNode: + let items = node.children + let openingTag = renderStandardOpening(node) + let closingTag = renderStandardClosing(node) + + // Special case: empty children + if items.isEmpty { + result += spaces + result += openingTag + result += closingTag + if level > 0 { + result += newline + } + } + // Special case: begins with a TextNode + else if let firstText = items.first as? TextNode { + // Ignore render identation is true for the text node + if firstText.ignoreRenderIndentation { + result += spaces + result += openingTag + for child in items { + if let text = child as? TextNode, + text.ignoreRenderIndentation + { + result += text.value + } + else { + result += renderInline(child) + } + } + result += closingTag + if level > 0 { + result += newline + } + } + else { + result += spaces + result += renderInline(node) + if level > 0 { + result += newline + } + } + } + // Block form: children on their own lines + else { + result += spaces + result += openingTag + result += newline + for child in items { + result += renderWithIndentation( + child, + level: level + 1, + isInsideList: true + ) + } + result += spaces + result += closingTag + if level > 0 { + result += newline + } + } + case let node as ShortNode: + let shortTag = renderShort(node) + result += spaces + result += shortTag + result += isInsideList ? newline : "" + case let node as CommentNode: + let commentTag = renderComment(node) + result += spaces + result += commentTag + result += isInsideList ? newline : "" + case let node as TextNode: + if node.ignoreRenderIndentation { + result += node.value + } + else { + result += spaces + result += node.value + if isInsideList { + result += newline + } + } + default: + fatalError("Unknown node type `\(String(describing: node))`.") + } + return result + } +} diff --git a/Sources/SGML/Attributes/Attribute.swift b/Sources/SGML/Attributes/Attribute.swift new file mode 100644 index 0000000..8a9b609 --- /dev/null +++ b/Sources/SGML/Attributes/Attribute.swift @@ -0,0 +1,11 @@ +public protocol Attribute: Sendable { + static var name: String { get } + var value: String? { get } +} + +extension Attribute { + + public static var name: String { + .init(describing: self).lowercased() + } +} diff --git a/Sources/SGML/Attributes/AttributeStore.swift b/Sources/SGML/Attributes/AttributeStore.swift new file mode 100644 index 0000000..8a7e34d --- /dev/null +++ b/Sources/SGML/Attributes/AttributeStore.swift @@ -0,0 +1,104 @@ +import DOM + +//import Collections + +public struct AttributeStore: Sendable { + + private var storage: [String: [String?]] + + public init() { + self.storage = [:] + } + + // MARK: - api + + public mutating func set( + name: String, + value: String? + ) { + storage[name] = [value] + } + + public mutating func add( + name: String, + value: String? + ) { + if storage[name] == nil { + storage[name] = [] + } + guard let value, !value.isEmpty else { + return + } + guard !storage[name]!.contains(value) else { + return + } + storage[name]?.append(value) + } + + public mutating func remove( + name: String + ) { + storage[name] = nil + } + + public mutating func remove( + name: String, + value: String?, + preservingEmptyAttribute: Bool = false + ) { + guard storage[name] != nil else { + return + } + storage[name] = storage[name]!.filter { $0 != value } + + if !preservingEmptyAttribute { + if storage[name]!.isEmpty { + storage[name] = nil + } + } + } + + public func has( + name: String + ) -> Bool { + storage[name] != nil + } + + public func has( + name: String, + value: String? + ) -> Bool { + if let values = storage[name] { + return values.contains(value) + } + return false + } + + public func get( + name: String + ) -> String? { + storage[name]?.compactMap { $0 }.sorted().joined(separator: " ") + } + + // MARK: - DOM + + public var properties: [Property] { + storage.map { name, value in + let values = value.compactMap { $0 }.sorted() + return .init( + name: name, + value: values.isEmpty ? nil : values.joined(separator: " ") + ) + } + .sorted { lhs, rhs in + let lhsNil = (lhs.value == nil) + let rhsNil = (rhs.value == nil) + + // valued first, nil-valued at end + if lhsNil != rhsNil { + return !lhsNil && rhsNil + } + return lhs.name < rhs.name + } + } +} diff --git a/Sources/SGML/Builders/Builder.swift b/Sources/SGML/Builders/Builder.swift new file mode 100644 index 0000000..3e5ed71 --- /dev/null +++ b/Sources/SGML/Builders/Builder.swift @@ -0,0 +1,51 @@ +@resultBuilder +public enum Builder { + + // Turn a single expression into a component + public static func buildExpression( + _ expression: Element + ) -> [Element] { + [expression] + } + + // Combine components from the block + public static func buildBlock( + _ components: [Element]... + ) -> [Element] { + components.flatMap { $0 } + } + + // if without else + public static func buildOptional( + _ component: [Element]? + ) -> [Element] { + component ?? [] + } + + // if/else + public static func buildEither( + first component: [Element] + ) -> [Element] { + component + } + + public static func buildEither( + second component: [Element] + ) -> [Element] { + component + } + + // for loops + public static func buildArray( + _ components: [[Element]] + ) -> [Element] { + components.flatMap { $0 } + } + + // #available, etc. + public static func buildLimitedAvailability( + _ component: [Element] + ) -> [Element] { + component + } +} diff --git a/Sources/SGML/Documents/DocType.swift b/Sources/SGML/Documents/DocType.swift new file mode 100644 index 0000000..c50e1eb --- /dev/null +++ b/Sources/SGML/Documents/DocType.swift @@ -0,0 +1,14 @@ +public enum DocType: Sendable { + case unspecified + case html + case xml + + /// + /// HTML 4.01: + /// + /// + /// XHTML 1.1: + /// + /// + case custom(String) +} diff --git a/Sources/SGML/Documents/Document.swift b/Sources/SGML/Documents/Document.swift new file mode 100644 index 0000000..77f52e7 --- /dev/null +++ b/Sources/SGML/Documents/Document.swift @@ -0,0 +1,20 @@ +public struct Document: Sendable { + + public let type: DocType + public let root: Element + + public init( + type: DocType = .unspecified, + root: Element + ) { + self.type = type + self.root = root + } + + public func render( + indent: UInt8 = 0 + ) -> String { + let renderer = Renderer(indent: indent) + return renderer.render(document: self) + } +} diff --git a/Sources/SGML/Elements/Comment.swift b/Sources/SGML/Elements/Comment.swift new file mode 100644 index 0000000..7186e7e --- /dev/null +++ b/Sources/SGML/Elements/Comment.swift @@ -0,0 +1,14 @@ +import DOM + +public struct Comment: Element { + + public var value: String + + public init(_ value: String) { + self.value = value + } + + public var node: Node { + CommentNode(value: value) + } +} diff --git a/Sources/SGML/Elements/Element.swift b/Sources/SGML/Elements/Element.swift new file mode 100644 index 0000000..a59a963 --- /dev/null +++ b/Sources/SGML/Elements/Element.swift @@ -0,0 +1,5 @@ +import DOM + +public protocol Element: Sendable { + var node: Node { get } +} diff --git a/Sources/SGML/Elements/Text.swift b/Sources/SGML/Elements/Text.swift new file mode 100644 index 0000000..0fc55f8 --- /dev/null +++ b/Sources/SGML/Elements/Text.swift @@ -0,0 +1,22 @@ +import DOM + +public struct Text: Element { + + public var text: String + public var isRaw: Bool + + public init( + _ text: String, + isRaw: Bool = false + ) { + self.text = text + self.isRaw = isRaw + } + + public var node: Node { + TextNode( + value: text, + ignoreRenderIndentation: isRaw + ) + } +} diff --git a/Sources/SGML/Renderer.swift b/Sources/SGML/Renderer.swift new file mode 100644 index 0000000..8d9bbd2 --- /dev/null +++ b/Sources/SGML/Renderer.swift @@ -0,0 +1,43 @@ +import DOM + +public struct Renderer: Sendable { + + public var indent: UInt8 + + public init( + indent: UInt8 = 0 + ) { + self.indent = indent + } + + public func render( + document: Document + ) -> String { + let renderer = DOM.Renderer( + indent: indent + ) + let doctype = render(type: document.type) + let doc = renderer.render(node: document.root.node) + if indent > 0, !doctype.isEmpty { + return doctype + "\n" + doc + } + return doctype + doc + } + + // MARK: - private + + private func render( + type: DocType + ) -> String { + switch type { + case .unspecified: + "" + case .html: + #""# + case .xml: + #""# + case .custom(let value): + value + } + } +} diff --git a/Sources/SGML/Tags/ShortTag.swift b/Sources/SGML/Tags/ShortTag.swift new file mode 100644 index 0000000..5c5d1a2 --- /dev/null +++ b/Sources/SGML/Tags/ShortTag.swift @@ -0,0 +1,15 @@ +import DOM + +public protocol ShortTag: Tag, Attributes { + +} + +extension ShortTag { + + public var node: Node { + ShortNode( + name: Self.name, + properties: attributes.properties + ) + } +} diff --git a/Sources/SGML/Tags/StandardTag.swift b/Sources/SGML/Tags/StandardTag.swift new file mode 100644 index 0000000..39707b1 --- /dev/null +++ b/Sources/SGML/Tags/StandardTag.swift @@ -0,0 +1,16 @@ +import DOM + +public protocol StandardTag: Tag, Container, Attributes { + +} + +extension StandardTag { + + public var node: Node { + StandardNode( + name: Self.name, + properties: attributes.properties, + children: children.map(\.node) + ) + } +} diff --git a/Sources/SGML/Tags/Tag.swift b/Sources/SGML/Tags/Tag.swift new file mode 100644 index 0000000..b896b9c --- /dev/null +++ b/Sources/SGML/Tags/Tag.swift @@ -0,0 +1,12 @@ +import DOM + +public protocol Tag: Element, Mutable { + static var name: String { get } +} + +extension Tag { + + public static var name: String { + .init(describing: self).lowercased() + } +} diff --git a/Sources/SGML/Traits/Attributes.swift b/Sources/SGML/Traits/Attributes.swift new file mode 100644 index 0000000..d172f25 --- /dev/null +++ b/Sources/SGML/Traits/Attributes.swift @@ -0,0 +1,142 @@ +public protocol Attributes { + var attributes: AttributeStore { get set } +} + +extension Attributes where Self: Mutable { + + public func setAttribute( + name: String, + value: String? + ) -> Self { + modify { + $0.attributes.set(name: name, value: value) + } + } + + public func setAttribute( + _ attribute: T + ) -> Self { + setAttribute(name: T.name, value: attribute.value) + } + + public func setAttributes( + _ attributes: [T] + ) -> Self { + modify { + for attribute in attributes { + $0.attributes.set(name: T.name, value: attribute.value) + } + } + } + + // MARK: - add + + public func addAttribute( + name: String, + value: String? + ) -> Self { + modify { + $0.attributes.add(name: name, value: value) + } + } + + public func addAttribute( + _ attribute: T + ) -> Self { + addAttribute(name: T.name, value: attribute.value) + } + + public func addAttributes( + _ attributes: [T] + ) -> Self { + modify { + for attribute in attributes { + $0.attributes.add(name: T.name, value: attribute.value) + } + } + } + + // MARK: - remove + + public func removeAttribute( + name: String + ) -> Self { + modify { + $0.attributes.remove(name: name) + } + } + + public func removeAttribute( + _: T.Type + ) -> Self { + removeAttribute(name: T.name) + } + + public func removeAttribute( + name: String, + value: String?, + preservingEmptyAttribute: Bool = false + ) -> Self { + modify { + $0.attributes.remove( + name: name, + value: value, + preservingEmptyAttribute: preservingEmptyAttribute + ) + } + } + + public func removeAttribute( + _ attribute: T, + preservingEmptyAttribute: Bool = false + ) -> Self { + removeAttribute( + name: T.name, + value: attribute.value, + preservingEmptyAttribute: preservingEmptyAttribute + ) + } + + // MARK: - has + + public func hasAttribute( + name: String + ) -> Bool { + attributes.has(name: name) + + } + + public func hasAttribute( + _: T.Type + ) -> Bool { + hasAttribute(name: T.name) + } + + public func hasAttribute( + name: String, + value: String? + ) -> Bool { + attributes.has(name: name, value: value) + + } + + public func hasAttribute( + _ attribute: T + ) -> Bool { + hasAttribute(name: T.name, value: attribute.value) + } + + // MARK: - get + + public func getAttribute( + name: String + ) -> String? { + attributes.get(name: name) + } + + public func getAttribute( + _: T.Type + ) -> String? { + getAttribute(name: T.name) + } +} diff --git a/Sources/SGML/Traits/Container.swift b/Sources/SGML/Traits/Container.swift new file mode 100644 index 0000000..802fac3 --- /dev/null +++ b/Sources/SGML/Traits/Container.swift @@ -0,0 +1,22 @@ +public protocol Container { + var children: [Element] { get set } +} + +extension Container where Self: Mutable { + + public func addChild( + _ element: Element + ) -> Self { + modify { + $0.children.append(element) + } + } + + public func addChildren( + _ elements: [T] + ) -> Self { + modify { + $0.children.append(contentsOf: elements) + } + } +} diff --git a/Sources/SGML/Traits/Mutable.swift b/Sources/SGML/Traits/Mutable.swift new file mode 100644 index 0000000..9410fa7 --- /dev/null +++ b/Sources/SGML/Traits/Mutable.swift @@ -0,0 +1,17 @@ +public protocol Mutable: Sendable { + + func modify( + _ block: (inout Self) -> Void + ) -> Self +} + +extension Mutable { + + public func modify( + _ block: (inout Self) -> Void + ) -> Self { + var mutableSelf = self + block(&mutableSelf) + return mutableSelf + } +} diff --git a/Sources/SwiftCSS/MediaQuery.swift b/Sources/SwiftCSS/MediaQuery.swift new file mode 100644 index 0000000..ba9ae16 --- /dev/null +++ b/Sources/SwiftCSS/MediaQuery.swift @@ -0,0 +1,145 @@ +///// Represents a CSS media query. +//public struct MediaQuery: Sendable { +// +// /// Operators. +// public enum Operators: String, Sendable { +// /// Specifies an AND operator. +// case and +// /// Specifies a NOT operator. +// case not +// /// Specifies an OR operator. +// case or = "," +// } +// +// /// Devices. +// public enum Devices: String, Sendable { +// /// Default; suitable for all devices. +// case all +// /// Speech synthesizers. +// case aural +// /// Braille feedback devices. +// case braille +// /// Handheld devices (small screen, limited bandwidth). +// case handheld +// /// Projectors. +// case projection +// /// Print preview mode/printed pages. +// case print +// /// Computer screens. +// case screen +// /// Teletypes and similar media using a fixed-pitch character grid. +// case tty +// /// Television type devices (low resolution, limited scroll ability). +// case tv +// +// } +// +// /// Device orientation. +// public enum Orientation: String, Sendable { +// /// Portrait orientation. +// case portrait +// /// Landscape orientation. +// case landscape +// } +// +// /// Scan +// public enum Scan: String, Sendable { +// /// Progressive scan. +// case progressive +// /// Interlace scan. +// case interlace +// } +// +// /// Grid. +// public enum Grid: String, Sendable { +// /// Grid. +// case yes = "1" +// /// Bitmap. +// case no = "0" +// } +// +// /// Device color scheme. +// public enum ColorScheme: String, Sendable { +// /// Light mode. +// case light +// /// Dark mode. +// case dark +// } +// +// public enum Prefix: String, Sendable { +// case none = "" +// case min +// case max +// } +// +// public enum VVV: Sendable { +// /// Specifies the width of the targeted display area. +// case width(Prefix, String) +// /// Specifies the height of the targeted display area. +// case height(Prefix, String) +// /// Specifies the width of the target display/paper. +// case deviceWidth(Prefix, String) +// /// Specifies the height of the target display/paper. +// case deviceHeight(Prefix, String) +// /// Specifies the orientation of the target display/paper. +// case orientation(Orientation) +// /// Specifies the width/height ratio of the targeted display area. +// case aspectRatio(Prefix, String) +// /// Specifies the device-width/device-height ratio of the target display/paper. +// case deviceAspectRatio(Prefix, String) +// /// Specifies the bits per color of target display. +// case color(Prefix, String) +// /// Specifies the number of colors the target display can handle. +// case colorIndex(Prefix, String) +// /// Specifies the bits per pixel in a monochrome frame buffer. +// case monochrome(Prefix, String) +// /// Specifies the pixel density (dpi or dpcm) of the target display/paper. +// case resolution(Prefix, String) +// /// Specifies scanning method of a tv display. +// case scan(Scan) +// /// Specifies if the output device is grid or bitmap. +// case grid(Grid) +// /// Color scheme preference. +// case prefersColorScheme(ColorScheme) +// } +// +// /// Raw representation of the media query. +// var value: String +//} +// +//extension MediaQuery { +// +// public struct Value { +// +// let rawValue: String +// +// private init(_ rawValue: String) { +// self.rawValue = rawValue +// } +// +// /// Device width in pixels. +// public static func deviceWidth(px: Int) -> Self { +// .init("(device-width: \(px)px)") +// } +// +// /// Device height in pixels. +// public static func deviceHeight(px: Int) -> Self { +// .init("(device-height: \(px)px)") +// } +// +// /// Device pixel ratio with webkit prefix. +// public static func webkitDevicePixelRatio(_ value: Int) -> Self { +// .init("(device-pixel-ratio: \(value))") +// } +// +// /// Device orientation. +// public static func orientation(_ value: Orientation) -> Self { +// .init("(orientation: \(value.rawValue))") +// } +// +// /// Preferred color scheme. +// public static func prefersColorScheme(_ value: ColorScheme) -> Self { +// .init("(prefers-color-scheme: \(value.rawValue))") +// } +// } +//} diff --git a/Sources/SwiftHTML/Attributes/ActionAttribute.swift b/Sources/SwiftHTML/Attributes/ActionAttribute.swift new file mode 100644 index 0000000..ec8c185 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ActionAttribute.swift @@ -0,0 +1,22 @@ +public struct ActionAttribute: HTMLAttribute { + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol ActionAttributeModifier { + +} + +extension ActionAttributeModifier where Self: Attributes & Mutable { + + public func action( + _ value: String? + ) -> Self { + setAttribute(ActionAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/AltAttribute.swift b/Sources/SwiftHTML/Attributes/AltAttribute.swift new file mode 100644 index 0000000..d578401 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/AltAttribute.swift @@ -0,0 +1,22 @@ +public struct AltAttribute: HTMLAttribute { + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol AltAttributeModifier { + +} + +extension AltAttributeModifier where Self: Attributes & Mutable { + + public func alt( + _ value: String? + ) -> Self { + setAttribute(AltAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/AutocompleteAttribute.swift b/Sources/SwiftHTML/Attributes/AutocompleteAttribute.swift new file mode 100644 index 0000000..fee9f17 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/AutocompleteAttribute.swift @@ -0,0 +1,18 @@ +public struct AutocompleteAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol AutocompleteAttributeModifier { + +} + +extension AutocompleteAttributeModifier where Self: Attributes & Mutable { + + public func autocomplete() -> Self { + setAttribute(AutocompleteAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/AutofocusAttribute.swift b/Sources/SwiftHTML/Attributes/AutofocusAttribute.swift new file mode 100644 index 0000000..e13bf98 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/AutofocusAttribute.swift @@ -0,0 +1,18 @@ +public struct AutofocusAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol AutofocusAttributeModifier { + +} + +extension AutofocusAttributeModifier where Self: Attributes & Mutable { + + public func autofocus() -> Self { + setAttribute(AutofocusAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/AutoplayAttribute.swift b/Sources/SwiftHTML/Attributes/AutoplayAttribute.swift new file mode 100644 index 0000000..d5bc154 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/AutoplayAttribute.swift @@ -0,0 +1,18 @@ +public struct AutoplayAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol AutoplayAttributeModifier { + +} + +extension AutoplayAttributeModifier where Self: Attributes & Mutable { + + public func autoplay() -> Self { + setAttribute(AutoplayAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/BlockingAttribute.swift b/Sources/SwiftHTML/Attributes/BlockingAttribute.swift new file mode 100644 index 0000000..979fd31 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/BlockingAttribute.swift @@ -0,0 +1,27 @@ +public struct BlockingAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + case render + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol BlockingAttributeModifier { + +} + +extension BlockingAttributeModifier where Self: Attributes & Mutable { + + public func blocking( + _ value: BlockingAttribute.Value? = .render + ) -> Self { + setAttribute(BlockingAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/CheckedAttribute.swift b/Sources/SwiftHTML/Attributes/CheckedAttribute.swift new file mode 100644 index 0000000..ca0c927 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/CheckedAttribute.swift @@ -0,0 +1,18 @@ +public struct CheckedAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol CheckedAttributeModifier { + +} + +extension CheckedAttributeModifier where Self: Attributes & Mutable { + + public func checked() -> Self { + setAttribute(CheckedAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/CiteAttribute.swift b/Sources/SwiftHTML/Attributes/CiteAttribute.swift new file mode 100644 index 0000000..b8ec478 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/CiteAttribute.swift @@ -0,0 +1,24 @@ +public struct CiteAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol CiteAttributeModifier { + +} + +extension CiteAttributeModifier where Self: Attributes & Mutable { + + /// Sets a cite attribute. + public func cite( + _ value: String? + ) -> Self { + setAttribute(CiteAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/ClassAttribute.swift b/Sources/SwiftHTML/Attributes/ClassAttribute.swift new file mode 100644 index 0000000..e46a44b --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ClassAttribute.swift @@ -0,0 +1,69 @@ +public struct ClassAttribute: HTMLAttribute { + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol ClassAttributeModifier { + +} + +extension ClassAttributeModifier where Self: Attributes & Mutable { + + /// Sets a class attribute. + public func setClass( + _ value: String? + ) -> Self { + setAttribute(ClassAttribute(value)) + } + + /// Adds a class attribute. + public func addClass( + _ value: String? + ) -> Self { + addAttribute(ClassAttribute(value)) + } + + /// Removes a class attribute. + public func removeClass( + _ value: String? + ) -> Self { + removeAttribute(ClassAttribute(value)) + } + + /// Toggles a class attribute. + public func toggleClass( + _ value: String? + ) -> Self { + if hasAttribute(ClassAttribute(value)) { + removeAttribute(ClassAttribute(value)) + } + else { + addAttribute(ClassAttribute(value)) + } + } + + // MARK: - + + /// Add class attribute values. + public func `class`( + _ values: [String] + ) -> Self { + var mutatingSelf = self + for item in values { + mutatingSelf = mutatingSelf.addClass(item) + } + return mutatingSelf + } + + /// Add class attribute values. + public func `class`( + _ values: String... + ) -> Self { + `class`(values) + } +} diff --git a/Sources/SwiftHTML/Attributes/ClosedbyAttribute.swift b/Sources/SwiftHTML/Attributes/ClosedbyAttribute.swift new file mode 100644 index 0000000..f609ac4 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ClosedbyAttribute.swift @@ -0,0 +1,29 @@ +public struct ClosedbyAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + case any + case closeRequest + case none + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol ClosedbyAttributeModifier { + +} + +extension ClosedbyAttributeModifier where Self: Attributes & Mutable { + + public func closedby( + _ value: ClosedbyAttribute.Value? + ) -> Self { + setAttribute(ClosedbyAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/ColSpanAttribute.swift b/Sources/SwiftHTML/Attributes/ColSpanAttribute.swift new file mode 100644 index 0000000..e75dcab --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ColSpanAttribute.swift @@ -0,0 +1,23 @@ +public struct ColspanAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: Int? = nil + ) { + self.value = value.map { String($0) } + } +} + +public protocol ColspanAttributeModifier { + +} + +extension ColspanAttributeModifier where Self: Attributes & Mutable { + + public func colspan( + _ value: Int? + ) -> Self { + setAttribute(ColspanAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/ControlsAttribute.swift b/Sources/SwiftHTML/Attributes/ControlsAttribute.swift new file mode 100644 index 0000000..90e8166 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ControlsAttribute.swift @@ -0,0 +1,18 @@ +public struct ControlsAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol ControlsAttributeModifier { + +} + +extension ControlsAttributeModifier where Self: Attributes & Mutable { + + public func controls() -> Self { + setAttribute(ControlsAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/CrossoriginAttribute.swift b/Sources/SwiftHTML/Attributes/CrossoriginAttribute.swift new file mode 100644 index 0000000..9dfe0b4 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/CrossoriginAttribute.swift @@ -0,0 +1,28 @@ +public struct CrossoriginAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + case anonymous + case useCredentials = "use-credentials" + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol CrossoriginAttributeModifier { + +} + +extension CrossoriginAttributeModifier where Self: Attributes & Mutable { + + public func crossorigin( + _ value: CrossoriginAttribute.Value? + ) -> Self { + setAttribute(CrossoriginAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/DataAttribute.swift b/Sources/SwiftHTML/Attributes/DataAttribute.swift new file mode 100644 index 0000000..48e3205 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/DataAttribute.swift @@ -0,0 +1,23 @@ +public struct DataAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol DataAttributeModifier { + +} + +extension DataAttributeModifier where Self: Attributes & Mutable { + + public func data( + _ value: String? + ) -> Self { + setAttribute(DataAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/DatetimeAttribute.swift b/Sources/SwiftHTML/Attributes/DatetimeAttribute.swift new file mode 100644 index 0000000..e6e5c01 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/DatetimeAttribute.swift @@ -0,0 +1,24 @@ +public struct DatetimeAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol DatetimeAttributeModifier { + +} + +extension DatetimeAttributeModifier where Self: Attributes & Mutable { + + /// Sets an Datetime attribute. + public func datetime( + _ value: String? + ) -> Self { + setAttribute(DatetimeAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/DirAttribute.swift b/Sources/SwiftHTML/Attributes/DirAttribute.swift new file mode 100644 index 0000000..1d3ecb1 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/DirAttribute.swift @@ -0,0 +1,32 @@ +public struct DirAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + /// The contents of the element are explicitly directionally isolated left-to-right text. + case ltr + /// The contents of the element are explicitly directionally isolated right-to-left text. + case rtl + /// The contents of the element are explicitly directionally isolated text, but the direction is to be determined programmatically using the contents of the element (as described below). + case auto + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol DirAttributeModifier { + +} + +extension DirAttributeModifier where Self: Attributes & Mutable { + + public func dir( + _ value: DirAttribute.Value? + ) -> Self { + setAttribute(DirAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/DisabledAttribute.swift b/Sources/SwiftHTML/Attributes/DisabledAttribute.swift new file mode 100644 index 0000000..15c7ec0 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/DisabledAttribute.swift @@ -0,0 +1,18 @@ +public struct DisabledAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol DisabledAttributeModifier { + +} + +extension DisabledAttributeModifier where Self: Attributes & Mutable { + + public func disabled() -> Self { + setAttribute(DisabledAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/DownloadAttribute.swift b/Sources/SwiftHTML/Attributes/DownloadAttribute.swift new file mode 100644 index 0000000..c6a6313 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/DownloadAttribute.swift @@ -0,0 +1,27 @@ +public struct DownloadAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol DownloadAttributeModifier { + +} + +extension DownloadAttributeModifier where Self: Attributes & Mutable { + + public func download( + _ value: String? + ) -> Self { + setAttribute(DownloadAttribute(value)) + } + + public func download() -> Self { + setAttribute(DownloadAttribute(nil)) + } +} diff --git a/Sources/SwiftHTML/Attributes/EnctypeAttribute.swift b/Sources/SwiftHTML/Attributes/EnctypeAttribute.swift new file mode 100644 index 0000000..1121854 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/EnctypeAttribute.swift @@ -0,0 +1,29 @@ +public struct EnctypeAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + case urlencoded = "application/x-www-form-urlencoded" + case multipart = "multipart/form-data" + case plain = "text/plain" + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol EnctypeAttributeModifier { + +} + +extension EnctypeAttributeModifier where Self: Attributes & Mutable { + + public func enctype( + _ value: EnctypeAttribute.Value? + ) -> Self { + setAttribute(EnctypeAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/FetchpriorityAttribute.swift b/Sources/SwiftHTML/Attributes/FetchpriorityAttribute.swift new file mode 100644 index 0000000..4e8126c --- /dev/null +++ b/Sources/SwiftHTML/Attributes/FetchpriorityAttribute.swift @@ -0,0 +1,32 @@ +public struct FetchpriorityAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + /// Fetch the external script at a high priority relative to other external scripts. + case high + /// Fetch the external script at a low priority relative to other external scripts. + case low + /// Don't set a preference for the fetch priority. This is the default. It is used if no value or an invalid value is set. + case auto + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol FetchpriorityAttributeModifier { + +} + +extension FetchpriorityAttributeModifier where Self: Attributes & Mutable { + + public func fetchpriority( + _ value: FetchpriorityAttribute.Value? + ) -> Self { + setAttribute(FetchpriorityAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/ForAttribute.swift b/Sources/SwiftHTML/Attributes/ForAttribute.swift new file mode 100644 index 0000000..d2ec225 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ForAttribute.swift @@ -0,0 +1,22 @@ +public struct ForAttribute: HTMLAttribute { + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol ForAttributeModifier { + +} + +extension ForAttributeModifier where Self: Attributes & Mutable { + + public func `for`( + _ value: String? + ) -> Self { + setAttribute(ForAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/FormActionAttribute.swift b/Sources/SwiftHTML/Attributes/FormActionAttribute.swift new file mode 100644 index 0000000..8f9fb99 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/FormActionAttribute.swift @@ -0,0 +1,22 @@ +public struct FormActionAttribute: HTMLAttribute { + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol FormActionAttributeModifier { + +} + +extension FormActionAttributeModifier where Self: Attributes & Mutable { + + public func formAction( + _ value: String? + ) -> Self { + setAttribute(FormActionAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/FormAttribute.swift b/Sources/SwiftHTML/Attributes/FormAttribute.swift new file mode 100644 index 0000000..232f3c2 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/FormAttribute.swift @@ -0,0 +1,24 @@ +public struct FormAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol FormAttributeModifier { + +} + +extension FormAttributeModifier where Self: Attributes & Mutable { + + /// Sets an Form attribute. + public func form( + _ value: String? + ) -> Self { + setAttribute(FormAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/FormEnctypeAttribute.swift b/Sources/SwiftHTML/Attributes/FormEnctypeAttribute.swift new file mode 100644 index 0000000..afdff90 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/FormEnctypeAttribute.swift @@ -0,0 +1,23 @@ +public struct FormEnctypeAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: EnctypeAttribute.Value? + ) { + self.value = value?.rawValue + } +} + +public protocol FormEnctypeAttributeModifier { + +} + +extension FormEnctypeAttributeModifier where Self: Attributes & Mutable { + + public func formEnctype( + _ value: EnctypeAttribute.Value? + ) -> Self { + setAttribute(FormEnctypeAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/FormMethodAttribute.swift b/Sources/SwiftHTML/Attributes/FormMethodAttribute.swift new file mode 100644 index 0000000..aae5936 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/FormMethodAttribute.swift @@ -0,0 +1,23 @@ +public struct FormMethodAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: MethodAttribute.Value? + ) { + self.value = value?.rawValue + } +} + +public protocol FormMethodAttributeModifier { + +} + +extension FormMethodAttributeModifier where Self: Attributes & Mutable { + + public func formMethod( + _ value: MethodAttribute.Value? + ) -> Self { + setAttribute(FormMethodAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/FormTargetAttribute.swift b/Sources/SwiftHTML/Attributes/FormTargetAttribute.swift new file mode 100644 index 0000000..8c4ae2c --- /dev/null +++ b/Sources/SwiftHTML/Attributes/FormTargetAttribute.swift @@ -0,0 +1,51 @@ +public struct FormTargetAttribute: HTMLAttribute { + + public enum Value { + /// Opens the linked document in a new window or tab + case blank + /// Opens the linked document in the same frame as it was clicked (this is default) + case `default` + /// Opens the linked document in the parent frame + case parent + /// Opens the linked document in the full body of the window + case top + /// Opens the linked document in the named iframe + case frame(String) + + var rawValue: String { + switch self { + case .blank: + return "_blank" + case .`default`: + return "_self" + case .parent: + return "_parent" + case .top: + return "_top" + case .frame(let name): + return name + } + } + } + + public var value: String? + + public init( + _ value: FormTargetAttribute.Value? + ) { + self.value = value?.rawValue + } +} + +public protocol FormTargetAttributeModifier { + +} + +extension FormTargetAttributeModifier where Self: Attributes & Mutable { + + public func formTarget( + _ value: FormTargetAttribute.Value? + ) -> Self { + setAttribute(FormTargetAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/HeightAttribute.swift b/Sources/SwiftHTML/Attributes/HeightAttribute.swift new file mode 100644 index 0000000..8fa3c14 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/HeightAttribute.swift @@ -0,0 +1,23 @@ +public struct HeightAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: Int? = nil + ) { + self.value = value.map { String($0) } + } +} + +public protocol HeightAttributeModifier { + +} + +extension HeightAttributeModifier where Self: Attributes & Mutable { + + public func height( + _ value: Int? + ) -> Self { + setAttribute(HeightAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/HrefAttribute.swift b/Sources/SwiftHTML/Attributes/HrefAttribute.swift new file mode 100644 index 0000000..b832460 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/HrefAttribute.swift @@ -0,0 +1,24 @@ +public struct HrefAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol HrefAttributeModifier { + +} + +extension HrefAttributeModifier where Self: Attributes & Mutable { + + /// Sets a href attribute. + public func href( + _ value: String? + ) -> Self { + setAttribute(HrefAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/HreflangAttribute.swift b/Sources/SwiftHTML/Attributes/HreflangAttribute.swift new file mode 100644 index 0000000..6df2683 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/HreflangAttribute.swift @@ -0,0 +1,23 @@ +public struct HreflangAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol HreflangAttributeModifier { + +} + +extension HreflangAttributeModifier where Self: Attributes & Mutable { + + public func hreflang( + _ value: String? + ) -> Self { + setAttribute(HrefAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/IdAttribute.swift b/Sources/SwiftHTML/Attributes/IdAttribute.swift new file mode 100644 index 0000000..186c529 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/IdAttribute.swift @@ -0,0 +1,24 @@ +public struct IdAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol IdAttributeModifier { + +} + +extension IdAttributeModifier where Self: Attributes & Mutable { + + /// Sets an id attribute. + public func id( + _ value: String? + ) -> Self { + setAttribute(IdAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/IntegrityAttribute.swift b/Sources/SwiftHTML/Attributes/IntegrityAttribute.swift new file mode 100644 index 0000000..3f2682d --- /dev/null +++ b/Sources/SwiftHTML/Attributes/IntegrityAttribute.swift @@ -0,0 +1,23 @@ +public struct IntegrityAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol IntegrityAttributeModifier { + +} + +extension IntegrityAttributeModifier where Self: Attributes & Mutable { + + public func integrity( + _ value: String? + ) -> Self { + setAttribute(IntegrityAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/LabelAttribute.swift b/Sources/SwiftHTML/Attributes/LabelAttribute.swift new file mode 100644 index 0000000..b4b901f --- /dev/null +++ b/Sources/SwiftHTML/Attributes/LabelAttribute.swift @@ -0,0 +1,22 @@ +public struct LabelAttribute: HTMLAttribute { + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol LabelAttributeModifier { + +} + +extension LabelAttributeModifier where Self: Attributes & Mutable { + + public func label( + _ value: String? + ) -> Self { + setAttribute(LabelAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/LoadingAttribute.swift b/Sources/SwiftHTML/Attributes/LoadingAttribute.swift new file mode 100644 index 0000000..86242c8 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/LoadingAttribute.swift @@ -0,0 +1,28 @@ +public struct LoadingAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + case eager + case lazy + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol LoadingAttributeModifier { + +} + +extension LoadingAttributeModifier where Self: Attributes & Mutable { + + public func loading( + _ value: LoadingAttribute.Value? + ) -> Self { + setAttribute(LoadingAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/LoopAttribute.swift b/Sources/SwiftHTML/Attributes/LoopAttribute.swift new file mode 100644 index 0000000..e8742d9 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/LoopAttribute.swift @@ -0,0 +1,18 @@ +public struct LoopAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol LoopAttributeModifier { + +} + +extension LoopAttributeModifier where Self: Attributes & Mutable { + + public func loop() -> Self { + setAttribute(LoopAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/MediaAttribute.swift b/Sources/SwiftHTML/Attributes/MediaAttribute.swift new file mode 100644 index 0000000..900d2da --- /dev/null +++ b/Sources/SwiftHTML/Attributes/MediaAttribute.swift @@ -0,0 +1,24 @@ +public struct MediaAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? + ) { + self.value = value + } +} + +public protocol MediaAttributeModifier { + +} + +extension MediaAttributeModifier where Self: Attributes & Mutable { + + /// Specifies on what device the linked document will be displayed. + public func media( + _ value: String + ) -> Self { + setAttribute(MediaAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/MethodAttribute.swift b/Sources/SwiftHTML/Attributes/MethodAttribute.swift new file mode 100644 index 0000000..cd9416d --- /dev/null +++ b/Sources/SwiftHTML/Attributes/MethodAttribute.swift @@ -0,0 +1,28 @@ +public struct MethodAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + case get + case post + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol MethodAttributeModifier { + +} + +extension MethodAttributeModifier where Self: Attributes & Mutable { + + public func method( + _ value: MethodAttribute.Value? + ) -> Self { + setAttribute(MethodAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/MultipleAttribute.swift b/Sources/SwiftHTML/Attributes/MultipleAttribute.swift new file mode 100644 index 0000000..79fc200 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/MultipleAttribute.swift @@ -0,0 +1,18 @@ +public struct MultipleAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol MultipleAttributeModifier { + +} + +extension MultipleAttributeModifier where Self: Attributes & Mutable { + + public func multiple() -> Self { + setAttribute(MultipleAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/MutedAttribute.swift b/Sources/SwiftHTML/Attributes/MutedAttribute.swift new file mode 100644 index 0000000..9f5677c --- /dev/null +++ b/Sources/SwiftHTML/Attributes/MutedAttribute.swift @@ -0,0 +1,18 @@ +public struct MutedAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol MutedAttributeModifier { + +} + +extension MutedAttributeModifier where Self: Attributes & Mutable { + + public func muted() -> Self { + setAttribute(MutedAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/NameAttribute.swift b/Sources/SwiftHTML/Attributes/NameAttribute.swift new file mode 100644 index 0000000..deb30da --- /dev/null +++ b/Sources/SwiftHTML/Attributes/NameAttribute.swift @@ -0,0 +1,24 @@ +public struct NameAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol NameAttributeModifier { + +} + +extension NameAttributeModifier where Self: Attributes & Mutable { + + /// Sets a name attribute. + public func name( + _ value: String? + ) -> Self { + setAttribute(NameAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/OpenAttribute.swift b/Sources/SwiftHTML/Attributes/OpenAttribute.swift new file mode 100644 index 0000000..49f8f10 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/OpenAttribute.swift @@ -0,0 +1,18 @@ +public struct OpenAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol OpenAttributeModifier { + +} + +extension OpenAttributeModifier where Self: Attributes & Mutable { + + public func open() -> Self { + setAttribute(OpenAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/PingAttribute.swift b/Sources/SwiftHTML/Attributes/PingAttribute.swift new file mode 100644 index 0000000..19508ca --- /dev/null +++ b/Sources/SwiftHTML/Attributes/PingAttribute.swift @@ -0,0 +1,31 @@ +public struct PingAttribute: HTMLAttribute { + public var value: String? + + public init( + _ value: [String]? = nil + ) { + self.value = value?.joined(separator: " ") + } +} + +public protocol PingAttributeModifier { + +} + +extension PingAttributeModifier where Self: Attributes & Mutable { + + public func ping( + _ value: String? + ) -> Self { + if let value { + return ping([value]) + } + return setAttribute(PingAttribute(nil)) + } + + public func ping( + _ value: [String] + ) -> Self { + setAttribute(PingAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/PlaceholderAttribute.swift b/Sources/SwiftHTML/Attributes/PlaceholderAttribute.swift new file mode 100644 index 0000000..7a8e0c4 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/PlaceholderAttribute.swift @@ -0,0 +1,23 @@ +public struct PlaceholderAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol PlaceholderAttributeModifier { + +} + +extension PlaceholderAttributeModifier where Self: Attributes & Mutable { + + public func placeholder( + _ value: String? + ) -> Self { + setAttribute(PlaceholderAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/PreloadAttribute.swift b/Sources/SwiftHTML/Attributes/PreloadAttribute.swift new file mode 100644 index 0000000..1798371 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/PreloadAttribute.swift @@ -0,0 +1,29 @@ +public struct PreloadAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + case auto + case metadata + case none + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol PreloadAttributeModifier { + +} + +extension PreloadAttributeModifier where Self: Attributes & Mutable { + + public func preload( + _ value: PreloadAttribute.Value? + ) -> Self { + setAttribute(PreloadAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/ReadonlyAttribute.swift b/Sources/SwiftHTML/Attributes/ReadonlyAttribute.swift new file mode 100644 index 0000000..9fa72b2 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ReadonlyAttribute.swift @@ -0,0 +1,18 @@ +public struct ReadonlyAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol ReadonlyAttributeModifier { + +} + +extension ReadonlyAttributeModifier where Self: Attributes & Mutable { + + public func readonly() -> Self { + setAttribute(ReadonlyAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/ReferrerPolicyAttribute.swift b/Sources/SwiftHTML/Attributes/ReferrerPolicyAttribute.swift new file mode 100644 index 0000000..25acb26 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ReferrerPolicyAttribute.swift @@ -0,0 +1,43 @@ +public struct ReferrerPolicyAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + /// No referrer information is sent + case noReferrer = "no-referrer" + /// Default. Sends the origin, path, and query string if the protocol security level stays the same or is higher (HTTP to HTTP, HTTPS to HTTPS, HTTP to HTTPS is ok). Sends nothing to less secure level (HTTPS to HTTP is not ok) + case noReferrerWhenDowngrade = "no-referrer-when-downgrade" + /// Sends the origin (scheme, host, and port) of the document + case origin + /// Sends the origin of the document for cross-origin request. Sends the origin, path, and query string for same-origin request + case originWhenCrossOrigin = "origin-when-cross-origin" + /// Sends a referrer for same-origin request. Sends no referrer for cross-origin request + case sameOrigin = "same-origin" + /// ??? + ///case strictOrigin = "strict-origin" + /// Sends the origin if the protocol security level stays the same or is higher (HTTP to HTTP, HTTPS to HTTPS, and HTTP to HTTPS is ok). Sends nothing to less secure level (HTTPS to HTTP) + case strictOriginWhenCrossOrigin = "strict-origin-when-cross-origin" + /// Sends the origin, path, and query string (regardless of security). Use this value carefully! + case unsafeUrl = "unsafe-url" + } + + public var value: String? + + public init( + _ value: Value + ) { + self.value = value.rawValue + } +} + +public protocol ReferrerPolicyAttributeModifier { + +} + +extension ReferrerPolicyAttributeModifier where Self: Attributes & Mutable { + + /// Set a referrer policy attribute. + public func referrerPolicy( + _ value: ReferrerPolicyAttribute.Value + ) -> Self { + setAttribute(ReferrerPolicyAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/RelAttribute.swift b/Sources/SwiftHTML/Attributes/RelAttribute.swift new file mode 100644 index 0000000..638fc28 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/RelAttribute.swift @@ -0,0 +1,55 @@ +public struct RelAttribute: HTMLAttribute { + public static let name = "rel" + + public enum Value: String, Sendable { + /// Provides a link to an alternate representation of the document (i.e. print page, translated or mirror) + case alternate + /// Provides a link to the author of the document + case author + /// Permanent URL used for bookmarking + case bookmark + /// Indicates that the referenced document is not part of the same site as the current document + case external + /// Provides a link to a help document + case help + /// Provides a link to licensing information for the document + case license + /// Provides a link to the next document in the series + case next + /// Links to an unendorsed document, like a paid link. + /// ("nofollow" is used by Google, to specify that the Google search spider should not follow that link) + case nofollow + /// Requires that any browsing context created by following the hyperlink must not have an opener browsing context + case noopenero + /// Makes the referrer unknown. No referer header will be included when the user clicks the hyperlink + case noreferrer + /// The previous document in a selection + case prev + /// Links to a search tool for the document + case search + /// A tag (keyword) for the current document + case tag + } + + public var value: String? + + public init( + _ value: Value + ) { + self.value = value.rawValue + } +} + +public protocol RelAttributeModifier { + +} + +extension RelAttributeModifier where Self: Attributes & Mutable { + + /// Set a rel attribute. + public func rel( + _ value: RelAttribute.Value + ) -> Self { + setAttribute(RelAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/RequiredAttribute.swift b/Sources/SwiftHTML/Attributes/RequiredAttribute.swift new file mode 100644 index 0000000..9a650a3 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/RequiredAttribute.swift @@ -0,0 +1,18 @@ +public struct RequiredAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol RequiredAttributeModifier { + +} + +extension RequiredAttributeModifier where Self: Attributes & Mutable { + + public func required() -> Self { + setAttribute(RequiredAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/RowspanAttribute.swift b/Sources/SwiftHTML/Attributes/RowspanAttribute.swift new file mode 100644 index 0000000..55b86cf --- /dev/null +++ b/Sources/SwiftHTML/Attributes/RowspanAttribute.swift @@ -0,0 +1,23 @@ +public struct RowspanAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: Int? = nil + ) { + self.value = value.map { String($0) } + } +} + +public protocol RowspanAttributeModifier { + +} + +extension RowspanAttributeModifier where Self: Attributes & Mutable { + + public func rowspan( + _ value: Int? + ) -> Self { + setAttribute(RowspanAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/SizeAttribute.swift b/Sources/SwiftHTML/Attributes/SizeAttribute.swift new file mode 100644 index 0000000..9b9ce88 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/SizeAttribute.swift @@ -0,0 +1,23 @@ +public struct SizeAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: Int? = nil + ) { + self.value = value.map { String($0) } + } +} + +public protocol SizeAttributeModifier { + +} + +extension SizeAttributeModifier where Self: Attributes & Mutable { + + public func size( + _ value: Int? + ) -> Self { + setAttribute(SizeAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/SizesAttribute.swift b/Sources/SwiftHTML/Attributes/SizesAttribute.swift new file mode 100644 index 0000000..31a99f1 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/SizesAttribute.swift @@ -0,0 +1,23 @@ +public struct SizesAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol SizesAttributeModifier { + +} + +extension SizesAttributeModifier where Self: Attributes & Mutable { + + public func sizes( + _ value: String? + ) -> Self { + setAttribute(SizesAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/SpanAttribute.swift b/Sources/SwiftHTML/Attributes/SpanAttribute.swift new file mode 100644 index 0000000..7124c3e --- /dev/null +++ b/Sources/SwiftHTML/Attributes/SpanAttribute.swift @@ -0,0 +1,23 @@ +public struct SpanAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: Int? = nil + ) { + self.value = value.map { String($0) } + } +} + +public protocol SpanAttributeModifier { + +} + +extension SpanAttributeModifier where Self: Attributes & Mutable { + + public func span( + _ value: Int? + ) -> Self { + setAttribute(SpanAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/SrcAttribute.swift b/Sources/SwiftHTML/Attributes/SrcAttribute.swift new file mode 100644 index 0000000..f65eb88 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/SrcAttribute.swift @@ -0,0 +1,23 @@ +public struct SrcAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol SrcAttributeModifier { + +} + +extension SrcAttributeModifier where Self: Attributes & Mutable { + + public func src( + _ value: String? + ) -> Self { + setAttribute(SrcAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/SrcsetAttribute.swift b/Sources/SwiftHTML/Attributes/SrcsetAttribute.swift new file mode 100644 index 0000000..c01e96d --- /dev/null +++ b/Sources/SwiftHTML/Attributes/SrcsetAttribute.swift @@ -0,0 +1,23 @@ +public struct SrcsetAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol SrcsetAttributeModifier { + +} + +extension SrcsetAttributeModifier where Self: Attributes & Mutable { + + public func srcset( + _ value: String? + ) -> Self { + setAttribute(SrcsetAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/StyleAttribute.swift b/Sources/SwiftHTML/Attributes/StyleAttribute.swift new file mode 100644 index 0000000..dce7d27 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/StyleAttribute.swift @@ -0,0 +1,24 @@ +public struct StyleAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol StyleAttributeModifier { + +} + +extension StyleAttributeModifier where Self: Attributes & Mutable { + + /// Sets an style attribute. + public func style( + _ value: String? + ) -> Self { + setAttribute(StyleAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/TargetAttribute.swift b/Sources/SwiftHTML/Attributes/TargetAttribute.swift new file mode 100644 index 0000000..017153d --- /dev/null +++ b/Sources/SwiftHTML/Attributes/TargetAttribute.swift @@ -0,0 +1,35 @@ +public struct TargetAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + /// Opens the link in a new window or tab. + case blank = "_blank" + /// Default; opens the link in the same frame as it was clicked. + case `self` = "_self" + /// Opens the link in the parent frame. + case parent = "_parent" + /// Opens the link in the full body of the window. + case top = "_top" + } + + public var value: String? + + public init( + _ value: Value + ) { + self.value = value.rawValue + } +} + +public protocol TargetAttributeModifier { + +} + +extension TargetAttributeModifier where Self: Attributes & Mutable { + + /// Sets a target attribute. + public func target( + _ value: TargetAttribute.Value + ) -> Self { + setAttribute(TargetAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/TitleAttribute.swift b/Sources/SwiftHTML/Attributes/TitleAttribute.swift new file mode 100644 index 0000000..e5b534e --- /dev/null +++ b/Sources/SwiftHTML/Attributes/TitleAttribute.swift @@ -0,0 +1,36 @@ +public struct TitleAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +/// A type that can modify the `title` attribute on an element. +/// +/// Conform to this protocol to gain the `title(_:)` convenience API +/// for setting the HTML `title` attribute via attribute storage. +public protocol TitleAttributeModifier { + +} + +extension TitleAttributeModifier where Self: Attributes & Mutable { + + /// Sets the HTML `title` attribute on the receiver. + /// + /// Use this to provide advisory information, such as a tooltip, + /// that is shown when the user hovers over the element. + /// + /// - Parameter value: The value of the `title` attribute. Pass + /// `nil` to remove the attribute from the element. + /// + /// - Returns: A modified copy of the element with the updated `title` attribute. + public func title( + _ value: String? + ) -> Self { + setAttribute(TitleAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/TranslateAttribute.swift b/Sources/SwiftHTML/Attributes/TranslateAttribute.swift new file mode 100644 index 0000000..ade10ab --- /dev/null +++ b/Sources/SwiftHTML/Attributes/TranslateAttribute.swift @@ -0,0 +1,30 @@ +public struct TranslateAttribute: HTMLAttribute { + + public enum Value: String, Sendable { + /// Specifies that the content of the element should be translated. + case yes + /// Specifies that the content of the element must not be translated. + case no + } + + public var value: String? + + public init( + _ value: Value? + ) { + self.value = value?.rawValue + } +} + +public protocol TranslateAttributeModifier { + +} + +extension TranslateAttributeModifier where Self: Attributes & Mutable { + + public func translate( + _ value: TranslateAttribute.Value? + ) -> Self { + setAttribute(TranslateAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/TypeAttribute.swift b/Sources/SwiftHTML/Attributes/TypeAttribute.swift new file mode 100644 index 0000000..5048bd3 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/TypeAttribute.swift @@ -0,0 +1,23 @@ +public struct TypeAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol TypeAttributeModifier { + +} + +extension TypeAttributeModifier where Self: Attributes & Mutable { + + public func type( + _ value: String? + ) -> Self { + setAttribute(TypeAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/TypemustmatchAttribute.swift b/Sources/SwiftHTML/Attributes/TypemustmatchAttribute.swift new file mode 100644 index 0000000..754f9c7 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/TypemustmatchAttribute.swift @@ -0,0 +1,18 @@ +public struct TypemustmatchAttribute: HTMLAttribute { + public var value: String? + + public init() { + self.value = nil + } +} + +public protocol TypemustmatchAttributeModifier { + +} + +extension TypemustmatchAttributeModifier where Self: Attributes & Mutable { + + public func typemustmatch() -> Self { + setAttribute(TypemustmatchAttribute()) + } +} diff --git a/Sources/SwiftHTML/Attributes/UsemapAttribute.swift b/Sources/SwiftHTML/Attributes/UsemapAttribute.swift new file mode 100644 index 0000000..f1f2122 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/UsemapAttribute.swift @@ -0,0 +1,28 @@ +public struct UsemapAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + if let value, !value.isEmpty { + self.value = "#" + value + } + else { + self.value = nil + } + } +} + +public protocol UsemapAttributeModifier { + +} + +extension UsemapAttributeModifier where Self: Attributes & Mutable { + + public func usemap( + _ value: String? + ) -> Self { + setAttribute(UsemapAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/ValueAttribute.swift b/Sources/SwiftHTML/Attributes/ValueAttribute.swift new file mode 100644 index 0000000..c21717c --- /dev/null +++ b/Sources/SwiftHTML/Attributes/ValueAttribute.swift @@ -0,0 +1,23 @@ +public struct ValueAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? = nil + ) { + self.value = value + } +} + +public protocol ValueAttributeModifier { + +} + +extension ValueAttributeModifier where Self: Attributes & Mutable { + + public func value( + _ value: String? + ) -> Self { + setAttribute(ValueAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/WidthAttribute.swift b/Sources/SwiftHTML/Attributes/WidthAttribute.swift new file mode 100644 index 0000000..97bc195 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/WidthAttribute.swift @@ -0,0 +1,23 @@ +public struct WidthAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: Int? = nil + ) { + self.value = value.map { String($0) } + } +} + +public protocol WidthAttributeModifier { + +} + +extension WidthAttributeModifier where Self: Attributes & Mutable { + + public func width( + _ value: Int? + ) -> Self { + setAttribute(WidthAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/_GlobalAttributes.swift b/Sources/SwiftHTML/Attributes/_GlobalAttributes.swift new file mode 100644 index 0000000..4e454af --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_GlobalAttributes.swift @@ -0,0 +1,493 @@ +// https://html.spec.whatwg.org/multipage/dom.html#global-attributes +// https://www.w3schools.com/tags/ref_standardattributes.asp +public protocol GlobalAttributeModifier: + IdAttributeModifier, + ClassAttributeModifier, + DirAttributeModifier, + StyleAttributeModifier, + TitleAttributeModifier, + DirAttributeModifier, + TranslateAttributeModifier, + AutofocusAttributeModifier +{ + +} + +extension GlobalAttributeModifier where Self: Attributes & Mutable { + + public func spellcheck( + _ value: Bool + ) -> Self { + setAttribute(name: "spellcheck", value: String(value)) + } +} + +public protocol HTMLAttribute: Attribute { + +} + +extension HTMLAttribute { + + public static var name: String { + String(String(describing: self).lowercased().dropLast(9)) + } +} + +// ✅ id +// slot +// ✅ class +// accesskey +// autocapitalize +// autocorrect +// ✅autofocus +// contenteditable +// ✅ dir +// draggable +// enterkeyhint +// headingoffset +// headingreset +// hidden +// inert +// inputmode +// is +// itemid +// itemprop +// itemref +// itemscope +// itemtype +// lang +// nonce +// popover +// ✅spellcheck +// ✅ style +// tabindex +// ✅ title +// ✅translate +// writingsuggestions + +//public enum Draggable: String { +// /// Specifies that the element is draggable +// case `true` +// /// Specifies that the element is not draggable +// case `false` +// /// Uses the default behavior of the browser +// case auto +//} +// +// +//extension Tag { +// +// // MARK: - other global attributes +// +// /// Specifies a shortcut key to activate/focus an element +// public func accesskey(_ value: Character) -> Self { +// attribute("accesskey", String(value)) +// } +// +// /// Specifies whether the content of an element is editable or not +// public func contenteditable(_ value: Bool) -> Self { +// attribute("contenteditable", String(value)) +// } +// +// /// Used to store custom data private to the page or application +// public func data(key: String, _ value: String) -> Self { +// attribute("data-" + key, value) +// } +// +// /// Specifies whether an element is draggable or not +// public func draggable(_ value: Draggable = .auto) -> Self { +// attribute("draggable", value.rawValue) +// } +// +// /// Specifies that an element is not yet, or is no longer, relevant +// public func hidden(_ value: Bool? = nil) -> Self { +// attribute("hidden", value?.description) +// } +// +// /// Specifies the language of the element's content +// public func lang(_ value: String) -> Self { +// attribute("lang", value) +// } +// +// /// Specifies whether the element is to have its spelling and grammar checked or not + +// +// /// Specifies the tabbing order of an element +// public func tabindex(_ value: Int) -> Self { +// attribute("tabindex", String(value)) +// } + +// +//extension Tag { +// +// // MARK: - Window Event Attributes +// +// /// Script to be run after the document is printed +// public func onAfterPrint(_ value: String) -> Self { +// attribute("onafterprint", value) +// } +// +// /// Script to be run before the document is printed +// public func onBeforePrint(_ value: String) -> Self { +// attribute("onbeforeprint", value) +// } +// +// /// Script to be run when the document is about to be unloaded +// public func onBeforeUnload(_ value: String) -> Self { +// attribute("onbeforeunload", value) +// } +// +// /// Script to be run when an error occurs +// public func onError(_ value: String) -> Self { +// attribute("onerror", value) +// } +// +// /// Script to be run when there has been changes to the anchor part of the a URL +// public func onHashChange(_ value: String) -> Self { +// attribute("onhashchange", value) +// } +// +// /// Fires after the page is finished loading +// public func onLoad(_ value: String) -> Self { +// attribute("onload", value) +// } +// +// /// Script to be run when the message is triggered +// public func onMessage(_ value: String) -> Self { +// attribute("onmessage", value) +// } +// +// /// Script to be run when the browser starts to work offline +// public func onOffline(_ value: String) -> Self { +// attribute("onoffline", value) +// } +// +// /// Script to be run when the browser starts to work online +// public func onOnline(_ value: String) -> Self { +// attribute("ononline", value) +// } +// +// /// Script to be run when a user navigates away from a page +// public func onPageHide(_ value: String) -> Self { +// attribute("onpagehide", value) +// } +// +// /// Script to be run when a user navigates to a page +// public func onPageShow(_ value: String) -> Self { +// attribute("onpageshow", value) +// } +// +// /// Script to be run when the window's history changes +// public func onPopState(_ value: String) -> Self { +// attribute("onpopstate", value) +// } +// +// /// Fires when the browser window is resized +// public func onResize(_ value: String) -> Self { +// attribute("onresize", value) +// } +// +// /// Script to be run when a Web Storage area is updated +// public func onStorage(_ value: String) -> Self { +// attribute("onstorage", value) +// } +// +// /// Fires once a page has unloaded (or the browser window has been closed) +// public func onUnload(_ value: String) -> Self { +// attribute("onunload", value) +// } +// +// // MARK: - Form Events +// +// /// Fires the moment that the element loses focus +// public func onBlur(_ value: String) -> Self { +// attribute("onblur", value) +// } +// +// /// Fires the moment when the value of the element is changed +// public func onChange(_ value: String) -> Self { +// attribute("onchange", value) +// } +// +// /// Script to be run when a context menu is triggered +// public func onContextMenu(_ value: String) -> Self { +// attribute("oncontextmenu", value) +// } +// +// /// Fires the moment when the element gets focus +// public func onFocus(_ value: String) -> Self { +// attribute("onfocus", value) +// } +// +// /// Script to be run when an element gets user input +// public func onInput(_ value: String) -> Self { +// attribute("oninput", value) +// } +// +// /// Script to be run when an element is invalid +// public func onInvalid(_ value: String) -> Self { +// attribute("oninvalid", value) +// } +// +// /// Fires when the Reset button in a form is clicked +// public func onReset(_ value: String) -> Self { +// attribute("onreset", value) +// } +// +// /// Fires when the user writes something in a search field (for ) +// public func onSearch(_ value: String) -> Self { +// attribute("onsearch", value) +// } +// +// /// Fires after some text has been selected in an element +// public func onSelect(_ value: String) -> Self { +// attribute("onselect", value) +// } +// +// /// Fires when a form is submitted +// public func onSubmit(_ value: String) -> Self { +// attribute("onsubmit", value) +// } +// +// // MARK: - Keyboard Events +// +// /// Fires when a user is pressing a key +// public func onKeyDown(_ value: String) -> Self { +// attribute("onkeydown", value) +// } +// +// /// Fires when a user presses a key +// public func onKeyPress(_ value: String) -> Self { +// attribute("onkeypress", value) +// } +// +// /// Fires when a user releases a key +// public func onKeyUp(_ value: String) -> Self { +// attribute("onkeyup", value) +// } +// +// // MARK: - Mouse Events +// +// /// Fires on a mouse click on the element +// public func onClick(_ value: String) -> Self { +// attribute("onclick", value) +// } +// +// /// Fires on a mouse double-click on the element +// public func onDoubleClick(_ value: String) -> Self { +// attribute("ondblclick", value) +// } +// +// /// Fires when a mouse button is pressed down on an element +// public func onMouseDown(_ value: String) -> Self { +// attribute("onmousedown", value) +// } +// +// /// Fires when the mouse pointer is moving while it is over an element +// public func onMouseMove(_ value: String) -> Self { +// attribute("onmousemove", value) +// } +// +// /// Fires when the mouse pointer moves out of an element +// public func onMouseOut(_ value: String) -> Self { +// attribute("onmouseout", value) +// } +// +// /// Fires when the mouse pointer moves over an element +// public func onMouseOver(_ value: String) -> Self { +// attribute("onmouseover", value) +// } +// +// /// Fires when a mouse button is released over an element +// public func onMouseUp(_ value: String) -> Self { +// attribute("onmouseup", value) +// } +// +// /// Fires when the mouse wheel rolls up or down over an element +// public func onWheel(_ value: String) -> Self { +// attribute("onwheel", value) +// } +// +// // MARK: - Drag Events +// +// /// Script to be run when an element is dragged +// public func onDrag(_ value: String) -> Self { +// attribute("ondrag", value) +// } +// +// /// Script to be run at the end of a drag operation +// public func onDragEnd(_ value: String) -> Self { +// attribute("ondragend", value) +// } +// +// /// Script to be run when an element has been dragged to a valid drop target +// public func onDragEnter(_ value: String) -> Self { +// attribute("ondragenter", value) +// } +// +// /// Script to be run when an element leaves a valid drop target +// public func onDragLeave(_ value: String) -> Self { +// attribute("ondragleave", value) +// } +// +// /// Script to be run when an element is being dragged over a valid drop target +// public func onDragOver(_ value: String) -> Self { +// attribute("ondragover", value) +// } +// +// /// Script to be run at the start of a drag operation +// public func onDragStart(_ value: String) -> Self { +// attribute("ondragstart", value) +// } +// +// /// Script to be run when dragged element is being dropped +// public func onDrop(_ value: String) -> Self { +// attribute("ondrop", value) +// } +// +// /// Script to be run when an element's scrollbar is being scrolled +// public func onScroll(_ value: String) -> Self { +// attribute("onscroll", value) +// } +// +// // MARK: - Clipboard Events +// +// /// Fires when the user copies the content of an element +// public func onCopy(_ value: String) -> Self { +// attribute("oncopy", value) +// } +// +// /// Fires when the user cuts the content of an element +// public func onCut(_ value: String) -> Self { +// attribute("oncut", value) +// } +// +// /// Fires when the user pastes some content in an element +// public func onPaste(_ value: String) -> Self { +// attribute("onpaste", value) +// } +// +// // MARK: - Media Events +// +// /// Script to be run on abort +// public func onAbort(_ value: String) -> Self { +// attribute("onabort", value) +// } +// +// /// Script to be run when a file is ready to start playing (when it has buffered enough to begin) +// public func onCanPlay(_ value: String) -> Self { +// attribute("oncanplay", value) +// } +// +// /// Script to be run when a file can be played all the way to the end without pausing for buffering +// public func onCanPlaythrough(_ value: String) -> Self { +// attribute("oncanplaythrough", value) +// } +// +// /// Script to be run when the cue changes in a element +// public func onCueChange(_ value: String) -> Self { +// attribute("oncuechange", value) +// } +// +// /// Script to be run when the length of the media changes +// public func onDurationChange(_ value: String) -> Self { +// attribute("ondurationchange", value) +// } +// +// /// Script to be run when something bad happens and the file is suddenly unavailable (like unexpectedly disconnects) +// public func onEmptied(_ value: String) -> Self { +// attribute("onemptied", value) +// } +// +// /// Script to be run when the media has reach the end (a useful event for messages like "thanks for listening") +// public func onEnded(_ value: String) -> Self { +// attribute("onended", value) +// } +// +// /// Script to be run when an error occurs when the file is being loaded +// // func onError(_ value: String) -> Self { +// // attribute("onerror", value) +// // } +// +// /// Script to be run when media data is loaded +// public func onLoadedData(_ value: String) -> Self { +// attribute("onloadeddata", value) +// } +// +// /// Script to be run when meta data (like dimensions and duration) are loaded +// public func onLoadedMetadata(_ value: String) -> Self { +// attribute("onloadedmetadata", value) +// } +// +// /// Script to be run just as the file begins to load before anything is actually loaded +// public func onLoadStart(_ value: String) -> Self { +// attribute("onloadstart", value) +// } +// +// /// Script to be run when the media is paused either by the user or programmatically +// public func onPause(_ value: String) -> Self { +// attribute("onpause", value) +// } +// +// /// Script to be run when the media is ready to start playing +// public func onPlay(_ value: String) -> Self { +// attribute("onplay", value) +// } +// +// /// Script to be run when the media actually has started playing +// public func onPlaying(_ value: String) -> Self { +// attribute("onplaying", value) +// } +// +// /// Script to be run when the browser is in the process of getting the media data +// public func onProgress(_ value: String) -> Self { +// attribute("onprogress", value) +// } +// +// /// Script to be run each time the playback rate changes (like when a user switches to a slow motion or fast forward mode) +// public func onRateChange(_ value: String) -> Self { +// attribute("onratechange", value) +// } +// +// /// Script to be run when the seeking attribute is set to false indicating that seeking has ended +// public func onSeeked(_ value: String) -> Self { +// attribute("onseeked", value) +// } +// +// /// Script to be run when the seeking attribute is set to true indicating that seeking is active +// public func onSeeking(_ value: String) -> Self { +// attribute("onseeking", value) +// } +// +// /// Script to be run when the browser is unable to fetch the media data for whatever reason +// public func onStalled(_ value: String) -> Self { +// attribute("onstalled", value) +// } +// +// /// Script to be run when fetching the media data is stopped before it is completely loaded for whatever reason +// public func onSuspend(_ value: String) -> Self { +// attribute("onsuspend", value) +// } +// +// /// Script to be run when the playing position has changed (like when the user fast forwards to a different point in the media) +// public func onTimeUpdate(_ value: String) -> Self { +// attribute("ontimeupdate", value) +// } +// +// /// Script to be run each time the volume is changed which (includes setting the volume to "mute") +// public func onVolumeChange(_ value: String) -> Self { +// attribute("onvolumechange", value) +// } +// +// /// Script to be run when the media has paused but is expected to resume (like when the media pauses to buffer more data) +// public func onWaiting(_ value: String) -> Self { +// attribute("onwaiting", value) +// } +// +// // MARK: - Misc Events +// +// /// Fires when the user opens or closes the
element +// public func onToggle(_ value: String) -> Self { +// attribute("ontoggle", value) +// } +//} diff --git a/Sources/SwiftHTML/ContentModel/ContentModel.swift b/Sources/SwiftHTML/ContentModel/ContentModel.swift new file mode 100644 index 0000000..f8c1450 --- /dev/null +++ b/Sources/SwiftHTML/ContentModel/ContentModel.swift @@ -0,0 +1,38 @@ +public struct ContentModel: Sendable, OptionSet { + + public let rawValue: UInt8 + + public init( + rawValue: UInt8 + ) { + self.rawValue = rawValue + } + + /// [Specification](https://html.spec.whatwg.org/#embedded-content). + public static let embedded: Self = .init(rawValue: 1 << 0) + /// [Specification](https://html.spec.whatwg.org/#flow-content). + public static let flow: Self = .init(rawValue: 1 << 1) + /// [Specification](https://html.spec.whatwg.org/#heading-content). + public static let heading: Self = .init(rawValue: 1 << 2) + /// [Specification](https://html.spec.whatwg.org/#interactive-content). + public static let interactive: Self = .init(rawValue: 1 << 3) + /// [Specification](https://html.spec.whatwg.org/#metadata-content). + // base 1x + // title 1x + // link meta noscript script style template + public static let metadata: Self = .init(rawValue: 1 << 4) + /// [Specification](https://html.spec.whatwg.org/#palpable-content). + public static let palpable: Self = .init(rawValue: 1 << 5) + /// [Specification](https://html.spec.whatwg.org/#phrasing-content). + public static let phrasing: Self = .init(rawValue: 1 << 6) + /// [Specification](https://html.spec.whatwg.org/#sectioning-content). + public static let sectioning: Self = .init(rawValue: 1 << 7) +} + +public protocol ContentModelRepresentable { + var categories: ContentModel { get } +} + +protocol HTMLTag: Tag, ContentModelRepresentable {} +protocol HTMLShortTag: ShortTag, ContentModelRepresentable {} +protocol HTMLStandardTag: StandardTag, ContentModelRepresentable {} diff --git a/Sources/SwiftHTML/Exports.swift b/Sources/SwiftHTML/Exports.swift new file mode 100644 index 0000000..f61113d --- /dev/null +++ b/Sources/SwiftHTML/Exports.swift @@ -0,0 +1,2 @@ +// NOTE: Comment, Text, Document, Renderer comes directly from SGML. +@_exported import SGML diff --git a/Sources/SwiftHTML/Extensions/Array+Extensions.swift b/Sources/SwiftHTML/Extensions/Array+Extensions.swift new file mode 100644 index 0000000..b43635e --- /dev/null +++ b/Sources/SwiftHTML/Extensions/Array+Extensions.swift @@ -0,0 +1,8 @@ +extension Array { + + func joinedElementsAsString( + separator: String = "," + ) -> String { + map { "\($0)" }.joined(separator: separator) + } +} diff --git a/Sources/SwiftHTML/Extensions/Mutable+Extensions.swift b/Sources/SwiftHTML/Extensions/Mutable+Extensions.swift new file mode 100644 index 0000000..9beb1cb --- /dev/null +++ b/Sources/SwiftHTML/Extensions/Mutable+Extensions.swift @@ -0,0 +1,13 @@ +extension Mutable { + + public func check( + _ condition: Bool, + _ trueBlock: (Self) -> Self, + else falseBlock: ((Self) -> Self)? = nil + ) -> Self { + if condition { + return trueBlock(self) + } + return falseBlock?(self) ?? self + } +} diff --git a/Sources/SwiftHTML/Tags/ATag.swift b/Sources/SwiftHTML/Tags/ATag.swift new file mode 100644 index 0000000..09e4bed --- /dev/null +++ b/Sources/SwiftHTML/Tags/ATag.swift @@ -0,0 +1,64 @@ +/// The `` tag defines a hyperlink, which is used to link from one page to another. +/// +/// The most important attribute of the `` element is the href attribute, which indicates the link's destination. +/// +/// By default, links will appear as follows in all browsers: +/// +/// - An unvisited link is underlined and blue +/// - A visited link is underlined and purple +/// - An active link is underlined and red +public struct A: + HTMLStandardTag, + /// attribute modifiers + GlobalAttributeModifier, + DownloadAttributeModifier, + HrefAttributeModifier, + HreflangAttributeModifier, + MediaAttributeModifier, // NOTE: W3C, but not spec + PingAttributeModifier, + ReferrerPolicyAttributeModifier, + RelAttributeModifier, + TargetAttributeModifier, + TypeAttributeModifier +{ + /// The attribute storage for the tag. + public var attributes: AttributeStore + + /// The child elements contained within the tag. + public var children: [Element] + + /// The content model category for the tag. + public var categories: ContentModel { + var contentModel: ContentModel = [ + .flow, .phrasing, + ] + if hasAttribute(HrefAttribute.self) { + contentModel.insert(.palpable) + } + return contentModel + } + + init( + attributes: AttributeStore = .init(), + children: [Element] + ) { + self.attributes = attributes + self.children = children + } + + public init( + _ contents: String + ) { + self.init( + children: [ + Text(contents) + ] + ) + } + + public init( + @Builder _ block: () -> [Element] + ) { + self.init(children: block()) + } +} diff --git a/Sources/SwiftHTML/Tags/AbbrTag.swift b/Sources/SwiftHTML/Tags/AbbrTag.swift new file mode 100644 index 0000000..74d6d8b --- /dev/null +++ b/Sources/SwiftHTML/Tags/AbbrTag.swift @@ -0,0 +1,47 @@ +/// +/// The `` tag defines an abbreviation or an acronym, like "HTML", "CSS", "Mr.", "Dr.", "ASAP", "ATM". +/// +/// **Tip:** Use the global title attribute to show the description for the abbreviation/acronym when you mouse over the element. +/// +/// [HTML Standard - The abbr element](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-abbr-element) +/// +public struct Abbr: + HTMLStandardTag, + // attribute modifiers + GlobalAttributeModifier +{ + /// The attribute storage for the tag. + public var attributes: AttributeStore + + /// The child elements contained within the tag. + public var children: [Element] + + /// The content model category for the tag. + public var categories: ContentModel { + [ + .flow, .phrasing, .palpable, + ] + } + + init( + attributes: AttributeStore = .init(), + children: [Element] + ) { + self.attributes = attributes + self.children = children + } + + /// Creates a tag containing the given text. + /// + /// - Parameter contents: The textual content representing the abbreviation. + public init( + _ contents: String + ) { + // Phrasing content. + self.init( + children: [ + Text(contents) + ] + ) + } +} diff --git a/Sources/SwiftHTML/Tags/AddressTag.swift b/Sources/SwiftHTML/Tags/AddressTag.swift new file mode 100644 index 0000000..b0d495a --- /dev/null +++ b/Sources/SwiftHTML/Tags/AddressTag.swift @@ -0,0 +1,63 @@ +/// +/// The `
` tag defines the contact information for the author/owner of a document or an article. +/// +/// The contact information can be an email address, URL, physical address, phone number, social media handle, etc. +/// +/// The text in the `
` element usually renders in italic, and browsers will always add a line break before and after the `
` element. +/// +/// [HTML Standard - The address element](https://html.spec.whatwg.org/multipage/sections.html#the-address-element) +/// +public struct Address: + HTMLStandardTag, + // attribute modifiers + GlobalAttributeModifier +{ + /// The attribute storage for the tag. + public var attributes: AttributeStore + + /// The child elements contained within the tag. + public var children: [Element] + + /// The content model category for the tag. + public var categories: ContentModel { + [ + .flow, .palpable, + ] + } + + init( + attributes: AttributeStore = .init(), + children: [Element] + ) { + self.attributes = attributes + self.children = children + } + + /// Creates a tag containing the given text. + /// + /// - Parameter contents: The textual content. + public init( + _ contents: String + ) { + self.init( + children: [ + Text(contents) + ] + ) + } + + /// Creates an `
` element using a result builder. + /// + /// Use this initializer when you want to compose the element’s children declaratively. The closure can return any valid phrasing content, which will be inserted as the children of the `
` tag. + /// + /// - Parameter block: A closure that produces the child elements for the `
` element. + public init( + @Builder _ block: () -> [Element] + ) { + // Flow content, + // but with no heading content descendants, + // no sectioning content descendants, + // and no header, footer, or address element descendants. + self.init(children: block()) + } +} diff --git a/Sources/SwiftHTML/Tags/AreaTag.swift b/Sources/SwiftHTML/Tags/AreaTag.swift new file mode 100644 index 0000000..c07d933 --- /dev/null +++ b/Sources/SwiftHTML/Tags/AreaTag.swift @@ -0,0 +1,123 @@ +/// +/// The tag defines an area inside an image map (an image map is an image with clickable areas). +/// +/// elements are always nested inside a tag. +/// +/// **Note:** The usemap attribute in is associated with the element's name attribute, and creates a relationship between the image and the map. +/// +/// [HTML Standard - The area element](https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element) +/// [W3C Reference - HTML area tag](https://www.w3schools.com/tags/tag_area.asp) +/// +public struct Area: + HTMLShortTag, + // attribute modifiers + GlobalAttributeModifier, + AltAttributeModifier, + DownloadAttributeModifier, + HrefAttributeModifier, + PingAttributeModifier, + ReferrerPolicyAttributeModifier, + RelAttributeModifier, + TargetAttributeModifier +{ + // MARK: - attributes + + public struct Shape: Attribute { + + public enum Value: String { + /// Specifies the entire region + case `default` + /// Defines a rectangular region + case rect + /// Defines a circular region + case circle + /// Defines a polygonal region + case poly + } + + public var value: String? + + init( + _ value: Value? = nil + ) { + self.value = value?.rawValue + } + } + + // MARK: - + + public struct Coords: Attribute { + + public var value: String? + + init( + _ value: String? = nil + ) { + self.value = value + } + + init( + _ values: [Int] + ) { + self.value = values.joinedElementsAsString() + } + + init( + _ values: [Float] + ) { + self.value = values.joinedElementsAsString() + } + + init( + _ values: [Double] + ) { + self.value = values.joinedElementsAsString() + } + } + + // MARK: - tag + + /// The attribute storage for the tag. + public var attributes: AttributeStore + + /// The content model category for the tag. + public var categories: ContentModel { + [ + .flow + ] + } + + public init() { + self.attributes = .init() + } + + public func shape( + _ value: Shape.Value? + ) -> Self { + setAttribute(Shape(value)) + } + + public func coords( + _ value: String? + ) -> Self { + setAttribute(Coords(value)) + } + + public func coords( + _ values: Int... + ) -> Self { + setAttribute(Coords(values)) + } + + public func coords( + _ values: Float... + ) -> Self { + setAttribute(Coords(values)) + } + + public func double( + _ values: Double... + ) -> Self { + setAttribute(Coords(values)) + } +} diff --git a/Sources/SwiftHTML/Tags/ArticleTag.swift b/Sources/SwiftHTML/Tags/ArticleTag.swift new file mode 100644 index 0000000..a30f1aa --- /dev/null +++ b/Sources/SwiftHTML/Tags/ArticleTag.swift @@ -0,0 +1,46 @@ +/// The `
` tag specifies independent, self-contained content. +/// +/// An article should make sense on its own and it should be possible to distribute it independently from the rest of the site. +/// +/// Potential sources for the `
` element: +/// +/// - Forum post +/// - Blog post +/// - News story +/// +/// **Note:** The `
` element does not render as anything special in a browser. +/// However, you can use CSS to style the `
` element (see example below). +public struct Article: + HTMLStandardTag, + /// attribute modifiers + GlobalAttributeModifier +{ + + /// The attribute storage for the tag. + public var attributes: AttributeStore + + /// The child elements contained within the tag. + public var children: [Element] + + /// The content model category for the tag. + public var categories: ContentModel { + [ + .flow, .sectioning, .palpable, + ] + } + + init( + attributes: AttributeStore = .init(), + children: [Element] + ) { + self.attributes = attributes + self.children = children + } + + public init( + @Builder _ block: () -> [Element] + ) { + // Flow content. + self.init(children: block()) + } +} diff --git a/Sources/SwiftHTML/Tags/AsideTag.swift b/Sources/SwiftHTML/Tags/AsideTag.swift new file mode 100644 index 0000000..fa82d23 --- /dev/null +++ b/Sources/SwiftHTML/Tags/AsideTag.swift @@ -0,0 +1,42 @@ +/// The `