tigerbeetle 0.0.34 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/ext/tb_client/extconf.rb +13 -13
  4. data/ext/tb_client/tigerbeetle/LICENSE +177 -0
  5. data/ext/tb_client/tigerbeetle/build.zig +2327 -0
  6. data/ext/tb_client/tigerbeetle/src/aof.zig +1000 -0
  7. data/ext/tb_client/tigerbeetle/src/build_multiversion.zig +808 -0
  8. data/ext/tb_client/tigerbeetle/src/cdc/amqp/protocol.zig +1283 -0
  9. data/ext/tb_client/tigerbeetle/src/cdc/amqp/spec.zig +1704 -0
  10. data/ext/tb_client/tigerbeetle/src/cdc/amqp/types.zig +341 -0
  11. data/ext/tb_client/tigerbeetle/src/cdc/amqp.zig +1450 -0
  12. data/ext/tb_client/tigerbeetle/src/cdc/runner.zig +1659 -0
  13. data/ext/tb_client/tigerbeetle/src/clients/c/samples/main.c +406 -0
  14. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/context.zig +1084 -0
  15. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/echo_client.zig +286 -0
  16. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/packet.zig +158 -0
  17. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal.zig +229 -0
  18. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal_fuzz.zig +110 -0
  19. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.h +386 -0
  20. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.zig +34 -0
  21. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_exports.zig +281 -0
  22. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header.zig +312 -0
  23. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header_test.zig +138 -0
  24. data/ext/tb_client/tigerbeetle/src/clients/c/test.zig +466 -0
  25. data/ext/tb_client/tigerbeetle/src/clients/docs_samples.zig +157 -0
  26. data/ext/tb_client/tigerbeetle/src/clients/docs_types.zig +90 -0
  27. data/ext/tb_client/tigerbeetle/src/clients/dotnet/ci.zig +203 -0
  28. data/ext/tb_client/tigerbeetle/src/clients/dotnet/docs.zig +79 -0
  29. data/ext/tb_client/tigerbeetle/src/clients/dotnet/dotnet_bindings.zig +542 -0
  30. data/ext/tb_client/tigerbeetle/src/clients/go/ci.zig +109 -0
  31. data/ext/tb_client/tigerbeetle/src/clients/go/docs.zig +86 -0
  32. data/ext/tb_client/tigerbeetle/src/clients/go/go_bindings.zig +370 -0
  33. data/ext/tb_client/tigerbeetle/src/clients/go/pkg/native/tb_client.h +386 -0
  34. data/ext/tb_client/tigerbeetle/src/clients/java/ci.zig +167 -0
  35. data/ext/tb_client/tigerbeetle/src/clients/java/docs.zig +126 -0
  36. data/ext/tb_client/tigerbeetle/src/clients/java/java_bindings.zig +996 -0
  37. data/ext/tb_client/tigerbeetle/src/clients/java/src/client.zig +748 -0
  38. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni.zig +3238 -0
  39. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_tests.zig +1718 -0
  40. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_thread_cleaner.zig +190 -0
  41. data/ext/tb_client/tigerbeetle/src/clients/node/ci.zig +104 -0
  42. data/ext/tb_client/tigerbeetle/src/clients/node/docs.zig +75 -0
  43. data/ext/tb_client/tigerbeetle/src/clients/node/node.zig +522 -0
  44. data/ext/tb_client/tigerbeetle/src/clients/node/node_bindings.zig +267 -0
  45. data/ext/tb_client/tigerbeetle/src/clients/node/src/c.zig +3 -0
  46. data/ext/tb_client/tigerbeetle/src/clients/node/src/translate.zig +379 -0
  47. data/ext/tb_client/tigerbeetle/src/clients/python/ci.zig +131 -0
  48. data/ext/tb_client/tigerbeetle/src/clients/python/docs.zig +63 -0
  49. data/ext/tb_client/tigerbeetle/src/clients/python/python_bindings.zig +588 -0
  50. data/ext/tb_client/tigerbeetle/src/clients/rust/assets/tb_client.h +386 -0
  51. data/ext/tb_client/tigerbeetle/src/clients/rust/ci.zig +73 -0
  52. data/ext/tb_client/tigerbeetle/src/clients/rust/docs.zig +106 -0
  53. data/ext/tb_client/tigerbeetle/src/clients/rust/rust_bindings.zig +305 -0
  54. data/ext/tb_client/tigerbeetle/src/config.zig +296 -0
  55. data/ext/tb_client/tigerbeetle/src/constants.zig +790 -0
  56. data/ext/tb_client/tigerbeetle/src/copyhound.zig +202 -0
  57. data/ext/tb_client/tigerbeetle/src/counting_allocator.zig +72 -0
  58. data/ext/tb_client/tigerbeetle/src/direction.zig +11 -0
  59. data/ext/tb_client/tigerbeetle/src/docs_website/build.zig +158 -0
  60. data/ext/tb_client/tigerbeetle/src/docs_website/src/content.zig +156 -0
  61. data/ext/tb_client/tigerbeetle/src/docs_website/src/docs.zig +252 -0
  62. data/ext/tb_client/tigerbeetle/src/docs_website/src/file_checker.zig +313 -0
  63. data/ext/tb_client/tigerbeetle/src/docs_website/src/html.zig +87 -0
  64. data/ext/tb_client/tigerbeetle/src/docs_website/src/page_writer.zig +63 -0
  65. data/ext/tb_client/tigerbeetle/src/docs_website/src/redirects.zig +47 -0
  66. data/ext/tb_client/tigerbeetle/src/docs_website/src/search_index_writer.zig +28 -0
  67. data/ext/tb_client/tigerbeetle/src/docs_website/src/service_worker_writer.zig +61 -0
  68. data/ext/tb_client/tigerbeetle/src/docs_website/src/single_page_writer.zig +169 -0
  69. data/ext/tb_client/tigerbeetle/src/docs_website/src/website.zig +46 -0
  70. data/ext/tb_client/tigerbeetle/src/ewah.zig +445 -0
  71. data/ext/tb_client/tigerbeetle/src/ewah_benchmark.zig +128 -0
  72. data/ext/tb_client/tigerbeetle/src/ewah_fuzz.zig +171 -0
  73. data/ext/tb_client/tigerbeetle/src/fuzz_tests.zig +179 -0
  74. data/ext/tb_client/tigerbeetle/src/integration_tests.zig +662 -0
  75. data/ext/tb_client/tigerbeetle/src/io/common.zig +155 -0
  76. data/ext/tb_client/tigerbeetle/src/io/darwin.zig +1093 -0
  77. data/ext/tb_client/tigerbeetle/src/io/linux.zig +1880 -0
  78. data/ext/tb_client/tigerbeetle/src/io/test.zig +1005 -0
  79. data/ext/tb_client/tigerbeetle/src/io/windows.zig +1598 -0
  80. data/ext/tb_client/tigerbeetle/src/io.zig +34 -0
  81. data/ext/tb_client/tigerbeetle/src/iops.zig +134 -0
  82. data/ext/tb_client/tigerbeetle/src/list.zig +236 -0
  83. data/ext/tb_client/tigerbeetle/src/lsm/binary_search.zig +848 -0
  84. data/ext/tb_client/tigerbeetle/src/lsm/binary_search_benchmark.zig +179 -0
  85. data/ext/tb_client/tigerbeetle/src/lsm/cache_map.zig +424 -0
  86. data/ext/tb_client/tigerbeetle/src/lsm/cache_map_fuzz.zig +420 -0
  87. data/ext/tb_client/tigerbeetle/src/lsm/compaction.zig +2117 -0
  88. data/ext/tb_client/tigerbeetle/src/lsm/composite_key.zig +182 -0
  89. data/ext/tb_client/tigerbeetle/src/lsm/forest.zig +1119 -0
  90. data/ext/tb_client/tigerbeetle/src/lsm/forest_fuzz.zig +1102 -0
  91. data/ext/tb_client/tigerbeetle/src/lsm/forest_table_iterator.zig +200 -0
  92. data/ext/tb_client/tigerbeetle/src/lsm/groove.zig +1495 -0
  93. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge.zig +739 -0
  94. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge_benchmark.zig +166 -0
  95. data/ext/tb_client/tigerbeetle/src/lsm/manifest.zig +754 -0
  96. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level.zig +1294 -0
  97. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level_fuzz.zig +510 -0
  98. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log.zig +1263 -0
  99. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log_fuzz.zig +628 -0
  100. data/ext/tb_client/tigerbeetle/src/lsm/node_pool.zig +247 -0
  101. data/ext/tb_client/tigerbeetle/src/lsm/scan_buffer.zig +116 -0
  102. data/ext/tb_client/tigerbeetle/src/lsm/scan_builder.zig +543 -0
  103. data/ext/tb_client/tigerbeetle/src/lsm/scan_fuzz.zig +938 -0
  104. data/ext/tb_client/tigerbeetle/src/lsm/scan_lookup.zig +293 -0
  105. data/ext/tb_client/tigerbeetle/src/lsm/scan_merge.zig +362 -0
  106. data/ext/tb_client/tigerbeetle/src/lsm/scan_range.zig +99 -0
  107. data/ext/tb_client/tigerbeetle/src/lsm/scan_state.zig +17 -0
  108. data/ext/tb_client/tigerbeetle/src/lsm/scan_tree.zig +1036 -0
  109. data/ext/tb_client/tigerbeetle/src/lsm/schema.zig +617 -0
  110. data/ext/tb_client/tigerbeetle/src/lsm/scratch_memory.zig +84 -0
  111. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array.zig +1500 -0
  112. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_benchmark.zig +149 -0
  113. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_fuzz.zig +7 -0
  114. data/ext/tb_client/tigerbeetle/src/lsm/set_associative_cache.zig +865 -0
  115. data/ext/tb_client/tigerbeetle/src/lsm/table.zig +607 -0
  116. data/ext/tb_client/tigerbeetle/src/lsm/table_memory.zig +843 -0
  117. data/ext/tb_client/tigerbeetle/src/lsm/table_value_iterator.zig +105 -0
  118. data/ext/tb_client/tigerbeetle/src/lsm/timestamp_range.zig +40 -0
  119. data/ext/tb_client/tigerbeetle/src/lsm/tree.zig +630 -0
  120. data/ext/tb_client/tigerbeetle/src/lsm/tree_fuzz.zig +933 -0
  121. data/ext/tb_client/tigerbeetle/src/lsm/zig_zag_merge.zig +557 -0
  122. data/ext/tb_client/tigerbeetle/src/message_buffer.zig +469 -0
  123. data/ext/tb_client/tigerbeetle/src/message_bus.zig +1214 -0
  124. data/ext/tb_client/tigerbeetle/src/message_bus_fuzz.zig +936 -0
  125. data/ext/tb_client/tigerbeetle/src/message_pool.zig +343 -0
  126. data/ext/tb_client/tigerbeetle/src/multiversion.zig +2195 -0
  127. data/ext/tb_client/tigerbeetle/src/queue.zig +390 -0
  128. data/ext/tb_client/tigerbeetle/src/repl/completion.zig +201 -0
  129. data/ext/tb_client/tigerbeetle/src/repl/parser.zig +1356 -0
  130. data/ext/tb_client/tigerbeetle/src/repl/terminal.zig +496 -0
  131. data/ext/tb_client/tigerbeetle/src/repl.zig +1034 -0
  132. data/ext/tb_client/tigerbeetle/src/scripts/amqp.zig +973 -0
  133. data/ext/tb_client/tigerbeetle/src/scripts/cfo.zig +1866 -0
  134. data/ext/tb_client/tigerbeetle/src/scripts/changelog.zig +304 -0
  135. data/ext/tb_client/tigerbeetle/src/scripts/ci.zig +227 -0
  136. data/ext/tb_client/tigerbeetle/src/scripts/client_readmes.zig +658 -0
  137. data/ext/tb_client/tigerbeetle/src/scripts/devhub.zig +466 -0
  138. data/ext/tb_client/tigerbeetle/src/scripts/release.zig +1058 -0
  139. data/ext/tb_client/tigerbeetle/src/scripts.zig +105 -0
  140. data/ext/tb_client/tigerbeetle/src/shell.zig +1195 -0
  141. data/ext/tb_client/tigerbeetle/src/stack.zig +260 -0
  142. data/ext/tb_client/tigerbeetle/src/state_machine/auditor.zig +911 -0
  143. data/ext/tb_client/tigerbeetle/src/state_machine/workload.zig +2079 -0
  144. data/ext/tb_client/tigerbeetle/src/state_machine.zig +4872 -0
  145. data/ext/tb_client/tigerbeetle/src/state_machine_fuzz.zig +288 -0
  146. data/ext/tb_client/tigerbeetle/src/state_machine_tests.zig +3128 -0
  147. data/ext/tb_client/tigerbeetle/src/static_allocator.zig +82 -0
  148. data/ext/tb_client/tigerbeetle/src/stdx/bit_set.zig +157 -0
  149. data/ext/tb_client/tigerbeetle/src/stdx/bounded_array.zig +292 -0
  150. data/ext/tb_client/tigerbeetle/src/stdx/debug.zig +65 -0
  151. data/ext/tb_client/tigerbeetle/src/stdx/flags.zig +1414 -0
  152. data/ext/tb_client/tigerbeetle/src/stdx/mlock.zig +92 -0
  153. data/ext/tb_client/tigerbeetle/src/stdx/prng.zig +677 -0
  154. data/ext/tb_client/tigerbeetle/src/stdx/radix.zig +336 -0
  155. data/ext/tb_client/tigerbeetle/src/stdx/ring_buffer.zig +511 -0
  156. data/ext/tb_client/tigerbeetle/src/stdx/sort_test.zig +112 -0
  157. data/ext/tb_client/tigerbeetle/src/stdx/stdx.zig +1160 -0
  158. data/ext/tb_client/tigerbeetle/src/stdx/testing/low_level_hash_vectors.zig +142 -0
  159. data/ext/tb_client/tigerbeetle/src/stdx/testing/snaptest.zig +361 -0
  160. data/ext/tb_client/tigerbeetle/src/stdx/time_units.zig +275 -0
  161. data/ext/tb_client/tigerbeetle/src/stdx/unshare.zig +295 -0
  162. data/ext/tb_client/tigerbeetle/src/stdx/vendored/aegis.zig +436 -0
  163. data/ext/tb_client/tigerbeetle/src/stdx/windows.zig +48 -0
  164. data/ext/tb_client/tigerbeetle/src/stdx/zipfian.zig +402 -0
  165. data/ext/tb_client/tigerbeetle/src/storage.zig +489 -0
  166. data/ext/tb_client/tigerbeetle/src/storage_fuzz.zig +180 -0
  167. data/ext/tb_client/tigerbeetle/src/testing/bench.zig +146 -0
  168. data/ext/tb_client/tigerbeetle/src/testing/cluster/grid_checker.zig +53 -0
  169. data/ext/tb_client/tigerbeetle/src/testing/cluster/journal_checker.zig +61 -0
  170. data/ext/tb_client/tigerbeetle/src/testing/cluster/manifest_checker.zig +76 -0
  171. data/ext/tb_client/tigerbeetle/src/testing/cluster/message_bus.zig +110 -0
  172. data/ext/tb_client/tigerbeetle/src/testing/cluster/network.zig +412 -0
  173. data/ext/tb_client/tigerbeetle/src/testing/cluster/state_checker.zig +331 -0
  174. data/ext/tb_client/tigerbeetle/src/testing/cluster/storage_checker.zig +458 -0
  175. data/ext/tb_client/tigerbeetle/src/testing/cluster.zig +1198 -0
  176. data/ext/tb_client/tigerbeetle/src/testing/exhaustigen.zig +128 -0
  177. data/ext/tb_client/tigerbeetle/src/testing/fixtures.zig +181 -0
  178. data/ext/tb_client/tigerbeetle/src/testing/fuzz.zig +144 -0
  179. data/ext/tb_client/tigerbeetle/src/testing/id.zig +97 -0
  180. data/ext/tb_client/tigerbeetle/src/testing/io.zig +317 -0
  181. data/ext/tb_client/tigerbeetle/src/testing/marks.zig +126 -0
  182. data/ext/tb_client/tigerbeetle/src/testing/packet_simulator.zig +533 -0
  183. data/ext/tb_client/tigerbeetle/src/testing/reply_sequence.zig +154 -0
  184. data/ext/tb_client/tigerbeetle/src/testing/state_machine.zig +389 -0
  185. data/ext/tb_client/tigerbeetle/src/testing/storage.zig +1247 -0
  186. data/ext/tb_client/tigerbeetle/src/testing/table.zig +249 -0
  187. data/ext/tb_client/tigerbeetle/src/testing/time.zig +98 -0
  188. data/ext/tb_client/tigerbeetle/src/testing/tmp_tigerbeetle.zig +212 -0
  189. data/ext/tb_client/tigerbeetle/src/testing/vortex/constants.zig +26 -0
  190. data/ext/tb_client/tigerbeetle/src/testing/vortex/faulty_network.zig +580 -0
  191. data/ext/tb_client/tigerbeetle/src/testing/vortex/java_driver/ci.zig +39 -0
  192. data/ext/tb_client/tigerbeetle/src/testing/vortex/logged_process.zig +214 -0
  193. data/ext/tb_client/tigerbeetle/src/testing/vortex/rust_driver/ci.zig +34 -0
  194. data/ext/tb_client/tigerbeetle/src/testing/vortex/supervisor.zig +766 -0
  195. data/ext/tb_client/tigerbeetle/src/testing/vortex/workload.zig +543 -0
  196. data/ext/tb_client/tigerbeetle/src/testing/vortex/zig_driver.zig +181 -0
  197. data/ext/tb_client/tigerbeetle/src/tidy.zig +1448 -0
  198. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_driver.zig +227 -0
  199. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_load.zig +1069 -0
  200. data/ext/tb_client/tigerbeetle/src/tigerbeetle/cli.zig +1422 -0
  201. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect.zig +1658 -0
  202. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect_integrity.zig +518 -0
  203. data/ext/tb_client/tigerbeetle/src/tigerbeetle/libtb_client.zig +36 -0
  204. data/ext/tb_client/tigerbeetle/src/tigerbeetle/main.zig +646 -0
  205. data/ext/tb_client/tigerbeetle/src/tigerbeetle.zig +958 -0
  206. data/ext/tb_client/tigerbeetle/src/time.zig +236 -0
  207. data/ext/tb_client/tigerbeetle/src/trace/event.zig +745 -0
  208. data/ext/tb_client/tigerbeetle/src/trace/statsd.zig +462 -0
  209. data/ext/tb_client/tigerbeetle/src/trace.zig +556 -0
  210. data/ext/tb_client/tigerbeetle/src/unit_tests.zig +321 -0
  211. data/ext/tb_client/tigerbeetle/src/vopr.zig +1785 -0
  212. data/ext/tb_client/tigerbeetle/src/vortex.zig +101 -0
  213. data/ext/tb_client/tigerbeetle/src/vsr/checkpoint_trailer.zig +473 -0
  214. data/ext/tb_client/tigerbeetle/src/vsr/checksum.zig +208 -0
  215. data/ext/tb_client/tigerbeetle/src/vsr/checksum_benchmark.zig +43 -0
  216. data/ext/tb_client/tigerbeetle/src/vsr/client.zig +768 -0
  217. data/ext/tb_client/tigerbeetle/src/vsr/client_replies.zig +532 -0
  218. data/ext/tb_client/tigerbeetle/src/vsr/client_sessions.zig +338 -0
  219. data/ext/tb_client/tigerbeetle/src/vsr/clock.zig +1019 -0
  220. data/ext/tb_client/tigerbeetle/src/vsr/fault_detector.zig +279 -0
  221. data/ext/tb_client/tigerbeetle/src/vsr/free_set.zig +1381 -0
  222. data/ext/tb_client/tigerbeetle/src/vsr/free_set_fuzz.zig +315 -0
  223. data/ext/tb_client/tigerbeetle/src/vsr/grid.zig +1460 -0
  224. data/ext/tb_client/tigerbeetle/src/vsr/grid_blocks_missing.zig +757 -0
  225. data/ext/tb_client/tigerbeetle/src/vsr/grid_scrubber.zig +797 -0
  226. data/ext/tb_client/tigerbeetle/src/vsr/journal.zig +2586 -0
  227. data/ext/tb_client/tigerbeetle/src/vsr/marzullo.zig +308 -0
  228. data/ext/tb_client/tigerbeetle/src/vsr/message_header.zig +1777 -0
  229. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch.zig +715 -0
  230. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch_fuzz.zig +185 -0
  231. data/ext/tb_client/tigerbeetle/src/vsr/repair_budget.zig +333 -0
  232. data/ext/tb_client/tigerbeetle/src/vsr/replica.zig +12355 -0
  233. data/ext/tb_client/tigerbeetle/src/vsr/replica_format.zig +416 -0
  234. data/ext/tb_client/tigerbeetle/src/vsr/replica_reformat.zig +165 -0
  235. data/ext/tb_client/tigerbeetle/src/vsr/replica_test.zig +2910 -0
  236. data/ext/tb_client/tigerbeetle/src/vsr/routing.zig +1075 -0
  237. data/ext/tb_client/tigerbeetle/src/vsr/superblock.zig +1603 -0
  238. data/ext/tb_client/tigerbeetle/src/vsr/superblock_fuzz.zig +484 -0
  239. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums.zig +405 -0
  240. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +355 -0
  241. data/ext/tb_client/tigerbeetle/src/vsr/sync.zig +29 -0
  242. data/ext/tb_client/tigerbeetle/src/vsr.zig +1727 -0
  243. data/lib/tb_client/shared_lib.rb +12 -5
  244. data/lib/tigerbeetle/client.rb +1 -1
  245. data/lib/tigerbeetle/platforms.rb +9 -0
  246. data/lib/tigerbeetle/version.rb +2 -2
  247. data/tigerbeetle.gemspec +22 -5
  248. metadata +242 -3
  249. data/ext/tb_client/pkg.tar.gz +0 -0
