makiri 0.10.0 → 0.11.0.rc2
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.
- checksums.yaml +4 -4
- data/.github/workflows/libfuzzer.yml +1 -1
- data/.github/workflows/release.yml +11 -2
- data/.github/workflows/security.yml +7 -8
- data/CHANGELOG.md +164 -0
- data/NOKOGIRI_DIFFERENCES.md +158 -10
- data/README.md +8 -3
- data/Rakefile +25 -9
- data/ext/makiri/rust/Cargo.toml +7 -7
- data/ext/makiri/rust/build.rs +52 -30
- data/ext/makiri/rust/clippy.toml +28 -9
- data/ext/makiri/rust/extconf.rb +3 -1
- data/ext/makiri/rust/fuzz/Cargo.toml +9 -2
- data/ext/makiri/rust/fuzz/fuzz_targets/common.rs +4 -11
- data/ext/makiri/rust/fuzz/fuzz_targets/css.rs +3 -3
- data/ext/makiri/rust/fuzz/fuzz_targets/html.rs +5 -5
- data/ext/makiri/rust/fuzz/fuzz_targets/html_css.rs +101 -0
- data/ext/makiri/rust/fuzz/fuzz_targets/html_xpath.rs +5 -2
- data/ext/makiri/rust/fuzz/fuzz_targets/xml_xpath.rs +15 -17
- data/ext/makiri/rust/fuzz/fuzz_targets/xpath.rs +22 -24
- data/ext/makiri/rust/src/bridge/doc.rs +64 -37
- data/ext/makiri/rust/src/bridge/fragment.rs +121 -102
- data/ext/makiri/rust/src/bridge/gvl.rs +59 -19
- data/ext/makiri/rust/src/bridge/html.rs +334 -250
- data/ext/makiri/rust/src/bridge/mod.rs +8 -0
- data/ext/makiri/rust/src/bridge/node_set.rs +131 -137
- data/ext/makiri/rust/src/bridge/node_wrap.rs +34 -0
- data/ext/makiri/rust/src/bridge/ruby.rs +89 -59
- data/ext/makiri/rust/src/bridge/stack.rs +17 -0
- data/ext/makiri/rust/src/bridge/string.rs +364 -188
- data/ext/makiri/rust/src/bridge/typed.rs +47 -40
- data/ext/makiri/rust/src/bridge/wrapper.rs +425 -111
- data/ext/makiri/rust/src/bridge/xml.rs +201 -180
- data/ext/makiri/rust/src/bridge/xml_decode.rs +44 -24
- data/ext/makiri/rust/src/bridge/xpath/context_object.rs +68 -38
- data/ext/makiri/rust/src/bridge/xpath/handler.rs +101 -61
- data/ext/makiri/rust/src/bridge/xpath/mod.rs +193 -115
- data/ext/makiri/rust/src/caught.rs +1 -3
- data/ext/makiri/rust/src/cbuf/verify.rs +66 -40
- data/ext/makiri/rust/src/cbuf.rs +237 -237
- data/ext/makiri/rust/src/css/build.rs +91 -42
- data/ext/makiri/rust/src/css/lower.rs +243 -185
- data/ext/makiri/rust/src/css/mod.rs +67 -22
- data/ext/makiri/rust/src/cutf8.rs +44 -25
- data/ext/makiri/rust/src/dom_rules/tests.rs +322 -0
- data/ext/makiri/rust/src/dom_rules.rs +238 -0
- data/ext/makiri/rust/src/{xpath/msg.rs → engine_error.rs} +64 -73
- data/ext/makiri/rust/src/falloc/cstr/verify.rs +40 -0
- data/ext/makiri/rust/src/falloc/cstr.rs +15 -24
- data/ext/makiri/rust/src/falloc/inject.rs +27 -16
- data/ext/makiri/rust/src/falloc/mod.rs +191 -67
- data/ext/makiri/rust/src/falloc/verify.rs +30 -16
- data/ext/makiri/rust/src/glue/hash.rs +50 -0
- data/ext/makiri/rust/src/glue/html_doc.rs +96 -98
- data/ext/makiri/rust/src/glue/html_node/css.rs +93 -36
- data/ext/makiri/rust/src/glue/html_node/mod.rs +88 -152
- data/ext/makiri/rust/src/glue/html_node/mutate.rs +327 -175
- data/ext/makiri/rust/src/glue/html_node/read.rs +202 -218
- data/ext/makiri/rust/src/glue/html_node/serialize.rs +24 -33
- data/ext/makiri/rust/src/glue/kwargs.rs +104 -0
- data/ext/makiri/rust/src/glue/mod.rs +6 -0
- data/ext/makiri/rust/src/glue/node.rs +18 -15
- data/ext/makiri/rust/src/glue/node_set.rs +85 -89
- data/ext/makiri/rust/src/glue/query.rs +71 -51
- data/ext/makiri/rust/src/glue/stylesheet.rs +41 -38
- data/ext/makiri/rust/src/glue/xml_doc.rs +41 -47
- data/ext/makiri/rust/src/glue/xml_node/css.rs +18 -23
- data/ext/makiri/rust/src/glue/xml_node/mod.rs +84 -130
- data/ext/makiri/rust/src/glue/xml_node/mutate.rs +218 -167
- data/ext/makiri/rust/src/glue/xml_node/ns.rs +43 -26
- data/ext/makiri/rust/src/glue/xml_node/read.rs +176 -130
- data/ext/makiri/rust/src/glue/xml_node/serialize.rs +61 -44
- data/ext/makiri/rust/src/glue/xpath_context.rs +41 -39
- data/ext/makiri/rust/src/init.rs +232 -202
- data/ext/makiri/rust/src/lexbor/abi.rs +34 -18
- data/ext/makiri/rust/src/lexbor/adapter/arena_bytes.rs +183 -22
- data/ext/makiri/rust/src/lexbor/adapter/cross_import.rs +238 -200
- data/ext/makiri/rust/src/lexbor/adapter/dom_index.rs +42 -82
- data/ext/makiri/rust/src/lexbor/adapter/html/attrs.rs +452 -0
- data/ext/makiri/rust/src/lexbor/adapter/html/build.rs +196 -117
- data/ext/makiri/rust/src/lexbor/adapter/html/mod.rs +667 -224
- data/ext/makiri/rust/src/lexbor/adapter/html/mutate.rs +145 -119
- data/ext/makiri/rust/src/lexbor/adapter/mod.rs +45 -4
- data/ext/makiri/rust/src/lexbor/adapter/post_parse.rs +239 -129
- data/ext/makiri/rust/src/lexbor/adapter/source_loc.rs +119 -180
- data/ext/makiri/rust/src/lexbor/adapter/text_index.rs +125 -69
- data/ext/makiri/rust/src/lexbor/adapter/tree_guard.rs +387 -0
- data/ext/makiri/rust/src/lexbor/chunks.rs +98 -0
- data/ext/makiri/rust/src/lexbor/contains_guard.rs +196 -243
- data/ext/makiri/rust/src/lexbor/css_engine.rs +44 -11
- data/ext/makiri/rust/src/lexbor/css_match/compile.rs +378 -0
- data/ext/makiri/rust/src/lexbor/css_match/mod.rs +413 -0
- data/ext/makiri/rust/src/lexbor/css_match/positions.rs +244 -0
- data/ext/makiri/rust/src/lexbor/css_match/query.rs +871 -0
- data/ext/makiri/rust/src/lexbor/css_match/scratch.rs +153 -0
- data/ext/makiri/rust/src/lexbor/css_match/simple.rs +459 -0
- data/ext/makiri/rust/src/lexbor/css_match/state.rs +242 -0
- data/ext/makiri/rust/src/lexbor/css_match/tree.rs +100 -0
- data/ext/makiri/rust/src/lexbor/css_parser.rs +119 -31
- data/ext/makiri/rust/src/lexbor/css_tokens.rs +204 -0
- data/ext/makiri/rust/src/lexbor/fragment.rs +226 -128
- data/ext/makiri/rust/src/lexbor/memory.rs +78 -0
- data/ext/makiri/rust/src/lexbor/mod.rs +21 -2
- data/ext/makiri/rust/src/lexbor/selector_cache.rs +356 -0
- data/ext/makiri/rust/src/lexbor/selectors.rs +165 -317
- data/ext/makiri/rust/src/lexbor/serialize.rs +108 -67
- data/ext/makiri/rust/src/lexbor/stylesheet.rs +62 -155
- data/ext/makiri/rust/src/lexbor/tests.rs +2263 -1
- data/ext/makiri/rust/src/lexbor/xpath.rs +77 -81
- data/ext/makiri/rust/src/lib.rs +37 -12
- data/ext/makiri/rust/src/node_type.rs +63 -0
- data/ext/makiri/rust/src/ptr_table.rs +81 -21
- data/ext/makiri/rust/src/rust_tests.rs +46 -42
- data/ext/makiri/rust/src/stack.rs +48 -0
- data/ext/makiri/rust/src/text.rs +43 -160
- data/ext/makiri/rust/src/token.rs +9 -11
- data/ext/makiri/rust/src/{lexbor/adapter/utf8_input.rs → utf8_input.rs} +7 -21
- data/ext/makiri/rust/src/xml/arena.rs +289 -190
- data/ext/makiri/rust/src/xml/attr_key.rs +91 -0
- data/ext/makiri/rust/src/xml/chars/mod.rs +42 -19
- data/ext/makiri/rust/src/xml/dom_name.rs +94 -3
- data/ext/makiri/rust/src/xml/index.rs +2 -2
- data/ext/makiri/rust/src/xml/mod.rs +1 -0
- data/ext/makiri/rust/src/xml/model.rs +302 -169
- data/ext/makiri/rust/src/xml/mutate/attr.rs +182 -94
- data/ext/makiri/rust/src/xml/mutate/copy.rs +84 -46
- data/ext/makiri/rust/src/xml/mutate/edit.rs +21 -68
- data/ext/makiri/rust/src/xml/mutate/factory.rs +57 -60
- data/ext/makiri/rust/src/xml/mutate/insert.rs +114 -254
- data/ext/makiri/rust/src/xml/mutate/mod.rs +18 -28
- data/ext/makiri/rust/src/xml/mutate/ns.rs +242 -76
- data/ext/makiri/rust/src/xml/qname.rs +48 -0
- data/ext/makiri/rust/src/xml/selftest.rs +182 -129
- data/ext/makiri/rust/src/xml/serialize/bindings.rs +253 -0
- data/ext/makiri/rust/src/xml/serialize/c14n.rs +210 -115
- data/ext/makiri/rust/src/xml/serialize/mod.rs +96 -8
- data/ext/makiri/rust/src/xml/serialize/out.rs +17 -5
- data/ext/makiri/rust/src/xml/serialize/xml.rs +168 -207
- data/ext/makiri/rust/src/xml/tree/cursor.rs +20 -44
- data/ext/makiri/rust/src/xml/tree/dtd.rs +7 -7
- data/ext/makiri/rust/src/xml/tree/mod.rs +110 -172
- data/ext/makiri/rust/src/xml/tree/scope.rs +11 -17
- data/ext/makiri/rust/src/xml/xpath.rs +52 -36
- data/ext/makiri/rust/src/xpath/abi.rs +6 -10
- data/ext/makiri/rust/src/xpath/ast.rs +61 -26
- data/ext/makiri/rust/src/xpath/ast_ops.rs +2 -3
- data/ext/makiri/rust/src/xpath/attr_pred.rs +14 -18
- data/ext/makiri/rust/src/xpath/axis.rs +24 -24
- data/ext/makiri/rust/src/xpath/ctx.rs +229 -190
- data/ext/makiri/rust/src/xpath/dom.rs +23 -33
- data/ext/makiri/rust/src/xpath/eval.rs +166 -127
- data/ext/makiri/rust/src/xpath/funcs/ext.rs +195 -47
- data/ext/makiri/rust/src/xpath/funcs/mod.rs +339 -233
- data/ext/makiri/rust/src/xpath/lex.rs +36 -85
- data/ext/makiri/rust/src/xpath/limits.rs +63 -9
- data/ext/makiri/rust/src/xpath/mod.rs +7 -6
- data/ext/makiri/rust/src/xpath/nodetest.rs +47 -28
- data/ext/makiri/rust/src/xpath/number.rs +76 -64
- data/ext/makiri/rust/src/xpath/order.rs +39 -38
- data/ext/makiri/rust/src/xpath/parse.rs +108 -89
- data/ext/makiri/rust/src/xpath/step_index.rs +44 -42
- data/ext/makiri/rust/src/xpath/str_cache.rs +76 -29
- data/ext/makiri/rust/src/xpath/tests.rs +248 -21
- data/ext/makiri/rust/src/xpath/value.rs +295 -151
- data/ext/makiri/rust/src/xpath/verify.rs +53 -0
- data/ext/makiri/rust/tests/alloc_inject_map_insert.rs +47 -0
- data/lib/makiri/attr.rb +15 -0
- data/lib/makiri/cdata_section.rb +8 -0
- data/lib/makiri/clone_via_dup.rb +19 -0
- data/lib/makiri/comment.rb +7 -0
- data/lib/makiri/css.rb +5 -4
- data/lib/makiri/document_fragment.rb +5 -2
- data/lib/makiri/document_type.rb +2 -3
- data/lib/makiri/element.rb +9 -0
- data/lib/makiri/error.rb +26 -0
- data/lib/makiri/html/document.rb +19 -14
- data/lib/makiri/html/node_methods.rb +9 -8
- data/lib/makiri/html.rb +3 -3
- data/lib/makiri/node.rb +13 -72
- data/lib/makiri/node_path.rb +95 -0
- data/lib/makiri/node_set.rb +50 -25
- data/lib/makiri/processing_instruction.rb +10 -8
- data/lib/makiri/reader_aliases.rb +28 -0
- data/lib/makiri/text.rb +7 -0
- data/lib/makiri/version.rb +1 -1
- data/lib/makiri/xml/builder.rb +17 -8
- data/lib/makiri/xml/document.rb +23 -3
- data/lib/makiri/xml/node_methods.rb +26 -32
- data/lib/makiri/xml.rb +4 -3
- data/lib/makiri/xpath.rb +8 -8
- data/lib/makiri/xpath_context.rb +1 -1
- data/lib/makiri/xpath_syntax.rb +43 -0
- data/lib/makiri.rb +13 -7
- data/script/api_manifest.rb +25 -3
- data/script/check_alloc_failures.rb +350 -16
- data/script/check_unsafe_boundaries.rb +370 -66
- data/script/leaks_harness.rb +14 -1
- data/suppressions/ruby.supp +10 -8
- metadata +35 -5
- data/ext/makiri/rust/src/falloc/calloc_verify.rs +0 -139
- data/ext/makiri/rust/src/falloc/raw.rs +0 -62
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5c68dc8b6bb133010a1ec3aada51eea868a0b645d1e02832b36b929f7ef5f2e
|
|
4
|
+
data.tar.gz: 66c4ff9d957074f37c4d1e9d1c1570b328ca86f1acdabc49c8d8d1435e24330d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d66a4deb197ac04e51b9234425d740dec346cf5e4de4e2a29aedd6ad50e299ee4f6f3229a698cab3bf54e1756472fe5771fb7266df1180d87d2769ba39d757d9
|
|
7
|
+
data.tar.gz: ceb2392bddf7d1a4683f72239e13fc4785522cadc5ebf33a18b2ca67b3b1f1f97a49ecb4e72e12218463aafe7dc3aecc75efe8c540e36c56ff74b5d30f7bff9f
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
#
|
|
102
|
-
# clean exception or a baseline-identical result,
|
|
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
|
|
141
|
-
#
|
|
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:
|
|
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,5 +1,169 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0.rc2] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
* `Makiri::HTML::Document#create_element_ns(namespace_uri, qualified_name)`
|
|
8
|
+
(DOM `createElementNS`). An SVG or MathML element made this way keeps its
|
|
9
|
+
name's case and behaves like a parsed one. An upper-case name in the HTML
|
|
10
|
+
namespace that names a known element (`BR`, `DIV`) raises `Makiri::Error`.
|
|
11
|
+
* `Makiri::XML::Element#set_loose_dom_attribute(qualified_name, value)` (DOM
|
|
12
|
+
`setAttribute`): the attribute is in no namespace and keeps the name as
|
|
13
|
+
given (`xmlns`, `xlink:href`, `v-on:click`). `to_xml` raises while the
|
|
14
|
+
document holds one XML cannot write.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
* A NUL in an element, attribute, doctype or PI target name raises
|
|
19
|
+
`ArgumentError`, like any other invalid name. A namespace that does not fit
|
|
20
|
+
the name still raises `Makiri::Error`.
|
|
21
|
+
* HTML type selectors are case-sensitive on SVG and MathML elements, as in
|
|
22
|
+
browsers: `fegaussianblur` no longer matches `feGaussianBlur`.
|
|
23
|
+
* `set_attribute_ns` accepts the XML namespace with any prefix or none again
|
|
24
|
+
(rc1 raised); `to_xml` writes such an attribute as `xml:name`.
|
|
25
|
+
* XML `set_attribute_ns` accepts a namespace declaration XML forbids, such as
|
|
26
|
+
`xmlns:foo=""`. It binds nothing, and `to_xml` raises while it is present.
|
|
27
|
+
* XML `create_document_type` accepts any public and system id, and any name
|
|
28
|
+
without whitespace, NUL or `>`. `to_xml` raises if the doctype cannot be
|
|
29
|
+
written as XML.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
* `import_node` from XML to HTML keeps an upper-case XHTML element name
|
|
34
|
+
(`Foo`) instead of lower-casing it, and raises for one that names a known
|
|
35
|
+
element (`BR`).
|
|
36
|
+
|
|
37
|
+
## [0.11.0.rc1] - 2026-09-29
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
* HTML `#css` / `#at_css` / `#matches?` now match with Makiri's own Rust
|
|
42
|
+
implementation instead of Lexbor's `lxb_selectors` (selectors are still
|
|
43
|
+
parsed by Lexbor). Where the answers differ:
|
|
44
|
+
* `:nth-child(An+B of S)` / `:nth-last-child(... of S)` count by the CSS
|
|
45
|
+
definition; Lexbor miscounted when `S` was a comma list, held a
|
|
46
|
+
combinator, or carried pseudo-classes such as `:enabled`.
|
|
47
|
+
* `:disabled`, `:enabled` and `:checked` follow the HTML Standard: a
|
|
48
|
+
control inside a `<fieldset disabled>` (outside its first `<legend>`) is
|
|
49
|
+
disabled, `<option>` / `<optgroup>` count, and `:enabled` matches only
|
|
50
|
+
form elements.
|
|
51
|
+
* Attribute selector names are matched by qualified name, and
|
|
52
|
+
case-sensitively on SVG/MathML elements: `[href]` no longer finds
|
|
53
|
+
`xlink:href`, nor `[viewbox]` an SVG `viewBox`.
|
|
54
|
+
* `#id` / `.class` match only the no-namespace `id` / `class` attribute.
|
|
55
|
+
* A selector chain is capped at 64 compounds (as `Makiri::XML` already
|
|
56
|
+
was), and a query at 50M steps of work (XPath's limit); past either,
|
|
57
|
+
`Makiri::Error`.
|
|
58
|
+
* The column combinator `||` raises `Makiri::Error` instead of matching
|
|
59
|
+
nothing.
|
|
60
|
+
|
|
61
|
+
### Removed
|
|
62
|
+
|
|
63
|
+
* `:lexbor-contains()` in HTML `#css` / `#at_css` / `#matches?`: it still
|
|
64
|
+
parses, but raises `Makiri::Error`. `Makiri::XML`'s `#css` keeps it. See
|
|
65
|
+
NOKOGIRI_DIFFERENCES.md.
|
|
66
|
+
* `Node#name=` / `#node_name=`. The DOM cannot rename an element, and renaming
|
|
67
|
+
a Lexbor element in place could crash (`div` to `template`). Create a new
|
|
68
|
+
element and `replace` the old one.
|
|
69
|
+
|
|
70
|
+
### Security
|
|
71
|
+
|
|
72
|
+
* Hardening: every heap block the vendored Lexbor allocates carries 16 bytes
|
|
73
|
+
of slack past its end, so a small overrun cannot reach neighbouring memory
|
|
74
|
+
(not in sanitizer builds).
|
|
75
|
+
* Adding an `id` or `class` attribute beside an existing one
|
|
76
|
+
(`set_attribute_ns(nil, "ID", v)`, a namespaced `id`) no longer frees the
|
|
77
|
+
existing one under a held `Attr`. HTML attribute reads and writes follow the
|
|
78
|
+
DOM: `#[]`, `#key?`, `#[]=` and `#delete` match the qualified name
|
|
79
|
+
(`svg_a["href"]` no longer returns `xlink:href`), copies and imports keep
|
|
80
|
+
every attribute, and a no-namespace name keeps its case.
|
|
81
|
+
* `content=` on an HTML element and `delete(name)` detach the nodes they
|
|
82
|
+
remove instead of freeing them under a live wrapper.
|
|
83
|
+
* `el[name] = value` on an existing attribute no longer leaves a destroyed
|
|
84
|
+
attribute linked when storing the value runs out of memory.
|
|
85
|
+
* A checked argument String is locked while its bytes are in use, so a later
|
|
86
|
+
argument's `#to_s` cannot rewrite it (it raises instead); a receiver frozen
|
|
87
|
+
by an argument's `#to_s` is not edited; and a mutator's argument can no
|
|
88
|
+
longer rebuild the document's indexes in the middle of the edit.
|
|
89
|
+
* HTML element and attribute names follow the DOM's rules:
|
|
90
|
+
`create_element("img src=x onerror=alert(1)")` raises `ArgumentError`
|
|
91
|
+
instead of writing that markup. See NOKOGIRI_DIFFERENCES.md.
|
|
92
|
+
* HTML parsing bounds the tree depth (`max_tree_depth:`, default 400) and the
|
|
93
|
+
`<option>`s one `<select>` receives (10,000), raising `Makiri::Error` past
|
|
94
|
+
either; both made the parse quadratic.
|
|
95
|
+
* `Makiri::Lexbor::CSS.parse_stylesheet` no longer crashes on a
|
|
96
|
+
`:lexbor-contains()` after a string ended by CR, FF or a newline.
|
|
97
|
+
* A CSS query that runs out of memory raises instead of answering with what it
|
|
98
|
+
had collected; a selector-parse OOM is reported as OOM, not
|
|
99
|
+
`CSS::SyntaxError`.
|
|
100
|
+
* Inputs whose cost grew faster than their size are linear or budgeted: many
|
|
101
|
+
namespace bindings, the `preceding` axis and reverse-axis steps, XML
|
|
102
|
+
duplicate-attribute checks, `contains` / `substring-*` / `translate` on long
|
|
103
|
+
strings, XML CSS `:nth-*`, CDATA full of `]]>`, and stylesheets with
|
|
104
|
+
rejected `:lexbor-contains()`.
|
|
105
|
+
* A panic in any Ruby method Makiri defines raises `Makiri::InternalError`
|
|
106
|
+
(rescuable), not `fatal`.
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
* A document that grows by editing (appended nodes, `inner_html=`) reports
|
|
111
|
+
its new size to the GC, so memory pressure from it triggers collections; it
|
|
112
|
+
was reported once, at parse time.
|
|
113
|
+
* A node wrapped while memory runs out raises `Makiri::Error` instead of
|
|
114
|
+
coming back as a second Ruby object for the same node, without the first
|
|
115
|
+
one's `freeze`, instance variables or singleton methods.
|
|
116
|
+
* Insertion follows the DOM's pre-insertion rules in HTML and XML alike: no
|
|
117
|
+
cycles through a template's contents (which hung `dup`), no children on
|
|
118
|
+
Text/Comment/PI/DocumentType/Attr, no Text directly under an XML Document,
|
|
119
|
+
and an XML `DocumentFragment` takes children.
|
|
120
|
+
* `inner_html` / `inner_html=` on a `<template>` target its contents, agreeing
|
|
121
|
+
with `#to_html` and `content_fragment`.
|
|
122
|
+
* HTML `#keys` / `#values` raise on out-of-memory instead of returning a
|
|
123
|
+
truncated Array.
|
|
124
|
+
* `dup`, `clone_node` and `import_node` keep an element's name as written
|
|
125
|
+
(`linearGradient`, `q:Bar`), and a copied XML doctype keeps its PUBLIC id.
|
|
126
|
+
* Namespaces across HTML <-> XML `import_node`: attributes keep their own
|
|
127
|
+
namespace, a prefixed element keeps its prefix and case, names XML cannot
|
|
128
|
+
write are refused where they would be written, and copied `xmlns`
|
|
129
|
+
attributes no longer move elements between namespaces.
|
|
130
|
+
* XML namespaces: `set_attribute_ns` on a detached element survives insertion;
|
|
131
|
+
`[]=` on an existing attribute changes only its value; the mutators refuse
|
|
132
|
+
what the parser refuses (forbidden declarations, duplicate expanded names,
|
|
133
|
+
bad doctype names and PUBLIC ids); `canonicalize` raises instead of writing a
|
|
134
|
+
wrong namespace; and a refused declaration says which rule it broke.
|
|
135
|
+
* `to_xml` output re-parses: CDATA holding `]]>`, a SYSTEM id holding `"`, and
|
|
136
|
+
attributes with a namespace but no prefix.
|
|
137
|
+
* XPath: `string()` of a number follows libxml2 (as Nokogiri does);
|
|
138
|
+
`substring()` rounds per spec; node-set vs boolean comparisons follow §3.4;
|
|
139
|
+
the `xml` prefix is always bound; a comparison no longer raises
|
|
140
|
+
`LimitExceeded` because its values added up past 64 MB.
|
|
141
|
+
* `Node#path` round-trips through `#at_xpath` for CDATA, PIs, and namespaced
|
|
142
|
+
nodes (SVG/MathML, XML namespaces, `xlink:href`, Vue/Word-style names); an
|
|
143
|
+
unreachable node answers `"?"` as in Nokogiri.
|
|
144
|
+
* CSS over XML agrees with the HTML matcher on `.x\ y`, `:root`, empty and
|
|
145
|
+
whitespace attribute values, `$=` with non-ASCII, `:empty` beside a comment,
|
|
146
|
+
and `[|a]`; the `s` modifier is accepted.
|
|
147
|
+
* An HTML document refuses a second root element or a text child; an
|
|
148
|
+
attribute's parent is always its owner element.
|
|
149
|
+
* `Node#line` of a node copied from another document is nil.
|
|
150
|
+
* `Makiri::XML` nodes compare with `<=>` in document order (an attribute with
|
|
151
|
+
itself too), and `XML::Document#dup` / `#clone` return a copy.
|
|
152
|
+
* `clone_node` on a Document raises instead of returning the document.
|
|
153
|
+
* `NodeSet#css` / `#xpath` / `#at_css` / `#at_xpath` pass their extra
|
|
154
|
+
arguments through; `#xpath` of a non-node-set expression raises
|
|
155
|
+
`ArgumentError`, as in Nokogiri.
|
|
156
|
+
* `XML::Builder` no longer answers `respond_to?` for `to_ary` and similar.
|
|
157
|
+
* Namespace Hashes and `register_namespace` arguments are read as a Hash and
|
|
158
|
+
with `String()`, with one length cap and one error message.
|
|
159
|
+
* A rejected stylesheet rule's `selector_text` is sliced by Lexbor's offsets.
|
|
160
|
+
|
|
161
|
+
### Performance
|
|
162
|
+
|
|
163
|
+
* `NodeSet#at_css` / `#at_xpath` stop at the first node with a match.
|
|
164
|
+
* `XML::Node#canonicalize` looks namespaces up by prefix in the scope it keeps:
|
|
165
|
+
a 1000-deep document of declarations went from 0.7 s to 0.02 s.
|
|
166
|
+
|
|
3
167
|
## [0.10.0] - 2026-09-22
|
|
4
168
|
|
|
5
169
|
### Fixed
|
data/NOKOGIRI_DIFFERENCES.md
CHANGED
|
@@ -26,11 +26,34 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
26
26
|
exactly (`//*[@refX]`, not `@refx`). Only ASCII folds: `Ø` still differs from `ø`.
|
|
27
27
|
This holds in `namespace_matching: :lax` too.
|
|
28
28
|
* `Nokogiri::HTML5` is case-sensitive there.
|
|
29
|
+
* `Node#path` names a node by expanded name where a bare name would not reach it
|
|
30
|
+
* An SVG, MathML or namespaced-XML node, and an HTML name that is no plain
|
|
31
|
+
XPath name (`<o:p>`, `xml:lang`, Vue's `@click`), give
|
|
32
|
+
`*[local-name()='path' and namespace-uri()='http://www.w3.org/2000/svg']`,
|
|
33
|
+
which `#at_xpath` evaluates with no prefix registered. Nokogiri writes
|
|
34
|
+
`svg:svg` or `/*/*[2]` for the first kinds; the paths are equivalent, the
|
|
35
|
+
strings are not.
|
|
36
|
+
* A node not attached to its document answers `"?"`, as a doctype does.
|
|
37
|
+
Nokogiri answers `"/div/p"` for a detached `<div><p>`, which is the path of
|
|
38
|
+
the document's own `/div/p` when it has one.
|
|
29
39
|
* A foreign element's namespace declarations are not attributes
|
|
30
40
|
* `<svg xmlns="...">` has no `@xmlns` for `//*[@xmlns]` or `@*`, as in browsers
|
|
31
41
|
and in XPath's data model. An `xmlns` on an HTML element is an ordinary
|
|
32
42
|
attribute and stays visible.
|
|
33
43
|
|
|
44
|
+
* A `StandardError` raised by a custom-function handler becomes `Makiri::Error`
|
|
45
|
+
("handler raised: <message>"), with the handler's exception as its `#cause`
|
|
46
|
+
* Nokogiri re-raises the handler's exception itself. Anything that is not a
|
|
47
|
+
`StandardError` (`Interrupt`, `SystemExit`, Timeout's exception) and a
|
|
48
|
+
`throw` reach the caller unchanged in both.
|
|
49
|
+
|
|
50
|
+
* A number literal is read as the nearest double; libxml2's own reader is not
|
|
51
|
+
correctly rounded for a literal with more digits than a double holds, so
|
|
52
|
+
`string(0.72609133372266155)` is `0.726091333722662` in Makiri and
|
|
53
|
+
`0.726091333722661` in Nokogiri (the digits differ in the last place). The
|
|
54
|
+
number is then written by libxml2's rule in both (`string(1234567890.5)` is
|
|
55
|
+
`1.2345678905e+09`); only the value read differs.
|
|
56
|
+
|
|
34
57
|
## XML
|
|
35
58
|
|
|
36
59
|
* `Makiri::XML` is XML 1.0 (Fifth Edition) only and non-validating.
|
|
@@ -84,6 +107,19 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
84
107
|
invents one (`ns1`, `ns2`, ...) rather than shadow the other, as browsers do.
|
|
85
108
|
* An element in no namespace stays that way under a default namespace,
|
|
86
109
|
serialized as `xmlns=""`.
|
|
110
|
+
* `create_document_type` takes what the DOM's `createDocumentType` takes -
|
|
111
|
+
any id, and a name without whitespace, NUL or `>` - and `to_xml` refuses a
|
|
112
|
+
doctype XML cannot write (`create_document_type("q", "abcde", %(x"'y))`).
|
|
113
|
+
Nokogiri writes that system id as `"x"'y"`, which parses but reads
|
|
114
|
+
back as the id `x"'y` - a literal expands no references.
|
|
115
|
+
* `set_attribute_ns(XMLNS_NS, "xmlns:foo", "")` - a declaration Namespaces in
|
|
116
|
+
XML forbids, which the DOM's `setAttributeNS` accepts - is kept as an
|
|
117
|
+
attribute that binds nothing, and `to_xml` refuses the tree while it is
|
|
118
|
+
there. `root["xmlns:foo"] = ""` still raises.
|
|
119
|
+
* An attribute in the XML namespace is written as `xml:local`, whatever
|
|
120
|
+
prefix it was given (`set_attribute_ns(XML_NS, "a:bb")`, as the DOM
|
|
121
|
+
allows): Namespaces in XML binds that namespace to `xml` alone. It re-reads
|
|
122
|
+
to the same namespace and local name. `canonicalize` writes it so too.
|
|
87
123
|
* Nodes from the factories (`create_element` and friends) still take their
|
|
88
124
|
namespace from the context they are first inserted into, so a subtree can be
|
|
89
125
|
built detached and attached afterwards. Only later moves carry.
|
|
@@ -100,6 +136,14 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
100
136
|
the property-based differential), including namespaces, prolog/epilog comments
|
|
101
137
|
and PIs, and adjacent-CDATA coalescing.
|
|
102
138
|
|
|
139
|
+
* `to_xml` keeps an element's namespace when an `xmlns` attribute on it says
|
|
140
|
+
otherwise
|
|
141
|
+
* `root["xmlns"] = "urn:x"` on an element in no namespace is not written: the
|
|
142
|
+
element stays in no namespace when the output is re-read, as the DOM Parsing
|
|
143
|
+
and Serialization spec asks. Nokogiri writes `<r xmlns="urn:x">`, which moves
|
|
144
|
+
the element into `urn:x` on re-parse. Create the element in the namespace
|
|
145
|
+
instead (`create_element("r", "xmlns" => "urn:x")`, or parse it so).
|
|
146
|
+
|
|
103
147
|
## HTML parsing
|
|
104
148
|
|
|
105
149
|
* `<?php ... ?>` in HTML input is a **ProcessingInstruction** node; `#to_html`
|
|
@@ -109,6 +153,79 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
109
153
|
still produces the older bogus comment (`<!--?php ... ?-->`), and
|
|
110
154
|
`Nokogiri::HTML` (libxml2) its own comment.
|
|
111
155
|
|
|
156
|
+
* A tree deeper than `max_tree_depth` raises **`Makiri::Error`**, where
|
|
157
|
+
`Nokogiri::HTML5` raises `ArgumentError`; the default (400) and boundary are
|
|
158
|
+
Nokogiri's. `inner_html=`, `outer_html=` and `Node#parse` take no keyword.
|
|
159
|
+
`Nokogiri::HTML` (libxml2) instead stops at depth 256 and returns the
|
|
160
|
+
truncated document.
|
|
161
|
+
* More than 10,000 `<option>`s in one `<select>` raise `Makiri::Error`;
|
|
162
|
+
Nokogiri has no such limit.
|
|
163
|
+
|
|
164
|
+
## HTML mutation
|
|
165
|
+
|
|
166
|
+
* There is no `Node#name=` / `#node_name=` (on HTML or XML nodes). Nokogiri
|
|
167
|
+
renames a node in place and keeps its identity; the DOM has no rename, and
|
|
168
|
+
Lexbor keeps elements such as `<template>`, `<option>` and `<style>` in
|
|
169
|
+
structs of their own, so a node cannot change its tag safely. Create an
|
|
170
|
+
element of the new name, move the attributes and children, and `replace`
|
|
171
|
+
the old one (see CHANGELOG.md).
|
|
172
|
+
* Element and attribute names follow the WHATWG DOM's rules
|
|
173
|
+
* `create_element`, `[]=` and `set_attribute_ns` raise `ArgumentError` for a
|
|
174
|
+
name the DOM refuses - one holding whitespace, `/`, `>` (or `=` for an
|
|
175
|
+
attribute) - where `Nokogiri::HTML5` accepts it and writes it into the
|
|
176
|
+
markup as it stands: `create_element("img src=x onerror=alert(1)")`
|
|
177
|
+
serializes as that tag.
|
|
178
|
+
The names HTML actually uses (`data-*`, `aria-*`, `@click`, `:href`,
|
|
179
|
+
`v-on:x`, custom elements) are accepted.
|
|
180
|
+
* `set_attribute_ns(nil, "x:y")` raises, as the DOM's `setAttributeNS` does:
|
|
181
|
+
a prefix needs a namespace. `[]=` (HTML) and `set_loose_dom_attribute`
|
|
182
|
+
(XML) are the DOM's `setAttribute`, which makes such an attribute.
|
|
183
|
+
* A refused name raises `ArgumentError` - a NUL in it too - and a namespace
|
|
184
|
+
that does not fit the name (`set_attribute_ns`, `create_element_ns`)
|
|
185
|
+
raises `Makiri::Error`. Invalid UTF-8 raises `Makiri::Error` for every
|
|
186
|
+
argument, names included.
|
|
187
|
+
* `create_element_ns` refuses an HTML-namespace name in upper case that
|
|
188
|
+
lower-cases to an element Lexbor knows (`BR`, `DIV`), where the DOM makes
|
|
189
|
+
an unknown element: Lexbor would make that element (`BR` void, its
|
|
190
|
+
children never written). Other names keep their case (`MY-EL`). Nokogiri
|
|
191
|
+
has no `create_element_ns`.
|
|
192
|
+
* An HTML `<template>` follows the WHATWG content model, which Nokogiri does
|
|
193
|
+
not: its parsed contents live in the separate fragment `Element#content_fragment`
|
|
194
|
+
returns, `template.children` is empty, and `inner_html` / `inner_html=`
|
|
195
|
+
special-case the contents (the WHATWG DOM special-cases `innerHTML` alone;
|
|
196
|
+
`append_child`, `content=`, and `children` act on the element's own empty
|
|
197
|
+
children, as the specification says). Nokogiri treats `<template>` as an
|
|
198
|
+
ordinary element, with the parsed nodes as its children, so
|
|
199
|
+
`template.inner_html` and `template.children` answer the other way round and
|
|
200
|
+
there is no `content_fragment`.
|
|
201
|
+
* An HTML document has one root element and no text child, as the DOM requires;
|
|
202
|
+
`doc << element` beside an existing root raises.
|
|
203
|
+
* An insertion the DOM refuses - a child under a text, comment, PI, doctype
|
|
204
|
+
or attribute node, text under an XML Document, a second root - raises
|
|
205
|
+
`Makiri::Error` in both representations. Nokogiri refuses the same ones with
|
|
206
|
+
`ArgumentError` (or `RuntimeError` for a second XML root).
|
|
207
|
+
* Moving HTML into an XML document (`xml_doc.import_node(html_node)`, or
|
|
208
|
+
inserting one) keeps every name's namespace, and refuses what XML cannot
|
|
209
|
+
write that way. An attribute in no namespace whose name has a prefix other
|
|
210
|
+
than `xml` - `v-on:click`, `fb:like`, an `xlink:href` on an HTML (not SVG)
|
|
211
|
+
element - raises `Makiri::Error`: as XML it would be a prefix bound to
|
|
212
|
+
nothing. Nokogiri copies it and writes `v-on:click="..."` into output that is
|
|
213
|
+
not namespace-well-formed. An element named with a colon (`<fb:like>`)
|
|
214
|
+
crosses as a DOM-loose name, which `to_xml` refuses.
|
|
215
|
+
* A known gap, in Lexbor's tag table: an HTML document that already holds a
|
|
216
|
+
parsed element named with a colon (`<x:y>`, one local name) and then
|
|
217
|
+
receives, by `import_node` from another document, a prefixed element
|
|
218
|
+
written the same way (`x:y` from XML: prefix `x`, local name `y`) re-points
|
|
219
|
+
the table's entry for that spelling - the parsed element then no longer
|
|
220
|
+
matches CSS `x\:y`. Copies within one document do not touch the table.
|
|
221
|
+
* An XML element with a prefix, imported into HTML, keeps it (`h:div` in
|
|
222
|
+
XHTML has the local name `div`). Two readers then disagree, as they do in
|
|
223
|
+
browsers: CSS's `div` matches it (Lexbor matches the local name), XPath's
|
|
224
|
+
`//div` does not (an HTML element's name test reads its qualified name). And
|
|
225
|
+
Lexbor's HTML serializer writes the prefix (`<h:div>`), where the HTML
|
|
226
|
+
standard writes the local name, so the HTML does not re-parse to the same
|
|
227
|
+
element.
|
|
228
|
+
|
|
112
229
|
## CSS
|
|
113
230
|
|
|
114
231
|
* Most jQuery/Nokogiri CSS extensions are not supported (`:gt`, `:lt`, `:eq`, `:first`, ...)
|
|
@@ -116,13 +233,18 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
116
233
|
text-containment extension. Use XPath (`xpath("//p[contains(., 'x')]")`) or
|
|
117
234
|
Enumerable (`css('li')[1]`) for the rest.
|
|
118
235
|
Standard Level-4 selectors (`:is` / `:where` / `:has`) are supported; some of which Nokogiri rejects.
|
|
119
|
-
* `:lexbor-contains("text")`
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
nodes
|
|
125
|
-
|
|
236
|
+
* `:lexbor-contains("text")` - Lexbor's spelling of the jQuery `:contains()`
|
|
237
|
+
substring filter, matching an element whose text contains the string;
|
|
238
|
+
append ` i` (`:lexbor-contains("text" i)`) for an ASCII case-insensitive
|
|
239
|
+
match. (Nokogiri's name `:contains` is not an alias.) **XML only**: it
|
|
240
|
+
lowers to XPath `child::text()[contains(., "text")]`, testing the
|
|
241
|
+
element's immediate child text nodes, not the deep string-value. HTML no
|
|
242
|
+
longer supports it - `lexbor::css_match`, the safe-Rust port that
|
|
243
|
+
replaced Lexbor's own matcher for HTML, deliberately does not reimplement
|
|
244
|
+
it, so a well-formed `:lexbor-contains()` now raises `Makiri::Error`
|
|
245
|
+
("could not be run") on HTML rather than ever matching (it still PARSES,
|
|
246
|
+
and an actually malformed one still raises `Makiri::CSS::SyntaxError`, as
|
|
247
|
+
before).
|
|
126
248
|
* Untyped `:*-of-type` (`:first-of-type`, `:nth-of-type(an+b)`, ... with no type
|
|
127
249
|
selector) is supported and correct on both HTML and XML - the "type" is the
|
|
128
250
|
element's own expanded name.
|
|
@@ -138,12 +260,35 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
138
260
|
prefix IS resolved against the bindings, when the namespace matters.
|
|
139
261
|
* `Makiri::XML` resolves CSS prefixes properly - it lowers the selector to the
|
|
140
262
|
XPath engine, which registers the bindings.
|
|
263
|
+
* The same holds for attribute selectors: HTML `[|href]` (no namespace) and
|
|
264
|
+
plain `[href]` also find an SVG `xlink:href`, which `Nokogiri::HTML5`
|
|
265
|
+
does not.
|
|
266
|
+
`Makiri::XML` reads `[|a]` as the no-namespace attribute.
|
|
267
|
+
* A selector under a node matches the way `Element#querySelectorAll` does in a
|
|
268
|
+
browser, not scoped to that node, on HTML: `at_css("#c").css("div p")` finds a
|
|
269
|
+
`p` inside `#c` when `#c` is itself a `div`, since the selector is matched
|
|
270
|
+
against the whole document and only the results are kept to descendants.
|
|
271
|
+
`Nokogiri::HTML5` and `Nokogiri::XML` scope the selector to the node (`#c`
|
|
272
|
+
cannot be the `div`), and so does `Makiri::XML`, which lowers the selector to
|
|
273
|
+
an XPath from the node. Lexbor has no `:scope`; for a scoped match on HTML,
|
|
274
|
+
use XPath from the node (`xpath(".//div//p")`).
|
|
275
|
+
* The attribute case modifiers (`[a="x" i]`, `[a="x" s]`): HTML supports both,
|
|
276
|
+
through Lexbor's matcher. `Makiri::XML` accepts `s` (case-sensitive, which XML
|
|
277
|
+
values are anyway) and refuses `i` with `Makiri::CSS::SyntaxError`. Nokogiri
|
|
278
|
+
refuses both on either representation.
|
|
141
279
|
* `#matches?` answers for a DETACHED node (`document.create_element("p")
|
|
142
280
|
.matches?("p")` is true, on both representations). Nokogiri raises
|
|
143
281
|
`NoMethodError` there - it implements `#matches?` as a search from
|
|
144
282
|
`ancestors.last`, which a detached node does not have.
|
|
145
|
-
*
|
|
146
|
-
|
|
283
|
+
* Type selectors in an HTML document follow the HTML Standard's
|
|
284
|
+
case-sensitivity rule, as browsers do: lower-cased for an HTML element (`LI`
|
|
285
|
+
matches `<li>`), as written for any other (`feGaussianBlur` matches the SVG
|
|
286
|
+
element, `fegaussianblur` does not). An HTML element named in upper case
|
|
287
|
+
(`create_element_ns(XHTML, "MY-EL")`, which keeps its name as the DOM does)
|
|
288
|
+
therefore matches no type selector.
|
|
289
|
+
* `Nokogiri::HTML5` is case-sensitive on HTML elements too, so `LI` does not
|
|
290
|
+
match `<li>` there. `Makiri::XML`'s `#css` is case-sensitive, as XML names
|
|
291
|
+
are.
|
|
147
292
|
|
|
148
293
|
## Serialization
|
|
149
294
|
|
|
@@ -170,7 +315,10 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
|
|
|
170
315
|
and attribute values (`[]=`, `set_attribute_ns`) - and stored/read back
|
|
171
316
|
verbatim, matching the WHATWG DOM / browsers (`document.createTextNode("\0")`).
|
|
172
317
|
It is still rejected in names, tag names, namespaces, PI target/data, CSS
|
|
173
|
-
selectors, and XPath expressions/variable names
|
|
318
|
+
selectors, and XPath expressions/variable names. A NUL in a name given to a
|
|
319
|
+
factory or setter (element, attribute, doctype and PI target names) raises
|
|
320
|
+
`ArgumentError`, as any other refused name does; anywhere else it raises
|
|
321
|
+
`Makiri::Error`.
|
|
174
322
|
* On re-parse, the HTML tokenizer replaces a U+0000 in text/attributes with
|
|
175
323
|
U+FFFD (WHATWG), so a serialized-then-reparsed round-trip is not byte-identical.
|
|
176
324
|
* `Makiri::XML` rejects NUL everywhere: XML 1.0 has no legal U+0000 character,
|
data/README.md
CHANGED
|
@@ -23,11 +23,13 @@ XPath 1.0 evaluation in its own native engine, with no libxml2 dependency.
|
|
|
23
23
|
* Native XML 1.0 parser
|
|
24
24
|
* A strict, non-validating, fail-closed parser with its own node arena (not
|
|
25
25
|
Lexbor's HTML DOM), queried through the same native XPath engine, with
|
|
26
|
-
in-place tree edits (attributes, content,
|
|
26
|
+
in-place tree edits (attributes, content, remove).
|
|
27
27
|
* Conformance is held by the W3C XML Conformance Test Suite, an XPath
|
|
28
28
|
differential, and property-based testing vs Nokogiri (see below).
|
|
29
29
|
* Bounded, fail-closed execution
|
|
30
30
|
* XPath evaluation is bounded by per-evaluation limits on work, memory, and recursion.
|
|
31
|
+
* HTML parsing bounds the tree depth (`max_tree_depth:`, default 400) and the
|
|
32
|
+
`<option>`s per `<select>` (10,000).
|
|
31
33
|
* Ownership and borrowing are kept explicit across layers, with owned/borrowed
|
|
32
34
|
string types and verified text at engine boundaries.
|
|
33
35
|
* Programmatic invalid input, limit violations, allocation failures, and unsupported constructs
|
|
@@ -65,6 +67,10 @@ link.parent.name # => "div"
|
|
|
65
67
|
# Source location (reconstructed from the tokenizer, no Lexbor patches)
|
|
66
68
|
doc.at_css("p").line # => 3
|
|
67
69
|
|
|
70
|
+
# Nesting deeper than 400 elements raises Makiri::Error (Nokogiri's default);
|
|
71
|
+
# max_tree_depth: raises the limit, and a negative value disables it
|
|
72
|
+
Makiri::HTML(deep_html, max_tree_depth: 2000)
|
|
73
|
+
|
|
68
74
|
# Serialization
|
|
69
75
|
doc.at_css("#main").to_html # => "<div id=\"main\" ...>...</div>"
|
|
70
76
|
doc.at_css("#main").inner_html # => "\n <p class=\"lead\">Hello</p>\n..."
|
|
@@ -132,11 +138,10 @@ e = doc.at_xpath("//entry")
|
|
|
132
138
|
e["id"] = "9" # add or replace an attribute (value escaped on output)
|
|
133
139
|
e["dc:k"] = "v" # a prefixed name resolves against the in-scope xmlns
|
|
134
140
|
e.content = "Bye" # replace an element's children with text
|
|
135
|
-
e.name = "post" # rename in place (identity + namespace re-resolved)
|
|
136
141
|
e.delete("id") # remove an attribute
|
|
137
142
|
doc.at_xpath("//draft").remove
|
|
138
143
|
|
|
139
|
-
doc.root.to_xml # => "<feed xmlns:dc=\"urn:dc\"><
|
|
144
|
+
doc.root.to_xml # => "<feed xmlns:dc=\"urn:dc\"><entry dc:k=\"v\">Bye</entry></feed>"
|
|
140
145
|
```
|
|
141
146
|
|
|
142
147
|
XML subtrees can be built using `Document#create_element` and other node factory methods,
|
data/Rakefile
CHANGED
|
@@ -649,8 +649,20 @@ task :oom do
|
|
|
649
649
|
# The hook is compiled in only under MAKIRI_ALLOC_INJECT=1 (zero overhead in
|
|
650
650
|
# a normal build), so this needs its own rebuild; see
|
|
651
651
|
# script/check_alloc_failures.rb for the protocol and the property gated.
|
|
652
|
-
|
|
653
|
-
|
|
652
|
+
sanitize = ENV["MAKIRI_SANITIZE"].to_s
|
|
653
|
+
build_env = { "MAKIRI_ALLOC_INJECT" => "1" }
|
|
654
|
+
build_env["MAKIRI_SANITIZE"] = sanitize unless sanitize.empty?
|
|
655
|
+
sh(build_env, "#{FileUtils::RUBY} -S rake clean compile")
|
|
656
|
+
|
|
657
|
+
run_env = {}
|
|
658
|
+
unless sanitize.empty?
|
|
659
|
+
assert_sanitized!("oom", sanitize)
|
|
660
|
+
run_env = {
|
|
661
|
+
"ASAN_OPTIONS" => ASAN_ENV_OPTIONS,
|
|
662
|
+
"MAKIRI_SANITIZE" => sanitize,
|
|
663
|
+
}.merge(asan_preload_env(sanitize))
|
|
664
|
+
end
|
|
665
|
+
sh(run_env, "#{FileUtils::RUBY} -Ilib script/check_alloc_failures.rb")
|
|
654
666
|
puts "(injection build left in place; run `rake clean compile` to restore a normal build)"
|
|
655
667
|
end
|
|
656
668
|
|
|
@@ -795,10 +807,11 @@ namespace :fuzz do
|
|
|
795
807
|
# (halt_on_error). The default (isolated) is the complete net:
|
|
796
808
|
# it also survives + attributes a genuine segfault and catches a
|
|
797
809
|
# hang via the per-query timeout, at much lower throughput.
|
|
810
|
+
# ISOLATED=0 same as FAST=1; ISOLATED=1 (like the default) keeps isolation.
|
|
798
811
|
# SKIP_BUILD=1 reuse the current build instead of rebuilding (refuses to run
|
|
799
812
|
# if it is not a sanitizer build, so you never fuzz a plain ext).
|
|
800
813
|
# FUZZ_TIME=N seconds per surface (default 90).
|
|
801
|
-
# FUZZ_ARGS=... run a single custom invocation instead of the
|
|
814
|
+
# FUZZ_ARGS=... run a single custom invocation instead of the five surfaces.
|
|
802
815
|
desc "Run the fuzzer under AddressSanitizer (FAST=1 non-isolated, SKIP_BUILD=1 reuse build)"
|
|
803
816
|
task :sanitize do
|
|
804
817
|
sanitize = ENV["MAKIRI_SANITIZE"] || "address"
|
|
@@ -818,12 +831,15 @@ namespace :fuzz do
|
|
|
818
831
|
if ENV["FUZZ_ARGS"]
|
|
819
832
|
sh(env, "#{FileUtils::RUBY} -Ilib spec/fuzz/run.rb #{ENV['FUZZ_ARGS']}")
|
|
820
833
|
else
|
|
821
|
-
|
|
834
|
+
truthy = ->(name) { %w[1 true yes].include?(ENV[name].to_s.downcase) }
|
|
835
|
+
explicit_isolated = ENV.key?("ISOLATED") && !ENV["ISOLATED"].to_s.strip.empty?
|
|
836
|
+
isolated = explicit_isolated ? truthy.call("ISOLATED") : !truthy.call("FAST")
|
|
837
|
+
iso = isolated ? "--isolated" : "--in-process"
|
|
822
838
|
secs = ENV["FUZZ_TIME"] || "90"
|
|
823
|
-
# Cover every surface under the sanitizer: the query engine (XPath
|
|
824
|
-
#
|
|
825
|
-
# surface
|
|
826
|
-
["", "--target xml", "--target mutate", "--target xmlcss"].each do |surface|
|
|
839
|
+
# Cover every surface under the sanitizer: the query engine (XPath over the
|
|
840
|
+
# default fixtures), the HTML CSS selector path, the XML parser (hostile
|
|
841
|
+
# documents), the XML mutation surface, and CSS over an XML document.
|
|
842
|
+
["", "--target css", "--target xml", "--target mutate", "--target xmlcss"].each do |surface|
|
|
827
843
|
sh(env, "#{FileUtils::RUBY} -Ilib spec/fuzz/run.rb #{surface} #{iso} --time #{secs}".squeeze(" ").strip)
|
|
828
844
|
end
|
|
829
845
|
end
|
|
@@ -841,7 +857,7 @@ namespace :fuzz do
|
|
|
841
857
|
# library and headers (the crate's build.rs links the archive and generates
|
|
842
858
|
# the layout from the headers), which `rake compile` produces - hence the
|
|
843
859
|
# dependency, which is about Lexbor rather than about the bundle.
|
|
844
|
-
FUZZ_TARGETS = %w[xml xpath xml_xpath css html html_xpath].freeze
|
|
860
|
+
FUZZ_TARGETS = %w[xml xpath xml_xpath css html html_xpath html_css].freeze
|
|
845
861
|
|
|
846
862
|
# The local mode: TARGETS=css,html_xpath narrows the run to what you touched,
|
|
847
863
|
# and the time is FUZZ_TIME seconds per target (default 60) or FUZZ_BUDGET
|
data/ext/makiri/rust/Cargo.toml
CHANGED
|
@@ -23,7 +23,7 @@ path = "src/lib.rs"
|
|
|
23
23
|
# about an unexpected cfg name (and CI's `clippy -D warnings` turns that into an
|
|
24
24
|
# error). The proof modules are compiled only under it.
|
|
25
25
|
[lints.rust]
|
|
26
|
-
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] }
|
|
26
|
+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)', 'cfg(makiri_asan)'] }
|
|
27
27
|
|
|
28
28
|
# The panic gate. A panic now reaches Ruby as an exception (see the profile
|
|
29
29
|
# below), but it is still an INTERNAL error nobody asked for - `rescue => e`
|
|
@@ -32,15 +32,15 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] }
|
|
|
32
32
|
# few sites that want one carry an `#[allow]` with a reason. Test code is
|
|
33
33
|
# exempt in `lib.rs`.
|
|
34
34
|
#
|
|
35
|
-
# `expect_used`
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# `indexing_slicing` and `arithmetic_side_effects` (469 hits, mostly the
|
|
35
|
+
# `expect_used` joined them once method registration stopped `.expect`ing each
|
|
36
|
+
# definition (223 hits, now `?`): what remains is a handful of invariants, each
|
|
37
|
+
# with an `#[allow(..., reason = ...)]`. Not here are `indexing_slicing` and `arithmetic_side_effects` (469 hits, mostly the
|
|
39
38
|
# engine's own bounds-checked walks): gating those needs a different argument
|
|
40
39
|
# than "a panic aborts", since release builds keep `overflow-checks` on
|
|
41
40
|
# precisely so a wrapped size stops the process instead of answering wrong.
|
|
42
41
|
[lints.clippy]
|
|
43
42
|
unwrap_used = "warn"
|
|
43
|
+
expect_used = "warn"
|
|
44
44
|
panic = "warn"
|
|
45
45
|
|
|
46
46
|
[features]
|
|
@@ -88,13 +88,13 @@ rb-sys = { version = "0.9", optional = true }
|
|
|
88
88
|
# an `extern "C"` boundary into an abort, because unwinding through C frames
|
|
89
89
|
# built without unwind tables is undefined behaviour. So every callback catches
|
|
90
90
|
# its own panic and the caller re-raises it past the C frames - `caught::
|
|
91
|
-
# PanicLatch`, installed in all
|
|
91
|
+
# PanicLatch`, installed in all eight (the CSS traversal, the serializer sink,
|
|
92
92
|
# the tokenizer hook, the GVL trampoline, and `bridge::ruby::protect`, which
|
|
93
93
|
# covers every `rb_protect`). The GC callbacks in `bridge/typed.rs` keep
|
|
94
94
|
# aborting on purpose; see CLAUDE.md.
|
|
95
95
|
#
|
|
96
96
|
# Anything that must be released across a panic is therefore a `Drop`, not a
|
|
97
|
-
# statement after the work. `lexbor::selectors::
|
|
97
|
+
# statement after the work. `lexbor::selectors::Session` and
|
|
98
98
|
# `post_parse::DocOwner` are the two that had to be converted.
|
|
99
99
|
#
|
|
100
100
|
# overflow-checks stays ON in release: a wrapped size is exactly the class of
|