addons/customParser/CustomParser.js

require("regexp-match-indices/auto");

/**
* CustomParser class for parsing scripts.
* @extends ParserBase
*/
class CustomParser extends require('www/js/ParserBase.js').ParserBase {
    /**
    * Creates an instance of CustomParser.
    * @param {string} script - The script to parse.
    * @param {object} options - Options for parsing.
    * @param {object} options.modelStr - Model string.
    * @param {object} options.model - Model object.
    * @param {function} callback - Callback function.
    */
    constructor(script, options, callback) {
        super(script, options, callback)

        this.modelStr   = options.modelStr || {};

        /**
         * The model data.
         * @type {object}
         */ 
        this.model      = options.model || {};
        //this.debugLevel = this.options.debugLevel || common.debugLevel();
        this.debugLevel = 0
        this.parsedData = [];

        /**
         * The transData object.
         * @type {object}
         * @property {Array} data - The data array.
         * @property {Array} context - The context array.
         * @property {Array} tags - The tags array.
         * @property {Array} parameters - The parameters array.
         * @property {object} indexIds - The indexIds object.
        */
        this.transData  = {
            data:[],
            context:[],
            tags:[],
            parameters:[],
            indexIds:{}
        };
        this.$elm = $("<div></div>");
        
    }
}
/**
* Event subscription method.
* @param {string} evt - Event name.
* @param {function} fn - Event handler function.
*/
CustomParser.prototype.on = function(evt, fn) {
    this.$elm.on(evt, fn)
}

/**
* Event unsubscription method.
* @param {string} evt - Event name.
* @param {function} fn - Event handler function.
*/
CustomParser.prototype.off = function(evt, fn) {
    this.$elm.off(evt, fn)
}

/**
* Event subscription method for a single occurrence.
* @param {string} evt - Event name.
* @param {function} fn - Event handler function.
*/
CustomParser.prototype.one = function(evt, fn) {
    this.$elm.one(evt, fn)
}

/**
* Triggers an event.
* @param {string} evt - Event name.
* @param {*} param - Event parameter.
*/
CustomParser.prototype.trigger = function(evt, param) {
    this.$elm.trigger(evt, param)
}

/**
* Sets the model.
* @param {object} model - Model object.
* @returns {object} The set model.
*/
CustomParser.prototype.setModel = function(model) {
    this.model = model || {};
    return this.model;
}

/**
* Gets the model.
* @returns {object} The model.
*/
CustomParser.prototype.getModel = function() {
    return this.model;
}

/**
* Parses a capture group.
* @param {string|Array} str - The string or array to parse.
* @returns {Array} The parsed capture group.
*/
CustomParser.prototype.parseCaptureGroup = function(str) {
    if (Array.isArray(str)) return str;
    if (typeof str !== "string") return [0];
    var result = [];
    
    str = str.replace(/\s+/g, '').split(",");
    for (var i in str) {
        result.push(parseInt(str[i]));
    }
    return result;
}

