makiri 0.10.0.rc1-aarch64-linux → 0.11.0-aarch64-linux

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/libfuzzer.yml +1 -1
  3. data/.github/workflows/release.yml +11 -2
  4. data/.github/workflows/security.yml +7 -8
  5. data/CHANGELOG.md +306 -29
  6. data/NOKOGIRI_DIFFERENCES.md +325 -0
  7. data/README.md +17 -154
  8. data/Rakefile +39 -9
  9. data/lib/makiri/3.2/makiri.so +0 -0
  10. data/lib/makiri/3.3/makiri.so +0 -0
  11. data/lib/makiri/3.4/makiri.so +0 -0
  12. data/lib/makiri/4.0/makiri.so +0 -0
  13. data/lib/makiri/attr.rb +15 -0
  14. data/lib/makiri/cdata_section.rb +8 -0
  15. data/lib/makiri/clone_via_dup.rb +19 -0
  16. data/lib/makiri/comment.rb +7 -0
  17. data/lib/makiri/css.rb +5 -4
  18. data/lib/makiri/document_fragment.rb +5 -2
  19. data/lib/makiri/document_type.rb +2 -3
  20. data/lib/makiri/element.rb +9 -0
  21. data/lib/makiri/error.rb +26 -0
  22. data/lib/makiri/html/document.rb +19 -14
  23. data/lib/makiri/html/node_methods.rb +9 -8
  24. data/lib/makiri/html.rb +3 -3
  25. data/lib/makiri/node.rb +14 -81
  26. data/lib/makiri/node_path.rb +95 -0
  27. data/lib/makiri/node_set.rb +50 -25
  28. data/lib/makiri/processing_instruction.rb +10 -8
  29. data/lib/makiri/reader_aliases.rb +28 -0
  30. data/lib/makiri/text.rb +7 -0
  31. data/lib/makiri/version.rb +1 -1
  32. data/lib/makiri/xml/builder.rb +17 -8
  33. data/lib/makiri/xml/document.rb +23 -3
  34. data/lib/makiri/xml/namespace.rb +17 -0
  35. data/lib/makiri/xml/node_methods.rb +26 -25
  36. data/lib/makiri/xml.rb +4 -3
  37. data/lib/makiri/xpath.rb +8 -8
  38. data/lib/makiri/xpath_context.rb +1 -1
  39. data/lib/makiri/xpath_syntax.rb +43 -0
  40. data/lib/makiri.rb +14 -7
  41. data/script/api_manifest.rb +25 -3
  42. data/script/check_alloc_failures.rb +398 -16
  43. data/script/check_unsafe_boundaries.rb +480 -66
  44. data/script/leaks_harness.rb +14 -1
  45. data/suppressions/ruby.supp +10 -8
  46. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5a56937c4116a0f55e5e0d8885c311b1f1b1ed2176808f161a67625fc21b71f
4
- data.tar.gz: 9ea9e72c0b8b813d6a6754d2bf5198f3478ebc9f47df570ac53eb3c79b864ce6
3
+ metadata.gz: 5b203c0ffb03e49f10b8e30afd6b070d36fd054867b7b3fd0e1376778b9e4b94
4
+ data.tar.gz: 3a26a99a690546bcc5367fab22645b8c47bd65dec58df75df5a7a93950de3386
5
5
  SHA512:
6
- metadata.gz: 9fb9f54c36318f05e2d8970c1342d1ade6b36d9fc85045df4296a352e2d759a29b11428443b61232be9d7fcf88b20d7bd2e12503927584444f8cd5373701818c
7
- data.tar.gz: 0cadc6458fc773c334d892ff764e6bc41c837808693b86183d0c819d9bc496ba7ffc78bd7a50adce5de77a5a687d63bfb6f8aa12c1ce09b6b791a060e94ded03
6
+ metadata.gz: 5397148284ec99562ffe74e4f633f903405c420cc7593c996b600d13c65e9f94a48bf69a477a45063cd6e2a369b87f4d55f324bce1bcc0087b7f37cc6d73f5d4
7
+ data.tar.gz: 232787e75468ce0da009f1755538c062cabab7bdadd0a3a0eeb8b9e5f388f59eb0f4159e9f2b8152761f7c7c9e6619a3aa5568955358435d26f8a130e31a472e
@@ -25,7 +25,7 @@ jobs:
25
25
  strategy:
26
26
  fail-fast: false
27
27
  matrix:
28
- target: [xml, xpath, xml_xpath, css, html, html_xpath]
28
+ target: [xml, xpath, xml_xpath, css, html, html_xpath, html_css]
29
29
 
30
30
  steps:
31
31
  - name: Checkout (with vendored Lexbor submodule)
@@ -86,9 +86,18 @@ jobs:
86
86
  # names the GNU triple; this installs the std that goes with it.
87
87
  # Empty elsewhere, so the other runners are unaffected.
88
88
  targets: ${{ runner.os == 'Windows' && 'x86_64-pc-windows-gnu' || '' }}
