Add support for custom text escaping (#11)
* Add support for custom text escaping * swift format * Remove withDefaultDelimiters * Update README.md * Don't pass content type into partial
This commit is contained in:
@@ -146,3 +146,9 @@ Hello world
|
||||
</body>
|
||||
```
|
||||
Note the `{{$head}}` section in `base.mustache` is replaced with the `{{$head}}` section included inside the `{{<base}}` partial reference from `mypage.mustache`. The same occurs with the `{{$body}}` section. In that case though a default value is supplied for the situation where a `{{$body}}` section is not supplied.
|
||||
|
||||
### Pragma
|
||||
|
||||
The syntax `{{% var: value}}` can be used to set template rendering configuration variables specific to Hummingbird Mustache. The only variable you can set at the moment is `CONTENT_TYPE`. This can be set to either to `HTML` or `TEXT` and defines how variables are escaped. A content type of `TEXT` means no variables are escaped and a content type of `HTML` will do HTML escaping of the rendered text. The content type defaults to `HTML`.
|
||||
|
||||
Given input object "<>", template `{{%CONTENT_TYPE: HTML}}{{.}}` will render as `<>` and `{{%CONTENT_TYPE: TEXT}}{{.}}` will render as `<>`.
|
||||
|
||||
43
Sources/HummingbirdMustache/ContentType.swift
Normal file
43
Sources/HummingbirdMustache/ContentType.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
/// Protocol for content types
|
||||
public protocol HBMustacheContentType {
|
||||
/// escape text for this content type eg for HTML replace "<" with "<"
|
||||
func escapeText(_ text: String) -> String
|
||||
}
|
||||
|
||||
/// Text content type where no character is escaped
|
||||
struct HBTextContentType: HBMustacheContentType {
|
||||
func escapeText(_ text: String) -> String {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/// HTML content where text is escaped for HTML output
|
||||
struct HBHTMLContentType: HBMustacheContentType {
|
||||
func escapeText(_ text: String) -> String {
|
||||
return text.htmlEscape()
|
||||
}
|
||||
}
|
||||
|
||||
/// Map of strings to content types.
|
||||
///
|
||||
/// The string is read from the "CONTENT_TYPE" pragma `{{% CONTENT_TYPE: type}}`. Replace type with
|
||||
/// the content type required. The default available types are `TEXT` and `HTML`. You can register your own
|
||||
/// with `HBMustacheContentTypes.register`.
|
||||
public enum HBMustacheContentTypes {
|
||||
static func get(_ name: String) -> HBMustacheContentType? {
|
||||
return self.types[name]
|
||||
}
|
||||
|
||||
/// Register new content type
|
||||
/// - Parameters:
|
||||
/// - contentType: Content type
|
||||
/// - name: String to identify it
|
||||
public static func register(_ contentType: HBMustacheContentType, named name: String) {
|
||||
self.types[name] = contentType
|
||||
}
|
||||
|
||||
static var types: [String: HBMustacheContentType] = [
|
||||
"HTML": HBHTMLContentType(),
|
||||
"TEXT": HBTextContentType(),
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,7 @@ struct HBMustacheContext {
|
||||
let sequenceContext: HBMustacheSequenceContext?
|
||||
let indentation: String?
|
||||
let inherited: [String: HBMustacheTemplate]?
|
||||
let contentType: HBMustacheContentType
|
||||
|
||||
/// initialize context with a single objectt
|
||||
init(_ object: Any) {
|
||||
@@ -11,25 +12,34 @@ struct HBMustacheContext {
|
||||
self.sequenceContext = nil
|
||||
self.indentation = nil
|
||||
self.inherited = nil
|
||||
self.contentType = HBHTMLContentType()
|
||||
}
|
||||
|
||||
private init(
|
||||
stack: [Any],
|
||||
sequenceContext: HBMustacheSequenceContext?,
|
||||
indentation: String?,
|
||||
inherited: [String: HBMustacheTemplate]?
|
||||
inherited: [String: HBMustacheTemplate]?,
|
||||
contentType: HBMustacheContentType
|
||||
) {
|
||||
self.stack = stack
|
||||
self.sequenceContext = sequenceContext
|
||||
self.indentation = indentation
|
||||
self.inherited = inherited
|
||||
self.contentType = contentType
|
||||
}
|
||||
|
||||
/// return context with object add to stack
|
||||
func withObject(_ object: Any) -> HBMustacheContext {
|
||||
var stack = self.stack
|
||||
stack.append(object)
|
||||
return .init(stack: stack, sequenceContext: nil, indentation: self.indentation, inherited: self.inherited)
|
||||
return .init(
|
||||
stack: stack,
|
||||
sequenceContext: nil,
|
||||
indentation: self.indentation,
|
||||
inherited: self.inherited,
|
||||
contentType: self.contentType
|
||||
)
|
||||
}
|
||||
|
||||
/// return context with indent and parameter information for invoking a partial
|
||||
@@ -50,13 +60,36 @@ struct HBMustacheContext {
|
||||
} else {
|
||||
inherits = self.inherited
|
||||
}
|
||||
return .init(stack: self.stack, sequenceContext: nil, indentation: indentation, inherited: inherits)
|
||||
return .init(
|
||||
stack: self.stack,
|
||||
sequenceContext: nil,
|
||||
indentation: indentation,
|
||||
inherited: inherits,
|
||||
contentType: HBHTMLContentType()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// return context with sequence info and sequence element added to stack
|
||||
func withSequence(_ object: Any, sequenceContext: HBMustacheSequenceContext) -> HBMustacheContext {
|
||||
var stack = self.stack
|
||||
stack.append(object)
|
||||
return .init(stack: stack, sequenceContext: sequenceContext, indentation: self.indentation, inherited: self.inherited)
|
||||
return .init(
|
||||
stack: stack,
|
||||
sequenceContext: sequenceContext,
|
||||
indentation: self.indentation,
|
||||
inherited: self.inherited,
|
||||
contentType: self.contentType
|
||||
)
|
||||
}
|
||||
|
||||
/// return context with sequence info and sequence element added to stack
|
||||
func withContentType(_ contentType: HBMustacheContentType) -> HBMustacheContext {
|
||||
return .init(
|
||||
stack: self.stack,
|
||||
sequenceContext: self.sequenceContext,
|
||||
indentation: self.indentation,
|
||||
inherited: self.inherited,
|
||||
contentType: contentType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,19 @@ extension HBParser {
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read while closure returns true
|
||||
/// - Parameter while: closure
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(while cb: (Character) -> Bool) -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd(),
|
||||
cb(unsafeCurrent())
|
||||
{
|
||||
unsafeAdvance()
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read while character at current position is in supplied set
|
||||
/// - Parameter while: character set to check
|
||||
/// - Returns: String read from buffer
|
||||
|
||||
@@ -22,6 +22,10 @@ extension HBMustacheTemplate {
|
||||
case illegalTokenInsideInheritSection
|
||||
/// text found inside inherit section of partial
|
||||
case textInsideInheritSection
|
||||
/// config variable syntax is wrong
|
||||
case invalidConfigVariableSyntax
|
||||
/// unrecognised config variable
|
||||
case unrecognisedConfigVariable
|
||||
}
|
||||
|
||||
struct ParserState {
|
||||
@@ -51,13 +55,6 @@ extension HBMustacheTemplate {
|
||||
newValue.endDelimiter = end
|
||||
return newValue
|
||||
}
|
||||
|
||||
func withDefaultDelimiters(start _: String, end _: String) -> ParserState {
|
||||
var newValue = self
|
||||
newValue.startDelimiter = "{{"
|
||||
newValue.endDelimiter = "}}"
|
||||
return newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// parse mustache text to generate a list of tokens
|
||||
@@ -236,6 +233,14 @@ extension HBMustacheTemplate {
|
||||
state = try self.parserSetDelimiter(&parser, state: state)
|
||||
setNewLine = self.isStandalone(&parser, state: state)
|
||||
|
||||
case "%":
|
||||
// read config variable
|
||||
parser.unsafeAdvance()
|
||||
if let token = try self.readConfigVariable(&parser, state: state) {
|
||||
tokens.append(token)
|
||||
}
|
||||
setNewLine = self.isStandalone(&parser, state: state)
|
||||
|
||||
default:
|
||||
// variable
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
@@ -327,6 +332,34 @@ extension HBMustacheTemplate {
|
||||
return state.withDelimiters(start: String(startDelimiter), end: String(endDelimiter))
|
||||
}
|
||||
|
||||
static func readConfigVariable(_ parser: inout HBParser, state: ParserState) throws -> Token? {
|
||||
let variable: Substring
|
||||
let value: Substring
|
||||
|
||||
do {
|
||||
parser.read(while: \.isWhitespace)
|
||||
variable = parser.read(while: self.sectionNameCharsWithoutBrackets)
|
||||
parser.read(while: \.isWhitespace)
|
||||
guard try parser.read(":") else { throw Error.invalidConfigVariableSyntax }
|
||||
parser.read(while: \.isWhitespace)
|
||||
value = parser.read(while: self.sectionNameCharsWithoutBrackets)
|
||||
guard try parser.read(string: state.endDelimiter) else { throw Error.invalidConfigVariableSyntax }
|
||||
} catch {
|
||||
throw Error.invalidConfigVariableSyntax
|
||||
}
|
||||
|
||||
// do both variable and value have content
|
||||
guard variable.count > 0, value.count > 0 else { throw Error.invalidConfigVariableSyntax }
|
||||
|
||||
switch variable {
|
||||
case "CONTENT_TYPE":
|
||||
guard let contentType = HBMustacheContentTypes.get(String(value)) else { throw Error.unrecognisedConfigVariable }
|
||||
return .contentType(contentType)
|
||||
default:
|
||||
throw Error.unrecognisedConfigVariable
|
||||
}
|
||||
}
|
||||
|
||||
static func hasLineFinished(_ parser: inout HBParser) -> Bool {
|
||||
var parser2 = parser
|
||||
if parser.reachedEnd() { return true }
|
||||
|
||||
@@ -8,22 +8,24 @@ extension HBMustacheTemplate {
|
||||
/// - Returns: Rendered text
|
||||
func render(context: HBMustacheContext) -> String {
|
||||
var string = ""
|
||||
var context = context
|
||||
|
||||
if let indentation = context.indentation, indentation != "" {
|
||||
for token in tokens {
|
||||
if string.last == "\n" {
|
||||
string += indentation
|
||||
}
|
||||
string += self.renderToken(token, context: context)
|
||||
string += self.renderToken(token, context: &context)
|
||||
}
|
||||
} else {
|
||||
for token in tokens {
|
||||
string += self.renderToken(token, context: context)
|
||||
string += self.renderToken(token, context: &context)
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
func renderToken(_ token: Token, context: HBMustacheContext) -> String {
|
||||
func renderToken(_ token: Token, context: inout HBMustacheContext) -> String {
|
||||
switch token {
|
||||
case .text(let text):
|
||||
return text
|
||||
@@ -32,7 +34,7 @@ extension HBMustacheTemplate {
|
||||
if let template = child as? HBMustacheTemplate {
|
||||
return template.render(context: context)
|
||||
} else {
|
||||
return String(describing: child).htmlEscape()
|
||||
return context.contentType.escapeText(String(describing: child))
|
||||
}
|
||||
}
|
||||
case .unescapedVariable(let variable, let transform):
|
||||
@@ -58,6 +60,9 @@ extension HBMustacheTemplate {
|
||||
if let template = library?.getTemplate(named: name) {
|
||||
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
|
||||
}
|
||||
|
||||
case .contentType(let contentType):
|
||||
context = context.withContentType(contentType)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public final class HBMustacheTemplate {
|
||||
case invertedSection(name: String, transform: String? = nil, template: HBMustacheTemplate)
|
||||
case inheritedSection(name: String, template: HBMustacheTemplate)
|
||||
case partial(String, indentation: String?, inherits: [String: HBMustacheTemplate]?)
|
||||
case contentType(HBMustacheContentType)
|
||||
}
|
||||
|
||||
let tokens: [Token]
|
||||
|
||||
@@ -31,6 +31,11 @@ final class TemplateParserTests: XCTestCase {
|
||||
let template = try HBMustacheTemplate(string: "{{ section }}")
|
||||
XCTAssertEqual(template.tokens, [.variable(name: "section")])
|
||||
}
|
||||
|
||||
func testContentType() throws {
|
||||
let template = try HBMustacheTemplate(string: "{{% CONTENT_TYPE:TEXT}}")
|
||||
XCTAssertEqual(template.tokens, [.contentType(HBTextContentType())])
|
||||
}
|
||||
}
|
||||
|
||||
extension HBMustacheTemplate: Equatable {
|
||||
@@ -52,6 +57,8 @@ extension HBMustacheTemplate.Token: Equatable {
|
||||
return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
|
||||
case (.partial(let name1, let indent1, _), .partial(let name2, let indent2, _)):
|
||||
return name1 == name2 && indent1 == indent2
|
||||
case (.contentType(let contentType), .contentType(let contentType2)):
|
||||
return type(of: contentType) == type(of: contentType2)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -92,6 +92,13 @@ final class TemplateRendererTests: XCTestCase {
|
||||
XCTAssertEqual(template.render(Test(test: .init(string: "sub"))), "test sub")
|
||||
}
|
||||
|
||||
func testTextEscaping() throws {
|
||||
let template1 = try HBMustacheTemplate(string: "{{% CONTENT_TYPE:TEXT}}{{.}}")
|
||||
XCTAssertEqual(template1.render("<>"), "<>")
|
||||
let template2 = try HBMustacheTemplate(string: "{{% CONTENT_TYPE:HTML}}{{.}}")
|
||||
XCTAssertEqual(template2.render("<>"), "<>")
|
||||
}
|
||||
|
||||
/// variables
|
||||
func testMustacheManualExample1() throws {
|
||||
let template = try HBMustacheTemplate(string: """
|
||||
|
||||
Reference in New Issue
Block a user