galaxykate / tracery

Tracery: a story-grammar generation library for javascript
Apache License 2.0
2.11k stars 246 forks source link

Some idea on how to implement the parameters for modifiers #52

Open 4skinSkywalker opened 2 years ago

4skinSkywalker commented 2 years ago

Hi, I've fiddled with a piece of code which is similar to yours in terms of functionalities.

I've distinguished allocation from interpolation:

Allocation: [[ key = {{ value | modifier : parameter }} ]] Interpolation: {{ value | modifier : parameter }}

As you can see the allocation can also contains an interpolation, but it's not necessary when you have to pop, to do that: [[ key = POP ]]

With this syntax is possible to chain modifier with their parameter: {{ value | modifier1 | modifier2 : param1 | modifier3 : param1 : param2 }} In the example value is fed to modifier1 which has no params, the result is then fed into modifier2 which has a single param and all is ultimately fed into modifier3 which has a couple of params.

class Grammar {
    constructor() {

        this.rules = {};
        this.modifiers = {};
        this.variables = {};

        this.randomFn = Math.random;
    }

    // rule: "key -> value1 | value2 | value3"
    setRule(rule) {
        let validationRegex = /^ *[^->]+ * -> *[^->|]+( *\| *[^->| ]+[^->|]*)* *$/;
        if (!validationRegex.test(rule)) {
            console.error("Something wrong in your rule");
            return;
        }
        let [key, values] = rule.split(/ *-> */);
        values = values.split(/ *\| */);
        this.rules[key] = values;
    }

    setModifier(name, fn) {
        this.modifiers[name] = fn;
    }

    pickRandom(list) {
        return list[Math.floor(this.randomFn() * list.length)];
    }

    evaluate(expr) {
        let [value, ...modifiers] = expr.split(/ *\| */);
        if (this.variables[expr]) {
            return this.variables[expr][this.variables[expr].length - 1];
        }
        if (!this.rules[value]) {
           console.error("No rule found with the name: " + value);
           return;
        }
        value = this.pickRandom(this.rules[value]);
        if (!modifiers.length) {
            return value;
        }
        for (let modifier of modifiers) {
            let [modifierName, ...params] = modifier.split(/ *: */);
            if (!this.modifiers[modifierName]) {
                console.error("No modifier found with the name: " + modifierName);
                return;
            }
            value = this.modifiers[modifierName](value, ...params)
        }
        return value;
    }

    expand(text) {

        console.log(text); // Console each step

        let interpolation = " *{{ *([^|]+?( *\\| *([^: ]+( *: *[^: ]+)*) *?)*) *}} *";
        let allocation = ` *\\[\\[ *([^{}[\\]]+?) *=(${interpolation}| *POP *)\\]\\] *`;
        let interpolationRegex = new RegExp(`^${interpolation}$`);
        let allocationRegex = new RegExp(`^${allocation}$`);

        if (!(new RegExp(interpolation)).test(text) && !(new RegExp(allocation)).test(text)) {
            return text;
        }

        let result = text.replace(/(\[\[(.*?)\]\]|{{(.*?)}})/g, text => {

            if (allocationRegex.test(text)) {
                let [_, key, pop, expr] = text.match(allocationRegex);

                if (pop.trim() === "POP") {
                    if (!this.variables[key]) {
                        console.error("Nothing to pop at: " + key);
                        return;
                    }
                    this.variables[key].pop();
                    if (!this.variables[key].length) {
                        delete this.variables[key];
                    }
                    return "";
                }

                let value = this.evaluate(expr);

                if (!this.variables[key]) {
                    this.variables[key] = [];
                }
                this.variables[key].push(value);

                return value;
            }

            if (interpolationRegex.test(text)) {
                let expr = text.match(interpolationRegex)[1];
                return this.evaluate(expr);
            }

            console.error("Something wrong in your text");

        });

        this.expand(result);
    }
}

var g = new Grammar();

g.setRule("adj -> dark | stormy | beautiful");
g.setRule("noun -> {{adj}} night");

g.setModifier("discapitalize", (text, howMany) => {
    howMany = Number(howMany);
    let result = "";
    for (let i = 0; i < text.length; i++) {
        if (i < howMany) {
            result += text[i].toLowerCase();
        } else {
            result += text[i];
        }
    }
    return result;
});
g.setModifier("capitalize", (text, howMany) => {
    howMany = Number(howMany);
    let result = "";
    for (let i = 0; i < text.length; i++) {
        if (i < howMany) {
            result += text[i].toUpperCase();
        } else {
            result += text[i];
        }
    }
    return result;
});

g.expand("It was a [[ adjective = {{ adj | capitalize : 3 | discapitalize : 2 }} ]] and {{ adjective }} {{ noun }}");

// The above expansion will result in the following rounds:
// Step 0: It was a [[ adjective = {{ adj | capitalize : 3 | discapitalize : 2 }} ]] and {{ adjective }} {{ noun }}
// Step 1: It was a beAutiful and beAutiful {{adj}} night
// Step 2: It was a beAutiful and beAutiful dark night