1
// ! development file for text2
2
use cssparser::Parser;
3
use markup5ever::{expanded_name, local_name, ns};
4
use pango::IsAttribute;
5
use rctree::NodeEdge;
6

            
7
use crate::element::{set_attribute, Element, ElementData, ElementTrait};
8
use crate::error::ParseError;
9
use crate::layout::FontProperties;
10
use crate::length::{Horizontal, Length, NormalizeParams, Vertical};
11
use crate::node::{Node, NodeData};
12
use crate::parsers::{CommaSeparatedList, Parse, ParseValue};
13
use crate::properties::WhiteSpace;
14
use crate::session::Session;
15
use crate::text::BidiControl;
16
use crate::xml;
17
use crate::{parse_identifiers, rsvg_log};
18

            
19
/// Type for the `x/y/dx/dy` attributes of the `<text>` and `<tspan>` elements
20
///
21
/// https://svgwg.org/svg2-draft/text.html#TSpanAttributes
22
///
23
/// Explanation of this type:
24
///
25
/// * Option - the attribute can be specified or not, so make it optional
26
///
27
///  CommaSeparatedList<Length<Horizontal>> - This type knows how to parse a list of values
28
///  that are separated by commas and/or spaces; the values are eventually available as a Vec.
29
///
30
/// * 1 is the minimum number of elements in the list, so one can have x="42" for example.
31
///
32
/// * 4096 is an arbitrary limit on the number of length values for each array, as a mitigation
33
///   against malicious files which may want to have millions of elements to exhaust memory.
34
type OptionalLengthList<N> = Option<CommaSeparatedList<Length<N>, 1, 4096>>;
35

            
36
/// Type for the `rotate` attribute of the `<text>` and `<tspan>` elements
37
///
38
/// https://svgwg.org/svg2-draft/text.html#TSpanAttributes
39
///
40
/// See [`OptionalLengthList`] for a description of the structure of the type.
41
type OptionalRotateList = Option<CommaSeparatedList<f64, 1, 4096>>;
42

            
43
/// Enum for the `lengthAdjust` attribute
44
///
45
/// https://svgwg.org/svg2-draft/text.html#LengthAdjustProperty
46
#[derive(Debug, Default, Copy, Clone, PartialEq)]
47
enum LengthAdjust {
48
    #[default]
49
    Spacing,
50
    SpacingAndGlyphs,
51
}
52

            
53
impl Parse for LengthAdjust {
54
    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
55
        Ok(parse_identifiers!(
56
            parser,
57
            "spacing" => LengthAdjust::Spacing,
58
            "spacingAndGlyphs" => LengthAdjust::SpacingAndGlyphs,
59
        )?)
60
    }
61
}
62

            
63
#[allow(dead_code)]
64
#[derive(Default)]
65
pub struct Text2 {
66
    x: OptionalLengthList<Horizontal>,
67
    y: OptionalLengthList<Vertical>,
68
    dx: OptionalLengthList<Horizontal>,
69
    dy: OptionalLengthList<Vertical>,
70
    rotate: OptionalRotateList,
71
    text_length: Length<Horizontal>,
72
    length_adjust: LengthAdjust, // Implemented
73
}
74

            
75
// HOMEWORK
76
//
77
// see text.rs and how it implements set_attributes() for the Text element.
78
// The attributes are described here:
79
//
80
// https://svgwg.org/svg2-draft/text.html#TSpanAttributes
81
//
82
// Attributes to parse:
83
//   "x"
84
//   "y"
85
//   "dx"
86
//   "dy"
87
//   "rotate"
88
//   "textLength"
89
//   "lengthAdjust"
90
impl ElementTrait for Text2 {
91
4
    fn set_attributes(&mut self, attrs: &xml::Attributes, session: &Session) {
92
6
        for (attr, value) in attrs.iter() {
93
6
            match attr.expanded() {
94
                expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session),
95
                expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session),
96
                expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session),
97
                expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session),
98
                expanded_name!("", "rotate") => {
99
                    set_attribute(&mut self.rotate, attr.parse(value), session)
100
                }
101
                expanded_name!("", "textLength") => {
102
                    set_attribute(&mut self.text_length, attr.parse(value), session)
103
                }
104
                expanded_name!("", "lengthAdjust") => {
105
                    set_attribute(&mut self.length_adjust, attr.parse(value), session)
106
                }
