├── .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 | 
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 |
--------------------------------------------------------------------------------