diff --git a/src/visitor.js b/src/visitor.js index a9305fc..dc540ed 100644 --- a/src/visitor.js +++ b/src/visitor.js @@ -62,12 +62,12 @@ class BaseVisitor { class RiScriptVisitor extends BaseVisitor { constructor(riScript, context = {}) { super(riScript); - this.context = context; this.order = 0; this.trace = 0; this.indent = 0; this.choices = {}; + this.context = context; this.isNoRepeat = false; this.Symbols = this.scripting.Symbols; @@ -624,45 +624,6 @@ class RiScriptVisitor extends BaseVisitor { return value; } - // value is not yet resolved, so store with transform for later - restoreTransforms(value, txs) { - if (typeof value === 'string') { - const choiceRE = new RegExp('^' + this.Escaped.OPEN_CHOICE + '.*' + this.Escaped.CLOSE_CHOICE + '$'); - const symbolRE = new RegExp(`(${this.Escaped.DYNAMIC}|${this.Escaped.STATIC}[A-Za-z_0-9])[A-Za-z_0-9]*`); - if (!choiceRE.test(value) && !symbolRE.test(value)) { - // wrap in choice to preserve - value = this.Symbols.OPEN_CHOICE + value + this.Symbols.CLOSE_CHOICE; - } - if (txs) { - txs.forEach((tx) => (value += tx.image)); // append transform strings - } - if (this.traceTx) console.log('restoreTransforms:', value); - } - return value; - } - - castValues(obj) { - let madeCast = false; - Object.entries(obj).forEach(([k, v]) => { - const num = parseFloat(v); - if (!isNaN(num)) { - madeCast = true; - obj[k] = num; // update object with casted value - } - }); - return madeCast; - } - - contextIsResolved(table) { - let allResolved = true; - Object.entries(table).forEach(([key, val]) => { - if (!this.scripting.isParseable(val)) { - allResolved = false; - } - }); - return allResolved; - } - applyTransform(target, transform) { const image = transform.image; @@ -675,19 +636,19 @@ class RiScriptVisitor extends BaseVisitor { // function in dynamics if (typeof this.dynamics[tx] === 'function') { - result = this.dynamics[tx].call(this.scripting, target); + result = this.dynamics[tx].bind(this.context)(target); } // function in statics else if (typeof this.statics[tx] === 'function') { - result = this.statics[tx].call(this.scripting, target); + result = this.statics[tx].call(this.context, target); } // function in context else if (typeof this.context[tx] === 'function') { - result = this.context[tx].call(this.scripting, target); + result = this.context[tx].call(this.context, target); } // function in transforms else if (typeof this.scripting.transforms[tx] === 'function') { - result = this.scripting.transforms[tx].call(this.scripting, target); + result = this.scripting.transforms[tx].call(this.context, target); } // member functions (usually on String) else if (typeof target[tx] === 'function') { @@ -712,6 +673,45 @@ class RiScriptVisitor extends BaseVisitor { return result; } + // value is not yet resolved, so store with transform for later + restoreTransforms(value, txs) { + if (typeof value === 'string') { + const choiceRE = new RegExp('^' + this.Escaped.OPEN_CHOICE + '.*' + this.Escaped.CLOSE_CHOICE + '$'); + const symbolRE = new RegExp(`(${this.Escaped.DYNAMIC}|${this.Escaped.STATIC}[A-Za-z_0-9])[A-Za-z_0-9]*`); + if (!choiceRE.test(value) && !symbolRE.test(value)) { + // wrap in choice to preserve + value = this.Symbols.OPEN_CHOICE + value + this.Symbols.CLOSE_CHOICE; + } + if (txs) { + txs.forEach((tx) => (value += tx.image)); // append transform strings + } + if (this.traceTx) console.log('restoreTransforms:', value); + } + return value; + } + + castValues(obj) { + let madeCast = false; + Object.entries(obj).forEach(([k, v]) => { + const num = parseFloat(v); + if (!isNaN(num)) { + madeCast = true; + obj[k] = num; // update object with casted value + } + }); + return madeCast; + } + + contextIsResolved(table) { + let allResolved = true; + Object.entries(table).forEach(([key, val]) => { + if (!this.scripting.isParseable(val)) { + allResolved = false; + } + }); + return allResolved; + } + lookupsToString() { const dyns = {}, stats = {}; Object.entries(this.dynamics || {}).forEach(([k, v]) => (dyns[`$${k}`] = v)); diff --git a/test/grammar.tests.js b/test/grammar.tests.js index e1672a6..81b9420 100644 --- a/test/grammar.tests.js +++ b/test/grammar.tests.js @@ -19,7 +19,7 @@ describe(title, function () { }); describe('Characters', function () { - it('Handles character choice in context', function () { + it('Handles character choice in context', function () { let context, script, res; // simple case @@ -116,32 +116,98 @@ describe(title, function () { expect(res).to.be.oneOf(['Lucy', 'Sam']); }); - LTR && it('Should reference context from transforms', function () { - const context = { - characters: [{ - name: 'Lucy', - pronoun: 'she', - car: 'Acura' + it('Should reference context from transforms', function () { + let ctx, rules, res; + ctx = { + prop: 42, + func: function () { + return this.prop; }, - { - name: 'Sam', - pronoun: 'he', - car: 'Subaru' - }], - chooseChar: function () { - console.log('chooseChar:', typeof this); - let chars = this.visitor.context.characters; - return chars[Math.floor(Math.random() * chars.length)]; + }; + rules = { start: "$.func" } + res = RiGrammar.expand(rules, ctx); + expect(res).eq('42'); + + ctx = { + prop: { num: 42 }, + func: function () { + return this.prop.num; + }, + }; + rules = { start: "$.func" } + res = RiGrammar.expand(rules, ctx); + expect(res).eq('42'); + + ctx = { + prop: { num: 42 }, + func: function () { + return this.prop; + }, + }; + rules = { start: "$.func.num" } + res = RiGrammar.expand(rules, ctx); + expect(res).eq('42'); + + ctx = { + char: { name: 'bill', age: 42 }, + func: function () { + return this.char; + }, + }; + rules = { start: "{$chosen=$.func} $chosen.name is $chosen.age years old" } + res = RiGrammar.expand(rules, ctx); + expect(res).eq('bill is 42 years old'); + + ctx = { + chars: [{ name: 'bill', age: 42 }, { name: 'dave', age: 43 }], + func: function () { + return this.chars[Math.floor(Math.random() * this.chars.length)]; + }, + }; + rules = { start: "{#chosen=$.func} $chosen.name is $chosen.age years old" } + res = RiGrammar.expand(rules, ctx); + expect(res).to.be.oneOf([ + 'bill is 42 years old', + 'dave is 43 years old', + ]); + + ctx = { + chars: [{ name: 'bill', age: 42 }, { name: 'dave', age: 43 }], + func: function () { + return this.chars[Math.floor(Math.random() * this.chars.length)]; + }, + }; + rules = { start: "{#chosen=$.func} $sent", sent: "$chosen.name is $chosen.age years old" } + res = RiGrammar.expand(rules, ctx); + expect(res).to.be.oneOf([ + 'bill is 42 years old', + 'dave is 43 years old', + ]); + + ctx = { + characters: [ + { + name: 'Lucy', + pronoun: 'she', + car: 'Acura' + }, + { + name: 'Sam', + pronoun: 'he', + car: 'Subaru' + } + ], + subject: function () { + return this.characters[Math.floor(Math.random() * this.characters.length)]; } } - const rules = { - start: "{$person=$chooseChar()} $sentence", - sentence: "$Meet $person.name. $person.pronoun.cap() drives $person.car", - "#person": "$sam | $lucy" + rules = { + start: "{#person=$.subject()} $sentence", + sentence: "Meet $person.name. $person.pronoun.cap() drives $person.car.art().", } - let result = RiGrammar.expand(rules, context); - expect(result).to.be.oneOf([ - 'Meet Lucy. She drives a Acura.', + res = RiGrammar.expand(rules, ctx); + expect(res).to.be.oneOf([ + 'Meet Lucy. She drives an Acura.', 'Meet Sam. He drives a Subaru.', ]); }); diff --git a/test/riscript.tests.js b/test/riscript.tests.js index eea148d..811f52e 100644 --- a/test/riscript.tests.js +++ b/test/riscript.tests.js @@ -987,10 +987,10 @@ describe(title, function () { }); - it('Pass scripting as this', function () { + it('Pass context as this', function () { let checkThis = function (word) { - expect(this).eq(riscript); - return word + (this == riscript ? ' success' : ' failure'); + expect(this).eq(riscript.visitor.context); + return word + (this === riscript.visitor.context ? ' success' : ' failure'); } let res = riscript.evaluate('[hello].checkThis', { checkThis }); expect(res).eq('hello success'); @@ -1120,7 +1120,7 @@ describe(title, function () { expect(res).eq('The dog.rhymes'); let addRhyme2 = function (word) { - return word + ' rhymes with bog' + this.RiTa.randi(1); + return word + ' rhymes with bog' + riscript.RiTa.randi(1); } expect(riscript.transforms.rhymes2).is.undefined; riscript.addTransform('rhymes2', addRhyme2);