107
6
                _ => (),
108
            }
109
        }
110
4
    }
111
}
112

            
113
#[derive(Default)]
114
#[allow(dead_code)]
115
struct Character {
116
    // https://www.w3.org/TR/SVG2/text.html#TextLayoutAlgorithm
117
    // Section "11.5.1 Setup"
118
    //
119
    // global_index: u32,
120
    // x: f64,
121
    // y: f64,
122
    // angle: Angle,
123
    // hidden: bool,
124
    addressable: bool,
125
    character: char,
126
    // must_include: bool,
127
    // middle: bool,
128
    // anchored_chunk: bool,
129
}
130

            
131
//              <tspan>   hello</tspan>
132
// addressable:        tffttttt
133

            
134
//              <tspan direction="ltr">A <tspan direction="rtl"> B </tspan> C</tspan>
135
//              A xx B xx C          "xx" are bidi control characters
136
// addressable: ttfffttffft
137

            
138
// HOMEWORK
139
#[allow(unused)]
140
12
fn collapse_white_space(input: &str, white_space: WhiteSpace) -> Vec<Character> {
141
12
    match white_space {
142
8
        WhiteSpace::Normal | WhiteSpace::NoWrap => compute_normal_nowrap(input),
143
4
        WhiteSpace::Pre | WhiteSpace::PreWrap => compute_pre_prewrap(input),
144
        _ => unimplemented!(),
145
    }
146
12
}
147

            
148
207
fn is_bidi_control(ch: char) -> bool {
149
    use crate::text::directional_formatting_characters::*;
150
207
    matches!(ch, LRE | RLE | LRO | RLO | PDF | LRI | RLI | FSI | PDI)
151
207
}
152

            
153
// move to inline constant if conditions needs to change
154
121
fn is_space(ch: char) -> bool {
155
121
    matches!(ch, ' ' | '\t' | '\n')
156
121
}
157

            
158
// Summary of white-space rules from https://www.w3.org/TR/css-text-3/#white-space-property
159
//
160
//              New Lines   Spaces and Tabs   Text Wrapping   End-of-line   End-of-line
161
//                                                            spaces        other space separators
162
// -----------------------------------------------------------------------------------------------
163
// normal       Collapse    Collapse          Wrap            Remove        Hang
164
// pre          Preserve    Preserve          No wrap         Preserve      No wrap
165
// nowrap       Collapse    Collapse          No wrap         Remove        Hang
166
// pre-wrap     Preserve    Preserve          Wrap            Hang          Hang
167
// break-spaces Preserve    Preserve          Wrap            Wrap          Wrap
168
// pre-line     Preserve    Collapse          Wrap            Remove        Hang
169

            
170
8
fn compute_normal_nowrap(input: &str) -> Vec<Character> {
171
8
    let mut result: Vec<Character> = Vec::with_capacity(input.len());
172
8

            
173
8
    let mut prev_was_space: bool = false;
174

            
175
127
    for ch in input.chars() {
176
127
        if is_bidi_control(ch) {
177
6
            result.push(Character {
178
6
                addressable: false,
179
6
                character: ch,
180
6
            });
181
6
            continue;
182
121
        }
183
121

            
184
121
        if is_space(ch) {
185
42
            if prev_was_space {
186
24
                result.push(Character {
187
24
                    addressable: false,
188
24
                    character: ch,
189
24
                });
190
24
            } else {
191
18
                result.push(Character {
192
18
                    addressable: true,
193
18
                    character: ch,
194
18
                });
195
18
                prev_was_space = true;
196
18
            }
197
79
        } else {
198
79
            result.push(Character {
199
79
                addressable: true,
200
79
                character: ch,
201
79
            });
202
79

            
203
79
            prev_was_space = false;
204
79
        }
205
    }
206

            
207
8
    result
208
8
}
209

            
210
4
fn compute_pre_prewrap(input: &str) -> Vec<Character> {
211
4
    let mut result: Vec<Character> = Vec::with_capacity(input.len());
212

            
213
80
    for ch in input.chars() {
214
80
        if is_bidi_control(ch) {
215
4
            result.push(Character {
216
4
                addressable: false,
217
4
                character: ch,
218
4
            });
219
76
        } else {
220
76
            result.push(Character {
221
76
                addressable: true,
222
76
                character: ch,
223
76
            });
224
76
        }
225
    }
226

            
227
4
    result
228
4
}
229

            
230
38
fn get_bidi_control(element: &Element) -> BidiControl {
231
38
    // Extract bidi control logic to separate function to avoid duplication
232
38
    let computed_values = element.get_computed_values();
233
38

            
234
38
    let unicode_bidi = computed_values.unicode_bidi();
235
38
    let direction = computed_values.direction();
236
38

            
237
38
    BidiControl::from_unicode_bidi_and_direction(unicode_bidi, direction)
238
38
}
239

            
240
// FIXME: Remove the following line when this code actually starts getting used outside of tests.
241
#[allow(unused)]
242
4
fn collect_text_from_node(node: &Node) -> String {
243
4
    let mut result = String::new();
244

            
245
68
    for edge in node.traverse() {
246
68
        match edge {
247
34
            NodeEdge::Start(child_node) => match *child_node.borrow() {
248
21
                NodeData::Text(ref text) => {
249
21
                    result.push_str(&text.get_string());
250
21
                }
251

            
252
13
                NodeData::Element(ref element) => match element.element_data {
253
                    ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
254
13
                        let bidi_control = get_bidi_control(element);
255

            
256
18
                        for &ch in bidi_control.start {
257
5
                            result.push(ch);
258
5
                        }
259
                    }
260
                    _ => {}
261
                },
262
            },
263

            
264
34
            NodeEdge::End(child_node) => {
265
34
                if let NodeData::Element(ref element) = *child_node.borrow() {
266
13
                    match element.element_data {
267
                        ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
268
13
                            let bidi_control = get_bidi_control(element);
269

            
270
18
                            for &ch in bidi_control.end {
271
5
                                result.push(ch);
272
5
                            }
273
                        }
274

            
275
                        _ => {}
276
                    }
277
21
                }
278
            }
279
        }
