use arrayvec::ArrayVec;
use icu_segmenter::WordSegmenter;
use malloc_size_of_derive::MallocSizeOf;
use servo_base::text::Utf32CodeUnits;
use style::computed_values::_webkit_text_security::T as WebKitTextSecurity;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::properties::ComputedValues;
use style::values::specified::text::{TextTransform, TextTransformCase};
use crate::flow::inline::construct::InlineFormattingContextBuilder;
const MAX_CASE_MAPPING_LENGTH: usize = 3;
#[derive(Clone)]
pub struct CharacterTransformIteration {
consumed: Utf32CodeUnits,
characters: ArrayVec<char, MAX_CASE_MAPPING_LENGTH>,
}
impl CharacterTransformIteration {
fn case_mapped(iterator: impl ExactSizeIterator<Item = char>) -> Self {
debug_assert!(iterator.len() <= MAX_CASE_MAPPING_LENGTH);
Self {
consumed: Utf32CodeUnits(1),
characters: iterator.collect(),
}
}
fn one_to_one(character: char) -> Self {
Self {
consumed: Utf32CodeUnits(1),
characters: std::iter::once(character).collect(),
}
}
fn collapse(amount_collapsed: usize, character: Option<char>) -> Self {
Self {
consumed: Utf32CodeUnits(amount_collapsed),
characters: character.into_iter().collect(),
}
}
fn is_one_to_one(&self) -> bool {
self.characters.len() == 1 && self.consumed.0 == 1
}
pub fn characters(&self) -> &[char] {
&self.characters
}
}
pub struct WhitespaceCollapse<InputIterator> {
input_iterator: InputIterator,
white_space_collapse: WhiteSpaceCollapse,
trimming_leading_white_space: bool,
following_newline: bool,
character_pending_to_return: Option<char>,
}
impl<InputIterator: Iterator<Item = char>> WhitespaceCollapse<InputIterator> {
pub fn new(
input_iterator: InputIterator,
white_space_collapse: WhiteSpaceCollapse,
should_trim_leading_white_space: bool,
) -> Self {
Self {
input_iterator,
white_space_collapse,
following_newline: false,
trimming_leading_white_space: should_trim_leading_white_space,
character_pending_to_return: None,
}
}
fn iteration_for_collapsed_whitespace(
&self,
collapsed_whitespace: usize,
) -> CharacterTransformIteration {
if !self.following_newline && !self.trimming_leading_white_space {
CharacterTransformIteration::collapse(collapsed_whitespace, Some(' '))
} else {
CharacterTransformIteration::collapse(collapsed_whitespace, None)
}
}
fn iteration_for_collected_white_space(
&self,
collected_whitespace: usize,
) -> Option<CharacterTransformIteration> {
(collected_whitespace != 0)
.then(|| self.iteration_for_collapsed_whitespace(collected_whitespace))
}
}
impl<InputIterator: Iterator<Item = char>> Iterator for WhitespaceCollapse<InputIterator> {
type Item = CharacterTransformIteration;
fn next(&mut self) -> Option<Self::Item> {
if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
{
return match self.input_iterator.next() {
Some('\r') => Some(CharacterTransformIteration::one_to_one(' ')),
next => next.map(CharacterTransformIteration::one_to_one),
};
}
if let Some(character) = self.character_pending_to_return.take() {
self.trimming_leading_white_space = false;
self.following_newline = false;
return Some(CharacterTransformIteration::one_to_one(character));
}
let mut collected_whitespace = 0;
while let Some(character) = self.input_iterator.next() {
if InlineFormattingContextBuilder::is_document_white_space(character) &&
character != '\n'
{
collected_whitespace += 1;
continue;
}
if character == '\n' {
let iteration = if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
CharacterTransformIteration::collapse(collected_whitespace + 1, Some('\n'))
} else {
self.iteration_for_collapsed_whitespace(collected_whitespace + 1)
};
self.following_newline = true;
return Some(iteration);
}
if let Some(iteration) = self.iteration_for_collected_white_space(collected_whitespace)
{
self.character_pending_to_return = Some(character);
return Some(iteration);
}
self.trimming_leading_white_space = false;
self.following_newline = false;
return Some(CharacterTransformIteration::one_to_one(character));
}
self.iteration_for_collected_white_space(collected_whitespace)
}
}
pub(crate) struct TextTransformationIterator<'a>(
Box<dyn Iterator<Item = CharacterTransformIteration> + 'a>,
);
impl<'a> TextTransformationIterator<'a> {
pub(crate) fn new(
text: &'a str,
style: &ComputedValues,
trim_leading_white_space: bool,
on_word_boundary: bool,
) -> Self {
let text_security = style.clone__webkit_text_security();
let chars = text
.chars()
.map(move |character| map_character_for_webkit_text_security(text_security, character));
let white_space_collapse = style.clone_white_space_collapse();
let iterator =
WhitespaceCollapse::new(chars, white_space_collapse, trim_leading_white_space);
let text_transform = style.clone_text_transform();
let iterator = match text_transform.case() {
TextTransformCase::None => {
Box::new(iterator) as Box<dyn Iterator<Item = CharacterTransformIteration>>
},
TextTransformCase::Lowercase => {
Box::new(simple_case_transform_iterator(iterator, |character| {
CharacterTransformIteration::case_mapped(character.to_lowercase())
}))
},
TextTransformCase::Uppercase => {
Box::new(simple_case_transform_iterator(iterator, |character| {
CharacterTransformIteration::case_mapped(character.to_uppercase())
}))
},
TextTransformCase::Capitalize => Box::new(capitalization_iterator(
iterator,
text.len(),
on_word_boundary,
)),
};
if text_transform.intersects(TextTransform::FULL_WIDTH) {
}
if text_transform.intersects(TextTransform::FULL_SIZE_KANA) {
}
Self(iterator)
}
}
impl Iterator for TextTransformationIterator<'_> {
type Item = CharacterTransformIteration;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
fn simple_case_transform_iterator(
input_iterator: impl Iterator<Item = CharacterTransformIteration>,
mapping: impl Fn(char) -> CharacterTransformIteration,
) -> impl Iterator<Item = CharacterTransformIteration> {
input_iterator.map(move |iteration| {
if iteration.is_one_to_one() {
mapping(iteration.characters[0])
} else {
iteration
}
})
}
pub(crate) fn capitalization_iterator(
input_iterator: impl Iterator<Item = CharacterTransformIteration>,
size_hint: usize,
allow_word_at_start: bool,
) -> impl Iterator<Item = CharacterTransformIteration> {
let mut iterations: Vec<_> = input_iterator.collect();
let mut string = String::with_capacity(size_hint);
for iteration in &iterations {
string.extend(iteration.characters());
}
let word_segmenter = WordSegmenter::new_auto();
let mut bounds = word_segmenter.segment_str(&string).peekable();
let mut current_byte_index = 0;
for iteration in iterations.iter_mut() {
let bytes_to_advance: usize = iteration
.characters()
.iter()
.map(|character| character.len_utf8())
.sum();
if bytes_to_advance == 0 {
continue;
}
let at_word_start = bounds.peek() == Some(¤t_byte_index);
if at_word_start {
bounds.next();
}
if iteration.is_one_to_one() &&
at_word_start &&
(current_byte_index != 0 || allow_word_at_start)
{
*iteration =
CharacterTransformIteration::case_mapped(iteration.characters[0].to_uppercase());
}
current_byte_index += bytes_to_advance;
}
iterations.into_iter()
}
fn map_character_for_webkit_text_security(mode: WebKitTextSecurity, character: char) -> char {
if let WebKitTextSecurity::None = mode {
return character;
}
match character {
'\u{200B}' => '\u{200B}',
'\n' => '\n',
_ => match mode {
WebKitTextSecurity::None => character, WebKitTextSecurity::Circle => '○',
WebKitTextSecurity::Disc => '●',
WebKitTextSecurity::Square => '■',
},
}
}
#[derive(MallocSizeOf, Clone, Copy)]
struct OffsetMapKnownPosition {
original_offset: Utf32CodeUnits,
final_offset: Utf32CodeUnits,
}
#[derive(Default, MallocSizeOf)]
pub struct OffsetMap {
known_positions: Vec<OffsetMapKnownPosition>,
last_range_maps_one_to_one: bool,
}
impl std::fmt::Debug for OffsetMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OffsetMap")
.field("total_original_size", &self.total_original_size())
.field("total_final_size", &self.total_final_size())
.finish()
}
}
static IMPLICIT_KNOWN_POSITION_AT_START: OffsetMapKnownPosition = OffsetMapKnownPosition {
original_offset: Utf32CodeUnits(0),
final_offset: Utf32CodeUnits(0),
};
impl OffsetMap {
fn last_known_position(&self) -> &OffsetMapKnownPosition {
self.known_positions
.last()
.unwrap_or(&IMPLICIT_KNOWN_POSITION_AT_START)
}
pub fn total_original_size(&self) -> Utf32CodeUnits {
self.last_known_position().original_offset
}
pub fn total_final_size(&self) -> Utf32CodeUnits {
self.last_known_position().final_offset
}
pub fn push_range(
&mut self,
additional_original_length: Utf32CodeUnits,
additional_final_length: Utf32CodeUnits,
) {
let this_range_maps_one_to_one = additional_original_length == additional_final_length;
if this_range_maps_one_to_one &&
self.last_range_maps_one_to_one &&
let Some(last) = self.known_positions.last_mut()
{
last.original_offset += additional_original_length;
last.final_offset += additional_final_length;
} else {
let last = self.last_known_position();
self.known_positions.push(OffsetMapKnownPosition {
original_offset: last.original_offset + additional_original_length,
final_offset: last.final_offset + additional_final_length,
});
}
self.last_range_maps_one_to_one = this_range_maps_one_to_one;
}
pub(crate) fn push_iteration(&mut self, iteration: &CharacterTransformIteration) {
self.push_range(
iteration.consumed,
Utf32CodeUnits(iteration.characters.len()),
);
}
pub fn map(&self, target_original_offset: Utf32CodeUnits) -> Utf32CodeUnits {
self.map_common(
target_original_offset,
|position| position.original_offset,
|position| position.final_offset,
)
}
pub fn reverse_map(&self, target_final_offset: Utf32CodeUnits) -> Utf32CodeUnits {
self.map_common(
target_final_offset,
|position| position.final_offset,
|position| position.original_offset,
)
}
fn map_common(
&self,
target_offset: Utf32CodeUnits,
get_input_offset: impl Copy + Fn(&OffsetMapKnownPosition) -> Utf32CodeUnits,
get_output_offset: impl Fn(&OffsetMapKnownPosition) -> Utf32CodeUnits,
) -> Utf32CodeUnits {
if target_offset.0 == 0 {
return Utf32CodeUnits(0);
}
match self
.known_positions
.binary_search_by_key(&target_offset, get_input_offset)
{
Ok(index) => {
get_output_offset(&self.known_positions[index])
},
Err(index) => {
if let Some(position_after) = self.known_positions.get(index) {
let position_before = if index > 0 {
&self.known_positions[index - 1]
} else {
&IMPLICIT_KNOWN_POSITION_AT_START
};
debug_assert!(target_offset > get_input_offset(position_before));
debug_assert!(target_offset < get_input_offset(position_after));
let offset_within_range = target_offset - get_input_offset(position_before);
let candidate = get_output_offset(position_before) + offset_within_range;
let upper_bound = get_output_offset(position_after);
upper_bound.min(candidate)
} else {
get_output_offset(self.last_known_position())
}
},
}
}
}
#[test]
fn test_offsetmap_basic_expansion() {
let original_string = "aßΰb";
let final_string = "ASS\u{3a5}\u{308}\u{301}B";
assert_eq!(original_string.to_uppercase(), final_string);
let mut offset_map = OffsetMap::default();
offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
'a'.to_uppercase(),
));
offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
'ß'.to_uppercase(),
));
offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
'ΰ'.to_uppercase(),
));
offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
'b'.to_uppercase(),
));
assert_eq!(offset_map.map(Utf32CodeUnits(0)).0, 0);
assert_eq!(offset_map.map(Utf32CodeUnits(1)).0, 1);
assert_eq!(offset_map.map(Utf32CodeUnits(2)).0, 3);
assert_eq!(offset_map.map(Utf32CodeUnits(3)).0, 6);
assert_eq!(offset_map.map(Utf32CodeUnits(4)).0, 7);
assert_eq!(offset_map.map(Utf32CodeUnits(5)).0, 7);
assert_eq!(offset_map.map(Utf32CodeUnits(100)).0, 7);
let map_substring = |offset: usize, length: usize| {
let start = offset_map
.map(Utf32CodeUnits(offset))
.to_utf8_code_units_in(final_string);
let end = offset_map
.map(Utf32CodeUnits(offset + length))
.to_utf8_code_units_in(final_string);
&final_string[start.0..end.0]
};
assert_eq!(map_substring(0, 1), "A");
assert_eq!(map_substring(0, 2), "ASS");
assert_eq!(map_substring(0, 3), "ASS\u{3a5}\u{308}\u{301}");
assert_eq!(map_substring(0, 4), "ASS\u{3a5}\u{308}\u{301}B");
assert_eq!(map_substring(1, 1), "SS");
}
#[test]
fn test_offsetmap_basic_collapse() {
let _original_string = " aaa b \nc";
let final_string = "aaa b\nc";
let mut offset_map = OffsetMap::default();
offset_map.push_iteration(&CharacterTransformIteration::collapse(2, None));
offset_map.push_iteration(&CharacterTransformIteration::one_to_one('a'));
offset_map.push_iteration(&CharacterTransformIteration::one_to_one('a'));
offset_map.push_iteration(&CharacterTransformIteration::one_to_one('a'));
assert_eq!(
offset_map.known_positions.len(),
2,
"Consecutive one-to-one mappings are merged"
);
offset_map.push_iteration(&CharacterTransformIteration::collapse(2, Some(' ')));
offset_map.push_iteration(&CharacterTransformIteration::one_to_one('b'));
offset_map.push_iteration(&CharacterTransformIteration::collapse(2, Some('\n')));
offset_map.push_iteration(&CharacterTransformIteration::one_to_one('c'));
assert_eq!(offset_map.map(Utf32CodeUnits(0)).0, 0);
assert_eq!(offset_map.map(Utf32CodeUnits(1)).0, 0);
assert_eq!(offset_map.map(Utf32CodeUnits(2)).0, 0);
assert_eq!(offset_map.map(Utf32CodeUnits(3)).0, 1);
assert_eq!(offset_map.map(Utf32CodeUnits(4)).0, 2);
assert_eq!(offset_map.map(Utf32CodeUnits(5)).0, 3);
assert_eq!(offset_map.map(Utf32CodeUnits(6)).0, 4);
assert_eq!(offset_map.map(Utf32CodeUnits(7)).0, 4);
assert_eq!(offset_map.map(Utf32CodeUnits(8)).0, 5);
assert_eq!(offset_map.map(Utf32CodeUnits(9)).0, 6);
assert_eq!(offset_map.map(Utf32CodeUnits(10)).0, 6);
assert_eq!(offset_map.map(Utf32CodeUnits(11)).0, 7);
assert_eq!(offset_map.map(Utf32CodeUnits(12)).0, 7);
assert_eq!(offset_map.map(Utf32CodeUnits(100)).0, 7);
let map_substring = |offset: usize, length: usize| {
let start = offset_map.map(Utf32CodeUnits(offset)).0;
let end = offset_map.map(Utf32CodeUnits(offset + length)).0;
&final_string[start..end]
};
assert_eq!(map_substring(0, 1), "");
assert_eq!(map_substring(0, 3), "a");
assert_eq!(map_substring(0, 5), "aaa");
assert_eq!(map_substring(0, 6), "aaa ");
assert_eq!(map_substring(0, 7), "aaa ");
assert_eq!(map_substring(0, 8), "aaa b");
assert_eq!(map_substring(0, 11), "aaa b\nc");
}