diff --git a/crates/core/src/document/html/engine.rs b/crates/core/src/document/html/engine.rs
index 89895d3f..51451555 100644
--- a/crates/core/src/document/html/engine.rs
+++ b/crates/core/src/document/html/engine.rs
@@ -701,6 +701,48 @@ impl Engine {
inlines.push(InlineMaterial::LineBreak);
return;
},
+ "ruby" => {
+ // Ruby needs to be applied to the first text element only, with the content from the succeeding rt tag.
+ // So grab all text until rt, and attach its content to the first text child.
+ let mut text_datas = Vec::new();
+ for child in node.children() {
+ match child.data() {
+ NodeData::Element(ElementData { name, .. }) => {
+ if name == "rt" {
+ let mut ruby_content = "".to_owned();
+ for subchild in child.children() {
+ match subchild.data() {
+ NodeData::Text(TextData { text, .. }) => {
+ ruby_content.push_str(&decode_entities(text))
+ },
+ _ => {},
+ }
+ }
+ style.ruby = Some(ruby_content);
+
+ for TextData {text, offset} in &text_datas {
+ inlines.push(InlineMaterial::Text(TextMaterial {
+ offset: *offset,
+ text: decode_entities(text).into_owned(),
+ style: style.clone(),
+ }));
+ style.ruby = None;
+ }
+
+ text_datas.clear();
+ }
+ },
+ NodeData::Text(text_data) => {
+ text_datas.push(text_data.clone());
+ },
+ _ => {},
+ }
+ }
+ return;
+ },
+ "rt" => {
+ return;
+ },
_ => {},
}
@@ -810,151 +852,190 @@ impl Engine {
font.set_size(font_size, self.dpi);
font.plan(" 0.", None, None)
};
- let mut start_index = 0;
- for (end_index, _is_hardbreak) in LineBreakIterator::new(text) {
- for chunk in text[start_index..end_index].split_inclusive(char::is_whitespace) {
- if let Some((i, c)) = chunk.char_indices().next_back() {
- let j = i + if c.is_whitespace() { 0 } else { c.len_utf8() };
- if j > 0 {
- let buf = &text[start_index..start_index+j];
- let local_offset = offset + start_index;
- let mut plan = {
- let font = self.fonts.as_mut().unwrap()
- .get_mut(style.font_kind,
- style.font_style,
- style.font_weight);
- font.set_size(font_size, self.dpi);
- font.plan(buf, None, style.font_features.as_deref())
- };
- plan.space_out(style.letter_spacing);
-
- items.push(ParagraphItem::Box {
- width: plan.width,
- data: ParagraphElement::Text(TextElement {
- offset: local_offset,
- language: style.language.clone(),
- text: buf.to_string(),
- plan,
- font_features: style.font_features.clone(),
- font_kind: style.font_kind,
- font_style: style.font_style,
- font_weight: style.font_weight,
- vertical_align: style.vertical_align,
- letter_spacing: style.letter_spacing,
- font_size,
- color: style.color,
- uri: style.uri.clone(),
- }),
- });
- }
- if c.is_whitespace() {
- if c == '\n' && parent_style.retain_whitespace {
- let stretch = if parent_style.text_align == TextAlign::Center { big_stretch } else { line_width };
-
- items.push(ParagraphItem::Penalty { penalty: INFINITE_PENALTY, width: 0, flagged: false });
- items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
-
- items.push(ParagraphItem::Penalty { width: 0, penalty: -INFINITE_PENALTY, flagged: false });
- if parent_style.text_align == TextAlign::Center {
- items.push(ParagraphItem::Box { width: 0, data: ParagraphElement::Nothing });
- items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
- items.push(ParagraphItem::Glue { width: 0, stretch: big_stretch, shrink: 0 });
- }
- start_index += chunk.len();
- continue;
+ if let Some(ruby_text) = style.ruby.clone() {
+ // When an inline-block has ruby annotated, it's no longer allowed to linebreak.
+ // In this case, the entire block has to be pushed as a ParagraphItem::Box
+ // with the ruby characters rendered on top of the entire box.
+ let mut plan = {
+ let font = self.fonts.as_mut().unwrap()
+ .get_mut(style.font_kind,
+ style.font_style,
+ style.font_weight);
+ font.set_size(font_size, self.dpi);
+ font.plan(text, None, style.font_features.as_deref())
+ };
+ plan.space_out(style.letter_spacing);
+
+ items.push(ParagraphItem::Box {
+ width: plan.width,
+ data: ParagraphElement::Text(TextElement {
+ offset: *offset,
+ language: style.language.clone(),
+ text: text.to_string(),
+ ruby: Some(ruby_text),
+ plan,
+ font_features: style.font_features.clone(),
+ font_kind: style.font_kind,
+ font_style: style.font_style,
+ font_weight: style.font_weight,
+ vertical_align: style.vertical_align,
+ letter_spacing: style.letter_spacing,
+ font_size,
+ color: style.color,
+ uri: style.uri.clone(),
+ }),
+ });
+ }
+ else {
+ let mut start_index = 0;
+
+ for (end_index, _is_hardbreak) in LineBreakIterator::new(text) {
+ for chunk in text[start_index..end_index].split_inclusive(char::is_whitespace) {
+ if let Some((i, c)) = chunk.char_indices().next_back() {
+ let j = i + if c.is_whitespace() { 0 } else { c.len_utf8() };
+ if j > 0 {
+ let buf = &text[start_index..start_index+j];
+ let local_offset = offset + start_index;
+ let mut plan = {
+ let font = self.fonts.as_mut().unwrap()
+ .get_mut(style.font_kind,
+ style.font_style,
+ style.font_weight);
+ font.set_size(font_size, self.dpi);
+ font.plan(buf, None, style.font_features.as_deref())
+ };
+ plan.space_out(style.letter_spacing);
+
+ items.push(ParagraphItem::Box {
+ width: plan.width,
+ data: ParagraphElement::Text(TextElement {
+ offset: local_offset,
+ language: style.language.clone(),
+ text: buf.to_string(),
+ ruby: None,
+ plan,
+ font_features: style.font_features.clone(),
+ font_kind: style.font_kind,
+ font_style: style.font_style,
+ font_weight: style.font_weight,
+ vertical_align: style.vertical_align,
+ letter_spacing: style.letter_spacing,
+ font_size,
+ color: style.color,
+ uri: style.uri.clone(),
+ }),
+ });
}
+ if c.is_whitespace() {
+ if c == '\n' && parent_style.retain_whitespace {
+ let stretch = if parent_style.text_align == TextAlign::Center { big_stretch } else { line_width };
- let last_c = text[..start_index+i].chars().next_back().or_else(|| {
- if index > 0 {
- inlines[index-1].text().and_then(|text| text.chars().next_back())
- } else {
- None
- }
- });
+ items.push(ParagraphItem::Penalty { penalty: INFINITE_PENALTY, width: 0, flagged: false });
+ items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
- let has_more = text[start_index+i..].chars().any(|c| !c.is_xml_whitespace()) ||
- inlines[index+1..].iter().any(|m| m.text().map_or(false,
- |text| text.chars().any(|c| !c.is_xml_whitespace())));
+ items.push(ParagraphItem::Penalty { width: 0, penalty: -INFINITE_PENALTY, flagged: false });
- if !parent_style.retain_whitespace && c.is_xml_whitespace() &&
- (last_c.map(|c| c.is_xml_whitespace()) != Some(false) || !has_more) {
+ if parent_style.text_align == TextAlign::Center {
+ items.push(ParagraphItem::Box { width: 0, data: ParagraphElement::Nothing });
+ items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
+ items.push(ParagraphItem::Glue { width: 0, stretch: big_stretch, shrink: 0 });
+ }
start_index += chunk.len();
continue;
- }
+ }
- let mut width = if !parent_style.retain_whitespace {
- space_plan.glyph_advance(0)
- } else if let Some(index) = FONT_SPACES.chars().position(|x| x == c) {
- space_plan.glyph_advance(index)
- } else if let Some(ratio) = WORD_SPACE_RATIOS.get(&c) {
- (space_plan.glyph_advance(0) as f32 * ratio) as i32
- } else if let Some(ratio) = EM_SPACE_RATIOS.get(&c) {
- pt_to_px(style.font_size * ratio, self.dpi).round() as i32
- } else {
- space_plan.glyph_advance(0)
- };
+ let last_c = text[..start_index+i].chars().next_back().or_else(|| {
+ if index > 0 {
+ inlines[index-1].text().and_then(|text| text.chars().next_back())
+ } else {
+ None
+ }
+ });
- width += match style.word_spacing {
- WordSpacing::Normal => 0,
- WordSpacing::Length(l) => l,
- WordSpacing::Ratio(r) => (r * width as f32) as i32,
- } + style.letter_spacing;
+ let has_more = text[start_index+i..].chars().any(|c| !c.is_xml_whitespace()) ||
+ inlines[index+1..].iter().any(|m| m.text().map_or(false,
+ |text| text.chars().any(|c| !c.is_xml_whitespace())));
- let is_unbreakable = c == '\u{00A0}' || c == '\u{202F}' || c == '\u{2007}';
+ if !parent_style.retain_whitespace && c.is_xml_whitespace() &&
+ (last_c.map(|c| c.is_xml_whitespace()) != Some(false) || !has_more) {
+ start_index += chunk.len();
+ continue;
+ }
- if (is_unbreakable || (parent_style.retain_whitespace && c.is_xml_whitespace())) &&
- (last_c == Some('\n') || last_c.is_none()) {
- items.push(ParagraphItem::Box { width: 0, data: ParagraphElement::Nothing });
- }
+ let mut width = if !parent_style.retain_whitespace {
+ space_plan.glyph_advance(0)
+ } else if let Some(index) = FONT_SPACES.chars().position(|x| x == c) {
+ space_plan.glyph_advance(index)
+ } else if let Some(ratio) = WORD_SPACE_RATIOS.get(&c) {
+ (space_plan.glyph_advance(0) as f32 * ratio) as i32
+ } else if let Some(ratio) = EM_SPACE_RATIOS.get(&c) {
+ pt_to_px(style.font_size * ratio, self.dpi).round() as i32
+ } else {
+ space_plan.glyph_advance(0)
+ };
- if is_unbreakable {
- items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
- }
+ width += match style.word_spacing {
+ WordSpacing::Normal => 0,
+ WordSpacing::Length(l) => l,
+ WordSpacing::Ratio(r) => (r * width as f32) as i32,
+ } + style.letter_spacing;
- match parent_style.text_align {
- TextAlign::Justify => {
- items.push(ParagraphItem::Glue { width, stretch: width/2, shrink: width/3 });
- },
- TextAlign::Center => {
- if style.font_kind == FontKind::Monospace || is_unbreakable {
- items.push(ParagraphItem::Glue { width, stretch: 0, shrink: 0 });
- } else {
- let stretch = 3 * width;
- items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
- items.push(ParagraphItem::Penalty { width: 0, penalty: 0, flagged: false });
- items.push(ParagraphItem::Glue { width, stretch: -2 * stretch, shrink: 0 });
- items.push(ParagraphItem::Box { width: 0, data: ParagraphElement::Nothing });
- items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
- items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
- }
- },
- TextAlign::Left | TextAlign::Right => {
- if style.font_kind == FontKind::Monospace || is_unbreakable {
- items.push(ParagraphItem::Glue { width, stretch: 0, shrink: 0 });
- } else {
- let stretch = 3 * width;
- items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
- items.push(ParagraphItem::Penalty { width: 0, penalty: 0, flagged: false });
- items.push(ParagraphItem::Glue { width, stretch: -stretch, shrink: 0 });
- }
- },
- }
- } else if end_index < text.len() {
- let penalty = if c == '-' { self.hyphen_penalty } else { 0 };
- let flagged = penalty > 0;
- if matches!(parent_style.text_align, TextAlign::Justify | TextAlign::Center) {
- items.push(ParagraphItem::Penalty { width: 0, penalty, flagged });
- } else {
- let stretch = 3 * space_plan.glyph_advance(0);
- items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
- items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
- items.push(ParagraphItem::Penalty { width: 0, penalty: 10*penalty, flagged: true });
- items.push(ParagraphItem::Glue { width: 0, stretch: -stretch, shrink: 0 });
+ let is_unbreakable = c == '\u{00A0}' || c == '\u{202F}' || c == '\u{2007}';
+
+ if (is_unbreakable || (parent_style.retain_whitespace && c.is_xml_whitespace())) &&
+ (last_c == Some('\n') || last_c.is_none()) {
+ items.push(ParagraphItem::Box { width: 0, data: ParagraphElement::Nothing });
+ }
+
+ if is_unbreakable {
+ items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
+ }
+
+ match parent_style.text_align {
+ TextAlign::Justify => {
+ items.push(ParagraphItem::Glue { width, stretch: width/2, shrink: width/3 });
+ },
+ TextAlign::Center => {
+ if style.font_kind == FontKind::Monospace || is_unbreakable {
+ items.push(ParagraphItem::Glue { width, stretch: 0, shrink: 0 });
+ } else {
+ let stretch = 3 * width;
+ items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
+ items.push(ParagraphItem::Penalty { width: 0, penalty: 0, flagged: false });
+ items.push(ParagraphItem::Glue { width, stretch: -2 * stretch, shrink: 0 });
+ items.push(ParagraphItem::Box { width: 0, data: ParagraphElement::Nothing });
+ items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
+ items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
+ }
+ },
+ TextAlign::Left | TextAlign::Right => {
+ if style.font_kind == FontKind::Monospace || is_unbreakable {
+ items.push(ParagraphItem::Glue { width, stretch: 0, shrink: 0 });
+ } else {
+ let stretch = 3 * width;
+ items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
+ items.push(ParagraphItem::Penalty { width: 0, penalty: 0, flagged: false });
+ items.push(ParagraphItem::Glue { width, stretch: -stretch, shrink: 0 });
+ }
+ },
+ }
+ } else if end_index < text.len() {
+ let penalty = if c == '-' { self.hyphen_penalty } else { 0 };
+ let flagged = penalty > 0;
+ if matches!(parent_style.text_align, TextAlign::Justify | TextAlign::Center) {
+ items.push(ParagraphItem::Penalty { width: 0, penalty, flagged });
+ } else {
+ let stretch = 3 * space_plan.glyph_advance(0);
+ items.push(ParagraphItem::Penalty { width: 0, penalty: INFINITE_PENALTY, flagged: false });
+ items.push(ParagraphItem::Glue { width: 0, stretch, shrink: 0 });
+ items.push(ParagraphItem::Penalty { width: 0, penalty: 10*penalty, flagged: true });
+ items.push(ParagraphItem::Glue { width: 0, stretch: -stretch, shrink: 0 });
+ }
}
}
+ start_index += chunk.len();
}
- start_index += chunk.len();
}
}
},
@@ -1236,7 +1317,7 @@ impl Engine {
for i in last_index..index {
match items[i] {
- ParagraphItem::Box { ref data, width } => {
+ ParagraphItem::Box { ref data, mut width } => {
match data {
ParagraphElement::Text(element) => {
let pt = pt!(position.x, position.y - element.vertical_align);
@@ -1267,6 +1348,30 @@ impl Engine {
font_size: element.font_size,
color: element.color,
}));
+ if let Some(ruby) = &element.ruby {
+ let ruby_plan = {
+ let font = self.fonts.as_mut().unwrap()
+ .get_mut(element.font_kind, element.font_style, element.font_weight);
+ font.set_size(element.font_size / 2, self.dpi);
+ font.plan(ruby.to_string(), None, style.font_features.as_deref())
+ };
+ if width < ruby_plan.width {
+ width = ruby_plan.width;
+ }
+ page.push(DrawCommand::ExtraText(TextCommand {
+ offset: element.offset + root_data.start_offset,
+ position: pt + pt!(0, -ascender),
+ rect,
+ text: ruby.to_string(),
+ plan: ruby_plan,
+ uri: element.uri.clone(),
+ font_kind: element.font_kind,
+ font_style: element.font_style,
+ font_weight: element.font_weight,
+ font_size: element.font_size / 2,
+ color: element.color,
+ }));
+ }
},
ParagraphElement::Image(element) => {
while let Some(offset) = markers.get(markers_index) {
@@ -1490,6 +1595,7 @@ impl Engine {
letter_spacing: element.letter_spacing,
color: element.color,
uri: element.uri.clone(),
+ ruby: element.ruby.clone(),
}),
}
}
diff --git a/crates/core/src/document/html/layout.rs b/crates/core/src/document/html/layout.rs
index 0941cad3..e247beb3 100644
--- a/crates/core/src/document/html/layout.rs
+++ b/crates/core/src/document/html/layout.rs
@@ -69,6 +69,7 @@ pub struct StyleData {
pub vertical_align: i32,
pub list_style_type: Option,
pub uri: Option,
+ pub ruby: Option,
}
#[derive(Debug, Copy, Clone)]
@@ -193,6 +194,7 @@ impl Default for StyleData {
vertical_align: 0,
list_style_type: None,
uri: None,
+ ruby: None,
}
}
}
@@ -326,6 +328,7 @@ pub struct TextElement {
pub offset: usize,
pub language: Option,
pub text: String,
+ pub ruby: Option,
pub plan: RenderPlan,
pub font_features: Option>,
pub font_kind: FontKind,