Skip to content

Commit cafc36a

Browse files
committed
Add option to use custom templates and navbar links
1 parent 1f1a16a commit cafc36a

17 files changed

Lines changed: 769 additions & 298 deletions

File tree

Cargo.lock

Lines changed: 274 additions & 197 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@ Current todos:
5353
- [x] Asset items
5454
- [x] Filtering
5555
- [ ] Sorting
56-
- [ ] More customization options
57-
- [ ] Custom templates
58-
- [ ] Custom navbar links
59-
- [ ] Custom links on the resource page
56+
- [x] More customization options
57+
- [x] Custom templates
58+
- [x] Custom navbar links
6059
- [x] Categories
6160
- [ ] More language library support
6261
- [ ] Dart
@@ -79,6 +78,23 @@ steps:
7978
8079
If you only want to generate the index or docs separately, you can use the `generate` or `docs` commands.
8180

81+
### Custom docs templates and public files
82+
83+
You can override the built-in Handlebars templates and bundled public files by passing a custom root directory:
84+
85+
```bash
86+
luna docs output/docs output/index.json --custom-root custom-docs
87+
```
88+
89+
Supported override structure inside `custom-docs`:
90+
91+
- `templates/**/*.hbs`
92+
- `components/**/*.hbs`
93+
- `layouts/**/*.hbs`
94+
- `public/**/*` (for files like `search.js`, `download.js`, `main.css`, ...)
95+
96+
Only files you provide are overridden; all other files still use Luna defaults.
97+
8298
## Contributing
8399

84100
We are happy to see that you are interested in contributing to Luna.

SECURITY.md

Lines changed: 0 additions & 32 deletions
This file was deleted.

api/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ edition = "2024"
88
[dependencies]
99
serde = { version = "1", features = ["derive"] }
1010
serde_json = "1"
11-
toml = "0.9"
11+
toml = "1.0"

api/src/models/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ pub struct RepositoryInfo {
5050
pub privacy: Option<String>,
5151
#[serde(default)]
5252
pub links: HashMap<String, String>,
53+
#[serde(default)]
54+
pub navbar_links: HashMap<String, String>,
5355
}
5456

