Generate docs basde on package root .roc file

This commit is contained in:
Richard Feldman 2022-12-01 12:58:33 -05:00
parent e549456668
commit 15590fb31b
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
6 changed files with 115 additions and 188 deletions

View file

@ -270,12 +270,13 @@ pub fn build_app<'a>() -> Command<'a> {
) )
.subcommand( .subcommand(
Command::new(CMD_DOCS) Command::new(CMD_DOCS)
.about("Generate documentation for Roc modules (Work In Progress)") .about("Generate documentation for a Roc package")
.arg(Arg::new(DIRECTORY_OR_FILES) .arg(Arg::new(ROC_FILE)
.multiple_values(true) .multiple_values(true)
.required(false) .help("The package's main .roc file")
.help("The directory or files to build documentation for")
.allow_invalid_utf8(true) .allow_invalid_utf8(true)
.required(false)
.default_value(DEFAULT_ROC_FILENAME),
) )
) )
.subcommand(Command::new(CMD_GLUE) .subcommand(Command::new(CMD_GLUE)

View file

@ -209,37 +209,9 @@ fn main() -> io::Result<()> {
Ok(0) Ok(0)
} }
Some((CMD_DOCS, matches)) => { Some((CMD_DOCS, matches)) => {
let maybe_values = matches.values_of_os(DIRECTORY_OR_FILES); let root_filename = matches.value_of_os(ROC_FILE).unwrap();
let mut values: Vec<OsString> = Vec::new(); generate_docs_html(PathBuf::from(root_filename));
match maybe_values {
None => {
let mut os_string_values: Vec<OsString> = Vec::new();
read_all_roc_files(
&std::env::current_dir()?.as_os_str().to_os_string(),
&mut os_string_values,
)?;
for os_string in os_string_values {
values.push(os_string);
}
}
Some(os_values) => {
for os_str in os_values {
values.push(os_str.to_os_string());
}
}
}
let mut roc_files = Vec::new();
// Populate roc_files
for os_str in values {
let metadata = fs::metadata(os_str.clone())?;
roc_files_recursive(os_str.as_os_str(), metadata.file_type(), &mut roc_files)?;
}
generate_docs_html(roc_files);
Ok(0) Ok(0)
} }

View file

@ -1,23 +1,15 @@
use crate::docs::DocEntry::DetachedDoc; use crate::docs::DocEntry::DetachedDoc;
use crate::docs::TypeAnnotation::{Apply, BoundVariable, Function, NoTypeAnn, Record, TagUnion}; use crate::docs::TypeAnnotation::{Apply, BoundVariable, Function, NoTypeAnn, Record, TagUnion};
use crate::file::LoadedModule;
use roc_can::scope::Scope; use roc_can::scope::Scope;
use roc_collections::MutMap;
use roc_module::ident::ModuleName; use roc_module::ident::ModuleName;
use roc_module::symbol::IdentIds; use roc_module::symbol::{IdentIds, ModuleId};
use roc_parse::ast::AssignedField; use roc_parse::ast::AssignedField;
use roc_parse::ast::{self, ExtractSpaces, TypeHeader}; use roc_parse::ast::{self, ExtractSpaces, TypeHeader};
use roc_parse::ast::{CommentOrNewline, TypeDef, ValueDef}; use roc_parse::ast::{CommentOrNewline, TypeDef, ValueDef};
// Documentation generation requirements // Documentation generation requirements
#[derive(Debug)]
pub struct Documentation {
pub name: String,
pub version: String,
pub docs: String,
pub modules: Vec<LoadedModule>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct ModuleDocumentation { pub struct ModuleDocumentation {
pub name: String, pub name: String,
@ -100,8 +92,9 @@ pub fn generate_module_docs(
scope: Scope, scope: Scope,
module_name: ModuleName, module_name: ModuleName,
parsed_defs: &roc_parse::ast::Defs, parsed_defs: &roc_parse::ast::Defs,
exposed_module_ids: &[ModuleId],
) -> ModuleDocumentation { ) -> ModuleDocumentation {
let entries = generate_entry_docs(&scope.locals.ident_ids, parsed_defs); let entries = generate_entry_docs(&scope.locals.ident_ids, parsed_defs, exposed_module_ids);
ModuleDocumentation { ModuleDocumentation {
name: module_name.as_str().to_string(), name: module_name.as_str().to_string(),
@ -140,6 +133,7 @@ fn detached_docs_from_comments_and_new_lines<'a>(
fn generate_entry_docs<'a>( fn generate_entry_docs<'a>(
ident_ids: &'a IdentIds, ident_ids: &'a IdentIds,
defs: &roc_parse::ast::Defs<'a>, defs: &roc_parse::ast::Defs<'a>,
exposed_module_ids: &[ModuleId],
) -> Vec<DocEntry> { ) -> Vec<DocEntry> {
use roc_parse::ast::Pattern; use roc_parse::ast::Pattern;
@ -165,7 +159,7 @@ fn generate_entry_docs<'a>(
Err(value_index) => match &defs.value_defs[value_index.index()] { Err(value_index) => match &defs.value_defs[value_index.index()] {
ValueDef::Annotation(loc_pattern, loc_ann) => { ValueDef::Annotation(loc_pattern, loc_ann) => {
if let Pattern::Identifier(identifier) = loc_pattern.value { if let Pattern::Identifier(identifier) = loc_pattern.value {
// Check if the definition is exposed // Check if this module exposes the def
if ident_ids.get_id(identifier).is_some() { if ident_ids.get_id(identifier).is_some() {
let name = identifier.to_string(); let name = identifier.to_string();
let doc_def = DocDef { let doc_def = DocDef {

View file

@ -376,6 +376,12 @@ fn start_phase<'a>(
state.cached_types.lock().contains_key(&module_id) state.cached_types.lock().contains_key(&module_id)
}; };
let exposed_module_ids = state
.platform_data
.as_ref()
.map(|data| data.exposed_modules)
.unwrap_or_default();
BuildTask::CanonicalizeAndConstrain { BuildTask::CanonicalizeAndConstrain {
parsed, parsed,
dep_idents, dep_idents,
@ -384,6 +390,7 @@ fn start_phase<'a>(
aliases, aliases,
abilities_store, abilities_store,
skip_constraint_gen, skip_constraint_gen,
exposed_module_ids,
} }
} }
@ -1159,6 +1166,7 @@ enum BuildTask<'a> {
exposed_symbols: VecSet<Symbol>, exposed_symbols: VecSet<Symbol>,
aliases: MutMap<Symbol, Alias>, aliases: MutMap<Symbol, Alias>,
abilities_store: PendingAbilitiesStore, abilities_store: PendingAbilitiesStore,
exposed_module_ids: &'a [ModuleId],
skip_constraint_gen: bool, skip_constraint_gen: bool,
}, },
Solve { Solve {
@ -4371,7 +4379,7 @@ fn build_header<'a>(
}; };
home = module_ids.get_or_insert(&name); home = module_ids.get_or_insert(&name);
// Ensure this module has an entry in the exposed_ident_ids map. // Ensure this module has an entry in the ident_ids_by_module map.
ident_ids_by_module.get_or_insert(home); ident_ids_by_module.get_or_insert(home);
// For each of our imports, add an entry to deps_by_name // For each of our imports, add an entry to deps_by_name
@ -5209,6 +5217,7 @@ fn canonicalize_and_constrain<'a>(
imported_abilities_state: PendingAbilitiesStore, imported_abilities_state: PendingAbilitiesStore,
parsed: ParsedModule<'a>, parsed: ParsedModule<'a>,
skip_constraint_gen: bool, skip_constraint_gen: bool,
exposed_module_ids: &[ModuleId],
) -> CanAndCon { ) -> CanAndCon {
let canonicalize_start = Instant::now(); let canonicalize_start = Instant::now();
@ -5273,17 +5282,24 @@ fn canonicalize_and_constrain<'a>(
} }
HeaderType::Interface { name, .. } HeaderType::Interface { name, .. }
| HeaderType::Builtin { name, .. } | HeaderType::Builtin { name, .. }
| HeaderType::Hosted { name, .. } => { | HeaderType::Hosted { name, .. }
if exposed_module_ids.contains(&parsed.module_id) =>
{
let mut scope = module_output.scope.clone(); let mut scope = module_output.scope.clone();
scope.add_docs_imports(); scope.add_docs_imports();
let docs = crate::docs::generate_module_docs( let docs = crate::docs::generate_module_docs(
scope, scope,
name.as_str().into(), name.as_str().into(),
&parsed_defs_for_docs, &parsed_defs_for_docs,
exposed_module_ids,
); );
Some(docs) Some(docs)
} }
HeaderType::Interface { .. } | HeaderType::Builtin { .. } | HeaderType::Hosted { .. } => {
// This module isn't exposed by the platform, so don't generate docs for it!
None
}
}; };
// _before has an underscore because it's unused in --release builds // _before has an underscore because it's unused in --release builds
@ -6126,6 +6142,7 @@ fn run_task<'a>(
aliases, aliases,
abilities_store, abilities_store,
skip_constraint_gen, skip_constraint_gen,
exposed_module_ids,
} => { } => {
let can_and_con = canonicalize_and_constrain( let can_and_con = canonicalize_and_constrain(
arena, arena,
@ -6136,6 +6153,7 @@ fn run_task<'a>(
abilities_store, abilities_store,
parsed, parsed,
skip_constraint_gen, skip_constraint_gen,
exposed_module_ids,
); );
Ok(Msg::CanonicalizedAndConstrained(can_and_con)) Ok(Msg::CanonicalizedAndConstrained(can_and_con))

View file

@ -10,7 +10,7 @@ use roc_code_markup::markup::nodes::MarkupNode;
use roc_code_markup::slow_pool::SlowPool; use roc_code_markup::slow_pool::SlowPool;
use roc_highlight::highlight_parser::{highlight_defs, highlight_expr}; use roc_highlight::highlight_parser::{highlight_defs, highlight_expr};
use roc_load::docs::{DocEntry, TypeAnnotation}; use roc_load::docs::{DocEntry, TypeAnnotation};
use roc_load::docs::{Documentation, ModuleDocumentation, RecordField}; use roc_load::docs::{ModuleDocumentation, RecordField};
use roc_load::{ExecutionMode, LoadConfig, LoadedModule, LoadingProblem, Threading}; use roc_load::{ExecutionMode, LoadConfig, LoadedModule, LoadingProblem, Threading};
use roc_module::symbol::{IdentIdsByModule, Interns, ModuleId}; use roc_module::symbol::{IdentIdsByModule, Interns, ModuleId};
use roc_packaging::cache::{self, RocCacheDir}; use roc_packaging::cache::{self, RocCacheDir};
@ -25,21 +25,20 @@ mod html;
const BUILD_DIR: &str = "./generated-docs"; const BUILD_DIR: &str = "./generated-docs";
pub fn generate_docs_html(filenames: Vec<PathBuf>) { pub fn generate_docs_html(root_file: PathBuf) {
let build_dir = Path::new(BUILD_DIR); let build_dir = Path::new(BUILD_DIR);
let loaded_modules = load_modules_for_files(filenames); let loaded_module = load_module_for_docs(root_file);
// TODO: get info from a package module; this is all hardcoded for now. // TODO get these from the platform's source file rather than hardcoding them!
let package = Documentation { let package_name = "Documentation".to_string();
name: "documentation".to_string(), let version = String::new();
version: "".to_string(),
docs: "Package introduction or README.".to_string(),
modules: loaded_modules,
};
if !build_dir.exists() { // Clear out the generated-docs dir (we'll create a fresh one at the end)
fs::create_dir_all(build_dir).expect("TODO gracefully handle unable to create build dir"); if build_dir.exists() {
fs::remove_dir_all(build_dir)
.expect("TODO gracefully handle being unable to delete build dir");
} }
fs::create_dir_all(build_dir).expect("TODO gracefully handle being unable to create build dir");
// Copy over the assets // Copy over the assets
fs::write( fs::write(
@ -60,29 +59,18 @@ pub fn generate_docs_html(filenames: Vec<PathBuf>) {
) )
.expect("TODO gracefully handle failing to make the favicon"); .expect("TODO gracefully handle failing to make the favicon");
let module_pairs = package.modules.iter().flat_map(|loaded_module| { let module_pairs = loaded_module
loaded_module .documentation
.documentation .iter()
.iter() .flat_map(|(module_id, module)| {
.filter_map(move |(module_id, module)| { let exposed_values = loaded_module
// TODO it seems this `documentation` dictionary has entries for .exposed_values
// every module, but only the current module has any info in it. .iter()
// We disregard the others, but probably this shouldn't bother .map(|symbol| symbol.as_str(&loaded_module.interns).to_string())
// being a hash map in the first place if only one of its entries .collect::<Vec<String>>();
// actually has interesting information in it?
if *module_id == loaded_module.module_id {
let exposed_values = loaded_module
.exposed_values
.iter()
.map(|symbol| symbol.as_str(&loaded_module.interns).to_string())
.collect::<Vec<String>>();
Some((module, exposed_values)) Some((module, exposed_values))
} else { });
None
}
})
});
let template_html = include_str!("./static/index.html") let template_html = include_str!("./static/index.html")
.replace("<!-- search.js -->", "/search.js") .replace("<!-- search.js -->", "/search.js")
@ -93,7 +81,7 @@ pub fn generate_docs_html(filenames: Vec<PathBuf>) {
&module_pairs &module_pairs
.clone() .clone()
.map(|(module, _)| { .map(|(module, _)| {
let href = sidebar_link_url(module); let href = sidebar_link_url(module.name.as_str());
format!(r#"<link rel="prefetch" href="{href}"/>"#) format!(r#"<link rel="prefetch" href="{href}"/>"#)
}) })
@ -106,48 +94,40 @@ pub fn generate_docs_html(filenames: Vec<PathBuf>) {
); );
// Write each package's module docs html file // Write each package's module docs html file
for loaded_module in package.modules.iter() { for (module_id, module_docs) in loaded_module.documentation.iter() {
for (module_id, module_docs) in loaded_module.documentation.iter() { let module_name = module_docs.name.as_str();
if *module_id == loaded_module.module_id { let module_dir = build_dir.join(module_name.replace('.', "/").as_str());
let module_dir = build_dir.join(module_docs.name.replace('.', "/").as_str());
fs::create_dir_all(&module_dir) fs::create_dir_all(&module_dir)
.expect("TODO gracefully handle not being able to create the module dir"); .expect("TODO gracefully handle not being able to create the module dir");
let rendered_module = template_html let rendered_module = template_html
.replace( .replace(
"<!-- Page title -->", "<!-- Page title -->",
page_title(&package, module_docs).as_str(), page_title(package_name.as_str(), module_name).as_str(),
) )
.replace( .replace(
"<!-- Package Name and Version -->", "<!-- Package Name and Version -->",
render_name_and_version(package.name.as_str(), package.version.as_str()) render_name_and_version(package_name.as_str(), version.as_str()).as_str(),
.as_str(), )
) .replace(
.replace( "<!-- Module Docs -->",
"<!-- Module Docs -->", render_module_documentation(module_docs, &loaded_module).as_str(),
render_module_documentation(module_docs, loaded_module).as_str(), );
);
fs::write(module_dir.join("index.html"), rendered_module).expect( fs::write(module_dir.join("index.html"), rendered_module)
"TODO gracefully handle failing to write index.html inside module's dir", .expect("TODO gracefully handle failing to write index.html inside module's dir");
);
}
}
} }
println!("🎉 Docs generated in {}", build_dir.display()); println!("🎉 Docs generated in {}", build_dir.display());
} }
fn sidebar_link_url(module: &ModuleDocumentation) -> String { fn sidebar_link_url(module_name: &str) -> String {
format!("{}{}", base_url(), module.name.as_str()) format!("{}{}", base_url(), module_name)
} }
fn page_title(package: &Documentation, module: &ModuleDocumentation) -> String { fn page_title(package_name: &str, module_name: &str) -> String {
let package_name = &package.name; format!("<title>{module_name} - {package_name}</title>")
let module_name = &module.name;
let title = format!("<title>{module_name} - {package_name}</title>");
title
} }
// converts plain-text code to highlighted html // converts plain-text code to highlighted html
@ -222,12 +202,8 @@ fn render_module_documentation(
if should_render_entry { if should_render_entry {
buf.push_str("<section>"); buf.push_str("<section>");
let mut href = String::new();
href.push('#');
href.push_str(doc_def.name.as_str());
let name = doc_def.name.as_str(); let name = doc_def.name.as_str();
let href = format!("#{name}");
let mut content = String::new(); let mut content = String::new();
content.push_str( content.push_str(
@ -241,15 +217,11 @@ fn render_module_documentation(
let type_ann = &doc_def.type_annotation; let type_ann = &doc_def.type_annotation;
match type_ann { if !matches!(type_ann, TypeAnnotation::NoTypeAnn) {
TypeAnnotation::NoTypeAnn => {} content.push_str(" : ");
_ => { type_annotation_to_html(0, &mut content, type_ann, false);
content.push_str(" : ");
}
} }
type_annotation_to_html(0, &mut content, type_ann, false);
buf.push_str( buf.push_str(
html_to_string( html_to_string(
"h3", "h3",
@ -388,7 +360,7 @@ fn render_sidebar<'a, I: Iterator<Item = (&'a ModuleDocumentation, Vec<String>)>
let mut buf = String::new(); let mut buf = String::new();
for (module, exposed_values) in modules { for (module, exposed_values) in modules {
let href = sidebar_link_url(module); let href = sidebar_link_url(module.name.as_str());
let mut sidebar_entry_content = String::new(); let mut sidebar_entry_content = String::new();
sidebar_entry_content.push_str( sidebar_entry_content.push_str(
@ -449,35 +421,29 @@ fn render_sidebar<'a, I: Iterator<Item = (&'a ModuleDocumentation, Vec<String>)>
buf buf
} }
pub fn load_modules_for_files(filenames: Vec<PathBuf>) -> Vec<LoadedModule> { pub fn load_module_for_docs(filename: PathBuf) -> LoadedModule {
let arena = Bump::new(); let arena = Bump::new();
let mut modules = Vec::with_capacity(filenames.len()); let load_config = LoadConfig {
target_info: roc_target::TargetInfo::default_x86_64(), // This is just type-checking for docs, so "target" doesn't matter
for filename in filenames { render: roc_reporting::report::RenderTarget::ColorTerminal,
let load_config = LoadConfig { palette: roc_reporting::report::DEFAULT_PALETTE,
target_info: roc_target::TargetInfo::default_x86_64(), // This is just type-checking for docs, so "target" doesn't matter threading: Threading::AllAvailable,
render: roc_reporting::report::RenderTarget::ColorTerminal, exec_mode: ExecutionMode::Check,
palette: roc_reporting::report::DEFAULT_PALETTE, };
threading: Threading::AllAvailable, match roc_load::load_and_typecheck(
exec_mode: ExecutionMode::Check, &arena,
}; filename,
match roc_load::load_and_typecheck( Default::default(),
&arena, RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
filename, load_config,
Default::default(), ) {
RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), Ok(loaded) => loaded,
load_config, Err(LoadingProblem::FormattedReport(report)) => {
) { eprintln!("{}", report);
Ok(loaded) => modules.push(loaded), std::process::exit(1);
Err(LoadingProblem::FormattedReport(report)) => {
eprintln!("{}", report);
std::process::exit(1);
}
Err(e) => panic!("{:?}", e),
} }
Err(e) => panic!("{:?}", e),
} }
modules
} }
const INDENT: &str = " "; const INDENT: &str = " ";

View file

@ -1,51 +1,27 @@
//! Provides a binary that is only used for static build servers. //! Provides a binary that is only used for static build servers.
use clap::{Arg, Command}; use clap::{Arg, Command};
use roc_docs::generate_docs_html; use roc_docs::generate_docs_html;
use std::fs::{self, FileType};
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; pub const ROC_FILE: &str = "ROC_FILE";
const DEFAULT_ROC_FILENAME: &str = "main.roc";
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
let matches = Command::new("roc-docs") let matches = Command::new("roc-docs")
.about("Build HTML documentation files from the given .roc files") .about("Generate documentation for a Roc package")
.arg( .arg(
Arg::new(DIRECTORY_OR_FILES) Arg::new(ROC_FILE)
.multiple_values(true) .multiple_values(true)
.required(true) .help("The package's main .roc file")
.help("The directory or files to build documentation for") .allow_invalid_utf8(true)
.allow_invalid_utf8(true), .required(false)
.default_value(DEFAULT_ROC_FILENAME),
) )
.get_matches(); .get_matches();
let mut roc_files = Vec::new();
// Populate roc_files // Populate roc_files
for os_str in matches.values_of_os(DIRECTORY_OR_FILES).unwrap() { generate_docs_html(PathBuf::from(matches.value_of_os(ROC_FILE).unwrap()));
let metadata = fs::metadata(os_str)?;
roc_files_recursive(os_str, metadata.file_type(), &mut roc_files)?;
}
generate_docs_html(roc_files);
Ok(())
}
fn roc_files_recursive<P: AsRef<Path>>(
path: P,
file_type: FileType,
roc_files: &mut Vec<PathBuf>,
) -> io::Result<()> {
if file_type.is_dir() {
for entry_res in fs::read_dir(path)? {
let entry = entry_res?;
roc_files_recursive(entry.path(), entry.file_type()?, roc_files)?;
}
} else {
roc_files.push(path.as_ref().to_path_buf());
}
Ok(()) Ok(())
} }