280
    }
281

            
282
4
    result
283
4
}
284

            
285
/// A range onto which font properties are applied.
286
///
287
/// The indices are relative to a certain string, which is then passed on to Pango.
288
/// The font properties will get translated to a pango::AttrList.
289
#[allow(unused)]
290
struct Attributes {
291
    /// Start byte offset within the `text` of [`FormattedText`].
292
    start_index: usize,
293

            
294
    /// End byte offset within the `text` of [`FormattedText`].
295
    end_index: usize,
296

            
297
    /// Font style and properties for this range of text.
298
    props: FontProperties,
299
}
300

            
301
/// Text and ranged attributes just prior to text layout.
302
///
303
/// This is what gets shipped to Pango for layout.
304
#[allow(unused)]
305
struct FormattedText {
306
    text: String,
307
    attributes: Vec<Attributes>,
308
}
309

            
310
// HOMEWORK:
311
//
312
// Traverse the text_node in the same way as when collecting the text.  See the comment below
313
// on what needs to happen while traversing.  We are building a FormattedText that has only
314
// the addressable characters AND the BidiControl chars, and the corresponding Attributtes/
315
// for text styling.
316
//
317
//
318
#[allow(unused)]
319
2
fn build_formatted_text(
320
2
    characters: &[Character],
321
2
    text_node: &Node,
322
2
    params: &NormalizeParams,
323
2
) -> FormattedText {
324
2
    let mut indices_stack = Vec::new();
325
2
    let mut byte_index = 0;
326
2
    let mut num_visited_characters = 0;
327
2
    let mut text = String::new();
328
2
    let mut attributes = Vec::new();
329

            
330
30
    for edge in text_node.traverse() {
331
30
        match edge {
332
15
            NodeEdge::Start(child_node) => match *child_node.borrow() {
333
6
                NodeData::Element(ref element) => match element.element_data {
334
                    ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
335
6
                        indices_stack.push(byte_index);
336
6
                        let bidi_control = get_bidi_control(element);
337
7
                        for &ch in bidi_control.start {
338
1
                            byte_index += ch.len_utf8();
339
1
                            num_visited_characters += 1;
340
1
                            text.push(ch);
341
1
                        }
342
                    }
343
                    _ => {}
344
                },
345
9
                NodeData::Text(_) => {}
346
            },
347

            
348
15
            NodeEdge::End(child_node) => match *child_node.borrow() {
349
6
                NodeData::Element(ref element) => match element.element_data {
350
                    ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
351
6
                        let bidi_control = get_bidi_control(element);
352
7
                        for &ch in bidi_control.end {
353
1
                            byte_index += ch.len_utf8();
354
1
                            num_visited_characters += 1;
355
1
                            text.push(ch);
356
1
                        }
357

            
358
6
                        let start_index = indices_stack
359
6
                            .pop()
360
6
                            .expect("start_index must be pushed already");
361
6
                        let values = element.get_computed_values();
362
6
                        let font_props = FontProperties::new(values, params);
363
6

            
364
6
                        if byte_index > start_index {
365
6
                            attributes.push(Attributes {
366
6
                                start_index,
367
6
                                end_index: byte_index,
368
6
                                props: font_props,
369
6
                            });
370
6
                        }
371
                    }
372
                    _ => {}
373
                },
374

            
375
9
                NodeData::Text(ref text_ref) => {
376
9
                    let text_len = text_ref.get_string().chars().count();
377
53
                    for character in characters
378
9
                        .iter()
379
9
                        .skip(num_visited_characters)
380
9
                        .take(text_len)
381
                    {
382
53
                        if character.addressable {
383
41
                            text.push(character.character);
384
41
                            byte_index += character.character.len_utf8();
385
41
                        }
386
53
                        num_visited_characters += 1;
387
                    }
388
                }
389
            },
390
        }
