diff --git a/README.md b/README.md index 7acb9ee..bcdec84 100644 --- a/README.md +++ b/README.md @@ -74,35 +74,54 @@ That's it. ## Creating custom tags -You can define your own custom tags by subclassing the `Tag` or `EmptyTag` class. +You can define your own custom tags a few different ways, from simple to complex cases. -You can follow the same pattern if you take a look at the core tags. +1. Subclass `Tag` and it will render with your class name lowercased. ```swift -open class Div: Tag { +class MyTag: Tag { } +// +``` + +2. Subclass `Tag` and override the `name` property. + +```swift +class MyTag: Tag { + + override open class var name: String { "myTag" } } -//
- standard tag +// +``` + +3. Subclass `Tag` and override the `type` property. `Type` determines how your class will render. For example, to create an empty tag... -open class Br: EmptyTag { +```swift +class MyTag: Tag { + override open class var type: `Type` { .empty } } -//
- no closing tag +// ``` -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. +4. Subclass `Tag`, create your own custom initializer and then call the `Tag` designated initializer. ```swift -open class LastBuildDate: Tag { - - open override class func createNode() -> Node { - Node(type: .standard, name: "lastBuildDate") +class MyTag: Tag { + + init(myAttributeValue: String, @TagBuilder _ builder: () -> Tag) { + let attribute = .init(key: "myKey", value: myAttributeValue) + super.init(type: .empty, + name: "myTag", + attributes: [attribute], + builder: [builder()] + ) } } -// - standard tag with custom name +// ``` It is also possible to create tags with altered content or default attributes. @@ -129,6 +148,50 @@ open class Rss: Tag { // ... - tag with a default attribute ``` +To customize tags of a particular `Node` type... + +```swift +// Bracketed tags... ...use StandardTag class +class MyClass: StandardTag { } + +// Single tag... ...use EmptyTag class +class MyClass: EmptyTag { } + +// Comment tag... ...use CommentTag class +class MyClass: CommentTag { } +``` + +When you need to combined multiple tags into one, but don't want the container to render, use the `GroupTag`. This class is especially handy when a function parameter requires one `Tag` but you need to supply many. + +```swift +class MyTag: Tag { + + init(myAttributeValue: String, @TagBuilder _ builder: () -> Tag) { + let attribute = .init(key: "myKey", value: myAttributeValue) + let node = Node(type: .empty, name: "myTag", attributes: [attribute]) + super.init(node: node, [builder()]) + } + + convenience init(anotherValue: String) { + self.init(myAttributeValue: anotherValue) { + // must return one Tag for builder parameter + // use GroupTag because it will not render + GroupTag { + TagA() + TagB() + TagC() + } + } + } +} + +// +// +// +``` + +If you just need to render basic HTML — `
`, `

