makiri 0.13.0-aarch64-linux → 0.15.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c38568d5e92468d6f4da1275cf823cdc615d079c6a0293236ad5c85897647df1
4
- data.tar.gz: 33701440893a286012dcdba48d78cd30d69af8d27fff9bc3691603e5e4151900
3
+ metadata.gz: d9bef2471bfcd87d3e3c31bf6f4eeecef71202d81bf0bb46b7b71eee526cd1a4
4
+ data.tar.gz: 1836c1d1ee430cba8a058d63411259483ad84f3f4a1fdb69aae53621f1093f68
5
5
  SHA512:
6
- metadata.gz: '0586c38ca0fbbac3cc9da9d3147db3d58c2f1380991a06bf412f17d92029023fbfca96289c91b8bfc30a1a25e1a26a8df8e8e5ada81cbfaf62074eae396868d1'
7
- data.tar.gz: 85f351046a3e65e9e0c8f8d725b5aa585555f6055798f4d80ace09fe16f8ef28b8f00124ac9ce7de3e5e1047ae0b01cf263636f712818d402600bc0e996b1e9f
6
+ metadata.gz: 3f8c5b7d20a0190f05afd7e1d8c453979eeaf3dc7d1ec58e5314fdeb803e84d5a3bbbf6e1fa533b3827d0bc70b8b41269d5a5d2d06d79339c1244e3a5a10b7a4
7
+ data.tar.gz: e93f2d6a31337fc9e23782b4e3e5a37196e5ef95a92b206868721f8dc04d0263a593eb5361fef0c2fa0a67ab037a3777faafb65bcbe06fa0f45bdf215430bdd1
@@ -5,6 +5,13 @@ on:
5
5
  branches: [main, master]
6
6
  pull_request:
7
7
 
8
+ # A new push to a pull request supersedes the run on its previous head. The
9
+ # event is in the group so a push to main never queues behind (or replaces) a
10
+ # nightly run of the same workflow.
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
13
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
14
+
8
15
  jobs:
9
16
  test:
10
17
  name: Ruby ${{ matrix.ruby }} on ${{ matrix.os }}
@@ -15,6 +15,13 @@ on:
15
15
  - cron: "37 5 * * *" # nightly, offset from the other workflows
16
16
  workflow_dispatch:
17
17
 
18
+ # A new push to a pull request supersedes the run on its previous head. The
19
+ # event is in the group so a push to main never queues behind (or replaces) a
20
+ # nightly run of the same workflow.
21
+ concurrency:
22
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
23
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
24
+
18
25
  jobs:
19
26
  # PR + push: HTML5 parsing conformance, plus the XPath and CSS differentials
20
27
  # over the curated corpus and a moderate batch of generated XPath expressions.
@@ -37,6 +37,13 @@ on:
37
37
  - cron: "41 4 * * *"
38
38
  workflow_dispatch:
39
39
 
40
+ # A new push to a pull request supersedes the run on its previous head. The
41
+ # event is in the group so a push to main never queues behind (or replaces) a
42
+ # nightly run of the same workflow.
43
+ concurrency:
44
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
45
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
46
+
40
47
  env:
41
48
  # Short on a push or PR, long on the nightly schedule.
42
49
  LONG: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
@@ -139,8 +146,12 @@ jobs:
139
146
  # also starts from the other's corpus (read-only: new units go to the
140
147
  # first directory). The cache prefix is libfuzzer.yml's, so the corpus
141
148
  # accumulated there carries over.
149
+ # Restored here, saved after the minimization below - and not on a pull
150
+ # request: a cache a PR saves is scoped to that PR's ref, so main and the
151
+ # nightly never read it, and minimizing it (~5 min) only bought a cache
152
+ # nobody restores.
142
153
  - name: Restore the corpus
143
- uses: actions/cache@v6
154
+ uses: actions/cache/restore@v6
144
155
  with:
145
156
  path: ext/makiri/rust/fuzz/corpus/${{ matrix.target }}
146
157
  key: cargo-fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
@@ -171,6 +182,32 @@ jobs:
171
182
  -dict=css.dict \
172
183
  -print_final_stats=1
173
184
 
185
+ # The corpus is saved after every main/nightly run, so without this it
186
+ # only grows: every nightly hour adds units and nothing ever drops one.
187
+ # It reached ~58k files (with the sibling, ~107k units replayed before
188
+ # the first mutation) of which libFuzzer kept ~2.8k, and a short run
189
+ # spent its whole 180 s replaying - 0 new units, no fuzzing at all.
190
+ # Minimizing before the save keeps only what adds coverage. The first
191
+ # run after a long gap pays one replay of the backlog; later ones a
192
+ # few thousand units.
193
+ - name: Minimize the corpus before it is cached
194
+ if: github.event_name != 'pull_request'
195
+ working-directory: ext/makiri/rust/fuzz
196
+ run: |
197
+ before=$(ls corpus/${{ matrix.target }} | wc -l)
198
+ cargo fuzz cmin ${{ matrix.target }} corpus/${{ matrix.target }} -- \
199
+ -max_len=4096 \
200
+ -timeout=30
201
+ after=$(ls corpus/${{ matrix.target }} | wc -l)
202
+ echo "corpus/${{ matrix.target }}: $before -> $after files"
203
+
204
+ - name: Save the corpus
205
+ if: github.event_name != 'pull_request'
206
+ uses: actions/cache/save@v6
207
+ with:
208
+ path: ext/makiri/rust/fuzz/corpus/${{ matrix.target }}
209
+ key: cargo-fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
210
+
174
211
  - name: Upload reproducers
175
212
  if: failure()
176
213
  uses: actions/upload-artifact@v4
@@ -8,6 +8,13 @@ on:
8
8
  - cron: "17 19 * * *"
9
9
  workflow_dispatch:
