├── .gitignore ├── .swift-version ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Templater │ └── Templater.swift └── Tests ├── LinuxMain.swift └── Templater └── TemplaterTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | .build 8 | Packages 9 | DerivedData 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | 22 | ## Other 23 | *.xccheckout 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 63 | 64 | fastlane/report.xml 65 | fastlane/screenshots 66 | 67 | *.xcodeproj 68 | 69 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | DEVELOPMENT-SNAPSHOT-2016-07-25-a 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: trusty 7 | osx_image: xcode8 8 | install: 9 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/02090c7ede5a637b76e6df1710e83cd0bbe7dcdf/swiftenv-install.sh)" 10 | script: 11 | - swift build 12 | - swift test 13 | notifications: 14 | email: 15 | on_success: never 16 | on_failure: change 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at czechboy0@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Honza Dvorsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "Templater" 5 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Templater 2 | 3 | [![Build Status](https://travis-ci.org/czechboy0/Templater.svg?branch=master)](https://travis-ci.org/czechboy0/Templater) 4 | ![Platforms](https://img.shields.io/badge/platforms-Linux%20%7C%20OS%20X-blue.svg) 5 | ![Package Managers](https://img.shields.io/badge/package%20managers-SwiftPM-yellow.svg) 6 | 7 | [![Blog](https://img.shields.io/badge/blog-honzadvorsky.com-green.svg)](http://honzadvorsky.com) 8 | [![Twitter Czechboy0](https://img.shields.io/badge/twitter-czechboy0-green.svg)](http://twitter.com/czechboy0) 9 | 10 | > Very basic Swift templating engine. macOS and Linux ready. 11 | 12 | Templater is a very simple tool aimed at solving the simple task: in a string template, fill in a few variables with certain values. Something like [Stencil](https://github.com/kylef/Stencil) or [Mustache](http://mustache.github.io). Just a super simple version of them. 13 | 14 | # :question: Why? 15 | I needed to fill in a large markdown file with fresh data every day, to generate a pretty [report](https://github.com/czechboy0/swiftpm-packages-statistics). The existing libraries seemed like an overkill for the simple usecase, so I wrote a lightweight (literally less than 100 lines of code) solution myself. 16 | 17 | # :rocket: Features 18 | 19 | The template string must contain one or more strings like `{{ variable_name }}`, such as `Hello, {{ name }}!`. When you render this template with the context of `name = "world"`, you'll get `Hello, world!`. 20 | 21 | # :hammer: Usage 22 | 23 | ```swift 24 | do { 25 | //create the template 26 | let template = Template("Hello, {{ name }}! Today is {{ day }} and it will be {{ weather }}.") 27 | 28 | //have your context 29 | let context = [ 30 | "name": "Tim", 31 | "day": "Thursday", 32 | "weather": "sunny" 33 | ] 34 | 35 | //render the context 36 | let result = try template.fill(with: context) 37 | 38 | //result: "Hello, Tim! Today is Thursday and it will be sunny." 39 | } catch { 40 | print("Template error: \(error)") 41 | } 42 | ``` 43 | 44 | # Installation 45 | 46 | ## Swift Package Manager 47 | 48 | ```swift 49 | .Package(url: "https://github.com/czechboy0/Templater.git", majorVersion: 0, minor: 2) 50 | ``` 51 | 52 | :blue_heart: Code of Conduct 53 | ------------ 54 | Please note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 55 | 56 | :gift_heart: Contributing 57 | ------------ 58 | Please create an issue with a description of your problem or open a pull request with a fix. 59 | 60 | :v: License 61 | ------- 62 | MIT 63 | 64 | :alien: Author 65 | ------ 66 | Honza Dvorsky - http://honzadvorsky.com, [@czechboy0](http://twitter.com/czechboy0) 67 | -------------------------------------------------------------------------------- /Sources/Templater/Templater.swift: -------------------------------------------------------------------------------- 1 | //Templater.swift 2 | 3 | public struct Template { 4 | public let contents: String 5 | 6 | public init(_ contents: String) { 7 | self.contents = contents 8 | } 9 | 10 | public func fill(with context: [String: String]) throws -> String { 11 | return try _run(template: contents, context: context) 12 | } 13 | } 14 | 15 | private typealias CharacterTriplet = (Character, Character, Character) 16 | 17 | private struct BufferOfThree { 18 | var storage: CharacterTriplet = (Character(" "), Character(" "), Character(" ")) 19 | mutating func push(_ char: Character) { 20 | storage.0 = storage.1 21 | storage.1 = storage.2 22 | storage.2 = char 23 | } 24 | 25 | func contains(chars: CharacterTriplet) -> Bool { 26 | return chars.0 == storage.0 && chars.1 == storage.1 && chars.2 == storage.2 27 | } 28 | } 29 | 30 | public enum TemplateError: Error { 31 | case malformedTemplate(String) 32 | case valuesNotFoundInContext([String]) 33 | case variablesNotFoundInTemplate([String]) 34 | } 35 | 36 | private func _run(template: String, context: [String: String]) throws -> String { 37 | 38 | var chars = template.characters 39 | var curr = chars.startIndex 40 | 41 | let charsStart: CharacterTriplet = (Character("{"), Character("{"), Character(" ")) 42 | let charsEnd: CharacterTriplet = (Character(" "), Character("}"), Character("}")) 43 | 44 | var buffer = BufferOfThree() 45 | var isOpen = false 46 | var varName: [Character] = [] 47 | var openIndex: String.Index? = nil 48 | 49 | var unusedValues = Set(context.keys) 50 | 51 | while curr < chars.endIndex { 52 | if isOpen { 53 | //expect close, otherwise read insides 54 | if buffer.contains(chars: charsEnd) { 55 | //found a new var 56 | guard let openIdx = openIndex else { throw TemplateError.malformedTemplate(template) } 57 | let range = Range(uncheckedBounds: (openIdx, curr)) 58 | let actualVarName = varName.dropLast(3) 59 | let name = String(actualVarName) 60 | unusedValues.remove(name) 61 | guard let value = context[name] else { 62 | throw TemplateError.valuesNotFoundInContext([name]) 63 | } 64 | chars.replaceSubrange(range, with: value.characters) 65 | 66 | //FIXME: be smarter and continue at the offset index instead of starting over (simpler) 67 | curr = chars.startIndex 68 | 69 | isOpen = false 70 | openIndex = nil 71 | varName = [] 72 | } else { 73 | varName.append(chars[curr]) 74 | } 75 | } else { 76 | //expect open, otherwise skip 77 | if buffer.contains(chars: charsStart) { 78 | openIndex = chars.index(curr, offsetBy: -3) 79 | isOpen = true 80 | varName.append(chars[curr]) 81 | } 82 | } 83 | buffer.push(chars[curr]) 84 | chars.formIndex(after: &curr) 85 | } 86 | 87 | guard unusedValues.isEmpty else { 88 | throw TemplateError.variablesNotFoundInTemplate(Array(unusedValues)) 89 | } 90 | 91 | return String(chars) 92 | } 93 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TemplaterTestSuite 3 | 4 | XCTMain([ 5 | testCase(TemplaterTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/Templater/TemplaterTests.swift: -------------------------------------------------------------------------------- 1 | 2 | @testable import Templater 3 | import XCTest 4 | 5 | class TemplaterTests: XCTestCase { 6 | 7 | func testEmpty() throws { 8 | let rendered = try Template("").fill(with: [:]) 9 | XCTAssertEqual(rendered, "") 10 | } 11 | 12 | func testMissingVariable() throws { 13 | XCTAssertThrowsError(try Template("Hello, {{ name }}!").fill(with: ["name": "world", "weather": "nice"])) 14 | } 15 | 16 | func testMissingValue() throws { 17 | XCTAssertThrowsError(try Template("Hello, {{ name }}! Weather is {{ weather }} today.").fill(with: ["name": "world"])) 18 | } 19 | 20 | func testSimple() throws { 21 | let rendered = try Template("Hello, {{ name }}!").fill(with: ["name": "world"]) 22 | XCTAssertEqual(rendered, "Hello, world!") 23 | } 24 | 25 | func testMany() throws { 26 | let context = [ 27 | "name": "world", 28 | "weather": "pretty nice", 29 | "team": "Warriors" 30 | ] 31 | let rendered = try Template("Hello, {{ name }}! Weather is {{ weather }} today! How did the {{ team }} play today?").fill(with: context) 32 | let exp = "Hello, world! Weather is pretty nice today! How did the Warriors play today?" 33 | XCTAssertEqual(rendered, exp) 34 | } 35 | 36 | func testOneValueMultiUse() throws { 37 | let rendered = try Template("Hello, {{ name }}! Your name is {{ name }}, right?").fill(with: ["name": "world"]) 38 | XCTAssertEqual(rendered, "Hello, world! Your name is world, right?") 39 | } 40 | } 41 | 42 | extension TemplaterTests { 43 | static var allTests : [(String, (TemplaterTests) -> () throws -> Void)] { 44 | return [ 45 | ("testEmpty", testEmpty), 46 | ("testMissingVariable", testMissingVariable), 47 | ("testMissingValue", testMissingValue), 48 | ("testSimple", testSimple), 49 | ("testMany", testMany), 50 | ("testOneValueMultiUse", testOneValueMultiUse), 51 | ] 52 | } 53 | } 54 | 55 | --------------------------------------------------------------------------------