Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/docs/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ Let's see what parameters we have:
# <p>A Text</p>
```

* **`fragment_context`** - Parses a fragment as if it were the contents of this context element (like setting `innerHTML` on it). Pass a tag name such as `"td"`, or a `QualName` for an element in another namespace, such as `QualName("svg", "svg")`; a name without a namespace is an HTML element. Implies `full_document=False`. default: None (fragments are parsed in a `body` context).
* **`fragment_context`** - Parses a fragment as if it were the contents of this context element (like setting `innerHTML` on it). Pass a tag name such as `"td"`, or a `QualName` for an element in another namespace, such as `QualName("svg", "svg")`; a name without a namespace is an HTML element. Implies `full_document=False`. The parsed tree is serialized as the contents of the context element, so that, for example, text parsed in a `script` context isn't escaped. default: None (fragments are parsed in a `body` context).

=== "\"tbody\""

Expand Down
11 changes: 11 additions & 0 deletions python/tests/test_dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,3 +546,14 @@ def test_attach_beside_orphan():
orphan.create_element("b", ordering=ordering)

assert dom.serialize(indent=0) == "<p>x</p>"


def test_serializer_children_raw_text():
dom = markupever.parse(
"<script>if (a < b && c) {}</script><style>a > b { }</style><p>a < b</p>",
markupever.HtmlOptions(full_document=False),
)
script, style, p = dom.root().children()
assert script.serialize(indent=0, include_self=False) == "if (a < b && c) {}"
assert style.serialize(indent=0, include_self=False) == "a > b { }"
assert p.serialize(indent=0, include_self=False) == "a &lt; b"
29 changes: 29 additions & 0 deletions python/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,32 @@ def test_meta_content_ending_in_charset():
f'<meta http-equiv="Content-Type" content="{content}"><p>x</p>'
)
assert dom.select_one("p").text() == "x"


def test_fragment_context_serialize():
# A fragment is serialized as the children of its context element, so that it round-trips.
src = 'console.log("<br>") &amp;'
for context, text, html in [
("script", src, src),
("style", src, src),
("xmp", src, src),
("plaintext", src, src),
("textarea", 'console.log("<br>") &', 'console.log("&lt;br&gt;") &amp;'),
("title", 'console.log("<br>") &', 'console.log("&lt;br&gt;") &amp;'),
]:
options = markupever.HtmlOptions(fragment_context=context)
dom = markupever.parse(src, options)
assert dom.root().text() == text
assert dom.serialize() == html
assert dom.root().serialize(include_self=False) == html
assert markupever.parse(dom.serialize(), options).root().text() == text

# Text in other contexts is escaped.
dom = markupever.parse("a &lt; b", markupever.HtmlOptions(full_document=False))
assert dom.serialize() == "a &lt; b"

# Descendants of the fragment's nodes are serialized as usual.
dom = markupever.parse(
"<p>a &lt; b</p>", markupever.HtmlOptions(fragment_context="div")
)
assert dom.serialize() == "<p>a &lt; b</p>"
15 changes: 13 additions & 2 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,10 +482,21 @@ pub fn serialize(

let serializer = ::treedom::Serializer::new(&dom, node.id, indent);

let traversal_scope = if include_self {
let value = dom.get(node.id).unwrap().value();

let traversal_scope = if let Some(context) = value.document().and(dom.fragment_context()) {
// A fragment's nodes are the children of the document node, but were parsed as
// children of the context element, so serialize them as its children: for example,
// text parsed in a <script> context is raw text.
::treedom::markup5ever::serialize::TraversalScope::ChildrenOnly(Some(context.clone()))
} else if include_self {
::treedom::markup5ever::serialize::TraversalScope::IncludeNode
} else {
::treedom::markup5ever::serialize::TraversalScope::ChildrenOnly(None)
// The serializer needs the parent's name to know whether its text
// children are raw text (as in <script> or <style>).
let name = value.element().map(|element| element.name.clone());

::treedom::markup5ever::serialize::TraversalScope::ChildrenOnly(name)
};

if !is_html {
Expand Down
10 changes: 10 additions & 0 deletions treedom/src/dom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub type NamespaceMap = HashMap<markup5ever::Prefix, markup5ever::Namespace>;
pub struct IDTreeDOM {
pub(super) tree: ego_tree::Tree<interface::Interface>,
pub(super) namespaces: NamespaceMap,
pub(super) fragment_context: Option<markup5ever::QualName>,
}

impl IDTreeDOM {
Expand All @@ -19,6 +20,7 @@ impl IDTreeDOM {
Self {
tree: ego_tree::Tree::new(root.into()),
namespaces,
fragment_context: None,
}
}

Expand All @@ -30,6 +32,7 @@ impl IDTreeDOM {
Self {
tree: ego_tree::Tree::with_capacity(root.into(), capacity),
namespaces,
fragment_context: None,
}
}

Expand All @@ -40,6 +43,12 @@ impl IDTreeDOM {
pub fn namespaces_mut(&mut self) -> &mut NamespaceMap {
&mut self.namespaces
}

/// Returns the context element that this DOM was parsed in, if it was parsed as a fragment.
/// Its root's children are the fragment's nodes.
pub fn fragment_context(&self) -> Option<&markup5ever::QualName> {
self.fragment_context.as_ref()
}
}

impl std::ops::Deref for IDTreeDOM {
Expand Down Expand Up @@ -70,6 +79,7 @@ impl std::fmt::Debug for IDTreeDOM {
f.debug_struct("IDTreeDOM")
.field("tree", &self.tree)
.field("namespaces", &self.namespaces)
.field("fragment_context", &self.fragment_context)
.finish()?;
}

Expand Down
17 changes: 12 additions & 5 deletions treedom/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ pub struct ParserSink {
quirks_mode: Cell<markup5ever::interface::QuirksMode>,
namespaces: RefCell<HashMap<markup5ever::Prefix, markup5ever::Namespace>>,
lineno: Cell<u64>,
// Whether this sink is parsing a fragment, whose nodes html5ever puts in an <html> root element
fragment: bool,
// The context element if this sink is parsing a fragment, whose nodes html5ever puts in an
// <html> root element
fragment_context: Option<markup5ever::QualName>,
// Attribute names of the elements passed to add_attrs_if_missing (only <html> and <body>),
// so that repeated calls don't each scan the element's attributes
attr_names: RefCell<HashMap<ego_tree::NodeId, HashSet<markup5ever::QualName>>>,
Expand All @@ -35,7 +36,7 @@ impl ParserSink {
quirks_mode: Cell::new(markup5ever::interface::QuirksMode::NoQuirks),
namespaces: RefCell::new(HashMap::new()),
lineno: Cell::new(1),
fragment: false,
fragment_context: None,
attr_names: RefCell::new(HashMap::new()),
}
}
Expand All @@ -62,7 +63,7 @@ impl ParserSink {
let mut tree = self.tree.into_inner();

// The fragment parsing algorithm returns the children of the <html> root element
if self.fragment {
if self.fragment_context.is_some() {
if let Some(html) = tree.root().first_child().map(|x| x.id()) {
tree.root_mut().reparent_from_id_append(html);
tree.get_mut(html).unwrap().detach();
Expand All @@ -72,6 +73,7 @@ impl ParserSink {
IDTreeDOM {
tree,
namespaces: self.namespaces.into_inner(),
fragment_context: self.fragment_context,
}
}

Expand Down Expand Up @@ -118,7 +120,7 @@ impl ParserSink {
tree_builder,
};
let sink = Self {
fragment: true,
fragment_context: Some(context.clone()),
..Self::new()
};

Expand Down Expand Up @@ -514,6 +516,11 @@ mod tests {
assert_eq!(&*tr.value().element().unwrap().name.local, "tr");
let td = tr.first_child().unwrap();
assert_eq!(&*td.value().element().unwrap().name.local, "td");
// The DOM remembers the context
assert_eq!(
dom.fragment_context().map(|name| &*name.local),
Some("tbody")
);

// In an svg context, elements are created in the SVG namespace
let dom = parse_fragment(
Expand Down
Loading