89
- - name: Install libclang (bindgen, via rb-sys)
89
+ # clang/lld/llvm are for the vendored Lexbor's LTO, not for bindgen: on
90
+ # Linux the whole chain has to be LLVM or extconf turns LTO off (gcc
91
+ # emits GIMPLE, which rust-lld cannot read - see the note in
92
+ # extconf.rb). It DETECTS them, so a runner without them still builds,
93
+ # just without LTO - which is what this job used to ship, silently,
94
+ # to every user who installs the precompiled gem (see the measured win
95
+ # in CLAUDE.md's "Vendored Lexbor is built with LTO" section: this is
96
+ # the job that produces those gems, and `ci.yml` installing the chain
97
+ # only exercises the fast path in CI, it does not ship it).
98
+ - name: Install libclang (bindgen) and the LLVM chain (Lexbor LTO)
90
99
  if: runner.os == 'Linux'
91
- run: sudo apt-get update && sudo apt-get install -y libclang-dev
100
+ run: sudo apt-get update && sudo apt-get install -y libclang-dev clang lld llvm
92
101
  - uses: ruby/setup-ruby@v1
93
102
  with:
94
103
  ruby-version: ${{ matrix.ruby }}
@@ -55,7 +55,7 @@ jobs:
55
55
  bundler-cache: true
56
56
 
57
57
  - name: Run short fuzz under sanitizers
58
- run: bundle exec rake fuzz:sanitize FUZZ_ARGS="--time 30"
58
+ run: FUZZ_TIME=30 bundle exec rake fuzz:sanitize
59
59
 
60
60
  # macOS-only malloc-leak gate: ASan everywhere runs with detect_leaks=0 (Ruby
61
61
  # and Lexbor are uninstrumented), so this is the ONLY automated leak check. It
@@ -98,9 +98,9 @@ jobs:
98
98
  run: bundle exec rake leaks
99
99
 
100
100
  # OOM-injection sweep: rebuilds with MAKIRI_ALLOC_INJECT=1 and fails each core
101
- # C allocation site in turn, gating that every OOM branch fails closed - a
102
- # clean exception or a baseline-identical result, never truncated output
103
- # (see script/check_alloc_failures.rb).
101
+ # Rust allocation site in turn, gating that every OOM branch fails closed - a
102
+ # clean exception or a baseline-identical result, with stateful objects reusable
103
+ # after the failure (see script/check_alloc_failures.rb).
104
104
  security-alloc-inject:
105
105
  name: OOM-injection sweep
106
106
  runs-on: ubuntu-latest
@@ -137,9 +137,8 @@ jobs:
137
137
  - name: Run the OOM-injection sweep
138
138
  run: bundle exec rake oom
139
139
 
140
- # Nightly: fuzz EVERY target (the 30s PR fuzz covers only the default xpath
141
- # target; the CSS engine reuse and the XML parser/mutator are each their own
142
- # documented memory-safety risk, so each gets a full 300s run).
140
+ # Nightly: fuzz EVERY target for a longer run. The PR job already exercises
141
+ # every surface for 30s; nightly gives each target a full 300s run.
143
142
  security-fuzz-nightly:
144
143
  name: Nightly sanitized fuzz (${{ matrix.target }})
145
144
  runs-on: ubuntu-latest
@@ -178,7 +177,7 @@ jobs:
178
177
  bundler-cache: true
179
178
 
180
179
  - name: Run nightly fuzz under sanitizers
181
- run: bundle exec rake fuzz:sanitize FUZZ_ARGS="--target ${{ matrix.target }} --time 300"
180
+ run: FUZZ_ARGS="--target ${{ matrix.target }} --time 300" bundle exec rake fuzz:sanitize
182
181
 
183
182
  # Nightly: the whole spec suite with Lexbor ITSELF built under ASan (mraw
184
183
  # poisoning on), catching intra-arena overflows that a plain ASan build cannot
