///|
/// Parse `{#id .class key=value}` into a normalized attribute array.
fn parse_markdown_attributes(source : String) -> Array[MarkdownAttribute]? {
let text = source.trim(chars=" \t\r\n").to_owned()
guard text.length() >= 2 &&
text.unsafe_get(0) == '{' &&
text.unsafe_get(text.length() - 1) == '}' else {
return None
}
let attributes : Array[MarkdownAttribute] = []
let mut i = 1
let end = text.length() - 1
while i < end {
while i < end && is_space_or_tab(text.unsafe_get(i)) {
i = i + 1
}
if i >= end {
break
}
let prefix = text.unsafe_get(i)
if prefix == '#' || prefix == '.' {
i = i + 1
let start = i
while i < end && !is_space_or_tab(text.unsafe_get(i)) {
i = i + 1
}
guard i > start else { return None }
attributes.push({
name: if prefix == '#' {
"id"
} else {
"class"
},
value: text.unsafe_substring(start~, end=i),
})
continue
}
let name_start = i
while i < end {
let c = text.unsafe_get(i)
if c == '=' || is_space_or_tab(c) {
break
}
guard is_ascii_alnum_unit(c) || c == '-' || c == '_' || c == ':' else {
return None
}
i = i + 1
}
guard i > name_start else { return None }
let name = text.unsafe_substring(start=name_start, end=i)
while i < end && is_space_or_tab(text.unsafe_get(i)) {
i = i + 1
}
let mut value = ""
if i < end && text.unsafe_get(i) == '=' {
i = i + 1
while i < end && is_space_or_tab(text.unsafe_get(i)) {
i = i + 1
}
guard i < end else { return None }
let quote = text.unsafe_get(i)
if quote == '"' || quote == '\'' {
i = i + 1
let value_start = i
while i < end && text.unsafe_get(i) != quote {
i = i + 1
}
guard i < end else { return None }
value = text.unsafe_substring(start=value_start, end=i)
i = i + 1
} else {
let value_start = i
while i < end && !is_space_or_tab(text.unsafe_get(i)) {
i = i + 1
}
guard i > value_start else { return None }
value = text.unsafe_substring(start=value_start, end=i)
}
}
attributes.push({ name, value })
}
if attributes.is_empty() {
None
} else {
Some(attributes)
}
}
///|
/// Parse `:name[label]{attributes}` starting at `start`.
/// The attribute list is optional; nested Markdown in the label is left to
/// higher-level directive handlers rather than being interpreted here.
fn parse_inline_directive(text : String, start : Int) -> (Inline, Int)? {
let len = text.length()
guard start + 2 < len && text.unsafe_get(start) == ':' else { return None }
let mut i = start + 1
let name_start = i
while i < len {
let c = text.unsafe_get(i)
if !is_ascii_alnum_unit(c) && c != '-' && c != '_' {
break
}
i = i + 1
}
guard i > name_start && i < len && text.unsafe_get(i) == '[' else {
return None
}
let name = text.unsafe_substring(start=name_start, end=i)
i = i + 1
let label_start = i
let mut escaped = false
while i < len {
let c = text.unsafe_get(i)
guard c != '\n' && c != '\r' else { return None }
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == ']' {
break
}
i = i + 1
}
guard i < len && text.unsafe_get(i) == ']' else { return None }
let label_source = text.unsafe_substring(start=label_start, end=i)
let label_buf = StringBuilder()
let mut j = 0
while j < label_source.length() {
if label_source.unsafe_get(j) == '\\' &&
j + 1 < label_source.length() &&
(
label_source.unsafe_get(j + 1) == ']' ||
label_source.unsafe_get(j + 1) == '\\'
) {
j = j + 1
}
label_buf.write_string(label_source.unsafe_substring(start=j, end=j + 1))
j = j + 1
}
i = i + 1
let mut attributes : Array[MarkdownAttribute] = []
if i < len && text.unsafe_get(i) == '{' {
let attr_start = i
while i < len && text.unsafe_get(i) != '}' && text.unsafe_get(i) != '\n' {
i = i + 1
}
guard i < len && text.unsafe_get(i) == '}' else { return None }
let attr_source = text.unsafe_substring(start=attr_start, end=i + 1)
guard parse_markdown_attributes(attr_source) is Some(parsed) else {
return None
}
attributes = parsed
i = i + 1
}
Some(
(
Inline::Directive(
name~,
label=label_buf.to_string(),
attributes~,
span=Span::new(start, i),
),
i,
),
)
}