on master help sign in
Derive the language server's knowledge from the compiler's tables

Element, directive, attribute and prefix completions now come from ast::TAGS, parser::known_directives() and vocab; all prose lives in one docs.rs table shared by completion details and hover, with generic hovers for CSS properties (linking MDN) and HTML attributes. The LSP drops from 5974 to about 4600 lines, and removed or renamed syntax disappears from completions automatically. @data and @svg join the directive list so they're offered too.

commit7be4fc3acc9b85de588e13b05df921138b6cff30 authoriko <mail@iko.soy> dateSat 26 Sep 2026, 02:08 (2 weeks ago) parentbbd9209
6 files changed+571−1916
crates/htmlang-core/src/parser.rs+8 src/bin/htmlang_lsp/analysis.rs+9−79 src/bin/htmlang_lsp/completion.rs+171−991 src/bin/htmlang_lsp/docs.rs+378 src/bin/htmlang_lsp/hover.rs+4−846 src/bin/htmlang_lsp/main.rs+1
crates/htmlang-core/src/parser.rs+8
@@ -2511,6 +2511,12 @@ fn argument_is_special(kind: &ElementKind) -> bool {
25112511 .is_some_and(|spec| matches!(spec.arg, TagArg::Attr(_)))
25122512}
25132513
2514+/// The directive names (without `@`) the parser recognizes, for tools such
2515+/// as the language server.
2516+pub fn known_directives() -> &'static [&'static str] {
2517+ KNOWN_DIRECTIVES
2518+}
2519+
25142520const KNOWN_DIRECTIVES: &[&str] = &[
25152521 "page",
25162522 "let",
@@ -2536,6 +2542,8 @@ const KNOWN_DIRECTIVES: &[&str] = &[
25362542 "assert",
25372543 "markdown",
25382544 "manifest",
2545+ "data",
2546+ "svg",
25392547];
25402548
25412549fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseError> {
src/bin/htmlang_lsp/analysis.rs+9−79
@@ -1040,19 +1040,9 @@ pub(crate) fn semantic_tokens(text: &str, result: &ParseResult) -> Vec<SemanticT
10401040 i += 1;
10411041 }
10421042 let word = &line[start..i];
1043− let token_type = match word {
1044− "@page" | "@let" | "@if" | "@else" | "@each" | "@include" | "@import"
1045− | "@meta" | "@head" | "@style" | "@keyframes" | "@match" | "@case"
1046− | "@default" | "@slot" | "@children" | "@warn"
1047− | "@fragment" | "@breakpoint"
1048− | "@font-face" | "@json-ld" | "@assert" | "@theme"
1049− | "@deprecated" | "@extends" | "@data" | "@env" | "@fetch"
1050− | "@svg" => 0, // keyword
1051− _ => {
1052− // Check if it's a user function call (starts with @ but not a builtin element)
1053− if is_builtin_element(word) { 0 } else { 2 } // function
1054− }
1055− };
1043+ // Directives and built-in elements are keywords; any other
1044+ // `@name` is a function call.
1045+ let token_type = if is_builtin_name(&word[1..]) { 0 } else { 2 };
10561046 // Mark unused definitions with deprecated modifier (dimmed)
10571047 let modifier = if trimmed.starts_with("@let ") && word != "@let" {
10581048 let name_part = &word[1..]; // strip @
@@ -1110,72 +1100,12 @@ pub(crate) fn semantic_tokens(text: &str, result: &ParseResult) -> Vec<SemanticT
11101100 tokens
11111101}
11121102
1113−fn is_builtin_element(word: &str) -> bool {
1114− matches!(
1115− word,
1116− "@row"
1117− | "@column"
1118− | "@el"
1119− | "@text"
1120− | "@paragraph"
1121− | "@image"
1122− | "@link"
1123− | "@input"
1124− | "@button"
1125− | "@select"
1126− | "@textarea"
1127− | "@option"
1128− | "@label"
1129− | "@raw"
1130− | "@nav"
1131− | "@header"
1132− | "@footer"
1133− | "@main"
1134− | "@section"
1135− | "@article"
1136− | "@aside"
1137− | "@list"
1138− | "@item"
1139− | "@table"
1140− | "@thead"
1141− | "@tbody"
1142− | "@tr"
1143− | "@td"
1144− | "@th"
1145− | "@video"
1146− | "@audio"
1147− | "@form"
1148− | "@details"
1149− | "@summary"
1150− | "@blockquote"
1151− | "@cite"
1152− | "@code"
1153− | "@pre"
1154− | "@hr"
1155− | "@figure"
1156− | "@figcaption"
1157− | "@progress"
1158− | "@meter"
1159− | "@fragment"
1160− | "@dialog"
1161− | "@dl"
1162− | "@dt"
1163− | "@dd"
1164− | "@fieldset"
1165− | "@legend"
1166− | "@picture"
1167− | "@source"
1168− | "@time"
1169− | "@mark"
1170− | "@kbd"
1171− | "@abbr"
1172− | "@datalist"
1173− | "@script"
1174− | "@noscript"
1175− | "@address"
1176− | "@search"
1177− | "@breadcrumb"
1178− )
1103+/// A directive or built-in element name (without `@`), from the compiler.
1104+fn is_builtin_name(name: &str) -> bool {
1105+ htmlang::parser::known_directives().contains(&name)
1106+ || htmlang::ast::ElementKind::from_name(name).is_some()
1107+ // Directives the parser handles outside its directive list
1108+ || matches!(name, "data" | "svg")
11791109}
11801110
11811111#[allow(clippy::too_many_arguments)]
src/bin/htmlang_lsp/completion.rs+171−991
@@ -1,5 +1,9 @@
1+use htmlang::ast::ElementKind;
2+use htmlang::vocab;
13use tower_lsp::lsp_types::*;
24
5+use crate::docs;
6+
37pub(crate) fn completions(text: &str, position: Position) -> Vec<CompletionItem> {
48 let lines: Vec<&str> = text.lines().collect();
59 let line = match lines.get(position.line as usize) {
@@ -22,37 +26,11 @@ pub(crate) fn completions(text: &str, position: Position) -> Vec<CompletionItem>
2226 return variable_completions(text, edit_range);
2327 }
2428
25− // State prefix (hover:, active:, focus:) or media prefix (dark:, print:)
26− if let Some(colon) = current_word.find(':') {
27− let prefix = &current_word[..colon];
28− if matches!(
29− prefix,
30− "hover"
31− | "active"
32− | "focus"
33− | "focus-visible"
34− | "focus-within"
35− | "disabled"
36− | "checked"
37− | "placeholder"
38− | "first"
39− | "last"
40− | "odd"
41− | "even"
42− | "before"
43− | "after"
44− | "dark"
45− | "print"
46− | "sm"
47− | "md"
48− | "lg"
49− | "xl"
50− | "2xl"
51− | "motion-safe"
52− | "motion-reduce"
53− | "landscape"
54− | "portrait"
55− ) {
29+ // After a state/media prefix (`hover:`, `md:`, `nth:2n:`), offer
30+ // the styles it can apply to.
31+ if let Some(colon) = current_word.rfind(':') {
32+ let prefix = &current_word[..=colon];
33+ if vocab::is_prefixed(prefix) {
5634 return state_attr_completions(prefix, edit_range);
5735 }
5836 }
@@ -122,44 +100,6 @@ pub(crate) fn in_brackets(text: &str) -> bool {
122100 depth > 0
123101}
124102
125−/// Extract design tokens from any `@theme` blocks in the source.
126−///
127−/// `@theme` opens a block whose indented children are `name value` pairs.
128−/// Each pair becomes both a `$variable` reference and a CSS custom property
129−/// at codegen time. We surface them as completions and hover targets.
130−pub(crate) fn collect_theme_tokens(text: &str) -> Vec<(String, String)> {
131− let mut out = Vec::new();
132− let lines: Vec<&str> = text.lines().collect();
133− let mut i = 0;
134− while i < lines.len() {
135− let line = lines[i];
136− let trimmed = line.trim_start();
137− if trimmed == "@theme" || trimmed.starts_with("@theme ") {
138− let header_indent = line.len() - trimmed.len();
139− let mut j = i + 1;
140− while j < lines.len() {
141− let l = lines[j];
142− if l.trim().is_empty() {
143− j += 1;
144− continue;
145− }
146− let indent = l.len() - l.trim_start().len();
147− if indent <= header_indent {
148− break;
149− }
150− let body = l.trim();
151− if let Some((name, value)) = body.split_once(char::is_whitespace) {
152− out.push((name.trim().to_string(), value.trim().to_string()));
153− }
154− j += 1;
155− }
156− i = j;
157− } else {
158− i += 1;
159− }
160− }
161− out
162−}
163103
164104fn item(
165105 label: &str,
@@ -180,205 +120,42 @@ fn item(
180120 }
181121}
182122
123+/// Elements (from the compiler) and standard-library components.
183124fn element_completions(range: Range) -> Vec<CompletionItem> {
184− [
185− ("@row", "Horizontal layout (flexbox row)"),
186− ("@column", "Vertical layout (flexbox column)"),
187− ("@el", "Generic container"),
188− ("@text", "Styled inline text (span)"),
189− ("@paragraph", "Flowing text block (p)"),
190− ("@image", "Image element"),
191− ("@link", "Anchor/link element"),
192− ("@input", "Form input element (self-closing)"),
193− ("@button", "Button element"),
194− ("@select", "Select dropdown element"),
195− ("@textarea", "Multi-line text input"),
196− ("@option", "Option inside @select"),
197− ("@label", "Label element"),
198− ("@raw", "Raw HTML escape hatch"),
199− ("@children", "Slot for caller's children (inside component)"),
200− ("@slot", "Named slot inside component (e.g., @slot header)"),
201− // Semantic elements
202− ("@nav", "Navigation container (nav)"),
203− ("@header", "Page/section header (header)"),
204− ("@footer", "Page/section footer (footer)"),
205− ("@main", "Main content area (main)"),
206− ("@section", "Thematic section (section)"),
207− ("@article", "Self-contained content (article)"),
208− ("@aside", "Sidebar/tangential content (aside)"),
209− // List elements
210− ("@list", "List container (ul/ol, use [ordered] for ol)"),
211− ("@item", "List item (li)"),
212− // Table elements
213− ("@table", "Table element"),
214− ("@thead", "Table head group"),
215− ("@tbody", "Table body group"),
216− ("@tr", "Table row"),
217− ("@td", "Table cell"),
218− ("@th", "Table header cell"),
219− // Media elements
220− ("@video", "Video element"),
221− ("@audio", "Audio element"),
222− // Additional elements
223− ("@form", "Form container (form)"),
224− ("@details", "Disclosure widget (details)"),
225− ("@summary", "Summary for @details"),
226− ("@blockquote", "Block quotation"),
227− ("@cite", "Citation/source reference"),
228− ("@code", "Inline code (monospace)"),
229− ("@pre", "Preformatted text block"),
230− ("@hr", "Horizontal rule/divider"),
231− ("@figure", "Figure with optional caption"),
232− ("@figcaption", "Caption for @figure"),
233− ("@progress", "Progress bar (value, max attributes)"),
234− ("@meter", "Meter/gauge element (value, min, max)"),
235− ("@fragment", "Group children without a wrapper element"),
236− // Dialog & interactive
237− ("@dialog", "Dialog/modal element (dialog)"),
238− // Definition lists
239− ("@dl", "Description list (dl)"),
240− ("@dt", "Description term (dt)"),
241− ("@dd", "Description details (dd)"),
242− // Form grouping
243− ("@fieldset", "Fieldset group (fieldset)"),
244− ("@legend", "Legend for @fieldset (legend)"),
245− // Picture/responsive images
246− ("@picture", "Responsive image container (picture)"),
247− (
248− "@source",
249− "Media source for @picture/@video/@audio (source)",
250− ),
251− // Heading elements
252− ("@h1", "Heading level 1 (h1)"),
253− ("@h2", "Heading level 2 (h2)"),
254− ("@h3", "Heading level 3 (h3)"),
255− ("@h4", "Heading level 4 (h4)"),
256− ("@h5", "Heading level 5 (h5)"),
257− ("@h6", "Heading level 6 (h6)"),
258− // Inline semantics
259− ("@time", "Date/time element (time)"),
260− ("@mark", "Highlighted/marked text (mark)"),
261− ("@kbd", "Keyboard input (kbd)"),
262− ("@abbr", "Abbreviation (abbr)"),
263− // Datalist
264− ("@datalist", "Predefined options for @input (datalist)"),
265− // New elements
266− ("@iframe", "Embedded external page (iframe)"),
267− ("@output", "Form calculation result (output)"),
268− ("@canvas", "Drawing surface for scripts (canvas)"),
269− ("@script", "Inline script element (script)"),
270− (
271− "@noscript",
272− "Fallback content when scripts disabled (noscript)",
273− ),
274− ("@address", "Contact information (address)"),
275− ("@search", "Search section (search)"),
276− ("@breadcrumb", "Breadcrumb navigation (nav with aria)"),
277− // Overlay positioning
278− (
279− "@in-front",
280− "Overlay layer rendered on top of the parent (absolute, inset 0)",
281− ),
282− (
283− "@behind",
284− "Background layer rendered behind the parent's content (absolute, inset 0, z-index -1)",
285− ),
286− ]
287− .iter()
288− .map(|(name, detail)| item(name, CompletionItemKind::KEYWORD, detail, name, range))
289− .collect()
125+ let elements = ElementKind::all_names().map(|name| {
126+ let detail = docs::element_summary(name).unwrap_or_default();
127+ (name, detail)
128+ });
129+ let components = docs::COMPONENTS
130+ .iter()
131+ .map(|doc| (doc.name, format!("{} (standard library)", doc.summary)));
132+ elements
133+ .chain(components)
134+ .map(|(name, detail)| {
135+ let label = format!("@{}", name);
136+ item(&label, CompletionItemKind::KEYWORD, &detail, &label, range)
137+ })
138+ .collect()
290139}
291140
141+/// Directives, from the parser's directive list.
292142fn directive_completions(range: Range) -> Vec<CompletionItem> {
293− [
294− ("@page", "Set HTML page title", "@page "),
295− (
296− "@let",
297− "Define a variable, attribute bundle, or component",
298− "@let ",
299− ),
300− ("@keyframes", "Define a CSS animation", "@keyframes "),
301− ("@if", "Conditional rendering", "@if "),
302− ("@else if", "Else-if branch", "@else if "),
303− ("@else", "Else branch", "@else"),
304− (
305− "@each",
306− "Loop over values (@each $var, $i in list)",
307− "@each ",
308− ),
309− (
310− "@include",
311− "Include another .hl file (DOM + definitions)",
312− "@include ",
313− ),
314− (
315− "@import",
316− "Import definitions only (no DOM nodes)",
317− "@import ",
318− ),
319− ("@meta", "Add a <meta> tag to <head>", "@meta "),
320− ("@head", "Add raw content to <head>", "@head"),
321− ("@style", "Add raw CSS to stylesheet", "@style"),
322− (
323− "@slot",
324− "Named slot in component for caller content",
325− "@slot ",
326− ),
327− ("@match", "Pattern matching on a value", "@match "),
328− ("@case", "Match case (inside @match)", "@case "),
329− ("@default", "Default case (inside @match)", "@default"),
330− ("@warn", "Emit a compile-time warning", "@warn "),
331− (
332− "@breakpoint",
333− "Define custom responsive breakpoint",
334− "@breakpoint ",
335− ),
336− (
337− "@theme",
338− "Define design tokens (colors, spacing, fonts)",
339− "@theme",
340− ),
341− (
342− "@deprecated",
343− "Mark next @let component as deprecated",
344− "@deprecated ",
345− ),
346− (
347− "@extends",
348− "Inherit a layout template and fill @slot blocks",
349− "@extends ",
350− ),
351− ("@font-face", "Define a custom font face", "@font-face"),
352− (
353− "@json-ld",
354− "Add JSON-LD structured data to head",
355− "@json-ld",
356− ),
357− (
358− "@assert",
359− "Compile-time assertion for variable values",
360− "@assert ",
361− ),
362− (
363− "@data",
364− "Load JSON data file into template variables",
365− "@data ",
366− ),
367− ("@env", "Access compile-time environment variable", "@env "),
368− ("@fetch", "Fetch data from URL at compile time", "@fetch "),
369− ("@svg", "Inline SVG file with optional attributes", "@svg "),
370− ]
371− .iter()
372− .map(|(name, detail, insert)| item(name, CompletionItemKind::SNIPPET, detail, insert, range))
373− .collect()
143+ htmlang::parser::known_directives()
144+ .iter()
145+ .map(|name| {
146+ let label = format!("@{}", name);
147+ let detail = docs::directive(name).map_or("Directive", |d| d.summary);
148+ item(&label, CompletionItemKind::SNIPPET, detail, &format!("{} ", label), range)
149+ })
150+ .collect()
374151}
375152
376153fn snippet_completions(range: Range) -> Vec<CompletionItem> {
377154 let snippets: &[(&str, &str, &str)] = &[
378155 (
379− "card component",
380− "Reusable card with title and content",
381− "@let card \\$title\n @el [padding 20, background white, rounded 8]\n @text [bold] \\$title\n @children",
156+ "function",
157+ "Define a reusable function",
158+ "@let ${1:name} \\$${2:param}\n @el [${3:padding 16}]\n @children",
382159 ),
383160 (
384161 "responsive layout",
@@ -390,85 +167,30 @@ fn snippet_completions(range: Range) -> Vec<CompletionItem> {
390167 "Navigation bar with horizontal items",
391168 "@nav [padding 16, background #1a1a2e]\n @row [spacing 20, align-items center]",
392169 ),
393− (
394− "hero section",
395− "Hero section with title and subtitle",
396− "@column [padding 80, center-x, center-y, min-height 60vh]\n @text [bold, size 48] ${1:Title}\n @paragraph [size 18, color #666] ${2:Subtitle}",
397− ),
398170 (
399171 "each with else",
400172 "Loop with empty-state fallback",
401173 "@each \\$${1:item} in ${2:list}\n @text \\$${1:item}\n@else\n @text [color #888] No items found.",
402174 ),
403175 (
404− "button with hover",
405− "Interactive button with hover effect",
406− "@el [padding 12 24, background ${1:#3b82f6}, hover:background ${2:#2563eb}, rounded 8, cursor pointer, transition all 0.15s ease] > @link ${3:url}\n @text [color white, bold] ${4:Click me}",
407− ),
408− (
409− "form with inputs",
410− "Form with labeled inputs and submit button",
411− "@form [spacing 16]\n @label ${1:Name}\n @input [type text, name ${2:name}, placeholder ${3:Enter name}, required]\n @label ${4:Email}\n @input [type email, name ${5:email}, placeholder ${6:Enter email}, required]\n @button [type submit, padding 12 24, background ${7:#3b82f6}, color white, rounded 8, bold, cursor pointer] Submit",
412− ),
413− (
414− "grid layout",
415− "Responsive grid with columns",
416− "@grid [grid-cols ${1:3}, gap ${2:20}]\n @el [padding 20, background ${3:#f3f4f6}, rounded 8]\n ${4:Item 1}\n @el [padding 20, background ${3:#f3f4f6}, rounded 8]\n ${5:Item 2}\n @el [padding 20, background ${3:#f3f4f6}, rounded 8]\n ${6:Item 3}",
417− ),
418− (
419− "footer section",
420− "Footer with columns and copyright",
421− "@footer [padding 40, background ${1:#1a1a2e}, color ${2:#ccc}]\n @row [spacing 40, wrap]\n @column [spacing 10, width fill]\n @text [bold, color white] ${3:Company}\n @link ${4:#} ${5:About}\n @link ${6:#} ${7:Contact}\n @column [spacing 10, width fill]\n @text [bold, color white] ${8:Resources}\n @link ${9:#} ${10:Documentation}\n @text [size 14, color #888, center-x] \\u00a9 2026 ${11:Company Name}",
422− ),
423− (
424− "avatar with image",
425− "Circular avatar with fallback",
426− "@avatar [width ${1:48}, height ${1:48}, background ${2:#e5e7eb}]\n @image [width ${1:48}, height ${1:48}, object-fit cover, alt ${3:avatar}] ${4:url}",
427− ),
428− (
429− "carousel horizontal",
430− "Scroll-snap horizontal carousel",
431− "@carousel [gap ${1:16}, padding ${2:16}]\n @el [width ${3:300}, padding 20, background ${4:#f3f4f6}, rounded 8]\n ${5:Slide 1}\n @el [width ${3:300}, padding 20, background ${4:#f3f4f6}, rounded 8]\n ${6:Slide 2}\n @el [width ${3:300}, padding 20, background ${4:#f3f4f6}, rounded 8]\n ${7:Slide 3}",
432− ),
433− (
434− "dark mode toggle",
435− "Element with light/dark mode styles",
436− "@el [padding 20, background ${1:white}, dark:background ${2:#1a1a2e}, color ${3:#333}, dark:color ${4:#eee}, rounded 8, transition all 0.2s ease]\n ${5:Content}",
437− ),
438− (
439− "truncated text",
440− "Text with ellipsis overflow",
441− "@text [max-width ${1:200}, truncate] ${2:Long text that will be truncated...}",
442− ),
443− (
444− "@let component",
445− "Define a reusable component",
446− "@let ${1:name} \\$${2:param}\n @el [${3:padding 16}]\n @children",
447− ),
448− (
449− "@each loop",
450− "Iterate over a list of items",
451− "@each \\$${1:item} in ${2:items}\n @text \\$${1:item}",
452− ),
453− (
454− "@if conditional",
176+ "if / else",
455177 "Conditional rendering block",
456178 "@if ${1:condition}\n ${2:content}\n@else\n ${3:fallback}",
457179 ),
458180 (
459− "@match pattern",
460− "Pattern matching block",
461− "@match \\$${1:value}\n @case ${2:option1}\n ${3:content}\n @default\n ${4:fallback}",
181+ "form with inputs",
182+ "Form with a labeled input and a submit button",
183+ "@form [spacing 16] ${1:/submit}\n @label [for=${2:email}] ${3:Email}\n @input [type=email, name=$2, id=$2, required]\n @button [type=submit] Submit",
462184 ),
463185 (
464− "@data JSON load",
465− "Load variables from a JSON file",
466− "@data ${1:data.json}",
186+ "grid layout",
187+ "Grid with equal columns",
188+ "@grid [grid-cols ${1:3}, spacing ${2:20}]\n @el [padding 20]\n ${3:Item}",
467189 ),
468190 (
469− "@component scoped",
470− "Define a scoped component with styles",
471− "@component ${1:name} \\$${2:param}\n @style\n .inner { ${3:padding: 16px;} }\n @el [class inner]\n @children",
191+ "dark mode",
192+ "Element with light and dark styles",
193+ "@el [background ${1:white}, dark:background ${2:#1a1a2e}, color ${3:#333}, dark:color ${4:#eee}]\n ${5:Content}",
472194 ),
473195 ];
474196
@@ -617,7 +339,7 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
617339 "list",
618340 "accept",
619341 ],
620− "button" | "btn" => &[
342+ "button" => &[
621343 "type",
622344 "disabled",
623345 "name",
@@ -641,7 +363,7 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
641363 "autofocus",
642364 "spellcheck",
643365 ],
644− "option" | "opt" => &["value", "selected", "disabled", "label"],
366+ "option" => &["value", "selected", "disabled", "label"],
645367 "form" => &[
646368 "action",
647369 "method",
@@ -651,7 +373,7 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
651373 "enctype",
652374 "name",
653375 ],
654− "image" | "img" => &[
376+ "image" => &[
655377 "src",
656378 "alt",
657379 "width",
@@ -707,19 +429,14 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
707429fn attr_value_completions(before: &str, range: Range) -> Option<Vec<CompletionItem>> {
708430 let bracket_content = before.rsplit('[').next()?;
709431 let segment = bracket_content.rsplit(',').next()?.trim_start();
710− let attr_end = segment
711− .find(|c: char| c.is_whitespace())
712− .unwrap_or(segment.len());
713− let attr = &segment[..attr_end];
714− let after_attr = &segment[attr_end..];
715− if !after_attr.starts_with(' ') {
716− return None;
717− }
718− // Only fire when no value has been started yet.
719− let typed = after_attr.trim_start();
720− if !typed.is_empty() {
721− return None;
722− }
432+ // A style (`cursor `) or an HTML attribute (`type=`) with no value typed yet.
433+ let attr = segment
434+ .strip_suffix('=')
435+ .filter(|a| !a.contains(char::is_whitespace))
436+ .or_else(|| {
437+ let (attr, rest) = segment.split_once(' ')?;
438+ rest.trim().is_empty().then_some(attr)
439+ })?;
723440
724441 // Strip state prefix to find the base attribute.
725442 let base_attr = if let Some(pos) = attr.rfind(':') {
@@ -903,644 +620,75 @@ fn attr_value_completions(before: &str, range: Range) -> Option<Vec<CompletionIt
903620 )
904621}
905622
623+/// Attributes, from the compiler's vocabulary: htmlang attributes and CSS
624+/// properties (`key value`), HTML attributes (`key=value`, or bare for
625+/// booleans), and state/media prefixes. Attributes the owning element
626+/// specifically uses sort first.
906627fn attr_completions(range: Range, element: Option<&str>) -> Vec<CompletionItem> {
907− [
908− // Layout
909− ("spacing", "Gap between children (supports CSS units)", true),
910− ("gap", "Gap between children (alias for spacing)", true),
911− (
912− "padding",
913− "Inner padding (1/2/3/4 values, supports CSS units)",
914− true,
915− ),
916− ("padding-x", "Horizontal padding", true),
917− ("padding-y", "Vertical padding", true),
918− // Sizing
919− ("width", "Width (px/% | fill | shrink)", true),
920− ("height", "Height (px/% | fill | shrink)", true),
921− ("min-width", "Minimum width", true),
922− ("max-width", "Maximum width", true),
923− ("min-height", "Minimum height", true),
924− ("max-height", "Maximum height", true),
925− // Alignment
926− ("center-x", "Center horizontally", false),
927− ("center-y", "Center vertically", false),
928− ("align-left", "Align left", false),
929− ("align-right", "Align right", false),
930− ("align-top", "Align top", false),
931− ("align-bottom", "Align bottom", false),
932− // Style
933− ("background", "Background color/value", true),
934− ("color", "Text color", true),
935− ("border", "Border (width [color])", true),
936− ("border-top", "Top border (width [color])", true),
937− ("border-bottom", "Bottom border (width [color])", true),
938− ("border-left", "Left border (width [color])", true),
939− ("border-right", "Right border (width [color])", true),
940− ("rounded", "Border radius", true),
941− ("bold", "Bold text", false),
942− ("italic", "Italic text", false),
943− ("underline", "Underlined text", false),
944− ("size", "Font size", true),
945− ("font", "Font family", true),
946− ("transition", "CSS transition", true),
947− ("cursor", "CSS cursor type", true),
948− ("opacity", "Opacity (0-1)", true),
949− // Typography
950− (
951− "text-align",
952− "Text alignment (left/center/right/justify)",
953− true,
954− ),
955− ("line-height", "Line height (unitless or px)", true),
956− ("letter-spacing", "Letter spacing", true),
957− (
958− "text-transform",
959− "Text transform (uppercase/lowercase/capitalize)",
960− true,
961− ),
962− (
963− "white-space",
964− "White-space behavior (nowrap/pre/normal)",
965− true,
966− ),
967− // Overflow & positioning
968− ("overflow", "Overflow behavior (hidden/scroll/auto)", true),
969− (
970− "position",
971− "Position type (relative/absolute/fixed/sticky)",
972− true,
973− ),
974− ("top", "Top offset (for positioned elements)", true),
975− ("right", "Right offset (for positioned elements)", true),
976− ("bottom", "Bottom offset (for positioned elements)", true),
977− ("left", "Left offset (for positioned elements)", true),
978− ("z-index", "Stack order (integer)", true),
979− // Display & visibility
980− (
981− "display",
982− "Display mode (none/block/inline/flex/grid)",
983− true,
984− ),
985− ("visibility", "Visibility (visible/hidden)", true),
986− // Transform & filters
987− ("transform", "CSS transform (e.g., rotate(45deg))", true),
988− (
989− "backdrop-filter",
990− "Backdrop filter (e.g., blur(10px))",
991− true,
992− ),
993− // Effects
994− ("shadow", "Box shadow (CSS value)", true),
995− // Flow
996− ("wrap", "Enable flex-wrap", false),
997− ("gap-x", "Horizontal gap between children", true),
998− ("gap-y", "Vertical gap between children", true),
999− // Grid
1000− ("grid", "Enable CSS grid layout", false),
1001− ("grid-cols", "Grid columns (number or template)", true),
1002− ("grid-rows", "Grid rows (number or template)", true),
1003− ("col-span", "Span columns in grid", true),
1004− ("row-span", "Span rows in grid", true),
1005− // Container queries
1006− ("container", "Enable container queries (inline-size)", false),
1007− (
1008− "container-name",
1009− "Container name for @container queries",
1010− true,
1011− ),
1012− (
1013− "container-type",
1014− "Container type (inline-size/size/normal)",
1015− true,
1016− ),
1017− // Identity
1018− ("id", "HTML id attribute", true),
1019− ("class", "HTML class attribute", true),
1020− // Animation
1021− ("animation", "CSS animation (e.g., name 0.3s ease)", true),
1022− // Form
1023− ("type", "Input type (text/email/password/submit/...)", true),
1024− ("placeholder", "Placeholder text", true),
1025− ("name", "Form field name", true),
1026− ("value", "Form field value", true),
1027− ("disabled", "Disable the element", false),
1028− ("required", "Mark field as required", false),
1029− ("checked", "Checkbox/radio checked state", false),
1030− ("for", "Label target (id of associated input)", true),
1031− ("action", "Form action URL", true),
1032− ("method", "Form method (get/post)", true),
1033− ("rows", "Textarea rows", true),
1034− ("cols", "Textarea columns", true),
1035− ("maxlength", "Maximum input length", true),
1036− ("multiple", "Allow multiple selections", false),
1037− // Accessibility
1038− ("alt", "Alternative text (for images)", true),
1039− ("role", "ARIA role", true),
1040− ("tabindex", "Tab order", true),
1041− ("title", "Tooltip text", true),
1042− ("aria-label", "Accessible label", true),
1043− ("aria-hidden", "Hide from assistive tech (true/false)", true),
1044− ("data-", "Custom data attribute", true),
1045− // CSS: aspect-ratio, outline, logical properties, scroll-snap
1046− ("aspect-ratio", "CSS aspect ratio (e.g., 16/9, 1)", true),
1047− ("outline", "Outline (width [color])", true),
1048− (
1049− "padding-inline",
1050− "Inline (horizontal) padding for i18n",
1051− true,
1052− ),
1053− ("padding-block", "Block (vertical) padding for i18n", true),
1054− ("margin-inline", "Inline (horizontal) margin for i18n", true),
1055− ("margin-block", "Block (vertical) margin for i18n", true),
1056− (
1057− "scroll-snap-type",
1058− "Scroll snap behavior (x/y mandatory/proximity)",
1059− true,
1060− ),
1061− (
1062− "scroll-snap-align",
1063− "Snap alignment (start/center/end)",
1064− true,
1065− ),
1066− // Media attributes
1067− (
1068− "controls",
1069− "Show media controls (for @video, @audio)",
1070− false,
1071− ),
1072− ("autoplay", "Auto-play media", false),
1073− ("loop", "Loop media playback", false),
1074− ("muted", "Mute media", false),
1075− ("poster", "Video poster image URL", true),
1076− ("preload", "Media preload hint (auto/metadata/none)", true),
1077− ("loading", "Loading behavior (lazy/eager)", true),
1078− ("decoding", "Image decoding (async/sync/auto)", true),
1079− // List
1080− ("ordered", "Use ordered list (ol instead of ul)", false),
1081− // Media src
1082− ("src", "Source URL for media elements", true),
1083− // Margin
1084− ("margin", "Outer margin (1/2/3/4 values)", true),
1085− ("margin-x", "Horizontal margin", true),
1086− ("margin-y", "Vertical margin", true),
1087− // Filter & object
1088− (
1089− "filter",
1090− "CSS filter (blur, brightness, grayscale, etc.)",
1091− true,
1092− ),
1093− (
1094− "object-fit",
1095− "Object fit for images (cover/contain/fill)",
1096− true,
1097− ),
1098− ("object-position", "Object position within container", true),
1099− // Text extras
1100− ("text-shadow", "Text shadow (CSS value)", true),
1101− ("text-overflow", "Text overflow (ellipsis/clip)", true),
1102− // Interaction
1103− ("pointer-events", "Pointer events (none/auto)", true),
1104− ("user-select", "User selection (none/text/all)", true),
1105− // Flexbox/grid alignment
1106− (
1107− "justify-content",
1108− "Main axis alignment (center/space-between/etc.)",
1109− true,
1110− ),
1111− (
1112− "align-items",
1113− "Cross axis alignment (center/baseline/etc.)",
1114− true,
1115− ),
1116− // Flex item
1117− ("order", "Flex/grid item order", true),
1118− // Background extras
1119− (
1120− "background-size",
1121− "Background size (cover/contain/auto)",
1122− true,
1123− ),
1124− (
1125− "background-position",
1126− "Background position (center/top/etc.)",
1127− true,
1128− ),
1129− (
1130− "background-repeat",
1131− "Background repeat (no-repeat/repeat/etc.)",
1132− true,
1133− ),
1134− // Text wrapping
1135− (
1136− "word-break",
1137− "Word break behavior (break-all/keep-all)",
1138− true,
1139− ),
1140− ("overflow-wrap", "Overflow wrap (break-word/anywhere)", true),
1141− // New element attrs
1142− ("open", "Details initially open", false),
1143− ("novalidate", "Disable form validation", false),
1144− ("low", "Meter low threshold", true),
1145− ("high", "Meter high threshold", true),
1146− ("optimum", "Meter optimum value", true),
1147− ("colspan", "Table cell column span", true),
1148− ("rowspan", "Table cell row span", true),
1149− (
1150− "scope",
1151− "Table header scope (col/row/colgroup/rowgroup)",
1152− true,
1153− ),
1154− ("inline", "Inline SVG images into output", false),
1155− // Hidden
1156− ("hidden", "Hide element (display:none)", false),
1157− // Overflow directional
1158− (
1159− "overflow-x",
1160− "Horizontal overflow (hidden/scroll/auto)",
1161− true,
1162− ),
1163− ("overflow-y", "Vertical overflow (hidden/scroll/auto)", true),
1164− // Inset
1165− ("inset", "Shorthand for top/right/bottom/left", true),
1166− // Modern form theming
1167− ("accent-color", "Accent color for form controls", true),
1168− ("caret-color", "Text cursor color", true),
1169− (
1170− "color-scheme",
1171− "Color scheme preference (light/dark/light dark)",
1172− true,
1173− ),
1174− (
1175− "appearance",
1176− "Form element appearance (none to reset)",
1177− true,
1178− ),
1179− // Popover API
1180− (
1181− "popover",
1182− "Make element a popover (HTML Popover API)",
1183− false,
1184− ),
1185− ("popovertarget", "ID of popover to toggle", true),
1186− (
1187− "popovertargetaction",
1188− "Popover action (toggle/show/hide)",
1189− true,
1190− ),
1191− // Input hints
1192− (
1193− "inputmode",
1194− "Virtual keyboard type (numeric/email/search/tel/url)",
1195− true,
1196− ),
1197− (
1198− "enterkeyhint",
1199− "Enter key label (done/go/next/search/send)",
1200− true,
1201− ),
1202− (
1203− "fetchpriority",
1204− "Resource fetch priority (high/low/auto)",
1205− true,
1206− ),
1207− (
1208− "translate",
1209− "Whether element should be translated (yes/no)",
1210− true,
1211− ),
1212− ("spellcheck", "Spell check mode (true/false)", true),
1213− // List styling
1214− (
1215− "list-style",
1216− "List style type (disc/circle/square/none)",
1217− true,
1218− ),
1219− // Table styling
1220− (
1221− "border-collapse",
1222− "Border collapse mode (collapse/separate)",
1223− true,
1224− ),
1225− ("border-spacing", "Spacing between table cell borders", true),
1226− // Text decoration
1227− (
1228− "text-decoration",
1229− "Text decoration (underline/overline/line-through)",
1230− true,
1231− ),
1232− ("text-decoration-color", "Text decoration color", true),
1233− (
1234− "text-decoration-thickness",
1235− "Text decoration thickness",
1236− true,
1237− ),
1238− (
1239− "text-decoration-style",
1240− "Text decoration style (solid/dashed/dotted/wavy)",
1241− true,
1242− ),
1243− // Grid/flex placement
1244− (
1245− "place-items",
1246− "Shorthand for align-items + justify-items",
1247− true,
1248− ),
1249− (
1250− "place-self",
1251− "Shorthand for align-self + justify-self",
1252− true,
1253− ),
1254− // Scroll behavior
1255− ("scroll-behavior", "Scroll behavior (smooth/auto)", true),
1256− // Resize
1257− (
1258− "resize",
1259− "Resize behavior (none/both/horizontal/vertical)",
1260− true,
1261− ),
1262− // State prefixes
1263− ("hover:", "Style on hover", false),
1264− ("active:", "Style on active/click", false),
1265− ("focus:", "Style on focus", false),
1266− // New pseudo-state prefixes
1267− ("focus-visible:", "Style on keyboard focus", false),
1268− ("focus-within:", "Style when child has focus", false),
1269− ("disabled:", "Style when disabled", false),
1270− ("checked:", "Style when checked", false),
1271− ("placeholder:", "Style placeholder text", false),
1272− // Child selectors
1273− ("first:", "Style first child", false),
1274− ("last:", "Style last child", false),
1275− ("odd:", "Style odd children (1st, 3rd, ...)", false),
1276− ("even:", "Style even children (2nd, 4th, ...)", false),
1277− // Responsive prefixes
1278− ("sm:", "Style at 640px+ (small)", false),
1279− ("md:", "Style at 768px+ (medium)", false),
1280− ("lg:", "Style at 1024px+ (large)", false),
1281− ("xl:", "Style at 1280px+ (extra large)", false),
1282− // Additional responsive prefixes
1283− ("2xl:", "Style at 1536px+ (2x extra large)", false),
1284− // Motion prefixes
1285− ("motion-safe:", "Style when motion is allowed", false),
1286− (
1287− "motion-reduce:",
1288− "Style when reduced motion preferred",
1289− false,
1290− ),
1291− // Orientation prefixes
1292− ("landscape:", "Style in landscape orientation", false),
1293− ("portrait:", "Style in portrait orientation", false),
1294− // Media prefixes
1295− ("dark:", "Style in dark color scheme", false),
1296− ("print:", "Style for print media", false),
1297− // Clipping & blending
1298− ("clip-path", "Clip path (circle, polygon, etc.)", true),
1299− (
1300− "mix-blend-mode",
1301− "Blend mode (multiply, screen, overlay, etc.)",
1302− true,
1303− ),
1304− ("background-blend-mode", "Background blend mode", true),
1305− // Writing mode
1306− (
1307− "writing-mode",
1308− "Writing mode (horizontal-tb, vertical-rl, etc.)",
1309− true,
1310− ),
1311− // Multi-column layout
1312− (
1313− "column-count",
1314− "Number of columns in multi-column layout",
1315− true,
1316− ),
1317− ("column-gap", "Gap between columns", true),
1318− // Text
1319− ("text-indent", "First-line text indentation", true),
1320− ("hyphens", "Hyphenation behavior (none/manual/auto)", true),
1321− // Flex item sizing
1322− ("flex-grow", "Flex grow factor", true),
1323− ("flex-shrink", "Flex shrink factor", true),
1324− ("flex-basis", "Flex basis (initial main size)", true),
1325− // Stacking context
1326− ("isolation", "Create stacking context (isolate/auto)", true),
1327− // Grid/flex placement
1328− (
1329− "place-content",
1330− "Shorthand for align-content + justify-content",
1331− true,
1332− ),
1333− // Background image
1334− (
1335− "background-image",
1336− "Background image (url or gradient)",
1337− true,
1338− ),
1339− // New CSS properties
1340− (
1341− "font-weight",
1342− "Font weight (100-900, bold, lighter, bolder)",
1343− true,
1344− ),
1345− ("font-style", "Font style (normal/italic/oblique)", true),
1346− ("text-wrap", "Text wrapping (balance/pretty/nowrap)", true),
1347− (
1348− "will-change",
1349− "Performance hint for animations (transform, opacity)",
1350− true,
1351− ),
1352− (
1353− "touch-action",
1354− "Touch behavior (none/pan-x/pan-y/manipulation)",
1355− true,
1356− ),
1357− (
1358− "vertical-align",
1359− "Vertical alignment (middle/top/bottom/baseline)",
1360− true,
1361− ),
1362− (
1363− "contain",
1364− "CSS containment (layout/paint/content/strict)",
1365− true,
1366− ),
1367− (
1368− "content-visibility",
1369− "Content visibility (auto/visible/hidden)",
1370− true,
1371− ),
1372− (
1373− "scroll-margin",
1374− "Scroll margin (for scroll-snap and anchor offsets)",
1375− true,
1376− ),
1377− ("scroll-margin-top", "Scroll margin top", true),
1378− (
1379− "scroll-padding",
1380− "Scroll padding (for scroll-snap containers)",
1381− true,
1382− ),
1383− ("scroll-padding-top", "Scroll padding top", true),
1384− // Pseudo-element content
1385− (
1386− "content",
1387− "Content for ::before/::after (use with before:/after: prefix)",
1388− true,
1389− ),
1390− // Iframe/form/link attributes
1391− ("sandbox", "Iframe sandbox restrictions", true),
1392− ("allow", "Iframe permissions policy", true),
1393− ("allowfullscreen", "Allow iframe fullscreen", false),
1394− (
1395− "target",
1396− "Link/form target (_blank/_self/_parent/_top)",
1397− true,
1398− ),
1399− // CSS shorthands
1400− (
1401− "truncate",
1402− "Truncate text with ellipsis (single line)",
1403− false,
1404− ),
1405− ("line-clamp", "Clamp text to N lines with ellipsis", true),
1406− ("blur", "Apply blur filter (px)", true),
1407− ("backdrop-blur", "Apply backdrop blur filter (px)", true),
1408− // Direction
1409− ("direction", "Text direction (ltr/rtl)", true),
1410− // Container query prefixes
1411− ("cq-sm:", "Container query at 640px+", false),
1412− ("cq-md:", "Container query at 768px+", false),
1413− ("cq-lg:", "Container query at 1024px+", false),
1414− // Pseudo-elements
1415− ("before:", "Style ::before pseudo-element", false),
1416− ("after:", "Style ::after pseudo-element", false),
1417− ("selection:", "Style text selection", false),
1418− // Grid areas
1419− (
1420− "grid-template-areas",
1421− "Define named grid areas (quoted string)",
1422− true,
1423− ),
1424− ("grid-area", "Place element in a named grid area", true),
1425− // View transitions
1426− (
1427− "view-transition-name",
1428− "Assign a view transition name",
1429− true,
1430− ),
1431− // Has pseudo-selector
1432− (
1433− "has(",
1434− "Style when element has matching children :has()",
1435− false,
1436− ),
1437− // Critical CSS hint
1438− ("critical", "Mark as above-fold critical CSS", false),
1439− // New pseudo-state prefixes
1440− ("visited:", "Style visited links", false),
1441− ("empty:", "Style when element has no children", false),
1442− (
1443− "target:",
1444− "Style when element is the URL fragment target",
1445− false,
1446− ),
1447− ("valid:", "Style when form element is valid", false),
1448− ("invalid:", "Style when form element is invalid", false),
1449− // New CSS properties
1450− (
1451− "text-underline-offset",
1452− "Offset of text underline from its default position",
1453− true,
1454− ),
1455− (
1456− "column-width",
1457− "Ideal width of columns in multi-column layout",
1458− true,
1459− ),
1460− (
1461− "column-rule",
1462− "Rule between columns (width style color)",
1463− true,
1464− ),
1465− ]
1466− .iter()
1467− .map(|(name, detail, takes_value)| {
1468− // HTML attributes are written `key=value`, styles `key value`.
1469− let is_html = !htmlang::vocab::is_style_attribute(name)
1470− && (htmlang::vocab::HTML_ATTRIBUTES.contains(name)
1471− || name.starts_with("aria-")
1472− || name.starts_with("data-"));
1473− let insert = match (*takes_value, is_html) {
1474− (true, true) => format!("{}=", name),
1475− (true, false) => format!("{} ", name),
1476− (false, _) => name.to_string(),
1477− };
1478− let mut completion = item(name, CompletionItemKind::PROPERTY, detail, &insert, range);
1479− // Boost element-specific attributes to the top of the list when we
1480− // know which element owns the brackets. The default sort prefix is
1481− // "5_" so unboosted entries land below the prioritized ones, while
1482− // staying above snippets (which use "zz_").
1483− let boosted = element
1484− .map(|e| element_specific_attrs(e).contains(name))
1485− .unwrap_or(false);
1486− completion.sort_text = Some(if boosted {
1487− format!("0_{}", name)
628+ let boosted = element.map_or(&[][..], element_specific_attrs);
629+ let mut items = Vec::new();
630+ let mut push = |label: String, insert: String, detail: &str, rank: &str, name: &str| {
631+ let mut completion = item(&label, CompletionItemKind::PROPERTY, detail, &insert, range);
632+ let rank = if boosted.contains(&name) { "0" } else { rank };
633+ completion.sort_text = Some(format!("{}_{}", rank, label));
634+ items.push(completion);
635+ };
636+ for name in vocab::HTMLANG_ATTRIBUTES {
637+ let doc = docs::attribute(name);
638+ let insert = if doc.is_none_or(|d| d.takes_value()) {
639+ format!("{} ", name)
1488640 } else {
1489− format!("5_{}", name)
1490− });
1491− completion
1492− })
1493− .collect()
641+ name.to_string()
642+ };
643+ let detail = doc.map_or("htmlang attribute", |d| d.summary);
644+ push(name.to_string(), insert, detail, "2", name);
645+ }
646+ for name in vocab::CSS_PROPERTIES {
647+ let detail = docs::attribute(name).map_or("CSS property", |d| d.summary);
648+ push(name.to_string(), format!("{} ", name), detail, "5", name);
649+ }
650+ for name in vocab::BOOLEAN_HTML_ATTRS {
651+ if !vocab::is_style_attribute(name) {
652+ push(name.to_string(), name.to_string(), "HTML attribute (boolean)", "3", name);
653+ }
654+ }
655+ for name in vocab::HTML_ATTRIBUTES {
656+ if !vocab::BOOLEAN_HTML_ATTRS.contains(name) {
657+ push(format!("{}=", name), format!("{}=", name), "HTML attribute", "3", name);
658+ }
659+ }
660+ let prefixes = vocab::PSEUDO_PREFIXES
661+ .iter()
662+ .map(|(p, _)| *p)
663+ .chain(vocab::RESPONSIVE_PREFIXES.iter().copied())
664+ .chain(vocab::MEDIA_PREFIXES.iter().copied())
665+ .chain(vocab::CONTAINER_QUERY_PREFIXES.iter().copied());
666+ for prefix in prefixes {
667+ let detail = docs::prefix_selector(prefix).unwrap_or_default();
668+ push(prefix.to_string(), prefix.to_string(), &detail, "6", prefix);
669+ }
670+ items
1494671}
1495672
673+/// Styles that can follow a state/media prefix: `hover:color`, `md:padding`.
1496674fn state_attr_completions(prefix: &str, range: Range) -> Vec<CompletionItem> {
1497− [
1498− ("background", "Background color/value", true),
1499− ("color", "Text color", true),
1500− ("border", "Border (width [color])", true),
1501− ("border-top", "Top border (width [color])", true),
1502− ("border-bottom", "Bottom border (width [color])", true),
1503− ("border-left", "Left border (width [color])", true),
1504− ("border-right", "Right border (width [color])", true),
1505− ("rounded", "Border radius", true),
1506− ("bold", "Bold text", false),
1507− ("italic", "Italic text", false),
1508− ("underline", "Underlined text", false),
1509− ("size", "Font size", true),
1510− ("opacity", "Opacity (0-1)", true),
1511− ("cursor", "CSS cursor type", true),
1512− ("shadow", "Box shadow (CSS value)", true),
1513− ("text-shadow", "Text shadow", true),
1514− ("transform", "CSS transform", true),
1515− ("filter", "CSS filter", true),
1516− ("display", "Display mode", true),
1517− ("visibility", "Visibility", true),
1518− ("pointer-events", "Pointer events", true),
1519− ("user-select", "User selection", true),
1520− // Layout attrs for pseudo-states
1521− ("width", "Width", true),
1522− ("height", "Height", true),
1523− ("padding", "Inner padding", true),
1524− ("padding-x", "Horizontal padding", true),
1525− ("padding-y", "Vertical padding", true),
1526− ("margin", "Outer margin", true),
1527− ("margin-x", "Horizontal margin", true),
1528− ("margin-y", "Vertical margin", true),
1529− ("outline", "Outline", true),
1530− ("text-decoration", "Text decoration", true),
1531− ("text-decoration-color", "Text decoration color", true),
1532− ]
1533− .iter()
1534− .map(|(name, detail, takes_value)| {
1535− let full = format!("{}:{}", prefix, name);
1536− let insert = if *takes_value {
1537− format!("{} ", full)
1538− } else {
1539− full.clone()
1540− };
1541− item(&full, CompletionItemKind::PROPERTY, detail, &insert, range)
1542− })
1543− .collect()
675+ let htmlang = vocab::HTMLANG_ATTRIBUTES
676+ .iter()
677+ .map(|name| (*name, docs::attribute(name).is_none_or(|d| d.takes_value())));
678+ let css = vocab::CSS_PROPERTIES.iter().map(|name| (*name, true));
679+ htmlang
680+ .chain(css)
681+ .map(|(name, takes_value)| {
682+ let full = format!("{}{}", prefix, name);
683+ let insert = if takes_value {
684+ format!("{} ", full)
685+ } else {
686+ full.clone()
687+ };
688+ let detail = docs::attribute(name).map_or("CSS property", |d| d.summary);
689+ item(&full, CompletionItemKind::PROPERTY, detail, &insert, range)
690+ })
691+ .collect()
1544692}
1545693
1546694fn color_value_completions(before: &str, range: Range) -> Option<Vec<CompletionItem>> {
@@ -1649,14 +797,10 @@ fn color_value_completions(before: &str, range: Range) -> Option<Vec<CompletionI
1649797fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
1650798 let mut items = Vec::new();
1651799
1652− for (name, value) in collect_theme_tokens(text) {
1653− items.push(item(
1654− &format!("${}", name),
1655− CompletionItemKind::COLOR,
1656− &format!("@theme \u{2014} {}", value),
1657− &format!("${}", name),
1658− range,
1659− ));
800+ for doc in docs::BUNDLES {
801+ let label = format!("${}", doc.name);
802+ let detail = format!("{} (standard-library bundle)", doc.summary);
803+ items.push(item(&label, CompletionItemKind::CONSTANT, &detail, &label, range));
1660804 }
1661805
1662806 for line in text.lines() {
@@ -1797,15 +941,51 @@ mod tests {
1797941 }
1798942
1799943 #[test]
1800− fn theme_tokens_extracted_from_block() {
1801− let text = "@theme\n primary #3b82f6\n spacing-md 16\n@row\n";
1802− let tokens = collect_theme_tokens(text);
1803− assert_eq!(
1804− tokens,
1805− vec![
1806− ("primary".into(), "#3b82f6".into()),
1807− ("spacing-md".into(), "16".into()),
1808− ]
1809− );
944+ fn element_and_directive_completions_come_from_the_compiler() {
945+ let range = Range::default();
946+ let elements = element_completions(range);
947+ for name in ElementKind::all_names() {
948+ let label = format!("@{}", name);
949+ assert!(elements.iter().any(|i| i.label == label), "missing {}", label);
950+ }
951+ assert!(elements.iter().any(|i| i.label == "@badge"));
952+ let directives = directive_completions(range);
953+ for name in htmlang::parser::known_directives() {
954+ assert!(directives.iter().any(|i| i.label == format!("@{}", name)));
955+ }
956+ }
957+
958+ #[test]
959+ fn attribute_completions_use_the_right_form() {
960+ let items = attr_completions(Range::default(), Some("input"));
961+ let insert = |label: &str| {
962+ let item = items.iter().find(|i| i.label == label).unwrap();
963+ match &item.text_edit {
964+ Some(CompletionTextEdit::Edit(edit)) => edit.new_text.clone(),
965+ _ => panic!("no edit for {}", label),
966+ }
967+ };
968+ assert_eq!(insert("spacing"), "spacing ");
969+ assert_eq!(insert("bold"), "bold");
970+ assert_eq!(insert("opacity"), "opacity ");
971+ assert_eq!(insert("type="), "type=");
972+ assert_eq!(insert("required"), "required");
973+ assert_eq!(insert("hover:"), "hover:");
974+ let boosted = items.iter().find(|i| i.label == "type=").unwrap();
975+ assert!(boosted.sort_text.as_deref().unwrap().starts_with("0_"));
976+ }
977+
978+ #[test]
979+ fn prefixed_attribute_completions() {
980+ let items = completions("@el [hover:", Position::new(0, 11));
981+ assert!(items.iter().any(|i| i.label == "hover:background"));
982+ let items = completions("@el [md:", Position::new(0, 8));
983+ assert!(items.iter().any(|i| i.label == "md:padding"));
984+ }
985+
986+ #[test]
987+ fn value_completions_after_html_attribute() {
988+ let items = completions("@input [type=", Position::new(0, 13));
989+ assert!(items.iter().any(|i| i.label == "email"));
1810990 }
1811991}
src/bin/htmlang_lsp/docs.rsnew file+378
@@ -0,0 +1,378 @@
1+//! Documentation for htmlang's names, shared by hover and completion.
2+//!
3+//! The *names* themselves come from the compiler (`ElementKind`, `TAGS`,
4+//! `vocab`, the parser's directive list); this file only adds prose. A name
5+//! without an entry here still gets a summary derived from the compiler's
6+//! tables, so new elements and attributes are never missing from the editor.
7+
8+use htmlang::ast::{ElementKind, TagArg};
9+use htmlang::vocab;
10+
11+pub(crate) struct Doc {
12+ pub name: &'static str,
13+ pub summary: &'static str,
14+ /// An example; for attributes, `name` alone means it takes no value.
15+ pub usage: &'static str,
16+}
17+
18+impl Doc {
19+ /// Does this attribute take a value (`spacing 20` vs `bold`)?
20+ pub fn takes_value(&self) -> bool {
21+ self.usage.trim() != self.name
22+ }
23+}
24+
25+const fn doc(name: &'static str, summary: &'static str, usage: &'static str) -> Doc {
26+ Doc {
27+ name,
28+ summary,
29+ usage,
30+ }
31+}
32+
33+/// Elements (names without `@`). Elements not listed get a summary derived
34+/// from their row in `TAGS`.
35+pub(crate) const ELEMENTS: &[Doc] = &[
36+ doc("row", "Horizontal layout: a flex row.", "@row [spacing 10]\n @text A\n @text B"),
37+ doc("column", "Vertical layout: a flex column.", "@column [spacing 10]\n @text A\n @text B"),
38+ doc("el", "Generic container (a flex column).", "@el [padding 20, background white]\n Content"),
39+ doc("text", "Styled inline text (`<span>`).", "@text [bold, size 24] Hello"),
40+ doc(
41+ "paragraph",
42+ "Flowing text (`<p>`); holds inline `{@...}` elements.",
43+ "@paragraph\n Read the {@link /docs docs}.",
44+ ),
45+ doc("link", "Link (`<a>`); text after the URL is its content.", "@link /about About us"),
46+ doc("image", "Image (`<img>`); the argument is the source.", "@image [alt=Logo, width 120] logo.png"),
47+ doc("script", "Script; its indented body is JavaScript, kept verbatim.", "@script\n console.log(1)"),
48+ doc(
49+ "fragment",
50+ "Groups children without a wrapper element.",
51+ "@fragment\n @text A\n @text B",
52+ ),
53+ doc(
54+ "children",
55+ "Inside a function body: where the caller's content goes.",
56+ "@let card\n @el [padding 20]\n @children",
57+ ),
58+ doc(
59+ "slot",
60+ "Named placeholder in a function or layout, filled by the caller's `@slot` block.",
61+ "@slot header",
62+ ),
63+ doc("grid", "Grid container (`display: grid`).", "@grid [grid-cols 3, spacing 20]"),
64+ doc("stack", "Children layered on top of each other (`position: relative`).", "@stack"),
65+ doc(
66+ "in-front",
67+ "Overlay layer filling the parent, painted on top of its content.",
68+ "@el [width 200, height 200]\n @in-front\n @text Overlay",
69+ ),
70+ doc(
71+ "behind",
72+ "Layer filling the parent, painted behind its content.",
73+ "@el [padding 40]\n @behind\n @el [background #fef3c7, width fill, height fill]",
74+ ),
75+ doc("list", "List (`<ul>`; `[ordered]` for `<ol>`).", "@list [ordered]\n @item First"),
76+ doc("item", "List item (`<li>`).", "@item First"),
77+ doc("form", "Form; the argument is its `action`.", "@form [method=post] /subscribe"),
78+ doc("input", "Form input (void element).", "@input [type=email, name=email, required]"),
79+ doc("button", "Button.", "@button [type=submit] Send"),
80+ doc("iframe", "Embedded page; the argument is its `src`.", "@iframe [sandbox] https://example.com"),
81+ doc("video", "Video; the argument is its `src`.", "@video [controls] movie.mp4"),
82+ doc("audio", "Audio; the argument is its `src`.", "@audio [controls] song.mp3"),
83+];
84+
85+/// Directives (names without `@`).
86+pub(crate) const DIRECTIVES: &[Doc] = &[
87+ doc(
88+ "page",
89+ "Produces a full HTML document with this title. Attributes set `lang`, `favicon`, `canonical` and `base`.",
90+ "@page [lang en] My Site",
91+ ),
92+ doc(
93+ "let",
94+ "Defines a value, a computed value (`= expr`), an attribute bundle (`[...]`) or a function (indented body).",
95+ "@let primary #3b82f6\n@let gap = 8 * 2\n@let card [padding 20]\n@let button $label\n @el [$card] $label",
96+ ),
97+ doc("include", "Inserts another `.hl` file here, with its definitions.", "@include header.hl"),
98+ doc("extends", "Renders this page inside a layout; `@slot` blocks fill its slots.", "@extends layout.hl"),
99+ doc("raw", "Pastes HTML into the output verbatim.", "@raw \"\"\"<div></div>\"\"\""),
100+ doc("markdown", "Markdown (indented body or file), converted to HTML.", "@markdown\n # Title"),
101+ doc("style", "Raw CSS; overrides generated styles.", "@style\n .note { color: gray; }"),
102+ doc("keyframes", "CSS animation, in attribute syntax or raw CSS.", "@keyframes fade\n from [opacity 0]\n to [opacity 1]"),
103+ doc("head", "Raw HTML added to `<head>`.", "@head\n <link rel=\"icon\" href=\"f.ico\">"),
104+ doc("meta", "A `<meta>` tag; `og:` names become Open Graph tags.", "@meta description A small site"),
105+ doc("if", "Renders its body when the condition holds; `@else if` / `@else` follow.", "@if $count > 2 and not $hidden\n @text Many"),
106+ doc("else", "Fallback branch of `@if` or `@each`.", "@else\n @text None"),
107+ doc(
108+ "each",
109+ "Repeats its body for each item of a list or range; `$_index` counts from 0.",
110+ "@each $item in apple, banana\n @text $item\n@each $i in 1..10 step 2\n @text $i",
111+ ),
112+ doc("assert", "Fails the build when the condition is false.", "@assert $spacing == 16"),
113+ doc("warn", "Emits a compile-time warning.", "@warn Remember to set a title"),
114+ doc("data", "Loads a JSON file as variables.", "@data site.json"),
115+ doc("svg", "Inlines an SVG file.", "@svg [width 24] icon.svg"),
116+];
117+
118+/// htmlang's own attributes, plus CSS properties htmlang treats specially.
119+pub(crate) const ATTRIBUTES: &[Doc] = &[
120+ doc("spacing", "Gap between children.", "spacing 20"),
121+ doc("padding", "Inner space: 1 to 4 values; bare numbers are px.", "padding 12 24"),
122+ doc("padding-x", "Horizontal padding.", "padding-x 16"),
123+ doc("padding-y", "Vertical padding.", "padding-y 8"),
124+ doc("margin", "Outer space: 1 to 4 values; bare numbers are px.", "margin 0 auto"),
125+ doc("margin-x", "Horizontal margin.", "margin-x 16"),
126+ doc("margin-y", "Vertical margin.", "margin-y 8"),
127+ doc("width", "`fill` takes the remaining space, `shrink` fits the content, or a size.", "width fill"),
128+ doc("height", "`fill` takes the remaining space, `shrink` fits the content, or a size.", "height 200"),
129+ doc("center-x", "Centers the element horizontally in its parent.", "center-x"),
130+ doc("center-y", "Centers the element vertically in its parent.", "center-y"),
131+ doc("align-left", "Aligns the element to the left of its parent.", "align-left"),
132+ doc("align-right", "Aligns the element to the right of its parent.", "align-right"),
133+ doc("align-top", "Aligns the element to the top of its parent.", "align-top"),
134+ doc("align-bottom", "Aligns the element to the bottom of its parent.", "align-bottom"),
135+ doc("wrap", "Lets a row's children wrap onto new lines.", "wrap"),
136+ doc("grid-cols", "Number of equal grid columns.", "grid-cols 3"),
137+ doc("grid-rows", "Number of equal grid rows.", "grid-rows 2"),
138+ doc("col-span", "Columns a grid child spans.", "col-span 2"),
139+ doc("row-span", "Rows a grid child spans.", "row-span 2"),
140+ doc("hidden", "Hides the element (`display: none`).", "hidden"),
141+ doc("border", "Solid border: width, then color.", "border 1 #e5e7eb"),
142+ doc("rounded", "Corner radius.", "rounded 8"),
143+ doc("bold", "Bold text.", "bold"),
144+ doc("italic", "Italic text.", "italic"),
145+ doc("underline", "Underlined text.", "underline"),
146+ doc("size", "Font size; bare numbers are px.", "size 18"),
147+ doc("font", "Font family; quote a stack with commas.", "font \"Inter, sans-serif\""),
148+ doc("line-height", "Line height; integers of 2 or more are px.", "line-height 1.5"),
149+ doc("ordered", "On `@list`: a numbered list (`<ol>`).", "ordered"),
150+ doc("inline", "On `@image` or `@svg`: embed the file in the page.", "inline"),
151+ doc("responsive", "On `@image`: widths for a generated `srcset`.", "responsive 400 800 1200"),
152+];
153+
154+/// Standard-library components (`std.hl`), used like elements.
155+pub(crate) const COMPONENTS: &[Doc] = &[
156+ doc("badge", "Small pill for counts and statuses.", "@badge [background red, color white] 3"),
157+ doc("tag", "Label with slightly rounded corners.", "@tag v2.0"),
158+ doc("chip", "Outlined, fully rounded label.", "@chip Rust"),
159+ doc("avatar", "Circular frame for an image or initials.", "@avatar [width 48, height 48]\n AB"),
160+ doc("spacer", "Takes up the remaining space in a row or column.", "@spacer"),
161+ doc("tooltip", "Text that shows `tip` when hovered.", "@tooltip [tip More info] Hover me"),
162+ doc("carousel", "Horizontally scrolling row that snaps to each child.", "@carousel [spacing 16]\n @el Slide 1"),
163+ doc("breadcrumb", "Breadcrumb trail of `@item`s.", "@breadcrumb\n @item > @link / Home\n @item Docs"),
164+];
165+
166+/// Standard-library attribute bundles, used as `[$name]`.
167+pub(crate) const BUNDLES: &[Doc] = &[
168+ doc("skeleton", "Loading placeholder with a shimmer animation.", "@el [$skeleton, height 20]"),
169+ doc("no-scrollbar", "Hides scrollbars while keeping scrolling.", "@el [$no-scrollbar, overflow-x auto]"),
170+];
171+
172+fn find(table: &'static [Doc], name: &str) -> Option<&'static Doc> {
173+ table.iter().find(|d| d.name == name)
174+}
175+
176+pub(crate) fn attribute(name: &str) -> Option<&'static Doc> {
177+ find(ATTRIBUTES, name)
178+}
179+
180+pub(crate) fn directive(name: &str) -> Option<&'static Doc> {
181+ find(DIRECTIVES, name)
182+}
183+
184+pub(crate) fn component(name: &str) -> Option<&'static Doc> {
185+ find(COMPONENTS, name)
186+}
187+
188+pub(crate) fn bundle(name: &str) -> Option<&'static Doc> {
189+ find(BUNDLES, name)
190+}
191+
192+/// One-line summary of an element: from `ELEMENTS`, or derived from its
193+/// row in `TAGS`.
194+pub(crate) fn element_summary(name: &str) -> Option<String> {
195+ if let Some(doc) = find(ELEMENTS, name) {
196+ return Some(doc.summary.to_string());
197+ }
198+ let spec = ElementKind::from_name(name)?.spec()?;
199+ let mut summary = format!("Renders `<{}>`", spec.html);
200+ if spec.css.contains("flex-direction:column") {
201+ summary.push_str(", laid out as a column");
202+ }
203+ if spec.void {
204+ summary.push_str(" (void element)");
205+ }
206+ summary.push('.');
207+ match spec.arg {
208+ TagArg::Attr(attr) => summary.push_str(&format!(" The argument is its `{}`.", attr)),
209+ TagArg::Text | TagArg::Child => summary.push_str(" Text after it is its content."),
210+ }
211+ Some(summary)
212+}
213+
214+fn markdown(title: &str, summary: &str, usage: &str) -> String {
215+ if usage.is_empty() {
216+ format!("**{}** \u{2014} {}", title, summary)
217+ } else {
218+ format!("**{}** \u{2014} {}\n\n```htmlang\n{}\n```", title, summary, usage)
219+ }
220+}
221+
222+/// Hover text for a name: `@element`, `@directive`, `@component`, an
223+/// attribute (possibly prefixed, like `hover:color`), or a prefix itself.
224+pub(crate) fn hover(word: &str) -> Option<String> {
225+ if let Some(name) = word.strip_prefix('@') {
226+ if let Some(doc) = component(name) {
227+ return Some(markdown(
228+ word,
229+ &format!("{} (standard library)", doc.summary),
230+ doc.usage,
231+ ));
232+ }
233+ if let Some(doc) = directive(name) {
234+ return Some(markdown(word, doc.summary, doc.usage));
235+ }
236+ if htmlang::parser::known_directives().contains(&name) {
237+ return Some(markdown(word, "Directive.", ""));
238+ }
239+ let summary = element_summary(name)?;
240+ let usage = find(ELEMENTS, name).map_or("", |d| d.usage);
241+ return Some(markdown(word, &summary, usage));
242+ }
243+ if let Some(name) = word.strip_prefix('$') {
244+ let doc = bundle(name)?;
245+ return Some(markdown(
246+ word,
247+ &format!("{} (standard-library bundle)", doc.summary),
248+ doc.usage,
249+ ));
250+ }
251+ if let Some(selector) = prefix_selector(word) {
252+ return Some(markdown(word, &selector, ""));
253+ }
254+ let base = vocab::base_attribute(word);
255+ let prefix = &word[..word.len() - base.len()];
256+ let text = attribute_hover(base)?;
257+ Some(if prefix.is_empty() {
258+ text
259+ } else {
260+ format!(
261+ "{}\n\n*With `{}`:* {}",
262+ text,
263+ prefix,
264+ prefix_selector(prefix).unwrap_or_default()
265+ )
266+ })
267+}
268+
269+fn attribute_hover(name: &str) -> Option<String> {
270+ if let Some(doc) = attribute(name) {
271+ return Some(markdown(name, doc.summary, doc.usage));
272+ }
273+ if vocab::HTMLANG_ATTRIBUTES.contains(&name) {
274+ return Some(markdown(name, "htmlang attribute.", ""));
275+ }
276+ if vocab::is_css_property(name) {
277+ return Some(format!(
278+ "**{name}** \u{2014} CSS property `{name}`, passed through unchanged.\n\n[MDN](https://developer.mozilla.org/docs/Web/CSS/{name})"
279+ ));
280+ }
281+ if vocab::BOOLEAN_HTML_ATTRS.contains(&name) {
282+ return Some(format!(
283+ "**{name}** \u{2014} Boolean HTML attribute, written bare: `[{name}]`."
284+ ));
285+ }
286+ if vocab::HTML_ATTRIBUTES.contains(&name) || name.starts_with("aria-") || name.starts_with("data-")
287+ {
288+ return Some(format!(
289+ "**{name}** \u{2014} HTML attribute, written `{name}=value`."
290+ ));
291+ }
292+ None
293+}
294+
295+/// What a state/media prefix like `hover:` or `md:` applies to.
296+pub(crate) fn prefix_selector(prefix: &str) -> Option<String> {
297+ if !prefix.ends_with(':') {
298+ return None;
299+ }
300+ if let Some((_, selector)) = vocab::PSEUDO_PREFIXES.iter().find(|(p, _)| *p == prefix) {
301+ return Some(if selector.starts_with(" > ") {
302+ "Styles each direct child (`> *`).".to_string()
303+ } else {
304+ format!("Applies in the `{}` state.", selector)
305+ });
306+ }
307+ let name = prefix.trim_end_matches(':');
308+ if vocab::RESPONSIVE_PREFIXES.contains(&prefix) {
309+ return Some(format!("Applies from the `{}` viewport width up.", name));
310+ }
311+ if vocab::CONTAINER_QUERY_PREFIXES.contains(&prefix) {
312+ return Some(format!(
313+ "Applies when the container is at least `{}` wide.",
314+ name.trim_start_matches("cq-")
315+ ));
316+ }
317+ if vocab::MEDIA_PREFIXES.contains(&prefix) {
318+ return Some(format!("Applies under the `{}` media condition.", name));
319+ }
320+ if name.starts_with("nth:") {
321+ return Some("Applies to children matching `:nth-child(...)`.".to_string());
322+ }
323+ if name.starts_with("has(") {
324+ return Some("Applies when the element contains a match (`:has(...)`).".to_string());
325+ }
326+ None
327+}
328+
329+#[cfg(test)]
330+mod tests {
331+ use super::*;
332+
333+ #[test]
334+ fn every_element_has_a_hover() {
335+ for name in ElementKind::all_names() {
336+ let text = hover(&format!("@{}", name));
337+ assert!(
338+ text.as_deref().is_some_and(|t| !t.is_empty()),
339+ "no hover for @{}",
340+ name
341+ );
342+ }
343+ }
344+
345+ #[test]
346+ fn every_htmlang_attribute_has_a_hover() {
347+ for name in vocab::HTMLANG_ATTRIBUTES {
348+ let text = hover(name);
349+ assert!(
350+ text.as_deref().is_some_and(|t| !t.is_empty()),
351+ "no hover for {}",
352+ name
353+ );
354+ }
355+ }
356+
357+ #[test]
358+ fn documented_names_exist_in_the_compiler() {
359+ for doc in ELEMENTS {
360+ assert!(ElementKind::from_name(doc.name).is_some(), "@{} is not an element", doc.name);
361+ }
362+ for doc in ATTRIBUTES {
363+ assert!(vocab::is_style_attribute(doc.name), "{} is not a style attribute", doc.name);
364+ }
365+ }
366+
367+ #[test]
368+ fn hovers_for_css_html_and_prefixes() {
369+ assert!(hover("opacity").unwrap().contains("developer.mozilla.org"));
370+ assert!(hover("placeholder").unwrap().contains("placeholder=value"));
371+ assert!(hover("required").unwrap().contains("Boolean"));
372+ assert!(hover("hover:color").unwrap().contains(":hover"));
373+ assert!(hover("md:").unwrap().contains("viewport"));
374+ assert!(hover("@badge").unwrap().contains("standard library"));
375+ assert!(hover("$skeleton").unwrap().contains("bundle"));
376+ assert!(hover("@nav").unwrap().contains("<nav>"));
377+ }
378+}
src/bin/htmlang_lsp/hover.rs+4−846
@@ -1,6 +1,6 @@
11use tower_lsp::lsp_types::*;
22
3−use crate::completion::collect_theme_tokens;
3+use crate::docs;
44
55pub(crate) fn hover_at(text: &str, position: Position) -> Option<Hover> {
66 let lines: Vec<&str> = text.lines().collect();
@@ -9,11 +9,11 @@ pub(crate) fn hover_at(text: &str, position: Position) -> Option<Hover> {
99 let word = word_at(line, col)?;
1010
1111 let doc = if let Some(var_name) = word.strip_prefix('$') {
12− hover_theme_token(text, var_name).or_else(|| hover_variable(text, var_name))
12+ hover_variable(text, var_name).or_else(|| docs::hover(&word))
1313 } else if let Some(fn_name) = word.strip_prefix('@') {
14− hover_user_fn(text, fn_name).or_else(|| hover_builtin(&word))
14+ hover_user_fn(text, fn_name).or_else(|| docs::hover(&word))
1515 } else {
16− hover_builtin(&word)
16+ docs::hover(&word)
1717 }?;
1818
1919 Some(Hover {
@@ -45,15 +45,6 @@ pub(crate) fn is_word_byte(c: u8) -> bool {
4545 c.is_ascii_alphanumeric() || c == b'@' || c == b'$' || c == b'-' || c == b'_' || c == b':'
4646}
4747
48−fn hover_theme_token(text: &str, name: &str) -> Option<String> {
49− let tokens = collect_theme_tokens(text);
50− let (token_name, value) = tokens.iter().find(|(n, _)| n == name)?;
51− Some(format!(
52− "**${}** \u{2014} `@theme` token\n\nValue: `{}`\n\nAlso available as the CSS custom property `--{}`.",
53− token_name, value, token_name
54− ))
55−}
56−
5748fn hover_variable(text: &str, name: &str) -> Option<String> {
5849 for line in text.lines() {
5950 let trimmed = line.trim();
@@ -156,836 +147,3 @@ fn hover_user_fn(text: &str, name: &str) -> Option<String> {
156147 }
157148 None
158149}
159−
160−fn hover_builtin(word: &str) -> Option<String> {
161− // Strip state prefix for attribute lookup
162− let (state, base) = if let Some(rest) = word.strip_prefix("hover:") {
163− (Some("hover"), rest)
164− } else if let Some(rest) = word.strip_prefix("active:") {
165− (Some("active"), rest)
166− } else if let Some(rest) = word.strip_prefix("focus:") {
167− (Some("focus"), rest)
168− } else if let Some(rest) = word.strip_prefix("focus-visible:") {
169− (Some("focus-visible"), rest)
170− } else if let Some(rest) = word.strip_prefix("focus-within:") {
171− (Some("focus-within"), rest)
172− } else if let Some(rest) = word.strip_prefix("disabled:") {
173− (Some("disabled"), rest)
174− } else if let Some(rest) = word.strip_prefix("checked:") {
175− (Some("checked"), rest)
176− } else if let Some(rest) = word.strip_prefix("placeholder:") {
177− (Some("placeholder"), rest)
178− } else if let Some(rest) = word.strip_prefix("first:") {
179− (Some("first"), rest)
180− } else if let Some(rest) = word.strip_prefix("last:") {
181− (Some("last"), rest)
182− } else if let Some(rest) = word.strip_prefix("odd:") {
183− (Some("odd"), rest)
184− } else if let Some(rest) = word.strip_prefix("even:") {
185− (Some("even"), rest)
186− } else if let Some(rest) = word.strip_prefix("before:") {
187− (Some("before"), rest)
188− } else if let Some(rest) = word.strip_prefix("after:") {
189− (Some("after"), rest)
190− } else if let Some(rest) = word.strip_prefix("sm:") {
191− (Some("sm"), rest)
192− } else if let Some(rest) = word.strip_prefix("md:") {
193− (Some("md"), rest)
194− } else if let Some(rest) = word.strip_prefix("lg:") {
195− (Some("lg"), rest)
196− } else if let Some(rest) = word.strip_prefix("xl:") {
197− (Some("xl"), rest)
198− } else if let Some(rest) = word.strip_prefix("2xl:") {
199− (Some("2xl"), rest)
200− } else if let Some(rest) = word.strip_prefix("motion-safe:") {
201− (Some("motion-safe"), rest)
202− } else if let Some(rest) = word.strip_prefix("motion-reduce:") {
203− (Some("motion-reduce"), rest)
204− } else if let Some(rest) = word.strip_prefix("landscape:") {
205− (Some("landscape"), rest)
206− } else if let Some(rest) = word.strip_prefix("portrait:") {
207− (Some("portrait"), rest)
208− } else if let Some(rest) = word.strip_prefix("visited:") {
209− (Some("visited"), rest)
210− } else if let Some(rest) = word.strip_prefix("empty:") {
211− (Some("empty"), rest)
212− } else if let Some(rest) = word.strip_prefix("target:") {
213− (Some("target"), rest)
214− } else if let Some(rest) = word.strip_prefix("valid:") {
215− (Some("valid"), rest)
216− } else if let Some(rest) = word.strip_prefix("invalid:") {
217− (Some("invalid"), rest)
218− } else {
219− (None, word)
220− };
221−
222− let doc = match base {
223− // Elements
224− "@row" => {
225− "**@row** \u{2014} Horizontal layout\n\nRenders as `<div>` with `display: flex; flex-direction: row`.\n\nChildren are laid out left-to-right."
226− }
227− "@column" => {
228− "**@column** \u{2014} Vertical layout\n\nRenders as `<div>` with `display: flex; flex-direction: column`.\n\nChildren are laid out top-to-bottom."
229− }
230− "@el" => {
231− "**@el** \u{2014} Generic container\n\nRenders as `<div>` with column flex layout."
232− }
233− "@text" => {
234− "**@text** \u{2014} Inline text\n\nRenders as `<span>`.\n\nUsage: `@text [bold, size 24] Hello world`"
235− }
236− "@paragraph" => {
237− "**@paragraph** \u{2014} Text block\n\nRenders as `<p>`.\n\nSupports inline elements: `{@text [bold] word}`"
238− }
239− "@h1" => {
240− "**@h1** \u{2014} Heading level 1\n\nRenders as `<h1>`.\n\nUsage: `@h1 Page Title`"
241− }
242− "@h2" => {
243− "**@h2** \u{2014} Heading level 2\n\nRenders as `<h2>`.\n\nUsage: `@h2 Section Title`"
244− }
245− "@h3" => {
246− "**@h3** \u{2014} Heading level 3\n\nRenders as `<h3>`.\n\nUsage: `@h3 Subsection`"
247− }
248− "@h4" => "**@h4** \u{2014} Heading level 4\n\nRenders as `<h4>`.",
249− "@h5" => "**@h5** \u{2014} Heading level 5\n\nRenders as `<h5>`.",
250− "@h6" => "**@h6** \u{2014} Heading level 6\n\nRenders as `<h6>`.",
251− "@image" => {
252− "**@image** \u{2014} Image\n\nRenders as `<img>`.\n\nUsage: `@image [width 200] https://example.com/photo.jpg`"
253− }
254− "@link" => {
255− "**@link** \u{2014} Hyperlink\n\nRenders as `<a>`.\n\nUsage: `@link [color blue] https://example.com Link text`"
256− }
257− "@raw" => {
258− "**@raw** \u{2014} Raw HTML\n\nPasses content through without processing.\n\nUsage: `@raw \"\"\"<div>custom html</div>\"\"\"`"
259− }
260− "@page" => {
261− "**@page** \u{2014} Page title\n\nSets the HTML `<title>` and wraps output in a full document. Attributes set `lang`, `favicon`, `canonical` and `base`.\n\nUsage: `@page [lang en, favicon /favicon.png] My Page Title`"
262− }
263− "@let" => {
264− "**@let** \u{2014} Definition\n\nDefines a variable, attribute bundle, or component.\n\n- Variable: `@let primary #3b82f6`\n- Attribute bundle: `@let card-style [padding 20, rounded 8]`\n- Component:\n```\n@let card $title\n @el [padding 20]\n @text [bold] $title\n @children\n```"
265− }
266− "@keyframes" => {
267− "**@keyframes** \u{2014} CSS Animation\n\nDefines keyframes for CSS animations.\n\n```\n@keyframes fade-in\n from{opacity:0}to{opacity:1}\n```\n\nUse with `animation` attribute: `[animation fade-in 0.3s ease]`"
268− }
269− "@children" => {
270− "**@children** \u{2014} Children slot\n\nPlaceholder inside a component body replaced with the caller's children."
271− }
272− "@input" => {
273− "**@input** \u{2014} Form input\n\nRenders as self-closing `<input>`.\n\nUsage: `@input [type text, placeholder Name, name user]`"
274− }
275− "@button" => {
276− "**@button** \u{2014} Button\n\nRenders as `<button>`.\n\nUsage: `@button [type submit] Click me`"
277− }
278− "@select" => {
279− "**@select** \u{2014} Select dropdown\n\nRenders as `<select>`. Use `@option` children.\n\nUsage: `@select [name color]`"
280− }
281− "@textarea" => {
282− "**@textarea** \u{2014} Multi-line text input\n\nRenders as `<textarea>`.\n\nUsage: `@textarea [name bio, rows 4] Default text`"
283− }
284− "@option" => {
285− "**@option** \u{2014} Select option\n\nRenders as `<option>`.\n\nUsage: `@option [value red] Red`"
286− }
287− "@label" => {
288− "**@label** \u{2014} Form label\n\nRenders as `<label>`.\n\nUsage: `@label [for email] Email Address`"
289− }
290− "@if" => {
291− "**@if** \u{2014} Conditional\n\nConditionally includes children at compile time.\n\n```\n@if $theme == dark\n @el [background #333]\n@else if $theme == light\n @el [background white]\n@else\n @el [background gray]\n```"
292− }
293− "@each" => {
294− "**@each** \u{2014} Loop\n\nRepeat children for each item in a comma-separated list.\nOptional index variable.\n\n```\n@each $color, $i in red,green,blue\n @text $i: $color\n```"
295− }
296− "@include" => {
297− "**@include** \u{2014} Include file\n\nIncludes another .hl file (DOM nodes + definitions).\n\nUsage: `@include header.hl`"
298− }
299− "@import" => {
300− "**@import** \u{2014} Import definitions\n\nImports `@let` definitions from another .hl file without emitting DOM nodes.\n\nUsage: `@import theme.hl`"
301− }
302− "@meta" => {
303− "**@meta** \u{2014} Meta tag\n\nAdds a `<meta>` tag to `<head>`.\n\nUsage: `@meta description A portfolio site`"
304− }
305− "@head" => {
306− "**@head** \u{2014} Head content\n\nAdds raw content to `<head>` (fonts, icons, etc.).\n\n```\n@head\n <link rel=\"icon\" href=\"favicon.ico\">\n```"
307− }
308− "@style" => {
309− "**@style** \u{2014} Custom CSS\n\nAdds raw CSS to the stylesheet.\n\n```\n@style\n .custom { border: 1px solid red; }\n @container sidebar (min-width: 400px) { ... }\n```"
310− }
311− "@slot" => {
312− "**@slot** \u{2014} Named slot\n\nDefines a named insertion point inside a component. Callers fill it with `@slot name` + children.\n\n```\n@let layout\n @slot header\n @children\n @slot footer\n```"
313− }
314− // Semantic elements
315− "@nav" => {
316− "**@nav** \u{2014} Navigation\n\nRenders as `<nav>`. Semantic landmark for navigation links."
317− }
318− "@header" => {
319− "**@header** \u{2014} Header\n\nRenders as `<header>`. Page or section header."
320− }
321− "@footer" => {
322− "**@footer** \u{2014} Footer\n\nRenders as `<footer>`. Page or section footer."
323− }
324− "@main" => {
325− "**@main** \u{2014} Main content\n\nRenders as `<main>`. Primary content of the page."
326− }
327− "@section" => {
328− "**@section** \u{2014} Section\n\nRenders as `<section>`. Thematic grouping of content."
329− }
330− "@article" => {
331− "**@article** \u{2014} Article\n\nRenders as `<article>`. Self-contained, independently distributable content."
332− }
333− "@aside" => {
334− "**@aside** \u{2014} Aside\n\nRenders as `<aside>`. Content tangentially related to surrounding content."
335− }
336− // List elements
337− "@list" => {
338− "**@list** \u{2014} List\n\nRenders as `<ul>` (or `<ol>` with `[ordered]`).\n\nUsage:\n```\n@list [ordered]\n @item First\n @item Second\n```"
339− }
340− "@item" => "**@item** \u{2014} List item\n\nRenders as `<li>`. Use inside `@list`.",
341− // Table elements
342− "@table" => {
343− "**@table** \u{2014} Table\n\nRenders as `<table>`.\n\n```\n@table\n @thead\n @tr\n @th Name\n @th Age\n @tbody\n @tr\n @td Alice\n @td 30\n```"
344− }
345− "@thead" => "**@thead** \u{2014} Table head\n\nRenders as `<thead>`. Groups header rows.",
346− "@tbody" => "**@tbody** \u{2014} Table body\n\nRenders as `<tbody>`. Groups body rows.",
347− "@tr" => "**@tr** \u{2014} Table row\n\nRenders as `<tr>`.",
348− "@td" => "**@td** \u{2014} Table cell\n\nRenders as `<td>`. Regular table data cell.",
349− "@th" => {
350− "**@th** \u{2014} Table header cell\n\nRenders as `<th>`. Header cell (typically bold/centered)."
351− }
352− // Media elements
353− "@video" => {
354− "**@video** \u{2014} Video\n\nRenders as `<video>`.\n\nUsage: `@video [controls] demo.mp4`"
355− }
356− "@audio" => {
357− "**@audio** \u{2014} Audio\n\nRenders as `<audio>`.\n\nUsage: `@audio [controls] song.mp3`"
358− }
359− // Additional elements
360− "@form" => {
361− "**@form** \u{2014} Form\n\nRenders as `<form>`. Container for form elements.\n\nUsage: `@form [method post] /submit`"
362− }
363− "@details" => {
364− "**@details** \u{2014} Disclosure\n\nRenders as `<details>`. Use `[open]` for initially expanded.\n\nContains `@summary` for the toggle label."
365− }
366− "@summary" => {
367− "**@summary** \u{2014} Summary\n\nRenders as `<summary>`. Toggle label inside `@details`.\n\nUsage: `@summary Click to expand`"
368− }
369− "@blockquote" => {
370− "**@blockquote** \u{2014} Block quotation\n\nRenders as `<blockquote>`. Semantic quotation container."
371− }
372− "@cite" => {
373− "**@cite** \u{2014} Citation\n\nRenders as `<cite>`. Source or reference for a quotation.\n\nUsage: `@cite The Great Gatsby`"
374− }
375− "@code" => {
376− "**@code** \u{2014} Code\n\nRenders as `<code>` with monospace font.\n\nUsage: `@code console.log(\"hello\")`"
377− }
378− "@pre" => {
379− "**@pre** \u{2014} Preformatted\n\nRenders as `<pre>` with preserved whitespace and monospace font."
380− }
381− "@hr" => {
382− "**@hr** \u{2014} Horizontal Rule\n\nRenders as self-closing `<hr>`. Visual divider.\n\nUsage: `@hr [border-top 1 #ccc]`"
383− }
384− "@figure" => {
385− "**@figure** \u{2014} Figure\n\nRenders as `<figure>`. Container for media with optional `@figcaption`.\n\n```\n@figure\n @image photo.jpg\n @figcaption A nice photo\n```"
386− }
387− "@figcaption" => {
388− "**@figcaption** \u{2014} Figure caption\n\nRenders as `<figcaption>`. Caption text inside `@figure`."
389− }
390− "@progress" => {
391− "**@progress** \u{2014} Progress bar\n\nRenders as `<progress>`.\n\nUsage: `@progress [value 70, max 100]`"
392− }
393− "@meter" => {
394− "**@meter** \u{2014} Meter\n\nRenders as `<meter>`. Gauge for scalar measurement.\n\nUsage: `@meter [value 0.7, min 0, max 1, low 0.3, high 0.8]`"
395− }
396− "@fragment" => {
397− "**@fragment** \u{2014} Fragment\n\nGroups children without emitting a wrapper element. Renders children directly in the parent."
398− }
399− // New elements
400− "@dialog" => {
401− "**@dialog** \u{2014} Dialog\n\nRenders as `<dialog>`. Modal or non-modal dialog box.\n\nUsage: `@dialog [open] Dialog content`"
402− }
403− "@dl" => {
404− "**@dl** \u{2014} Description list\n\nRenders as `<dl>`. Contains `@dt` and `@dd` pairs."
405− }
406− "@dt" => {
407− "**@dt** \u{2014} Description term\n\nRenders as `<dt>`. Term in a `@dl` description list."
408− }
409− "@dd" => {
410− "**@dd** \u{2014} Description details\n\nRenders as `<dd>`. Details for a `@dt` term."
411− }
412− "@fieldset" => {
413− "**@fieldset** \u{2014} Fieldset\n\nRenders as `<fieldset>`. Groups related form elements.\n\nUse `@legend` for a caption."
414− }
415− "@legend" => {
416− "**@legend** \u{2014} Legend\n\nRenders as `<legend>`. Caption for a `@fieldset`."
417− }
418− "@picture" => {
419− "**@picture** \u{2014} Picture\n\nRenders as `<picture>`. Container for responsive image sources.\n\nUse `@source` children for different media queries."
420− }
421− "@source" => {
422− "**@source** \u{2014} Source\n\nRenders as `<source>`. Media source for `@picture`, `@video`, or `@audio`.\n\nUsage: `@source [src image.webp, type image/webp]`"
423− }
424− "@time" => {
425− "**@time** \u{2014} Time\n\nRenders as `<time>`. Machine-readable date/time.\n\nUsage: `@time [datetime 2024-01-15] January 15`"
426− }
427− "@mark" => "**@mark** \u{2014} Mark\n\nRenders as `<mark>`. Highlighted or marked text.",
428− "@kbd" => {
429− "**@kbd** \u{2014} Keyboard input\n\nRenders as `<kbd>`. Represents keyboard input.\n\nUsage: `@kbd Ctrl+C`"
430− }
431− "@abbr" => {
432− "**@abbr** \u{2014} Abbreviation\n\nRenders as `<abbr>`. Abbreviation with optional title.\n\nUsage: `@abbr [title Hypertext Markup Language] HTML`"
433− }
434− "@datalist" => {
435− "**@datalist** \u{2014} Datalist\n\nRenders as `<datalist>`. Provides predefined options for `@input`.\n\nUsage: `@datalist [id colors]`"
436− }
437− // Directives
438− "@match" => {
439− "**@match** \u{2014} Pattern matching\n\nMatch a value against cases.\n\n```\n@match $theme\n @case dark\n @el [background #333]\n @case light\n @el [background white]\n @default\n @el [background gray]\n```"
440− }
441− "@case" => {
442− "**@case** \u{2014} Match case\n\nA case inside `@match`. Matches when the value equals the case value."
443− }
444− "@default" => {
445− "**@default** \u{2014} Default case\n\nFallback case inside `@match` when no other case matches."
446− }
447− "@warn" => {
448− "**@warn** \u{2014} Compile warning\n\nEmit a custom warning during compilation.\n\nUsage: `@warn This value is deprecated`"
449− }
450− "@breakpoint" => {
451− "**@breakpoint** \u{2014} Custom breakpoint\n\nDefines a custom responsive breakpoint.\n\nUsage: `@breakpoint tablet 600`"
452− }
453− "@theme" => {
454− "**@theme** \u{2014} Design tokens\n\nDefines centralized design tokens (colors, spacing, fonts).\nEach token becomes both a `$variable` and a `--css-custom-property`.\n\n```\n@theme\n primary #3b82f6\n spacing-md 16\n font-body system-ui, sans-serif\n```"
455− }
456− "@deprecated" => {
457− "**@deprecated** `<message>`\n\nMarks the next `@let` component as deprecated. Callers get a compile-time warning.\n\n```\n@deprecated Use @new-card instead\n@let old-card $title\n ...\n```"
458− }
459− "@extends" => {
460− "**@extends** `<file.hl>`\n\nInherit a layout template. Fill named `@slot` blocks.\n\n```\n@extends layout.hl\n@slot content\n My page content\n@slot sidebar\n Sidebar content\n```"
461− }
462− "@font-face" => {
463− "**@font-face** \u{2014} Custom font\n\nDefines a custom font face. Generates a CSS `@font-face` rule.\n\n```\n@font-face\n family Inter\n src url(/fonts/Inter.woff2)\n weight 400 700\n```"
464− }
465− "@json-ld" => {
466− "**@json-ld** \u{2014} Structured data\n\nAdds JSON-LD structured data to `<head>` as `<script type=\"application/ld+json\">`.\n\n```\n@json-ld\n type Organization\n name My Company\n url https://example.com\n```"
467− }
468− // Attributes
469− "spacing" | "gap" => {
470− "**spacing** `<value>`\n\nGap between children. Supports CSS units (px, rem, em, %).\nMaps to CSS `gap`."
471− }
472− "padding" => {
473− "**padding** `<value>` | `<y> <x>` | `<t> <h> <b>` | `<t> <r> <b> <l>`\n\nInner padding. Supports CSS units. Accepts 1\u{2013}4 values."
474− }
475− "padding-x" => {
476− "**padding-x** `<value>`\n\nHorizontal padding (left + right). Supports CSS units."
477− }
478− "padding-y" => {
479− "**padding-y** `<value>`\n\nVertical padding (top + bottom). Supports CSS units."
480− }
481− "width" => {
482− "**width** `<value>` | `fill` | `shrink`\n\n- Number/unit: fixed width (e.g., `300`, `50%`, `80ch`)\n- `fill`: expand to fill parent\n- `shrink`: prevent flex shrinking"
483− }
484− "height" => {
485− "**height** `<value>` | `fill` | `shrink`\n\n- Number/unit: fixed height (e.g., `300`, `100vh`)\n- `fill`: expand to fill parent\n- `shrink`: prevent flex shrinking"
486− }
487− "min-width" => "**min-width** `<value>` \u{2014} Minimum width. Supports CSS units.",
488− "max-width" => "**max-width** `<value>` \u{2014} Maximum width. Supports CSS units.",
489− "min-height" => "**min-height** `<value>` \u{2014} Minimum height. Supports CSS units.",
490− "max-height" => "**max-height** `<value>` \u{2014} Maximum height. Supports CSS units.",
491− "center-x" => {
492− "**center-x**\n\nCenter horizontally.\n\nIn column parent: `align-self: center`\nOtherwise: auto margins."
493− }
494− "center-y" => {
495− "**center-y**\n\nCenter vertically.\n\nIn row parent: `align-self: center`\nOtherwise: auto margins."
496− }
497− "align-left" => "**align-left** \u{2014} Align to the left edge.",
498− "align-right" => "**align-right** \u{2014} Align to the right edge.",
499− "align-top" => "**align-top** \u{2014} Align to the top edge.",
500− "align-bottom" => "**align-bottom** \u{2014} Align to the bottom edge.",
501− "background" => {
502− "**background** `<color>` \u{2014} Background color or CSS background value."
503− }
504− "color" => "**color** `<color>` \u{2014} Text color.",
505− "border" => {
506− "**border** `<width> [color]`\n\nBorder. Width in pixels, color defaults to `currentColor`."
507− }
508− "border-top" => "**border-top** `<width> [color]` \u{2014} Top border.",
509− "border-bottom" => "**border-bottom** `<width> [color]` \u{2014} Bottom border.",
510− "border-left" => "**border-left** `<width> [color]` \u{2014} Left border.",
511− "border-right" => "**border-right** `<width> [color]` \u{2014} Right border.",
512− "rounded" => "**rounded** `<value>` \u{2014} Border radius. Supports CSS units.",
513− "bold" => "**bold** \u{2014} Bold text (`font-weight: bold`).",
514− "italic" => "**italic** \u{2014} Italic text (`font-style: italic`).",
515− "underline" => "**underline** \u{2014} Underlined text.",
516− "size" => "**size** `<value>` \u{2014} Font size in pixels.",
517− "font" => "**font** `<family>` \u{2014} Font family.",
518− "transition" => {
519− "**transition** `<value>` \u{2014} CSS transition (e.g., `all 0.15s ease`)."
520− }
521− "cursor" => "**cursor** `<value>` \u{2014} CSS cursor (e.g., `pointer`).",
522− "opacity" => "**opacity** `<value>` \u{2014} Opacity from 0 to 1.",
523− "text-align" => {
524− "**text-align** `<value>` \u{2014} Text alignment (`left`, `center`, `right`, `justify`)."
525− }
526− "line-height" => {
527− "**line-height** `<value>` \u{2014} Line height. Unitless (e.g., `1.5`) or pixels."
528− }
529− "overflow" => {
530− "**overflow** `<value>` \u{2014} Overflow behavior (`hidden`, `scroll`, `auto`, `visible`)."
531− }
532− "position" => {
533− "**position** `<value>` \u{2014} Position type (`relative`, `absolute`, `fixed`, `sticky`)."
534− }
535− "top" => "**top** `<value>` \u{2014} Top offset for positioned elements.",
536− "right" => "**right** `<value>` \u{2014} Right offset for positioned elements.",
537− "bottom" => "**bottom** `<value>` \u{2014} Bottom offset for positioned elements.",
538− "left" => "**left** `<value>` \u{2014} Left offset for positioned elements.",
539− "z-index" => "**z-index** `<value>` \u{2014} Stack order (integer).",
540− "display" => {
541− "**display** `<value>` \u{2014} Display mode (`none`, `block`, `inline`, `flex`, `grid`)."
542− }
543− "visibility" => "**visibility** `<value>` \u{2014} Visibility (`visible`, `hidden`).",
544− "transform" => {
545− "**transform** `<value>` \u{2014} CSS transform (e.g., `rotate(45deg)`, `scale(1.5)`)."
546− }
547− "backdrop-filter" => {
548− "**backdrop-filter** `<value>` \u{2014} Backdrop filter (e.g., `blur(10px)`)."
549− }
550− "letter-spacing" => {
551− "**letter-spacing** `<value>` \u{2014} Letter spacing. Supports CSS units."
552− }
553− "text-transform" => {
554− "**text-transform** `<value>` \u{2014} Text transform (`uppercase`, `lowercase`, `capitalize`)."
555− }
556− "white-space" => {
557− "**white-space** `<value>` \u{2014} White-space behavior (`nowrap`, `pre`, `normal`)."
558− }
559− "grid" => "**grid** \u{2014} Enable CSS grid layout on this element.",
560− "grid-cols" => {
561− "**grid-cols** `<value>` \u{2014} Grid template columns. Number for equal columns, or CSS value."
562− }
563− "grid-rows" => {
564− "**grid-rows** `<value>` \u{2014} Grid template rows. Number for equal rows, or CSS value."
565− }
566− "col-span" => "**col-span** `<value>` \u{2014} Span N columns in a grid.",
567− "row-span" => "**row-span** `<value>` \u{2014} Span N rows in a grid.",
568− "shadow" => {
569− "**shadow** `<value>` \u{2014} Box shadow. Raw CSS value (e.g., `0 2px 4px rgba(0,0,0,0.1)`)."
570− }
571− "gap-x" => {
572− "**gap-x** `<value>` \u{2014} Horizontal gap between children in pixels. Maps to `column-gap`."
573− }
574− "gap-y" => {
575− "**gap-y** `<value>` \u{2014} Vertical gap between children in pixels. Maps to `row-gap`."
576− }
577− "wrap" => "**wrap** \u{2014} Enable flex-wrap for children.",
578− "id" => "**id** `<value>` \u{2014} HTML id attribute.",
579− "class" => "**class** `<value>` \u{2014} HTML class attribute.",
580− "animation" => {
581− "**animation** `<value>` \u{2014} CSS animation shorthand (e.g., `fade-in 0.3s ease`).\n\nDefine animations with `@keyframes`."
582− }
583− "container" => {
584− "**container** \u{2014} Enable container queries (`container-type: inline-size`)."
585− }
586− "container-name" => {
587− "**container-name** `<value>` \u{2014} Name this container for `@container` queries."
588− }
589− "container-type" => {
590− "**container-type** `<value>` \u{2014} Container type (`inline-size`, `size`, `normal`)."
591− }
592− // Form attributes
593− "type" => {
594− "**type** `<value>` \u{2014} Input type (`text`, `email`, `password`, `submit`, etc.)."
595− }
596− "placeholder" => "**placeholder** `<value>` \u{2014} Placeholder text for inputs.",
597− "name" => "**name** `<value>` \u{2014} Form field name.",
598− "value" => "**value** `<value>` \u{2014} Form field value.",
599− "disabled" => "**disabled** \u{2014} Disable the element.",
600− "required" => "**required** \u{2014} Mark field as required.",
601− "checked" => "**checked** \u{2014} Checkbox/radio checked state.",
602− "for" => "**for** `<id>` \u{2014} Label target (id of the associated input).",
603− "rows" => "**rows** `<value>` \u{2014} Number of visible rows for textarea.",
604− "cols" => "**cols** `<value>` \u{2014} Number of visible columns for textarea.",
605− "maxlength" => "**maxlength** `<value>` \u{2014} Maximum input length.",
606− // Accessibility
607− "alt" => "**alt** `<value>` \u{2014} Alternative text for images.",
608− "role" => "**role** `<value>` \u{2014} ARIA role (e.g., `navigation`, `banner`, `main`).",
609− "tabindex" => {
610− "**tabindex** `<value>` \u{2014} Tab order. `0` = natural order, `-1` = skip."
611− }
612− "title" => "**title** `<value>` \u{2014} Tooltip text.",
613− // New CSS attributes
614− "aspect-ratio" => {
615− "**aspect-ratio** `<value>` \u{2014} CSS aspect ratio (e.g., `16/9`, `1`, `4/3`)."
616− }
617− "outline" => {
618− "**outline** `<width> [color]` \u{2014} Outline (like border but doesn't affect layout)."
619− }
620− "padding-inline" => {
621− "**padding-inline** `<value>` \u{2014} Horizontal padding (logical property, i18n-aware)."
622− }
623− "padding-block" => {
624− "**padding-block** `<value>` \u{2014} Vertical padding (logical property, i18n-aware)."
625− }
626− "margin-inline" => {
627− "**margin-inline** `<value>` \u{2014} Horizontal margin (logical property, i18n-aware)."
628− }
629− "margin-block" => {
630− "**margin-block** `<value>` \u{2014} Vertical margin (logical property, i18n-aware)."
631− }
632− "scroll-snap-type" => {
633− "**scroll-snap-type** `<value>` \u{2014} Scroll snap type (`x mandatory`, `y proximity`)."
634− }
635− "scroll-snap-align" => {
636− "**scroll-snap-align** `<value>` \u{2014} Scroll snap alignment (`start`, `center`, `end`)."
637− }
638− // Media/image attributes
639− "loading" => {
640− "**loading** `<value>` \u{2014} Loading behavior for images (`lazy`, `eager`)."
641− }
642− "decoding" => {
643− "**decoding** `<value>` \u{2014} Image decoding mode (`async`, `sync`, `auto`)."
644− }
645− "controls" => "**controls** \u{2014} Show media controls (for @video, @audio).",
646− "autoplay" => "**autoplay** \u{2014} Auto-play media.",
647− "loop" => "**loop** \u{2014} Loop media playback.",
648− "muted" => "**muted** \u{2014} Mute media.",
649− "poster" => "**poster** `<url>` \u{2014} Poster image for video.",
650− "preload" => {
651− "**preload** `<value>` \u{2014} Media preload hint (`auto`, `metadata`, `none`)."
652− }
653− "ordered" => "**ordered** \u{2014} Use ordered list (`<ol>` instead of `<ul>`).",
654− "src" => "**src** `<url>` \u{2014} Source URL for media elements.",
655− // New CSS attributes
656− "margin" => {
657− "**margin** `<value>` | `<y> <x>` | `<t> <h> <b>` | `<t> <r> <b> <l>`\n\nOuter margin. Supports CSS units. Accepts 1\u{2013}4 values."
658− }
659− "margin-x" => "**margin-x** `<value>` \u{2014} Horizontal margin (left + right).",
660− "margin-y" => "**margin-y** `<value>` \u{2014} Vertical margin (top + bottom).",
661− "filter" => {
662− "**filter** `<value>` \u{2014} CSS filter (e.g., `blur(5px)`, `brightness(1.2)`, `grayscale(1)`)."
663− }
664− "object-fit" => {
665− "**object-fit** `<value>` \u{2014} How content fits its container (`cover`, `contain`, `fill`, `none`, `scale-down`)."
666− }
667− "object-position" => {
668− "**object-position** `<value>` \u{2014} Position of content within container (e.g., `center`, `top left`)."
669− }
670− "text-shadow" => {
671− "**text-shadow** `<value>` \u{2014} Text shadow. Raw CSS value (e.g., `1px 1px 2px rgba(0,0,0,0.3)`)."
672− }
673− "text-overflow" => {
674− "**text-overflow** `<value>` \u{2014} Text overflow behavior (`ellipsis`, `clip`). Combine with `white-space nowrap` and `overflow hidden`."
675− }
676− "pointer-events" => {
677− "**pointer-events** `<value>` \u{2014} Pointer event behavior (`none`, `auto`)."
678− }
679− "user-select" => {
680− "**user-select** `<value>` \u{2014} Text selection behavior (`none`, `text`, `all`, `auto`)."
681− }
682− "justify-content" => {
683− "**justify-content** `<value>` \u{2014} Main axis alignment (`center`, `space-between`, `space-around`, `flex-start`, `flex-end`)."
684− }
685− "align-items" => {
686− "**align-items** `<value>` \u{2014} Cross axis alignment (`center`, `flex-start`, `flex-end`, `stretch`, `baseline`)."
687− }
688− "order" => "**order** `<value>` \u{2014} Flex/grid item order (integer).",
689− "background-size" => {
690− "**background-size** `<value>` \u{2014} Background size (`cover`, `contain`, `auto`, or dimensions)."
691− }
692− "background-position" => {
693− "**background-position** `<value>` \u{2014} Background position (`center`, `top`, `bottom left`, etc.)."
694− }
695− "background-repeat" => {
696− "**background-repeat** `<value>` \u{2014} Background repeat (`no-repeat`, `repeat`, `repeat-x`, `repeat-y`)."
697− }
698− "word-break" => {
699− "**word-break** `<value>` \u{2014} Word breaking behavior (`break-all`, `keep-all`, `normal`)."
700− }
701− "overflow-wrap" => {
702− "**overflow-wrap** `<value>` \u{2014} Overflow wrapping (`break-word`, `anywhere`, `normal`)."
703− }
704− // New element attributes
705− "open" => "**open** \u{2014} Initially expand `@details` element.",
706− "novalidate" => "**novalidate** \u{2014} Disable form validation.",
707− "low" => "**low** `<value>` \u{2014} Low threshold for `@meter`.",
708− "high" => "**high** `<value>` \u{2014} High threshold for `@meter`.",
709− "optimum" => "**optimum** `<value>` \u{2014} Optimum value for `@meter`.",
710− "colspan" => "**colspan** `<value>` \u{2014} Number of columns a cell spans.",
711− "rowspan" => "**rowspan** `<value>` \u{2014} Number of rows a cell spans.",
712− "scope" => {
713− "**scope** `<value>` \u{2014} Header scope (`col`, `row`, `colgroup`, `rowgroup`)."
714− }
715− "inline" => "**inline** \u{2014} Inline SVG image content into the HTML output.",
716− "color-scheme" => {
717− "**color-scheme** `<value>` \u{2014} Color scheme preference (`light`, `dark`, `light dark`)."
718− }
719− "appearance" => {
720− "**appearance** `<value>` \u{2014} Form element appearance (`none` to remove native styling)."
721− }
722− "popover" => {
723− "**popover** \u{2014} Make element a popover (HTML Popover API). Shown/hidden declaratively."
724− }
725− "popovertarget" => "**popovertarget** `<id>` \u{2014} ID of the popover element to toggle.",
726− "popovertargetaction" => {
727− "**popovertargetaction** `<value>` \u{2014} Popover action (`toggle`, `show`, `hide`)."
728− }
729− "inputmode" => {
730− "**inputmode** `<value>` \u{2014} Virtual keyboard type (`numeric`, `email`, `search`, `tel`, `url`)."
731− }
732− "enterkeyhint" => {
733− "**enterkeyhint** `<value>` \u{2014} Enter key label (`done`, `go`, `next`, `search`, `send`)."
734− }
735− "fetchpriority" => {
736− "**fetchpriority** `<value>` \u{2014} Resource fetch priority (`high`, `low`, `auto`)."
737− }
738− "translate" => {
739− "**translate** `<value>` \u{2014} Whether element should be translated (`yes`, `no`)."
740− }
741− "spellcheck" => "**spellcheck** `<value>` \u{2014} Spell check mode (`true`, `false`).",
742− "hidden" => "**hidden** \u{2014} Hide element (`display: none`).",
743− "overflow-x" => {
744− "**overflow-x** `<value>` \u{2014} Horizontal overflow (`hidden`, `scroll`, `auto`, `visible`)."
745− }
746− "overflow-y" => {
747− "**overflow-y** `<value>` \u{2014} Vertical overflow (`hidden`, `scroll`, `auto`, `visible`)."
748− }
749− "inset" => {
750− "**inset** `<value>` \u{2014} Shorthand for `top`, `right`, `bottom`, `left`. Maps to CSS `inset`."
751− }
752− "accent-color" => {
753− "**accent-color** `<color>` \u{2014} Accent color for form controls (checkboxes, radios, range)."
754− }
755− "caret-color" => "**caret-color** `<color>` \u{2014} Color of the text input cursor.",
756− "list-style" => {
757− "**list-style** `<value>` \u{2014} List style type (`disc`, `circle`, `square`, `decimal`, `none`)."
758− }
759− "border-collapse" => {
760− "**border-collapse** `<value>` \u{2014} Table border model (`collapse`, `separate`)."
761− }
762− "border-spacing" => {
763− "**border-spacing** `<value>` \u{2014} Spacing between table cell borders (when `border-collapse: separate`)."
764− }
765− "text-decoration" => {
766− "**text-decoration** `<value>` \u{2014} Text decoration (`underline`, `overline`, `line-through`, `none`)."
767− }
768− "text-decoration-color" => {
769− "**text-decoration-color** `<color>` \u{2014} Color of text decoration."
770− }
771− "text-decoration-thickness" => {
772− "**text-decoration-thickness** `<value>` \u{2014} Thickness of text decoration."
773− }
774− "text-decoration-style" => {
775− "**text-decoration-style** `<value>` \u{2014} Style of text decoration (`solid`, `dashed`, `dotted`, `wavy`, `double`)."
776− }
777− "place-items" => {
778− "**place-items** `<value>` \u{2014} Shorthand for `align-items` and `justify-items`."
779− }
780− "place-self" => {
781− "**place-self** `<value>` \u{2014} Shorthand for `align-self` and `justify-self`."
782− }
783− "scroll-behavior" => {
784− "**scroll-behavior** `<value>` \u{2014} Scroll behavior (`smooth`, `auto`)."
785− }
786− "resize" => {
787− "**resize** `<value>` \u{2014} Resize behavior (`none`, `both`, `horizontal`, `vertical`)."
788− }
789− // New CSS attributes
790− "clip-path" => {
791− "**clip-path** `<value>` \u{2014} Clip path (`circle()`, `polygon()`, `inset()`, `url()`)."
792− }
793− "mix-blend-mode" => {
794− "**mix-blend-mode** `<value>` \u{2014} Blend mode (`multiply`, `screen`, `overlay`, `darken`, `lighten`)."
795− }
796− "background-blend-mode" => {
797− "**background-blend-mode** `<value>` \u{2014} Background blend mode for layered backgrounds."
798− }
799− "writing-mode" => {
800− "**writing-mode** `<value>` \u{2014} Writing direction (`horizontal-tb`, `vertical-rl`, `vertical-lr`)."
801− }
802− "column-count" => {
803− "**column-count** `<value>` \u{2014} Number of columns in multi-column layout."
804− }
805− "column-gap" => {
806− "**column-gap** `<value>` \u{2014} Gap between columns. Supports CSS units."
807− }
808− "text-indent" => {
809− "**text-indent** `<value>` \u{2014} Indentation of the first line of text."
810− }
811− "hyphens" => {
812− "**hyphens** `<value>` \u{2014} Hyphenation behavior (`none`, `manual`, `auto`)."
813− }
814− "flex-grow" => {
815− "**flex-grow** `<value>` \u{2014} Flex grow factor (number). Controls how much an item grows."
816− }
817− "flex-shrink" => {
818− "**flex-shrink** `<value>` \u{2014} Flex shrink factor (number). Controls how much an item shrinks."
819− }
820− "flex-basis" => {
821− "**flex-basis** `<value>` \u{2014} Initial main size of a flex item (e.g., `200px`, `auto`, `0`)."
822− }
823− "isolation" => {
824− "**isolation** `<value>` \u{2014} Creates a new stacking context (`isolate`, `auto`)."
825− }
826− "place-content" => {
827− "**place-content** `<value>` \u{2014} Shorthand for `align-content` and `justify-content`."
828− }
829− "background-image" => {
830− "**background-image** `<value>` \u{2014} Background image (`url()` or gradient function)."
831− }
832− // New CSS properties (batch 2)
833− "font-weight" => {
834− "**font-weight** `<value>` \u{2014} Font weight (`100`\u{2013}`900`, `bold`, `lighter`, `bolder`). More precise than `bold`."
835− }
836− "font-style" => {
837− "**font-style** `<value>` \u{2014} Font style (`normal`, `italic`, `oblique`). More precise than `italic`."
838− }
839− "text-wrap" => {
840− "**text-wrap** `<value>` \u{2014} Text wrapping behavior (`balance`, `pretty`, `nowrap`, `wrap`). `balance` is great for headings."
841− }
842− "will-change" => {
843− "**will-change** `<value>` \u{2014} Performance hint for upcoming changes (`transform`, `opacity`, `scroll-position`)."
844− }
845− "touch-action" => {
846− "**touch-action** `<value>` \u{2014} Touch behavior for mobile (`none`, `pan-x`, `pan-y`, `manipulation`, `auto`)."
847− }
848− "vertical-align" => {
849− "**vertical-align** `<value>` \u{2014} Vertical alignment for inline elements (`middle`, `top`, `bottom`, `baseline`, `text-top`)."
850− }
851− "contain" => {
852− "**contain** `<value>` \u{2014} CSS containment for performance (`layout`, `paint`, `content`, `strict`, `size`)."
853− }
854− "content-visibility" => {
855− "**content-visibility** `<value>` \u{2014} Content rendering optimization (`auto`, `visible`, `hidden`). `auto` enables lazy rendering."
856− }
857− "scroll-margin" => {
858− "**scroll-margin** `<value>` \u{2014} Scroll margin (for scroll-snap and anchor link offsets). Supports CSS units."
859− }
860− "scroll-padding" => {
861− "**scroll-padding** `<value>` \u{2014} Scroll padding for scroll-snap containers. Supports CSS units."
862− }
863− "content" => {
864− "**content** `<value>` \u{2014} CSS content property. Use with `before:` or `after:` prefix.\n\nExample: `before:content \u{2192}, before:color red`"
865− }
866− // New elements
867− "@iframe" => {
868− "**@iframe** \u{2014} Embedded page\n\nRenders as `<iframe>`.\n\nUsage: `@iframe [width fill, height 400] https://example.com`\n\nAttributes: `sandbox`, `allow`, `allowfullscreen`"
869− }
870− "@output" => {
871− "**@output** \u{2014} Form output\n\nRenders as `<output>`. Displays calculation results in forms.\n\nUsage: `@output [for a b] Result`"
872− }
873− "@canvas" => {
874− "**@canvas** \u{2014} Drawing surface\n\nRenders as `<canvas>`. Use with `@raw` JavaScript for drawing.\n\nUsage: `@canvas [width 400, height 300, id myCanvas]`"
875− }
876− "@script" => {
877− "**@script** \u{2014} Script\n\nRenders as `<script>`. Inline JavaScript.\n\nUsage: `@script console.log(\"hello\")`"
878− }
879− "@noscript" => {
880− "**@noscript** \u{2014} NoScript fallback\n\nRenders as `<noscript>`. Shown when JavaScript is disabled.\n\nUsage: `@noscript Please enable JavaScript.`"
881− }
882− "@address" => {
883− "**@address** \u{2014} Address\n\nRenders as `<address>`. Contact information for the nearest `@article` or `@body`.\n\nUsage: `@address hello@example.com`"
884− }
885− "@search" => {
886− "**@search** \u{2014} Search\n\nRenders as `<search>`. Semantic container for search functionality.\n\nUsage:\n```\n@search\n @input [type search, placeholder Search...]\n @button Search\n```"
887− }
888− "@breadcrumb" => {
889− "**@breadcrumb** \u{2014} Breadcrumb\n\nRenders as `<nav aria-label=\"Breadcrumb\">`. Semantic breadcrumb navigation.\n\nUsage:\n```\n@breadcrumb\n @link / Home\n @link /docs Docs\n @text Current Page\n```"
890− }
891− // Convenience elements
892− "@grid" => {
893− "**@grid** \u{2014} CSS Grid container\n\nRenders as `<div>` with `display: grid`.\n\nUsage: `@grid [grid-cols 3, gap 20]`"
894− }
895− "@stack" => {
896− "**@stack** \u{2014} Stack container\n\nRenders as `<div>` with `position: relative`. Children can be absolutely positioned on top of each other."
897− }
898− "@in-front" => {
899− "**@in-front** \u{2014} Overlay layer\n\nRenders as `<div>` with `position: absolute; inset: 0`, painted on top of the parent's content. The parent automatically becomes a positioning context (`position: relative; isolation: isolate`).\n\nUsage:\n```\n@el [width 200, height 200, background blue]\n Main content\n @in-front\n @text [color white] Overlay\n```"
900− }
901− "@behind" => {
902− "**@behind** \u{2014} Background layer\n\nRenders as `<div>` with `position: absolute; inset: 0; z-index: -1`, painted behind the parent's content. The parent automatically becomes a positioning context (`position: relative; isolation: isolate`).\n\nUsage:\n```\n@el [padding 40]\n @behind\n @el [background #fef3c7, width fill, height fill]\n Foreground text\n```"
903− }
904− "@spacer" => {
905− "**@spacer** \u{2014} Flexible spacer\n\nRenders as `<div>` with `flex: 1`. Pushes siblings apart in flex containers."
906− }
907− "@badge" => {
908− "**@badge** \u{2014} Badge\n\nRenders as `<span>` with pill shape, centered text.\n\nUsage: `@badge [background #3b82f6, color white] NEW`"
909− }
910− "@tooltip" => {
911− "**@tooltip** \u{2014} Tooltip\n\nRenders as `<span>` with `title` attribute for native tooltips.\n\nUsage: `@tooltip [cursor help] Hover me`"
912− }
913− "@avatar" => {
914− "**@avatar** \u{2014} Avatar\n\nRenders as `<div>` with circular shape, centered content, `overflow: hidden`.\n\nUsage:\n```\n@avatar [width 48, height 48, background #e5e7eb]\n @image [object-fit cover] photo.jpg\n```"
915− }
916− "@carousel" => {
917− "**@carousel** \u{2014} Horizontal carousel\n\nRenders as `<div>` with horizontal scroll-snap.\nChildren auto-receive `scroll-snap-align: start` and `flex-shrink: 0`.\n\nUsage:\n```\n@carousel [gap 16]\n @el [width 300] Slide 1\n @el [width 300] Slide 2\n```"
918− }
919− "@chip" => {
920− "**@chip** \u{2014} Chip / pill\n\nRenders as `<span>` with rounded borders, inline-flex layout.\n\nUsage: `@chip [background #e5e7eb] Category`"
921− }
922− "@tag" => {
923− "**@tag** \u{2014} Tag label\n\nRenders as `<span>` with subtle rounded rectangle, bold small text.\n\nUsage: `@tag [background #dbeafe, color #1e40af] v2.0`"
924− }
925− // CSS shorthands
926− "truncate" => {
927− "**truncate** \u{2014} Truncate with ellipsis\n\nShorthand for: `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`\n\nUsage: `@text [max-width 200, truncate] Long text here...`"
928− }
929− "line-clamp" => {
930− "**line-clamp** `<N>` \u{2014} Multi-line truncation\n\nClamps text to N lines with ellipsis.\n\nUsage: `@paragraph [line-clamp 3] Long paragraph...`"
931− }
932− "blur" => {
933− "**blur** `<value>` \u{2014} Apply blur filter\n\nShorthand for `filter: blur(Npx)`.\n\nUsage: `[blur 4]` \u{2192} `filter: blur(4px)`"
934− }
935− "backdrop-blur" => {
936− "**backdrop-blur** `<value>` \u{2014} Apply backdrop blur\n\nShorthand for `backdrop-filter: blur(Npx)`.\n\nUsage: `[backdrop-blur 10]` \u{2192} `backdrop-filter: blur(10px)`"
937− }
938− "direction" => "**direction** `<value>` \u{2014} Text direction (`ltr`, `rtl`).",
939− // Grid areas
940− "grid-template-areas" => {
941− "**grid-template-areas** `<value>` \u{2014} Define named grid areas.\n\nUsage: `[grid, grid-template-areas \"header header\" \"sidebar main\"]`\n\nChildren use `grid-area` to place themselves."
942− }
943− "grid-area" => {
944− "**grid-area** `<name>` \u{2014} Place element in a named grid area.\n\nUsage: `@el [grid-area header] ...`\n\nRequires parent with `grid-template-areas`."
945− }
946− // View transitions
947− "view-transition-name" => {
948− "**view-transition-name** `<name>` \u{2014} Assign a View Transition API name.\n\nEnables smooth transitions between pages.\n\nUsage: `@el [view-transition-name hero]`"
949− }
950− // Has pseudo-selector
951− "has(" => {
952− "**has(selector):** \u{2014} Parent selector pseudo-class.\n\nStyle an element based on its children.\n\nUsage: `@el [has(.active):background blue, has(img):padding 0]`\n\nGenerates CSS `:has()` selector."
953− }
954− // Critical
955− "critical" => {
956− "**critical** \u{2014} Mark element as above-fold.\n\nHint for build tools to prioritize this element's CSS."
957− }
958− // New CSS properties
959− "text-underline-offset" => {
960− "**text-underline-offset** `<value>` \u{2014} Offset of text underline from its default position. Supports CSS units.\n\nUsage: `[underline, text-underline-offset 4]`"
961− }
962− "column-width" => {
963− "**column-width** `<value>` \u{2014} Ideal column width in multi-column layout. Browser determines column count.\n\nUsage: `[column-width 200]`"
964− }
965− "column-rule" => {
966− "**column-rule** `<value>` \u{2014} Rule between columns (shorthand for width, style, color).\n\nUsage: `[column-rule 1px solid #ccc]`"
967− }
968− // Variable filters
969− "\\$|uppercase" => {
970− "**|uppercase** \u{2014} Convert variable to UPPERCASE.\n\nUsage: `$name|uppercase`"
971− }
972− "\\$|lowercase" => {
973− "**|lowercase** \u{2014} Convert variable to lowercase.\n\nUsage: `$name|lowercase`"
974− }
975− "\\$|capitalize" => {
976− "**|capitalize** \u{2014} Capitalize first letter.\n\nUsage: `$name|capitalize`"
977− }
978− "\\$|trim" => {
979− "**|trim** \u{2014} Strip leading/trailing whitespace.\n\nUsage: `$name|trim`"
980− }
981− "\\$|length" => "**|length** \u{2014} Get string length.\n\nUsage: `$name|length`",
982− "\\$|reverse" => "**|reverse** \u{2014} Reverse string.\n\nUsage: `$name|reverse`",
983− _ => return None,
984− };
985−
986− Some(if let Some(state) = state {
987− format!("*({} state)* {}", state, doc)
988− } else {
989− doc.to_string()
990− })
991−}
src/bin/htmlang_lsp/main.rs+1
@@ -12,6 +12,7 @@ use tower_lsp::{Client, LanguageServer, LspService, Server};
1212
1313mod analysis;
1414mod completion;
15+mod docs;
1516mod hover;
1617mod navigation;
1718mod state;