cadmus_core/document/html/
layout.rs

1use crate::color::Color;
2use crate::color::BLACK;
3use crate::font::{Font, FontFamily, RenderPlan};
4use crate::geom::{Edge, Point, Rectangle};
5pub use crate::metadata::TextAlign;
6use fxhash::FxHashMap;
7use kl_hyphenate::{Language, Load, Standard};
8use lazy_static::lazy_static;
9use std::fmt::Debug;
10use std::fs;
11use std::path::{Path, PathBuf};
12
13pub const DEFAULT_HYPH_LANG: &str = "en";
14
15#[derive(Debug, Clone)]
16pub struct RootData {
17    pub start_offset: usize,
18    pub spine_dir: PathBuf,
19    pub rect: Rectangle,
20}
21
22#[derive(Debug, Clone)]
23pub struct DrawState {
24    pub position: Point,
25    pub floats: FxHashMap<usize, Vec<Rectangle>>,
26    pub prefix: Option<String>,
27    pub min_column_widths: Vec<i32>,
28    pub max_column_widths: Vec<i32>,
29    pub column_widths: Vec<i32>,
30    pub center_table: bool,
31}
32
33impl Default for DrawState {
34    fn default() -> Self {
35        DrawState {
36            position: Point::default(),
37            floats: FxHashMap::default(),
38            prefix: None,
39            min_column_widths: Vec::new(),
40            max_column_widths: Vec::new(),
41            column_widths: Vec::new(),
42            center_table: false,
43        }
44    }
45}
46
47#[derive(Debug, Clone)]
48pub struct StyleData {
49    pub display: Display,
50    pub float: Option<Float>,
51    pub width: i32,
52    pub height: i32,
53    pub margin: Edge,
54    pub padding: Edge,
55    pub start_x: i32,
56    pub end_x: i32,
57    pub retain_whitespace: bool,
58    pub text_align: TextAlign,
59    pub text_indent: i32,
60    pub line_height: i32,
61    pub language: Option<String>,
62    pub font_kind: FontKind,
63    pub font_style: FontStyle,
64    pub font_weight: FontWeight,
65    pub font_size: f32,
66    pub font_features: Option<Vec<String>>,
67    pub color: Color,
68    pub letter_spacing: i32,
69    pub word_spacing: WordSpacing,
70    pub vertical_align: i32,
71    pub list_style_type: Option<ListStyleType>,
72    pub uri: Option<String>,
73}
74
75#[derive(Debug, Copy, Clone)]
76pub enum WordSpacing {
77    Normal,
78    Length(i32),
79    Ratio(f32),
80}
81
82#[derive(Debug, Copy, Clone, Eq, PartialEq)]
83pub enum Float {
84    Left,
85    Right,
86}
87
88#[derive(Debug, Copy, Clone, Eq, PartialEq)]
89pub enum Display {
90    Block,
91    Inline,
92    InlineTable,
93    None,
94}
95
96#[derive(Debug, Copy, Clone, Eq, PartialEq)]
97pub enum ListStyleType {
98    Disc,
99    Circle,
100    Square,
101    Decimal,
102    LowerRoman,
103    UpperRoman,
104    LowerAlpha,
105    UpperAlpha,
106    LowerGreek,
107    UpperGreek,
108    None,
109}
110
111#[derive(Debug, Clone)]
112pub struct ChildArtifact {
113    pub sibling_style: SiblingStyle,
114    pub rects: Vec<Option<Rectangle>>,
115}
116
117#[derive(Debug, Clone)]
118pub struct SiblingStyle {
119    pub padding: Edge,
120    pub margin: Edge,
121}
122
123#[derive(Debug, Clone)]
124pub struct LineStats {
125    pub width: i32,
126    pub merged_width: i32,
127    pub glues_count: usize,
128    pub started: bool,
129}
130
131impl Default for LineStats {
132    fn default() -> Self {
133        LineStats {
134            width: 0,
135            merged_width: 0,
136            glues_count: 0,
137            started: false,
138        }
139    }
140}
141
142impl Default for SiblingStyle {
143    fn default() -> Self {
144        SiblingStyle {
145            padding: Edge::default(),
146            margin: Edge::default(),
147        }
148    }
149}
150
151#[derive(Debug, Clone)]
152pub struct LoopContext {
153    pub index: usize,
154    pub sibling_style: SiblingStyle,
155    pub is_first: bool,
156    pub is_last: bool,
157}
158
159impl Default for LoopContext {
160    fn default() -> Self {
161        LoopContext {
162            index: 0,
163            sibling_style: SiblingStyle::default(),
164            is_first: false,
165            is_last: false,
166        }
167    }
168}
169
170impl Default for StyleData {
171    fn default() -> Self {
172        StyleData {
173            display: Display::Block,
174            float: None,
175            width: 0,
176            height: 0,
177            margin: Edge::default(),
178            padding: Edge::default(),
179            start_x: 0,
180            end_x: 0,
181            retain_whitespace: false,
182            text_align: TextAlign::Left,
183            text_indent: 0,
184            line_height: 0,
185            language: None,
186            font_kind: FontKind::Serif,
187            font_style: FontStyle::Normal,
188            font_weight: FontWeight::Normal,
189            font_size: 0.0,
190            font_features: None,
191            color: BLACK,
192            letter_spacing: 0,
193            word_spacing: WordSpacing::Normal,
194            vertical_align: 0,
195            list_style_type: None,
196            uri: None,
197        }
198    }
199}
200
201#[derive(Debug, Clone)]
202pub enum InlineMaterial {
203    Text(TextMaterial),
204    Image(ImageMaterial),
205    Glue(GlueMaterial),
206    Penalty(PenaltyMaterial),
207    Box(i32),
208    LineBreak,
209}
210
211impl InlineMaterial {
212    pub fn offset(&self) -> Option<usize> {
213        match self {
214            InlineMaterial::Text(TextMaterial { offset, .. })
215            | InlineMaterial::Image(ImageMaterial { offset, .. }) => Some(*offset),
216            _ => None,
217        }
218    }
219
220    pub fn text(&self) -> Option<&str> {
221        match self {
222            InlineMaterial::Text(TextMaterial { ref text, .. }) => Some(text),
223            _ => None,
224        }
225    }
226}
227
228#[derive(Debug, Clone)]
229pub struct TextMaterial {
230    pub offset: usize,
231    pub text: String,
232    pub style: StyleData,
233}
234
235#[derive(Debug, Clone)]
236pub struct ImageMaterial {
237    pub offset: usize,
238    pub path: String,
239    pub style: StyleData,
240}
241
242#[derive(Debug, Clone)]
243pub struct GlueMaterial {
244    pub width: i32,
245    pub stretch: i32,
246    pub shrink: i32,
247}
248
249#[derive(Debug, Clone)]
250pub struct PenaltyMaterial {
251    pub width: i32,
252    pub penalty: i32,
253    pub flagged: bool,
254}
255
256#[derive(Debug, Copy, Clone, Eq, PartialEq)]
257pub enum FontKind {
258    Serif,
259    SansSerif,
260    Monospace,
261    Cursive,
262    Fantasy,
263}
264
265#[derive(Debug, Copy, Clone)]
266pub enum FontStyle {
267    Normal,
268    Italic,
269}
270
271#[derive(Debug, Copy, Clone)]
272pub enum FontWeight {
273    Normal,
274    Bold,
275}
276
277pub struct Fonts {
278    pub serif: FontFamily,
279    pub sans_serif: FontFamily,
280    pub monospace: FontFamily,
281    pub cursive: Font,
282    pub fantasy: Font,
283}
284
285impl Fonts {
286    pub fn get_mut(
287        &mut self,
288        font_kind: FontKind,
289        font_style: FontStyle,
290        font_weight: FontWeight,
291    ) -> &mut Font {
292        match font_kind {
293            FontKind::Serif => match (font_style, font_weight) {
294                (FontStyle::Normal, FontWeight::Normal) => &mut self.serif.regular,
295                (FontStyle::Normal, FontWeight::Bold) => &mut self.serif.bold,
296                (FontStyle::Italic, FontWeight::Normal) => &mut self.serif.italic,
297                (FontStyle::Italic, FontWeight::Bold) => &mut self.serif.bold_italic,
298            },
299            FontKind::SansSerif => match (font_style, font_weight) {
300                (FontStyle::Normal, FontWeight::Normal) => &mut self.sans_serif.regular,
301                (FontStyle::Normal, FontWeight::Bold) => &mut self.sans_serif.bold,
302                (FontStyle::Italic, FontWeight::Normal) => &mut self.sans_serif.italic,
303                (FontStyle::Italic, FontWeight::Bold) => &mut self.sans_serif.bold_italic,
304            },
305            FontKind::Monospace => match (font_style, font_weight) {
306                (FontStyle::Normal, FontWeight::Normal) => &mut self.monospace.regular,
307                (FontStyle::Normal, FontWeight::Bold) => &mut self.monospace.bold,
308                (FontStyle::Italic, FontWeight::Normal) => &mut self.monospace.italic,
309                (FontStyle::Italic, FontWeight::Bold) => &mut self.monospace.bold_italic,
310            },
311            FontKind::Cursive => &mut self.cursive,
312            FontKind::Fantasy => &mut self.fantasy,
313        }
314    }
315}
316
317#[derive(Debug, Clone)]
318pub enum ParagraphElement {
319    Text(TextElement),
320    Image(ImageElement),
321    Nothing,
322}
323
324#[derive(Debug, Clone)]
325pub struct TextElement {
326    pub offset: usize,
327    pub language: Option<String>,
328    pub text: String,
329    pub plan: RenderPlan,
330    pub font_features: Option<Vec<String>>,
331    pub font_kind: FontKind,
332    pub font_style: FontStyle,
333    pub font_weight: FontWeight,
334    pub font_size: u32,
335    pub letter_spacing: i32,
336    pub vertical_align: i32,
337    pub color: Color,
338    pub uri: Option<String>,
339}
340
341#[derive(Debug, Clone)]
342pub struct ImageElement {
343    pub offset: usize,
344    pub width: i32,
345    pub height: i32,
346    pub scale: f32,
347    pub vertical_align: i32,
348    pub display: Display,
349    pub margin: Edge,
350    pub float: Option<Float>,
351    pub path: String,
352    pub uri: Option<String>,
353}
354
355#[derive(Debug, Clone)]
356pub enum DrawCommand {
357    Text(TextCommand),
358    ExtraText(TextCommand),
359    Image(ImageCommand),
360    Marker(usize),
361}
362
363#[derive(Debug, Clone)]
364pub struct TextCommand {
365    pub offset: usize,
366    pub position: Point,
367    pub text: String,
368    pub plan: RenderPlan,
369    pub font_kind: FontKind,
370    pub font_style: FontStyle,
371    pub font_weight: FontWeight,
372    pub font_size: u32,
373    pub color: Color,
374    pub uri: Option<String>,
375    pub rect: Rectangle,
376}
377
378#[derive(Debug, Clone)]
379pub struct ImageCommand {
380    pub offset: usize,
381    pub position: Point,
382    pub scale: f32,
383    pub path: String,
384    pub uri: Option<String>,
385    pub rect: Rectangle,
386}
387
388impl DrawCommand {
389    pub fn offset(&self) -> usize {
390        match *self {
391            DrawCommand::Text(TextCommand { offset, .. }) => offset,
392            DrawCommand::ExtraText(TextCommand { offset, .. }) => offset,
393            DrawCommand::Image(ImageCommand { offset, .. }) => offset,
394            DrawCommand::Marker(offset) => offset,
395        }
396    }
397
398    pub fn rect(&self) -> Option<Rectangle> {
399        match *self {
400            DrawCommand::Text(TextCommand { rect, .. }) => Some(rect),
401            DrawCommand::ExtraText(TextCommand { rect, .. }) => Some(rect),
402            DrawCommand::Image(ImageCommand { rect, .. }) => Some(rect),
403            _ => None,
404        }
405    }
406
407    pub fn position_mut(&mut self) -> Option<&mut Point> {
408        match *self {
409            DrawCommand::Text(TextCommand {
410                ref mut position, ..
411            }) => Some(position),
412            DrawCommand::ExtraText(TextCommand {
413                ref mut position, ..
414            }) => Some(position),
415            DrawCommand::Image(ImageCommand {
416                ref mut position, ..
417            }) => Some(position),
418            _ => None,
419        }
420    }
421}
422
423pub fn collapse_margins(a: i32, b: i32) -> i32 {
424    if a >= 0 && b >= 0 {
425        a.max(b)
426    } else if a < 0 && b < 0 {
427        a.min(b)
428    } else {
429        a + b
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_hyph_lang() {
439        assert_eq!(hyph_lang("zh-latn-pinyin"), Some(Language::Chinese));
440        assert_eq!(hyph_lang("EN"), Some(Language::EnglishUS));
441        assert_eq!(hyph_lang("en-GB"), Some(Language::EnglishGB));
442        assert_eq!(hyph_lang("DE-ZZZ"), Some(Language::German1996));
443        assert_eq!(hyph_lang("de-CH-uuu"), Some(Language::GermanSwiss));
444        assert_eq!(hyph_lang("y"), None);
445    }
446}
447
448pub fn hyph_lang(name: &str) -> Option<Language> {
449    HYPHENATION_LANGUAGES
450        .get(name)
451        .or_else(|| HYPHENATION_LANGUAGES.get(name.to_lowercase().as_str()))
452        .or_else(|| {
453            let name_lc = name.to_lowercase();
454            let mut s = name_lc.as_str();
455            while let Some(index) = s.rfind('-') {
456                s = &s[..index];
457                let opt = HYPHENATION_LANGUAGES.get(s);
458                if opt.is_some() {
459                    return opt;
460                }
461            }
462            None
463        })
464        .cloned()
465}
466
467lazy_static! {
468pub static ref HYPHENATION_LANGUAGES: FxHashMap<&'static str, Language> = [
469    ("af", Language::Afrikaans),
470    ("hy", Language::Armenian),
471    ("as", Language::Assamese),
472    ("eu", Language::Basque),
473    ("be", Language::Belarusian),
474    ("bn", Language::Bengali),
475    ("bg", Language::Bulgarian),
476    ("ca", Language::Catalan),
477    ("zh-latn-pinyin", Language::Chinese),
478    ("cop", Language::Coptic),
479    ("hr", Language::Croatian),
480    ("cs", Language::Czech),
481    ("da", Language::Danish),
482    ("nl", Language::Dutch),
483    ("en-gb", Language::EnglishGB),
484    ("en-us", Language::EnglishUS),
485    ("en", Language::EnglishUS),
486    ("eo", Language::Esperanto),
487    ("et", Language::Estonian),
488    ("mul-ethi", Language::Ethiopic),
489    ("fi", Language::Finnish),
490    ("fr", Language::French),
491    ("fur", Language::Friulan),
492    ("gl", Language::Galician),
493    ("ka", Language::Georgian),
494    ("de", Language::German1996),
495    ("de-1901", Language::German1901),
496    ("de-1996", Language::German1996),
497    ("de-ch-1901", Language::GermanSwiss),
498    ("de-ch", Language::GermanSwiss),
499    ("grc", Language::GreekAncient),
500    ("el-monoton", Language::GreekMono),
501    ("el-polyton", Language::GreekPoly),
502    ("gu", Language::Gujarati),
503    ("hi", Language::Hindi),
504    ("hu", Language::Hungarian),
505    ("is", Language::Icelandic),
506    ("id", Language::Indonesian),
507    ("ia", Language::Interlingua),
508    ("ga", Language::Irish),
509    ("it", Language::Italian),
510    ("kn", Language::Kannada),
511    ("kmr", Language::Kurmanji),
512    ("la", Language::Latin),
513    ("la-x-classic", Language::LatinClassic),
514    ("la-x-liturgic", Language::LatinLiturgical),
515    ("lv", Language::Latvian),
516    ("lt", Language::Lithuanian),
517    ("mk", Language::Macedonian),
518    ("ml", Language::Malayalam),
519    ("mr", Language::Marathi),
520    ("mn-cyrl", Language::Mongolian),
521    ("nb", Language::NorwegianBokmal),
522    ("nn", Language::NorwegianNynorsk),
523    ("oc", Language::Occitan),
524    ("or", Language::Oriya),
525    ("pi", Language::Pali),
526    ("pa", Language::Panjabi),
527    ("pms", Language::Piedmontese),
528    ("pl", Language::Polish),
529    ("pt", Language::Portuguese),
530    ("ro", Language::Romanian),
531    ("rm", Language::Romansh),
532    ("ru", Language::Russian),
533    ("sa", Language::Sanskrit),
534    ("sr-cyrl", Language::SerbianCyrillic),
535    ("sh-cyrl", Language::SerbocroatianCyrillic),
536    ("sh-latn", Language::SerbocroatianLatin),
537    ("cu", Language::SlavonicChurch),
538    ("sk", Language::Slovak),
539    ("sl", Language::Slovenian),
540    ("es", Language::Spanish),
541    ("sv", Language::Swedish),
542    ("ta", Language::Tamil),
543    ("te", Language::Telugu),
544    ("th", Language::Thai),
545    ("tr", Language::Turkish),
546    ("tk", Language::Turkmen),
547    ("uk", Language::Ukrainian),
548    ("hsb", Language::Uppersorbian),
549    ("cy", Language::Welsh)].iter().cloned().collect();
550
551pub static ref HYPHENATION_PATTERNS: FxHashMap<Language, Standard> = {
552    let mut map = FxHashMap::default();
553    for lang in HYPHENATION_LANGUAGES.values() {
554        if map.contains_key(lang) {
555            continue;
556        }
557        let base = Path::new("hyphenation-patterns")
558                        .join(lang.code());
559        let path = base.with_extension("standard.bincode");
560        if let Ok(mut patterns) = Standard::from_path(*lang, path) {
561            let path = base.with_extension("bounds");
562            if let Ok(pair) = fs::read_to_string(path) {
563                let bounds = pair.trim_end().split(' ')
564                                 .filter_map(|s| s.parse().ok())
565                                 .collect::<Vec<usize>>();
566                if bounds.len() == 2 {
567                    patterns.minima.0 = bounds[0];
568                    patterns.minima.1 = bounds[1];
569                }
570            }
571            map.insert(*lang, patterns);
572        }
573    }
574    map
575};
576
577pub static ref EM_SPACE_RATIOS: FxHashMap<char, f32> = [
578    // En quad.
579    ('\u{2000}', 0.5),
580    // Em quad.
581    ('\u{2001}', 1.0),
582    // En space.
583    ('\u{2002}', 0.5),
584    // Em space.
585    ('\u{2003}', 1.0),
586    // Three-per-em space.
587    ('\u{2004}', 0.33),
588    // Four-per-em space.
589    ('\u{2005}', 0.25),
590    // Six-per-em space.
591    ('\u{2006}', 0.16)].iter().cloned().collect();
592
593pub static ref WORD_SPACE_RATIOS: FxHashMap<char, f32> = [
594    // Tabulation
595    ('\t', 4.0),
596    // No-break space
597    ('\u{00A0}', 1.0),
598    // Narrow no-break space
599    ('\u{202F}', 0.5),
600    // Thin space.
601    ('\u{2009}', 0.5),
602    // Hair space.
603    ('\u{200A}', 0.25)].iter().cloned().collect();
604}
605
606pub const FONT_SPACES: &str = " \u{2007}\u{2008}";