data/CHANGELOG.md CHANGED
@@ -1,34 +1,311 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0] - 2026-09-30
4
+
5
+ No code changes since 0.11.0.rc2. Coming from 0.10.x, read the rc2 and rc1
6
+ notes below as well.
7
+
8
+ ## [0.11.0.rc2] - 2026-09-30
9
+
10
+ ### Added
11
+
12
+ * `Makiri::HTML::Document#create_element_ns(namespace_uri, qualified_name)`
13
+ (DOM `createElementNS`). An SVG or MathML element made this way keeps its
14
+ name's case and behaves like a parsed one. An upper-case name in the HTML
15
+ namespace that names a known element (`BR`, `DIV`) raises `Makiri::Error`.
16
+ * `Makiri::XML::Element#set_loose_dom_attribute(qualified_name, value)` (DOM
17
+ `setAttribute`): the attribute is in no namespace and keeps the name as
18
+ given (`xmlns`, `xlink:href`, `v-on:click`). `to_xml` raises while the
19
+ document holds one XML cannot write.
20
+
21
+ ### Changed
22
+
23
+ * A NUL in an element, attribute, doctype or PI target name raises
24
+ `ArgumentError`, like any other invalid name. A namespace that does not fit
25
+ the name still raises `Makiri::Error`.
26
+ * HTML type selectors are case-sensitive on SVG and MathML elements, as in
27
+ browsers: `fegaussianblur` no longer matches `feGaussianBlur`.
28
+ * `set_attribute_ns` accepts the XML namespace with any prefix or none again
29
+ (rc1 raised); `to_xml` writes such an attribute as `xml:name`.
30
+ * XML `set_attribute_ns` accepts a namespace declaration XML forbids, such as
31
+ `xmlns:foo=""`. It binds nothing, and `to_xml` raises while it is present.
32
+ * XML `create_document_type` accepts any public and system id, and any name
33
+ without whitespace, NUL or `>`. `to_xml` raises if the doctype cannot be
34
+ written as XML.
35
+
36
+ ### Fixed
37
+
38
+ * `import_node` from XML to HTML keeps an upper-case XHTML element name
39
+ (`Foo`) instead of lower-casing it, and raises for one that names a known
40
+ element (`BR`).
41
+
42
+ ## [0.11.0.rc1] - 2026-09-29
43
+
44
+ ### Changed
45
+
46
+ * HTML `#css` / `#at_css` / `#matches?` now match with Makiri's own Rust
47
+ implementation instead of Lexbor's `lxb_selectors` (selectors are still
48
+ parsed by Lexbor). Where the answers differ:
49
+ * `:nth-child(An+B of S)` / `:nth-last-child(... of S)` count by the CSS
50
+ definition; Lexbor miscounted when `S` was a comma list, held a
51
+ combinator, or carried pseudo-classes such as `:enabled`.
52
+ * `:disabled`, `:enabled` and `:checked` follow the HTML Standard: a
53
+ control inside a `<fieldset disabled>` (outside its first `<legend>`) is
54
+ disabled, `<option>` / `<optgroup>` count, and `:enabled` matches only
55
+ form elements.
56
+ * Attribute selector names are matched by qualified name, and
57
+ case-sensitively on SVG/MathML elements: `[href]` no longer finds
58
+ `xlink:href`, nor `[viewbox]` an SVG `viewBox`.
59
+ * `#id` / `.class` match only the no-namespace `id` / `class` attribute.
60
+ * A selector chain is capped at 64 compounds (as `Makiri::XML` already
61
+ was), and a query at 50M steps of work (XPath's limit); past either,
62
+ `Makiri::Error`.
63
+ * The column combinator `||` raises `Makiri::Error` instead of matching
64
+ nothing.
65
+
66
+ ### Removed
67
+
68
+ * `:lexbor-contains()` in HTML `#css` / `#at_css` / `#matches?`: it still
69
+ parses, but raises `Makiri::Error`. `Makiri::XML`'s `#css` keeps it. See
70
+ NOKOGIRI_DIFFERENCES.md.
71
+ * `Node#name=` / `#node_name=`. The DOM cannot rename an element, and renaming
72
+ a Lexbor element in place could crash (`div` to `template`). Create a new
73
+ element and `replace` the old one.
74
+
75
+ ### Security
76
+
77
+ * Hardening: every heap block the vendored Lexbor allocates carries 16 bytes
78
+ of slack past its end, so a small overrun cannot reach neighbouring memory
79
+ (not in sanitizer builds).
80
+ * Adding an `id` or `class` attribute beside an existing one
81
+ (`set_attribute_ns(nil, "ID", v)`, a namespaced `id`) no longer frees the
82
+ existing one under a held `Attr`. HTML attribute reads and writes follow the
83
+ DOM: `#[]`, `#key?`, `#[]=` and `#delete` match the qualified name
84
+ (`svg_a["href"]` no longer returns `xlink:href`), copies and imports keep
85
+ every attribute, and a no-namespace name keeps its case.
86
+ * `content=` on an HTML element and `delete(name)` detach the nodes they
87
+ remove instead of freeing them under a live wrapper.
88
+ * `el[name] = value` on an existing attribute no longer leaves a destroyed
89
+ attribute linked when storing the value runs out of memory.
90
+ * A checked argument String is locked while its bytes are in use, so a later
91
+ argument's `#to_s` cannot rewrite it (it raises instead); a receiver frozen
92
+ by an argument's `#to_s` is not edited; and a mutator's argument can no
93
+ longer rebuild the document's indexes in the middle of the edit.
94
+ * HTML element and attribute names follow the DOM's rules:
95
+ `create_element("img src=x onerror=alert(1)")` raises `ArgumentError`
96
+ instead of writing that markup. See NOKOGIRI_DIFFERENCES.md.
97
+ * HTML parsing bounds the tree depth (`max_tree_depth:`, default 400) and the
98
+ `<option>`s one `<select>` receives (10,000), raising `Makiri::Error` past
99
+ either; both made the parse quadratic.
100
+ * `Makiri::Lexbor::CSS.parse_stylesheet` no longer crashes on a
101
+ `:lexbor-contains()` after a string ended by CR, FF or a newline.
102
+ * A CSS query that runs out of memory raises instead of answering with what it
103
+ had collected; a selector-parse OOM is reported as OOM, not
104
+ `CSS::SyntaxError`.
105
+ * Inputs whose cost grew faster than their size are linear or budgeted: many
106
+ namespace bindings, the `preceding` axis and reverse-axis steps, XML
107
+ duplicate-attribute checks, `contains` / `substring-*` / `translate` on long
108
+ strings, XML CSS `:nth-*`, CDATA full of `]]>`, and stylesheets with
109
+ rejected `:lexbor-contains()`.
110
+ * A panic in any Ruby method Makiri defines raises `Makiri::InternalError`
111
+ (rescuable), not `fatal`.
112
+
113
+ ### Fixed
114
+
115
+ * A document that grows by editing (appended nodes, `inner_html=`) reports
116
+ its new size to the GC, so memory pressure from it triggers collections; it
117
+ was reported once, at parse time.
118
+ * A node wrapped while memory runs out raises `Makiri::Error` instead of
119
+ coming back as a second Ruby object for the same node, without the first
120
+ one's `freeze`, instance variables or singleton methods.
121
+ * Insertion follows the DOM's pre-insertion rules in HTML and XML alike: no
122
+ cycles through a template's contents (which hung `dup`), no children on
123
+ Text/Comment/PI/DocumentType/Attr, no Text directly under an XML Document,
124
+ and an XML `DocumentFragment` takes children.
125
+ * `inner_html` / `inner_html=` on a `<template>` target its contents, agreeing
126
+ with `#to_html` and `content_fragment`.
127
+ * HTML `#keys` / `#values` raise on out-of-memory instead of returning a
128
+ truncated Array.
129
+ * `dup`, `clone_node` and `import_node` keep an element's name as written
130
+ (`linearGradient`, `q:Bar`), and a copied XML doctype keeps its PUBLIC id.
131
+ * Namespaces across HTML <-> XML `import_node`: attributes keep their own
132
+ namespace, a prefixed element keeps its prefix and case, names XML cannot
133
+ write are refused where they would be written, and copied `xmlns`
134
+ attributes no longer move elements between namespaces.
135
+ * XML namespaces: `set_attribute_ns` on a detached element survives insertion;
136
+ `[]=` on an existing attribute changes only its value; the mutators refuse
137
+ what the parser refuses (forbidden declarations, duplicate expanded names,
138
+ bad doctype names and PUBLIC ids); `canonicalize` raises instead of writing a
139
+ wrong namespace; and a refused declaration says which rule it broke.
140
+ * `to_xml` output re-parses: CDATA holding `]]>`, a SYSTEM id holding `"`, and
141
+ attributes with a namespace but no prefix.
142
+ * XPath: `string()` of a number follows libxml2 (as Nokogiri does);
143
+ `substring()` rounds per spec; node-set vs boolean comparisons follow §3.4;
144
+ the `xml` prefix is always bound; a comparison no longer raises
145
+ `LimitExceeded` because its values added up past 64 MB.
146
+ * `Node#path` round-trips through `#at_xpath` for CDATA, PIs, and namespaced
147
+ nodes (SVG/MathML, XML namespaces, `xlink:href`, Vue/Word-style names); an
148
+ unreachable node answers `"?"` as in Nokogiri.
149
+ * CSS over XML agrees with the HTML matcher on `.x\ y`, `:root`, empty and
150
+ whitespace attribute values, `$=` with non-ASCII, `:empty` beside a comment,
151
+ and `[|a]`; the `s` modifier is accepted.
152
+ * An HTML document refuses a second root element or a text child; an
153
+ attribute's parent is always its owner element.
154
+ * `Node#line` of a node copied from another document is nil.
155
+ * `Makiri::XML` nodes compare with `<=>` in document order (an attribute with
156
+ itself too), and `XML::Document#dup` / `#clone` return a copy.
157
+ * `clone_node` on a Document raises instead of returning the document.
158
+ * `NodeSet#css` / `#xpath` / `#at_css` / `#at_xpath` pass their extra
159
+ arguments through; `#xpath` of a non-node-set expression raises
160
+ `ArgumentError`, as in Nokogiri.
161
+ * `XML::Builder` no longer answers `respond_to?` for `to_ary` and similar.
162
+ * Namespace Hashes and `register_namespace` arguments are read as a Hash and
163
+ with `String()`, with one length cap and one error message.
164
+ * A rejected stylesheet rule's `selector_text` is sliced by Lexbor's offsets.
165
+
166
+ ### Performance
167
+
168
+ * `NodeSet#at_css` / `#at_xpath` stop at the first node with a match.
169
+ * `XML::Node#canonicalize` looks namespaces up by prefix in the scope it keeps:
170
+ a 1000-deep document of declarations went from 0.7 s to 0.02 s.
171
+
172
+ ## [0.10.0] - 2026-09-22
173
+
174
+ ### Fixed
175
+
176
+ * A frozen node now raises `FrozenError` when it is the ARGUMENT of a tree
177
+ mutation, not only the receiver: `b.add_child(a)` relinks `a` exactly as
178
+ `a.remove` does. Both `Makiri::XML` and `Makiri::HTML`. A fragment argument
179
+ splices its children, which have no wrapper of their own to check.
180
+ * A `Makiri::XML` mutation that exceeds the document's own `max_bytes` /
181
+ `max_nodes` now raises `Makiri::XML::LimitExceeded` instead of reporting the
182
+ refusal as out of memory.
183
+ * Inserting a `DocumentFragment` is all or nothing on `add_child`, `before` and
184
+ `after`, as it already was on `replace`: a child the rules refuse no longer
185
+ leaves the earlier ones linked in a document the caller was told had not
186
+ changed.
187
+ * A rejected `Makiri::XML::Document#fragment` no longer charges the document for
188
+ the nodes it discarded. 100k rejected fragments grew a `<r/>` document to
189
+ 77 MB; it is now 516 bytes.
190
+ * A failed `Makiri::XML` parse reports the FIRST failure for all four kinds
191
+ (`syntax` was sticky while `limit` and `unsupported` overwrote each other).
192
+ * `Makiri::XML#to_xml`'s serializer allocates fallibly again, so running out of
193
+ memory raises instead of aborting the process.
194
+ * `:lexbor-contains()` now rejects an argument the bundled CSS parser does not
195
+ take, the way any unknown pseudo-class is rejected: `Makiri::CSS::SyntaxError`
196
+ from `#css` / `#at_css` / `#matches?`, and a `:bad_style` rule from
197
+ `Makiri::Lexbor::CSS.parse_stylesheet`. Well-formed uses are unchanged.
198
+
199
+ ### Performance
200
+
201
+ * `Makiri::XML#to_xml` plans namespaces from a binding stack instead of
202
+ re-walking each ancestor's attribute list, which cost O(depth^2 x attributes).
203
+ 403 KB of nested prefixed attributes took 4.88s and now takes 0.001s. A
204
+ crafted document fails closed with `Makiri::Error` ("namespace planning
205
+ exceeded its step budget") rather than running on.
206
+ * Setting an attribute on a `Makiri::XML` element walks the attribute list once
207
+ instead of twice (4096 attributes: 47ms -> 27ms).
208
+
209
+ ## [0.10.0.rc2] - 2026-09-20
210
+
211
+ ### Added
212
+
213
+ * `XPathContext.new` now registers namespace bindings passed via
214
+ `prefix: uri` (previously ignored).
215
+ * `#css`, `#at_css`, and `#matches?` accept `(selector, namespaces = nil)`
216
+ on HTML documents for consistency with XML.
217
+ * Standardized error message format for rejected CSS selectors across XML
218
+ and HTML: `"<reason>: <selector>"`.
219
+ * Unified argument handling for `#xpath` and `#at_xpath` between HTML and XML.
220
+ * HTML supports per-query namespace bindings and keyword prefixes
221
+ (e.g., `s: uri`).
222
+ * XML supports custom-function handlers.
223
+ * XML nodes now support HTML-style node readers and aliases
224
+ (`#first_element_child`, `#next_element`, `#tag_name`, `#attr`,
225
+ `#node_name`, etc.).
226
+
227
+ ### Changed
228
+
229
+ * Updated vendored Lexbor to `v3.0.0-66`.
230
+ * Parsed `<?php ... ?>` tags in HTML input as `ProcessingInstruction` nodes
231
+ instead of comments, adhering to the HTML Standard.
232
+ * Vendored Lexbor updated from `v3.0.0-25` to `v3.0.0-66` (`05b5d37`), for an
233
+ out-of-bounds write in `lxb_dom_character_data_replace` reachable through
234
+ `#content=`, UTF-16 surrogate rejection, exact `meta` charset matching and
235
+ buffer-capacity fixes. (v3.0.1 is not usable: it lacks the two CSS-selector
236
+ fixes Makiri upstreamed.)
237
+ * `<?php ... ?>` in HTML input is now a `ProcessingInstruction` node rather
238
+ than a Comment, serializes as `<?target data?>`, and `<?` at end of input is
239
+ ignored - the HTML Standard's processing-instruction tokens, which Lexbor now
240
+ implements. `Nokogiri::HTML5` still answers with a comment.
241
+ * `Makiri::XML#matches?` tests the node locally via tree walking instead of
242
+ performing a full-document search.
243
+ * `Makiri::XML::Namespace` is now an immutable `Data` object defined in Ruby.
244
+ * `XML::Node#value` returns its text content (or attribute value for
245
+ attributes), aligning with HTML node behavior.
246
+ * XPath over HTML now handles element/attribute names case-insensitively
247
+ and properly handles `xmlns` attributes according to browser specs.
248
+ * `Makiri::XML` checks the internal DTD subset for well-formedness and
249
+ raises `Makiri::XML::SyntaxError` if unsupported constructs (e.g., attribute
250
+ defaults, entity references) would alter the document tree.
251
+ * Documents with `version="1.x"` are now accepted and parsed as XML 1.0.
252
+ * Processing instruction (PI) targets containing colons (e.g., `<?a:b?>`)
253
+ are now rejected per Namespaces in XML specs.
254
+
255
+ ### Fixed
256
+
257
+ * `Makiri::XML::DocumentType#prefix` now returns `nil` instead of
258
+ the PUBLIC ID.
259
+ * `Makiri::XML#last_element_child` now correctly returns an `Element`
260
+ instead of arbitrary child nodes (like comments or text).
261
+ * `Element#local_name` and `local-name()` in XPath preserve camelCase
262
+ for SVG/MathML elements (e.g., `foreignObject`).
263
+ * `inner_html=` and `outer_html=` are now atomic operations to prevent
264
+ leaving the DOM in a corrupted state on failure.
265
+ * XPath over HTML assigns empty/correct namespaces to attributes instead of
266
+ inheriting from their parent element.
267
+ * Corrected `lang()` evaluation to check the nearest language attribute
268
+ hierarchically.
269
+ * Fixed `namespace_matching: :lax` on XML to be strict, aligning with
270
+ `Nokogiri::XML` / `libxml2` behavior.
271
+ * Moving an HTML node to another document now correctly removes it
272
+ from the source document's indexes.
273
+ * Prevented XPath custom handlers from creating new nodes on the document
274
+ being evaluated.
275
+ * Fixed CSS universal selectors (`p|*`, `|*`) on XML to respect namespace
276
+ boundaries.
277
+ * CSS column combinator (`a || b`) on XML now raises
278
+ `Makiri::CSS::SyntaxError` instead of being misparsed as descendant selector.
279
+
3
280
  ## [0.10.0.rc1] - 2026-09-19
4
281
 
5
282
  ### Changed
6
283
 
7
- * **The native extension is rewritten in Rust.** The C glue, the XPath engine,
284
+ * The native extension is rewritten in Rust. The C glue, the XPath engine,
8
285
  the XML reader and the CSS lowering are now one Rust crate; the only C left is
9
286
  the vendored Lexbor, still unpatched. The Ruby API is unchanged, and answers
10
287
  were checked against the C build's recorded output as well as the existing
11
288
  differential suites against Nokogiri.
12
289
 
13
- * **Installing from source needs a Rust toolchain.** `cargo` (stable) and
290
+ * Installing from source needs a Rust toolchain. `cargo` (stable) and
14
291
  libclang are required alongside CMake, and `rb_sys` becomes a runtime
15
292
  dependency of the source gem, because its `extconf.rb` runs at install time.
16
293
  The precompiled platform gems need none of this and do not depend on
17
294
  `rb_sys`.
18
- * **Faster.** Makiri now beats both Nokogiri and nokolexbor on every
295
+ * Faster. Makiri now beats both Nokogiri and nokolexbor on every
19
296
  `rake bench` row, parse included - previously ~1.25x slower than nokolexbor.
20
297
  Source locations are stamped on the first `#line` or mutation rather than
21
298
  during every parse, and the vendored Lexbor is built with link-time
22
299
  optimization where the linker supports it (macOS; Linux with clang,
23
300
  llvm-ar and lld; not Windows).
24
- * **An internal failure is an exception, not a crash.** It used to end the
301
+ * An internal failure is an exception, not a crash. It used to end the
25
302
  host process with SIGABRT. Now it unwinds on the thread that ran the call:
26
303
  `ensure` blocks run and the process keeps working. On entry points that
27
304
  handle input (parse, XPath, CSS, serialization, text) it is the new
28
- **`Makiri::InternalError`**, which descends from `Exception`, not
305
+ `Makiri::InternalError`, which descends from `Exception`, not
29
306
  `StandardError`, so a bare `rescue => e` does not swallow it; elsewhere it
30
307
  is Ruby's `fatal`.
31
- * **An XPath handler may not modify the document being evaluated.** Every
308
+ * An XPath handler may not modify the document being evaluated. Every
32
309
  mutator on that document raises `Makiri::Error` while an evaluation with a
33
310
  handler runs, because the evaluator holds names and values from it for the
34
311
  whole walk.
@@ -46,14 +323,14 @@
46
323
 
47
324
  * `Node#attribute_by_qualified_name(name)` and
48
325
  `Node#attribute_value_by_qualified_name(name)`: the attribute whose
49
- **qualified** name is exactly `name` — its node, and its value — or nil.
326
+ qualified name is exactly `name` — its node, and its value — or nil.
50
327
  `#[]` cannot answer this: it also finds a prefixed attribute by its local
51
328
  name (`svg_a["href"]` returns `xlink:href`), and it lower-cases what it looks
52
329
  up (`el["DATA-X"]` finds `data-x`). The new match is byte-exact.
53
330
 
54
331
  ### Changed
55
332
 
56
- * **`Makiri::XML` follows the DOM namespace model.** A node's namespace URI is
333
+ * `Makiri::XML` follows the DOM namespace model. A node's namespace URI is
57
334
  decided once — by the parser, or by the context it is first inserted into —
58
335
  and does not change afterwards; the serializer emits whatever xmlns
59
336
  declarations the output needs:
@@ -71,24 +348,24 @@
71
348
  Nodes from the factories (`create_element` and friends) still take their
72
349
  namespace from the context they are first inserted into.
73
350
 
74
- * **Inserting a node from another document adopts it.** `add_child` / `before` /
351
+ * Inserting a node from another document adopts it. `add_child` / `before` /
75
352
  `after` / `replace` bring the node over and remove it from the document it
76
353
  came from, instead of copying it (`Makiri::XML`) or raising (`Makiri::HTML`).
77
354
  A spliced fragment is left empty; a rejected insert leaves the source
78
355
  document untouched.
79
356
 
80
- The node handed back is a **different object** than the one passed in, so use
357
+ The node handed back is a different object than the one passed in, so use
81
358
  the return value afterwards rather than the argument. `Document#import_node`
82
359
  is unchanged: it copies and leaves the source alone.
83
360
 
84
- * **XML serialization is capped at the nesting depth the parser accepts.**
361
+ * XML serialization is capped at the nesting depth the parser accepts.
85
362
  `#to_xml` and `#canonicalize` raise `Makiri::Error` past it, rather than
86
363
  emitting XML that Makiri could not read back. A deeper tree is still fine to
87
364
  hold, walk and query.
88
365
 
89
366
  ### Fixed
90
367
 
91
- * XPath axes from an **attribute** context node on the XML backend now follow
368
+ * XPath axes from an attribute context node on the XML backend now follow
92
369
  XPath 1.0 §2.2: `following-sibling` and `preceding-sibling` are empty, and
93
370
  `following` / `preceding` exclude attribute nodes. They used to return the
94
371
  element's later attributes.
@@ -294,7 +571,7 @@
294
571
 
295
572
  ### Added
296
573
 
297
- * **Native XML 1.0 reader + in-place editor** - `Makiri::XML::Document.parse(source)`
574
+ * Native XML 1.0 reader + in-place editor - `Makiri::XML::Document.parse(source)`
298
575
  / `Makiri::XML(source)`. No libxml2: a strict, fail-closed parser builds its own
299
576
  node arena (case- and namespace-preserving), queried by the native XPath engine.
300
577
  * Strict & secure: fail-closed decode (bad UTF-8 / NUL -> `XML::SyntaxError`),
@@ -304,9 +581,9 @@
304
581
  encoding is a fatal error, not a silent mis-decode.
305
582
  * DoS-bounded by a single arena byte ceiling (default 256 MiB; raise per parse
306
583
  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.
584
+ * `<!DOCTYPE>` recognized but not processed (`#internal_subset` ->
585
+ `XML::DocumentType`); zero entity/DTD I/O, so XXE and billion-laughs are
586
+ structurally impossible. Kept off the tree, as in libxml2.
310
587
  * Read API mirrors Nokogiri: `#xpath` / `#at_xpath` (`{prefix => uri}`),
311
588
  name/namespace readers, `#text`, `#[]`, traversal, and namespace introspection
312
589
  (`Makiri::XML::Namespace`); `XPathContext` works over XML nodes too.
@@ -334,7 +611,7 @@
334
611
  * `NodeSet#[]` accepts a `Range` or `start, length` (like `Array#[]`).
335
612
  * `Node` / `NodeSet` / `Document` `#dup` / `#clone` now return real independent
336
613
  copies (`#dup(0)` shallow; `#clone(freeze:)` honoured).
337
- * A **frozen node is genuinely immutable** - every mutator raises `FrozenError`.
614
+ * A frozen node is genuinely immutable - every mutator raises `FrozenError`.
338
615
 
339
616
  ### Changed
340
617
 
@@ -346,7 +623,7 @@
346
623
  added `Node#cdata?`.
347
624
  * Text-index range table uses `uint32` bounds (24 -> 16 B/entry; ~27% less retained
348
625
  index, byte-identical text).
349
- * Parsing **honours the input String's encoding** - Shift_JIS / EUC-JP / ... are now
626
+ * Parsing honours the input String's encoding - Shift_JIS / EUC-JP / ... are now
350
627
  transcoded to UTF-8 instead of mangled.
351
628
  * Parsing skips its UTF-8 validation scan when the String's coderange already proves
352
629
  it valid.
@@ -355,7 +632,7 @@
355
632
 
356
633
  ### Fixed
357
634
 
358
- * **Hardened the HTML/XML representation boundary.** HTML (Lexbor) and XML (arena)
635
+ * Hardened the HTML/XML representation boundary. HTML (Lexbor) and XML (arena)
359
636
  nodes are now distinct TypedData types, so the wrong representation raises
360
637
  `TypeError` instead of corrupting memory:
361
638
  * `Node#==` / `XPathContext#node=` with an XML `Document` no longer aborts the
@@ -434,12 +711,12 @@
434
711
  ## [0.1.0] - 2026-06-02
435
712
 
436
713
  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**.
714
+ selectors for Ruby - built on vendored [Lexbor](https://lexbor.com/) with no
715
+ libxml2 / libxslt dependency at any layer.
439
716
 
440
717
  ### Added
441
718
 
442
- **Parsing & DOM**
719
+ Parsing & DOM
443
720
 
444
721
  * `Makiri::HTML` / `Makiri.parse` - HTML5 parsing via vendored, unpatched Lexbor,
445
722
  with browser-compatible UTF-8 decoding (invalid bytes → U+FFFD; parsing never
@@ -454,7 +731,7 @@ libxml2 / libxslt dependency at any layer**.
454
731
  quirks_mode,internal_subset,errors}` and `Makiri::DocumentType#{public_id,
455
732
  system_id,external_id}`.
456
733
 
457
- **XPath**
734
+ XPath
458
735
 
459
736
  * Native XPath 1.0 query engine (no libxml2/libxslt): `Node#{xpath,at_xpath}`
460
737
  and `Makiri::XPathContext` (`evaluate`, namespace/variable binding, custom
@@ -462,7 +739,7 @@ libxml2 / libxslt dependency at any layer**.
462
739
  built-in functions with spec-faithful semantics (XML NCNames including
463
740
  non-ASCII, node-set vs node-set comparisons per §3.4, document order per §5.1,
464
741
  Unicode-aware `translate`/`substring`).
465
- * Namespace matching is **strict by default** (HTML5/WHATWG-faithful, like
742
+ * Namespace matching is strict by default (HTML5/WHATWG-faithful, like
466
743
  browsers' `document.evaluate` and `Nokogiri::HTML5`); pass
467
744
  `namespace_matching: :lax` for the namespace-agnostic, `Nokogiri::HTML`-style
468
745
  match.
@@ -470,12 +747,12 @@ libxml2 / libxslt dependency at any layer**.
470
747
  (operation / recursion-depth / node-set / string-byte caps) that raise
471
748
  `Makiri::XPath::LimitExceeded` on overrun.
472
749
 
473
- **CSS**
750
+ CSS
474
751
 
475
752
  * `Node#{css,at_css,matches?}` via Lexbor's selector engine (descendant-only,
476
753
  document order). Malformed selectors raise `Makiri::CSS::SyntaxError`.
477
754
 
478
- **Mutation & serialization**
755
+ Mutation & serialization
479
756
 
480
757
  * DOM mutation: `add_child`/`<<`, `add_previous_sibling`/`before`,
481
758
  `add_next_sibling`/`after`, `remove`/`unlink`, `replace`; attribute `[]=` and
@@ -494,7 +771,7 @@ libxml2 / libxslt dependency at any layer**.
494
771
  `NodeSet#{|,+,&,-,css,xpath,search,at,last,remove}`, and `Element.new` /
495
772
  `Text.new`.
496
773
 
497
- **Safety & concurrency**
774
+ Safety & concurrency
498
775
 
499
776
  * UTF-8 text-input contract: HTML and fragment parsing are lenient (invalid
500
777
  bytes → U+FFFD, never reject), while strings passed to the XPath / CSS /
@@ -505,7 +782,7 @@ libxml2 / libxslt dependency at any layer**.
505
782
  context across threads cannot corrupt memory. Fail-closed string caps and
506
783
  iterative (non-recursive) tree walks resist stack-exhaustion DoS.
507
784
 
508
- **Performance** (`rake bench`, vs Nokogiri/libxml2)
785
+ Performance (`rake bench`, vs Nokogiri/libxml2)
509
786
 
510
787
  * Meets or beats Nokogiri on every benchmarked operation: parse ~3×, css ~12×,
511
788
  at_css ~1000×, serialize ~4×, `//tag` ~3.4×, `[@attr='v']` predicate ~1.5×,
@@ -513,7 +790,7 @@ libxml2 / libxslt dependency at any layer**.
513
790
  a document element index (for `//tag`), a direct-attribute predicate fast
514
791
  path, and a hashed per-evaluate string-value cache.
515
792
 
516
- **Tooling**
793
+ Tooling
517
794
 
518
795
  * Vendored Lexbor as a git submodule (pinned v3.0.0, applied without patches).
519
796
  Build hardening flags; AddressSanitizer+UBSan build (`rake sanitize`);