js/CodeRunner.js

/**=====LICENSE STATEMENT START=====
    Translator++ 
    CAT (Computer-Assisted Translation) tools and framework to create quality
    translations and localizations efficiently.
        
    Copyright (C) 2018  Dreamsavior<dreamsavior@gmail.com>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
=====LICENSE STATEMENT END=====*/
var die = function(text) {
    throw(`Process halted by user request --2472KAFSDFJ--:${text}`);
}

class BaseIterator {
    constructor(options) {
        this.options    = options || {};
        this.userInput  = this.options.userInput || {}
        if (window) window.die = die;
    }
}

BaseIterator.prototype.getObj = function() {
    return trans.getObjectById(this.file);
}


class CodeRunner extends require("www/js/BasicEventHandler.js") {
    constructor (options) {
        super();
        this.options    = options || {};
        this.editedCellPool = []
    }
}

CodeRunner.prototype.onExecutionStart = function() {
    this.editedCellPool = [];
    this.hasEditedCell = false;
}

CodeRunner.prototype.onCellEdit = function(file, row, col, value, previousValue) {
    console.log("Automation set a cell at:",file, row, col, value, previousValue);
    this.hasEditedCell = true;
    var data = {
        file:file,
        row:row,
        col:col,
        value:value,
        previousValue:previousValue
    }
    this.trigger("cellEdit", data)
    this.editedCellPool.push(data)
}

CodeRunner.prototype.finalize = async function(origin, options) {
    trans.grid.render();
    this.isBreak = false;
    this.trigger("executionEnd", {
        changedCells: this.editedCellPool,
        handler:origin
    })
    trans.trigger("codeEditorExecutionEnd", {
        changedCells: this.editedCellPool,
        handler:origin
    })
    if (this.hasEditedCell) {
        trans.refreshGrid();
        trans.evalTranslationProgress();
        this.hasEditedCell = false;
    }
    return true;
}

CodeRunner.prototype.executeGlobal = async function(fn, options) {
    this.onExecutionStart();

	fn = fn || async function() {};
	options= options || {};
    this.userInput = options.userInput || {}
    await fn.call(this);
    this.finalize("global", options)
	
	return true;
}

CodeRunner.prototype.executeObjectIterator = async function(fn, options) {
    var codeRunner = this;
    this.onExecutionStart();
	fn = fn || async function() {};
	options= options || {};

	var iteratorObj = new BaseIterator(options);

	var selectedFiles = trans.getCheckedFiles();
	if (selectedFiles.length < 1) selectedFiles = trans.getAllFiles();

	var iteratorMain = async function() {
        this.index      = -1;
        this.maxIndex   = selectedFiles.length - 1;
        this.isLast     = false;
        if (this.maxIndex < 0) this.maxIndex = 0;

		for (var fileId in selectedFiles) {
            this.index++;
			this.file = selectedFiles[fileId];
			if (!trans.project.files[this.file]) continue;
			if (empty(trans.project.files[this.file].data)) continue;

            if (this.index>=this.maxIndex) this.isLast = true;

            await fn.call(this);
            if (codeRunner.isBreak) return codeRunner.finalize("object", options);
		}
        codeRunner.finalize("object", options)
	}
	
	await iteratorMain.apply(iteratorObj);
	return true;
}

/**
 * Custom script for each rows
 * @param  {} fn
 * @param  {} options
 */
