Skip to content

Commit b5b6d3e

Browse files
committed
update doc_cli html style
1 parent 06b9332 commit b5b6d3e

8 files changed

Lines changed: 273 additions & 132 deletions

File tree

crates/emmylua_doc_cli/src/html_generator/generators.rs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use emmylua_code_analysis::{
22
DbIndex, LuaDecl, LuaDeclId, LuaMemberKey, LuaMemberOwner, LuaSemanticDeclId, LuaType,
3-
LuaTypeCache, LuaTypeDecl, LuaTypeDeclId, ModuleInfo,
3+
LuaTypeCache, LuaTypeDecl, ModuleInfo,
44
};
55
use emmylua_parser::VisibilityKind;
66

7-
use crate::html_generator::types::HtmlDoc;
7+
use crate::html_generator::html_type;
8+
use crate::html_generator::types::{HtmlDoc, HtmlMember};
89
use crate::markdown_generator::generator::collect_property;
910

1011
use super::html_type::{render_const_type_html, render_function_signature_html, render_type_html};
@@ -15,7 +16,7 @@ pub struct GenContext<'a> {
1516
pub db: &'a DbIndex,
1617
/// Maps a type declaration id to a page href (prefixed relative to the
1718
/// page being generated).
18-
pub linker: &'a dyn Fn(&LuaTypeDeclId) -> Option<String>,
19+
pub linker: &'a super::html_type::TypeLinker<'a>,
1920
}
2021

2122
pub fn build_type_doc(ctx: &GenContext, typ: &LuaTypeDecl) -> Option<HtmlDoc> {
@@ -204,6 +205,16 @@ fn build_simple_global(ctx: &GenContext, decl: &LuaDecl, doc: &mut HtmlDoc) {
204205
}
205206
}
206207

