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..3c93d83 100644 --- a/Package.swift +++ b/Package.swift @@ -1,51 +1,100 @@ -// 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"]), - ], - dependencies: [ - + .library(name: "SwiftSVG", targets: ["SwiftSVG"]), ], 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: [ + .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..4231ebe 100644 --- a/README.md +++ b/README.md @@ -1,250 +1,218 @@ -# 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 { -// -``` - -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) - -// -``` - -You can create your own attribute modifier via an extension. - -```swift -public extension Guid { + static let name = "lastBuildDate" - func isPermalink(_ value: Bool = true) -> Self { - attribute("isPermalink", String(value)) - } + // ... } ``` -There are other built-in type-safe attribute modifiers available on tags. +### Attributes - -## Composing tags - -You can come up with your own `Tag` composition system by introducing a new protocol. +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 -protocol TagRepresentable { - - @TagBuilder - func build() -> Tag +// very simple attribute +struct Class: Attribute { + var value: String? + + init(_ value: Value) { + self.value = value + } } -struct ListComponent: TagRepresentable { +// custom name and value type +struct Alignment: Attribute { - let items: [String] - - init(_ items: [String]) { - self.items = items + enum Value: String { + case left + case right } - func build() -> Tag { - Ul { - for item in items { - Li(item) - } - } + static let name = "align" + var value: String? + + init(_ value: Value) { + self.value = value.rawValue } } - -let tag = ListComponent(["a", "b", "c"]).build() ``` -This way it is also possible to extend the `TagBuilder` to support the new protocol. +You can set, add, or remove attributes—or even modify individual attribute values—on any tag that supports attributes: ```swift -extension TagBuilder { - - static func buildExpression(_ expression: Tag) -> Tag { - expression - } - - static func buildExpression(_ expression: TagRepresentable) -> Tag { - expression.build() - } -} +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) + ) ``` -Sometimes you'll need extra parameters for the build function, so you have to call the build method by hand. +There are built-in, type-safe attributes and helper modifiers available for the standard tags. -In those cases it is recommended to introduce a `render` function instead of using build. -```swift +### Container elements -let tag = WebIndexTemplate(ctx) { - ListComponent(["a", "b", "c"]) - .render(req) -} -.render(req) -``` +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. -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. +```swift +// TODO +``` ## 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..69b0448 --- /dev/null +++ b/Sources/SGML/Attributes/AttributeStore.swift @@ -0,0 +1,99 @@ +import DOM + +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 !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..b25b3b9 --- /dev/null +++ b/Sources/SGML/Builders/Builder.swift @@ -0,0 +1,9 @@ +@resultBuilder +public enum Builder { + + public static func buildBlock( + _ elements: T... + ) -> [T] { + elements + } +} 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/Enctype.swift b/Sources/SwiftHTML/Attributes/Enctype.swift new file mode 100644 index 0000000..88365cb --- /dev/null +++ b/Sources/SwiftHTML/Attributes/Enctype.swift @@ -0,0 +1,12 @@ +//// +//// Enctype.swift +//// SwiftHtml +//// +//// Created by Tibor Bodecs on 2021. 07. 23.. +//// +// +//public enum Enctype: String { +// case urlencoded = "application/x-www-form-urlencoded" +// case multipart = "multipart/form-data" +// case plain = "text/plain" +//} diff --git a/Sources/SwiftHTML/Attributes/Events.swift b/Sources/SwiftHTML/Attributes/Events.swift new file mode 100644 index 0000000..ef37249 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/Events.swift @@ -0,0 +1,380 @@ +//// +//// Events.swift +//// SwiftHtml +//// +//// Created by Tibor Bodecs on 2021. 07. 23.. +//// +// +//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/Attributes/Global.swift b/Sources/SwiftHTML/Attributes/Global.swift new file mode 100644 index 0000000..75d34d0 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/Global.swift @@ -0,0 +1,200 @@ +//// +//// Global.swift +//// SwiftHtml +//// +//// Created by Tibor Bodecs on 2021. 07. 19.. +//// +// +///// https://www.w3schools.com/tags/ref_standardattributes.asp +//public enum TextDirection: String { +// /// Default. Left-to-right text direction +// case ltr +// /// Right-to-left text direction +// case rtl +// /// Let the browser figure out the text direction, based on the content (only recommended if the text direction is unknown) +// case auto +//} +// +//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 +//} +// +//public enum Translate: String { +// /// 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 +//} +// +//extension Tag { +// +// // MARK: - style management +// +// /// find an existing style attribute and return the value as an array of strings or an empty array +// private var styleArray: [String] { +// node.attributes.first { $0.key == "style" }?.value?.styleArray ?? [] +// } +// +// /// Specifies one stylename for an element (refers to a style in a style sheet) +// public func style(_ value: String?, _ condition: Bool = true) -> Self { +// guard let value, !value.isEmpty else { return self } +// return attribute("style", value, condition) +// } +// +// /// Specifies multiple stylenames for an element (refers to a style in a style sheet) +// public func style(_ values: [String], _ condition: Bool = true) -> Self { +// /// @NOTE: explicit true flag is needed, otherwise Swift won't know which function to call... +// style(values.styleString, condition) +// } +// +// /// Specifies multiple stylenames for an element (refers to a style in a style sheet) +// public func style(_ values: String...) -> Self { +// style(values) +// } +// +// /// Adds a single value to the style list if the condition is true +// /// +// /// Note: If the value is empty or nil it won't be added to the list +// /// +// public func style(add value: String?, _ condition: Bool = true) -> Self { +// guard let value = value else { +// return self +// } +// return style(add: [value], condition) +// } +// +// /// Adds an array of values to the style list if the condition is true +// /// +// /// Note: If the value is empty it won't be added to the list +// /// +// public func style(add values: [String], _ condition: Bool = true) -> Self { +// let newValues = styleArray + values.filter { !$0.isEmpty } +// +// var newValue: String? = nil +// if !newValues.isEmpty { +// newValue = newValues.styleString +// } +// return style(newValue, condition) +// } +// +// /// Removes a given style values if the condition is true +// public func style(remove value: String?, _ condition: Bool = true) -> Self { +// guard let value = value else { +// return self +// } +// return style(remove: [value], condition) +// } +// +// /// Removes an array of style values if the condition is true +// public func style(remove values: [String], _ condition: Bool = true) -> Self +// { +// let newClasses = styleArray.filter { !values.contains($0) } +// if newClasses.isEmpty { +// return deleteAttribute("style") +// } +// return style(newClasses, condition) +// } +// +// /// Removes a given style value with its key name if the condition is true +// /// `.style(removeByKey: "font-size")` as opposed to `.style(remove: "font-size: 12rem")` +// public func style(removeByKey value: String?, _ condition: Bool = true) +// -> Self +// { +// guard let value = value else { +// return self +// } +// return style(removeByKey: [value], condition) +// } +// +// /// Removes an array of style values with the key name if the condition is true +// /// `.style(removeByKey:[ "font-size"])` as opposed to `.style(remove: ["font-size: 12rem"])` +// public func style(removeByKey values: [String], _ condition: Bool = true) +// -> Self +// { +// let newClasses = styleArray.filter { +// !values.contains(String($0.prefix(while: { $0 != ":" }))) +// } +// if newClasses.isEmpty { +// return deleteAttribute("style") +// } +// return style(newClasses, condition) +// } +// +// /// toggles a single style value +// public func style(toggle value: String?, _ condition: Bool = true) -> Self { +// guard let value = value else { +// return self +// } +// if styleArray.contains(value) { +// return style(remove: value, condition) +// } +// return style(add: value, condition) +// } +// +// // 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 the text direction for the content in an element +// public func dir(_ value: TextDirection = .ltr) -> Self { +// attribute("dir", value.rawValue) +// } +// +// /// 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 a unique id for an element +// public func `id`(_ value: String) -> Self { +// attribute("id", value) +// } +// +// /// 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 +// public func spellcheck(_ value: Bool) -> Self { +// attribute("spellcheck", String(value)) +// } +// +// /// Specifies the tabbing order of an element +// public func tabindex(_ value: Int) -> Self { +// attribute("tabindex", String(value)) +// } +// +// /// Specifies extra information about an element +// public func title(_ value: String) -> Self { +// attribute("title", value) +// } +// +// /// Specifies whether the content of an element should be translated or not +// public func translate(_ value: Translate) -> Self { +// attribute("translate", value.rawValue) +// } +//} diff --git a/Sources/SwiftHTML/Attributes/IntegrityAttribute.swift b/Sources/SwiftHTML/Attributes/IntegrityAttribute.swift new file mode 100644 index 0000000..e11845c --- /dev/null +++ b/Sources/SwiftHTML/Attributes/IntegrityAttribute.swift @@ -0,0 +1,10 @@ +public struct IntegrityAttribute: HTMLAttribute { + + public var value: String? + + public init( + _ value: String? + ) { + self.value = value + } +} diff --git a/Sources/SwiftHTML/Attributes/Loading.swift b/Sources/SwiftHTML/Attributes/Loading.swift new file mode 100644 index 0000000..c171166 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/Loading.swift @@ -0,0 +1,11 @@ +//// +//// Loading.swift +//// SwiftHtml +//// +//// Created by Tibor Bodecs on 2021. 07. 23.. +//// +// +//public enum Loading: String { +// case eager +// case lazy +//} diff --git a/Sources/SwiftHTML/Attributes/Method.swift b/Sources/SwiftHTML/Attributes/Method.swift new file mode 100644 index 0000000..9564695 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/Method.swift @@ -0,0 +1,11 @@ +//// +//// Method.swift +//// SwiftHtml +//// +//// Created by Tibor Bodecs on 2021. 07. 23.. +//// +// +//public enum Method: String { +// case get +// case post +//} diff --git a/Sources/SwiftHTML/Attributes/TargetFrameAttribute.swift b/Sources/SwiftHTML/Attributes/TargetFrameAttribute.swift new file mode 100644 index 0000000..0405dcc --- /dev/null +++ b/Sources/SwiftHTML/Attributes/TargetFrameAttribute.swift @@ -0,0 +1,34 @@ +// +// File.swift +// swift-html +// +// Created by Tibor Bödecs on 2025. 11. 23.. +// +// +//public enum TargetFrame { +// /// 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 let .frame(name): +// return name +// } +// } +//} diff --git a/Sources/SwiftHTML/Attributes/_Final/AltAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/AltAttribute.swift new file mode 100644 index 0000000..d578401 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/AutoplayAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/AutoplayAttribute.swift new file mode 100644 index 0000000..d5bc154 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/ClassAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/ClassAttribute.swift new file mode 100644 index 0000000..2903604 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/ClassAttribute.swift @@ -0,0 +1,45 @@ +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 { + + removeAttribute(ClassAttribute(value)) + } +} diff --git a/Sources/SwiftHTML/Attributes/_Final/ControlsAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/ControlsAttribute.swift new file mode 100644 index 0000000..90e8166 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/CrossoriginAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/CrossoriginAttribute.swift new file mode 100644 index 0000000..9dfe0b4 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/DownloadAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/DownloadAttribute.swift new file mode 100644 index 0000000..b6b9c1b --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/DownloadAttribute.swift @@ -0,0 +1,23 @@ +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)) + } +} diff --git a/Sources/SwiftHTML/Attributes/_Final/HrefAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/HrefAttribute.swift new file mode 100644 index 0000000..b832460 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/HreflangAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/HreflangAttribute.swift new file mode 100644 index 0000000..6df2683 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/IdAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/IdAttribute.swift new file mode 100644 index 0000000..186c529 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/LoopAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/LoopAttribute.swift new file mode 100644 index 0000000..e8742d9 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/MediaAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/MediaAttribute.swift new file mode 100644 index 0000000..900d2da --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/MutedAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/MutedAttribute.swift new file mode 100644 index 0000000..9f5677c --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/NameAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/NameAttribute.swift new file mode 100644 index 0000000..deb30da --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/PingAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/PingAttribute.swift new file mode 100644 index 0000000..19508ca --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/PreloadAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/PreloadAttribute.swift new file mode 100644 index 0000000..1798371 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/ReferrerPolicyAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/ReferrerPolicyAttribute.swift new file mode 100644 index 0000000..25acb26 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/RelAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/RelAttribute.swift new file mode 100644 index 0000000..638fc28 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/SrcAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/SrcAttribute.swift new file mode 100644 index 0000000..f65eb88 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/StyleAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/StyleAttribute.swift new file mode 100644 index 0000000..dce7d27 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/TargetAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/TargetAttribute.swift new file mode 100644 index 0000000..017153d --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/TitleAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/TitleAttribute.swift new file mode 100644 index 0000000..e5b534e --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Final/TypeAttribute.swift b/Sources/SwiftHTML/Attributes/_Final/TypeAttribute.swift new file mode 100644 index 0000000..5048bd3 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Final/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/_Protocols.swift b/Sources/SwiftHTML/Attributes/_Protocols.swift new file mode 100644 index 0000000..1bfe7a8 --- /dev/null +++ b/Sources/SwiftHTML/Attributes/_Protocols.swift @@ -0,0 +1,52 @@ +// https://html.spec.whatwg.org/multipage/dom.html#global-attributes +public protocol GlobalAttributeModifier: + IdAttributeModifier, + ClassAttributeModifier, + StyleAttributeModifier, + TitleAttributeModifier +{ + +} + +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 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..d9a7027 --- /dev/null +++ b/Sources/SwiftHTML/Tags/ATag.swift @@ -0,0 +1,98 @@ +/// 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()) + } +} + +//extension A { + +// /// Specifies that the target will be downloaded when a user clicks on the hyperlink +// public func download(_ value: String? = nil) -> Self { +// flagAttribute("download", value) +// } +// +// /// Specifies the language of the linked document +// public func hreflang(_ value: String) -> Self { +// attribute("hreflang", value) +// } +// /// Specifies a space-separated list of URLs to which, when the link is followed, post requests with the body ping will be sent by the browser (in the background). +// /// +// /// Typically used for tracking. +// public func ping(_ value: [String]) -> Self { +// attribute("ping", value.joined(separator: " ")) +// } +// +// /// Specifies which referrer information to send with the link +// public func refererPolicy(_ value: RefererPolicy = .origin) -> Self { +// attribute("referrerpolicy", value.rawValue) +// } +// +// /// Specifies where to open the linked document +// public func target(_ value: TargetFrame, _ condition: Bool = true) -> Self { +// attribute("target", value.rawValue, condition) +// } +// +// /// The type attribute specifies the Internet media type (formerly known as MIME type) of the linked document. +// public func type(_ value: String) -> Self { +// attribute("type", value) +// } +//} 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..5f2b023 --- /dev/null +++ b/Sources/SwiftHTML/Tags/AsideTag.swift @@ -0,0 +1,42 @@ +/// The `