lectures.alex.balgavy.eu

Lecture notes from university.
git clone git://git.alex.balgavy.eu/lectures.alex.balgavy.eu.git
Log | Files | Refs | Submodules

html_completions.py (21951B)


      1 import sublime, sublime_plugin
      2 import re
      3 
      4 
      5 def match(rex, str):
      6     m = rex.match(str)
      7     if m:
      8         return m.group(0)
      9     else:
     10         return None
     11 
     12 def make_completion(tag):
     13     # make it look like
     14     # ("table\tTag", "table>$1</table>"),
     15     return (tag + '\tTag', tag + '>$0</' + tag + '>')
     16 
     17 def get_tag_to_attributes():
     18     """
     19     Returns a dictionary with attributes accociated to tags
     20     This assumes that all tags can have global attributes as per MDN:
     21     https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
     22     """
     23 
     24     # Map tags to specific attributes applicable for that tag
     25     tag_dict = {
     26         'a' : ['charset', 'coords', 'download', 'href', 'hreflang', 'media', 'name', 'ping', 'rel', 'rev', 'shape', 'target', 'type'],
     27         'abbr' : ['title'],
     28         'address' : [],
     29         'applet' : ['align', 'alt', 'archive', 'code', 'codebase', 'height', 'hspace', 'name', 'object', 'vspace', 'width'],
     30         'area' : ['alt', 'coords', 'download', 'href', 'hreflang', 'media', 'nohref', 'rel', 'shape', 'target'],
     31         'article' : [],
     32         'aside' : [],
     33         'audio' : ['autoplay', 'buffered', 'controls', 'loop', 'muted', 'played', 'preload', 'src', 'volume'],
     34         'b' : [],
     35         'base' : ['href', 'target'],
     36         'basefont' : ['color', 'face', 'size'],
     37         'bdi' : [],
     38         'bdo' : [],
     39         'blockquote' : ['cite'],
     40         'body' : ['alink', 'background', 'bgcolor', 'link', 'onafterprint', 'onbeforeprint', 'onbeforeunload', 'onhashchange', 'onmessage', 'onoffline', 'ononline', 'onpopstate', 'onredo', 'onstorage', 'onundo', 'onunload', 'text', 'vlink'],
     41         'br' : ['clear'],
     42         'button' : ['autofocus', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'name', 'type', 'value'],
     43         'canvas' : ['height', 'width'],
     44         'caption' : ['align'],
     45         'cite' : [],
     46         'code' : [],
     47         'col' : ['align', 'char', 'charoff', 'span', 'valign', 'width'],
     48         'colgroup' : ['align', 'char', 'charoff', 'span', 'valign', 'width'],
     49         'content' : ['select'],
     50         'data' : ['value'],
     51         'datalist' : [],
     52         'dd' : [],
     53         'del' : ['cite', 'datetime'],
     54         'details' : ['open'],
     55         'dfn' : [],
     56         'dir' : ['compact'],
     57         'div' : ['align'],
     58         'dl' : ['compact'],
     59         'dt' : [],
     60         'element' : [],
     61         'em' : [],
     62         'embed' : ['height', 'src', 'type', 'width'],
     63         'fieldset' : ['disabled', 'form', 'name'],
     64         'figcaption' : [],
     65         'figure' : [],
     66         'font' : ['color', 'face', 'size'],
     67         'footer' : [],
     68         'form' : ['accept-charset', 'accept', 'action', 'autocomplete', 'enctype', 'method', 'name', 'novalidate', 'target'],
     69         'frame' : ['frameborder', 'longdesc', 'marginheight', 'marginwidth', 'name', 'noresize', 'scrolling', 'src'],
     70         'frameset' : ['cols', 'onunload', 'rows'],
     71         'h1' : ['align'],
     72         'h2' : ['align'],
     73         'h3' : ['align'],
     74         'h4' : ['align'],
     75         'h5' : ['align'],
     76         'h6' : ['align'],
     77         'head' : ['profile'],
     78         'header' : [],
     79         'hr' : ['align', 'noshade', 'size', 'width'],
     80         'html' : ['manifest', 'version', 'xmlns'],
     81         'i' : [],
     82         'iframe' : ['align', 'frameborder', 'height', 'longdesc', 'marginheight', 'marginwidth', 'name', 'sandbox', 'scrolling', 'seamless', 'src', 'srcdoc', 'width'],
     83         'img' : ['align', 'alt', 'border', 'crossorigin', 'height', 'hspace', 'ismap', 'longdesc', 'name', 'sizes', 'src', 'srcset', 'usemap', 'vspace', 'width'],
     84         'input' : ['accept', 'align', 'alt', 'autocomplete', 'autofocus', 'autosave', 'checked', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'height', 'inputmode', 'ismap', 'list', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'selectionDirection', 'size', 'spellcheck', 'src', 'step', 'tabindex', 'type', 'usemap', 'value', 'width'],
     85         'ins' : ['cite', 'datetime'],
     86         'isindex' : ['prompt'],
     87         'kbd' : [],
     88         'keygen' : ['autofocus', 'challenge', 'disabled', 'form', 'keytype', 'name'],
     89         'label' : ['for', 'form'],
     90         'legend' : [],
     91         'li' : ['type', 'value'],
     92         'link' : ['charset', 'crossorigin', 'href', 'hreflang', 'media', 'rel', 'rev', 'sizes', 'target', 'type'],
     93         'main' : [],
     94         'map' : ['name'],
     95         'mark' : [],
     96         'menu' : ['compact'],
     97         'meta' : ['charset', 'content', 'http-equiv', 'name', 'scheme'],
     98         'meter' : ['value', 'min', 'max', 'low', 'high', 'optimum', 'form'],
     99         'nav' : [],
    100         'noframes' : [],
    101         'noscript' : [],
    102         'object' : ['align', 'archive', 'border', 'classid', 'codebase', 'codetype', 'data', 'declare', 'form', 'height', 'hspace', 'name', 'standby', 'type', 'typemustmatch', 'usemap', 'vspace', 'width'],
    103         'ol' : ['compact', 'reversed', 'start', 'type'],
    104         'optgroup' : ['disabled', 'label'],
    105         'option' : ['disabled', 'label', 'selected', 'value'],
    106         'output' : ['for', 'form', 'name'],
    107         'p' : ['align'],
    108         'param' : ['name', 'type', 'value', 'valuetype'],
    109         'picture' : [],
    110         'pre' : ['width'],
    111         'progress' : ['max', 'value'],
    112         'q' : ['cite'],
    113         'rp' : [],
    114         'rt' : [],
    115         'rtc' : [],
    116         's' : [],
    117         'samp' : [],
    118         'script' : ['async', 'charset', 'defer', 'language', 'src', 'type'],
    119         'section' : [],
    120         'select' : ['autofocus', 'disabled', 'form', 'multiple', 'name', 'required', 'size'],
    121         'shadow' : [],
    122         'small' : [],
    123         'source' : ['src', 'type'],
    124         'span' : [],
    125         'strong' : [],
    126         'style' : ['disabled', 'media', 'scoped', 'title', 'type'],
    127         'sub' : [],
    128         'summary': [],
    129         'sup' : [],
    130         'table' : ['align', 'bgcolor', 'border', 'cellpadding', 'cellspacing', 'frame', 'rules', 'summary', 'width'],
    131         'tbody' : ['align', 'char', 'charoff', 'valign'],
    132         'td' : ['abbr', 'align', 'axis', 'bgcolor', 'char', 'charoff', 'colspan', 'headers', 'height', 'nowrap', 'rowspan', 'scope', 'valign', 'width'],
    133         'template' : ['content'],
    134         'textarea' : ['autocomplete', 'autofocus', 'cols', 'disabled', 'form', 'maxlength', 'minlength', 'name', 'placeholder', 'readonly', 'required', 'rows', 'selectionDirection', 'selectionEnd', 'selectionStart', 'spellcheck', 'wrap'],
    135         'tfoot' : ['align', 'char', 'charoff', 'valign'],
    136         'th' : ['abbr', 'align', 'axis', 'bgcolor', 'char', 'charoff', 'colspan', 'headers', 'height', 'nowrap', 'rowspan', 'scope', 'valign', 'width'],
    137         'thead' : ['align', 'char', 'charoff', 'valign'],
    138         'time' : ['datetime'],
    139         'title' : [],
    140         'tr' : ['align', 'bgcolor', 'char', 'charoff', 'valign'],
    141         'track' : ['default', 'kind', 'label', 'src', 'srclang'],
    142         'u' : [],
    143         'ul' : ['compact', 'type'],
    144         'var' : [],
    145         'video' : ['autoplay', 'autobuffer', 'buffered', 'controls', 'crossorigin', 'height', 'loop', 'muted', 'played', 'preload', 'poster', 'src', 'width'],
    146         'wbr' : []
    147     }
    148 
    149     # Assume that global attributes are common to all HTML elements
    150     global_attributes = [
    151         'accesskey', 'class', 'contenteditable', 'contextmenu', 'dir',
    152         'hidden', 'id', 'lang', 'style', 'tabindex', 'title', 'translate'
    153     ]
    154 
    155     # Extend `global_attributes` by the event handler attributes
    156     global_attributes.extend([
    157         'onabort', 'onautocomplete', 'onautocompleteerror', 'onauxclick', 'onblur',
    158         'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick',
    159         'onclose', 'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag',
    160         'ondragend', 'ondragenter', 'ondragexit', 'ondragleave', 'ondragover',
    161         'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended',
    162         'onerror', 'onfocus', 'oninput', 'oninvalid', 'onkeydown',
    163         'onkeypress', 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata',
    164         'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave',
    165         'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup',
    166         'onmousewheel', 'onpause', 'onplay', 'onplaying', 'onprogress',
    167         'onratechange', 'onreset', 'onresize', 'onscroll', 'onseeked',
    168         'onseeking', 'onselect', 'onshow', 'onsort', 'onstalled', 'onsubmit',
    169         'onsuspend', 'ontimeupdate', 'ontoggle', 'onvolumechange', 'onwaiting'
    170     ])
    171 
    172     for attributes in tag_dict.values():
    173         attributes.extend(global_attributes)
    174 
    175     # Remove `dir` attribute from `bdi` key, because it is *not* inherited
    176     # from the global attributes
    177     if 'bdi' in tag_dict:
    178         tag_dict['bdi'] = [attr for attr in tag_dict['bdi'] if attr != 'dir']
    179 
    180     return tag_dict
    181 
    182 
    183 class HtmlTagCompletions(sublime_plugin.EventListener):
    184     """
    185     Provide tag completions for HTML
    186     It matches just after typing the first letter of a tag name
    187     """
    188     def __init__(self):
    189         completion_list = self.default_completion_list()
    190         self.prefix_completion_dict = {}
    191         # construct a dictionary where the key is first character of
    192         # the completion list to the completion
    193         for s in completion_list:
    194             prefix = s[0][0]
    195             self.prefix_completion_dict.setdefault(prefix, []).append(s)
    196 
    197         # construct a dictionary from (tag, attribute[0]) -> [attribute]
    198         self.tag_to_attributes = get_tag_to_attributes()
    199 
    200     def on_query_completions(self, view, prefix, locations):
    201         # Only trigger within HTML
    202         if not view.match_selector(locations[0], "text.html - (source - source text.html)"
    203            " - string.quoted - meta.tag.style.end punctuation.definition.tag.begin"):
    204             return []
    205 
    206         # check if we are inside a tag
    207         is_inside_tag = view.match_selector(locations[0],
    208                 "text.html meta.tag - text.html punctuation.definition.tag.begin")
    209 
    210         return self.get_completions(view, prefix, locations, is_inside_tag)
    211 
    212     def get_completions(self, view, prefix, locations, is_inside_tag):
    213         # see if it is in tag.attr or tag#attr format
    214         if not is_inside_tag:
    215             tag_attr_expr = self.expand_tag_attributes(view, locations)
    216             if tag_attr_expr != []:
    217                 return (tag_attr_expr, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
    218 
    219         pt = locations[0] - len(prefix) - 1
    220         ch = view.substr(sublime.Region(pt, pt + 1))
    221 
    222         # print('prefix:', prefix)
    223         # print('location0:', locations[0])
    224         # print('substr:', view.substr(sublime.Region(locations[0], locations[0] + 3)), '!end!')
    225         # print('is_inside_tag', is_inside_tag)
    226         # print('ch:', ch)
    227 
    228         completion_list = []
    229         if is_inside_tag and ch != '<':
    230             if ch in [' ', '\t', '\n']:
    231                 # maybe trying to type an attribute
    232                 completion_list = self.get_attribute_completions(view, locations[0], prefix)
    233             # only ever trigger completion inside a tag if the previous character is a <
    234             # this is needed to stop completion from happening when typing attributes
    235             return (completion_list, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
    236 
    237         if prefix == '':
    238             # need completion list to match
    239             return ([], sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
    240 
    241         # match completion list using prefix
    242         completion_list = self.prefix_completion_dict.get(prefix[0], [])
    243 
    244         # if the opening < is not here insert that
    245         if ch != '<':
    246             completion_list = [(pair[0], '<' + pair[1]) for pair in completion_list]
    247 
    248         flags = 0
    249         if is_inside_tag:
    250             flags = sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS
    251 
    252         return (completion_list, flags)
    253 
    254     def default_completion_list(self):
    255         """
    256         Generate a default completion list for HTML
    257         """
    258         default_list = []
    259         normal_tags = ([
    260             'abbr', 'acronym', 'address', 'applet', 'article', 'aside',
    261             'audio', 'b', 'basefont', 'bdi', 'bdo', 'big', 'blockquote',
    262             'body', 'button', 'center', 'canvas', 'caption', 'cdata',
    263             'cite', 'colgroup', 'code', 'content', 'data', 'datalist',
    264             'dir', 'div', 'dd', 'del', 'details', 'dfn', 'dl', 'dt', 'element',
    265             'em', 'embed', 'fieldset', 'figure', 'figcaption', 'font', 'footer',
    266             'form', 'frame', 'frameset', 'head', 'header', 'h1', 'h2', 'h3',
    267             'h4', 'h5', 'h6', 'i', 'ins', 'isindex', 'kbd', 'keygen',
    268             'li', 'label', 'legend', 'main', 'map', 'mark', 'meter',
    269             'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
    270             'option', 'output', 'p', 'picture', 'pre', 'q', 'rp',
    271             'rt', 'rtc', 'ruby', 's', 'samp', 'section', 'select', 'shadow',
    272             'small', 'span', 'strong', 'sub', 'summary', 'sup',
    273             'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th',
    274             'thead', 'time', 'title', 'tr', 'tt', 'u', 'ul', 'var',
    275             'video'
    276         ])
    277 
    278         for tag in normal_tags:
    279             default_list.append(make_completion(tag))
    280             default_list.append(make_completion(tag.upper()))
    281 
    282         default_list += ([
    283             ('a\tTag', 'a href=\"$1\">$0</a>'),
    284             ('area\tTag', 'area shape=\"$1\" coords=\"$2\" href=\"$3\">'),
    285             ('audio\tTag', 'audio src=\"$1\">$0</audio>'),
    286             ('base\tTag', 'base href=\"$1\">'),
    287             ('br\tTag', 'br>'),
    288             ('col\tTag', 'col>'),
    289             ('hr\tTag', 'hr>'),
    290             ('iframe\tTag', 'iframe src=\"$1\">$0</iframe>'),
    291             ('input\tTag', 'input type=\"$1\" name=\"$2\">'),
    292             ('img\tTag', 'img src=\"$1\">'),
    293             ('link\tTag', 'link rel=\"stylesheet\" type=\"text/css\" href=\"$1\">'),
    294             ('meta\tTag', 'meta ${1:charset=\"utf-8\"}>'),
    295             ('param\tTag', 'param name=\"$1\" value=\"$2\">'),
    296             ('progress\tTag', 'progress value=\"$1\" max=\"$2\">'),
    297             ('script\tTag', 'script${2: type=\"${1:text/javascript}\"}>$0</script>'),
    298             ('source\tTag', 'source src=\"$1\" type=\"$2\">'),
    299             ('style\tTag', 'style type=\"${1:text/css}\">$0</style>'),
    300             ('track\tTag', 'track kind=\"$1\" src=\"$2\">'),
    301             ('wbr\tTag', 'wbr>'),
    302             ('video\tTag', 'video src=\"$1\">$0</video>')
    303         ])
    304 
    305         return default_list
    306 
    307     # This responds to on_query_completions, but conceptually it's expanding
    308     # expressions, rather than completing words.
    309     #
    310     # It expands these simple expressions:
    311     # tag.class
    312     # tag#id
    313     def expand_tag_attributes(self, view, locations):
    314         # Get the contents of each line, from the beginning of the line to
    315         # each point
    316         lines = [view.substr(sublime.Region(view.line(l).a, l))
    317             for l in locations]
    318 
    319         # Reverse the contents of each line, to simulate having the regex
    320         # match backwards
    321         lines = [l[::-1] for l in lines]
    322 
    323         # Check the first location looks like an expression
    324         rex = re.compile("([\w-]+)([.#])(\w+)")
    325         expr = match(rex, lines[0])
    326         if not expr:
    327             return []
    328 
    329         # Ensure that all other lines have identical expressions
    330         for i in range(1, len(lines)):
    331             ex = match(rex, lines[i])
    332             if ex != expr:
    333                 return []
    334 
    335         # Return the completions
    336         arg, op, tag = rex.match(expr).groups()
    337 
    338         arg = arg[::-1]
    339         tag = tag[::-1]
    340         expr = expr[::-1]
    341 
    342         if op == '.':
    343             snippet = '<{0} class=\"{1}\">$1</{0}>$0'.format(tag, arg)
    344         else:
    345             snippet = '<{0} id=\"{1}\">$1</{0}>$0'.format(tag, arg)
    346 
    347         return [(expr, snippet)]
    348 
    349     def get_attribute_completions(self, view, pt, prefix):
    350         SEARCH_LIMIT = 500
    351         search_start = max(0, pt - SEARCH_LIMIT - len(prefix))
    352         line = view.substr(sublime.Region(search_start, pt + SEARCH_LIMIT))
    353 
    354         line_head = line[0:pt - search_start]
    355         line_tail = line[pt - search_start:]
    356 
    357         # find the tag from end of line_head
    358         i = len(line_head) - 1
    359         tag = None
    360         space_index = len(line_head)
    361         while i >= 0:
    362             c = line_head[i]
    363             if c == '<':
    364                 # found the open tag
    365                 tag = line_head[i + 1:space_index]
    366                 break
    367             elif c == ' ':
    368                 space_index = i
    369             i -= 1
    370 
    371         # check that this tag looks valid
    372         if not tag or not tag.isalnum():
    373             return []
    374 
    375         # determines whether we need to close the tag
    376         # default to closing the tag
    377         suffix = '>'
    378 
    379         for c in line_tail:
    380             if c == '>':
    381                 # found end tag
    382                 suffix = ''
    383                 break
    384             elif c == '<':
    385                 # found another open tag, need to close this one
    386                 break
    387 
    388         if suffix == '' and not line_tail.startswith(' ') and not line_tail.startswith('>'):
    389             # add a space if not there
    390             suffix = ' '
    391 
    392         # got the tag, now find all attributes that match
    393         attributes = self.tag_to_attributes.get(tag, [])
    394         # ("class\tAttr", "class="$1">"),
    395         attri_completions = [(a + '\tAttr', a + '="$1"' + suffix) for a in attributes]
    396         return attri_completions
    397 
    398 
    399 # unit testing
    400 # to run it in sublime text:
    401 # import HTML.html_completions
    402 # HTML.html_completions.Unittest.run()
    403 
    404 import unittest
    405 
    406 class Unittest(unittest.TestCase):
    407 
    408     class Sublime:
    409         INHIBIT_WORD_COMPLETIONS = 1
    410         INHIBIT_EXPLICIT_COMPLETIONS = 2
    411 
    412     # this view contains a hard coded one line super simple HTML fragment
    413     class View:
    414         def __init__(self):
    415             self.buf = '<tr><td class="a">td.class</td></tr>'
    416 
    417         def line(self, pt):
    418             # only ever 1 line
    419             return sublime.Region(0, len(self.buf))
    420 
    421         def substr(self, region):
    422             return self.buf[region.a : region.b]
    423 
    424     def run():
    425         # redefine the modules to use the mock version
    426         global sublime
    427 
    428         sublime_module = sublime
    429         # use the normal region
    430         Unittest.Sublime.Region = sublime.Region
    431         sublime = Unittest.Sublime
    432 
    433         test = Unittest()
    434         test.test_simple_completion()
    435         test.test_inside_tag_completion()
    436         test.test_inside_tag_no_completion()
    437         test.test_expand_tag_attributes()
    438 
    439         # set it back after testing
    440         sublime = sublime_module
    441 
    442     # def get_completions(self, view, prefix, locations, is_inside_tag):
    443     def test_simple_completion(self):
    444         # <tr><td class="a">td.class</td></tr>
    445         view = Unittest.View()
    446         completor = HtmlTagCompletions()
    447 
    448         # simulate typing 'tab' at the start of the line, it is outside a tag
    449         completion_list, flags = completor.get_completions(view, 'tab', [0], False)
    450 
    451         # should give a bunch of tags that starts with t
    452         self.assertEqual(completion_list[0], ('table\tTag', '<table>$0</table>'))
    453         self.assertEqual(completion_list[1], ('tbody\tTag', '<tbody>$0</tbody>'))
    454         # don't suppress word based completion
    455         self.assertEqual(flags, 0)
    456 
    457     def test_inside_tag_completion(self):
    458         # <tr><td class="a">td.class</td></tr>
    459         view = Unittest.View()
    460         completor = HtmlTagCompletions()
    461 
    462         # simulate typing 'h' after <tr><, i.e. <tr><h
    463         completion_list, flags = completor.get_completions(view, 'h', [6], True)
    464 
    465         # should give a bunch of tags that starts with h, and without <
    466         self.assertEqual(completion_list[0], ('head\tTag', 'head>$0</head>'))
    467         self.assertEqual(completion_list[1], ('header\tTag', 'header>$0</header>'))
    468         self.assertEqual(completion_list[2], ('h1\tTag', 'h1>$0</h1>'))
    469         # suppress word based completion
    470         self.assertEqual(flags, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
    471 
    472         # simulate typing 'he' after <tr><, i.e. <tr><he
    473         completion_list, flags = completor.get_completions(view, 'he', [7], True)
    474 
    475         # should give a bunch of tags that starts with h, and without < (it filters only on the first letter of the prefix)
    476         self.assertEqual(completion_list[0], ('head\tTag', 'head>$0</head>'))
    477         self.assertEqual(completion_list[1], ('header\tTag', 'header>$0</header>'))
    478         self.assertEqual(completion_list[2], ('h1\tTag', 'h1>$0</h1>'))
    479         # suppress word based completion
    480         self.assertEqual(flags, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
    481 
    482     def test_inside_tag_no_completion(self):
    483         # <tr><td class="a">td.class</td></tr>
    484         view = Unittest.View()
    485         completor = HtmlTagCompletions()
    486 
    487         # simulate typing 'h' after <tr><td , i.e. <tr><td h
    488         completion_list, flags = completor.get_completions(view, 'h', [8], True)
    489 
    490         # should give nothing, but disable word based completions, since it is inside a tag
    491         self.assertEqual(completion_list, [])
    492         self.assertEqual(flags, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
    493 
    494     def test_expand_tag_attributes(self):
    495         # <tr><td class="a">td.class</td></tr>
    496         view = Unittest.View()
    497         completor = HtmlTagCompletions()
    498 
    499         # simulate typing tab after td.class
    500         completion_list, flags = completor.get_completions(view, '', [26], False)
    501 
    502         # should give just one completion, and suppress word based completion
    503         self.assertEqual(completion_list, [('td.class', '<td class="class">$1</td>$0')])
    504         self.assertEqual(flags, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)