208+
/// Renders a field path like `Vector.x` with syntax highlighting
209+
/// (`owner` as variable, `.` as operator, `name` as property).
210+
fn field_name_html(owner: &str, name: &str) -> String {
211+
format!(
212+
"<span class=\"hl-var\">{}</span><span class=\"hl-op\">.</span><span class=\"hl-prop\">{}</span>",
213+
html_escape(owner),
214+
html_escape(name)
215+
)
216+
}
217+
207218
/// Collects public methods and fields of a member owner with linked signatures.
208219
pub fn collect_members(
209220
ctx: &GenContext,
@@ -252,18 +263,14 @@ pub fn collect_members(
252263
false,
253264
ctx.linker,
254265
));
255-
let mut member = crate::html_generator::types::HtmlMember::from_property(
256-
title_name,
257-
display,
258-
member_property,
259-
);
266+
let mut member = HtmlMember::from_property(title_name, display, member_property);
260267
if let Some((params, returns)) =
261268
super::html_type::function_details_html(db, member_type, ctx.linker)
262269
{
263270
member.params = params;
264271
member.returns = returns;
265272
}
266-
member.overloads = super::html_type::signature_overloads_html(
273+
member.overloads = html_type::signature_overloads_html(
267274
db,
268275
member_type,
269276
&format!("{}.{}", owner_name, name),
@@ -276,25 +283,23 @@ pub fn collect_members(
276283
} else if member_type.is_const() {
277284
let const_type_display = render_const_type_html(db, member_type, ctx.linker);
278285
let display = signature_pre(format!(
279-
"{}.{}: {}",
280-
html_escape(owner_name),
281-
html_escape(&name),
286+
"{}<span class=\"hl-op\">:</span> {}",
287+
field_name_html(owner_name, &name),
282288
const_type_display
283289
));
284-
fields.push(crate::html_generator::types::HtmlMember::from_property(
290+
fields.push(HtmlMember::from_property(
285291
title_name,
286292
display,
287293
member_property,
288294
));
289295
} else {
290296
let typ_display = render_type_html(db, member_type, ctx.linker);
291297
let display = signature_pre(format!(
292-
"{}.{} : {}",
293-
html_escape(owner_name),
294-
html_escape(&name),
298+
"{} <span class=\"hl-op\">:</span> {}",
299+
field_name_html(owner_name, &name),
295300
typ_display
296301
));
297-
fields.push(crate::html_generator::types::HtmlMember::from_property(
302+
fields.push(HtmlMember::from_property(
298303
title_name,
299304
display,
300305
member_property,

crates/emmylua_doc_cli/src/html_generator/html_type.rs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,21 @@ pub fn render_type_style(
3434
) -> String {
3535
match typ {
3636
// ─── primitives ─────────────────────────────────────────────
37-
LuaType::Unknown => "unknown".to_string(),
38-
LuaType::Any => "any".to_string(),
39-
LuaType::Nil => "nil".to_string(),
40-
LuaType::Table => "table".to_string(),
41-
LuaType::Userdata => "userdata".to_string(),
42-
LuaType::Function => "function".to_string(),
43-
LuaType::Thread => "thread".to_string(),
44-
LuaType::Boolean => "boolean".to_string(),
45-
LuaType::String => "string".to_string(),
46-
LuaType::Integer => "integer".to_string(),
47-
LuaType::Number => "number".to_string(),
48-
LuaType::Io => "io".to_string(),
49-
LuaType::SelfInfer => "self".to_string(),
50-
LuaType::Global => "global".to_string(),
51-
LuaType::Never => "never".to_string(),
37+
LuaType::Unknown => type_kw("unknown"),
38+
LuaType::Any => type_kw("any"),
39+
LuaType::Nil => type_kw("nil"),
40+
LuaType::Table => type_kw("table"),
41+
LuaType::Userdata => type_kw("userdata"),
42+
LuaType::Function => type_kw("function"),
43+
LuaType::Thread => type_kw("thread"),
44+
LuaType::Boolean => type_kw("boolean"),
45+
LuaType::String => type_kw("string"),
46+
LuaType::Integer => type_kw("integer"),
47+
LuaType::Number => type_kw("number"),
48+
LuaType::Io => type_kw("io"),
49+
LuaType::SelfInfer => type_kw("self"),
50+
LuaType::Global => type_kw("global"),
51+
LuaType::Never => type_kw("never"),
5252
LuaType::Language(s) => html_escape(s),
5353

5454
// ─── constants ──────────────────────────────────────────────
@@ -145,7 +145,12 @@ pub fn render_type_style(
145145
.collect();
146146
format!("{{{}}}", parts.join(", "))
147147
}
148-
LuaType::TplRef(tpl) => html_escape(tpl.get_name()),
148+
LuaType::TplRef(tpl) => {
149+
format!(
150+
"<span class=\"hl-type\">{}</span>",
151+
html_escape(tpl.get_name())
152+
)
153+
}
149154
LuaType::Variadic(variadic) => match variadic.get_type(0) {
150155
Some(inner) => format!(
151156
"...{}",
@@ -187,7 +192,8 @@ fn render_named(db: &DbIndex, id: &LuaTypeDeclId, linker: &TypeLinker) -> String
187192
html_escape(&name)
188193
)
189194
} else {
190-
html_escape(&name)
195+
// No documentation page for this type — render it as a type name.
196+
format!("<span class=\"hl-type\">{}</span>", html_escape(&name))
191197
}
192198
}
193199

@@ -202,6 +208,9 @@ pub fn render_const_type_html(db: &DbIndex, typ: &LuaType, linker: &TypeLinker)
202208
}
203209
}
204210

211+
/// A function's parameter and return-value rows.
212+
pub type FunctionDetails = (Vec<HtmlParam>, Vec<HtmlParam>);
213+
205214
/// Extracts parameter and return-value rows (with descriptions) for a function
206215
/// type, used to render detail tables under a method signature.
207216
///
@@ -212,7 +221,7 @@ pub fn function_details_html(
212221
db: &DbIndex,
213222
typ: &LuaType,
214223
linker: &TypeLinker,
215-
) -> Option<(Vec<HtmlParam>, Vec<HtmlParam>)> {
224+
) -> Option<FunctionDetails> {
216225
let (params, returns) = match typ {
217226
LuaType::Signature(signature_id) => {
218227
let signature = db.get_signature_index().get(signature_id)?;
@@ -276,6 +285,11 @@ fn kw(text: &str) -> String {
276285
format!("<span class=\"hl-kw\">{text}</span>")
277286
}
278287

288+
/// Wraps a primitive type name in a syntax-highlight span.
289+
fn type_kw(text: &str) -> String {
290+
kw(text)
291+
}
292+
279293
/// Wraps a function/method name in a syntax-highlight span.
280294
fn fn_name(text: &str) -> String {
281295
format!("<span class=\"hl-fn\">{}</span>", html_escape(text))

crates/emmylua_doc_cli/src/html_generator/mod.rs

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use emmylua_code_analysis::{DbIndex, LuaDeclId, LuaTypeDeclId};
1111
use tera::Tera;
1212

1313
use crate::OutputDestination;
14-
use types::{HtmlDoc, NavGroup, NavItem, NavModel};
14+
use types::{HtmlDoc, NavGroup, NavItem, NavModel, NavTreeNode};
1515

1616
use self::generators::{GenContext, build_global_doc, build_module_doc, build_type_doc};
1717

@@ -227,11 +227,14 @@ fn mark_active(nav: &mut NavModel, kind: &str, name: &str) {
227227
}
228228
}
229229

230-
/// Splits each nav list into letter groups (used by the sidebar).
230+
/// Prepares the nav for rendering: letter groups (index page), the module
231+
/// hierarchy tree and the pre-rendered sidebar HTML.
231232
fn finalize_nav(nav: &mut NavModel) {
232233
nav.type_groups = build_groups(&nav.types);
233234
nav.module_groups = build_groups(&nav.modules);
234235
nav.global_groups = build_groups(&nav.globals);
236+
nav.type_tree = build_type_tree(&nav.types);
237+
nav.sidebar_html = build_sidebar_html(nav);
235238
}
236239

237240
fn build_groups(items: &[NavItem]) -> Vec<NavGroup> {
@@ -276,6 +279,160 @@ fn escape(name: String) -> String {
276279
super::markdown_generator::escape_type_name(&name)
277280
}
278281

282+
// ─── Module hierarchy tree ───────────────────────────────────────────────
283+
284+
/// Builds the module/namespace hierarchy tree from the flat type nav items.
285+
///
286+
/// Each item name is `"class lsp.CodeActionKind"`; the namespace path
287+
/// (`lsp`) becomes folders and the final segment a leaf.
288+
fn build_type_tree(items: &[NavItem]) -> Vec<NavTreeNode> {
289+
let mut roots: Vec<NavTreeNode> = Vec::new();
290+
for item in items {
291+
let (kind, full_name) = split_type_name(&item.name);
292+
let segments: Vec<&str> = full_name.split('.').collect();
293+
insert_tree(&mut roots, &segments, item, kind);
294+
}
295+
sort_tree(&mut roots);
296+
set_open(&mut roots);
297+
roots
298+
}
299+
300+
fn split_type_name(name: &str) -> (&str, &str) {
301+
for prefix in ["class ", "enum ", "alias "] {
302+
if let Some(rest) = name.strip_prefix(prefix) {
303+
return (prefix.trim(), rest);
304+
}
305+
}
306+
("class", name)
307+
}
308+
309+
fn insert_tree(nodes: &mut Vec<NavTreeNode>, path: &[&str], item: &NavItem, kind: &str) {
310+
let head = path[0];
311+
if path.len() == 1 {
312+
nodes.push(NavTreeNode {
313+
label: head.to_string(),
314+
data_name: Some(item.name.clone()),
315+
kind: kind.to_string(),
316+
href: Some(item.href.clone()),
317+
active: item.active,
318+
open: false,
319+
children: Vec::new(),
320+
});
321+
return;
322+
}
323+
let idx = match nodes
324+
.iter()
325+
.position(|n| n.href.is_none() && n.label == head)
326+
{
327+
Some(idx) => idx,
328+
None => {
329+
nodes.push(NavTreeNode {
330+
label: head.to_string(),
331+
data_name: None,
332+
kind: String::new(),
333+
href: None,
334+
active: false,
335+
open: false,
336+
children: Vec::new(),
337+
});
338+
nodes.len() - 1
339+
}
340+
};
341+
insert_tree(&mut nodes[idx].children, &path[1..], item, kind);
342+
}
343+
344+
fn sort_tree(nodes: &mut [NavTreeNode]) {
345+
nodes.sort_by(|a, b| a.label.cmp(&b.label));
346+
for node in nodes.iter_mut() {
347+
sort_tree(&mut node.children);
348+
}
349+
}
350+
351+
/// Marks folders on the active branch as open. Returns whether `nodes` contain
352+
/// an active item.
353+
fn set_open(nodes: &mut [NavTreeNode]) -> bool {
354+
let mut has_active = false;
355+
for node in nodes.iter_mut() {
356+
if node.active {
357+
has_active = true;
358+
} else if set_open(&mut node.children) {
359+
node.open = true;
360+
has_active = true;
361+
}
362+
}
363+
has_active
364+
}
365+
366+
/// Renders the full sidebar: the type hierarchy tree plus flat module/global
367+
/// lists. Kept in Rust (not the template) so the tree can recurse freely.
368+
fn build_sidebar_html(nav: &NavModel) -> String {
369+
let mut html = String::new();
370+
if !nav.type_tree.is_empty() {
371+
html.push_str(
372+
"<div class=\"sidebar-category\"><div class=\"category-title\">Types</div><ul>",
373+
);
374+
html.push_str(&render_tree_html(&nav.type_tree, &nav.root_prefix));
375+
html.push_str("</ul></div>");
376+
}
377+
if !nav.modules.is_empty() {
378+
html.push_str(
379+
"<div class=\"sidebar-category\"><div class=\"category-title\">Modules</div><ul>",
380+
);
381+
for item in &nav.modules {
382+
html.push_str(&render_flat_item(item, &nav.root_prefix));
383+
}
384+
html.push_str("</ul></div>");
385+
}
386+
if !nav.globals.is_empty() {
387+
html.push_str(
388+
"<div class=\"sidebar-category\"><div class=\"category-title\">Globals</div><ul>",
389+
);
390+
for item in &nav.globals {
391+
html.push_str(&render_flat_item(item, &nav.root_prefix));
392+
}
393+
html.push_str("</ul></div>");
394+
}
395+
html
396+
}
397+
398+
fn render_flat_item(item: &NavItem, root_prefix: &str) -> String {
399+
let active_cls = if item.active { " active" } else { "" };
400+
format!(
401+
"<li><a data-name=\"{}\" href=\"{}{}\" class=\"nav-item{active_cls}\">{}</a></li>",
402+
render::html_escape(&item.name),
403+
root_prefix,
404+
render::html_escape(&item.href),
405+
render::html_escape(&item.name)
406+
)
407+
}
408+
409+
fn render_tree_html(nodes: &[NavTreeNode], root_prefix: &str) -> String {
410+
let mut html = String::new();
411+
for node in nodes {
412+
if let Some(href) = &node.href {
413+
let active_cls = if node.active { " active" } else { "" };
414+
let data = node.data_name.as_deref().unwrap_or(&node.label);
415+
html.push_str(&format!(
416+
"<li><a data-name=\"{}\" href=\"{}{}\" class=\"nav-item{active_cls}\"><span class=\"kind-dot kind-{}\"></span>{}</a></li>",
417+
render::html_escape(data),
418+
root_prefix,
419+
render::html_escape(href),
420+
render::html_escape(&node.kind),
421+
render::html_escape(&node.label)
422+
));
423+
} else {
424+
let open = if node.open { " open" } else { "" };
425+
html.push_str(&format!(
426+
"<li><details{open}><summary class=\"group-label\">{}</summary><ul>",
427+
render::html_escape(&node.label)
428+
));
429+
html.push_str(&render_tree_html(&node.children, root_prefix));
430+
html.push_str("</ul></details></li>");
431+
}
432+
}
433+
html
434+
}
435+
279436
fn render_page(
280437
tl: &Tera,
281438
template: &str,

0 commit comments

Comments
 (0)