Compare commits

...

7 Commits

Author SHA1 Message Date
Nate Butler
8f195b511d use hbs 2025-04-28 15:42:55 -04:00
Nate Butler
db669083fa tidy, remove hero 2025-04-28 13:44:45 -04:00
Nate Butler
ff468f6b52 Move css/js to their own files 2025-04-28 13:37:08 -04:00
Nate Butler
7f46a19f93 wip 2025-04-28 13:33:20 -04:00
Nate Butler
4f6a5deacc Fix absolute paths -> relative 2025-04-28 13:28:43 -04:00
Nate Butler
7272794452 Tidy, ignoew out dir 2025-04-28 13:24:00 -04:00
Nate Butler
35b6e95122 site wip 2025-04-28 13:05:08 -04:00
18 changed files with 1624 additions and 6 deletions

143
Cargo.lock generated
View File

@@ -87,7 +87,7 @@ dependencies = [
"linkme",
"log",
"lsp",
"markdown",
"markdown 0.1.0",
"menu",
"multi_buffer",
"ordered-float 2.10.1",
@@ -4371,7 +4371,7 @@ dependencies = [
"linkme",
"log",
"lsp",
"markdown",
"markdown 0.1.0",
"pretty_assertions",
"project",
"rand 0.8.5",
@@ -4680,7 +4680,7 @@ dependencies = [
"linkify",
"log",
"lsp",
"markdown",
"markdown 0.1.0",
"menu",
"multi_buffer",
"ordered-float 2.10.1",
@@ -5869,6 +5869,15 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width 0.1.14",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -6023,7 +6032,7 @@ dependencies = [
"linkify",
"linkme",
"log",
"markdown",
"markdown 0.1.0",
"menu",
"multi_buffer",
"notifications",
@@ -6272,6 +6281,25 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "gpui_site"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"fs_extra",
"gpui",
"handlebars 4.5.0",
"html-escape",
"markdown 0.3.0",
"pulldown-cmark 0.9.6",
"serde",
"serde_json",
"syntect",
"toml 0.8.20",
"walkdir",
]
[[package]]
name = "gpui_tokio"
version = "0.1.0"
@@ -6586,6 +6614,15 @@ version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]]
name = "html-escape"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
"utf8-width",
]
[[package]]
name = "html5ever"
version = "0.27.0"
@@ -8114,6 +8151,12 @@ dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkify"
version = "0.10.0"
@@ -8498,6 +8541,17 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "markdown"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef3aab6a1d529b112695f72beec5ee80e729cb45af58663ec902c8fac764ecdd"
dependencies = [
"lazy_static",
"pipeline",
"regex",
]
[[package]]
name = "markdown_preview"
version = "0.1.0"
@@ -9596,6 +9650,28 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags 1.3.2",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "oo7"
version = "0.4.3"
@@ -10652,6 +10728,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pipeline"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0"
[[package]]
name = "piper"
version = "0.2.4"
@@ -11287,6 +11369,18 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
dependencies = [
"bitflags 2.9.0",
"getopts",
"memchr",
"unicase",
]
[[package]]
name = "pulldown-cmark"
version = "0.10.3"
@@ -11664,7 +11758,7 @@ dependencies = [
"gpui",
"language",
"log",
"markdown",
"markdown 0.1.0",
"menu",
"ordered-float 2.10.1",
"paths",
@@ -14210,6 +14304,28 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "syntect"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
dependencies = [
"bincode",
"bitflags 1.3.2",
"flate2",
"fnv",
"once_cell",
"onig",
"plist",
"regex-syntax 0.8.5",
"serde",
"serde_derive",
"serde_json",
"thiserror 1.0.69",
"walkdir",
"yaml-rust",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
@@ -15682,7 +15798,7 @@ name = "ui_prompt"
version = "0.1.0"
dependencies = [
"gpui",
"markdown",
"markdown 0.1.0",
"menu",
"settings",
"theme",
@@ -15865,6 +15981,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -18336,6 +18458,15 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yaml-rust2"
version = "0.8.1"

View File

@@ -67,6 +67,7 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/gpui_tokio",
"crates/gpui_site",
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
@@ -272,6 +273,7 @@ google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false, features = [
"http_client",
] }
gpui_site = { path = "crates/gpui_site" }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }

1
crates/gpui_site/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/out

View File

@@ -0,0 +1,24 @@
[package]
name = "gpui_site"
version = "0.1.0"
edition = "2021"
description = "Static site generator for the gpui documentation"
[dependencies]
gpui = { path = "../gpui" }
markdown = "0.3"
pulldown-cmark = "0.9"
syntect = "5.2.0"
handlebars = "4.3"
clap = { version = "4.4", features = ["derive"] }
fs_extra = "1.3"
walkdir = "2.4"
toml = "0.8"
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
html-escape = "0.2"
[[bin]]
name = "gpui_site"
path = "src/main.rs"

View File

@@ -0,0 +1,314 @@
/* Base styles */
:root {
--bg-color: #ffffff;
--text-color: #333333;
--primary-color: #3060b8;
--secondary-color: #5a80d0;
--accent-color: #ff6b6b;
--border-color: #e0e0e0;
--code-bg: #f5f5f5;
--header-bg: #1a1a2e;
--header-text: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a2e;
--text-color: #e0e0e0;
--primary-color: #5a80d0;
--secondary-color: #3060b8;
--accent-color: #ff6b6b;
--border-color: #444;
--code-bg: #282c34;
--header-bg: #0f0f1a;
--header-text: #ffffff;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background: var(--bg-color);
}
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
header {
background: var(--header-bg);
color: var(--header-text);
padding: 1rem 0;
}
header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--header-text);
text-decoration: none;
}
nav ul {
display: flex;
list-style: none;
}
nav ul li {
margin-left: 1.5rem;
position: relative;
}
nav ul li a, nav ul li span {
color: var(--header-text);
text-decoration: none;
font-weight: 500;
cursor: pointer;
}
nav ul li a:hover {
text-decoration: underline;
}
nav ul li ul {
display: none;
position: absolute;
background: var(--header-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.5rem 0;
min-width: 150px;
flex-direction: column;
z-index: 100;
}
nav ul li:hover ul {
display: flex;
}
nav ul li ul li {
margin: 0;
padding: 0.5rem 1rem;
}
/* Hero section */
.hero {
text-align: center;
padding: 4rem 0;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.tagline {
font-size: 1.5rem;
color: var(--secondary-color);
margin-bottom: 2rem;
}
.cta-buttons {
display: flex;
justify-content: center;
gap: 1rem;
}
.button {
display: inline-block;
padding: 0.8rem 1.5rem;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
transition: all 0.3s ease;
}
.button.primary {
background: var(--primary-color);
color: white;
}
.button.secondary {
background: transparent;
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
/* Content section */
.content {
padding: 3rem 0;
}
.content h2 {
font-size: 2rem;
margin: 2rem 0 1rem;
}
.content p {
margin-bottom: 1.5rem;
}
.content code {
background: var(--code-bg);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
}
.content pre {
background: var(--code-bg);
padding: 1rem;
border-radius: 5px;
overflow-x: auto;
margin: 1.5rem 0;
}
.content pre code {
padding: 0;
background: transparent;
}
/* Examples grid */
.examples-grid {
padding: 3rem 0;
}
.examples-grid h2 {
font-size: 2rem;
margin-bottom: 2rem;
text-align: center;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.example-card {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
text-decoration: none;
color: var(--text-color);
transition: all 0.3s ease;
}
.example-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
.example-card h3 {
color: var(--primary-color);
margin-bottom: 0.5rem;
}
/* Example page */
.example {
padding: 2rem 0;
}
.example h1 {
margin-bottom: 1rem;
}
.code-container {
margin: 2rem 0;
border-radius: 8px;
overflow: hidden;
}
.example-info {
background: var(--code-bg);
padding: 1.5rem;
border-radius: 8px;
margin-top: 2rem;
}
/* Documentation page */
.documentation {
padding: 2rem 0;
}
.documentation h1 {
margin-bottom: 2rem;
}
.documentation .content h2 {
margin-top: 3rem;
}
.documentation .content h3 {
margin-top: 2rem;
margin-bottom: 1rem;
}
.documentation .content ul,
.documentation .content ol {
margin-left: 2rem;
margin-bottom: 1.5rem;
}
/* Footer */
footer {
background: var(--header-bg);
color: var(--header-text);
padding: 2rem 0;
margin-top: 3rem;
text-align: center;
}
footer a {
color: var(--primary-color);
}
/* Responsive */
@media (max-width: 768px) {
header .container {
flex-direction: column;
gap: 1rem;
}
nav ul {
flex-wrap: wrap;
justify-content: center;
}
nav ul li {
margin: 0.5rem;
}
.hero h1 {
font-size: 2.5rem;
}
.tagline {
font-size: 1.2rem;
}
.grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,40 @@
// Basic JavaScript functionality for the gpui site
document.addEventListener('DOMContentLoaded', function() {
// Add syntax highlighting if needed
// This is a placeholder - we're using server-side syntax highlighting in this example
// Toggle mobile navigation
const navToggle = document.querySelector('.nav-toggle');
if (navToggle) {
navToggle.addEventListener('click', function() {
const nav = document.querySelector('nav ul');
nav.classList.toggle('show');
});
}
// Add clipboard functionality to code blocks
document.querySelectorAll('pre code').forEach((block) => {
const copyButton = document.createElement('button');
copyButton.className = 'copy-button';
copyButton.textContent = 'Copy';
const pre = block.parentNode;
pre.style.position = 'relative';
pre.appendChild(copyButton);
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(block.textContent).then(() => {
copyButton.textContent = 'Copied!';
setTimeout(() => {
copyButton.textContent = 'Copy';
}, 2000);
}, () => {
copyButton.textContent = 'Failed!';
setTimeout(() => {
copyButton.textContent = 'Copy';
}, 2000);
});
});
});
});

View File

@@ -0,0 +1,309 @@
/* Base styles */
:root {
--bg-color: #ffffff;
--text-color: #333333;
--primary-color: #3060b8;
--secondary-color: #5a80d0;
--accent-color: #ff6b6b;
--border-color: #e0e0e0;
--code-bg: #f5f5f5;
--header-bg: #1a1a2e;
--header-text: #ffffff;
}
/* @media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a2e;
--text-color: #e0e0e0;
--primary-color: #5a80d0;
--secondary-color: #3060b8;
--accent-color: #ff6b6b;
--border-color: #444;
--code-bg: #282c34;
--header-bg: #0f0f1a;
--header-text: #ffffff;
}
} */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
line-height: 1.6;
color: var(--text-color);
background: var(--bg-color);
}
.container {
width: 100%;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
header {
padding: 1rem 0;
}
header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
text-decoration: none;
}
nav ul {
display: flex;
list-style: none;
}
nav ul li {
margin-left: 1.5rem;
position: relative;
}
nav ul li a,
nav ul li span {
text-decoration: none;
cursor: pointer;
}
nav ul li a:hover {
text-decoration: underline;
}
nav ul li ul {
display: none;
position: absolute;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.5rem 0;
min-width: 150px;
flex-direction: column;
z-index: 100;
}
nav ul li:hover ul {
display: flex;
}
nav ul li ul li {
margin: 0;
padding: 0.5rem 1rem;
}
/* Hero section */
.hero {
text-align: center;
padding: 4rem 0;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.tagline {
font-size: 1.5rem;
color: var(--secondary-color);
margin-bottom: 2rem;
}
.cta-buttons {
display: flex;
justify-content: center;
gap: 1rem;
}
.button {
display: inline-block;
padding: 0.8rem 1.5rem;
border-radius: 4px;
text-decoration: none;
transition: all 0.3s ease;
}
.button.primary {
background: var(--primary-color);
color: white;
}
.button.secondary {
background: transparent;
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Content section */
.content {
padding: 3rem 0;
}
.content h2 {
font-size: 2rem;
margin: 2rem 0 1rem;
}
.content p {
margin-bottom: 1.5rem;
}
.content code {
background: var(--code-bg);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}
.content pre {
background: var(--code-bg);
padding: 1rem;
border-radius: 5px;
overflow-x: auto;
margin: 1.5rem 0;
}
.content pre code {
padding: 0;
background: transparent;
}
/* Examples grid */
.examples-grid {
padding: 3rem 0;
}
.examples-grid h2 {
font-size: 2rem;
margin-bottom: 2rem;
text-align: center;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.example-card {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
text-decoration: none;
color: var(--text-color);
transition: all 0.3s ease;
}
.example-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.example-card h3 {
color: var(--primary-color);
margin-bottom: 0.5rem;
}
/* Example page */
.example {
padding: 2rem 0;
}
.example h1 {
margin-bottom: 1rem;
}
.code-container {
margin: 2rem 0;
border-radius: 8px;
overflow: hidden;
}
.example-info {
background: var(--code-bg);
padding: 1.5rem;
border-radius: 8px;
margin-top: 2rem;
}
/* Documentation page */
.documentation {
padding: 2rem 0;
}
.documentation h1 {
margin-bottom: 2rem;
}
.documentation .content h2 {
margin-top: 3rem;
}
.documentation .content h3 {
margin-top: 2rem;
margin-bottom: 1rem;
}
.documentation .content ul,
.documentation .content ol {
margin-left: 2rem;
margin-bottom: 1.5rem;
}
/* Footer */
footer {
background: var(--header-bg);
color: var(--header-text);
padding: 2rem 0;
margin-top: 3rem;
text-align: center;
}
footer a {
color: var(--primary-color);
}
/* Responsive */
@media (max-width: 768px) {
header .container {
flex-direction: column;
gap: 1rem;
}
nav ul {
flex-wrap: wrap;
justify-content: center;
}
nav ul li {
margin: 0.5rem;
}
.hero h1 {
font-size: 2.5rem;
}
.tagline {
font-size: 1.2rem;
}
.grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,21 @@
// Basic JavaScript functionality for the gpui site
document.addEventListener("DOMContentLoaded", function () {
// Add clipboard functionality to code blocks
// document.querySelectorAll('pre code').forEach((block) => {
// const copyButton = document.createElement('button');
// copyButton.className = 'copy-button';
// copyButton.textContent = 'Copy';
// const pre = block.parentNode;
// pre.style.position = 'relative';
// pre.appendChild(copyButton);
// copyButton.addEventListener('click', () => {
// navigator.clipboard.writeText(block.textContent).then(() => {
// copyButton.textContent = 'Copied!';
// setTimeout(() => {
// copyButton.textContent = 'Copy';
// }, 2000);
// });
// });
// });
});

View File

@@ -0,0 +1,225 @@
use crate::templates::ExampleInfo;
use anyhow::{Context, Result};
use std::path::Path;
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;
/// Collect information about examples in the gpui crate
pub fn collect_examples(gpui_dir: &Path) -> Result<Vec<ExampleInfo>> {
let examples_dir = gpui_dir.join("examples");
let mut examples = Vec::new();
// Check if the examples directory exists
if !examples_dir.exists() {
return Ok(examples);
}
// Read the Cargo.toml to get example information
let cargo_toml_path = gpui_dir.join("Cargo.toml");
let cargo_toml = std::fs::read_to_string(&cargo_toml_path).with_context(|| {
format!(
"Failed to read Cargo.toml from {}",
cargo_toml_path.display()
)
})?;
let cargo_data: toml::Value =
toml::from_str(&cargo_toml).with_context(|| "Failed to parse Cargo.toml")?;
// Extract example definitions from Cargo.toml
if let Some(example_array) = cargo_data.get("example").and_then(|v| v.as_array()) {
for example in example_array {
if let (Some(name), Some(path)) = (
example.get("name").and_then(|v| v.as_str()),
example.get("path").and_then(|v| v.as_str()),
) {
// We don't need the example_path variable here
let title = title_case(name);
// Read the first comment block to extract description, if any
let mut description = format!("Example demonstrating {}", title.to_lowercase());
if let Ok(content) = std::fs::read_to_string(gpui_dir.join(path)) {
if let Some(comment) = extract_first_comment(&content) {
description = comment;
}
}
examples.push(ExampleInfo {
name: name.to_string(),
title,
description,
path: format!("{}.html", name),
});
}
}
} else {
// If no example definitions in Cargo.toml, scan the examples directory
for entry in walkdir::WalkDir::new(&examples_dir) {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") {
let file_stem = path
.file_stem()
.ok_or_else(|| {
anyhow::anyhow!("Failed to get file stem for {}", path.display())
})?
.to_string_lossy();
// Skip mod.rs or lib.rs files
if file_stem == "mod" || file_stem == "lib" {
continue;
}
let name = file_stem.to_string();
let title = title_case(&name);
// Read the first comment block to extract description, if any
let mut description = format!("Example demonstrating {}", title.to_lowercase());
if let Ok(content) = std::fs::read_to_string(path) {
if let Some(comment) = extract_first_comment(&content) {
description = comment;
}
}
examples.push(ExampleInfo {
name: name.clone(),
title,
description,
path: format!("{}.html", name),
});
}
}
}
// Sort examples by name
examples.sort_by(|a, b| a.name.cmp(&b.name));
Ok(examples)
}
/// Read an example file and return its highlighted HTML content
pub fn read_example_file(gpui_dir: &Path, example_name: &str) -> Result<String> {
// First try to find the example in Cargo.toml
let cargo_toml_path = gpui_dir.join("Cargo.toml");
let cargo_toml = std::fs::read_to_string(cargo_toml_path)?;
let cargo_data: toml::Value = toml::from_str(&cargo_toml)?;
let mut example_path = None;
// Look for example in Cargo.toml
if let Some(example_array) = cargo_data.get("example").and_then(|v| v.as_array()) {
for example in example_array {
if let (Some(name), Some(path)) = (
example.get("name").and_then(|v| v.as_str()),
example.get("path").and_then(|v| v.as_str()),
) {
if name == example_name {
example_path = Some(gpui_dir.join(path));
break;
}
}
}
}
// If not found in Cargo.toml, look in examples directory
if example_path.is_none() {
let examples_dir = gpui_dir.join("examples");
let rs_file = examples_dir.join(format!("{}.rs", example_name));
if rs_file.exists() {
example_path = Some(rs_file);
} else {
// Check if it's in a subdirectory
let dir_file = examples_dir
.join(example_name)
.join(format!("{}.rs", example_name));
if dir_file.exists() {
example_path = Some(dir_file);
}
}
}
// Read and highlight the code
if let Some(path) = example_path {
let code = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read example file: {}", path.display()))?;
// Syntax highlight the code
highlight_rust_code(&code)
} else {
Err(anyhow::anyhow!("Example file not found: {}", example_name))
}
}
/// Highlight Rust code
fn highlight_rust_code(code: &str) -> Result<String> {
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = ThemeSet::load_defaults();
let theme = &theme_set.themes["base16-ocean.dark"];
let syntax = syntax_set
.find_syntax_by_extension("rs")
.ok_or_else(|| anyhow::anyhow!("Could not find Rust syntax"))?;
let highlighted = highlighted_html_for_string(code, &syntax_set, syntax, theme)?;
Ok(highlighted)
}
/// Extract the first comment block from a Rust file
fn extract_first_comment(content: &str) -> Option<String> {
let mut comment_lines = Vec::new();
let mut in_multiline_comment = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("//") {
// Single line comment
let comment_text = trimmed.trim_start_matches("//").trim();
comment_lines.push(comment_text.to_string());
} else if trimmed.starts_with("/*") {
// Start of multi-line comment
in_multiline_comment = true;
let comment_text = trimmed.trim_start_matches("/*").trim();
if !comment_text.is_empty() {
comment_lines.push(comment_text.to_string());
}
} else if in_multiline_comment && trimmed.contains("*/") {
let comment_text = trimmed.split("*/").next().unwrap_or("").trim();
if !comment_text.is_empty() {
comment_lines.push(comment_text.to_string());
}
break;
} else if in_multiline_comment {
// Middle of multi-line comment
comment_lines.push(trimmed.to_string());
} else if !trimmed.is_empty() && !comment_lines.is_empty() {
// We've hit non-comment code, stop looking
break;
}
}
if comment_lines.is_empty() {
None
} else {
Some(comment_lines.join(" "))
}
}
/// Convert a snake_case string to Title Case
fn title_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect::<Vec<_>>()
.join(" ")
}

View File

@@ -0,0 +1,175 @@
use crate::examples::{collect_examples, read_example_file};
use crate::markdown::read_markdown_file;
use crate::templates::{DocInfo, SiteContent, TemplateEngine};
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
/// Generate the complete gpui site
pub fn generate_site(gpui_dir: &Path, output_dir: &Path) -> Result<()> {
// Create necessary directories
fs::create_dir_all(output_dir.join("examples"))?;
fs::create_dir_all(output_dir.join("docs"))?;
fs::create_dir_all(output_dir.join("css"))?;
fs::create_dir_all(output_dir.join("js"))?;
// Collect examples
let examples = collect_examples(gpui_dir)?;
println!("Found {} examples", examples.len());
// Collect docs
let docs = collect_docs(gpui_dir)?;
println!("Found {} docs", docs.len());
// Create site content
let site_content = SiteContent {
title: "gpui".to_string(),
content: process_readme(gpui_dir)?,
examples: examples.clone(),
docs: docs.clone(),
};
// Create template engine
let templates_dir = output_dir.join("templates");
let template_engine = TemplateEngine::new(&templates_dir)?;
// Generate index page
let index_html = template_engine.render_index(&site_content)?;
fs::write(output_dir.join("index.html"), index_html)?;
// Generate example pages
for example in &examples {
let code = read_example_file(gpui_dir, &example.name)?;
let html = template_engine.render_example(example, &code, &site_content)?;
fs::write(output_dir.join("examples").join(&example.path), html)?;
}
// Generate doc pages
for doc in &docs {
let doc_path = get_doc_path(gpui_dir, &doc.name)?;
let content = read_markdown_file(&doc_path)?;
let html = template_engine.render_doc(doc, &content, &site_content)?;
fs::write(output_dir.join("docs").join(&doc.path), html)?;
}
// Copy assets
generate_css(output_dir)?;
generate_js(output_dir)?;
Ok(())
}
/// Process the README for the index page
fn process_readme(gpui_dir: &Path) -> Result<String> {
let readme_path = gpui_dir.join("README.md");
read_markdown_file(&readme_path)
}
/// Collect documentation files
fn collect_docs(gpui_dir: &Path) -> Result<Vec<DocInfo>> {
let docs_dir = gpui_dir.join("docs");
let mut docs = Vec::new();
// Check if docs directory exists
if !docs_dir.exists() {
return Ok(docs);
}
// Add README.md as intro document
docs.push(DocInfo {
name: "README.md".to_string(),
title: "Introduction".to_string(),
path: "intro.html".to_string(),
});
// Walk docs directory
for entry in walkdir::WalkDir::new(&docs_dir) {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "md") {
let file_name = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("Failed to get file name for {}", path.display()))?
.to_string_lossy();
let file_stem = path
.file_stem()
.ok_or_else(|| anyhow::anyhow!("Failed to get file stem for {}", path.display()))?
.to_string_lossy();
// Extract title from filename
let title = title_case(&file_stem);
docs.push(DocInfo {
name: file_name.to_string(),
title,
path: format!("{}.html", file_stem),
});
}
}
// Sort docs by name, but keep README.md as first item
let intro = docs.remove(0);
docs.sort_by(|a, b| a.name.cmp(&b.name));
docs.insert(0, intro);
Ok(docs)
}
/// Get the path to a doc file
fn get_doc_path(gpui_dir: &Path, doc_name: &str) -> Result<PathBuf> {
if doc_name == "README.md" {
Ok(gpui_dir.join("README.md"))
} else {
Ok(gpui_dir.join("docs").join(doc_name))
}
}
/// Generate CSS files
fn generate_css(output_dir: &Path) -> Result<()> {
// Create the css directory if it doesn't exist
let css_dir = output_dir.join("css");
fs::create_dir_all(&css_dir)?;
// Read the CSS file from our assets directory
let css_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/assets/css/styles.css");
let css_content = fs::read_to_string(&css_path)
.with_context(|| format!("Failed to read CSS file from {}", css_path.display()))?;
// Write to the output directory
fs::write(css_dir.join("styles.css"), css_content)?;
Ok(())
}
/// Generate JS files
fn generate_js(output_dir: &Path) -> Result<()> {
// Create the js directory if it doesn't exist
let js_dir = output_dir.join("js");
fs::create_dir_all(&js_dir)?;
// Read the JS file from our assets directory
let js_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/assets/js/main.js");
let js_content = fs::read_to_string(&js_path)
.with_context(|| format!("Failed to read JS file from {}", js_path.display()))?;
// Write to the output directory
fs::write(js_dir.join("main.js"), js_content)?;
Ok(())
}
/// Convert a snake_case string to Title Case
fn title_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect::<Vec<_>>()
.join(" ")
}

