htmlfold.vim (4585B)
1 " HTML folding script, :h ft-html-plugin 2 " Latest Change: 2025 May 10 3 " Original Author: Aliaksei Budavei <0x000c70@gmail.com> 4 5 function! htmlfold#MapBalancedTags() abort 6 " Describe only _a capturable-name prefix_ for start and end patterns of 7 " a tag so that start tags with attributes spanning across lines can also be 8 " matched with a single call of "getline()". 9 let tag = '\m\c</\=\([0-9A-Za-z-]\+\)' 10 let names = [] 11 let pairs = [] 12 let ends = [] 13 let pos = getpos('.') 14 15 try 16 call cursor(1, 1) 17 let [lnum, cnum] = searchpos(tag, 'cnW') 18 19 " Pair up nearest non-inlined tags in scope. 20 while lnum > 0 21 let name_attr = synIDattr(synID(lnum, cnum, 0), 'name') 22 23 if name_attr ==# 'htmlTag' || name_attr ==# 'htmlScriptTag' 24 let name = get(matchlist(getline(lnum), tag, (cnum - 1)), 1, '') 25 26 if !empty(name) 27 call insert(names, tolower(name), 0) 28 call insert(pairs, [lnum, -1], 0) 29 endif 30 elseif name_attr ==# 'htmlEndTag' 31 let name = get(matchlist(getline(lnum), tag, (cnum - 1)), 1, '') 32 33 if !empty(name) 34 let idx = index(names, tolower(name)) 35 36 if idx >= 0 37 " Dismiss inlined balanced tags and opened-only tags. 38 if pairs[idx][0] != lnum 39 let pairs[idx][1] = lnum 40 call add(ends, lnum) 41 endif 42 43 " Claim a pair. 44 let names[: idx] = repeat([''], (idx + 1)) 45 endif 46 endif 47 endif 48 49 " Advance the cursor, at "<", past "</a", "<a>", etc. 50 call cursor(lnum, (cnum + 3)) 51 let [lnum, cnum] = searchpos(tag, 'cnW') 52 endwhile 53 finally 54 call setpos('.', pos) 55 endtry 56 57 if empty(ends) 58 return {} 59 endif 60 61 let folds = {} 62 let pending_end = ends[0] 63 let level = 0 64 65 while !empty(pairs) 66 let [start, end] = remove(pairs, -1) 67 68 if end < 0 69 continue 70 endif 71 72 if start >= pending_end 73 " Mark a sibling tag. 74 call remove(ends, 0) 75 76 while start >= ends[0] 77 " Mark a parent tag. 78 call remove(ends, 0) 79 let level -= 1 80 endwhile 81 82 let pending_end = ends[0] 83 else 84 " Mark a child tag. 85 let level += 1 86 endif 87 88 " Flatten the innermost inlined folds. 89 let folds[start] = get(folds, start, ('>' . level)) 90 let folds[end] = get(folds, end, ('<' . level)) 91 endwhile 92 93 return folds 94 endfunction 95 96 " See ":help vim9-mix". 97 if !has("vim9script") 98 finish 99 endif 100 101 def! g:htmlfold#MapBalancedTags(): dict<string> 102 # Describe only _a capturable-name prefix_ for start and end patterns of 103 # a tag so that start tags with attributes spanning across lines can also be 104 # matched with a single call of "getline()". 105 const tag: string = '\m\c</\=\([0-9A-Za-z-]\+\)' 106 var names: list<string> = [] 107 var pairs: list<list<number>> = [] 108 var ends: list<number> = [] 109 const pos: list<number> = getpos('.') 110 111 try 112 cursor(1, 1) 113 var [lnum: number, cnum: number] = searchpos(tag, 'cnW') 114 115 # Pair up nearest non-inlined tags in scope. 116 while lnum > 0 117 const name_attr: string = synIDattr(synID(lnum, cnum, 0), 'name') 118 119 if name_attr ==# 'htmlTag' || name_attr ==# 'htmlScriptTag' 120 const name: string = get(matchlist(getline(lnum), tag, (cnum - 1)), 1, '') 121 122 if !empty(name) 123 insert(names, tolower(name), 0) 124 insert(pairs, [lnum, -1], 0) 125 endif 126 elseif name_attr ==# 'htmlEndTag' 127 const name: string = get(matchlist(getline(lnum), tag, (cnum - 1)), 1, '') 128 129 if !empty(name) 130 const idx: number = index(names, tolower(name)) 131 132 if idx >= 0 133 # Dismiss inlined balanced tags and opened-only tags. 134 if pairs[idx][0] != lnum 135 pairs[idx][1] = lnum 136 add(ends, lnum) 137 endif 138 139 # Claim a pair. 140 names[: idx] = repeat([''], (idx + 1)) 141 endif 142 endif 143 endif 144 145 # Advance the cursor, at "<", past "</a", "<a>", etc. 146 cursor(lnum, (cnum + 3)) 147 [lnum, cnum] = searchpos(tag, 'cnW') 148 endwhile 149 finally 150 setpos('.', pos) 151 endtry 152 153 if empty(ends) 154 return {} 155 endif 156 157 var folds: dict<string> = {} 158 var pending_end: number = ends[0] 159 var level: number = 0 160 161 while !empty(pairs) 162 const [start: number, end: number] = remove(pairs, -1) 163 164 if end < 0 165 continue 166 endif 167 168 if start >= pending_end 169 # Mark a sibling tag. 170 remove(ends, 0) 171 172 while start >= ends[0] 173 # Mark a parent tag. 174 remove(ends, 0) 175 level -= 1 176 endwhile 177 178 pending_end = ends[0] 179 else 180 # Mark a child tag. 181 level += 1 182 endif 183 184 # Flatten the innermost inlined folds. 185 folds[start] = get(folds, start, ('>' .. level)) 186 folds[end] = get(folds, end, ('<' .. level)) 187 endwhile 188 189 return folds 190 enddef 191 192 " vim: fdm=syntax sw=2 ts=8 noet