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.
Files changed (133) hide show
  1. {frostwork-0.2.1 → frostwork-0.2.2}/CHANGELOG.md +7 -0
  2. {frostwork-0.2.1 → frostwork-0.2.2}/Cargo.lock +1 -1
  3. {frostwork-0.2.1 → frostwork-0.2.2}/Cargo.toml +1 -1
  4. {frostwork-0.2.1 → frostwork-0.2.2}/PKG-INFO +1 -1
  5. {frostwork-0.2.1 → frostwork-0.2.2}/pyproject.toml +2 -2
  6. {frostwork-0.2.1 → frostwork-0.2.2}/src/lib.rs +17 -17
  7. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/navigation.rs +11 -0
  8. {frostwork-0.2.1 → frostwork-0.2.2}/src/tokenizer.rs +1 -1
  9. {frostwork-0.2.1 → frostwork-0.2.2}/.github/actions/oracle/action.yml +0 -0
  10. {frostwork-0.2.1 → frostwork-0.2.2}/.github/dependabot.yml +0 -0
  11. {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/bench-migration.yml +0 -0
  12. {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/ci.yml +0 -0
  13. {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/publish.yml +0 -0
  14. {frostwork-0.2.1 → frostwork-0.2.2}/.github/workflows/scheduled.yml +0 -0
  15. {frostwork-0.2.1 → frostwork-0.2.2}/.gitignore +0 -0
  16. {frostwork-0.2.1 → frostwork-0.2.2}/.pre-commit-config.yaml +0 -0
  17. {frostwork-0.2.1 → frostwork-0.2.2}/AGENTS.md +0 -0
  18. {frostwork-0.2.1 → frostwork-0.2.2}/CLAUDE.md +0 -0
  19. {frostwork-0.2.1 → frostwork-0.2.2}/LICENSE +0 -0
  20. {frostwork-0.2.1 → frostwork-0.2.2}/Makefile +0 -0
  21. {frostwork-0.2.1 → frostwork-0.2.2}/README.md +0 -0
  22. {frostwork-0.2.1 → frostwork-0.2.2}/SECURITY.md +0 -0
  23. {frostwork-0.2.1 → frostwork-0.2.2}/docs/BENCHMARKS.md +0 -0
  24. {frostwork-0.2.1 → frostwork-0.2.2}/docs/COMPATIBILITY.md +0 -0
  25. {frostwork-0.2.1 → frostwork-0.2.2}/docs/DESIGN.md +0 -0
  26. {frostwork-0.2.1 → frostwork-0.2.2}/docs/MIGRATION.md +0 -0
  27. {frostwork-0.2.1 → frostwork-0.2.2}/docs/PYTHON.md +0 -0
  28. {frostwork-0.2.1 → frostwork-0.2.2}/docs/RELEASING.md +0 -0
  29. {frostwork-0.2.1 → frostwork-0.2.2}/docs/SUPPORT_SNAPSHOT.md +0 -0
  30. {frostwork-0.2.1 → frostwork-0.2.2}/docs/TESTING.md +0 -0
  31. {frostwork-0.2.1 → frostwork-0.2.2}/docs/WEBPOET_SURFACE.md +0 -0
  32. {frostwork-0.2.1 → frostwork-0.2.2}/examples/basic.py +0 -0
  33. {frostwork-0.2.1 → frostwork-0.2.2}/examples/basic.rs +0 -0
  34. {frostwork-0.2.1 → frostwork-0.2.2}/python/AGENTS.md +0 -0
  35. {frostwork-0.2.1 → frostwork-0.2.2}/python/CLAUDE.md +0 -0
  36. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/__init__.py +0 -0
  37. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/_frostwork.pyi +0 -0
  38. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/_navigation.py +0 -0
  39. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/audit.py +0 -0
  40. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/encoding.py +0 -0
  41. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/page.py +0 -0
  42. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/py.typed +0 -0
  43. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/scan.py +0 -0
  44. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/validation.py +0 -0
  45. {frostwork-0.2.1 → frostwork-0.2.2}/python/frostwork/webpoet.py +0 -0
  46. {frostwork-0.2.1 → frostwork-0.2.2}/requirements-bench.txt +0 -0
  47. {frostwork-0.2.1 → frostwork-0.2.2}/requirements-release.txt +0 -0
  48. {frostwork-0.2.1 → frostwork-0.2.2}/requirements-test.txt +0 -0
  49. {frostwork-0.2.1 → frostwork-0.2.2}/src/bin/bench.rs +0 -0
  50. {frostwork-0.2.1 → frostwork-0.2.2}/src/bin/differ.rs +0 -0
  51. {frostwork-0.2.1 → frostwork-0.2.2}/src/diagnostics.rs +0 -0
  52. {frostwork-0.2.1 → frostwork-0.2.2}/src/encoding.rs +0 -0
  53. {frostwork-0.2.1 → frostwork-0.2.2}/src/entities.rs +0 -0
  54. {frostwork-0.2.1 → frostwork-0.2.2}/src/implied_close/generated.rs +0 -0
  55. {frostwork-0.2.1 → frostwork-0.2.2}/src/implied_close/mod.rs +0 -0
  56. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/compile.rs +0 -0
  57. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/decode.rs +0 -0
  58. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/deferred.rs +0 -0
  59. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/frame.rs +0 -0
  60. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/matching.rs +0 -0
  61. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/mod.rs +0 -0
  62. {frostwork-0.2.1 → frostwork-0.2.2}/src/matcher/sig.rs +0 -0
  63. {frostwork-0.2.1 → frostwork-0.2.2}/src/mutate.rs +0 -0
  64. {frostwork-0.2.1 → frostwork-0.2.2}/src/page.rs +0 -0
  65. {frostwork-0.2.1 → frostwork-0.2.2}/src/python.rs +0 -0
  66. {frostwork-0.2.1 → frostwork-0.2.2}/src/selector.rs +0 -0
  67. {frostwork-0.2.1 → frostwork-0.2.2}/src/xpath.rs +0 -0
  68. {frostwork-0.2.1 → frostwork-0.2.2}/tests/conftest.py +0 -0
  69. {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/README.md +0 -0
  70. {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/catalog/pages/test-listing.html +0 -0
  71. {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/catalog/selectors.json +0 -0
  72. {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/docgen/pages/test-api.html +0 -0
  73. {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/docgen/selectors.json +0 -0
  74. {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/tabular/pages/test-report.html +0 -0
  75. {frostwork-0.2.1 → frostwork-0.2.2}/tests/corpus/tabular/selectors.json +0 -0
  76. {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/article.html +0 -0
  77. {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/listing.html +0 -0
  78. {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/manifest.json +0 -0
  79. {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/pages.py +0 -0
  80. {frostwork-0.2.1 → frostwork-0.2.2}/tests/migration/product.html +0 -0
  81. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_audit_context.py +0 -0
  82. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_composition.py +0 -0
  83. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_doc_examples.py +0 -0
  84. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_docs_check.py +0 -0
  85. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_encoding.py +0 -0
  86. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_gates.py +0 -0
  87. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_make_gates.py +0 -0
  88. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_migration.py +0 -0
  89. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_navigation.py +0 -0
  90. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_python.py +0 -0
  91. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_release.py +0 -0
  92. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_schema.py +0 -0
  93. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_typing.py +0 -0
  94. {frostwork-0.2.1 → frostwork-0.2.2}/tests/test_validation.py +0 -0
  95. {frostwork-0.2.1 → frostwork-0.2.2}/tests/typing_fixture.py +0 -0
  96. {frostwork-0.2.1 → frostwork-0.2.2}/tools/AGENTS.md +0 -0
  97. {frostwork-0.2.1 → frostwork-0.2.2}/tools/CLAUDE.md +0 -0
  98. {frostwork-0.2.1 → frostwork-0.2.2}/tools/ab_bench.py +0 -0
  99. {frostwork-0.2.1 → frostwork-0.2.2}/tools/abi3_smoke.py +0 -0
  100. {frostwork-0.2.1 → frostwork-0.2.2}/tools/audit_tree_rules.py +0 -0
  101. {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_corpus.py +0 -0
  102. {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_encoding.py +0 -0
  103. {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_engines.py +0 -0
  104. {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_matrix.py +0 -0
  105. {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_mem.py +0 -0
  106. {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_navigation.py +0 -0
  107. {frostwork-0.2.1 → frostwork-0.2.2}/tools/bench_webpoet.py +0 -0
  108. {frostwork-0.2.1 → frostwork-0.2.2}/tools/check_docs.py +0 -0
  109. {frostwork-0.2.1 → frostwork-0.2.2}/tools/conformant.py +0 -0
  110. {frostwork-0.2.1 → frostwork-0.2.2}/tools/corpus_fetch.py +0 -0
  111. {frostwork-0.2.1 → frostwork-0.2.2}/tools/decoder_sweep.py +0 -0
  112. {frostwork-0.2.1 → frostwork-0.2.2}/tools/diff_fuzz.py +0 -0
  113. {frostwork-0.2.1 → frostwork-0.2.2}/tools/diff_lxml.py +0 -0
  114. {frostwork-0.2.1 → frostwork-0.2.2}/tools/diff_webpoet.py +0 -0
  115. {frostwork-0.2.1 → frostwork-0.2.2}/tools/enc_cases.py +0 -0
  116. {frostwork-0.2.1 → frostwork-0.2.2}/tools/enc_check.py +0 -0
  117. {frostwork-0.2.1 → frostwork-0.2.2}/tools/encpages.py +0 -0
  118. {frostwork-0.2.1 → frostwork-0.2.2}/tools/families.py +0 -0
  119. {frostwork-0.2.1 → frostwork-0.2.2}/tools/foreign.py +0 -0
  120. {frostwork-0.2.1 → frostwork-0.2.2}/tools/gen_tree_rules.py +0 -0
  121. {frostwork-0.2.1 → frostwork-0.2.2}/tools/mutate_rules.py +0 -0
  122. {frostwork-0.2.1 → frostwork-0.2.2}/tools/mutate_webpoet.py +0 -0
  123. {frostwork-0.2.1 → frostwork-0.2.2}/tools/oracle.py +0 -0
  124. {frostwork-0.2.1 → frostwork-0.2.2}/tools/release_check.py +0 -0
  125. {frostwork-0.2.1 → frostwork-0.2.2}/tools/sel_fuzz.py +0 -0
  126. {frostwork-0.2.1 → frostwork-0.2.2}/tools/seq_sweep.py +0 -0
  127. {frostwork-0.2.1 → frostwork-0.2.2}/tools/soak.py +0 -0
  128. {frostwork-0.2.1 → frostwork-0.2.2}/tools/support_snapshot.py +0 -0
  129. {frostwork-0.2.1 → frostwork-0.2.2}/tools/verify_migration.py +0 -0
  130. {frostwork-0.2.1 → frostwork-0.2.2}/tools/verify_pypi.py +0 -0
  131. {frostwork-0.2.1 → frostwork-0.2.2}/tools/webpoet_cases.py +0 -0
  132. {frostwork-0.2.1 → frostwork-0.2.2}/tools/webpoet_structure.py +0 -0
  133. {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
@@ -50,7 +50,7 @@ dependencies = [
50
50
 
51
51
  [[package]]
52
52
  name = "frostwork"
53
- version = "0.2.1"
53
+ version = "0.2.2"
54
54
  dependencies = [
55
55
  "chardetng",
56
56
  "encoding_rs",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "frostwork"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
  description = "Treeless, one-pass HTML extraction — CSS/XPath selectors matched on a streaming scan, no DOM."
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frostwork
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -5,7 +5,7 @@ build-backend = "maturin"
5
5
 
6
6
  [project]
7
7
  name = "frostwork"
8
- version = "0.2.1"
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.1"
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 mut attrs = Vec::new();
756
- memchr::memchr_iter(b'<', bytes).any(|i| {
757
- let tag_end = |name: &[u8]| {
758
- let end = i + 1 + name.len();
759
- (bytes.get(i + 1..end).is_some_and(|candidate| candidate.eq_ignore_ascii_case(name))
760
- && bytes.get(end).is_some_and(|&c| !tokenizer::is_name_char(c)))
761
- .then_some(end)
762
- };
763
- (base && tag_end(b"base").is_some())
764
- || (refresh
765
- && tag_end(b"meta").is_some_and(|end| {
766
- attrs.clear();
767
- tokenizer::scan_attrs(bytes, end, &mut attrs);
768
- attrs.iter().any(|(name, _)| name.eq_ignore_ascii_case(b"http-equiv"))
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
- pub(crate) fn scan_attrs<'a>(
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