use super::{ContentItem, ContentSection};
use crate::actions::deps::{DependencyViewGenerator, DependencyViewOptions};
use crate::color::set_colorize;
use crate::convert::doc::common::{
make_figure_label, make_label, make_listing_label, make_section_label, make_table_label,
ArgumentType, BlockFormat, Formatter, LinkFormat, PageFormat, TextFormat,
};
use crate::convert::doc::{AnnotationCategories, BookConfig, DocumentationWriter, Heading};
use crate::convert::rdf::RdfModelGenerator;
use crate::draw::OutputFormat;
use crate::Generator;
use console::Term;
use indicatif::{ProgressBar, ProgressStyle};
use sdml_core::error::Error;
use sdml_core::load::ModuleLoader;
use sdml_core::model::annotations::HasAnnotations;
use sdml_core::model::check::MaybeIncomplete;
use sdml_core::model::constraints::ConstraintBody;
use sdml_core::model::definitions::{
DatatypeDef, Definition, EntityDef, EnumDef, EventDef, HasMembers, HasVariants, PropertyDef,
StructureDef, TypeClassDef, UnionDef,
};
use sdml_core::model::identifiers::{Identifier, IdentifierReference};
use sdml_core::model::members::{Member, MemberKind, PseudoSequenceType, DEFAULT_CARDINALITY};
use sdml_core::model::modules::Module;
use sdml_core::model::values::{LanguageString, SimpleValue, Value};
use sdml_core::model::{HasBody, HasName, HasNameReference, HasOptionalBody};
use sdml_core::store::ModuleStore;
use sdml_errors::diagnostics::UseColor;
use sdml_errors::Source;
use std::collections::HashMap;
use std::fmt::Display;
use std::fs::{read_to_string, File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use tracing::trace;
#[derive(Debug, Default)]
pub struct DocumentationGenerator {
source: Option<Source>,
annotation_categories: AnnotationCategories,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct OrgModeFormatter;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SourceBlockArguments {
pub add_line_numbers: bool,
pub continue_line_numbers: bool,
pub line_number_start: Option<usize>,
pub remove_source_labels: bool,
pub label_syntax: Option<String>,
pub preserve_global_indentation: bool,
pub eval: SourceEval,
pub exports: SourceExports,
pub results: SourceResults,
pub session: Option<String>,
pub cmd_line: Option<String>,
pub cache: bool,
pub noweb: bool,
pub hlines: bool,
pub tangle: bool,
pub rest: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum SourceEval {
#[default]
Yes,
No,
Query,
NoExport,
QueryExport,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum SourceExports {
None,
#[default]
Code,
Results,
Both,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SourceResults {
pub collection: Option<SourceResultsCollection>,
pub as_type: Option<SourceResultsType>,
pub format: Option<SourceResultsFormat>,
pub handling: Option<SourceResultsHandling>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum SourceResultsCollection {
#[default]
Value,
Output,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SourceResultsFormat {
Raw,
Code,
Drawer,
Html,
LaTeX,
Link,
Graphics,
Org,
PrettyPrint,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SourceResultsFile {
pub file_name: Option<PathBuf>,
pub output_dir: Option<PathBuf>,
pub file_extension: Option<String>,
pub file_mode: Option<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SourceResultsType {
Table,
Vector,
List,
Scalar,
Verbatim,
File(SourceResultsFile),
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum SourceResultsHandling {
#[default]
Replace,
Silent,
Discard,
Append,
Prepend,
None,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct IncludeArguments {
pub block_name: Option<String>,
pub language: Option<String>,
pub min_level: Option<usize>,
pub from_line: Option<usize>,
pub to_line: Option<usize>,
pub only_contents: bool,
pub rest: Vec<String>,
}
pub fn write_heading(heading: Heading, writer: &mut dyn Write) -> Result<(), Error> {
let heading_string = if let Some(label) = heading.label() {
FORMATTER.heading_with_id(heading.title(), heading.level().into(), label)
} else {
FORMATTER.heading(heading.title(), heading.level().into())
};
writer.write_all(heading_string.as_bytes())?;
Ok(())
}
const FORMATTER: OrgModeFormatter = OrgModeFormatter::new();
type Appendix = String;
impl Generator for DocumentationGenerator {
type Options = AnnotationCategories;
fn generate_with_options<W>(
&mut self,
module: &Module,
cache: &impl ModuleStore,
options: Self::Options,
_: Option<PathBuf>,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
self.annotation_categories = options;
set_colorize(UseColor::Never);
self.write_preamble(
&format!("Module {}", FORMATTER.mono(module.name())),
"en",
true,
writer,
)?;
self.write_module_description(Heading::new_section("Description"), module, cache, writer)?;
self.write_module_uml_overview(module, cache, writer)?;
write_heading(Heading::new_section("Dependencies"), writer)?;
self.write_module_dependency_table(module, cache, writer)?;
write_heading(Heading::new_section("Definitions"), writer)?;
self.write_module_definitions(Heading::LEVEL_SECTION, module, cache, writer)?;
self.write_module_dependency_graph(
Heading::new_section("Appendix: Dependency Graph"),
module,
cache,
writer,
)?;
self.write_module_sdml_listing(
Heading::new_section("Appendix: SDML Source"),
module,
writer,
)?;
self.write_module_rdf_listing(
Heading::new_section("Appendix: RDF Source"),
module,
cache,
writer,
)?;
Ok(())
}
}
impl DocumentationWriter<IncludeArguments, SourceBlockArguments, OrgModeFormatter>
for DocumentationGenerator
{
fn formatter() -> &'static OrgModeFormatter {
&FORMATTER
}
fn write_book<T>(
&mut self,
loader: &mut T,
cache: &mut impl ModuleStore,
config: BookConfig,
) -> Result<(), Error>
where
T: ModuleLoader,
{
trace!(config = ?&config, "DocumentationGenerator::write()");
let console = Term::stdout();
let config_item_count = config.item_count();
let index_progress = ProgressBar::new((config_item_count + 1) as u64).with_style(
ProgressStyle::default_bar()
.template("{bar} {pos:>3}/{len:3}")
.unwrap(),
);
set_colorize(UseColor::Never);
self.annotation_categories = config.annotation_categories.clone();
let output_directory = config.output_file.parent().unwrap_or(Path::new("./"));
if console.is_term() {
console.write_line(&format!(
"Creating documentation in directory {output_directory:?}"
))?;
}
trace!(index_file = ?config.output_file, "Creating the index file");
let mut index_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&config.output_file)?;
trace!("Writing preamble");
let default_language = "en".to_string();
self.write_preamble(
&config.title,
config
.language
.as_ref()
.unwrap_or(&default_language)
.as_str(),
config.options.include_toc,
&mut index_file,
)?;
index_progress.inc(1);
if let Some(file_name) = &config.introduction {
trace!("Including introduction");
include_file(file_name, config.options.multi_part, &mut index_file)?;
index_progress.inc(1);
}
trace!("Write top-level section");
let appendices = self.write_section(
loader,
cache,
&config,
1,
&config.content,
true,
&mut index_file,
&index_progress,
)?;
write_heading(Heading::new_section("Appendices"), &mut index_file)?;
let mut appendices_file = if config.options.multi_part {
let directory = config.output_file.parent().unwrap_or(Path::new("./"));
let appendices_path = directory.join("_appendices.org");
index_file.write_all(
FORMATTER
.include_file_with_args(
appendices_path.to_string_lossy(),
IncludeArguments::default(),
)
.as_bytes(),
)?;
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(appendices_path)?
} else {
index_file
};
trace!("Writing appendices");
for appendix in appendices {
appendices_file.write_all(appendix.as_bytes())?;
appendices_file.write_all(b"\n")?;
}
index_progress.inc(1);
index_progress.finish_and_clear();
Ok(())
}
fn write_preamble<W>(
&mut self,
title: &str,
language: &str,
include_toc: bool,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
let source_block_arguments = SourceBlockArguments::default();
let headers = [
FORMATTER.title(title),
FORMATTER.language(language),
FORMATTER
.style_uri("https://fniessen.github.io/org-html-themes/org/theme-readtheorg.setup"),
FORMATTER.style_inline("<style>table {{ min-width: 50%; }}</style>"),
FORMATTER.style_inline("<style>img {{ max-width: 800px; height: auto; }}</style>"),
FORMATTER.style_inline("<style>div.figure {{ text-align: center; }}</style>"),
FORMATTER.options(format!(
"h:5 toc:{} ^:{{}}",
if include_toc { "3" } else { "'nil" }
)),
String::from("\n"),
FORMATTER.source(
"emacs-lisp",
"(require 'ob-dot)\n(require 'ob-sdml)",
SourceBlockArguments {
exports: SourceExports::None,
..source_block_arguments
},
),
String::from("\n"),
]
.join("");
writer.write_all(headers.as_bytes())?;
Ok(())
}
fn write_module_description<W>(
&mut self,
heading: super::Heading,
module: &Module,
_: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
write_heading(heading, writer)?;
let module_body = module.body();
write_annotations(module_body, &self.annotation_categories, writer)?;
Ok(())
}
fn write_module_definitions<W>(
&mut self,
heading_level: u8,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
write_definitions(
heading_level,
module,
cache,
&self.annotation_categories,
writer,
)?;
Ok(())
}
fn write_module_uml_overview<W>(
&mut self,
module: &Module,
_: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
let name = module.name();
let link_label = format!("module-{name}-uml-class");
let listing_name = format!("lst:{link_label}");
let figure_name = format!("fig:{link_label}");
if let Some(source) = &self.source {
writer.write_all(FORMATTER.name_target(&listing_name).as_bytes())?;
writer.write_all(
FORMATTER
.source(
"sdml",
source,
SourceBlockArguments {
cmd_line: Some(
"draw --diagram uml-class --output-format svg".to_string(),
),
results: SourceResults {
as_type: Some(SourceResultsType::File(SourceResultsFile {
file_name: Some(PathBuf::from(format!("./{link_label}.svg"))),
..SourceResultsFile::default()
})),
..SourceResults::default()
},
exports: SourceExports::Results,
noweb: true,
..SourceBlockArguments::default()
},
)
.as_bytes(),
)?;
writer.write_all(b"\n")?;
writer.write_all(
FORMATTER
.figure_with(
figure_name,
format!("Module {} UML Class Diagram", FORMATTER.mono(name)),
format!("file:./{link_label}.svg"),
Some(listing_name),
)
.as_bytes(),
)?;
}
Ok(())
}
fn write_module_sdml_listing<W>(
&mut self,
heading: super::Heading,
module: &Module,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
if self.source.is_some() {
write_heading(heading, writer)?;
let name = module.name();
let link_label = format!("module-{name}-src-sdml");
let link_back_label = format!("module-{name}-uml-class");
writer.write_all(b"\n")?;
writer.write_all(
FORMATTER
.source_with(
"sdml",
FORMATTER.noweb_target(format!("lst:{link_back_label}")),
SourceBlockArguments {
exports: SourceExports::Code,
noweb: true,
..SourceBlockArguments::default()
},
format!("lst:{link_label}"),
format!("Module {} SDML Source", FORMATTER.mono(name)),
)
.as_bytes(),
)?;
}
Ok(())
}
fn write_module_rdf_listing<W>(
&mut self,
heading: super::Heading,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
write_heading(heading, writer)?;
let mut generator = RdfModelGenerator::default();
let rdf =
generator.generate_to_string(module, cache, Default::default(), Default::default())?;
let name = module.name();
let link_label = format!("module-{name}-src-rdf");
writer.write_all(b"\n")?;
writer.write_all(
FORMATTER
.source_with(
"ttl",
rdf,
SourceBlockArguments::default(),
format!("lst:{link_label}"),
format!("Module {} RDF Source", FORMATTER.mono(name)),
)
.as_bytes(),
)?;
Ok(())
}
fn write_module_dependency_table<W>(
&mut self,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
let module_body = module.body();
let mut imported_modules: Vec<&Identifier> =
module_body.imported_modules().into_iter().collect();
if !imported_modules.is_empty() {
imported_modules.sort();
let mut data: Vec<Vec<String>> = vec![vec!["Name".to_string(), "Base IRI".to_string()]];
for import in imported_modules {
let base = if let Some(module) = cache.get(import) {
if let Some(base_uri) = module.base_uri() {
base_uri.value().to_string()
} else {
FORMATTER.italic("relative")
}
} else {
FORMATTER.italic("not loaded")
};
data.push(vec![FORMATTER.mono(import), base]);
}
let name = module.name();
writer.write_all(
FORMATTER
.table_with(
&data,
true,
make_table_label(&["module", name.as_ref(), "imports"]),
format!("Module {} Imports", FORMATTER.mono(name)),
)
.as_bytes(),
)?;
writer.write_all(b"\n")?;
}
Ok(())
}
fn write_module_dependency_graph<W>(
&mut self,
heading: super::Heading,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
write_heading(heading, writer)?;
writer.write_all(b"\n")?;
let options = DependencyViewOptions::default().as_dot_graph(OutputFormat::Source);
let mut generator = DependencyViewGenerator::default();
let dot_graph = generator.generate_to_string(module, cache, options, Default::default())?;
let name = module.name();
let link_label = format!("module-{name}-dep-graph");
writer.write_all(
FORMATTER
.name_target(format!("lst:{link_label}"))
.as_bytes(),
)?;
writer.write_all(
FORMATTER
.source(
"dot",
dot_graph,
SourceBlockArguments {
exports: SourceExports::Results,
results: SourceResults {
as_type: Some(SourceResultsType::File(SourceResultsFile {
file_name: Some(PathBuf::from(format!("./{link_label}.svg"))),
..SourceResultsFile::default()
})),
..SourceResults::default()
},
..SourceBlockArguments::default()
},
)
.as_bytes(),
)?;
writer.write_all(b"\n")?;
writer.write_all(
FORMATTER
.figure_with(
make_figure_label(&[&link_label]),
format!("Module {} Dependency Graph", FORMATTER.mono(name)),
format!("./{link_label}.svg"),
Some(make_listing_label(&[link_label])),
)
.as_bytes(),
)?;
Ok(())
}
}
impl DocumentationGenerator {
pub fn new(source: Option<Source>, annotation_categories: AnnotationCategories) -> Self {
Self {
source,
annotation_categories,
}
}
pub fn without_source() -> Self {
Self::default()
}
pub fn with_source(source: Source) -> Self {
Self::new(Some(source), Default::default())
}
#[allow(clippy::too_many_arguments)]
fn write_section(
&mut self,
loader: &mut impl ModuleLoader,
cache: &mut impl ModuleStore,
config: &BookConfig,
level: u8,
section: &ContentSection,
is_top: bool,
index_file: &mut File,
index_progress: &ProgressBar,
) -> Result<Vec<Appendix>, Error> {
trace!(
level = level,
heading = §ion.heading,
count = section.items.len(),
"DocumentationGenerator::write_section"
);
let mut appendices = Vec::default();
let heading = Heading::new(level, §ion.heading);
write_heading(heading, index_file)?;
for item in §ion.items {
let additional = self.write_item(
loader,
cache,
config,
level,
item,
is_top,
index_file,
index_progress,
)?;
appendices.extend(additional);
}
Ok(appendices)
}
#[allow(clippy::too_many_arguments)]
fn write_item(
&mut self,
loader: &mut impl ModuleLoader,
cache: &mut impl ModuleStore,
config: &BookConfig,
level: u8,
item: &ContentItem,
is_top: bool,
index_file: &mut File,
index_progress: &ProgressBar,
) -> Result<Vec<Appendix>, Error> {
trace!(level = level,
item = ?item,
"DocumentationGenerator::write_item");
let mut result = Vec::default();
match item {
ContentItem::SourceFile { module_path } => {
let _file_name = if let Ok(canonical) = module_path.canonicalize() {
canonical
} else {
module_path.clone()
};
index_progress.inc(1);
todo!();
}
ContentItem::SourceModule { module } => {
let module_name = loader.load(module, loader.get_file_id(module), cache, true)?;
result.push(self.write_module(
loader,
cache,
config,
level + 1,
&module_name,
index_file,
)?);
index_progress.inc(1);
}
ContentItem::Include { include_file_path } => {
include_file(include_file_path, config.options.multi_part, index_file)?;
index_progress.inc(1);
}
ContentItem::Section { sub_section } => result.extend(self.write_section(
loader,
cache,
config,
if is_top { level } else { level + 1 },
sub_section,
false,
index_file,
index_progress,
)?),
}
Ok(result)
}
fn write_module(
&mut self,
loader: &mut impl ModuleLoader,
cache: &mut impl ModuleStore,
config: &BookConfig,
level: u8,
module_name: &Identifier,
index_file: &mut File,
) -> Result<Appendix, Error> {
trace!(
level = level,
module = ?module_name,
"DocumentationGenerator::write_module"
);
if config.options.multi_part {
let directory = config.output_file.parent().unwrap_or(Path::new("./"));
let module_path = directory.join(format!("{module_name}.org"));
index_file.write_all(
FORMATTER
.include_file(module_path.to_string_lossy())
.as_bytes(),
)?;
let mut module_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(module_path)?;
self.write_module_file(loader, cache, level, module_name, &mut module_file)
} else {
self.write_module_file(loader, cache, level, module_name, index_file)
}
}
fn write_module_file(
&mut self,
loader: &mut impl ModuleLoader,
cache: &impl ModuleStore,
level: u8,
module_name: &Identifier,
file: &mut File,
) -> Result<Appendix, Error> {
let module = cache.get(module_name).unwrap();
self.source = loader.get_source_by_name(module_name);
self.write_module_actual(cache, level, module, file)
}
fn write_module_actual(
&mut self,
cache: &impl ModuleStore,
level: u8,
module: &Module,
index_file: &mut File,
) -> Result<Appendix, Error> {
let module_name = module.name();
trace!(level = level, "DocumentationGenerator::write_module_actual");
let heading = Heading::new(level, format!("Module ={module_name}="))
.with_label(make_label("sec", &[module_name.as_ref()]));
self.write_module_description(heading, module, cache, index_file)?;
self.write_module_uml_overview(module, cache, index_file)?;
index_file.write_all(Self::formatter().pseudo_heading("Dependencies").as_bytes())?;
self.write_module_dependency_table(module, cache, index_file)?;
index_file.write_all(Self::formatter().pseudo_heading("Definitions").as_bytes())?;
self.write_module_definitions(level, module, cache, index_file)?;
let mut buf = BufWriter::new(Vec::new());
let heading = format!("Module ={module_name}=");
write_heading(Heading::new_subsection(heading), &mut buf)?;
self.write_module_dependency_graph(
Heading::new_subsubsection("Dependency Graph"),
module,
cache,
&mut buf,
)?;
self.write_module_sdml_listing(
Heading::new_subsubsection("SDML Source"),
module,
&mut buf,
)?;
self.write_module_rdf_listing(
Heading::new_subsubsection("RDF Representation"),
module,
cache,
&mut buf,
)?;
let bytes = buf.into_inner().unwrap();
Ok(String::from_utf8(bytes)?)
}
}
impl PageFormat<IncludeArguments> for OrgModeFormatter {
#[inline(always)]
fn title<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("#+TITLE: {}", s.as_ref())
}
#[inline(always)]
fn language<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("#+LANGUAGE: {}", s.as_ref())
}
#[inline(always)]
fn style_uri<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("#+SETUPFILE: {}", s.as_ref())
}
#[inline(always)]
fn style_inline<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("#+HTML_HEAD: {}", s.as_ref())
}
#[inline(always)]
fn options<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("#+OPTIONS: {}", s.as_ref())
}
#[inline(always)]
fn include_file<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("#+INCLUDE: {}", s.as_ref())
}
#[inline(always)]
fn include_file_with_args<S>(&self, s: S, arguments: IncludeArguments) -> String
where
S: AsRef<str>,
{
let arguments: String = arguments.into();
let arg_spacer = if arguments.is_empty() { "" } else { " " };
format!("#+INCLUDE: {}{}{}", s.as_ref(), arg_spacer, arguments)
}
}
impl BlockFormat<SourceBlockArguments> for OrgModeFormatter {
#[inline(always)]
fn heading<S>(&self, text: S, depth: usize) -> String
where
S: AsRef<str>,
{
if depth == 0 {
self.pseudo_heading(text)
} else {
format!(
"{} {}\n\n",
&(0..depth).map(|_| "*").collect::<String>(),
text.as_ref()
)
}
}
#[inline(always)]
fn heading_with_id<S1, S2>(&self, text: S1, depth: usize, id: S2) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
if depth == 0 {
self.pseudo_heading_with_id(text, id)
} else {
format!(
"{} {}\n{}\n\n",
&(0..depth).map(|_| "*").collect::<String>(),
text.as_ref(),
self.property_drawer(
[("CUSTOM_ID".to_string(), id.as_ref().to_string())]
.into_iter()
.collect()
)
)
}
}
#[inline(always)]
fn pseudo_heading<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("*{}*\n\n", s.as_ref())
}
#[inline(always)]
fn pseudo_heading_with_id<S1, S2>(&self, text: S1, id: S2) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
format!("<<{}>>{}", id.as_ref(), self.pseudo_heading(text))
}
#[inline(always)]
fn paragraph<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("{}\n\n", s.as_ref())
}
#[inline(always)]
fn quote<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
self.make_block("QUOTE", s)
}
#[inline(always)]
fn verbatim<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
self.make_block("VERBATIM", s)
}
#[inline(always)]
fn indented_verbatim<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!(" : {}\n\n", s.as_ref().replace('\n', "\n : "))
}
#[inline(always)]
fn example<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
self.make_block("EXAMPLE", s)
}
#[inline(always)]
fn export<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
self.make_block("EXPORT", s)
}
#[inline(always)]
fn center<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
self.make_block("CENTER", s)
}
#[inline(always)]
fn comment<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
self.make_block("COMMENT", s)
}
#[inline(always)]
fn line_comment<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("# {}\n\n", s.as_ref().replace('\n', "\n# "))
}
#[inline(always)]
fn source<S1, S2>(&self, language: S1, src: S2, arguments: SourceBlockArguments) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let arguments: String = vec![format!("{} ", language.as_ref()), arguments.into()]
.into_iter()
.collect::<String>();
self.make_block_with_args("SRC", src, arguments)
}
#[inline(always)]
fn source_with<S1, S2, S3, S4>(
&self,
language: S1,
src: S2,
arguments: SourceBlockArguments,
id: S3,
caption: S4,
) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
S3: AsRef<str>,
S4: AsRef<str>,
{
format!(
"{}{}{}",
self.name_target(id),
self.caption(caption),
self.source(language, src, arguments)
)
}
#[inline(always)]
fn inline_source<S1, S2>(
&self,
language: S1,
src: S2,
arguments: SourceBlockArguments,
) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let arguments: String = arguments.into();
format!(
"src_{}[{}]{{{}}}",
language.as_ref(),
arguments,
src.as_ref()
)
}
#[inline(always)]
fn figure_with<S1, S2, S3, S4>(
&self,
id: S1,
caption: S2,
file_name: S3,
result_of: Option<S4>,
) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
S3: AsRef<str>,
S4: AsRef<str>,
{
format!(
"{}{}{}[[{}]]\n\n",
self.name_target(id),
self.caption(caption),
if let Some(result_of) = result_of {
self.results_of(result_of)
} else {
String::default()
},
file_name.as_ref()
)
}
#[inline(always)]
fn caption<S>(&self, text: S) -> String
where
S: AsRef<str>,
{
format!("#+CAPTION: {}", text.as_ref())
}
#[inline(always)]
fn results_of<S>(&self, id: S) -> String
where
S: AsRef<str>,
{
format!("#+RESULTS: {}", id.as_ref())
}
#[inline(always)]
fn ordered_list(&self, items: &[String], indentation: usize) -> String {
let indentation = &(0..(indentation * 2)).map(|_| " ").collect::<String>();
format!(
"{}\n\n",
items
.iter()
.map(|s| format!("{}- {}", indentation, s))
.collect::<Vec<String>>()
.join("\n")
)
}
#[inline(always)]
fn unordered_list(&self, items: &[String], indentation: usize) -> String {
let indentation = &(0..(indentation * 3)).map(|_| " ").collect::<String>();
format!(
"{}\n\n",
items
.iter()
.map(|s| format!("{}1. {}", indentation, s))
.collect::<Vec<String>>()
.join("\n")
)
}
#[inline(always)]
fn definition_list(&self, items: &HashMap<String, String>) -> String {
self.unordered_list(
&items
.iter()
.map(|(key, value)| format!("{key} :: {value}"))
.collect::<Vec<String>>(),
Default::default(),
)
}
#[inline(always)]
fn table_with<S1, S2, S3>(
&self,
data: &[Vec<S1>],
header_row: bool,
id: S2,
caption: S3,
) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
S3: AsRef<str>,
{
format!(
"{}{}{}",
self.name_target(id),
self.caption(caption),
self.table(data, header_row)
)
}
#[inline(always)]
fn table_header_row<S>(&self, columns: &[S], add_hline: bool) -> String
where
S: AsRef<str>,
{
let columns = columns
.iter()
.map(|s| self.bold(s))
.collect::<Vec<String>>();
format!(
"{}{}",
self.table_row(&columns),
if add_hline {
let widths = columns
.iter()
.map(|s| {
let s: &str = s.as_str();
s.len()
})
.collect::<Vec<usize>>();
self.table_hline(&widths)
} else {
String::default()
}
)
}
#[inline(always)]
fn table_hline(&self, widths: &[usize]) -> String {
format!(
"|{}|\n",
widths
.iter()
.map(|w| (0..*w).map(|_| "-").collect::<String>())
.collect::<Vec<String>>()
.join("+")
)
}
#[inline(always)]
fn table_row<S>(&self, values: &[S]) -> String
where
S: AsRef<str>,
{
format!(
"| {} |\n",
values
.iter()
.map(|v| v.as_ref())
.collect::<Vec<&str>>()
.join(" | ")
)
}
}
impl TextFormat for OrgModeFormatter {
#[inline(always)]
fn bold<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("*{}*", s.as_ref())
}
#[inline(always)]
fn italic<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("/{}/", s.as_ref())
}
#[inline(always)]
fn underline<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("_{}_", s.as_ref())
}
#[inline(always)]
fn mono<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("={}=", s.as_ref())
}
#[inline(always)]
fn code<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("~{}~", s.as_ref())
}
#[inline(always)]
fn strikethrough<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("+{}+", s.as_ref())
}
#[inline(always)]
fn superscript<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("^{{{}}}", s.as_ref())
}
#[inline(always)]
fn subscript<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("_{{{}}}", s.as_ref())
}
}
impl LinkFormat for OrgModeFormatter {
#[inline(always)]
fn noweb_target<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("<<{}>>", s.as_ref())
}
#[inline(always)]
fn name_target<S>(&self, s: S) -> String
where
S: AsRef<str>,
{
format!("#+NAME: {}\n", s.as_ref())
}
#[inline(always)]
fn make_id_link<S>(&self, id: S) -> String
where
S: AsRef<str>,
{
format!("#{}", id.as_ref())
}
#[inline(always)]
fn make_heading_link<S>(&self, heading: S) -> String
where
S: AsRef<str>,
{
format!("*{}", heading.as_ref())
}
#[inline(always)]
fn link<S>(&self, target: S) -> String
where
S: AsRef<str>,
{
format!("[[{}]]", target.as_ref())
}
#[inline(always)]
fn link_with_description<S1, S2>(&self, target: S1, description: S2) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
format!("[[{}][{}]]", target.as_ref(), description.as_ref())
}
}
impl Formatter<IncludeArguments, SourceBlockArguments> for OrgModeFormatter {}
impl OrgModeFormatter {
#[inline(always)]
pub const fn new() -> Self {
OrgModeFormatter {}
}
#[inline(always)]
fn make_block<S1, S2>(&self, kind: S1, content: S2) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
self.make_block_with_args(kind, content, "")
}
#[inline(always)]
fn make_block_with_args<S1, S2, S3>(&self, kind: S1, content: S2, arguments: S3) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
S3: AsRef<str>,
{
let kind = kind.as_ref();
let arguments = arguments.as_ref();
let arg_spacer = if arguments.is_empty() { "" } else { " " };
let content = content.as_ref();
format!(
r##"#+BEGIN_{kind}{arg_spacer}{arguments}
{content}
#+END_{kind}
"##,
)
}
#[inline(always)]
pub fn drawer<S>(&self, name: S) -> String
where
S: AsRef<str>,
{
self.drawer_with_content(name, "")
}
#[inline(always)]
pub fn drawer_with_content<S1, S2>(&self, name: S1, content: S2) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let name = self.property_name(name.as_ref().to_uppercase());
let end = self.property_name("END");
let content = content.as_ref();
if content.is_empty() {
"{name}\n{end}\n".to_string()
} else {
format!(
r##"{name}
{content}{end}
"##
)
}
}
#[inline(always)]
pub fn property_drawer(&self, properties: HashMap<String, String>) -> String {
self.drawer_with_content(
"PROPERTIES",
properties
.iter()
.map(|(key, value)| self.property(key, value))
.collect::<String>(),
)
}
#[inline(always)]
fn property_name<S>(&self, name: S) -> String
where
S: AsRef<str>,
{
format!(":{}:", name.as_ref())
}
#[inline(always)]
pub fn property<S1, S2>(&self, name: S1, value: S2) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
{
format!("{} {}\n", self.property_name(name), value.as_ref())
}
#[inline(always)]
pub fn logbook_drawer<S1, S2, S3>(&self, log_entries: &[(S1, S2, Option<S3>)]) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
S3: AsRef<str>,
{
self.drawer_with_content(
"LOGBOOK",
log_entries
.iter()
.map(|(headline, date, note)| {
self.log_entry(
headline,
date,
note.as_ref().map(|v| v.as_ref().to_string()),
)
})
.collect::<String>(),
)
}
#[inline(always)]
pub fn log_entry<S1, S2, S3>(&self, headline: S1, date: S2, note: Option<S3>) -> String
where
S1: AsRef<str>,
S2: AsRef<str>,
S3: AsRef<str>,
{
format!(
r##"- {} <{}>{}\n"##,
headline.as_ref(),
date.as_ref(),
if let Some(note) = note {
format!(" ||\n {}", note.as_ref())
} else {
String::default()
}
)
}
}
impl From<SourceBlockArguments> for String {
fn from(value: SourceBlockArguments) -> Self {
let mut results = Vec::default();
match (
value.add_line_numbers,
value.continue_line_numbers,
value.line_number_start,
) {
(_, true, Some(n)) => results.push(format!("+n {n}")),
(true, false, Some(n)) => results.push(format!("-n {n}")),
(_, true, None) => results.push("+n".to_string()),
(true, false, None) => results.push("-n".to_string()),
_ => {}
}
if value.remove_source_labels {
results.push("-r".into());
}
if let Some(label_syntax) = &value.label_syntax {
results.push(format!("-l \"{label_syntax}\""));
}
if value.preserve_global_indentation {
results.push("-i".into());
}
results.push(value.eval.to_string());
results.push(value.exports.to_string());
results.push(value.results.to_string());
if let Some(session) = &value.session {
results.push(session.to_string());
}
if let Some(cmd_line) = &value.cmd_line {
results.push(format!(":cmdline {cmd_line}"));
}
if value.cache {
results.push(":cache yes".into());
}
if value.noweb {
results.push(":noweb yes".into());
}
if value.hlines {
results.push(":hlines yes".into());
}
if value.tangle {
results.push(":tangle yes".into());
}
results.join(" ")
}
}
impl ArgumentType for SourceBlockArguments {
fn is_default(&self) -> bool {
!self.add_line_numbers
&& !self.continue_line_numbers
&& self.line_number_start.is_none()
&& !self.remove_source_labels
&& self.label_syntax.is_none()
&& !self.preserve_global_indentation
&& self.eval == SourceEval::default()
&& self.exports == SourceExports::default()
&& self.results.is_default()
&& self.session.is_none()
&& self.cmd_line.is_none()
&& !self.cache
&& !self.noweb
&& !self.hlines
&& !self.tangle
&& self.rest.is_empty()
}
}
impl Display for SourceEval {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
":eval {}",
match self {
Self::Yes => "yes",
Self::No => "no",
Self::Query => "query",
Self::NoExport => "no-export",
Self::QueryExport => "query-export",
}
)
}
}
impl Display for SourceExports {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
":exports {}",
match self {
Self::None => "none",
Self::Code => "code",
Self::Results => "results",
Self::Both => "both",
}
)
}
}
impl SourceResults {
fn is_default(&self) -> bool {
self.collection.is_none()
&& self.as_type.is_none()
&& self.format.is_none()
&& self.handling.is_none()
}
}
impl Display for SourceResults {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.collection.is_some()
|| self.as_type.is_some()
|| self.format.is_some()
|| self.handling.is_some()
{
write!(
f,
":results{}{}{}{}",
if let Some(collection) = &self.collection {
format!(" {collection}")
} else {
String::default()
},
if let Some(as_type) = &self.as_type {
format!(" {as_type}")
} else {
String::default()
},
if let Some(format) = &self.format {
format!(" {format}")
} else {
String::default()
},
if let Some(handling) = &self.handling {
format!(" {handling}")
} else {
String::default()
}
)
} else {
Ok(())
}
}
}
impl Display for SourceResultsCollection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Value => "value",
Self::Output => "output",
}
)
}
}
impl Display for SourceResultsFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Raw => "raw",
Self::Code => "code",
Self::Drawer => "drawer",
Self::Html => "html",
Self::LaTeX => "latex",
Self::Link => "link",
Self::Graphics => "graphics",
Self::Org => "org",
Self::PrettyPrint => "pp",
}
)
}
}
impl ArgumentType for SourceResultsFile {
fn is_default(&self) -> bool {
self.file_name.is_none()
&& self.output_dir.is_none()
&& self.file_extension.is_none()
&& self.file_mode.is_none()
}
}
impl From<SourceResultsFile> for String {
fn from(value: SourceResultsFile) -> Self {
Self::from(&value)
}
}
impl From<&SourceResultsFile> for String {
fn from(value: &SourceResultsFile) -> Self {
vec![
if let Some(file_name) = &value.file_name {
format!(" :file {}", file_name.to_string_lossy().into_owned())
} else {
String::default()
},
if let Some(output_dir) = &value.output_dir {
format!(" :output-dir {}", output_dir.to_string_lossy().into_owned())
} else {
String::default()
},
if let Some(file_extension) = &value.file_extension {
format!(" :file-ext {file_extension}")
} else {
String::default()
},
if let Some(file_mode) = value.file_mode {
format!(" :file-mode (identity #o{:o})", file_mode)
} else {
String::default()
},
]
.into_iter()
.collect::<String>()
}
}
impl Display for SourceResultsType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Table => "table".to_string(),
Self::Vector => "vector".to_string(),
Self::List => "list".to_string(),
Self::Scalar => "scalar".to_string(),
Self::Verbatim => "verbatim".to_string(),
Self::File(file) => String::from(file),
}
)
}
}
impl Display for SourceResultsHandling {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Replace => "replace",
Self::Silent => "silent",
Self::Discard => "discard",
Self::Append => "append",
Self::Prepend => "prepend",
Self::None => "none",
}
)
}
}
impl From<IncludeArguments> for String {
fn from(value: IncludeArguments) -> Self {
let mut results = Vec::default();
if let Some(block_name) = &value.block_name {
results.push(block_name.to_string());
}
if let Some(language) = &value.language {
results.push(language.to_string());
}
if let Some(min_level) = &value.min_level {
if *min_level > 0 {
results.push(format!(":minlevel {min_level}"));
}
}
match (value.from_line, value.to_line) {
(Some(from), Some(to)) => results.push(format!(":lines {from}-{to}")),
(Some(from), None) => results.push(format!(":lines {from}-")),
(None, Some(to)) => results.push(format!(":lines -{to}")),
_ => {}
}
if value.only_contents {
results.push(":only-contents t".to_string());
}
let rest: Vec<String> = value.rest.clone();
results.extend(rest);
results.join(" ")
}
}
impl ArgumentType for IncludeArguments {
fn is_default(&self) -> bool {
self.block_name.is_none()
&& self.language.is_none()
&& self.min_level.is_none()
&& self.from_line.is_none()
&& self.to_line.is_none()
&& !self.only_contents
&& self.rest.is_empty()
}
}
fn include_file<P>(file_name: P, as_link: bool, index_file: &mut File) -> Result<(), Error>
where
P: AsRef<Path>,
{
let file_name = file_name.as_ref();
let file_name = if let Ok(canonical) = file_name.canonicalize() {
canonical
} else {
file_name.to_path_buf()
};
if as_link {
index_file.write_all(
FORMATTER
.include_file(file_name.to_string_lossy())
.as_bytes(),
)?;
} else {
let included = read_to_string(&file_name)?;
index_file.write_all(
FORMATTER
.line_comment(format!(">>> INCLUDED {file_name:?}"))
.as_bytes(),
)?;
index_file.write_all(included.as_bytes())?;
index_file.write_all(
FORMATTER
.line_comment(format!("<<< INCLUDED {file_name:?}"))
.as_bytes(),
)?;
}
Ok(())
}
fn write_annotations(
annotated: &impl HasAnnotations,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
if annotated.has_annotation_properties() {
let mut property_map: HashMap<&IdentifierReference, Vec<&Value>> = HashMap::default();
for property in annotated.annotation_properties() {
if let Some(values) = property_map.get_mut(property.name_reference()) {
values.push(property.value());
} else {
property_map.insert(property.name_reference(), vec![property.value()]);
}
}
let mut values: Vec<&LanguageString> = Vec::default();
for property in categories.label_properties() {
if let Some(vs) = property_map.get(property) {
values.extend(vs.iter().filter_map(|v| {
if let Value::Simple(SimpleValue::String(s)) = v {
Some(s)
} else {
None
}
}));
}
}
if !values.is_empty() {
let values = values
.iter()
.map(|v| {
format!(
"{}{}",
v.value(),
if let Some(lang) = v.language() {
FORMATTER.superscript(FORMATTER.italic(lang))
} else {
String::default()
}
)
})
.collect::<Vec<String>>();
writer.write_all(
FORMATTER
.paragraph(format!(
"{}: {}",
FORMATTER.bold("Labels"),
values.join(", ")
))
.as_bytes(),
)?;
}
write_string_property_block(
"Definition",
categories.definition_properties(),
&property_map,
writer,
)?;
write_string_property_block(
"Description",
categories.description_properties(),
&property_map,
writer,
)?;
write_string_property_block(
"Comments",
categories.comment_properties(),
&property_map,
writer,
)?;
}
if annotated.has_constraints() {
writer.write_all(
FORMATTER
.pseudo_heading("Annotation Constraints")
.as_bytes(),
)?;
for constraint in annotated.annotation_constraints() {
writer.write_all(format!("{}: ", FORMATTER.bold(constraint.name())).as_bytes())?;
let body = match constraint.body() {
ConstraintBody::Informal(v) => format!(
"{}{}",
v.value(),
if let Some(language) = v.language() {
format!(" [{}]", FORMATTER.italic(language))
} else {
String::default()
}
),
ConstraintBody::Formal(_) => format!("... [{}]", FORMATTER.italic("sdml-cl")),
};
writer.write_all(body.as_bytes())?;
}
}
Ok(())
}
fn write_string_property_block(
heading: &str,
properties: &[IdentifierReference],
property_map: &HashMap<&IdentifierReference, Vec<&Value>>,
writer: &mut dyn Write,
) -> Result<(), Error> {
let mut values: Vec<&LanguageString> = Vec::default();
for property in properties {
if let Some(vs) = property_map.get(property) {
values.extend(vs.iter().filter_map(|v| {
if let Value::Simple(SimpleValue::String(s)) = v {
Some(s)
} else {
None
}
}));
}
}
if !values.is_empty() {
writer.write_all(FORMATTER.pseudo_heading(heading).as_bytes())?;
let values = values
.iter()
.map(|v| {
format!(
"{}{}",
v.value(),
if let Some(lang) = v.language() {
FORMATTER.superscript(FORMATTER.italic(lang))
} else {
String::default()
}
)
})
.collect::<Vec<String>>();
writer.write_all(
if values.len() == 1 {
FORMATTER.paragraph(values.first().unwrap())
} else {
FORMATTER.unordered_list(&values, 0)
}
.as_bytes(),
)?;
}
Ok(())
}
#[allow(single_use_lifetimes)]
fn write_definitions(
parent_level: u8,
module: &Module,
cache: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
for definition in module.body().definitions() {
match definition {
Definition::Datatype(v) => {
write_datatype(parent_level, module, v, cache, categories, writer)?
}
Definition::Entity(v) => {
write_entity(parent_level, module, v, cache, categories, writer)?
}
Definition::Enum(v) => write_enum(parent_level, module, v, cache, categories, writer)?,
Definition::Event(v) => {
write_event(parent_level, module, v, cache, categories, writer)?
}
Definition::Property(v) => {
write_property(parent_level, module, v, cache, categories, writer)?
}
Definition::Rdf(_) => todo!(),
Definition::Structure(v) => {
write_structure(parent_level, module, v, cache, categories, writer)?
}
Definition::TypeClass(v) => {
write_typeclass(parent_level, module, v, cache, categories, writer)?
}
Definition::Union(v) => {
write_union(parent_level, module, v, cache, categories, writer)?
}
}
}
Ok(())
}
fn write_datatype(
parent_level: u8,
module: &Module,
datatype: &DatatypeDef,
_: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = datatype.name();
write_heading(
Heading::new(
parent_level + 1,
format!("Datatype {}", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"definition",
module.name().as_ref(),
name.as_ref(),
])),
writer,
)?;
writer.write_all(
FORMATTER.paragraph(
format!(
"The datatype {} is based on the existing datatype {}.{}",
FORMATTER.bold(FORMATTER.mono(name)),
FORMATTER.mono(datatype.base_type().to_string()),
if datatype.is_opaque() {
format!(
" It is marked as an {} datatype, meaning constraints may only use strict equality tests between values.",
FORMATTER.italic("opaque"),
)
} else {
String::default()
}
)).as_bytes())?;
if let Some(body) = datatype.body() {
write_annotations(body, categories, writer)?;
}
writer.write_all(b"\n")?;
Ok(())
}
fn write_entity(
parent_level: u8,
module: &Module,
entity: &EntityDef,
cache: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = entity.name();
write_heading(
Heading::new(parent_level + 1, format!("Entity {}", FORMATTER.mono(name))).with_label(
make_section_label(&["definition", module.name().as_ref(), name.as_ref()]),
),
writer,
)?;
if !entity.is_incomplete(module, cache) {
if let Some(body) = entity.body() {
if body.has_annotations() {
write_annotations(body, categories, writer)?;
}
for member in body.members() {
write_member(
parent_level + 1,
module,
name,
member,
cache,
categories,
writer,
)?;
}
}
} else {
writer.write_all(type_is_incomplete().as_bytes())?;
}
Ok(())
}
fn write_enum(
parent_level: u8,
module: &Module,
an_enum: &EnumDef,
cache: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = an_enum.name();
write_heading(
Heading::new(
parent_level + 1,
format!("Enumeration {}", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"definition",
module.name().as_ref(),
name.as_ref(),
])),
writer,
)?;
if !an_enum.is_incomplete(module, cache) {
let body = an_enum.body().unwrap();
write_annotations(body, categories, writer)?;
for variant in body.variants() {
let name = variant.name();
write_heading(
Heading::new(
parent_level + 2,
format!("Variant {}", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"variant",
module.name().as_ref(),
an_enum.name().as_ref(),
name.as_ref(),
])),
writer,
)?;
if let Some(body) = variant.body() {
write_annotations(body, categories, writer)?;
}
}
} else {
writer.write_all(type_is_incomplete().as_bytes())?;
}
Ok(())
}
fn write_event(
parent_level: u8,
module: &Module,
event: &EventDef,
cache: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = event.name();
write_heading(
Heading::new(parent_level + 1, format!("Event {}", FORMATTER.mono(name))).with_label(
make_section_label(&["definition", module.name().as_ref(), name.as_ref()]),
),
writer,
)?;
if !event.is_incomplete(module, cache) {
if let Some(body) = event.body() {
if body.has_annotations() {
write_annotations(body, categories, writer)?;
}
for member in body.members() {
write_member(
parent_level + 1,
module,
name,
member,
cache,
categories,
writer,
)?;
}
} else {
writer.write_all(type_is_incomplete().as_bytes())?;
}
} else {
writer.write_all(type_is_incomplete().as_bytes())?;
}
Ok(())
}
fn write_property(
parent_level: u8,
module: &Module,
property: &PropertyDef,
_: &impl ModuleStore,
_: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = property.name();
write_heading(
Heading::new(
parent_level + 1,
format!("Property {}", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"definition",
module.name().as_ref(),
name.as_ref(),
])),
writer,
)?;
Ok(())
}
fn write_structure(
parent_level: u8,
module: &Module,
structure: &StructureDef,
cache: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = structure.name();
write_heading(
Heading::new(
parent_level + 1,
format!("Structure {}", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"definition",
module.name().as_ref(),
name.as_ref(),
])),
writer,
)?;
if !structure.is_incomplete(module, cache) {
if let Some(body) = structure.body() {
if body.has_annotations() {
write_annotations(body, categories, writer)?;
}
for member in body.members() {
write_member(
parent_level + 1,
module,
name,
member,
cache,
categories,
writer,
)?;
}
}
} else {
writer.write_all(type_is_incomplete().as_bytes())?;
}
Ok(())
}
fn write_member(
parent_level: u8,
module: &Module,
parent_def: &Identifier,
member: &Member,
_: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = member.name();
match member.kind() {
MemberKind::Reference(_property) => {
write_heading(
Heading::new(
parent_level + 1,
format!("Member {} (property)", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"member",
module.name().as_ref(),
parent_def.as_ref(),
name.as_ref(),
])),
writer,
)?;
}
MemberKind::Definition(member) => {
write_heading(
Heading::new(parent_level + 1, format!("Member {}", FORMATTER.mono(name)))
.with_label(make_section_label(&[
"member",
module.name().as_ref(),
parent_def.as_ref(),
name.as_ref(),
])),
writer,
)?;
let cardinality = member.target_cardinality();
let cardinality_string = if cardinality == &DEFAULT_CARDINALITY {
format!("a {} value of type ", FORMATTER.bold("required"))
} else {
let seq_type_string = match cardinality.sequence_type() {
PseudoSequenceType::Maybe => "an optional",
PseudoSequenceType::Bag => "an unordered bag",
PseudoSequenceType::List => "an ordered list",
PseudoSequenceType::Set => "a set",
PseudoSequenceType::UnorderedSet => "an unordered set",
};
let range = cardinality.range();
let range_string = match (range.min_occurs(), range.max_occurs()) {
(0, None) => ", with zero or more values,".to_string(),
(1, None) => ", with one or more values,".to_string(),
(min, None) => format!(", with {min} or more values,"),
(min, Some(max)) => {
if min == max {
format!(", with {min} values,")
} else {
format!(", with between {min} and {max} values,")
}
}
};
format!(
"{}{range_string} of type ",
FORMATTER.italic(seq_type_string)
)
};
writer.write_all(
FORMATTER
.paragraph(format!(
"Member {} is {}{}.\n\n",
FORMATTER.mono(name),
cardinality_string,
FORMATTER.mono(member.target_type().to_string())
))
.as_bytes(),
)?;
if let Some(body) = member.body() {
write_annotations(body, categories, writer)?;
}
}
}
Ok(())
}
fn write_typeclass(
parent_level: u8,
module: &Module,
typeclass: &TypeClassDef,
cache: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = typeclass.name();
write_heading(
Heading::new(
parent_level + 1,
format!("Typeclass {}", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"definition",
module.name().as_ref(),
name.as_ref(),
])),
writer,
)?;
if !typeclass.is_incomplete(module, cache) {
if let Some(body) = typeclass.body() {
if body.has_annotations() {
write_annotations(body, categories, writer)?;
}
}
} else {
writer.write_all(type_is_incomplete().as_bytes())?;
}
Ok(())
}
fn write_union(
parent_level: u8,
module: &Module,
union: &UnionDef,
cache: &impl ModuleStore,
categories: &AnnotationCategories,
writer: &mut dyn Write,
) -> Result<(), Error> {
let name = union.name();
write_heading(
Heading::new(parent_level + 1, format!("Union {}", FORMATTER.mono(name))).with_label(
make_section_label(&["definition", module.name().as_ref(), name.as_ref()]),
),
writer,
)?;
if !union.is_incomplete(module, cache) {
if let Some(body) = union.body() {
write_annotations(body, categories, writer)?;
for variant in body.variants() {
let name = variant.name();
write_heading(
Heading::new(
parent_level + 2,
format!("Variant {}", FORMATTER.mono(name)),
)
.with_label(make_section_label(&[
"variant".to_string(),
module.name().to_string(),
union.name().to_string(),
if let Some(label) = variant.rename() {
label.to_string()
} else {
variant.name_reference().to_string()
},
])),
writer,
)?;
writer.write_all(
FORMATTER
.indented_verbatim(format!(
"{}{}",
variant.name_reference(),
if let Some(label) = variant.rename() {
format!(" as {label}")
} else {
String::default()
},
))
.as_bytes(),
)?;
if let Some(body) = variant.body() {
write_annotations(body, categories, writer)?;
}
}
}
} else {
writer.write_all(type_is_incomplete().as_bytes())?;
}
Ok(())
}
#[inline(always)]
fn type_is_incomplete() -> String {
FORMATTER.paragraph(format!(
"This type is currently {}.",
FORMATTER.italic("incomplete")
))
}