View File

@@ -0,0 +1,64 @@
use anyhow::{Context, Result};
use clap::Parser;
use std::path::PathBuf;
mod templates;
mod markdown;
mod examples;
mod generator;
/// Static site generator for gpui documentation
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Output directory for the generated site
#[arg(short, long, default_value = "out")]
output_dir: PathBuf,
/// gpui crate directory
#[arg(short, long)]
gpui_dir: Option<PathBuf>,
}
fn main() -> Result<()> {
let args = Args::parse();
// Determine gpui directory
let gpui_dir = args.gpui_dir.unwrap_or_else(|| {
// Default to the sibling directory if not specified
let current_dir = std::env::current_dir()
.expect("Failed to get current directory");
current_dir
.parent()
.expect("Failed to get parent directory")
.join("gpui")
});
// Determine output directory - make it relative to the gpui_site crate directory
let output_dir = if args.output_dir.is_absolute() {
args.output_dir
} else {
// Find path to gpui_site crate directory regardless of where we're running from
let workspace_root = std::env::current_dir().expect("Failed to get current directory");
let gpui_site_dir = workspace_root.join("crates").join("gpui_site");
// Create the output path relative to the gpui_site directory
gpui_site_dir.join(&args.output_dir)
};
// Create output directory if it doesn't exist
std::fs::create_dir_all(&output_dir)
.with_context(|| format!("Failed to create output directory: {}", output_dir.display()))?;
println!("Output directory: {}", output_dir.display());
println!("Generating gpui site from {} to {}",
gpui_dir.display(),
output_dir.display());
// Generate the site
generator::generate_site(&gpui_dir, &output_dir)?;
println!("Site generation complete!");
Ok(())
}

