// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// An iterator over paragraphs in input text.
///
/// Behavior follows `unicode-bidi` paragraph ranges:
/// paragraph separator characters (`BidiClass::B`) remain in the paragraph range
/// but are trimmed from the returned paragraph text.
pub struct BidiParagraphs {
text : String
ranges : Array[(Int, Int)]
cursor : Int
pos : Int
}
///|
fn bidi_paragraphs_is_ascii_fast_path(text : String) -> Bool {
for ch in text {
if ch.to_int() > 0x7F {
return false
}
if ch.is_control() && ch != '\n' && ch != '\r' && ch != '\t' {
return false
}
}
true
}
///|
fn bidi_paragraphs_ascii_ranges(text : String) -> Array[(Int, Int)] {
let ranges : Array[(Int, Int)] = []
let len = text.length()
let mut start = 0
for p in text.iter2() {
let i = p.0
let ch = p.1
if ch == '\n' {
ranges.push((start, i))
start = i + 1
}
}
if start < len {
ranges.push((start, len))
}
ranges
}
///|
fn bidi_paragraphs_ranges(text : String) -> Array[(Int, Int)] {
if bidi_paragraphs_is_ascii_fast_path(text) {
return bidi_paragraphs_ascii_ranges(text)
}
let ranges : Array[(Int, Int)] = []
let info = BidiInfo::new(text)
for para in info.paragraphs() {
ranges.push((para.range_start(), para.range_end()))
}
ranges
}
///|
fn bidi_paragraphs_trim_trailing_b_separator(para : String) -> String {
let mut last : (Int, Char)? = None
for p in para.iter2() {
last = Some((p.0, p.1))
}
match last {
None => para
Some((idx, ch)) => {
let class = @moon_swash.CharInfo::from_char(ch).properties().bidi_class()
if class is B {
slice_string(para, 0, idx)
} else {
para
}
}
}
}
///|
pub fn BidiParagraphs::new(text : String) -> BidiParagraphs {
let ranges = bidi_paragraphs_ranges(text)
let pos = if ranges.length() == 0 { text.length() } else { ranges[0].0 }
BidiParagraphs::{ text, ranges, cursor: 0, pos }
}
///|
/// Returns (next, paragraph_text) if any.
pub fn BidiParagraphs::next(self : BidiParagraphs) -> (BidiParagraphs, String)? {
if self.cursor >= self.ranges.length() {
return None
}
let range = self.ranges[self.cursor]
let para_raw = slice_string(self.text, range.0, range.1)
let para = bidi_paragraphs_trim_trailing_b_separator(para_raw)
let next_cursor = self.cursor + 1
let next_pos = if next_cursor >= self.ranges.length() {
self.text.length()
} else {
self.ranges[next_cursor].0
}
Some((BidiParagraphs::{ ..self, cursor: next_cursor, pos: next_pos }, para))
}
///|
pub fn BidiParagraphs::iter(self : BidiParagraphs) -> Iter[String] {
let mut it = self
Iter::new(fn() {
match it.next() {
None => None
Some((next, para)) => {
it = next
Some(para)
}
}
})
}