From c25b7a52e71be96f022c532dab8c5cd93b3ca26d Mon Sep 17 00:00:00 2001 From: "T. R. Bernstein" <137705289+trbernstein@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:54:19 +0200 Subject: [PATCH] feat: Allow tokens to be escaped --- Sources/Stencil/Lexer.swift | 22 +++++++++++++++------- Tests/StencilTests/LexerSpec.swift | 16 ++++++++++++++++ Tests/StencilTests/TemplateSpec.swift | 6 ++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Sources/Stencil/Lexer.swift b/Sources/Stencil/Lexer.swift index 0140d63..a8bb13d 100644 --- a/Sources/Stencil/Lexer.swift +++ b/Sources/Stencil/Lexer.swift @@ -62,7 +62,7 @@ struct Lexer { /// - string: The content string of the token /// - range: The range within the template content, used for smart /// error reporting - func createToken(string: String, at range: Range) -> Token { + func createToken(string: String, at range: Range, _ isInEscapeMode: Bool = false) -> Token { func strip(length: (Int, Int) = (Self.tagLength, Self.tagLength)) -> String { guard string.count > (length.0 + length.1) else { return "" } let trimmed = String(string.dropFirst(length.0).dropLast(length.1)) @@ -73,7 +73,7 @@ struct Lexer { return trimmed } - if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") { + if !isInEscapeMode && (string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#")) { let behaviour = string.hasPrefix("{%") ? behaviour(string: string, tagLength: Self.tagLength) : .unspecified let stripLengths = ( Self.tagLength + (behaviour.leading != .unspecified ? 1 : 0), @@ -108,14 +108,14 @@ struct Lexer { let scanner = Scanner(templateString) while !scanner.isEmpty { - if let (char, text) = scanner.scanForTokenStart(Self.tokenChars) { + if let (char, text, isInEscapeMode) = scanner.scanForTokenStart(Self.tokenChars) { if !text.isEmpty { tokens.append(createToken(string: text, at: scanner.range)) } guard let end = Self.tokenCharMap[char] else { continue } let result = scanner.scanForTokenEnd(end) - tokens.append(createToken(string: result, at: scanner.range)) + tokens.append(createToken(string: result, at: scanner.range, isInEscapeMode)) } else { tokens.append(createToken(string: scanner.content, at: scanner.range)) scanner.content = "" @@ -148,6 +148,7 @@ class Scanner { private static let tokenStartDelimiter: Unicode.Scalar = "{" /// And the corresponding end delimiter for a token. private static let tokenEndDelimiter: Unicode.Scalar = "}" + private static let tokenDelimiterEscape: Unicode.Scalar = "\\" init(_ content: String) { self.originalContent = content @@ -203,18 +204,25 @@ class Scanner { /// - Parameter tokenChars: List of token start characters to search for. /// - Returns: The found token start character, together with the content /// before the token, or nil of no token start was found. - func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String)? { + // swiftlint:disable:next large_tuple + func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String, Bool)? { var foundBrace = false + var isInEscapeMode = false + var lastChar: Unicode.Scalar = " " range = range.upperBound..