Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Features:

Bug fixes:

* Place ``<template>`` elements in ``<head>`` when they appear before
``<body>``, matching the WHATWG "in head" / "in body" insertion-mode rules.
Template contents are still stored as ordinary children rather than a
content document fragment. (#595)
* The sanitizer now permits ``<summary>`` tags. It used to allow ``<details>``
already. (#423)

Expand Down
3 changes: 3 additions & 0 deletions html5lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@
"Unexpected end of file. Expected inhead-noscript content",
"char-in-head-noscript":
"Unexpected non-space character. Expected inhead-noscript content",
"eof-in-template":
"Unexpected end of file. Expected template content",
"XXX-undefined-error":
"Undefined error (this sucks and should be fixed)",
}
Expand Down Expand Up @@ -410,6 +412,7 @@
(namespaces["html"], "table"),
(namespaces["html"], "tbody"),
(namespaces["html"], "td"),
(namespaces["html"], "template"),
(namespaces["html"], "textarea"),
(namespaces["html"], "tfoot"),
(namespaces["html"], "th"),
Expand Down
78 changes: 76 additions & 2 deletions html5lib/html5parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,15 @@ def startTagScript(self, token):
self.parser.originalPhase = self.parser.phase
self.parser.phase = self.parser.phases["text"]

def startTagTemplate(self, token):
# WHATWG "in head": insert the element, then switch to "in template".
# Template contents stay as ordinary children (html5lib does not yet
# implement the template content document fragment).
self.tree.insertElement(token)
self.parser.framesetOK = False
self.parser.originalPhase = self.parser.phase
self.parser.phase = self.parser.phases["inTemplate"]

def startTagOther(self, token):
self.anythingElse()
return token
Expand All @@ -774,6 +783,7 @@ def anythingElse(self):
(("noframes", "style"), startTagNoFramesStyle),
("noscript", startTagNoscript),
("script", startTagScript),
("template", startTagTemplate),
(("base", "basefont", "bgsound", "command", "link"),
startTagBaseLinkCommand),
("meta", startTagMeta),
Expand Down Expand Up @@ -909,7 +919,7 @@ def anythingElse(self):
("body", startTagBody),
("frameset", startTagFrameset),
(("base", "basefont", "bgsound", "link", "meta", "noframes", "script",
"style", "title"),
"style", "template", "title"),
startTagFromHead),
("head", startTagHead)
])
Expand All @@ -919,6 +929,69 @@ def anythingElse(self):
endTagHandler.default = endTagOther


class InTemplatePhase(Phase):
"""Simplified "in template" insertion mode.

Places template contents as ordinary children of the ``template`` element.
Full WHATWG template content fragments and the stack of template insertion
modes are not implemented.
"""
__slots__ = tuple()

def processEOF(self):
self.parser.parseError("eof-in-template")
self.endTagTemplate(impliedTagToken("template"))
return True

def processComment(self, token):
self.tree.insertComment(token)

def processCharacters(self, token):
self.tree.insertText(token["data"])

def processSpaceCharacters(self, token):
self.tree.insertText(token["data"])

def startTagTemplate(self, token):
return self.parser.phases["inHead"].processStartTag(token)

def startTagOther(self, token):
self.tree.insertElement(token)

def endTagTemplate(self, token):
for i, node in enumerate(reversed(self.tree.openElements)):
if node.name == "template":
del self.tree.openElements[-(i + 1):]
break
else:
self.parser.parseError("unexpected-end-tag", {"name": "template"})
return
self.parser.phase = self.parser.originalPhase

def endTagOther(self, token):
# Pop a matching open element inside the template when present.
for node in reversed(self.tree.openElements):
if node.name == "template":
self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
return
if node.name == token["name"]:
while self.tree.openElements[-1].name != token["name"]:
self.tree.openElements.pop()
self.tree.openElements.pop()
return
self.parser.parseError("unexpected-end-tag", {"name": token["name"]})

startTagHandler = _utils.MethodDispatcher([
("template", startTagTemplate),
])
startTagHandler.default = startTagOther

endTagHandler = _utils.MethodDispatcher([
("template", endTagTemplate),
])
endTagHandler.default = endTagOther


class InBodyPhase(Phase):
# http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody
# the really-really-really-very crazy mode
Expand Down Expand Up @@ -1581,7 +1654,7 @@ def endTagOther(self, token):
startTagHandler = _utils.MethodDispatcher([
("html", Phase.startTagHtml),
(("base", "basefont", "bgsound", "command", "link", "meta",
"script", "style", "title"),
"script", "style", "template", "title"),
startTagProcessInHead),
("body", startTagBody),
("frameset", startTagFrameset),
Expand Down Expand Up @@ -2752,6 +2825,7 @@ def processEndTag(self, token):
"inHead": InHeadPhase,
"inHeadNoscript": InHeadNoscriptPhase,
"afterHead": AfterHeadPhase,
"inTemplate": InTemplatePhase,
"inBody": InBodyPhase,
"text": TextPhase,
"inTable": InTablePhase,
Expand Down
18 changes: 18 additions & 0 deletions html5lib/tests/test_parser2.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,21 @@ def test_self_closing_col():
parser = HTMLParser()
parser.parseFragment('<table><colgroup><col /></colgroup></table>')
assert not parser.errors


def test_template_before_body_goes_in_head():
# Issue #595: <template> before <body> must use the in-head rules.
doc = parse("<template></template>", treebuilder="dom", namespaceHTMLElements=False)
head = [c for c in doc.documentElement.childNodes if getattr(c, "localName", None) == "head"][0]
body = [c for c in doc.documentElement.childNodes if getattr(c, "localName", None) == "body"][0]
head_names = [c.localName for c in head.childNodes if getattr(c, "localName", None)]
body_names = [c.localName for c in body.childNodes if getattr(c, "localName", None)]
assert head_names == ["template"]
assert body_names == []


def test_template_before_body_keeps_text_content():
doc = parse("<template>hi</template>", treebuilder="dom", namespaceHTMLElements=False)
template = doc.getElementsByTagName("template")[0]
assert template.parentNode.localName == "head"
assert [c.data for c in template.childNodes if getattr(c, "data", None) is not None] == ["hi"]