Skip to main content

cadmus_core/document/html/
layout.rs

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