spry-mkcomponent.vala 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. namespace Spry.Tools {
  2. public class Mkcomponent : Object {
  3. private static bool show_version = false;
  4. private static string? output_file = null;
  5. private static string? html_file = null;
  6. private const OptionEntry[] options = {
  7. { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output file path (alternative to positional OUTPUT_FILE)", "FILE" },
  8. { "html", 'H', 0, OptionArg.FILENAME, ref html_file, "HTML template file (default: <input_vala>.html)", "FILE" },
  9. { "version", 'v', 0, OptionArg.NONE, ref show_version, "Show version information", null },
  10. { null }
  11. };
  12. public static int main(string[] args) {
  13. try {
  14. var opt_context = new OptionContext("<INPUT_VALA_FILE> [OUTPUT_FILE] - Inject HTML markup into Vala component files");
  15. opt_context.set_help_enabled(true);
  16. opt_context.add_main_entries(options, null);
  17. opt_context.set_description(
  18. "Arguments:\n" +
  19. " INPUT_VALA_FILE Input Vala component file\n" +
  20. " OUTPUT_FILE Output Vala file (optional, default: input filename in build directory)\n" +
  21. "\n" +
  22. "The output file can be specified either as a positional argument or via -o/--output."
  23. );
  24. opt_context.parse(ref args);
  25. } catch (OptionError e) {
  26. stderr.printf("Error: %s\n", e.message);
  27. stderr.printf("Run '%s --help' for more information.\n", args[0]);
  28. return 1;
  29. }
  30. if (show_version) {
  31. stdout.printf("spry-mkcomponent 0.1\n");
  32. return 0;
  33. }
  34. // Get input file and optional output file from remaining arguments
  35. string? input_file = null;
  36. string? positional_output = null;
  37. if (args.length > 1) {
  38. input_file = args[1];
  39. }
  40. if (args.length > 2) {
  41. positional_output = args[2];
  42. }
  43. if (input_file == null) {
  44. stderr.printf("Error: No input file specified.\n");
  45. stderr.printf("Run '%s --help' for more information.\n", args[0]);
  46. return 1;
  47. }
  48. // Determine output file: -o flag takes precedence over positional argument
  49. string actual_output;
  50. if (output_file != null) {
  51. actual_output = output_file;
  52. } else if (positional_output != null) {
  53. actual_output = positional_output;
  54. } else {
  55. // Default: use the input filename (will be written to build directory by meson)
  56. actual_output = input_file;
  57. }
  58. // Determine HTML file
  59. string actual_html;
  60. if (html_file != null) {
  61. actual_html = html_file;
  62. } else {
  63. // Default: append .html to the input vala file path
  64. actual_html = input_file + ".html";
  65. }
  66. try {
  67. var tool = new Mkcomponent();
  68. tool.process(input_file, actual_output, actual_html);
  69. return 0;
  70. } catch (Error e) {
  71. stderr.printf("%s\n", e.message);
  72. return 1;
  73. }
  74. }
  75. public void process(string input_path, string output_path, string html_path) throws Error {
  76. // Check if input file exists
  77. var input_file = File.new_for_path(input_path);
  78. if (!input_file.query_exists()) {
  79. throw new IOError.NOT_FOUND(@"Error: Input file '$(input_path)' does not exist");
  80. }
  81. // Check if HTML file exists
  82. var html_file_obj = File.new_for_path(html_path);
  83. if (!html_file_obj.query_exists()) {
  84. throw new IOError.NOT_FOUND(@"Error: HTML template file '$(html_path)' does not exist");
  85. }
  86. // Read input Vala file
  87. string vala_content = read_file_content(input_path);
  88. // Read HTML template
  89. string html_content = read_file_content(html_path);
  90. // Normalize line endings to Unix style
  91. vala_content = vala_content.replace("\r\n", "\n").replace("\r", "\n");
  92. html_content = html_content.replace("\r\n", "\n").replace("\r", "\n");
  93. // First check if there's a markup property with a getter body (to give specific error)
  94. Regex getter_with_body_regex;
  95. try {
  96. // This pattern matches a markup property with a getter that has a body
  97. getter_with_body_regex = new Regex(
  98. @"public\\s+override\\s+string\\s+markup\\s*\\{\\s*get\\s*\\{",
  99. RegexCompileFlags.MULTILINE
  100. );
  101. } catch (RegexError e) {
  102. throw new IOError.FAILED(@"Internal error: Failed to compile regex: $(e.message)");
  103. }
  104. if (getter_with_body_regex.match(vala_content)) {
  105. throw new IOError.FAILED(@"Error: The markup property in '$(input_path)' already has a getter defined. Expected empty getter: { get; }");
  106. }
  107. // Now check for the empty getter pattern
  108. MatchInfo match_info;
  109. Regex markup_property_regex;
  110. try {
  111. // Pattern to match: public override string markup { get; }
  112. // Allow for various whitespace patterns
  113. markup_property_regex = new Regex(
  114. @"public\\s+override\\s+string\\s+markup\\s*\\{\\s*get\\s*;\\s*\\}",
  115. RegexCompileFlags.MULTILINE
  116. );
  117. } catch (RegexError e) {
  118. throw new IOError.FAILED(@"Internal error: Failed to compile regex: $(e.message)");
  119. }
  120. if (!markup_property_regex.match(vala_content, 0, out match_info)) {
  121. throw new IOError.NOT_FOUND(@"Error: No 'public override string markup' property found in '$(input_path)'");
  122. }
  123. // Escape any triple quotes in HTML content by replacing """ with \"\"\"
  124. string escaped_html = html_content.replace("\"\"\"", "\\\"\\\"\\\"");
  125. // Determine the indentation to use for the HTML content
  126. // Find the line containing the markup property and extract its indentation
  127. string indentation = extract_indentation(vala_content);
  128. // Generate the replacement - we replace just the "{ get; }" part
  129. // Regex to match just the getter part: { get; }
  130. Regex getter_regex;
  131. try {
  132. getter_regex = new Regex(
  133. @"\\{\\s*get\\s*;\\s*\\}",
  134. 0
  135. );
  136. } catch (RegexError e) {
  137. throw new IOError.FAILED(@"Internal error: Failed to compile getter regex: $(e.message)");
  138. }
  139. // Build the replacement getter - HTML content placed verbatim between triple quotes
  140. string replacement = @"{ get {\n$(indentation)$(indentation)return \"\"\"$(escaped_html)\"\"\";\n$(indentation)}}";
  141. // Perform the replacement
  142. string output_content;
  143. try {
  144. output_content = getter_regex.replace(vala_content, -1, 0, replacement);
  145. } catch (RegexError e) {
  146. throw new IOError.FAILED(@"Internal error: Failed to replace markup property: $(e.message)");
  147. }
  148. // Write output file
  149. write_file_content(output_path, output_content);
  150. }
  151. private string read_file_content(string path) throws Error {
  152. var file = File.new_for_path(path);
  153. var input_stream = new DataInputStream(file.read());
  154. var builder = new StringBuilder();
  155. string line;
  156. while ((line = input_stream.read_line(null)) != null) {
  157. builder.append(line);
  158. builder.append("\n");
  159. }
  160. input_stream.close();
  161. return builder.str;
  162. }
  163. private void write_file_content(string path, string content) throws Error {
  164. var file = File.new_for_path(path);
  165. // Ensure parent directory exists
  166. var parent = file.get_parent();
  167. if (parent != null && !parent.query_exists()) {
  168. parent.make_directory_with_parents();
  169. }
  170. var output_stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
  171. output_stream.put_string(content);
  172. output_stream.close();
  173. }
  174. private string extract_indentation(string content) {
  175. // Find the line containing "public override string markup" and extract its leading whitespace
  176. Regex indent_regex;
  177. try {
  178. indent_regex = new Regex(
  179. @"^([ \\t]*)public\\s+override\\s+string\\s+markup",
  180. RegexCompileFlags.MULTILINE
  181. );
  182. } catch (RegexError e) {
  183. return " "; // Default to 4 spaces
  184. }
  185. MatchInfo match;
  186. if (indent_regex.match(content, 0, out match)) {
  187. return match.fetch(1) ?? " ";
  188. }
  189. return " "; // Default to 4 spaces
  190. }
  191. }
  192. }