///|
/// Replaces the minimal set of full-width punctuation with half-width
/// equivalents: `,` `。` `!` `?` `:` `;` `(` `)` `“”` `‘’` become `,`
/// `.` `!` `?` `:` `;` `(` `)` `"` `'`.
///
/// Converted punctuation is spaced apart from its neighbors: every converted
/// punctuation except the opening bracket `(` and the opening quotes `“` `‘`
/// gains a trailing space when a non-whitespace, non-punctuation character
/// follows, and the opening bracket and quotes gain a leading space when a
/// non-whitespace, non-punctuation character precedes them. Full-width
/// punctuation counts as punctuation for this purpose, so adjacent converted
/// punctuation stays tight: `你好(世界)!` renders as `你好 (世界)!`,
/// `(世界)。` renders as `(世界).`, while `a,!b` renders as `a, !b`. `prev`
/// and `next` supply the neighboring characters when `s` is rendered as part
/// of a longer run that starts or ends outside `s`.
///
/// A link destination `(url)` right after `](` is copied unchanged so
/// punctuation inside URLs is preserved. All other characters are kept
/// unchanged.
fn to_halfwidth_punct(
s : String,
prev~ : Char? = None,
next~ : Char? = None,
) -> String {
let chars = s.iter().to_array()
let buf = StringBuilder()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c == ']' && i + 1 < chars.length() && chars[i + 1] == '(' {
buf.write_char(']')
i += 1
while i < chars.length() {
let u = chars[i]
buf.write_char(u)
i += 1
if u == ')' {
break
}
}
continue
}
let converted = match c {
',' => ','
'。' => '.'
'!' => '!'
'?' => '?'
':' => ':'
';' => ';'
'(' => '('
')' => ')'
'“' | '”' => '"'
'‘' | '’' => '\''
_ => c
}
let is_open = c == '(' || c == '“' || c == '‘'
let lead_space = is_open && {
if i > 0 {
!chars[i - 1].is_whitespace() && !is_spacing_punct(chars[i - 1])
} else {
match prev {
Some(p) => !p.is_whitespace() && !is_spacing_punct(p)
None => false
}
}
}
if lead_space {
buf.write_char(' ')
}
buf.write_char(converted)
let trail_space = converted != c && !is_open && {
if i + 1 < chars.length() {
!chars[i + 1].is_whitespace() && !is_spacing_punct(chars[i + 1])
} else {
match next {
Some(n) => !n.is_whitespace() && !is_spacing_punct(n)
None => false
}
}
}
if trail_space {
buf.write_char(' ')
}
i += 1
}
buf.to_string()
}
///|
fn is_spacing_punct(c : Char) -> Bool {
match c {
',' | '。' | '!' | '?' | ':' | ';' | '(' | ')' | '“' | '”' |
'‘' | '’' => true
_ => false
}
}
///|
fn inline_first_char(inline : @parsing.Inline) -> Char? {
match inline {
Plain(text) => text.iter().next()
Strong(inlines) => array_first_char(inlines)
Emph(inlines) => array_first_char(inlines)
_ => None
}
}
///|
fn array_first_char(inlines : Array[@parsing.Inline]) -> Char? {
for i in inlines {
match inline_first_char(i) {
Some(c) => return Some(c)
None => ()
}
}
None
}
///|
fn inline_last_char(inline : @parsing.Inline) -> Char? {
match inline {
Plain(text) => text.iter().last()
Strong(inlines) => array_last_char(inlines)
Emph(inlines) => array_last_char(inlines)
_ => None
}
}
///|
fn array_last_char(inlines : Array[@parsing.Inline]) -> Char? {
for i = inlines.length() - 1; i >= 0; i = i - 1 {
match inline_last_char(inlines[i]) {
Some(c) => return Some(c)
None => ()
}
}
None
}
///|
let global_macros : @katex_parser.Macros = @katex_parser.Macros::make()
///|
pub(all) enum KatexTarget {
Unicode
Mathml
}
///|
fn render_math(latex : String, display : Bool, target~ : KatexTarget) -> String {
let settings = @katex_parser.Settings::make(
display_mode=display,
global_group=true,
macro_store=global_macros,
)
try {
let nodes = @katex_parser.parse(latex, settings~)
match target {
Unicode => @katex_unicode.render(nodes)
Mathml => @katex_mathml.render(nodes)
}
} catch {
_ => latex
}
}