///|
pub fn markdown_block_to_rich_text(
block : MarkdownBlock,
base : @core.FontSpec,
base_dir? : String? = None,
) -> RichTextBlock {
match block.kind {
Heading(level) => {
let font = {
..base,
size: base.size * markdown_heading_size_scale(level),
weight: 700,
}
RichTextBlock::new(
runs=markdown_inlines_to_rich_text(block.inlines, font, base_dir~),
font=Some(font),
line_height=Some(base.size * markdown_heading_line_height_scale(level)),
// Typora-style heading spacing: breath above, a little below.
inset={
top: base.size * 0.6,
right: 0.0,
bottom: base.size * 0.3,
left: 0.0,
},
)
}
UnorderedListItem =>
RichTextBlock::new(
runs=markdown_inlines_to_rich_text(block.inlines, base, base_dir~),
prefix="• ",
line_height=Some(base.size * 1.55),
)
TaskListItem(checked) =>
RichTextBlock::new(
runs=markdown_inlines_to_rich_text(block.inlines, base, base_dir~),
prefix=if checked { "☑ " } else { "☐ " },
line_height=Some(base.size * 1.55),
)
OrderedListItem(number) =>
RichTextBlock::new(
runs=markdown_inlines_to_rich_text(block.inlines, base, base_dir~),
prefix="\{number}. ",
line_height=Some(base.size * 1.55),
)
Blockquote =>
RichTextBlock::new(
runs=markdown_inlines_to_rich_text(block.inlines, base, base_dir~),
color=Some(@core.Color::black().multiply_alpha(0.82)),
background=Some(
@core.Brush::solid(@core.Color::rgba(r=0.12, g=0.13, b=0.15, a=0.05)),
),
border_left=Some(
@core.BorderStyle::new(
brush=@core.Brush::solid(@core.Color::black().multiply_alpha(0.32)),
width=4.0,
),
),
inset={ top: 6.0, right: 0.0, bottom: 12.0, left: 14.0 },
line_height=Some(base.size * 1.55),
corner_radius=4.0,
)
CodeBlock =>
RichTextBlock::new(
runs=markdown_code_block_runs(block, base),
background=Some(
@core.Brush::solid(@core.Color::rgba(r=0.12, g=0.13, b=0.15, a=0.06)),
),
inset={ top: 12.0, right: 14.0, bottom: 12.0, left: 14.0 },
line_height=Some(base.size * 1.1),
corner_radius=8.0,
)
HorizontalRule =>
RichTextBlock::new(runs=[], line_height=Some(base.size * 1.0), rule=true)
Table =>
RichTextBlock::new(
runs=[
RichTextRun::new(
text=block.text,
font=Some({
..base,
families: @core.FontFamilyStack::monospace(),
size: base.size * 0.92,
}),
),
],
table=markdown_table_preview(block, base),
background=Some(
@core.Brush::solid(@core.Color::rgba(r=0.08, g=0.12, b=0.16, a=0.05)),
),
inset={ top: 6.0, right: 10.0, bottom: 6.0, left: 10.0 },
line_height=Some(base.size * 1.35),
corner_radius=6.0,
)
FootnoteDefinition(label) =>
RichTextBlock::new(
runs=[
RichTextRun::new(
text="[^" + label + "]: ",
visual_text=Some(label),
font=Some({ ..base, size: base.size * 0.78, weight: 700 }),
color=Some(@core.Color::black().multiply_alpha(0.58)),
background=Some(
@core.Brush::solid(
@core.Color::rgba(r=0.18, g=0.4, b=0.7, a=0.08),
),
),
background_radius=4.0,
),
..markdown_inlines_to_rich_text(block.inlines, base, base_dir~),
],
color=Some(@core.Color::black().multiply_alpha(0.82)),
border_left=Some(
@core.BorderStyle::new(
brush=@core.Brush::solid(@core.Color::black().multiply_alpha(0.22)),
width=3.0,
),
),
inset={ top: 2.0, right: 0.0, bottom: 2.0, left: 12.0 },
line_height=Some(base.size * 1.45),
)
HtmlBlock =>
RichTextBlock::new(
runs=[
RichTextRun::new(
text=block.text,
font=Some({
..base,
families: @core.FontFamilyStack::monospace(),
size: base.size * 0.9,
}),
color=Some(@core.Color::black().multiply_alpha(0.7)),
),
],
background=Some(
@core.Brush::solid(@core.Color::rgba(r=0.12, g=0.12, b=0.12, a=0.06)),
),
inset={ top: 4.0, right: 10.0, bottom: 4.0, left: 10.0 },
line_height=Some(base.size * 1.25),
corner_radius=6.0,
)
FrontMatter =>
RichTextBlock::new(
runs=[
RichTextRun::new(
text=block.text,
font=Some({
..base,
families: @core.FontFamilyStack::monospace(),
size: base.size * 0.9,
}),
color=Some(@core.Color::black().multiply_alpha(0.74)),
),
],
background=Some(
@core.Brush::solid(@core.Color::rgba(r=0.20, g=0.28, b=0.36, a=0.07)),
),
inset={ top: 6.0, right: 10.0, bottom: 6.0, left: 10.0 },
line_height=Some(base.size * 1.25),
corner_radius=6.0,
)
Paragraph =>
RichTextBlock::new(
runs=markdown_inlines_to_rich_text(block.inlines, base, base_dir~),
line_height=Some(base.size * 1.55),
// Paragraph spacing below for readability between blocks.
inset={ top: 0.0, right: 0.0, bottom: base.size * 0.4, left: 0.0 },
)
}
}
///|
fn markdown_code_block_runs(
block : MarkdownBlock,
base : @core.FontSpec,
) -> Array[RichTextRun] {
let font = {
..base,
families: @core.FontFamilyStack::monospace(),
size: base.size * 0.92,
}
match block.code_language {
Some(language) => {
let runs : Array[RichTextRun] = [
RichTextRun::new(
text=language,
font=Some({ ..font, size: base.size * 0.78, weight: 700 }),
visual_text=Some(language),
color=Some(@core.Color::black().multiply_alpha(0.56)),
),
RichTextRun::new(text="\n", font=Some(font)),
]
// Apply syntax highlighting when the language is supported; otherwise emit
// the source as a single plain run.
if code_highlight_supports_language(language) {
for span in code_highlight_tokenize(block.text, language) {
let color = if span.kind is CodePlain {
None
} else {
Some(code_highlight_color(span.kind))
}
runs.push(RichTextRun::new(text=span.text, font=Some(font), color~))
}
} else {
runs.push(RichTextRun::new(text=block.text, font=Some(font)))
}
runs
}
None => [RichTextRun::new(text=block.text, font=Some(font))]
}
}
///|
fn markdown_heading_size_scale(level : Int) -> Double {
if level <= 1 {
1.45
} else if level == 2 {
1.22
} else if level == 3 {
1.08
} else if level == 4 {
1.0
} else if level == 5 {
0.94
} else {
0.9
}
}
///|
fn markdown_heading_line_height_scale(level : Int) -> Double {
if level <= 1 {
2.0
} else if level == 2 {
1.65
} else if level == 3 {
1.45
} else {
1.35
}
}
///|
fn markdown_inlines_to_rich_text(
inlines : Array[MarkdownInline],
base : @core.FontSpec,
base_dir? : String? = None,
) -> Array[RichTextRun] {
let runs : Array[RichTextRun] = []
for span in inlines {
runs.push(
RichTextRun::new(
text=markdown_inline_source_text(span),
visual_text=Some(markdown_inline_visual_text(span)),
image_source=markdown_inline_image_source(span, base_dir~),
image_size=markdown_inline_image_size(span, base),
font=Some(markdown_inline_font(span.kind, base)),
color=markdown_inline_color(span.kind),
background=markdown_inline_background(span.kind),
decoration=markdown_inline_decoration(span.kind),
),
)
}
runs
}
///|
fn markdown_inline_visual_text(inline : MarkdownInline) -> String {
let text = match inline.kind {
Image => markdown_inline_source_text(inline)
FootnoteReference => inline.text
_ => inline.text
}
text.replace(old="\n", new=" ")
}
///|
fn markdown_inline_image_source(
inline : MarkdownInline,
base_dir? : String? = None,
) -> String? {
match inline.kind {
Image =>
match inline.target {
Some(target) => Some(markdown_resolve_image_source(target, base_dir))
None => None
}
_ => None
}
}
///|
/// Resolve a markdown image target against the document's directory. Absolute
/// paths, `data:`/`http:`/`https:` URIs and absolute POSIX paths are returned
/// unchanged; relative paths are joined to `base_dir` (the directory of the
/// open `.md` file) so the renderer — which resolves `DrawImage` sources
/// against the process cwd — can find images sitting next to the document.
fn markdown_resolve_image_source(target : String, base_dir : String?) -> String {
match base_dir {
Some(dir) =>
if dir == "" || markdown_target_is_absolute(target) {
target
} else {
markdown_join_path(dir, target)
}
None => target
}
}
///|
/// True for sources that should not be re-rooted: data/URL URIs and absolute
/// POSIX/Windows paths.
fn markdown_target_is_absolute(target : String) -> Bool {
target.has_prefix("data:") ||
target.has_prefix("http://") ||
target.has_prefix("https://") ||
target.has_prefix("file://") ||
target.has_prefix("/") ||
target.has_prefix("\\") ||
(target.length() >= 2 && target[1] == ':')
}
///|
/// Join a directory and a relative path with a single `/` separator. Does not
/// canonicalize `..` segments; the renderer/host fs resolves them.
fn markdown_join_path(dir : String, rel : String) -> String {
let dir_chars = dir.to_array()
let has_slash = dir_chars.length() > 0 &&
(
dir_chars[dir_chars.length() - 1] == '/' ||
dir_chars[dir_chars.length() - 1] == '\\'
)
if has_slash {
dir + rel
} else {
dir + "/" + rel
}
}
///|
fn markdown_inline_image_size(
inline : MarkdownInline,
base : @core.FontSpec,
) -> @core.Size? {
match inline.kind {
Image =>
Some(@core.Size::new(width=base.size * 8.0, height=base.size * 4.5))
_ => None
}
}
///|
fn markdown_inline_source_text(inline : MarkdownInline) -> String {
if inline.kind is HtmlInline {
return markdown_html_inline_source(inline.text)
}
if inline.kind is FootnoteReference {
return "[^" + inline.text + "]"
}
if inline.text == "" {
"image"
} else {
inline.text
}
}
///|
fn markdown_inline_font(
kind : MarkdownInlineKind,
font : @core.FontSpec,
) -> @core.FontSpec {
match kind {
Bold => { ..font, weight: 700 }
Italic => { ..font, weight: 500 }
Strikethrough => { ..font, weight: 400 }
Link | Autolink => { ..font, weight: 500 }
Image => { ..font, weight: 600 }
FootnoteReference => { ..font, size: font.size * 0.76, weight: 700 }
HtmlInline =>
{
..font,
families: @core.FontFamilyStack::named("SFMono-Regular")
.with_fallback(@core.FontFamily::UiMonospace)
.with_fallback(@core.FontFamily::Monospace),
size: font.size * 0.9,
}
Code =>
{
..font,
families: @core.FontFamilyStack::named("SFMono-Regular")
.with_fallback(@core.FontFamily::UiMonospace)
.with_fallback(@core.FontFamily::Monospace),
size: font.size * 0.92,
}
Plain => font
}
}
///|
/// Inline run colors. Links/autolinks render in Typora's blue so they read as
/// actionable; everything else inherits the block foreground (None).
fn markdown_inline_color(kind : MarkdownInlineKind) -> @core.Color? {
match kind {
Link | Autolink => Some(@core.Color::rgba(r=0.13, g=0.45, b=0.94, a=1.0))
_ => None
}
}
///|
fn markdown_inline_background(kind : MarkdownInlineKind) -> @core.Brush? {
match kind {
Code => Some(@core.Brush::solid(@core.Color::black().multiply_alpha(0.08)))
Image =>
Some(@core.Brush::solid(@core.Color::rgba(r=0.18, g=0.4, b=0.7, a=0.08)))
FootnoteReference =>
Some(@core.Brush::solid(@core.Color::rgba(r=0.18, g=0.4, b=0.7, a=0.08)))
HtmlInline =>
Some(@core.Brush::solid(@core.Color::black().multiply_alpha(0.06)))
Plain | Bold | Italic | Strikethrough | Link | Autolink => None
}
}
///|
fn markdown_inline_decoration(kind : MarkdownInlineKind) -> RichTextDecoration? {
match kind {
Strikethrough => Some(RichTextDecoration::Strikethrough)
Plain
| Bold
| Italic
| Code
| Link
| Autolink
| Image
| FootnoteReference
| HtmlInline => None
}
}