Skip to content

Commit 73b71c7

Browse files
committed
Place template elements in head before body
Handle template start tags with the in-head rules and a simplified in-template phase so <template> before <body> lands in <head>, matching WHATWG insertion-mode requirements. Contents remain ordinary children. Fixes #595
1 parent fd4f032 commit 73b71c7

4 files changed

Lines changed: 101 additions & 2 deletions

File tree

‎CHANGES.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Features:
1818

1919
Bug fixes:
2020

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

‎html5lib/constants.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@
289289
"Unexpected end of file. Expected inhead-noscript content",
290290
"char-in-head-noscript":
291291
"Unexpected non-space character. Expected inhead-noscript content",
292+
"eof-in-template":
293+
"Unexpected end of file. Expected template content",
292294
"XXX-undefined-error":
293295
"Undefined error (this sucks and should be fixed)",
294296
}
@@ -410,6 +412,7 @@
410412
(namespaces["html"], "table"),
411413
(namespaces["html"], "tbody"),
412414
(namespaces["html"], "td"),
415+
(namespaces["html"], "template"),
413416
(namespaces["html"], "textarea"),
414417
(namespaces["html"], "tfoot"),
415418
(namespaces["html"], "th"),

‎html5lib/html5parser.py‎

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,15 @@ def startTagScript(self, token):
749749
self.parser.originalPhase = self.parser.phase
750750
self.parser.phase = self.parser.phases["text"]
751751

752+
def startTagTemplate(self, token):
753+
# WHATWG "in head": insert the element, then switch to "in template".
754+
# Template contents stay as ordinary children (html5lib does not yet
755+
# implement the template content document fragment).
756+
self.tree.insertElement(token)
757+
self.parser.framesetOK = False
758+
self.parser.originalPhase = self.parser.phase
759+
self.parser.phase = self.parser.phases["inTemplate"]
760+
752761
def startTagOther(self, token):
753762
self.anythingElse()
754763
return token
@@ -774,6 +783,7 @@ def anythingElse(self):
774783
(("noframes", "style"), startTagNoFramesStyle),
775784
("noscript", startTagNoscript),
776785
("script", startTagScript),
786+
("template", startTagTemplate),
777787
(("base", "basefont", "bgsound", "command", "link"),
778788
startTagBaseLinkCommand),
779789
("meta", startTagMeta),
@@ -909,7 +919,7 @@ def anythingElse(self):
909919
("body", startTagBody),
910920
("frameset", startTagFrameset),
911921
(("base", "basefont", "bgsound", "link", "meta", "noframes", "script",
912-
"style", "title"),
922+
"style", "template", "title"),
913923
startTagFromHead),
914924
("head", startTagHead)
915925
])
@@ -919,6 +929,69 @@ def anythingElse(self):
919929
endTagHandler.default = endTagOther
920930

921931

932+
class InTemplatePhase(Phase):
933+
"""Simplified "in template" insertion mode.
934+
935+
Places template contents as ordinary children of the ``template`` element.
936+
Full WHATWG template content fragments and the stack of template insertion
937+
modes are not implemented.
938+
"""
939+
__slots__ = tuple()
940+
941+
def processEOF(self):
942+
self.parser.parseError("eof-in-template")
943+
self.endTagTemplate(impliedTagToken("template"))
944+
return True
945+
946+
def processComment(self, token):
947+
self.tree.insertComment(token)
948+
949+
def processCharacters(self, token):
950+
self.tree.insertText(token["data"])
951+
952+
def processSpaceCharacters(self, token):
953+
self.tree.insertText(token["data"])
954+
955+
def startTagTemplate(self, token):
956+
return self.parser.phases["inHead"].processStartTag(token)
957+
958+
def startTagOther(self, token):
959+
self.tree.insertElement(token)
960+
961+
def endTagTemplate(self, token):
962+
for i, node in enumerate(reversed(self.tree.openElements)):
963+
if node.name == "template":
964+
del self.tree.openElements[-(i + 1):]
965+
break
966+
else:
967+
self.parser.parseError("unexpected-end-tag", {"name": "template"})
968+
return
969+
self.parser.phase = self.parser.originalPhase
970+
971+
def endTagOther(self, token):
972+
# Pop a matching open element inside the template when present.
973+
for node in reversed(self.tree.openElements):
974+
if node.name == "template":
975+
self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
976+
return
977+
if node.name == token["name"]:
978+
while self.tree.openElements[-1].name != token["name"]:
979+
self.tree.openElements.pop()
980+
self.tree.openElements.pop()
981+
return
982+
self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
983+
984+
startTagHandler = _utils.MethodDispatcher([
985+
("template", startTagTemplate),
986+
])
987+
startTagHandler.default = startTagOther
988+
989+
endTagHandler = _utils.MethodDispatcher([
990+
("template", endTagTemplate),
991+
])
992+
endTagHandler.default = endTagOther
993+
994+
922995
class InBodyPhase(Phase):
923996
# http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody
924997
# the really-really-really-very crazy mode
@@ -1581,7 +1654,7 @@ def endTagOther(self, token):
15811654
startTagHandler = _utils.MethodDispatcher([
15821655
("html", Phase.startTagHtml),
15831656
(("base", "basefont", "bgsound", "command", "link", "meta",
1584-
"script", "style", "title"),
1657+
"script", "style", "template", "title"),
15851658
startTagProcessInHead),
15861659
("body", startTagBody),
15871660
("frameset", startTagFrameset),
@@ -2752,6 +2825,7 @@ def processEndTag(self, token):
27522825
"inHead": InHeadPhase,
27532826
"inHeadNoscript": InHeadNoscriptPhase,
27542827
"afterHead": AfterHeadPhase,
2828+
"inTemplate": InTemplatePhase,
27552829
"inBody": InBodyPhase,
27562830
"text": TextPhase,
27572831
"inTable": InTablePhase,

‎html5lib/tests/test_parser2.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,21 @@ def test_self_closing_col():
9191
parser = HTMLParser()
9292
parser.parseFragment('<table><colgroup><col /></colgroup></table>')
9393
assert not parser.errors
94+
95+
96+
def test_template_before_body_goes_in_head():
97+
# Issue #595: <template> before <body> must use the in-head rules.
98+
doc = parse("<template></template>", treebuilder="dom", namespaceHTMLElements=False)
99+
head = [c for c in doc.documentElement.childNodes if getattr(c, "localName", None) == "head"][0]
100+
body = [c for c in doc.documentElement.childNodes if getattr(c, "localName", None) == "body"][0]
101+
head_names = [c.localName for c in head.childNodes if getattr(c, "localName", None)]
102+
body_names = [c.localName for c in body.childNodes if getattr(c, "localName", None)]
103+
assert head_names == ["template"]
104+
assert body_names == []
105+
106+
107+
def test_template_before_body_keeps_text_content():
108+
doc = parse("<template>hi</template>", treebuilder="dom", namespaceHTMLElements=False)
109+
template = doc.getElementsByTagName("template")[0]
110+
assert template.parentNode.localName == "head"
111+
assert [c.data for c in template.childNodes if getattr(c, "data", None) is not None] == ["hi"]

0 commit comments

Comments
 (0)