CodeRunner.prototype.executeRowIterator = async function(fn, options) {
    var codeRunner = this;
    this.onExecutionStart();

	fn = fn || async function() {};
	options= options || {};

    class IteratorRow extends BaseIterator {
        constructor( options) {
            super(options);

            this.getBestTranslationCol = function() {
                return trans.getTranslationColFromRow(this.cells);
            }

            this.getBestTranslation = function() {
                const bestCell = this.getBestTranslationCol();
                return this.cells[bestCell];
            }

            this.getBestTranslationCellInfo = function() {
                const bestCell = this.getBestTranslationCol();
                return this.cells[bestCell];
            }
            console.log("this obj", this);
        }
    }
	var iteratorObj = new IteratorRow(options);

	var selectedFiles = trans.getCheckedFiles();
	if (selectedFiles.length < 1) selectedFiles = trans.getAllFiles();



	var iteratorMain = async function() {
        // count the rows
        this.index      = -1;
        this.maxIndex   = 0;
        this.isLast     = false;
        for (let fileId in selectedFiles) {
            //console.log("length of", fileId, trans.getData(selectedFiles[fileId]).length);
            this.maxIndex += trans.getData(selectedFiles[fileId]).length;
        }
        if (this.maxIndex > 0) this.maxIndex = this.maxIndex-1; // because we start from 0;

		for (let fileId in selectedFiles) {
			this.file = selectedFiles[fileId];
			if (!trans.project.files[this.file]) continue;
			if (empty(trans.project.files[this.file].data)) continue;
			
			for (var rowId=0; rowId<trans.project.files[this.file].data.length; rowId++) {
                this.index++;
                //if (!trans.project.files[this.file].data[rowId][trans.keyColumn]) continue;
				this.rowId 		= rowId;
				this._cells 	= trans.project.files[this.file].data[rowId] || [];
                this.keyText 	= this._cells[trans.keyColumn];
                this.cells = new Proxy(this._cells, {
                    get: function(target, name) {
                        return target[name];
                    },
                    set: function(target, name, value) {
                        codeRunner.onCellEdit(this.file, rowId, name, value, target[name])
                        target[name] = value
                    }
                }); 
                this._tags 		= trans.project.files[this.file]?.tags[rowId] || [];
				this.tags 		= new Proxy(this._tags, {
                    get: (target, name) => {
                        return target[name];
                    },
                    set: (target, name, value) => {
                        target[name] = value
                        if (!Array.isArray(trans?.project?.files?.[this.file]?.tags)) return false;
                        trans.project.files[this.file].tags[rowId] = JSON.parse((JSON.stringify(target)));
                        return true;
                    }
                })
				this.parameters	= trans.project.files[this.file].parameters[rowId];
				this.context	= trans.project.files[this.file].context[rowId];
				this.comments   = trans.project.files[this.file].comments || undefined;
				
                if (this.index>=this.maxIndex) this.isLast = true;
                await fn.call(this);
                if (codeRunner.isBreak) return codeRunner.finalize("row", options);
			}
		}
        codeRunner.finalize("row", options)
	}
	
	await iteratorMain.apply(iteratorObj);
	return true;
}


CodeRunner.prototype.executeSelectedCells = async function(fn, options) {
    var codeRunner = this;
    this.onExecutionStart();

	fn = fn || async function() {};
	options= options || {};

    class IteratorSelectedCells extends BaseIterator {
        constructor(options) {
            super(options)
        }
    }
    IteratorSelectedCells.prototype.setText = function(text) {
        if (this.colId == 0) return;
        try {
            codeRunner.onCellEdit(this.file, this.rowId, this.colId, text, trans.project.files[this.file].data[this.rowId][this.colId])
            trans.project.files[this.file].data[this.rowId][this.colId] = text;
        } catch (e) {
            console.warn(`Error when trying to set the text on cell: ${this.file} ${this.rowId},${this.colId}\n`, e);
        }
    }

	var iteratorObj = new IteratorSelectedCells(options);

	var cellRanges = trans.grid.getSelectedRange();
	if (empty(cellRanges)) return;

	var iteratorMain = async function() {
        this.index      = -1;
        this.maxIndex   = 0;
        this.isLast     = false;
        for (let rangeId in cellRanges) {
            this.maxIndex += cellRanges[rangeId].getAll().length;
        }
        if (this.maxIndex > 0) this.maxIndex = this.maxIndex-1; // because we start from 0;

        this.file       = trans.getSelectedId();
        var fileId = this.file;

        if (!trans.project.files[this.file]) return;
        if (empty(trans.project.files[this.file].data)) return;

        var processed = {};

		for (let rangeId in cellRanges) {
            var thisCoords = cellRanges[rangeId].getAll();
            for (var i=0; i<thisCoords.length; i++) {
                this.index++;
                var rowId = thisCoords[i].row;
                var colId = thisCoords[i].col;
                if (processed[rowId+","+colId]) continue; // already processed
                processed[rowId+","+colId] = true;

                this.rowId 		= rowId;
				this._cells 	= trans.project.files[this.file].data[rowId] || [];
                this.keyText 	= this._cells[trans.keyColumn];
                this.cells = new Proxy(this._cells, {
                    get: function(target, name) {
                        return target[name];
                    },
                    set: function(target, name, value) {
                        codeRunner.onCellEdit(fileId, rowId, name, value, target[name])
                        target[name] = value
                    }
                });  
				this._tags 		= trans.project.files[this.file]?.tags[rowId] || [];
				this.tags 		= new Proxy(this._tags, {
                    get: (target, name) => {
                        return target[name];
                    },
                    set: (target, name, value) => {
                        target[name] = value
                        if (!Array.isArray(trans?.project?.files?.[fileId]?.tags)) return false;
                        trans.project.files[fileId].tags[rowId] = JSON.parse((JSON.stringify(target)));
                        return true;
                    }
                })
				this.parameters	= trans.project.files[this.file].parameters[rowId];
				this.context	= trans.project.files[this.file].context[rowId];
				this.comments   = trans.project.files[this.file].comments || undefined;
 
                this.colId      = colId;
                this.text       = trans.project.files[this.file].data[rowId][colId];
                this.cellCoords = thisCoords[i];

                if (this.index>=this.maxIndex) this.isLast = true;
                await fn.call(this);
                if (codeRunner.isBreak) return codeRunner.finalize("cell", options);
            }
		}
        codeRunner.finalize("cell", options)
	}
	
	await iteratorMain.apply(iteratorObj);
	return true;
}

