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