10
10
 
11
+ # A new push to a pull request supersedes the run on its previous head. The
12
+ # event is in the group so a push to main never queues behind (or replaces) a
13
+ # nightly run of the same workflow.
14
+ concurrency:
15
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
16
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
17
+
11
18
  jobs:
12
19
  # A "C safety lint" job stood here (`rake security:clint`, over
13
20
  # script/check_c_safety.rb). It enforced rules about C that no longer exists -
@@ -23,6 +23,13 @@ on:
23
23
  - cron: "0 2 * * *"
24
24
  workflow_dispatch:
25
25
 
26
+ # A new push to a pull request supersedes the run on its previous head. The
27
+ # event is in the group so a push to main never queues behind (or replaces) a
28
+ # nightly run of the same workflow.
29
+ concurrency:
30
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
31
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
32
+
26
33
  jobs:
27
34
  # Valgrind memcheck on the full spec suite. Complements ASan by catching
28
35
  # layout-independent errors ASan misses: use of uninitialised values, and
@@ -111,8 +118,8 @@ jobs:
111
118
  # (parse/drop cycles) scale their iteration count down under stress
112
119
  # (`gc_churn_iters` / `GC_COMPACT_ITERS`) because each stressed iteration is
113
120
  # orders of magnitude heavier; `GC_COMPACT_ITERS` below tunes the total runtime
114
- # (~6-9 min on CI at 200). An earlier version forced GC.stress onto the
115
- # *entire* suite (~800 examples): it ran 1h40m+ and never finished, while
121
+ # (~14.5 min on CI at 200 in one job, hence the three shards below). An
122
+ # earlier version forced GC.stress onto the *entire* suite (~800 examples): it ran 1h40m+ and never finished, while
116
123
  # testing borrowed-pointer discipline on hundreds of examples that have none.
117
124
  # The rest of the suite still runs in ci.yml.
118
125
  #
@@ -123,7 +130,7 @@ jobs:
123
130
  # Pull requests, nightly and on demand - not on push (the valgrind job is the
124
131
  # post-merge gate). See the note under `on:` for why pull requests.
125
132
  if: github.event_name != 'push'
126
- name: GC.auto_compact + GC.stress (Ruby ${{ matrix.ruby }})
133
+ name: GC.auto_compact + GC.stress (Ruby ${{ matrix.ruby }}, ${{ matrix.shard.name }})
127
134
  runs-on: ubuntu-latest
128
135
  timeout-minutes: 30
129
136
  env:
@@ -137,6 +144,17 @@ jobs:
137
144
  fail-fast: false
138
145
  matrix:
139
146
  ruby: ["3.4"]
147
+ # Split by spec file into three ~equal parts (measured per file: the
148
+ # handler spec alone is a third, attribute + source_location another).
149
+ # The last shard is everything ELSE, so a newly tagged file lands
150
+ # there rather than in no shard; rebalance if it grows past the others.
151
+ shard:
152
+ - name: xpath_handler
153
+ args: spec/xpath_handler_spec.rb
154
+ - name: attribute
155
+ args: spec/attribute_spec.rb spec/source_location_spec.rb
156
+ - name: rest
157
+ args: --exclude-pattern "spec/{xpath_handler,attribute,source_location}_spec.rb" spec
140
158
 
141
159
  steps:
142
160
  - name: Checkout (with vendored Lexbor submodule)
@@ -166,4 +184,4 @@ jobs:
166
184
  # auto_compact globally and wrap each example in GC.stress; --tag gc_compact
167
185
  # limits the run to the borrowed-pointer examples.
168
186
  - name: Run GC-sensitive examples under GC.auto_compact + GC.stress
169
- run: bundle exec rspec --tag gc_compact spec
187
+ run: bundle exec rspec --tag gc_compact ${{ matrix.shard.args }}
@@ -1,11 +1,37 @@
1
1
  name: Verify
2
2
 
3
3
  on:
4
+ # Ten minutes of proofs, over the Ruby-free engine only - so they run when
5
+ # that engine (or how it is built) changes, not on every push.
4
6
  push:
5
7
  branches: [main, master]
8
+ paths: &kani-paths
9
+ # The proofs build `--no-default-features`, so the Ruby/Lexbor layers
10
+ # below are compiled out of them; a change confined to those cannot
11
+ # change a proof's outcome. Everything else under src/ stays in, since
12
+ # the harnesses reach the engine's shared modules (limits, text, ...).
13
+ - "ext/makiri/rust/src/**"
14
+ - "!ext/makiri/rust/src/bridge/**"
15
+ - "!ext/makiri/rust/src/glue/**"
16
+ - "!ext/makiri/rust/src/lexbor/**"
17
+ - "!ext/makiri/rust/src/css/**"
18
+ - "!ext/makiri/rust/src/init.rs"
19
+ - "ext/makiri/rust/build.rs"
20
+ - "ext/makiri/rust/Cargo.toml"
21
+ - "ext/makiri/rust/Cargo.lock"
22
+ - "Rakefile"
23
+ - ".github/workflows/verify.yml"
6
24
  pull_request:
25
+ paths: *kani-paths
7
26
  workflow_dispatch:
8
27
 
28
+ # A new push to a pull request supersedes the run on its previous head. The
29
+ # event is in the group so a push to main never queues behind (or replaces) a
30
+ # nightly run of the same workflow.
31
+ concurrency:
32
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
33
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
34
+
9
35
  jobs:
10
36
  # A CBMC job stood here and proved ext/makiri/{core,xml,xpath}/*.c. Those
11
37
  # files are gone, and `rake verify` with them: every one of its fifteen
