Compare commits
7 Commits
github-tok
...
gpui-site
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f195b511d | ||
|
|
db669083fa | ||
|
|
ff468f6b52 | ||
|
|
7f46a19f93 | ||
|
|
4f6a5deacc | ||
|
|
7272794452 | ||
|
|
35b6e95122 |
143
Cargo.lock
generated
143
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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
1
crates/gpui_site/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/out
|
||||
24
crates/gpui_site/Cargo.toml
Normal file
24
crates/gpui_site/Cargo.toml
Normal 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"
|
||||
314
crates/gpui_site/assets/css/styles.css
Normal file
314
crates/gpui_site/assets/css/styles.css
Normal 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;
|
||||
}
|
||||
}
|
||||
40
crates/gpui_site/assets/js/main.js
Normal file
40
crates/gpui_site/assets/js/main.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
309
crates/gpui_site/src/assets/css/styles.css
Normal file
309
crates/gpui_site/src/assets/css/styles.css
Normal 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;
|
||||
}
|
||||
}
|
||||
21
crates/gpui_site/src/assets/js/main.js
Normal file
21
crates/gpui_site/src/assets/js/main.js
Normal 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);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
});
|
||||
225
crates/gpui_site/src/examples.rs
Normal file
225
crates/gpui_site/src/examples.rs
Normal 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(" ")
|
||||
}
|
||||
175
crates/gpui_site/src/generator.rs
Normal file
175
crates/gpui_site/src/generator.rs
Normal 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(" ")
|
||||
}
|
||||
64
crates/gpui_site/src/main.rs
Normal file
64
crates/gpui_site/src/main.rs
Normal 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(())
|
||||
}
|
||||
75
crates/gpui_site/src/markdown.rs
Normal file
75
crates/gpui_site/src/markdown.rs
Normal 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)
|
||||
}
|
||||
96
crates/gpui_site/src/templates.rs
Normal file
96
crates/gpui_site/src/templates.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
50
crates/gpui_site/src/templates/base.hbs
Normal file
50
crates/gpui_site/src/templates/base.hbs
Normal 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>
|
||||
50
crates/gpui_site/src/templates/base_subdir.hbs
Normal file
50
crates/gpui_site/src/templates/base_subdir.hbs
Normal 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>
|
||||
8
crates/gpui_site/src/templates/doc.hbs
Normal file
8
crates/gpui_site/src/templates/doc.hbs
Normal file
@@ -0,0 +1,8 @@
|
||||
{{#> base_subdir}}
|
||||
<article class="documentation">
|
||||
<h1>{{doc.title}}</h1>
|
||||
<div class="content">
|
||||
{{{content}}}
|
||||
</div>
|
||||
</article>
|
||||
{{/base_subdir}}
|
||||
16
crates/gpui_site/src/templates/example.hbs
Normal file
16
crates/gpui_site/src/templates/example.hbs
Normal 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}}
|
||||
17
crates/gpui_site/src/templates/index.hbs
Normal file
17
crates/gpui_site/src/templates/index.hbs
Normal 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}}
|
||||
Reference in New Issue
Block a user