neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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