5557
impl Named for RepositoryInfo {

cli/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ edition = "2024"
88
[dependencies]
99
clap = { version = "4", features = ["derive"] }
1010
luna_api = { path = "../api" }
11-
toml = "0.9"
11+
toml = "1.0"
1212
serde = { version = "1", features = ["derive"] }
1313
thiserror = "2"
1414
serde_json = "1"
@@ -20,7 +20,7 @@ anyhow = "1.0"
2020
axum = "0.8"
2121
tokio = { version = "1", features = ["full"] }
2222
tower-http = { version = "0.6", features = ["fs"] }
23-
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"] }
23+
reqwest = { version = "0.13", features = ["blocking", "rustls"] }
2424
sha2 = "0.10"
2525
hex = "0.4"
2626

cli/assets/layouts/asset.hbs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,22 @@
1515
</ul>
1616
</div>
1717
{{/if}}
18-
<nav aria-label="Asset navigation">
18+
<nav aria-label="Asset navigation" class="row wrap justify-between">
1919
<ul class="row wrap gap-xs">
2020
<li><a class="btn {{#if (eq activeTab 'home')}}active{{/if}}" href="{{asset.author}}/{{asset.name}}">Home</a></li>
2121
<li><a class="btn {{#if (eq activeTab 'changes')}}active{{/if}}" href="{{asset.author}}/{{asset.name}}/changes.html">Changes</a></li>
2222
</ul>
23+
<ul class="row wrap gap-xs">
24+
{{#if asset.website}}
25+
<a class="btn secondary" href="{{asset.website}}" target="_blank" rel="noopener noreferrer">Website</a>
26+
{{/if}}
27+
{{#if asset.source}}
28+
<a class="btn secondary" href="{{asset.source}}" target="_blank" rel="noopener noreferrer">Source</a>
29+
{{/if}}
30+
{{#if asset.sponsor}}
31+
<a class="btn secondary" href="{{asset.sponsor}}" target="_blank" rel="noopener noreferrer">Sponsor</a>
32+
{{/if}}
33+
</ul>
2334
</nav>
2435
</section>
2536
{{> @partial-block }}

cli/assets/layouts/main.hbs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
<a class="btn secondary {{#if (eq active_page 'authors')}}active{{/if}}" href="authors.html">Authors</a>
99
<a class="btn secondary {{#if (eq active_page 'search')}}active{{/if}}" href="search.html">Search</a>
1010
<div class="h-divider"></div>
11-
<a class="btn secondary" href="https://github.com/LinwoodDev/Luna">GitHub</a>
11+
{{#each info.navbar_links}}
12+
<a class="btn secondary" href="{{this}}" target="_blank" rel="noopener noreferrer">{{@key}}</a>
13+
{{/each}}
1214
</nav>
1315
</header>
1416
<div class="container-lg">

cli/src/docs.rs

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use clap::ValueEnum;
22
use sha2::{Digest, Sha256};
3-
use std::{fs, io::Write, path::Path};
3+
use std::{fs, io::Write, path::{Path, PathBuf}};
44

55
use handlebars::handlebars_helper;
66
use luna_api::models::{RepositoryData, asset::Version};
@@ -87,13 +87,72 @@ fn build_engine() -> Result<HandlebarsTemplateEngine<'static>, DocsError> {
8787
engine.registry().register_helper("dec", Box::new(dec));
8888
engine.registry().register_helper("sub", Box::new(sub));
8989
engine.registry().register_helper("slug", Box::new(slug));
90-
engine
91-
.registry()
92-
.register_embed_templates::<Templates>()
93-
.map_err(|e| DocsError::Template(Box::new(e)))?;
90+
register_embed_templates(&mut engine)?;
9491
Ok(engine)
9592
}
9693

94+
fn register_embed_templates(engine: &mut HandlebarsTemplateEngine<'static>) -> Result<(), DocsError> {
95+
for file in Templates::iter() {
96+
let path = file.as_ref();
97+
let content = Templates::get(path).unwrap();
98+
let content = std::str::from_utf8(&content.data)
99+
.map_err(|e| DocsError::Template(Box::new(e)))?;
100+
engine
101+
.registry()
102+
.register_template_string(path, content)
103+
.map_err(|e| DocsError::Template(Box::new(e)))?;
104+
}
105+
106+
Ok(())
107+
}
108+
109+
fn register_custom_templates(
110+
engine: &mut HandlebarsTemplateEngine<'static>,
111+
custom_root: Option<&Path>,
112+
) -> Result<(), DocsError> {
113+
114+
let Some(custom_root) = custom_root else {
115+
return Ok(());
116+
};
117+
if !custom_root.exists() {
118+
return Ok(());
119+
}
120+
121+
for file in collect_files(custom_root)? {
122+
if file.extension().and_then(|e| e.to_str()) != Some("hbs") {
123+
continue;
124+
}
125+
let rel_path = file
126+
.strip_prefix(custom_root)
127+
.map_err(std::io::Error::other)?;
128+
let template_name = rel_path.to_string_lossy().replace('\\', "/");
129+
let content = fs::read_to_string(&file)?;
130+
engine
131+
.registry()
132+
.register_template_string(&template_name, content)
133+
.map_err(|e| DocsError::Template(Box::new(e)))?;
134+
}
135+
136+
Ok(())
137+
}
138+
139+
fn collect_files(path: &Path) -> Result<Vec<PathBuf>, DocsError> {
140+
let mut files = Vec::new();
141+
if !path.exists() {
142+
return Ok(files);
143+
}
144+
for entry in fs::read_dir(path)? {
145+
let entry = entry?;
146+
let entry_path = entry.path();
147+
if entry_path.is_dir() {
148+
files.extend(collect_files(&entry_path)?);
149+
} else {
150+
files.push(entry_path);
151+
}
152+
}
153+
Ok(files)
154+
}
155+
97156
fn bundle_assets(
98157
data: &mut RepositoryData,
99158
output: &Path,
@@ -252,6 +311,7 @@ pub fn generate_docs(
252311
output: String,
253312
page_size: usize,
254313
bundle: BundleStrategy,
314+
custom_root: Option<String>,
255315
) -> Result<(), DocsError> {
256316
let output_path = Path::new(&output);
257317
if !output_path.exists() {
@@ -267,7 +327,9 @@ pub fn generate_docs(
267327
let _ = page_size;
268328
let mut router = LunaRouter::new();
269329
let root = "/".to_string();
270-
let engine = build_engine()?;
330+
let custom_root = custom_root.map(PathBuf::from);
331+
let mut engine = build_engine()?;
332+
register_custom_templates(&mut engine, custom_root.as_deref())?;
271333

272334
let add_route = |router: &mut LunaRouter,
273335
path: &str,
@@ -459,13 +521,13 @@ pub fn generate_docs(
459521
}
460522
}
461523

462-
copy_public(&output)?;
524+
copy_public(&output, custom_root.as_deref())?;
463525
router.generate(&output)?;
464526

465527
Ok(())
466528
}
467529

468-
fn copy_public(output: &str) -> Result<(), DocsError> {
530+
fn copy_public(output: &str, custom_root: Option<&Path>) -> Result<(), DocsError> {
469531
for file in Public::iter() {
470532
let path = file.as_ref();
471533
let content = Public::get(path).unwrap();
@@ -477,5 +539,20 @@ fn copy_public(output: &str) -> Result<(), DocsError> {
477539
let mut file = fs::File::create(path)?;
478540
file.write_all(&content.data)?;
479541
}
542+
543+
if let Some(custom_root) = custom_root {
544+
let public_root = custom_root.join("public");
545+
for file in collect_files(&public_root)? {
546+
let rel = file
547+
.strip_prefix(&public_root)
548+
.map_err(std::io::Error::other)?;
549+
let target = Path::new(output).join(rel);
550+
if let Some(parent) = target.parent() {
551+
fs::create_dir_all(parent)?;
552+
}
553+
fs::copy(file, target)?;
554+
}
555+
}
556+
480557
Ok(())
481558
}

cli/src/main.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ struct DocsArgs {
8181
/// The strategy for bundling assets.
8282
#[arg(long, default_value_t = docs::BundleStrategy::Images, value_enum)]
8383
bundle: docs::BundleStrategy,
84+
/// Optional root directory for custom docs templates and static files.
85+
///
86+
/// Expected structure:
87+
/// - templates/**/*.hbs
88+
/// - components/**/*.hbs
89+
/// - layouts/**/*.hbs
90+
/// - public/**/*
91+
#[arg(long)]
92+
custom_root: Option<String>,
8493
}
8594

8695
fn main() -> Result<()> {
@@ -90,10 +99,10 @@ fn main() -> Result<()> {
9099
// matches just as you would the top level cmd
91100
match &cli.command {
92101
Commands::Generate { path } => generate(path.to_owned())?,
93-
Commands::Docs(args) => docs(args.path.to_owned(), args.index.to_owned(), args.page_size, args.bundle.clone())?,
102+
Commands::Docs(args) => docs(args.path.to_owned(), args.index.to_owned(), args.page_size, args.bundle.clone(), args.custom_root.to_owned())?,
94103
Commands::Build(args) => {
95104
generate(args.index.to_owned())?;
96-
docs(args.path.to_owned(), args.index.to_owned(), args.page_size, args.bundle.clone())?;
105+
docs(args.path.to_owned(), args.index.to_owned(), args.page_size, args.bundle.clone(), args.custom_root.to_owned())?;
97106
}
98107
Commands::Preview(args) => preview(args.path.to_owned(), args.port)?,
99108
Commands::Index { args, path } => {
@@ -148,14 +157,14 @@ fn inspect(data: &RepositoryData, args: &InspectArgs) -> Result<()> {
148157
Ok(())
149158
}
150159

151-
fn docs(path: String, index: String, page_size: usize, bundle: docs::BundleStrategy) -> Result<()> {
160+
fn docs(path: String, index: String, page_size: usize, bundle: docs::BundleStrategy, custom_root: Option<String>) -> Result<()> {
152161
let index_content = std::fs::read_to_string(&index)
153162
.with_context(|| format!("Could not read index file {index}"))?;
154163

155164
let data = RepositoryData::from_index(&index_content)
156165
.context("Could not parse index file")?;
157166

158-
docs::generate_docs(&data, path, page_size, bundle)
167+
docs::generate_docs(&data, path, page_size, bundle, custom_root)
159168
.context("Error while generating docs")?;
160169

161170
println!("Successfully generated docs.");

0 commit comments

Comments
 (0)