makiri 0.10.0.rc1 → 0.10.0

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +137 -29
  3. data/NOKOGIRI_DIFFERENCES.md +177 -0
  4. data/README.md +9 -151
  5. data/Rakefile +14 -0
  6. data/ext/makiri/rust/build.rs +161 -26
  7. data/ext/makiri/rust/clippy.toml +26 -11
  8. data/ext/makiri/rust/fuzz/fuzz_targets/common.rs +1 -1
  9. data/ext/makiri/rust/fuzz/fuzz_targets/css.rs +11 -6
  10. data/ext/makiri/rust/fuzz/fuzz_targets/html.rs +1 -1
  11. data/ext/makiri/rust/fuzz/fuzz_targets/html_xpath.rs +4 -6
  12. data/ext/makiri/rust/src/bridge/doc.rs +48 -197
  13. data/ext/makiri/rust/src/bridge/fragment.rs +150 -71
  14. data/ext/makiri/rust/src/bridge/gvl.rs +25 -3
  15. data/ext/makiri/rust/src/bridge/html.rs +534 -0
  16. data/ext/makiri/rust/src/bridge/mod.rs +26 -19
  17. data/ext/makiri/rust/src/bridge/node_set.rs +235 -398
  18. data/ext/makiri/rust/src/bridge/ruby.rs +74 -169
  19. data/ext/makiri/rust/src/bridge/string.rs +162 -150
  20. data/ext/makiri/rust/src/bridge/typed.rs +279 -20
  21. data/ext/makiri/rust/src/bridge/wrapper.rs +627 -0
  22. data/ext/makiri/rust/src/bridge/xml.rs +367 -640
  23. data/ext/makiri/rust/src/bridge/xml_decode.rs +63 -200
  24. data/ext/makiri/rust/src/bridge/xpath/context_object.rs +299 -0
  25. data/ext/makiri/rust/src/bridge/xpath/handler.rs +307 -0
  26. data/ext/makiri/rust/src/bridge/xpath/mod.rs +307 -0
  27. data/ext/makiri/rust/src/cbuf.rs +8 -0
  28. data/ext/makiri/rust/src/css/build.rs +38 -49
  29. data/ext/makiri/rust/src/css/lower.rs +255 -309
  30. data/ext/makiri/rust/src/css/mod.rs +62 -40
  31. data/ext/makiri/rust/src/cutf8.rs +33 -0
  32. data/ext/makiri/rust/src/falloc/cstr.rs +8 -11
  33. data/ext/makiri/rust/src/falloc/inject.rs +16 -16
  34. data/ext/makiri/rust/src/falloc/mod.rs +97 -91
  35. data/ext/makiri/rust/src/falloc/raw.rs +15 -46
  36. data/ext/makiri/rust/src/glue/css.rs +21 -0
  37. data/ext/makiri/rust/src/glue/{doc.rs → html_doc.rs} +51 -32
  38. data/ext/makiri/rust/src/glue/html_node/css.rs +115 -0
  39. data/ext/makiri/rust/src/glue/html_node/mod.rs +36 -50
  40. data/ext/makiri/rust/src/glue/html_node/mutate.rs +296 -11
  41. data/ext/makiri/rust/src/glue/html_node/read.rs +34 -139
  42. data/ext/makiri/rust/src/glue/html_node/serialize.rs +80 -0
  43. data/ext/makiri/rust/src/glue/mod.rs +36 -34
  44. data/ext/makiri/rust/src/glue/node.rs +5 -10
  45. data/ext/makiri/rust/src/glue/node_set.rs +166 -8
  46. data/ext/makiri/rust/src/glue/query.rs +274 -0
  47. data/ext/makiri/rust/src/glue/stylesheet.rs +170 -0
  48. data/ext/makiri/rust/src/glue/xml_doc.rs +154 -0
  49. data/ext/makiri/rust/src/glue/xml_node/css.rs +109 -0
  50. data/ext/makiri/rust/src/glue/xml_node/mod.rs +78 -103
  51. data/ext/makiri/rust/src/glue/xml_node/mutate.rs +329 -14
  52. data/ext/makiri/rust/src/glue/xml_node/ns.rs +76 -177
  53. data/ext/makiri/rust/src/glue/xml_node/read.rs +188 -133
  54. data/ext/makiri/rust/src/glue/xml_node/serialize.rs +12 -4
  55. data/ext/makiri/rust/src/glue/xml_node/strings.rs +25 -0
  56. data/ext/makiri/rust/src/glue/xpath_context.rs +98 -0
  57. data/ext/makiri/rust/src/gvl.rs +162 -0
  58. data/ext/makiri/rust/src/init.rs +18 -25
  59. data/ext/makiri/rust/src/{lexbor_abi.rs → lexbor/abi.rs} +16 -173
  60. data/ext/makiri/rust/src/lexbor/adapter/arena_bytes.rs +79 -0
  61. data/ext/makiri/rust/src/lexbor/adapter/cross_import.rs +110 -243
  62. data/ext/makiri/rust/src/lexbor/adapter/dom_index.rs +46 -121
  63. data/ext/makiri/rust/src/lexbor/adapter/html/build.rs +385 -0
  64. data/ext/makiri/rust/src/lexbor/adapter/html/mod.rs +961 -0
  65. data/ext/makiri/rust/src/lexbor/adapter/html/mutate.rs +321 -0
  66. data/ext/makiri/rust/src/lexbor/adapter/mod.rs +8 -5
  67. data/ext/makiri/rust/src/lexbor/adapter/post_parse.rs +80 -275
  68. data/ext/makiri/rust/src/lexbor/adapter/source_loc.rs +28 -29
  69. data/ext/makiri/rust/src/lexbor/adapter/text_index.rs +67 -143
  70. data/ext/makiri/rust/src/lexbor/adapter/utf8_input.rs +44 -65
  71. data/ext/makiri/rust/src/lexbor/contains_guard.rs +299 -0
  72. data/ext/makiri/rust/src/lexbor/css_engine.rs +209 -0
  73. data/ext/makiri/rust/src/lexbor/css_parser.rs +277 -261
  74. data/ext/makiri/rust/src/lexbor/fragment.rs +111 -183
  75. data/ext/makiri/rust/src/lexbor/mod.rs +21 -12
  76. data/ext/makiri/rust/src/lexbor/selectors.rs +242 -313
  77. data/ext/makiri/rust/src/lexbor/serialize.rs +24 -39
  78. data/ext/makiri/rust/src/lexbor/stylesheet.rs +92 -230
  79. data/ext/makiri/rust/src/lexbor/tests.rs +233 -0
  80. data/ext/makiri/rust/src/lexbor/xpath.rs +80 -38
  81. data/ext/makiri/rust/src/lib.rs +16 -8
  82. data/ext/makiri/rust/src/limits.rs +10 -0
  83. data/ext/makiri/rust/src/ptr_table.rs +228 -0
  84. data/ext/makiri/rust/src/rust_tests.rs +45 -45
  85. data/ext/makiri/rust/src/text.rs +3 -0
  86. data/ext/makiri/rust/src/token.rs +11 -0
  87. data/ext/makiri/rust/src/xml/arena.rs +231 -99
  88. data/ext/makiri/rust/src/xml/chars/expand.rs +159 -0
  89. data/ext/makiri/rust/src/xml/chars/mod.rs +181 -0
  90. data/ext/makiri/rust/src/xml/dom_name.rs +84 -0
  91. data/ext/makiri/rust/src/xml/encoding_sniff.rs +245 -0
  92. data/ext/makiri/rust/src/xml/index.rs +90 -67
  93. data/ext/makiri/rust/src/xml/mod.rs +2 -2
  94. data/ext/makiri/rust/src/xml/model.rs +14 -14
  95. data/ext/makiri/rust/src/xml/mutate/attr.rs +154 -0
  96. data/ext/makiri/rust/src/xml/mutate/copy.rs +206 -0
  97. data/ext/makiri/rust/src/xml/mutate/edit.rs +100 -0
  98. data/ext/makiri/rust/src/xml/mutate/factory.rs +138 -0
  99. data/ext/makiri/rust/src/xml/mutate/insert.rs +488 -0
  100. data/ext/makiri/rust/src/xml/mutate/mod.rs +66 -0
  101. data/ext/makiri/rust/src/xml/mutate/ns.rs +174 -0
  102. data/ext/makiri/rust/src/xml/qname.rs +61 -39
  103. data/ext/makiri/rust/src/xml/selftest.rs +884 -887
  104. data/ext/makiri/rust/src/xml/serialize/c14n.rs +243 -0
  105. data/ext/makiri/rust/src/xml/serialize/mod.rs +110 -0
  106. data/ext/makiri/rust/src/xml/serialize/out.rs +89 -0
  107. data/ext/makiri/rust/src/xml/serialize/xml.rs +511 -0
  108. data/ext/makiri/rust/src/xml/tree/cursor.rs +367 -0
  109. data/ext/makiri/rust/src/xml/tree/decl.rs +102 -0
  110. data/ext/makiri/rust/src/xml/tree/dtd.rs +442 -0
  111. data/ext/makiri/rust/src/xml/tree/mod.rs +710 -0
  112. data/ext/makiri/rust/src/xml/tree/scope.rs +93 -0
  113. data/ext/makiri/rust/src/xml/xpath.rs +39 -22
  114. data/ext/makiri/rust/src/xpath/abi.rs +2 -3
  115. data/ext/makiri/rust/src/xpath/ast_ops.rs +1 -32
  116. data/ext/makiri/rust/src/xpath/attr_pred.rs +20 -25
  117. data/ext/makiri/rust/src/xpath/axis.rs +37 -50
  118. data/ext/makiri/rust/src/xpath/ctx.rs +82 -14
  119. data/ext/makiri/rust/src/xpath/dom.rs +67 -28
  120. data/ext/makiri/rust/src/xpath/eval.rs +132 -182
  121. data/ext/makiri/rust/src/xpath/funcs/ext.rs +115 -0
  122. data/ext/makiri/rust/src/xpath/{funcs.rs → funcs/mod.rs} +280 -218
  123. data/ext/makiri/rust/src/xpath/lex.rs +3 -3
  124. data/ext/makiri/rust/src/xpath/limits.rs +1 -1
  125. data/ext/makiri/rust/src/xpath/mod.rs +3 -3
  126. data/ext/makiri/rust/src/xpath/msg.rs +56 -18
  127. data/ext/makiri/rust/src/xpath/nodetest.rs +155 -159
  128. data/ext/makiri/rust/src/xpath/number.rs +2 -2
  129. data/ext/makiri/rust/src/xpath/order.rs +163 -127
  130. data/ext/makiri/rust/src/xpath/parse.rs +5 -5
  131. data/ext/makiri/rust/src/xpath/step_index.rs +33 -59
  132. data/ext/makiri/rust/src/xpath/str_cache.rs +97 -0
  133. data/ext/makiri/rust/src/xpath/tests.rs +13 -6
  134. data/ext/makiri/rust/src/xpath/value.rs +68 -110
  135. data/ext/makiri/rust/src/xpath/verify.rs +2 -3
  136. data/lib/makiri/node.rb +2 -10
  137. data/lib/makiri/version.rb +1 -1
  138. data/lib/makiri/xml/namespace.rb +17 -0
  139. data/lib/makiri/xml/node_methods.rb +8 -1
  140. data/lib/makiri.rb +1 -0
  141. data/script/check_alloc_failures.rb +48 -0
  142. data/script/check_unsafe_boundaries.rb +170 -60
  143. data/vendor/lexbor/CMakeLists.txt +23 -16
  144. data/vendor/lexbor/config.cmake +3 -3
  145. data/vendor/lexbor/lexbor.pc.in +2 -2
  146. data/vendor/lexbor/source/lexbor/core/conv.c +13 -8
  147. data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +3 -0
  148. data/vendor/lexbor/source/lexbor/css/selectors/selector.c +30 -3
  149. data/vendor/lexbor/source/lexbor/css/selectors/state.c +34 -5
  150. data/vendor/lexbor/source/lexbor/css/syntax/parser.c +38 -12
  151. data/vendor/lexbor/source/lexbor/css/syntax/state.c +100 -115
  152. data/vendor/lexbor/source/lexbor/dom/interfaces/attr_const.h +74 -39
  153. data/vendor/lexbor/source/lexbor/dom/interfaces/attr_res.h +150 -34
  154. data/vendor/lexbor/source/lexbor/dom/interfaces/character_data.c +1 -1
  155. data/vendor/lexbor/source/lexbor/encoding/encode.c +16 -3
  156. data/vendor/lexbor/source/lexbor/html/attribute_steps_res.h +4 -2
  157. data/vendor/lexbor/source/lexbor/html/element_steps_res.h +4 -2
  158. data/vendor/lexbor/source/lexbor/html/encoding.c +12 -3
  159. data/vendor/lexbor/source/lexbor/html/interface_res.h +36 -2
  160. data/vendor/lexbor/source/lexbor/html/serialize.c +1 -1
  161. data/vendor/lexbor/source/lexbor/html/serialize_ext.c +1 -1
  162. data/vendor/lexbor/source/lexbor/html/tag_res.h +11 -2
  163. data/vendor/lexbor/source/lexbor/html/token.c +1 -0
  164. data/vendor/lexbor/source/lexbor/html/token.h +17 -0
  165. data/vendor/lexbor/source/lexbor/html/tokenizer/error.c +5 -1
  166. data/vendor/lexbor/source/lexbor/html/tokenizer/error.h +8 -0
  167. data/vendor/lexbor/source/lexbor/html/tokenizer/state.c +361 -5
  168. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/after_after_body.c +12 -1
  169. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/after_after_frameset.c +12 -1
  170. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/after_body.c +11 -0
  171. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/after_frameset.c +10 -0
  172. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/after_head.c +10 -0
  173. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/before_head.c +10 -0
  174. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/before_html.c +11 -0
  175. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/foreign_content.c +17 -0
  176. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +32 -20
  177. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_column_group.c +17 -0
  178. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_frameset.c +10 -0
  179. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_head.c +10 -0
  180. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_head_noscript.c +1 -0
  181. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_table.c +18 -0
  182. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_template.c +1 -0
  183. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/initial.c +11 -0
  184. data/vendor/lexbor/source/lexbor/html/tree/open_elements_res.h +4 -2
  185. data/vendor/lexbor/source/lexbor/html/tree.c +42 -0
  186. data/vendor/lexbor/source/lexbor/html/tree.h +5 -0
  187. data/vendor/lexbor/source/lexbor/ns/res.h +38 -38
  188. data/vendor/lexbor/source/lexbor/selectors/selectors.c +83 -2
  189. data/vendor/lexbor/source/lexbor/style/attribute_steps_res.h +4 -2
  190. data/vendor/lexbor/source/lexbor/style/dom/interfaces/document.c +4 -0
  191. data/vendor/lexbor/source/lexbor/style/element_steps_res.h +4 -2
  192. data/vendor/lexbor/source/lexbor/style/tree/open_elements_res.h +4 -2
  193. data/vendor/lexbor/source/lexbor/tag/const.h +203 -202
  194. data/vendor/lexbor/source/lexbor/tag/res.h +136 -134
  195. data/vendor/lexbor/source/lexbor/unicode/idna.c +19 -9
  196. data/vendor/lexbor/source/lexbor/url/base.h +1 -1
  197. data/vendor/lexbor/source/lexbor/url/url.c +147 -52
  198. data/vendor/lexbor/source/lexbor/url/url.h +122 -0
  199. data/vendor/lexbor/source/lexbor/utils/http.c +22 -1
  200. metadata +52 -22
  201. data/ext/makiri/rust/src/bridge/lexbor.rs +0 -1258
  202. data/ext/makiri/rust/src/bridge/selectors.rs +0 -166
  203. data/ext/makiri/rust/src/bridge/serialize.rs +0 -92
  204. data/ext/makiri/rust/src/bridge/xpath.rs +0 -1154
  205. data/ext/makiri/rust/src/falloc/calloc.rs +0 -16
  206. data/ext/makiri/rust/src/glue/xml.rs +0 -506
  207. data/ext/makiri/rust/src/glue/xml_node/abi.rs +0 -74
  208. data/ext/makiri/rust/src/glue/xpath.rs +0 -17
  209. data/ext/makiri/rust/src/lexbor/adapter/html.rs +0 -1535
  210. data/ext/makiri/rust/src/lexbor/ffi.rs +0 -26
  211. data/ext/makiri/rust/src/xml/api.rs +0 -18
  212. data/ext/makiri/rust/src/xml/chars.rs +0 -287
  213. data/ext/makiri/rust/src/xml/mutate.rs +0 -1120
  214. data/ext/makiri/rust/src/xml/parse.rs +0 -46
  215. data/ext/makiri/rust/src/xml/serialize.rs +0 -715
  216. data/ext/makiri/rust/src/xml/tree.rs +0 -963
  217. data/ext/makiri/rust/src/xpath/runtime_abi/cache.rs +0 -172
  218. data/ext/makiri/rust/src/xpath/runtime_abi.rs +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 222e16ff55d6b0a91b495ec3f0a3919b3de5633afa5ec7a5363475f6c097a111
