frostwork 0.2.1__tar.gz → 0.2.2__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.
- {frostwork-0.2.1 → frostwork-0.2.2}/CHANGELOG.md +7 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/Cargo.lock +1 -1
- {frostwork-0.2.1 → frostwork-0.2.2}/Cargo.toml +1 -1
- {frostwork-0.2.1 → frostwork-0.2.2}/PKG-INFO +1 -1
- {frostwork-0.2.1 → frostwork-0.2.2}/pyproject.toml +2 -2
- {frostwork-0.2.1 → frostwork-0.2.2}/src/lib.rs +17 -17
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/navigation.rs +11 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/tokenizer.rs +1 -1
- {frostwork-0.2.1 → frostwork-0.2.2}/.github/actions/oracle/action.yml +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/.github/dependabot.yml +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/bench-migration.yml +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/ci.yml +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/publish.yml +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/scheduled.yml +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/.gitignore +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/.pre-commit-config.yaml +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/AGENTS.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/CLAUDE.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/LICENSE +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/Makefile +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/README.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/SECURITY.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/BENCHMARKS.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/COMPATIBILITY.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/DESIGN.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/MIGRATION.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/PYTHON.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/RELEASING.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/SUPPORT_SNAPSHOT.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/TESTING.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/docs/WEBPOET_SURFACE.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/examples/basic.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/examples/basic.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/AGENTS.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/CLAUDE.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/__init__.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/_frostwork.pyi +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/_navigation.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/audit.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/encoding.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/page.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/py.typed +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/scan.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/validation.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/webpoet.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/requirements-bench.txt +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/requirements-release.txt +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/requirements-test.txt +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/bin/bench.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/bin/differ.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/diagnostics.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/encoding.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/entities.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/implied_close/generated.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/implied_close/mod.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/compile.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/decode.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/deferred.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/frame.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/matching.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/mod.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/sig.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/mutate.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/page.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/python.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/selector.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/src/xpath.rs +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/conftest.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/README.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/catalog/pages/test-listing.html +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/catalog/selectors.json +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/docgen/pages/test-api.html +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/docgen/selectors.json +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/tabular/pages/test-report.html +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/tabular/selectors.json +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/article.html +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/listing.html +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/manifest.json +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/pages.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/product.html +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_audit_context.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_composition.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_doc_examples.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_docs_check.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_encoding.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_gates.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_make_gates.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_migration.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_navigation.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_python.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_release.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_schema.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_typing.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_validation.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tests/typing_fixture.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/AGENTS.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/CLAUDE.md +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/ab_bench.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/abi3_smoke.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/audit_tree_rules.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_corpus.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_encoding.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_engines.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_matrix.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_mem.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_navigation.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_webpoet.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/check_docs.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/conformant.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/corpus_fetch.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/decoder_sweep.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/diff_fuzz.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/diff_lxml.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/diff_webpoet.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/enc_cases.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/enc_check.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/encpages.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/families.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/foreign.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/gen_tree_rules.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/mutate_rules.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/mutate_webpoet.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/oracle.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/release_check.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/sel_fuzz.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/seq_sweep.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/soak.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/support_snapshot.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/verify_migration.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/verify_pypi.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/webpoet_cases.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/webpoet_structure.py +0 -0
- {frostwork-0.2.1 → frostwork-0.2.2}/tools/webpoet_surface.py +0 -0
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable user-facing changes will be recorded here. Frostwork will follow semantic versioning after its
|
|
4
4
|
first public release.
|
|
5
5
|
|
|
6
|
+
## 0.2.2 (2026-11-05)
|
|
7
|
+
|
|
8
|
+
### Performance
|
|
9
|
+
|
|
10
|
+
- `navigation()` no longer takes quadratic time on a document with many unclosed `<meta` tags, e.g.
|
|
11
|
+
`"<meta " * 80000`, which took over 30 seconds in 0.2.1.
|
|
12
|
+
|
|
6
13
|
## 0.2.1 (2026-11-05)
|
|
7
14
|
|
|
8
15
|
### Correctness
|
|
@@ -5,7 +5,7 @@ build-backend = "maturin"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "frostwork"
|
|
8
|
-
version = "0.2.
|
|
8
|
+
version = "0.2.2"
|
|
9
9
|
description = "Treeless, one-pass HTML extraction — CSS/XPath selectors matched on a streaming scan, no DOM."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -89,7 +89,7 @@ module-name = "frostwork._frostwork"
|
|
|
89
89
|
python-source = "python"
|
|
90
90
|
|
|
91
91
|
[tool.bumpversion]
|
|
92
|
-
current_version = "0.2.
|
|
92
|
+
current_version = "0.2.2"
|
|
93
93
|
commit = true
|
|
94
94
|
tag = true
|
|
95
95
|
tag_name = "{new_version}"
|
|
@@ -750,24 +750,24 @@ impl Plan {
|
|
|
750
750
|
|
|
751
751
|
/// Whether `bytes` can hold a `<base>`, when `base`, or a `<meta>` with an `http-equiv` attribute,
|
|
752
752
|
/// which every meta refresh needs, when `refresh`. Tags are matched ASCII-case-insensitively wherever
|
|
753
|
-
/// `<name` is followed by the end of a tag name, a superset of the start tags named `name
|
|
753
|
+
/// `<name` is followed by the end of a tag name, a superset of the start tags named `name`, and
|
|
754
|
+
/// `http-equiv` anywhere after the first `<meta`, a superset of its attributes. Linear in `bytes`.
|
|
754
755
|
fn may_change(bytes: &[u8], base: bool, refresh: bool) -> bool {
|
|
755
|
-
let
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
})
|
|
756
|
+
let tag_at = |i: usize, name: &[u8]| {
|
|
757
|
+
let end = i + 1 + name.len();
|
|
758
|
+
bytes.get(i + 1..end).is_some_and(|candidate| candidate.eq_ignore_ascii_case(name))
|
|
759
|
+
&& bytes.get(end).is_some_and(|&c| !tokenizer::is_name_char(c))
|
|
760
|
+
};
|
|
761
|
+
let tags = || memchr::memchr_iter(b'<', bytes);
|
|
762
|
+
(base && tags().any(|i| tag_at(i, b"base")))
|
|
763
|
+
|| (refresh
|
|
764
|
+
&& tags().find(|&i| tag_at(i, b"meta")).is_some_and(|i| {
|
|
765
|
+
// `q` is the rarest byte of `http-equiv` in markup, so few candidates reach the comparison.
|
|
766
|
+
let rest = &bytes[i..];
|
|
767
|
+
memchr::memchr2_iter(b'q', b'Q', rest).any(|q| {
|
|
768
|
+
q >= 6 && rest.get(q - 6..q + 4).is_some_and(|w| w.eq_ignore_ascii_case(b"http-equiv"))
|
|
769
|
+
})
|
|
770
|
+
}))
|
|
771
771
|
}
|
|
772
772
|
|
|
773
773
|
/// The base URL and meta refresh of `html`, as a browser reads them; see
|
|
@@ -702,6 +702,7 @@ mod tests {
|
|
|
702
702
|
let utf16: Vec<u8> = "\u{feff}<meta http-equiv=refresh content=3>".encode_utf16().flat_map(u16::to_le_bytes).collect();
|
|
703
703
|
assert_eq!(refresh(&utf16), to(3, DOC));
|
|
704
704
|
assert_eq!(refresh(br#"<meta a=">" http-equiv=refresh content=5>"#), to(5, DOC));
|
|
705
|
+
assert_eq!(refresh(b"<script>'<meta a=\"</script><meta http-equiv=refresh content=6>\"'</script>"), to(6, DOC));
|
|
705
706
|
assert_eq!(refresh(b"<meta name=refresh content=4><p http-equiv=refresh>http-equiv</p>"), None);
|
|
706
707
|
let base = |html: &[u8]| nav(html, None, true).base_url;
|
|
707
708
|
assert_eq!(base(b"<BASE\thref=/a/>"), Some("http://h/a/".into()));
|
|
@@ -709,6 +710,16 @@ mod tests {
|
|
|
709
710
|
assert_eq!(base(b"<basefont href=/c/>"), Some(DOC.into()));
|
|
710
711
|
}
|
|
711
712
|
|
|
713
|
+
#[test]
|
|
714
|
+
fn many_unclosed_meta_tags_take_linear_time() {
|
|
715
|
+
let evil = "<meta ".repeat(80000);
|
|
716
|
+
let start = std::time::Instant::now();
|
|
717
|
+
assert_eq!(nav(evil.as_bytes(), None, true).meta_refresh, None);
|
|
718
|
+
let good = evil + r#"<meta http-equiv="refresh" content="3;url=/next">"#;
|
|
719
|
+
assert_eq!(nav(good.as_bytes(), None, true).meta_refresh, Some((3, "http://h/next".into())));
|
|
720
|
+
assert!(start.elapsed().as_secs() < 2, "{:?}", start.elapsed());
|
|
721
|
+
}
|
|
722
|
+
|
|
712
723
|
#[test]
|
|
713
724
|
fn delay_is_whole_seconds() {
|
|
714
725
|
let delay = |content: &str| nav(refresh(content).as_bytes(), None, true).meta_refresh.map(|(d, _)| d);
|
|
@@ -477,7 +477,7 @@ fn handle_markup<'a, S: TokenSink<'a>>(
|
|
|
477
477
|
/// After `</img\nsrc="http:>`, whose value is unterminated and so runs on to the next quote 300 bytes
|
|
478
478
|
/// later, that reading keeps an `</a>`, two `</div>`s and a whole `<div id='HTML3'>` start tag.
|
|
479
479
|
/// Reporting an element the document does not contain is the FALSE-POSITIVE direction.
|
|
480
|
-
|
|
480
|
+
fn scan_attrs<'a>(
|
|
481
481
|
b: &'a [u8],
|
|
482
482
|
mut i: usize,
|
|
483
483
|
attr_buf: &mut Vec<(&'a [u8], Option<&'a [u8]>)>,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|