use std::collections::BTreeMap;
use zenith_core::{
BlockStyle, Diagnostic, FontStyle, ListKind, MdBlock, ResolvedToken, TextNode, TextSpan,
};
use zenith_layout::TextDirection;
use crate::ir::Color;
use super::super::paint::resolve_property_color;
use super::ctx::{NodeShape, ShapeEnv};
use super::markdown_block::{
BLOCKQUOTE_INDENT_PX, BlockStyleCascade, CODE_BLOCK_BG, HR_COLOR, HR_THICKNESS_PX,
LIST_INDENT_PX, ResolvedBlockStyle, block_role, resolve_block_style_core,
};
use super::pack::{LineDecoration, LineStyle};
use super::shape::{
LINK_COLOR, ResolvedSpan, WordMetrics, WordToken, resolve_font_family_name,
resolve_font_weight, resolve_vertical_align, shape_words,
};
pub(in crate::compile) struct BlockDescriptor {
pub(in crate::compile) tokens: Vec<WordToken>,
pub(in crate::compile) metrics: WordMetrics,
pub(in crate::compile) line_style: LineStyle,
pub(in crate::compile) space_before_px: f64,
pub(in crate::compile) space_after_px: f64,
pub(in crate::compile) is_spacer: bool,
pub(in crate::compile) left_indent_px: f64,
pub(in crate::compile) decoration: Option<LineDecoration>,
}
#[derive(Clone, Copy)]
pub(in crate::compile) struct BlockStyleEnv<'a> {
pub(in crate::compile) resolved: &'a BTreeMap<String, ResolvedToken>,
pub(in crate::compile) page_block_styles: &'a [BlockStyle],
pub(in crate::compile) doc_block_styles: &'a [BlockStyle],
}
#[derive(Clone, Copy)]
pub(in crate::compile) struct ChainSourceShape<'a> {
pub(in crate::compile) families: &'a [String],
pub(in crate::compile) node_font_size: f32,
pub(in crate::compile) base_weight: u16,
pub(in crate::compile) direction: TextDirection,
}
pub(in crate::compile) fn shape_source_blocks(
src: &TextNode,
blocks: &[MdBlock],
shape: ChainSourceShape,
style_env: BlockStyleEnv,
shape_env: ShapeEnv,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<BlockDescriptor> {
let mut descriptors: Vec<BlockDescriptor> = Vec::with_capacity(blocks.len());
for block in blocks {
let role = block_role(block);
let style = resolve_block_style_core(BlockStyleCascade {
role,
node_styles: src.block_styles.as_slice(),
page_styles: style_env.page_block_styles,
doc_styles: style_env.doc_block_styles,
resolved: style_env.resolved,
node_font_size: f64::from(shape.node_font_size),
node_font_family: src.font_family.as_ref(),
node_font_weight: src.font_weight.as_ref(),
node_fill: src.fill.as_ref(),
node_align: src.align.as_ref(),
});
let block_font_size = style.font_size_px as f32;
let deco_thickness = (block_font_size as f64 / 14.0).max(1.0);
let override_families: Option<Vec<String>> = style.font_family.as_ref().map(|fp| {
vec![resolve_font_family_name(
Some(fp),
style_env.resolved,
"Noto Sans",
)]
});
let block_families: &[String] = override_families.as_deref().unwrap_or(shape.families);
let block_weight = resolve_font_weight(
style.font_weight.as_ref(),
style_env.resolved,
shape.base_weight,
);
let block_fill: Option<Color> = style
.fill
.as_ref()
.and_then(|fp| resolve_property_color(fp, style_env.resolved, diagnostics, &src.id));
if matches!(block, MdBlock::HorizontalRule) {
let gap = style.space_before_px + style.space_after_px;
descriptors.push(BlockDescriptor {
tokens: Vec::new(),
metrics: WordMetrics {
ascent: 0.0,
line_height: gap,
space_advance: 0.0,
},
line_style: LineStyle {
ascent: 0.0,
space_advance: 0.0,
font_size: block_font_size,
deco_thickness,
},
space_before_px: 0.0,
space_after_px: 0.0,
is_spacer: true,
left_indent_px: 0.0,
decoration: Some(LineDecoration::Rule {
color: HR_COLOR,
thickness: HR_THICKNESS_PX,
}),
});
continue;
}
let bctx = BlockShapeCtx {
style: &style,
font_size: block_font_size,
weight: block_weight,
fill: block_fill,
resolved: style_env.resolved,
node_id: &src.id,
};
let spans = block_spans(block, bctx, diagnostics);
let (tokens, metrics) = shape_words(
&spans,
block_families,
NodeShape {
font_size: block_font_size,
base_weight: block_weight,
direction: shape.direction,
},
shape_env,
diagnostics,
&src.id,
src.source_span,
);
let (left_indent_px, decoration) = match block {
MdBlock::Blockquote { .. } => (BLOCKQUOTE_INDENT_PX, None),
MdBlock::ListItem { depth, .. } => ((*depth as f64) * LIST_INDENT_PX, None),
MdBlock::CodeBlock { .. } => (0.0, Some(LineDecoration::Background(CODE_BLOCK_BG))),
MdBlock::Heading { .. } | MdBlock::Paragraph { .. } => (0.0, None),
MdBlock::HorizontalRule => (0.0, None),
};
descriptors.push(BlockDescriptor {
tokens,
metrics,
line_style: LineStyle {
ascent: metrics.ascent,
space_advance: metrics.space_advance,
font_size: block_font_size,
deco_thickness,
},
space_before_px: style.space_before_px,
space_after_px: style.space_after_px,
is_spacer: false,
left_indent_px,
decoration,
});
}
descriptors
}
#[derive(Clone, Copy)]
struct BlockShapeCtx<'a> {
style: &'a ResolvedBlockStyle,
font_size: f32,
weight: u16,
fill: Option<Color>,
resolved: &'a BTreeMap<String, ResolvedToken>,
node_id: &'a str,
}
fn block_spans(
block: &MdBlock,
ctx: BlockShapeCtx,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<ResolvedSpan> {
match block {
MdBlock::Heading { spans, .. }
| MdBlock::Paragraph { spans }
| MdBlock::Blockquote { spans } => text_spans_to_resolved(spans, None, ctx, diagnostics),
MdBlock::ListItem {
kind,
ordinal,
spans,
..
} => {
let marker = match kind {
ListKind::Unordered => "• ".to_owned(),
ListKind::Ordered => format!("{}. ", ordinal.unwrap_or(1)),
};
text_spans_to_resolved(spans, Some(marker), ctx, diagnostics)
}
MdBlock::CodeBlock { content, .. } => {
vec![ResolvedSpan {
text: content.clone(),
color: ctx.fill.unwrap_or(Color::srgb(0, 0, 0, 255)),
underline: false,
strikethrough: false,
highlight: None,
code: true,
link: None,
weight: ctx.weight,
style: FontStyle::Normal,
font_size: ctx.font_size,
baseline_dy: 0.0,
}]
}
MdBlock::HorizontalRule => Vec::new(),
}
}
fn text_spans_to_resolved(
spans: &[TextSpan],
marker: Option<String>,
ctx: BlockShapeCtx,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<ResolvedSpan> {
let resolved = ctx.resolved;
let node_id = ctx.node_id;
let block_italic = ctx.style.italic == Some(true);
let mut out: Vec<ResolvedSpan> = Vec::new();
if let Some(m) = marker {
out.push(ResolvedSpan {
text: m,
color: ctx.fill.unwrap_or(Color::srgb(0, 0, 0, 255)),
underline: false,
strikethrough: false,
highlight: None,
code: false,
link: None,
weight: ctx.weight,
style: if block_italic {
FontStyle::Italic
} else {
FontStyle::Normal
},
font_size: ctx.font_size,
baseline_dy: 0.0,
});
}
for span in spans {
if span.text.is_empty() {
continue;
}
let is_link = span.link.is_some();
let color = span
.fill
.as_ref()
.and_then(|fp| resolve_property_color(fp, resolved, diagnostics, node_id))
.or(is_link.then_some(LINK_COLOR))
.or(ctx.fill)
.unwrap_or(Color::srgb(0, 0, 0, 255));
let highlight: Option<Color> = span
.highlight
.as_ref()
.and_then(|hp| resolve_property_color(hp, resolved, diagnostics, node_id));
let code = span.code == Some(true);
let link = span.link.clone();
let weight = resolve_font_weight(span.font_weight.as_ref(), resolved, ctx.weight);
let font_style = if span.italic == Some(true) || block_italic {
FontStyle::Italic
} else {
FontStyle::Normal
};
let (span_font_size, baseline_dy) =
resolve_vertical_align(span.vertical_align.as_deref(), ctx.font_size);
out.push(ResolvedSpan {
text: span.text.clone(),
color,
underline: span.underline == Some(true) || is_link,
strikethrough: span.strikethrough == Some(true),
highlight,
code,
link,
weight,
style: font_style,
font_size: span_font_size,
baseline_dy,
});
}
out
}