blob: 76ccaefaf4927e576cbf89d3f7f8941561b6b27a [file] [log] [blame]
Aliaksei Budaveidc7ed8f2025-05-10 21:40:41 +02001" HTML folding script, :h ft-html-plugin
2" Latest Change: 2025 May 10
3" Original Author: Aliaksei Budavei <0x000c70@gmail.com>
4
5function! 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
94endfunction
95
96" See ":help vim9-mix".
97if !has("vim9script")
98 finish
99endif
100
101def! 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
190enddef
191
192" vim: fdm=syntax sw=2 ts=8 noet