1" Vim completion script 2" Language: XHTML 1.0 Strict 3" Maintainer: Mikolaj Machowski ( mikmach AT wp DOT pl ) 4" Last Change: 2006 Feb 6 5 6function! htmlcomplete#CompleteTags(findstart, base) 7 if a:findstart 8 " locate the start of the word 9 let line = getline('.') 10 let start = col('.') - 1 11 let curline = line('.') 12 let compl_begin = col('.') - 2 13 while start >= 0 && line[start - 1] =~ '\(\k\|[:.-]\)' 14 let start -= 1 15 endwhile 16 " Handling of entities {{{ 17 if start >= 0 && line[start - 1] =~ '&' 18 let b:entitiescompl = 1 19 let b:compl_context = '' 20 return start 21 endif 22 " }}} 23 " Handling of <style> tag {{{ 24 let stylestart = searchpair('<style\>', '', '<\/style\>', "bnW") 25 let styleend = searchpair('<style\>', '', '<\/style\>', "nW") 26 if stylestart != 0 && styleend != 0 27 if stylestart <= curline && styleend >= curline 28 let start = col('.') - 1 29 let b:csscompl = 1 30 while start >= 0 && line[start - 1] =~ '\(\k\|-\)' 31 let start -= 1 32 endwhile 33 endif 34 endif 35 " }}} 36 " Handling of <script> tag {{{ 37 let scriptstart = searchpair('<script\>', '', '<\/script\>', "bnW") 38 let scriptend = searchpair('<script\>', '', '<\/script\>', "nW") 39 if scriptstart != 0 && scriptend != 0 40 if scriptstart <= curline && scriptend >= curline 41 let start = col('.') - 1 42 let b:jscompl = 1 43 let b:jsrange = [scriptstart, scriptend] 44 while start >= 0 && line[start - 1] =~ '\k' 45 let start -= 1 46 endwhile 47 " We are inside of <script> tag. But we should also get contents 48 " of all linked external files and (secondary, less probably) other <script> tags 49 " This logic could possible be done in separate function - may be 50 " reused in events scripting (also with option could be reused for 51 " CSS 52 let b:js_extfiles = [] 53 let l = line('.') 54 let c = col('.') 55 call cursor(1,1) 56 while search('<\@<=script\>', 'W') && line('.') <= l 57 if synIDattr(synID(line('.'),col('.')-1,0),"name") !~? 'comment' 58 let sname = matchstr(getline('.'), '<script[^>]*src\s*=\s*\([''"]\)\zs.\{-}\ze\1') 59 if filereadable(sname) 60 let b:js_extfiles += readfile(sname) 61 endif 62 endif 63 endwhile 64 call cursor(1,1) 65 let js_scripttags = [] 66 while search('<script\>', 'W') && line('.') < l 67 if matchstr(getline('.'), '<script[^>]*src') == '' 68 let js_scripttag = getline(line('.'), search('</script>', 'W')) 69 let js_scripttags += js_scripttag 70 endif 71 endwhile 72 let b:js_extfiles += js_scripttags 73 call cursor(l,c) 74 unlet! l c 75 endif 76 endif 77 " }}} 78 if !exists("b:csscompl") && !exists("b:jscompl") 79 let b:compl_context = getline('.')[0:(compl_begin)] 80 if b:compl_context !~ '<[^>]*$' 81 " Look like we may have broken tag. Check previous lines. 82 let i = 1 83 while 1 84 let context_line = getline(curline-i) 85 if context_line =~ '<[^>]*$' 86 " Yep, this is this line 87 let context_lines = getline(curline-i, curline) 88 let b:compl_context = join(context_lines, ' ') 89 break 90 elseif context_line =~ '>[^<]*$' 91 " Normal tag line, no need for completion at all 92 let b:compl_context = '' 93 break 94 endif 95 let i += 1 96 endwhile 97 " Make sure we don't have counter 98 unlet! i 99 endif 100 let b:compl_context = matchstr(b:compl_context, '.*\zs<.*') 101 " Return proper start for on-events. Without that beginning of 102 " completion will be badly reported 103 if b:compl_context =~? 'on[a-z]*\s*=\s*\(''[^'']*\|"[^"]*\)$' 104 let start = col('.') - 1 105 while start >= 0 && line[start - 1] =~ '\k' 106 let start -= 1 107 endwhile 108 endif 109 else 110 let b:compl_context = getline('.')[0:compl_begin] 111 endif 112 return start 113 else 114 " Initialize base return lists 115 let res = [] 116 let res2 = [] 117 " a:base is very short - we need context 118 let context = b:compl_context 119 " Check if we should do CSS completion inside of <style> tag 120 " or JS completion inside of <script> tag 121 if exists("b:csscompl") 122 unlet! b:csscompl 123 let context = b:compl_context 124 unlet! b:compl_context 125 return csscomplete#CompleteCSS(0, context) 126 elseif exists("b:jscompl") 127 unlet! b:jscompl 128 return javascriptcomplete#CompleteJS(0, a:base) 129 else 130 if len(b:compl_context) == 0 && !exists("b:entitiescompl") 131 return [] 132 endif 133 let context = matchstr(b:compl_context, '.\zs.*') 134 endif 135 unlet! b:compl_context 136 " Entities completion {{{ 137 if exists("b:entitiescompl") 138 unlet! b:entitiescompl 139 140 if !exists("g:xmldata_xhtml10s") 141 runtime! autoload/xml/xhtml10s.vim 142 endif 143 144 let entities = g:xmldata_xhtml10s['vimxmlentities'] 145 146 if len(a:base) == 1 147 for m in entities 148 if m =~ '^'.a:base 149 call add(res, m.';') 150 endif 151 endfor 152 return res 153 else 154 for m in entities 155 if m =~? '^'.a:base 156 call add(res, m.';') 157 elseif m =~? a:base 158 call add(res2, m.';') 159 endif 160 endfor 161 162 return res + res2 163 endif 164 165 166 endif 167 " }}} 168 if context =~ '>' 169 " Generally if context contains > it means we are outside of tag and 170 " should abandon action - with one exception: <style> span { bo 171 if context =~ 'style[^>]\{-}>[^<]\{-}$' 172 return csscomplete#CompleteCSS(0, context) 173 elseif context =~ 'script[^>]\{-}>[^<]\{-}$' 174 let b:jsrange = [line('.'), search('<\/script\>', 'nW')] 175 return javascriptcomplete#CompleteJS(0, context) 176 else 177 return [] 178 endif 179 endif 180 181 " If context contains > it means we are already outside of tag and we 182 " should abandon action 183 " If context contains white space it is attribute. 184 " It can be also value of attribute. 185 " We have to get first word to offer proper completions 186 if context == '' 187 let tag = '' 188 else 189 let tag = split(context)[0] 190 endif 191 " Get last word, it should be attr name 192 let attr = matchstr(context, '.*\s\zs.*') 193 " Possible situations where any prediction would be difficult: 194 " 1. Events attributes 195 if context =~ '\s' 196 " Sort out style, class, and on* cases 197 if context =~? "\\(on[a-z]*\\|id\\|style\\|class\\)\\s*=\\s*[\"']" 198 " Id, class completion {{{ 199 if context =~? "\\(id\\|class\\)\\s*=\\s*[\"'][a-zA-Z0-9_ -]*$" 200 if context =~? "class\\s*=\\s*[\"'][a-zA-Z0-9_ -]*$" 201 let search_for = "class" 202 elseif context =~? "id\\s*=\\s*[\"'][a-zA-Z0-9_ -]*$" 203 let search_for = "id" 204 endif 205 " Handle class name completion 206 " 1. Find lines of <link stylesheet> 207 " 1a. Check file for @import 208 " 2. Extract filename(s?) of stylesheet, 209 call cursor(1,1) 210 let head = getline(search('<head\>'), search('<\/head>')) 211 let headjoined = join(copy(head), ' ') 212 if headjoined =~ '<style' 213 " Remove possibly confusing CSS operators 214 let stylehead = substitute(headjoined, '+>\*[,', ' ', 'g') 215 if search_for == 'class' 216 let styleheadlines = split(stylehead) 217 let headclasslines = filter(copy(styleheadlines), "v:val =~ '\\([a-zA-Z0-9:]\\+\\)\\?\\.[a-zA-Z0-9_-]\\+'") 218 else 219 let stylesheet = split(headjoined, '[{}]') 220 " Get all lines which fit id syntax 221 let classlines = filter(copy(stylesheet), "v:val =~ '#[a-zA-Z0-9_-]\\+'") 222 " Filter out possible color definitions 223 call filter(classlines, "v:val !~ ':\\s*#[a-zA-Z0-9_-]\\+'") 224 " Filter out complex border definitions 225 call filter(classlines, "v:val !~ '\\(none\\|hidden\\|dotted\\|dashed\\|solid\\|double\\|groove\\|ridge\\|inset\\|outset\\)\\s*#[a-zA-Z0-9_-]\\+'") 226 let templines = join(classlines, ' ') 227 let headclasslines = split(templines) 228 call filter(headclasslines, "v:val =~ '#[a-zA-Z0-9_-]\\+'") 229 endif 230 let internal = 1 231 else 232 let internal = 0 233 endif 234 let styletable = [] 235 let secimportfiles = [] 236 let filestable = filter(copy(head), "v:val =~ '\\(@import\\|link.*stylesheet\\)'") 237 for line in filestable 238 if line =~ "@import" 239 let styletable += [matchstr(line, "import\\s\\+\\(url(\\)\\?[\"']\\?\\zs\\f\\+\\ze")] 240 elseif line =~ "<link" 241 let styletable += [matchstr(line, "href\\s*=\\s*[\"']\\zs\\f\\+\\ze")] 242 endif 243 endfor 244 for file in styletable 245 if filereadable(file) 246 let stylesheet = readfile(file) 247 let secimport = filter(copy(stylesheet), "v:val =~ '@import'") 248 if len(secimport) > 0 249 for line in secimport 250 let secfile = matchstr(line, "import\\s\\+\\(url(\\)\\?[\"']\\?\\zs\\f\\+\\ze") 251 let secfile = fnamemodify(file, ":p:h").'/'.secfile 252 let secimportfiles += [secfile] 253 endfor 254 endif 255 endif 256 endfor 257 let cssfiles = styletable + secimportfiles 258 let classes = [] 259 for file in cssfiles 260 if filereadable(file) 261 let stylesheet = readfile(file) 262 let stylefile = join(stylesheet, ' ') 263 let stylefile = substitute(stylefile, '+>\*[,', ' ', 'g') 264 if search_for == 'class' 265 let stylesheet = split(stylefile) 266 let classlines = filter(copy(stylesheet), "v:val =~ '\\([a-zA-Z0-9:]\\+\\)\\?\\.[a-zA-Z0-9_-]\\+'") 267 else 268 let stylesheet = split(stylefile, '[{}]') 269 " Get all lines which fit id syntax 270 let classlines = filter(copy(stylesheet), "v:val =~ '#[a-zA-Z0-9_-]\\+'") 271 " Filter out possible color definitions 272 call filter(classlines, "v:val !~ ':\\s*#[a-zA-Z0-9_-]\\+'") 273 " Filter out complex border definitions 274 call filter(classlines, "v:val !~ '\\(none\\|hidden\\|dotted\\|dashed\\|solid\\|double\\|groove\\|ridge\\|inset\\|outset\\)\\s*#[a-zA-Z0-9_-]\\+'") 275 let templines = join(classlines, ' ') 276 let stylelines = split(templines) 277 let classlines = filter(stylelines, "v:val =~ '#[a-zA-Z0-9_-]\\+'") 278 279 endif 280 endif 281 " We gathered classes definitions from all external files 282 let classes += classlines 283 endfor 284 if internal == 1 285 let classes += headclasslines 286 endif 287 288 if search_for == 'class' 289 let elements = {} 290 for element in classes 291 if element =~ '^\.' 292 let class = matchstr(element, '^\.\zs[a-zA-Z][a-zA-Z0-9_-]*\ze') 293 let class = substitute(class, ':.*', '', '') 294 if has_key(elements, 'common') 295 let elements['common'] .= ' '.class 296 else 297 let elements['common'] = class 298 endif 299 else 300 let class = matchstr(element, '[a-zA-Z1-6]*\.\zs[a-zA-Z][a-zA-Z0-9_-]*\ze') 301 let tagname = tolower(matchstr(element, '[a-zA-Z1-6]*\ze.')) 302 if tagname != '' 303 if has_key(elements, tagname) 304 let elements[tagname] .= ' '.class 305 else 306 let elements[tagname] = class 307 endif 308 endif 309 endif 310 endfor 311 312 if has_key(elements, tag) && has_key(elements, 'common') 313 let values = split(elements[tag]." ".elements['common']) 314 elseif has_key(elements, tag) && !has_key(elements, 'common') 315 let values = split(elements[tag]) 316 elseif !has_key(elements, tag) && has_key(elements, 'common') 317 let values = split(elements['common']) 318 else 319 return [] 320 endif 321 322 elseif search_for == 'id' 323 " Find used IDs 324 " 1. Catch whole file 325 let filelines = getline(1, line('$')) 326 " 2. Find lines with possible id 327 let used_id_lines = filter(filelines, 'v:val =~ "id\\s*=\\s*[\"''][a-zA-Z0-9_-]\\+"') 328 " 3a. Join all filtered lines 329 let id_string = join(used_id_lines, ' ') 330 " 3b. And split them to be sure each id is in separate item 331 let id_list = split(id_string, 'id\s*=\s*') 332 " 4. Extract id values 333 let used_id = map(id_list, 'matchstr(v:val, "[\"'']\\zs[a-zA-Z0-9_-]\\+\\ze")') 334 let joined_used_id = ','.join(used_id, ',').',' 335 336 let allvalues = map(classes, 'matchstr(v:val, ".*#\\zs[a-zA-Z0-9_-]\\+")') 337 338 let values = [] 339 340 for element in classes 341 if joined_used_id !~ ','.element.',' 342 let values += [element] 343 endif 344 345 endfor 346 347 endif 348 349 " We need special version of sbase 350 let classbase = matchstr(context, ".*[\"']") 351 let classquote = matchstr(classbase, '.$') 352 353 let entered_class = matchstr(attr, ".*=\\s*[\"']\\zs.*") 354 355 for m in sort(values) 356 if m =~? '^'.entered_class 357 call add(res, m . classquote) 358 elseif m =~? entered_class 359 call add(res2, m . classquote) 360 endif 361 endfor 362 363 return res + res2 364 365 elseif context =~? "style\\s*=\\s*[\"'][^\"']*$" 366 return csscomplete#CompleteCSS(0, context) 367 368 endif 369 " }}} 370 " Complete on-events {{{ 371 if context =~? 'on[a-z]*\s*=\s*\(''[^'']*\|"[^"]*\)$' 372 " We have to: 373 " 1. Find external files 374 let b:js_extfiles = [] 375 let l = line('.') 376 let c = col('.') 377 call cursor(1,1) 378 while search('<\@<=script\>', 'W') && line('.') <= l 379 if synIDattr(synID(line('.'),col('.')-1,0),"name") !~? 'comment' 380 let sname = matchstr(getline('.'), '<script[^>]*src\s*=\s*\([''"]\)\zs.\{-}\ze\1') 381 if filereadable(sname) 382 let b:js_extfiles += readfile(sname) 383 endif 384 endif 385 endwhile 386 " 2. Find at least one <script> tag 387 call cursor(1,1) 388 let js_scripttags = [] 389 while search('<script\>', 'W') && line('.') < l 390 if matchstr(getline('.'), '<script[^>]*src') == '' 391 let js_scripttag = getline(line('.'), search('</script>', 'W')) 392 let js_scripttags += js_scripttag 393 endif 394 endwhile 395 let b:js_extfiles += js_scripttags 396 397 " 3. Proper call for javascriptcomplete#CompleteJS 398 call cursor(l,c) 399 let js_context = matchstr(a:base, '\k\+$') 400 let js_shortcontext = substitute(a:base, js_context.'$', '', '') 401 let b:compl_context = context 402 let b:jsrange = [l, l] 403 unlet! l c 404 return javascriptcomplete#CompleteJS(0, js_context) 405 406 endif 407 408 " }}} 409 let stripbase = matchstr(context, ".*\\(on[a-zA-Z]*\\|style\\|class\\)\\s*=\\s*[\"']\\zs.*") 410 " Now we have context stripped from all chars up to style/class. 411 " It may fail with some strange style value combinations. 412 if stripbase !~ "[\"']" 413 return [] 414 endif 415 endif 416 " Value of attribute completion {{{ 417 " If attr contains =\s*[\"'] we catched value of attribute 418 if attr =~ "=\s*[\"']" 419 " Let do attribute specific completion 420 let attrname = matchstr(attr, '.*\ze\s*=') 421 let entered_value = matchstr(attr, ".*=\\s*[\"']\\zs.*") 422 let values = [] 423 if attrname == 'media' 424 let values = ["screen", "tty", "tv", "projection", "handheld", "print", "braille", "aural", "all"] 425 elseif attrname == 'xml:space' 426 let values = ["preserve"] 427 elseif attrname == 'shape' 428 let values = ["rect", "circle", "poly", "default"] 429 elseif attrname == 'valuetype' 430 let values = ["data", "ref", "object"] 431 elseif attrname == 'method' 432 let values = ["get", "post"] 433 elseif attrname == 'dir' 434 let values = ["ltr", "rtl"] 435 elseif attrname == 'frame' 436 let values = ["void", "above", "below", "hsides", "lhs", "rhs", "vsides", "box", "border"] 437 elseif attrname == 'rules' 438 let values = ["none", "groups", "rows", "all"] 439 elseif attrname == 'align' 440 let values = ["left", "center", "right", "justify", "char"] 441 elseif attrname == 'valign' 442 let values = ["top", "middle", "bottom", "baseline"] 443 elseif attrname == 'scope' 444 let values = ["row", "col", "rowgroup", "colgroup"] 445 elseif attrname == 'href' 446 " Now we are looking for local anchors defined by name or id 447 if entered_value =~ '^#' 448 let file = join(getline(1, line('$')), ' ') 449 " Split it be sure there will be one id/name element in 450 " item, it will be also first word [a-zA-Z0-9_-] in element 451 let oneelement = split(file, "\\(meta \\)\\@<!\\(name\\|id\\)\\s*=\\s*[\"']") 452 for i in oneelement 453 let values += ['#'.matchstr(i, "^[a-zA-Z][a-zA-Z0-9%_-]*")] 454 endfor 455 endif 456 elseif attrname == 'type' 457 if context =~ '^input' 458 let values = ["text", "password", "checkbox", "radio", "submit", "reset", "file", "hidden", "image", "button"] 459 elseif context =~ '^button' 460 let values = ["button", "submit", "reset"] 461 elseif context =~ '^style' 462 let values = ["text/css"] 463 elseif context =~ '^script' 464 let values = ["text/javascript"] 465 endif 466 else 467 return [] 468 endif 469 470 if len(values) == 0 471 return [] 472 endif 473 474 " We need special version of sbase 475 let attrbase = matchstr(context, ".*[\"']") 476 let attrquote = matchstr(attrbase, '.$') 477 478 for m in values 479 " This if is needed to not offer all completions as-is 480 " alphabetically but sort them. Those beginning with entered 481 " part will be as first choices 482 if m =~ '^'.entered_value 483 call add(res, m . attrquote.' ') 484 elseif m =~ entered_value 485 call add(res2, m . attrquote.' ') 486 endif 487 endfor 488 489 return res + res2 490 491 endif 492 " }}} 493 " Attribute completion {{{ 494 " Shorten context to not include last word 495 let sbase = matchstr(context, '.*\ze\s.*') 496 497 " Load data {{{ 498 if !exists("g:xmldata_xhtml10s") 499 runtime! autoload/xml/xhtml10s.vim 500 endif 501 " }}} 502 " 503 let attrs = keys(g:xmldata_xhtml10s[tag][1]) 504 505 for m in sort(attrs) 506 if m =~ '^'.attr 507 call add(res, m) 508 elseif m =~ attr 509 call add(res2, m) 510 endif 511 endfor 512 let menu = res + res2 513 if has_key(g:xmldata_xhtml10s, 'vimxmlattrinfo') 514 let final_menu = [] 515 for i in range(len(menu)) 516 let item = menu[i] 517 if has_key(g:xmldata_xhtml10s['vimxmlattrinfo'], item) 518 let m_menu = g:xmldata_xhtml10s['vimxmlattrinfo'][item][0] 519 let m_info = g:xmldata_xhtml10s['vimxmlattrinfo'][item][1] 520 if m_menu !~ 'Bool' 521 let item .= '="' 522 endif 523 else 524 let m_menu = '' 525 let m_info = '' 526 let item .= '="' 527 endif 528 let final_menu += [{'word':item, 'menu':m_menu, 'info':m_info}] 529 endfor 530 else 531 let final_menu = map(menu, 'v:val."=\""') 532 endif 533 return final_menu 534 535 endif 536 " }}} 537 " Close tag {{{ 538 let b:unaryTagsStack = "base meta link hr br param img area input col" 539 if context =~ '^\/' 540 let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack") 541 return [opentag.">"] 542 endif 543 " Deal with tag completion. 544 let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack") 545 if opentag == '' 546 " Hack for sometimes failing GetLastOpenTag. 547 " As far as I tested fail isn't GLOT fault but problem 548 " of invalid document - not properly closed tags and other mish-mash. 549 " If returns empty string assume <body>. Safe bet. 550 let opentag = 'body' 551 endif 552 " }}} 553 " Load data {{{ 554 if !exists("g:xmldata_xhtml10s") 555 runtime! autoload/xml/xhtml10s.vim 556 endif 557 " }}} 558 " Tag completion {{{ 559 560 let tags = g:xmldata_xhtml10s[opentag][0] 561 562 for m in sort(tags) 563 if m =~ '^'.context 564 call add(res, m) 565 elseif m =~ context 566 call add(res2, m) 567 endif 568 endfor 569 let menu = res + res2 570 if has_key(g:xmldata_xhtml10s, 'vimxmltaginfo') 571 let final_menu = [] 572 for i in range(len(menu)) 573 let item = menu[i] 574 if has_key(g:xmldata_xhtml10s['vimxmltaginfo'], item) 575 let m_menu = g:xmldata_xhtml10s['vimxmltaginfo'][item][0] 576 let m_info = g:xmldata_xhtml10s['vimxmltaginfo'][item][1] 577 else 578 let m_menu = '' 579 let m_info = '' 580 endif 581 let final_menu += [{'word':item, 'menu':m_menu, 'info':m_info}] 582 endfor 583 else 584 let final_menu = menu 585 endif 586 return final_menu 587 588 589 " }}} 590 endif 591endfunction 592" vim:set foldmethod=marker: 593