///|
fn resolve_target(
from : String,
raw : String,
wiki : Bool,
paths : Map[String, Bool],
) -> (String, String) {
let value = raw.trim().to_owned()
if value.contains(":") || value.has_prefix("//") {
return (value, "external")
}
if value.has_prefix("/") || value.contains("\\") || value.contains("?") {
return (value, "unsupported")
}
let bare = value.split("#").to_array()[0]
.to_owned()
.replace_all(old="%20", new=" ")
if bare.contains("%") {
return (value, "unsupported")
}
if bare == "" {
return (from, "fragment_unchecked")
}
let folders = from.split("/").to_array()
let resolved : Array[String] = folders[:folders.length() - 1].map(s => {
s.to_owned()
})
for part in bare.split("/") {
if part == "." || part == "" {
continue
}
if part == ".." {
if resolved.is_empty() {
return (value, "outside")
}
ignore(resolved.pop())
} else {
resolved.push(part.to_owned())
}
}
let joined = resolved.join("/")
let candidates = [joined]
if wiki &&
!joined.to_lower().has_suffix(".md") &&
!joined.to_lower().has_suffix(".txt") {
candidates.push(joined + ".md")
}
let found = candidates.iter().find_first(p => paths.contains(p))
if found is Some(target) {
return (
target,
if value.contains("#") {
"fragment_unchecked"
} else {
"linked"
},
)
}
if wiki {
let names = paths
.keys()
.filter(p => {
(!bare.contains("/") && basename(p) == basename(bare)) ||
p == bare ||
p == bare + ".md"
})
.to_array()
if names.length() == 1 {
return (
names[0],
if value.contains("#") {
"fragment_unchecked"
} else {
"linked"
},
)
}
if names.length() > 1 {
return (value, "ambiguous")
}
}
if !wiki &&
!bare.to_lower().has_suffix(".md") &&
!bare.to_lower().has_suffix(".txt") {
return (value, "attachment_unchecked")
}
(joined, "missing")
}
///|
/// A bounded link scanner for ordinary inline and wiki links. Not CommonMark.
fn document_links(doc : Document, paths : Map[String, Bool]) -> Array[Link] {
if !doc.path.to_lower().has_suffix(".md") {
return []
}
let links = []
let lines = doc.content.split("\n").to_array()
let mut fence : Char = ' '
let mut fence_size = 0
let mut front_end = -1
if lines[0].trim() == "---" {
for i = 1; i < lines.length() && i < 100; i = i + 1 {
if lines[i].trim() == "---" {
front_end = i
break
}
}
}
for i, source in lines {
let line = source.trim().to_owned()
if i <= front_end {
continue
}
if source.has_prefix(" ") || source.has_prefix("\t") {
continue
}
let chars = line.to_array()
if chars.length() >= 3 && (chars[0] == '`' || chars[0] == '~') {
let mut n = 0
while n < chars.length() && chars[n] == chars[0] {
n += 1
}
if fence_size == 0 && n >= 3 {
fence = chars[0]
fence_size = n
continue
}
if chars[0] == fence &&
n >= fence_size &&
String::from_array(chars[n:]).trim() == "" {
fence_size = 0
continue
}
}
if fence_size > 0 {
continue
}
// Inline code is omitted, including unmatched code spans for conservatism.
let mut code = 0
let mut text = ""
let mut cursor = 0
while cursor < chars.length() {
if chars[cursor] == '`' {
let mut count = 0
while cursor < chars.length() && chars[cursor] == '`' {
count += 1
cursor += 1
}
if code == 0 {
code = count
} else if code == count {
code = 0
}
} else {
if code == 0 {
text += chars[cursor].to_string()
}
cursor += 1
}
}
let wiki_parts = text.split("[[").to_array()
for j = 1; j < wiki_parts.length(); j = j + 1 {
if wiki_parts[j].split_once("]]") is Some((inside, _)) {
let raw = inside.split("|").to_array()[0].to_owned()
let (target, status) = resolve_target(doc.path, raw, true, paths)
links.push({ from: doc.path, target, line: i + 1, status, })
if links.length() > 5000 {
return links
}
}
}
let parts = text.split("](").to_array()
for j = 1; j < parts.length(); j = j + 1 {
if !parts[j - 1].contains("[") {
continue
}
if parts[j].split_once(")") is Some((inside, _)) {
let raw = inside.to_owned()
if raw.contains("(") || raw.contains("\"") || raw.contains("\t") {
continue
}
let (target, status) = resolve_target(doc.path, raw, false, paths)
links.push({ from: doc.path, target, line: i + 1, status, })
if links.length() > 5000 {
return links
}
}
}
}
links
}