CodeRunner.prototype.getDialogArguments = function(key) {
    try {
        return window.dialogArguments[key]
    } catch (e) {
        return console.warn("No arguments:", key);
    }
}

CodeRunner.prototype.executeFoundCells = async function(fn, options) {

    var codeRunner  = this;
    this.onExecutionStart();

	fn              = fn || async function() {};
	options         = options || {};

    class IteratorFoundCells extends BaseIterator {
        constructor( options) {
            super(options)
        }
    }
    IteratorFoundCells.prototype.setText = function(text) {
        if (this.colId == 0) return;
        try {
            trans.project.files[this.file].data[this.rowId][this.colId] = text;
        } catch (e) {
            console.warn(`Error when trying to set the text on cell: ${this.file} ${this.rowId},${this.colId}\n`, e);
        }
    }

	var iteratorObj = new IteratorFoundCells(options);

    var foundCells = options.foundCells || this.getDialogArguments('foundCells');
    console.log("found cells:", foundCells);
	if (empty(foundCells)) return;

	var iteratorMain = async function() {
        console.log("Found cell:", foundCells);
        this.index      = -1;
        this.maxIndex   = foundCells.length - 1;
        this.isLast     = false;
        if (this.maxIndex < 0) this.maxIndex = 0;
        var processed = {};

		for (var idx in foundCells) {
            this.index++;
            var thisCoords = foundCells[idx];
            var rowId = thisCoords.row;
            var colId = thisCoords.col;
            if (processed[thisCoords.file+","+rowId+","+colId]) continue;
            processed[thisCoords.file+","+rowId+","+colId] = true;

            this.file 		= thisCoords.file;
            this.rowId 		= rowId;
            this._cells 	= trans.project.files[this.file].data[rowId] || [];
            this.keyText 	= this._cells[trans.keyColumn];
            this.cells = new Proxy(this._cells, {
                get: function(target, name) {
                    return target[name];
                },
                set: function(target, name, value) {
                    codeRunner.onCellEdit(thisCoords.file, rowId, name, value, target[name])
                    target[name] = value
                }
            }); 
            this._tags 		= trans.project.files[this.file].tags?.[rowId] || [];
            this.tags 		= new Proxy(this._tags, {
                get: (target, name) => {
                    return target[name];
                },
                set: (target, name, value) => {
                    target[name] = value
                    if (!Array.isArray(trans?.project?.files?.[this.file]?.tags)) return false;
                    trans.project.files[this.file].tags[rowId] = JSON.parse((JSON.stringify(target)));
                    return true;
                }
            })

            this.parameters	= trans.project.files[this.file].parameters?.[rowId] || [];
            this.context	= trans.project.files[this.file].context?.[rowId] || [];
            this.comments   = trans.project.files[this.file].comments || undefined;
            
            this.colId      = colId;
            this.text       = trans.project.files[this.file].data[rowId][colId];
            this.cellCoords = {
                row:rowId,
                col:colId
            };

            if (this.index>=this.maxIndex) this.isLast = true;
            
            await fn.call(this);
            if (codeRunner.isBreak) return codeRunner.finalize("find", options);
            
        }
        codeRunner.finalize("find", options)
	}
	
	await iteratorMain.apply(iteratorObj);
	return true;
}