391
    }
392

            
393
2
    FormattedText { text, attributes }
394
2
}
395

            
396
/// Builds a Pango attribute list from a FormattedText structure.
397
///
398
/// This function converts the text styling information in FormattedText
399
/// into Pango attributes that can be applied to a Pango layout.
400
#[allow(unused)]
401
fn build_pango_attr_list(session: &Session, formatted_text: &FormattedText) -> pango::AttrList {
402
    let attr_list = pango::AttrList::new();
403

            
404
    if formatted_text.text.is_empty() {
405
        return attr_list;
406
    }
407

            
408
    for attribute in &formatted_text.attributes {
409
        // Skip invalid or empty ranges
410
        if attribute.start_index >= attribute.end_index {
411
            continue;
412
        }
413

            
414
        // Validate indices
415
        let start_index = attribute.start_index.min(formatted_text.text.len());
416
        let end_index = attribute.end_index.min(formatted_text.text.len());
417

            
418
        assert!(start_index <= end_index);
419

            
420
        let start_index =
421
            u32::try_from(start_index).expect("Pango attribute index must fit in u32");
422
        let end_index = u32::try_from(end_index).expect("Pango attribute index must fit in u32");
423

            
424
        // Create font description
425
        let mut font_desc = pango::FontDescription::new();
426
        font_desc.set_family(&attribute.props.font_family.0);
427

            
428
        // Handle font size scaling with bounds checking
429
        if let Some(font_size) = PangoUnits::from_pixels(attribute.props.font_size) {
430
            font_desc.set_size(font_size.0);
431
        } else {
432
            rsvg_log!(
433
                session,
434
                "font-size {} is out of bounds; skipping attribute range",
435
                attribute.props.font_size
436
            );
437
        }
438

            
439
        font_desc.set_weight(pango::Weight::from(attribute.props.font_weight));
440
        font_desc.set_style(pango::Style::from(attribute.props.font_style));
441
        font_desc.set_stretch(pango::Stretch::from(attribute.props.font_stretch));
442
        font_desc.set_variant(pango::Variant::from(attribute.props.font_variant));
443

            
444
        let mut font_attr = pango::AttrFontDesc::new(&font_desc).upcast();
445
        font_attr.set_start_index(start_index);
446
        font_attr.set_end_index(end_index);
447
        attr_list.insert(font_attr);
448

            
449
        // Add letter spacing with bounds checking
450
        if attribute.props.letter_spacing != 0.0 {
451
            if let Some(spacing) = PangoUnits::from_pixels(attribute.props.letter_spacing) {
452
                let mut spacing_attr = pango::AttrInt::new_letter_spacing(spacing.0).upcast();
453
                spacing_attr.set_start_index(start_index);
454
                spacing_attr.set_end_index(end_index);
455
                attr_list.insert(spacing_attr);
456
            } else {
457
                rsvg_log!(
458
                    session,
459
                    "letter-spacing {} is out of bounds; skipping attribute range",
460
                    attribute.props.letter_spacing
461
                );
462
            }
463
        }
464

            
465
        // Add text decoration attributes
466
        if attribute.props.text_decoration.overline {
467
            let mut overline_attr = pango::AttrInt::new_overline(pango::Overline::Single).upcast();
468
            overline_attr.set_start_index(start_index);
469
            overline_attr.set_end_index(end_index);
470
            attr_list.insert(overline_attr);
471
        }
472

            
473
        if attribute.props.text_decoration.underline {
474
            let mut underline_attr =
475
                pango::AttrInt::new_underline(pango::Underline::Single).upcast();
476
            underline_attr.set_start_index(start_index);
477
            underline_attr.set_end_index(end_index);
478
            attr_list.insert(underline_attr);
479
        }
480

            
481
        if attribute.props.text_decoration.strike {
482
            let mut strike_attr = pango::AttrInt::new_strikethrough(true).upcast();
483
            strike_attr.set_start_index(start_index);
484
            strike_attr.set_end_index(end_index);
485
            attr_list.insert(strike_attr);
486
        }
487
    }
488

            
489
    attr_list
490
}
491

            
492
struct PangoUnits(i32);
493

            
494
impl PangoUnits {
495
    fn from_pixels(v: f64) -> Option<Self> {
496
        // We want (v * f64::from(pango::SCALE) + 0.5) as i32
497
        // But check for overflow.
498
        cast::i32(v * f64::from(pango::SCALE) + 0.5)
499
            .ok()
500
            .map(PangoUnits)
501
    }
502
}
503

            
504
#[cfg(test)]
505
mod tests {
506
    use crate::document::Document;
507
    use crate::dpi::Dpi;
508
    use crate::element::ElementData;
509
    use crate::node::NodeBorrow;
510
    use crate::properties::{FontStyle, FontWeight};
511

            
512
    use super::*;
513

            
514
    #[test]
515
1
    fn collects_text_in_a_single_string() {
516
1
        let doc_str = br##"<?xml version="1.0" encoding="UTF-8"?>
517
1
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
518
1

            
519
1
  <text2 id="sample">
520
1
    Hello
521
1
    <tspan font-style="italic">
522
1
      <tspan font-weight="bold">bold</tspan>
523
1
      world!
524
1
    </tspan>
525
1
    How are you.
526
1
  </text2>
527
1
</svg>
528
1
"##;
529
1

            
530
1
        let document = Document::load_from_bytes(doc_str);
531
1

            
532
1
        let text2_node = document.lookup_internal_node("sample").unwrap();
533
1
        assert!(matches!(
534
1
            *text2_node.borrow_element_data(),
535
            ElementData::Text2(_)
536
        ));
537

            
538
1
        let text_string = collect_text_from_node(&text2_node);
539
1
        assert_eq!(
540
1
            text_string,
541
1
            "\n    \
542
1
             Hello\n    \
543
1
             \n      \
544
1
             bold\n      \
545
1
             world!\n    \
546
1
             \n    \
547
1
             How are you.\
548
1
             \n  "
549
1
        );
550
1
    }
551

            
552
    #[test]
553
1
    fn adds_bidi_control_characters() {
554
1
        let doc_str = br##"<?xml version="1.0" encoding="UTF-8"?>
555
1
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
556
1

            
557
1
  <text2 id="sample">
558
1
    Hello
559
1
    <tspan direction="rtl" unicode-bidi="embed">
560
1
      <tspan direction="ltr" unicode-bidi="isolate-override">bold</tspan>
561
1
      world!
562
1
    </tspan>
563
1
    How are <tspan direction="rtl" unicode-bidi="isolate">you</tspan>.
564
1
  </text2>
565
1
</svg>
566
1
"##;
567
1

            
568
1
        let document = Document::load_from_bytes(doc_str);
569
1

            
570
1
        let text2_node = document.lookup_internal_node("sample").unwrap();
571
1
        assert!(matches!(
572
1
            *text2_node.borrow_element_data(),
573
            ElementData::Text2(_)
574
        ));
575

            
576
1
        let text_string = collect_text_from_node(&text2_node);
577
1
        assert_eq!(
578
1
            text_string,
579
1
            "\n    \
580
1
             Hello\n    \
581
1
             \u{202b}\n      \
582
1
             \u{2068}\u{202d}bold\u{202c}\u{2069}\n      \
583
1
             world!\n    \
584
1
             \u{202c}\n    \
585
1
             How are \u{2067}you\u{2069}.\
586
1
             \n  "
587
1
        );
588
1
    }
589

            
590
    // Takes a string made of 't' and 'f' characters, and compares it
591
    // to the `addressable` field of the Characters slice.
592
10
    fn check_true_false_template(template: &str, characters: &[Character]) {
593
10
        assert_eq!(characters.len(), template.len());
594

            
595
        // HOMEWORK
596
        // it's a loop with assert_eq!(characters[i].addressable, ...);
597
152
        for (i, ch) in template.chars().enumerate() {
598
152
            assert_eq!(characters[i].addressable, ch == 't');
599
        }
600
10
    }
601

            
602
5
    fn check_modes_with_identical_processing(
603
5
        string: &str,
604
5
        template: &str,
605
5
        mode1: WhiteSpace,
606
5
        mode2: WhiteSpace,
607
5
    ) {
608
5
        let result1 = collapse_white_space(string, mode1);
609
5
        check_true_false_template(template, &result1);
610
5

            
611
5
        let result2 = collapse_white_space(string, mode2);
612
5
        check_true_false_template(template, &result2);
613
5
    }
614

            
615
    // white-space="normal" and "nowrap"; these are processed in the same way
616

            
617
    #[rustfmt::skip]
618
    #[test]
619
1
    fn handles_white_space_normal_trivial_case() {
620
1
        check_modes_with_identical_processing(
621
1
            "hello  world",
622
1
            "ttttttfttttt",
623
1
            WhiteSpace::Normal,
624
1
            WhiteSpace::NoWrap
625
1
        );
626
1
    }
627

            
628
    #[rustfmt::skip]
629
    #[test]
630
1
    fn handles_white_space_normal_start_of_the_line() {
631
1
        check_modes_with_identical_processing(
632
1
            "   hello  world",
633
1
            "tffttttttfttttt",
634
1
            WhiteSpace::Normal,
635
1
            WhiteSpace::NoWrap
636
1
        );
637
1
    }
638

            
639
    #[rustfmt::skip]
640
    #[test]
641
1
    fn handles_white_space_normal_ignores_bidi_control() {
642
1
        check_modes_with_identical_processing(
643
1
            "A \u{202b} B \u{202c} C",
644
1
            "ttffttfft",
645
1
            WhiteSpace::Normal,
646
1
            WhiteSpace::NoWrap
647
1
        );
648
1
    }
649

            
650
    // FIXME: here, we need to collapse newlines.  See section https://www.w3.org/TR/css-text-3/#line-break-transform
651
    //
652
    // Also, we need to test that consecutive newlines get replaced by a single space, FOR NOW,
653
    // at least for languages where inter-word spaces actually exist.  For ideographic languages,
654
    // consecutive newlines need to be removed.
655
    /*
656
    #[rustfmt::skip]
657
    #[test]
658
    fn handles_white_space_normal_collapses_newlines() {
659
        check_modes_with_identical_processing(
660
            "A \n  B \u{202c} C\n\n",
661
            "ttfffttffttf",
662
            WhiteSpace::Normal,
663
            WhiteSpace::NoWrap
664
        );
665
    }
666
    */
667

            
668
    // white-space="pre" and "pre-wrap"; these are processed in the same way
669

            
670
    #[rustfmt::skip]
671
    #[test]
672
1
    fn handles_white_space_pre_trivial_case() {
673
1
        check_modes_with_identical_processing(
674
1
            "   hello  \n  \n  \n\n\nworld",
675
1
            "tttttttttttttttttttttttt",
676
1
            WhiteSpace::Pre,
677
1
            WhiteSpace::PreWrap
678
1
        );
679
1
    }
680

            
681
    #[rustfmt::skip]
682
    #[test]
683
1
    fn handles_white_space_pre_ignores_bidi_control() {
684
1
        check_modes_with_identical_processing(
685
1
            "A  \u{202b} \n\n\n B \u{202c} C  ",
686
1
            "tttftttttttftttt",
687
1
            WhiteSpace::Pre,
688
1
            WhiteSpace::PreWrap
689
1
        );
690
1
    }
691

            
692
    // This is just to have a way to construct a `NormalizeParams` for tests; we don't
693
    // actually care what it contains.
694
2
    fn dummy_normalize_params() -> NormalizeParams {
695
2
        NormalizeParams::from_dpi(Dpi::new(96.0, 96.0))
696
2
    }
697

            
698
    #[test]
699
1
    fn builds_non_bidi_formatted_text() {
700
1
        let doc_str = r##"<?xml version="1.0" encoding="UTF-8"?>
701
1
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
702
1

            
703
1
  <text2 id="sample" font-family="Foobar">
704
1
    Hello <tspan font-weight="bold">böld</tspan> world <tspan font-style="italic">in italics</tspan>!
705
1
  </text2>
706
1
</svg>
707
1
"##;
708
1

            
709
1
        let document = Document::load_from_bytes(doc_str.as_bytes());
710
1

            
711
1
        let text2_node = document.lookup_internal_node("sample").unwrap();
712
1
        assert!(matches!(
713
1
            *text2_node.borrow_element_data(),
714
            ElementData::Text2(_)
715
        ));
716

            
717
1
        let collected_text = collect_text_from_node(&text2_node);
718
1
        let collapsed_characters = collapse_white_space(&collected_text, WhiteSpace::Normal);
719
1

            
720
1
        let formatted = build_formatted_text(
721
1
            &collapsed_characters,
722
1
            &text2_node,
723
1
            &dummy_normalize_params(),
724
1
        );
725
1

            
726
1
        assert_eq!(&formatted.text, "\nHello böld world in italics!\n");
727

            
728
        // "böld" (note that the ö takes two bytes in UTF-8)
729
1
        assert_eq!(formatted.attributes[0].start_index, 7);
730
1
        assert_eq!(formatted.attributes[0].end_index, 12);
731
1
        assert_eq!(formatted.attributes[0].props.font_weight, FontWeight::Bold);
732

            
733
        // "in italics"
734
1
        assert_eq!(formatted.attributes[1].start_index, 19);
735
1
        assert_eq!(formatted.attributes[1].end_index, 29);
736
1
        assert_eq!(formatted.attributes[1].props.font_style, FontStyle::Italic);
737

            
738
        // the whole string
739
1
        assert_eq!(formatted.attributes[2].start_index, 0);
740
1
        assert_eq!(formatted.attributes[2].end_index, 31);
741
1
        assert_eq!(formatted.attributes[2].props.font_family.0, "Foobar");
742
1
    }
743

            
744
    #[test]
745
1
    fn builds_bidi_formatted_text() {
746
1
        let doc_str = r##"<?xml version="1.0" encoding="UTF-8"?>
747
1
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
748
1

            
749
1
  <text2 id="sample" font-family="Foobar">
750
1
    LTR<tspan direction="rtl" unicode-bidi="embed" font-style="italic">RTL</tspan><tspan font-weight="bold">LTR</tspan>
751
1
  </text2>
752
1
</svg>
753
1
"##;
754
1

            
755
1
        let document = Document::load_from_bytes(doc_str.as_bytes());
756
1

            
757
1
        let text2_node = document.lookup_internal_node("sample").unwrap();
758
1
        assert!(matches!(
759
1
            *text2_node.borrow_element_data(),
760
            ElementData::Text2(_)
761
        ));
762

            
763
1
        let collected_text = collect_text_from_node(&text2_node);
764
1
        let collapsed_characters = collapse_white_space(&collected_text, WhiteSpace::Normal);
765
1

            
766
1
        let formatted = build_formatted_text(
767
1
            &collapsed_characters,
768
1
            &text2_node,
769
1
            &dummy_normalize_params(),
770
1
        );
771
1

            
772
1
        assert_eq!(&formatted.text, "\nLTR\u{202b}RTL\u{202c}LTR\n");
773

            
774
        // "RTL" surrounded by bidi control chars
775
1
        assert_eq!(formatted.attributes[0].start_index, 4);
776
1
        assert_eq!(formatted.attributes[0].end_index, 13);
777
1
        assert_eq!(formatted.attributes[0].props.font_style, FontStyle::Italic);
778

            
779
        // "LTR" at the end
780
1
        assert_eq!(formatted.attributes[1].start_index, 13);
781
1
        assert_eq!(formatted.attributes[1].end_index, 16);
782
1
        assert_eq!(formatted.attributes[1].props.font_weight, FontWeight::Bold);
783

            
784
        // the whole string
785
1
        assert_eq!(formatted.attributes[2].start_index, 0);
786
1
        assert_eq!(formatted.attributes[2].end_index, 17);
787
1
        assert_eq!(formatted.attributes[2].props.font_family.0, "Foobar");
788
1
    }
789
}