/* DokuWiki MoaiEditor Match_headers.js file Version : 0.5a (May 6, 2026) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ MoaiEditor.MatchHeaders = class { constructor(outer) { // Arguments this.outer = outer; // Variables this.nodes = []; // Array of header nodes in the html {i, text, type, handle, matchline} this.candidates = []; // Array of candidate lines {i, text, type, linenum} } // ┌───────────────────────────────────┐ // │ Public │ // └───────────────────────────────────┘ findAll () { // Find all header matches for (let node of this.nodes) { // Find the line const line = this.findMatch(node); // Update the header array (for header clicks and table of contents) node.matchline = line; // Append to the matches array (for synchonized scrolling) if (line !== null) this.outer.matches.add (node.type, node.handle, line, line, 'header'); } } // ──────────────────────────────────── findMatch (node) { // Check for similar headers var matches = []; for (let candidate of this.candidates) if (this.same(node, candidate)) matches.push(candidate); // If there are no matches return null if (matches.length == 0) return null; // If there is just one match, return the line number if (matches.length == 1) return matches[0].linenum; // If there is more than one match, check if one of them has the same neighbors var maxScore = -1; var bestMatch = null; for (let candidate of matches) { const score = this.neighborsScore(node, candidate); if (score > maxScore) { maxScore = score; bestMatch = candidate; } } return bestMatch.linenum; } // ──────────────────────────────────── parseText (lines) { this.candidates = []; var i = 0; var l = -1; // Iterarate every line of text for (let line of lines) { l += 1; // Continue if it's not a header line const header = this.isHeader(line); if (!header) continue; // Otherwise store the header this.candidates.push({ i : i, type : header.type, text : header.text, linenum : l, }); // Increment index i += 1; } } // ┌───────────────────────────────────┐ // │ Private │ // └───────────────────────────────────┘ neighborsScore (node, candidate) { var d = 1; var score = 0; var idx_node = node.i; var idx_text = candidate.i; while (true) { // Left neighbors var leftNodeNeighbor = this.getNeighbor(this.nodes, idx_node-d); var leftTextNeighbor = this.getNeighbor(this.candidates, idx_text-d); if (this.same(leftNodeNeighbor, leftTextNeighbor)) score += 1; // Right neighbors var rightNodeNeighbor = this.getNeighbor(this.nodes, idx_node+d); var rightTextNeighbor = this.getNeighbor(this.candidates, idx_text+d); if (this.same(rightNodeNeighbor, rightTextNeighbor)) score += 1; // Exit condition if (score % 2 != 0 || leftNodeNeighbor === null || rightNodeNeighbor === null) break; // Increment distance d += 1; } return score/(2*d); } // ──────────────────────────────────── same (node, candidate) { if (node === null && candidate === null) return true; if (node === null || candidate === null) return false; if (node.type != candidate.type) return false; if (node.text != candidate.text) return false; return true; } // ──────────────────────────────────── getNeighbor (array, idx) { if (idx < 0 || idx > array.length-1) return null; return array[idx]; } // ──────────────────────────────────── isHeader (line) { // Fail if the line does not include '==' if (line.search("==") == -1) return null; // Fail if the line starts with two or more spaces if (line.substr(0,2) == ' ') return null; // Fail if the trimmed line does not end in '==' if (line.trim().slice(-2) != '==') return null; // Search all sequences of two or more '=' let regex = /(={2,})/gd; let matches = [...line.matchAll(regex)]; // Fail if we have less than two sequences if (matches.length < 2) return null; // Extract the content of the header (inner part) var first = matches[0]; var last = matches.at(-1); var pos_start = first.indices[0][1]; var pos_end = last.index; var content = line.substring(pos_start, pos_end); var text = content.trim(); // Fail if the content is empty (or whitespace) if (text.length == 0) return null; // Determine the type of header (h1, h2, etc) var len = first.indices[0][1] - first.indices[0][0]; var type; if (len < 6) type = ['H5','H4','H3','H2'][len-2]; else type = 'H1'; // Return the data return { type : type, text : text, content : content, contentStart : pos_start, contentEnd : pos_end }; } }; // End Class