statum-mkpstm.vala 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235
  1. using Xml;
  2. using Html;
  3. using Astralis;
  4. using Invercargill;
  5. using Invercargill.DataStructures;
  6. using Invercargill.Expressions;
  7. namespace Statum.Tools {
  8. /**
  9. * `statum-mkpstm` — the build-time Statum pre-rendering pipeline.
  10. *
  11. * Turns an app HTML page (plus its templates/fragments) into a Vala
  12. * {@link Statum.StatumPage} subclass carrying precompressed variant byte
  13. * arrays and a compiled {@link Statum.StatumPage.select_variant} that
  14. * evaluates `pstm-guard`s and the `pstm-if` matrix against the request's held
  15. * slots using the Invercargill runtime expression evaluator.
  16. *
  17. * See `phase-1-plan.md` Subsystem B for the supported `pstm-*` directives.
  18. */
  19. public class Mkpstm : Object {
  20. private static string? output_file = null;
  21. private static string? namespace_name = null;
  22. private static string? class_name_override = null;
  23. private static string? route_override = null;
  24. private static List<string> template_dirs = new List<string>();
  25. private Dictionary<string, Xml.Doc*> templates_by_name = new Dictionary<string, Xml.Doc*>();
  26. private Dictionary<string, Xml.Doc*> fragments_by_name = new Dictionary<string, Xml.Doc*>();
  27. private const OptionEntry[] options = {
  28. { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output Vala file name", "FILE" },
  29. { "ns", '\0', 0, OptionArg.STRING, ref namespace_name, "Namespace for the generated class", "NAMESPACE" },
  30. { "name", 'n', 0, OptionArg.STRING, ref class_name_override, "Generated class name (default: <InputName>Page)", "NAME" },
  31. { "route", 'r', 0, OptionArg.STRING, ref route_override, "Page route (default: from <pstm-uri>)", "ROUTE" },
  32. { "template-dir", 't', 0, OptionArg.FILENAME_ARRAY, ref template_dirs, "Directory to search for templates/fragments (repeatable)", "DIR" },
  33. { null }
  34. };
  35. public static int main(string[] args) {
  36. try {
  37. var opt = new OptionContext("PAGE_FILE - Generate a Statum StatumPage");
  38. opt.add_main_entries(options, null);
  39. opt.parse(ref args);
  40. } catch (OptionError e) {
  41. stderr.printf("Error: %s\n", e.message);
  42. return 1;
  43. }
  44. string? page_file = null;
  45. if (args.length > 1) {
  46. page_file = args[1];
  47. }
  48. if (page_file == null) {
  49. stderr.printf("Error: No page file specified.\n");
  50. return 1;
  51. }
  52. try {
  53. var tool = new Mkpstm();
  54. var generated = tool.run((!)page_file);
  55. var out_path = tool.output_file ?? (make_pascal_case((!)page_file) + "Page.vala");
  56. write_file(out_path, generated);
  57. stdout.printf("Generated: %s\n", out_path);
  58. return 0;
  59. } catch (GLib.Error e) {
  60. stderr.printf("Error: %s\n", e.message);
  61. return 1;
  62. }
  63. }
  64. public string run(string page_file) throws GLib.Error {
  65. if (template_dirs.length() == 0) {
  66. var parent = File.new_for_path(page_file).get_parent();
  67. template_dirs.append(parent != null ? ((!)parent).get_path() : ".");
  68. }
  69. load_templates_and_fragments(page_file);
  70. var doc = parse_html_file(page_file);
  71. // Capture the page's routes from the original document before
  72. // composition (structural <pstm-*> directives are dropped while
  73. // splicing templates, so they would no longer be present afterwards).
  74. var routes = read_routes(doc);
  75. var body = find_body(doc);
  76. if (body == null) {
  77. throw new IOError.FAILED("Page has no <body> element");
  78. }
  79. doc = compose_templates(doc, (!)body);
  80. inline_fragments(doc);
  81. rewrite_resources(doc);
  82. var collected = collect(doc);
  83. var class_name = class_name_override ?? (make_pascal_case(page_file) + "Page");
  84. var route = route_override ?? (routes.length > 0 ? routes[0] : "/");
  85. var variants = build_variants(doc, collected.groups);
  86. return emit(class_name, route, collected, variants);
  87. }
  88. // ------------------------------------------------------------------
  89. // Template/fragment loading
  90. // ------------------------------------------------------------------
  91. private void load_templates_and_fragments(string page_file) throws GLib.Error {
  92. foreach (var dir in template_dirs) {
  93. var dir_file = File.new_for_path(dir);
  94. if (!dir_file.query_exists()) {
  95. continue;
  96. }
  97. var enumerator = dir_file.enumerate_children("standard::name", 0);
  98. FileInfo info;
  99. while ((info = enumerator.next_file()) != null) {
  100. var name = info.get_name();
  101. if (!name.has_suffix(".html") || name.has_prefix(".")) {
  102. continue;
  103. }
  104. var path = Path.build_filename(dir, name);
  105. if (path == page_file) {
  106. continue;
  107. }
  108. var tdoc = parse_html_file(path);
  109. var tname = read_pstm_name(tdoc);
  110. if (tname == null) {
  111. continue;
  112. }
  113. if (find_first_element(tdoc->get_root_element(), "pstm-content") != null) {
  114. templates_by_name.set((!)tname, tdoc);
  115. } else {
  116. fragments_by_name.set((!)tname, tdoc);
  117. }
  118. }
  119. }
  120. }
  121. // ------------------------------------------------------------------
  122. // Composition
  123. // ------------------------------------------------------------------
  124. private Xml.Doc* compose_templates(Xml.Doc* doc, Xml.Node* body) throws GLib.Error {
  125. Xml.Doc* current_doc = doc;
  126. Xml.Node* current_body = body;
  127. string merged_title = read_directive_in_doc(current_doc, "pstm-title") ?? "";
  128. // Guard against cyclic template chains (A templates B templates A).
  129. var visited = new HashSet<string>();
  130. while (true) {
  131. var parent_name = read_directive_in_doc(current_doc, "pstm-template");
  132. if (parent_name == null || parent_name.length == 0) {
  133. break;
  134. }
  135. if (visited.contains((!)parent_name)) {
  136. break;
  137. }
  138. visited.add((!)parent_name);
  139. Xml.Doc* parent_doc;
  140. if (!templates_by_name.try_get((!)parent_name, out parent_doc)) {
  141. throw new IOError.FAILED(@"Unknown template \"$((!)parent_name)\"");
  142. }
  143. var parent_body = find_body(parent_doc);
  144. if (parent_body == null) {
  145. throw new IOError.FAILED(@"Template \"$((!)parent_name)\" has no <body>");
  146. }
  147. // Splice this body's children into the parent's <pstm-content>
  148. // outlet, but drop structural <pstm-*> directives (pstm-template,
  149. // pstm-name, pstm-title, pstm-uri). These are page-level metadata
  150. // and must NOT be carried into the parent — otherwise the
  151. // spliced <pstm-template> would be re-discovered on the next pass
  152. // and recompose forever.
  153. var outlet = find_first_element(parent_body, "pstm-content");
  154. for (var child = current_body->children; child != null; child = child->next) {
  155. if (is_structural_directive(child)) {
  156. continue;
  157. }
  158. var copied = child->doc_copy(parent_doc, 1);
  159. if (copied == null) {
  160. continue;
  161. }
  162. if (outlet != null) {
  163. outlet->add_prev_sibling(copied);
  164. } else {
  165. parent_body->add_child(copied);
  166. }
  167. }
  168. if (outlet != null) {
  169. outlet->unlink();
  170. }
  171. var parent_title = read_directive_in_doc(parent_doc, "pstm-title");
  172. if (parent_title != null && parent_title.length > 0) {
  173. merged_title = merged_title + (!)parent_title;
  174. }
  175. current_doc = parent_doc;
  176. current_body = parent_body;
  177. }
  178. apply_title(current_doc, merged_title);
  179. return current_doc;
  180. }
  181. private static string? read_directive_in_doc(Xml.Doc* doc, string name) {
  182. var root = doc->get_root_element();
  183. if (root == null) {
  184. return null;
  185. }
  186. return read_directive_text(root, name);
  187. }
  188. private static bool is_structural_directive(Xml.Node* node) {
  189. if (!is_element(node)) {
  190. return false;
  191. }
  192. var name = (string) node->name;
  193. return name == "pstm-template" || name == "pstm-name" || name == "pstm-title" || name == "pstm-uri" || name == "pstm-content";
  194. }
  195. private void inline_fragments(Xml.Doc* doc) {
  196. bool changed = true;
  197. while (changed) {
  198. changed = false;
  199. var includes = find_all_elements(doc, "pstm-fragment");
  200. foreach (var include in includes) {
  201. if (inline_one_fragment(doc, include)) {
  202. changed = true;
  203. }
  204. }
  205. }
  206. }
  207. private bool inline_one_fragment(Xml.Doc* doc, Xml.Node* include) {
  208. var name = include->get_prop("name");
  209. if (name == null) {
  210. return false;
  211. }
  212. Xml.Doc* fragment_doc;
  213. if (!fragments_by_name.try_get((!)name, out fragment_doc)) {
  214. return false;
  215. }
  216. var fragment_body = find_body(fragment_doc);
  217. if (fragment_body == null) {
  218. return false;
  219. }
  220. var inputs = collect_input_names(fragment_body);
  221. var materialise_as = include->get_prop("pstm-materialise-as");
  222. bool wrap = (materialise_as != null) || inputs.length > 0;
  223. string tag = materialise_as ?? "div";
  224. if (wrap) {
  225. var parent = include->parent;
  226. if (parent == null) {
  227. return false;
  228. }
  229. var wrapper = parent->new_child(null, tag, null);
  230. wrapper->unlink();
  231. include->add_prev_sibling(wrapper);
  232. for (var child = fragment_body->children; child != null; child = child->next) {
  233. var copied = child->doc_copy(doc, 1);
  234. if (copied != null) {
  235. wrapper->add_child(copied);
  236. }
  237. }
  238. foreach (var input_name in inputs) {
  239. var expr = include->get_prop(input_name);
  240. if (expr != null) {
  241. wrapper->set_prop(@"stm-def-$input_name-as", (!)expr);
  242. }
  243. }
  244. } else {
  245. for (var child = fragment_body->children; child != null; child = child->next) {
  246. var copied = child->doc_copy(doc, 1);
  247. if (copied != null) {
  248. include->add_prev_sibling(copied);
  249. }
  250. }
  251. }
  252. include->unlink();
  253. return true;
  254. }
  255. private string[] collect_input_names(Xml.Node* fragment_body) {
  256. var names = new string[0];
  257. for (var n = fragment_body->children; n != null; n = n->next) {
  258. if (is_element(n) && (string) n->name == "pstm-input") {
  259. var input_name = n->get_prop("name");
  260. if (input_name != null) {
  261. names += (!)input_name;
  262. }
  263. }
  264. }
  265. return names;
  266. }
  267. private void rewrite_resources(Xml.Doc* doc) {
  268. var root = doc->get_root_element();
  269. if (root != null) {
  270. rewrite_resources_in(root);
  271. }
  272. }
  273. private void rewrite_resources_in(Xml.Node* node) {
  274. if (!is_element(node)) {
  275. return;
  276. }
  277. string[] to_add_attr = new string[0];
  278. string[] to_add_val = new string[0];
  279. string[] to_remove = new string[0];
  280. for (var attr = node->properties; attr != null; attr = attr->next) {
  281. var attr_name = (string) attr->name;
  282. if (attr_name.has_prefix("pstm-res-")) {
  283. var target_attr = attr_name.substring("pstm-res-".length);
  284. var value = node->get_prop(attr_name);
  285. if (value != null) {
  286. to_add_attr += target_attr;
  287. to_add_val += @"/_statum/resource/$((!)value)";
  288. }
  289. to_remove += attr_name;
  290. }
  291. }
  292. for (int i = 0; i < to_add_attr.length; i++) {
  293. node->set_prop(to_add_attr[i], to_add_val[i]);
  294. }
  295. foreach (var attr_name in to_remove) {
  296. node->unset_prop(attr_name);
  297. }
  298. for (var child = node->children; child != null; child = child->next) {
  299. rewrite_resources_in(child);
  300. }
  301. }
  302. // ------------------------------------------------------------------
  303. // Group/guard collection
  304. // ------------------------------------------------------------------
  305. private Collected collect(Xml.Doc* doc) {
  306. var result = new Collected();
  307. var root = doc->get_root_element();
  308. if (root != null) {
  309. collect_guards(root, result);
  310. collect_groups_from(root, result);
  311. }
  312. return result;
  313. }
  314. private void collect_guards(Xml.Node* node, Collected result) {
  315. if (!is_element(node)) {
  316. return;
  317. }
  318. if (node->get_prop("pstm-guard") != null) {
  319. result.guards += new Guard() {
  320. predicate = node->get_prop("predicate") ?? "",
  321. require = node->get_prop("require"),
  322. redirect = node->get_prop("redirect"),
  323. status = node->get_prop("status")
  324. };
  325. }
  326. for (var child = node->children; child != null; child = child->next) {
  327. collect_guards(child, result);
  328. }
  329. }
  330. private void collect_groups_from(Xml.Node* node, Collected result) {
  331. var n = node->children;
  332. while (n != null) {
  333. if (!is_element(n)) {
  334. n = n->next;
  335. continue;
  336. }
  337. if (n->get_prop("pstm-if") != null) {
  338. var chain = gather_chain(n);
  339. result.groups += chain_to_group(chain);
  340. foreach (var branch in chain) {
  341. collect_groups_from(branch, result);
  342. }
  343. n = chain[chain.length - 1]->next;
  344. continue;
  345. }
  346. collect_groups_from(n, result);
  347. n = n->next;
  348. }
  349. }
  350. private Xml.Node*[] gather_chain(Xml.Node* if_node) {
  351. Xml.Node*[] chain = new Xml.Node*[0];
  352. chain += if_node;
  353. var n = if_node->next;
  354. while (n != null) {
  355. if (!is_element(n)) {
  356. n = n->next;
  357. continue;
  358. }
  359. if (n->get_prop("pstm-else-if") != null || n->get_prop("pstm-else") != null) {
  360. chain += n;
  361. n = n->next;
  362. } else {
  363. break;
  364. }
  365. }
  366. return chain;
  367. }
  368. private IfGroup chain_to_group(Xml.Node*[] chain) {
  369. var conditions = new string[0];
  370. bool has_else = false;
  371. foreach (var node in chain) {
  372. var else_if = node->get_prop("pstm-else-if");
  373. if (node->get_prop("pstm-if") != null) {
  374. conditions += node->get_prop("pstm-if") ?? "";
  375. } else if (else_if != null) {
  376. conditions += (!)else_if;
  377. } else if (node->get_prop("pstm-else") != null) {
  378. has_else = true;
  379. }
  380. }
  381. return new IfGroup() { conditions = conditions, has_else = has_else };
  382. }
  383. // ------------------------------------------------------------------
  384. // Variant rendering
  385. // ------------------------------------------------------------------
  386. private VariantData[] build_variants(Xml.Doc* doc, IfGroup[] groups) throws GLib.Error {
  387. int total = 1;
  388. foreach (var group in groups) {
  389. total *= (group.conditions.length + 1);
  390. }
  391. if (total < 1) {
  392. total = 1;
  393. }
  394. VariantData[] variants = new VariantData[0];
  395. for (var index = 0; index < total; index++) {
  396. int[] choices = new int[groups.length];
  397. int stride = 1;
  398. for (int gi = 0; gi < groups.length; gi++) {
  399. int outcomes = groups[gi].conditions.length + 1;
  400. choices[gi] = (index / stride) % outcomes;
  401. stride *= outcomes;
  402. }
  403. var copy = doc->copy(1);
  404. apply_choices(copy, choices);
  405. strip_pstm(copy);
  406. var html = serialize(copy);
  407. delete copy;
  408. variants += compress_variant(html);
  409. }
  410. return variants;
  411. }
  412. private VariantData compress_variant(string html) throws GLib.Error {
  413. var data = new VariantData();
  414. data.identity = html.data;
  415. var buffer = new ByteBuffer.from_byte_array(html.data);
  416. var gzip = new GzipCompressor(9).compress_buffer(buffer, null).to_array();
  417. var zstd = new ZstdCompressor(19).compress_buffer(buffer, null).to_array();
  418. var br = new BrotliCompressor(11).compress_buffer(buffer, null).to_array();
  419. data.has_gzip = gzip.length < html.length;
  420. data.has_zstd = zstd.length < html.length;
  421. data.has_br = br.length < html.length;
  422. data.gzip = data.has_gzip ? gzip : null;
  423. data.zstd = data.has_zstd ? zstd : null;
  424. data.br = data.has_br ? br : null;
  425. return data;
  426. }
  427. private void apply_choices(Xml.Doc* doc, int[] choices) {
  428. var root = doc->get_root_element();
  429. if (root == null) {
  430. return;
  431. }
  432. int gi = 0;
  433. apply_choices_from(root, choices, ref gi);
  434. }
  435. private void apply_choices_from(Xml.Node* node, int[] choices, ref int gi) {
  436. var n = node->children;
  437. while (n != null) {
  438. if (!is_element(n)) {
  439. n = n->next;
  440. continue;
  441. }
  442. if (n->get_prop("pstm-if") != null) {
  443. var chain = gather_chain(n);
  444. var after = chain[chain.length - 1]->next;
  445. int conds = count_conditions(chain);
  446. int choice = (gi < choices.length) ? choices[gi] : conds;
  447. gi++;
  448. Xml.Node* keep = null;
  449. int idx = 0;
  450. foreach (var branch in chain) {
  451. bool is_conditional = branch->get_prop("pstm-if") != null || branch->get_prop("pstm-else-if") != null;
  452. if (is_conditional) {
  453. if (idx == choice) {
  454. keep = branch;
  455. }
  456. idx++;
  457. } else if (branch->get_prop("pstm-else") != null) {
  458. if (choice >= conds) {
  459. keep = branch;
  460. }
  461. }
  462. }
  463. foreach (var branch in chain) {
  464. if (branch == keep) {
  465. continue;
  466. }
  467. branch->unlink();
  468. }
  469. if (keep != null) {
  470. apply_choices_from(keep, choices, ref gi);
  471. }
  472. n = after;
  473. continue;
  474. }
  475. apply_choices_from(n, choices, ref gi);
  476. n = n->next;
  477. }
  478. }
  479. private int count_conditions(Xml.Node*[] chain) {
  480. int count = 0;
  481. foreach (var node in chain) {
  482. if (node->get_prop("pstm-if") != null || node->get_prop("pstm-else-if") != null) {
  483. count++;
  484. }
  485. }
  486. return count;
  487. }
  488. private void strip_pstm(Xml.Doc* doc) {
  489. var root = doc->get_root_element();
  490. if (root != null) {
  491. strip_pstm_from(root);
  492. }
  493. }
  494. private void strip_pstm_from(Xml.Node* node) {
  495. // pstm-guard's parameters (predicate/require/redirect/status) are not
  496. // pstm-prefixed, so remove them alongside the pstm-guard marker.
  497. bool is_guard = node->get_prop("pstm-guard") != null;
  498. string[] to_remove = new string[0];
  499. for (var attr = node->properties; attr != null; attr = attr->next) {
  500. var attr_name = (string) attr->name;
  501. if (attr_name.has_prefix("pstm-")) {
  502. to_remove += attr_name;
  503. } else if (is_guard && (attr_name == "predicate" || attr_name == "require" || attr_name == "redirect" || attr_name == "status")) {
  504. to_remove += attr_name;
  505. }
  506. }
  507. foreach (var attr_name in to_remove) {
  508. node->unset_prop(attr_name);
  509. }
  510. var child = node->children;
  511. while (child != null) {
  512. var next = child->next;
  513. if (is_element(child) && ((string) child->name).has_prefix("pstm-")) {
  514. child->unlink();
  515. } else if (is_element(child)) {
  516. strip_pstm_from(child);
  517. }
  518. child = next;
  519. }
  520. }
  521. // ------------------------------------------------------------------
  522. // Code generation
  523. // ------------------------------------------------------------------
  524. private string emit(string class_name, string route, Collected collected, VariantData[] variants) throws GLib.Error {
  525. var page_hash = compute_hash(variants.length > 0 ? variants[0].identity : new uint8[0]);
  526. var b = new StringBuilder();
  527. string i1 = namespace_name != null ? " " : "";
  528. string i2 = namespace_name != null ? " " : " ";
  529. string i3 = namespace_name != null ? " " : " ";
  530. string i4 = namespace_name != null ? " " : " ";
  531. b.append("using Statum;\n");
  532. b.append("using Invercargill;\n");
  533. b.append("using Invercargill.DataStructures;\n");
  534. b.append("using Astralis;\n\n");
  535. if (namespace_name != null) {
  536. b.append_printf("namespace %s {\n\n", namespace_name);
  537. }
  538. b.append_printf("%s// Generated by statum-mkpstm\n", i1);
  539. b.append_printf("%spublic class %s : StatumPage {\n\n", i1, class_name);
  540. b.append_printf("%spublic const string ROUTE = \"%s\";\n", i2, escape(route));
  541. b.append_printf("%spublic override string content_type { get { return \"text/html; charset=utf-8\"; } }\n\n", i2);
  542. emit_select_variant(b, i2, i3, collected, variants.length);
  543. emit_encoding_methods(b, i2, i3, i4, variants, page_hash);
  544. emit_byte_arrays(b, i2, i3, variants);
  545. emit_helpers(b, i2, i3);
  546. b.append_printf("%s}\n", i1);
  547. if (namespace_name != null) {
  548. b.append("}\n");
  549. }
  550. return b.str;
  551. }
  552. private void emit_select_variant(StringBuilder b, string i2, string i3, Collected collected, int variant_count) {
  553. b.append_printf("%spublic override StatumVariantSelection select_variant(ReadOnlyAssociative<string, HeldSlot> held) throws GLib.Error {\n", i2);
  554. foreach (var guard in collected.guards) {
  555. string predicate_check;
  556. var compiled = @"el_truthy($(compile_predicate_string(guard.predicate)))";
  557. if (guard.require != null) {
  558. predicate_check = @"held.has(\"$(escape((!)guard.require))\") && $(compiled)";
  559. } else {
  560. predicate_check = compiled;
  561. }
  562. if (guard.redirect != null) {
  563. b.append_printf("%sif (!(%s)) {\n", i3, predicate_check);
  564. b.append_printf("%sreturn StatumVariantSelection.redirect_to(\"%s\");\n", i3, escape((!)guard.redirect));
  565. b.append_printf("%s}\n", i3);
  566. } else {
  567. var status = guard.status ?? "FORBIDDEN";
  568. b.append_printf("%sif (!(%s)) {\n", i3, predicate_check);
  569. b.append_printf("%sreturn StatumVariantSelection.failure(StatusCode.%s);\n", i3, status);
  570. b.append_printf("%s}\n", i3);
  571. }
  572. }
  573. // Compute the variant index as a mixed-radix number of per-group choices.
  574. for (int gi = 0; gi < collected.groups.length; gi++) {
  575. var group = collected.groups[gi];
  576. b.append_printf("%sint g%d;\n", i3, gi);
  577. b.append_printf("%s{\n", i3);
  578. b.append_printf("%sint choice = %d;\n", i3, group.conditions.length);
  579. for (int ci = 0; ci < group.conditions.length; ci++) {
  580. var compiled = @"el_truthy($(compile_predicate_string(group.conditions[ci])))";
  581. if (ci == 0) {
  582. b.append_printf("%sif (%s) { choice = 0; }\n", i3, compiled);
  583. } else {
  584. b.append_printf("%selse if (%s) { choice = %d; }\n", i3, compiled, ci);
  585. }
  586. }
  587. b.append_printf("%sg%d = choice;\n", i3, gi);
  588. b.append_printf("%s}\n", i3);
  589. }
  590. if (collected.groups.length == 0) {
  591. b.append_printf("%sreturn StatumVariantSelection.variant(0);\n", i3);
  592. } else {
  593. b.append_printf("%sint index = 0;\n", i3);
  594. int stride = 1;
  595. for (int gi = 0; gi < collected.groups.length; gi++) {
  596. b.append_printf("%sindex += g%d * %d;\n", i3, gi, stride);
  597. stride *= collected.groups[gi].conditions.length + 1;
  598. }
  599. b.append_printf("%sif (index < 0 || index >= %d) { index = 0; }\n", i3, variant_count);
  600. b.append_printf("%sreturn StatumVariantSelection.variant(index);\n", i3);
  601. }
  602. b.append_printf("%s}\n\n", i2);
  603. }
  604. private void emit_encoding_methods(StringBuilder b, string i2, string i3, string i4, VariantData[] variants, string page_hash) {
  605. // get_best_encoding: prefer the smallest supported, available encoding.
  606. b.append_printf("%spublic override string get_best_encoding(int variant, Set<string> supported) {\n", i2);
  607. b.append_printf("%sif (supported.has(\"br\") && has_encoding(variant, 3)) return \"br\";\n", i3);
  608. b.append_printf("%sif (supported.has(\"zstd\") && has_encoding(variant, 2)) return \"zstd\";\n", i3);
  609. b.append_printf("%sif (supported.has(\"gzip\") && has_encoding(variant, 1)) return \"gzip\";\n", i3);
  610. b.append_printf("%sreturn \"identity\";\n", i3);
  611. b.append_printf("%s}\n\n", i2);
  612. // get_encoding: nested switch over (variant, encoding).
  613. b.append_printf("%spublic override unowned uint8[] get_encoding(int variant, string encoding) {\n", i2);
  614. b.append_printf("%sswitch (variant) {\n", i3);
  615. for (int v = 0; v < variants.length; v++) {
  616. b.append_printf("%scase %d:\n", i4, v);
  617. b.append_printf("%sswitch (encoding) {\n", i4);
  618. b.append_printf("%scase \"identity\": return V_%d_IDENTITY;\n", i4, v);
  619. if (variants[v].has_gzip) {
  620. b.append_printf("%scase \"gzip\": return V_%d_GZIP;\n", i4, v);
  621. }
  622. if (variants[v].has_zstd) {
  623. b.append_printf("%scase \"zstd\": return V_%d_ZSTD;\n", i4, v);
  624. }
  625. if (variants[v].has_br) {
  626. b.append_printf("%scase \"br\": return V_%d_BR;\n", i4, v);
  627. }
  628. b.append_printf("%sdefault: return V_%d_IDENTITY;\n", i4, v);
  629. b.append_printf("%s}\n", i4);
  630. }
  631. b.append_printf("%s}\n", i3);
  632. b.append_printf("%sreturn V_0_IDENTITY;\n", i3);
  633. b.append_printf("%s}\n\n", i2);
  634. // get_etag_for
  635. b.append_printf("%spublic override string get_etag_for(int variant, string encoding) {\n", i2);
  636. b.append_printf("%sreturn \"\\\"%s-%%s-%%d\\\"\".printf(encoding, variant);\n", i3, page_hash);
  637. b.append_printf("%s}\n\n", i2);
  638. // has_encoding
  639. b.append_printf("%sprivate static bool has_encoding(int variant, int encoding) {\n", i2);
  640. b.append_printf("%sswitch (variant) {\n", i3);
  641. for (int v = 0; v < variants.length; v++) {
  642. b.append_printf("%scase %d: return encoding == 0%s%s%s;\n", i4, v,
  643. variants[v].has_gzip ? " || encoding == 1" : "",
  644. variants[v].has_zstd ? " || encoding == 2" : "",
  645. variants[v].has_br ? " || encoding == 3" : "");
  646. }
  647. b.append_printf("%s}\n", i3);
  648. b.append_printf("%sreturn false;\n", i3);
  649. b.append_printf("%s}\n\n", i2);
  650. }
  651. private void emit_byte_arrays(StringBuilder b, string i2, string i3, VariantData[] variants) {
  652. for (int v = 0; v < variants.length; v++) {
  653. emit_one_array(b, i2, i3, @"V_$(v)_IDENTITY", variants[v].identity);
  654. if (variants[v].has_gzip && variants[v].gzip != null) {
  655. emit_one_array(b, i2, i3, @"V_$(v)_GZIP", (!)variants[v].gzip);
  656. }
  657. if (variants[v].has_zstd && variants[v].zstd != null) {
  658. emit_one_array(b, i2, i3, @"V_$(v)_ZSTD", (!)variants[v].zstd);
  659. }
  660. if (variants[v].has_br && variants[v].br != null) {
  661. emit_one_array(b, i2, i3, @"V_$(v)_BR", (!)variants[v].br);
  662. }
  663. }
  664. }
  665. private void emit_one_array(StringBuilder b, string i2, string i3, string name, uint8[] data) {
  666. b.append_printf("%sprivate const uint8[] %s = {\n", i2, name);
  667. for (int i = 0; i < data.length; i++) {
  668. if (i == 0) {
  669. b.append(i3);
  670. } else if (i % 16 == 0) {
  671. b.append(",\n").append(i3);
  672. } else {
  673. b.append(", ");
  674. }
  675. b.append_printf("0x%02x", data[i]);
  676. }
  677. b.append_printf("\n%s};\n\n", i2);
  678. }
  679. private void emit_helpers(StringBuilder b, string i2, string i3) {
  680. // Static helpers used by the compiled predicates in select_variant.
  681. // Each predicate AST node is translated at build time into calls to
  682. // these, so no expression is parsed/evaluated at request time.
  683. b.append_printf("%sprivate static Element? held_pub(ReadOnlyAssociative<string, HeldSlot> held, string type_name) {\n", i2);
  684. b.append_printf("%sHeldSlot slot;\n", i3);
  685. b.append_printf("%sif (held.try_get(type_name, out slot)) { return new NativeElement<Properties>(slot.@public); }\n", i3);
  686. b.append_printf("%sreturn null;\n", i3);
  687. b.append_printf("%s}\n", i2);
  688. b.append_printf("%sprivate static Element? held_priv(ReadOnlyAssociative<string, HeldSlot> held, string type_name) {\n", i2);
  689. b.append_printf("%sHeldSlot slot;\n", i3);
  690. b.append_printf("%sif (held.try_get(type_name, out slot)) { return new NativeElement<Properties>(slot.@private); }\n", i3);
  691. b.append_printf("%sreturn null;\n", i3);
  692. b.append_printf("%s}\n", i2);
  693. b.append_printf("%sprivate static Element? el_get(Element? target, string name) {\n", i2);
  694. b.append_printf("%sif (target == null) { return null; }\n", i3);
  695. b.append_printf("%sProperties props;\n", i3);
  696. b.append_printf("%sif (!((!)target).try_get_as<Properties>(out props)) { return null; }\n", i3);
  697. b.append_printf("%sElement value;\n", i3);
  698. b.append_printf("%sif (!props.try_get(name, out value)) { return null; }\n", i3);
  699. b.append_printf("%sreturn value;\n", i3);
  700. b.append_printf("%s}\n", i2);
  701. b.append_printf("%sprivate static bool el_is_null(Element? e) {\n", i2);
  702. b.append_printf("%sreturn e == null || ((!)e) is NullElement || ((!)e).is_null();\n", i3);
  703. b.append_printf("%s}\n", i2);
  704. b.append_printf("%sprivate static bool el_truthy(Element? e) {\n", i2);
  705. b.append_printf("%sif (el_is_null(e)) { return false; }\n", i3);
  706. b.append_printf("%sbool? b = ((!)e).as_bool_or_null();\n", i3);
  707. b.append_printf("%sreturn b != null ? (!)b : true;\n", i3);
  708. b.append_printf("%s}\n", i2);
  709. b.append_printf("%sprivate static double? el_num_of(Element e) {\n", i2);
  710. b.append_printf("%sint? i = e.as_int_or_null(); if (i != null) { return (double)(!)i; }\n", i3);
  711. b.append_printf("%sint64? i64 = e.as_int64_or_null(); if (i64 != null) { return (double)(!)i64; }\n", i3);
  712. b.append_printf("%sdouble? d = e.as_double_or_null(); if (d != null) { return d; }\n", i3);
  713. b.append_printf("%sfloat? f = e.as_float_or_null(); if (f != null) { return (double)(!)f; }\n", i3);
  714. b.append_printf("%slong? l = e.as_long_or_null(); if (l != null) { return (double)(!)l; }\n", i3);
  715. b.append_printf("%sreturn null;\n", i3);
  716. b.append_printf("%s}\n", i2);
  717. b.append_printf("%sprivate static double? el_num_of_opt(Element? e) {\n", i2);
  718. b.append_printf("%sif (el_is_null(e)) { return null; }\n", i3);
  719. b.append_printf("%sreturn el_num_of((!)e);\n", i3);
  720. b.append_printf("%s}\n", i2);
  721. b.append_printf("%sprivate static int? el_cmp_values(Element a, Element b) {\n", i2);
  722. b.append_printf("%sdouble? na = el_num_of(a), nb = el_num_of(b);\n", i3);
  723. b.append_printf("%sif (na != null && nb != null) { return na < nb ? -1 : (na > nb ? 1 : 0); }\n", i3);
  724. b.append_printf("%sstring? sa = a.as_string_or_null(), sb = b.as_string_or_null();\n", i3);
  725. b.append_printf("%sif (sa != null && sb != null) { int c = strcmp((!)sa, (!)sb); return c < 0 ? -1 : (c > 0 ? 1 : 0); }\n", i3);
  726. b.append_printf("%sreturn null;\n", i3);
  727. b.append_printf("%s}\n", i2);
  728. b.append_printf("%sprivate static bool el_values_equal(Element a, Element b) {\n", i2);
  729. b.append_printf("%sint? c = el_cmp_values(a, b);\n", i3);
  730. b.append_printf("%sif (c != null) { return (!)c == 0; }\n", i3);
  731. b.append_printf("%sbool? ba = a.as_bool_or_null(), bb = b.as_bool_or_null();\n", i3);
  732. b.append_printf("%sif (ba != null && bb != null) { return ba == bb; }\n", i3);
  733. b.append_printf("%sreturn false;\n", i3);
  734. b.append_printf("%s}\n", i2);
  735. b.append_printf("%sprivate static Element? el_str(string s) { return new NativeElement<string>(s); }\n", i2);
  736. b.append_printf("%sprivate static Element? el_num(double d) { return new NativeElement<double?>(d); }\n", i2);
  737. b.append_printf("%sprivate static Element? el_bool(bool b) { return new NativeElement<bool>(b); }\n", i2);
  738. b.append_printf("%sprivate static Element? el_null() { return new NullElement(); }\n", i2);
  739. b.append_printf("%sprivate static Element? el_eq(Element? a, Element? b) {\n", i2);
  740. b.append_printf("%sbool an = el_is_null(a), bn = el_is_null(b);\n", i3);
  741. b.append_printf("%sif (an || bn) { return el_bool(an == bn); }\n", i3);
  742. b.append_printf("%sreturn el_bool(el_values_equal((!)a, (!)b));\n", i3);
  743. b.append_printf("%s}\n", i2);
  744. b.append_printf("%sprivate static Element? el_neq(Element? a, Element? b) { return el_bool(!el_truthy(el_eq(a, b))); }\n", i2);
  745. b.append_printf("%sprivate static Element? el_gt(Element? a, Element? b) {\n", i2);
  746. b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3);
  747. b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3);
  748. b.append_printf("%sreturn el_bool(c != null && (!)c > 0);\n", i3);
  749. b.append_printf("%s}\n", i2);
  750. b.append_printf("%sprivate static Element? el_ge(Element? a, Element? b) {\n", i2);
  751. b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3);
  752. b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3);
  753. b.append_printf("%sreturn el_bool(c != null && (!)c >= 0);\n", i3);
  754. b.append_printf("%s}\n", i2);
  755. b.append_printf("%sprivate static Element? el_lt(Element? a, Element? b) {\n", i2);
  756. b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3);
  757. b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3);
  758. b.append_printf("%sreturn el_bool(c != null && (!)c < 0);\n", i3);
  759. b.append_printf("%s}\n", i2);
  760. b.append_printf("%sprivate static Element? el_le(Element? a, Element? b) {\n", i2);
  761. b.append_printf("%sif (el_is_null(a) || el_is_null(b)) { return el_bool(false); }\n", i3);
  762. b.append_printf("%sint? c = el_cmp_values((!)a, (!)b);\n", i3);
  763. b.append_printf("%sreturn el_bool(c != null && (!)c <= 0);\n", i3);
  764. b.append_printf("%s}\n", i2);
  765. b.append_printf("%sprivate static Element? el_and(Element? a, Element? b) { return el_bool(el_truthy(a) && el_truthy(b)); }\n", i2);
  766. b.append_printf("%sprivate static Element? el_or(Element? a, Element? b) { return el_bool(el_truthy(a) || el_truthy(b)); }\n", i2);
  767. b.append_printf("%sprivate static Element? el_not(Element? a) { return el_bool(!el_truthy(a)); }\n", i2);
  768. b.append_printf("%sprivate static Element? el_arith(Element? a, Element? b, string op) {\n", i2);
  769. b.append_printf("%sdouble? x = el_num_of_opt(a), y = el_num_of_opt(b);\n", i3);
  770. b.append_printf("%sif (x == null || y == null) { return null; }\n", i3);
  771. b.append_printf("%sdouble xv = (!)x, yv = (!)y;\n", i3);
  772. b.append_printf("%sif (op == \"+\") { return el_num(xv + yv); }\n", i3);
  773. b.append_printf("%sif (op == \"-\") { return el_num(xv - yv); }\n", i3);
  774. b.append_printf("%sif (op == \"*\") { return el_num(xv * yv); }\n", i3);
  775. b.append_printf("%sif (op == \"/\") { return yv == 0 ? null : el_num(xv / yv); }\n", i3);
  776. b.append_printf("%sif (op == \"%%\") { return yv == 0 ? null : el_num((double)((int64)xv %% (int64)yv)); }\n", i3);
  777. b.append_printf("%sreturn null;\n", i3);
  778. b.append_printf("%s}\n", i2);
  779. b.append_printf("%sprivate static Element? el_add(Element? a, Element? b) { return el_arith(a, b, \"+\"); }\n", i2);
  780. b.append_printf("%sprivate static Element? el_sub(Element? a, Element? b) { return el_arith(a, b, \"-\"); }\n", i2);
  781. b.append_printf("%sprivate static Element? el_mul(Element? a, Element? b) { return el_arith(a, b, \"*\"); }\n", i2);
  782. b.append_printf("%sprivate static Element? el_div(Element? a, Element? b) { return el_arith(a, b, \"/\"); }\n", i2);
  783. b.append_printf("%sprivate static Element? el_mod(Element? a, Element? b) { return el_arith(a, b, \"%%\"); }\n", i2);
  784. 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);
  785. b.append_printf("%sprivate static Element? el_ternary(Element? c, Element? a, Element? b) { return el_truthy(c) ? a : b; }\n", i2);
  786. b.append("\n");
  787. }
  788. // ------------------------------------------------------------------
  789. // Predicate compilation (build-time AST → Vala)
  790. // ------------------------------------------------------------------
  791. private string compile_predicate_string(string predicate) {
  792. try {
  793. return compile_predicate(ExpressionParser.parse(predicate));
  794. } catch (ExpressionError e) {
  795. // Unparseable predicate → always false.
  796. return "el_bool(false)";
  797. }
  798. }
  799. private string compile_predicate(Expression expr) {
  800. if (expr is VariableExpression) {
  801. var v = (VariableExpression) expr;
  802. return @"held_pub(held, \"$(escape(v.variable_name))\")";
  803. }
  804. if (expr is PropertyExpression) {
  805. var p = (PropertyExpression) expr;
  806. if (p.property_name == "private" && p.target is VariableExpression) {
  807. var root = (VariableExpression) p.target;
  808. return @"held_priv(held, \"$(escape(root.variable_name))\")";
  809. }
  810. return @"el_get($(compile_predicate(p.target)), \"$(escape(p.property_name))\")";
  811. }
  812. if (expr is LiteralExpression) {
  813. return compile_literal((LiteralExpression) expr);
  814. }
  815. if (expr is UnaryExpression) {
  816. var u = (UnaryExpression) expr;
  817. var operand = compile_predicate(u.operand);
  818. if (u.operator == UnaryOperator.NOT) {
  819. return @"el_not($(operand))";
  820. }
  821. if (u.operator == UnaryOperator.NEGATE) {
  822. return @"el_neg($(operand))";
  823. }
  824. return "el_null()";
  825. }
  826. if (expr is BinaryExpression) {
  827. return compile_binary((BinaryExpression) expr);
  828. }
  829. if (expr is BracketedExpression) {
  830. return compile_predicate(((BracketedExpression) expr).inner);
  831. }
  832. if (expr is TernaryExpression) {
  833. var t = (TernaryExpression) expr;
  834. return @"el_ternary($(compile_predicate(t.condition)), $(compile_predicate(t.true_expression)), $(compile_predicate(t.false_expression)))";
  835. }
  836. // Unsupported node (function call, lambda, lot literal, parameter).
  837. return "el_null()";
  838. }
  839. private string compile_binary(BinaryExpression b) {
  840. var l = compile_predicate(b.left);
  841. var r = compile_predicate(b.right);
  842. switch (b.op) {
  843. case BinaryOperator.EQUAL: return @"el_eq($(l), $(r))";
  844. case BinaryOperator.NOT_EQUAL: return @"el_neq($(l), $(r))";
  845. case BinaryOperator.GREATER_THAN: return @"el_gt($(l), $(r))";
  846. case BinaryOperator.LESS_THAN: return @"el_lt($(l), $(r))";
  847. case BinaryOperator.GREATER_EQUAL: return @"el_ge($(l), $(r))";
  848. case BinaryOperator.LESS_EQUAL: return @"el_le($(l), $(r))";
  849. case BinaryOperator.AND: return @"el_and($(l), $(r))";
  850. case BinaryOperator.OR: return @"el_or($(l), $(r))";
  851. case BinaryOperator.ADD: return @"el_add($(l), $(r))";
  852. case BinaryOperator.SUBTRACT: return @"el_sub($(l), $(r))";
  853. case BinaryOperator.MULTIPLY: return @"el_mul($(l), $(r))";
  854. case BinaryOperator.DIVIDE: return @"el_div($(l), $(r))";
  855. case BinaryOperator.MODULO: return @"el_mod($(l), $(r))";
  856. default: return "el_null()";
  857. }
  858. }
  859. private string compile_literal(LiteralExpression lit) {
  860. var v = lit.value;
  861. if (v == null || v is NullElement) {
  862. return "el_null()";
  863. }
  864. string? s;
  865. if (v.try_get_as<string>(out s)) {
  866. return @"el_str(\"$(escape_vala_string((!)s))\")";
  867. }
  868. bool? bv;
  869. if (v.try_get_as<bool>(out bv)) {
  870. return (!)bv ? "el_bool(true)" : "el_bool(false)";
  871. }
  872. var d = numeric_value_of(v);
  873. if (d != null) {
  874. return @"el_num($((!)d))";
  875. }
  876. return "el_null()";
  877. }
  878. private static double? numeric_value_of(Invercargill.Element v) {
  879. int64? i64;
  880. if (v.try_get_as<int64?>(out i64)) {
  881. return (double)(!)i64;
  882. }
  883. int? i;
  884. if (v.try_get_as<int?>(out i)) {
  885. return (double)(!)i;
  886. }
  887. double? d;
  888. if (v.try_get_as<double?>(out d)) {
  889. return d;
  890. }
  891. float? f;
  892. if (v.try_get_as<float?>(out f)) {
  893. return (double)(!)f;
  894. }
  895. long? l;
  896. if (v.try_get_as<long?>(out l)) {
  897. return (double)(!)l;
  898. }
  899. return null;
  900. }
  901. private static string escape_vala_string(string s) {
  902. return s.replace("\\", "\\\\")
  903. .replace("\"", "\\\"")
  904. .replace("\n", "\\n")
  905. .replace("\r", "\\r")
  906. .replace("\t", "\\t");
  907. }
  908. // ------------------------------------------------------------------
  909. // DOM helpers
  910. // ------------------------------------------------------------------
  911. private Xml.Doc* parse_html_file(string path) throws GLib.Error {
  912. var file = File.new_for_path(path);
  913. if (!file.query_exists()) {
  914. throw new IOError.NOT_FOUND(@"Page file '$path' does not exist");
  915. }
  916. uint8[] contents;
  917. string etag_out;
  918. file.load_contents(null, out contents, out etag_out);
  919. // Parse with libxml2's HTML parser (mirrors Astralis' MarkupDocument)
  920. // so the tree has HTML semantics and serializes as HTML, not XML.
  921. int opts = (int)(Html.ParserOption.RECOVER |
  922. Html.ParserOption.NOERROR |
  923. Html.ParserOption.NOWARNING |
  924. Html.ParserOption.NOBLANKS |
  925. Html.ParserOption.NONET);
  926. char[] buffer = ((string) contents).to_utf8();
  927. var doc = Html.Doc.read_memory(buffer, buffer.length, path, "utf-8", opts);
  928. if (doc == null) {
  929. throw new IOError.FAILED(@"Failed to parse HTML file '$path'");
  930. }
  931. return doc;
  932. }
  933. private static Xml.Node* find_body(Xml.Doc* doc) {
  934. var root = doc->get_root_element();
  935. if (root == null) {
  936. return null;
  937. }
  938. return find_first_element(root, "body");
  939. }
  940. private static Xml.Node* find_first_element(Xml.Node* root, string name) {
  941. if (!is_element(root)) {
  942. return null;
  943. }
  944. if (((string) root->name).down() == name.down()) {
  945. return root;
  946. }
  947. for (var child = root->children; child != null; child = child->next) {
  948. var found = find_first_element(child, name);
  949. if (found != null) {
  950. return found;
  951. }
  952. }
  953. return null;
  954. }
  955. private static Xml.Node*[] find_all_elements(Xml.Doc* doc, string name) {
  956. var collected = new Series<Xml.Node*>();
  957. var root = doc->get_root_element();
  958. if (root != null) {
  959. find_all_from(root, name, collected);
  960. }
  961. return collected.to_array();
  962. }
  963. private static void find_all_from(Xml.Node* node, string name, Series<Xml.Node*> result) {
  964. if (is_element(node) && ((string) node->name).down() == name.down()) {
  965. result.add(node);
  966. }
  967. if (is_element(node)) {
  968. for (var child = node->children; child != null; child = child->next) {
  969. find_all_from(child, name, result);
  970. }
  971. }
  972. }
  973. private static string? read_directive_text(Xml.Node* body, string directive) {
  974. var node = find_first_element(body, directive);
  975. if (node == null) {
  976. return null;
  977. }
  978. var text = element_text(node);
  979. return text.strip().length > 0 ? text.strip() : null;
  980. }
  981. private static string? read_pstm_name(Xml.Doc* doc) {
  982. var node = find_first_element(doc->get_root_element(), "pstm-name");
  983. if (node == null) {
  984. return null;
  985. }
  986. return element_text(node).strip();
  987. }
  988. private static string[] read_routes(Xml.Doc* doc) {
  989. string[] routes = new string[0];
  990. var nodes = find_all_elements(doc, "pstm-uri");
  991. foreach (var node in nodes) {
  992. var text = element_text(node).strip();
  993. if (text.length > 0) {
  994. routes += text;
  995. }
  996. }
  997. return routes;
  998. }
  999. private static string element_text(Xml.Node* node) {
  1000. var b = new StringBuilder();
  1001. for (var child = node->children; child != null; child = child->next) {
  1002. if (child->type == ElementType.TEXT_NODE && child->content != null) {
  1003. b.append((string) child->content);
  1004. } else if (is_element(child)) {
  1005. b.append(element_text(child));
  1006. }
  1007. }
  1008. return b.str;
  1009. }
  1010. private static void apply_title(Xml.Doc* doc, string? title) {
  1011. if (title == null || title.length == 0) {
  1012. return;
  1013. }
  1014. var head = find_first_element(doc->get_root_element(), "head");
  1015. if (head == null) {
  1016. return;
  1017. }
  1018. var title_node = find_first_element(head, "title");
  1019. if (title_node == null) {
  1020. title_node = head->new_child(null, "title", null);
  1021. }
  1022. for (var child = title_node->children; child != null; child = child->next) {
  1023. child->unlink();
  1024. }
  1025. title_node->add_content(title);
  1026. }
  1027. private static string serialize(Xml.Doc* doc) {
  1028. // Serialize with libxml2's HTML serializer (htmlDocDumpMemory) so the
  1029. // output is valid HTML: no <?xml?> prolog, no self-closing
  1030. // `<script/>`, and correct void/non-void element handling. The doc was
  1031. // produced by the HTML parser, so it carries HTML structure.
  1032. string mem;
  1033. int len;
  1034. ((Html.Doc*) doc)->dump_memory(out mem, out len);
  1035. return len > 0 ? mem.substring(0, len) : mem;
  1036. }
  1037. private static bool is_element(Xml.Node* node) {
  1038. return node != null && node->type == ElementType.ELEMENT_NODE;
  1039. }
  1040. private static string compute_hash(uint8[] data) {
  1041. var checksum = new Checksum(ChecksumType.SHA512);
  1042. checksum.update(data, data.length);
  1043. return checksum.get_string();
  1044. }
  1045. private static string escape(string s) {
  1046. return s.replace("\\", "\\\\").replace("\"", "\\\"");
  1047. }
  1048. private static void write_file(string path, string contents) throws GLib.Error {
  1049. var file = File.new_for_path(path);
  1050. var stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
  1051. stream.put_string(contents);
  1052. stream.close();
  1053. }
  1054. private static string make_pascal_case(string input) {
  1055. var basename = Path.get_basename(input);
  1056. var dot = basename.last_index_of(".");
  1057. if (dot > 0) {
  1058. basename = basename.substring(0, dot);
  1059. }
  1060. var result = new StringBuilder();
  1061. var capitalize_next = true;
  1062. foreach (var c in basename.data) {
  1063. if (c == '-' || c == '_' || c == ' ' || c == '.') {
  1064. capitalize_next = true;
  1065. } else {
  1066. if (capitalize_next) {
  1067. result.append_printf("%c", c >= 'a' && c <= 'z' ? c - 32 : c);
  1068. capitalize_next = false;
  1069. } else {
  1070. result.append_printf("%c", c);
  1071. }
  1072. }
  1073. }
  1074. return result.str;
  1075. }
  1076. // ------------------------------------------------------------------
  1077. // Model
  1078. // ------------------------------------------------------------------
  1079. private class Guard : Object {
  1080. public string predicate;
  1081. public string? require;
  1082. public string? redirect;
  1083. public string? status;
  1084. }
  1085. private class IfGroup : Object {
  1086. public string[] conditions = new string[0];
  1087. public bool has_else;
  1088. }
  1089. private class Collected : Object {
  1090. public Guard[] guards = new Guard[0];
  1091. public IfGroup[] groups = new IfGroup[0];
  1092. }
  1093. private class VariantData : Object {
  1094. public uint8[] identity;
  1095. public uint8[]? gzip;
  1096. public uint8[]? zstd;
  1097. public uint8[]? br;
  1098. public bool has_gzip;
  1099. public bool has_zstd;
  1100. public bool has_br;
  1101. }
  1102. }
  1103. }