use crate::entity::EntityId;
use crate::render;
#[derive(Debug, Clone, Default)]
pub struct LlmsTxtContext {
pub authority: Option<String>,
pub href_prefix: String,
pub wider_project: Vec<(String, String)>,
}
pub fn entity_md_link(href_prefix: &str, id: &str, title: &str) -> String {
let text = title.replace('[', "(").replace(']', ")");
if href_prefix.is_empty() {
format!("[{text}](entity/{id})")
} else {
format!("[{text}]({href_prefix}/entity/{id})")
}
}
pub fn strip_frontmatter(md: &str) -> String {
let mut lines = md.lines();
if lines.next() == Some("---") {
let mut closed = false;
let mut body: Vec<&str> = Vec::new();
for line in lines {
if !closed && line == "---" {
closed = true;
continue;
}
if closed {
body.push(line);
}
}
if closed {
return body.join("\n").trim_start_matches('\n').to_string();
}
}
md.to_string()
}
pub fn linkify_wikilinks(
body: String,
id_titles: &[(String, String)],
href_prefix: &str,
source_mem: &str,
) -> String {
let mut by_id: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
let mut local: std::collections::HashMap<&str, (&str, &str)> = std::collections::HashMap::new();
let mut foreign: std::collections::HashMap<&str, Option<(&str, &str)>> =
std::collections::HashMap::new();
for (id, title) in id_titles {
by_id.insert(id.as_str(), title.as_str());
if let Some((mem, slug)) = id.split_once("--") {
if mem == source_mem {
local.insert(slug, (id.as_str(), title.as_str()));
} else {
foreign
.entry(slug)
.and_modify(|e| *e = None)
.or_insert(Some((id.as_str(), title.as_str())));
}
}
}
let resolve = |target: &str| -> Option<(String, String)> {
if let Some(title) = by_id.get(target) {
return Some((target.to_string(), title.to_string()));
}
if !target.contains("::")
&& let Some((prefix, slug)) = target.split_once(':')
&& !prefix.is_empty()
&& !slug.is_empty()
{
let id = crate::entity::EntityId::new(prefix, slug);
return by_id
.get(id.as_ref())
.map(|title| (id.to_string(), title.to_string()));
}
if let Some((id, title)) = local.get(target) {
return Some((id.to_string(), title.to_string()));
}
foreign
.get(target)
.copied()
.flatten()
.map(|(id, title)| (id.to_string(), title.to_string()))
};
let masked = crate::markdown::mask_code_blocks_and_spans(&body);
let mut out = String::with_capacity(body.len());
let mut cursor = 0usize;
let bytes = masked.as_bytes();
let mut i = 0usize;
while i + 1 < bytes.len() {
if bytes[i] == b'['
&& bytes[i + 1] == b'['
&& let Some(rel) = masked[i + 2..].find("]]")
{
let inner_start = i + 2;
let inner_end = inner_start + rel;
let inner = &body[inner_start..inner_end];
let label = inner.split_once('|').map(|(_, l)| l.trim());
let target = crate::entity::id::strip_wiki_link_decorations(inner);
out.push_str(&body[cursor..i]);
match resolve(&target) {
Some((id, title)) => {
out.push_str(&entity_md_link(href_prefix, &id, label.unwrap_or(&title)))
}
None => out.push_str(label.unwrap_or(&target)),
}
cursor = inner_end + 2;
i = inner_end + 2;
continue;
}
i += 1;
}
out.push_str(&body[cursor..]);
out
}
impl crate::Engine {
pub fn render_llms_txt(
&self,
mem: &str,
ctx: &LlmsTxtContext,
) -> Result<String, crate::engine::EngineError> {
let mounted = self
.mounts
.iter()
.find(|m| m.mount.mem == mem)
.ok_or_else(|| crate::engine::EngineError::UnknownMem(mem.to_string()))?;
let schema_pin = mounted
.mount
.schema
.as_ref()
.map(|s| s.as_display())
.unwrap_or_default();
let config = self.mem_config_for(mem);
let subject = config
.and_then(|c| c.description.clone())
.unwrap_or_else(|| mem.to_string());
let provenance = match (self.mem_origin_class(mem), ctx.authority.is_some()) {
(crate::render::OriginClass::FirstParty, true) => {
"first-party (this deployment vouches for the content as its own)"
}
(crate::render::OriginClass::ThirdParty, true) => {
"third-party (this deployment does not vouch for the content)"
}
(crate::render::OriginClass::FirstParty, false) => {
"first-party (authored in the workspace this export came from)"
}
(crate::render::OriginClass::ThirdParty, false) => {
"third-party (a read-only mount — someone else's published content)"
}
};
let id_titles = self.entity_id_titles();
let mut ids: Vec<String> = self
.store
.all_entities()
.filter(|e| e.mem == mem && !e.stub)
.map(|e| e.id.to_string())
.collect();
ids.sort();
let count = ids.len();
let heading = match &ctx.authority {
Some(a) => format!("# {a} — {subject}\n\nAuthority: {a}\n"),
None if subject == mem => format!("# {mem}\n\nMem: {mem}\n"),
None => format!("# {mem} — {subject}\n\nMem: {mem}\n"),
};
let wider = if ctx.wider_project.is_empty() {
String::new()
} else {
let lines: String = ctx
.wider_project
.iter()
.map(|(url, what)| format!("- {url} — {what}.\n"))
.collect();
format!("The wider project:\n{lines}\n")
};
let links_sentence = if ctx.href_prefix.is_empty() {
"Entity references are relative links to that entity's own page."
} else {
"Entity references are absolute links to that entity's own page."
};
let mut out = format!(
"{heading}\
Subject: {subject}\n\
Schema: {schema_pin}\n\
Entities: {count}\n\
Provenance: {provenance}\n\n\
{wider}\
Every non-stub entity of this Memstead graph follows, once, with its type and \
sections. {links_sentence}\n\n\
---\n\n"
);
for id in &ids {
let Some(entity) = self.get_entity(&EntityId::canonical(id)) else {
continue;
};
let md = strip_frontmatter(&render::render_entity_markdown(entity, None));
let typed = match md.split_once('\n') {
Some((title_line, rest)) => format!(
"{title_line}\n\n_Type: {}_\n\n{}",
entity.entity_type,
rest.trim_start_matches('\n')
),
None => format!("{md}\n\n_Type: {}_", entity.entity_type),
};
out.push_str(&linkify_wikilinks(typed, &id_titles, &ctx.href_prefix, mem));
if !out.ends_with('\n') {
out.push('\n');
}
out.push_str("\n---\n\n");
}
Ok(out)
}
fn entity_id_titles(&self) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = self
.store
.all_entities()
.filter(|e| !e.stub)
.map(|e| (e.id.to_string(), e.title.clone()))
.collect();
out.sort();
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn titles() -> Vec<(String, String)> {
vec![
("engine--mem".to_string(), "Mem".to_string()),
("flagship--mem".to_string(), "Mem".to_string()),
("engine--pipeline".to_string(), "Pipeline".to_string()),
]
}
#[test]
fn wiki_links_resolve_in_three_passes_and_never_guess() {
let t = titles();
assert_eq!(
linkify_wikilinks("See [[engine--pipeline]].".to_string(), &t, "", "engine"),
"See [Pipeline](entity/engine--pipeline)."
);
assert_eq!(
linkify_wikilinks("A [[mem]].".to_string(), &t, "", "engine"),
"A [Mem](entity/engine--mem)."
);
assert_eq!(
linkify_wikilinks("A [[mem]].".to_string(), &t, "", "flagship"),
"A [Mem](entity/flagship--mem)."
);
assert_eq!(
linkify_wikilinks("A [[pipeline]].".to_string(), &t, "", "flagship"),
"A [Pipeline](entity/engine--pipeline)."
);
assert_eq!(
linkify_wikilinks("A [[mem]].".to_string(), &t, "", "plugin"),
"A mem.",
"an ambiguous foreign slug degrades to plain text, never a guess"
);
}
#[test]
fn only_two_link_forms_are_emitted() {
let t = titles();
let rel = linkify_wikilinks("[[engine--mem]]".to_string(), &t, "", "engine");
assert_eq!(rel, "[Mem](entity/engine--mem)");
assert!(!rel.contains("(/entity/"), "never root-relative: {rel}");
let abs = linkify_wikilinks(
"[[engine--mem]]".to_string(),
&t,
"https://example.com",
"engine",
);
assert_eq!(abs, "[Mem](https://example.com/entity/engine--mem)");
}
#[test]
fn bracketed_titles_cannot_break_the_link() {
let t = vec![("m--x".to_string(), "A [bracketed] title".to_string())];
assert_eq!(
linkify_wikilinks("[[m--x]]".to_string(), &t, "", "m"),
"[A (bracketed) title](entity/m--x)"
);
}
#[test]
fn code_spans_and_fences_are_never_rewritten() {
let t = titles();
let body = "Prose [[engine--mem]] resolves.\n\n\
Inline `[[engine--mem]]` does not.\n\n\
```\n[[engine--mem]]\n```\n";
let out = linkify_wikilinks(body.to_string(), &t, "", "engine");
assert!(
out.contains("Prose [Mem](entity/engine--mem) resolves."),
"prose still resolves: {out}"
);
assert!(
out.contains("Inline `[[engine--mem]]` does not."),
"an inline code span is left alone: {out}"
);
assert!(
out.contains("```\n[[engine--mem]]\n```"),
"a fenced block is left alone: {out}"
);
}
#[test]
fn alias_and_anchor_wiki_link_forms_resolve() {
let t = titles();
assert_eq!(
linkify_wikilinks("See [[engine--mem|the mem]].".to_string(), &t, "", "engine"),
"See [the mem](entity/engine--mem)."
);
assert_eq!(
linkify_wikilinks(
"See [[engine--mem#Identity]].".to_string(),
&t,
"",
"engine"
),
"See [Mem](entity/engine--mem)."
);
assert_eq!(
linkify_wikilinks("See [[mem#Identity|here]].".to_string(), &t, "", "engine"),
"See [here](entity/engine--mem)."
);
assert_eq!(
linkify_wikilinks("See [[ghost|that thing]].".to_string(), &t, "", "engine"),
"See that thing."
);
}
#[test]
fn colon_cross_mem_form_resolves_as_a_qualified_reference() {
let t = titles();
assert_eq!(
linkify_wikilinks("See [[flagship:mem]].".to_string(), &t, "", "engine"),
"See [Mem](entity/flagship--mem)."
);
assert_eq!(
linkify_wikilinks(
"See [[engine:mem|the mem]].".to_string(),
&t,
"",
"flagship"
),
"See [the mem](entity/engine--mem)."
);
assert_eq!(
linkify_wikilinks("See [[flagship:pipeline]].".to_string(), &t, "", "engine"),
"See flagship:pipeline."
);
}
#[test]
fn an_unresolvable_reference_degrades_to_plain_text() {
let t = titles();
assert_eq!(
linkify_wikilinks("See [[ghost]].".to_string(), &t, "", "engine"),
"See ghost."
);
assert_eq!(
linkify_wikilinks(
"- **USES**: [[engine--phantom]]".to_string(),
&t,
"",
"engine"
),
"- **USES**: engine--phantom"
);
}
#[test]
fn frontmatter_is_stripped_only_when_present() {
assert_eq!(
strip_frontmatter("---\ntype: spec\n---\n\n# Title\n\nBody."),
"# Title\n\nBody."
);
assert_eq!(strip_frontmatter("# Title\n\nBody."), "# Title\n\nBody.");
assert_eq!(strip_frontmatter("---\nno close"), "---\nno close");
}
}