///|
fn is_letter(code : Int) -> Bool {
(code >= 65 && code <= 90) || (code >= 97 && code <= 122)
}
///|
fn is_digit_char(code : Int) -> Bool {
code >= 48 && code <= 57
}
///|
fn is_separator_char(code : Int) -> Bool {
if is_letter(code) || is_digit_char(code) {
return false
}
code == 32 ||
code == 9 ||
code == 10 ||
code == 13 ||
code == 45 ||
code == 95 ||
code == 46 ||
code == 47 ||
code == 58 ||
code == 44 ||
code == 59 ||
code == 124 ||
code == 40 ||
code == 41 ||
code == 91 ||
code == 93 ||
code == 123 ||
code == 125 ||
code == 33 ||
code == 63
}
///|
fn normalize_text(text : String) -> String {
ascii_lower(trim_ascii(text))
}
///|
fn normalize_compact(text : String) -> String {
let lower = normalize_text(text)
let mut out = ""
let mut index = 0
while index < lower.length() {
let code = code_at(lower, index)
if is_separator_char(code) {
if out.length() > 0 && code != 47 {
let last = code_at(out, out.length() - 1)
if last != 32 {
out = out + " "
}
} else if code == 47 {
out = out + "/"
}
} else {
out = out + slice(lower, index, index + 1)
}
index += 1
}
trim_ascii(out)
}
///|
pub fn contains_word(text : String, word : String) -> Bool {
let haystack = normalize_compact(text)
let needle = normalize_compact(word)
if needle.length() == 0 {
return true
}
if haystack.length() < needle.length() {
return false
}
let mut index = 0
while index + needle.length() <= haystack.length() {
if slice(haystack, index, index + needle.length()) == needle {
let before_ok = index == 0 ||
is_separator_char(code_at(haystack, index - 1))
let after_index = index + needle.length()
let after_ok = after_index == haystack.length() ||
is_separator_char(code_at(haystack, after_index))
if before_ok && after_ok {
return true
}
}
index += 1
}
false
}
///|
pub fn count_word(text : String, word : String) -> Int {
let haystack = normalize_compact(text)
let needle = normalize_compact(word)
if needle.length() == 0 || haystack.length() < needle.length() {
return 0
}
let mut count = 0
let mut index = 0
while index + needle.length() <= haystack.length() {
if slice(haystack, index, index + needle.length()) == needle {
let before_ok = index == 0 ||
is_separator_char(code_at(haystack, index - 1))
let after_index = index + needle.length()
let after_ok = after_index == haystack.length() ||
is_separator_char(code_at(haystack, after_index))
if before_ok && after_ok {
count += 1
index += needle.length()
} else {
index += 1
}
} else {
index += 1
}
}
count
}
///|
pub fn any_match(text : String, items : Array[String]) -> Bool {
let mut index = 0
while index < items.length() {
if contains_word(text, items[index]) {
return true
}
index += 1
}
false
}
///|
pub fn all_match(text : String, items : Array[String]) -> Bool {
let mut index = 0
while index < items.length() {
if !contains_word(text, items[index]) {
return false
}
index += 1
}
true
}
///|
pub fn is_heading_line(line : String) -> Bool {
let text = trim_ascii(line)
text.length() > 0 && code_at(text, 0) == 35
}
///|
pub fn heading_level(line : String) -> Int {
let text = trim_ascii(line)
let mut index = 0
while index < text.length() && code_at(text, index) == 35 {
index += 1
}
index
}
///|
pub fn heading_text(line : String) -> String {
let text = trim_ascii(line)
let mut index = 0
while index < text.length() && code_at(text, index) == 35 {
index += 1
}
trim_ascii(slice(text, index, text.length()))
}
///|
pub fn is_fence_line(line : String) -> Bool {
let text = trim_ascii(line)
text.length() >= 3 &&
code_at(text, 0) == 96 &&
code_at(text, 1) == 96 &&
code_at(text, 2) == 96
}
///|
pub fn is_bullet_line(line : String) -> Bool {
let text = trim_ascii(line)
text.length() > 1 &&
(code_at(text, 0) == 45 || code_at(text, 0) == 42) &&
code_at(text, 1) == 32
}
///|
pub fn is_numbered_line(line : String) -> Bool {
let text = trim_ascii(line)
if text.length() < 3 {
return false
}
let first = code_at(text, 0)
let second = code_at(text, 1)
let third = code_at(text, 2)
is_digit_char(first) && second == 46 && third == 32
}
///|
pub fn is_command_line(line : String) -> Bool {
let text = trim_ascii(line)
starts_with(text, "moon ") ||
starts_with(text, "moon\t") ||
starts_with(text, "git ") ||
starts_with(text, "curl ") ||
starts_with(text, "npm ") ||
starts_with(text, "pnpm ")
}
///|
pub fn is_markdown_link_line(line : String) -> Bool {
let text = trim_ascii(line)
contains(text, "](") && contains(text, "http")
}
///|
pub fn is_badge_line(line : String) -> Bool {
let text = trim_ascii(line)
contains(text, "shields.io") || contains(text, "badge")
}
///|
pub fn extract_inline_link_target(line : String) -> String? {
let text = trim_ascii(line)
match index_of(text, "](") {
None => None
Some(start) => {
let mut index = start + 2
while index < text.length() {
if code_at(text, index) == 41 {
return Some(slice(text, start + 2, index))
}
index += 1
}
None
}
}
}
///|
pub fn extract_inline_link_text(line : String) -> String? {
let text = trim_ascii(line)
match index_of(text, "[") {
None => None
Some(start) =>
match index_of(text, "](") {
None => None
Some(end) =>
if end > start {
Some(slice(text, start + 1, end))
} else {
None
}
}
}
}
///|
pub fn split_tokens(text : String) -> Array[String] {
let tokens : Array[String] = []
let mut current = ""
let mut index = 0
while index < text.length() {
let code = code_at(text, index)
if is_separator_char(code) {
if current.length() > 0 {
tokens.push(current)
current = ""
}
} else {
current = current + slice(text, index, index + 1)
}
index += 1
}
if current.length() > 0 {
tokens.push(current)
}
tokens
}
///|
pub fn first_token(text : String) -> String {
let tokens = split_tokens(text)
if tokens.length() == 0 {
""
} else {
tokens[0]
}
}
///|
pub fn last_token(text : String) -> String {
let tokens = split_tokens(text)
if tokens.length() == 0 {
""
} else {
tokens[tokens.length() - 1]
}
}
///|
pub fn remove_trailing_punctuation(text : String) -> String {
let value = trim_ascii(text)
let mut end = value.length()
while end > 0 {
let code = code_at(value, end - 1)
if code == 46 ||
code == 44 ||
code == 59 ||
code == 58 ||
code == 33 ||
code == 63 {
end -= 1
} else {
break
}
}
trim_ascii(slice(value, 0, end))
}
///|
pub fn canonicalize_token(text : String) -> String {
normalize_compact(remove_trailing_punctuation(text))
}