innertext 0.2.2__tar.gz → 0.2.4__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.
@@ -115,7 +115,7 @@ dependencies = [
115
115
 
116
116
  [[package]]
117
117
  name = "innertext-core"
118
- version = "0.2.2"
118
+ version = "0.2.4"
119
119
  dependencies = [
120
120
  "html5ever",
121
121
  "markup5ever",
@@ -128,7 +128,7 @@ dependencies = [
128
128
 
129
129
  [[package]]
130
130
  name = "innertext-java"
131
- version = "0.2.2"
131
+ version = "0.2.4"
132
132
  dependencies = [
133
133
  "innertext-core",
134
134
  "jni",
@@ -137,7 +137,7 @@ dependencies = [
137
137
 
138
138
  [[package]]
139
139
  name = "innertext-node"
140
- version = "0.2.2"
140
+ version = "0.2.4"
141
141
  dependencies = [
142
142
  "innertext-core",
143
143
  "napi",
@@ -146,7 +146,7 @@ dependencies = [
146
146
 
147
147
  [[package]]
148
148
  name = "innertext-python"
149
- version = "0.2.2"
149
+ version = "0.2.4"
150
150
  dependencies = [
151
151
  "innertext-core",
152
152
  "pyo3",
@@ -6,7 +6,7 @@ members = ["crates/innertext-core", "bindings/innertext-python"]
6
6
  resolver = "2"
7
7
 
8
8
  [workspace.package]
9
- version = "0.2.2"
9
+ version = "0.2.4"
10
10
  edition = "2021"
11
11
  license = "MIT"
12
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: innertext
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -55,6 +55,26 @@ print(innertext.inner_text(html)) # "Hello World"
55
55
  print(innertext.text_content(html)) # "Hello hidden console.log("not shown")"
56
56
  ```
57
57
 
58
+ ## Packaging Notes
59
+
60
+ The published Python package uses a two-layer layout:
61
+ - `innertext` is a regular Python package with a runtime `__init__.py`
62
+ - the compiled Rust extension is loaded as `innertext._innertext`
63
+
64
+ This avoids namespace-package import issues and ensures `import innertext`
65
+ always exposes `inner_text`, `outer_text`, and `text_content`.
66
+
67
+ ## Type Hints
68
+
69
+ This package ships with PEP 561-compatible typing metadata:
70
+ - `innertext/__init__.pyi` defines the public API types
71
+ - `innertext/py.typed` marks the package as typed
72
+
73
+ Type checkers such as mypy and pyright can therefore infer:
74
+ - `inner_text(html: str) -> str`
75
+ - `outer_text(html: str) -> str`
76
+ - `text_content(html: str) -> str`
77
+
58
78
  ## API
59
79
 
60
80
  ### Functions
@@ -27,6 +27,26 @@ print(innertext.inner_text(html)) # "Hello World"
27
27
  print(innertext.text_content(html)) # "Hello hidden console.log("not shown")"
28
28
  ```
29
29
 
30
+ ## Packaging Notes
31
+
32
+ The published Python package uses a two-layer layout:
33
+ - `innertext` is a regular Python package with a runtime `__init__.py`
34
+ - the compiled Rust extension is loaded as `innertext._innertext`
35
+
36
+ This avoids namespace-package import issues and ensures `import innertext`
37
+ always exposes `inner_text`, `outer_text`, and `text_content`.
38
+
39
+ ## Type Hints
40
+
41
+ This package ships with PEP 561-compatible typing metadata:
42
+ - `innertext/__init__.pyi` defines the public API types
43
+ - `innertext/py.typed` marks the package as typed
44
+
45
+ Type checkers such as mypy and pyright can therefore infer:
46
+ - `inner_text(html: str) -> str`
47
+ - `outer_text(html: str) -> str`
48
+ - `text_content(html: str) -> str`
49
+
30
50
  ## API
31
51
 
32
52
  ### Functions
@@ -27,6 +27,26 @@ print(innertext.inner_text(html)) # "Hello World"
27
27
  print(innertext.text_content(html)) # "Hello hidden console.log("not shown")"
28
28
  ```
29
29
 
30
+ ## Packaging Notes
31
+
32
+ The published Python package uses a two-layer layout:
33
+ - `innertext` is a regular Python package with a runtime `__init__.py`
34
+ - the compiled Rust extension is loaded as `innertext._innertext`
35
+
36
+ This avoids namespace-package import issues and ensures `import innertext`
37
+ always exposes `inner_text`, `outer_text`, and `text_content`.
38
+
39
+ ## Type Hints
40
+
41
+ This package ships with PEP 561-compatible typing metadata:
42
+ - `innertext/__init__.pyi` defines the public API types
43
+ - `innertext/py.typed` marks the package as typed
44
+
45
+ Type checkers such as mypy and pyright can therefore infer:
46
+ - `inner_text(html: str) -> str`
47
+ - `outer_text(html: str) -> str`
48
+ - `text_content(html: str) -> str`
49
+
30
50
  ## API
31
51
 
32
52
  ### Functions
@@ -29,7 +29,7 @@ fn text_content(html: &str) -> Result<String, PyErr> {
29
29
  }
30
30
 
31
31
  #[pymodule]
32
- fn innertext(m: &Bound<'_, PyModule>) -> PyResult<()> {
32
+ fn _innertext(m: &Bound<'_, PyModule>) -> PyResult<()> {
33
33
  m.add_function(wrap_pyfunction!(inner_text, m)?)?;
34
34
  m.add_function(wrap_pyfunction!(outer_text, m)?)?;
35
35
  m.add_function(wrap_pyfunction!(text_content, m)?)?;
@@ -22,6 +22,8 @@ pub(crate) fn extract(dom: &RcDom) -> String {
22
22
  }
23
23
 
24
24
  let normalized = normalize_tokens(tokens);
25
+ let normalized = drop_inter_block_separator_spaces(normalized);
26
+ let normalized = drop_boundaries_between_required_breaks(normalized);
25
27
  let folded = fold_required_break_runs(normalized);
26
28
  serialize_tokens(folded)
27
29
  }
@@ -133,6 +135,48 @@ fn normalize_tokens(mut tokens: Vec<RenderToken>) -> Vec<RenderToken> {
133
135
  tokens
134
136
  }
135
137
 
138
+ fn drop_inter_block_separator_spaces(tokens: Vec<RenderToken>) -> Vec<RenderToken> {
139
+ let mut out = Vec::with_capacity(tokens.len());
140
+
141
+ for i in 0..tokens.len() {
142
+ if let RenderToken::Text(text) = &tokens[i] {
143
+ let prev_is_marker = i > 0
144
+ && matches!(
145
+ tokens.get(i - 1),
146
+ Some(RenderToken::RequiredBreak(_) | RenderToken::CollapseBoundary)
147
+ );
148
+ let next_is_marker = matches!(
149
+ tokens.get(i + 1),
150
+ Some(RenderToken::RequiredBreak(_) | RenderToken::CollapseBoundary)
151
+ );
152
+ if text.chars().all(|ch| ch == ' ') && prev_is_marker && next_is_marker {
153
+ continue;
154
+ }
155
+ }
156
+ out.push(tokens[i].clone());
157
+ }
158
+
159
+ out
160
+ }
161
+
162
+ fn drop_boundaries_between_required_breaks(tokens: Vec<RenderToken>) -> Vec<RenderToken> {
163
+ let mut out = Vec::with_capacity(tokens.len());
164
+
165
+ for i in 0..tokens.len() {
166
+ if matches!(tokens[i], RenderToken::CollapseBoundary) {
167
+ let prev_is_break =
168
+ i > 0 && matches!(tokens.get(i - 1), Some(RenderToken::RequiredBreak(_)));
169
+ let next_is_break = matches!(tokens.get(i + 1), Some(RenderToken::RequiredBreak(_)));
170
+ if prev_is_break && next_is_break {
171
+ continue;
172
+ }
173
+ }
174
+ out.push(tokens[i].clone());
175
+ }
176
+
177
+ out
178
+ }
179
+
136
180
  fn fold_required_break_runs(tokens: Vec<RenderToken>) -> Vec<RenderToken> {
137
181
  let mut out = Vec::new();
138
182
  let mut i = 0;
@@ -51,6 +51,48 @@ fn sibling_blocks_have_two_newlines_between_them() {
51
51
  assert_eq!(got, "Block A\nBlock B");
52
52
  }
53
53
 
54
+ #[test]
55
+ fn formatting_whitespace_between_p_tags_does_not_change_inner_text() {
56
+ let compact = "<p>Some text</p><p>Some more text which <span>is inside a span</span></p><p style='display: none'>This text is hidden</p>";
57
+ let spaced = "<p>Some text</p>\n\n<p>Some more text which <span>is inside a span</span></p><p style='display: none'>This text is hidden</p>";
58
+ let heavily_spaced = "<p>Some text</p>\n\n\n\n <p>Some more text which <span>is inside a span</span></p><p style='display: none'>This text is hidden</p>";
59
+
60
+ let expected = inner_text_from_html(compact).expect("must extract compact text");
61
+ let spaced_actual = inner_text_from_html(spaced).expect("must extract spaced text");
62
+ let heavily_spaced_actual =
63
+ inner_text_from_html(heavily_spaced).expect("must extract heavily spaced text");
64
+
65
+ assert_eq!(spaced_actual, expected);
66
+ assert_eq!(heavily_spaced_actual, expected);
67
+ assert_eq!(
68
+ expected,
69
+ "Some text\n\nSome more text which is inside a span"
70
+ );
71
+ }
72
+
73
+ #[test]
74
+ fn formatting_whitespace_between_div_tags_does_not_change_inner_text() {
75
+ let compact = "<div>Block A</div><div>Block B</div>";
76
+ let spaced = "<div>Block A</div>\n\n\n <div>Block B</div>";
77
+
78
+ let expected = inner_text_from_html(compact).expect("must extract compact text");
79
+ let actual = inner_text_from_html(spaced).expect("must extract spaced text");
80
+
81
+ assert_eq!(actual, expected);
82
+ assert_eq!(expected, "Block A\nBlock B");
83
+ }
84
+
85
+ #[test]
86
+ fn hidden_block_with_separator_whitespace_does_not_add_extra_gaps() {
87
+ let compact = "<p>One</p><p style='display:none'>Hidden</p><p>Two</p>";
88
+ let spaced = "<p>One</p>\n\n\n<p style='display:none'>Hidden</p>\n\n\n<p>Two</p>";
89
+
90
+ let expected = inner_text_from_html(compact).expect("must extract compact text");
91
+ let actual = inner_text_from_html(spaced).expect("must extract spaced text");
92
+
93
+ assert_eq!(actual, expected);
94
+ }
95
+
54
96
  // ── outerText getter ───────────────────────────────────────────────────────
55
97
 
56
98
  #[test]
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "innertext"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Pure Rust WHATWG-compliant innerText, outerText, and textContent extraction"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -38,5 +38,6 @@ Homepage = "https://github.com/joshfayer/innertext"
38
38
 
39
39
  [tool.maturin]
40
40
  features = ["pyo3/extension-module", "pyo3/abi3-py37"]
41
+ module-name = "innertext._innertext"
41
42
  manifest-path = "bindings/innertext-python/Cargo.toml"
42
43
  python-source = "python"
@@ -0,0 +1,8 @@
1
+ """innertext - Pure Rust WHATWG-compliant text extraction from HTML.
2
+
3
+ This package re-exports functions from the compiled Rust extension.
4
+ """
5
+
6
+ from ._innertext import inner_text, outer_text, text_content
7
+
8
+ __all__ = ["inner_text", "outer_text", "text_content"]
File without changes