|
@@ -0,0 +1,1235 @@
|
|
|
|
|
+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<string> template_dirs = new List<string>();
|
|
|
|
|
+
|
|
|
|
|
+ private Dictionary<string, Xml.Doc*> templates_by_name = new Dictionary<string, Xml.Doc*>();
|
|
|
|
|
+ private Dictionary<string, Xml.Doc*> fragments_by_name = new Dictionary<string, Xml.Doc*>();
|
|
|
|
|
+
|
|
|
|
|
+ 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: <InputName>Page)", "NAME" },
|
|
|
|
|
+ { "route", 'r', 0, OptionArg.STRING, ref route_override, "Page route (default: from <pstm-uri>)", "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 <pstm-*> 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 <body> 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<string>();
|
|
|
|
|
+
|
|
|
|
|
+ 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 <body>");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Splice this body's children into the parent's <pstm-content>
|
|
|
|
|
+ // outlet, but drop structural <pstm-*> 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 <pstm-template> 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<string, HeldSlot> 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<string> 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<string, HeldSlot> 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<Properties>(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<string, HeldSlot> 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<Properties>(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<Properties>(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<string>(s); }\n", i2);
|
|
|
|
|
+ b.append_printf("%sprivate static Element? el_num(double d) { return new NativeElement<double?>(d); }\n", i2);
|
|
|
|
|
+ b.append_printf("%sprivate static Element? el_bool(bool b) { return new NativeElement<bool>(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<string>(out s)) {
|
|
|
|
|
+ return @"el_str(\"$(escape_vala_string((!)s))\")";
|
|
|
|
|
+ }
|
|
|
|
|
+ bool? bv;
|
|
|
|
|
+ if (v.try_get_as<bool>(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<int64?>(out i64)) {
|
|
|
|
|
+ return (double)(!)i64;
|
|
|
|
|
+ }
|
|
|
|
|
+ int? i;
|
|
|
|
|
+ if (v.try_get_as<int?>(out i)) {
|
|
|
|
|
+ return (double)(!)i;
|
|
|
|
|
+ }
|
|
|
|
|
+ double? d;
|
|
|
|
|
+ if (v.try_get_as<double?>(out d)) {
|
|
|
|
|
+ return d;
|
|
|
|
|
+ }
|
|
|
|
|
+ float? f;
|
|
|
|
|
+ if (v.try_get_as<float?>(out f)) {
|
|
|
|
|
+ return (double)(!)f;
|
|
|
|
|
+ }
|
|
|
|
|
+ long? l;
|
|
|
|
|
+ if (v.try_get_as<long?>(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<Xml.Node*>();
|
|
|
|
|
+ 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<Xml.Node*> 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 <?xml?> prolog, no self-closing
|
|
|
|
|
+ // `<script/>`, and correct void/non-void element handling. The doc was
|
|
|
|
|
+ // produced by the HTML parser, so it carries HTML structure.
|
|
|
|
|
+ string mem;
|
|
|
|
|
+ int len;
|
|
|
|
|
+ ((Html.Doc*) doc)->dump_memory(out mem, out len);
|
|
|
|
|
+ return len > 0 ? mem.substring(0, len) : mem;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static bool is_element(Xml.Node* node) {
|
|
|
|
|
+ return node != null && node->type == ElementType.ELEMENT_NODE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string compute_hash(uint8[] data) {
|
|
|
|
|
+ var checksum = new Checksum(ChecksumType.SHA512);
|
|
|
|
|
+ checksum.update(data, data.length);
|
|
|
|
|
+ return checksum.get_string();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string escape(string s) {
|
|
|
|
|
+ return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void write_file(string path, string contents) throws GLib.Error {
|
|
|
|
|
+ var file = File.new_for_path(path);
|
|
|
|
|
+ var stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
|
|
|
|
|
+ stream.put_string(contents);
|
|
|
|
|
+ stream.close();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string make_pascal_case(string input) {
|
|
|
|
|
+ var basename = Path.get_basename(input);
|
|
|
|
|
+ var dot = basename.last_index_of(".");
|
|
|
|
|
+ if (dot > 0) {
|
|
|
|
|
+ basename = basename.substring(0, dot);
|
|
|
|
|
+ }
|
|
|
|
|
+ var result = new StringBuilder();
|
|
|
|
|
+ var capitalize_next = true;
|
|
|
|
|
+ foreach (var c in basename.data) {
|
|
|
|
|
+ if (c == '-' || c == '_' || c == ' ' || c == '.') {
|
|
|
|
|
+ capitalize_next = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (capitalize_next) {
|
|
|
|
|
+ result.append_printf("%c", c >= 'a' && c <= 'z' ? c - 32 : c);
|
|
|
|
|
+ capitalize_next = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.append_printf("%c", c);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result.str;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // Model
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ private class Guard : Object {
|
|
|
|
|
+ public string predicate;
|
|
|
|
|
+ public string? require;
|
|
|
|
|
+ public string? redirect;
|
|
|
|
|
+ public string? status;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private class IfGroup : Object {
|
|
|
|
|
+ public string[] conditions = new string[0];
|
|
|
|
|
+ public bool has_else;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private class Collected : Object {
|
|
|
|
|
+ public Guard[] guards = new Guard[0];
|
|
|
|
|
+ public IfGroup[] groups = new IfGroup[0];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private class VariantData : Object {
|
|
|
|
|
+ public uint8[] identity;
|
|
|
|
|
+ public uint8[]? gzip;
|
|
|
|
|
+ public uint8[]? zstd;
|
|
|
|
|
+ public uint8[]? br;
|
|
|
|
|
+ public bool has_gzip;
|
|
|
|
|
+ public bool has_zstd;
|
|
|
|
|
+ public bool has_br;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|