Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
run: cp scripts/install.sh _site/install.sh

- name: Write CNAME for custom domain
run: echo "beamtalk.dev" > _site/CNAME
run: echo "www.beamtalk.dev" > _site/CNAME

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Beamtalk

[![CI](https://github.com/jamesc/beamtalk/actions/workflows/ci.yml/badge.svg)](https://github.com/jamesc/beamtalk/actions/workflows/ci.yml)
[![API Docs](https://img.shields.io/badge/docs-API%20Reference-blue)](https://jamesc.github.io/beamtalk/apidocs/)
[![API Docs](https://img.shields.io/badge/docs-API%20Reference-blue)](https://www.beamtalk.dev/apidocs/)
[![Rust coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jamesc/beamtalk/badges/rust-coverage.json)](https://github.com/jamesc/beamtalk/actions/workflows/ci.yml)
[![Erlang coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jamesc/beamtalk/badges/erlang-coverage.json)](https://github.com/jamesc/beamtalk/actions/workflows/ci.yml)

Expand Down Expand Up @@ -623,7 +623,7 @@ BEAMTALK_REPL_PORT=9999
## Documentation

📚 **[Documentation Index](docs/README.md)** — Start here for a guided tour
🌐 **[API Reference](https://jamesc.github.io/beamtalk/apidocs/)** — Standard library API docs (auto-generated)
🌐 **[API Reference](https://www.beamtalk.dev/apidocs/)** — Standard library API docs (auto-generated)
📖 **[Documentation Site](https://jamesc.github.io/beamtalk/)** — Full docs including language features, principles, and architecture

### Core Documents
Expand Down
56 changes: 56 additions & 0 deletions crates/beamtalk-cli/src/commands/doc/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ body {
color: var(--fg);
}

.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 9;
}
.sidebar-overlay.active { display: block; }

@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); }
.sidebar.open {
Expand All @@ -235,6 +244,42 @@ body {
.sidebar-toggle { display: block; }
.main-content { margin-left: 0; padding: 2rem 1.25rem; padding-top: 3.5rem; }
.nav-links a:not(.nav-github):not(:first-child) { display: none; }

/* Collapse TOC to single column */
.toc ul { column-count: 1; }

/* Make tables scroll horizontally */
table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Scale hero heading */
.landing-hero-text h1 { font-size: 2rem; }

/* Scale page headings */
h1 { font-size: 1.5rem; }

/* Stack method header on narrow screens */
.method-header { flex-direction: column; gap: 0.4rem; }
.source-link { margin-left: 0; }

/* Code block horizontal scroll with tighter padding */
pre { padding: 1rem; font-size: 0.82rem; }
}

@media (max-width: 480px) {
/* Very small screens: tighter padding */
.main-content { padding: 1.5rem 1rem; padding-top: 3.5rem; }
.landing-content { padding: 1.5rem 1rem 1.5rem; }

/* Full-width CTAs */
.landing-cta { flex-direction: column; }
.btn-primary, .btn-secondary { width: 100%; text-align: center; }

/* Smallest heading */
.landing-hero-text h1 { font-size: 1.75rem; }

/* Reduce method card padding */
.method { padding: 1rem; }

}

/* --- Typography --- */
Expand Down Expand Up @@ -428,6 +473,17 @@ tbody tr:hover { background: var(--sidebar-bg); }
.hierarchy-tree > ul { border-left: none; padding-left: 0; }
.hierarchy-tree li { margin: 0.25rem 0; font-size: 0.9rem; }
.hierarchy-tree a { font-weight: 500; }
.hierarchy-tree .class-summary {
font-size: 0.8rem;
color: var(--fg-muted);
margin-left: 0.4rem;
}

/* --- ADR list --- */
.adr-list { list-style: none; padding: 0; margin: 1.5rem 0; }
.adr-list li { margin: 0.4rem 0; font-size: 0.95rem; }
.adr-list a { color: var(--accent); text-decoration: none; }
.adr-list a:hover { text-decoration: underline; }

/* --- Inherited methods --- */
.inherited-section { margin-top: 2.5rem; }
Expand Down
44 changes: 36 additions & 8 deletions crates/beamtalk-cli/src/commands/doc/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,17 @@ pub(super) fn write_hierarchy_tree(html: &mut String, classes: &[ClassInfo]) {
v.sort_unstable();
}

// Build name → first-line summary map
let summaries: HashMap<&str, &str> = classes
.iter()
.filter_map(|c| {
c.doc_comment
.as_ref()
.and_then(|d| d.lines().next())
.map(|line| (c.name.as_str(), line))
})
.collect();

// Find roots (classes without a parent in our set)
let class_names: std::collections::HashSet<&str> =
classes.iter().map(|c| c.name.as_str()).collect();
Expand All @@ -166,23 +177,40 @@ pub(super) fn write_hierarchy_tree(html: &mut String, classes: &[ClassInfo]) {
html.push_str("<h2>Class Hierarchy</h2>\n");
html.push_str("<ul>\n");
for root in &roots {
write_hierarchy_node(html, root, &children);
write_hierarchy_node(html, root, &children, &summaries);
}
html.push_str("</ul>\n</div>\n");
}

/// Recursively write a hierarchy tree node.
fn write_hierarchy_node(html: &mut String, name: &str, children: &HashMap<String, Vec<&str>>) {
let _ = write!(
html,
"<li><a href=\"{name}.html\">{name}</a>",
name = html_escape(name),
);
fn write_hierarchy_node(
html: &mut String,
name: &str,
children: &HashMap<String, Vec<&str>>,
summaries: &HashMap<&str, &str>,
) {
let escaped = html_escape(name);
let _ = write!(html, "<li><a href=\"{escaped}.html\">{escaped}</a>");

if let Some(summary) = summaries.get(name) {
// Strip a leading "ClassName — " prefix if present (common doc style)
let separator = " — ";
let text = summary
.find(separator)
.map_or(*summary, |i| summary[i + separator.len()..].trim());
if !text.is_empty() {
let _ = write!(
html,
" <span class=\"class-summary\">{}</span>",
html_escape(text)
);
}
}

if let Some(kids) = children.get(name) {
html.push_str("\n<ul>\n");
for kid in kids {
write_hierarchy_node(html, kid, children);
write_hierarchy_node(html, kid, children, summaries);
}
html.push_str("</ul>\n");
}
Expand Down
65 changes: 57 additions & 8 deletions crates/beamtalk-cli/src/commands/doc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use assets::{write_css, write_search_js};
use extractor::{collect_inherited_methods, find_source_files, parse_class_info};
use layout::build_sidebar_html;
use renderer::{write_class_page, write_index_page};
use site::{generate_prose_docs, write_site_landing_page};
use site::{generate_adr_docs, generate_prose_docs, write_site_landing_page};

/// Prose documentation pages to render from the docs/ directory.
const PROSE_PAGES: &[(&str, &str, &str)] = &[
Expand Down Expand Up @@ -66,6 +66,7 @@ const PROSE_PAGES: &[(&str, &str, &str)] = &[
"agent-native-development.html",
"Agent-Native Development",
),
("beamtalk-tooling.md", "tooling.html", "Tooling"),
(
"known-limitations.md",
"known-limitations.html",
Expand All @@ -88,12 +89,13 @@ pub fn run(path: &str, output_dir: &str) -> Result<()> {
Ok(())
}

/// Generate the full documentation site with landing page, API docs, and prose pages.
/// Generate the full documentation site with landing page, API docs, prose pages, and ADRs.
///
/// Site structure:
/// - `/` — Landing page with navigation
/// - `/apidocs/` — API reference (class docs from `.bt` files)
/// - `/docs/` — Prose documentation pages rendered from markdown
/// - `/adr/` — Architecture Decision Records
#[instrument(skip_all, fields(lib_path = %lib_path, docs_path = %docs_path, output = %output_dir))]
pub fn run_site(lib_path: &str, docs_path: &str, output_dir: &str) -> Result<()> {
info!("Generating documentation site");
Expand All @@ -109,14 +111,17 @@ pub fn run_site(lib_path: &str, docs_path: &str, output_dir: &str) -> Result<()>
let apidocs_path = output_path.join("apidocs");
generate_api_docs(&lib_source, &apidocs_path, "../")?;

// 2. Generate prose docs in docs/ subdirectory
// 2. Generate ADR pages; collect link-rewriting pairs for prose rendering
let docs_source = Utf8PathBuf::from(docs_path);
generate_prose_docs(&docs_source, &output_path, PROSE_PAGES)?;
let adr_links = generate_adr_docs(&docs_source, &output_path)?;

// 3. Generate landing page
// 3. Generate prose docs in docs/ subdirectory (with ADR link rewriting)
generate_prose_docs(&docs_source, &output_path, PROSE_PAGES, &adr_links)?;

// 4. Generate landing page
write_site_landing_page(&output_path, PROSE_PAGES)?;

// 4. Write shared CSS to root (prose pages and landing page reference it)
// 5. Write shared CSS to root (prose pages and landing page reference it)
write_css(&output_path)?;

println!(" Site root: {output_path}/");
Expand Down Expand Up @@ -408,6 +413,38 @@ mod tests {
assert!(html.contains("&lt;script&gt;"));
}

#[test]
fn test_render_doc_drops_html_comments() {
// Multi-line block comment (as seen in stdlib/src/README.md)
let multiline = "<!--\nCopyright 2026 James Casey\nSPDX-License-Identifier: Apache-2.0\n-->\n# Title\n\nBody.";
let html = render_doc(multiline);
assert!(
!html.contains("<!--"),
"HTML comment delimiters should be removed"
);
assert!(
!html.contains("Copyright"),
"license text should not appear in output"
);
assert!(
html.contains("<h1>Title</h1>"),
"content after comment should render"
);

// Single-line inline comment
let inline = "<!-- short comment -->\n# Title\n";
let html = render_doc(inline);
assert!(!html.contains("<!--"), "inline comment should be removed");

// Inline comment within paragraph text
let inline2 = "Before <!-- note --> after.";
let html = render_doc(inline2);
assert!(
!html.contains("<!--"),
"inline comment in paragraph should be removed"
);
}

#[test]
fn test_collect_inherited_methods() {
let parent = ClassInfo {
Expand Down Expand Up @@ -589,6 +626,11 @@ mod tests {
"# Security\n\nSecurity model.",
)
.unwrap();
fs::write(
docs_dir.join("beamtalk-tooling.md"),
"# Tooling\n\nCLI and REPL.",
)
.unwrap();
fs::write(
docs_dir.join("known-limitations.md"),
"# Known Limitations\n\nWhat's not yet supported.",
Expand All @@ -597,11 +639,12 @@ mod tests {

run_site(lib_dir.as_str(), docs_dir.as_str(), out_dir.as_str()).unwrap();

// Landing page exists
// Landing page exists and links to all sections
let landing = fs::read_to_string(out_dir.join("index.html")).unwrap();
assert!(landing.contains("Beamtalk"));
assert!(landing.contains("apidocs/"));
assert!(landing.contains("docs/language-features.html"));
assert!(landing.contains("adr/"));

// API docs in subdirectory
assert!(out_dir.join("apidocs/index.html").exists());
Expand All @@ -612,15 +655,21 @@ mod tests {
let api_index = fs::read_to_string(out_dir.join("apidocs/index.html")).unwrap();
assert!(api_index.contains("../"));

// Prose docs
// Prose docs (including new Tooling page)
assert!(out_dir.join("docs/language-features.html").exists());
assert!(out_dir.join("docs/principles.html").exists());
assert!(out_dir.join("docs/tooling.html").exists());
assert!(out_dir.join("docs/known-limitations.html").exists());

let prose = fs::read_to_string(out_dir.join("docs/language-features.html")).unwrap();
assert!(prose.contains("Language Features"));
assert!(prose.contains("../style.css"));
assert!(prose.contains("../apidocs/"));
// Prose nav includes ADR link
assert!(prose.contains("../adr/"));

// ADR index (no ADR dir in test, so directory is absent but run_site succeeds)
// The adr/ dir is only created when docs/ADR/ exists in the source

// Root CSS
assert!(out_dir.join("style.css").exists());
Expand Down
Loading