View File

@@ -0,0 +1,75 @@
use anyhow::Result;
use html_escape;
use pulldown_cmark::{html, Options, Parser};
use std::path::Path;
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;
/// Process markdown content to HTML with code highlighting
pub fn markdown_to_html(content: &str) -> Result<String> {
// Setup parser with CommonMark options
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(content, options);
// Transform to HTML
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
// Process code blocks for syntax highlighting
let html_with_highlighted_code = highlight_code_blocks(&html_output)?;
Ok(html_with_highlighted_code)
}
/// Highlight code blocks in HTML
fn highlight_code_blocks(html: &str) -> Result<String> {
// This is a simplified version - a real implementation would need to parse HTML
// and replace code blocks with highlighted versions
// Load syntax highlighting resources
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = ThemeSet::load_defaults();
let theme = &theme_set.themes["base16-ocean.dark"];
// For this simple example, we'll just look for Rust code blocks
// A real implementation would need proper HTML parsing
let mut result = html.to_string();
// Replace code blocks with syntax highlighted versions
// This is a very naive implementation for demonstration
if let Some(start) = result.find("<code class=\"language-rust\">") {
if let Some(end) = result[start..].find("</code>") {
let code_start = start + "<code class=\"language-rust\">".len();
let code_end = start + end;
let code = &result[code_start..code_end];
// Unescape HTML entities in the code
let unescaped_code = html_escape::decode_html_entities(code);
// Highlight the code
let highlighted = highlighted_html_for_string(
&unescaped_code,
&syntax_set,
syntax_set.find_syntax_by_extension("rs").unwrap(),
theme,
)?;
// Replace the original code block with the highlighted version
result.replace_range(code_start..code_end, &highlighted);
}
}
Ok(result)
}
/// Read a markdown file and convert it to HTML
pub fn read_markdown_file(path: &Path) -> Result<String> {
let content = std::fs::read_to_string(path)?;
markdown_to_html(&content)
}

