///|
/// Presentation-only projection of raw HTML container blocks, registered as
/// a `MarkdownBlockView` contribution.
///
/// Raw HTML stays raw by default. This view only recognizes `div`/`center`
/// containers whose children are all in a narrow displayable subset:
/// images (optionally wrapped in a single anchor), line breaks, comments,
/// ` `-style spacing, paragraphs, headings, spans, plain text, and
/// nested containers (flattened into rows). Everything else — tables,
/// lists, unknown tags, malformed structure — declines so the block keeps
/// its raw source presentation. The canonical block source remains the
/// editable truth; this module never rewrites it.
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)
}
///|
/// Mutable walker state shared while projecting one container block.
priv struct MarkdownHtmlImageGalleryWalk {
chars : Array[Char]
index : Ref[Int]
base : @core.FontSpec
base_dir : String?
runs : Array[RichTextRun]
line_open : Ref[Bool]
invalid : Ref[Bool]
}
///|
pub let markdown_html_image_gallery_view : MarkdownBlockView = {
name: "html-image-gallery",
matches: markdown_html_image_gallery_signature,
project: (block, context) => {
markdown_html_image_gallery_project(
block.text,
context.base,
context.base_dir,
)
},
}
///|
/// Cheap candidacy signature: first tag of an HTML block must be an opened
/// `div`/`center`. No body parsing happens here so the height walk can
/// probe every block through the registry.
fn markdown_html_image_gallery_signature(block : MarkdownBlock) -> Bool {
match block.kind {
HtmlBlock => {
let chars = block.text.to_array()
match
markdown_html_image_gallery_tag_at(
chars,
markdown_html_image_gallery_skip_space(chars, 0),
) {
Some(parsed) =>
!parsed.tag.closing &&
(parsed.tag.name == "div" || parsed.tag.name == "center")
None => false
}
}
_ => false
}
}
///|
fn markdown_html_image_gallery_project(
html : String,
base : @core.FontSpec,
base_dir : String?,
) -> MarkdownBlockViewProjection? {
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 walk : MarkdownHtmlImageGalleryWalk = {
chars,
index: Ref(opening.end),
base,
base_dir,
runs: [],
line_open: Ref(false),
invalid: Ref(false),
}
markdown_html_image_gallery_walk_body(walk, outer.name)
if !walk.invalid.val {
markdown_html_image_gallery_walk_tail(walk)
markdown_html_image_gallery_strip_trailing_newline(walk)
}
if walk.invalid.val || walk.runs.length() == 0 {
None
} else {
Some({ runs: walk.runs, line_align: Some(line_align), })
}
}
None => None
}
}
///|
/// Walk a container body until its matching closing tag is consumed,
/// marking the walk invalid on any construct outside the displayable
/// subset.
fn markdown_html_image_gallery_walk_body(
walk : MarkdownHtmlImageGalleryWalk,
closing : String,
) -> Unit {
let chars = walk.chars
while !walk.invalid.val && walk.index.val < chars.length() {
walk.index.val = markdown_html_image_gallery_skip_space(
chars,
walk.index.val,
)
if walk.index.val >= chars.length() {
break
}
match markdown_html_image_gallery_comment_end(chars, walk.index.val) {
Some(end) => {
walk.index.val = end
continue
}
None => ()
}
if chars[walk.index.val] != '<' {
markdown_html_image_gallery_read_text(walk)
continue
}
let element_start = walk.index.val
match markdown_html_image_gallery_tag_at(chars, walk.index.val) {
None => walk.invalid.val = true
Some(parsed) => {
let tag = parsed.tag
walk.index.val = parsed.end
if tag.closing {
if tag.name == closing {
return
}
walk.invalid.val = true
} else {
match tag.name {
"img" => markdown_html_image_gallery_walk_image(walk, tag)
"br" => {
walk.runs.push(
RichTextRun::new(
text=tag.raw,
visual_text=Some("\n"),
font=Some(walk.base),
),
)
walk.line_open.val = false
}
"div" | "center" =>
if tag.self_closing {
walk.invalid.val = true
} else {
markdown_html_image_gallery_break_before(walk)
markdown_html_image_gallery_walk_body(walk, tag.name)
markdown_html_image_gallery_break_before(walk)
}
"a" => markdown_html_image_gallery_walk_anchor(walk)
"p" =>
markdown_html_image_gallery_block_text(
walk,
element_start,
tag.name,
walk.base,
true,
)
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" =>
markdown_html_image_gallery_block_text(
walk,
element_start,
tag.name,
{ ..walk.base, size: walk.base.size * 1.15, weight: 700, },
false,
)
"span" => markdown_html_image_gallery_span_text(walk, element_start)
_ => walk.invalid.val = true
}
}
}
}
}
if !walk.invalid.val {
// Reached the end of the block without the container's own closing
// tag: the structure is malformed, so the block stays raw.
walk.invalid.val = true
}
}
///|
/// Walk what follows the first closed container: trailing text and sibling
/// containers remain displayable; anything else keeps the block raw.
fn markdown_html_image_gallery_walk_tail(
walk : MarkdownHtmlImageGalleryWalk,
) -> Unit {
let chars = walk.chars
while !walk.invalid.val && walk.index.val < chars.length() {
walk.index.val = markdown_html_image_gallery_skip_space(
chars,
walk.index.val,
)
if walk.index.val >= chars.length() {
break
}
match markdown_html_image_gallery_comment_end(chars, walk.index.val) {
Some(end) => {
walk.index.val = end
continue
}
None => ()
}
if chars[walk.index.val] != '<' {
markdown_html_image_gallery_read_text(walk)
continue
}
match markdown_html_image_gallery_tag_at(chars, walk.index.val) {
None => walk.invalid.val = true
Some(parsed) => {
let tag = parsed.tag
walk.index.val = parsed.end
if !tag.closing &&
!tag.self_closing &&
(tag.name == "div" || tag.name == "center") {
markdown_html_image_gallery_break_before(walk)
markdown_html_image_gallery_walk_body(walk, tag.name)
markdown_html_image_gallery_break_before(walk)
} else {
walk.invalid.val = true
}
}
}
}
}
///|
fn markdown_html_image_gallery_walk_image(
walk : MarkdownHtmlImageGalleryWalk,
tag : MarkdownHtmlImageGalleryTag,
) -> Unit {
match markdown_html_image_gallery_image_run(tag, walk.base, walk.base_dir) {
Some(run) => {
walk.runs.push(run)
walk.line_open.val = true
}
None => walk.invalid.val = true
}
}
///|
/// An anchor is displayable only when it wraps a single image (badge rows).
fn markdown_html_image_gallery_walk_anchor(
walk : MarkdownHtmlImageGalleryWalk,
) -> Unit {
let chars = walk.chars
while !walk.invalid.val {
walk.index.val = markdown_html_image_gallery_skip_space(
chars,
walk.index.val,
)
if walk.index.val >= chars.length() {
walk.invalid.val = true
return
}
match markdown_html_image_gallery_comment_end(chars, walk.index.val) {
Some(end) => {
walk.index.val = end
continue
}
None => ()
}
match markdown_html_image_gallery_tag_at(chars, walk.index.val) {
None => {
walk.invalid.val = true
return
}
Some(parsed) => {
let tag = parsed.tag
walk.index.val = parsed.end
if tag.closing {
if tag.name == "a" {
return
}
walk.invalid.val = true
return
}
if tag.name != "img" {
walk.invalid.val = true
return
}
markdown_html_image_gallery_walk_image(walk, tag)
}
}
}
}
///|
fn markdown_html_image_gallery_read_text(
walk : MarkdownHtmlImageGalleryWalk,
) -> Unit {
let chars = walk.chars
let start = walk.index.val
while walk.index.val < chars.length() && chars[walk.index.val] != '<' {
walk.index.val = walk.index.val + 1
}
if walk.index.val == start {
return
}
if markdown_html_image_gallery_space_entity_only(chars, start, walk.index.val) {
if walk.line_open.val {
markdown_html_image_gallery_push_inline(
walk,
String::from_array(chars[start:walk.index.val]),
" ",
walk.base,
None,
)
}
return
}
match markdown_html_image_gallery_decode_text(chars, start, walk.index.val) {
Some(visual) => {
markdown_html_image_gallery_break_before(walk)
markdown_html_image_gallery_push_inline(
walk,
String::from_array(chars[start:walk.index.val]),
visual,
walk.base,
Some(@core.Color::black().multiply_alpha(0.78)),
)
markdown_html_image_gallery_push_newline(walk)
}
None => ()
}
}
///|
fn markdown_html_image_gallery_block_text(
walk : MarkdownHtmlImageGalleryWalk,
element_start : Int,
name : String,
font : @core.FontSpec,
dim : Bool,
) -> Unit {
let color = if dim {
Some(@core.Color::black().multiply_alpha(0.78))
} else {
None
}
// Fast path: a pure-text paragraph/heading projects as one run covering
// the whole element, which keeps the source mapping exact. Rich content
// (nested anchors/images) falls through to the inline walker below.
match markdown_html_image_gallery_inline_content(walk, name) {
(true, None) => ()
(true, Some(visual)) => {
markdown_html_image_gallery_break_before(walk)
markdown_html_image_gallery_push_inline(
walk,
String::from_array(walk.chars[element_start:walk.index.val]),
visual,
font,
color,
)
markdown_html_image_gallery_push_newline(walk)
}
(false, _) => {
markdown_html_image_gallery_break_before(walk)
let before = walk.runs.length()
if !markdown_html_image_gallery_walk_inline_until(walk, name, font, color) {
walk.invalid.val = true
return
}
if walk.runs.length() > before {
markdown_html_image_gallery_push_newline(walk)
}
}
}
}
///|
fn markdown_html_image_gallery_span_text(
walk : MarkdownHtmlImageGalleryWalk,
element_start : Int,
) -> Unit {
let color = Some(@core.Color::black().multiply_alpha(0.78))
match markdown_html_image_gallery_inline_content(walk, "span") {
(true, None) => ()
(true, Some(visual)) =>
markdown_html_image_gallery_push_inline(
walk,
String::from_array(walk.chars[element_start:walk.index.val]),
visual,
walk.base,
color,
)
(false, _) =>
if !markdown_html_image_gallery_walk_inline_until(
walk,
"span",
walk.base,
color,
) {
walk.invalid.val = true
}
}
}
///|
/// Walk the inline children of an opened `closing` element, pushing text,
/// images, anchors and line breaks as runs. Returns `false` — declining the
/// whole container — on any construct outside the displayable subset.
fn markdown_html_image_gallery_walk_inline_until(
walk : MarkdownHtmlImageGalleryWalk,
closing : String,
font : @core.FontSpec,
color : @core.Color?,
) -> Bool {
let chars = walk.chars
while walk.index.val < chars.length() {
match markdown_html_image_gallery_comment_end(chars, walk.index.val) {
Some(end) => {
walk.index.val = end
continue
}
None => ()
}
if chars[walk.index.val] != '<' {
let start = walk.index.val
while walk.index.val < chars.length() && chars[walk.index.val] != '<' {
walk.index.val = walk.index.val + 1
}
match
markdown_html_image_gallery_decode_text_inline(
chars,
start,
walk.index.val,
) {
None => ()
Some(visual) =>
// A whitespace-only segment only becomes a run mid-line, so a
// paragraph's own indentation never leaks a leading space.
if visual != " " || walk.line_open.val {
markdown_html_image_gallery_push_inline(
walk,
String::from_array(chars[start:walk.index.val]),
visual,
font,
color,
)
}
}
continue
}
let tag_start = walk.index.val
match markdown_html_image_gallery_tag_at(chars, tag_start) {
None => return false
Some(parsed) => {
let tag = parsed.tag
walk.index.val = parsed.end
if tag.closing {
return tag.name == closing
}
match tag.name {
"img" =>
match
markdown_html_image_gallery_image_run(
tag,
walk.base,
walk.base_dir,
) {
Some(run) => {
walk.runs.push(run)
walk.line_open.val = true
}
None => return false
}
"br" => {
walk.runs.push(
RichTextRun::new(
text=tag.raw,
visual_text=Some("\n"),
font=Some(walk.base),
),
)
walk.line_open.val = false
}
"a" =>
if !markdown_html_image_gallery_walk_inline_anchor(
walk, tag_start, font, color,
) {
return false
}
"span" | "strong" | "b" | "em" | "i" | "code" =>
if !markdown_html_image_gallery_walk_inline_until(
walk,
tag.name,
font,
color,
) {
return false
}
_ => return false
}
}
}
}
false
}
///|
/// Inline anchor: displayable only when it wraps either a single image or
/// plain text (badge rows and inline links). Mixed content declines so the
/// container keeps its raw source presentation.
fn markdown_html_image_gallery_walk_inline_anchor(
walk : MarkdownHtmlImageGalleryWalk,
element_start : Int,
font : @core.FontSpec,
color : @core.Color?,
) -> Bool {
let chars = walk.chars
let inner_start = walk.index.val
let mut has_text = false
let mut has_image = false
let mut image_run : RichTextRun? = None
while walk.index.val < chars.length() {
match markdown_html_image_gallery_comment_end(chars, walk.index.val) {
Some(end) => {
walk.index.val = end
continue
}
None => ()
}
if chars[walk.index.val] != '<' {
let start = walk.index.val
while walk.index.val < chars.length() && chars[walk.index.val] != '<' {
walk.index.val = walk.index.val + 1
}
if markdown_html_image_gallery_decode_text_inline(
chars,
start,
walk.index.val,
)
is Some(_) {
has_text = true
}
continue
}
let tag_start = walk.index.val
match markdown_html_image_gallery_tag_at(chars, tag_start) {
None => return false
Some(parsed) => {
let tag = parsed.tag
walk.index.val = parsed.end
if tag.closing {
if tag.name != "a" {
return false
}
if has_image && !has_text {
match image_run {
Some(run) => {
walk.runs.push(run)
walk.line_open.val = true
return true
}
None => return false
}
}
if has_text && !has_image {
match
markdown_html_image_gallery_decode_text_inline(
chars, inner_start, tag_start,
) {
None => ()
Some(visual) =>
if visual != " " || walk.line_open.val {
markdown_html_image_gallery_push_inline(
walk,
String::from_array(chars[element_start:walk.index.val]),
visual,
font,
color,
)
}
}
return true
}
return false
}
if tag.name == "img" && !has_text && !has_image {
match
markdown_html_image_gallery_image_run(tag, walk.base, walk.base_dir) {
Some(run) => {
image_run = Some(run)
has_image = true
}
None => return false
}
} else {
return false
}
}
}
}
false
}
///|
/// Inline text decoding: collapses whitespace runs to single spaces, keeps a
/// single trailing space so adjacent inline elements stay separated, and
/// returns `Some(" ")` for a whitespace-only segment (the caller drops it at
/// line start).
fn markdown_html_image_gallery_decode_text_inline(
chars : Array[Char],
start : Int,
end : Int,
) -> String? {
let output = StringBuilder()
let mut index = start
let mut pending_space = false
let mut started = false
while index < end {
let ch = chars[index]
if markdown_html_image_gallery_space(ch) {
if started {
pending_space = true
}
index = index + 1
} else {
let (text, next) = match
markdown_html_image_gallery_entity_at(chars, index, end) {
Some((text, next)) => (text, next)
None => (String::from_array([ch]), index + 1)
}
if text == " " {
if started {
pending_space = true
}
} else {
if pending_space {
output.write_string(" ")
pending_space = false
}
output.write_string(text)
started = true
}
index = next
}
}
if !started {
return if start < end { Some(" ") } else { None }
}
if pending_space {
output.write_string(" ")
}
Some(output.to_string())
}
///|
/// Collect the plain-text content of an opened `name` element up to its
/// closing tag. Any nested markup inside declines the element.
fn markdown_html_image_gallery_inline_content(
walk : MarkdownHtmlImageGalleryWalk,
name : String,
) -> (Bool, String?) {
let chars = walk.chars
let start = walk.index.val
let mut index = start
while index < chars.length() {
if chars[index] == '<' {
if markdown_html_image_gallery_ci_at(chars, index, "" + name) {
let mut cursor = index + 2 + name.length()
while cursor < chars.length() && chars[cursor] != '>' {
cursor = cursor + 1
}
if cursor >= chars.length() {
return (false, None)
}
walk.index.val = cursor + 1
return (
true,
markdown_html_image_gallery_decode_text(chars, start, index),
)
}
return (false, None)
}
index = index + 1
}
(false, None)
}
///|
fn markdown_html_image_gallery_push_newline(
walk : MarkdownHtmlImageGalleryWalk,
) -> Unit {
walk.runs.push(
RichTextRun::new(text="", visual_text=Some("\n"), font=Some(walk.base)),
)
walk.line_open.val = false
}
///|
/// Drop the newline-only runs the flat projection appends after the last
/// block-level child. Those separators exist to keep interior rows apart, but
/// after the container's final child nothing follows to separate from, so the
/// walk's trailing `push_newline` would lay out as a full empty line that HTML
/// never renders there.
fn markdown_html_image_gallery_strip_trailing_newline(
walk : MarkdownHtmlImageGalleryWalk,
) -> Unit {
while walk.runs.length() > 0 {
let last = walk.runs[walk.runs.length() - 1]
if last.text == "" && last.visual_text == Some("\n") {
let _ = walk.runs.pop()
} else {
break
}
}
}
///|
fn markdown_html_image_gallery_break_before(
walk : MarkdownHtmlImageGalleryWalk,
) -> Unit {
if walk.line_open.val {
markdown_html_image_gallery_push_newline(walk)
}
}
///|
fn markdown_html_image_gallery_push_inline(
walk : MarkdownHtmlImageGalleryWalk,
raw : String,
visual : String,
font : @core.FontSpec,
color : @core.Color?,
) -> Unit {
let run = match color {
None =>
RichTextRun::new(text=raw, visual_text=Some(visual), font=Some(font))
Some(color) =>
RichTextRun::new(
text=raw,
visual_text=Some(visual),
font=Some(font),
color=Some(color),
)
}
walk.runs.push(run)
walk.line_open.val = true
}
///|
/// Decode HTML entities and collapse whitespace runs into single spaces.
/// Returns `None` when the segment carries no visible content.
fn markdown_html_image_gallery_decode_text(
chars : Array[Char],
start : Int,
end : Int,
) -> String? {
let output = StringBuilder()
let mut index = start
let mut pending_space = false
let mut visible = false
while index < end {
let ch = chars[index]
if markdown_html_image_gallery_space(ch) {
pending_space = true
index = index + 1
} else {
let (text, next) = match
markdown_html_image_gallery_entity_at(chars, index, end) {
Some((text, next)) => (text, next)
None => (String::from_array([ch]), index + 1)
}
if text == " " {
pending_space = true
} else {
if pending_space && visible {
output.write_string(" ")
}
pending_space = false
output.write_string(text)
visible = true
}
index = next
}
}
if visible {
Some(output.to_string())
} else {
None
}
}
///|
fn markdown_html_image_gallery_space_entity_only(
chars : Array[Char],
start : Int,
end : Int,
) -> Bool {
let mut index = start
while index < end {
let ch = chars[index]
if markdown_html_image_gallery_space(ch) {
index = index + 1
} else if ch == '&' {
match markdown_html_image_gallery_entity_at(chars, index, end) {
Some((" ", next)) => index = next
_ => return false
}
} else {
return false
}
}
true
}
///|
fn markdown_html_image_gallery_entity_at(
chars : Array[Char],
index : Int,
end : Int,
) -> (String, Int)? {
// chars[index] == '&'
let name_start = index + 1
let mut cursor = name_start
while cursor < end && markdown_html_image_gallery_entity_char(chars[cursor]) {
cursor = cursor + 1
}
if cursor >= end || chars[cursor] != ';' || cursor == name_start {
return None
}
let name = String::from_array(chars[name_start:cursor]).to_lower()
match name {
"nbsp" => Some((" ", cursor + 1))
"amp" => Some(("&", cursor + 1))
"lt" => Some(("<", cursor + 1))
"gt" => Some((">", cursor + 1))
"quot" => Some(("\"", cursor + 1))
"#39" => Some(("'", cursor + 1))
_ => None
}
}
///|
fn markdown_html_image_gallery_entity_char(ch : Char) -> Bool {
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '#'
}
///|
/// Skip an HTML comment at `index`, returning its end when a comment
/// starts there. An unterminated comment consumes the remaining text.
fn markdown_html_image_gallery_comment_end(
chars : Array[Char],
index : Int,
) -> Int? {
if index + 4 > chars.length() ||
chars[index] != '<' ||
chars[index + 1] != '!' ||
chars[index + 2] != '-' ||
chars[index + 3] != '-' {
return None
}
let mut cursor = index + 4
while cursor + 3 <= chars.length() {
if chars[cursor] == '-' &&
chars[cursor + 1] == '-' &&
chars[cursor + 2] == '>' {
return Some(cursor + 3)
}
cursor = cursor + 1
}
Some(chars.length())
}
///|
fn markdown_html_image_gallery_ci_at(
chars : Array[Char],
index : Int,
needle : String,
) -> Bool {
let needle_chars = needle.to_array()
if index + needle_chars.length() > chars.length() {
return false
}
for offset in 0.. Bool {
actual == want_lower ||
(
actual >= 'A' &&
actual <= 'Z' &&
actual.to_int() + 32 == want_lower.to_int()
)
}
///|
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(TextStart)
Some(value) =>
match value.trim().to_owned().to_lower() {
"center" => Some(TextCenter)
"right" => Some(TextEnd)
"left" => Some(TextStart)
_ => None
}
}
}
///|
/// Vertical breathing room added around every gallery row. The layout stacks
/// image rows at exactly their pixel height, so without this padding rows
/// (mascot, badge row, single badge) sit flush against each other; the extra
/// height letterboxes the image inside its (Contain-fitted) line frame.
let markdown_html_image_gallery_row_gap : Double = 12.0
///|
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 resolved = markdown_resolve_image_source(source, base_dir)
let (image_size, image_width_fraction) = markdown_html_image_gallery_image_size(
tag.attributes,
base,
resolved,
)
let image_size = @core.Size::new(
width=image_size.width,
height=image_size.height + markdown_html_image_gallery_row_gap,
)
Some(
RichTextRun::new(
text=tag.raw,
visual_text=Some(alt),
image_source=Some(resolved),
image_size=Some(image_size),
image_width_fraction~,
font=Some({ ..base, weight: 600, }),
),
)
}
_ => None
}
}
///|
/// Box size plus optional content-column fraction for one gallery image.
/// Explicit pixel attributes win; otherwise the intrinsic header size (when
/// the host can provide it) supplies the aspect ratio, so percentage-width
/// images keep their real shape instead of a placeholder 16:9 box.
fn markdown_html_image_gallery_image_size(
attributes : Array[MarkdownHtmlImageGalleryAttribute],
base : @core.FontSpec,
source : String,
) -> (@core.Size, Double?) {
let fallback = @core.Size::new(width=base.size * 8.0, height=base.size * 4.5)
let intrinsic = match markdown_image_metrics_size(source) {
Some(size) if size.width > 0.0 && size.height > 0.0 => Some(size)
_ => None
}
let aspect = match intrinsic {
Some(size) => size.width / size.height
None => fallback.width / fallback.height
}
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
}
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)), _) =>
(intrinsic.unwrap_or(fallback), Some(fraction))
(None, Some(Pixels(height))) =>
(@core.Size::new(width=height * aspect, height~), None)
(None, Some(Fraction(_))) | (None, None) =>
(intrinsic.unwrap_or(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 == '_'
}