@@ -14,14 +14,10 @@ struct DiagnosticJson {
16fn compile(input_path: &str, dev: bool, error_overlay: bool, check_only: bool, output_path: Option<&str>, format_json: bool, json_collector: Option<&Mutex<Vec<DiagnosticJson>>>) -> (bool, Vec<PathBuf>) {
16fn compile(input_path: &str, dev: bool, error_overlay: bool, check_only: bool, output_path: Option<&str>, format_json: bool, json_collector: Option<&Mutex<Vec<DiagnosticJson>>>) -> (bool, Vec<PathBuf>) {
17− compile_inner(input_path, dev, error_overlay, check_only, output_path, format_json, json_collector, false)
17+ compile_inner(input_path, dev, error_overlay, check_only, output_path, format_json, json_collector, false, false)
20−fn compile_minified(input_path: &str, output_path: Option<&str>) -> (bool, Vec<PathBuf>) {
21− compile_inner(input_path, false, false, false, output_path, false, None, true)
24−fn compile_inner(input_path: &str, dev: bool, error_overlay: bool, check_only: bool, output_path: Option<&str>, format_json: bool, json_collector: Option<&Mutex<Vec<DiagnosticJson>>>, minify: bool) -> (bool, Vec<PathBuf>) {
20+fn compile_inner(input_path: &str, dev: bool, error_overlay: bool, check_only: bool, output_path: Option<&str>, format_json: bool, json_collector: Option<&Mutex<Vec<DiagnosticJson>>>, minify: bool, compat: bool) -> (bool, Vec<PathBuf>) {
25 let input = match fs::read_to_string(input_path) {
21 let input = match fs::read_to_string(input_path) {
26 Ok(s) => s,
22 Ok(s) => s,
27 Err(e) => {
23 Err(e) => {
@@ -90,12 +86,13 @@ fn compile_inner(input_path: &str, dev: bool, error_overlay: bool, check_only: b
90 let _ = fs::write(&out_path, &error_html);
86 let _ = fs::write(&out_path, &error_html);
93− let html = if minify {
89+ let html = match (minify, dev, compat) {
94− htmlang::codegen::generate_minified(&result.document)
90+ (true, _, true) => htmlang::codegen::generate_minified_compat(&result.document),
95− } else if dev {
91+ (true, _, false) => htmlang::codegen::generate_minified(&result.document),
92+ (false, true, true) => htmlang::codegen::generate_dev_compat(&result.document),
96− htmlang::codegen::generate_dev(&result.document)
93+ (false, true, false) => htmlang::codegen::generate_dev(&result.document),
97− } else {
94+ (false, false, true) => htmlang::codegen::generate_compat(&result.document),
98− htmlang::codegen::generate(&result.document)
95+ (false, false, false) => htmlang::codegen::generate(&result.document),
100 match fs::write(&out_path, &html) {
97 match fs::write(&out_path, &html) {
101 Ok(()) => eprintln!("wrote {}", out_path.display()),
98 Ok(()) => eprintln!("wrote {}", out_path.display()),
@@ -124,6 +121,11 @@ fn print_json_diagnostics(diagnostics: &[DiagnosticJson]) {
124 println!("{}", json);
121 println!("{}", json);
124+fn json_escape_string(s: &str) -> String {
125+ let escaped = s.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
126+ format!("\"{}\"", escaped)
127fn kebab_to_title(s: &str) -> String {
129fn kebab_to_title(s: &str) -> String {
128 s.split('-')
130 s.split('-')
129 .map(|word| {
131 .map(|word| {
@@ -247,6 +249,7 @@ Options:
247 -o, --output <path> Output file/directory path
249 -o, --output <path> Output file/directory path
248 -d, --dev Development mode
250 -d, --dev Development mode
249 -c, --check Check for errors without writing output
251 -c, --check Check for errors without writing output
252+ --compat Add vendor prefixes for broader browser support
250 --format json Output diagnostics as JSON to stdout
253 --format json Output diagnostics as JSON to stdout
251 -h, --help Show this help
254 -h, --help Show this help
252 -V, --version Show version
255 -V, --version Show version
@@ -677,6 +680,7 @@ fn main() {
677 let mut dev = false;
680 let mut dev = false;
678 let mut check = false;
681 let mut check = false;
679 let mut format_json = false;
682 let mut format_json = false;
683+ let mut compat = false;
680 let mut open_browser = false;
684 let mut open_browser = false;
681 let mut port: u16 = 3000;
685 let mut port: u16 = 3000;
682 let mut output_path: Option<String> = None;
686 let mut output_path: Option<String> = None;
@@ -716,6 +720,7 @@ fn main() {
716 let mut src_dir = None;
720 let mut src_dir = None;
717 let mut out_dir = None;
721 let mut out_dir = None;
718 let mut build_minify = false;
722 let mut build_minify = false;
723+ let mut build_compat = false;
719 let mut i = 2;
724 let mut i = 2;
720 while i < args.len() {
725 while i < args.len() {
721 match args[i].as_str() {
726 match args[i].as_str() {
@@ -724,6 +729,7 @@ fn main() {
724 out_dir = args.get(i).map(|s| s.as_str());
729 out_dir = args.get(i).map(|s| s.as_str());
726 "--minify" => build_minify = true,
731 "--minify" => build_minify = true,
732+ "--compat" => build_compat = true,
727 _ if src_dir.is_none() => src_dir = Some(args[i].as_str()),
733 _ if src_dir.is_none() => src_dir = Some(args[i].as_str()),
729 eprintln!("unknown argument: {}", args[i]);
735 eprintln!("unknown argument: {}", args[i]);
@@ -782,8 +788,8 @@ fn main() {
784 let path_str = file.to_string_lossy().to_string();
790 let path_str = file.to_string_lossy().to_string();
785− let (has_errors, _) = if build_minify {
791+ let (has_errors, _) = if build_minify || build_compat {
786− compile_minified(&path_str, effective_out.as_deref())
792+ compile_inner(&path_str, false, false, false, effective_out.as_deref(), false, None, build_minify, build_compat)
788 compile(&path_str, false, false, false, effective_out.as_deref(), false, None)
794 compile(&path_str, false, false, false, effective_out.as_deref(), false, None)
@@ -832,13 +838,28 @@ fn main() {
833 // Handle "lint" subcommand
839 // Handle "lint" subcommand
834 if args.len() >= 2 && args[1] == "lint" {
840 if args.len() >= 2 && args[1] == "lint" {
835− let target = if args.len() >= 3 { &args[2] } else { "." };
841+ let mut lint_target = ".";
842+ let mut lint_json = false;
844+ while i < args.len() {
845+ match args[i].as_str() {
848+ if args.get(i).map(|s| s.as_str()) == Some("json") {
852+ _ if lint_target == "." => lint_target = &args[i],
836− let path = Path::new(target);
857+ let path = Path::new(lint_target);
837 let mut all_warnings = Vec::new();
858 let mut all_warnings = Vec::new();
838 if path.is_dir() {
859 if path.is_dir() {
839 let hl_files = collect_hl_files_recursive(path);
860 let hl_files = collect_hl_files_recursive(path);
840 if hl_files.is_empty() {
861 if hl_files.is_empty() {
841− eprintln!("no .hl files found in {}", target);
862+ eprintln!("no .hl files found in {}", lint_target);
842 process::exit(1);
863 process::exit(1);
844 for file in &hl_files {
865 for file in &hl_files {
@@ -846,9 +867,20 @@ fn main() {
846 all_warnings.extend(lint_file(&path_str));
867 all_warnings.extend(lint_file(&path_str));
849− all_warnings.extend(lint_file(target));
870+ all_warnings.extend(lint_file(lint_target));
873+ let mut json = String::from("{\"diagnostics\":[");
874+ for (i, w) in all_warnings.iter().enumerate() {
875+ if i > 0 { json.push(','); }
876+ json.push_str(&format!(
877+ "{{\"severity\":\"warning\",\"message\":{}}}",
878+ json_escape_string(w)
881+ json.push_str("]}");
882+ println!("{}", json);
851− if all_warnings.is_empty() {
883+ } else if all_warnings.is_empty() {
852 eprintln!("no issues found");
884 eprintln!("no issues found");
854 for w in &all_warnings {
886 for w in &all_warnings {
@@ -1438,7 +1470,23 @@ fn main() {
1439 // Handle "dead-code" subcommand — project-wide unused definitions
1471 // Handle "dead-code" subcommand — project-wide unused definitions
1440 if args.len() >= 2 && args[1] == "dead-code" {
1472 if args.len() >= 2 && args[1] == "dead-code" {
1441− let target = if args.len() >= 3 { &args[2] } else { "." };
1473+ let mut dc_json = false;
1474+ let mut dc_target = ".".to_string();
1476+ while i < args.len() {
1477+ match args[i].as_str() {
1480+ if args.get(i).map(|s| s.as_str()) == Some("json") {
1484+ _ if dc_target == "." => dc_target = args[i].clone(),
1489+ let target = dc_target.as_str();
1442 let path = Path::new(target);
1490 let path = Path::new(target);
1443 let hl_files = if path.is_dir() {
1491 let hl_files = if path.is_dir() {
1444 collect_hl_files_recursive(path)
1492 collect_hl_files_recursive(path)
@@ -1506,29 +1554,43 @@ fn main() {
1508 // Pass 2: report unused
1556 // Pass 2: report unused
1509− let mut count = 0;
1557+ let mut unused: Vec<(String, String, String, usize)> = Vec::new(); // (kind, name, file, line)
1510 for (name, file, line) in &all_fn_defs {
1558 for (name, file, line) in &all_fn_defs {
1511 if !all_refs.contains(name) {
1559 if !all_refs.contains(name) {
1512− println!("unused @fn '{}' — {}:{}", name, file, line);
1560+ unused.push(("@fn".to_string(), name.clone(), file.clone(), *line));
1516 for (name, file, line) in &all_def_defs {
1563 for (name, file, line) in &all_def_defs {
1517 if !all_refs.contains(name) {
1564 if !all_refs.contains(name) {
1518− println!("unused @define '{}' — {}:{}", name, file, line);
1565+ unused.push(("@define".to_string(), name.clone(), file.clone(), *line));
1522 for (name, file, line) in &all_let_defs {
1568 for (name, file, line) in &all_let_defs {
1523 if !all_refs.contains(name) && !name.starts_with("--") {
1569 if !all_refs.contains(name) && !name.starts_with("--") {
1524− println!("unused @let '{}' — {}:{}", name, file, line);
1570+ unused.push(("@let".to_string(), name.clone(), file.clone(), *line));
1574+ let mut json = String::from("{\"unused\":[");
1575+ for (i, (kind, name, file, line)) in unused.iter().enumerate() {
1576+ if i > 0 { json.push(','); }
1577+ json.push_str(&format!(
1578+ "{{\"kind\":{},\"name\":{},\"file\":{},\"line\":{}}}",
1579+ json_escape_string(kind),
1580+ json_escape_string(name),
1581+ json_escape_string(file),
1527− }
1585+ json.push_str("]}");
1528− if count == 0 {
1586+ println!("{}", json);
1587+ } else if unused.is_empty() {
1529 eprintln!("no unused definitions found");
1588 eprintln!("no unused definitions found");
1530 } else {
1589 } else {
1590+ for (kind, name, file, line) in &unused {
1591+ println!("unused {} '{}' — {}:{}", kind, name, file, line);
1531− eprintln!("\n{} unused definition(s) found", count);
1593+ eprintln!("\n{} unused definition(s) found", unused.len());
@@ -2364,6 +2426,7 @@ compile();
2364 "--watch" | "-w" => watch = true,
2426 "--watch" | "-w" => watch = true,
2365 "--dev" | "-d" => dev = true,
2427 "--dev" | "-d" => dev = true,
2366 "--check" | "-c" => check = true,
2428 "--check" | "-c" => check = true,
2429+ "--compat" => compat = true,
2367 "--open" => open_browser = true,
2430 "--open" => open_browser = true,
2368 "--format" => {
2431 "--format" => {
@@ -2454,7 +2517,7 @@ compile();
2455 out_p.to_string_lossy().to_string()
2518 out_p.to_string_lossy().to_string()
2457− let (has_errors, included) = compile(&path_str, dev, serve, check, effective_out.as_deref(), format_json, json_collector.as_ref());
2520+ let (has_errors, included) = compile_inner(&path_str, dev, serve, check, effective_out.as_deref(), format_json, json_collector.as_ref(), false, compat);
2458 if has_errors {
2521 if has_errors {
2459 any_errors = true;
2522 any_errors = true;
@@ -2497,7 +2560,7 @@ compile();
2498 // --- Single file mode ---
2561 // --- Single file mode ---
2499 let json_collector_single = if format_json { Some(Mutex::new(Vec::new())) } else { None };
2562 let json_collector_single = if format_json { Some(Mutex::new(Vec::new())) } else { None };
2500− let (has_errors, included_files) = compile(&input_path, dev, serve, check, output_path.as_deref(), format_json, json_collector_single.as_ref());
2563+ let (has_errors, included_files) = compile_inner(&input_path, dev, serve, check, output_path.as_deref(), format_json, json_collector_single.as_ref(), false, compat);
2501 if format_json {
2564 if format_json {
2502 if let Some(ref collector) = json_collector_single {
2565 if let Some(ref collector) = json_collector_single {
2503 print_json_diagnostics(&collector.lock().unwrap());
2566 print_json_diagnostics(&collector.lock().unwrap());