View File

@@ -0,0 +1,96 @@
use anyhow::Result;
use handlebars::Handlebars;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
/// Represents site content for templating
#[derive(Serialize)]
pub struct SiteContent {
pub title: String,
pub content: String,
pub examples: Vec<ExampleInfo>,
pub docs: Vec<DocInfo>,
}
/// Information about a code example
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExampleInfo {
pub name: String,
pub title: String,
pub description: String,
pub path: String,
}
/// Information about a documentation page
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocInfo {
pub name: String,
pub title: String,
pub path: String,
}
pub struct TemplateEngine {
handlebars: Handlebars<'static>,
}
impl TemplateEngine {
pub fn new(_output_dir: &Path) -> Result<Self> {
let mut handlebars = Handlebars::new();
// Get the path to our template files in the source directory
let template_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/templates");
// Register partials and templates
handlebars.register_template_file("base", template_dir.join("base.hbs"))?;
handlebars.register_template_file("base_subdir", template_dir.join("base_subdir.hbs"))?;
handlebars.register_template_file("index", template_dir.join("index.hbs"))?;
handlebars.register_template_file("example", template_dir.join("example.hbs"))?;
handlebars.register_template_file("doc", template_dir.join("doc.hbs"))?;
Ok(Self { handlebars })
}
/// Render the index page
pub fn render_index(&self, content: &SiteContent) -> Result<String> {
let rendered = self.handlebars.render("index", content)?;
Ok(rendered)
}
/// Render an example page
pub fn render_example(
&self,
example: &ExampleInfo,
code: &str,
content: &SiteContent,
) -> Result<String> {
let ctx = serde_json::json!({
"title": &example.title,
"example": example,
"code": code,
"examples": &content.examples,
"docs": &content.docs,
});
let rendered = self.handlebars.render("example", &ctx)?;
Ok(rendered)
}
/// Render a documentation page
pub fn render_doc(
&self,
doc: &DocInfo,
content: &str,
site_content: &SiteContent,
) -> Result<String> {
let ctx = serde_json::json!({
"title": &doc.title,
"doc": doc,
"content": content,
"examples": &site_content.examples,
"docs": &site_content.docs,
});
let rendered = self.handlebars.render("doc", &ctx)?;
Ok(rendered)
}
}

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}} - gpui</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<header>
<div class="container">
<a href="index.html" class="logo">gpui</a>
<nav>
<ul>
<li><a href="index.html">Home</a></li>
<li>
<span>Examples</span>
<ul>
{{#each examples}}
<li><a href="examples/{{this.path}}">{{this.title}}</a></li>
{{/each}}
</ul>
</li>
<li>
<span>Docs</span>
<ul>
{{#each docs}}
<li><a href="docs/{{this.path}}">{{this.title}}</a></li>
{{/each}}
</ul>
</li>
<li><a href="https://github.com/zed-industries/zed/tree/main/crates/gpui">GitHub</a></li>
</ul>
</nav>
</div>
</header>
<main class="container">
{{> @partial-block}}
</main>
<footer>
<div class="container">
<p>gpui is part of the <a href="https://github.com/zed-industries/zed">Zed</a> project © Zed Industries, Inc.</p>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}} - gpui</title>
<link rel="stylesheet" href="../css/styles.css">
</head>
<body>
<header>
<div class="container">
<a href="../index.html" class="logo">gpui</a>
<nav>
<ul>
<li><a href="../index.html">Home</a></li>
<li>
<span>Examples</span>
<ul>
{{#each examples}}
<li><a href="../examples/{{this.path}}">{{this.title}}</a></li>
{{/each}}
</ul>
</li>
<li>
<span>Docs</span>
<ul>
{{#each docs}}
<li><a href="../docs/{{this.path}}">{{this.title}}</a></li>
{{/each}}
</ul>
</li>
<li><a href="https://github.com/zed-industries/zed/tree/main/crates/gpui">GitHub</a></li>
</ul>
</nav>
</div>
</header>
<main class="container">
{{> @partial-block}}
</main>
<footer>
<div class="container">
<p>gpui is part of the <a href="https://github.com/zed-industries/zed">Zed</a> project u00a9 Zed Industries, Inc.</p>
</div>
</footer>
<script src="../js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
{{#> base_subdir}}
<article class="documentation">
<h1>{{doc.title}}</h1>
<div class="content">
{{{content}}}
</div>
</article>
{{/base_subdir}}

View File

@@ -0,0 +1,16 @@
{{#> base_subdir}}
<article class="example">
<h1>{{example.title}}</h1>
<p>{{example.description}}</p>
<div class="code-container">
{{{code}}}
</div>
<div class="example-info">
<h3>Running this example</h3>
<p>You can run this example with:</p>
<pre><code>cargo run --example {{example.name}}</code></pre>
</div>
</article>
{{/base_subdir}}

View File

@@ -0,0 +1,17 @@
{{#> base}}
<section class="content">
{{{content}}}
</section>
<section class="examples-grid">
<h2>Examples</h2>
<div class="grid">
{{#each examples}}
<a href="examples/{{this.path}}" class="example-card">
<h3>{{this.title}}</h3>
<p>{{this.description}}</p>
</a>
{{/each}}
</div>
</section>
{{/base}}