1/* DokuWiki MoaiEditor Match_tools.js file 2 Version : 0.5a (May 6, 2026) 3 Author : MoaiTools <info@moaitools.org> 4 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 5 6MoaiEditor.MatchTools = class { 7 8 constructor(outer) { 9 10 // Arguments 11 this.outer = outer; 12 13 // Objects 14 this.media = new MoaiEditor.MatchMedia(this); 15 } 16 // ──────────────────────────────────── 17 hasBlockTags (text) { 18 19 const tags = [ 20 /<code\b/, 21 /<file\b/, 22 /<wrap\b/, 23 /<WRAP\b/, 24 '</code>', 25 '</file>', 26 '</wrap>', 27 '</WRAP>', 28 ]; 29 for (let tag of tags) { 30 let [index, next] = this.indexOf(text, tag); 31 if (index > -1) 32 return true; 33 } 34 return false; 35 } 36 // ──────────────────────────────────── 37 hasImbaInlineTags (text) { 38 39 const tags = [ 40 ['**','**'], 41 ['__','__'], 42 ['//','//'], 43 ["''","''"], 44 ['%%','%%'], 45 ['<sub>','</sub>'], 46 ['<sup>','</sup>'], 47 ['<del>','</del>'], 48 ['<nowiki>','</nowiki>'] 49 ]; 50 for (let pair of tags) { 51 const imbalanced = this.hasImbaTags(text, pair[0], pair[1]); 52 if (imbalanced) 53 return true; 54 } 55 return false; 56 } 57 // ──────────────────────────────────── 58 hasImbaTags (text, open, close) { 59 60 var i = 0; 61 var next = 0; 62 var index; 63 var start = 0; 64 while (true) { 65 66 // Opening tag 67 start = next; 68 [index, next] = this.indexOf(text, open, start); 69 if (index == -1) 70 return false; 71 72 // Closing tag 73 start = next; 74 [index, next] = this.indexOf(text, close, start); 75 if (index == -1) 76 return true; 77 78 // Safety valve 79 i += 1; 80 if (i > 1000) { 81 console.warn("MoaiEditor :: Match Tools :: imbalancedTags :: infinite loop"); 82 return true; 83 } 84 } 85 } 86 // ──────────────────────────────────── 87 indexOf (string, re, start=0) { 88 // String 89 if (typeof re === 'string') { 90 const index = string.indexOf(re, start); 91 return [index, index + re.length]; 92 } 93 // Regex 94 var indexOf = string.substring(start).search(re); 95 var index = (indexOf >= 0) ? (indexOf + start) : indexOf; 96 return [index, index + 1] 97 } 98 // ──────────────────────────────────── 99 wordsInString (words, string) { 100 101 102 // Count the matching words in the string considering order 103 var n = 0; 104 var m = 0; 105 for (let word of words) { 106 if (word.trim().length == 0) 107 continue; 108 const index = string.indexOf(word); 109 110 if (index > -1) { 111 m += 1; 112 string = string.substring(index + word.length) 113 } 114 n += 1; 115 } 116 // Calc score 117 var score = null; if (n > 0) score = m/n; 118 119 // Return 120 return {n:n, m:m, score:score}; 121 } 122 // ──────────────────────────────────── 123 replaceLinks (block) { 124 125 // Get the text from the block 126 var text = block.text; 127 if ('cleantext' in block) 128 text = block.cleantext; 129 var nolink = text; 130 131 // Replace [[square bracket links]] by what 'node.textContent' would show 132 var matches = text.match(/\[\[.*?\]\](?!\])/g); 133 if (matches === null) 134 matches = []; 135 for (let match of matches) { 136 137 // Remove the square brackets and trim space 138 var string = match.substring(2, match.length-2).trim(); 139 140 // If it is empty, show 'start' 141 if (string.length == 0) { 142 text = this.strReplaceFirst(text, match, 'start'); 143 continue; 144 } 145 // If it has a text description, show it 146 var [left, right] = this.lpartition(string, '|'); 147 if (right.length > 0) { 148 text = this.strReplaceFirst(text, match, right.trim()); 149 nolink = this.strReplaceFirst(text, match, ''); 150 continue; 151 } 152 // If it is an external link, show it like it is 153 string = left; 154 if (string.startsWith('http://') || string.startsWith('https://')) { 155 text = this.strReplaceFirst(text, match, string); 156 nolink = this.strReplaceFirst(text, match, ''); 157 continue; 158 } 159 // If it is an internal link with section tag, show the section 160 var [left, right] = this.lpartition(string, '#'); 161 if (right.length > 0) { 162 text = this.strReplaceFirst(text, match, right.trim()); 163 nolink = this.strReplaceFirst(text, match, ''); 164 continue; 165 } 166 // If it is a normal internal link show the part after the last ':' 167 string = left; 168 var lastpart = this.rpartition(string, ':'); 169 if (lastpart.length > 0) { 170 text = this.strReplaceFirst(text, match, lastpart); 171 nolink = this.strReplaceFirst(text, match, ''); 172 continue; 173 } 174 // If that last part is empty (for example in 'animals:mammals::') show 'start' 175 text = this.strReplaceFirst(text, match, 'start'); 176 nolink = this.strReplaceFirst(text, match, ''); 177 } 178 // Replace urls so we dont trigger the '//' inline tag 179 const urls = nolink.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\b/g); 180 if (urls !== null) 181 for (let url of urls) 182 nolink = nolink.replace(url, ""); 183 184 // Store cleaned text 185 block.cleantext = text; 186 block.nolinktext = nolink; // Used just for not triggering inline tags like '//', etc 187 } 188 // ──────────────────────────────────── 189 strReplaceFirst (string, search, replace) { 190 const index = string.indexOf(search); 191 return string.substring(0, index) + replace + string.substring(index + search.length); 192 } 193 // ──────────────────────────────────── 194 lpartition (string, char) { 195 const index = string.indexOf(char); 196 if (index == -1) 197 return [string, '']; 198 const left = string.substring(0, index); 199 const right = string.substring(index+1); 200 return [left, right]; 201 } 202 rpartition (string, char) { 203 string = string.replace(/:{1}$/, ''); // If the last character is the separator, remove it 204 //string = string.trim(char); 205 const index = string.lastIndexOf(char); 206 if (index == -1) 207 return string; 208 const left = string.substring(0, index); 209 const right = string.substring(index+1); 210 return right; 211 } 212 // ┌───────────────────────────────────┐ 213 // │ Text content │ 214 // └───────────────────────────────────┘ 215 216 sameWords (score, text1, text2, separator) { 217 218 // Define separators 219 const separators = { 220 whitespace : /\s+/, 221 tables : /falta/, 222 }; 223 const sep = separators[separator]; 224 225 // Split by any whitespace (space, tab, new line) 226 var words1 = text1.split(sep); 227 var words2 = text2.split(sep); 228 229 // Filter out empty strings 230 words1 = words1.filter(str => (str !== '')); 231 words2 = words2.filter(str => (str !== '')); 232 233 234 // Compare words considering order both ways, add the scores 235 const [n1, m1] = this.compareWords(words1, words2); 236 const [n2, m2] = this.compareWords(words2, words1); 237 const m = m1+m2; 238 const n = n1+n2; 239 240 // Update the score 241 score.m += m; 242 score.n += n; 243 } 244 // ──────────────────────────────────── 245 compareWords (words1, words2) { 246 247 // Copy array because it will be trimmed 248 var words = words2.slice(); 249 250 // Count the matching words considering order 251 var n = 0; 252 var m = 0; 253 var i = -1; 254 for (let word1 of words1) { 255 if (word1.trim().length == 0) 256 continue; 257 const index = words.indexOf(word1); 258 259 if (index > -1) { 260 if (index > i) 261 m += 1; 262 i = index-1; 263 words.splice(index, 1); 264 } 265 n += 1; 266 } 267 // Return matches and number of comparisons 268 return [n, m]; 269 } 270 // ──────────────────────────────────── 271 sameLinks () { 272 // Devolver null si el nodo no tiene <a>, o un porcentaje de coincidencias y el numero de coincidencias (8, 75%) 273 } 274}; // End Class 275