1/*  DokuWiki MoaiEditor Match_headers.js file
2    Author  : MoaiTools <info@moaitools.org>
3    License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */
4
5MoaiEditor.MatchHeaders = class {
6
7    constructor(outer) {
8
9        // Arguments
10        this.outer = outer;
11
12        // Variables
13        this.nodes = [];            // Array of header nodes in the html      {i, text, type, handle, matchline}
14        this.candidates = [];       // Array of candidate lines               {i, text, type, linenum}
15    }
16    // ┌───────────────────────────────────┐
17    // │ Public                            │
18    // └───────────────────────────────────┘
19
20    findAll () {
21
22
23        // Find all header matches
24        for (let node of this.nodes) {
25            // Find the line
26            const line = this.findMatch(node);
27            // Update the header array (for header clicks and table of contents)
28            node.matchline = line;
29            // Append to the matches array (for synchonized scrolling)
30            if (line !== null)
31                this.outer.matches.add (node.type, node.handle, line, line, 'header');
32        }
33    }
34
35    findMatch (node) {
36
37        // Check for similar headers
38        var matches = [];
39        for (let candidate of this.candidates)
40            if (this.same(node, candidate))
41                matches.push(candidate);
42        // If there are no matches return null
43        if (matches.length == 0)
44            return null;
45        // If there is just one match, return the line number
46        if (matches.length == 1)
47            return matches[0].linenum;
48        // If there is more than one match, check if one of them has the same neighbors
49        var maxScore = -1;
50        var bestMatch = null;
51        for (let candidate of matches) {
52            const score = this.neighborsScore(node, candidate);
53            if (score > maxScore) {
54                maxScore = score;
55                bestMatch = candidate;
56            }
57        }
58        return bestMatch.linenum;
59    }
60
61    parseText (lines) {
62        this.candidates = [];
63        var i = 0;
64        var l = -1;
65        // Iterarate every line of text
66        for (let line of lines) {
67            l += 1;
68            // Continue if it's not a header line
69            const header = this.isHeader(line);
70            if (!header)
71                continue;
72            // Otherwise store the header
73            this.candidates.push({
74                i       : i,
75                type    : header.type,
76                text    : header.text,
77                linenum : l,
78            });
79            // Increment index
80            i += 1;
81        }
82    }
83    // ┌───────────────────────────────────┐
84    // │ Private                           │
85    // └───────────────────────────────────┘
86
87    neighborsScore (node, candidate) {
88
89        var d = 1;
90        var score = 0;
91        var idx_node = node.i;
92        var idx_text = candidate.i;
93        while (true) {
94            // Left neighbors
95            var leftNodeNeighbor = this.getNeighbor(this.nodes, idx_node-d);
96            var leftTextNeighbor = this.getNeighbor(this.candidates, idx_text-d);
97            if (this.same(leftNodeNeighbor, leftTextNeighbor))
98                score += 1;
99            // Right neighbors
100            var rightNodeNeighbor = this.getNeighbor(this.nodes, idx_node+d);
101            var rightTextNeighbor = this.getNeighbor(this.candidates, idx_text+d);
102            if (this.same(rightNodeNeighbor, rightTextNeighbor))
103                score += 1;
104            // Exit condition
105            if (score % 2 != 0  ||  leftNodeNeighbor === null   ||  rightNodeNeighbor === null)
106                break;
107            // Increment distance
108            d += 1;
109        }
110        return score/(2*d);
111    }
112
113    same (node, candidate) {
114        if (node === null  &&  candidate === null)
115            return true;
116        if (node === null  ||  candidate === null)
117            return false;
118        if (node.type != candidate.type)
119            return false;
120        if (node.text != candidate.text)
121            return false;
122        return true;
123    }
124
125    getNeighbor (array, idx) {
126        if (idx < 0  ||  idx > array.length-1)
127            return null;
128        return array[idx];
129    }
130
131    isHeader (line) {
132
133        // Fail if the line does not include '=='
134        if (line.search("==") == -1)
135            return null;
136        // Fail if the line starts with two or more spaces
137        if (line.substr(0,2) == '  ')
138            return null;
139        // Fail if the trimmed line does not end in '=='
140        if (line.trim().slice(-2) != '==')
141            return null;
142        // Search all sequences of two or more '='
143        let regex = /(={2,})/gd;
144        let matches = [...line.matchAll(regex)];
145        // Fail if we have less than two sequences
146        if (matches.length < 2)
147            return null;
148        // Extract the content of the header (inner part)
149        var first = matches[0];
150        var last  = matches.at(-1);
151        var pos_start = first.indices[0][1];
152        var pos_end   = last.index;
153        var content = line.substring(pos_start, pos_end);
154        var text = content.trim();
155        // Fail if the content is empty (or whitespace)
156        if (text.length == 0)
157            return null;
158        // Determine the type of header (h1, h2, etc)
159        var len = first.indices[0][1] - first.indices[0][0];
160        var type;
161        if (len < 6)
162            type = ['H5','H4','H3','H2'][len-2];
163        else
164            type = 'H1';
165        // Return the data
166        return {
167            type         : type,
168            text         : text,
169            content      : content,
170            contentStart : pos_start,
171            contentEnd   : pos_end
172        };
173    }
174}; // End Class
175