///|
/// Named entities that labels may use, as `name:code point` pairs.
let named_packed = "amp:38,lt:60,gt:62,quot:34,apos:39,nbsp:160,shy:173,iexcl:161,cent:162,pound:163,curren:164,yen:165,sect:167,copy:169,laquo:171,not:172,reg:174,deg:176,plusmn:177,sup2:178,sup3:179,micro:181,para:182,middot:183,sup1:185,raquo:187,frac14:188,frac12:189,frac34:190,iquest:191,times:215,divide:247,ndash:8211,mdash:8212,lsquo:8216,rsquo:8217,sbquo:8218,ldquo:8220,rdquo:8221,bdquo:8222,dagger:8224,Dagger:8225,bull:8226,hellip:8230,permil:8240,prime:8242,Prime:8243,lsaquo:8249,rsaquo:8250,euro:8364,trade:8482,larr:8592,uarr:8593,rarr:8594,darr:8595,harr:8596,lArr:8656,uArr:8657,rArr:8658,dArr:8659,hArr:8660,forall:8704,part:8706,exist:8707,empty:8709,nabla:8711,isin:8712,notin:8713,ni:8715,prod:8719,sum:8721,minus:8722,radic:8730,infin:8734,and:8743,or:8744,cap:8745,cup:8746,int:8747,there4:8756,sim:8764,cong:8773,asymp:8776,ne:8800,equiv:8801,le:8804,ge:8805,sub:8834,sup:8835,sube:8838,supe:8839,oplus:8853,otimes:8855,perp:8869,sdot:8901,loz:9674,spades:9824,clubs:9827,hearts:9829,diams:9830,check:10003,cross:10007,AMP:38,LT:60,GT:62,QUOT:34,lsqb:91,rsqb:93,lcub:123,rcub:125,verbar:124,VerticalLine:124,midast:42,DiacriticalGrave:96,Hat:94,UnderBar:95,equals:61,colon:58,semi:59,num:35,percnt:37,lpar:40,rpar:41,lbrack:91,rbrack:93,lbrace:123,rbrace:125,vert:124,sol:47,bsol:92,quest:63,excl:33,ast:42,plus:43,comma:44,period:46,dollar:36,commat:64,grave:96,hat:94,lowbar:95,tilde:126,Alpha:913,Beta:914,Gamma:915,Delta:916,Epsilon:917,Zeta:918,Eta:919,Theta:920,Iota:921,Kappa:922,Lambda:923,Mu:924,Nu:925,Xi:926,Omicron:927,Pi:928,Rho:929,Sigma:931,Tau:932,Upsilon:933,Phi:934,Chi:935,Psi:936,Omega:937,alpha:945,beta:946,gamma:947,delta:948,epsilon:949,zeta:950,eta:951,theta:952,iota:953,kappa:954,lambda:955,mu:956,nu:957,xi:958,omicron:959,pi:960,rho:961,sigmaf:962,sigma:963,tau:964,upsilon:965,phi:966,chi:967,psi:968,omega:969"
///|
let named : Map[String, Int32] = {
let out = Map([])
for pair in named_packed.split(",") {
guard pair.split_once(":") is Some((name, code)) else { continue }
out[name.to_owned()] = @string.parse_int(code) catch { _ => 0 }
}
out
}
///|
fn from_code_point(n : Int32) -> String {
let out = StringBuilder()
out.write_char(Int32::unsafe_to_char(n))
out.to_string()
}
///|
/// The character of a numeric entity, or `None` for one that names no character.
fn code_point(digits : String, base : Int32) -> String? {
let mut value = 0
for i in 0..= 'a'.to_int() {
c - 'a'.to_int() + 10
} else {
c - 'A'.to_int() + 10
}
value = value * base + d
if value > 0x10ffff {
return None
}
}
if value > 0 && (value < 0xd800 || value > 0xdfff) {
Some(from_code_point(value))
} else {
None
}
}
///|
/// The entity starting with `&` at `at`: where it ends and its characters, or `None`.
/// Answers as `&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|(\w+));` does.
fn entity_at(text : String, at : Int32) -> (Int32, String)? {
let end_of = (from : Int32, accept : (Int32) -> Bool) => {
let mut i = from
while accept(@util.code_at(text, i)) {
i += 1
}
i
}
let terminated = (from : Int32, to : Int32) => {
to > from && @util.code_at(text, to) == ';'.to_int()
}
let whole = (to : Int32, value : String?) => {
match value {
Some(chars) => Some((to + 1, chars))
None => Some((to + 1, @util.slice(text, at, end=to + 1)))
}
}
if @util.code_at(text, at + 1) == '#'.to_int() {
let dec_end = end_of(at + 2, @util.is_digit)
if terminated(at + 2, dec_end) {
return whole(
dec_end,
code_point(@util.slice(text, at + 2, end=dec_end), 10),
)
}
let x = @util.code_at(text, at + 2)
if x == 'x'.to_int() || x == 'X'.to_int() {
let hex_end = end_of(at + 3, @util.is_hex)
if terminated(at + 3, hex_end) {
return whole(
hex_end,
code_point(@util.slice(text, at + 3, end=hex_end), 16),
)
}
}
return None
}
let name_end = end_of(at + 1, @util.is_word)
if !terminated(at + 1, name_end) {
return None
}
whole(
name_end,
named.get(@util.slice(text, at + 1, end=name_end)).map(from_code_point),
)
}
///|
/// Mermaid's placeholders run from `fl°` to `¶ß`, with a second `°` marking a numeric code.
/// Like Mermaid, they are turned back into entities, so that each code is decoded once.
fn restore_placeholders(text : String) -> String {
let out = StringBuilder()
let mut last = 0
let mut from = @util.index_of(text, "\u{fb02}\u{b0}")
while from != -1 {
let stop = @util.index_of(text, "\u{b6}", from=from + 2)
if stop == -1 {
break
}
if @util.code_at(text, stop + 1) == 0xdf {
let numeric = @util.code_at(text, from + 2) == 0xb0
@util.write_range(out, text, last, from)
out.write_string(if numeric { "" } else { "&" })
@util.write_range(out, text, from + (if numeric { 3 } else { 2 }), stop)
out.write_string(";")
last = stop + 2
}
from = @util.index_of(text, "\u{fb02}\u{b0}", from=stop + 1)
}
if last == 0 {
return text
}
@util.write_range(out, text, last, text.length())
out.to_string()
}
///|
/// The end of `fl°°?\w+¶ß` at `i`, or -1.
pub fn marker_end(text : String, i : Int32) -> Int32 {
if @util.code_at(text, i) != 0xfb02 || @util.code_at(text, i + 1) != '°' {
return -1
}
let mut k = i + 2
if @util.code_at(text, k) == '°' {
k += 1
}
let word = k
while @util.is_word(@util.code_at(text, k)) {
k += 1
}
if k > word && @util.starts_with_at(text, "¶ß", k) {
k + 2
} else {
-1
}
}
///|
/// `text` with each of Mermaid's placeholders written back as the `#name;` or `#123;` code it hides,
/// as `/fl°°?(\w+)¶ß/g` finds them.
pub fn written_codes(text : String) -> String {
if !text.contains("\u{b6}\u{df}") {
return text
}
let out = StringBuilder()
let mut last = 0
let mut from = @util.index_of(text, "\u{fb02}\u{b0}")
while from != -1 {
let end = marker_end(text, from)
if end != -1 {
let start = if @util.code_at(text, from + 2) == 0xb0 {
from + 3
} else {
from + 2
}
@util.write_range(out, text, last, from)
out.write_string("#")
@util.write_range(out, text, start, end - 2)
out.write_string(";")
last = end
from = @util.index_of(text, "\u{fb02}\u{b0}", from=last)
} else {
from = @util.index_of(text, "\u{fb02}\u{b0}", from=from + 1)
}
}
if last == 0 {
return text
}
@util.write_range(out, text, last, text.length())
out.to_string()
}
///|
/// Turns Mermaid's hidden `#name;` codes and HTML entities into characters.
pub fn decode_entities(text : String) -> String {
let restored = if text.contains("\u{b6}\u{df}") {
restore_placeholders(text)
} else {
text
}
if !restored.contains("&") {
return restored
}
let out = StringBuilder()
let mut last = 0
let mut at = @util.index_of(restored, "&")
while at != -1 {
match entity_at(restored, at) {
Some((end, chars)) => {
@util.write_range(out, restored, last, at)
out.write_string(chars)
last = end
at = @util.index_of(restored, "&", from=end)
}
None => at = @util.index_of(restored, "&", from=at + 1)
}
}
if last == 0 {
return restored
}
@util.write_range(out, restored, last, restored.length())
out.to_string()
}