use std::collections::BTreeMap;
use std::process::ExitCode;
use aion_awl::guide::{self, GrammarForm, GrammarPosition, GrammarRelation, GuideEntry, WordKind};
use anyhow::Context as _;
use serde::Serialize;
#[derive(Debug, Serialize)]
struct WireEntry {
word: &'static str,
kind: &'static str,
summary: &'static str,
examples: &'static [&'static str],
section: WireSection,
citation: String,
form: &'static str,
positions: Vec<&'static str>,
children: Vec<&'static str>,
siblings: Vec<&'static str>,
parents: Vec<&'static str>,
at_document_level: bool,
}
#[derive(Debug, Serialize)]
struct WireSection {
number: u8,
title: &'static str,
}
#[derive(Debug, Serialize)]
struct WireForm {
id: &'static str,
label: &'static str,
description: &'static str,
}
#[derive(Debug, Serialize)]
struct WirePosition {
id: &'static str,
label: &'static str,
}
#[derive(Debug, Serialize)]
struct WireRelation {
id: &'static str,
label: &'static str,
description: &'static str,
}
#[derive(Debug, Serialize)]
struct WireGuide {
guide: &'static str,
reference: &'static str,
authoring: &'static str,
workers: &'static str,
commands: &'static str,
declared_commands: &'static str,
forms: Vec<WireForm>,
positions: Vec<WirePosition>,
relations: Vec<WireRelation>,
entries: Vec<WireEntry>,
doc_comments: WireDocComments,
reference_text: &'static str,
}
#[derive(Debug, Serialize)]
struct WireDocComments {
summary: &'static str,
sites: Vec<WireDocSite>,
}
#[derive(Debug, Serialize)]
struct WireDocSite {
id: &'static str,
description: &'static str,
admitted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
refusal: Option<String>,
}
impl WireEntry {
fn of(entry: GuideEntry) -> anyhow::Result<Self> {
let form = guide::form_of(entry.word()).with_context(|| {
format!(
"`{}` has a glossary entry but the grammar table offers it in no position, so it \
has no form; place it in `aion_awl::guide::grammar`",
entry.word()
)
})?;
Ok(Self {
word: entry.word(),
kind: entry.kind().as_str(),
summary: entry.summary(),
examples: entry.examples(),
section: WireSection {
number: entry.section().number(),
title: entry.section().title(),
},
citation: entry.citation(),
form: form.id(),
positions: guide::positions_of(entry.word())
.into_iter()
.map(GrammarPosition::id)
.collect(),
children: entry.children(),
siblings: entry.siblings(),
parents: entry.parents(),
at_document_level: entry.stands_at_the_document_level(),
})
}
}
pub(crate) fn json(word: Option<&str>) -> anyhow::Result<String> {
let entries = match word {
Some(word) => guide::for_word(word)
.into_iter()
.map(WireEntry::of)
.collect::<anyhow::Result<Vec<_>>>()?,
None => guide::all()
.into_iter()
.map(WireEntry::of)
.collect::<anyhow::Result<Vec<_>>>()?,
};
let document = WireGuide {
guide: guide::GUIDE_PATH,
reference: guide::REFERENCE_PATH,
authoring: guide::AUTHORING_PATH,
workers: guide::WORKERS_PATH,
commands: guide::COMMANDS_PATH,
declared_commands: guide::DECLARED_COMMANDS_PATH,
forms: GrammarForm::ALL
.into_iter()
.map(|form| WireForm {
id: form.id(),
label: form.label(),
description: form.description(),
})
.collect(),
positions: GrammarPosition::ALL
.into_iter()
.map(|position| WirePosition {
id: position.id(),
label: position.label(),
})
.collect(),
relations: GrammarRelation::ALL
.into_iter()
.map(|relation| WireRelation {
id: relation.id(),
label: relation.label(),
description: relation.description(),
})
.collect(),
entries,
doc_comments: WireDocComments {
summary: guide::DOC_COMMENT_SUMMARY,
sites: guide::doc_comment_sites()
.into_iter()
.map(|site| WireDocSite {
id: site.id,
description: site.description,
admitted: site.admitted,
refusal: site.refusal,
})
.collect(),
},
reference_text: guide::reference_text(),
};
let mut rendered =
serde_json::to_string_pretty(&document).context("the glossary could not serialise")?;
rendered.push('\n');
Ok(rendered)
}
pub(crate) fn run(word: Option<&str>, json_output: bool, reference: bool) -> ExitCode {
if reference {
print!("{}", guide::reference_text());
return ExitCode::SUCCESS;
}
if let Some(word) = word {
let Some(entry) = guide::for_word(word) else {
eprintln!(
"error: `{word}` is not a word of AWL; run `aion awl guide` for every word the \
language has"
);
return ExitCode::FAILURE;
};
if json_output {
return emit_json(Some(entry.word()));
}
print!("{}", render_one(entry));
return ExitCode::SUCCESS;
}
if json_output {
return emit_json(None);
}
print!("{}", render_all());
ExitCode::SUCCESS
}
fn emit_json(word: Option<&str>) -> ExitCode {
match json(word) {
Ok(rendered) => {
print!("{rendered}");
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("error: the glossary could not be rendered as JSON: {error:#}");
ExitCode::FAILURE
}
}
}
fn kind_noun(kind: WordKind) -> &'static str {
match kind {
WordKind::Reserved => "reserved word",
WordKind::Positional => "positional word",
}
}
fn indented(example: &str) -> String {
let mut block = String::new();
for line in example.lines() {
if line.is_empty() {
block.push('\n');
} else {
block.push_str(" ");
block.push_str(line);
block.push('\n');
}
}
block
}
const RELATION_WIDTH: usize = 78;
fn relation_items(entry: GuideEntry, relation: GrammarRelation) -> Vec<String> {
let mut items: Vec<String> = Vec::new();
if relation == GrammarRelation::Parents && entry.stands_at_the_document_level() {
items.push("the document".to_owned());
}
items.extend(
relation
.words_of(entry.word())
.into_iter()
.map(str::to_owned),
);
items
}
fn relation_row(
entry: GuideEntry,
relation: GrammarRelation,
label_width: usize,
) -> Option<String> {
let items = relation_items(entry, relation);
if items.is_empty() {
return None;
}
let indent = 2 + label_width + 2;
let mut lines: Vec<String> = Vec::new();
let mut line = format!(" {:label_width$} ", relation.label());
for (index, item) in items.iter().enumerate() {
let piece = if index + 1 == items.len() {
item.clone()
} else {
format!("{item},")
};
if line.trim_end().len() > indent && line.len() + piece.len() + 1 > RELATION_WIDTH {
lines.push(line.trim_end().to_owned());
line = " ".repeat(indent);
} else if line.len() > indent {
line.push(' ');
}
line.push_str(&piece);
}
lines.push(line.trim_end().to_owned());
Some(lines.join("\n"))
}
fn relation_rows(entry: GuideEntry) -> String {
let label_width = GrammarRelation::ALL
.into_iter()
.map(|relation| relation.label().len())
.max()
.unwrap_or_default();
let rows: Vec<String> = GrammarRelation::ALL
.into_iter()
.filter_map(|relation| relation_row(entry, relation, label_width))
.collect();
if rows.is_empty() {
return String::new();
}
format!("{}\n", rows.join("\n"))
}
fn render_one(entry: GuideEntry) -> String {
let numbered = entry.examples().len() > 1;
let blocks = entry
.examples()
.iter()
.enumerate()
.map(|(index, example)| {
if numbered {
format!(" Example {}:\n\n{}", index + 1, indented(example))
} else {
indented(example)
}
})
.collect::<Vec<_>>()
.join("\n");
format!(
"{} ({})\n {}\n{}\n{blocks}\n {}\n Full reference: {}\n",
entry.word(),
kind_noun(entry.kind()),
entry.summary(),
relation_rows(entry),
entry.citation(),
guide::REFERENCE_COMMAND
)
}
fn render_all() -> String {
let entries = guide::all();
let width = entries
.iter()
.map(|entry| entry.word().len())
.max()
.unwrap_or_default();
let mut lines: Vec<String> = vec![
"AWL — every word of the language, and where it is explained in full.".to_owned(),
"Run `aion awl guide <word>` for one word with a checked example.".to_owned(),
String::new(),
format!(" Full reference {}", guide::REFERENCE_COMMAND),
format!(" Guide {}", guide::GUIDE_PATH),
format!(" Reference {}", guide::REFERENCE_PATH),
format!(" Authoring path {}", guide::AUTHORING_PATH),
format!(" Workers {}", guide::WORKERS_PATH),
format!(" Commands {}", guide::COMMANDS_PATH),
format!(" Declared bodies {}", guide::DECLARED_COMMANDS_PATH),
];
let mut sections: BTreeMap<u8, Vec<GuideEntry>> = BTreeMap::new();
for entry in entries {
sections
.entry(entry.section().number())
.or_default()
.push(entry);
}
for (number, grouped) in sections {
let title = grouped.first().map_or("", |entry| entry.section().title());
lines.push(String::new());
lines.push(format!("§{number} — {title}"));
for entry in grouped {
lines.push(format!(
" {:width$} {}",
entry.word(),
entry.summary(),
width = width
));
}
}
let mut rendered = lines.join("\n");
rendered.push('\n');
rendered
}
#[cfg(test)]
#[path = "awl_guide_tests.rs"]
mod awl_guide_tests;