/**
* Parses the script.
*/
CustomParser.prototype.parse = async function() {
    console.log("Parsing with model", this.model);
    if (empty(this.model)) return console.warn("Model is not defined");
    if (empty(this.model.rules)) return console.warn("model.rules is not defined");
    var theString       = this.script;
    
    var mask = (string, start, end, maskChar = " ") => {
        var prev = string.substring(0, start);
        var after = string.substring(end, string.length);
        var mask = maskChar.repeat(string.substring(start, end).length);
        return prev+mask+after;
    }
    
    var evalRegex = (string) => {
        if (string.constructor.name == "RegExp") return string;
        return common.evalRegExpStr(string);
    }
    
    var evalAsyncFunction = (string) => {
        if (typeof string == "function") return string;
        try {
            let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
            return new AsyncFunction("text", "thisModel", string);
            //return new Function("text", "thisModel", string);
        } catch (e) {
            console.warn(e);
            return function() {};
        }
    }
    
    var isValidOffsetPair = (obj) => {
        try {
            if ("start" in obj && "end" in obj) return true;
        } catch (e) {
            return false;
        }
        return false;
    }
    
    var processHooks = async () => {
        if (!this.model) return;
        if (!empty(this.model.toGrid)) {
            if (typeof this.model.toGrid == "function") this.filterText = this.model.toGrid;
            try {
                var filterText = new Function('text', 'context', 'parameters', this.model.toGrid);
                this.filterText = filterText;
            } catch (e) {
                // do nothing
            }
        }
        if (!empty(this.model.fromGrid)) {
            if (typeof this.model.fromGrid == "function") this.unfilterText = this.model.fromGrid;
            try {
                var unfilterText = new Function('text', 'context', 'parameters', 'info', this.model.fromGrid);
                this.unfilterText = unfilterText;
            } catch (e) {
                // do nothing
            }
            
        }
    }
    
    /**
    * Process the rule
    * @param  {} thisRule
    * @param  {} initialOffset
    */
    var processRule = async (thisRule, initialOffset) => {
        initialOffset = initialOffset || 0;
        // var maxIteration = 999;
        console.log("fetchingRules", thisRule);
        if (thisRule.type == "regex") {
            if (!thisRule.pattern) return;
            var thisPattern = evalRegex(thisRule.pattern);
            console.log("pattern evaluated", thisPattern);
            var captureGroups = this.parseCaptureGroup(thisRule.captureGroups) || [0];
            if (Array.isArray(captureGroups) == false) captureGroups = [captureGroups];
            if (this.debugLevel) console.log("Capture groups:", captureGroups);
            // var iteration=0;
            var lastOffsetPair = "";
            var matchAllResult = theString.matchAll(thisPattern);
            // javascript exec can lock us in infinite loop when encouter a blank match
            //while((matches=thisPattern.exec(theString)) != null) {
            for (var matches of matchAllResult) {    
                if (this.debugLevel) console.log("Matches", matches);
                // iterate through all selected groups
                for (var x=0; x<captureGroups.length; x++) {
                    //console.log("Handling capture index", captureGroups[x]);
                    var captureIndex    = parseInt(captureGroups[x] || 0); // default is index 0
                    if (!matches.indices[captureIndex]) continue;
                    var offsetStart     = initialOffset + matches.indices[captureIndex][0]
                    var offsetEnd       = initialOffset + matches.indices[captureIndex][1]
                    if (this.debugLevel) console.log("offsetStart", offsetStart, "offsetEnd", offsetEnd);
                    if (!empty(thisRule.innerRule)) {
                        // instead of pushing the result, process inner pattern
                        await processRule(thisRule.innerRule, offsetStart);
                    } else if (thisRule.action == "mask") {
                        theString = mask(theString, offsetStart, offsetEnd);
                    } else if (thisRule.action == "captureMask") {
                        this.parsedData.push({
                            translation     : this.registerString(matches[captureIndex], ["start", offsetStart, "end", offsetEnd], {start:offsetStart, end:offsetEnd}),
                            start           : offsetStart,
                            end             : offsetEnd
                        });
                        theString = mask(theString, offsetStart, offsetEnd);
                    } else {
                        this.parsedData.push({
                            translation     : this.registerString(matches[captureIndex], ["start", offsetStart, "end", offsetEnd], {start:offsetStart, end:offsetEnd}),
                            start           : offsetStart,
                            end             : offsetEnd
                        });
                    }
                    
                    var currentOffsetPair = offsetStart+","+offsetEnd
                    if (lastOffsetPair == currentOffsetPair) {
                        // circular reference
                        console.warn(`Last offset ${lastOffsetPair} is same with the current offset ${currentOffsetPair}. To prevent circular refference the parser will mask the ofset character`);
                        
                        theString = mask(theString, offsetStart-1, offsetEnd+1);
                    }
                    lastOffsetPair = currentOffsetPair;
                    
                }
                /*
                iteration++;
                if (iteration > maxIteration) {
                    console.error(`Process halted! Iteration surpassed maximum allowed iteration. ${maxIteration}`);
                    break;
                }
                */
            }
        } else if (thisRule.type == "function") {
            if (!thisRule.function) return;
            var thisFunction = evalAsyncFunction(thisRule.function);
            console.log("calling function");
            var result = await thisFunction.call(this, theString, thisRule);
            console.log("result of the execution:", result);
            if (empty(result)) return;
            if (Array.isArray(result) == false) result = [result];
            for (var r in result) {
                var offsetPair = result[r];
                if (!isValidOffsetPair(offsetPair)) return;
                this.parsedData.push({
                    translation     : this.registerString(theString.substring(offsetPair.start, offsetPair.end), ["start", offsetPair.start, "end", offsetPair.end], {start:offsetPair.start, end:offsetPair.end}),
                    start           : offsetPair.start,
                    end             : offsetPair.end
                });
            }
        }
    }
    
    await processHooks();
    for (var i in this.model.rules) {
        await processRule(this.model.rules[i], 0);
    }
    
    if (this.debugLevel) console.log("String after parsed:");
    if (this.debugLevel) console.log(theString);
}

/**
* Converts CustomParser instance to string.
* @returns {string} The string representation of the instance.
*/
CustomParser.prototype.toString = function() {
    if (empty(this.parsedData)) return this.script;
    
    var replaceOffset = function(string, start, end, replacement) {
        replacement = replacement || ""
        var before = string.substring(0, start);
        var after  = string.substring(end, string.length);
        return before+replacement+after;
    }
    
    var thisScript = this.script;
    this.writableData = this.writableData || [];
    
    for (var i=0; i<this.parsedData.length; i++) {
        if (!this.writableData[i]) continue;
        this.parsedData[i].translation = this.writableData[i];
    }
    
    // IMPORTANT! sort by start offset DESC
    this.parsedData.sort((a,b) => {return b.start - a.start});
    
    for (let i=0; i<this.parsedData.length; i++) {
        thisScript = replaceOffset(thisScript, this.parsedData[i].start, this.parsedData[i].end, this.parsedData[i].translation);
    }
    
    return thisScript;
}

/**
* Checks if the provided model is valid.
* @param {object|string} model - The model object or JSON string.
* @returns {boolean} True if the model is valid, otherwise false.
*/
CustomParser.isValidModel = function(model) {
    if (typeof model == "string") {
        if (!common.isJSON(model)) return false;
        model = JSON.parse(model);
    }
    if (!model) return false;
    if (!model.files) return false;
    if (Array.isArray(model.files) == false ) return false;
    return true;
}



module.exports = CustomParser