namespace Spry.Tools { public class Mkcomponent : Object { private static bool show_version = false; private static string? output_file = null; private static string? html_file = null; private const OptionEntry[] options = { { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output file path (alternative to positional OUTPUT_FILE)", "FILE" }, { "html", 'H', 0, OptionArg.FILENAME, ref html_file, "HTML template file (default: .html)", "FILE" }, { "version", 'v', 0, OptionArg.NONE, ref show_version, "Show version information", null }, { null } }; public static int main(string[] args) { try { var opt_context = new OptionContext(" [OUTPUT_FILE] - Inject HTML markup into Vala component files"); opt_context.set_help_enabled(true); opt_context.add_main_entries(options, null); opt_context.set_description( "Arguments:\n" + " INPUT_VALA_FILE Input Vala component file\n" + " OUTPUT_FILE Output Vala file (optional, default: input filename in build directory)\n" + "\n" + "The output file can be specified either as a positional argument or via -o/--output." ); opt_context.parse(ref args); } catch (OptionError e) { stderr.printf("Error: %s\n", e.message); stderr.printf("Run '%s --help' for more information.\n", args[0]); return 1; } if (show_version) { stdout.printf("spry-mkcomponent 0.1\n"); return 0; } // Get input file and optional output file from remaining arguments string? input_file = null; string? positional_output = null; if (args.length > 1) { input_file = args[1]; } if (args.length > 2) { positional_output = args[2]; } if (input_file == null) { stderr.printf("Error: No input file specified.\n"); stderr.printf("Run '%s --help' for more information.\n", args[0]); return 1; } // Determine output file: -o flag takes precedence over positional argument string actual_output; if (output_file != null) { actual_output = output_file; } else if (positional_output != null) { actual_output = positional_output; } else { // Default: use the input filename (will be written to build directory by meson) actual_output = input_file; } // Determine HTML file string actual_html; if (html_file != null) { actual_html = html_file; } else { // Default: append .html to the input vala file path actual_html = input_file + ".html"; } try { var tool = new Mkcomponent(); tool.process(input_file, actual_output, actual_html); return 0; } catch (Error e) { stderr.printf("%s\n", e.message); return 1; } } public void process(string input_path, string output_path, string html_path) throws Error { // Check if input file exists var input_file = File.new_for_path(input_path); if (!input_file.query_exists()) { throw new IOError.NOT_FOUND(@"Error: Input file '$(input_path)' does not exist"); } // Check if HTML file exists var html_file_obj = File.new_for_path(html_path); if (!html_file_obj.query_exists()) { throw new IOError.NOT_FOUND(@"Error: HTML template file '$(html_path)' does not exist"); } // Read input Vala file string vala_content = read_file_content(input_path); // Read HTML template string html_content = read_file_content(html_path); // Normalize line endings to Unix style vala_content = vala_content.replace("\r\n", "\n").replace("\r", "\n"); html_content = html_content.replace("\r\n", "\n").replace("\r", "\n"); // First check if there's a markup property with a getter body (to give specific error) Regex getter_with_body_regex; try { // This pattern matches a markup property with a getter that has a body getter_with_body_regex = new Regex( @"public\\s+override\\s+string\\s+markup\\s*\\{\\s*get\\s*\\{", RegexCompileFlags.MULTILINE ); } catch (RegexError e) { throw new IOError.FAILED(@"Internal error: Failed to compile regex: $(e.message)"); } if (getter_with_body_regex.match(vala_content)) { throw new IOError.FAILED(@"Error: The markup property in '$(input_path)' already has a getter defined. Expected empty getter: { get; }"); } // Now check for the empty getter pattern MatchInfo match_info; Regex markup_property_regex; try { // Pattern to match: public override string markup { get; } // Allow for various whitespace patterns markup_property_regex = new Regex( @"public\\s+override\\s+string\\s+markup\\s*\\{\\s*get\\s*;\\s*\\}", RegexCompileFlags.MULTILINE ); } catch (RegexError e) { throw new IOError.FAILED(@"Internal error: Failed to compile regex: $(e.message)"); } if (!markup_property_regex.match(vala_content, 0, out match_info)) { throw new IOError.NOT_FOUND(@"Error: No 'public override string markup' property found in '$(input_path)'"); } // Escape any triple quotes in HTML content by replacing """ with \"\"\" string escaped_html = html_content.replace("\"\"\"", "\\\"\\\"\\\""); // Determine the indentation to use for the HTML content // Find the line containing the markup property and extract its indentation string indentation = extract_indentation(vala_content); // Generate the replacement - we replace just the "{ get; }" part // Regex to match just the getter part: { get; } Regex getter_regex; try { getter_regex = new Regex( @"\\{\\s*get\\s*;\\s*\\}", 0 ); } catch (RegexError e) { throw new IOError.FAILED(@"Internal error: Failed to compile getter regex: $(e.message)"); } // Build the replacement getter - HTML content placed verbatim between triple quotes string replacement = @"{ get {\n$(indentation)$(indentation)return \"\"\"$(escaped_html)\"\"\";\n$(indentation)}}"; // Perform the replacement string output_content; try { output_content = getter_regex.replace(vala_content, -1, 0, replacement); } catch (RegexError e) { throw new IOError.FAILED(@"Internal error: Failed to replace markup property: $(e.message)"); } // Write output file write_file_content(output_path, output_content); } private string read_file_content(string path) throws Error { var file = File.new_for_path(path); var input_stream = new DataInputStream(file.read()); var builder = new StringBuilder(); string line; while ((line = input_stream.read_line(null)) != null) { builder.append(line); builder.append("\n"); } input_stream.close(); return builder.str; } private void write_file_content(string path, string content) throws Error { var file = File.new_for_path(path); // Ensure parent directory exists var parent = file.get_parent(); if (parent != null && !parent.query_exists()) { parent.make_directory_with_parents(); } var output_stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE)); output_stream.put_string(content); output_stream.close(); } private string extract_indentation(string content) { // Find the line containing "public override string markup" and extract its leading whitespace Regex indent_regex; try { indent_regex = new Regex( @"^([ \\t]*)public\\s+override\\s+string\\s+markup", RegexCompileFlags.MULTILINE ); } catch (RegexError e) { return " "; // Default to 4 spaces } MatchInfo match; if (indent_regex.match(content, 0, out match)) { return match.fetch(1) ?? " "; } return " "; // Default to 4 spaces } } }