Created
May 10, 2015 21:49
-
-
Save karlwilcox/f9582c5afd6b22b0d515 to your computer and use it in GitHub Desktop.
An object which builds HTML code for subsequent use in a UIWebView. The HTML code is always valid as elements are created and closed automatically as required.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// HTMLBuilder.swift | |
// | |
// Created by Karl Wilcox on 16/03/2015. | |
// Copyright (c) 2015 Karl Wilcox. All rights reserved. | |
// | |
import Foundation | |
/// Builds and returns valid HTML for use in UIWebView | |
/* Example Usage: | |
* let page = HTMLBuilder() | |
* page.stylesheet = "stylesheet.css" | |
* page.startDiv("content") | |
* page.AddHeading("A Heading") | |
* page.addPlainText("The content is always valid HTML") | |
* page.addListItem("item 1") | |
* page.addListItem("item 2") | |
* let stringForUIWebView = page.HTMLasString | |
*/ | |
class HTMLBuilder { | |
/* public type for specifying the list */ | |
enum listType { case none, bullet, numbered } | |
/* private vars to track what state the HTML is in */ | |
private var content:String = "" | |
private var inParagraph = false | |
private var inList = listType.none | |
private var divStack:[String] = [] | |
private var newLine = "" | |
/// prettyPrint boolean, use newlines | |
var prettyPrint:Bool = false { | |
didSet { | |
if prettyPrint == true { | |
newLine = "\n" | |
} else { | |
newLine = "" | |
} | |
} | |
} | |
/// stylesheetName the basename (without .css) of the required stylesheet | |
var stylesheetName:String = "" { | |
didSet { | |
if stylesheetName != "" { | |
stylesheetName = "<link type=\"text/css\" rel=\"stylesheet\" href=\"\(stylesheetName).css\"/>\(newLine)" | |
} | |
} | |
} | |
/// paragraphClassAttribute sets class attribute of subsequently added paragraphs | |
var paragraphClassAttribute:String = "" { | |
didSet { | |
if paragraphClassAttribute != "" { | |
paragraphClassAttribute = " class=\"\(paragraphClassAttribute)\" " | |
} | |
} | |
} | |
/// listClassAttribute sets class attribute of subsequently added list (<ol> or <ul>) | |
var listClassAttribute:String = "" { | |
didSet { | |
if listClassAttribute != "" { | |
listClassAttribute = " class=\"\(listClassAttribute)\" " | |
} | |
} | |
} | |
/// rawHTML returns the actual code, and then deletes contents, allowing re-use with new content | |
var HTMLasString:String { | |
addMatchingCloseIfNeeded() | |
while divStack.count > 0 { | |
endDiv() | |
} | |
let valueToReturn = "<head>\(newLine)\(stylesheetName)</head>\(newLine)<body>\(newLine)\(content)</body>\(newLine)" | |
content = "" // makes it a reusable object | |
return valueToReturn | |
} | |
private func addMatchingCloseIfNeeded() { | |
if inList == .bullet { | |
content += "</ul>\(newLine)" | |
inList = .none | |
} else if inList == .numbered { | |
content += "</ol>\(newLine)" | |
inList = .none | |
} else if inParagraph { | |
content += "</p>\(newLine)" | |
inParagraph = false | |
} | |
} | |
private func addStartParaIfNeeded() { | |
if !inParagraph { | |
content += "<p\(paragraphClassAttribute)>" | |
inParagraph = true | |
} | |
} | |
private func escapeHTMLCharacters(var text:String) ->String { | |
let replacements = ["&", "&", // This MUST be first! | |
"\"", """, | |
"'", "'", | |
"<", "<", | |
">", ">" | |
] | |
for i in stride(from: 0, to: replacements.count, by: 2) { | |
text = text.stringByReplacingOccurrencesOfString(replacements[i], withString: replacements[i+1], options: nil, range: nil) | |
} | |
return text | |
} | |
/// Adds a heading to the HTML, of any level, with an optional class | |
/// | |
/// :param: title The text content of the heading | |
/// :param: headingClass? value of class attribute to, or nil for no attribute at all | |
/// :param: level heading level (digit only, 1 to 5 allowed) | |
func addHeading(title:String, headingClass:String? = nil, var level:Int = 1) { | |
var classAttribute = "" | |
level = min(5,max(1,level)) | |
addMatchingCloseIfNeeded() | |
if headingClass != nil && headingClass! != "" { | |
classAttribute = " class=\"\(headingClass!)\" " | |
} | |
content += "<h\(level)\(classAttribute)>\(escapeHTMLCharacters(title))</h\(level)>\(newLine)" | |
} | |
/// Starts a new paragraph. Only needed if already in a paragraph | |
func newParagraph() { | |
addMatchingCloseIfNeeded() | |
content += "<p\(paragraphClassAttribute)>" | |
} | |
/// Adds just plain text to the current paragraph, escaping HTML characters as required | |
/// The current class attribute will be added, if set | |
/// | |
/// :param: text the text to be added, any embedded HTML will be escaped! | |
func addPlainText(text:String) { | |
addStartParaIfNeeded() | |
content += escapeHTMLCharacters(text) + " " | |
} | |
/// inserts raw HTML - use with care!!! | |
func addInnerHTML(text:String) { | |
content += text | |
} | |
/// Adds bold text to the current paragraph, escaping HTML characters as required | |
/// | |
/// :param: text the text to be added, any embedded HTML will be escaped! | |
func addBoldText(text:String) { | |
addStartParaIfNeeded() | |
content += "<strong>\(escapeHTMLCharacters(text))</strong> " | |
} | |
/// Adds italic text to the current paragraph, escaping HTML characters as required | |
/// | |
/// :param: text the text to be added, any embedded HTML will be escaped! | |
func addItalicText(text:String) { | |
addStartParaIfNeeded() | |
content += "<em>\(escapeHTMLCharacters(text))</em> " | |
} | |
/// Adds a link to the current paragraph, with optional link text | |
/// | |
/// :param: url a URL string, which will be properly encoded so may contain spaces etc. | |
/// :param: text optional string, if present will be used as the link text instead of the URL | |
func addLink(url:String, text:String? = nil) { | |
addStartParaIfNeeded() | |
content += "<a href=\"\(url.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!)\"> " | |
if text != nil && text! != "" { | |
content += escapeHTMLCharacters(text!) | |
} else { | |
content += escapeHTMLCharacters(url) | |
} | |
content += "</a>" | |
} | |
/// Adds a list item of a given type, ending paragraph and starting list if necessary | |
/// | |
/// :param: text the contain of the list item (sorry, plain text only at the moment) | |
/// :param: type optional, set to .bullet (the default) or .numbered, if .none or missing, continue same | |
func addListItem(text:String, type:listType = .none) { | |
if inParagraph { | |
content += "</p>\(newLine)" | |
inParagraph = false | |
} | |
switch inList { | |
case .none: // not in any list, so start a new one | |
if type == .numbered { | |
content += "<ol\(listClassAttribute)>\(newLine)" | |
inList = .numbered | |
} else { // wether it is .none or .bullet, go with bulleted | |
content += "<ul\(listClassAttribute)>\(newLine)" | |
inList = .bullet | |
} | |
case .bullet: // already in a bullet, change if different type | |
if type == .numbered { | |
content += "</ul><ol\(listClassAttribute)>\(newLine)" | |
inList = .numbered | |
} | |
case .numbered: // already in a numbered list, change if different type | |
if type == .bullet { | |
content += "</ol><ul\(listClassAttribute)>\(newLine)" | |
inList = .bullet | |
} | |
} | |
// Now add the list item | |
content += "<li>\(escapeHTMLCharacters(text))</li>\(newLine)" | |
} | |
/// Adds an image, ending paragraph or list as necessary (images are on their own) | |
/// | |
/// :param: url location of the image, will be properly encoded | |
/// :param: imageClass optional string will be set as the value of the class attribute | |
/// :param: width optional string, used as value of width attribute | |
/// :param: height optional string used as value of height attribute | |
func addImage(url: String, imageClass:String? = nil, width:String? = nil, height:String? = nil) { | |
var widthAttribute = "" | |
var heightAttribute = "" | |
var classAttribute = "" | |
addMatchingCloseIfNeeded() | |
if imageClass != nil && imageClass! != "" { | |
classAttribute = " class=\"\(imageClass!)\" " | |
} | |
if width != nil && width! != "" { | |
widthAttribute = " width=\"\(width!)\" " | |
} | |
if height != nil && height! != "" { | |
heightAttribute = " height=\"\(height!)\" " | |
} | |
content += "<img\(classAttribute) src=\"\(url.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!)\"\(widthAttribute)\(heightAttribute)/>\(newLine)" | |
} | |
private var dummyID = 1 | |
/// Starts a div, with an optional class and enforced ID, closing any paras or lists first | |
/// | |
/// :param: divID optional ID, if nil a dummy unique value will be used | |
/// :param: classAttribute optional string used as value of class attribute | |
func startDiv(divID:String?, divClass:String? = nil) { | |
var idValue = "" | |
var classAttribute = "" | |
addMatchingCloseIfNeeded() // can't start divs in paragraphs or lists | |
if divID == nil { | |
idValue = "div\(dummyID)" | |
dummyID += 1 | |
} else { | |
idValue = divID! | |
} | |
divStack.append(idValue) | |
if divClass != nil && divClass != "" { | |
classAttribute = " class=\"\(divClass!)\" " | |
} | |
content += "<div id=\"\(idValue)\"\(classAttribute)>\(newLine)" | |
} | |
/// ends the most recent div, harmless if no existing div | |
func endDiv() { | |
if divStack.count > 0 { | |
addMatchingCloseIfNeeded() // must end paragraph or list before ending div | |
if prettyPrint { | |
content += "<!-- \(divStack.last) -->" | |
} | |
content += "</div>\(newLine)" | |
divStack.removeLast() | |
} | |
} | |
func printRaw() { | |
println(content) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment