using Xml; using Html; using Astralis; using Invercargill; using Invercargill.DataStructures; using Invercargill.Expressions; namespace Statum.Tools { /** * `statum-mkpstm` — the build-time Statum pre-rendering pipeline. * * Turns an app HTML page (plus its templates/fragments) into a Vala * {@link Statum.StatumPage} subclass carrying precompressed variant byte * arrays and a compiled {@link Statum.StatumPage.select_variant} that * evaluates `pstm-guard`s and the `pstm-if` matrix against the request's held * slots using the Invercargill runtime expression evaluator. * * See `phase-1-plan.md` Subsystem B for the supported `pstm-*` directives. */ public class Mkpstm : Object { private static string? output_file = null; private static string? namespace_name = null; private static string? class_name_override = null; private static string? route_override = null; private static List template_dirs = new List(); private Dictionary templates_by_name = new Dictionary(); private Dictionary fragments_by_name = new Dictionary(); private const OptionEntry[] options = { { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output Vala file name", "FILE" }, { "ns", '\0', 0, OptionArg.STRING, ref namespace_name, "Namespace for the generated class", "NAMESPACE" }, { "name", 'n', 0, OptionArg.STRING, ref class_name_override, "Generated class name (default: Page)", "NAME" }, { "route", 'r', 0, OptionArg.STRING, ref route_override, "Page route (default: from )", "ROUTE" }, { "template-dir", 't', 0, OptionArg.FILENAME_ARRAY, ref template_dirs, "Directory to search for templates/fragments (repeatable)", "DIR" }, { null } }; public static int main(string[] args) { try { var opt = new OptionContext("PAGE_FILE - Generate a Statum StatumPage"); opt.add_main_entries(options, null); opt.parse(ref args); } catch (OptionError e) { stderr.printf("Error: %s\n", e.message); return 1; } string? page_file = null; if (args.length > 1) { page_file = args[1]; } if (page_file == null) { stderr.printf("Error: No page file specified.\n"); return 1; } try { var tool = new Mkpstm(); var generated = tool.run((!)page_file); var out_path = tool.output_file ?? (make_pascal_case((!)page_file) + "Page.vala"); write_file(out_path, generated); stdout.printf("Generated: %s\n", out_path); return 0; } catch (GLib.Error e) { stderr.printf("Error: %s\n", e.message); return 1; } } public string run(string page_file) throws GLib.Error { if (template_dirs.length() == 0) { var parent = File.new_for_path(page_file).get_parent(); template_dirs.append(parent != null ? ((!)parent).get_path() : "."); } load_templates_and_fragments(page_file); var doc = parse_html_file(page_file); // Capture the page's routes from the original document before // composition (structural directives are dropped while // splicing templates, so they would no longer be present afterwards). var routes = read_routes(doc); var body = find_body(doc); if (body == null) { throw new IOError.FAILED("Page has no element"); } doc = compose_templates(doc, (!)body); inline_fragments(doc); rewrite_resources(doc); var collected = collect(doc); var class_name = class_name_override ?? (make_pascal_case(page_file) + "Page"); var route = route_override ?? (routes.length > 0 ? routes[0] : "/"); var variants = build_variants(doc, collected.groups); return emit(class_name, route, collected, variants); } // ------------------------------------------------------------------ // Template/fragment loading // ------------------------------------------------------------------ private void load_templates_and_fragments(string page_file) throws GLib.Error { foreach (var dir in template_dirs) { var dir_file = File.new_for_path(dir); if (!dir_file.query_exists()) { continue; } var enumerator = dir_file.enumerate_children("standard::name", 0); FileInfo info; while ((info = enumerator.next_file()) != null) { var name = info.get_name(); if (!name.has_suffix(".html") || name.has_prefix(".")) { continue; } var path = Path.build_filename(dir, name); if (path == page_file) { continue; } var tdoc = parse_html_file(path); var tname = read_pstm_name(tdoc); if (tname == null) { continue; } if (find_first_element(tdoc->get_root_element(), "pstm-content") != null) { templates_by_name.set((!)tname, tdoc); } else { fragments_by_name.set((!)tname, tdoc); } } } } // ------------------------------------------------------------------ // Composition // ------------------------------------------------------------------ private Xml.Doc* compose_templates(Xml.Doc* doc, Xml.Node* body) throws GLib.Error { Xml.Doc* current_doc = doc; Xml.Node* current_body = body; string merged_title = read_directive_in_doc(current_doc, "pstm-title") ?? ""; // Guard against cyclic template chains (A templates B templates A). var visited = new HashSet(); while (true) { var parent_name = read_directive_in_doc(current_doc, "pstm-template"); if (parent_name == null || parent_name.length == 0) { break; } if (visited.contains((!)parent_name)) { break; } visited.add((!)parent_name); Xml.Doc* parent_doc; if (!templates_by_name.try_get((!)parent_name, out parent_doc)) { throw new IOError.FAILED(@"Unknown template \"$((!)parent_name)\""); } var parent_body = find_body(parent_doc); if (parent_body == null) { throw new IOError.FAILED(@"Template \"$((!)parent_name)\" has no "); } // Splice this body's children into the parent's // outlet, but drop structural directives (pstm-template, // pstm-name, pstm-title, pstm-uri). These are page-level metadata // and must NOT be carried into the parent — otherwise the // spliced would be re-discovered on the next pass // and recompose forever. var outlet = find_first_element(parent_body, "pstm-content"); for (var child = current_body->children; child != null; child = child->next) { if (is_structural_directive(child)) { continue; } var copied = child->doc_copy(parent_doc, 1); if (copied == null) { continue; } if (outlet != null) { outlet->add_prev_sibling(copied); } else { parent_body->add_child(copied); } } if (outlet != null) { outlet->unlink(); } var parent_title = read_directive_in_doc(parent_doc, "pstm-title"); if (parent_title != null && parent_title.length > 0) { merged_title = merged_title + (!)parent_title; } current_doc = parent_doc; current_body = parent_body; } apply_title(current_doc, merged_title); return current_doc; } private static string? read_directive_in_doc(Xml.Doc* doc, string name) { var root = doc->get_root_element(); if (root == null) { return null; } return read_directive_text(root, name); } private static bool is_structural_directive(Xml.Node* node) { if (!is_element(node)) { return false; } var name = (string) node->name; return name == "pstm-template" || name == "pstm-name" || name == "pstm-title" || name == "pstm-uri" || name == "pstm-content"; } private void inline_fragments(Xml.Doc* doc) { bool changed = true; while (changed) { changed = false; var includes = find_all_elements(doc, "pstm-fragment"); foreach (var include in includes) { if (inline_one_fragment(doc, include)) { changed = true; } } } } private bool inline_one_fragment(Xml.Doc* doc, Xml.Node* include) { var name = include->get_prop("name"); if (name == null) { return false; } Xml.Doc* fragment_doc; if (!fragments_by_name.try_get((!)name, out fragment_doc)) { return false; } var fragment_body = find_body(fragment_doc); if (fragment_body == null) { return false; } var inputs = collect_input_names(fragment_body); var materialise_as = include->get_prop("pstm-materialise-as"); bool wrap = (materialise_as != null) || inputs.length > 0; string tag = materialise_as ?? "div"; if (wrap) { var parent = include->parent; if (parent == null) { return false; } var wrapper = parent->new_child(null, tag, null); wrapper->unlink(); include->add_prev_sibling(wrapper); for (var child = fragment_body->children; child != null; child = child->next) { var copied = child->doc_copy(doc, 1); if (copied != null) { wrapper->add_child(copied); } } foreach (var input_name in inputs) { var expr = include->get_prop(input_name); if (expr != null) { wrapper->set_prop(@"stm-def-$input_name-as", (!)expr); } } } else { for (var child = fragment_body->children; child != null; child = child->next) { var copied = child->doc_copy(doc, 1); if (copied != null) { include->add_prev_sibling(copied); } } } include->unlink(); return true; } private string[] collect_input_names(Xml.Node* fragment_body) { var names = new string[0]; for (var n = fragment_body->children; n != null; n = n->next) { if (is_element(n) && (string) n->name == "pstm-input") { var input_name = n->get_prop("name"); if (input_name != null) { names += (!)input_name; } } } return names; } private void rewrite_resources(Xml.Doc* doc) { var root = doc->get_root_element(); if (root != null) { rewrite_resources_in(root); } } private void rewrite_resources_in(Xml.Node* node) { if (!is_element(node)) { return; } string[] to_add_attr = new string[0]; string[] to_add_val = new string[0]; string[] to_remove = new string[0]; for (var attr = node->properties; attr != null; attr = attr->next) { var attr_name = (string) attr->name; if (attr_name.has_prefix("pstm-res-")) { var target_attr = attr_name.substring("pstm-res-".length); var value = node->get_prop(attr_name); if (value != null) { to_add_attr += target_attr; to_add_val += @"/_statum/resource/$((!)value)"; } to_remove += attr_name; } } for (int i = 0; i < to_add_attr.length; i++) { node->set_prop(to_add_attr[i], to_add_val[i]); } foreach (var attr_name in to_remove) { node->unset_prop(attr_name); } for (var child = node->children; child != null; child = child->next) { rewrite_resources_in(child); } } // ------------------------------------------------------------------ // Group/guard collection // ------------------------------------------------------------------ private Collected collect(Xml.Doc* doc) { var result = new Collected(); var root = doc->get_root_element(); if (root != null) { collect_guards(root, result); collect_groups_from(root, result); } return result; } private void collect_guards(Xml.Node* node, Collected result) { if (!is_element(node)) { return; } if (node->get_prop("pstm-guard") != null) { result.guards += new Guard() { predicate = node->get_prop("predicate") ?? "", require = node->get_prop("require"), redirect = node->get_prop("redirect"), status = node->get_prop("status") }; } for (var child = node->children; child != null; child = child->next) { collect_guards(child, result); } } private void collect_groups_from(Xml.Node* node, Collected result) { var n = node->children; while (n != null) { if (!is_element(n)) { n = n->next; continue; } if (n->get_prop("pstm-if") != null) { var chain = gather_chain(n); result.groups += chain_to_group(chain); foreach (var branch in chain) { collect_groups_from(branch, result); } n = chain[chain.length - 1]->next; continue; } collect_groups_from(n, result); n = n->next; } } private Xml.Node*[] gather_chain(Xml.Node* if_node) { Xml.Node*[] chain = new Xml.Node*[0]; chain += if_node; var n = if_node->next; while (n != null) { if (!is_element(n)) { n = n->next; continue; } if (n->get_prop("pstm-else-if") != null || n->get_prop("pstm-else") != null) { chain += n; n = n->next; } else { break; } } return chain; } private IfGroup chain_to_group(Xml.Node*[] chain) { var conditions = new string[0]; bool has_else = false; foreach (var node in chain) { var else_if = node->get_prop("pstm-else-if"); if (node->get_prop("pstm-if") != null) { conditions += node->get_prop("pstm-if") ?? ""; } else if (else_if != null) { conditions += (!)else_if; } else if (node->get_prop("pstm-else") != null) { has_else = true; } } return new IfGroup() { conditions = conditions, has_else = has_else }; } // ------------------------------------------------------------------ // Variant rendering // ------------------------------------------------------------------ private VariantData[] build_variants(Xml.Doc* doc, IfGroup[] groups) throws GLib.Error { int total = 1; foreach (var group in groups) { total *= (group.conditions.length + 1); } if (total < 1) { total = 1; } VariantData[] variants = new VariantData[0]; for (var index = 0; index < total; index++) { int[] choices = new int[groups.length]; int stride = 1; for (int gi = 0; gi < groups.length; gi++) { int outcomes = groups[gi].conditions.length + 1; choices[gi] = (index / stride) % outcomes; stride *= outcomes; } var copy = doc->copy(1); apply_choices(copy, choices); strip_pstm(copy); var html = serialize(copy); delete copy; variants += compress_variant(html); } return variants; } private VariantData compress_variant(string html) throws GLib.Error { var data = new VariantData(); data.identity = html.data; var buffer = new ByteBuffer.from_byte_array(html.data); var gzip = new GzipCompressor(9).compress_buffer(buffer, null).to_array(); var zstd = new ZstdCompressor(19).compress_buffer(buffer, null).to_array(); var br = new BrotliCompressor(11).compress_buffer(buffer, null).to_array(); data.has_gzip = gzip.length < html.length; data.has_zstd = zstd.length < html.length; data.has_br = br.length < html.length; data.gzip = data.has_gzip ? gzip : null; data.zstd = data.has_zstd ? zstd : null; data.br = data.has_br ? br : null; return data; } private void apply_choices(Xml.Doc* doc, int[] choices) { var root = doc->get_root_element(); if (root == null) { return; } int gi = 0; apply_choices_from(root, choices, ref gi); } private void apply_choices_from(Xml.Node* node, int[] choices, ref int gi) { var n = node->children; while (n != null) { if (!is_element(n)) { n = n->next; continue; } if (n->get_prop("pstm-if") != null) { var chain = gather_chain(n); var after = chain[chain.length - 1]->next; int conds = count_conditions(chain); int choice = (gi < choices.length) ? choices[gi] : conds; gi++; Xml.Node* keep = null; int idx = 0; foreach (var branch in chain) { bool is_conditional = branch->get_prop("pstm-if") != null || branch->get_prop("pstm-else-if") != null; if (is_conditional) { if (idx == choice) { keep = branch; } idx++; } else if (branch->get_prop("pstm-else") != null) { if (choice >= conds) { keep = branch; } } } foreach (var branch in chain) { if (branch == keep) { continue; } branch->unlink(); } if (keep != null) { apply_choices_from(keep, choices, ref gi); } n = after; continue; } apply_choices_from(n, choices, ref gi); n = n->next; } } private int count_conditions(Xml.Node*[] chain) { int count = 0; foreach (var node in chain) { if (node->get_prop("pstm-if") != null || node->get_prop("pstm-else-if") != null) { count++; } } return count; } private void strip_pstm(Xml.Doc* doc) { var root = doc->get_root_element(); if (root != null) { strip_pstm_from(root); } } private void strip_pstm_from(Xml.Node* node) { // pstm-guard's parameters (predicate/require/redirect/status) are not // pstm-prefixed, so remove them alongside the pstm-guard marker. bool is_guard = node->get_prop("pstm-guard") != null; string[] to_remove = new string[0]; for (var attr = node->properties; attr != null; attr = attr->next) { var attr_name = (string) attr->name; if (attr_name.has_prefix("pstm-")) { to_remove += attr_name; } else if (is_guard && (attr_name == "predicate" || attr_name == "require" || attr_name == "redirect" || attr_name == "status")) { to_remove += attr_name; } } foreach (var attr_name in to_remove) { node->unset_prop(attr_name); } var child = node->children; while (child != null) { var next = child->next; if (is_element(child) && ((string) child->name).has_prefix("pstm-")) { child->unlink(); } else if (is_element(child)) { strip_pstm_from(child); } child = next; } } // ------------------------------------------------------------------ // Code generation // ------------------------------------------------------------------ private string emit(string class_name, string route, Collected collected, VariantData[] variants) throws GLib.Error { var page_hash = compute_hash(variants.length > 0 ? variants[0].identity : new uint8[0]); var b = new StringBuilder(); string i1 = namespace_name != null ? " " : ""; string i2 = namespace_name != null ? " " : " "; string i3 = namespace_name != null ? " " : " "; string i4 = namespace_name != null ? " " : " "; b.append("using Statum;\n"); b.append("using Invercargill;\n"); b.append("using Invercargill.DataStructures;\n"); b.append("using Astralis;\n\n"); if (namespace_name != null) { b.append_printf("namespace %s {\n\n", namespace_name); } b.append_printf("%s// Generated by statum-mkpstm\n", i1); b.append_printf("%spublic class %s : StatumPage {\n\n", i1, class_name); b.append_printf("%spublic const string ROUTE = \"%s\";\n", i2, escape(route)); b.append_printf("%spublic override string content_type { get { return \"text/html; charset=utf-8\"; } }\n\n", i2); emit_select_variant(b, i2, i3, collected, variants.length); emit_encoding_methods(b, i2, i3, i4, variants, page_hash); emit_byte_arrays(b, i2, i3, variants); emit_helpers(b, i2, i3); b.append_printf("%s}\n", i1); if (namespace_name != null) { b.append("}\n"); } return b.str; } private void emit_select_variant(StringBuilder b, string i2, string i3, Collected collected, int variant_count) { b.append_printf("%spublic override StatumVariantSelection select_variant(ReadOnlyAssociative held) throws GLib.Error {\n", i2); foreach (var guard in collected.guards) { string predicate_check; var compiled = @"el_truthy($(compile_predicate_string(guard.predicate)))"; if (guard.require != null) { predicate_check = @"held.has(\"$(escape((!)guard.require))\") && $(compiled)"; } else { predicate_check = compiled; } if (guard.redirect != null) { b.append_printf("%sif (!(%s)) {\n", i3, predicate_check); b.append_printf("%sreturn StatumVariantSelection.redirect_to(\"%s\");\n", i3, escape((!)guard.redirect)); b.append_printf("%s}\n", i3); } else { var status = guard.status ?? "FORBIDDEN"; b.append_printf("%sif (!(%s)) {\n", i3, predicate_check); b.append_printf("%sreturn StatumVariantSelection.failure(StatusCode.%s);\n", i3, status); b.append_printf("%s}\n", i3); } } // Compute the variant index as a mixed-radix number of per-group choices. for (int gi = 0; gi < collected.groups.length; gi++) { var group = collected.groups[gi]; b.append_printf("%sint g%d;\n", i3, gi); b.append_printf("%s{\n", i3); b.append_printf("%sint choice = %d;\n", i3, group.conditions.length); for (int ci = 0; ci < group.conditions.length; ci++) { var compiled = @"el_truthy($(compile_predicate_string(group.conditions[ci])))"; if (ci == 0) { b.append_printf("%sif (%s) { choice = 0; }\n", i3, compiled); } else { b.append_printf("%selse if (%s) { choice = %d; }\n", i3, compiled, ci); } } b.append_printf("%sg%d = choice;\n", i3, gi); b.append_printf("%s}\n", i3); } if (collected.groups.length == 0) { b.append_printf("%sreturn StatumVariantSelection.variant(0);\n", i3); } else { b.append_printf("%sint index = 0;\n", i3); int stride = 1; for (int gi = 0; gi < collected.groups.length; gi++) { b.append_printf("%sindex += g%d * %d;\n", i3, gi, stride); stride *= collected.groups[gi].conditions.length + 1; } b.append_printf("%sif (index < 0 || index >= %d) { index = 0; }\n", i3, variant_count); b.append_printf("%sreturn StatumVariantSelection.variant(index);\n", i3); } b.append_printf("%s}\n\n", i2); } private void emit_encoding_methods(StringBuilder b, string i2, string i3, string i4, VariantData[] variants, string page_hash) { // get_best_encoding: prefer the smallest supported, available encoding. b.append_printf("%spublic override string get_best_encoding(int variant, Set supported) {\n", i2); b.append_printf("%sif (supported.has(\"br\") && has_encoding(variant, 3)) return \"br\";\n", i3); b.append_printf("%sif (supported.has(\"zstd\") && has_encoding(variant, 2)) return \"zstd\";\n", i3); b.append_printf("%sif (supported.has(\"gzip\") && has_encoding(variant, 1)) return \"gzip\";\n", i3); b.append_printf("%sreturn \"identity\";\n", i3); b.append_printf("%s}\n\n", i2); // get_encoding: nested switch over (variant, encoding). b.append_printf("%spublic override unowned uint8[] get_encoding(int variant, string encoding) {\n", i2); b.append_printf("%sswitch (variant) {\n", i3); for (int v = 0; v < variants.length; v++) { b.append_printf("%scase %d:\n", i4, v); b.append_printf("%sswitch (encoding) {\n", i4); b.append_printf("%scase \"identity\": return V_%d_IDENTITY;\n", i4, v); if (variants[v].has_gzip) { b.append_printf("%scase \"gzip\": return V_%d_GZIP;\n", i4, v); } if (variants[v].has_zstd) { b.append_printf("%scase \"zstd\": return V_%d_ZSTD;\n", i4, v); } if (variants[v].has_br) { b.append_printf("%scase \"br\": return V_%d_BR;\n", i4, v); } b.append_printf("%sdefault: return V_%d_IDENTITY;\n", i4, v); b.append_printf("%s}\n", i4); } b.append_printf("%s}\n", i3); b.append_printf("%sreturn V_0_IDENTITY;\n", i3); b.append_printf("%s}\n\n", i2); // get_etag_for b.append_printf("%spublic override string get_etag_for(int variant, string encoding) {\n", i2); b.append_printf("%sreturn \"\\\"%s-%%s-%%d\\\"\".printf(encoding, variant);\n", i3, page_hash); b.append_printf("%s}\n\n", i2); // has_encoding b.append_printf("%sprivate static bool has_encoding(int variant, int encoding) {\n", i2); b.append_printf("%sswitch (variant) {\n", i3); for (int v = 0; v < variants.length; v++) { b.append_printf("%scase %d: return encoding == 0%s%s%s;\n", i4, v, variants[v].has_gzip ? " || encoding == 1" : "", variants[v].has_zstd ? " || encoding == 2" : "", variants[v].has_br ? " || encoding == 3" : ""); } b.append_printf("%s}\n", i3); b.append_printf("%sreturn false;\n", i3); b.append_printf("%s}\n\n", i2); } private void emit_byte_arrays(StringBuilder b, string i2, string i3, VariantData[] variants) { for (int v = 0; v < variants.length; v++) { emit_one_array(b, i2, i3, @"V_$(v)_IDENTITY", variants[v].identity); if (variants[v].has_gzip && variants[v].gzip != null) { emit_one_array(b, i2, i3, @"V_$(v)_GZIP", (!)variants[v].gzip); } if (variants[v].has_zstd && variants[v].zstd != null) { emit_one_array(b, i2, i3, @"V_$(v)_ZSTD", (!)variants[v].zstd); } if (variants[v].has_br && variants[v].br != null) { emit_one_array(b, i2, i3, @"V_$(v)_BR", (!)variants[v].br); } } } private void emit_one_array(StringBuilder b, string i2, string i3, string name, uint8[] data) { b.append_printf("%sprivate const uint8[] %s = {\n", i2, name); for (int i = 0; i < data.length; i++) { if (i == 0) { b.append(i3); } else if (i % 16 == 0) { b.append(",\n").append(i3); } else { b.append(", "); } b.append_printf("0x%02x", data[i]); } b.append_printf("\n%s};\n\n", i2); } private void emit_helpers(StringBuilder b, string i2, string i3) { // Static helpers used by the compiled predicates in select_variant. // Each predicate AST node is translated at build time into calls to // these, so no expression is parsed/evaluated at request time. b.append_printf("%sprivate static Element? held_pub(ReadOnlyAssociative held, string type_name) {\n", i2); b.append_printf("%sHeldSlot slot;\n", i3); b.append_printf("%sif (held.try_get(type_name, out slot)) { return new NativeElement(slot.@public); }\n", i3); b.append_printf("%sreturn null;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? held_priv(ReadOnlyAssociative held, string type_name) {\n", i2); b.append_printf("%sHeldSlot slot;\n", i3); b.append_printf("%sif (held.try_get(type_name, out slot)) { return new NativeElement(slot.@private); }\n", i3); b.append_printf("%sreturn null;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_get(Element? target, string name) {\n", i2); b.append_printf("%sif (target == null) { return null; }\n", i3); b.append_printf("%sProperties props;\n", i3); b.append_printf("%sif (!((!)target).try_get_as(out props)) { return null; }\n", i3); b.append_printf("%sElement value;\n", i3); b.append_printf("%sif (!props.try_get(name, out value)) { return null; }\n", i3); b.append_printf("%sreturn value;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static bool el_is_null(Element? e) {\n", i2); b.append_printf("%sreturn e == null || ((!)e) is NullElement || ((!)e).is_null();\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static bool el_truthy(Element? e) {\n", i2); b.append_printf("%sif (el_is_null(e)) { return false; }\n", i3); b.append_printf("%sbool? b = ((!)e).as_bool_or_null();\n", i3); b.append_printf("%sreturn b != null ? (!)b : true;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static double? el_num_of(Element e) {\n", i2); b.append_printf("%sint? i = e.as_int_or_null(); if (i != null) { return (double)(!)i; }\n", i3); b.append_printf("%sint64? i64 = e.as_int64_or_null(); if (i64 != null) { return (double)(!)i64; }\n", i3); b.append_printf("%sdouble? d = e.as_double_or_null(); if (d != null) { return d; }\n", i3); b.append_printf("%sfloat? f = e.as_float_or_null(); if (f != null) { return (double)(!)f; }\n", i3); b.append_printf("%slong? l = e.as_long_or_null(); if (l != null) { return (double)(!)l; }\n", i3); b.append_printf("%sreturn null;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static double? el_num_of_opt(Element? e) {\n", i2); b.append_printf("%sif (el_is_null(e)) { return null; }\n", i3); b.append_printf("%sreturn el_num_of((!)e);\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static int? el_cmp_values(Element a, Element b) {\n", i2); b.append_printf("%sdouble? na = el_num_of(a), nb = el_num_of(b);\n", i3); b.append_printf("%sif (na != null && nb != null) { return na < nb ? -1 : (na > nb ? 1 : 0); }\n", i3); b.append_printf("%sstring? sa = a.as_string_or_null(), sb = b.as_string_or_null();\n", i3); b.append_printf("%sif (sa != null && sb != null) { int c = strcmp((!)sa, (!)sb); return c < 0 ? -1 : (c > 0 ? 1 : 0); }\n", i3); b.append_printf("%sreturn null;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static bool el_values_equal(Element a, Element b) {\n", i2); b.append_printf("%sint? c = el_cmp_values(a, b);\n", i3); b.append_printf("%sif (c != null) { return (!)c == 0; }\n", i3); b.append_printf("%sbool? ba = a.as_bool_or_null(), bb = b.as_bool_or_null();\n", i3); b.append_printf("%sif (ba != null && bb != null) { return ba == bb; }\n", i3); b.append_printf("%sreturn false;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_str(string s) { return new NativeElement(s); }\n", i2); b.append_printf("%sprivate static Element? el_num(double d) { return new NativeElement(d); }\n", i2); b.append_printf("%sprivate static Element? el_bool(bool b) { return new NativeElement(b); }\n", i2); b.append_printf("%sprivate static Element? el_null() { return new NullElement(); }\n", i2); b.append_printf("%sprivate static Element? el_eq(Element? a, Element? b) {\n", i2); b.append_printf("%sbool an = el_is_null(a), bn = el_is_null(b);\n", i3); b.append_printf("%sif (an || bn) { return el_bool(an == bn); }\n", i3); b.append_printf("%sreturn el_bool(el_values_equal((!)a, (!)b));\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_neq(Element? a, Element? b) { return el_bool(!el_truthy(el_eq(a, b))); }\n", i2); b.append_printf("%sprivate static Element? el_gt(Element? a, Element? b) {\n", i2); b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3); b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3); b.append_printf("%sreturn el_bool(c != null && (!)c > 0);\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_ge(Element? a, Element? b) {\n", i2); b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3); b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3); b.append_printf("%sreturn el_bool(c != null && (!)c >= 0);\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_lt(Element? a, Element? b) {\n", i2); b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3); b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3); b.append_printf("%sreturn el_bool(c != null && (!)c < 0);\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_le(Element? a, Element? b) {\n", i2); b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3); b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3); b.append_printf("%sreturn el_bool(c != null && (!)c <= 0);\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_and(Element? a, Element? b) { return el_bool(el_truthy(a) && el_truthy(b)); }\n", i2); b.append_printf("%sprivate static Element? el_or(Element? a, Element? b) { return el_bool(el_truthy(a) || el_truthy(b)); }\n", i2); b.append_printf("%sprivate static Element? el_not(Element? a) { return el_bool(!el_truthy(a)); }\n", i2); b.append_printf("%sprivate static Element? el_arith(Element? a, Element? b, string op) {\n", i2); b.append_printf("%sdouble? x = el_num_of_opt(a), y = el_num_of_opt(b);\n", i3); b.append_printf("%sif (x == null || y == null) { return null; }\n", i3); b.append_printf("%sdouble xv = (!)x, yv = (!)y;\n", i3); b.append_printf("%sif (op == \"+\") { return el_num(xv + yv); }\n", i3); b.append_printf("%sif (op == \"-\") { return el_num(xv - yv); }\n", i3); b.append_printf("%sif (op == \"*\") { return el_num(xv * yv); }\n", i3); b.append_printf("%sif (op == \"/\") { return yv == 0 ? null : el_num(xv / yv); }\n", i3); b.append_printf("%sif (op == \"%%\") { return yv == 0 ? null : el_num((double)((int64)xv %% (int64)yv)); }\n", i3); b.append_printf("%sreturn null;\n", i3); b.append_printf("%s}\n", i2); b.append_printf("%sprivate static Element? el_add(Element? a, Element? b) { return el_arith(a, b, \"+\"); }\n", i2); b.append_printf("%sprivate static Element? el_sub(Element? a, Element? b) { return el_arith(a, b, \"-\"); }\n", i2); b.append_printf("%sprivate static Element? el_mul(Element? a, Element? b) { return el_arith(a, b, \"*\"); }\n", i2); b.append_printf("%sprivate static Element? el_div(Element? a, Element? b) { return el_arith(a, b, \"/\"); }\n", i2); b.append_printf("%sprivate static Element? el_mod(Element? a, Element? b) { return el_arith(a, b, \"%%\"); }\n", i2); b.append_printf("%sprivate static Element? el_neg(Element? a) { double? x = el_num_of_opt(a); return x == null ? null : el_num(-(!)x); }\n", i2); b.append_printf("%sprivate static Element? el_ternary(Element? c, Element? a, Element? b) { return el_truthy(c) ? a : b; }\n", i2); b.append("\n"); } // ------------------------------------------------------------------ // Predicate compilation (build-time AST → Vala) // ------------------------------------------------------------------ private string compile_predicate_string(string predicate) { try { return compile_predicate(ExpressionParser.parse(predicate)); } catch (ExpressionError e) { // Unparseable predicate → always false. return "el_bool(false)"; } } private string compile_predicate(Expression expr) { if (expr is VariableExpression) { var v = (VariableExpression) expr; return @"held_pub(held, \"$(escape(v.variable_name))\")"; } if (expr is PropertyExpression) { var p = (PropertyExpression) expr; if (p.property_name == "private" && p.target is VariableExpression) { var root = (VariableExpression) p.target; return @"held_priv(held, \"$(escape(root.variable_name))\")"; } return @"el_get($(compile_predicate(p.target)), \"$(escape(p.property_name))\")"; } if (expr is LiteralExpression) { return compile_literal((LiteralExpression) expr); } if (expr is UnaryExpression) { var u = (UnaryExpression) expr; var operand = compile_predicate(u.operand); if (u.operator == UnaryOperator.NOT) { return @"el_not($(operand))"; } if (u.operator == UnaryOperator.NEGATE) { return @"el_neg($(operand))"; } return "el_null()"; } if (expr is BinaryExpression) { return compile_binary((BinaryExpression) expr); } if (expr is BracketedExpression) { return compile_predicate(((BracketedExpression) expr).inner); } if (expr is TernaryExpression) { var t = (TernaryExpression) expr; return @"el_ternary($(compile_predicate(t.condition)), $(compile_predicate(t.true_expression)), $(compile_predicate(t.false_expression)))"; } // Unsupported node (function call, lambda, lot literal, parameter). return "el_null()"; } private string compile_binary(BinaryExpression b) { var l = compile_predicate(b.left); var r = compile_predicate(b.right); switch (b.op) { case BinaryOperator.EQUAL: return @"el_eq($(l), $(r))"; case BinaryOperator.NOT_EQUAL: return @"el_neq($(l), $(r))"; case BinaryOperator.GREATER_THAN: return @"el_gt($(l), $(r))"; case BinaryOperator.LESS_THAN: return @"el_lt($(l), $(r))"; case BinaryOperator.GREATER_EQUAL: return @"el_ge($(l), $(r))"; case BinaryOperator.LESS_EQUAL: return @"el_le($(l), $(r))"; case BinaryOperator.AND: return @"el_and($(l), $(r))"; case BinaryOperator.OR: return @"el_or($(l), $(r))"; case BinaryOperator.ADD: return @"el_add($(l), $(r))"; case BinaryOperator.SUBTRACT: return @"el_sub($(l), $(r))"; case BinaryOperator.MULTIPLY: return @"el_mul($(l), $(r))"; case BinaryOperator.DIVIDE: return @"el_div($(l), $(r))"; case BinaryOperator.MODULO: return @"el_mod($(l), $(r))"; default: return "el_null()"; } } private string compile_literal(LiteralExpression lit) { var v = lit.value; if (v == null || v is NullElement) { return "el_null()"; } string? s; if (v.try_get_as(out s)) { return @"el_str(\"$(escape_vala_string((!)s))\")"; } bool? bv; if (v.try_get_as(out bv)) { return (!)bv ? "el_bool(true)" : "el_bool(false)"; } var d = numeric_value_of(v); if (d != null) { return @"el_num($((!)d))"; } return "el_null()"; } private static double? numeric_value_of(Invercargill.Element v) { int64? i64; if (v.try_get_as(out i64)) { return (double)(!)i64; } int? i; if (v.try_get_as(out i)) { return (double)(!)i; } double? d; if (v.try_get_as(out d)) { return d; } float? f; if (v.try_get_as(out f)) { return (double)(!)f; } long? l; if (v.try_get_as(out l)) { return (double)(!)l; } return null; } private static string escape_vala_string(string s) { return s.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t"); } // ------------------------------------------------------------------ // DOM helpers // ------------------------------------------------------------------ private Xml.Doc* parse_html_file(string path) throws GLib.Error { var file = File.new_for_path(path); if (!file.query_exists()) { throw new IOError.NOT_FOUND(@"Page file '$path' does not exist"); } uint8[] contents; string etag_out; file.load_contents(null, out contents, out etag_out); // Parse with libxml2's HTML parser (mirrors Astralis' MarkupDocument) // so the tree has HTML semantics and serializes as HTML, not XML. int opts = (int)(Html.ParserOption.RECOVER | Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING | Html.ParserOption.NOBLANKS | Html.ParserOption.NONET); char[] buffer = ((string) contents).to_utf8(); var doc = Html.Doc.read_memory(buffer, buffer.length, path, "utf-8", opts); if (doc == null) { throw new IOError.FAILED(@"Failed to parse HTML file '$path'"); } return doc; } private static Xml.Node* find_body(Xml.Doc* doc) { var root = doc->get_root_element(); if (root == null) { return null; } return find_first_element(root, "body"); } private static Xml.Node* find_first_element(Xml.Node* root, string name) { if (!is_element(root)) { return null; } if (((string) root->name).down() == name.down()) { return root; } for (var child = root->children; child != null; child = child->next) { var found = find_first_element(child, name); if (found != null) { return found; } } return null; } private static Xml.Node*[] find_all_elements(Xml.Doc* doc, string name) { var collected = new Series(); var root = doc->get_root_element(); if (root != null) { find_all_from(root, name, collected); } return collected.to_array(); } private static void find_all_from(Xml.Node* node, string name, Series result) { if (is_element(node) && ((string) node->name).down() == name.down()) { result.add(node); } if (is_element(node)) { for (var child = node->children; child != null; child = child->next) { find_all_from(child, name, result); } } } private static string? read_directive_text(Xml.Node* body, string directive) { var node = find_first_element(body, directive); if (node == null) { return null; } var text = element_text(node); return text.strip().length > 0 ? text.strip() : null; } private static string? read_pstm_name(Xml.Doc* doc) { var node = find_first_element(doc->get_root_element(), "pstm-name"); if (node == null) { return null; } return element_text(node).strip(); } private static string[] read_routes(Xml.Doc* doc) { string[] routes = new string[0]; var nodes = find_all_elements(doc, "pstm-uri"); foreach (var node in nodes) { var text = element_text(node).strip(); if (text.length > 0) { routes += text; } } return routes; } private static string element_text(Xml.Node* node) { var b = new StringBuilder(); for (var child = node->children; child != null; child = child->next) { if (child->type == ElementType.TEXT_NODE && child->content != null) { b.append((string) child->content); } else if (is_element(child)) { b.append(element_text(child)); } } return b.str; } private static void apply_title(Xml.Doc* doc, string? title) { if (title == null || title.length == 0) { return; } var head = find_first_element(doc->get_root_element(), "head"); if (head == null) { return; } var title_node = find_first_element(head, "title"); if (title_node == null) { title_node = head->new_child(null, "title", null); } for (var child = title_node->children; child != null; child = child->next) { child->unlink(); } title_node->add_content(title); } private static string serialize(Xml.Doc* doc) { // Serialize with libxml2's HTML serializer (htmlDocDumpMemory) so the // output is valid HTML: no prolog, no self-closing // `