pyhtml2text 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/CHANGELOG.md +11 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/Cargo.lock +1 -1
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/Cargo.toml +1 -1
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/PKG-INFO +8 -4
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/README.md +7 -3
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/pyproject.toml +1 -1
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/tests/test_basic.py +17 -9
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/tests/test_markdown.py +2 -0
- pyhtml2text-0.3.1/vendor/README.md +48 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tree_builder/mod.rs +95 -118
- pyhtml2text-0.3.1/vendor/html5ever/src/tree_builder/open_elems.rs +501 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tree_builder/rules.rs +124 -101
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tree_builder/tag_sets.rs +10 -0
- pyhtml2text-0.3.0/vendor/README.md +0 -13
- pyhtml2text-0.3.0/vendor/html5ever/src/tree_builder/open_elems.rs +0 -134
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/.gitignore +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/LICENSE +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/python/pyhtml2text/__init__.py +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/python/pyhtml2text/__init__.pyi +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/python/pyhtml2text/py.typed +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/src/lib.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/src/markdown.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/tests/markdown_check.py +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/Cargo.toml +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/LICENSE-APACHE +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/LICENSE-MIT +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/README.md +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/driver.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/encoding.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/lib.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/macros.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/serialize/mod.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tokenizer/char_ref/mod.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tokenizer/interface.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tokenizer/mod.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tokenizer/states.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tree_builder/data.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/tree_builder/types.rs +0 -0
- {pyhtml2text-0.3.0 → pyhtml2text-0.3.1}/vendor/html5ever/src/util/str.rs +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
- The vendored html5ever now carries
|
|
6
|
+
[servo/html5ever#800](https://github.com/servo/html5ever/pull/800) by Josef
|
|
7
|
+
Guenther in place of our narrower patch. Every "in scope" check and the
|
|
8
|
+
other searches of the stack of open elements are now O(1), so unmatched
|
|
9
|
+
end tags and end tags inside SVG or MathML no longer make deeply nested
|
|
10
|
+
HTML quadratic: 32,000 nested `<span>` followed by as many unmatched
|
|
11
|
+
`</em>` went from 3.4 s to 0.02 s. Output is unchanged.
|
|
12
|
+
- About 0.4 µs more per call, and 0–2% on real pages.
|
|
13
|
+
|
|
3
14
|
## 0.3.0 (2026-10-15)
|
|
4
15
|
|
|
5
16
|
- New `html2markdown(html, *, base_url=None)`: HTML to CommonMark with
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyhtml2text
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
@@ -122,14 +122,18 @@ To plain text:
|
|
|
122
122
|
profile-guided optimisation, which makes them about 25% faster. Other
|
|
123
123
|
platforms get the same code without it.
|
|
124
124
|
- Deeply nested HTML is parsed in linear time, so hostile input can't stall a
|
|
125
|
-
worker: 40,000 nested lists take 0.14 s, against 6 s with stock html5ever
|
|
125
|
+
worker: 40,000 nested lists take 0.14 s, against 6 s with stock html5ever,
|
|
126
|
+
and 100,000 levels of nesting followed by unmatched end tags take under
|
|
127
|
+
0.1 s.
|
|
126
128
|
|
|
127
129
|
## Credits
|
|
128
130
|
|
|
129
131
|
The plain-text rendering is done by rust-html2text by Chris Emerson (MIT).
|
|
130
132
|
This package uses [a fork](https://github.com/jaredlynskey/rust-html2text/tree/pyhtml2text-perf)
|
|
131
|
-
that adds `base_url` and some performance changes, and a
|
|
132
|
-
[html5ever](https://github.com/servo/html5ever) (MIT/Apache-2.0
|
|
133
|
+
that adds `base_url` and some performance changes, and a copy of
|
|
134
|
+
[html5ever](https://github.com/servo/html5ever) (MIT/Apache-2.0) with
|
|
135
|
+
[servo/html5ever#800](https://github.com/servo/html5ever/pull/800) by Josef
|
|
136
|
+
Guenther applied, which makes parsing deeply nested HTML linear (see
|
|
133
137
|
`vendor/`). Markdown output is checked with
|
|
134
138
|
[pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) (MIT). It
|
|
135
139
|
isn't affiliated with or endorsed by any of these projects, so please report
|
|
@@ -106,14 +106,18 @@ To plain text:
|
|
|
106
106
|
profile-guided optimisation, which makes them about 25% faster. Other
|
|
107
107
|
platforms get the same code without it.
|
|
108
108
|
- Deeply nested HTML is parsed in linear time, so hostile input can't stall a
|
|
109
|
-
worker: 40,000 nested lists take 0.14 s, against 6 s with stock html5ever
|
|
109
|
+
worker: 40,000 nested lists take 0.14 s, against 6 s with stock html5ever,
|
|
110
|
+
and 100,000 levels of nesting followed by unmatched end tags take under
|
|
111
|
+
0.1 s.
|
|
110
112
|
|
|
111
113
|
## Credits
|
|
112
114
|
|
|
113
115
|
The plain-text rendering is done by rust-html2text by Chris Emerson (MIT).
|
|
114
116
|
This package uses [a fork](https://github.com/jaredlynskey/rust-html2text/tree/pyhtml2text-perf)
|
|
115
|
-
that adds `base_url` and some performance changes, and a
|
|
116
|
-
[html5ever](https://github.com/servo/html5ever) (MIT/Apache-2.0
|
|
117
|
+
that adds `base_url` and some performance changes, and a copy of
|
|
118
|
+
[html5ever](https://github.com/servo/html5ever) (MIT/Apache-2.0) with
|
|
119
|
+
[servo/html5ever#800](https://github.com/servo/html5ever/pull/800) by Josef
|
|
120
|
+
Guenther applied, which makes parsing deeply nested HTML linear (see
|
|
117
121
|
`vendor/`). Markdown output is checked with
|
|
118
122
|
[pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) (MIT). It
|
|
119
123
|
isn't affiliated with or endorsed by any of these projects, so please report
|
|
@@ -74,16 +74,24 @@ def test_rejects_other_types():
|
|
|
74
74
|
|
|
75
75
|
def test_deep_nesting_is_linear():
|
|
76
76
|
# Parsing used to be quadratic in nesting depth (40,000 nested lists took
|
|
77
|
-
# 6s); with the patched html5ever
|
|
77
|
+
# 6s); with the patched html5ever each of these takes well under a second.
|
|
78
|
+
# pyhtml2text 0.3.0, whose patch didn't cover them, took 33s and 50s on the
|
|
79
|
+
# last two.
|
|
78
80
|
import platform
|
|
79
81
|
import sys
|
|
80
82
|
import time
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
cases = [
|
|
85
|
+
"<ul><li>" * 100_000 + "x",
|
|
86
|
+
"<div>" * 100_000 + "x</p></p>",
|
|
87
|
+
"<span>" * 100_000 + "x" + "</em>" * 100_000,
|
|
88
|
+
"<svg>" + "<g>" * 100_000 + "</x>" * 100_000 + "</svg>x",
|
|
89
|
+
]
|
|
90
|
+
for html in cases:
|
|
91
|
+
start = time.perf_counter()
|
|
92
|
+
out = html2text(html)
|
|
93
|
+
elapsed = time.perf_counter() - start
|
|
94
|
+
assert out.rstrip().endswith("x")
|
|
95
|
+
# Only time it where tests run natively; emulated CI runners are slow.
|
|
96
|
+
if platform.machine().lower() in ("x86_64", "amd64") or sys.platform == "darwin":
|
|
97
|
+
assert elapsed < 10, f"took {elapsed:.1f}s on {html[:20]!r}..."
|
|
@@ -171,6 +171,8 @@ def test_hostile_input_stays_linear():
|
|
|
171
171
|
"<ul><li>" * 100_000 + "x",
|
|
172
172
|
"<blockquote>" * 100_000 + "x",
|
|
173
173
|
"<table><tr><td>" * 10_000 + "x",
|
|
174
|
+
"<span>" * 100_000 + "x" + "</em>" * 100_000,
|
|
175
|
+
"<svg>" + "<g>" * 100_000 + "</x>" * 100_000 + "</svg>x",
|
|
174
176
|
"<table><tr><td rowspan=65534 colspan=1000>a</td><td>b</td></tr>" + "<tr></tr>" * 200_000 + "</table>",
|
|
175
177
|
]
|
|
176
178
|
for html in cases:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Vendored crates
|
|
2
|
+
|
|
3
|
+
## html5ever 0.39.0
|
|
4
|
+
|
|
5
|
+
Copied from the crates.io release (`src/`, licences, README and manifest;
|
|
6
|
+
examples, tests and benchmarks removed). Dual-licensed MIT / Apache-2.0,
|
|
7
|
+
see `html5ever/LICENSE-*`.
|
|
8
|
+
|
|
9
|
+
Patched with [servo/html5ever#800](https://github.com/servo/html5ever/pull/800)
|
|
10
|
+
by Josef Guenther ("Fix quadratic parsing of deeply nested markup", commit
|
|
11
|
+
`67c7e7c423c5d1acab1872c630cae61e2bc45211`), which is not merged yet. The
|
|
12
|
+
PR is against html5ever 0.40.1; its `src/` changes apply unchanged to
|
|
13
|
+
0.39.0, apart from line offsets. Like the rest of html5ever, they are
|
|
14
|
+
available under MIT or Apache-2.0.
|
|
15
|
+
|
|
16
|
+
Without it, tree building is quadratic in nesting depth (see
|
|
17
|
+
[servo/html5ever#788](https://github.com/servo/html5ever/issues/788)): most
|
|
18
|
+
start and end tags search the stack of open elements from the top, and on
|
|
19
|
+
deeply nested input each search walks the whole stack. The PR keeps the
|
|
20
|
+
positions of open elements by name and by the sets those searches look for,
|
|
21
|
+
so "in scope" checks and the other lookups are O(1). Two cases stay
|
|
22
|
+
quadratic: the adoption agency removing elements from the middle of the
|
|
23
|
+
stack, and searches of the list of active formatting elements.
|
|
24
|
+
|
|
25
|
+
This replaces an earlier, narrower patch of ours that only covered the
|
|
26
|
+
"`p` in button scope" check. That patch left unmatched end tags and end tags
|
|
27
|
+
in foreign content quadratic: at 32,000 levels, `html2text()` took 3.4 s on
|
|
28
|
+
nested `<span>` followed by unmatched `</em>`, and 4.5 s on SVG `<g>`
|
|
29
|
+
followed by unmatched `</x>`. Both now take 0.02 s or less.
|
|
30
|
+
|
|
31
|
+
Checked against stock 0.39.0 before vendoring:
|
|
32
|
+
|
|
33
|
+
- html5ever's own tests pass, including the PR's new test, and so do all
|
|
34
|
+
3,515 html5lib tree-construction tests, in release and debug builds.
|
|
35
|
+
- The tree, quirks mode and exact parse errors are identical to stock 0.39.0
|
|
36
|
+
on every html5lib input (documents and fragments), 300,000 random tag-soup
|
|
37
|
+
records (30% of them fragments, in 23 contexts) and 28 real pages,
|
|
38
|
+
including the single-page HTML standard.
|
|
39
|
+
- pyhtml2text's output is byte-for-byte unchanged on the benchmark corpus
|
|
40
|
+
(`bench/bench.py --check`).
|
|
41
|
+
|
|
42
|
+
The extra bookkeeping costs about 0.4 µs per call, and 0–2% on real pages.
|
|
43
|
+
|
|
44
|
+
Once the PR is in an html5ever release that rust-html2text uses, drop this
|
|
45
|
+
directory and the `[patch.crates-io]` entry in `Cargo.toml`. Upgrading
|
|
46
|
+
rust-html2text to one that uses a newer html5ever without updating this copy
|
|
47
|
+
silently drops the patch (cargo only warns that it is unused), so
|
|
48
|
+
`test_deep_nesting_is_linear` checks that it is still in effect.
|
|
@@ -14,7 +14,7 @@ pub use crate::interface::{AppendNode, AppendText, Attribute, NodeOrText};
|
|
|
14
14
|
pub use crate::interface::{LimitedQuirks, NoQuirks, Quirks, QuirksMode};
|
|
15
15
|
pub use markup5ever::interface::tree_builder::create_element_with_flags;
|
|
16
16
|
|
|
17
|
-
use self::open_elems::{
|
|
17
|
+
use self::open_elems::{OpenElems, TrackedSet};
|
|
18
18
|
use self::types::*;
|
|
19
19
|
|
|
20
20
|
use crate::tendril::StrTendril;
|
|
@@ -546,7 +546,8 @@ where
|
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
fn end(&self) {
|
|
549
|
-
|
|
549
|
+
let mut open_elems = self.open_elems.borrow_mut();
|
|
550
|
+
while let Some(elem) = open_elems.pop() {
|
|
550
551
|
self.sink.pop(&elem);
|
|
551
552
|
}
|
|
552
553
|
}
|
|
@@ -743,12 +744,7 @@ where
|
|
|
743
744
|
});
|
|
744
745
|
};
|
|
745
746
|
|
|
746
|
-
let Some(fmt_elem_stack_index) = self
|
|
747
|
-
.open_elems
|
|
748
|
-
.borrow()
|
|
749
|
-
.iter()
|
|
750
|
-
.rposition(|n| self.sink.same_node(n, &fmt_elem))
|
|
751
|
-
else {
|
|
747
|
+
let Some(fmt_elem_stack_index) = self.position_in_open_elems(&fmt_elem) else {
|
|
752
748
|
self.sink
|
|
753
749
|
.parse_error(Borrowed("Formatting element not open"));
|
|
754
750
|
self.active_formatting.borrow_mut().remove(fmt_elem_index);
|
|
@@ -756,7 +752,11 @@ where
|
|
|
756
752
|
};
|
|
757
753
|
|
|
758
754
|
// 7.
|
|
759
|
-
if !self
|
|
755
|
+
if !self
|
|
756
|
+
.open_elems
|
|
757
|
+
.borrow()
|
|
758
|
+
.in_scope(TrackedSet::DefaultScope, fmt_elem_stack_index)
|
|
759
|
+
{
|
|
760
760
|
self.sink
|
|
761
761
|
.parse_error(Borrowed("Formatting element not in scope"));
|
|
762
762
|
return;
|
|
@@ -778,7 +778,7 @@ where
|
|
|
778
778
|
.find(|&(_, open_element)| self.elem_in(open_element, special_tag))
|
|
779
779
|
.map(|(i, h)| (i, h.clone()));
|
|
780
780
|
|
|
781
|
-
let Some((furthest_block_index, furthest_block)) = maybe_furthest_block else {
|
|
781
|
+
let Some((mut furthest_block_index, furthest_block)) = maybe_furthest_block else {
|
|
782
782
|
// 10.
|
|
783
783
|
self.open_elems.borrow_mut().truncate(fmt_elem_stack_index);
|
|
784
784
|
self.active_formatting.borrow_mut().remove(fmt_elem_index);
|
|
@@ -816,12 +816,14 @@ where
|
|
|
816
816
|
self.position_in_active_formatting(&node)
|
|
817
817
|
.map(|position| self.active_formatting.borrow_mut().remove(position));
|
|
818
818
|
self.open_elems.borrow_mut().remove(node_index);
|
|
819
|
+
furthest_block_index -= 1;
|
|
819
820
|
continue;
|
|
820
821
|
}
|
|
821
822
|
|
|
822
823
|
let Some(node_formatting_index) = self.position_in_active_formatting(&node) else {
|
|
823
824
|
// 13.6.
|
|
824
825
|
self.open_elems.borrow_mut().remove(node_index);
|
|
826
|
+
furthest_block_index -= 1;
|
|
825
827
|
continue;
|
|
826
828
|
};
|
|
827
829
|
|
|
@@ -841,10 +843,11 @@ where
|
|
|
841
843
|
tag.attrs.clone(),
|
|
842
844
|
tag.had_duplicate_attributes,
|
|
843
845
|
);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
.
|
|
847
|
-
.
|
|
846
|
+
self.open_elems.borrow_mut().replace(
|
|
847
|
+
node_index,
|
|
848
|
+
new_element.clone(),
|
|
849
|
+
self.sink.elem_name(&new_element).expanded(),
|
|
850
|
+
);
|
|
848
851
|
self.active_formatting.borrow_mut()[node_formatting_index] =
|
|
849
852
|
FormatEntry::Element(new_element.clone(), tag);
|
|
850
853
|
node = new_element;
|
|
@@ -910,37 +913,28 @@ where
|
|
|
910
913
|
}
|
|
911
914
|
|
|
912
915
|
// 19.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
.
|
|
917
|
-
.
|
|
918
|
-
.
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
916
|
+
// Move the formatting element up to just above the furthest block, which moves the
|
|
917
|
+
// elements in between down by one, and put the new element in its place.
|
|
918
|
+
{
|
|
919
|
+
let mut open_elems = self.open_elems.borrow_mut();
|
|
920
|
+
open_elems.move_up(fmt_elem_stack_index, furthest_block_index);
|
|
921
|
+
open_elems.replace(
|
|
922
|
+
furthest_block_index,
|
|
923
|
+
new_element.clone(),
|
|
924
|
+
self.sink.elem_name(&new_element).expanded(),
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
self.sink.pop(&fmt_elem);
|
|
924
928
|
|
|
925
929
|
// 20.
|
|
926
930
|
}
|
|
927
931
|
}
|
|
928
932
|
|
|
929
|
-
fn open_elem_kind(&self, elem: &Handle) -> ElemKind {
|
|
930
|
-
let name = self.sink.elem_name(elem);
|
|
931
|
-
let name = name.expanded();
|
|
932
|
-
if *name.ns == ns!(html) && *name.local == local_name!("p") {
|
|
933
|
-
ElemKind::P
|
|
934
|
-
} else if button_scope(name) {
|
|
935
|
-
ElemKind::ButtonScopeBoundary
|
|
936
|
-
} else {
|
|
937
|
-
ElemKind::Other
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
933
|
fn push(&self, elem: &Handle) {
|
|
942
|
-
let
|
|
943
|
-
self.open_elems
|
|
934
|
+
let name = self.sink.elem_name(elem);
|
|
935
|
+
self.open_elems
|
|
936
|
+
.borrow_mut()
|
|
937
|
+
.push(elem.clone(), name.expanded());
|
|
944
938
|
}
|
|
945
939
|
|
|
946
940
|
fn pop(&self) -> Handle {
|
|
@@ -954,12 +948,24 @@ where
|
|
|
954
948
|
elem
|
|
955
949
|
}
|
|
956
950
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
951
|
+
/// The position of `elem` in the stack of open elements, if it is open.
|
|
952
|
+
fn position_in_open_elems(&self, elem: &Handle) -> Option<usize> {
|
|
953
|
+
let name = self.sink.elem_name(elem);
|
|
954
|
+
let open_elems = self.open_elems.borrow();
|
|
955
|
+
let positions = if *name.ns() == ns!(html) {
|
|
956
|
+
open_elems.positions_named(name.local_name())
|
|
957
|
+
} else {
|
|
958
|
+
open_elems.positions_foreign_named(name.local_name())
|
|
959
|
+
};
|
|
960
|
+
let position = positions
|
|
961
961
|
.iter()
|
|
962
|
-
.
|
|
962
|
+
.rev()
|
|
963
|
+
.find(|&position| self.sink.same_node(&open_elems[position], elem));
|
|
964
|
+
position
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
fn remove_from_stack(&self, elem: &Handle) {
|
|
968
|
+
let position = self.position_in_open_elems(elem);
|
|
963
969
|
if let Some(position) = position {
|
|
964
970
|
self.open_elems.borrow_mut().remove(position);
|
|
965
971
|
self.sink.pop(elem);
|
|
@@ -969,12 +975,7 @@ where
|
|
|
969
975
|
fn is_marker_or_open(&self, entry: &FormatEntry<Handle>) -> bool {
|
|
970
976
|
match *entry {
|
|
971
977
|
FormatEntry::Marker => true,
|
|
972
|
-
FormatEntry::Element(ref node, _) => self
|
|
973
|
-
.open_elems
|
|
974
|
-
.borrow()
|
|
975
|
-
.iter()
|
|
976
|
-
.rev()
|
|
977
|
-
.any(|n| self.sink.same_node(n, node)),
|
|
978
|
+
FormatEntry::Element(ref node, _) => self.position_in_open_elems(node).is_some(),
|
|
978
979
|
}
|
|
979
980
|
}
|
|
980
981
|
|
|
@@ -1075,48 +1076,22 @@ where
|
|
|
1075
1076
|
/// Signal an error depending on the state of the stack of open elements at
|
|
1076
1077
|
/// the end of the body.
|
|
1077
1078
|
fn check_body_end(&self) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
for elem in self.open_elems.borrow().iter() {
|
|
1083
|
-
let error = {
|
|
1084
|
-
let elem_name = self.sink.elem_name(elem);
|
|
1085
|
-
let name = elem_name.expanded();
|
|
1086
|
-
if body_end_ok(name) {
|
|
1087
|
-
continue;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
if self.opts.exact_errors {
|
|
1091
|
-
Cow::from(format!("Unexpected open tag {name:?} at end of body"))
|
|
1092
|
-
} else {
|
|
1093
|
-
Cow::from("Unexpected open tag at end of body")
|
|
1094
|
-
}
|
|
1095
|
-
};
|
|
1096
|
-
self.sink.parse_error(error);
|
|
1097
|
-
// FIXME: Do we keep checking after finding one bad tag?
|
|
1098
|
-
// The spec suggests not.
|
|
1079
|
+
let open_elems = self.open_elems.borrow();
|
|
1080
|
+
// FIXME: Do we keep checking after finding one bad tag?
|
|
1081
|
+
// The spec suggests not.
|
|
1082
|
+
let Some(position) = open_elems.positions_in(TrackedSet::NotBodyEndOk).first() else {
|
|
1099
1083
|
return;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
for node in self.open_elems.borrow().iter().rev() {
|
|
1109
|
-
if pred(node.clone()) {
|
|
1110
|
-
return true;
|
|
1111
|
-
}
|
|
1112
|
-
if scope(self.sink.elem_name(node).expanded()) {
|
|
1113
|
-
return false;
|
|
1084
|
+
};
|
|
1085
|
+
let error = {
|
|
1086
|
+
let elem_name = self.sink.elem_name(&open_elems[position]);
|
|
1087
|
+
let name = elem_name.expanded();
|
|
1088
|
+
if self.opts.exact_errors {
|
|
1089
|
+
Cow::from(format!("Unexpected open tag {name:?} at end of body"))
|
|
1090
|
+
} else {
|
|
1091
|
+
Cow::from("Unexpected open tag at end of body")
|
|
1114
1092
|
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// supposed to be impossible, because <html> is always in scope
|
|
1118
|
-
|
|
1119
|
-
false
|
|
1093
|
+
};
|
|
1094
|
+
self.sink.parse_error(error);
|
|
1120
1095
|
}
|
|
1121
1096
|
|
|
1122
1097
|
fn elem_in<TagSet>(&self, elem: &Handle, set: TagSet) -> bool
|
|
@@ -1132,21 +1107,22 @@ where
|
|
|
1132
1107
|
}
|
|
1133
1108
|
|
|
1134
1109
|
fn in_html_elem_named(&self, name: LocalName) -> bool {
|
|
1135
|
-
self.open_elems
|
|
1136
|
-
.borrow()
|
|
1137
|
-
.iter()
|
|
1138
|
-
.any(|elem| self.html_elem_named(elem, name.clone()))
|
|
1110
|
+
!self.open_elems.borrow().positions_named(&name).is_empty()
|
|
1139
1111
|
}
|
|
1140
1112
|
|
|
1141
1113
|
fn current_node_named(&self, name: LocalName) -> bool {
|
|
1142
1114
|
self.html_elem_named(&self.current_node(), name)
|
|
1143
1115
|
}
|
|
1144
1116
|
|
|
1145
|
-
fn in_scope_named
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1117
|
+
fn in_scope_named(&self, scope: TrackedSet, name: LocalName) -> bool {
|
|
1118
|
+
self.open_elems.borrow().named_in_scope(scope, &name)
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
fn in_scope_any_named(&self, scope: TrackedSet, names: &[LocalName]) -> bool {
|
|
1122
|
+
let open_elems = self.open_elems.borrow();
|
|
1123
|
+
names
|
|
1124
|
+
.iter()
|
|
1125
|
+
.any(|name| open_elems.named_in_scope(scope, name))
|
|
1150
1126
|
}
|
|
1151
1127
|
|
|
1152
1128
|
/// <https://html.spec.whatwg.org/#generate-implied-end-tags>
|
|
@@ -1234,15 +1210,8 @@ where
|
|
|
1234
1210
|
self.expect_to_close(local_name!("p"));
|
|
1235
1211
|
}
|
|
1236
1212
|
|
|
1237
|
-
/// Whether the stack of open elements has a `p` element in button scope.
|
|
1238
|
-
fn p_in_button_scope(&self) -> bool {
|
|
1239
|
-
let result = self.open_elems.borrow().has_p_in_button_scope();
|
|
1240
|
-
debug_assert_eq!(result, self.in_scope_named(button_scope, local_name!("p")));
|
|
1241
|
-
result
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
1213
|
fn close_p_element_in_button_scope(&self) {
|
|
1245
|
-
if self.
|
|
1214
|
+
if self.in_scope_named(TrackedSet::ButtonScope, local_name!("p")) {
|
|
1246
1215
|
self.close_p_element();
|
|
1247
1216
|
}
|
|
1248
1217
|
}
|
|
@@ -1290,7 +1259,15 @@ where
|
|
|
1290
1259
|
// https://html.spec.whatwg.org/multipage/#reset-the-insertion-mode-appropriately
|
|
1291
1260
|
fn reset_insertion_mode(&self) -> InsertionMode {
|
|
1292
1261
|
let open_elems = self.open_elems.borrow();
|
|
1293
|
-
|
|
1262
|
+
// The steps below stop at the topmost element in this set, unless it is the first one,
|
|
1263
|
+
// which they look at with the context element in its place.
|
|
1264
|
+
let topmost = open_elems
|
|
1265
|
+
.positions_in(TrackedSet::ResetInsertionMode)
|
|
1266
|
+
.last()
|
|
1267
|
+
.filter(|&i| i > 0);
|
|
1268
|
+
let first = (!open_elems.is_empty()).then_some(0);
|
|
1269
|
+
for i in topmost.into_iter().chain(first) {
|
|
1270
|
+
let mut node = &open_elems[i];
|
|
1294
1271
|
let last = i == 0usize;
|
|
1295
1272
|
let context_elem = self.context_elem.borrow();
|
|
1296
1273
|
if let (true, Some(ctx)) = (last, context_elem.as_ref()) {
|
|
@@ -1587,20 +1564,20 @@ where
|
|
|
1587
1564
|
}
|
|
1588
1565
|
|
|
1589
1566
|
fn process_end_tag_in_body(&self, tag: Tag) {
|
|
1590
|
-
// Look back for a matching open element.
|
|
1591
|
-
let
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
if self.elem_in(elem, special_tag) {
|
|
1567
|
+
// Look back for a matching open element, as far as the topmost special element.
|
|
1568
|
+
let match_idx = {
|
|
1569
|
+
let open_elems = self.open_elems.borrow();
|
|
1570
|
+
let topmost_match = open_elems.positions_named(&tag.name).last();
|
|
1571
|
+
if topmost_match.is_some_and(|i| open_elems.in_scope(TrackedSet::Special, i)) {
|
|
1572
|
+
topmost_match
|
|
1573
|
+
} else if !open_elems.positions_in(TrackedSet::Special).is_empty() {
|
|
1599
1574
|
self.sink
|
|
1600
1575
|
.parse_error(Borrowed("Found special tag while closing generic tag"));
|
|
1601
1576
|
return;
|
|
1577
|
+
} else {
|
|
1578
|
+
None
|
|
1602
1579
|
}
|
|
1603
|
-
}
|
|
1580
|
+
};
|
|
1604
1581
|
|
|
1605
1582
|
let Some(match_idx) = match_idx else {
|
|
1606
1583
|
// I believe this is impossible, because the root
|