/* DokuWiki MoaiEditor Match_tools.js file Version : 0.5 (May 5, 2026) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ MoaiEditor.MatchTools = class { constructor(outer) { // Arguments this.outer = outer; // Objects this.media = new MoaiEditor.MatchMedia(this); } // ──────────────────────────────────── hasBlockTags (text) { const tags = [ /', '', '', '', ]; for (let tag of tags) { let [index, next] = this.indexOf(text, tag); if (index > -1) return true; } return false; } // ──────────────────────────────────── hasImbaInlineTags (text) { const tags = [ ['**','**'], ['__','__'], ['//','//'], ["''","''"], ['%%','%%'], ['',''], ['',''], ['',''], ['',''] ]; for (let pair of tags) { const imbalanced = this.hasImbaTags(text, pair[0], pair[1]); if (imbalanced) return true; } return false; } // ──────────────────────────────────── hasImbaTags (text, open, close) { var i = 0; var next = 0; var index; var start = 0; while (true) { // Opening tag start = next; [index, next] = this.indexOf(text, open, start); if (index == -1) return false; // Closing tag start = next; [index, next] = this.indexOf(text, close, start); if (index == -1) return true; // Safety valve i += 1; if (i > 1000) { console.warn("MoaiEditor :: Match Tools :: imbalancedTags :: infinite loop"); return true; } } } // ──────────────────────────────────── indexOf (string, re, start=0) { // String if (typeof re === 'string') { const index = string.indexOf(re, start); return [index, index + re.length]; } // Regex var indexOf = string.substring(start).search(re); var index = (indexOf >= 0) ? (indexOf + start) : indexOf; return [index, index + 1] } // ──────────────────────────────────── wordsInString (words, string) { // Count the matching words in the string considering order var n = 0; var m = 0; for (let word of words) { if (word.trim().length == 0) continue; const index = string.indexOf(word); if (index > -1) { m += 1; string = string.substring(index + word.length) } n += 1; } // Calc score var score = null; if (n > 0) score = m/n; // Return return {n:n, m:m, score:score}; } // ──────────────────────────────────── replaceLinks (block) { // Get the text from the block var text = block.text; if ('cleantext' in block) text = block.cleantext; var nolink = text; // Replace [[square bracket links]] by what 'node.textContent' would show var matches = text.match(/\[\[.*?\]\](?!\])/g); if (matches === null) matches = []; for (let match of matches) { // Remove the square brackets and trim space var string = match.substring(2, match.length-2).trim(); // If it is empty, show 'start' if (string.length == 0) { text = this.strReplaceFirst(text, match, 'start'); continue; } // If it has a text description, show it var [left, right] = this.lpartition(string, '|'); if (right.length > 0) { text = this.strReplaceFirst(text, match, right.trim()); nolink = this.strReplaceFirst(text, match, ''); continue; } // If it is an external link, show it like it is string = left; if (string.startsWith('http://') || string.startsWith('https://')) { text = this.strReplaceFirst(text, match, string); nolink = this.strReplaceFirst(text, match, ''); continue; } // If it is an internal link with section tag, show the section var [left, right] = this.lpartition(string, '#'); if (right.length > 0) { text = this.strReplaceFirst(text, match, right.trim()); nolink = this.strReplaceFirst(text, match, ''); continue; } // If it is a normal internal link show the part after the last ':' string = left; var lastpart = this.rpartition(string, ':'); if (lastpart.length > 0) { text = this.strReplaceFirst(text, match, lastpart); nolink = this.strReplaceFirst(text, match, ''); continue; } // If that last part is empty (for example in 'animals:mammals::') show 'start' text = this.strReplaceFirst(text, match, 'start'); nolink = this.strReplaceFirst(text, match, ''); } // Replace urls so we dont trigger the '//' inline tag const urls = nolink.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\b/g); if (urls !== null) for (let url of urls) nolink = nolink.replace(url, ""); // Store cleaned text block.cleantext = text; block.nolinktext = nolink; // Used just for not triggering inline tags like '//', etc } // ──────────────────────────────────── strReplaceFirst (string, search, replace) { const index = string.indexOf(search); return string.substring(0, index) + replace + string.substring(index + search.length); } // ──────────────────────────────────── lpartition (string, char) { const index = string.indexOf(char); if (index == -1) return [string, '']; const left = string.substring(0, index); const right = string.substring(index+1); return [left, right]; } rpartition (string, char) { string = string.replace(/:{1}$/, ''); // If the last character is the separator, remove it //string = string.trim(char); const index = string.lastIndexOf(char); if (index == -1) return string; const left = string.substring(0, index); const right = string.substring(index+1); return right; } // ┌───────────────────────────────────┐ // │ Text content │ // └───────────────────────────────────┘ sameWords (score, text1, text2, separator) { // Define separators const separators = { whitespace : /\s+/, tables : /falta/, }; const sep = separators[separator]; // Split by any whitespace (space, tab, new line) var words1 = text1.split(sep); var words2 = text2.split(sep); // Filter out empty strings words1 = words1.filter(str => (str !== '')); words2 = words2.filter(str => (str !== '')); // Compare words considering order both ways, add the scores const [n1, m1] = this.compareWords(words1, words2); const [n2, m2] = this.compareWords(words2, words1); const m = m1+m2; const n = n1+n2; // Update the score score.m += m; score.n += n; } // ──────────────────────────────────── compareWords (words1, words2) { // Copy array because it will be trimmed var words = words2.slice(); // Count the matching words considering order var n = 0; var m = 0; var i = -1; for (let word1 of words1) { if (word1.trim().length == 0) continue; const index = words.indexOf(word1); if (index > -1) { if (index > i) m += 1; i = index-1; words.splice(index, 1); } n += 1; } // Return matches and number of comparisons return [n, m]; } // ──────────────────────────────────── sameLinks () { // Devolver null si el nodo no tiene , o un porcentaje de coincidencias y el numero de coincidencias (8, 75%) } }; // End Class