data/CHANGELOG.md CHANGED
@@ -1,5 +1,110 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.15.0] - 2026-10-09
4
+
5
+ ### Added
6
+
7
+ * `Makiri::HTML::Document#create_loose_dom_element(qualified_name, prefix,
8
+ local_name, namespace_uri)`, like the XML Document's: the local name is
9
+ taken as given (`"Foo:Bar"` keeps its colon and case). Names the DOM
10
+ refuses raise `ArgumentError`.
11
+ * `Makiri::XML::Document#quirks_mode?` (always `false`) and `#compat_mode`
12
+ (always `"CSS1Compat"`), as on the HTML Document.
13
+
14
+ ### Changed
15
+
16
+ * `:lexbor-contains()` is no longer supported. `#css` / `#at_css` /
17
+ `#matches?` raise `Makiri::CSS::SyntaxError` for it (HTML and XML), and
18
+ `parse_stylesheet` reports its rule as `:bad_style`. Use XPath `contains()`
19
+ instead.
20
+ * HTML `#css` / `#at_css` / `#matches?` raise `Makiri::Error` ("could not be
21
+ run") for `:current(S)` instead of matching nothing.
22
+
23
+ ### Fixed
24
+
25
+ * `parse_stylesheet` keeps escapes in a selector's `:text`: `.md\:block` came
26
+ back as `.md:block`. `:current(S)` keeps its argument, and a rule
27
+ with `[|a]` is no longer reported as `:bad_style`.
28
+ * `parse_stylesheet` trims only CSS whitespace and never an escaped one:
29
+ `screen\ ` came back as `screen\`. A `:bad_style`'s `selector_text` no
30
+ longer keeps the whitespace before its `{`.
31
+ * HTML `:has()` with a descendant combinator is no longer slow on deep
32
+ documents: `:has(* * x)` exceeded the work budget at 200 levels.
33
+
34
+ ## [0.14.0] - 2026-10-04
35
+
36
+ ### Added
37
+
38
+ * `Document#attribute_version` (HTML and XML): an Integer that increases
39
+ whenever an attribute is added, removed or set (to the same value too),
40
+ including through an Attr node's `content=` or `remove`. Child-list and
41
+ text edits leave it as it is, so it can key a cache of attributes.
42
+ * `Makiri::XML::Document#create_element_ns(namespace_uri, qualified_name)`,
43
+ like the HTML Document's and the DOM's `createElementNS`. The element keeps
44
+ its namespace wherever it is inserted; a name that is not an XML QName
45
+ (`f}oo`) makes an element that `to_xml` refuses.
46
+ * `Makiri::HTML::Document.new`: an empty HTML document in no-quirks mode.
47
+ * `Makiri::HTML::Document#quirks_mode?` and `#compat_mode` (`"BackCompat"` /
48
+ `"CSS1Compat"`, like the DOM's `compatMode`).
49
+ * HTML `Attr#remove` / `#unlink` remove the attribute from its element, as on
50
+ XML. They raised "use delete(name) to remove an attribute".
51
+
52
+ ### Changed
53
+
54
+ * `tree_version` counts child-list changes only. A Text, Comment, CDATA or
55
+ PI node's `content=` no longer moves it, and an HTML Attr node's
56
+ `content=` moves `attribute_version` instead.
57
+ * `tree_version` and `attribute_version` are documented as cache keys: an
58
+ unchanged version means nothing it covers changed, while a changed one may
59
+ be conservative (an edit refused part-way can still move it).
60
+ * `namespace_uri` and `prefix` are documented as `nil`, never `""`, when
61
+ there is no namespace or prefix, in HTML and XML.
62
+ * `Document#dup` copies the tree node by node instead of re-parsing its
63
+ serialization. An HTML copy keeps a tree with no `<html>` root as it is and
64
+ keeps the quirks mode, and `Node#line` on it is `nil`. An XML copy keeps
65
+ every name and namespace as written (a re-parse invented `ns1:` prefixes),
66
+ copies data XML cannot write, and keeps the original's `max_bytes`.
67
+ * XML `create_processing_instruction` accepts the target `xml` (in any case),
68
+ as the DOM does; `to_xml` and `canonicalize` refuse a tree holding one.
69
+ * HTML `create_processing_instruction` raises `ArgumentError`, as XML does,
70
+ for data containing `?>`. It raised `Makiri::Error`.
71
+ * A refused XML insertion names the DOM rule it breaks, in the same words as
72
+ HTML. It said "invalid placement".
73
+
74
+ ### Fixed
75
+
76
+ * An Attr's `content=` and `remove` raise `FrozenError` when the element
77
+ that owns it is frozen, in HTML and XML.
78
+ * XML `Attr#content=` sets the attribute's value. It raised "operation
79
+ unsupported".
80
+ * HTML `title=` and `meta_encoding=` do nothing on a document with no
81
+ `<head>`, as the DOM's title setter does. They inserted the element directly
82
+ under the root element.
83
+ * HTML `Document#root` and CSS `:root` return the document element, or `nil`.
84
+ Without an `<html>` element they returned the first child of any kind, such
85
+ as a comment, and `title=` raised on such a document.
86
+ * An edit refused for its arguments - a frozen or non-node argument to an
87
+ HTML insertion, an invalid name given to XML `[]=`, `set_attribute_ns` or
88
+ `set_loose_dom_attribute`, a refused HTML `Attr#remove` - no longer moves a
89
+ version.
90
+ * An XML copy that runs out of the document's byte budget (`import_node`,
91
+ `clone_node`, an insertion from another document) gives back what it used.
92
+ The document could not be edited any more after one.
93
+ * Importing HTML into an XML document stores each namespace URI once instead
94
+ of once per element, which used up the byte budget faster.
95
+ * An HTML Attr imported on its own from another HTML document keeps its
96
+ namespace URI as written. It was lower-cased.
97
+ * `to_xml` and `canonicalize` write an element in the XML namespace as
98
+ `xml:local`. They wrote a declaration that does not parse. An element in
99
+ the XMLNS namespace is refused with `Makiri::Error`.
100
+ * XML XPath: a prefixed attribute on a detached element, whose namespace is
101
+ not decided yet, no longer matches an unprefixed name test.
102
+ * XML: references are read by one grammar everywhere. The internal subset
103
+ accepted a character reference to a character XML does not allow
104
+ (`<!ENTITY x "&#0;">`), and a malformed reference next to an entity
105
+ reference was reported as an unsupported DTD construct instead of as
106
+ malformed XML.
107
+
3
108
  ## [0.13.0] - 2026-10-03
4
109
 
5
110
  ### Added
@@ -890,14 +995,3 @@ Tooling
890
995
  benchmark harness (`rake bench`); conformance harnesses (html5lib-tests, WPT
891
996
  domxpath, CSS differential vs `Nokogiri::HTML5`). GitHub Actions CI across
892
997
  Ruby 3.2–4.0 × Ubuntu/macOS plus a sanitizer job.
893
-
894
- [0.9.0]: https://github.com/takahashim/makiri/compare/v0.8.0...v0.9.0
895
- [0.8.0]: https://github.com/takahashim/makiri/compare/v0.7.0...v0.8.0
896
- [0.7.0]: https://github.com/takahashim/makiri/compare/v0.6.0...v0.7.0
897
- [0.6.0]: https://github.com/takahashim/makiri/compare/v0.5.1...v0.6.0
898
- [0.5.1]: https://github.com/takahashim/makiri/compare/v0.5.0...v0.5.1
899
- [0.5.0]: https://github.com/takahashim/makiri/compare/v0.4.0...v0.5.0
900
- [0.4.0]: https://github.com/takahashim/makiri/compare/v0.3.0...v0.4.0
901
- [0.3.0]: https://github.com/takahashim/makiri/compare/v0.2.0...v0.3.0
902
- [0.2.0]: https://github.com/takahashim/makiri/compare/v0.1.0...v0.2.0
903
- [0.1.0]: https://github.com/takahashim/makiri/releases/tag/v0.1.0
@@ -245,18 +245,12 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
245
245
  text-containment extension. Use XPath (`xpath("//p[contains(., 'x')]")`) or
246
246
  Enumerable (`css('li')[1]`) for the rest.
247
247
  Standard Level-4 selectors (`:is` / `:where` / `:has`) are supported; some of which Nokogiri rejects.
248
- * `:lexbor-contains("text")` - Lexbor's spelling of the jQuery `:contains()`
249
- substring filter, matching an element whose text contains the string;
250
- append ` i` (`:lexbor-contains("text" i)`) for an ASCII case-insensitive
251
- match. (Nokogiri's name `:contains` is not an alias.) **XML only**: it
252
- lowers to XPath `child::text()[contains(., "text")]`, testing the
253
- element's immediate child text nodes, not the deep string-value. HTML no
254
- longer supports it - `lexbor::css_match`, the safe-Rust port that
255
- replaced Lexbor's own matcher for HTML, deliberately does not reimplement
256
- it, so a well-formed `:lexbor-contains()` now raises `Makiri::Error`
257
- ("could not be run") on HTML rather than ever matching (it still PARSES,
258
- and an actually malformed one still raises `Makiri::CSS::SyntaxError`, as
259
- before).
248
+ * There is no text-containment pseudo-class. Nokogiri's `:contains()` is
249
+ not supported, and neither is Lexbor's `:lexbor-contains()`: it raises
250
+ `Makiri::CSS::SyntaxError` on HTML and XML alike, like any unknown
251
+ pseudo-class (Lexbor's parser for it had memory-safety bugs, so Makiri
252
+ never lets it run). Use XPath instead:
253
+ `xpath('//p[contains(., "text")]')`.
260
254
  * Untyped `:*-of-type` (`:first-of-type`, `:nth-of-type(an+b)`, ... with no type
261
255
  selector) is supported and correct on both HTML and XML - the "type" is the
262
256
  element's own expanded name.
data/README.md CHANGED
@@ -184,6 +184,12 @@ and unusually large documents can raise it with `max_bytes:`.
184
184
  Makiri::XML(huge_xml, max_bytes: 512 * 1024 * 1024) # also Makiri::XML::Document.parse(..., max_bytes:)
185
185
  ```
186
186
 
187
+ The limit covers the document's whole life, not only the parse: nodes and
188
+ values made by editing it count too, and a replaced value or a removed node
189
+ keeps its bytes until the document is gone. A document edited for a long time
190
+ can be compacted with `dup`, which copies only what is in it now, under the
191
+ same limit.
192
+
187
193
  ### Node identity
188
194
 
189
195
  Navigating to the same node always gives the same Ruby object, for as long as
data/Rakefile CHANGED
@@ -94,10 +94,12 @@ end
94
94
  # the report down to errors whose stack touches our extension, so we no longer
95
95
  # have to fetch ruby.supp from ruby/ruby (that path was removed upstream).
96
96
  #
97
- # We keep this job's historical contract: catch *use of uninitialised values*
98
- # and *invalid reads/writes* (incl. intra-arena overflows) - NOT leaks (leak
99
- # detection stays with `rake leaks`). So we override ruby_memcheck's defaults,
100
- # which disable undef-value errors and turn on full leak-check.
97
+ # The job catches *use of uninitialised values* and *invalid reads/writes*
98
+ # (incl. intra-arena overflows), and also any "definitely lost" block whose
99
+ # stack touches our extension - the one leak gate with no frequency threshold
100
+ # (`rake leaks`, macOS, flags only stacks repeated >= 30x). It once meant to
101
+ # leave leaks out and could not: see `--leak-check` below. ruby_memcheck's
102
+ # defaults disable undef-value errors, so we override them.
101
103
  #
102
104
  # `filter_all_errors: true` is essential: by default ruby_memcheck only applies
103
105
  # its "stack must touch the makiri binary" filter to *leak*-kind errors
@@ -146,7 +148,19 @@ begin
146
148
  # post-merge push gate turns it off (VALGRIND_TRACK_ORIGINS=no) to run in
147
149
  # ~half the time, while the nightly / manual runs keep it on for the backtrace.
148
150
  "--track-origins=#{ENV.fetch('VALGRIND_TRACK_ORIGINS', 'yes')}",
149
- "--leak-check=no", # leaks are `rake leaks`' job, not this one
151
+ # Leaks ARE checked here, whatever this says: ruby_memcheck always runs
152
+ # valgrind with --xml=yes, and in XML mode memcheck writes its leak
153
+ # records even under --leak-check=no (a "definitely lost" in a spec's
154
+ # child process failed run 37864917248 that way). So say what happens,
155
+ # and keep ruby_memcheck's own leak kinds - these options REPLACE its
156
+ # defaults rather than adding to them, and without the line below
157
+ # valgrind's default (definite,possible) fails the job on a "possibly
158
+ # lost" too. A spec that runs Ruby in a child must leave
159
+ # RUBY_FREE_AT_EXIT set there (it is inherited): without it the child's
160
+ # live strings at exit are reported as definitely lost through our
161
+ # frames (PR #55).
162
+ "--leak-check=full",
163
+ "--show-leak-kinds=definite",
150
164
  ],
151
165
  )
152
166
 
Binary file
Binary file
Binary file
Binary file
@@ -26,22 +26,37 @@ module Makiri
26
26
  _parse(String(source), max_tree_depth)
27
27
  end
28
28
 
29
- # An independent copy of the whole document (like Nokogiri's Document#dup).
30
- # Built by serialising and re-parsing, so the copy shares no nodes with the
31
- # original - Node#dup's clone_node delegation is wrong for a document node,
32
- # hence this override. (A DOM mutated into a shape the HTML parser would not
33
- # itself produce, e.g. a foster-parented table cell, may be re-normalised on
34
- # re-parse; a freshly parsed document round-trips unchanged.) Any level
35
- # argument is ignored. #clone is this too (see {CloneViaDup}).
29
+ # An independent copy of the whole document (like Nokogiri's Document#dup):
30
+ # an empty document in the same quirks mode, given a deep copy of each of
31
+ # this one's children (+import_node+, which carries a <template>'s
32
+ # contents too), so the copy shares no nodes with the original and has
33
+ # the same tree - whatever shape it was built into, which a re-parse of
34
+ # +to_html+ would not keep (a Document.new without an <html> root gained
35
+ # the html/head/body shell and quirks mode). Node#dup's clone_node
36
+ # delegation is wrong for a document node, hence this override. Any
37
+ # level argument is ignored. #clone is this too (see {CloneViaDup}).
36
38
  #
37
- # Node#line on the copy is the line in the SERIALISED text it was parsed
38
- # from, not in the original source: `to_html` does not keep the original
39
- # line breaks between tags, so the two differ.
40
- #
41
- # The re-parse has no tree-depth limit: the tree is already here, however
42
- # it was built, and a copy must not fail where the original stands.
39
+ # Node#line is nil on the copy: its nodes were not parsed from a source.
43
40
  def dup(*)
44
- self.class.parse(to_html, max_tree_depth: -1)
41
+ _empty_copy.tap do |copy|
42
+ children.each { |child| copy.add_child(copy.import_node(child, true)) }
43
+ end
44
+ end
45
+
46
+ # Whether the document is in quirks mode - the mode a missing or legacy
47
+ # doctype puts it in. Limited-quirks mode is not quirks mode here, as the
48
+ # DOM's +compatMode+ has it. (#quirks_mode is the raw value: 0 no-quirks,
49
+ # 1 quirks, 2 limited-quirks.)
50
+ # @return [Boolean]
51
+ def quirks_mode?
52
+ quirks_mode == 1
53
+ end
54
+
55
+ # The DOM's +document.compatMode+: "BackCompat" in quirks mode, else
56
+ # "CSS1Compat".
57
+ # @return [String]
58
+ def compat_mode
59
+ quirks_mode? ? "BackCompat" : "CSS1Compat"
45
60
  end
46
61
 
47
62
  # The document's <body> element, or nil.
@@ -56,11 +71,13 @@ module Makiri
56
71
  at_css("head")
57
72
  end
58
73
 
59
- # Set the document title, creating <title> (in <head>) if absent.
74
+ # Set the document title, creating <title> (in <head>) if absent. A
75
+ # document with no <head> - a Document.new one before it has one - is
76
+ # left as it is, as the DOM's title setter leaves it.
60
77
  # @param text [String]
61
78
  # @return [String]
62
79
  def title=(text)
63
- ensure_in_head("title", "title").content = text
80
+ ensure_in_head("title", "title")&.content = text
64
81
  text
65
82
  end
66
83
 
@@ -89,21 +106,28 @@ module Makiri
89
106
  nil
90
107
  end
91
108
 
92
- # Set (or insert) a <meta charset> declaration.
109
+ # Set (or insert, in <head>) a <meta charset> declaration. Like #title=,
110
+ # it leaves a document with no <head> as it is.
93
111
  # @param value [String]
94
112
  # @return [String]
95
113
  def meta_encoding=(value)
96
- ensure_in_head("meta[charset]", "meta")["charset"] = value
114
+ ensure_in_head("meta[charset]", "meta")&.[]=("charset", value)
97
115
  value
98
116
  end
99
117
 
100
118
  private
101
119
 
102
120
  # The first node matching +css_query+, or a freshly created <+tag+>
103
- # appended to <head> (or the root when the document has no head). Shared by
104
- # #title= and #meta_encoding=, which then set content / attributes on it.
121
+ # appended to <head>; nil when there is neither a match nor a head - never
122
+ # an element put straight under the root, which the DOM's title setter
123
+ # does not do either. Shared by #title= and #meta_encoding=, which then
124
+ # set content / attributes on it.
105
125
  def ensure_in_head(css_query, tag)
106
- at_css(css_query) || Element.new(tag, self).tap { |el| (head || root).add_child(el) }
126
+ found = at_css(css_query)
127
+ return found if found
128
+
129
+ parent = head
130
+ parent && Element.new(tag, self).tap { |el| parent.add_child(el) }
107
131
  end
108
132
  end
109
133
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Makiri
4
- VERSION = "0.13.0"
4
+ VERSION = "0.15.0"
5
5
  end
@@ -20,25 +20,34 @@ module Makiri
20
20
  r ? r.replace(node) : add_child(node)
21
21
  end
22
22
 
23
- # An independent copy of the whole document (like Nokogiri's
24
- # Document#dup), sharing no nodes with the original. A document with a
25
- # root is serialised and re-parsed, like {Makiri::HTML::Document#dup}. One
26
- # without (still being built) is not well-formed, so it cannot be; what it
27
- # can hold before a root - comments and processing instructions - is
28
- # imported node by node instead. Any level argument is ignored, and
29
- # #clone is this too (see {CloneViaDup}).
30
- #
31
- # The copy is parsed with the default +max_bytes+, not the original's, and
32
- # its Node#line numbers are lines of the serialised text, which can
33
- # differ from the original source's.
23
+ # An independent copy of the whole document (like Nokogiri's Document#dup),
24
+ # made node for node rather than by serialising and re-parsing: the
25
+ # top-level order, every name, namespace URI and namespace state,
26
+ # attributes, the doctype and character data are the original's. Nothing
27
+ # has to be writable as XML, no namespace is resolved again (a re-parse
28
+ # gave an attribute set in a namespace an invented `ns1:` prefix), and the
29
+ # copy keeps the original's +max_bytes+ budget and whether it declared an
30
+ # encoding. Any level argument is ignored, and #clone is this too (see
31
+ # {CloneViaDup}).
34
32
  #
35
33
  # @return [Makiri::XML::Document]
36
34
  def dup(*)
37
- return self.class.parse(to_xml) if root
35
+ _copy
36
+ end
37
+
38
+ # Always false: an XML document is in no-quirks mode, as the DOM has it.
39
+ # The same name as {Makiri::HTML::Document#quirks_mode?}, so a caller
40
+ # need not ask which kind of document it holds.
41
+ # @return [false]
42
+ def quirks_mode?
43
+ false
44
+ end
38
45
 
39
- self.class.new.tap do |copy|
40
- children.each { |child| copy.add_child(copy.import_node(child, true)) }
41
- end
46
+ # The DOM's +document.compatMode+, which is always "CSS1Compat" for an
47
+ # XML document.
48
+ # @return [String]
49
+ def compat_mode
50
+ "CSS1Compat"
42
51
  end
43
52
  end
44
53
  end
@@ -345,8 +345,8 @@ SCENARIOS = {
345
345
  # CSS on XML: a different engine from the HTML one above - the selector is
346
346
  # lowered (css/) into an XPath AST, every node of which is a falloc site. One
347
347
  # selector per lowering shape: combinators, attribute operators, the nth and
348
- # of-type arithmetic, the selector-list pseudo-classes, :lexbor-contains and
349
- # namespaced type/universal selectors.
348
+ # of-type arithmetic, the selector-list pseudo-classes and namespaced
349
+ # type/universal selectors.
350
350
  "xml_css" => lambda do
351
351
  doc = Makiri::XML::Document.parse(<<~XML)
352
352
  <root xmlns:p="urn:p">
@@ -362,7 +362,7 @@ SCENARIOS = {
362
362
  ":nth-child(2n+1):not(:last-child)",
363
363
  "a:nth-last-of-type(1), :first-of-type:only-of-type",
364
364
  ":is(root > a, p|*):where(a) :empty",
365
- "root:has(> b + a) a:lexbor-contains(\"BETA\" i)",
365
+ "root:has(> b + a) a",
366
366
  "*|a, |b, p|*",
367
367
  ].map { |s| doc.css(s, ns).map { |n| n.name }.join(",") }.join("\n")
368
368
  end,
@@ -428,6 +428,7 @@ SCENARIOS = {
428
428
  "css_stylesheet" => lambda do
429
429
  css = <<~CSS
430
430
  div.a, p#b > span { color: red; margin: 0 !important }
431
+ .md\\:block, :is(.a\\,b, :not(#x\\.y)) > [a="q\\\\"] { color: red }
431
432
  p::before { content: "x" }
432
433
  @media (min-width: 600px) { .x { color: blue } }
433
434
  @font-face { font-family: F; src: url(f.woff) }
@@ -770,7 +771,7 @@ guard_docs = {
770
771
  "html" => Makiri::HTML::Document.parse("<p>hello</p>"),
771
772
  "xml" => Makiri::XML::Document.parse("<r><p>hello</p></r>")
772
773
  }
773
- [":lexbor-contains(#x)", "p:LEXBOR-CONTAINS(*)"].each do |sel|
774
+ [":lexbor-contains(#x)", "p:LEXBOR-CONTAINS(*)", %(p:lexbor-contains("x"))].each do |sel|
774
775
  guard_docs.each do |kind, doc|
775
776
  disarm
776
777
  3.times do
@@ -38,14 +38,14 @@ UNSAFE_ISLANDS = {
38
38
  "bridge/node_wrap.rs" => 1,
39
39
  "bridge/ruby.rs" => 18,
40
40
  "bridge/stack.rs" => 1,
41
- "bridge/string.rs" => 29,
41
+ "bridge/string.rs" => 30,
42
42
  "bridge/typed.rs" => 21,
43
- "bridge/wrapper.rs" => 19,
43
+ "bridge/wrapper.rs" => 20,
44
44
  "bridge/xml.rs" => 11,
45
- "bridge/xml_decode.rs" => 7,
45
+ "bridge/xml_decode.rs" => 5,
46
46
  "bridge/xpath/context_object.rs" => 3,
47
47
  "bridge/xpath/handler.rs" => 7,
48
- "bridge/xpath/mod.rs" => 5,
48
+ "bridge/xpath/mod.rs" => 7,
49
49
  "cbuf.rs" => 16,
50
50
  "cbuf/verify.rs" => 8,
51
51
  "falloc/cstr.rs" => 1,
@@ -55,9 +55,9 @@ UNSAFE_ISLANDS = {
55
55
  "lexbor/abi.rs" => 4,
56
56
  "lexbor/adapter/arena_bytes.rs" => 14,
57
57
  "lexbor/adapter/cross_import.rs" => 2,
58
- "lexbor/adapter/html/attrs.rs" => 23,
59
- "lexbor/adapter/html/build.rs" => 25,
60
- "lexbor/adapter/html/mod.rs" => 63,
58
+ "lexbor/adapter/html/attrs.rs" => 25,
59
+ "lexbor/adapter/html/build.rs" => 26,
60
+ "lexbor/adapter/html/mod.rs" => 67,
61
61
  "lexbor/adapter/html/mutate.rs" => 9,
62
62
  "lexbor/adapter/html/serialize.rs" => 18,
63
63
  "lexbor/adapter/post_parse.rs" => 13,
@@ -66,14 +66,14 @@ UNSAFE_ISLANDS = {
66
66
  "lexbor/adapter/tree_guard.rs" => 9,
67
67
  "lexbor/chunks.rs" => 2,
68
68
  "lexbor/css_engine.rs" => 14,
69
- "lexbor/css_parser.rs" => 21,
69
+ "lexbor/css_parser.rs" => 19,
70
70
  "lexbor/css_tokens.rs" => 10,
71
71
  "lexbor/fragment.rs" => 7,
72
72
  "lexbor/memory.rs" => 9,
73
73
  "lexbor/selector_cache.rs" => 15,
74
74
  "lexbor/selectors.rs" => 9,
75
75
  "lexbor/serialize.rs" => 2,
76
- "lexbor/stylesheet.rs" => 7,
76
+ "lexbor/stylesheet.rs" => 9,
77
77
  "lexbor/tests.rs" => 16,
78
78
  "lexbor/xpath.rs" => 9,
79
79
  "token.rs" => 1,
@@ -90,24 +90,25 @@ UNSAFE_ISLANDS = {
90
90
  FORBID_ROOTS = %w[css/mod.rs glue/mod.rs lexbor/css_match/mod.rs xml/mod.rs xpath/mod.rs].freeze
91
91
 
92
92
  FORBID_FILES = %w[
93
- css/build.rs css/lower.rs css/mod.rs
94
- cutf8.rs cutf8/verify.rs dom_rules.rs
95
- engine_error.rs falloc/verify.rs glue/css.rs
96
- glue/html_doc.rs glue/html_node/css.rs glue/html_node/mutate.rs
97
- glue/html_node/read.rs glue/html_node/serialize.rs glue/mod.rs
98
- glue/node.rs glue/node_set.rs glue/query.rs
99
- glue/xml_doc.rs glue/xml_node/css.rs glue/xml_node/mod.rs
100
- glue/xml_node/mutate.rs glue/xml_node/ns.rs glue/xml_node/read.rs
101
- glue/xml_node/serialize.rs glue/xml_node/strings.rs glue/xpath_context.rs
102
- lexbor/adapter/dom_index.rs lexbor/contains_guard.rs lexbor/css_match/mod.rs
103
- limits.rs node_type.rs ptr_table.rs
104
- rust_tests.rs stack.rs text.rs
105
- utf8_input.rs xml/arena.rs xml/attr_key.rs
106
- xml/chars/expand.rs xml/chars/mod.rs xml/dom_name.rs
107
- xml/encoding_sniff.rs xml/index.rs xml/mod.rs
108
- xml/model.rs xml/mutate/attr.rs xml/mutate/copy.rs
109
- xml/mutate/edit.rs xml/mutate/factory.rs xml/mutate/insert.rs
110
- xml/mutate/mod.rs xml/mutate/ns.rs xml/qname.rs
93
+ bridge/dom_error.rs css/build.rs css/lower.rs
94
+ css/mod.rs cutf8.rs cutf8/verify.rs
95
+ dom_rules.rs engine_error.rs falloc/verify.rs
96
+ glue/css.rs glue/html_doc.rs glue/html_node/css.rs
97
+ glue/html_node/mutate.rs glue/html_node/read.rs glue/html_node/serialize.rs
98
+ glue/mod.rs glue/node.rs glue/node_set.rs
99
+ glue/query.rs glue/xml_doc.rs glue/xml_node/css.rs
100
+ glue/xml_node/mod.rs glue/xml_node/mutate.rs glue/xml_node/ns.rs
101
+ glue/xml_node/read.rs glue/xml_node/serialize.rs glue/xml_node/strings.rs
102
+ glue/xpath_context.rs lexbor/adapter/dom_index.rs lexbor/contains_guard.rs
103
+ lexbor/css_match/mod.rs lexbor/selector_text.rs limits.rs
104
+ node_type.rs ptr_table.rs rust_tests.rs
105
+ stack.rs text.rs utf8_input.rs
106
+ xml/arena.rs xml/attr_key.rs xml/chars/expand.rs
107
+ xml/chars/mod.rs xml/dom_name.rs xml/encoding_sniff.rs
108
+ xml/index.rs xml/mod.rs xml/model.rs
109
+ xml/mutate/attr.rs xml/mutate/copy.rs xml/mutate/edit.rs
110
+ xml/mutate/factory.rs xml/mutate/insert.rs xml/mutate/mod.rs
111
+ xml/mutate/ns.rs xml/ns_scope.rs xml/qname.rs
111
112
  xml/selftest.rs xml/serialize/bindings.rs xml/serialize/c14n.rs
112
113
  xml/serialize/mod.rs xml/serialize/out.rs xml/serialize/xml.rs
113
114
  xml/tree/cursor.rs xml/tree/decl.rs xml/tree/dtd.rs
@@ -163,7 +164,9 @@ RAW_NODE_CROSSING =
163
164
  RAW_NODE_CROSSING_COUNTS = {
164
165
  "bridge/doc.rs" => 1,
165
166
  "bridge/fragment.rs" => 3,
166
- "bridge/html.rs" => 7,
167
+ # 8th: an Attr's owner, read as the wrapper cache's identity key for the
168
+ # owner-frozen check (`check_attr_owner_frozen`); it does not escape.
169
+ "bridge/html.rs" => 8,
167
170
  "bridge/wrapper.rs" => 3,
168
171
  }.freeze
169
172
 
@@ -425,7 +428,7 @@ if lexbor_abi != LEXBOR_ABI_COUNTS
425
428
  end
426
429
 
427
430
  # A Lexbor function is DECLARED in one place, `lexbor/abi.rs` (bindgen's output
428
- # plus the three exports no header declares). A second `extern "C"` declaration of the
431
+ # plus the exports no header declares). A second `extern "C"` declaration of the
429
432
  # same symbol gives it a second Rust type, which nothing checks agree, and on
430
433
  # macOS a declaration that matches no symbol is a NULL call rather than a link
431
434
  # error. `post_parse.rs` re-declared four generated functions that way.
@@ -442,6 +445,31 @@ unless lexbor_decls.empty?
442
445
  errors << "Lexbor functions declared outside lexbor/abi.rs: #{lexbor_decls.inspect}"
443
446
  end
444
447
 
448
+ # Lexbor's CSS parsers are CALLED in one place each - the function that puts
449
+ # their input through `contains_guard` first (CLAUDE.md: the guard is not
450
+ # optional, and a new parse entry must go through it). A type alone cannot
451
+ # say so: the raw ABI is reachable crate-wide, so a second call site would
452
+ # compile and reach the parser unguarded. Pinned here instead.
453
+ GUARDED_PARSES = {
454
+ "lxb_css_selectors_parse" => "lexbor/css_engine.rs", # SelectorParser::parse
455
+ "lxb_css_stylesheet_parse" => "lexbor/stylesheet.rs", # guarded_stylesheet
456
+ }.freeze
457
+ guarded_calls = Hash.new { |h, k| h[k] = Hash.new(0) }
458
+ Dir.glob(File.join(RUST, "**", "*.rs")).sort.each do |path|
459
+ relative = path.delete_prefix("#{RUST}/")
460
+ code = comments_removed(File.binread(path))
461
+ GUARDED_PARSES.each_key do |name|
462
+ count = code.scan(/\b#{name}\s*\(/).length
463
+ guarded_calls[name][relative] = count unless count.zero?
464
+ end
465
+ end
466
+ GUARDED_PARSES.each do |name, home|
467
+ next if guarded_calls[name] == { home => 1 }
468
+
469
+ errors << "#{name} must be called exactly once, in #{home} (behind contains_guard): " \
470
+ "#{guarded_calls[name].inspect}"
471
+ end
472
+
445
473
  # magnus's `RHash::foreach` runs its closure under magnus's own `protect`, below
446
474
  # `rb_hash_foreach`'s C frames, so a panic in it aborts the process instead of
447
475
  # raising. `glue::hash::hash_foreach` latches it; every Hash walk goes there.
@@ -462,7 +490,7 @@ end
462
490
  # is generated, so its signature is Lexbor's. A new hand declaration of a
463
491
  # header-declared function belongs in build.rs's allowlist instead; one with no
464
492
  # header belongs in build.rs's UNDECLARED_EXPORTS as well as here.
465
- LEXBOR_HAND_DECLS = 5
493
+ LEXBOR_HAND_DECLS = 4
466
494
  abi_decls = comments_removed(File.binread(File.join(RUST, "lexbor/abi.rs"))).scan(LEXBOR_DECL).length
467
495
  if abi_decls != LEXBOR_HAND_DECLS
468
496
  errors << "lexbor/abi.rs hand-declares #{abi_decls} Lexbor functions (expected " \
@@ -161,6 +161,20 @@
161
161
  ...
162
162
  }
163
163
 
164
+ # The global symbol table (`global_symbols.ids`) grows in chunks as symbols are
165
+ # interned, and lives for the life of the VM: RUBY_FREE_AT_EXIT reports its
166
+ # chunk arrays as "definitely lost" whenever the growth happened under one of
167
+ # our cfuncs - `parse_stylesheet` interns its hash keys with `rb_intern` on the
168
+ # first call, and whether that call crosses a chunk boundary depends on how many
169
+ # symbols the process made before it. Not ours to free.
170
+ {
171
+ ruby-global-symbol-table
172
+ Memcheck:Leak
173
+ ...
174
+ fun:set_id_entry*
175
+ ...
176
+ }
177
+
164
178
  # ---- Makiri's process-lifetime CSS compiled-selector cache ----
165
179
  # `lexbor::selector_cache` keeps compiled selector lists in a process-global
166
180
  # `HashMap<Box<[u8]>, _>` (bounded by CACHE_CAP) that lives until exit by
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: makiri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.15.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - takahashim