require("regexp-match-indices/auto");
var ui = window.ui || {
log: async function() { console.log(...arguments) },
logError: async function() { console.error(...arguments) }
};
/**
* 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;
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
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
}
}
// beforeWrite
if (!empty(this.model.beforeWrite)) {
if (typeof this.model.beforeWrite == "function") this.beforeWrite = this.model.beforeWrite;
try {
const beforeWrite = new AsyncFunction('text', this.model.beforeWrite);
this.beforeWrite = beforeWrite;
} catch (e) {
// do nothing
}
}
}
/**
* Process the rule
* @param {} thisRule
* @param {} initialOffset
*/
var processRule = async (thisRule, initialOffset) => {
try {
initialOffset = initialOffset || 0;
// var maxIteration = 999;
console.log("fetchingRules", thisRule);
if (thisRule.type == "regex") {
if (!thisRule.pattern) return;
var thisPattern = evalRegex(thisRule.pattern);
await ui.log("Evaluating pattern : ", thisPattern.toString());
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 = "";
let matchAllResult;
const processMatch = async (matches) => {
for (let 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]) return;
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;
}
}
// check if the pattern is global
if (thisPattern.global) {
matchAllResult = theString.matchAll(thisPattern);
if (this.debugLevel) console.log("Match all result", matchAllResult);
if (!matchAllResult) return;
// javascript exec can lock us in infinite loop when encouter a blank match
//while((matches=thisPattern.exec(theString)) != null) {
for (let matches of matchAllResult) {
if (this.debugLevel) console.log("Matches", matches);
// iterate through all selected groups
await processMatch(matches);
}
} else {
matchAllResult = theString.match(thisPattern);
if (this.debugLevel) console.log("Match all result", matchAllResult);
if (!matchAllResult) return;
await processMatch(matchAllResult);
}
} 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
});
}
}
} catch (e) {
console.error(e);
await ui.logError("Error processing rule :", e.toString());
}
}
await processHooks();
for (let 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(force) {
// cache the result that ensures the script is only translated once
// if this function is called multiple times, it will reverse the order.
if (!force) {
if (this.translatedScript) return this.translatedScript;
}
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);
}
this.translatedScript = thisScript;
return this.translatedScript;
}
/**
* 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