summaryrefslogtreecommitdiffstats
path: root/runtime/indent/html.vim
blob: 65e0ffc40cd1d0cc0406d21933ecb771d270e13d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
" Vim indent script for HTML
" Maintainer:	Bram Moolenaar
" Original Author: Andy Wokula <anwoku@yahoo.de>
" Last Change:	2022 Jan 31
" Version:	1.0 "{{{
" Description:	HTML indent script with cached state for faster indenting on a
"		range of lines.
"		Supports template systems through hooks.
"		Supports Closure stylesheets.
"
" Credits:
"	indent/html.vim (2006 Jun 05) from J. Zellner
"	indent/css.vim (2006 Dec 20) from N. Weibull
"
" History:
" 2014 June	(v1.0) overhaul (Bram)
" 2012 Oct 21	(v0.9) added support for shiftwidth()
" 2011 Sep 09	(v0.8) added HTML5 tags (thx to J. Zuckerman)
" 2008 Apr 28	(v0.6) revised customization
" 2008 Mar 09	(v0.5) fixed 'indk' issue (thx to C.J. Robinson)
"}}}

" Init Folklore, check user settings (2nd time ++)
if exists("b:did_indent") "{{{
  finish
endif

" Load the Javascript indent script first, it defines GetJavascriptIndent().
" Undo the rest.
" Load base python indent.
if !exists('*GetJavascriptIndent')
  runtime! indent/javascript.vim
endif
let b:did_indent = 1

setlocal indentexpr=HtmlIndent()
setlocal indentkeys=o,O,<Return>,<>>,{,},!^F

" Needed for % to work when finding start/end of a tag.
setlocal matchpairs+=<:>

let b:undo_indent = "setlocal inde< indk<"

" b:hi_indent keeps state to speed up indenting consecutive lines.
let b:hi_indent = {"lnum": -1}

"""""" Code below this is loaded only once. """""
if exists("*HtmlIndent") && !exists('g:force_reload_html')
  call HtmlIndent_CheckUserSettings()
  finish
endif

" Allow for line continuation below.
let s:cpo_save = &cpo
set cpo-=C
"}}}

" Pattern to match the name of a tag, including custom elements.
let s:tagname = '\w\+\(-\w\+\)*'

" Check and process settings from b:html_indent and g:html_indent... variables.
" Prefer using buffer-local settings over global settings, so that there can
" be defaults for all HTML files and exceptions for specific types of HTML
" files.
func HtmlIndent_CheckUserSettings()
  "{{{
  let inctags = ''
  if exists("b:html_indent_inctags")
    let inctags = b:html_indent_inctags
  elseif exists("g:html_indent_inctags")
    let inctags = g:html_indent_inctags
  endif
  let b:hi_tags = {}
  if len(inctags) > 0
    call s:AddITags(b:hi_tags, split(inctags, ","))
  endif

  let autotags = ''
  if exists("b:html_indent_autotags")
    let autotags = b:html_indent_autotags
  elseif exists("g:html_indent_autotags")
    let autotags = g:html_indent_autotags
  endif
  let b:hi_removed_tags = {}
  if len(autotags) > 0
    call s:RemoveITags(b:hi_removed_tags, split(autotags, ","))
  endif

  " Syntax names indicating being inside a string of an attribute value.
  let string_names = []
  if exists("b:html_indent_string_names")
    let string_names = b:html_indent_string_names
  elseif exists("g:html_indent_string_names")
    let string_names = g:html_indent_string_names
  endif
  let b:hi_insideStringNames = ['htmlString']
  if len(string_names) > 0
    for s in string_names
      call add(b:hi_insideStringNames, s)
    endfor
  endif

  " Syntax names indicating being inside a tag.
  let tag_names = []
  if exists("b:html_indent_tag_names")
    let tag_names = b:html_indent_tag_names
  elseif exists("g:html_indent_tag_names")
    let tag_names = g:html_indent_tag_names
  endif
  let b:hi_insideTagNames = ['htmlTag', 'htmlScriptTag']
  if len(tag_names) > 0
    for s in tag_names
      call add(b:hi_insideTagNames, s)
    endfor
  endif

  let indone = {"zero": 0
              \,"auto": "indent(prevnonblank(v:lnum-1))"
              \,"inc": "b:hi_indent.blocktagind + shiftwidth()"}

  let script1 = ''
  if exists("b:html_indent_script1")
    let script1 = b:html_indent_script1
  elseif exists("g:html_indent_script1")
    let script1 = g:html_indent_script1
  endif
  if len(script1) > 0
    let b:hi_js1indent = get(indone, script1, indone.zero)
  else
    let b:hi_js1indent = 0
  endif

  let style1 = ''
  if exists("b:html_indent_style1")
    let style1 = b:html_indent_style1
  elseif exists("g:html_indent_style1")
    let style1 = g:html_indent_style1
  endif
  if len(style1) > 0
    let b:hi_css1indent = get(indone, style1, indone.zero)
  else
    let b:hi_css1indent = 0
  endif

  if !exists('b:html_indent_line_limit')
    if exists('g:html_indent_line_limit')
      let b:html_indent_line_limit = g:html_indent_line_limit
    else
      let b:html_indent_line_limit = 200
    endif
  endif

  if exists('b:html_indent_attribute')
    let b:hi_attr_indent = b:html_indent_attribute
  elseif exists('g:html_indent_attribute')
    let b:hi_attr_indent = g:html_indent_attribute
  else
    let b:hi_attr_indent = 2
  endif

endfunc "}}}

" Init Script Vars
"{{{
let b:hi_lasttick = 0
let b:hi_newstate = {}
let s:countonly = 0
 "}}}

" Fill the s:indent_tags dict with known tags.
" The key is "tagname" or "/tagname".  {{{
" The value is:
" 1   opening tag
" 2   "pre"
" 3   "script"
" 4   "style"
" 5   comment start
" 6   conditional comment start
" -1  closing tag
" -2  "/pre"
" -3  "/script"
" -4  "/style"
" -5  comment end
" -6  conditional comment end
let s:indent_tags = {}
let s:endtags = [0,0,0,0,0,0,0]   " long enough for the highest index
"}}}

" Add a list of tag names for a pair of <tag> </tag> to "tags".
func s:AddITags(tags, taglist)
  "{{{
  for itag in a:taglist
    let a:tags[itag] = 1
    let a:tags['/' . itag] = -1
  endfor
endfunc "}}}

" Take a list of tag name pairs that are not to be used as tag pairs.
func s:RemoveITags(tags, taglist)
  "{{{
  for itag in a:taglist
    let a:tags[itag] = 1
    let a:tags['/' . itag] = 1
  endfor
endfunc "}}}

" Add a block tag, that is a tag with a different kind of indenting.
func s:AddBlockTag(tag, id, ...)
  "{{{
  if !(a:id >= 2 && a:id < len(s:endtags))
    echoerr 'AddBlockTag ' . a:id
    return
  endif
  let s:indent_tags[a:tag] = a:id
  if a:0 == 0
    let s:indent_tags['/' . a:tag] = -a:id
    let s:endtags[a:id] = "</" . a:tag . ">"
  else
    let s:indent_tags[a:1] = -a:id
    let s:endtags[a:id] = a:1
  endif
endfunc "}}}

" Add known tag pairs.
" Self-closing tags and tags that are sometimes {{{
" self-closing (e.g., <p>) are not here (when encountering </p> we can find
" the matching <p>, but not the other way around).
" Known self-closing tags: " 'p', 'img', 'source', 'area', 'keygen', 'track',
" 'wbr'.
" Old HTML tags:
call s:AddITags(s:indent_tags, [
    \ 'a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big',
    \ 'blockquote', 'body', 'button', 'caption', 'center', 'cite', 'code',
    \ 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset', 'font',
    \ 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html',
    \ 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li',
    \ 'map', 'menu', 'noframes', 'noscript', 'object', 'ol',
    \ 'optgroup', 'q', 's', 'samp', 'select', 'small', 'span', 'strong', 'sub',
    \ 'sup', 'table', 'textarea', 'title', 'tt', 'u', 'ul', 'var', 'th', 'td',
    \ 'tr', 'tbody', 'tfoot', 'thead'])

" New HTML5 elements:
call s:AddITags(s:indent_tags, [
    \ 'article', 'aside', 'audio', 'bdi', 'canvas', 'command', 'data',
    \ 'datalist', 'details', 'dialog', 'embed', 'figcaption', 'figure',
    \ 'footer', 'header', 'hgroup', 'main', 'mark', 'meter', 'nav', 'output',
    \ 'picture', 'progress', 'rp', 'rt', 'ruby', 'section', 'summary',
    \ 'svg', 'time', 'video'])

" Tags added for web components:
call s:AddITags(s:indent_tags, [
    \ 'content', 'shadow', 'template'])
"}}}

" Add Block Tags: these contain alien content
"{{{
call s:AddBlockTag('pre', 2)
call s:AddBlockTag('script', 3)
call s:AddBlockTag('style', 4)
call s:AddBlockTag('<!--', 5, '-->')
call s:AddBlockTag('<!--[', 6, '![endif]-->')
"}}}

" Return non-zero when "tagname" is an opening tag, not being a block tag, for
" which there should be a closing tag.  Can be used by scripts that include
" HTML indenting.
func HtmlIndent_IsOpenTag(tagname)
  "{{{
  if get(s:indent_tags, a:tagname) == 1
    return 1
  endif
  return get(b:hi_tags, a:tagname) == 1
endfunc "}}}

" Get the value for "tagname", taking care of buffer-local tags.
func s:get_tag(tagname)
  "{{{
  let i = get(s:indent_tags, a:tagname)
  if (i == 1 || i == -1) && get(b:hi_removed_tags, a:tagname) != 0
    return 0
  endif
  if i == 0
    let i = get(b:hi_tags, a:tagname)
  endif
  return i
endfunc "}}}

" Count the number of start and end tags in "text".
func s:CountITags(text)
  "{{{
  " Store the result in s:curind and s:nextrel.
  let s:curind = 0  " relative indent steps for current line [unit &sw]:
  let s:nextrel = 0  " relative indent steps for next line [unit &sw]:
  let s:block = 0		" assume starting outside of a block
  let s:countonly = 1	" don't change state
  call substitute(a:text, '<\zs/\=' . s:tagname . '\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
  let s:countonly = 0
endfunc "}}}

" Count the number of start and end tags in text.
func s:CountTagsAndState(text)
  "{{{
  " Store the result in s:curind and s:nextrel.  Update b:hi_newstate.block.
  let s:curind = 0  " relative indent steps for current line [unit &sw]:
  let s:nextrel = 0  " relative indent steps for next line [unit &sw]:

  let s:block = b:hi_newstate.block
  let tmp = substitute(a:text, '<\zs/\=' . s:tagname . '\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
  if s:block == 3
    let b:hi_newstate.scripttype = s:GetScriptType(matchstr(tmp, '\C.*<SCRIPT\>\zs[^>]*'))
  endif
  let b:hi_newstate.block = s:block
endfunc "}}}

" Used by s:CountITags() and s:CountTagsAndState().
func s:CheckTag(itag)
  "{{{
  " Returns an empty string or "SCRIPT".
  " a:itag can be "tag" or "/tag" or "<!--" or "-->"
  if (s:CheckCustomTag(a:itag))
    return ""
  endif
  let ind = s:get_tag(a:itag)
  if ind == -1
    " closing tag
    if s:block != 0
      " ignore itag within a block
      return ""
    endif
    if s:nextrel == 0
      let s:curind -= 1
    else
      let s:nextrel -= 1
    endif
  elseif ind == 1
    " opening tag
    if s:block != 0
      return ""
    endif
    let s:nextrel += 1
  elseif ind != 0
    " block-tag (opening or closing)
    return s:CheckBlockTag(a:itag, ind)
  " else ind==0 (other tag found): keep indent
  endif
  return ""
endfunc "}}}

" Used by s:CheckTag(). Returns an empty string or "SCRIPT".
func s:CheckBlockTag(blocktag, ind)
  "{{{
  if a:ind > 0
    " a block starts here
    if s:block != 0
      " already in a block (nesting) - ignore
      " especially ignore comments after other blocktags
      return ""
    endif
    let s:block = a:ind		" block type
    if s:countonly
      return ""
    endif
    let b:hi_newstate.blocklnr = v:lnum
    " save allover indent for the endtag
    let b:hi_newstate.blocktagind = b:hi_indent.baseindent + (s:nextrel + s:curind) * shiftwidth()
    if a:ind == 3
      return "SCRIPT"    " all except this must be lowercase
      " line is to be checked again for the type attribute
    endif
  else
    let s:block = 0
    " we get here if starting and closing a block-tag on the same line
  endif
  return ""
endfunc "}}}

" Used by s:CheckTag().
func s:CheckCustomTag(ctag)
  "{{{
  " Returns 1 if ctag is the tag for a custom element, 0 otherwise.
  " a:ctag can be "tag" or "/tag" or "<!--" or "-->"
  let pattern = '\%\(\w\+-\)\+\w\+'
  if match(a:ctag, pattern) == -1
    return 0
  endif
  if matchstr(a:ctag, '\/\ze.\+') == "/"
    " closing tag
    if s:block != 0
      " ignore ctag within a block
      return 1
    endif
    if s:nextrel == 0
      let s:curind -= 1
    else
      let s:nextrel -= 1
    endif
  else
    " opening tag
    if s:block != 0
      return 1
    endif
    let s:nextrel += 1
  endif
  return 1
endfunc "}}}

" Return the <script> type: either "javascript" or ""
func s:GetScriptType(str)
  "{{{
  if a:str == "" || a:str =~ "java"
    return "javascript"
  else
    return ""
  endif
endfunc "}}}

" Look back in the file, starting at a:lnum - 1, to compute a state for the
" start of line a:lnum.  Return the new state.
func s:FreshState(lnum)
  "{{{
  " A state is to know ALL relevant details about the
  " lines 1..a:lnum-1, initial calculating (here!) can be slow, but updating is
  " fast (incremental).
  " TODO: this should be split up in detecting the block type and computing the
  " indent for the block type, so that when we do not know the indent we do
  " not need to clear the whole state and re-detect the block type again.
  " State:
  "	lnum		last indented line == prevnonblank(a:lnum - 1)
  "	block = 0	a:lnum located within special tag: 0:none, 2:<pre>,
  "			3:<script>, 4:<style>, 5:<!--, 6:<!--[
  "	baseindent	use this indent for line a:lnum as a start - kind of
  "			autoindent (if block==0)
  "	scripttype = ''	type attribute of a script tag (if block==3)
  "	blocktagind	indent for current opening (get) and closing (set)
  "			blocktag (if block!=0)
  "	blocklnr	lnum of starting blocktag (if block!=0)
  "	inattr		line {lnum} starts with attributes of a tag
  let state = {}
  let state.lnum = prevnonblank(a:lnum - 1)
  let state.scripttype = ""
  let state.blocktagind = -1
  let state.block = 0
  let state.baseindent = 0
  let state.blocklnr = 0
  let state.inattr = 0

  if state.lnum == 0
    return state
  endif

  " Heuristic:
  " remember startline state.lnum
  " look back for <pre, </pre, <script, </script, <style, </style tags
  " remember stopline
  " if opening tag found,
  "	assume a:lnum within block
  " else
  "	look back in result range (stopline, startline) for comment
  "	    \ delimiters (<!--, -->)
  "	if comment opener found,
  "	    assume a:lnum within comment
  "	else
  "	    assume usual html for a:lnum
  "	    if a:lnum-1 has a closing comment
  "		look back to get indent of comment opener
  " FI

  " look back for a blocktag
  let stopline2 = v:lnum + 1
  if has_key(b:hi_indent, 'block') && b:hi_indent.block > 5
    let [stopline2, stopcol2] = searchpos('<!--', 'bnW')
  endif
  let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bnW")
  if stopline > 0 && stopline < stopline2
    " ugly ... why isn't there searchstr()
    let tagline = tolower(getline(stopline))
    let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol - 1)
    if blocktag[0] != "/"
      " opening tag found, assume a:lnum within block
      let state.block = s:indent_tags[blocktag]
      if state.block == 3
        let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol))
      endif
      let state.blocklnr = stopline
      " check preceding tags in the line:
      call s:CountITags(tagline[: stopcol-2])
      let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * shiftwidth()
      return state
    elseif stopline == state.lnum
      " handle special case: previous line (= state.lnum) contains a
      " closing blocktag which is preceded by line-noise;
      " blocktag == "/..."
      let swendtag = match(tagline, '^\s*</') >= 0
      if !swendtag
        let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bnW")
        call s:CountITags(tolower(getline(bline)[: bcol-2]))
        let state.baseindent = indent(bline) + (s:curind + s:nextrel) * shiftwidth()
        return state
      endif
    endif
  endif
  if stopline > stopline2
    let stopline = stopline2
    let stopcol = stopcol2
  endif

  " else look back for comment
  let [comlnum, comcol, found] = searchpos('\(<!--\[\)\|\(<!--\)\|-->', 'bpnW', stopline)
  if found == 2 || found == 3
    " comment opener found, assume a:lnum within comment
    let state.block = (found == 3 ? 5 : 6)
    let state.blocklnr = comlnum
    " check preceding tags in the line:
    call s:CountITags(tolower(getline(comlnum)[: comcol-2]))
    if found == 2
      let state.baseindent = b:hi_indent.baseindent
    endif
    let state.blocktagind = indent(comlnum) + (s:curind + s:nextrel) * shiftwidth()
    return state
  endif

  " else within usual HTML
  let text = tolower(getline(state.lnum))

  " Check a:lnum-1 for closing comment (we need indent from the opening line).
  " Not when other tags follow (might be --> inside a string).
  let comcol = stridx(text, '-->')
  if comcol >= 0 && match(text, '[<>]', comcol) <= 0
    call cursor(state.lnum, comcol + 1)
    let [comlnum, comcol] = searchpos('<!--', 'bW')
    if comlnum == state.lnum
      let text = text[: comcol-2]
    else
      let text = tolower(getline(comlnum)[: comcol-2])
    endif
    call s:CountITags(text)
    let state.baseindent = indent(comlnum) + (s:curind + s:nextrel) * shiftwidth()
    " TODO check tags that follow "-->"
    return state
  endif

  " Check if the previous line starts with end tag.
  let swendtag = match(text, '^\s*</') >= 0

  " If previous line ended in a closing tag, line up with the opening tag.
  if !swendtag && text =~ '</' . s:tagname . '\s*>\s*$'
    call cursor(state.lnum, 99999)
    normal! F<
    let start_lnum = HtmlIndent_FindStartTag()
    if start_lnum > 0
      let state.baseindent = indent(start_lnum)
      if col('.') > 2
        " check for tags before the matching opening tag.
        let text = getline(start_lnum)
        let swendtag = match(text, '^\s*</') >= 0
        call s:CountITags(text[: col('.') - 2])
        let state.baseindent += s:nextrel * shiftwidth()
        if !swendtag
          let state.baseindent += s:curind * shiftwidth()
        endif
      endif
      return state
    endif
  endif

  " Else: no comments. Skip backwards to find the tag we're inside.
  let [state.lnum, found] = HtmlIndent_FindTagStart(state.lnum)
  " Check if that line starts with end tag.
  let text = getline(state.lnum)
  let swendtag = match(text, '^\s*</') >= 0
  call s:CountITags(tolower(text))
  let state.baseindent = indent(state.lnum) + s:nextrel * shiftwidth()
  if !swendtag
    let state.baseindent += s:curind * shiftwidth()
  endif
  return state
endfunc "}}}

" Indent inside a <pre> block: Keep indent as-is.
func s:Alien2()
  "{{{
  return -1
endfunc "}}}

" Return the indent inside a <script> block for javascript.
func s:Alien3()
  "{{{
  let lnum = prevnonblank(v:lnum - 1)
  while lnum > 1 && getline(lnum) =~ '^\s*/[/*]'
    " Skip over comments to avoid that cindent() aligns with the <script> tag
    let lnum = prevnonblank(lnum - 1)
  endwhile
  if lnum < b:hi_indent.blocklnr
    " indent for <script> itself
    return b:hi_indent.blocktagind
  endif
  if lnum == b:hi_indent.blocklnr
    " indent for the first line after <script>
    return eval(b:hi_js1indent)
  endif
  if b:hi_indent.scripttype == "javascript"
    " indent for further lines
    return GetJavascriptIndent()
  else
    return -1
  endif
endfunc "}}}

" Return the indent inside a <style> block.
func s:Alien4()
  "{{{
  if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr
    " indent for first content line
    return eval(b:hi_css1indent)
  endif
  return s:CSSIndent()
endfunc "}}}

" Indending inside a <style> block.  Returns the indent.
func s:CSSIndent()
  "{{{
  " This handles standard CSS and also Closure stylesheets where special lines
  " start with @.
  " When the line starts with '*' or the previous line starts with "/*"
  " and does not end in "*/", use C indenting to format the comment.
  " Adopted $VIMRUNTIME/indent/css.vim
  let curtext = getline(v:lnum)
  if curtext =~ '^\s*[*]'
        \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*'
        \     && getline(v:lnum - 1) !~ '\*/\s*$')
    return cindent(v:lnum)
  endif

  let min_lnum = b:hi_indent.blocklnr
  let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum)
  let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum)
  if prev_lnum <= min_lnum
    " Just below the <style> tag, indent for first content line after comments.
    return eval(b:hi_css1indent)
  endif

  " If the current line starts with "}" align with its match.
  if curtext =~ '^\s*}'
    call cursor(v:lnum, 1)
    try
      normal! %
      " Found the matching "{", align with it after skipping unfinished lines.
      let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
      return indent(align_lnum)
    catch
      " can't find it, try something else, but it's most likely going to be
      " wrong
    endtry
  endif

  " add indent after {
  let brace_counts = HtmlIndent_CountBraces(prev_lnum)
  let extra = brace_counts.c_open * shiftwidth()

  let prev_text = getline(prev_lnum)
  let below_end_brace = prev_text =~ '}\s*$'

  " Search back to align with the first line that's unfinished.
  let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum)

  " Handle continuation lines if aligning with previous line and not after a
  " "}".
  if extra == 0 && align_lnum == prev_lnum && !below_end_brace
    let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:'
    let prev_special = prev_text =~ '^\s*\(/\*\|@\)'
    if curtext =~ '^\s*\(/\*\|@\)'
      " if the current line is not a comment or starts with @ (used by template
      " systems) reduce indent if previous line is a continuation line
      if !prev_hasfield && !prev_special
        let extra = -shiftwidth()
      endif
    else
      let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:'
      let prev_unfinished = s:CssUnfinished(prev_text)
      if prev_unfinished
        " Continuation line has extra indent if the previous line was not a
        " continuation line.
        let extra = shiftwidth()
        " Align with @if
        if prev_text =~ '^\s*@if '
          let extra = 4
        endif
      elseif cur_hasfield && !prev_hasfield && !prev_special
        " less indent below a continuation line
        let extra = -shiftwidth()
      endif
    endif
  endif

  if below_end_brace
    " find matching {, if that line starts with @ it's not the start of a rule
    " but something else from a template system
    call cursor(prev_lnum, 1)
    call search('}\s*$')
    try
      normal! %
      " Found the matching "{", align with it.
      let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
      let special = getline(align_lnum) =~ '^\s*@'
    catch
      let special = 0
    endtry
    if special
      " do not reduce indent below @{ ... }
      if extra < 0
        let extra += shiftwidth()
      endif
    else
      let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * shiftwidth()
    endif
  endif

  " if no extra indent yet...
  if extra == 0
    if brace_counts.p_open > brace_counts.p_close
      " previous line has more ( than ): add a shiftwidth
      let extra = shiftwidth()
    elseif brace_counts.p_open < brace_counts.p_close
      " previous line has more ) than (: subtract a shiftwidth
      let extra = -shiftwidth()
    endif
  endif

  return indent(align_lnum) + extra
endfunc "}}}

" Inside <style>: Whether a line is unfinished.
" 	tag:
" 	tag: blah
" 	tag: blah &&
" 	tag: blah ||
func s:CssUnfinished(text)
  "{{{
  return a:text =~ '\(||\|&&\|:\|\k\)\s*$'
endfunc "}}}

" Search back for the first unfinished line above "lnum".
func s:CssFirstUnfinished(lnum, min_lnum)
  "{{{
  let align_lnum = a:lnum
  while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1))
    let align_lnum -= 1
  endwhile
  return align_lnum
endfunc "}}}

" Find the non-empty line at or before "lnum" that is not a comment.
func s:CssPrevNonComment(lnum, stopline)
  "{{{
  " caller starts from a line a:lnum + 1 that is not a comment
  let lnum = prevnonblank(a:lnum)
  while 1
    let ccol = match(getline(lnum), '\*/')
    if ccol < 0
      " No comment end thus it's something else.
      return lnum
    endif
    call cursor(lnum, ccol + 1)
    " Search back for the /* that starts the comment
    let lnum = search('/\*', 'bW', a:stopline)
    if indent(".") == virtcol(".") - 1
      " The  found /* is at the start of the line. Now go back to the line
      " above it and again check if it is a comment.
      let lnum = prevnonblank(lnum - 1)
    else
      " /* is after something else, thus it's not a comment line.
      return lnum
    endif
  endwhile
endfunc "}}}

" Check the number of {} and () in line "lnum". Return a dict with the counts.
func HtmlIndent_CountBraces(lnum)
  "{{{
  let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g')
  let c_open = 0
  let c_close = 0
  let p_open = 0
  let p_close = 0
  for brace in split(brs, '\zs')
    if brace == "{"
      let c_open += 1
    elseif brace == "}"
      if c_open > 0
        let c_open -= 1
      else
        let c_close += 1
      endif
    elseif brace == '('
      let p_open += 1
    elseif brace == ')'
      if p_open > 0
        let p_open -= 1
      else
        let p_close += 1
      endif
    endif
  endfor
  return {'c_open': c_open,
        \ 'c_close': c_close,
        \ 'p_open': p_open,
        \ 'p_close': p_close}
endfunc "}}}

" Return the indent for a comment: <!-- -->
func s:Alien5()
  "{{{
  let curtext = getline(v:lnum)
  if curtext =~ '^\s*\zs-->'
    " current line starts with end of comment, line up with comment start.
    call cursor(v:lnum, 0)
    let lnum = search('<!--', 'b')
    if lnum > 0
      " TODO: what if <!-- is not at the start of the line?
      return indent(lnum)
    endif

    " Strange, can't find it.
    return -1
  endif

  let prevlnum = prevnonblank(v:lnum - 1)
  let prevtext = getline(prevlnum)
  let idx = match(prevtext, '^\s*\zs<!--')
  if idx >= 0
    " just below comment start, add a shiftwidth
    return indent(prevlnum) + shiftwidth()
  endif

  " Some files add 4 spaces just below a TODO line.  It's difficult to detect
  " the end of the TODO, so let's not do that.

  " Align with the previous non-blank line.
  return indent(prevlnum)
endfunc "}}}

" Return the indent for conditional comment: <!--[ ![endif]-->
func s:Alien6()
  "{{{
  let curtext = getline(v:lnum)
  if curtext =~ '\s*\zs<!\[endif\]-->'
    " current line starts with end of comment, line up with comment start.
    let lnum = search('<!--', 'bn')
    if lnum > 0
      return indent(lnum)
    endif
  endif
  return b:hi_indent.baseindent + shiftwidth()
endfunc "}}}

" When the "lnum" line ends in ">" find the line containing the matching "<".
func HtmlIndent_FindTagStart(lnum)
  "{{{
  " Avoids using the indent of a continuation line.
  " Moves the cursor.
  " Return two values:
  " - the matching line number or "lnum".
  " - a flag indicating whether we found the end of a tag.
  " This method is global so that HTML-like indenters can use it.
  " To avoid matching " > " or " < " inside a string require that the opening
  " "<" is followed by a word character and the closing ">" comes after a
  " non-white character.
  let idx = match(getline(a:lnum), '\S>\s*$')
  if idx > 0
    call cursor(a:lnum, idx)
    let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0]))
    if lnum > 0
      return [lnum, 1]
    endif
  endif
  return [a:lnum, 0]
endfunc "}}}

" Find the unclosed start tag from the current cursor position.
func HtmlIndent_FindStartTag()
  "{{{
  " The cursor must be on or before a closing tag.
  " If found, positions the cursor at the match and returns the line number.
  " Otherwise returns 0.
  let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs' . s:tagname . '\ze')
  let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW')
  if start_lnum > 0
    return start_lnum
  endif
  return 0
endfunc "}}}

" Moves the cursor from a "<" to the matching ">".
func HtmlIndent_FindTagEnd()
  "{{{
  " Call this with the cursor on the "<" of a start tag.
  " This will move the cursor to the ">" of the matching end tag or, when it's
  " a self-closing tag, to the matching ">".
  " Limited to look up to b:html_indent_line_limit lines away.
  let text = getline('.')
  let tagname = matchstr(text, s:tagname . '\|!--', col('.'))
  if tagname == '!--'
    call search('--\zs>')
  elseif s:get_tag('/' . tagname) != 0
    " tag with a closing tag, find matching "</tag>"
    call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit)
  else
    " self-closing tag, find the ">"
    call search('\S\zs>')
  endif
endfunc "}}}

" Indenting inside a start tag. Return the correct indent or -1 if unknown.
func s:InsideTag(foundHtmlString)
  "{{{
  if a:foundHtmlString
    " Inside an attribute string.
    " Align with the opening quote or use an external function.
    let lnum = v:lnum - 1
    if lnum > 1
      if exists('b:html_indent_tag_string_func')
        return b:html_indent_tag_string_func(lnum)
      endif
      " If there is a double quote in the previous line, indent with the
      " character after it.
      if getline(lnum) =~ '"'
	call cursor(lnum, 0)
	normal f"
	return virtcol('.')
      endif
      return indent(lnum)
    endif
  endif

  " Should be another attribute: " attr="val".  Align with the previous
  " attribute start.
  let lnum = v:lnum
  while lnum > 1
    let lnum -= 1
    let text = getline(lnum)
    " Find a match with one of these, align with "attr":
    "       attr=
    "  <tag attr=
    "  text<tag attr=
    "  <tag>text</tag>text<tag attr=
    " For long lines search for the first match, finding the last match
    " gets very slow.
    if len(text) < 300
      let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
    else
      let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
    endif
    if idx == -1
      " try <tag attr
      let idx = match(text, '<' . s:tagname . '\s\+\zs\w')
    endif
    if idx == -1
      " after just "<tag" indent two levels more by default
      let idx = match(text, '<' . s:tagname . '$')
      if idx >= 0
	call cursor(lnum, idx + 1)
	return virtcol('.') - 1 + shiftwidth() * b:hi_attr_indent
      endif
    endif
    if idx > 0
      " Found the attribute to align with.
      call cursor(lnum, idx)
      return virtcol('.')
    endif
  endwhile
  return -1
endfunc "}}}

" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
func HtmlIndent()
  "{{{
  if prevnonblank(v:lnum - 1) < 1
    " First non-blank line has no indent.
    return 0
  endif

  let curtext = tolower(getline(v:lnum))
  let indentunit = shiftwidth()

  let b:hi_newstate = {}
  let b:hi_newstate.lnum = v:lnum

  " When syntax HL is enabled, detect we are inside a tag.  Indenting inside
  " a tag works very differently. Do not do this when the line starts with
  " "<", it gets the "htmlTag" ID but we are not inside a tag then.
  if curtext !~ '^\s*<'
    normal! ^
    let stack = synstack(v:lnum, col('.'))  " assumes there are no tabs
    let foundHtmlString = 0
    for synid in reverse(stack)
      let name = synIDattr(synid, "name")
      if index(b:hi_insideStringNames, name) >= 0
        let foundHtmlString = 1
      elseif index(b:hi_insideTagNames, name) >= 0
        " Yes, we are inside a tag.
        let indent = s:InsideTag(foundHtmlString)
        if indent >= 0
          " Do not keep the state. TODO: could keep the block type.
          let b:hi_indent.lnum = 0
          return indent
        endif
      endif
    endfor
  endif

  " does the line start with a closing tag?
  let swendtag = match(curtext, '^\s*</') >= 0

  if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
    " use state (continue from previous line)
  else
    " start over (know nothing)
    let b:hi_indent = s:FreshState(v:lnum)
  endif

  if b:hi_indent.block >= 2
    " within block
    let endtag = s:endtags[b:hi_indent.block]
    let blockend = stridx(curtext, endtag)
    if blockend >= 0
      " block ends here
      let b:hi_newstate.block = 0
      " calc indent for REST OF LINE (may start more blocks):
      call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
      if swendtag && b:hi_indent.block != 5
        let indent = b:hi_indent.blocktagind + s:curind * indentunit
        let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
      else
        let indent = s:Alien{b:hi_indent.block}()
        let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
      endif
    else
      " block continues
      " indent this line with alien method
      let indent = s:Alien{b:hi_indent.block}()
    endif
  else
    " not within a block - within usual html
    let b:hi_newstate.block = b:hi_indent.block
    if swendtag
      " The current line starts with an end tag, align with its start tag.
      call cursor(v:lnum, 1)
      let start_lnum = HtmlIndent_FindStartTag()
      if start_lnum > 0
        " check for the line starting with something inside a tag:
        " <sometag               <- align here
        "    attr=val><open>     not here
        let text = getline(start_lnum)
        let angle = matchstr(text, '[<>]')
        if angle == '>'
          call cursor(start_lnum, 1)
          normal! f>%
          let start_lnum = line('.')
          let text = getline(start_lnum)
        endif

        let indent = indent(start_lnum)
        if col('.') > 2
          let swendtag = match(text, '^\s*</') >= 0
          call s:CountITags(text[: col('.') - 2])
          let indent += s:nextrel * shiftwidth()
          if !swendtag
            let indent += s:curind * shiftwidth()
          endif
        endif
      else
        " not sure what to do
        let indent = b:hi_indent.baseindent
      endif
      let b:hi_newstate.baseindent = indent
    else
      call s:CountTagsAndState(curtext)
      let indent = b:hi_indent.baseindent
      let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
    endif
  endif

  let b:hi_lasttick = b:changedtick
  call extend(b:hi_indent, b:hi_newstate, "force")
  return indent
endfunc "}}}

" Check user settings when loading this script the first time.
call HtmlIndent_CheckUserSettings()

let &cpo = s:cpo_save
unlet s:cpo_save

" vim: fdm=marker ts=8 sw=2 tw=78