@@ -14,14 +14,10 @@ struct DiagnosticJson {
1414}
1515
1616fn 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)
1818}
1919
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)
22−}
23−
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>) {
2521 let input = match fs::read_to_string(input_path) {
2622 Ok(s) => s,
2723 Err(e) => {
@@ -90,12 +86,13 @@ fn compile_inner(input_path: &str, dev: bool, error_overlay: bool, check_only: b
9086 let _ = fs::write(&out_path, &error_html);
9187 }
9288 } else {
93− let html = if minify {
94− htmlang::codegen::generate_minified(&result.document)
95− } else if dev {
96− htmlang::codegen::generate_dev(&result.document)
97− } else {
98− htmlang::codegen::generate(&result.document)
89+ let html = match (minify, dev, compat) {
90+ (true, _, true) => htmlang::codegen::generate_minified_compat(&result.document),
91+ (true, _, false) => htmlang::codegen::generate_minified(&result.document),
92+ (false, true, true) => htmlang::codegen::generate_dev_compat(&result.document),
93+ (false, true, false) => htmlang::codegen::generate_dev(&result.document),
94+ (false, false, true) => htmlang::codegen::generate_compat(&result.document),
95+ (false, false, false) => htmlang::codegen::generate(&result.document),
9996 };
10097 match fs::write(&out_path, &html) {
10198 Ok(()) => eprintln!("wrote {}", out_path.display()),
@@ -124,6 +121,11 @@ fn print_json_diagnostics(diagnostics: &[DiagnosticJson]) {
124121 println!("{}", json);
125122}
126123
124+fn json_escape_string(s: &str) -> String {
125+ let escaped = s.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
126+ format!("\"{}\"", escaped)
127+}
128+
127129fn kebab_to_title(s: &str) -> String {
128130 s.split('-')
129131 .map(|word| {
@@ -247,6 +249,7 @@ Options:
247249 -o, --output <path> Output file/directory path
248250 -d, --dev Development mode
249251 -c, --check Check for errors without writing output
252+ --compat Add vendor prefixes for broader browser support
250253 --format json Output diagnostics as JSON to stdout
251254 -h, --help Show this help
252255 -V, --version Show version
@@ -677,6 +680,7 @@ fn main() {
677680 let mut dev = false;
678681 let mut check = false;
679682 let mut format_json = false;
683+ let mut compat = false;
680684 let mut open_browser = false;
681685 let mut port: u16 = 3000;
682686 let mut output_path: Option<String> = None;
@@ -716,6 +720,7 @@ fn main() {
716720 let mut src_dir = None;
717721 let mut out_dir = None;
718722 let mut build_minify = false;
723+ let mut build_compat = false;
719724 let mut i = 2;
720725 while i < args.len() {
721726 match args[i].as_str() {
@@ -724,6 +729,7 @@ fn main() {
724729 out_dir = args.get(i).map(|s| s.as_str());
725730 }
726731 "--minify" => build_minify = true,
732+ "--compat" => build_compat = true,
727733 _ if src_dir.is_none() => src_dir = Some(args[i].as_str()),
728734 _ => {
729735 eprintln!("unknown argument: {}", args[i]);
@@ -782,8 +788,8 @@ fn main() {
782788 }
783789 }
784790 let path_str = file.to_string_lossy().to_string();
785− let (has_errors, _) = if build_minify {
786− compile_minified(&path_str, effective_out.as_deref())
791+ let (has_errors, _) = if build_minify || build_compat {
792+ compile_inner(&path_str, false, false, false, effective_out.as_deref(), false, None, build_minify, build_compat)
787793 } else {
788794 compile(&path_str, false, false, false, effective_out.as_deref(), false, None)
789795 };
@@ -832,13 +838,28 @@ fn main() {
832838
833839 // Handle "lint" subcommand
834840 if args.len() >= 2 && args[1] == "lint" {
835− let target = if args.len() >= 3 { &args[2] } else { "." };
836− let path = Path::new(target);
841+ let mut lint_target = ".";
842+ let mut lint_json = false;
843+ let mut i = 2;
844+ while i < args.len() {
845+ match args[i].as_str() {
846+ "--format" => {
847+ i += 1;
848+ if args.get(i).map(|s| s.as_str()) == Some("json") {
849+ lint_json = true;
850+ }
851+ }
852+ _ if lint_target == "." => lint_target = &args[i],
853+ _ => {}
854+ }
855+ i += 1;
856+ }
857+ let path = Path::new(lint_target);
837858 let mut all_warnings = Vec::new();
838859 if path.is_dir() {
839860 let hl_files = collect_hl_files_recursive(path);
840861 if hl_files.is_empty() {
841− eprintln!("no .hl files found in {}", target);
862+ eprintln!("no .hl files found in {}", lint_target);
842863 process::exit(1);
843864 }
844865 for file in &hl_files {
@@ -846,9 +867,20 @@ fn main() {
846867 all_warnings.extend(lint_file(&path_str));
847868 }
848869 } else {
849− all_warnings.extend(lint_file(target));
850− }
851− if all_warnings.is_empty() {
870+ all_warnings.extend(lint_file(lint_target));
871+ }
872+ if lint_json {
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)
879+ ));
880+ }
881+ json.push_str("]}");
882+ println!("{}", json);
883+ } else if all_warnings.is_empty() {
852884 eprintln!("no issues found");
853885 } else {
854886 for w in &all_warnings {
@@ -1438,7 +1470,23 @@ fn main() {
14381470
14391471 // Handle "dead-code" subcommand — project-wide unused definitions
14401472 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();
1475+ let mut i = 2;
1476+ while i < args.len() {
1477+ match args[i].as_str() {
1478+ "--format" => {
1479+ i += 1;
1480+ if args.get(i).map(|s| s.as_str()) == Some("json") {
1481+ dc_json = true;
1482+ }
1483+ }
1484+ _ if dc_target == "." => dc_target = args[i].clone(),
1485+ _ => {}
1486+ }
1487+ i += 1;
1488+ }
1489+ let target = dc_target.as_str();
14421490 let path = Path::new(target);
14431491 let hl_files = if path.is_dir() {
14441492 collect_hl_files_recursive(path)
@@ -1506,29 +1554,43 @@ fn main() {
15061554 }
15071555 }
15081556 // Pass 2: report unused
1509− let mut count = 0;
1557+ let mut unused: Vec<(String, String, String, usize)> = Vec::new(); // (kind, name, file, line)
15101558 for (name, file, line) in &all_fn_defs {
15111559 if !all_refs.contains(name) {
1512− println!("unused @fn '{}' — {}:{}", name, file, line);
1513− count += 1;
1560+ unused.push(("@fn".to_string(), name.clone(), file.clone(), *line));
15141561 }
15151562 }
15161563 for (name, file, line) in &all_def_defs {
15171564 if !all_refs.contains(name) {
1518− println!("unused @define '{}' — {}:{}", name, file, line);
1519− count += 1;
1565+ unused.push(("@define".to_string(), name.clone(), file.clone(), *line));
15201566 }
15211567 }
15221568 for (name, file, line) in &all_let_defs {
15231569 if !all_refs.contains(name) && !name.starts_with("--") {
1524− println!("unused @let '{}' — {}:{}", name, file, line);
1525− count += 1;
1570+ unused.push(("@let".to_string(), name.clone(), file.clone(), *line));
1571+ }
1572+ }
1573+ if dc_json {
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),
1582+ line
1583+ ));
15261584 }
1527− }
1528− if count == 0 {
1585+ json.push_str("]}");
1586+ println!("{}", json);
1587+ } else if unused.is_empty() {
15291588 eprintln!("no unused definitions found");
15301589 } else {
1531− eprintln!("\n{} unused definition(s) found", count);
1590+ for (kind, name, file, line) in &unused {
1591+ println!("unused {} '{}' — {}:{}", kind, name, file, line);
1592+ }
1593+ eprintln!("\n{} unused definition(s) found", unused.len());
15321594 }
15331595 return;
15341596 }
@@ -2364,6 +2426,7 @@ compile();
23642426 "--watch" | "-w" => watch = true,
23652427 "--dev" | "-d" => dev = true,
23662428 "--check" | "-c" => check = true,
2429+ "--compat" => compat = true,
23672430 "--open" => open_browser = true,
23682431 "--format" => {
23692432 i += 1;
@@ -2454,7 +2517,7 @@ compile();
24542517 }
24552518 out_p.to_string_lossy().to_string()
24562519 });
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);
24582521 if has_errors {
24592522 any_errors = true;
24602523 }
@@ -2497,7 +2560,7 @@ compile();
24972560
24982561 // --- Single file mode ---
24992562 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);
25012564 if format_json {
25022565 if let Some(ref collector) = json_collector_single {
25032566 print_json_diagnostics(&collector.lock().unwrap());