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