`, ``, etc. — then use one the many pre-made classes in the library called by the same name. + ## Attribute management You can set, add or delete the attributes of a given tag. diff --git a/Sources/SwiftHtml/Attributes/Events.swift b/Sources/SwiftHtml/Attributes/Events.swift index 28758d5..0f3379a 100644 --- a/Sources/SwiftHtml/Attributes/Events.swift +++ b/Sources/SwiftHtml/Attributes/Events.swift @@ -5,378 +5,114 @@ // Created by Tibor Bodecs on 2021. 07. 23.. // -public extension Tag { - - // MARK: - Window Event Attributes - - /// Script to be run after the document is printed - func onAfterPrint(_ value: String) -> Self { - attribute("onafterprint", value) - } - - /// Script to be run before the document is printed - func onBeforePrint(_ value: String) -> Self { - attribute("onbeforeprint", value) - } - - /// Script to be run when the document is about to be unloaded - func onBeforeUnload(_ value: String) -> Self { - attribute("onbeforeunload", value) - } - - /// Script to be run when an error occurs - 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 - func onHashChange(_ value: String) -> Self { - attribute("onhashchange", value) - } - - /// Fires after the page is finished loading - func onLoad(_ value: String) -> Self { - attribute("onload", value) - } - - /// Script to be run when the message is triggered - func onMessage(_ value: String) -> Self { - attribute("onmessage", value) - } - - /// Script to be run when the browser starts to work offline - func onOffline(_ value: String) -> Self { - attribute("onoffline", value) - } - - /// Script to be run when the browser starts to work online - func onOnline(_ value: String) -> Self { - attribute("ononline", value) - } - - /// Script to be run when a user navigates away from a page - func onPageHide(_ value: String) -> Self { - attribute("onpagehide", value) - } - - /// Script to be run when a user navigates to a page - func onPageShow(_ value: String) -> Self { - attribute("onpageshow", value) - } - - /// Script to be run when the window's history changes - func onPopState(_ value: String) -> Self { - attribute("onpopstate", value) - } - - /// Fires when the browser window is resized - func onResize(_ value: String) -> Self { - attribute("onresize", value) - } - - /// Script to be run when a Web Storage area is updated - func onStorage(_ value: String) -> Self { - attribute("onstorage", value) - } - - /// Fires once a page has unloaded (or the browser window has been closed) - func onUnload(_ value: String) -> Self { - attribute("onunload", value) - } - - // MARK: - Form Events - - /// Fires the moment that the element loses focus - func onBlur(_ value: String) -> Self { - attribute("onblur", value) - } - - /// Fires the moment when the value of the element is changed - func onChange(_ value: String) -> Self { - attribute("onchange", value) - } - - /// Script to be run when a context menu is triggered - func onContextMenu(_ value: String) -> Self { - attribute("oncontextmenu", value) - } - - /// Fires the moment when the element gets focus - func onFocus(_ value: String) -> Self { - attribute("onfocus", value) - } - - /// Script to be run when an element gets user input - func onInput(_ value: String) -> Self { - attribute("oninput", value) - } - - /// Script to be run when an element is invalid - func onInvalid(_ value: String) -> Self { - attribute("oninvalid", value) - } - - /// Fires when the Reset button in a form is clicked - func onReset(_ value: String) -> Self { - attribute("onreset", value) - } - - /// Fires when the user writes something in a search field (for ) - func onSearch(_ value: String) -> Self { - attribute("onsearch", value) - } - - /// Fires after some text has been selected in an element - func onSelect(_ value: String) -> Self { - attribute("onselect", value) - } - - /// Fires when a form is submitted - func onSubmit(_ value: String) -> Self { - attribute("onsubmit", value) - } - - // MARK: - Keyboard Events - - - /// Fires when a user is pressing a key - func onKeyDown(_ value: String) -> Self { - attribute("onkeydown", value) - } - - /// Fires when a user presses a key - func onKeyPress(_ value: String) -> Self { - attribute("onkeypress", value) - } - - /// Fires when a user releases a key - func onKeyUp(_ value: String) -> Self { - attribute("onkeyup", value) - } - - // MARK: - Mouse Events - - - /// Fires on a mouse click on the element - func onClick(_ value: String) -> Self { - attribute("onclick", value) - } - - /// Fires on a mouse double-click on the element - func onDoubleClick(_ value: String) -> Self { - attribute("ondblclick", value) - } - - /// Fires when a mouse button is pressed down on an element - func onMouseDown(_ value: String) -> Self { - attribute("onmousedown", value) - } - - /// Fires when the mouse pointer is moving while it is over an element - func onMouseMove(_ value: String) -> Self { - attribute("onmousemove", value) - } - - /// Fires when the mouse pointer moves out of an element - func onMouseOut(_ value: String) -> Self { - attribute("onmouseout", value) - } - - /// Fires when the mouse pointer moves over an element - func onMouseOver(_ value: String) -> Self { - attribute("onmouseover", value) - } - - /// Fires when a mouse button is released over an element - func onMouseUp(_ value: String) -> Self { - attribute("onmouseup", value) - } - - /// Fires when the mouse wheel rolls up or down over an element - func onWheel(_ value: String) -> Self { - attribute("onwheel", value) - } - - // MARK: - Drag Events - - /// Script to be run when an element is dragged - func onDrag(_ value: String) -> Self { - attribute("ondrag", value) - } - - /// Script to be run at the end of a drag operation - func onDragEnd(_ value: String) -> Self { - attribute("ondragend", value) - } - - /// Script to be run when an element has been dragged to a valid drop target - func onDragEnter(_ value: String) -> Self { - attribute("ondragenter", value) - } - - /// Script to be run when an element leaves a valid drop target - func onDragLeave(_ value: String) -> Self { - attribute("ondragleave", value) - } - - /// Script to be run when an element is being dragged over a valid drop target - func onDragOver(_ value: String) -> Self { - attribute("ondragover", value) - } - - /// Script to be run at the start of a drag operation - func onDragStart(_ value: String) -> Self { - attribute("ondragstart", value) - } - - /// Script to be run when dragged element is being dropped - func onDrop(_ value: String) -> Self { - attribute("ondrop", value) - } - - /// Script to be run when an element's scrollbar is being scrolled - func onScroll(_ value: String) -> Self { - attribute("onscroll", value) - } - - // MARK: - Clipboard Events - - /// Fires when the user copies the content of an element - func onCopy(_ value: String) -> Self { - attribute("oncopy", value) - } - - /// Fires when the user cuts the content of an element - func onCut(_ value: String) -> Self { - attribute("oncut", value) +public extension Attribute { + + enum Event: String { + // Windows + case afterPrint // Script to be run after the document is printed + case beforePrint // Script to be run before the document is printed + case beforeUnload // Script to be run when the document is about to be unloaded + case error // Script to be run when an error occurs + case hashChange // Script to be run when there has been changes to the anchor part of the a URL + case load // Fires after the page is finished loading + case message // Script to be run when the message is triggered + case offline // Script to be run when the browser starts to work offline + case online // Script to be run when the browser starts to work online + case pageHide // Script to be run when a user navigates away from a page + case pageShow // Script to be run when a user navigates to a page + case popState // Script to be run when the window's history changes + case resize // Fires when the browser window is resized + case storage // Script to be run when a Web Storage area is updated + case unload // Fires once a page has unloaded (or the browser window has been closed) + // Forms + case blur // Fires the moment that the element loses focus + case change // Fires the moment when the value of the element is changed + case contextMenu // Script to be run when a context menu is triggered + case focus // Fires the moment when the element gets focus + case input // Script to be run when an element gets user input + case invalid // Script to be run when an element is invalid + case reset // Fires when the Reset button in a form is clicked + case search // Fires when the user writes something in a search field (for ) + case select // Fires after some text has been selected in an element + case submit // Fires when a form is submitted + // Keyboard + case keyDown // Fires when a user is pressing a key + case keyPress // Fires when a user presses a key + case keyUp // Fires when a user releases a key + // Mouse + case click // Fires on a mouse click on the element + case doubleClick // Fires on a mouse double-click on the element + case mouseDown // Fires when a mouse button is pressed down on an element + case mouseMove // Fires when the mouse pointer is moving while it is over an element + case mouseOut // Fires when the mouse pointer moves out of an element + case mouseOver // Fires when the mouse pointer moves over an element + case mouseUp // Fires when a mouse button is released over an element + case wheel // Fires when the mouse wheel rolls up or down over an element + // Drag and Drop + case drag // Script to be run when an element is dragged + case dragEnd // Script to be run at the end of a drag operation + case dragEnter // Script to be run when an element has been dragged to a valid drop target + case dragLeave // Script to be run when an element leaves a valid drop target + case dragOver // Script to be run when an element is being dragged over a valid drop target + case dragStart // Script to be run at the start of a drag operation + case drop // Script to be run when dragged element is being dropped + case scroll // Script to be run when an element's scrollbar is being scrolled + // Clipboard + case copy // Fires when the user copies the content of an element + case cut // Fires when the user cuts the content of an element + case paste // Fires when the user pastes some content in an element + // Media + case abort // Script to be run on abort + case canPlay // Script to be run when a file is ready to start playing (when it has buffered enough to begin) + case canPlaythrough // Script to be run when a file can be played all the way to the end without pausing for buffering + case cueChange // Script to be run when the cue changes in a element + case durationChange // Script to be run when the length of the media changes + case emptied // Script to be run when something bad happens and the file is suddenly unavailable (like unexpectedly disconnects) + case ended // Script to be run when the media has reach the end (a useful event for messages like "thanks for listening") + case loadedData // Script to be run when media data is loaded + case loadedMetadata // Script to be run when meta data (like dimensions and duration) are loaded + case loadStart // Script to be run just as the file begins to load before anything is actually loaded + case pause // Script to be run when the media is paused either by the user or programmatically + case play // Script to be run when the media is ready to start playing + case playing // Script to be run when the media actually has started playing + case progress // Script to be run when the browser is in the process of getting the media data + case rateChange // Script to be run each time the playback rate changes (like when a user switches to a slow motion or fast forward mode) + case seeked // Script to be run when the seeking attribute is set to false indicating that seeking has ended + case seeking // Script to be run when the seeking attribute is set to true indicating that seeking is active + case stalled // Script to be run when the browser is unable to fetch the media data for whatever reason + case suspend // Script to be run when fetching the media data is stopped before it is completely loaded for whatever reason + case timeUpdate // Script to be run when the playing position has changed (like when the user fast forwards to a different point in the media) + case volumeChange // Script to be run each time the volume is changed which (includes setting the volume to "mute") + case waiting // Script to be run when the media has paused but is expected to resume (like when the media pauses to buffer more data) + // Misc + case toggle // Fires when the user opens or closes the

element + } + + typealias JSFunction = String + + struct EventFunction { + let event: Event + let function: JSFunction + + public init(_ event: Event, _ function: JSFunction) { + self.event = event + self.function = function + } } +} - /// Fires when the user pastes some content in an element - func onPaste(_ value: String) -> Self { - attribute("onpaste", value) - } - - // MARK: - Media Events +public extension Tag { - /// Script to be run on abort - 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) - 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 - func onCanPlaythrough(_ value: String) -> Self { - attribute("oncanplaythrough", value) - } - - /// Script to be run when the cue changes in a element - func onCueChange(_ value: String) -> Self { - attribute("oncuechange", value) - } - - /// Script to be run when the length of the media changes - 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) - 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") - 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 - func onLoadedData(_ value: String) -> Self { - attribute("onloadeddata", value) - } - - /// Script to be run when meta data (like dimensions and duration) are loaded - func onLoadedMetadata(_ value: String) -> Self { - attribute("onloadedmetadata", value) + @discardableResult + func onEvent(_ e: Attribute.Event, _ function: Attribute.JSFunction?, _ condition: Bool = true) -> Self { + attribute("on" + e.rawValue.lowercased(), function, condition) } - /// Script to be run just as the file begins to load before anything is actually loaded - func onLoadStart(_ value: String) -> Self { - attribute("onloadstart", value) - } - - /// Script to be run when the media is paused either by the user or programmatically - func onPause(_ value: String) -> Self { - attribute("onpause", value) - } - - /// Script to be run when the media is ready to start playing - func onPlay(_ value: String) -> Self { - attribute("onplay", value) - } - - /// Script to be run when the media actually has started playing - func onPlaying(_ value: String) -> Self { - attribute("onplaying", value) - } - - /// Script to be run when the browser is in the process of getting the media data - 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) - 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 - 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 - 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 - 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 - 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) - 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") - 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) - func onWaiting(_ value: String) -> Self { - attribute("onwaiting", value) - } - - // MARK: - Misc Events - - /// Fires when the user opens or closes the
element - func onToggle(_ value: String) -> Self { - attribute("ontoggle", value) + /// add multiple events and functions to a Tag + @discardableResult + func onEvents(_ efs: [Attribute.EventFunction]?, condition: Bool = true) -> Self { + guard let efs = efs, condition else { return self } + _ = efs.map { attribute(addTo: "on" + $0.event.rawValue.lowercased(), $0.function, separator: ";\r") } + return self } } diff --git a/Sources/SwiftHtml/Attributes/Global.swift b/Sources/SwiftHtml/Attributes/Global.swift index b1c5526..20ecd3b 100644 --- a/Sources/SwiftHtml/Attributes/Global.swift +++ b/Sources/SwiftHtml/Attributes/Global.swift @@ -61,21 +61,24 @@ public extension Tag { /// find an existing class attribute and return the value as an array of strings or an empty array private var classArray: [String] { - node.attributes.first { $0.key == "class" }?.value?.classArray ?? [] + attributes?.first { $0.key == "class" }?.value?.classArray ?? [] } /// Specifies one classname for an element (refers to a class in a style sheet) + @discardableResult func `class`(_ value: String?, _ condition: Bool = true) -> Self { attribute("class", value, condition) } /// Specifies multiple classnames for an element (refers to a class in a style sheet) + @discardableResult func `class`(_ values: [String], _ condition: Bool = true) -> Self { /// @NOTE: explicit true flag is needed, otherwise Swift won't know which function to call... `class`(values.classString, condition) } /// Specifies multiple classnames for an element (refers to a class in a style sheet) + @discardableResult func `class`(_ values: String...) -> Self { `class`(values) } @@ -84,6 +87,7 @@ public extension Tag { /// /// Note: If the value is empty or nil it won't be added to the list /// + @discardableResult func `class`(add value: String?, _ condition: Bool = true) -> Self { guard let value = value else { return self @@ -95,6 +99,7 @@ public extension Tag { /// /// Note: If the value is empty it won't be added to the list /// + @discardableResult func `class`(add values: [String], _ condition: Bool = true) -> Self { let newValues = classArray + values.filter { !$0.isEmpty } @@ -106,6 +111,7 @@ public extension Tag { } /// Removes a given class values if the condition is true + @discardableResult func `class`(remove value: String?, _ condition: Bool = true) -> Self { guard let value = value else { return self @@ -114,15 +120,17 @@ public extension Tag { } /// Removes an array of class values if the condition is true + @discardableResult func `class`(remove values: [String], _ condition: Bool = true) -> Self { let newClasses = classArray.filter { !values.contains($0) } if newClasses.isEmpty { - return deleteAttribute("class") + return deleteAttribute("class", condition) } return `class`(newClasses, condition) } /// toggles a single class value + @discardableResult func `class`(toggle value: String?, _ condition: Bool = true) -> Self { guard let value = value else { return self @@ -137,22 +145,25 @@ public extension Tag { /// 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 ?? [] + attributes?.first { $0.key == "style" }?.value?.styleArray ?? [] } /// Specifies one stylename for an element (refers to a style in a style sheet) + @discardableResult func style(_ value: String?, _ condition: Bool = true) -> Self { - guard let value, !value.isEmpty else { return self } + guard let value = 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) + @discardableResult 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) + @discardableResult func style(_ values: String...) -> Self { style(values) } @@ -161,6 +172,7 @@ public extension Tag { /// /// Note: If the value is empty or nil it won't be added to the list /// + @discardableResult func style(add value: String?, _ condition: Bool = true) -> Self { guard let value = value else { return self @@ -172,6 +184,7 @@ public extension Tag { /// /// Note: If the value is empty it won't be added to the list /// + @discardableResult func style(add values: [String], _ condition: Bool = true) -> Self { let newValues = styleArray + values.filter { !$0.isEmpty } @@ -183,6 +196,7 @@ public extension Tag { } /// Removes a given style values if the condition is true + @discardableResult func style(remove value: String?, _ condition: Bool = true) -> Self { guard let value = value else { return self @@ -191,16 +205,18 @@ public extension Tag { } /// Removes an array of style values if the condition is true + @discardableResult func style(remove values: [String], _ condition: Bool = true) -> Self { let newClasses = styleArray.filter { !values.contains($0) } if newClasses.isEmpty { - return deleteAttribute("style") + return deleteAttribute("style", condition) } 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")` + @discardableResult func style(removeByKey value: String?, _ condition: Bool = true) -> Self { guard let value = value else { return self @@ -211,15 +227,17 @@ public extension Tag { /// 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"])` + @discardableResult 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 deleteAttribute("style", condition) } return style(newClasses, condition) } /// toggles a single style value + @discardableResult func style(toggle value: String?, _ condition: Bool = true) -> Self { guard let value = value else { return self @@ -234,62 +252,68 @@ public extension Tag { // MARK: - other global attributes /// Specifies a shortcut key to activate/focus an element - func accesskey(_ value: Character) -> Self { - attribute("accesskey", String(value)) + @discardableResult + func accesskey(_ value: Character?, _ condition: Bool = true) -> Self { + attribute("accesskey", value != nil ? String(value!) : nil, condition) } /// Specifies whether the content of an element is editable or not - func contenteditable(_ value: Bool) -> Self { - attribute("contenteditable", String(value)) + @discardableResult + func contenteditable(_ value: Bool?, _ condition: Bool = true) -> Self { + attribute("contenteditable", value != nil ? String(value!) : nil, condition) } /// Used to store custom data private to the page or application - func data(key: String, _ value: String) -> Self { - attribute("data-" + key, value) + @discardableResult + func data(key: String, _ value: String?, _ condition: Bool = true) -> Self { + attribute("data-" + key, value, condition) } /// Specifies the text direction for the content in an element - func dir(_ value: TextDirection = .ltr) -> Self { - attribute("dir", value.rawValue) + @discardableResult + func dir(_ value: TextDirection? = .ltr, _ condition: Bool = true) -> Self { + attribute("dir", value?.rawValue, condition) } /// Specifies whether an element is draggable or not - func draggable(_ value: Draggable = .auto) -> Self { - attribute("draggable", value.rawValue) + @discardableResult + func draggable(_ value: Draggable? = .auto, _ condition: Bool = true) -> Self { + attribute("draggable", value?.rawValue, condition) } /// Specifies that an element is not yet, or is no longer, relevant - func hidden(_ value: Bool? = nil) -> Self { - attribute("hidden", value?.description) + @discardableResult + func hidden(_ value: Bool? = nil, _ condition: Bool = true) -> Self { + attribute("hidden", value?.description, condition) } /// Specifies a unique id for an element - func `id`(_ value: String) -> Self { - attribute("id", value) - } - - /// Specifies the language of the element's content - func lang(_ value: String) -> Self { - attribute("lang", value) + @discardableResult + func `id`(_ value: String?, _ condition: Bool = true) -> Self { + attribute("id", value, condition) } /// Specifies whether the element is to have its spelling and grammar checked or not - func spellcheck(_ value: Bool) -> Self { - attribute("spellcheck", String(value)) + @discardableResult + func spellcheck(_ value: Bool?, _ condition: Bool = true) -> Self { + attribute("spellcheck", value != nil ? String(value!) : nil, condition) } /// Specifies the tabbing order of an element - func tabindex(_ value: Int) -> Self { - attribute("tabindex", String(value)) + @discardableResult + func tabindex(_ value: Int?, _ condition: Bool = true) -> Self { + attribute("tabindex", value != nil ? String(value!) : nil, condition) } /// Specifies extra information about an element - func title(_ value: String) -> Self { - attribute("title", value) + @discardableResult + func title(_ value: String?, _ condition: Bool = true) -> Self { + attribute("title", value, condition) } /// Specifies whether the content of an element should be translated or not - func translate(_ value: Translate) -> Self { - attribute("translate", value.rawValue) + @discardableResult + func translate(_ value: Translate?, _ condition: Bool = true) -> Self { + attribute("translate", value?.rawValue, condition) } } diff --git a/Sources/SwiftHtml/Attributes/Lang.swift b/Sources/SwiftHtml/Attributes/Lang.swift new file mode 100644 index 0000000..aee16d5 --- /dev/null +++ b/Sources/SwiftHtml/Attributes/Lang.swift @@ -0,0 +1,478 @@ +// +// Language.swift +// +// +// Created by Brad Gourley on 7/26/23. +// + +public extension Attribute { + + // source https://www.w3schools.com/tags/ref_language_codes.asp + enum Language: String { + case ab + case aa + case af + case ak + case sq + case am + case ar + case an + case hy + case `as` + case av + case ae + case ay + case az + case bm + case ba + case eu + case be + case bn + case bh + case bi + case bs + case br + case bg + case my + case ca + case ch + case ce + case ny + case zh + case zhHans = "zh-Hans" + case zhHant = "zh-Hant" + case cv + case kw + case co + case cr + case hr + case cs + case da + case dv + case nl + case dz + case en + case eo + case et + case ee + case fo + case fj + case fi + case fr + case ff + case gl + case gd + case gv + case ka + case de + case el + case kl + case gn + case gu + case ht + case ha + case he + case hz + case hi + case ho + case hu + case `is` + case io + case ig + case id + case `in` + case ia + case ie + case iu + case ik + case ga + case it + case ja + case jv + case kn + case kr + case ks + case kk + case km + case ki + case rw + case rn + case ky + case kv + case kg + case ko + case ku + case kj + case lo + case la + case lv + case li + case ln + case lt + case lu + case lg + case lb + case mk + case mg + case ms + case ml + case mt + case mi + case mr + case mh + case mo + case mn + case na + case nv + case ng + case nd + case ne + case no + case nb + case nn + case ii + case oc + case oj + case cu + case or + case om + case os + case pi + case ps + case fa + case pl + case pt + case pa + case qu + case rm + case ro + case ru + case se + case sm + case sg + case sa + case sr + case sh + case st + case tn + case sn + case sd + case si + case ss + case sk + case sl + case so + case nr + case es + case su + case sw + case sv + case tl + case ty + case tg + case ta + case tt + case te + case th + case bo + case ti + case to + case ts + case tr + case tk + case tw + case ug + case uk + case ur + case uz + case ve + case vi + case vo + case wa + case cy + case wo + case fy + case xh + case yi + case yo + case za + case zu + case ji + } + + // source https://www.w3schools.com/tags/ref_country_codes.asp + enum Country: String { + case af + case al + case dz + case `as` + case ad + case ao + case aq + case ag + case ar + case am + case aw + case au + case at + case az + case bs + case bh + case bd + case bb + case by + case be + case bz + case bj + case bm + case bt + case bo + case ba + case bw + case bv + case br + case io + case bn + case bg + case bf + case bi + case kh + case cm + case ca + case cv + case ky + case cf + case td + case cl + case cn + case cx + case cc + case co + case km + case cg + case cd + case ck + case cr + case ci + case hr + case cu + case cy + case cz + case dk + case dj + case dm + case `do` + case ec + case eg + case sv + case gq + case er + case ee + case et + case fk + case fo + case fj + case fi + case fr + case gf + case pf + case tf + case ga + case gm + case ge + case de + case gh + case gi + case gr + case gl + case gd + case gp + case gu + case gt + case gn + case gw + case gy + case ht + case hm + case hn + case hk + case hu + case `is` + case `in` + case id + case ir + case iq + case ie + case il + case it + case jm + case jp + case jo + case kz + case ke + case ki + case kp + case kr + case kw + case kg + case la + case lv + case lb + case ls + case lr + case ly + case li + case lt + case lu + case mo + case mk + case mg + case mw + case my + case mv + case ml + case mt + case mh + case mq + case mr + case mu + case yt + case mx + case fm + case md + case mc + case mn + case me + case ms + case ma + case mz + case mm + case na + case nr + case np + case nl + case an + case nc + case nz + case ni + case ne + case ng + case nu + case nf + case mp + case no + case om + case pk + case pw + case ps + case pa + case pg + case py + case pe + case ph + case pn + case pl + case pt + case pr + case qa + case re + case ro + case ru + case rw + case sh + case kn + case lc + case pm + case vc + case ws + case sm + case st + case sa + case sn + case rs + case sc + case sl + case sg + case sk + case si + case sb + case so + case za + case gs + case ss + case es + case lk + case sd + case sr + case sj + case sz + case se + case ch + case sy + case tw + case tj + case tz + case th + case tl + case tg + case tk + case to + case tt + case tn + case tr + case tm + case tc + case tv + case ug + case ua + case ae + case gb + case us + case um + case uy + case uz + case vu + case ve + case vn + case vg + case vi + case wf + case eh + case ye + case zm + case zw + } + + struct Lang { + let language: Language + let country: Country? + + public init(_ language: Language, _ country: Country? = nil) { + self.language = language + self.country = country + } + + var rawValue: String { + guard let country = country else { + return language.rawValue + } + return language.rawValue + "-\(country.rawValue.uppercased())" + } + } +} + +public extension Tag { + + @discardableResult + func lang(_ l: Attribute.Lang?, _ condition: Bool = true) -> Self { + guard let l = l, condition else { return self } + return attribute("lang", l.rawValue) + } + + @discardableResult + func lang(_ language: Attribute.Language?, _ country: Attribute.Country? = nil, _ condition: Bool = true) -> Self { + guard let language = language else { return self } + let lang: Attribute.Lang = .init(language, country) + return self.lang(lang, condition) + } +} diff --git a/Sources/SwiftHtml/Attributes/Target.swift b/Sources/SwiftHtml/Attributes/Target.swift index b02493d..558d8b7 100644 --- a/Sources/SwiftHtml/Attributes/Target.swift +++ b/Sources/SwiftHtml/Attributes/Target.swift @@ -28,7 +28,7 @@ public enum TargetFrame { /// Opens the linked document in the named iframe case frame(String) - var rawValue: String { + public var rawValue: String { switch self { case .blank: return "_blank" diff --git a/Sources/SwiftHtml/Tags/A.swift b/Sources/SwiftHtml/Tags/A.swift index 0a342fd..b26e1dd 100644 --- a/Sources/SwiftHtml/Tags/A.swift +++ b/Sources/SwiftHtml/Tags/A.swift @@ -14,8 +14,9 @@ /// - An unvisited link is underlined and blue /// - A visited link is underlined and purple /// - An active link is underlined and red -open class A: Tag { - +open class A: StandardTag { + + override open class var name: String { .init(describing: A.self).lowercased() } } public extension A { @@ -51,28 +52,33 @@ public extension A { } /// Specifies that the target will be downloaded when a user clicks on the hyperlink - func download(_ value: String? = nil) -> Self { - flagAttribute("download", value) + @discardableResult + func download(_ value: String? = nil, _ condition: Bool = true) -> Self { + flagAttribute("download", value, condition) } /// Specifies the URL of the page the link goes to - func href(_ value: String) -> Self { - attribute("href", value) + @discardableResult + func href(_ value: String?, _ condition: Bool = true) -> Self { + attribute("href", value, condition) } /// Specifies the language of the linked document - func hreflang(_ value: String) -> Self { - attribute("hreflang", value) + @discardableResult + func hreflang(_ value: String?, _ condition: Bool = true) -> Self { + attribute("hreflang", value, condition) } /// Specifies what media/device the linked document is optimized for - func media(_ value: String) -> Self { - attribute("media", value) + @discardableResult + func media(_ value: String?, _ condition: Bool = true) -> Self { + attribute("media", value, condition) } /// Specifies what media/device the linked document is optimized for /// /// If multiple queries were provided they're going to be concatenated with an `and` operand + @discardableResult func media(_ queries: MediaQuery...) -> Self { return media(queries) } @@ -80,34 +86,40 @@ public extension A { /// Specifies what media/device the linked document is optimized for /// /// If multiple queries were provided they're going to be concatenated with an `and` operand - func media(_ queries: [MediaQuery]) -> Self { - return media(queries.map(\.value).joined(separator: " and ")) + @discardableResult + func media(_ queries: [MediaQuery]?, _ condition: Bool = true) -> Self { + return media(queries?.map(\.value).joined(separator: " and "), condition) } /// 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. - func ping(_ value: [String]) -> Self { - attribute("ping", value.joined(separator: " ")) + @discardableResult + func ping(_ value: [String]?, _ condition: Bool = true) -> Self { + attribute("ping", value?.joined(separator: " "), condition) } /// Specifies which referrer information to send with the link - func refererPolicy(_ value: RefererPolicy = .origin) -> Self { - attribute("referrerpolicy", value.rawValue) + @discardableResult + func refererPolicy(_ value: RefererPolicy? = .origin, _ condition: Bool = true) -> Self { + attribute("referrerpolicy", value?.rawValue, condition) } /// Specifies the relationship between the current document and the linked document - func rel(_ value: Rel) -> Self { - attribute("rel", value.rawValue) + @discardableResult + func rel(_ value: Rel?, _ condition: Bool = true) -> Self { + attribute("rel", value?.rawValue, condition) } /// Specifies where to open the linked document - func target(_ value: TargetFrame, _ condition: Bool = true) -> Self { - attribute("target", value.rawValue, condition) + @discardableResult + 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. - func type(_ value: String) -> Self { - attribute("type", value) + @discardableResult + func type(_ value: String?, _ condition: Bool = true) -> Self { + attribute("type", value, condition) } } diff --git a/Sources/SwiftHtml/Tags/Abbr.swift b/Sources/SwiftHtml/Tags/Abbr.swift index bbc38fc..ccc67e0 100644 --- a/Sources/SwiftHtml/Tags/Abbr.swift +++ b/Sources/SwiftHtml/Tags/Abbr.swift @@ -8,7 +8,8 @@ /// 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. -open class Abbr: Tag { - +open class Abbr: StandardTag { + + override open class var name: String { .init(describing: Abbr.self).lowercased() } } diff --git a/Sources/SwiftHtml/Tags/Address.swift b/Sources/SwiftHtml/Tags/Address.swift index 4d3fe25..a91a04d 100644 --- a/Sources/SwiftHtml/Tags/Address.swift +++ b/Sources/SwiftHtml/Tags/Address.swift @@ -10,6 +10,7 @@ /// 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. -open class Address: Tag { - +open class Address: StandardTag { + + override open class var name: String { .init(describing: Address.self).lowercased() } } diff --git a/Sources/SwiftHtml/Tags/Area.swift b/Sources/SwiftHtml/Tags/Area.swift index 0ce5ca8..d4019c4 100644 --- a/Sources/SwiftHtml/Tags/Area.swift +++ b/Sources/SwiftHtml/Tags/Area.swift @@ -50,7 +50,8 @@ open class Area: EmptyTag { /// Defines a polygonal region case poly } - + + override open class var name: String { .init(describing: Area.self).lowercased() } } public extension Area { diff --git a/Sources/SwiftHtml/Tags/Article.swift b/Sources/SwiftHtml/Tags/Article.swift index b78d593..1598a07 100644 --- a/Sources/SwiftHtml/Tags/Article.swift +++ b/Sources/SwiftHtml/Tags/Article.swift @@ -5,12 +5,6 @@ // Created by Tibor Bodecs on 2021. 07. 19.. // -public extension Node { - - static func article() -> Node { - Node(type: .standard, name: "article") - } -} /// The `
` tag specifies independent, self-contained content. /// @@ -24,7 +18,8 @@ public extension Node { /// /// **Note:** The `
` element does not render as anything special in a browser. /// However, you can use CSS to style the `
` element (see example below). -open class Article: Tag { - +open class Article: StandardTag { + + override open class var name: String { .init(describing: Article.self).lowercased() } } diff --git a/Sources/SwiftHtml/Tags/Aside.swift b/Sources/SwiftHtml/Tags/Aside.swift index 93b1ab9..77f8a4f 100644 --- a/Sources/SwiftHtml/Tags/Aside.swift +++ b/Sources/SwiftHtml/Tags/Aside.swift @@ -13,7 +13,8 @@ /// /// **Note:** The `