Skip to content

Instantly share code, notes, and snippets.

@hooman
Created April 10, 2017 03:20
Show Gist options
  • Save hooman/e4695d2e33d2120377176f0b164a3b90 to your computer and use it in GitHub Desktop.
Save hooman/e4695d2e33d2120377176f0b164a3b90 to your computer and use it in GitHub Desktop.
This is a prototype code of a DSL to make it easier to generate HTML within Swift source code.
//
// HTMLBuilder.swift
//
// This is a prototype code of a DSL to make it easier to generate HTML within Swift source code.
//
// This version of the prototype usues global constants for html elements because of a compiler limitation / bug.
// The intended behavior was having these globals static properties of `Html` type.
//
// Created by Hooman Mehr on 04/07/17.
// Copyright © 2017 Hooman Mehr. See the MIT license at the bottom.
//
import Foundation
// Convenience operator to help with code readability, used in `rendered()` function.
private func * (lhs: String, rhs: Int) -> String { return rhs <= 0 ? "" : Array(repeating: lhs, count: rhs).joined() }
// The main type that represents an HTML element.
public struct Html {
// Attribute key strings are defined as enum for spell-checking and efficiency.
public enum Attribute: String {
case `class`, id, href, src, width, height, font, alt, dir, encoding
// ...
}
// Tag strings are defined as enum for spell-checking and efficiency.
fileprivate enum Tag: String {
case html, head, title, meta, link, base, style, body
case span, div, h1, h2, h3, h4, h5, h6, p, ul, ol, li, a, img
case form, input, textarea, button, select, option, label
case table, col, th, tr, td
case code, b, u, i
// ...
}
// content held as enum for efficiency.
private enum Content {
case empty
case text(String)
case elements([Html])
}
// Stored properties:
// `attributes` and `content` are mutable to ease future support of prototype parameter subsitution.
private let _tag: Tag
public var attributes: [Attribute:String] = [:]
private var content: Content = .empty
// Public properties:
public var tag: String { return _tag.rawValue }
public var text: String {
get {
if case let .text(text) = content { return text } else { return "" }
}
set {
if newValue == "" { content = .empty } else { content = .text(Html.safeText(newValue)) }
}
}
public var elements: [Html] {
get {
if case let .elements(elements) = content { return elements } else { return [] }
}
set {
if newValue.count == 0 { content = .empty } else { content = .elements(newValue) }
}
}
// Use either of the above properties to set it empty.
public var isEmpty: Bool { if case .empty = content { return true } else { return false } }
// Main private initializer.
// FIXME: Change from `fileprivate` to `private` once compiler issue is fixed.
fileprivate init(_ tag: Tag) { _tag = tag }
//FIXME: Implement safeText(_ rawText: String)
private static func safeText(_ rawText: String) -> String { return rawText }
//FIXME: Implement safeAttribute(_ rawText: String)
private func safeAttribute(_ rawValue: String) -> String { return rawValue }
// This is not the main API, but is here for people who prefer this way of doing it.
public func attr(
// Deticated parameters for common attributes:
class classValue: String? = nil,
id: String? = nil,
href: URL? = nil,
src: URL? = nil,
// The rest go in a dictionary:
_ attributes: [Attribute:String]? = nil) -> Html
{
var result = self
if let classValue = classValue { result.attributes[.class] = safeAttribute(classValue) }
if let id = id { result.attributes[.id] = safeAttribute(id) }
if let href = href { result.attributes[.href] = safeAttribute(href.relativeString) }
if let src = src { result.attributes[.src] = safeAttribute(src.relativeString) }
if let attributes = attributes { for (key, value) in attributes { result.attributes[key] = safeAttribute(value) } }
return result
}
// Main API to make HTML, case 1: An element containing a list of manually specified elements.
public subscript(_ contents: Html...) -> Html
{
var result = self
result.elements = contents
print(contents.map({$0.tag}))
return result
}
// The main API to make HTML, case 2: An element containing a list of code generated elements.
public subscript(_ contents: [Html]) -> Html
{
var result = self
result.elements = contents
return result
}
// The main API to make HTML, case 3: An element containing text.
public subscript(_ text: String) -> Html
{
var result = self
result.text = text
return result
}
// The main API to specify element attributes.
public subscript(_ attributes: [Attribute:String]) -> Html
{
return attr(attributes)
}
private var renderedAttributes: String {
if attributes.count == 0 {
return ""
} else {
return " " + attributes.map({ "\($0.rawValue)=\"\(safeAttribute($1))\"" }).joined(separator: ", ")
}
}
// The main result API:
func rendered(indentLevel: Int = 0, indentWith indent: String = "\t", endLineWith endLine: String = "\n") -> String {
let openTag: String
let closeTag: String
let nextLevel: Int
let needsBreak = elements.count > 1
if needsBreak {
openTag = "\(indent*indentLevel)<\(tag)\(renderedAttributes)>\(endLine)"
closeTag = "\(endLine)\(indent*indentLevel)</\(tag)>"
nextLevel = indentLevel + 1
} else {
openTag = "\(indent*indentLevel)<\(tag)>"
closeTag = "</\(tag)>"
nextLevel = 0
}
return "\(openTag)\(text)\(elements.map({"\($0.rendered(indentLevel: nextLevel))"}).joined(separator: endLine))\(closeTag)"
}
}
// FIXME: Move these global constants to static properties of `Html` type, once compiler issue is resolved.
public let html = Html(.html)
public let head = Html(.head)
public let title = Html(.title)
public let meta = Html(.meta)
public let link = Html(.link)
public let base = Html(.base)
public let style = Html(.style)
public let body = Html(.body)
public let span = Html(.span)
public let div = Html(.div)
public let h1 = Html(.h1)
public let h2 = Html(.h2)
public let h3 = Html(.h3)
public let h4 = Html(.h4)
public let h5 = Html(.h5)
public let h6 = Html(.h6)
public let p = Html(.p)
public let ul = Html(.ul)
public let ol = Html(.ol)
public let li = Html(.li)
public let a = Html(.a)
public let img = Html(.img)
public let form = Html(.form)
public let input = Html(.input)
public let textarea = Html(.textarea)
public let button = Html(.button)
public let select = Html(.select)
public let option = Html(.option)
public let label = Html(.label)
public let table = Html(.table)
public let col = Html(.col)
public let th = Html(.th)
public let tr = Html(.tr)
public let td = Html(.td)
public let code = Html(.code)
public let b = Html(.b)
public let u = Html(.u)
public let i = Html(.i)
// ...
// FIXME: Once compiler issue is resolved, the following code will change into:
//
// let welcomePage: Html =
// .html[
// .head[.title["Welcome"]],
// .body[
// .h1[[.class: "welcome"]]["Welcome"],
// .div["This is only the beginning!"],
// .ol[
// (1...4).map({Html.li["Item #\($0)"]})
// ]
// ]
// ]
let welcomePage =
html[
head[title["Welcome"]],
body[
h1[[.class: "welcome"]]["Welcome"],
div["This is only the beginning!"],
ol[
(1...4).map({li["Item #\($0)"]})
]
]
]
print(welcomePage.rendered())
// <html>
// <head><title>Welcome</title></head>
// <body>
// <h1>Welcome</h1>
// <div>This is only the beginning!</div>
// <ol>
// <li>Item #1</li>
// <li>Item #2</li>
// <li>Item #3</li>
// <li>Item #4</li>
// </ol>
// </body>
// </html>
// A public initializer is deliberately left out. You can create a no-arg initializer to create
// your empty HTML template with all header information set up and use it as the starting point:
//
// let myPage = Html()[.body[...
//
// You can also create prototypes of other components and add them as elements directly, instead
// of building HTML from scratch.
// Copyright (c) 2017 Hooman Mehr ([email protected])
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
@hooman
Copy link
Author

hooman commented Apr 10, 2017

I had a half worked prototype for generating html in Swift. I saw a post by @ownesd and decided to touch it up and put it up here along with his sample HTML as my feedback for him. His original post is here: https://owensd.io/2017/04/05/swiccup/ and a followup I just saw: https://owensd.io/2017/04/06/swiccup-round-2/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment