unicode-segmentation-rs 0.3.2__tar.gz → 0.3.3__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 (34) hide show
  1. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/actions/pre-commit-setup/action.yml +4 -3
  2. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/workflows/ci.yml +2 -2
  3. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/workflows/pre-commit.yml +1 -0
  4. unicode_segmentation_rs-0.3.3/.github/workflows/security-audit.yml +42 -0
  5. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.pre-commit-config.yaml +2 -2
  6. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/Cargo.lock +11 -11
  7. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/Cargo.toml +2 -2
  8. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/PKG-INFO +9 -1
  9. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/README.md +8 -0
  10. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/src/lib.rs +149 -44
  11. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.editorconfig +0 -0
  12. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/FUNDING.yml +0 -0
  13. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  15. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  16. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/PULL_REQUEST_TEMPLATE.md.license +0 -0
  18. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/release.yml +0 -0
  19. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/renovate.json +0 -0
  20. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/renovate.json.license +0 -0
  21. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/workflows/dependency-review.yml +0 -0
  22. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.github/workflows/pull_requests.yaml +0 -0
  23. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.gitignore +0 -0
  24. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/.yamllint.yml +0 -0
  25. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/LICENSE +0 -0
  26. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/LICENSES/Apache-2.0.txt +0 -0
  27. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/LICENSES/CC0-1.0.txt +0 -0
  28. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/REUSE.toml +0 -0
  29. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/SECURITY.md +0 -0
  30. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/pyproject.toml +0 -0
  31. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/tests/test_gettext_wrap.py +0 -0
  32. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/tests/test_readme_examples.py +0 -0
  33. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/tests/test_unicode_segmentation.py +0 -0
  34. {unicode_segmentation_rs-0.3.2 → unicode_segmentation_rs-0.3.3}/unicode_segmentation_rs.pyi +0 -0
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: CC0-1.0
4
4
 
5
+ # This file is maintained in https://github.com/WeblateOrg/meta/
6
+
5
7
  name: pre-commit setup
6
8
  description: Prepare pre-commit environment
7
9
 
@@ -19,8 +21,7 @@ runs:
19
21
  - name: Setup Python
20
22
  uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
21
23
  with:
22
- python-version: '3.15'
23
- allow-prereleases: true
24
+ python-version: '3.14'
24
25
  - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
25
26
  with:
26
- version: 0.12.1
27
+ version: 0.12.3
@@ -53,7 +53,7 @@ jobs:
53
53
  with:
54
54
  save-cache: ${{ github.ref == 'refs/heads/main' }}
55
55
  cache-suffix: ${{ steps.setup_python.outputs.python-version }}
56
- version: 0.12.1
56
+ version: 0.12.3
57
57
  activate-environment: 'true'
58
58
  - run: uv sync
59
59
  - name: Run tests
@@ -315,7 +315,7 @@ jobs:
315
315
  persist-credentials: false
316
316
  - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
317
317
  - name: Generate artifact attestation
318
- uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
318
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
319
319
  with:
320
320
  subject-path: wheels-*/*
321
321
  - name: Publish to PyPI
@@ -3,6 +3,7 @@
3
3
  # SPDX-License-Identifier: CC0-1.0
4
4
 
5
5
  # This file is maintained in https://github.com/WeblateOrg/meta/
6
+
6
7
  name: Pre-commit check
7
8
 
8
9
  on:
@@ -0,0 +1,42 @@
1
+ # Copyright © Michal Čihař <michal@weblate.org>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ name: Security audit
6
+
7
+ on:
8
+ pull_request:
9
+ push:
10
+ branches:
11
+ - main
12
+ paths:
13
+ - Cargo.lock
14
+ - Cargo.toml
15
+ - .github/workflows/security-audit.yml
16
+ schedule:
17
+ - cron: 37 4 * * 1
18
+ workflow_dispatch:
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ env:
24
+ # renovate: datasource=crate depName=cargo-audit
25
+ CARGO_AUDIT_VERSION: 0.22.2
26
+
27
+ jobs:
28
+ rustsec:
29
+ name: RustSec audit
30
+ runs-on: ubuntu-24.04
31
+ timeout-minutes: 15
32
+ steps:
33
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
+ with:
35
+ persist-credentials: false
36
+ - name: Install cargo-audit
37
+ uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10
38
+ with:
39
+ fallback: none
40
+ tool: cargo-audit@${{ env.CARGO_AUDIT_VERSION }}
41
+ - name: Audit Rust dependencies
42
+ run: cargo audit --file Cargo.lock
@@ -21,7 +21,7 @@ repos:
21
21
  hooks:
22
22
  - id: yamllint
23
23
  - repo: https://github.com/astral-sh/ruff-pre-commit
24
- rev: v0.16.1
24
+ rev: v0.16.2
25
25
  hooks:
26
26
  - id: ruff-check
27
27
  args:
@@ -52,7 +52,7 @@ repos:
52
52
  hooks:
53
53
  - id: zizmor
54
54
  - repo: https://github.com/mongodb/kingfisher
55
- rev: v1.110.0
55
+ rev: v1.112.0
56
56
  hooks:
57
57
  - id: kingfisher-auto
58
58
  - repo: https://github.com/rvben/rumdl-pre-commit
@@ -37,9 +37,9 @@ dependencies = [
37
37
 
38
38
  [[package]]
39
39
  name = "pyo3"
40
- version = "0.29.1"
40
+ version = "0.29.2"
41
41
  source = "registry+https://github.com/rust-lang/crates.io-index"
42
- checksum = "fc153f5fd745cc038b5eed86622125969f8a39834a57bc96beaaf2512b1da729"
42
+ checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
43
43
  dependencies = [
44
44
  "libc",
45
45
  "once_cell",
@@ -51,18 +51,18 @@ dependencies = [
51
51
 
52
52
  [[package]]
53
53
  name = "pyo3-build-config"
54
- version = "0.29.1"
54
+ version = "0.29.2"
55
55
  source = "registry+https://github.com/rust-lang/crates.io-index"
56
- checksum = "bb77d9aa6d647507b55c69ee714d266d84c526c78ff0bc6dd8757f58591e64d1"
56
+ checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
57
57
  dependencies = [
58
58
  "target-lexicon",
59
59
  ]
60
60
 
61
61
  [[package]]
62
62
  name = "pyo3-ffi"
63
- version = "0.29.1"
63
+ version = "0.29.2"
64
64
  source = "registry+https://github.com/rust-lang/crates.io-index"
65
- checksum = "3160087aa5733bce7d9a729f8c55f85a2a53b74742a09613178eeebff0722253"
65
+ checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
66
66
  dependencies = [
67
67
  "libc",
68
68
  "pyo3-build-config",
@@ -70,9 +70,9 @@ dependencies = [
70
70
 
71
71
  [[package]]
72
72
  name = "pyo3-macros"
73
- version = "0.29.1"
73
+ version = "0.29.2"
74
74
  source = "registry+https://github.com/rust-lang/crates.io-index"
75
- checksum = "91f9d455db760a9a0b0ddeaac25f1390b8a36ba73dfbda9f127cac6fc340d4d5"
75
+ checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
76
76
  dependencies = [
77
77
  "proc-macro2",
78
78
  "pyo3-macros-backend",
@@ -82,9 +82,9 @@ dependencies = [
82
82
 
83
83
  [[package]]
84
84
  name = "pyo3-macros-backend"
85
- version = "0.29.1"
85
+ version = "0.29.2"
86
86
  source = "registry+https://github.com/rust-lang/crates.io-index"
87
- checksum = "e343bcec300ff262f5806a33a4e51b6d097a8a46435f512fcb83e95592581625"
87
+ checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
88
88
  dependencies = [
89
89
  "heck",
90
90
  "proc-macro2",
@@ -138,7 +138,7 @@ checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
138
138
 
139
139
  [[package]]
140
140
  name = "unicode-segmentation-rs"
141
- version = "0.3.2"
141
+ version = "0.3.3"
142
142
  dependencies = [
143
143
  "pyo3",
144
144
  "unicode-linebreak",
@@ -1,5 +1,5 @@
1
1
  [dependencies]
2
- pyo3 = {version = "0.29.1", features = ["extension-module", "abi3-py310"]}
2
+ pyo3 = {version = "0.29.2", features = ["extension-module", "abi3-py310"]}
3
3
  unicode-linebreak = "0.1.5"
4
4
  unicode-segmentation = "1.13.3"
5
5
  unicode-width = "0.2.2"
@@ -19,4 +19,4 @@ license = "Apache-2.0"
19
19
  name = "unicode-segmentation-rs"
20
20
  readme = "README.md"
21
21
  repository = "https://github.com/WeblateOrg/unicode-segmentation-rs"
22
- version = "0.3.2"
22
+ version = "0.3.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unicode-segmentation-rs
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3.10
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -26,6 +26,11 @@ Project-URL: Issue Tracker, https://github.com/WeblateOrg/unicode-segmentation-r
26
26
  Project-URL: Source Code, https://github.com/WeblateOrg/unicode-segmentation-rs
27
27
  Project-URL: Twitter, https://twitter.com/WeblateOrg
28
28
 
29
+ <a href="https://weblate.org/"><img alt="Weblate" src="https://s.weblate.org/cdn/Logo-Darktext-borders.png" height="80px" /></a>
30
+
31
+ **Weblate is libre software web-based continuous localization system,
32
+ used by over 2500 libre projects and companies in more than 165 countries.**
33
+
29
34
  # unicode-segmentation-rs
30
35
 
31
36
  Python bindings for the Rust [unicode-segmentation](https://docs.rs/unicode-segmentation/), [unicode-linebreak](https://docs.rs/unicode-linebreak/), and [unicode-width](https://docs.rs/unicode-width/) crates, providing Unicode text segmentation and width calculation according to Unicode standards.
@@ -235,6 +240,9 @@ print(lines)
235
240
 
236
241
  ## API Reference
237
242
 
243
+ Functions returning lists materialize the complete result. Applications processing very large or
244
+ untrusted text should enforce an input-size limit appropriate for their environment.
245
+
238
246
  ### `graphemes(text: str, is_extended: bool) -> list[str]`
239
247
 
240
248
  Split a string into grapheme clusters. Set `is_extended=True` for extended grapheme clusters (recommended).
@@ -1,3 +1,8 @@
1
+ <a href="https://weblate.org/"><img alt="Weblate" src="https://s.weblate.org/cdn/Logo-Darktext-borders.png" height="80px" /></a>
2
+
3
+ **Weblate is libre software web-based continuous localization system,
4
+ used by over 2500 libre projects and companies in more than 165 countries.**
5
+
1
6
  # unicode-segmentation-rs
2
7
 
3
8
  Python bindings for the Rust [unicode-segmentation](https://docs.rs/unicode-segmentation/), [unicode-linebreak](https://docs.rs/unicode-linebreak/), and [unicode-width](https://docs.rs/unicode-width/) crates, providing Unicode text segmentation and width calculation according to Unicode standards.
@@ -207,6 +212,9 @@ print(lines)
207
212
 
208
213
  ## API Reference
209
214
 
215
+ Functions returning lists materialize the complete result. Applications processing very large or
216
+ untrusted text should enforce an input-size limit appropriate for their environment.
217
+
210
218
  ### `graphemes(text: str, is_extended: bool) -> list[str]`
211
219
 
212
220
  Split a string into grapheme clusters. Set `is_extended=True` for extended grapheme clusters (recommended).
@@ -4,6 +4,8 @@
4
4
 
5
5
  #[pyo3::pymodule(gil_used = false)]
6
6
  mod unicode_segmentation_rs {
7
+ use std::ops::Range;
8
+
7
9
  use pyo3::prelude::*;
8
10
  use pyo3::types::PyList;
9
11
  use unicode_linebreak::{BreakOpportunity, linebreaks as unicode_linebreaks};
@@ -57,14 +59,16 @@ mod unicode_segmentation_rs {
57
59
 
58
60
  /// Get Unicode line-break opportunities as UTF-8 byte offsets.
59
61
  #[pyfunction]
60
- fn line_breaks(text: &str) -> PyResult<Vec<(usize, bool)>> {
62
+ fn line_breaks<'py>(py: Python<'py>, text: &str) -> PyResult<Bound<'py, PyList>> {
61
63
  if text.is_empty() {
62
- return Ok(vec![]);
64
+ return Ok(PyList::empty(py));
63
65
  }
64
66
 
65
- Ok(unicode_linebreaks(text)
66
- .map(|(offset, opportunity)| (offset, opportunity == BreakOpportunity::Mandatory))
67
- .collect())
67
+ PyList::new(
68
+ py,
69
+ unicode_linebreaks(text)
70
+ .map(|(offset, opportunity)| (offset, opportunity == BreakOpportunity::Mandatory)),
71
+ )
68
72
  }
69
73
 
70
74
  /// Split a string at every Unicode line-break opportunity.
@@ -100,21 +104,25 @@ mod unicode_segmentation_rs {
100
104
  /// - Handles CJK characters with proper width calculation
101
105
  /// - Breaks long words only when necessary
102
106
  #[pyfunction]
103
- fn gettext_wrap(text: &str, width: usize) -> PyResult<Vec<String>> {
107
+ fn gettext_wrap<'py>(
108
+ py: Python<'py>,
109
+ text: &str,
110
+ width: usize,
111
+ ) -> PyResult<Bound<'py, PyList>> {
104
112
  if text.is_empty() || width == 0 {
105
113
  return if text.is_empty() {
106
- Ok(vec![])
114
+ Ok(PyList::empty(py))
107
115
  } else {
108
- Ok(vec![text.to_string()])
116
+ PyList::new(py, [text])
109
117
  };
110
118
  }
111
119
 
112
- Ok(wrap_po_chunks(PoChunks::new(text), width))
120
+ PyList::new(py, wrap_po_chunks(text, PoChunks::new(text), width))
113
121
  }
114
122
 
115
- /// Iterator over borrowed chunks split using word boundaries and PO-specific rules.
123
+ /// Iterator over chunk byte ranges split using word boundaries and PO-specific rules.
116
124
  struct PoChunks<'a> {
117
- text: &'a str,
125
+ text_len: usize,
118
126
  word_bounds: UWordBoundIndices<'a>,
119
127
  chunk_start: Option<usize>,
120
128
  last_char: Option<char>,
@@ -124,7 +132,7 @@ mod unicode_segmentation_rs {
124
132
  impl<'a> PoChunks<'a> {
125
133
  fn new(text: &'a str) -> Self {
126
134
  Self {
127
- text,
135
+ text_len: text.len(),
128
136
  word_bounds: text.split_word_bound_indices(),
129
137
  chunk_start: None,
130
138
  last_char: None,
@@ -133,11 +141,7 @@ mod unicode_segmentation_rs {
133
141
  }
134
142
 
135
143
  /// Add a word-boundary segment and return the completed chunk's byte range, if any.
136
- fn add_segment(
137
- &mut self,
138
- segment_start: usize,
139
- segment: &str,
140
- ) -> Option<std::ops::Range<usize>> {
144
+ fn add_segment(&mut self, segment_start: usize, segment: &str) -> Option<Range<usize>> {
141
145
  let first_char = segment.chars().next().unwrap();
142
146
  let should_merge = self.last_char.is_some_and(|last_char| {
143
147
  (self.second_last_char.is_none()
@@ -176,8 +180,8 @@ mod unicode_segmentation_rs {
176
180
  }
177
181
  }
178
182
 
179
- impl<'a> Iterator for PoChunks<'a> {
180
- type Item = &'a str;
183
+ impl Iterator for PoChunks<'_> {
184
+ type Item = Range<usize>;
181
185
 
182
186
  fn next(&mut self) -> Option<Self::Item> {
183
187
  while let Some((segment_start, segment)) = self.word_bounds.next() {
@@ -196,48 +200,100 @@ mod unicode_segmentation_rs {
196
200
  );
197
201
  }
198
202
 
199
- return Some(&self.text[completed_start..segment_end]);
203
+ return Some(completed_start..segment_end);
200
204
  }
201
205
 
202
206
  if let Some(completed) = self.add_segment(segment_start, segment) {
203
- return Some(&self.text[completed]);
207
+ return Some(completed);
204
208
  }
205
209
  }
206
210
 
207
- self.chunk_start
208
- .take()
209
- .map(|start| &self.text[start..self.text.len()])
211
+ self.chunk_start.take().map(|start| start..self.text_len)
210
212
  }
211
213
  }
212
214
 
213
- /// Wrap borrowed chunks into lines respecting the width limit.
214
- fn wrap_po_chunks<'a>(chunks: impl IntoIterator<Item = &'a str>, width: usize) -> Vec<String> {
215
- let mut lines = Vec::new();
216
- let mut current_line = String::new();
217
- let mut current_width = 0;
215
+ /// Iterator that wraps chunk ranges into borrowed lines respecting the width limit.
216
+ struct WrappedLines<'a, I>
217
+ where
218
+ I: Iterator<Item = Range<usize>>,
219
+ {
220
+ text: &'a str,
221
+ chunks: I,
222
+ width: usize,
223
+ current_start: Option<usize>,
224
+ current_end: usize,
225
+ current_width: usize,
226
+ emit_current: bool,
227
+ }
218
228
 
219
- for chunk in chunks {
220
- let chunk_width: usize = chunk.width();
229
+ impl<'a, I> Iterator for WrappedLines<'a, I>
230
+ where
231
+ I: Iterator<Item = Range<usize>>,
232
+ {
233
+ type Item = &'a str;
221
234
 
222
- if current_width + chunk_width > width && !current_line.is_empty() {
223
- lines.push(std::mem::take(&mut current_line));
224
- current_width = 0;
235
+ fn next(&mut self) -> Option<Self::Item> {
236
+ if self.emit_current {
237
+ self.emit_current = false;
238
+ self.current_width = 0;
239
+ let start = self.current_start.take()?;
240
+ return Some(&self.text[start..self.current_end]);
225
241
  }
226
- current_line.push_str(chunk);
227
- current_width += chunk_width;
228
242
 
229
- // Force break on \n
230
- if chunk.ends_with("\\n") {
231
- lines.push(std::mem::take(&mut current_line));
232
- current_width = 0;
243
+ for chunk_range in self.chunks.by_ref() {
244
+ let chunk_start = chunk_range.start;
245
+ let chunk_end = chunk_range.end;
246
+ debug_assert_eq!(
247
+ self.current_end, chunk_start,
248
+ "wrapped chunks should be contiguous"
249
+ );
250
+ let chunk = &self.text[chunk_range];
251
+ let chunk_width = chunk.width();
252
+
253
+ if self.current_width + chunk_width > self.width {
254
+ if let Some(completed_start) = self.current_start.replace(chunk_start) {
255
+ let completed_end = self.current_end;
256
+ self.current_end = chunk_end;
257
+ self.current_width = chunk_width;
258
+ self.emit_current = chunk.ends_with("\\n");
259
+ return Some(&self.text[completed_start..completed_end]);
260
+ }
261
+ }
262
+
263
+ if self.current_start.is_none() {
264
+ self.current_start = Some(chunk_start);
265
+ }
266
+ self.current_end = chunk_end;
267
+ self.current_width += chunk_width;
268
+
269
+ // Force break on \n
270
+ if chunk.ends_with("\\n") {
271
+ self.current_width = 0;
272
+ let start = self.current_start.take()?;
273
+ return Some(&self.text[start..self.current_end]);
274
+ }
233
275
  }
234
- }
235
276
 
236
- if !current_line.is_empty() {
237
- lines.push(current_line);
277
+ self.current_width = 0;
278
+ self.current_start
279
+ .take()
280
+ .map(|start| &self.text[start..self.current_end])
238
281
  }
282
+ }
239
283
 
240
- lines
284
+ fn wrap_po_chunks<C>(text: &str, chunks: C, width: usize) -> WrappedLines<'_, C::IntoIter>
285
+ where
286
+ C: IntoIterator<Item = Range<usize>>,
287
+ {
288
+ WrappedLines {
289
+ text,
290
+ chunks: chunks.into_iter(),
291
+ width,
292
+ current_start: None,
293
+ current_end: 0,
294
+ current_width: 0,
295
+ emit_current: false,
296
+ }
241
297
  }
242
298
 
243
299
  /// Check if a string contains only mergeable characters
@@ -300,4 +356,53 @@ mod unicode_segmentation_rs {
300
356
  | '~'
301
357
  )
302
358
  }
359
+
360
+ #[cfg(test)]
361
+ mod tests {
362
+ use std::cell::Cell;
363
+
364
+ use super::wrap_po_chunks;
365
+
366
+ #[test]
367
+ fn wrapped_lines_are_produced_incrementally() {
368
+ let consumed = Cell::new(0);
369
+ let text = "first second third fourth";
370
+ let chunks = [0..6, 6..13, 13..19, 19..25]
371
+ .into_iter()
372
+ .inspect(|_| consumed.set(consumed.get() + 1));
373
+ let mut lines = wrap_po_chunks(text, chunks, 7);
374
+
375
+ assert_eq!(lines.next(), Some("first "));
376
+ assert_eq!(consumed.get(), 2);
377
+ assert_eq!(lines.collect::<Vec<_>>(), ["second ", "third ", "fourth"]);
378
+ assert_eq!(consumed.get(), 4);
379
+ }
380
+
381
+ #[test]
382
+ fn wrapped_lines_preserve_edge_cases() {
383
+ let text = "first \\nlast";
384
+ assert_eq!(
385
+ wrap_po_chunks(text, [0..6, 6..8, 8..12], 6).collect::<Vec<_>>(),
386
+ ["first ", "\\n", "last"]
387
+ );
388
+ assert!(
389
+ wrap_po_chunks("", std::iter::empty(), 10)
390
+ .collect::<Vec<_>>()
391
+ .is_empty()
392
+ );
393
+ assert_eq!(
394
+ wrap_po_chunks("unwrapped", std::iter::once(0..9), 0).collect::<Vec<_>>(),
395
+ ["unwrapped"]
396
+ );
397
+ }
398
+
399
+ #[test]
400
+ fn wrapped_lines_borrow_from_the_input() {
401
+ let text = "first second";
402
+ let line = wrap_po_chunks(text, [0..6, 6..12], 20).next().unwrap();
403
+
404
+ assert_eq!(line, text);
405
+ assert_eq!(line.as_ptr(), text.as_ptr());
406
+ }
407
+ }
303
408
  }