@@ -0,0 +1,252 @@
1
+ const std = @import("std");
2
+ const Allocator = std.mem.Allocator;
3
+ const LazyPath = std.Build.LazyPath;
4
+ const Website = @import("website.zig").Website;
5
+ const Html = @import("html.zig").Html;
6
+ const content = @import("./content.zig");
7
+
8
+ const base_path = "../../docs";
9
+
10
+ const SearchIndexEntry = struct {
11
+ page_path: []const u8,
12
+ html_path: LazyPath,
13
+ };
14
+ const SearchIndex = std.ArrayList(SearchIndexEntry);
15
+
16
+ pub fn build(
17
+ b: *std.Build,
18
+ output: *std.Build.Step.WriteFile,
19
+ website: Website,
20
+ ) !void {
21
+ const arena = b.allocator;
22
+
23
+ var search_index = SearchIndex.init(arena);
24
+
25
+ var page_buffer: [1 << 16]u8 = undefined;
26
+ var base = try b.build_root.handle.openDir(base_path, .{});
27
+ defer base.close();
28
+
29
+ const root_page = try content.load(arena, base, &page_buffer);
30
+
31
+ try tree_install(b, website, output, &search_index, root_page, root_page);
32
+
33
+ const run_search_index_writer = b.addRunArtifact(b.addExecutable(.{
34
+ .name = "search_index_writer",
35
+ .root_source_file = b.path("src/search_index_writer.zig"),
36
+ .target = b.graph.host,
37
+ }));
38
+ for (search_index.items) |entry| {
39
+ run_search_index_writer.addArg(entry.page_path);
40
+ run_search_index_writer.addFileArg(entry.html_path);
41
+ }
42
+ _ = output.addCopyFile(
43
+ run_search_index_writer.captureStdOut(),
44
+ "search-index.json",
45
+ );
46
+
47
+ try write_single_page(b, website, &search_index, root_page, output);
48
+
49
+ try write_404_page(b, website, output);
50
+ }
51
+
52
+ fn tree_install(
53
+ b: *std.Build,
54
+ website: Website,
55
+ output: *std.Build.Step.WriteFile,
56
+ search_index: *SearchIndex,
57
+ root: content.Page,
58
+ page: content.Page,
59
+ ) !void {
60
+ try page_install(b, website, output, search_index, root, page);
61
+ for (page.children) |child| try tree_install(b, website, output, search_index, root, child);
62
+ }
63
+
64
+ fn page_install(
65
+ b: *std.Build,
66
+ website: Website,
67
+ output: *std.Build.Step.WriteFile,
68
+ search_index: *SearchIndex,
69
+ root: content.Page,
70
+ page: content.Page,
71
+ ) !void {
72
+ const page_html = run_pandoc(b, website.pandoc_bin, page.path);
73
+
74
+ const page_path = page_url(b.allocator, page);
75
+
76
+ try search_index.append(.{
77
+ .page_path = page_path,
78
+ .html_path = page_html,
79
+ });
80
+
81
+ const nav_html = try Html.create(b.allocator);
82
+ try nav_fill(website, nav_html, root, .{ .target = page });
83
+
84
+ try nav_html.write(
85
+ @embedFile("html/single-page-link.html"),
86
+ .{ .url_prefix = website.url_prefix },
87
+ );
88
+
89
+ // Add trailing slash.
90
+ const page_path_canonical = if (page_path.len == 0) "" else b.fmt("{s}/", .{page_path});
91
+
92
+ const page_out = website.write_page(.{
93
+ .title = page.content.title,
94
+ .page_path = page_path_canonical,
95
+ .nav = nav_html.string(),
96
+ .content = page_html,
97
+ });
98
+ _ = output.addCopyFile(page_out, b.pathJoin(&.{ page_path, "index.html" }));
99
+ }
100
+
101
+ fn page_url(arena: Allocator, page: content.Page) []const u8 {
102
+ const url = cut_suffix(page.path, "/README.md") orelse cut_suffix(page.path, ".md").?;
103
+ if (cut_prefix(url, "../src/clients/")) |client| {
104
+ // Special case: docs for clients are in `/src/clients/$lang`, not under `/docs`.
105
+ return std.mem.concat(arena, u8, &.{ "coding/clients/", client }) catch @panic("OOM");
106
+ }
107
+
108
+ if (url.len == 1 and url[0] == '.') return "";
109
+ return cut_prefix(url, "./").?;
110
+ }
111
+
112
+ fn url2slug(arena: Allocator, url: []const u8) []const u8 {
113
+ const slug = arena.dupe(u8, url) catch @panic("OOM");
114
+ std.mem.replaceScalar(u8, slug, '/', '-');
115
+ return std.mem.concat(arena, u8, &.{ "#", slug }) catch @panic("OOM");
116
+ }
117
+
118
+ fn nav_fill(website: Website, html: *Html, node: content.Page, options: struct {
119
+ target: content.Page,
120
+ single_page: bool = false,
121
+ }) !void {
122
+ try html.write("<ol>\n", .{});
123
+ for (node.children, node.content.children) |node_child, content_child| {
124
+ var url = page_url(html.arena, node_child);
125
+ if (options.single_page) {
126
+ url = url2slug(html.arena, url);
127
+ } else {
128
+ url = try std.fmt.allocPrint(html.arena, "{s}/{s}/", .{ website.url_prefix, url });
129
+ }
130
+
131
+ if (node_child.children.len > 0) {
132
+ try html.write("<li>\n<details", .{});
133
+ if (nav_contains(node_child, options.target)) try html.write(" open", .{});
134
+ try html.write("><summary class=\"item\">", .{});
135
+ try html.write(
136
+ \\<a href="$url">$title</a>
137
+ , .{
138
+ .url = url,
139
+ // Fabio: index page titles are too long
140
+ .title = content_child.title,
141
+ });
142
+ try html.write("</summary>\n", .{});
143
+ try nav_fill(website, html, node_child, options);
144
+ try html.write("</details></li>\n", .{});
145
+ } else {
146
+ try html.write(
147
+ \\<li class="item"><a href="$url"$class>$title</a></li>
148
+ \\
149
+ , .{
150
+ .url = url,
151
+ .class = if (nav_same_page(node_child, options.target)) " class=\"target\"" else "",
152
+ .title = content_child.title,
153
+ });
154
+ }
155
+ }
156
+ try html.write("</ol>\n", .{});
157
+ }
158
+
159
+ fn nav_contains(node: content.Page, target: content.Page) bool {
160
+ if (nav_same_page(node, target)) return true;
161
+ for (node.children) |child| {
162
+ if (nav_contains(child, target)) return true;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ fn nav_same_page(a: content.Page, b: content.Page) bool {
168
+ return std.mem.eql(u8, a.path, b.path);
169
+ }
170
+
171
+ fn run_pandoc(
172
+ b: *std.Build,
173
+ pandoc_bin: std.Build.LazyPath,
174
+ source: []const u8,
175
+ ) std.Build.LazyPath {
176
+ const pandoc_step = std.Build.Step.Run.create(b, "run pandoc");
177
+ pandoc_step.addFileArg(pandoc_bin);
178
+ pandoc_step.addArgs(&.{ "--from", "gfm+smart-tex_math_dollars", "--to", "html5" });
179
+ pandoc_step.addPrefixedFileArg("--lua-filter=", b.path("pandoc/markdown-links.lua"));
180
+ pandoc_step.addPrefixedFileArg("--lua-filter=", b.path("pandoc/anchor-links.lua"));
181
+ pandoc_step.addPrefixedFileArg("--lua-filter=", b.path("pandoc/table-wrapper.lua"));
182
+ pandoc_step.addPrefixedFileArg("--lua-filter=", b.path("pandoc/code-block-buttons.lua"));
183
+ pandoc_step.addPrefixedFileArg("--lua-filter=", b.path("pandoc/edit-link-footer.lua"));
184
+ pandoc_step.addArg("--reference-location=section");
185
+ const result = pandoc_step.addPrefixedOutputFileArg("--output=", "pandoc-out.html");
186
+ pandoc_step.addFileArg(b.path(base_path).path(b, source));
187
+ return result;
188
+ }
189
+
190
+ fn write_single_page(
191
+ b: *std.Build,
192
+ website: Website,
193
+ search_index: *const SearchIndex,
194
+ root: content.Page,
195
+ docs: *std.Build.Step.WriteFile,
196
+ ) !void {
197
+ const run_single_page_writer = b.addRunArtifact(b.addExecutable(.{
198
+ .name = "single_page_writer",
199
+ .root_source_file = b.path("src/single_page_writer.zig"),
200
+ .target = b.graph.host,
201
+ }));
202
+ for (search_index.items) |entry| {
203
+ run_single_page_writer.addArg(entry.page_path);
204
+ run_single_page_writer.addFileArg(entry.html_path);
205
+ }
206
+ const nav_html = try Html.create(b.allocator);
207
+ try nav_fill(website, nav_html, root, .{ .target = root, .single_page = true });
208
+
209
+ const single_page = website.write_page(.{
210
+ .page_path = "single-page/",
211
+ .include_search = false,
212
+ .nav = nav_html.string(),
213
+ .content = run_single_page_writer.captureStdOut(),
214
+ });
215
+
216
+ _ = docs.addCopyFile(single_page, "single-page/index.html");
217
+ }
218
+
219
+ fn write_404_page(
220
+ b: *std.Build,
221
+ website: Website,
222
+ docs: *std.Build.Step.WriteFile,
223
+ ) !void {
224
+ const template = @embedFile("html/404.html");
225
+ var html = try Html.create(b.allocator);
226
+ try html.write(template, .{
227
+ .url_prefix = website.url_prefix,
228
+ .title = "Page not found",
229
+ .author = "TigerBeetle Team",
230
+ });
231
+ _ = docs.add("404.html", html.string());
232
+ }
233
+
234
+ pub fn cut(haystack: []const u8, needle: []const u8) ?struct { []const u8, []const u8 } {
235
+ const index = std.mem.indexOf(u8, haystack, needle) orelse return null;
236
+
237
+ return .{ haystack[0..index], haystack[index + needle.len ..] };
238
+ }
239
+
240
+ pub fn cut_prefix(text: []const u8, comptime prefix: []const u8) ?[]const u8 {
241
+ return if (std.mem.startsWith(u8, text, prefix))
242
+ text[prefix.len..]
243
+ else
244
+ null;
245
+ }
246
+
247
+ pub fn cut_suffix(text: []const u8, comptime suffix: []const u8) ?[]const u8 {
248
+ return if (std.mem.endsWith(u8, text, suffix))
249
+ text[0 .. text.len - suffix.len]
250
+ else
251
+ null;
252
+ }
@@ -0,0 +1,313 @@
1
+ //! Sanity checks that all the generated files look reasonable.
2
+
3
+ const std = @import("std");
4
+ const log = std.log.scoped(.validate);
5
+ const assert = std.debug.assert;
6
+
7
+ const file_size_max = 166 * 1024;
8
+ const search_index_size_max = 2000 * 1024;
9
+ const single_page_size_max = 2000 * 1024;
10
+
11
+ // If this is set to true, we check if we get a 200 response for any external links.
12
+ const check_links_external: bool = false;
13
+
14
+ var file_cache: std.StringHashMap([]const u8) = undefined;
15
+
16
+ pub fn main() !void {
17
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
18
+ const allocator = arena.allocator();
19
+ file_cache = std.StringHashMap([]const u8).init(allocator);
20
+ var args = std.process.args();
21
+ _ = args.skip();
22
+ const path = args.next().?;
23
+ assert(args.next() == null);
24
+ try validate_dir(allocator, path);
25
+ }
26
+
27
+ fn classify_file(path: []const u8) enum { text, binary, exception, unexpected } {
28
+ const text: []const []const u8 =
29
+ &.{ ".css", ".html", ".js", ".json", ".svg", ".xml" };
30
+ const binary: []const []const u8 =
31
+ &.{ ".avif", ".gif", ".jpg", ".png", ".ttf", ".webp", ".woff2" };
32
+ const exceptions: []const []const u8 =
33
+ &.{ "CNAME", ".nojekyll" };
34
+
35
+ const extension = std.fs.path.extension(path);
36
+ for (text) |text_extension| {
37
+ if (std.mem.eql(u8, extension, text_extension)) return .text;
38
+ }
39
+
40
+ for (binary) |binary_extension| {
41
+ if (std.mem.eql(u8, extension, binary_extension)) return .binary;
42
+ }
43
+
44
+ for (exceptions) |exception| {
45
+ if (std.mem.eql(u8, exception, path)) return .exception;
46
+ }
47
+
48
+ return .unexpected;
49
+ }
50
+
51
+ fn validate_dir(arena: std.mem.Allocator, path: []const u8) !void {
52
+ var dir = try std.fs.cwd().openDir(path, .{ .iterate = true });
53
+ defer dir.close();
54
+
55
+ var walker = try dir.walk(arena);
56
+ defer walker.deinit();
57
+
58
+ while (try walker.next()) |entry| switch (entry.kind) {
59
+ .file => try validate_file(.{ .arena = arena, .dir = dir, .path = entry.path }),
60
+ .directory => {},
61
+ else => {
62
+ log.err("unexpected file type: '{s}'", .{
63
+ try dir.realpathAlloc(arena, entry.path),
64
+ });
65
+ return error.UnsupportedFileType;
66
+ },
67
+ };
68
+ }
69
+
70
+ const FileValidationContext = struct {
71
+ arena: std.mem.Allocator,
72
+ dir: std.fs.Dir,
73
+ path: []const u8,
74
+ };
75
+
76
+ fn validate_file(context: FileValidationContext) !void {
77
+ const stat = context.dir.statFile(context.path) catch |err| {
78
+ log.err("unable to stat file '{s}': {s}", .{
79
+ try context.dir.realpathAlloc(context.arena, context.path),
80
+ @errorName(err),
81
+ });
82
+ return err;
83
+ };
84
+ const size_max: u64 = if (std.mem.eql(u8, context.path, "search-index.json"))
85
+ search_index_size_max
86
+ else if (std.mem.eql(u8, context.path, "single-page/index.html"))
87
+ single_page_size_max
88
+ else
89
+ file_size_max;
90
+ if (stat.size > size_max) {
91
+ log.err("file '{s}' with size {:.2} exceeds max file size of {:.2}", .{
92
+ try context.dir.realpathAlloc(context.arena, context.path),
93
+ std.fmt.fmtIntSizeBin(stat.size),
94
+ std.fmt.fmtIntSizeBin(size_max),
95
+ });
96
+ return error.FileSizeExceeded;
97
+ }
98
+
99
+ switch (classify_file(context.path)) {
100
+ .text => try validate_text_file(context),
101
+ .binary => {}, // Nothing to validate.
102
+ .exception => {}, // Nothing to validate.
103
+ .unexpected => {
104
+ log.err("file '{s}' has unsupported type '{s}'", .{
105
+ try context.dir.realpathAlloc(context.arena, context.path),
106
+ std.fs.path.extension(context.path),
107
+ });
108
+ return error.UnsupportedFileType;
109
+ },
110
+ }
111
+ }
112
+
113
+ fn validate_text_file(context: FileValidationContext) !void {
114
+ assert(classify_file(context.path) == .text);
115
+
116
+ const file = try context.dir.openFile(context.path, .{});
117
+ defer file.close();
118
+
119
+ try file.seekFromEnd(-1);
120
+ const last_byte = try file.reader().readByte();
121
+ if (last_byte != '\n') {
122
+ log.err("file '{s}' doesn't end with a newline", .{
123
+ try context.dir.realpathAlloc(context.arena, context.path),
124
+ });
125
+ return error.MissingNewline;
126
+ }
127
+
128
+ if (std.mem.endsWith(u8, context.path, ".html")) {
129
+ try check_links(context);
130
+ }
131
+ }
132
+
133
+ fn read_file_cached(arena: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) ![]const u8 {
134
+ if (file_cache.get(path)) |content| return content;
135
+
136
+ const content = try dir.readFileAlloc(arena, path, 2 * 1024 * 1024);
137
+ try file_cache.put(try arena.dupe(u8, path), content);
138
+
139
+ return content;
140
+ }
141
+
142
+ // These links don't work with https.
143
+ const http_exceptions = std.StaticStringMap(void).initComptime(.{
144
+ .{"http://www.bailis.org/blog/linearizability-versus-serializability/"},
145
+ .{"http://pmg.csail.mit.edu/papers/vr-revisited.pdf"},
146
+ });
147
+
148
+ // These links cause TLS errors with std.http.Client.
149
+ const https_exceptions = std.StaticStringMap(void).initComptime(.{
150
+ .{"https://www.eecg.utoronto.ca/~yuan/papers/failure_analysis_osdi14.pdf"},
151
+ .{"https://pmg.csail.mit.edu/papers/vr.pdf"},
152
+ .{"https://www.infoq.com/presentations/LMAX/"},
153
+ .{"https://kernel.dk/io_uring.pdf"},
154
+ .{"https://research.cs.wisc.edu/wind/Publications/latent-sigmetrics07.pdf"},
155
+ .{"https://security.googleblog.com/2023/06/learnings-from-kctf-vrps-42-linux.html"},
156
+ });
157
+
158
+ fn check_links(context: FileValidationContext) !void {
159
+ const html = try read_file_cached(context.arena, context.dir, context.path);
160
+
161
+ var link_iterator = LinkIterator.init(html);
162
+ errdefer log.err("[link checker] error in {s}:{}", .{
163
+ context.dir.realpathAlloc(context.arena, context.path) catch unreachable,
164
+ link_iterator.line_number,
165
+ });
166
+
167
+ while (link_iterator.next()) |link| {
168
+ try check_link(context, link);
169
+ }
170
+ }
171
+
172
+ fn check_link(context: FileValidationContext, link: Link) !void {
173
+ // Check schema.
174
+ {
175
+ if (std.mem.startsWith(u8, link.base, "mailto:")) {
176
+ return; // Ignore.
177
+ }
178
+
179
+ if (std.mem.startsWith(u8, link.base, "https://")) {
180
+ return check_link_external(context.arena, link);
181
+ }
182
+
183
+ if (std.mem.startsWith(u8, link.base, "http://")) {
184
+ if (http_exceptions.has(link.base)) {
185
+ return check_link_external(context.arena, link);
186
+ }
187
+
188
+ log.err("found insecure link: '{s}'", .{link.base});
189
+ return error.InsecureLink;
190
+ }
191
+ }
192
+
193
+ if (std.mem.indexOf(u8, link.base, "//") != null or
194
+ std.mem.indexOf(u8, link.base, "/./") != null)
195
+ {
196
+ log.err("redundant slash: '{s}'", .{link.base});
197
+ return error.RedundantSlash;
198
+ }
199
+
200
+ // Locate local link target.
201
+ var target = link.base;
202
+ const is_absolute = target.len > 0 and target[0] == '/';
203
+ if (is_absolute) {
204
+ target = target[1..];
205
+ } else if (std.fs.path.dirname(context.path)) |dirname| {
206
+ target = try std.fs.path.join(context.arena, &.{ dirname, target });
207
+ }
208
+
209
+ const is_directory = std.fs.path.extension(target).len == 0;
210
+ if (is_directory) {
211
+ target = try std.fs.path.join(context.arena, &.{ target, "index.html" });
212
+ }
213
+
214
+ if (!try path_exists(context.dir, target)) {
215
+ log.err("link target not found: '{s}'", .{target});
216
+ return error.TargetNotFound;
217
+ }
218
+
219
+ if (link.fragment) |fragment| {
220
+ try check_link_fragment(context, target, fragment);
221
+ }
222
+ }
223
+
224
+ fn check_link_external(arena: std.mem.Allocator, link: Link) !void {
225
+ if (!check_links_external) return;
226
+ if (https_exceptions.has(link.base)) return;
227
+
228
+ errdefer |err| log.err("got {} while checking external link '{s}'", .{ err, link.base });
229
+
230
+ log.info("checking external link '{s}'", .{link.base});
231
+
232
+ var client = std.http.Client{ .allocator = arena };
233
+ defer client.deinit();
234
+
235
+ const uri = try std.Uri.parse(link.base);
236
+ var header_buffer: [512 * 1024]u8 = undefined;
237
+ var request = try client.open(.GET, uri, .{ .server_header_buffer = &header_buffer });
238
+ defer request.deinit();
239
+
240
+ try request.send();
241
+ try request.finish();
242
+ try request.wait();
243
+
244
+ if (request.response.status != std.http.Status.ok) {
245
+ return error.WrongStatusResponse;
246
+ }
247
+ }
248
+
249
+ fn check_link_fragment(
250
+ context: FileValidationContext,
251
+ target_path: []const u8,
252
+ fragment: []const u8,
253
+ ) !void {
254
+ assert(std.mem.endsWith(u8, target_path, ".html"));
255
+
256
+ const html = try read_file_cached(context.arena, context.dir, target_path);
257
+ const needle = try std.mem.concat(context.arena, u8, &.{ "id=\"", fragment, "\"" });
258
+ if (std.mem.indexOf(u8, html, needle) == null) {
259
+ log.err("link target '{s}' does not contain anchor: '{s}'", .{ target_path, fragment });
260
+ return error.AnchorNotFound;
261
+ }
262
+ }
263
+
264
+ const Link = struct {
265
+ base: []const u8,
266
+ fragment: ?[]const u8 = null,
267
+
268
+ fn parse(text: []const u8) Link {
269
+ if (std.mem.lastIndexOfScalar(u8, text, '#')) |index| {
270
+ return .{
271
+ .base = text[0..index],
272
+ .fragment = text[index + 1 ..],
273
+ };
274
+ }
275
+ return .{ .base = text };
276
+ }
277
+ };
278
+
279
+ const LinkIterator = struct {
280
+ line_number: u32 = 1,
281
+ remaining: []const u8,
282
+
283
+ const href_prefix = "href=\"";
284
+
285
+ fn init(html: []const u8) LinkIterator {
286
+ return .{ .remaining = html };
287
+ }
288
+
289
+ fn next(self: *LinkIterator) ?Link {
290
+ const index = std.mem.indexOf(u8, self.remaining, href_prefix) orelse
291
+ return null;
292
+ const uri_start = index + href_prefix.len;
293
+ const uri_len = std.mem.indexOfScalar(u8, self.remaining[uri_start..], '"') orelse
294
+ return null;
295
+ const uri_end = uri_start + uri_len;
296
+ const uri_text = self.remaining[uri_start..][0..uri_len];
297
+
298
+ for (self.remaining[0..uri_start]) |c| {
299
+ if (c == '\n') self.line_number += 1;
300
+ }
301
+ self.remaining = self.remaining[uri_end..];
302
+
303
+ return Link.parse(uri_text);
304
+ }
305
+ };
306
+
307
+ fn path_exists(dir: std.fs.Dir, path: []const u8) !bool {
308
+ dir.access(path, .{}) catch |err| switch (err) {
309
+ error.FileNotFound => return false,
310
+ else => return err,
311
+ };
312
+ return true;
313
+ }
@@ -0,0 +1,87 @@
1
+ const std = @import("std");
2
+ const log = std.log.scoped(.template);
3
+
4
+ pub const Html = @This();
5
+
6
+ arena: std.mem.Allocator,
7
+ buffer: std.ArrayList(u8),
8
+ writer: std.ArrayList(u8).Writer,
9
+
10
+ pub fn create(arena: std.mem.Allocator) !*Html {
11
+ var html = try arena.create(Html);
12
+ html.* = .{
13
+ .arena = arena,
14
+ .buffer = std.ArrayList(u8).init(arena),
15
+ .writer = undefined,
16
+ };
17
+ html.writer = html.buffer.writer();
18
+ return html;
19
+ }
20
+
21
+ /// Replaces the variables in `$snake_case` matching the names of the fields in the
22
+ /// `replacement` struct.
23
+ ///
24
+ /// We picked the $dollar_name format over a {curly_braces} format to avoid potential ambiguities
25
+ /// with JavaScript function syntax in the template.
26
+ pub fn write(html: *Html, template: []const u8, replacements: anytype) !void {
27
+ const ReplacementsType = @TypeOf(replacements);
28
+ const replacements_type_info = @typeInfo(ReplacementsType);
29
+ if (replacements_type_info != .@"struct") @compileError("expected struct");
30
+
31
+ var unused = std.StringHashMap(void).init(html.arena);
32
+ inline for (replacements_type_info.@"struct".fields) |field| {
33
+ try unused.put(field.name, {});
34
+ }
35
+
36
+ var it = std.mem.tokenizeScalar(u8, template, '$');
37
+ if (template[0] != '$') if (it.next()) |prefix| try html.writer.writeAll(prefix);
38
+ while (it.next()) |chunk| {
39
+ const identifier_len = for (chunk, 0..) |c, index| {
40
+ switch (c) {
41
+ 'a'...'z', '_' => {},
42
+ else => break index,
43
+ }
44
+ } else chunk.len;
45
+
46
+ const identifier = chunk[0..identifier_len];
47
+ const found = inline for (replacements_type_info.@"struct".fields) |field| {
48
+ if (std.mem.eql(u8, field.name, identifier)) {
49
+ try html.writer.writeAll(switch (field.type) {
50
+ *Html => @field(replacements, field.name).string(),
51
+ else => @field(replacements, field.name),
52
+ });
53
+ _ = unused.remove(field.name);
54
+ break true;
55
+ }
56
+ } else false;
57
+ if (!found) {
58
+ log.err("Html.write: identifier '{s}' not found in replacements", .{identifier});
59
+ return error.IdentifierNotFound;
60
+ }
61
+
62
+ try html.writer.writeAll(chunk[identifier_len..]);
63
+ }
64
+
65
+ if (unused.count() > 0) {
66
+ var unused_it = unused.keyIterator();
67
+ while (unused_it.next()) |unused_identifier| {
68
+ log.err("Html.write: identifier '{s}' not found in template", .{unused_identifier.*});
69
+ }
70
+ return error.UnusedIdentifiers;
71
+ }
72
+ }
73
+
74
+ pub fn child(self: Html) !*Html {
75
+ return try Html.create(self.arena);
76
+ }
77
+
78
+ pub fn string(self: Html) []const u8 {
79
+ return self.buffer.items;
80
+ }
81
+
82
+ pub fn redirect(arena: std.mem.Allocator, url: []const u8) ![]const u8 {
83
+ const template = @embedFile("html/redirect.html");
84
+ var html = try Html.create(arena);
85
+ try html.write(template, .{ .url = url });
86
+ return html.string();
87
+ }