├── .swift-version ├── Playgrounds ├── Tracery.playground │ ├── Helpers.remap │ ├── Contents.o │ ├── Helpers.o │ ├── contents.xcplayground │ ├── Pages │ │ ├── Conclusion.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Modifiers.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Methods.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Calls.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Advanced Conditionals.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Introduction.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Advanced Recursion.xcplaygroundpage │ │ │ └── Contents.swift │ │ └── Advanced.xcplaygroundpage │ │ │ └── Contents.swift │ └── Sources │ │ └── Helpers.swift ├── Face Generator.playground │ ├── contents.xcplayground │ ├── Sources │ │ └── Helpers.swift │ └── Contents.swift └── Loops.playground │ ├── contents.xcplayground │ └── Contents.swift ├── codecov.yml ├── logo.png ├── Tracery.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── Common ├── RuleCandidatesProvider.swift ├── EmptyRulesetDetector.swift ├── Info.plist ├── RuleSelfReferenceIdentifer.swift ├── TagOverrideRuleIdentifier.swift ├── RulesetAnalyser.swift ├── Tracery.Logging.swift ├── TagStorage.swift ├── RuleCandidateSelector.swift ├── Parser.swift ├── Tracery.Text.swift ├── Tracery.swift ├── Lexer.swift └── CyclicReferenceIdentifier.swift ├── .travis.yml ├── CommonTesting ├── fable.txt ├── RecursiveRules.swift ├── UniformDistribution.swift ├── ExtensionCalls.swift ├── CustomProviders.swift ├── Objects.swift ├── TextFormat.swift ├── InlineRules.swift ├── CustomSelectors.swift ├── ErrorMessages.swift ├── Helpers.swift ├── Keywords.swift ├── CandidateProvider.swift ├── Performance.swift ├── Rules.swift ├── ExtensionModifier.swift ├── CandidateSelector.swift ├── TraceryioSamples.swift ├── ExtensionMethod.swift ├── Conditionals.swift ├── WeightedCandidates.swift └── Tags.swift ├── Tracery ├── Tracery │ └── Tracery.h ├── TraceryMac │ └── Tracery.h ├── Tracery.xcodeproj │ └── xcshareddata │ │ ├── xcbaselines │ │ └── B9127FDF1E77AEA40063D5A8.xcbaseline │ │ │ ├── D3844C72-F155-4280-8228-AFE7B6A04EA5.plist │ │ │ ├── B7543A48-BC01-48F5-A656-5443DAFD5174.plist │ │ │ └── Info.plist │ │ └── xcschemes │ │ ├── Tracery Tests macOS.xcscheme │ │ ├── Tracery Tests iOS.xcscheme │ │ ├── Tracery iOS.xcscheme │ │ └── Tracery macOS.xcscheme ├── Tracery Tests │ ├── Info.plist │ ├── ExtensionCalls.swift │ ├── Helpers.swift │ ├── CustomSelectors.swift │ ├── CustomProviders.swift │ ├── ErrorMessages.swift │ ├── CandidateProvider.swift │ ├── ExtensionModifier.swift │ ├── Rules.swift │ ├── RecursiveRules.swift │ ├── CandidateSelector.swift │ ├── Performance.swift │ ├── TraceryioSamples.swift │ ├── ExtensionMethod.swift │ └── Tags.swift └── Tracery Tests macOS │ └── Info.plist ├── Tracery.podspec ├── LICENSE ├── Package.swift └── .gitignore /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Helpers.remap: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: 5185aad9-8124-4544-bd75-e64d316ffef0 -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenziAhamed/Tracery/HEAD/logo.png -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Contents.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenziAhamed/Tracery/HEAD/Playgrounds/Tracery.playground/Contents.o -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Helpers.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenziAhamed/Tracery/HEAD/Playgrounds/Tracery.playground/Helpers.o -------------------------------------------------------------------------------- /Playgrounds/Face Generator.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Playgrounds/Loops.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Tracery.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Common/RuleCandidatesProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuleCandidatesProvider.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public protocol RuleCandidatesProvider { 13 | var candidates: [String] { get } 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode10.1 3 | 4 | script: 5 | - set -o pipefail 6 | - xcodebuild -workspace Tracery.xcworkspace -scheme "Tracery iOS" build 7 | - # xcodebuild -project "Tracery/Tracery.xcodeproj" -scheme "Tracery iOS" -destination "platform=iOS Simulator,name=iPhone X" test 8 | 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) 11 | -------------------------------------------------------------------------------- /CommonTesting/fable.txt: -------------------------------------------------------------------------------- 1 | [fable] 2 | #the# #adjective# #noun# 3 | #the# #noun# 4 | #the# #noun# Who #verb# The #adjective# #noun# 5 | 6 | [the] 7 | The 8 | The Strange Story of The 9 | The Tale of The 10 | A 11 | The Origin of The 12 | 13 | [adjective] 14 | Lonely 15 | Missing 16 | Greedy 17 | Valiant 18 | Blind 19 | 20 | [noun] 21 | Hare 22 | Hound 23 | Beggar 24 | Lion 25 | Frog 26 | 27 | [verb] 28 | Finds 29 | Meets 30 | Tricks 31 | Outwits 32 | -------------------------------------------------------------------------------- /Common/EmptyRulesetDetector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyRulesetDetector.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class EmptyRulesetDetector : RulesetAnalyser { 12 | var count = 0 13 | func visit(rule: String, mapping: RuleMapping) { 14 | count += 1 15 | } 16 | func end() { 17 | guard count == 0 else { return } 18 | warn("no expandable rules were found") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Tracery/Tracery/Tracery.h: -------------------------------------------------------------------------------- 1 | // 2 | // Tracery.h 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Tracery. 12 | FOUNDATION_EXPORT double TraceryVersionNumber; 13 | 14 | //! Project version string for Tracery. 15 | FOUNDATION_EXPORT const unsigned char TraceryVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tracery/TraceryMac/Tracery.h: -------------------------------------------------------------------------------- 1 | // 2 | // Tracery.h 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Tracery. 12 | FOUNDATION_EXPORT double TraceryVersionNumber; 13 | 14 | //! Project version string for Tracery. 15 | FOUNDATION_EXPORT const unsigned char TraceryVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tracery.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Playgrounds/Face Generator.playground/Sources/Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public extension UIColor { 5 | 6 | convenience init(hex:String) { 7 | var rgb:CUnsignedInt = 0 8 | let scanner = Scanner(string: hex) 9 | if hex.hasPrefix("#") { 10 | scanner.scanLocation = 1 11 | } 12 | scanner.scanHexInt32(&rgb) 13 | let r:CGFloat = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 14 | let g:CGFloat = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 15 | let b:CGFloat = CGFloat((rgb & 0x0000FF)) / 255.0 16 | 17 | self.init(red: r, green: g, blue: b, alpha: 1.0) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Tracery/Tracery.xcodeproj/xcshareddata/xcbaselines/B9127FDF1E77AEA40063D5A8.xcbaseline/D3844C72-F155-4280-8228-AFE7B6A04EA5.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | Performance 8 | 9 | testLexer() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.001 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /CommonTesting/RecursiveRules.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecursiveRules.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class RecursiveRules: XCTestCase { 13 | 14 | var limit = 0 15 | 16 | override func setUp() { 17 | limit = Tracery.maxStackDepth 18 | Tracery.maxStackDepth = 20 19 | } 20 | 21 | override func tearDown() { 22 | Tracery.maxStackDepth = limit 23 | } 24 | 25 | func testStackOverflow() { 26 | let t = Tracery {[ 27 | "a": "#b#", 28 | "b": "#a#", 29 | ]} 30 | XCTAssertTrue(t.expand("#a#").contains("stack overflow")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tracery/Tracery.xcodeproj/xcshareddata/xcbaselines/B9127FDF1E77AEA40063D5A8.xcbaseline/B7543A48-BC01-48F5-A656-5443DAFD5174.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | Performance 8 | 9 | testPerformanceOfStoryGrammarFromTraceryIO() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.0141 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Common/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /CommonTesting/UniformDistribution.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniformDistribution.swift 3 | // Tracery 4 | // 5 | // Created by Benzi Ahamed on 24/04/20. 6 | // Copyright © 2020 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class UniformDistribution: XCTestCase { 13 | 14 | func testDefaultSelectorHasUniformDistribution() { 15 | let t = Tracery {[ "o" : "{(h,t)}" ]} 16 | var headCount = 0 17 | var tailCount = 0 18 | let target = [1, 1, 2, 3, 5, 8, 13, 21, 34].randomElement()! * 2 19 | for _ in 0.. "MIT", :file => "LICENSE" } 15 | 16 | s.author = "Benzi Ahamed" 17 | 18 | s.platforms = { :ios => "8.0", :osx => "10.10" } 19 | 20 | s.source = { :git => "https://github.com/BenziAhamed/Tracery.git", :tag => "#{s.version}" } 21 | s.source_files = "Common" 22 | 23 | end 24 | -------------------------------------------------------------------------------- /Common/RuleSelfReferenceIdentifer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuleSelfReferenceIdentifer.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RuleSelfReferenceIdentifer : RulesetAnalyser { 12 | 13 | var selfReferences = [(rule: String, definition: String)]() 14 | 15 | func visit(rule: String, mapping: RuleMapping) { 16 | for candidate in mapping.candidates where candidate.text.contains("#\(rule)#") { 17 | selfReferences.append((rule, candidate.text)) 18 | } 19 | } 20 | 21 | func end() { 22 | let count = selfReferences.count 23 | guard count > 0 else { return } 24 | let text = count == 1 ? "rule" : "rules" 25 | warn("\(selfReferences.count) self referencing \(text) found") 26 | selfReferences.forEach { 27 | warn(" '\($0.rule)' - \($0.definition)") 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Benzi 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 | -------------------------------------------------------------------------------- /Common/TagOverrideRuleIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagOverrideRuleIdentifier.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | // NOTE: this only works in cases of unilevel 13 | // tag storage 14 | class UnilevelStorageTagOverrideRuleIndentifer : RulesetAnalyser { 15 | var allRules = [String]() 16 | var mappings = [(rule:String, mapping:RuleMapping)]() 17 | 18 | func visit(rule: String, mapping: RuleMapping) { 19 | allRules.append(rule) 20 | mappings.append((rule, mapping)) 21 | } 22 | func end() { 23 | mappings.forEach { entry in 24 | entry.mapping.candidates.forEach { candidate in 25 | candidate.value.nodes.forEach { node in 26 | if case let .tag(name, _) = node, allRules.contains(name) { 27 | print("⚠️ tag override in rule '\(entry.rule)', creating tag '\(name)' overrides pre-defined rule '\(name)'") 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /CommonTesting/CustomProviders.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomProviders.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Tracery 11 | 12 | 13 | class WeightedCandidateSet : RuleCandidatesProvider, RuleCandidateSelector { 14 | 15 | let candidates: [String] 16 | let weights: [Int] 17 | let sum: UInt32 18 | 19 | init(_ distribution:[String:Int]) { 20 | distribution.map { $0.value }.forEach { 21 | assert($0 > 0, "weights must be positive") 22 | } 23 | candidates = distribution.map { $0.key } 24 | weights = distribution.map { $0.value } 25 | sum = UInt32(weights.reduce(0, +)) 26 | } 27 | 28 | func pick(count: Int) -> Int { 29 | var choice = Int.random(in: 0.. Int { 30 | let count = t.expand("#count#", maintainContext: false) 31 | return count == "0" ? 1 : 0 32 | } 33 | } 34 | return LineSelector() 35 | }()) 36 | 37 | print(t.expand("#story#")) 38 | -------------------------------------------------------------------------------- /Common/RulesetAnalyser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RulesetAnalyser.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RulesetAnalyser { 12 | func visit(rule: String, mapping: RuleMapping) 13 | func end() 14 | } 15 | 16 | extension Tracery { 17 | 18 | func analyzeRuleBook() { 19 | 20 | guard options.isRuleAnalysisEnabled else { return } 21 | 22 | info("analying rules") 23 | 24 | var analyzers = [RulesetAnalyser]() 25 | 26 | analyzers.append(CyclicReferenceIdentifier()) 27 | analyzers.append(RuleSelfReferenceIdentifer()) 28 | if options.tagStorageType == .unilevel { 29 | analyzers.append(UnilevelStorageTagOverrideRuleIndentifer()) 30 | } 31 | analyzers.append(EmptyRulesetDetector()) 32 | 33 | ruleSet.forEach { rule, mapping in 34 | analyzers.forEach { analyzer in 35 | analyzer.visit(rule: rule, mapping: mapping) 36 | } 37 | } 38 | analyzers.forEach { $0.end() } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Tracery", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Tracery", 12 | targets: ["Tracery"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "Tracery", 23 | dependencies: [], 24 | path: "Common"), 25 | .testTarget( 26 | name: "TraceryTests", 27 | dependencies: ["Tracery"], 28 | path: "CommonTesting"), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /CommonTesting/Objects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Objects.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 26/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class Objects: XCTestCase { 13 | 14 | func testAllowAddingObjects() { 15 | let t = Tracery() 16 | t.add(object: "jack", named: "person") 17 | 18 | XCTAssertEqual(t.expand("#person#"), "jack") 19 | } 20 | 21 | func testObjectsCanRunModifiers() { 22 | let t = Tracery() 23 | t.add(object: "jack", named: "person") 24 | t.add(modifier: "caps") { $0.uppercased() } 25 | 26 | XCTAssertEqual(t.expand("#person.caps#"), "JACK") 27 | } 28 | 29 | func testNotFoundObjectsAreNotExpanded() { 30 | let t = Tracery() 31 | t.add(object: "jack", named: "person") 32 | XCTAssertEqual(t.expand("#person1#"), "{person1}") 33 | } 34 | 35 | func testObjectsCanBeAccessedFromDynamicRules() { 36 | let t = Tracery() 37 | t.add(object: "jack", named: "person") 38 | XCTAssertEqual(t.expand("#msg(#person# is here\\.)##msg#"), "jack is here.") 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /CommonTesting/TextFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFormat.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 21/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class TextFormat: XCTestCase { 13 | 14 | 15 | func testPlaintextFormat() { 16 | 17 | let lines = [ 18 | "[origin]", 19 | "hello world", 20 | ] 21 | 22 | let t = Tracery(lines: lines) 23 | 24 | XCTAssertEqual(t.expand("#origin#"), "hello world") 25 | 26 | } 27 | 28 | 29 | func testPlaintextFormatAllowsEmptyRuleCreation() { 30 | 31 | let lines = [ 32 | "[binary]", 33 | "0#binary#", 34 | "1#binary#", 35 | "#empty#", 36 | "", 37 | "[empty]", 38 | ] 39 | 40 | let t = Tracery(lines: lines) 41 | 42 | XCTAssertFalse(t.expand("#binary#").contains("stack overflow")) 43 | 44 | } 45 | 46 | func testPlaintextFile() { 47 | 48 | let fableFile = Bundle.main.executablePath! + "/CommonTesting/fable.txt" 49 | let t = Tracery.init(path: fableFile) 50 | 51 | for _ in 0..<10 { 52 | XCTAssertFalse(t.expand("#fable#").isEmpty) 53 | } 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Pages/Conclusion.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | /*: 4 | 5 | # Tracery Grammar 6 | 7 | This section attempts to describe the grammar specification for Tracery. 8 | 9 | 10 | ``` 11 | rule_candidate -> ( plain_text | rule | tag )* 12 | 13 | 14 | tag -> [ tag_name : tag_value ] 15 | 16 | tag_name -> plain_text 17 | 18 | tag_value -> tag_value_candidate (,tag_value_candidate)* 19 | 20 | tag_value_candidate -> rule_candidate 21 | 22 | 23 | rule -> # (tag)* | rule_name(.modifier|.call|.method)* | control_block* # 24 | 25 | rule_name -> plain_text 26 | 27 | modifier -> plain_text 28 | 29 | call -> plain_text 30 | 31 | method -> method_name ( param (,param)* ) 32 | 33 | method_name -> plain_text 34 | 35 | param -> plain_text | rule 36 | 37 | 38 | 39 | control_block -> if_block | while_block 40 | 41 | condition_operator -> == | != | in | not in 42 | 43 | condition -> rule condition_operator rule 44 | 45 | if_block -> [if condition then rule (else rule)] 46 | 47 | while_block -> [while condition do rule] 48 | 49 | 50 | 51 | ``` 52 | 53 | # Conclusion 54 | 55 | Tracery in Swift was developed by [Benzi](https://twitter.com/benziahamed). 56 | 57 | Original library in Javascript is available at [Tracery.io](http://www.tracery.io/). 58 | 59 | 60 | */ 61 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | func XCTAssertItemInArray(item: T, array: [T]) { 13 | XCTAssert(array.contains(item), "\(item) was not found in \(array)") 14 | } 15 | 16 | extension Array { 17 | 18 | func regexGenerateMatchesAnyItemPattern() -> String { 19 | return "(" + self.map { "\($0)" }.joined(separator: "|") + ")" 20 | } 21 | 22 | } 23 | 24 | extension Sequence { 25 | 26 | func mapDict(_ transform:(Iterator.Element)->(Key, Value)) -> Dictionary { 27 | var d = Dictionary() 28 | forEach { 29 | let (k,v) = transform($0) 30 | d[k] = v 31 | } 32 | return d 33 | } 34 | 35 | @discardableResult 36 | func scan(_ initial: T, _ combine: (T, Iterator.Element) throws -> T) rethrows -> [T] { 37 | var accu = initial 38 | return try map { e in 39 | accu = try combine(accu, e) 40 | return accu 41 | } 42 | } 43 | } 44 | 45 | 46 | 47 | //func dump(_ item: T) { 48 | // if let debuggable = item as? CustomDebugStringConvertible { 49 | // print(debuggable.debugDescription) 50 | // } 51 | // print(item) 52 | //} 53 | -------------------------------------------------------------------------------- /CommonTesting/InlineRules.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineRules.swift 3 | // Tracery Tests iOS 4 | // 5 | // Created by Benzi Ahamed on 23/04/20. 6 | // Copyright © 2020 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class InlineRules: XCTestCase { 13 | 14 | func testAnonymousInlineRulesAreWorkWithHash() { 15 | XCTAssertEqual(Tracery().expand("#(1)#"), "1") 16 | } 17 | 18 | func testAnonymousInlineRulesAreWorkWithCurlies() { 19 | XCTAssertEqual(Tracery().expand("{(1)}"), "1") 20 | } 21 | 22 | 23 | func testAnonymousInlineRulesAreWorkWithChoice() { 24 | XCTAssertItemInArray(item: Tracery().expand("{(1,2,3,4)}"), array: ["1","2","3","4"]) 25 | } 26 | 27 | func testNamedInlineRulesAreWork1() { 28 | XCTAssertEqual(Tracery().expand("{item(1)}{item}"), "1") 29 | } 30 | 31 | func testNamedInlineRulesAreWork2() { 32 | XCTAssertEqual(Tracery().expand("#item(1)#{item}"), "1") 33 | } 34 | 35 | func testNamedInlineRulesAreWork3() { 36 | XCTAssertEqual(Tracery().expand("{item(1)}#item#"), "1") 37 | } 38 | 39 | func testInlineRulesCanBeCleared() { 40 | XCTAssertEqual(Tracery().expand("{b(0)}{b}{b()}{b}"), "0") 41 | } 42 | 43 | func testInlineRulesAllowDynamicTags() { 44 | XCTAssertEqual(Tracery().expandVerbose("{([tag:0]{tag})}"), "0") 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Common/Tracery.Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tracery.Logging.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension Tracery { 13 | 14 | public enum LoggingLevel : Int { 15 | case none = 0 16 | case errors 17 | case warnings 18 | case info 19 | case verbose 20 | } 21 | 22 | public static var logLevel = LoggingLevel.errors 23 | 24 | static func log(level: LoggingLevel, message: @autoclosure () -> String) { 25 | guard logLevel.rawValue >= level.rawValue else { return } 26 | print(message()) 27 | } 28 | 29 | func trace(_ message: @autoclosure () -> String) { 30 | let indent = String(repeating: " ", count: ruleEvaluationLevel) 31 | Tracery.log(level: .verbose, message: "\(indent)\(message())") 32 | } 33 | 34 | } 35 | 36 | func info(_ message: @autoclosure () -> String) { 37 | Tracery.log(level: .info, message: "ℹ️ \(message())") 38 | } 39 | 40 | func warn(_ message: @autoclosure () -> String) { 41 | Tracery.log(level: .warnings, message: "⚠️ \(message())") 42 | } 43 | 44 | func error(_ message: @autoclosure () -> String) { 45 | Tracery.log(level: .errors, message: "⛔️ \(message())") 46 | } 47 | 48 | func trace(_ message: @autoclosure () -> String) { 49 | Tracery.log(level: .verbose, message: message()) 50 | } 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /CommonTesting/CustomSelectors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSelectors.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import Tracery 11 | 12 | class AlwaysPickFirst : RuleCandidateSelector { 13 | func pick(count: Int) -> Int { 14 | return 0 15 | } 16 | } 17 | 18 | class BogusSelector : RuleCandidateSelector { 19 | func pick(count: Int) -> Int { 20 | return -1 21 | } 22 | } 23 | 24 | class PickSpecificItem : RuleCandidateSelector { 25 | 26 | enum Offset { 27 | case fromStart(Int) 28 | case fromEnd(Int) 29 | } 30 | 31 | let offset: Offset 32 | 33 | init(offset: Offset) { 34 | self.offset = offset 35 | } 36 | 37 | func pick(count: Int) -> Int { 38 | switch offset { 39 | case let .fromStart(offset): 40 | return offset 41 | case let .fromEnd(offset): 42 | return count - 1 - offset 43 | } 44 | } 45 | } 46 | 47 | 48 | 49 | class SequentialSelector : RuleCandidateSelector { 50 | var i = 0 51 | func pick(count: Int) -> Int { 52 | defer { 53 | i += 1 54 | if i == count { 55 | i = 0 56 | } 57 | } 58 | return i 59 | } 60 | } 61 | 62 | class Arc4RandomSelector : RuleCandidateSelector { 63 | func pick(count: Int) -> Int { 64 | return Int(arc4random_uniform(UInt32(count))) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/CustomSelectors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSelectors.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import Tracery 11 | 12 | class AlwaysPickFirst : RuleCandidateSelector { 13 | func pick(count: Int) -> Int { 14 | return 0 15 | } 16 | } 17 | 18 | class BogusSelector : RuleCandidateSelector { 19 | func pick(count: Int) -> Int { 20 | return -1 21 | } 22 | } 23 | 24 | class PickSpecificItem : RuleCandidateSelector { 25 | 26 | enum Offset { 27 | case fromStart(Int) 28 | case fromEnd(Int) 29 | } 30 | 31 | let offset: Offset 32 | 33 | init(offset: Offset) { 34 | self.offset = offset 35 | } 36 | 37 | func pick(count: Int) -> Int { 38 | switch offset { 39 | case let .fromStart(offset): 40 | return offset 41 | case let .fromEnd(offset): 42 | return count - 1 - offset 43 | } 44 | } 45 | } 46 | 47 | 48 | 49 | class SequentialSelector : RuleCandidateSelector { 50 | var i = 0 51 | func pick(count: Int) -> Int { 52 | defer { 53 | i += 1 54 | if i == count { 55 | i = 0 56 | } 57 | } 58 | return i 59 | } 60 | } 61 | 62 | class Arc4RandomSelector : RuleCandidateSelector { 63 | func pick(count: Int) -> Int { 64 | return Int(arc4random_uniform(UInt32(count))) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/CustomProviders.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomProviders.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Tracery 11 | 12 | 13 | class WeightedCandidateSet : RuleCandidatesProvider, RuleCandidateSelector { 14 | 15 | let candidates: [String] 16 | let runningWeights: [(total:Int, target:Int)] 17 | let totalWeights: UInt32 18 | 19 | init(_ distribution:[String:Int]) { 20 | distribution.values.map { $0 }.forEach { 21 | assert($0 > 0, "weights must be positive") 22 | } 23 | let weightedCandidates = distribution 24 | .map { ($0, $1) } 25 | candidates = weightedCandidates 26 | .map { $0.0 } 27 | runningWeights = weightedCandidates 28 | .map { $0.1 } 29 | .scan(0, +) 30 | .enumerated() 31 | .map { ($0.element, $0.offset) } 32 | totalWeights = distribution 33 | .values 34 | .map { $0 } 35 | .reduce(0) { $0.0 + UInt32($0.1) } 36 | } 37 | 38 | func pick(count: Int) -> Int { 39 | let choice = Int.random(in: 0...runningWeights) 40 | for weight in runningWeights { 41 | if choice <= weight.total { 42 | return weight.target 43 | } 44 | } 45 | fatalError("unable to select target for choice:\(choice) weights:\(runningWeights) candidates:\(candidates)") 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Pages/Modifiers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | 4 | /*: 5 | # Modifiers 6 | 7 | When expanding a rule, sometimes we may need to capitalize its output, or transform it in some way. The Tracery engine allows for defining rule extensions. 8 | 9 | One kind of rule extension is known as a modifier. 10 | 11 | */ 12 | 13 | import Tracery 14 | 15 | var t = Tracery {[ 16 | "city": "new york" 17 | ]} 18 | 19 | // add a bunch of modifiers 20 | t.add(modifier: "caps") { return $0.uppercased() } 21 | t.add(modifier: "title") { return $0.capitalized } 22 | t.add(modifier: "reverse") { return String($0.characters.reversed()) } 23 | 24 | t.expand("#city.caps#") 25 | 26 | // output: NEW YORK 27 | 28 | t.expand("#city.title#") 29 | 30 | // output: New York 31 | 32 | t.expand("#city.reverse#") 33 | 34 | // output: kroy wen 35 | 36 | /*: 37 | The power of modifiers lies in the fact that they can be chained. 38 | */ 39 | 40 | t.expand("#city.reverse.caps#") 41 | 42 | // output: KROY WREN 43 | 44 | t.expand("There once was a man named #city.reverse.title#, who came from the city of #city.title#.") 45 | // output: There once was a man named Kroy Wen, who came from the city of New York. 46 | 47 | 48 | /*: 49 | 50 | > The original implementation at Tracery.io a couple of has modifiers that allows prefixing a/an to words, pluralization, caps etc. The library follows another approach and provides customization endopints so that one can add as many modifiers as required. 51 | 52 | The next rule expansion option is the ability to add custom rule methods. 53 | 54 | */ 55 | 56 | 57 | //: [Methods](@next) 58 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Sources/Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Tracery 3 | 4 | 5 | // scan is similar to reduce, but accumulates the intermediate results 6 | public extension Sequence { 7 | @discardableResult 8 | func scan(_ initial: T, _ combine: (T, Iterator.Element) throws -> T) rethrows -> [T] { 9 | var accu = initial 10 | return try map { e in 11 | accu = try combine(accu, e) 12 | return accu 13 | } 14 | } 15 | } 16 | 17 | // This class implements two protocols 18 | // RuleCandidateSelector - which as we have seen before is used to 19 | // to select content in a custom way 20 | // RuleCandidatesProvider - the protocol which needs to be 21 | // adhered to to provide customised content 22 | public class WeightedCandidateSet : RuleCandidatesProvider, RuleCandidateSelector { 23 | 24 | public let candidates: [String] 25 | let weights: [Int] 26 | 27 | public init(_ distribution:[String:Int]) { 28 | distribution.values.map { $0 }.forEach { 29 | assert($0 > 0, "weights must be positive") 30 | } 31 | candidates = distribution.map { $0.key } 32 | weights = distribution.map { $0.value } 33 | } 34 | 35 | public func pick(count: Int) -> Int { 36 | let sum = UInt32(weights.reduce(0, +)) 37 | var choice = Int(arc4random_uniform(sum)) 38 | var index = 0 39 | for weight in weights { 40 | choice = choice - weight 41 | if choice < 0 { 42 | return index 43 | } 44 | index += 1 45 | } 46 | fatalError() 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/ErrorMessages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorMessages.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class ErrorMessages: XCTestCase { 13 | 14 | func testErrorMessages() { 15 | 16 | let t = Tracery{[:]} 17 | 18 | XCTAssertEqual(t.expand("#rule"), "error: closing # not found for rule 'rule'") 19 | XCTAssertEqual(t.expand("#.#"), "error: expected modifier name after . for rule '', but got: '#' near '#.'") 20 | XCTAssertEqual(t.expand("#.(#"), "error: expected modifier name after . for rule '', but got: '(' near '#.'") 21 | XCTAssertEqual(t.expand("#.call(#"), "error: expected ) to close modifier call") 22 | XCTAssertEqual(t.expand("#.call(a,#"), "error: expected ) to close modifier call") 23 | XCTAssertEqual(t.expand("#.call(a,)#"), "error: parameter expected, but not found in modifier 'call'") 24 | XCTAssertEqual(t.expand("#[]#"), "error: empty [] not allowed") 25 | XCTAssertEqual(t.expand("#[tag]#"), "error: tag 'tag' must be followed by a :") 26 | XCTAssertEqual(t.expand("#[tag:]#"), "error: value expected for tag 'tag', but none found") 27 | XCTAssertEqual(t.expand("#[tag:#.(]#"), "error: unable to parse value '#.(' for tag 'tag' reason - expected modifier name after . for rule '', but got: '(' near '#.'") 28 | XCTAssertEqual(t.expand("[:number]"), "error: expected a tag name, but got: ':' near '['") 29 | XCTAssertEqual(t.expand("["), "error: expected a tag name") 30 | 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/CandidateProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CandidateProvider.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class CandidateProvider: XCTestCase { 13 | 14 | 15 | func testRuleCandidateProvidedCandidatesAreUsed() { 16 | class AnimalsProvider : RuleCandidatesProvider { 17 | let candidates = ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"] 18 | } 19 | let provider = AnimalsProvider() 20 | let t = Tracery { ["animal": provider] } 21 | for _ in 0.. Int { 32 | invokeCount += 1 33 | return Int.random(in: 0..<2) 34 | } 35 | } 36 | 37 | let nameProvider = Provider() 38 | let t = Tracery {[ 39 | "name": nameProvider 40 | ]} 41 | 42 | 43 | let callLimit = 10 44 | 45 | for _ in 0.. Notice how we create a tag called `name` which overrides the rule `name`. Tags always take precedence over rules. 60 | 61 | */ 62 | 63 | //: [Calls](@next) 64 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Pages/Calls.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | /*: 4 | # Calls 5 | 6 | There is one more type of rule extension, which is a `call`. Unlike modifiers and methods that work with arguments, parameters and are expected to return some string value, calls do not need to do these. 7 | 8 | Calls have the same syntax as that of a modifier `#rule.call_something#`, except that they do not modify any results. 9 | 10 | Just to show how calls work, we will create one to track a rule expansion. 11 | 12 | */ 13 | 14 | import Tracery 15 | 16 | var t = Tracery {[ 17 | "f": "f" 18 | "letter" : ["a", "b", "c", "d", "e", "#f.track#"] 19 | ]} 20 | 21 | t.add(call: "track") { 22 | print("rule 'f' was expanded") 23 | } 24 | 25 | t.expand("#letter#") 26 | 27 | /*: 28 | 29 | In the code snippet above, the rule letter has 5 candidates, 4 of which are basically string values, but the fifth one is a rule. Yes, rules can be mixed in freely and can appear anywhere at all. So in this case, rule `f` can be expanded to the basic string `f`. Notice we also have added the track call. 30 | 31 | Now, whenever `letter` choose the rule `f` as a candidate for expansion, `.track` will be called. 32 | 33 | 34 | > Rule extensions can be place on their own inside a pair `#`. For example, if we created a modifier that always adds 'yo' to its input, called it `yo`, and have a rule candidate like `#.yo#`, this evaluates to the string "yo"; the modifier is passed in the empty string as an input parameter since there were no rules to expand. 35 | 36 | At this point, we have pretty much covered the basics. The following sections cover more advanced topics that involved getting more control over the candidate selection process. 37 | 38 | */ 39 | 40 | //: [Advanced](@next) 41 | -------------------------------------------------------------------------------- /.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 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | Tracery.xcworkspace/xcuserdata/* 20 | **/xcuserdata/* 21 | **/xcuserdata 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xcuserstate 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | **/Playgrounds/*/timeline.xctimeline 35 | **/Playgrounds/*/playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | .swiftpm/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /CommonTesting/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Tracery 12 | 13 | func XCTAssertItemInArray(item: T, array: [T]) { 14 | XCTAssert(array.contains(where: { $0 == item }), "\(item) was not found in \(array)") 15 | } 16 | 17 | extension Array { 18 | 19 | func regexGenerateMatchesAnyItemPattern() -> String { 20 | return "(" + self.map { "\($0)" }.joined(separator: "|") + ")" 21 | } 22 | 23 | } 24 | 25 | extension Sequence { 26 | 27 | func mapDict(_ transform:(Iterator.Element)->(Key, Value)) -> Dictionary { 28 | var d = Dictionary() 29 | forEach { 30 | let (k,v) = transform($0) 31 | d[k] = v 32 | } 33 | return d 34 | } 35 | 36 | @discardableResult 37 | func scan(_ initial: T, _ combine: (T, Iterator.Element) throws -> T) rethrows -> [T] { 38 | var accu = initial 39 | return try map { e in 40 | accu = try combine(accu, e) 41 | return accu 42 | } 43 | } 44 | } 45 | 46 | 47 | 48 | extension Tracery { 49 | 50 | class func hierarchical(rules: ()->[String:Any]) -> Tracery { 51 | let options = TraceryOptions() 52 | options.tagStorageType = .heirarchical 53 | return Tracery.init(options, rules: rules) 54 | } 55 | 56 | class func hierarchical() -> Tracery { 57 | return hierarchical {[:]} 58 | } 59 | 60 | func expandVerbose(_ text: String) -> String { 61 | Tracery.logLevel = .verbose 62 | defer { 63 | Tracery.logLevel = .errors 64 | } 65 | return self.expand(text) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Tracery/Tracery.xcodeproj/xcshareddata/xcbaselines/B9127FDF1E77AEA40063D5A8.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | B7543A48-BC01-48F5-A656-5443DAFD5174 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2600 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro13,3 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | 31 | D3844C72-F155-4280-8228-AFE7B6A04EA5 32 | 33 | localComputer 34 | 35 | busSpeedInMHz 36 | 100 37 | cpuCount 38 | 1 39 | cpuKind 40 | Intel Core i5 41 | cpuSpeedInMHz 42 | 2300 43 | logicalCPUCoresPerPackage 44 | 4 45 | modelCode 46 | MacBookPro8,1 47 | physicalCPUCoresPerPackage 48 | 2 49 | platformIdentifier 50 | com.apple.platform.macosx 51 | 52 | targetArchitecture 53 | x86_64 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /CommonTesting/Keywords.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keywords.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 14/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class Keywords: XCTestCase { 13 | 14 | let keywords = ["if","then","else","do","while","not in","in"] 15 | 16 | func testKeywordCanBeAcceptedAsStandaloneText() { 17 | let t = Tracery() 18 | for keyword in keywords { 19 | XCTAssertEqual(t.expand(keyword), keyword) 20 | } 21 | } 22 | 23 | func testKeywordCanBeAcceptedAsRuleCandidate() { 24 | let t = Tracery {[ 25 | "word": keywords 26 | ]} 27 | for _ in 0.. 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Tracery/Tracery.xcodeproj/xcshareddata/xcschemes/Tracery Tests iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /CommonTesting/CandidateProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CandidateProvider.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class CandidateProvider: XCTestCase { 13 | 14 | 15 | func testRuleCandidateProvidedCandidatesAreUsed() { 16 | class AnimalsProvider : RuleCandidatesProvider { 17 | let candidates = ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"] 18 | } 19 | let provider = AnimalsProvider() 20 | let t = Tracery { ["animal": provider] } 21 | for _ in 0.. Int { 32 | invokeCount += 1 33 | return Int.random(in: 0..<2) 34 | } 35 | } 36 | 37 | let nameProvider = Provider() 38 | let t = Tracery {[ 39 | "name": nameProvider 40 | ]} 41 | 42 | 43 | let callLimit = 10 44 | 45 | for _ in 0.. String { 25 | // // create some candidates that recurse 26 | // var candidates = [String]() 27 | // for i in 0.. 0 1 2(end) 51 | // // any other sequence will break off 52 | // // rule -> 0 2(end) skipped 1 53 | // // rule -> 2(end) skipped 0, 1 54 | // 55 | // Tracery.logLevel = .verbose 56 | // 57 | // Tracery.maxStackDepth = 11 58 | // run(length: 10) 59 | // 60 | // // XCTAssertTrue(!run(length: Tracery.maxStackDepth).contains("stack overflow")) 61 | // // XCTAssertTrue(run(length: Tracery.maxStackDepth + 1).contains("stack overflow")) 62 | // 63 | // } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Tracery/Tracery.xcodeproj/xcshareddata/xcschemes/Tracery iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 44 | 45 | 51 | 52 | 58 | 59 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /CommonTesting/ExtensionModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionModifier.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class ExtensionModifier: XCTestCase { 13 | 14 | func testNoModifier() { 15 | let t = Tracery { 16 | [ "msg" : "hello world" ] 17 | } 18 | XCTAssertEqual(t.expand("#msg.no.mods.are.added#"), "hello world") 19 | } 20 | 21 | func testAddingModifier() { 22 | let t = Tracery { 23 | [ "msg" : "hello world" ] 24 | } 25 | t.add(modifier: "caps") { return $0.uppercased() } 26 | XCTAssertEqual(t.expand("#msg.caps#"), "hello world".uppercased()) 27 | } 28 | 29 | func testAddingModifierAndChainingInvalidModifiersRetainsIntermediateResults() { 30 | let t = Tracery { 31 | [ "msg" : "hello world" ] 32 | } 33 | t.add(modifier: "caps") { return $0.uppercased() } 34 | XCTAssertEqual(t.expand("#msg.caps#"), "hello world".uppercased()) 35 | XCTAssertEqual(t.expand("#msg.no.caps#"), "hello world".uppercased()) 36 | XCTAssertEqual(t.expand("#msg.still.caps.goes.here#"), "hello world".uppercased()) 37 | } 38 | 39 | 40 | func testModifiersGetCalledInCorrectOrder() { 41 | let t = Tracery { 42 | [ "msg" : "new york" ] 43 | } 44 | t.add(modifier: "caps") { return $0.uppercased() } 45 | t.add(modifier: "reversed") { return .init($0.reversed()) } 46 | t.add(modifier: "kebabed") { return $0.replacingOccurrences(of: " ", with: "-") } 47 | t.add(modifier: "prefix") { return "!" + $0 } 48 | 49 | XCTAssertEqual(t.expand("#msg.caps.reversed.kebabed.prefix#"), "!KROY-WEN") 50 | XCTAssertEqual(t.expand("#msg.caps.kebabed.reversed.prefix#"), "!KROY-WEN") 51 | XCTAssertEqual(t.expand("#msg.reversed.prefix.reversed#"), "new york!") 52 | XCTAssertEqual(t.expand("#msg.prefix.reversed.prefix#"), "!kroy wen!") 53 | XCTAssertEqual(t.expand("#msg.prefix.reversed.prefix.reversed#"), "!new york!") 54 | } 55 | 56 | func testInlineRulesAllowModifierCalls() { 57 | 58 | let t = Tracery { 59 | ["word" : "{(abc,bcd,cde).caps.reversed}"] 60 | } 61 | 62 | t.add(modifier: "caps") { $0.uppercased() } 63 | t.add(modifier: "reversed") { return String.init($0.reversed()) } 64 | 65 | let allowed:Set = ["CBA", "DCB", "EDC"] 66 | 67 | for _ in 0..<10 { 68 | XCTAssertTrue(allowed.contains(t.expand("{word}"))) 69 | } 70 | 71 | } 72 | 73 | } 74 | 75 | -------------------------------------------------------------------------------- /Common/TagStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagStorage.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 12/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct TagMapping { 12 | let candidates: [String] 13 | let selector: RuleCandidateSelector 14 | } 15 | 16 | extension TagMapping : CustomStringConvertible { 17 | var description: String { 18 | return candidates.joined(separator: ",") 19 | } 20 | } 21 | 22 | protocol TagStorage { 23 | var tracery:Tracery? { get set } 24 | mutating func store(name: String, tag: TagMapping) 25 | func get(name: String) -> TagMapping? 26 | mutating func removeAll() 27 | } 28 | 29 | public enum TaggingPolicy { 30 | case unilevel 31 | case heirarchical 32 | 33 | func storage() -> TagStorage { 34 | switch self { 35 | case .unilevel: return UnilevelTagStorage() 36 | case .heirarchical: return HierarchicalTagStorage() 37 | } 38 | } 39 | } 40 | 41 | 42 | // simple tag storage - entries are 43 | // stored in a plain list 44 | struct UnilevelTagStorage : TagStorage { 45 | private var storage:[String: TagMapping] 46 | weak var tracery:Tracery? = nil 47 | init() { 48 | self.storage = [String: TagMapping]() 49 | } 50 | mutating func store(name: String, tag: TagMapping) { 51 | storage[name] = tag 52 | } 53 | func get(name: String) -> TagMapping? { 54 | guard let mapping = storage[name] else { return nil } 55 | return mapping 56 | } 57 | mutating func removeAll() { 58 | storage.removeAll() 59 | } 60 | } 61 | 62 | // tags are scoped by stack depth level 63 | // new tags are stored at the current level 64 | // existing tags are retrieved from current level 65 | // if present, else levels are decremented 66 | // until a tag is found 67 | struct HierarchicalTagStorage : TagStorage { 68 | weak var tracery: Tracery? = nil 69 | var storage = [Int : UnilevelTagStorage]() 70 | mutating func store(name: String, tag: TagMapping) { 71 | guard let t = tracery else { return } 72 | if storage[t.ruleEvaluationLevel] == nil { 73 | var levelStorage = UnilevelTagStorage() 74 | levelStorage.tracery = tracery 75 | storage[t.ruleEvaluationLevel] = levelStorage 76 | } 77 | storage[t.ruleEvaluationLevel]!.store(name: name, tag: tag) 78 | } 79 | func get(name: String) -> TagMapping? { 80 | guard let t = tracery else { return nil } 81 | var level = t.ruleEvaluationLevel 82 | while level >= 0 { 83 | if let tag = storage[level]?.get(name: name) { 84 | return tag 85 | } 86 | level -= 1 87 | } 88 | return nil 89 | } 90 | mutating func removeAll() { 91 | storage.removeAll() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Common/RuleCandidateSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuleCandidateSelector.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol RuleCandidateSelector { 12 | func pick(count: Int) -> Int 13 | } 14 | 15 | 16 | class PickFirstContentSelector : RuleCandidateSelector { 17 | private init() { } 18 | static let shared = PickFirstContentSelector() 19 | func pick(count: Int) -> Int { 20 | return 0 21 | } 22 | } 23 | 24 | 25 | private extension MutableCollection { 26 | /// Shuffles the contents of this collection. 27 | mutating func shuffle() { 28 | let c = count 29 | guard c > 1 else { return } 30 | 31 | for (firstUnshuffled , unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) { 32 | let d = Int.random(in: 0.. Int { 54 | assert(indices.count == count) 55 | if index >= count { 56 | indices.shuffle() 57 | index = 0 58 | } 59 | defer { index += 1 } 60 | return indices[index] 61 | } 62 | 63 | } 64 | 65 | 66 | 67 | 68 | class WeightedSelector : RuleCandidateSelector { 69 | 70 | // static var nextId = 0 71 | // let id:Int = { 72 | // defer { WeightedSelector.nextId+=1 } 73 | // return WeightedSelector.nextId 74 | // }() 75 | 76 | let weights: [Int] 77 | let sum: UInt32 78 | 79 | init(_ distribution:[Int]) { 80 | weights = distribution 81 | sum = UInt32(weights.reduce(0, +)) 82 | } 83 | 84 | func pick(count: Int) -> Int { 85 | let choice = Int.random(in: 0.. Int { 92 | var choice = choice 93 | var index = 0 94 | for weight in weights { 95 | choice = choice - weight 96 | if choice < 0 { 97 | return index 98 | } 99 | index += 1 100 | } 101 | fatalError() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/CandidateSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CandidateSelector.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class CandidateSelector : XCTestCase { 13 | 14 | func testRuleCandidateSelectorIsInvoked() { 15 | let t = Tracery {[ 16 | "msg" : ["hello", "world"] 17 | ]} 18 | 19 | 20 | let selector = AlwaysPickFirst() 21 | 22 | t.setCandidateSelector(rule: "msg", selector: selector) 23 | 24 | XCTAssertEqual(t.expand("#msg#"), "hello") 25 | } 26 | 27 | 28 | func testRuleCandidateSelectorBogusSelectionsAreIgnored() { 29 | let t = Tracery {[ 30 | "msg" : ["hello", "world"] 31 | ]} 32 | 33 | 34 | let selector = BogusSelector() 35 | 36 | t.setCandidateSelector(rule: "msg", selector: selector) 37 | 38 | XCTAssertEqual(t.expand("#msg#"), "#msg#") 39 | } 40 | 41 | func testRuleCandidateSelectorReturnValueIsAlwaysHonoured() { 42 | 43 | let t = Tracery {[ 44 | "msg" : ["hello", "world"] 45 | ]} 46 | 47 | 48 | let selector = PickSpecificItem(offset: .fromEnd(0)) 49 | t.setCandidateSelector(rule: "msg", selector: selector) 50 | 51 | var tracker = [ 52 | "hello": 0, 53 | "world": 0, 54 | ] 55 | 56 | t.add(modifier: "track") { 57 | let count = tracker[$0] ?? 0 58 | tracker[$0] = count + 1 59 | return $0 60 | } 61 | 62 | let target = 10 63 | for _ in 0.. 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /CommonTesting/CandidateSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CandidateSelector.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class CandidateSelector : XCTestCase { 13 | 14 | func testRuleCandidateSelectorIsInvoked() { 15 | let t = Tracery {[ 16 | "msg" : ["hello", "world"] 17 | ]} 18 | 19 | 20 | let selector = AlwaysPickFirst() 21 | 22 | t.setCandidateSelector(rule: "msg", selector: selector) 23 | 24 | for _ in 0..<20 { 25 | XCTAssertEqual(t.expand("#msg#"), "hello") 26 | } 27 | } 28 | 29 | 30 | func testRuleCandidateSelectorBogusSelectionsAreIgnored() { 31 | let t = Tracery {[ 32 | "msg" : ["hello", "world"] 33 | ]} 34 | 35 | 36 | let selector = BogusSelector() 37 | 38 | t.setCandidateSelector(rule: "msg", selector: selector) 39 | 40 | XCTAssertEqual(t.expand("#msg#"), "{msg}") 41 | } 42 | 43 | func testRuleCandidateSelectorReturnValueIsAlwaysHonoured() { 44 | 45 | let t = Tracery {[ 46 | "msg" : ["hello", "world"] 47 | ]} 48 | 49 | 50 | let selector = PickSpecificItem(offset: .fromEnd(0)) 51 | t.setCandidateSelector(rule: "msg", selector: selector) 52 | 53 | var tracker = [ 54 | "hello": 0, 55 | "world": 0, 56 | ] 57 | 58 | t.add(modifier: "track") { 59 | let count = tracker[$0] ?? 0 60 | tracker[$0] = count + 1 61 | return $0 62 | } 63 | 64 | let target = 10 65 | for _ in 0.. Tracery { 39 | // // create some candidates that recurse 40 | // var candidates = [String]() 41 | // for i in 0..","«»","𛰫𛰬","⌜⌝","ᙅᙂ","ᙦᙣ","⁅⁆","⌈⌉","⌊⌋","⟦⟧","⦃⦄","⦗⦘","⫷⫸"] 59 | .map { braces -> String in 60 | let open = braces[braces.startIndex] 61 | let close = braces[braces.index(after: braces.startIndex)] 62 | return "[open:\(open)][close:\(close)]" 63 | } 64 | 65 | let t = Tracery(o) {[ 66 | "letter": ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P"], 67 | "bracetypes": braceTypes, 68 | "brace": [ 69 | "#open##symbol# #origin##symbol##close# ", 70 | "#open##symbol##close# #origin# #open##symbol##close#", 71 | "#open##symbol# #origin##symbol##close# #origin#", 72 | "", 73 | ], 74 | "origin": ["#[symbol:#letter#][#bracetypes#]brace#"] 75 | ]} 76 | 77 | // Tracery.logLevel = .verbose 78 | XCTAssertFalse(t.expand("#origin#").contains("stack overflow")) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/ExtensionMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionMethod.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class ExtensionMethod: XCTestCase { 13 | 14 | func testNoMethod() { 15 | let t = Tracery { 16 | [ "msg" : "hello world" ] 17 | } 18 | XCTAssertEqual(t.expand("#msg.call()#"), "hello world") 19 | XCTAssertEqual(t.expand("#msg.me()#"), "hello world") 20 | XCTAssertEqual(t.expand("#msg.maybe()#"), "hello world") 21 | XCTAssertEqual(t.expand("#msg.you().know()#"), "hello world") 22 | } 23 | 24 | func testMethodCanReceiveComplexParameters() { 25 | let t = Tracery {[ 26 | "msg" : "hello" 27 | ]} 28 | t.add(method: "combine") { input, args in 29 | return args.joined(separator: "-") 30 | } 31 | XCTAssertEqual(t.expand("#.combine(#msg#)#"), "hello") 32 | XCTAssertEqual(t.expand("#.combine(#msg# world)#"), "hello world") 33 | XCTAssertEqual(t.expand("#.combine(#msg# world,!)#"), "hello world-!") 34 | XCTAssertEqual(t.expand("#.combine(why,#msg# world)#"), "why-hello world") 35 | XCTAssertEqual(t.expand("#.combine(why,#msg# world,!)#"), "why-hello world-!") 36 | } 37 | 38 | func testMethod() { 39 | let t = Tracery { 40 | [ "msg" : "hello world" ] 41 | } 42 | t.add(method: "call") { input, args in 43 | XCTAssertEqual(input, "hello world") 44 | return input 45 | } 46 | XCTAssertEqual(t.expand("#msg.call()#"), "hello world") 47 | } 48 | 49 | func testMethodReceivesInputArguments() { 50 | let t = Tracery { 51 | [ "msg" : "hello world" ] 52 | } 53 | t.add(method: "call") { input, args in 54 | XCTAssertEqual(input, "hello world") 55 | XCTAssertEqual(args.count, 1) 56 | XCTAssertEqual(args[0], "me") 57 | return input 58 | } 59 | XCTAssertEqual(t.expand("#msg.call(me)#"), "hello world") 60 | } 61 | 62 | func testMethodReceivesInputArgumentsWithRulesExpanded() { 63 | let t = Tracery {[ 64 | "msg" : "hello world", 65 | "arg1": "this is cool", 66 | "arg2": "this is also cool", 67 | "arg3": "#arg4#", 68 | "arg4": "yes i am arg4", 69 | ]} 70 | t.add(method: "call") { input, args in 71 | XCTAssertEqual(input, "hello world") 72 | XCTAssertEqual(args.count, 4) 73 | XCTAssertEqual(args[0], "this is cool") 74 | XCTAssertEqual(args[1], "this is also cool") 75 | XCTAssertEqual(args[2], "yes i am arg4") 76 | XCTAssertEqual(args[3], "arg4") 77 | return input 78 | } 79 | XCTAssertEqual(t.expand("#msg.call(#arg1#,#arg2#,#arg3#,arg4)#"), "hello world") 80 | } 81 | 82 | func testMethodGetsCalledAlways() { 83 | let t = Tracery { 84 | [ "msg" : "hello world" ] 85 | } 86 | 87 | var callCount = 0 88 | t.add(method: "call") { input, args in 89 | XCTAssertEqual(input, "hello world") 90 | callCount += 1 91 | return input 92 | } 93 | 94 | let target = 10 95 | for _ in 0..","«»","𛰫𛰬","⌜⌝","ᙅᙂ","ᙦᙣ","⁅⁆","⌈⌉","⌊⌋","⟦⟧","⦃⦄","⦗⦘","⫷⫸"] 59 | var braceTypes = braces 60 | .map { braces -> String in 61 | let open = braces.first! 62 | let close = braces.last! 63 | return "[open:\(open)][close:\(close)]" 64 | } 65 | 66 | // round and curly braces needs to be escaped 67 | braceTypes.append("[open:\\(][close:\\)]") 68 | braceTypes.append("[open:\\{][close:\\}]") 69 | 70 | let t = Tracery(o) {[ 71 | "letter": ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P"], 72 | "bracetypes": braceTypes, 73 | "brace": [ 74 | "#open##symbol##origin##symbol##close# ", 75 | "#open##symbol##close##origin##open##symbol##close# ", 76 | "#open##symbol##origin##symbol##close##origin# ", 77 | " ", 78 | ], 79 | "origin": ["#[symbol:#letter#][#bracetypes#]brace#"] 80 | ]} 81 | 82 | // Tracery.logLevel = .verbose 83 | let output = t.expand("#origin#") 84 | print(output) 85 | XCTAssertFalse(output.contains("stack overflow")) 86 | 87 | // track open and close 88 | // of each brace 89 | var stackOfBraces = [Character]() 90 | func trackBraces(_ c: Character) { 91 | braces 92 | .filter { 93 | $0.range(of: "\(c)") != nil 94 | } 95 | .forEach { 96 | let leftBrace = $0[$0.startIndex] 97 | if leftBrace == c { 98 | stackOfBraces.append(c) 99 | } 100 | else { 101 | let expected = stackOfBraces.popLast() 102 | XCTAssertNotNil(expected) 103 | XCTAssertEqual(expected!, leftBrace) 104 | } 105 | } 106 | } 107 | output.forEach { trackBraces($0) } 108 | XCTAssertEqual(stackOfBraces.count, 0) 109 | 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CommonTesting/ExtensionMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionMethod.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class ExtensionMethod: XCTestCase { 13 | 14 | func testNoMethod() { 15 | let t = Tracery { 16 | [ "msg" : "hello world" ] 17 | } 18 | XCTAssertEqual(t.expand("#msg.call()#"), "hello world") 19 | XCTAssertEqual(t.expand("#msg.me()#"), "hello world") 20 | XCTAssertEqual(t.expand("#msg.maybe()#"), "hello world") 21 | XCTAssertEqual(t.expand("#msg.you().know()#"), "hello world") 22 | } 23 | 24 | func testMethodCanReceiveComplexParameters() { 25 | let t = Tracery {[ 26 | "msg" : "hello" 27 | ]} 28 | t.add(method: "combine") { input, args in 29 | return args.joined(separator: "-") 30 | } 31 | 32 | XCTAssertEqual(t.expand("#.combine(#msg#)#"), "hello") 33 | XCTAssertEqual(t.expand("#.combine(#msg# world)#"), "hello world") 34 | XCTAssertEqual(t.expand("#.combine(#msg# world,!)#"), "hello world-!") 35 | XCTAssertEqual(t.expand("#.combine(why,#msg# world)#"), "why-hello world") 36 | XCTAssertEqual(t.expand("#.combine(why,#msg# world,!)#"), "why-hello world-!") 37 | } 38 | 39 | func testMethod() { 40 | let t = Tracery { 41 | [ "msg" : "hello world" ] 42 | } 43 | t.add(method: "call") { input, args in 44 | XCTAssertEqual(input, "hello world") 45 | return input 46 | } 47 | XCTAssertEqual(t.expand("#msg.call()#"), "hello world") 48 | } 49 | 50 | func testMethodReceivesInputArguments() { 51 | let t = Tracery { 52 | [ "msg" : "hello world" ] 53 | } 54 | t.add(method: "call") { input, args in 55 | XCTAssertEqual(input, "hello world") 56 | XCTAssertEqual(args.count, 1) 57 | XCTAssertEqual(args[0], "me") 58 | return input 59 | } 60 | XCTAssertEqual(t.expand("#msg.call(me)#"), "hello world") 61 | } 62 | 63 | func testMethodReceivesInputArgumentsWithRulesExpanded() { 64 | let t = Tracery {[ 65 | "msg" : "hello world", 66 | "arg1": "this is cool", 67 | "arg2": "this is also cool", 68 | "arg3": "#arg4#", 69 | "arg4": "yes i am arg4", 70 | ]} 71 | t.add(method: "call") { input, args in 72 | XCTAssertEqual(input, "hello world") 73 | XCTAssertEqual(args.count, 4) 74 | XCTAssertEqual(args[0], "this is cool") 75 | XCTAssertEqual(args[1], "this is also cool") 76 | XCTAssertEqual(args[2], "yes i am arg4") 77 | XCTAssertEqual(args[3], "arg4") 78 | return input 79 | } 80 | XCTAssertEqual(t.expand("#msg.call(#arg1#,#arg2#,#arg3#,arg4)#"), "hello world") 81 | } 82 | 83 | func testMethodGetsCalledAlways() { 84 | let t = Tracery { 85 | [ "msg" : "hello world" ] 86 | } 87 | 88 | var callCount = 0 89 | t.add(method: "call") { input, args in 90 | XCTAssertEqual(input, "hello world") 91 | callCount += 1 92 | return input 93 | } 94 | 95 | let target = 10 96 | for _ in 0.. String in 130 | guard let arg = args.first, let count = Int.init(arg) else { 131 | return input 132 | } 133 | return String.init(repeating: input, count: count) 134 | } 135 | 136 | XCTAssertEqual(t.expand("#msg#"), "55555") 137 | } 138 | 139 | } 140 | 141 | -------------------------------------------------------------------------------- /CommonTesting/Conditionals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conditionals.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 14/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Tracery 12 | 13 | class Conditionals: XCTestCase { 14 | 15 | func testBasicIfBlockWorks() { 16 | 17 | let t = Tracery {[ 18 | "name": ["benzi"] 19 | ]} 20 | 21 | XCTAssertEqual(t.expand("[if #name#==benzi then ok]"), "") 22 | 23 | XCTAssertEqual(t.expand("[if #name# == benzi then ok]"), "ok") 24 | XCTAssertEqual(t.expand("[if #name#== benzi then ok]"), "ok") 25 | XCTAssertEqual(t.expand("[if #name#==benzi then ok]"), "ok") 26 | XCTAssertNotEqual(t.expand("[if #name#==benzithen ok]"), "ok") 27 | 28 | XCTAssertEqual(t.expand("[if #name# == benzi then ok else not-ok]"), "ok") 29 | XCTAssertEqual(t.expand("[if #name# != danny then ok else not-ok]"), "ok") 30 | 31 | } 32 | 33 | func testIfBlockWorksWithTags() { 34 | 35 | let t = Tracery {[ 36 | "name": ["benzi"] 37 | ]} 38 | 39 | XCTAssertEqual(t.expand("[tag:#name#][if #tag# == benzi then ok]"), "ok") 40 | XCTAssertEqual(t.expand("[tag:#name#][if #tag# != benzi then not-ok else ok]"), "ok") 41 | 42 | } 43 | 44 | 45 | func testIfBlockWithListConditionCheck() { 46 | let t = Tracery {[ 47 | "num": [0,1], 48 | "msg": "[if #value# in #num# then binary else not a binary]", 49 | "msg_then_2word": "[if #value# in #num# then binary digit else no]" 50 | ]} 51 | 52 | XCTAssertEqual(t.expand("[value:0]#msg#"), "binary") 53 | XCTAssertEqual(t.expand("[value:1]#msg#"), "binary") 54 | 55 | XCTAssertEqual(t.expand("[value:in]#msg#"), "not a binary") 56 | XCTAssertEqual(t.expand("[value:while]#msg#"), "not a binary") 57 | XCTAssertEqual(t.expand("[value:for]#msg#"), "not a binary") 58 | XCTAssertEqual(t.expand("[value:10001]#msg#"), "not a binary") 59 | 60 | XCTAssertEqual(t.expand("[value:0]#msg_then_2word#"), "binary digit") 61 | XCTAssertEqual(t.expand("[value:1]#msg_then_2word#"), "binary digit") 62 | 63 | 64 | XCTAssertEqual(t.expand("[tag:2,3][if #num# not in #tag# then ok]"), "ok") 65 | } 66 | 67 | 68 | func testIfBlockWorksWithHierarchicalTagStorageModel() { 69 | 70 | let t = Tracery.hierarchical {[ 71 | 72 | // create tag and check value 73 | "level1if_A" : "[tag:name][if #tag# == name then valid else invalid]", 74 | "call_A" : "#level1if_A#", 75 | 76 | // create tag, check value, use value 77 | "level1if_B" : "[tag:name][if #tag# == name then valid #tag# else invalid #tag#]", 78 | "call_B" : "#level1if_B#", 79 | 80 | // create tag at level n+1, not visible at n 81 | "create": "[tag:level2]", 82 | "level2_create" : "#create#", // if we set tag here, level is not incremented 83 | "level2_if": "[#level2_create#][if #tag# != level2 then valid]", 84 | "call_L2": "#level2_if#", 85 | 86 | "level2B_create" : "[tag:level2B]", // if we set tag here, it should be visible in if 87 | "level2B_if": "[#level2B_create#][if #tag# == level2B then valid]", 88 | "call_L2B": "#level2B_if#", 89 | ]} 90 | 91 | 92 | XCTAssertEqual(t.expand("#call_A#"), "valid") 93 | XCTAssertEqual(t.expand("#call_B#"), "valid name") 94 | XCTAssertEqual(t.expand("#call_L2#"), "valid") 95 | XCTAssertEqual(t.expand("#call_L2B#"), "valid") 96 | } 97 | 98 | 99 | func testIfBlockAllowsComplexConditionals() { 100 | let t = Tracery {[ 101 | "tag2": ["name1","name2","name3","#name4#"], 102 | "name4": "name" 103 | ]} 104 | XCTAssertEqual(t.expand("[if #[tag1:name]tag1# in #tag2# then ok else nope]"), "ok") 105 | } 106 | 107 | 108 | func testBasicWhileBlockWorks() { 109 | 110 | let t = Tracery {[ 111 | "binary": WeightedCandidateSet([ 112 | "0": 10, 113 | "1": 10, 114 | "": 1, 115 | ]), 116 | ]} 117 | 118 | var generated = -1 // start from -1 because the last generated binary will be empty 119 | t.add(call: "track") { 120 | generated += 1 121 | } 122 | 123 | let output = t.expand("[while #[digit:#binary.track#]digit# in #[options:0,1]options# do b]") 124 | 125 | XCTAssertFalse(output.contains("stack overflow")) 126 | XCTAssertEqual(output, String(repeating: "b", count: generated)) 127 | 128 | } 129 | 130 | func testWhileLoopGenerateNumberNotContainingZeroOrOne() { 131 | let t = Tracery {[ 132 | "digit" : [0,1,2,3,4,5,6,7,8,9], 133 | "binary": [0,1], 134 | ]} 135 | let output = t.expand("[while #[d:#digit#]d# not in #binary# do #d#]") 136 | XCTAssertNil(output.range(of: "0")) 137 | XCTAssertNil(output.range(of: "1")) 138 | } 139 | 140 | 141 | } 142 | -------------------------------------------------------------------------------- /CommonTesting/WeightedCandidates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeightedCandidates.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 24/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Tracery 11 | 12 | class WeightedCandidates: XCTestCase { 13 | 14 | func testWeightedCandidatesAllowZero1() { 15 | 16 | // create a binary number 17 | // generator, that only outputs 18 | // 1's 19 | 20 | let t = Tracery.init(lines: [ 21 | "[binary]", 22 | "1:2", 23 | "0:0", 24 | "##", 25 | "", 26 | "[origin]", 27 | "[while [b:#binary#]#b# != ## do #b#]", 28 | ]) 29 | 30 | for _ in 0..<100 { 31 | let output = t.expand("#origin#") 32 | XCTAssertEqual(output, String(repeating: "1", count: output.count)) 33 | } 34 | 35 | } 36 | 37 | 38 | func testWeightedCandidatesIgnoreLeadingSpaces() { 39 | 40 | // create a binary number 41 | // generator, that only outputs 42 | // 1's 43 | 44 | let t = Tracery.init(lines: [ 45 | "[binary]", 46 | "1#binary#:2", 47 | "0#binary#:0", 48 | "##", 49 | ]) 50 | 51 | for _ in 0..<100 { 52 | let output = t.expand("#binary#") 53 | XCTAssertEqual(output, String(repeating: "1", count: output.count)) 54 | } 55 | 56 | } 57 | 58 | 59 | func testWeightedBinaryNumberGenerator() { 60 | 61 | let t = Tracery.init(lines: [ 62 | "[binary]", 63 | "A", 64 | "B", 65 | "", 66 | "[number]", 67 | "#binary##number#:10", 68 | "#binary#:1", 69 | ]) 70 | 71 | 72 | let count = 20 73 | var total = 0 74 | print("iterations", count) 75 | for i in 1...count { 76 | let output = t.expand("#number#") 77 | print(i, "-", output, "(\(output.count))") 78 | total += output.count 79 | XCTAssertFalse(output.contains("stack overflow")) 80 | } 81 | 82 | let average = Double(total)/Double(count) 83 | print("AVG", average, "should be close to 11") 84 | // XCTAssertTrue(abs(average - 11.0) < 0.5, "\(average) is not close to 11.0") 85 | } 86 | 87 | 88 | func testNewRulesCanBeWeighted() { 89 | 90 | let t = Tracery {[ 91 | "b" : ["a#b#:10","b#b#:10","a","b"] 92 | ]} 93 | let count = 20 94 | var total = 0 95 | let target = 10 96 | print("iterations", count) 97 | for i in 1...count { 98 | let output = t.expand("#b(0,1)##n(#b##n#:\(target-1),#b#)##n#") 99 | print(i, "-", output, "(\(output.count))") 100 | total += output.count 101 | XCTAssertTrue(!output.contains("stack overflow")) 102 | if output.contains("stack overflow") { 103 | return 104 | } 105 | } 106 | let average = Double(total)/Double(count) 107 | print("AVG", average, "should be close to \(target)") 108 | } 109 | 110 | 111 | func testInlineRulesCanBeWeighted() { 112 | 113 | // binary number generator 114 | 115 | let t = Tracery() 116 | let count = 1 117 | var total = 0 118 | let target = 10 119 | print("iterations", count) 120 | for i in 1...count { 121 | // define inline rule b 122 | // choices 123 | // 0 124 | // 1 125 | // any(0,1) and then b : weighted to target-1 126 | // then trigger b 127 | let output = t.expand("#b(0,1,#(0,1)##b#:\(target-1))##b#") 128 | print(i, "-", output, "(\(output.count))") 129 | total += output.count 130 | XCTAssertTrue(!output.contains("stack overflow")) 131 | if output.contains("stack overflow") { 132 | return 133 | } 134 | } 135 | let average = Double(total)/Double(count) 136 | print("AVG", average, " after \(count) iterations, should be close to \(target)") 137 | } 138 | 139 | 140 | func testInlineRulesCanCallExistingRulesWithWeights() { 141 | 142 | // prints hi 'x' times more than bye 143 | // x = 9 means of 0.9 probability of gtting hi 144 | let x = 9 145 | 146 | let t = Tracery.init(lines:[ 147 | "[say_hi]", 148 | "hi", 149 | "", 150 | "[say_bye]", 151 | "bye", 152 | "", 153 | "[msg]", 154 | "#(#say_hi#:\(x),#say_bye#)#" 155 | ]) 156 | 157 | let iterations = 100 158 | 159 | var hiCount = 0, byeCount = 0 160 | for _ in 1...iterations { 161 | let output = t.expand("#msg#") 162 | XCTAssertItemInArray(item: output, array: ["hi","bye"]) 163 | hiCount += output == "hi" ? 1 : 0 164 | byeCount += output == "bye" ? 1 : 0 165 | } 166 | 167 | let moreness = Double(hiCount)/Double(hiCount + byeCount) 168 | print("after \(iterations) iterations, hi=\(hiCount) bye=\(byeCount) [by: \(moreness) expected: \(Double(x)/10.0)]") 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Common/Parser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parser.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | // MARK:- Parsing 13 | 14 | 15 | struct Modifier : CustomStringConvertible { 16 | var name: String 17 | var parameters: [ValueCandidate] 18 | 19 | var description: String { 20 | return ".\(name)(\(parameters.map { $0.description }.joined(separator: ", ")))" 21 | } 22 | } 23 | 24 | protocol ValueCandidateProtocol { 25 | var nodes: [ParserNode] { get } 26 | var hasWeight: Bool { get } 27 | var weight: Int { get } 28 | } 29 | 30 | struct ValueCandidate: ValueCandidateProtocol, CustomStringConvertible { 31 | var nodes: [ParserNode] 32 | var hasWeight: Bool { 33 | if let last = nodes.last, case .weight = last { return true } 34 | return false 35 | } 36 | var weight: Int { 37 | if let last = nodes.last, case let .weight(value) = last { return value } 38 | return 1 39 | } 40 | var description: String { 41 | return "<" + nodes.map { "\($0)" }.joined(separator: ",") + ">" 42 | } 43 | } 44 | 45 | enum ParserConditionOperator { 46 | case equalTo 47 | case notEqualTo 48 | case valueIn 49 | case valueNotIn 50 | } 51 | 52 | struct ParserCondition: CustomStringConvertible { 53 | let lhs: [ParserNode] 54 | let rhs: [ParserNode] 55 | let op: ParserConditionOperator 56 | var description: String { 57 | return "\(lhs) \(op) \(rhs)" 58 | } 59 | } 60 | 61 | enum ParserNode : CustomStringConvertible { 62 | 63 | // input nodes 64 | 65 | case text(String) 66 | case rule(name:String, mods:[Modifier]) 67 | case any(values:[ValueCandidate], selector:RuleCandidateSelector, mods:[Modifier]) 68 | case tag(name:String, values:[ValueCandidate]) 69 | case weight(value: Int) 70 | case createRule(name:String, values:[ValueCandidate]) 71 | 72 | // control flow 73 | 74 | indirect case ifBlock(condition:ParserCondition, thenBlock:[ParserNode], elseBlock:[ParserNode]?) 75 | indirect case whileBlock(condition:ParserCondition, doBlock:[ParserNode]) 76 | 77 | // procedures 78 | 79 | case runMod(name: String) 80 | case createTag(name: String, selector: RuleCandidateSelector) 81 | 82 | // args handling 83 | indirect case evaluateArg(nodes: [ParserNode]) 84 | 85 | // low level flow control 86 | 87 | indirect case branch(check:ParserConditionOperator, thenBlock:[ParserNode], elseBlock:[ParserNode]?) 88 | 89 | public var description: String { 90 | switch self { 91 | 92 | case let .rule(name, mods): 93 | if mods.count > 0 { 94 | let mods = mods.reduce("") { $0 + $1.description } 95 | return "⌽#\(name)\(mods)#" 96 | } 97 | return "⌽#\(name)#" 98 | 99 | case let .createRule(name, values): 100 | if values.count == 1 { return "⌽+#\(name)=\(values[0])#" } 101 | return "⌽+#\(name)=\(values)#" 102 | 103 | case let .tag(name, values): 104 | if values.count == 1 { return "⌽t:\(name)=\(values[0])" } 105 | return "⌽t:\(name)=(\(values))" 106 | 107 | case let .text(text): 108 | return "⌽`\(text)`" 109 | 110 | case let .any(values, _, mods): 111 | if mods.count > 0 { 112 | return "⌽any\(values)+\(mods)" 113 | } 114 | return "⌽any\(values)" 115 | 116 | case let .weight(value): 117 | return "⌽weight(\(value))" 118 | 119 | case let .runMod(name): 120 | return "⌽run(\(name))" 121 | 122 | case let .createTag(name, _): 123 | return "⌽+t:\(name)" 124 | 125 | case let .evaluateArg(nodes): 126 | return "⌽eval<\(nodes)>" 127 | 128 | case let .ifBlock(condition, thenBlock, elseBlock): 129 | if let elseBlock = elseBlock { 130 | return "⌽if(\(condition) then \(thenBlock) else \(elseBlock))" 131 | } 132 | return "⌽if(\(condition) then \(thenBlock))" 133 | 134 | 135 | case let .branch(check, thenBlock, elseBlock): 136 | if let elseBlock = elseBlock { 137 | return "⌽jump(args \(check) then \(thenBlock) else \(elseBlock))" 138 | } 139 | return "⌽jump(args \(check) then \(thenBlock))" 140 | 141 | case let .whileBlock(condition, doBlock): 142 | return "⌽while(\(condition) then \(doBlock))" 143 | 144 | } 145 | } 146 | } 147 | 148 | 149 | 150 | enum ParserError : Error, CustomStringConvertible { 151 | case error(String) 152 | var description: String { 153 | switch self { 154 | case let .error(msg): return msg 155 | } 156 | } 157 | } 158 | 159 | 160 | 161 | struct Parser { } 162 | 163 | extension Array where Element: ValueCandidateProtocol { 164 | func selector() -> RuleCandidateSelector { 165 | if count < 2 { 166 | return PickFirstContentSelector.shared 167 | } 168 | func hasWeights() -> Bool { 169 | for i in self { 170 | if i.hasWeight { return true } 171 | } 172 | return false 173 | } 174 | if hasWeights() { 175 | var weights = [Int]() 176 | for i in self { 177 | weights.append(i.weight) 178 | } 179 | return WeightedSelector(weights) 180 | } 181 | return DefaultContentSelector(count) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Common/Tracery.Text.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tracery.Text.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 21/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | // File format that can scan plain text 13 | 14 | extension Tracery { 15 | 16 | convenience public init(path: String) { 17 | if let reader = StreamReader(path: path) { 18 | self.init { TextParser.parse(lines: reader) } 19 | } 20 | else { 21 | warn("unable to parse input file: \(path)") 22 | self.init() 23 | } 24 | } 25 | 26 | convenience public init(lines: [String]) { 27 | self.init { TextParser.parse(lines: lines) } 28 | } 29 | 30 | } 31 | 32 | struct TextParser { 33 | 34 | enum State { 35 | case consumeRule 36 | case consumeDefinitions 37 | } 38 | 39 | static func parse(lines: S) -> [String: Any] where S.Iterator.Element == String { 40 | 41 | var ruleSet = [String: Any]() 42 | var state = State.consumeRule 43 | var rule = "" 44 | var candidates = [String]() 45 | 46 | func createRule() { 47 | if ruleSet[rule] != nil { 48 | warn("rule '\(rule)' defined twice, will be overwritten") 49 | } 50 | if candidates.count == 0 { 51 | candidates.append("") 52 | } 53 | ruleSet[rule] = candidates 54 | } 55 | 56 | for line in lines { 57 | switch state { 58 | case .consumeRule: 59 | if line.count > 2, line.hasPrefix("["), line.hasSuffix("]") { 60 | let start = line.index(after: line.startIndex) 61 | let end = line.index(before: line.endIndex) 62 | rule = String(line[start.. String? { 123 | precondition(fileHandle != nil, "Attempt to read from closed file") 124 | 125 | if atEof { 126 | return nil 127 | } 128 | 129 | // Read data chunks from file until a line delimiter is found: 130 | var range = buffer.range(of: delimData, options: [], in: NSMakeRange(0, buffer.length)) 131 | while range.location == NSNotFound { 132 | let tmpData = fileHandle.readData(ofLength: chunkSize) 133 | if tmpData.count == 0 { 134 | // EOF or read error. 135 | atEof = true 136 | if buffer.length > 0 { 137 | // Buffer contains last line in file (not terminated by delimiter). 138 | let line = String(data: buffer as Data, encoding: encoding) 139 | 140 | buffer.length = 0 141 | return line as String? 142 | } 143 | // No more lines. 144 | return nil 145 | } 146 | buffer.append(tmpData) 147 | range = buffer.range(of: delimData, options: [], in: NSMakeRange(0, buffer.length)) 148 | } 149 | 150 | // Convert complete line (excluding the delimiter) to a string: 151 | let line = String(data: buffer.subdata(with: NSMakeRange(0, range.location)), 152 | encoding: encoding) 153 | // Remove line (and the delimiter) from the buffer: 154 | buffer.replaceBytes(in: NSMakeRange(0, range.location + range.length), withBytes: nil, length: 0) 155 | 156 | return line 157 | } 158 | 159 | /// Start reading from the beginning of file. 160 | func rewind() -> Void { 161 | fileHandle.seek(toFileOffset: 0) 162 | buffer.length = 0 163 | atEof = false 164 | } 165 | 166 | /// Close the underlying file. No reading must be done after calling this method. 167 | func close() -> Void { 168 | fileHandle?.closeFile() 169 | fileHandle = nil 170 | } 171 | } 172 | 173 | extension StreamReader: Sequence { 174 | func makeIterator() -> AnyIterator { 175 | return AnyIterator { 176 | return self.nextLine() 177 | } 178 | } 179 | } 180 | 181 | 182 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Pages/Introduction.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | 3 | ![Tracery - powerful content generation](logo.png) 4 | 5 | {{GEN:TOC}} 6 | 7 | # Introduction 8 | 9 | 10 | Tracery is a content generation library originally created by [@GalaxyKate](http://www.galaxykate.com/); you can find more information at [Tracery.io](http://www.tracery.io) 11 | 12 | 13 | This implementation, while heavily inspired by the original, has more features added. 14 | 15 | The content generation in Tracery works based on an input set of rules. The rules determine how content should be generated. 16 | 17 | ## Installation 18 | 19 | - Clone or download this repository 20 | - To work with the playground, open Playgrounds/Tracery.playground 21 | - The project builds `iOS` and `macOS` framework targets, which can be linked to your projects 22 | 23 | ## Basic usage 24 | 25 | 26 | */ 27 | 28 | import Tracery 29 | 30 | // create a new Tracery engine 31 | 32 | var t = Tracery {[ 33 | "msg" : "hello world" 34 | ]} 35 | 36 | t.expand("well #msg#") 37 | 38 | // output: well hello world 39 | 40 | /*: 41 | 42 | We create an instance of the Tracery engine passing along a dictionary of rules. The keys to this dictionary are the rule names, and the value for each key represents the expansion of the rule. 43 | 44 | The we use Tracery to expand instances of specified rules 45 | 46 | Notice we provide as input a template string, which contains `#msg#`, that is the rule we wish to expand inside `#` marks. Tracery evaluates the template, recognizes a rule, and replaces it with its expansion. 47 | 48 | We can have multiple rules: 49 | */ 50 | 51 | t = Tracery {[ 52 | "name": "jack", 53 | "age": "10", 54 | "msg": "#name# is #age# years old", 55 | ]} 56 | 57 | t.expand("#msg#") // jack is 10 years old 58 | 59 | /*: 60 | Notice how we specify to expand `#msg#`, which then triggers the expansion of `#name#` and `#age#` rules? Tracery can recursively expand rules until no further expansion is possible. 61 | 62 | A rule can have multiple candidates expansions. 63 | */ 64 | 65 | t = Tracery {[ 66 | "name": ["jack", "john", "jacob"], // we can specify multiple values here 67 | "age": "10", 68 | "msg": "#name# is #age# years old", 69 | ]} 70 | 71 | t.expand("#msg#") 72 | 73 | // jacob is 10 years old <- name is randomly picked 74 | 75 | t.expand("#msg#") 76 | 77 | // jack is 10 years old <- name is randomly picked 78 | 79 | t.expand("#name# #name#") 80 | 81 | // will print out two different random names 82 | 83 | /*: 84 | In the snippet above, whenever Tracery sees the rule `#name#`, it will pick out one of the candidate values; in this example, name could be "jack" "john" or "jacob" 85 | 86 | This is what allows content generation. By specifying various candidates for each rule, every time `expand` is invoked yields a different result. 87 | 88 | Let us try to build a sentence based on a popular nursery rhyme. 89 | */ 90 | 91 | t = Tracery {[ 92 | "boy" : ["jack", "john"], 93 | "girl" : ["jill", "jenny"], 94 | "sentence": "#boy# and #girl# went up the hill." 95 | ]} 96 | 97 | t.expand("#sentence#") 98 | 99 | // output: john and jenny went up the hill 100 | 101 | /*: 102 | So we get the first part of the sentence, what if we wanted to add in a second line so that our final output becomes: 103 | 104 | "john and jenny went up the hill, john fell down, and so did jenny too" 105 | */ 106 | 107 | // the following will fail 108 | // to produce the correct output 109 | t.expand("#boy# and #girl# went up the hill, #boy# fell down, and so did #girl#") 110 | 111 | // sample output: 112 | // jack and jenny went up the hill, john fell down, and so did jill 113 | 114 | /*: 115 | The problem is that any occurence of a `#rule#` will be replaced by one of its candidate values. So when we write `#boy#` twice, it may get replaced with entirely different names. 116 | 117 | In order to remember values, we can use tags. 118 | 119 | # Tags 120 | 121 | Tags allow to persist the result of a rule expansion to a temporary variable. 122 | 123 | */ 124 | 125 | t = Tracery {[ 126 | "boy" : ["jack", "john"], 127 | "girl" : ["jill", "jenny"], 128 | "sentence": "[b:#boy#][g:#girl#] #b# and #g# went up the hill, #b# fell down, and so did #g#" 129 | ]} 130 | 131 | t.expand("#sentence#") 132 | 133 | // output: jack and jill went up the hill, jack fell down, and so did jill 134 | 135 | /*: 136 | Tags are created using the format `[tagName:tagValue]`. In the above snippet we first create two tags, `b` and `g` to hold values of `boy` and `girl` names respectively. Later on we can use `#b#` and `#g#` as if they were new rules and we Tracery will recall their stored values as required for substitution. 137 | 138 | Tags can also simply contain a value, or a group of values. Tags can also appear inside `#rules#`. Tags are variable, they can be set any number of times. 139 | 140 | 141 | ## Simple story 142 | 143 | Here is a more complex example that generates a _short_ story. 144 | 145 | */ 146 | 147 | t = Tracery {[ 148 | 149 | "name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"], 150 | "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"], 151 | "mood": ["vexed","indignant","impassioned","wistful","astute","courteous"], 152 | "story": ["#hero# traveled with her pet #heroPet#. #hero# was never #mood#, for the #heroPet# was always too #mood#."], 153 | "origin": ["#[hero:#name#][heroPet:#animal#]story#"] 154 | ]} 155 | 156 | t.expand("#origin#") 157 | 158 | // sample output: 159 | // Darcy traveled with her pet unicorn. Darcy was never vexed, for the unicorn was always too indignant. 160 | 161 | /*: 162 | 163 | ## Random numbers 164 | 165 | Here's another example to generate a random number: 166 | 167 | */ 168 | 169 | t.expand("[d:0,1,2,3,4,5,6,7,8,9] random 5-digit number: #d##d##d##d##d#") 170 | 171 | // sample output: 172 | // random 5-digit number: 68233 173 | 174 | /*: 175 | 176 | In 177 | 178 | > If a tag name matches a rule, the tag will take precedence and will always be evaluated. 179 | 180 | Now that we have the hang of things, we will look at rule modifiers. 181 | 182 | [Modifiers](@next) 183 | 184 | */ 185 | 186 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Pages/Advanced Recursion.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | /*: 4 | 5 | ## Recursion 6 | 7 | ### Rule Expansions 8 | 9 | It is possible to define recursive rules. When doing so, you must provide at least one rule candidate that exits out of the recursion. 10 | 11 | */ 12 | 13 | import Tracery 14 | 15 | // suppose we wish to generate a random binary string 16 | // if we write the rules as 17 | 18 | var t = Tracery {[ 19 | "binary": [ "0 #binary#", "1 #binary#" ] 20 | ]} 21 | 22 | t.expand("#binary#") 23 | 24 | 25 | // will output: 26 | // ⛔️ stack overflow 27 | 28 | // Since there is no option to exit out of the rule expansion 29 | // We can add one explicitly 30 | 31 | t = Tracery {[ 32 | "binary": [ "0#binary#", "1#binary#", "" ] 33 | ]} 34 | 35 | print(t.expand("attempt 2: #binary#")) 36 | 37 | // while this works, if we run this a couple of times 38 | // you will notice that the output is as follows: 39 | 40 | // all possible outputs: 41 | // attempt 2: 1 42 | // attempt 2: 0 43 | // attempt 2: 01 44 | // attempt 2: 10 45 | // attempt 2: <- empty 46 | 47 | // all possible outputs are limited because the built-in 48 | // candidate selector is guaranteed to quickly select the "" 49 | // candidate to maintain a strict uniform distribution 50 | // we can fix this 51 | 52 | t = Tracery {[ 53 | "binary": WeightedCandidateSet([ 54 | "0#binary#": 10, 55 | "1#binary#": 10, 56 | "": 1 57 | ]) 58 | ]} 59 | 60 | print(t.expand("attempt 3: #binary#")) 61 | 62 | // sample outputs: 63 | // attempt 3: 011101000010100001010 64 | // attempt 3: 1010110 65 | // attempt 3: 10101100101 and so on 66 | 67 | // Now we have more control, as we are stating that we are 20 times more 68 | // likely to continue with a binary rule than the exit 69 | 70 | /*: 71 | 72 | If you wish to have a random sequence of a speicific length, you may want to create a custom `RuleCandidateSelector`, or write up/generate a non-recursive set of rules. 73 | 74 | > You can control how deep recursive rules can get expanded by changing the `Tracery.maxStackDepth` property. 75 | 76 | 77 | ### Logging 78 | 79 | You can control logging behaviour by changing the `Tracery.logLevel` property. 80 | 81 | */ 82 | 83 | Tracery.logLevel = .verbose 84 | 85 | 86 | t = Tracery {[ 87 | "binary": WeightedCandidateSet([ 88 | "0#binary#": 10, 89 | "1#binary#": 10, 90 | "": 1 91 | ]) 92 | ]} 93 | 94 | print(t.expand("attempt 3: #binary#")) 95 | 96 | // sample output: 97 | // attempt 3: 101010100011001001011 98 | // attempt 3: 001001011111 99 | // attempt 3: 1110010111121111 100 | // attempt 3: 10 101 | 102 | // this will print the entire trace that Tracery generates, you will see detailed output regarding rule validation, parsing, rule expansion - useful to debug and understand how rules are processed. 103 | 104 | 105 | /*: 106 | 107 | The available logging options are: 108 | 109 | * `none` 110 | * `errors` - prints any parsing errors (default) 111 | * `warnings` - prints warnings, e.g. highlights recursive rules, cylic references, possibly invalid rules, etc 112 | * `info` - prints informational messages, e.g. what state the tracery engine is in 113 | * `verbose` - prints trace level messages, you can get detailed notes on how the engine parses text, and evaluates rules 114 | 115 | 116 | ### Chaining Evaluations 117 | 118 | Consider the following example: 119 | 120 | */ 121 | 122 | t = Tracery {[ 123 | "b" : ["0", "1"], 124 | "num": "#b##b#", 125 | "10": "one_zero", 126 | "00": "zero_zero", 127 | "01": "zero_one", 128 | "11": "one_one", 129 | ]} 130 | 131 | t.expand("#num#") 132 | 133 | // will print either 01, 10, 11 or 10 134 | 135 | t.add(modifier: "eval") { [unowned t] input in 136 | // treat input as a rule and expand it 137 | return t.expand("#\(input)#") 138 | } 139 | 140 | t.expand("#num.eval#") 141 | 142 | // will now print one_zero, zero_zero, zero_one or one_one 143 | 144 | /*: 145 | 146 | We now have a mechanism to expand a rule based on the expansion results of another rule. 147 | 148 | ### Hierarchical Tag Storage 149 | 150 | By default, tags have global scope. This means that a tag can be set anywhere and its value will be accessible by any rule at any level of rule expansion. We can restrict tag access using hierarchical storage. 151 | 152 | Rules are expandede using a stack. Each rule evaluation occurs at a specific depth on the stack. If a rule at level `n` expands to two sub-rules, the two sub-rules will be evaluated at level `n+1`. A tag's level `n` will be the same as that of the rule at level `n` which created it. 153 | 154 | When a rule at level `n` tries to expand a tag, `Tracery` will check if a tag exists at level `n`, or search levels n-1,...0 until its able to find a value. 155 | 156 | 157 | In the example below, we use hierarchical storage to push and pop matching open and close braces, at various levels of rule expansion. The matching close brace is _remembered_ when the rule sub-expansion finishes. 158 | 159 | */ 160 | 161 | let options = TraceryOptions() 162 | options.tagStorageType = .heirarchical 163 | 164 | let braceTypes = ["()","{}","<>","«»","𛰫𛰬","⌜⌝","ᙅᙂ","ᙦᙣ","⁅⁆","⌈⌉","⌊⌋","⟦⟧","⦃⦄","⦗⦘","⫷⫸"] 165 | .map { braces -> String in 166 | let open = braces[braces.startIndex] 167 | let close = braces[braces.index(after: braces.startIndex)] 168 | return "[open:\(open)][close:\(close)]" 169 | } 170 | 171 | let h = Tracery(options) {[ 172 | "letter": ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P"], 173 | "bracetypes": braceTypes, 174 | "brace": [ 175 | // open with current symbol, 176 | // create new symbol, open brace pair and evaluate a sub-rule call to brace 177 | // finally close off with orginal symbol and matching close brace pair 178 | "#open##symbol# #origin##symbol##close# ", 179 | 180 | "#open##symbol# #origin##symbol##close# #origin#", 181 | 182 | // exits recursion 183 | "", 184 | ], 185 | 186 | // start with a symbol and bracetype 187 | "origin": ["#[symbol:#letter#][#bracetypes#]brace#"] 188 | ]} 189 | 190 | h.expand("#origin#") 191 | 192 | // sample outputs: 193 | // {L ⌜D D⌝ (P 𛰫O O𛰬 P) L} 194 | // ⁅M ᙅK Kᙂ ᙦE {O O} Eᙣ M⁆ 195 | // ⌈C C⌉ 196 | // 197 | 198 | //: [Advanced contd.](@next) 199 | -------------------------------------------------------------------------------- /Playgrounds/Face Generator.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: # Pixel Face Generator 2 | //: Powered by [Tracery](https://github.com/BenziAhamed/Tracery) 3 | 4 | import UIKit 5 | import SpriteKit 6 | import PlaygroundSupport 7 | import Tracery 8 | 9 | 10 | let view = SKView.init(frame: .init(x: 0, y: 0, width: 400, height: 400)) 11 | PlaygroundPage.current.liveView = view 12 | 13 | var colors:[String: UIColor] = [ 14 | "blk" : .black, 15 | "w": .white, 16 | "bg": UIColor(red:0.152, green:0.698, blue:0.962, alpha:1), 17 | "cheekcol": UIColor(hex: "f28fb2").withAlphaComponent(0.32), 18 | "skin-1": UIColor(hex: "ffcc80"), 19 | "skin-2": UIColor(hex: "ffb74d"), 20 | "skin-3": UIColor(hex: "ffa726"), 21 | "skin-4": UIColor(hex: "ff9800"), 22 | "skin-5": UIColor(hex: "fb8c00"), 23 | "skin-6": UIColor(hex: "f57c00"), 24 | "skin-8": UIColor(hex: "a1887f"), 25 | "skin-9": UIColor(hex: "8d6e63"), 26 | "skin-10": UIColor(hex: "795548"), 27 | "skin-11": UIColor(hex: "6d4c41"), 28 | "skin-12": UIColor(hex: "5d4037"), 29 | ] 30 | 31 | /*: 32 | Our face is generated in a 8x8 block. We will exclusively use methods as the main mechanism to render the face. For example, the `.block` method is used to render a solid colored rectangle at the coordinates specified. 33 | */ 34 | 35 | var tracery = Tracery {[ 36 | 37 | "face": "#.block(1,1,7,7,#skin#)#", 38 | "eyes": "#.set(2,4,w)# #.set(5,4,w)#", 39 | 40 | "neck": "#.xf(2,0,4,#skin-dark#)#", 41 | "mouth": "#.block(3,2,5,3,#lips#)#", 42 | "freckles": ["", "", "", "#.set(1,2,#skin-dark#)# #.set(6,3,#skin-dark#)#", "#.set(4,5,#skin-dark#)#", "#.xf(5,6,2,blk)#"], 43 | 44 | "cheeks": ["", "", "", "#.set(1,3,cheekcol)# #.set(6,3,cheekcol)#"], 45 | 46 | // various hairstyles 47 | "hair-neil": "#.block(1,7,7,8,blk)##.block(1,6,4,7,blk)#", 48 | "hair-latino" : "#.block(1,6,7,8,blk)##.set(4,6,blk)#", 49 | "hair-army2" : "#.block(1,7,7,8,blk)##.set(1,6,blk)##.set(3,6,blk)##.set(5,6,blk)#", 50 | "hair-army1" : "#.block(1,7,7,8,blk)#", 51 | "hair-chinese" : "#.block(0,3,1,8,blk)##.block(7,3,8,8,blk)##hair-army1##.set(3,6,blk)##.set(4,6,blk)#", 52 | "hair-fran": "#.yf(0,4,3,blk)##.xf(1,7,6,blk)##.xf(3,6,5,blk)##.xf(5,5,3,blk)##.set(6,4,blk)#", 53 | "hair-messi": "#.xf(1,7,6,blk)##.xf(0,6,4,blk)##.xf(5,6,2,blk)##.xf(4,5,2,blk)##.set(1,5,blk)#", 54 | "hair-alena": "#hair-army1# #.yf(0,3,4,blk)# #.yf(7,3,4,blk)# #.xf(0,6,4,blk)#", 55 | "hair-alice": "#hair-army1# #.yf(0,3,4,blk)# #.yf(7,3,4,blk)# #.yf(6,3,2,blk)# #.xf(2,5,2,blk)# #.set(4,6,blk)#", 56 | 57 | "hair-opts" : "neil latino army1 army2 fran messi alena alice".components(separatedBy: " "), 58 | 59 | // select a hair-style 60 | "initHair": "[hair:\\#hair-#hair-opts#\\#]", 61 | 62 | // select and set skin tone colors 63 | "initSkin": [ 64 | "[skin:skin-1][skin-dark:skin-2][lips:skin-3]", 65 | "[skin:skin-2][skin-dark:skin-3][lips:skin-4]", 66 | "[skin:skin-3][skin-dark:skin-4][lips:skin-5]", 67 | "[skin:skin-4][skin-dark:skin-5][lips:skin-6]", 68 | "[skin:skin-8][skin-dark:skin-9][lips:skin-10]", 69 | "[skin:skin-9][skin-dark:skin-10][lips:skin-11]", 70 | "[skin:skin-10][skin-dark:skin-11][lips:skin-12]", 71 | ], 72 | 73 | // init 74 | "init": "#initHair# #initSkin#", 75 | 76 | // render a random face 77 | "gen" : "#init# #face# #eyes# #neck# #mouth# #freckles# #cheeks# #hair.eval#", 78 | ]} 79 | 80 | class Scene : SKScene { 81 | 82 | var prevUpdateTime: TimeInterval? = nil 83 | var elapsed: CGFloat = 0 84 | let grid = SKNode() 85 | var zpos: CGFloat = 0 86 | 87 | override func didMove(to view: SKView) { 88 | size = view.frame.size 89 | backgroundColor = colors["bg"]! 90 | grid.position = CGPoint(x: 80, y: 80) 91 | addChild(grid) 92 | setupTracery() 93 | generateFace() 94 | } 95 | 96 | //: We create and attach the necessary methods to Tracery 97 | 98 | func setupTracery() { 99 | // fills a rectangular area 100 | // with a solid color 101 | func fillBlock(_ args: [String]) -> String { 102 | let sx = CGFloat(Double(args[0]) ?? 0) 103 | let sy = CGFloat(Double(args[1]) ?? 0) 104 | let ex = CGFloat(Double(args[2]) ?? 1) 105 | let ey = CGFloat(Double(args[3]) ?? 1) 106 | let color = colors[args[4]]! 107 | 108 | let size = CGSize(width: (ex - sx) * 30, height: (ey - sy) * 30) 109 | let block = SKSpriteNode(color: color, size: size) 110 | block.anchorPoint = .zero 111 | block.position = CGPoint.init(x: 30 * sx, y: 30 * sy) 112 | block.zPosition = zpos 113 | 114 | grid.addChild(block) 115 | return "" 116 | } 117 | 118 | 119 | tracery.add(method: "block") { _, args in 120 | return fillBlock(args) 121 | } 122 | 123 | 124 | // set an individial pixel 125 | tracery.add(method: "set") { _, args in 126 | let sx = Int(args[0]) ?? 0 127 | let sy = Int(args[1]) ?? 0 128 | let color = args[2] 129 | return fillBlock(["\(sx)","\(sy)","\(sx+1)","\(sy+1)",color]) 130 | } 131 | 132 | // line across x axis 133 | tracery.add(method: "xf") { _, args in 134 | let sx = Int(args[0]) ?? 0 135 | let sy = Int(args[1]) ?? 0 136 | let count = Int(args[2]) ?? 0 137 | let color = args[3] 138 | return fillBlock(["\(sx)","\(sy)","\(sx+count)","\(sy+1)",color]) 139 | } 140 | 141 | // line across y axis 142 | tracery.add(method: "yf") { _, args in 143 | let sx = Int(args[0]) ?? 0 144 | let sy = Int(args[1]) ?? 0 145 | let count = Int(args[2]) ?? 0 146 | let color = args[3] 147 | return fillBlock(["\(sx)","\(sy)","\(sx+1)","\(sy+count)",color]) 148 | } 149 | 150 | tracery.add(modifier: "eval") { input in 151 | return tracery.expand(input, maintainContext: false) 152 | } 153 | } 154 | 155 | 156 | 157 | func generateFace() { 158 | zpos = 0 159 | grid.removeAllChildren() 160 | _ = tracery.expand("#gen#") 161 | } 162 | 163 | 164 | //: Generate a new face every second 165 | 166 | override func update(_ currentTime: TimeInterval) { 167 | elapsed += CGFloat(currentTime - (prevUpdateTime ?? currentTime)) 168 | prevUpdateTime = currentTime 169 | guard elapsed >= 1 else { return } 170 | elapsed = 0 171 | generateFace() 172 | } 173 | } 174 | 175 | view.presentScene(Scene()) 176 | 177 | -------------------------------------------------------------------------------- /Tracery/Tracery Tests/Tags.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tags.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class Tags: XCTestCase { 13 | 14 | func testDefaultStorageIsUnilevel() { 15 | let t = Tracery() 16 | XCTAssertEqual(t.options.tagStorageType, .unilevel) 17 | } 18 | 19 | func testTagsWork() { 20 | let t = Tracery() 21 | XCTAssertEqual(t.expand("[tag:value]#tag#"), "value") 22 | } 23 | 24 | func testUnilevelTagsCanBeSet() { 25 | let t = Tracery {[ 26 | "outside_rule" : "[tag:value]#tag#", 27 | "inside_rule" : "#[tag:value]tag#", 28 | ]} 29 | t.ruleNames.forEach { rule in 30 | XCTAssertEqual(t.expand("#\(rule)#"), "value") 31 | } 32 | } 33 | 34 | func testTagIsSetWithSingleValue() { 35 | let t = Tracery {[ 36 | "name": "benzi", 37 | "msg": "#[tag:#name#]#hello world #tag#" 38 | ]} 39 | XCTAssertEqual(t.expand("#msg#"), "hello world benzi") 40 | } 41 | 42 | func testAllowTagsToOverrideRules() { 43 | let t = Tracery {[ 44 | "name": "benzi", 45 | "msg": "#[name:override name]name#" 46 | ]} 47 | XCTAssertEqual(t.expand("#msg#"), "override name") 48 | } 49 | 50 | func testAllowTagsToOverrideTags() { 51 | let t = Tracery {[ 52 | "name": "benzi", 53 | "msg": "#[name:first time][name:second time]name#" 54 | ]} 55 | XCTAssertEqual(t.expand("#msg#"), "second time") 56 | } 57 | 58 | func testAllowTagNesting() { 59 | let t = Tracery() 60 | XCTAssertItemInArray(item: t.expand("[[tag1:jack][tag2:jill]tag:#tag1#,#tag2#]#tag#"), array: ["jack", "jill"]) 61 | } 62 | 63 | func testTagIsSetWithMultipleValues() { 64 | let t = Tracery {[ 65 | "name": "benzi", 66 | "msg": "[tag:#name#,jack]#tag#" 67 | ]} 68 | 69 | // Tracery.logLevel = .verbose 70 | 71 | let value1 = t.expand("#msg#") 72 | let value2 = t.expand("#msg#") 73 | 74 | XCTAssertItemInArray(item: value1, array: ["benzi", "jack"]) 75 | XCTAssertItemInArray(item: value2, array: ["benzi", "jack"]) 76 | 77 | } 78 | 79 | func testCreatingTagAndUsingItImmediately() { 80 | let t = Tracery {[ 81 | "msg": "#[tag:hello world]tag#" 82 | ]} 83 | XCTAssertEqual(t.expand("#msg#"), "hello world") 84 | } 85 | 86 | func testTagValueIsAlwaysPickedFromChoicesSpecified() { 87 | let choices = "jack,jill,jacob,jenny,jeremy,janet,jason,john" 88 | let t = Tracery {[ 89 | "msg": "#[tag:\(choices)]tag#" 90 | ]} 91 | 92 | let choicesArray = choices.components(separatedBy: ",") 93 | for _ in 0..[String: Any]) -> Tracery { 136 | let options = TraceryOptions() 137 | options.tagStorageType = .heirarchical 138 | let t = Tracery(options, rules: rules) 139 | return t 140 | } 141 | 142 | func testHierarchicalTagsDoNotOverrideAtDifferentLevels() { 143 | let t = hierarchicalTracery {[ 144 | "origin" : "[tag:level-0][#level1#]#tag#", 145 | "level1" : "[tag:level-1]#tag# [#level2#]", 146 | "level2" : "[tag:level-2]#tag# ", 147 | ]} 148 | XCTAssertEqual(t.expand("#origin#"), "level-1 level-2 level-0") 149 | } 150 | 151 | func testHierarchicalTagsOverrideAtSameLevels() { 152 | let t = hierarchicalTracery {[ 153 | "origin" : "[tag:level-0][#level-1A#][#level-1B#]#tag#", 154 | "level-1A" : "[tag:level-1A]#tag# ", 155 | "level-1B" : "[tag:level-1B]#tag# ", 156 | ]} 157 | XCTAssertEqual(t.expand("#origin#"), "level-1A level-1B level-0") 158 | } 159 | 160 | func testHierarchicalTagsStoredAndReadAtSameLevel() { 161 | let t = hierarchicalTracery {[ 162 | "origin" : "[tag:level-0]#tag#", 163 | ]} 164 | XCTAssertEqual(t.expand("#origin#"), "level-0") 165 | } 166 | 167 | func testHierarchicalTagsCanRetrieveTagValuesFromLowerLevels() { 168 | let t = hierarchicalTracery {[ 169 | "origin" : "[tag:root]#level-1#", 170 | "level-1" : "L1=#tag#, #level-2#", 171 | "level-2" : "L2=#tag#", 172 | ]} 173 | XCTAssertEqual(t.expand("#origin#"), "L1=root, L2=root") 174 | } 175 | 176 | func testHierarchicalTagsCannotRetrieveTagValuesFromUpperLevels() { 177 | let t = hierarchicalTracery {[ 178 | "origin" : "[tag:root]#level-1#", 179 | "level-1" : "L1=#tag#, #level-2#", 180 | "level-2" : "[#level-3#]L2=#tag#, #L3#", 181 | "level-3" : "[L3:do_not_print]" 182 | ]} 183 | XCTAssertEqual(t.expand("#origin#"), "L1=root, L2=root, #L3#") 184 | } 185 | 186 | func testHierarchicalTagsCanBeSet() { 187 | let t = hierarchicalTracery {[ 188 | "outside_rule" : "[tag:value]#tag#", 189 | "inside_rule" : "#[tag:value]tag#", 190 | "override_in_same_rule1": "[tag:value-out ]#[tag:value-in ]tag##tag#", 191 | "override_in_same_rule2": "[tag:value-out ]#tag##[tag:value-in ]tag#", 192 | "sub_tag_not_visible" : "[#sub_tag#]#tag2#", 193 | "sub_tag" : "[tag2:sub tag]" 194 | ]} 195 | 196 | XCTAssertEqual(t.expand("#outside_rule#"), "value") 197 | XCTAssertEqual(t.expand("#inside_rule#"), "value") 198 | XCTAssertEqual(t.expand("#override_in_same_rule1#"), "value-in value-in ") 199 | XCTAssertEqual(t.expand("#override_in_same_rule2#"), "value-out value-in ") 200 | XCTAssertEqual(t.expand("#sub_tag_not_visible#"), "#tag2#") 201 | } 202 | 203 | 204 | } 205 | -------------------------------------------------------------------------------- /Common/Tracery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tracery.swift 3 | // 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | struct RuleMapping { 12 | let candidates: [RuleCandidate] 13 | var selector: RuleCandidateSelector 14 | 15 | func select() -> RuleCandidate? { 16 | let index = selector.pick(count: candidates.count) 17 | guard index >= 0 && index < candidates.count else { return nil } 18 | return candidates[index] 19 | } 20 | } 21 | 22 | struct RuleCandidate { 23 | let text: String 24 | var value: ValueCandidate 25 | } 26 | 27 | 28 | public class TraceryOptions { 29 | public var tagStorageType = TaggingPolicy.unilevel 30 | public var isRuleAnalysisEnabled = true 31 | public var logLevel = Tracery.LoggingLevel.errors 32 | 33 | public init() { } 34 | } 35 | 36 | extension TraceryOptions { 37 | public static let defaultSet = TraceryOptions() 38 | } 39 | 40 | public class Tracery { 41 | 42 | var objects = [String: Any]() 43 | var ruleSet: [String: RuleMapping] 44 | var runTimeRuleSet = [String: RuleMapping]() 45 | var mods: [String: (String,[String])->String] 46 | var tagStorage: TagStorage 47 | var contextStack: ContextStack 48 | 49 | 50 | public var ruleNames: [String] { return ruleSet.keys.map { $0 } } 51 | 52 | convenience public init() { 53 | self.init {[:]} 54 | } 55 | 56 | let options: TraceryOptions 57 | 58 | public init(_ options: TraceryOptions = TraceryOptions.defaultSet, rules: () -> [String: Any]) { 59 | 60 | self.options = options 61 | mods = [:] 62 | ruleSet = [:] 63 | tagStorage = options.tagStorageType.storage() 64 | contextStack = ContextStack() 65 | tagStorage.tracery = self 66 | 67 | let rules = rules() 68 | 69 | rules.forEach { rule, value in 70 | add(rule: rule, definition: value) 71 | } 72 | 73 | analyzeRuleBook() 74 | 75 | info("tracery ready") 76 | } 77 | 78 | func createRuleCandidate(rule:String, text: String) -> RuleCandidate? { 79 | let e = error 80 | do { 81 | info("checking rule '\(rule)' - \(text)") 82 | return RuleCandidate( 83 | text: text, 84 | value: ValueCandidate(nodes: try Parser.gen2(Lexer.tokens(text))) 85 | ) 86 | } 87 | catch { 88 | e("rule '\(rule)' parse error - \(error)") 89 | return nil 90 | } 91 | } 92 | 93 | public func add(modifier: String, transform: @escaping (String)->String) { 94 | if mods[modifier] != nil { 95 | warn("overwriting modifier '\(modifier)'") 96 | } 97 | mods[modifier] = { input, _ in 98 | return transform(input) 99 | } 100 | } 101 | 102 | public func add(call: String, transform: @escaping () -> ()) { 103 | if mods[call] != nil { 104 | warn("overwriting call '\(call)'") 105 | } 106 | mods[call] = { input, _ in 107 | transform() 108 | return input 109 | } 110 | } 111 | 112 | public func add(method: String, transform: @escaping (String, [String])->String) { 113 | if mods[method] != nil { 114 | warn("overwriting method '\(method)'") 115 | } 116 | mods[method] = transform 117 | } 118 | 119 | public func setCandidateSelector(rule: String, selector: RuleCandidateSelector) { 120 | guard ruleSet[rule] != nil else { 121 | warn("rule '\(rule)' not found to set selector") 122 | return 123 | } 124 | ruleSet[rule]?.selector = selector 125 | } 126 | 127 | public func expand(_ input: String, maintainContext: Bool = false) -> String { 128 | do { 129 | if !maintainContext { 130 | ruleEvaluationLevel = 0 131 | runTimeRuleSet.removeAll() 132 | tagStorage.removeAll() 133 | } 134 | return try eval(input) 135 | } 136 | catch { 137 | return "error: \(error)" 138 | } 139 | } 140 | 141 | public static var maxStackDepth = 256 142 | 143 | fileprivate(set) var ruleEvaluationLevel: Int = 0 144 | 145 | func incrementEvaluationLevel() throws { 146 | ruleEvaluationLevel += 1 147 | // trace("⚙️ depth: \(ruleEvaluationLevel)") 148 | if ruleEvaluationLevel > Tracery.maxStackDepth { 149 | error("stack overflow") 150 | throw ParserError.error("stack overflow") 151 | } 152 | } 153 | 154 | func decrementEvaluationLevel() { 155 | ruleEvaluationLevel = max(ruleEvaluationLevel - 1, 0) 156 | // trace("⚙️ depth: \(ruleEvaluationLevel)") 157 | } 158 | } 159 | 160 | 161 | 162 | // MARK: Rule management 163 | extension Tracery { 164 | // add a rule and its definition to 165 | // the mapping table 166 | // errors if any are returned 167 | public func add(rule: String, definition value: Any) { 168 | 169 | // validate the rule name 170 | let tokens = Lexer.tokens(rule) 171 | guard tokens.count == 1, case .text = tokens[0] else { 172 | error("rule '\(rule)' ignored - names must be plaintext") 173 | return 174 | } 175 | if ruleSet[rule] != nil { 176 | warn("rule '\(rule)' will be re-written") 177 | } 178 | 179 | let values: [String] 180 | 181 | if let provider = value as? RuleCandidatesProvider { 182 | values = provider.candidates 183 | } 184 | else if let string = value as? String { 185 | values = [string] 186 | } 187 | else if let array = value as? [String] { 188 | values = array 189 | } 190 | else if let array = value as? Array { 191 | values = array.map { $0.description } 192 | } 193 | else { 194 | values = ["\(value)"] 195 | } 196 | 197 | let candidates = values.compactMap { createRuleCandidate(rule: rule, text: $0) } 198 | if candidates.count == 0 { 199 | warn("rule '\(rule)' ignored - no expansion candidates found") 200 | return 201 | } 202 | 203 | let selector: RuleCandidateSelector 204 | if let s = value as? RuleCandidateSelector { 205 | selector = s 206 | } 207 | else { 208 | selector = candidates.map { $0.value }.selector() 209 | } 210 | 211 | ruleSet[rule] = RuleMapping(candidates: candidates, selector: selector) 212 | } 213 | 214 | // Removes a rule 215 | public func remove(rule: String) { 216 | ruleSet[rule] = nil 217 | } 218 | } 219 | 220 | // MARK: object management 221 | extension Tracery { 222 | 223 | public func add(object: Any, named name: String) { 224 | objects[name] = object 225 | } 226 | 227 | public func remove(object name: String) { 228 | objects[name] = nil 229 | } 230 | 231 | public func configuredObjects() -> [String: Any] { 232 | return objects 233 | } 234 | 235 | } 236 | 237 | -------------------------------------------------------------------------------- /CommonTesting/Tags.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tags.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 11/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tracery 11 | 12 | class Tags: XCTestCase { 13 | 14 | func testDefaultStorageIsUnilevel() { 15 | let t = Tracery() 16 | XCTAssertEqual(t.options.tagStorageType, .unilevel) 17 | } 18 | 19 | func testTagsWork() { 20 | let t = Tracery() 21 | XCTAssertEqual(t.expandVerbose("[tag:value]#tag#"), "value") 22 | XCTAssertEqual(t.expandVerbose("{[tag:value]tag}"), "value") 23 | } 24 | 25 | func testUnilevelTagsCanBeSet() { 26 | let t = Tracery {[ 27 | "outside_rule" : "[tag:value]#tag#", 28 | "inside_rule" : "#[tag:value]tag#", 29 | ]} 30 | t.ruleNames.forEach { rule in 31 | XCTAssertEqual(t.expand("#\(rule)#"), "value") 32 | } 33 | } 34 | 35 | func testTagIsSetWithSingleValue() { 36 | let t = Tracery {[ 37 | "name": "benzi", 38 | "msg": "#[tag:#name#]#hello world #tag#" 39 | ]} 40 | XCTAssertEqual(t.expand("#msg#"), "hello world benzi") 41 | } 42 | 43 | func testAllowTagsToOverrideRules() { 44 | let t = Tracery {[ 45 | "name": "benzi", 46 | "msg": "#[name:override name]name#" 47 | ]} 48 | XCTAssertEqual(t.expand("#msg#"), "override name") 49 | } 50 | 51 | func testAllowTagsToOverrideTags() { 52 | let t = Tracery {[ 53 | "name": "benzi", 54 | "msg": "#[name:first time][name:second time]name#" 55 | ]} 56 | XCTAssertEqual(t.expand("#msg#"), "second time") 57 | } 58 | 59 | func testAllowTagNesting() { 60 | let t = Tracery() 61 | XCTAssertItemInArray(item: t.expand("[[tag1:jack][tag2:jill]tag:#tag1#,#tag2#]#tag#"), array: ["jack", "jill"]) 62 | } 63 | 64 | func testTagIsSetWithMultipleValues() { 65 | let t = Tracery {[ 66 | "name": "benzi", 67 | "msg": "[tag:#name#,jack]#tag#" 68 | ]} 69 | 70 | // Tracery.logLevel = .verbose 71 | 72 | let value1 = t.expand("#msg#") 73 | let value2 = t.expand("#msg#") 74 | 75 | XCTAssertItemInArray(item: value1, array: ["benzi", "jack"]) 76 | XCTAssertItemInArray(item: value2, array: ["benzi", "jack"]) 77 | 78 | } 79 | 80 | func testCreatingTagAndUsingItImmediately() { 81 | let t = Tracery {[ 82 | "msg": "#[tag:hello world]tag#" 83 | ]} 84 | XCTAssertEqual(t.expand("#msg#"), "hello world") 85 | } 86 | 87 | func testTagValueIsAlwaysPickedFromChoicesSpecified() { 88 | let choices = "jack,jill,jacob,jenny,jeremy,janet,jason,john" 89 | let t = Tracery {[ 90 | "msg": "#[tag:\(choices)]tag#" 91 | ]} 92 | 93 | let choicesArray = choices.components(separatedBy: ",") 94 | for _ in 0..[String: Any]) -> Tracery { 144 | let options = TraceryOptions() 145 | options.tagStorageType = .heirarchical 146 | let t = Tracery(options, rules: rules) 147 | return t 148 | } 149 | 150 | func testHierarchicalTagsDoNotOverrideAtDifferentLevels() { 151 | let t = hierarchicalTracery {[ 152 | "origin" : "[tag:level-0][#level1#]#tag#", 153 | "level1" : "[tag:level-1]#tag# [#level2#]", 154 | "level2" : "[tag:level-2]#tag# ", 155 | ]} 156 | XCTAssertEqual(t.expand("#origin#"), "level-1 level-2 level-0") 157 | } 158 | 159 | func testHierarchicalTagsOverrideAtSameLevels() { 160 | let t = hierarchicalTracery {[ 161 | "origin" : "[tag:level-0][#level-1A#][#level-1B#]#tag#", 162 | "level-1A" : "[tag:level-1A]#tag# ", 163 | "level-1B" : "[tag:level-1B]#tag# ", 164 | ]} 165 | XCTAssertEqual(t.expand("#origin#"), "level-1A level-1B level-0") 166 | } 167 | 168 | func testHierarchicalTagsStoredAndReadAtSameLevel() { 169 | let t = hierarchicalTracery {[ 170 | "origin" : "[tag:level-0]#tag#", 171 | ]} 172 | XCTAssertEqual(t.expand("#origin#"), "level-0") 173 | } 174 | 175 | func testHierarchicalTagsCanRetrieveTagValuesFromLowerLevels() { 176 | let t = hierarchicalTracery {[ 177 | "origin" : "[tag:root]#level-1#", 178 | "level-1" : "L1=#tag#, #level-2#", 179 | "level-2" : "L2=#tag#", 180 | ]} 181 | XCTAssertEqual(t.expand("#origin#"), "L1=root, L2=root") 182 | } 183 | 184 | func testHierarchicalTagsCannotRetrieveTagValuesFromUpperLevels() { 185 | let t = hierarchicalTracery {[ 186 | "origin" : "[tag:root]#level-1#", 187 | "level-1" : "L1=#tag#, #level-2#", 188 | "level-2" : "[#level-3#]L2=#tag#, #L3#", 189 | "level-3" : "[L3:do_not_print]" 190 | ]} 191 | XCTAssertEqual(t.expand("#origin#"), "L1=root, L2=root, {L3}") 192 | } 193 | 194 | func testHierarchicalTagsCanBeSet() { 195 | let t = hierarchicalTracery {[ 196 | "outside_rule" : "[tag:value]#tag#", 197 | "inside_rule" : "#[tag:value]tag#", 198 | "override_in_same_rule1": "[tag:value-out ]#[tag:value-in ]tag##tag#", 199 | "override_in_same_rule2": "[tag:value-out ]#tag##[tag:value-in ]tag#", 200 | "sub_tag_not_visible" : "[#sub_tag#]#tag2#", 201 | "sub_tag" : "[tag2:sub tag]" 202 | ]} 203 | 204 | XCTAssertEqual(t.expand("#outside_rule#"), "value") 205 | XCTAssertEqual(t.expand("#inside_rule#"), "value") 206 | XCTAssertEqual(t.expand("#override_in_same_rule1#"), "value-in value-in ") 207 | XCTAssertEqual(t.expand("#override_in_same_rule2#"), "value-out value-in ") 208 | XCTAssertEqual(t.expand("#sub_tag_not_visible#"), "{tag2}") 209 | } 210 | 211 | 212 | } 213 | -------------------------------------------------------------------------------- /Playgrounds/Tracery.playground/Pages/Advanced.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | /*: 4 | # Advanced Usage 5 | 6 | ## Custom Content Selectors 7 | 8 | We know that a rule can have multiple candidates. By default, Tracery chooses a candidate option randomly, but the selection process is guaranteed to be strictly uniform. 9 | 10 | 11 | That is to say, if there was a rule with 5 options, and that rule was evaluated 100 times, each of those 5 options would have been selected exactly 20 times. 12 | 13 | This is easy to demonstrate: 14 | 15 | */ 16 | 17 | 18 | import Tracery 19 | 20 | var t = Tracery {[ 21 | "option": [ "a", "b", "c", "d", "e" ] 22 | ]} 23 | 24 | var tracker = [String: Int]() 25 | 26 | t.add(modifier: "track") { input in 27 | let count = tracker[input] ?? 0 28 | tracker[input] = count + 1 29 | return input 30 | } 31 | 32 | func runOptionRule(times: Int, header: String) { 33 | tracker.removeAll() 34 | for _ in 0.. Int { 75 | return 0 76 | } 77 | } 78 | 79 | // attach this new selector to rule: option 80 | t.setCandidateSelector(rule: "option", selector: AlwaysPickFirst()) 81 | 82 | runOptionRule(times: 100, header: "pick first") 83 | 84 | // output will be: 85 | // a 100 86 | 87 | /*: 88 | 89 | As you can see, only `a` was selected. 90 | 91 | ### Custom Random Item Selector 92 | 93 | For another example, let's create a custom random selector. 94 | 95 | */ 96 | 97 | class Arc4RandomSelector : RuleCandidateSelector { 98 | func pick(count: Int) -> Int { 99 | return Int(arc4random_uniform(UInt32(count))) 100 | } 101 | } 102 | 103 | t.setCandidateSelector(rule: "option", selector: Arc4RandomSelector()) 104 | 105 | // do a new dry run 106 | runOptionRule(times: 100, header: "arc4 random") 107 | 108 | // sample output, will vary when you try 109 | // b 18 110 | // e 25 111 | // a 20 112 | // d 15 113 | // c 22 114 | 115 | /*: 116 | 117 | Notice how the distribution of value selection changes when using `arc4random_uniform`. As the number of runs increases over time, `arc4random_uniform` will tend towards a uniform distribution, unlike the default implementation in Tracery, which even with 5 runs guarantees all 5 options are picked once. 118 | 119 | */ 120 | 121 | t = Tracery {[ 122 | "option": [ "a", "b", "c", "d", "e" ] 123 | ]} 124 | 125 | t.add(modifier: "track") { input in 126 | let count = tracker[input] ?? 0 127 | tracker[input] = count + 1 128 | return input 129 | } 130 | 131 | runOptionRule(times: 5, header: "default") 132 | 133 | // output will be 134 | // b 1 135 | // e 1 136 | // a 1 137 | // d 1 138 | // c 1 139 | 140 | /*: 141 | 142 | Now that we know fairly well how content selection works for a given rule, let us tackle the problem of weighted distributions. 143 | 144 | Say we need a particular candidate to be chosen 5 times more often than another candidate. 145 | 146 | One way of specifying this would be as follows: 147 | 148 | */ 149 | 150 | t = Tracery {[ 151 | "option": [ "a", "a", "a", "a", "a", "b" ] 152 | ]} 153 | 154 | t.add(modifier: "track") { input in 155 | let count = tracker[input] ?? 0 156 | tracker[input] = count + 1 157 | return input 158 | } 159 | 160 | runOptionRule(times: 100, header: "default - weighted") 161 | 162 | // sample output, will vary over runs 163 | // b 17 ~> 20% of 100 164 | // a 83 ~> 80% of 100, i.e. 5 times 20 165 | 166 | /*: 167 | 168 | This may work out for simple cases, but if you have more candidates, and more complex weight distribution rules, things can get messy quite quick. 169 | 170 | In order to provide more flexibility over candidate representation, Tracery allows custom candidate providers. 171 | 172 | ## Custom Candidate Provider 173 | 174 | ### Weighted Distributions 175 | 176 | */ 177 | 178 | 179 | 180 | // This class implements two protocols 181 | // RuleCandidateSelector - which as we have seen before is used to 182 | // to select content in a custom way 183 | // RuleCandidatesProvider - the protocol which needs to be 184 | // adhered to to provide customised content 185 | class ExampleWeightedCandidateSet : RuleCandidatesProvider, RuleCandidateSelector { 186 | 187 | // required for RuleCandidatesProvider 188 | let candidates: [String] 189 | 190 | let runningWeights: [(total:Int, target:Int)] 191 | let totalWeights: UInt32 192 | 193 | init(_ distribution:[String:Int]) { 194 | distribution.values.map { $0 }.forEach { 195 | assert($0 > 0, "weights must be positive") 196 | } 197 | let weightedCandidates = distribution 198 | .map { ($0, $1) } 199 | candidates = weightedCandidates 200 | .map { $0.0 } 201 | runningWeights = weightedCandidates 202 | .map { $0.1 } 203 | .scan(0, +) 204 | .enumerated() 205 | .map { ($0.element, $0.offset) } 206 | totalWeights = distribution 207 | .values 208 | .map { $0 } 209 | .reduce(0) { $0.0 + UInt32($0.1) } 210 | } 211 | 212 | // required for RuleCandidateSelector 213 | func pick(count: Int) -> Int { 214 | let choice = Int(arc4random_uniform(totalWeights) + 1) // since running weight start at 1 215 | for weight in runningWeights { 216 | if choice <= weight.total { 217 | return weight.target 218 | } 219 | } 220 | // we will never reach here 221 | fatalError() 222 | } 223 | 224 | } 225 | 226 | t = Tracery {[ 227 | "option": ExampleWeightedCandidateSet(["a": 5, "b": 1]) 228 | ]} 229 | 230 | t.add(modifier: "track") { input in 231 | let count = tracker[input] ?? 0 232 | tracker[input] = count + 1 233 | return input 234 | } 235 | 236 | runOptionRule(times: 100, header: "custom weighted") 237 | 238 | // sample output, will vary by run 239 | // b 13 240 | // a 87 241 | // as before, option b is 5 times 242 | // more likely to chosen over a 243 | 244 | /*: 245 | By providing a custom cadidate provider as the expansion to a rule, we can get total control over listing candidates. In this implementation, instead of repeating `a` 5 times as before, we just need to specify `a` once, along with its intended weight for the overall distribution. 246 | 247 | This provides a very powerful mechanism to control what candidates are available for a rule to expand on. For example, you could write a custom cadidate provider that presents 50 candidates based on whether an external condition is met, or 100 otherwise. The possibilities are endless. 248 | 249 | In summary 250 | 251 | * Use _modifiers_ and _methods_ to modify the expanded rule 252 | * Custom *candidate selectors* can be specified at a rule level to control how rules get expanded 253 | * Using a custom *candidates provider* can give you more control over the expansion possibilities of a rule 254 | 255 | */ 256 | 257 | 258 | //: [Advanced contd.](@next) 259 | -------------------------------------------------------------------------------- /Common/Lexer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lexer.swift 3 | // Tracery 4 | // 5 | // Created by Benzi on 10/03/17. 6 | // Copyright © 2017 Benzi Ahamed. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK:- Lexical Analysis 12 | 13 | enum Token : CustomStringConvertible { 14 | 15 | case text(String) 16 | case op(String) 17 | case keyword(String) 18 | case number(Int) 19 | 20 | var rawText: String { 21 | switch self { 22 | case let .text(text): return text 23 | case let .op(c): return c 24 | case let .keyword(text): return text 25 | case let .number(value): return "\(value)" 26 | } 27 | } 28 | 29 | var description: String { 30 | switch self { 31 | case let .text(text): return "txt(\(text))" 32 | case let .op(c): return "op(\(c))" 33 | case let .keyword(text): return "key(\(text.uppercased()))" 34 | case let .number(value): return "num(\(value))" 35 | } 36 | } 37 | 38 | static let LEFT_SQUARE_BRACKET = Token.op("[") 39 | static let RIGHT_SQUARE_BRACKET = Token.op("]") 40 | static let COLON = Token.op(":") 41 | static let HASH = Token.op("#") 42 | static let COMMA = Token.op(",") 43 | static let DOT = Token.op(".") 44 | static let LEFT_ROUND_BRACKET = Token.op("(") 45 | static let RIGHT_ROUND_BRACKET = Token.op(")") 46 | static let LEFT_CURLY_BRACKET = Token.op("{") 47 | static let RIGHT_CURLY_BRACKET = Token.op("}") 48 | 49 | static let EQUAL_TO = Token.op("==") 50 | static let NOT_EQUAL_TO = Token.op("!=") 51 | 52 | static let KEYWORD_IF = Token.keyword("if") 53 | static let KEYWORD_THEN = Token.keyword("then") 54 | static let KEYWORD_ELSE = Token.keyword("else") 55 | static let KEYWORD_WHILE = Token.keyword("while") 56 | static let KEYWORD_DO = Token.keyword("do") 57 | static let KEYWORD_IN = Token.keyword("in") 58 | static let KEYWORD_NOT_IN = Token.keyword("not in") 59 | 60 | static let SPACE = Token.text(" ") 61 | 62 | var isConditionalOperator: Bool { 63 | return self == .EQUAL_TO 64 | || self == .NOT_EQUAL_TO 65 | || self == .KEYWORD_IN 66 | || self == .KEYWORD_NOT_IN 67 | } 68 | } 69 | 70 | extension Token : Equatable { } 71 | 72 | func ==(lhs: Token, rhs: Token) -> Bool { 73 | switch (lhs, rhs) { 74 | case let (.op(lhs), .op(rhs)): return lhs == rhs 75 | case let (.text(lhs), .text(rhs)): return lhs == rhs 76 | case let (.keyword(lhs), .keyword(rhs)): return lhs == rhs 77 | case let (.number(lhs), .number(rhs)): return lhs == rhs 78 | default: return false 79 | } 80 | } 81 | 82 | extension Character { 83 | var isReserved: Bool { 84 | switch self { 85 | case "[","]",":","#",",","(",")",".","{","}": return true 86 | default: return false 87 | } 88 | } 89 | } 90 | 91 | struct Lexer { 92 | 93 | static func tokens(_ input: String) -> [Token] { 94 | 95 | var index = input.startIndex 96 | var tokens = [Token]() 97 | 98 | func advance() { 99 | input.formIndex(after: &index) 100 | } 101 | 102 | func rewind(count: Int) { 103 | input.formIndex(&index, offsetBy: -count) 104 | } 105 | 106 | var current: Character? { 107 | return index < input.endIndex ? input[index] : nil 108 | } 109 | 110 | var lookahead: Character? { 111 | let next = input.index(after: index) 112 | return next < input.endIndex ? input[next] : nil 113 | } 114 | 115 | func getEscapedCharacter() -> Character? { 116 | if current == "\\" { 117 | advance() 118 | if let c = current { 119 | advance() 120 | return c 121 | } 122 | } 123 | return nil 124 | } 125 | 126 | func getToken() -> Token? { 127 | guard let c = current else { return nil } 128 | 129 | // two character operators 130 | 131 | if let next = lookahead { 132 | let token: Token? 133 | switch (c, next) { 134 | case ("=","="): token = .op("==") 135 | case ("!","="): token = .op("!=") 136 | default: token = nil 137 | } 138 | if token != nil { 139 | advance() 140 | advance() 141 | return token 142 | } 143 | } 144 | 145 | 146 | // everything else 147 | 148 | switch c { 149 | 150 | case let x where x.isReserved: 151 | advance() 152 | return .op("\(c)") 153 | 154 | case let x where "0"..."9" ~= x: 155 | var number = "" 156 | while let c = current, "0"..."9" ~= c { 157 | number.append(c) 158 | advance() 159 | } 160 | guard let value = Int(number) else { return .text(number) } 161 | return .number(value) 162 | 163 | case let c where c == " ": 164 | advance() 165 | return Token.SPACE 166 | 167 | default: 168 | var text = "" 169 | while let c = current, !c.isReserved { 170 | 171 | // if next batch is a dual character op 172 | // return immediately, the op will be consumed 173 | // in the next call 174 | if !text.isEmpty, let next = lookahead { 175 | switch (c, next) { 176 | case ("=","="), ("!","="): 177 | return .text(text) 178 | default: 179 | break 180 | } 181 | } 182 | 183 | // escape sequences 184 | if c == "\\" { 185 | if let c = getEscapedCharacter() { 186 | text.append(c) 187 | } 188 | return .text(text) 189 | } 190 | 191 | // consume current character 192 | text.append(c) 193 | advance() 194 | 195 | // key word check needs to be performed 196 | // only if we have consumed at least 3 characters 197 | guard text.count >= 3 else { continue } 198 | 199 | // check if we greedily consumed a keyword 200 | // keywords must be preceded by white space 201 | // unless its if or while, in which case 202 | // it must be preceded by [ 203 | // all keywords must be followed by a space 204 | let keywords = [ 205 | "if ", 206 | "then ", 207 | "else ", 208 | "while ", 209 | "do ", 210 | "not in ", 211 | "in ", 212 | ] 213 | for keyword in keywords { 214 | 215 | // check if we have consumed at least x character 216 | // as the keyword 217 | guard let prevCharIndex = input.index(index, offsetBy: -keyword.count-1, limitedBy: input.startIndex) else { continue } 218 | let prevChar = input[prevCharIndex] 219 | 220 | if text == keyword { 221 | if prevChar == " " { 222 | rewind(count: 1) 223 | return .keyword(text.trim(fromEnd: 1)) 224 | } 225 | if prevChar == "[", keyword == "if " || keyword == "while " { 226 | rewind(count: 1) 227 | return .keyword(text.trim(fromEnd: 1)) 228 | } 229 | 230 | } 231 | else if text.hasSuffix(keyword), prevChar == " " { 232 | let end = text.index(text.endIndex, offsetBy: -keyword.count) 233 | rewind(count: keyword.count) 234 | return .text(String(text[.. String { 258 | return String(self[.. String { 261 | return String(self[..() 23 | 24 | func getVertex(_ name: String) -> Vertex { 25 | func createVertex(name: String) -> Vertex { 26 | let v = Vertex(name) 27 | vLookup[v.rule] = v 28 | g.addVertex(v) 29 | return v 30 | } 31 | return vLookup[name] ?? createVertex(name: name) 32 | } 33 | 34 | func addEdge(_ v: Vertex, _ w: Vertex) { 35 | // add unique edges only 36 | if g.edges.contains(where: { $0.0 == v.graphIndex && $0.1 == w.graphIndex }) { 37 | return 38 | } 39 | g.addEdge(v, w) 40 | } 41 | 42 | 43 | func addRuleDependency(from vertex: Vertex, to condition: ParserCondition) { 44 | condition.lhs.forEach { 45 | addRuleDependency(from: vertex, to: $0) 46 | } 47 | condition.rhs.forEach { 48 | addRuleDependency(from: vertex, to: $0) 49 | } 50 | } 51 | 52 | func addRuleDependency(from vertex: Vertex, to parserNode: ParserNode) { 53 | switch parserNode { 54 | case let .rule(name, mods): 55 | // rule may just contain mods 56 | if !name.isEmpty { 57 | addEdge(vertex, getVertex(name)) 58 | } 59 | // analyse mods further since 60 | // it may contain parameters 61 | // that are rules 62 | mods.forEach { mod in 63 | mod.parameters.forEach { param in 64 | param.nodes.forEach { node in 65 | addRuleDependency(from: vertex, to: node) 66 | } 67 | } 68 | } 69 | 70 | case let .tag(_, values): 71 | // process all values that a tag 72 | // can expand to 73 | values.forEach { value in 74 | value.nodes.forEach { node in 75 | addRuleDependency(from: vertex, to: node) 76 | } 77 | } 78 | 79 | case let .ifBlock(condition, thenBlock, elseBlock): 80 | addRuleDependency(from: vertex, to: condition) 81 | thenBlock.forEach { addRuleDependency(from: vertex, to: $0) } 82 | elseBlock?.forEach { addRuleDependency(from: vertex, to: $0) } 83 | 84 | case let .whileBlock(condition, doBlock): 85 | addRuleDependency(from: vertex, to: condition) 86 | doBlock.forEach { addRuleDependency(from: vertex, to: $0) } 87 | 88 | default: 89 | break 90 | } 91 | } 92 | 93 | func visit(rule: String, mapping: RuleMapping) { 94 | let v = getVertex(rule) 95 | for candidate in mapping.candidates { 96 | for node in candidate.value.nodes { 97 | addRuleDependency(from: v, to: node) 98 | } 99 | } 100 | } 101 | 102 | func end() { 103 | 104 | // cycles of length 2 is possible only for a loop with 1 vertex 105 | // this means that a rule references itself, since we are analysing 106 | // those separately, we exclude such cycles 107 | 108 | let cycles = g.findCycles().filter{ $0.count > 2 } 109 | guard cycles.count > 0 else { return } 110 | 111 | warn("cyclic references were detected in the following rules:") 112 | cycles.forEach { cycle in 113 | let layout = cycle.compactMap { $0.rule }.joined(separator: " -> ") 114 | warn(" \(layout)") 115 | } 116 | 117 | 118 | } 119 | 120 | } 121 | 122 | 123 | 124 | 125 | // MARK:- Graph Theory 126 | 127 | fileprivate protocol GraphIndexAddressable : class { 128 | var graphIndex: Int { get set } 129 | } 130 | 131 | fileprivate class Graph where Vertex: GraphIndexAddressable { 132 | var vertices = [Vertex]() 133 | var edges = [(u:Int, v:Int)]() 134 | } 135 | 136 | // MARK: vertices 137 | extension Graph { 138 | func addVertex(_ v: Vertex) { 139 | v.graphIndex = vertices.count 140 | vertices.append(v) 141 | } 142 | func addVertices(_ vertices: Vertex...) { 143 | for v in vertices { 144 | addVertex(v) 145 | } 146 | } 147 | func getVertex(index: Int) -> Vertex? { 148 | return vertices.first(where: { $0.graphIndex == index }) 149 | } 150 | } 151 | 152 | // MARK: edges 153 | extension Graph { 154 | func addEdge(_ v: Vertex, _ w: Vertex) { 155 | assert(v.graphIndex < vertices.count) 156 | assert(w.graphIndex < vertices.count) 157 | edges.append((v.graphIndex, w.graphIndex)) 158 | } 159 | func successors(of vertex: Vertex) -> [Int] { 160 | return edges.filter { $0.u == vertex.graphIndex }.map { $0.v } 161 | } 162 | } 163 | 164 | // MARK: functional 165 | extension Graph { 166 | func map(_ transform: (Vertex) -> T) -> Graph { 167 | let g = Graph() 168 | g.vertices = self.vertices.map { 169 | let v = transform($0) 170 | v.graphIndex = $0.graphIndex 171 | return v 172 | } 173 | g.edges = self.edges 174 | return g 175 | } 176 | func filter(_ isIncluded: (Vertex)->Bool) -> Graph { 177 | let g = Graph() 178 | g.vertices = self.vertices.filter(isIncluded) 179 | let allowedIndices = g.vertices.map { $0.graphIndex } 180 | g.edges = self.edges.filter { edge in 181 | return allowedIndices.contains(edge.u) && allowedIndices.contains(edge.v) 182 | } 183 | return g 184 | } 185 | } 186 | 187 | fileprivate class TarjanVertex : GraphIndexAddressable { 188 | var graphIndex: Int = -1 189 | let vertex: Vertex 190 | var index = -1 191 | var lowlink = -1 192 | var onStack = false 193 | init(vertex: Vertex) { 194 | self.vertex = vertex 195 | } 196 | } 197 | 198 | fileprivate class StronglyConnectedComponent where Vertex: GraphIndexAddressable { 199 | var vertices = [Vertex]() 200 | var leastGraphIndex: Int { 201 | return vertices.compactMap{ $0.graphIndex }.min()! 202 | } 203 | } 204 | 205 | // https://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm 206 | fileprivate struct TarjanAlgorithm { 207 | 208 | static func findStronglyConnectedComponents(graph: Graph) -> [StronglyConnectedComponent] { 209 | 210 | var index = 0 211 | var g = graph.map(TarjanVertex.init) 212 | var s = [TarjanVertex]() 213 | var components = [StronglyConnectedComponent]() 214 | 215 | func strongConnect(_ v: TarjanVertex) { 216 | 217 | // Set the depth index for v to the smallest unused index 218 | v.index = index 219 | v.lowlink = index 220 | index += 1 221 | 222 | s.append(v) 223 | v.onStack = true 224 | 225 | // Consider successors of v 226 | for w in g.successors(of: v).compactMap({ g.getVertex(index: $0) }) { 227 | 228 | if w.index == -1 { 229 | // Successor w has not yet been visited; recurse on it 230 | strongConnect(w) 231 | v.lowlink = min(v.lowlink, w.lowlink) 232 | } 233 | else if w.onStack { 234 | // Successor w is in stack S and hence in the current SCC 235 | v.lowlink = min(v.lowlink, w.lowlink) 236 | } 237 | } 238 | 239 | // If v is a root node, pop the stack and generate an SCC 240 | if (v.lowlink == v.index) { 241 | // start a new strongly connected component 242 | let scc = StronglyConnectedComponent() 243 | while true { 244 | let w = s.removeLast() 245 | w.onStack = false 246 | scc.vertices.append(w.vertex) 247 | if w.graphIndex == v.graphIndex { 248 | break 249 | } 250 | } 251 | // output the current strongly connected component 252 | components.append(scc) 253 | } 254 | } 255 | 256 | for v in g.vertices { 257 | if v.index == -1 { 258 | strongConnect(v) 259 | } 260 | } 261 | 262 | return components 263 | 264 | } 265 | } 266 | 267 | extension Graph { 268 | func findStronglyConnectedComponents() -> [StronglyConnectedComponent] { 269 | return TarjanAlgorithm.findStronglyConnectedComponents(graph: self) 270 | } 271 | } 272 | 273 | 274 | // http://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF 275 | fileprivate struct JohnsonCircuitFindingAlgorithm { 276 | 277 | struct Blist { 278 | var items = [Int]() 279 | init() { } 280 | mutating func add(item: Int) { 281 | items.append(item) 282 | } 283 | mutating func remove(item: Int) { 284 | if let i = items.index(where: {$0 == item}) { 285 | items.remove(at: i) 286 | } 287 | } 288 | } 289 | 290 | static func findCycles(graph: Graph) -> [[Vertex]] { 291 | var cycles = [[Vertex]]() 292 | var B = [Blist](repeating: Blist(), count: graph.vertices.count) 293 | var blocked = [Bool](repeatElement(false, count: graph.vertices.count)) 294 | var s = 0 295 | var stack = [Int]() 296 | func circuit(_ v: Int) -> Bool { 297 | func unblock(_ u: Int) { 298 | blocked[u] = false 299 | for w in B[u].items { 300 | B[u].remove(item: w) 301 | if blocked[w] { 302 | unblock(w) 303 | } 304 | } 305 | } 306 | var f = false 307 | stack.append(v) 308 | blocked[v] = true 309 | for w in graph.successors(of: graph.getVertex(index: v)!) { 310 | if w == s { 311 | let first = graph.getVertex(index: stack[0])! 312 | cycles.append(stack.compactMap { graph.getVertex(index: $0) } + [first]) 313 | f = true 314 | } 315 | else if blocked[w] { 316 | f = circuit(w) 317 | } 318 | } 319 | if f { 320 | unblock(v) 321 | } 322 | else { 323 | for w in graph.successors(of: graph.getVertex(index: v)!) { 324 | if !B[w].items.contains(v) { 325 | B[w].add(item: v) 326 | } 327 | } 328 | } 329 | let unstacked = stack.removeLast() 330 | assert(unstacked == v) 331 | return f 332 | } 333 | 334 | s = 0 335 | while s < graph.vertices.count { 336 | // adjacency structure of strong component K with least 337 | // vertex in subgraph of G induced by {s, s+ 1, n}; 338 | let sccs = graph.filter({ $0.graphIndex >= s }).findStronglyConnectedComponents() 339 | guard let K = sccs.min(by: { $0.leastGraphIndex < $1.leastGraphIndex }) else { 340 | break 341 | } 342 | s = K.leastGraphIndex 343 | for v in K.vertices { 344 | let i = v.graphIndex 345 | blocked[i] = false 346 | B[i].items.removeAll() 347 | } 348 | _ = circuit(s) 349 | s = s + 1 350 | } 351 | 352 | return cycles 353 | } 354 | } 355 | 356 | extension Graph { 357 | func findCycles() -> [[Vertex]] { 358 | return JohnsonCircuitFindingAlgorithm.findCycles(graph: self) 359 | } 360 | } 361 | 362 | 363 | 364 | 365 | --------------------------------------------------------------------------------