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