CodeRunner.prototype.openFormWindow = async function(schema, def={}, options={}) {
    if (!schema) return;
    if (!schema.components?.length) return;
    
    const id 			= "formWindow";
    options 	= options || {};
    options.id 	= options.id || id;
    options.workspaceType = "formIO";


    console.log("Opening modal window", arguments);
    await common.localStorage.set("formIOLastSchema", schema)

    var {showModalDialog} = require("www/js/modalDialog.js");
    await showModalDialog("www/form.html", options, {id:id, width:480, height:520});
    console.log("script editor closed");
    return await common.localStorage.get("formIOLastValue");

}


// CodeRunner.prototype.getCodeInfo = function(code) {
//     if (!code) return console.warn("Code cannot empty!");

//     const infos = commentParser.parse(code);
//     if (!infos?.length) return;
    
//     var result = {

//     };
//     for (let i=0; i<infos.length; i++) {
//         if (!infos[i]) continue;
//         if (!infos[i].description) continue;
//         if (infos[i].description.substring(0, 5) !== ":info") continue;
//     }

//     return result;
// }



CodeRunner.prototype.isUserInputRequired = async function(code) {
    if (!code) return false;
    const commentParser = new (require("www/js/CommentParser.js"))(code);
    const schema = commentParser.getDescriptionByTag("form");
    if (!schema?.length) return false;
    return true;
}

CodeRunner.prototype.initUserInput = async function(code) {
    if (!code) return;
    const commentParser = new (require("www/js/CommentParser.js"))(code);
    const schema = commentParser.getDescriptionByTag("form");
    console.log("Form schema is", schema, commentParser);
    if (!common.isJSON(schema[0])) return console.warn("Invalid schema", schema, commentParser);
    const formSchema = JSON.parse(schema[0])
    var userData = await this.openFormWindow(formSchema);
    console.log("lastUserInput is ", userData);
    this.lastUserInput = userData?.data
    return userData;
}

CodeRunner.prototype.run = async function(code, workspace, options) {
    if (!code) return console.error("Empty code");
    workspace = workspace || this.workspace;
    options = options || {};
    options.userInput ||= this.lastUserInput;

    if (!options.userInput) {
        if (await this.isUserInputRequired(code)) {
            let userInput = await this.initUserInput(code);
            if (userInput?.state!=="submitted") {
                console.warn("Canceled because user input is not submitted");
                return;
            }
        }

        options.userInput = this.lastUserInput;
    }


    try {
        let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
        var fn = new AsyncFunction(code);
	} catch (e) {
        console.error("Error executing automation")
		throw e;
	}

    console.log("workspace is:", workspace);
    console.log("Options is:", options);
    console.log("%c↓↓The outputs from your script are below↓↓", "color:blue;font-size:1.5em;");
    console.log("==========================================");

    try {
        window.session = {};
        if (workspace == "gridSelection") {
            await this.executeSelectedCells(fn, options);
        } else if (workspace == "foundCells") {
            await this.executeFoundCells(fn, options);
        } else if (workspace == "objectIterator") {
            await this.executeObjectIterator(fn, options);
        } else if (workspace == "rowIterator") {
            await this.executeRowIterator(fn, options);
        } else if (workspace == "global") {
            await this.executeGlobal(fn, options);
        } else {
            throw "Unknown workspace : "+workspace;
        }
    } catch (e) {
        if (e.toString().includes('--2472KAFSDFJ--') == true) {
            var msg = e.toString().split(":");
            if (typeof options.onHalt == "function") {
                options.onHalt.call(this, msg[1]);
            }
            console.log(`Process halted by user request:\n${msg[1]}`);
        } else {
            console.error(e);
            throw e;
        }
    }
    console.log("%cExecution completed", "color:blue;font-size:1.5em;");

}

module.exports = CodeRunner;
module.exports.BaseIterator = BaseIterator;