///|
/// A deliberately small HTML subset for Markdown presentation blocks. Raw
/// HTML remains raw by default; this only recognizes a closed `div`/`center`
/// container whose body consists entirely of `img` and `br` tags.
priv struct MarkdownHtmlImageGallery {
runs : Array[RichTextRun]
line_align : @core.TextAlign
}
///|
priv struct MarkdownHtmlImageGalleryAttribute {
name : String
value : String?
}
///|
priv struct MarkdownHtmlImageGalleryTag {
name : String
attributes : Array[MarkdownHtmlImageGalleryAttribute]
raw : String
closing : Bool
self_closing : Bool
}
///|
priv struct MarkdownHtmlImageGalleryTagParseResult {
tag : MarkdownHtmlImageGalleryTag
end : Int
}
///|
priv enum MarkdownHtmlImageGalleryDimension {
Pixels(Double)
Fraction(Double)
}
///|
fn markdown_html_image_gallery(
html : String,
base : @core.FontSpec,
base_dir : String?,
) -> MarkdownHtmlImageGallery? {
let chars = html.to_array()
let start = markdown_html_image_gallery_skip_space(chars, 0)
match markdown_html_image_gallery_tag_at(chars, start) {
Some(opening) => {
let outer = opening.tag
if outer.closing ||
outer.self_closing ||
(outer.name != "div" && outer.name != "center") {
return None
}
let line_align = match
markdown_html_image_gallery_container_align(outer) {
Some(align) => align
None => return None
}
let runs : Array[RichTextRun] = []
let mut image_count = 0
let mut index = opening.end
while index < chars.length() {
index = markdown_html_image_gallery_skip_space(chars, index)
match markdown_html_image_gallery_tag_at(chars, index) {
Some(parsed) => {
let tag = parsed.tag
index = parsed.end
if tag.closing {
if tag.name != outer.name ||
image_count == 0 ||
markdown_html_image_gallery_skip_space(chars, index) !=
chars.length() {
return None
}
return Some({ runs, line_align })
}
match tag.name {
"img" =>
match
markdown_html_image_gallery_image_run(tag, base, base_dir) {
Some(run) => {
runs.push(run)
image_count = image_count + 1
}
None => return None
}
"br" =>
runs.push(
RichTextRun::new(
text=tag.raw,
visual_text=Some("\n"),
font=Some(base),
),
)
_ => return None
}
}
None => return None
}
} nobreak {
None
}
}
None => None
}
}
///|
fn markdown_html_image_gallery_container_align(
tag : MarkdownHtmlImageGalleryTag,
) -> @core.TextAlign? {
if tag.name == "center" {
return Some(TextCenter)
}
match markdown_html_image_gallery_attribute(tag.attributes, "align") {
None => Some(@core.TextAlign::TextStart)
Some(value) =>
match value.trim().to_owned().to_lower() {
"center" => Some(TextCenter)
"right" => Some(TextEnd)
"left" => Some(TextStart)
_ => None
}
}
}
///|
fn markdown_html_image_gallery_image_run(
tag : MarkdownHtmlImageGalleryTag,
base : @core.FontSpec,
base_dir : String?,
) -> RichTextRun? {
match markdown_html_image_gallery_attribute(tag.attributes, "src") {
Some(source) if source.trim().to_owned() != "" => {
let alt = markdown_html_image_gallery_attribute(tag.attributes, "alt").unwrap_or(
"",
)
let (image_size, image_width_fraction) = markdown_html_image_gallery_image_size(
tag.attributes,
base,
)
Some(
RichTextRun::new(
text=tag.raw,
visual_text=Some(alt),
image_source=Some(markdown_resolve_image_source(source, base_dir)),
image_size=Some(image_size),
image_width_fraction~,
font=Some({ ..base, weight: 600 }),
),
)
}
_ => None
}
}
///|
fn markdown_html_image_gallery_image_size(
attributes : Array[MarkdownHtmlImageGalleryAttribute],
base : @core.FontSpec,
) -> (@core.Size, Double?) {
let fallback = @core.Size::new(width=base.size * 8.0, height=base.size * 4.5)
let width = match markdown_html_image_gallery_attribute(attributes, "width") {
Some(value) => markdown_html_image_gallery_dimension(value)
None => None
}
let height = match
markdown_html_image_gallery_attribute(attributes, "height") {
Some(value) => markdown_html_image_gallery_dimension(value)
None => None
}
let aspect = fallback.width / fallback.height
match (width, height) {
(Some(Pixels(width)), Some(Pixels(height))) =>
(@core.Size::new(width~, height~), None)
(Some(Pixels(width)), _) =>
(@core.Size::new(width~, height=width / aspect), None)
(Some(Fraction(fraction)), Some(Pixels(height))) =>
(@core.Size::new(width=fallback.width, height~), Some(fraction))
(Some(Fraction(fraction)), _) => (fallback, Some(fraction))
(None, Some(Pixels(height))) =>
(@core.Size::new(width=height * aspect, height~), None)
(None, Some(Fraction(_))) | (None, None) => (fallback, None)
}
}
///|
fn markdown_html_image_gallery_dimension(
source : String,
) -> MarkdownHtmlImageGalleryDimension? {
let value = source.trim().to_owned()
if value == "" {
return None
}
let chars = value.to_array()
let percent = chars.length() > 1 && chars[chars.length() - 1] == '%'
let unit_length = if percent {
1
} else if value.to_lower().has_suffix("px") {
2
} else {
0
}
let number_end = chars.length() - unit_length
match markdown_html_image_gallery_decimal(chars, number_end) {
Some(number) if number > 0.0 =>
if percent {
Some(Fraction(min_double(1.0, number / 100.0)))
} else {
Some(Pixels(number))
}
_ => None
}
}
///|
fn markdown_html_image_gallery_decimal(
chars : Array[Char],
end : Int,
) -> Double? {
if end <= 0 {
return None
}
let mut result = 0.0
let mut fraction = 0.1
let mut decimal = false
let mut digit_count = 0
for index in 0.. {
digit_count = digit_count + 1
if decimal {
result = result + digit.to_double() * fraction
fraction = fraction / 10.0
} else {
result = result * 10.0 + digit.to_double()
}
}
None => return None
}
}
}
if digit_count == 0 {
None
} else {
Some(result)
}
}
///|
fn markdown_html_image_gallery_digit(ch : Char) -> Int? {
match ch {
'0' => Some(0)
'1' => Some(1)
'2' => Some(2)
'3' => Some(3)
'4' => Some(4)
'5' => Some(5)
'6' => Some(6)
'7' => Some(7)
'8' => Some(8)
'9' => Some(9)
_ => None
}
}
///|
fn markdown_html_image_gallery_attribute(
attributes : Array[MarkdownHtmlImageGalleryAttribute],
name : String,
) -> String? {
for attribute in attributes {
if attribute.name == name {
return attribute.value
}
}
None
}
///|
fn markdown_html_image_gallery_tag_at(
chars : Array[Char],
start : Int,
) -> MarkdownHtmlImageGalleryTagParseResult? {
if start < 0 || start >= chars.length() || chars[start] != '<' {
return None
}
let mut index = start + 1
let mut closing = false
if index < chars.length() && chars[index] == '/' {
closing = true
index = index + 1
}
let name_start = index
if index >= chars.length() ||
!markdown_html_image_gallery_tag_name_start(chars[index]) {
return None
}
index = index + 1
while index < chars.length() &&
markdown_html_image_gallery_tag_name_char(chars[index]) {
index = index + 1
}
let name = String::from_array(chars[name_start:index]).to_lower()
let attributes : Array[MarkdownHtmlImageGalleryAttribute] = []
let mut self_closing = false
while index < chars.length() {
index = markdown_html_image_gallery_skip_space(chars, index)
if index >= chars.length() {
return None
}
if chars[index] == '>' {
let end = index + 1
return Some({
tag: {
name,
attributes,
raw: String::from_array(chars[start:end]),
closing,
self_closing,
},
end,
})
}
if chars[index] == '/' &&
index + 1 < chars.length() &&
chars[index + 1] == '>' {
self_closing = true
let end = index + 2
return Some({
tag: {
name,
attributes,
raw: String::from_array(chars[start:end]),
closing,
self_closing,
},
end,
})
}
if closing || !markdown_html_image_gallery_attribute_name_char(chars[index]) {
return None
}
let attribute_start = index
index = index + 1
while index < chars.length() &&
markdown_html_image_gallery_attribute_name_char(chars[index]) {
index = index + 1
}
let attribute_name = String::from_array(chars[attribute_start:index]).to_lower()
index = markdown_html_image_gallery_skip_space(chars, index)
let mut value : String? = None
if index < chars.length() && chars[index] == '=' {
index = markdown_html_image_gallery_skip_space(chars, index + 1)
if index >= chars.length() {
return None
}
if chars[index] == '"' || chars[index] == '\'' {
let quote = chars[index]
let value_start = index + 1
index = value_start
while index < chars.length() && chars[index] != quote {
index = index + 1
}
if index >= chars.length() {
return None
}
value = Some(String::from_array(chars[value_start:index]))
index = index + 1
} else {
let value_start = index
while index < chars.length() &&
!markdown_html_image_gallery_space(chars[index]) &&
chars[index] != '>' &&
!(chars[index] == '/' &&
index + 1 < chars.length() &&
chars[index + 1] == '>') {
index = index + 1
}
if value_start == index {
return None
}
value = Some(String::from_array(chars[value_start:index]))
}
}
attributes.push({ name: attribute_name, value })
}
None
}
///|
fn markdown_html_image_gallery_skip_space(
chars : Array[Char],
start : Int,
) -> Int {
let mut index = start
while index < chars.length() &&
markdown_html_image_gallery_space(chars[index]) {
index = index + 1
}
index
}
///|
fn markdown_html_image_gallery_space(ch : Char) -> Bool {
ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'
}
///|
fn markdown_html_image_gallery_tag_name_start(ch : Char) -> Bool {
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
}
///|
fn markdown_html_image_gallery_tag_name_char(ch : Char) -> Bool {
markdown_html_image_gallery_tag_name_start(ch) ||
(ch >= '0' && ch <= '9') ||
ch == '-' ||
ch == ':'
}
///|
fn markdown_html_image_gallery_attribute_name_char(ch : Char) -> Bool {
markdown_html_image_gallery_tag_name_char(ch) || ch == '_'
}