linkify.bend source
linkify.bend on the hub · documented module
# bible-linkify surface (pure Bend): protect-aware Markdown walker + plain matcherimport Baseimport ./route.bend as Routeimport ./protect.bend as Protimport ./parse.bend as Pdef char1(+c: Char) -> String: String.from_list(c <> Nil{})# Linkify options (baseUrl / src / rewriteExisting), matching bible-linkify.type LinkOpts is Data: LinkOpts{+base: String, +src: String, rewrite: Bool}def default_opts() -> LinkOpts: LinkOpts{Route.default_base(), "", False{}}def opts_base(+base: String, +src: String, rewrite: Bool) -> LinkOpts: LinkOpts{Route.normalize_base(base), src, rewrite}def linkify_parts(+visible: String, +canonical: String) -> String: Route.markdown_route_link(visible, canonical)def linkify_parts_qv(+visible: String, +canonical: String, +translation: String) -> String: Route.markdown_route_link_qv(visible, canonical, translation)def linkify_parts_opts( +visible: String, +canonical: String, +translation: String, +opts: LinkOpts) -> String: match opts: case LinkOpts{+base, +src, rewrite}: Route.markdown_route_link_base_qvs(base, visible, canonical, translation, src)# Peek trailing " (ESV)" after a match — does not consume; code or "".type TvPh is Data: TvSkipSp{} TvSkipDec{sp: Bool} TvNeedOpen{} TvNeedOpenDec{is_open: Bool} TvCode{+acc: String, +letters: Nat, +digits: Nat} TvCls{+acc: String, +letters: Nat, +digits: Nat, is_close: Bool, ch: Char} TvAl{+acc: String, +letters: Nat, +digits: Nat, ch: Char, is_alpha: Bool, dig0: Bool} TvDg{+acc: String, +letters: Nat, +digits: Nat, ch: Char, is_digit: Bool} TvDone{+acc: String, +letters: Nat, +digits: Nat} TvCheck{+acc: String, ok: Bool}def tv_letter_ok(+letters: Nat) -> Bool: Bool.and(Nat.is_ge(letters, 2n), Nat.is_le(letters, 8n))def tv_digit_ok(+digits: Nat) -> Bool: Nat.is_le(digits, 2n)def peek_translation_go(fuel: Nat, ph: TvPh, rest: String) -> String: match fuel: case 0n: "" case 1n+p: match ph: case TvSkipSp{}: match rest: case SNil{}: "" case SCon{+h, +t}: peek_translation_go(p, TvSkipDec{Char.is_space(h)}, SCon{h, t}) case TvSkipDec{sp}: match sp: case True{}: match rest: case SNil{}: "" case SCon{h, t}: peek_translation_go(p, TvSkipSp{}, t) case False{}: peek_translation_go(p, TvNeedOpen{}, rest) case TvNeedOpen{}: match rest: case SNil{}: "" case SCon{+h, +t}: peek_translation_go(p, TvNeedOpenDec{Char.is_eq(h, '(')}, SCon{h, t}) case TvNeedOpenDec{is_open}: match is_open: case True{}: match rest: case SNil{}: "" case SCon{h, t}: peek_translation_go(p, TvCode{"", 0n, 0n}, t) case False{}: "" case TvCode{+acc, +letters, +digits}: match rest: case SNil{}: "" case SCon{+h, +t}: peek_translation_go( p, TvCls{acc, letters, digits, Char.is_eq(h, ')'), h}, SCon{h, t} ) case TvCls{+acc, +letters, +digits, is_close, +ch}: match is_close: case True{}: peek_translation_go(p, TvDone{acc, letters, digits}, rest) case False{}: peek_translation_go( p, TvAl{acc, letters, digits, ch, Char.is_alpha(ch), Nat.is_eq(digits, 0n)}, rest ) case TvAl{+acc, +letters, +digits, +ch, is_alpha, dig0}: match is_alpha: case True{}: match dig0: case True{}: match rest: case SNil{}: "" case SCon{h, t}: peek_translation_go( p, TvCode{String.append(acc, char1(ch)), Nat.add(letters, 1n), digits}, t ) case False{}: "" case False{}: peek_translation_go(p, TvDg{acc, letters, digits, ch, Char.is_digit(ch)}, rest) case TvDg{+acc, +letters, +digits, +ch, is_digit}: match is_digit: case True{}: match rest: case SNil{}: "" case SCon{h, t}: peek_translation_go( p, TvCode{String.append(acc, char1(ch)), letters, Nat.add(digits, 1n)}, t ) case False{}: "" case TvDone{+acc, +letters, +digits}: peek_translation_go( p, TvCheck{acc, Bool.and(tv_letter_ok(letters), tv_digit_ok(digits))}, rest ) case TvCheck{+acc, ok}: match ok: case True{}: String.to_upper(acc) case False{}: ""def peek_translation(+suffix: String) -> String: peek_translation_go( Nat.add(Nat.mul(3n, String.length(suffix)), 16n), TvSkipSp{}, suffix )def is_newline(+c: Char) -> Bool: match c: case Chr{+code}: U32.is_eq(code, 10)def is_alnum(+c: Char) -> Bool: Bool.or(Char.is_alpha(c), Char.is_digit(c))def is_wrapper_lead(+c: Char) -> Bool: Bool.or( Char.is_eq(c, '('), Bool.or( Char.is_eq(c, '['), Bool.or( Char.is_eq(c, '{'), Bool.or( Char.is_eq(c, '"'), Bool.or(Char.is_eq(c, '\''), Char.is_eq(c, '`')) ) ) ) )def is_wrapper_trail(+c: Char) -> Bool: Bool.or( Char.is_eq(c, ')'), Bool.or( Char.is_eq(c, ']'), Bool.or( Char.is_eq(c, '}'), Bool.or( Char.is_eq(c, '"'), Bool.or( Char.is_eq(c, '\''), Bool.or( Char.is_eq(c, '`'), Bool.or( Char.is_eq(c, ','), Bool.or( Char.is_eq(c, ';'), Bool.or( Char.is_eq(c, '.'), Bool.or(Char.is_eq(c, '!'), Char.is_eq(c, '?')) ) ) ) ) ) ) ) ) )# --- Passage signal ---type SigPh is Data: SigScan{has_a: Bool, has_d: Bool} SigBoth{has_a: Bool, has_d: Bool, both: Bool}def has_signal_go(fuel: Nat, ph: SigPh, rest: String) -> Bool: match fuel: case 0n: match ph: case SigScan{has_a, has_d}: Bool.and(has_a, has_d) case SigBoth{has_a, has_d, both}: both case 1n+p: match ph: case SigBoth{has_a, has_d, both}: match both: case True{}: True{} case False{}: match rest: case SNil{}: False{} case SCon{+h, +t}: has_signal_go( p, SigScan{ Bool.or(has_a, Char.is_alpha(h)), Bool.or(has_d, Char.is_digit(h)) }, t ) case SigScan{+has_a, +has_d}: has_signal_go(p, SigBoth{has_a, has_d, Bool.and(has_a, has_d)}, rest)def has_passage_signal(+s: String) -> Bool: has_signal_go(Nat.add(Nat.mul(4n, String.length(s)), 8n), SigScan{False{}, False{}}, s)# --- Trim wrappers: drop lead wrappers, reverse, drop lead (ex-trail), reverse ---type TrPh is Data: TrLead{} TrLeadDec{lead: Bool}def is_wrapper_either(+c: Char) -> Bool: Bool.or(is_wrapper_lead(c), is_wrapper_trail(c))def trim_pass_go(fuel: Nat, ph: TrPh, rest: String) -> String: match fuel: case 0n: rest case 1n+p: match ph: case TrLead{}: match rest: case SNil{}: "" case SCon{+h, +t}: trim_pass_go(p, TrLeadDec{is_wrapper_either(h)}, SCon{h, t}) case TrLeadDec{lead}: match lead: case True{}: match rest: case SNil{}: "" case SCon{h, t}: trim_pass_go(p, TrLead{}, t) case False{}: restdef trim_pass(+s: String) -> String: trim_pass_go(Nat.add(Nat.mul(3n, String.length(s)), 8n), TrLead{}, s)def trim_wrappers(+s: String) -> String: String.reverse(trim_pass(String.reverse(trim_pass(s))))# --- Candidate take ---def is_colon_dot_dash(+c: Char) -> Bool: match c: case Chr{+code}: Bool.or(U32.is_eq(code, 58), U32.is_eq(code, 45))def is_cand_char(+c: Char) -> Bool: Bool.or( is_alnum(c), Bool.or(Char.is_space(c), is_colon_dot_dash(c)) )type TcPh is Data: TcCheck{+n: Nat, has_d: Bool} TcGe{+n: Nat, has_d: Bool, ge: Bool} TcOk{+n: Nat, has_d: Bool, ok: Bool} TcSp{+n: Nat, has_d: Bool, is_sp: Bool} TcSpDig{+n: Nat} TcSpNod{+n: Nat} TcPeek{+n: Nat, next_alpha: Bool} TcTake{+n: Nat, has_d: Bool} TcTakeDig{+n: Nat, has_d: Bool, dig: Bool}def pick_sp(has_d: Bool, +n: Nat) -> TcPh: match has_d: case True{}: TcSpDig{n} case False{}: TcSpNod{n}def take_cand_go(fuel: Nat, ph: TcPh, +rest: String, acc: String) -> String: match fuel: case 0n: acc case 1n+p: match ph: case TcCheck{+n, +has_d}: take_cand_go(p, TcGe{n, has_d, Nat.is_ge(n, 40n)}, rest, acc) case TcGe{+n, +has_d, ge}: match ge: case True{}: acc case False{}: match rest: case SNil{}: acc case SCon{+h, +t}: take_cand_go(p, TcOk{n, has_d, is_cand_char(h)}, SCon{h, t}, acc) case TcOk{+n, +has_d, ok}: match ok: case False{}: acc case True{}: match rest: case SNil{}: acc case SCon{+h, +t}: take_cand_go(p, TcSp{n, has_d, Char.is_space(h)}, SCon{h, t}, acc) case TcSp{+n, +has_d, is_sp}: match is_sp: case True{}: take_cand_go(p, pick_sp(has_d, n), rest, acc) case False{}: take_cand_go(p, TcTake{n, has_d}, rest, acc) case TcSpDig{+n}: match rest: case SNil{}: acc case SCon{h, +t}: match t: case SNil{}: acc case SCon{+h2, t2}: take_cand_go(p, TcPeek{n, Char.is_alpha(h2)}, rest, acc) case TcSpNod{+n}: take_cand_go(p, TcTake{n, False{}}, rest, acc) case TcPeek{+n, next_alpha}: match next_alpha: case True{}: acc case False{}: take_cand_go(p, TcTake{n, True{}}, rest, acc) case TcTake{+n, +has_d}: match rest: case SNil{}: acc case SCon{+h, +t}: take_cand_go(p, TcTakeDig{n, has_d, Char.is_digit(h)}, SCon{h, t}, acc) case TcTakeDig{+n, +has_d, dig}: match rest: case SNil{}: acc case SCon{+h, +t}: take_cand_go( p, TcCheck{Nat.add(n, 1n), Bool.or(has_d, dig)}, t, String.append(acc, char1(h)) )def take_candidate(+s: String) -> String: take_cand_go(Nat.add(Nat.mul(6n, String.length(s)), 16n), TcCheck{0n, False{}}, s, "")type VisCan is Data: VisCan{visible: String, canonical: String}type CandGate is Data: CandNone{} CandSome{visible: String, canonical: String}def cand_pick(empty: Bool, +visible: String, +canonical: String) -> CandGate: match empty: case True{}: CandNone{} case False{}: CandSome{visible, canonical}def cand_from(+visible: String, +canonical: String) -> CandGate: cand_pick(String.is_empty(canonical), visible, canonical)def try_parse_visible_gated(g: CandGate) -> Maybe<&2, VisCan>: match g: case CandNone{}: None{} case CandSome{+visible, +canonical}: Some{VisCan{visible, canonical}}def try_parse_visible(+visible: String) -> Maybe<&2, VisCan>: try_parse_visible_gated( cand_from(String.trim(visible), P.try_canonical(String.trim(visible))) )def drop_last_rev(rev: String) -> String: match rev: case SNil{}: "" case SCon{h, t}: tdef drop_last(s: String) -> String: String.reverse(drop_last_rev(String.reverse(s)))type TwPh is Data: TwTry{s: String} TwEmpty{s: String, empty: Bool} TwShort{s: String, short: Bool} TwParsed{s: String, m: Maybe<&2, VisCan>}def try_windows_go(fuel: Nat, ph: TwPh) -> Maybe<&2, VisCan>: match fuel: case 0n: None{} case 1n+p: match ph: case TwTry{+s}: try_windows_go(p, TwEmpty{s, String.is_empty(s)}) case TwEmpty{+s, empty}: match empty: case True{}: None{} case False{}: try_windows_go(p, TwShort{s, Nat.is_lt(String.length(s), 3n)}) case TwShort{+s, short}: match short: case True{}: None{} case False{}: try_windows_go(p, TwParsed{s, try_parse_visible(trim_wrappers(s))}) case TwParsed{+s, m}: match m: case Some{vc}: Some{vc} case None{}: try_windows_go(p, TwTry{drop_last(s)})def try_windows(+raw: String) -> Maybe<&2, VisCan>: try_windows_go(Nat.add(Nat.mul(3n, String.length(raw)), 8n), TwTry{raw})# --- Urlish ---type ExPh is Data: ExGo{} ExDec{sp: Bool}def expand_token_left(fuel: Nat, ph: ExPh, +left_rev: String, acc: String) -> String: match fuel: case 0n: acc case 1n+p: match ph: case ExGo{}: match left_rev: case SNil{}: acc case SCon{+h, +t}: expand_token_left(p, ExDec{Char.is_space(h)}, left_rev, acc) case ExDec{sp}: match sp: case True{}: acc case False{}: match left_rev: case SNil{}: acc case SCon{+h, +t}: expand_token_left(p, ExGo{}, t, String.append(char1(h), acc))def expand_token_right(fuel: Nat, ph: ExPh, +right: String, acc: String) -> String: match fuel: case 0n: acc case 1n+p: match ph: case ExGo{}: match right: case SNil{}: acc case SCon{+h, +t}: expand_token_right(p, ExDec{Char.is_space(h)}, right, acc) case ExDec{sp}: match sp: case True{}: acc case False{}: match right: case SNil{}: acc case SCon{+h, +t}: expand_token_right(p, ExGo{}, t, String.append(acc, char1(h)))def token_around(+prefix_rev: String, +matched: String, +suffix: String) -> String: String.append( expand_token_left(Nat.add(String.length(prefix_rev), 4n), ExGo{}, prefix_rev, ""), String.append( matched, expand_token_right(Nat.add(String.length(suffix), 4n), ExGo{}, suffix, "") ) )def is_urlish_token(+tok: String) -> Bool: Bool.or( String.contains(String.to_lower(tok), "http://"), Bool.or( String.contains(String.to_lower(tok), "https://"), String.contains(String.to_lower(tok), "www.") ) )def is_book_num(+c: Char) -> Bool: Bool.or( Char.is_eq(c, '1'), Bool.or(Char.is_eq(c, '2'), Char.is_eq(c, '3')) )def looks_like_ref_start(+c: Char) -> Bool: Bool.or(Char.is_alpha(c), is_book_num(c))type NaPh is Data: NaDec{alnum: Bool}def not_alnum_next_go(ph: NaPh) -> Bool: match ph: case NaDec{alnum}: match alnum: case True{}: False{} case False{}: True{}def not_alnum_next(next_suffix: String) -> Bool: match next_suffix: case SNil{}: True{} case SCon{+h, t}: not_alnum_next_go(NaDec{is_alnum(h)})def standalone_from_prev(prev_alnum: Bool, +next_suffix: String) -> Bool: match prev_alnum: case True{}: False{} case False{}: not_alnum_next(next_suffix)def standalone_ok(+prev_rev: String, +next_suffix: String) -> Bool: match prev_rev: case SNil{}: standalone_from_prev(False{}, next_suffix) case SCon{+h, t}: standalone_from_prev(is_alnum(h), next_suffix)# --- Plain linkify ---type PlPh is Data: PlScan{} PlNotRef{} PlRefStart{} PlRefCont{} PlBoundOk{prev_alnum: Bool} PlTry{} PlTried{m: Maybe<&2, VisCan>} PlAfterTry{visible: String, canonical: String, empty: Bool} PlStand{visible: String, canonical: String, ok: Bool} PlUrl{visible: String, canonical: String, bad: Bool} PlEmit{ch: Char}def pick_pl_ref(at_start: Bool) -> PlPh: match at_start: case True{}: PlRefStart{} case False{}: PlRefCont{}def pick_pl(is_ref: Bool, at_start: Bool) -> PlPh: match is_ref: case False{}: PlNotRef{} case True{}: pick_pl_ref(at_start)def plain_append_link( +out: String, +visible: String, +canonical: String, +opts: LinkOpts) -> String: String.append(out, linkify_parts_opts(visible, canonical, "", opts))def plain_append_link_at( +out: String, +visible: String, +canonical: String, +suffix: String, +opts: LinkOpts) -> String: String.append( out, linkify_parts_opts(visible, canonical, peek_translation(suffix), opts) )def linkify_plain_go( fuel: Nat, ph: PlPh, +rest: String, +prev_rev: String, out: String, +opts: LinkOpts) -> String: match fuel: case 0n: out case 1n+p: match ph: case PlScan{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_plain_go( p, pick_pl(looks_like_ref_start(h), String.is_empty(prev_rev)), SCon{h, t}, prev_rev, out, opts ) case PlNotRef{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_plain_go(p, PlEmit{h}, SCon{h, t}, prev_rev, out, opts) case PlRefStart{}: linkify_plain_go(p, PlTry{}, rest, prev_rev, out, opts) case PlRefCont{}: match prev_rev: case SNil{}: linkify_plain_go(p, PlTry{}, rest, prev_rev, out, opts) case SCon{+phc, pt}: linkify_plain_go(p, PlBoundOk{is_alnum(phc)}, rest, prev_rev, out, opts) case PlBoundOk{prev_alnum}: match prev_alnum: case True{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_plain_go(p, PlEmit{h}, SCon{h, t}, prev_rev, out, opts) case False{}: linkify_plain_go(p, PlTry{}, rest, prev_rev, out, opts) case PlTry{}: match rest: case SNil{}: out case SCon{+h0, +t0}: linkify_plain_go( p, PlTried{try_windows(take_candidate(SCon{h0, t0}))}, SCon{h0, t0}, prev_rev, out, opts ) case PlTried{m}: match m: case None{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_plain_go(p, PlEmit{h}, SCon{h, t}, prev_rev, out, opts) case Some{vc}: match vc: case VisCan{+visible, +canonical}: linkify_plain_go( p, PlScan{}, Prot.str_drop(String.length(visible), rest), String.append(String.reverse(visible), prev_rev), plain_append_link_at( out, visible, canonical, Prot.str_drop(String.length(visible), rest), opts ), opts ) case PlAfterTry{+visible, +canonical, +empty}: match empty: case True{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_plain_go(p, PlEmit{h}, SCon{h, t}, prev_rev, out, opts) case False{}: linkify_plain_go( p, PlStand{ visible, canonical, standalone_ok(prev_rev, Prot.str_drop(String.length(visible), rest)) }, rest, prev_rev, out, opts ) case PlStand{+visible, +canonical, +ok}: match ok: case False{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_plain_go(p, PlEmit{h}, SCon{h, t}, prev_rev, out, opts) case True{}: linkify_plain_go( p, PlUrl{ visible, canonical, is_urlish_token( token_around(prev_rev, visible, Prot.str_drop(String.length(visible), rest)) ) }, rest, prev_rev, out, opts ) case PlUrl{+visible, +canonical, +bad}: match bad: case True{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_plain_go(p, PlEmit{h}, SCon{h, t}, prev_rev, out, opts) case False{}: linkify_plain_go( p, PlScan{}, Prot.str_drop(String.length(visible), rest), String.append(String.reverse(visible), prev_rev), plain_append_link_at( out, visible, canonical, Prot.str_drop(String.length(visible), rest), opts ), opts ) case PlEmit{+ch}: match rest: case SNil{}: out case SCon{h, +t}: linkify_plain_go( p, PlScan{}, t, SCon{ch, prev_rev}, String.append(out, char1(ch)), opts )type LpGate is Data: LpGate{ok: Bool}type FullHit2 is Data: FullMiss2{} FullExact{visible: String, canonical: String} FullPrefix{visible: String, canonical: String, rest: String}def classify_rest(exact: Bool, +visible: String, +canonical: String, +rest: String) -> FullHit2: match exact: case True{}: FullExact{visible, canonical} case False{}: FullPrefix{visible, canonical, rest}def classify_hit(m: Maybe<&2, VisCan>, +text: String) -> FullHit2: match m: case None{}: FullMiss2{} case Some{vc}: match vc: case VisCan{+visible, +canonical}: classify_rest( Nat.is_eq(String.length(visible), String.length(text)), visible, canonical, Prot.str_drop(String.length(visible), text) )def linkify_plain_from_hit(h: FullHit2, +text: String, +opts: LinkOpts) -> String: match h: case FullExact{+visible, +canonical}: plain_append_link("", visible, canonical, opts) case FullPrefix{+visible, +canonical, +rest}: String.append( plain_append_link_at("", visible, canonical, rest, opts), linkify_plain_go( Nat.add(Nat.mul(8n, String.length(rest)), 64n), PlScan{}, rest, String.reverse(visible), "", opts ) ) case FullMiss2{}: linkify_plain_go( Nat.add(Nat.mul(8n, String.length(text)), 64n), PlScan{}, text, "", "", opts )def linkify_plain_gated(g: LpGate, +text: String, +opts: LinkOpts) -> String: match g: case LpGate{ok}: match ok: case False{}: text case True{}: linkify_plain_from_hit( classify_hit(try_windows(take_candidate(text)), text), text, opts )def linkify_plain_opts(+text: String, +opts: LinkOpts) -> String: linkify_plain_gated(LpGate{has_passage_signal(text)}, text, opts)def linkify_plain(+text: String) -> String: linkify_plain_opts(text, default_opts())# --- Extract label from [label](url); "" if image or malformed ---type MlPh is Data: MlHead{} MlHeadDec{is_bang: Bool, is_br: Bool} MlLab{+acc: String} MlLabDec{+acc: String, nl: Bool, is_br: Bool, ch: Char} MlMid{+label: String} MlMidDec{+label: String, is_par: Bool} MlUrl{+label: String} MlUrlDec{+label: String, nl: Bool, is_par: Bool} MlDone{+label: String, empty: Bool}def md_label_go(fuel: Nat, ph: MlPh, rest: String) -> String: match fuel: case 0n: "" case 1n+p: match ph: case MlHead{}: match rest: case SNil{}: "" case SCon{+h, +t}: md_label_go( p, MlHeadDec{Char.is_eq(h, '!'), Char.is_eq(h, '[')}, SCon{h, t} ) case MlHeadDec{is_bang, is_br}: match is_bang: case True{}: "" case False{}: match is_br: case True{}: match rest: case SNil{}: "" case SCon{h, t}: md_label_go(p, MlLab{""}, t) case False{}: "" case MlLab{+acc}: match rest: case SNil{}: "" case SCon{+h, +t}: md_label_go( p, MlLabDec{acc, is_newline(h), Char.is_eq(h, ']'), h}, SCon{h, t} ) case MlLabDec{+acc, nl, is_br, +ch}: match nl: case True{}: "" case False{}: match is_br: case True{}: match rest: case SNil{}: "" case SCon{h, t}: md_label_go(p, MlMid{acc}, t) case False{}: match rest: case SNil{}: "" case SCon{h, t}: md_label_go(p, MlLab{String.append(acc, char1(ch))}, t) case MlMid{+label}: match rest: case SNil{}: "" case SCon{+h, +t}: md_label_go(p, MlMidDec{label, Char.is_eq(h, '(')}, SCon{h, t}) case MlMidDec{+label, is_par}: match is_par: case True{}: match rest: case SNil{}: "" case SCon{h, t}: md_label_go(p, MlUrl{label}, t) case False{}: "" case MlUrl{+label}: match rest: case SNil{}: "" case SCon{+h, +t}: md_label_go( p, MlUrlDec{label, is_newline(h), Char.is_eq(h, ')')}, SCon{h, t} ) case MlUrlDec{+label, nl, is_par}: match nl: case True{}: "" case False{}: match is_par: case True{}: match rest: case SNil{}: "" case SCon{h, +t}: md_label_go(p, MlDone{label, String.is_empty(t)}, t) case False{}: match rest: case SNil{}: "" case SCon{h, t}: md_label_go(p, MlUrl{label}, t) case MlDone{+label, empty}: match empty: case True{}: label case False{}: ""def md_extract_label(+token: String) -> String: md_label_go( Nat.add(Nat.mul(4n, String.length(token)), 16n), MlHead{}, token )type RwEq is Data: RwEq{+visible: String, +canonical: String, eq: Bool}def rewrite_from_eq(g: RwEq, +opts: LinkOpts) -> String: match g: case RwEq{+visible, +canonical, eq}: match eq: case True{}: linkify_parts_opts(visible, canonical, "", opts) case False{}: ""def rewrite_from_parse( +label: String, m: Maybe<&2, VisCan>, +opts: LinkOpts) -> String: match m: case None{}: "" case Some{vc}: match vc: case VisCan{+visible, +canonical}: rewrite_from_eq(RwEq{visible, canonical, String.eq(visible, label)}, opts)type RwPick is Data: RwPick{+token: String, +repl: String, empty: Bool}def maybe_rewrite_pick_go(g: RwPick) -> String: match g: case RwPick{+token, +repl, empty}: match empty: case True{}: token case False{}: repldef maybe_rewrite_pick(+token: String, +repl: String) -> String: maybe_rewrite_pick_go(RwPick{token, repl, String.is_empty(repl)})def maybe_rewrite_md_link(+token: String, +opts: LinkOpts) -> String: maybe_rewrite_pick( token, rewrite_from_parse( String.trim(md_extract_label(token)), try_parse_visible(String.trim(md_extract_label(token))), opts ) )def is_md_protect(+rest: String, +plen: Nat) -> Bool: Bool.and(Nat.is_gt(plen, 0n), Nat.is_eq(Prot.md_link_len(rest), plen))def want_rewrite_md(+opts: LinkOpts, +rest: String, +plen: Nat) -> Bool: match opts: case LinkOpts{base, src, rewrite}: match rewrite: case False{}: False{} case True{}: is_md_protect(rest, plen)# --- Markdown walker ---type MdWalk is Data: MwCheck{at_bol: Bool, at_doc: Bool} MwDecide{at_bol: Bool, plen: Nat, z: Bool} MwProtect{len: Nat, at_bol: Bool} MwProtDec{len: Nat, at_bol: Bool, z: Bool, nl: Bool} MwPlainEmit{at_bol: Bool} MwFlush{at_bol: Bool, plen: Nat, z: Bool} MwTryRw{at_bol: Bool, plen: Nat, do_rw: Bool}def linkify_md_go( fuel: Nat, ph: MdWalk, +rest: String, plain: String, out: String, +opts: LinkOpts) -> String: match fuel: case 0n: String.append(out, linkify_plain_opts(plain, opts)) case 1n+p: match ph: case MwCheck{+at_bol, +at_doc}: match rest: case SNil{}: String.append(out, linkify_plain_opts(plain, opts)) case SCon{+h, +t}: linkify_md_go( p, MwDecide{ at_bol, Prot.frontmatter_or_protect(at_doc, at_bol, SCon{h, t}), False{} }, SCon{h, t}, plain, out, opts ) case MwDecide{+at_bol, +plen, z}: linkify_md_go( p, MwFlush{at_bol, plen, Nat.is_eq(plen, 0n)}, rest, plain, out, opts ) case MwFlush{+at_bol, +plen, z}: match z: case True{}: linkify_md_go(p, MwPlainEmit{at_bol}, rest, plain, out, opts) case False{}: linkify_md_go( p, MwTryRw{at_bol, plen, want_rewrite_md(opts, rest, plen)}, rest, plain, out, opts ) case MwTryRw{+at_bol, +plen, do_rw}: match do_rw: case True{}: linkify_md_go( p, MwCheck{False{}, False{}}, Prot.str_drop(plen, rest), "", String.append( String.append(out, linkify_plain_opts(plain, opts)), maybe_rewrite_md_link(Prot.str_take(plen, rest), opts) ), opts ) case False{}: linkify_md_go( p, MwProtect{plen, at_bol}, rest, "", String.append(out, linkify_plain_opts(plain, opts)), opts ) case MwProtect{+len, +at_bol}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_md_go( p, MwProtDec{len, at_bol, Nat.is_eq(len, 0n), is_newline(h)}, SCon{h, t}, plain, out, opts ) case MwProtDec{+len, +at_bol, z, nl}: match z: case True{}: linkify_md_go(p, MwCheck{at_bol, False{}}, rest, plain, out, opts) case False{}: match rest: case SNil{}: out case SCon{+h, +t}: linkify_md_go( p, MwProtect{Nat.sub(len, 1n), nl}, t, plain, String.append(out, char1(h)), opts ) case MwPlainEmit{+at_bol}: match rest: case SNil{}: String.append(out, linkify_plain_opts(plain, opts)) case SCon{+h, +t}: linkify_md_go( p, MwCheck{is_newline(h), False{}}, t, String.append(plain, char1(h)), out, opts )def linkify_markdown_opts(+source: String, +opts: LinkOpts) -> String: linkify_md_go( Nat.add(Nat.mul(10n, String.length(source)), 64n), MwCheck{True{}, True{}}, source, "", "", opts )def linkify_markdown(+source: String) -> String: linkify_markdown_opts(source, default_opts())def linkify_markdown_src(+source: String, +src: String) -> String: linkify_markdown_opts(source, opts_base(Route.default_base(), src, False{}))def linkify_markdown_rewrite(+source: String) -> String: linkify_markdown_opts(source, opts_base(Route.default_base(), "", True{}))def opts_cli(+src: String, rewrite: Bool) -> LinkOpts: opts_base(Route.default_base(), src, rewrite)def linkify_markdown_cli(+source: String, +src: String, rewrite: Bool) -> String: linkify_markdown_opts(source, opts_cli(src, rewrite))def show_links_rest(xs: List<&2, String>) -> IO(Unit): match xs: case Nil{}: IO.print("") case h <> t: do IO<Unit>: IO.print(h) IO.print("\n") show_links_rest(t)def show_links(xs: List<&2, String>) -> IO(Unit): match xs: case Nil{}: IO.print("(no links)\n") case h <> t: do IO<Unit>: IO.print(h) IO.print("\n") show_links_rest(t)