///|
/// What a typed reference points to.
pub(all) enum RefTarget {
ToDocument(DocKey)
ToRecord(register~ : RegisterName, record~ : DocKey)
ToFile(StorePath)
ToUri(String)
/// A free-text scope, such as `all queries in project X`.
FreeText(String)
/// The value does not have the form that the field requires.
Malformed(String)
/// The value has the right form, but names nothing that exists.
Unresolved(String)
} derive(Eq, Debug)
///|
/// One typed reference of a document. The key is the frontmatter key, such as
/// `sources`.
pub struct TypedReference {
key : FieldKey
value : String
line : Int32?
target : RefTarget
} derive(Eq, Debug)
///|
pub fn TypedReference::is_resolved(self : TypedReference) -> Bool {
match self.target {
ToDocument(_) | ToRecord(..) | ToFile(_) | ToUri(_) | FreeText(_) => true
Malformed(_) | Unresolved(_) => false
}
}
///|
/// Raised when a document reference does not parse.
pub(all) suberror DocRefError {
/// The text is neither `/path.md` nor `bundle-label:/path.md`.
NotADocRef(String)
/// The label part is not a valid bundle label.
BadLabel(text~ : String, error~ : ValueError)
/// The path part escapes the bundle or names no file.
BadPath(text~ : String, error~ : PathError)
} derive(Eq, Debug)
///|
/// A document reference: `bundle-label:/path.md`, or `/path.md` in the same
/// bundle. It holds a valid label and a valid bundle path.
struct DocRef {
bundle : BundleLabel?
path : BundlePath
} derive(Eq, Debug)
///|
/// Parses `bundle-label:/path.md` or `/path.md`.
pub fn DocRef::DocRef(text : String) -> DocRef raise DocRefError {
let t = text.trim(chars=" \t").to_owned()
let (label_text, path_text) = if t.has_prefix("/") {
(None, t)
} else {
match t.find(":/") {
Some(i) if i > 0 => (Some(t[:i].to_owned()), t[i + 1:].to_owned())
_ => raise NotADocRef(text)
}
}
let bundle = match label_text {
Some(l) =>
Some(BundleLabel(l) catch { e => raise BadLabel(text~, error=e) })
None => None
}
let path = BundlePath(path_text) catch { e => raise BadPath(text~, error=e) }
{ bundle, path, }
}
///|
/// The bundle label, or `None` for a reference in the same bundle.
pub fn DocRef::bundle(self : DocRef) -> BundleLabel? {
self.bundle
}
///|
pub fn DocRef::path(self : DocRef) -> BundlePath {
self.path
}
///|
fn path_target(
knowledge : Knowledge,
bundle : Bundle,
doc : Document,
value : String,
resolver : &LinkResolver?,
) -> RefTarget {
if value.contains(" ") && !value.has_prefix("/") && !has_scheme(value) {
return FreeText(value)
}
match resolve_field_path(bundle, doc, value) {
ExternalUri(uri) => ToUri(uri)
SameDocument(_) => ToDocument(DocKey::of(bundle, doc))
Unresolvable(v) => Unresolved(v)
StoreTarget(path~, ..) =>
match knowledge.document_at(path) {
Some((b, d)) => ToDocument(DocKey::of(b, d))
None => {
let known = match resolver {
Some(r) => r.exists(path)
None => false
}
if known {
ToFile(path)
} else {
Unresolved(value)
}
}
}
}
}
///|
fn doc_ref_target(
knowledge : Knowledge,
bundle : Bundle,
value : String,
) -> RefTarget {
let r = DocRef(value) catch { _ => return Malformed(value) }
let target_bundle = match r.bundle {
None => Some(bundle)
Some(label) => knowledge.bundle(label).to_option()
}
match target_bundle {
None => Unresolved(value)
Some(b) =>
match b.document(r.path) {
Some(d) => ToDocument(DocKey::of(b, d))
None => Unresolved(value)
}
}
}
///|
/// The `sources` key, which is a valid field key.
let sources_key : FieldKey = { text: "sources", }
///|
/// The typed references of a document: each `sources[].resource`, and each
/// value of the reference fields of `profiles`. A profile whose definitions
/// break a rule adds no references; `validate` reports it.
pub fn typed_references(
knowledge : Knowledge,
bundle : Bundle,
doc : Document,
profiles~ : ArrayView[&Profile] = [],
resolver? : &LinkResolver,
) -> Array[TypedReference] {
let meta = doc.metadata()
let out = []
for source in meta.sources() {
out.push({
key: sources_key,
value: source.resource,
line: meta.line_of("sources"),
target: path_target(knowledge, bundle, doc, source.resource, resolver),
})
}
let registers = registers_of(profiles)
for profile in profiles {
let fields = profile.reference_fields() catch { _ => [] }
for field in fields {
let key = field.key.to_string()
for value in meta.texts(key) {
let target = match field.kind {
PathRef => path_target(knowledge, bundle, doc, value, resolver)
DocumentRef => doc_ref_target(knowledge, bundle, value)
RecordRef(register~) =>
match registers.iter().find_first(r => r.name() == register) {
None => Unresolved(value)
Some(reg) =>
match reg.find(reg.records(bundle), value) {
Found(record) => ToRecord(register~, record=record.key())
_ => Unresolved(value)
}
}
}
out.push({ key: field.key, value, line: meta.line_of(key), target, })
}
}
}
out
}
///|
/// Raised when a GitHub URL is not a commit-pinned file URL.
pub(all) suberror GitHubUrlError {
/// The URL does not start with `https://github.com/`.
NotGitHub(String)
/// The URL has no `blob` or `tree` part, or an empty organization,
/// repository or path.
MissingPart(String)
/// The commit is not 7 to 40 hexadecimal characters.
NotACommit(String)
} derive(Eq, Debug)
///|
/// A commit SHA of 7 to 40 hexadecimal characters.
struct CommitSha {
text : String
} derive(Eq, Hash, Debug)
///|
fn is_hex(text : String) -> Bool {
text.length() > 0 &&
text
.iter()
.all(c => {
(c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
})
}
///|
pub fn CommitSha::CommitSha(text : String) -> CommitSha raise GitHubUrlError {
if text.length() < 7 || text.length() > 40 || !is_hex(text) {
raise NotACommit(text)
}
{ text, }
}
///|
pub fn CommitSha::to_string(self : CommitSha) -> String {
self.text
}
///|
/// The parts of a commit-pinned GitHub URL. No part is empty.
struct GitHubSource {
org : String
repo : String
commit : CommitSha
path : String
} derive(Eq, Debug)
///|
/// Parses `https://github.com///blob//` (or `tree`).
pub fn GitHubSource::GitHubSource(
url : String,
) -> GitHubSource raise GitHubUrlError {
let prefix = "https://github.com/"
guard url.has_prefix(prefix) else { raise NotGitHub(url) }
let rest = url[prefix.length():].to_owned()
let rest = match rest.find("#") {
Some(i) => rest[:i].to_owned()
None => rest
}
let parts = rest.split("/").map(s => s.to_owned()).collect()
guard parts.length() >= 5 && (parts[2] == "blob" || parts[2] == "tree") else {
raise MissingPart(url)
}
let path = parts[4:].to_owned().join("/")
guard parts[0].length() > 0 && parts[1].length() > 0 && path.length() > 0 else {
raise MissingPart(url)
}
{ org: parts[0], repo: parts[1], commit: CommitSha(parts[3]), path, }
}
///|
pub fn GitHubSource::org(self : GitHubSource) -> String {
self.org
}
///|
pub fn GitHubSource::repo(self : GitHubSource) -> String {
self.repo
}
///|
pub fn GitHubSource::commit(self : GitHubSource) -> CommitSha {
self.commit
}
///|
/// The file path in the repository.
pub fn GitHubSource::path(self : GitHubSource) -> String {
self.path
}
///|
pub extend RefTarget with Eq::{not_equal, equal}
///|
pub extend RefTarget with @debug.Debug::{to_repr}
///|
pub extend TypedReference with Eq::{not_equal, equal}
///|
pub extend TypedReference with @debug.Debug::{to_repr}
///|
pub extend DocRefError with Eq::{not_equal, equal}
///|
pub extend DocRefError with @debug.Debug::{to_repr}
///|
pub extend DocRef with Eq::{not_equal, equal}
///|
pub extend DocRef with @debug.Debug::{to_repr}
///|
pub extend GitHubUrlError with Eq::{not_equal, equal}
///|
pub extend GitHubUrlError with @debug.Debug::{to_repr}
///|
pub extend CommitSha with Eq::{not_equal, equal}
///|
pub extend CommitSha with Hash::{hash, hash_combine}
///|
pub extend CommitSha with @debug.Debug::{to_repr}
///|
pub extend GitHubSource with Eq::{not_equal, equal}
///|
pub extend GitHubSource with @debug.Debug::{to_repr}