4
- data.tar.gz: 35d5eacf1d006ef49d9fb1b31c3728e7693c6db175d041f78987084196a74a62
3
+ metadata.gz: b6163794fb07f39d4476e91a5d9db367d82b02888e167bc41435bb12de32de49
4
+ data.tar.gz: 4c075f42cbdf69b39b9783e3ad66bd09acb48f380447e8e80a47402dcdd427e5
5
5
  SHA512:
6
- metadata.gz: 7f3fb141cb5ec35926f15f63f90cb5076f7d3faebc548ccd850014b526ddb2c8efab00f87424918fef73ecaf0f8edbf78646e4c00ce8d0b47882d38658be9748
7
- data.tar.gz: 849a3a1a2f0d8da33b7da177f40c57bff74fdcc1039d800b0c132645d70c011d7f41cc601dd2add2380049f250549462d19a097baf8fd2fd68ff33fee8c8a961
6
+ metadata.gz: d6f1d154116c49d5b42e57e08d584781af42c2d0a02a6d3a1d6073659437cd7b0920a30a42451fedc70b173de242c9738a34ea4fd8d582dd3d199094bbfe10ab
7
+ data.tar.gz: 7278778cf2e98166e03ef79364ef9a4b0feec3b410949c2e4889f82f899594da8ba492ac00a6d75de2ee7a553430e5dc69a9af52a41292ab86249af830dea7fe
data/CHANGELOG.md CHANGED
@@ -1,34 +1,142 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0] - 2026-09-22
4
+
5
+ ### Fixed
6
+
7
+ * A frozen node now raises `FrozenError` when it is the ARGUMENT of a tree
8
+ mutation, not only the receiver: `b.add_child(a)` relinks `a` exactly as
9
+ `a.remove` does. Both `Makiri::XML` and `Makiri::HTML`. A fragment argument
10
+ splices its children, which have no wrapper of their own to check.
11
+ * A `Makiri::XML` mutation that exceeds the document's own `max_bytes` /
12
+ `max_nodes` now raises `Makiri::XML::LimitExceeded` instead of reporting the
13
+ refusal as out of memory.
14
+ * Inserting a `DocumentFragment` is all or nothing on `add_child`, `before` and
15
+ `after`, as it already was on `replace`: a child the rules refuse no longer
16
+ leaves the earlier ones linked in a document the caller was told had not
17
+ changed.
18
+ * A rejected `Makiri::XML::Document#fragment` no longer charges the document for
19
+ the nodes it discarded. 100k rejected fragments grew a `<r/>` document to
20
+ 77 MB; it is now 516 bytes.
21
+ * A failed `Makiri::XML` parse reports the FIRST failure for all four kinds
22
+ (`syntax` was sticky while `limit` and `unsupported` overwrote each other).
23
+ * `Makiri::XML#to_xml`'s serializer allocates fallibly again, so running out of
24
+ memory raises instead of aborting the process.
25
+ * `:lexbor-contains()` now rejects an argument the bundled CSS parser does not
26
+ take, the way any unknown pseudo-class is rejected: `Makiri::CSS::SyntaxError`
27
+ from `#css` / `#at_css` / `#matches?`, and a `:bad_style` rule from
28
+ `Makiri::Lexbor::CSS.parse_stylesheet`. Well-formed uses are unchanged.
29
+
30
+ ### Performance
31
+
32
+ * `Makiri::XML#to_xml` plans namespaces from a binding stack instead of
33
+ re-walking each ancestor's attribute list, which cost O(depth^2 x attributes).
34
+ 403 KB of nested prefixed attributes took 4.88s and now takes 0.001s. A
35
+ crafted document fails closed with `Makiri::Error` ("namespace planning
36
+ exceeded its step budget") rather than running on.
37
+ * Setting an attribute on a `Makiri::XML` element walks the attribute list once
38
+ instead of twice (4096 attributes: 47ms -> 27ms).
39
+
40
+ ## [0.10.0.rc2] - 2026-09-20
41
+
42
+ ### Added
43
+
44
+ * `XPathContext.new` now registers namespace bindings passed via
45
+ `prefix: uri` (previously ignored).
46
+ * `#css`, `#at_css`, and `#matches?` accept `(selector, namespaces = nil)`
47
+ on HTML documents for consistency with XML.
48
+ * Standardized error message format for rejected CSS selectors across XML
49
+ and HTML: `"<reason>: <selector>"`.
50
+ * Unified argument handling for `#xpath` and `#at_xpath` between HTML and XML.
51
+ * HTML supports per-query namespace bindings and keyword prefixes
52
+ (e.g., `s: uri`).
53
+ * XML supports custom-function handlers.
54
+ * XML nodes now support HTML-style node readers and aliases
55
+ (`#first_element_child`, `#next_element`, `#tag_name`, `#attr`,
56
+ `#node_name`, etc.).
57
+
58
+ ### Changed
59
+
60
+ * Updated vendored Lexbor to `v3.0.0-66`.
61
+ * Parsed `<?php ... ?>` tags in HTML input as `ProcessingInstruction` nodes
62
+ instead of comments, adhering to the HTML Standard.
63
+ * Vendored Lexbor updated from `v3.0.0-25` to `v3.0.0-66` (`05b5d37`), for an
64
+ out-of-bounds write in `lxb_dom_character_data_replace` reachable through
65
+ `#content=`, UTF-16 surrogate rejection, exact `meta` charset matching and
66
+ buffer-capacity fixes. (v3.0.1 is not usable: it lacks the two CSS-selector
67
+ fixes Makiri upstreamed.)
68
+ * `<?php ... ?>` in HTML input is now a `ProcessingInstruction` node rather
69
+ than a Comment, serializes as `<?target data?>`, and `<?` at end of input is
70
+ ignored - the HTML Standard's processing-instruction tokens, which Lexbor now
71
+ implements. `Nokogiri::HTML5` still answers with a comment.
72
+ * `Makiri::XML#matches?` tests the node locally via tree walking instead of
73
+ performing a full-document search.
74
+ * `Makiri::XML::Namespace` is now an immutable `Data` object defined in Ruby.
75
+ * `XML::Node#value` returns its text content (or attribute value for
76
+ attributes), aligning with HTML node behavior.
77
+ * XPath over HTML now handles element/attribute names case-insensitively
78
+ and properly handles `xmlns` attributes according to browser specs.
79
+ * `Makiri::XML` checks the internal DTD subset for well-formedness and
80
+ raises `Makiri::XML::SyntaxError` if unsupported constructs (e.g., attribute
81
+ defaults, entity references) would alter the document tree.
82
+ * Documents with `version="1.x"` are now accepted and parsed as XML 1.0.
83
+ * Processing instruction (PI) targets containing colons (e.g., `<?a:b?>`)
84
+ are now rejected per Namespaces in XML specs.
85
+
86
+ ### Fixed
87
+
88
+ * `Makiri::XML::DocumentType#prefix` now returns `nil` instead of
89
+ the PUBLIC ID.
90
+ * `Makiri::XML#last_element_child` now correctly returns an `Element`
91
+ instead of arbitrary child nodes (like comments or text).
92
+ * `Element#local_name` and `local-name()` in XPath preserve camelCase
93
+ for SVG/MathML elements (e.g., `foreignObject`).
94
+ * `inner_html=` and `outer_html=` are now atomic operations to prevent
95
+ leaving the DOM in a corrupted state on failure.
96
+ * XPath over HTML assigns empty/correct namespaces to attributes instead of
97
+ inheriting from their parent element.
98
+ * Corrected `lang()` evaluation to check the nearest language attribute
99
+ hierarchically.
100
+ * Fixed `namespace_matching: :lax` on XML to be strict, aligning with
101
+ `Nokogiri::XML` / `libxml2` behavior.
102
+ * Moving an HTML node to another document now correctly removes it
103
+ from the source document's indexes.
104
+ * Prevented XPath custom handlers from creating new nodes on the document
105
+ being evaluated.
106
+ * Fixed CSS universal selectors (`p|*`, `|*`) on XML to respect namespace
107
+ boundaries.
108
+ * CSS column combinator (`a || b`) on XML now raises
109
+ `Makiri::CSS::SyntaxError` instead of being misparsed as descendant selector.
110
+
3
111
  ## [0.10.0.rc1] - 2026-09-19
4
112
 
5
113
  ### Changed
6
114
 
7
- * **The native extension is rewritten in Rust.** The C glue, the XPath engine,
115
+ * The native extension is rewritten in Rust. The C glue, the XPath engine,
8
116
  the XML reader and the CSS lowering are now one Rust crate; the only C left is
9
117
  the vendored Lexbor, still unpatched. The Ruby API is unchanged, and answers
10
118
  were checked against the C build's recorded output as well as the existing
11
119
  differential suites against Nokogiri.
12
120
 
13
- * **Installing from source needs a Rust toolchain.** `cargo` (stable) and
121
+ * Installing from source needs a Rust toolchain. `cargo` (stable) and
14
122
  libclang are required alongside CMake, and `rb_sys` becomes a runtime
15
123
  dependency of the source gem, because its `extconf.rb` runs at install time.
16
124
  The precompiled platform gems need none of this and do not depend on
17
125
  `rb_sys`.
18
- * **Faster.** Makiri now beats both Nokogiri and nokolexbor on every
126
+ * Faster. Makiri now beats both Nokogiri and nokolexbor on every
19
127
  `rake bench` row, parse included - previously ~1.25x slower than nokolexbor.
20
128
  Source locations are stamped on the first `#line` or mutation rather than
21
129
  during every parse, and the vendored Lexbor is built with link-time
22
130
  optimization where the linker supports it (macOS; Linux with clang,
23
131
  llvm-ar and lld; not Windows).
24
- * **An internal failure is an exception, not a crash.** It used to end the
132
+ * An internal failure is an exception, not a crash. It used to end the
25
133
  host process with SIGABRT. Now it unwinds on the thread that ran the call:
26
134
  `ensure` blocks run and the process keeps working. On entry points that
27
135
  handle input (parse, XPath, CSS, serialization, text) it is the new
28
- **`Makiri::InternalError`**, which descends from `Exception`, not
136
+ `Makiri::InternalError`, which descends from `Exception`, not
29
137
  `StandardError`, so a bare `rescue => e` does not swallow it; elsewhere it
30
138
  is Ruby's `fatal`.
31
- * **An XPath handler may not modify the document being evaluated.** Every
139
+ * An XPath handler may not modify the document being evaluated. Every
32
140
  mutator on that document raises `Makiri::Error` while an evaluation with a
33
141
  handler runs, because the evaluator holds names and values from it for the
34
142
  whole walk.
@@ -46,14 +154,14 @@
46
154
 
47
155
  * `Node#attribute_by_qualified_name(name)` and
48
156
  `Node#attribute_value_by_qualified_name(name)`: the attribute whose
49
- **qualified** name is exactly `name` — its node, and its value — or nil.
157
+ qualified name is exactly `name` — its node, and its value — or nil.
50
158
  `#[]` cannot answer this: it also finds a prefixed attribute by its local
51
159
  name (`svg_a["href"]` returns `xlink:href`), and it lower-cases what it looks
52
160
  up (`el["DATA-X"]` finds `data-x`). The new match is byte-exact.
53
161
 
54
162
  ### Changed
55
163
 
56
- * **`Makiri::XML` follows the DOM namespace model.** A node's namespace URI is
164
+ * `Makiri::XML` follows the DOM namespace model. A node's namespace URI is
57
165
  decided once — by the parser, or by the context it is first inserted into —
58
166
  and does not change afterwards; the serializer emits whatever xmlns
59
167
  declarations the output needs:
@@ -71,24 +179,24 @@
71
179
  Nodes from the factories (`create_element` and friends) still take their
72
180
  namespace from the context they are first inserted into.
73
181
 
74
- * **Inserting a node from another document adopts it.** `add_child` / `before` /
182
+ * Inserting a node from another document adopts it. `add_child` / `before` /
75
183
  `after` / `replace` bring the node over and remove it from the document it
76
184
  came from, instead of copying it (`Makiri::XML`) or raising (`Makiri::HTML`).
77
185
  A spliced fragment is left empty; a rejected insert leaves the source
78
186
  document untouched.
79
187
 
80
- The node handed back is a **different object** than the one passed in, so use
188
+ The node handed back is a different object than the one passed in, so use
81
189
  the return value afterwards rather than the argument. `Document#import_node`
82
190
  is unchanged: it copies and leaves the source alone.
83
191
 
84
- * **XML serialization is capped at the nesting depth the parser accepts.**
192
+ * XML serialization is capped at the nesting depth the parser accepts.
85
193
  `#to_xml` and `#canonicalize` raise `Makiri::Error` past it, rather than
86
194
  emitting XML that Makiri could not read back. A deeper tree is still fine to
87
195
  hold, walk and query.
88
196
 
89
197
  ### Fixed
90
198
 
91
- * XPath axes from an **attribute** context node on the XML backend now follow
199
+ * XPath axes from an attribute context node on the XML backend now follow
92
200
  XPath 1.0 §2.2: `following-sibling` and `preceding-sibling` are empty, and
93
201
  `following` / `preceding` exclude attribute nodes. They used to return the
94
202
  element's later attributes.
@@ -294,7 +402,7 @@
294
402
 
295
403
  ### Added
296
404
 
297
- * **Native XML 1.0 reader + in-place editor** - `Makiri::XML::Document.parse(source)`
405
+ * Native XML 1.0 reader + in-place editor - `Makiri::XML::Document.parse(source)`
298
406
  / `Makiri::XML(source)`. No libxml2: a strict, fail-closed parser builds its own
299
407
  node arena (case- and namespace-preserving), queried by the native XPath engine.
300
408
  * Strict & secure: fail-closed decode (bad UTF-8 / NUL -> `XML::SyntaxError`),
@@ -304,9 +412,9 @@
304
412
  encoding is a fatal error, not a silent mis-decode.
305
413
  * DoS-bounded by a single arena byte ceiling (default 256 MiB; raise per parse
306
414
  with `max_bytes:`).
307
- * `<!DOCTYPE>` recognized but **not processed** (`#internal_subset` ->
308
- `XML::DocumentType`); zero entity/DTD I/O, so **XXE and billion-laughs are
309
- structurally impossible**. Kept off the tree, as in libxml2.
415
+ * `<!DOCTYPE>` recognized but not processed (`#internal_subset` ->
416
+ `XML::DocumentType`); zero entity/DTD I/O, so XXE and billion-laughs are
417
+ structurally impossible. Kept off the tree, as in libxml2.
310
418
  * Read API mirrors Nokogiri: `#xpath` / `#at_xpath` (`{prefix => uri}`),
311
419
  name/namespace readers, `#text`, `#[]`, traversal, and namespace introspection
312
420
  (`Makiri::XML::Namespace`); `XPathContext` works over XML nodes too.
@@ -334,7 +442,7 @@
334
442
  * `NodeSet#[]` accepts a `Range` or `start, length` (like `Array#[]`).
335
443
  * `Node` / `NodeSet` / `Document` `#dup` / `#clone` now return real independent
336
444
  copies (`#dup(0)` shallow; `#clone(freeze:)` honoured).
337
- * A **frozen node is genuinely immutable** - every mutator raises `FrozenError`.
445
+ * A frozen node is genuinely immutable - every mutator raises `FrozenError`.
338
446
 
339
447
  ### Changed
340
448
 
@@ -346,7 +454,7 @@
346
454
  added `Node#cdata?`.
347
455
  * Text-index range table uses `uint32` bounds (24 -> 16 B/entry; ~27% less retained
348
456
  index, byte-identical text).
349
- * Parsing **honours the input String's encoding** - Shift_JIS / EUC-JP / ... are now
457
+ * Parsing honours the input String's encoding - Shift_JIS / EUC-JP / ... are now
350
458
  transcoded to UTF-8 instead of mangled.
351
459
  * Parsing skips its UTF-8 validation scan when the String's coderange already proves
352
460
  it valid.
@@ -355,7 +463,7 @@
355
463
 
356
464
  ### Fixed
357
465
 
358
- * **Hardened the HTML/XML representation boundary.** HTML (Lexbor) and XML (arena)
466
+ * Hardened the HTML/XML representation boundary. HTML (Lexbor) and XML (arena)
359
467
  nodes are now distinct TypedData types, so the wrong representation raises
360
468
  `TypeError` instead of corrupting memory:
361
469
  * `Node#==` / `XPathContext#node=` with an XML `Document` no longer aborts the
@@ -434,12 +542,12 @@
434
542
  ## [0.1.0] - 2026-06-02
435
543
 
436
544
  First public release. An HTML5 parser, a native XPath 1.0 query engine, and CSS
437
- selectors for Ruby - built on vendored [Lexbor](https://lexbor.com/) with **no
438
- libxml2 / libxslt dependency at any layer**.
545
+ selectors for Ruby - built on vendored [Lexbor](https://lexbor.com/) with no
546
+ libxml2 / libxslt dependency at any layer.
439
547
 
440
548
  ### Added
441
549
 
442
- **Parsing & DOM**
550
+ Parsing & DOM
443
551
 
444
552
  * `Makiri::HTML` / `Makiri.parse` - HTML5 parsing via vendored, unpatched Lexbor,
445
553
  with browser-compatible UTF-8 decoding (invalid bytes → U+FFFD; parsing never
@@ -454,7 +562,7 @@ libxml2 / libxslt dependency at any layer**.
454
562
  quirks_mode,internal_subset,errors}` and `Makiri::DocumentType#{public_id,
455
563
  system_id,external_id}`.
456
564
 
457
- **XPath**
565
+ XPath
458
566
 
459
567
  * Native XPath 1.0 query engine (no libxml2/libxslt): `Node#{xpath,at_xpath}`
460
568
  and `Makiri::XPathContext` (`evaluate`, namespace/variable binding, custom
@@ -462,7 +570,7 @@ libxml2 / libxslt dependency at any layer**.
462
570
  built-in functions with spec-faithful semantics (XML NCNames including
463
571
  non-ASCII, node-set vs node-set comparisons per §3.4, document order per §5.1,
464
572
  Unicode-aware `translate`/`substring`).
465
- * Namespace matching is **strict by default** (HTML5/WHATWG-faithful, like
573
+ * Namespace matching is strict by default (HTML5/WHATWG-faithful, like
466
574
  browsers' `document.evaluate` and `Nokogiri::HTML5`); pass
467
575
  `namespace_matching: :lax` for the namespace-agnostic, `Nokogiri::HTML`-style
468
576
  match.
@@ -470,12 +578,12 @@ libxml2 / libxslt dependency at any layer**.
470
578
  (operation / recursion-depth / node-set / string-byte caps) that raise
471
579
  `Makiri::XPath::LimitExceeded` on overrun.
472
580
 
473
- **CSS**
581
+ CSS
474
582
 
475
583
  * `Node#{css,at_css,matches?}` via Lexbor's selector engine (descendant-only,
476
584
  document order). Malformed selectors raise `Makiri::CSS::SyntaxError`.
477
585
 
478
- **Mutation & serialization**
586
+ Mutation & serialization
479
587
 
480
588
  * DOM mutation: `add_child`/`<<`, `add_previous_sibling`/`before`,
481
589
  `add_next_sibling`/`after`, `remove`/`unlink`, `replace`; attribute `[]=` and
@@ -494,7 +602,7 @@ libxml2 / libxslt dependency at any layer**.
494
602
  `NodeSet#{|,+,&,-,css,xpath,search,at,last,remove}`, and `Element.new` /
495
603
  `Text.new`.
496
604
 
497
- **Safety & concurrency**
605
+ Safety & concurrency
498
606
 
499
607
  * UTF-8 text-input contract: HTML and fragment parsing are lenient (invalid
500
608
  bytes → U+FFFD, never reject), while strings passed to the XPath / CSS /
@@ -505,7 +613,7 @@ libxml2 / libxslt dependency at any layer**.
505
613
  context across threads cannot corrupt memory. Fail-closed string caps and
506
614
  iterative (non-recursive) tree walks resist stack-exhaustion DoS.
507
615
 
508
- **Performance** (`rake bench`, vs Nokogiri/libxml2)
616
+ Performance (`rake bench`, vs Nokogiri/libxml2)
509
617
 
510
618
  * Meets or beats Nokogiri on every benchmarked operation: parse ~3×, css ~12×,
511
619
  at_css ~1000×, serialize ~4×, `//tag` ~3.4×, `[@attr='v']` predicate ~1.5×,
@@ -513,7 +621,7 @@ libxml2 / libxslt dependency at any layer**.
513
621
  a document element index (for `//tag`), a direct-attribute predicate fast
514
622
  path, and a hashed per-evaluate string-value cache.
515
623
 
516
- **Tooling**
624
+ Tooling
517
625
 
518
626
  * Vendored Lexbor as a git submodule (pinned v3.0.0, applied without patches).
519
627
  Build hardening flags; AddressSanitizer+UBSan build (`rake sanitize`);
@@ -0,0 +1,177 @@
1
+ # Differences from Nokogiri
2
+
3
+ Makiri targets a Nokogiri-compatible API, but a few behaviours differ. Where
4
+ they do, Makiri usually follows the web platform - the WHATWG standards and
5
+ what browsers do - rather than libxml2. Detailed, test-backed notes live in
6
+ `spec/conformance/README.md`.
7
+
8
+ ## XPath
9
+
10
+ * The `namespace::` axis is not implemented
11
+ * It raises `Makiri::Error` rather than returning a silently-empty result.
12
+ * Nokogiri (libxml2) supports it (for `<svg>` in HTML it yields the `xml` and `svg` namespace nodes).
13
+ For an element's namespace use `namespace-uri()` / `local-name()`, which are implemented.
14
+ * Unprefixed name tests are namespace-strict by default (HTML5/WHATWG-faithful, like browsers' `document.evaluate` and `Nokogiri::HTML5`)
15
+ * `//div` matches, but foreign elements need a registered prefix (`//svg:path`).
16
+ Pass `namespace_matching: :lax` to `Node#xpath` / `XPathContext.new` for the
17
+ namespace-agnostic match where `//path` finds an SVG element (the
18
+ `Nokogiri::HTML`/libxml2-HTML4 behaviour). Lax means "as Nokogiri does", so
19
+ on an XML document it changes nothing: `Nokogiri::XML` is namespace-strict too.
20
+ * `namespace-uri()` of an HTML element returns the XHTML URI (DOM-correct, as browsers report)
21
+ * `Nokogiri::HTML5` returns `""`.
22
+ * Name tests fold ASCII case on HTML elements, like browsers (WPT `domxpath`).
23
+ The HTML Standard's XPath section does not ask for this - it only sets the
24
+ default element namespace - so here Makiri follows the browsers over the text.
25
+ * `//DiV` matches `<div>` and `//div[@Id]` its `id`; SVG / MathML names compare
26
+ exactly (`//*[@refX]`, not `@refx`). Only ASCII folds: `Ø` still differs from `ø`.
27
+ This holds in `namespace_matching: :lax` too.
28
+ * `Nokogiri::HTML5` is case-sensitive there.
29
+ * A foreign element's namespace declarations are not attributes
30
+ * `<svg xmlns="...">` has no `@xmlns` for `//*[@xmlns]` or `@*`, as in browsers
31
+ and in XPath's data model. An `xmlns` on an HTML element is an ordinary
32
+ attribute and stays visible.
33
+
34
+ ## XML
35
+
36
+ * `Makiri::XML` is XML 1.0 (Fifth Edition) only and non-validating.
37
+ * A `version="1.x"` document is read as XML 1.0, as §2.8 says a 1.0 processor
38
+ does; a construct only XML 1.1 allows still fails.
39
+ * The internal DTD subset is checked but never applied. §5.1 requires even
40
+ a non-validating parser to apply its attribute defaults and entities, so a
41
+ document whose DTD would change the tree is refused
42
+ (`Makiri::XML::SyntaxError`, "unsupported DTD construct") rather than parsed
43
+ without them: an attribute default (`"d"` or `#FIXED "d"`), a non-CDATA
44
+ attribute type (`ID`, `NMTOKEN`, an enumeration, ... - they normalize the
45
+ value), a parameter-entity reference, or a reference to an entity the DTD
46
+ declares. Declarations that change nothing (`<!ELEMENT>`, `CDATA #IMPLIED` /
47
+ `#REQUIRED`, unused entity declarations, notations) are accepted.
48
+ Nokogiri/libxml2 by default parses such documents and leaves the defaults and
49
+ entities out (its `DTDATTR` / `NOENT` options apply them).
50
+ * External entities and the external subset are never fetched (no I/O), as
51
+ §5.1 allows a non-validating parser.
52
+ * Mutation supports in-place edits, the node factories, fragments
53
+ (`Document#fragment` / `DocumentFragment.parse`), node insertion, and building
54
+ a document from scratch (`XML::Document.new` + `#root=`); only handing a raw
55
+ markup string straight to `#add_child` is unsupported (parse it into a fragment
56
+ first). (`#to_xml` serialization is supported; HTML serialization - `to_html`
57
+ / `inner_html` / `outer_html` - is not.)
58
+ * A processing-instruction target with a colon can be created but not written.
59
+ * The parser rejects `<?a:b ...?>`, as Nokogiri does: Namespaces in XML §7
60
+ requires every Name other than element and attribute names to be an NCName.
61
+ * `create_processing_instruction("a:b", ...)` succeeds, as DOM
62
+ `createProcessingInstruction` does, but `#to_xml` / `#canonicalize` then raise,
63
+ as DOM Parsing's well-formed serializer does. Nokogiri writes `<?a:b ...?>`.
64
+ * `#freeze` on a node is ENFORCED: a frozen node's mutators raise `FrozenError`,
65
+ and so does passing a frozen node as the argument of an insertion, which
66
+ relinks it. Nokogiri reports `frozen?` but every mutator still mutates. The
67
+ check reaches the nodes the caller named; a fragment argument splices its
68
+ children, and those cannot be checked, because frozen-ness is a property of a
69
+ Ruby object and the arena keeps no map from a node back to its wrapper.
70
+
71
+ * A node's namespace URI is its identity, not something re-derived from the
72
+ declarations around it - the WHATWG DOM model, measured against Chrome 152
73
+ (`DOMParser` + `XMLSerializer`).
74
+ * Moving a node under an element that binds its prefix to a different URI does
75
+ not change `namespace_uri`; the serializer emits the declaration the
76
+ output needs (`<p:x xmlns:p="urn:a"/>`), and nothing when the destination
77
+ already agrees. libxml2 keeps the URI on an in-document move but does *not*
78
+ emit the declaration, so Nokogiri's tree and its own output disagree there.
79
+ * `#to_xml` on a node below the root is self-contained: it declares the
80
+ prefixes its subtree uses, so the output re-parses to the same namespaces
81
+ standing alone. Nokogiri omits them, and its subtree output does not
82
+ round-trip.
83
+ * Where one prefix would have to mean two things at once, the serializer
84
+ invents one (`ns1`, `ns2`, ...) rather than shadow the other, as browsers do.
85
+ * An element in no namespace stays that way under a default namespace,
86
+ serialized as `xmlns=""`.
87
+ * Nodes from the factories (`create_element` and friends) still take their
88
+ namespace from the context they are first inserted into, so a subtree can be
89
+ built detached and attached afterwards. Only later moves carry.
90
+ * Inserting a node from another document adopts it (`add_child` / `before` /
91
+ `after` / `replace`): it is brought over and taken out of the document it came
92
+ from, as `appendChild` does in the DOM and in both Chrome and Nokogiri.
93
+ * Each arena owns its own nodes, so the node cannot be relinked across them: it
94
+ is copied here and removed there. The one visible difference from Nokogiri
95
+ and browsers is that the node handed back is a different object than the
96
+ one passed in - use the return value afterwards, not the argument.
97
+ * `Document#import_node` is the copy: it leaves the source alone, like DOM
98
+ `importNode`.
99
+ * Otherwise the parsed tree is byte-identical to `Nokogiri::XML`'s (verified by
100
+ the property-based differential), including namespaces, prolog/epilog comments
101
+ and PIs, and adjacent-CDATA coalescing.
102
+
103
+ ## HTML parsing
104
+
105
+ * `<?php ... ?>` in HTML input is a **ProcessingInstruction** node; `#to_html`
106
+ writes it back as `<?php ... ?>`, and `<?` at end of input is ignored.
107
+ * The HTML Standard added processing-instruction tokens and tree-construction
108
+ rules for them; Makiri follows them through Lexbor. `Nokogiri::HTML5` (gumbo)
109
+ still produces the older bogus comment (`<!--?php ... ?-->`), and
110
+ `Nokogiri::HTML` (libxml2) its own comment.
111
+
112
+ ## CSS
113
+
114
+ * Most jQuery/Nokogiri CSS extensions are not supported (`:gt`, `:lt`, `:eq`, `:first`, ...)
115
+ * Makiri uses Lexbor's selector engine, which is standards-based apart from one
116
+ text-containment extension. Use XPath (`xpath("//p[contains(., 'x')]")`) or
117
+ Enumerable (`css('li')[1]`) for the rest.
118
+ Standard Level-4 selectors (`:is` / `:where` / `:has`) are supported; some of which Nokogiri rejects.
119
+ * `:lexbor-contains("text")` is supported (on both HTML and XML) - Lexbor's
120
+ spelling of the jQuery `:contains()` substring filter, matching an element
121
+ whose text contains the string; append ` i` (`:lexbor-contains("text" i)`)
122
+ for an ASCII case-insensitive match. (Nokogiri's name `:contains` is not an
123
+ alias.) Like Lexbor's matcher, it tests the element's immediate child text
124
+ nodes (not the deep string-value), so HTML and XML agree; on XML it lowers
125
+ to XPath `child::text()[contains(., "text")]`.
126
+ * Untyped `:*-of-type` (`:first-of-type`, `:nth-of-type(an+b)`, ... with no type
127
+ selector) is supported and correct on both HTML and XML - the "type" is the
128
+ element's own expanded name.
129
+ * Nokogiri (XML and HTML5) mistranslates these to first-/only-child
130
+ (`//*[position()=1]` / `//*[last()=1]`), so it under-matches; Makiri matches
131
+ Lexbor's HTML matcher.
132
+ * HTML CSS takes a `{prefix => uri}` Hash (`css(selector, ns)`) but does not
133
+ resolve prefixes against it: Lexbor's matcher matches a prefixed type selector
134
+ loosely, so `svg|path`, `|path` and `path` all find the SVG element whatever
135
+ is bound. `Nokogiri::HTML5` honours the binding (a wrong URI finds nothing).
136
+ * The Hash is accepted and unused rather than refused, so the `css(selector,
137
+ ns)` a caller writes for both representations works. Use `#xpath`, where a
138
+ prefix IS resolved against the bindings, when the namespace matters.
139
+ * `Makiri::XML` resolves CSS prefixes properly - it lowers the selector to the
140
+ XPath engine, which registers the bindings.
141
+ * `#matches?` answers for a DETACHED node (`document.create_element("p")
142
+ .matches?("p")` is true, on both representations). Nokogiri raises
143
+ `NoMethodError` there - it implements `#matches?` as a search from
144
+ `ancestors.last`, which a detached node does not have.
145
+ * * Type selectors are ASCII case-insensitive (CSS-correct for HTML; `LI` matches `<li>`)
146
+ * `Nokogiri::HTML5` is case-sensitive there.
147
+
148
+ ## Serialization
149
+
150
+ * Comment data is written literally, as the WHATWG serialization algorithm
151
+ says and as browsers do: `comment.content = "a-->b"` serializes to
152
+ `<!--a-->b-->`, which re-parses as the comment `"a"` followed by text.
153
+ * `Nokogiri::HTML5` escapes it to `<!--a--&gt;b-->` instead. That does not
154
+ round-trip either - comments do not decode entities, so the data comes back
155
+ as `"a--&gt;b"`. Neither library round-trips this; Makiri matches Chrome.
156
+ * The same applies to the children of `style` / `script` / `xmp` / `iframe` /
157
+ `noembed` / `noframes` / `plaintext`, which the algorithm also writes
158
+ literally. `noscript` is escaped, because Makiri parses it with scripting
159
+ disabled (its children are elements, not raw text) and escaping is what makes
160
+ that round-trip; `Nokogiri::HTML5` writes it literally and contradicts its own
161
+ parser there.
162
+
163
+ ## Text input (mutation APIs)
164
+
165
+ * Programmatic string arguments must be valid UTF-8 (invalid bytes raise
166
+ `Makiri::Error`, never silently repaired - unlike HTML *parsing*, which decodes
167
+ leniently to U+FFFD).
168
+ * An embedded NUL (U+0000) is accepted in HTML data-family content -
169
+ text/comment node content (`create_text_node`, `create_comment`, `content=`)
170
+ and attribute values (`[]=`, `set_attribute_ns`) - and stored/read back
171
+ verbatim, matching the WHATWG DOM / browsers (`document.createTextNode("\0")`).
172
+ It is still rejected in names, tag names, namespaces, PI target/data, CSS
173
+ selectors, and XPath expressions/variable names (a NUL there raises).
174
+ * On re-parse, the HTML tokenizer replaces a U+0000 in text/attributes with
175
+ U+FFFD (WHATWG), so a serialized-then-reparsed round-trip is not byte-identical.
176
+ * `Makiri::XML` rejects NUL everywhere: XML 1.0 has no legal U+0000 character,
177
+ so admitting it would produce non-well-formed XML.