on master help sign in
Derive the language server's knowledge from the compiler's tables
7be4fc3iko
crates/htmlang-core/src/parser.rs+8
@@ -2511,6 +2511,12 @@ fn argument_is_special(kind: &ElementKind) -> bool {
2511 .is_some_and(|spec| matches!(spec.arg, TagArg::Attr(_)))
2511 .is_some_and(|spec| matches!(spec.arg, TagArg::Attr(_)))
2512}
2512}
2513
2513
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+
2514const KNOWN_DIRECTIVES: &[&str] = &[
2520const KNOWN_DIRECTIVES: &[&str] = &[
2515 "page",
2521 "page",
2516 "let",
2522 "let",
@@ -2536,6 +2542,8 @@ const KNOWN_DIRECTIVES: &[&str] = &[
2536 "assert",
2542 "assert",
2537 "markdown",
2543 "markdown",
2538 "manifest",
2544 "manifest",
2545+ "data",
2546+ "svg",
2539];
2547];
2540
2548
2541fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseError> {
2549fn 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
1040 i += 1;
1040 i += 1;
1041 }
1041 }
1042 let word = &line[start..i];
1042 let word = &line[start..i];
1043+ // Directives and built-in elements are keywords; any other
1044+ // `@name` is a function call.
1043− let token_type = match word {
1045+ let token_type = if is_builtin_name(&word[1..]) { 0 } else { 2 };
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− };
1056 // Mark unused definitions with deprecated modifier (dimmed)
1046 // Mark unused definitions with deprecated modifier (dimmed)
1057 let modifier = if trimmed.starts_with("@let ") && word != "@let" {
1047 let modifier = if trimmed.starts_with("@let ") && word != "@let" {
1058 let name_part = &word[1..]; // strip @
1048 let name_part = &word[1..]; // strip @
@@ -1110,72 +1100,12 @@ pub(crate) fn semantic_tokens(text: &str, result: &ParseResult) -> Vec<SemanticT
1110 tokens
1100 tokens
1111}
1101}
1112
1102
1113−fn is_builtin_element(word: &str) -> bool {
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
1114− matches!(
1108+ || matches!(name, "data" | "svg")
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− )
1179}
1109}
1180
1110
1181#[allow(clippy::too_many_arguments)]
1111#[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;
1use tower_lsp::lsp_types::*;
3use tower_lsp::lsp_types::*;
2
4
5+use crate::docs;
6+
3pub(crate) fn completions(text: &str, position: Position) -> Vec<CompletionItem> {
7pub(crate) fn completions(text: &str, position: Position) -> Vec<CompletionItem> {
4 let lines: Vec<&str> = text.lines().collect();
8 let lines: Vec<&str> = text.lines().collect();
5 let line = match lines.get(position.line as usize) {
9 let line = match lines.get(position.line as usize) {
@@ -22,37 +26,11 @@ pub(crate) fn completions(text: &str, position: Position) -> Vec<CompletionItem>
22 return variable_completions(text, edit_range);
26 return variable_completions(text, edit_range);
23 }
27 }
24
28
25− // State prefix (hover:, active:, focus:) or media prefix (dark:, print:)
29+ // After a state/media prefix (`hover:`, `md:`, `nth:2n:`), offer
30+ // the styles it can apply to.
26− if let Some(colon) = current_word.find(':') {
31+ if let Some(colon) = current_word.rfind(':') {
27− let prefix = &current_word[..colon];
32+ let prefix = &current_word[..=colon];
28− if matches!(
29− prefix,
33+ if vocab::is_prefixed(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− ) {
56 return state_attr_completions(prefix, edit_range);
34 return state_attr_completions(prefix, edit_range);
57 }
35 }
58 }
36 }
@@ -122,44 +100,6 @@ pub(crate) fn in_brackets(text: &str) -> bool {
122 depth > 0
100 depth > 0
123}
101}
124
102
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−}
163
103
164fn item(
104fn item(
165 label: &str,
105 label: &str,
@@ -180,205 +120,42 @@ fn item(
180 }
120 }
181}
121}
182
122
123+/// Elements (from the compiler) and standard-library components.
183fn element_completions(range: Range) -> Vec<CompletionItem> {
124fn element_completions(range: Range) -> Vec<CompletionItem> {
184− [
125+ let elements = ElementKind::all_names().map(|name| {
185− ("@row", "Horizontal layout (flexbox row)"),
126+ let detail = docs::element_summary(name).unwrap_or_default();
186− ("@column", "Vertical layout (flexbox column)"),
127+ (name, detail)
187− ("@el", "Generic container"),
128+ });
188− ("@text", "Styled inline text (span)"),
129+ let components = docs::COMPONENTS
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()
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);
288− .map(|(name, detail)| item(name, CompletionItemKind::KEYWORD, detail, name, range))
136+ item(&label, CompletionItemKind::KEYWORD, &detail, &label, range)
137+ })
289− .collect()
138+ .collect()
290}
139}
291
140
141+/// Directives, from the parser's directive list.
292fn directive_completions(range: Range) -> Vec<CompletionItem> {
142fn directive_completions(range: Range) -> Vec<CompletionItem> {
293− [
143+ htmlang::parser::known_directives()
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()
144+ .iter()
145+ .map(|name| {
146+ let label = format!("@{}", name);
147+ let detail = docs::directive(name).map_or("Directive", |d| d.summary);
372− .map(|(name, detail, insert)| item(name, CompletionItemKind::SNIPPET, detail, insert, range))
148+ item(&label, CompletionItemKind::SNIPPET, detail, &format!("{} ", label), range)
149+ })
373− .collect()
150+ .collect()
374}
151}
375
152
376fn snippet_completions(range: Range) -> Vec<CompletionItem> {
153fn snippet_completions(range: Range) -> Vec<CompletionItem> {
377 let snippets: &[(&str, &str, &str)] = &[
154 let snippets: &[(&str, &str, &str)] = &[
378 (
155 (
379− "card component",
156+ "function",
380− "Reusable card with title and content",
157+ "Define a reusable function",
381− "@let card \\$title\n @el [padding 20, background white, rounded 8]\n @text [bold] \\$title\n @children",
158+ "@let ${1:name} \\$${2:param}\n @el [${3:padding 16}]\n @children",
382 ),
159 ),
383 (
160 (
384 "responsive layout",
161 "responsive layout",
@@ -390,85 +167,30 @@ fn snippet_completions(range: Range) -> Vec<CompletionItem> {
390 "Navigation bar with horizontal items",
167 "Navigation bar with horizontal items",
391 "@nav [padding 16, background #1a1a2e]\n @row [spacing 20, align-items center]",
168 "@nav [padding 16, background #1a1a2e]\n @row [spacing 20, align-items center]",
392 ),
169 ),
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− ),
398 (
170 (
399 "each with else",
171 "each with else",
400 "Loop with empty-state fallback",
172 "Loop with empty-state fallback",
401 "@each \\$${1:item} in ${2:list}\n @text \\$${1:item}\n@else\n @text [color #888] No items found.",
173 "@each \\$${1:item} in ${2:list}\n @text \\$${1:item}\n@else\n @text [color #888] No items found.",
402 ),
174 ),
403 (
175 (
404− "button with hover",
176+ "if / else",
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",
455 "Conditional rendering block",
177 "Conditional rendering block",
456 "@if ${1:condition}\n ${2:content}\n@else\n ${3:fallback}",
178 "@if ${1:condition}\n ${2:content}\n@else\n ${3:fallback}",
457 ),
179 ),
458 (
180 (
459− "@match pattern",
181+ "form with inputs",
460− "Pattern matching block",
182+ "Form with a labeled input and a submit button",
461− "@match \\$${1:value}\n @case ${2:option1}\n ${3:content}\n @default\n ${4:fallback}",
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",
462 ),
184 ),
463 (
185 (
464− "@data JSON load",
186+ "grid layout",
465− "Load variables from a JSON file",
187+ "Grid with equal columns",
466− "@data ${1:data.json}",
188+ "@grid [grid-cols ${1:3}, spacing ${2:20}]\n @el [padding 20]\n ${3:Item}",
467 ),
189 ),
468 (
190 (
469− "@component scoped",
191+ "dark mode",
470− "Define a scoped component with styles",
192+ "Element with light and dark styles",
471− "@component ${1:name} \\$${2:param}\n @style\n .inner { ${3:padding: 16px;} }\n @el [class inner]\n @children",
193+ "@el [background ${1:white}, dark:background ${2:#1a1a2e}, color ${3:#333}, dark:color ${4:#eee}]\n ${5:Content}",
472 ),
194 ),
473 ];
195 ];
474
196
@@ -617,7 +339,7 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
617 "list",
339 "list",
618 "accept",
340 "accept",
619 ],
341 ],
620− "button" | "btn" => &[
342+ "button" => &[
621 "type",
343 "type",
622 "disabled",
344 "disabled",
623 "name",
345 "name",
@@ -641,7 +363,7 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
641 "autofocus",
363 "autofocus",
642 "spellcheck",
364 "spellcheck",
643 ],
365 ],
644− "option" | "opt" => &["value", "selected", "disabled", "label"],
366+ "option" => &["value", "selected", "disabled", "label"],
645 "form" => &[
367 "form" => &[
646 "action",
368 "action",
647 "method",
369 "method",
@@ -651,7 +373,7 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
651 "enctype",
373 "enctype",
652 "name",
374 "name",
653 ],
375 ],
654− "image" | "img" => &[
376+ "image" => &[
655 "src",
377 "src",
656 "alt",
378 "alt",
657 "width",
379 "width",
@@ -707,19 +429,14 @@ fn element_specific_attrs(element: &str) -> &'static [&'static str] {
707fn attr_value_completions(before: &str, range: Range) -> Option<Vec<CompletionItem>> {
429fn attr_value_completions(before: &str, range: Range) -> Option<Vec<CompletionItem>> {
708 let bracket_content = before.rsplit('[').next()?;
430 let bracket_content = before.rsplit('[').next()?;
709 let segment = bracket_content.rsplit(',').next()?.trim_start();
431 let segment = bracket_content.rsplit(',').next()?.trim_start();
710− let attr_end = segment
432+ // A style (`cursor `) or an HTML attribute (`type=`) with no value typed yet.
711− .find(|c: char| c.is_whitespace())
433+ let attr = segment
712− .unwrap_or(segment.len());
434+ .strip_suffix('=')
713− let attr = &segment[..attr_end];
435+ .filter(|a| !a.contains(char::is_whitespace))
714− let after_attr = &segment[attr_end..];
436+ .or_else(|| {
715− if !after_attr.starts_with(' ') {
437+ let (attr, rest) = segment.split_once(' ')?;
716− return None;
438+ rest.trim().is_empty().then_some(attr)
717− }
439+ })?;
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− }
723
440
724 // Strip state prefix to find the base attribute.
441 // Strip state prefix to find the base attribute.
725 let base_attr = if let Some(pos) = attr.rfind(':') {
442 let base_attr = if let Some(pos) = attr.rfind(':') {
@@ -903,644 +620,75 @@ fn attr_value_completions(before: &str, range: Range) -> Option<Vec<CompletionIt
903 )
620 )
904}
621}
905
622
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.
906fn attr_completions(range: Range, element: Option<&str>) -> Vec<CompletionItem> {
627fn attr_completions(range: Range, element: Option<&str>) -> Vec<CompletionItem> {
907− [
628+ let boosted = element.map_or(&[][..], element_specific_attrs);
908− // Layout
629+ let mut items = Vec::new();
909− ("spacing", "Gap between children (supports CSS units)", true),
630+ let mut push = |label: String, insert: String, detail: &str, rank: &str, name: &str| {
910− ("gap", "Gap between children (alias for spacing)", true),
631+ let mut completion = item(&label, CompletionItemKind::PROPERTY, detail, &insert, range);
911− (
632+ let rank = if boosted.contains(&name) { "0" } else { rank };
912− "padding",
633+ completion.sort_text = Some(format!("{}_{}", rank, label));
913− "Inner padding (1/2/3/4 values, supports CSS units)",
634+ items.push(completion);
914− true,
635+ };
915− ),
636+ for name in vocab::HTMLANG_ATTRIBUTES {
916− ("padding-x", "Horizontal padding", true),
637+ let doc = docs::attribute(name);
917− ("padding-y", "Vertical padding", true),
638+ let insert = if doc.is_none_or(|d| d.takes_value()) {
918− // Sizing
639+ format!("{} ", name)
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)
1488 } else {
640 } else {
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);
1489− format!("5_{}", name)
648+ push(name.to_string(), format!("{} ", name), detail, "5", name);
1490− });
649+ }
1491− completion
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+ }
1492− })
659+ }
1493− .collect()
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
1494}
671}
1495
672
673+/// Styles that can follow a state/media prefix: `hover:color`, `md:padding`.
1496fn state_attr_completions(prefix: &str, range: Range) -> Vec<CompletionItem> {
674fn state_attr_completions(prefix: &str, range: Range) -> Vec<CompletionItem> {
1497− [
675+ let htmlang = vocab::HTMLANG_ATTRIBUTES
1498− ("background", "Background color/value", true),
676+ .iter()
1499− ("color", "Text color", true),
677+ .map(|name| (*name, docs::attribute(name).is_none_or(|d| d.takes_value())));
1500− ("border", "Border (width [color])", true),
678+ let css = vocab::CSS_PROPERTIES.iter().map(|name| (*name, true));
1501− ("border-top", "Top border (width [color])", true),
679+ htmlang
1502− ("border-bottom", "Bottom border (width [color])", true),
680+ .chain(css)
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)| {
681+ .map(|(name, takes_value)| {
1535− let full = format!("{}:{}", prefix, name);
682+ let full = format!("{}{}", prefix, name);
1536− let insert = if *takes_value {
683+ let insert = if takes_value {
1537− format!("{} ", full)
684+ format!("{} ", full)
1538− } else {
685+ } else {
1539− full.clone()
686+ full.clone()
1540− };
687+ };
688+ let detail = docs::attribute(name).map_or("CSS property", |d| d.summary);
1541− item(&full, CompletionItemKind::PROPERTY, detail, &insert, range)
689+ item(&full, CompletionItemKind::PROPERTY, detail, &insert, range)
1542− })
690+ })
1543− .collect()
691+ .collect()
1544}
692}
1545
693
1546fn color_value_completions(before: &str, range: Range) -> Option<Vec<CompletionItem>> {
694fn 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
1649fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
797fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
1650 let mut items = Vec::new();
798 let mut items = Vec::new();
1651
799
1652− for (name, value) in collect_theme_tokens(text) {
800+ for doc in docs::BUNDLES {
1653− items.push(item(
801+ let label = format!("${}", doc.name);
1654− &format!("${}", name),
802+ let detail = format!("{} (standard-library bundle)", doc.summary);
1655− CompletionItemKind::COLOR,
803+ items.push(item(&label, CompletionItemKind::CONSTANT, &detail, &label, range));
1656− &format!("@theme \u{2014} {}", value),
1657− &format!("${}", name),
1658− range,
1659− ));
1660 }
804 }
1661
805
1662 for line in text.lines() {
806 for line in text.lines() {
@@ -1797,15 +941,51 @@ mod tests {
1797 }
941 }
1798
942
1799 #[test]
943 #[test]
1800− fn theme_tokens_extracted_from_block() {
944+ fn element_and_directive_completions_come_from_the_compiler() {
1801− let text = "@theme\n primary #3b82f6\n spacing-md 16\n@row\n";
945+ let range = Range::default();
1802− let tokens = collect_theme_tokens(text);
946+ let elements = element_completions(range);
1803− assert_eq!(
947+ for name in ElementKind::all_names() {
1804− tokens,
948+ let label = format!("@{}", name);
1805− vec![
949+ assert!(elements.iter().any(|i| i.label == label), "missing {}", label);
1806− ("primary".into(), "#3b82f6".into()),
950+ }
1807− ("spacing-md".into(), "16".into()),
951+ assert!(elements.iter().any(|i| i.label == "@badge"));
1808− ]
952+ let directives = directive_completions(range);
1809− );
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"));
1810 }
990 }
1811}
991}
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 @@
1use tower_lsp::lsp_types::*;
1use tower_lsp::lsp_types::*;
2
2
3−use crate::completion::collect_theme_tokens;
3+use crate::docs;
4
4
5pub(crate) fn hover_at(text: &str, position: Position) -> Option<Hover> {
5pub(crate) fn hover_at(text: &str, position: Position) -> Option<Hover> {
6 let lines: Vec<&str> = text.lines().collect();
6 let lines: Vec<&str> = text.lines().collect();
@@ -9,11 +9,11 @@ pub(crate) fn hover_at(text: &str, position: Position) -> Option<Hover> {
9 let word = word_at(line, col)?;
9 let word = word_at(line, col)?;
10
10
11 let doc = if let Some(var_name) = word.strip_prefix('$') {
11 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))
13 } else if let Some(fn_name) = word.strip_prefix('@') {
13 } 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))
15 } else {
15 } else {
16− hover_builtin(&word)
16+ docs::hover(&word)
17 }?;
17 }?;
18
18
19 Some(Hover {
19 Some(Hover {
@@ -45,15 +45,6 @@ pub(crate) fn is_word_byte(c: u8) -> bool {
45 c.is_ascii_alphanumeric() || c == b'@' || c == b'$' || c == b'-' || c == b'_' || c == b':'
45 c.is_ascii_alphanumeric() || c == b'@' || c == b'$' || c == b'-' || c == b'_' || c == b':'
46}
46}
47
47
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−
57fn hover_variable(text: &str, name: &str) -> Option<String> {
48fn hover_variable(text: &str, name: &str) -> Option<String> {
58 for line in text.lines() {
49 for line in text.lines() {
59 let trimmed = line.trim();
50 let trimmed = line.trim();
@@ -156,836 +147,3 @@ fn hover_user_fn(text: &str, name: &str) -> Option<String> {
156 }
147 }
157 None
148 None
158}
149}
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};
12
12
13mod analysis;
13mod analysis;
14mod completion;
14mod completion;
15+mod docs;
15mod hover;
16mod hover;
16mod navigation;
17mod navigation;
17mod state;
18mod state;