p_css 0.1.9 → 0.2.0.beta1

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.
data/lib/css/native.rb ADDED
@@ -0,0 +1,179 @@
1
+ require_relative '../css'
2
+
3
+ # cibuildgem ships precompiled binaries at lib/css/<ruby_minor>/css_native.{so,bundle,dll}.
4
+ # When users compile the gem from source (no platform gem available),
5
+ # extconf.rb places the binary at lib/css/css_native.{so,bundle,dll}.
6
+ # Try the version-specific path first, fall back to the non-versioned one.
7
+ begin
8
+ ruby_minor = RUBY_VERSION[/\d+\.\d+/]
9
+ require "css/#{ruby_minor}/css_native"
10
+ rescue LoadError
11
+ require_relative 'css_native'
12
+ end
13
+
14
+ # Rust defines matches?, matches_any?, match_indices with a required
15
+ # `state` argument (3 args). Wrap them in Ruby so callers can pass 2
16
+ # args when stateful pseudos aren't relevant.
17
+ module CSS
18
+ module Native
19
+ class Snapshot
20
+ alias_method :_native_matches?, :matches?
21
+ alias_method :_native_matches_any?, :matches_any?
22
+ alias_method :_native_match_indices, :match_indices
23
+
24
+ def matches?(element, selector, state = nil)
25
+ _native_matches?(element, selector, state)
26
+ end
27
+
28
+ def matches_any?(element, selectors, state = nil)
29
+ _native_matches_any?(element, selectors, state)
30
+ end
31
+
32
+ def match_indices(element, selectors, state = nil)
33
+ _native_match_indices(element, selectors, state)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module CSS
40
+ module Native
41
+ # High-level wrapper: takes a Nokogiri element + a selector (string or
42
+ # parsed AST) and returns matches?(element). Falls back to the pure-Ruby
43
+ # Matcher when the selector contains features the native matcher doesn't
44
+ # support yet (pseudo-classes, :not, etc.).
45
+ #
46
+ # `document:` / `snapshot:` let callers control snapshot reuse across
47
+ # many matches against the same DOM. With neither, a per-document
48
+ # snapshot is cached by document identity.
49
+ class << self
50
+ def matches?(element, selector, snapshot: nil, document: nil, state: nil)
51
+ ast = selector.is_a?(String) ? CSS.parse_selector_list(selector) : selector
52
+ compiled = compile_or_nil(ast)
53
+ snap = snapshot || snapshot_for(document || element.document)
54
+
55
+ return CSS.matches?(element, ast, state: state) unless compiled
56
+
57
+ native_state = state.nil? ? nil : (state.is_a?(State) ? state : snap.compile_state(state))
58
+ snap.matches?(element, compiled, native_state)
59
+ end
60
+
61
+ def compile_or_nil(ast)
62
+ Selector.compile(ast)
63
+ rescue Unsupported
64
+ nil
65
+ end
66
+
67
+ private
68
+
69
+ def snapshot_for(document)
70
+ (@snapshots ||= {}.compare_by_identity)[document] ||= Snapshot.from_document(document)
71
+ end
72
+ end
73
+
74
+ # Subclass of CSS::Cascade that uses the native matcher for the inner
75
+ # rule-matching loop. Selectors are pre-compiled at construction —
76
+ # those that can't be compiled (pseudo-classes etc.) fall through to
77
+ # the pure-Ruby matcher, so behavior is identical to CSS::Cascade.
78
+ #
79
+ # Requires a Nokogiri document at construction; the snapshot is built
80
+ # once and reused for every resolve(). Mutate the DOM and you must
81
+ # construct a fresh CSS::Native::Cascade.
82
+ class Cascade < CSS::Cascade
83
+ def initialize(stylesheet, document, context: CSS::MediaQueries::Context.default)
84
+ super(stylesheet, context: context)
85
+
86
+ @snapshot = Snapshot.from_document(document)
87
+ @compiled_by_ast = {}.compare_by_identity
88
+
89
+ @entries.each do |entry|
90
+ entry.selector_pairs.each {|ast, _spec|
91
+ @compiled_by_ast[ast] = Native.compile_or_nil(ast)
92
+ }
93
+ end
94
+ end
95
+
96
+ # Override: batch every candidate's compiled selectors into one FFI
97
+ # hop per resolve (GVL released), then merge in any Ruby-fallback
98
+ # matches. Cuts per-resolve FFI cost from O(candidates) to O(1).
99
+ def resolve(element, inline_style: nil, state: nil)
100
+ cache = {}
101
+ candidates = collect_candidate_indexes(element, cache)
102
+ order = 0
103
+ matches = []
104
+ native_state = state && @snapshot.compile_state(state)
105
+
106
+ best_by_entry = native_pass(element, candidates, native_state)
107
+ ruby_fallback_pass(element, candidates, best_by_entry, cache, state)
108
+
109
+ candidates.each do |entry_idx|
110
+ spec = best_by_entry[entry_idx] or next
111
+
112
+ @entries[entry_idx].declarations.each {|decl|
113
+ order += 1
114
+ matches << CSS::Cascade::Match.new(declaration: decl, specificity: spec, inline: false, order: order)
115
+ }
116
+ end
117
+
118
+ if inline_style
119
+ inline_declarations(inline_style).each {|decl|
120
+ order += 1
121
+ matches << CSS::Cascade::Match.new(
122
+ declaration: decl,
123
+ specificity: CSS::Selectors::Specificity::ZERO,
124
+ inline: true,
125
+ order: order
126
+ )
127
+ }
128
+ end
129
+
130
+ pick_winners(matches)
131
+ end
132
+
133
+ private
134
+
135
+ # Flatten the candidates' compiled selectors into one batched
136
+ # match_indices call. Returns Hash{entry_idx => best_specificity}.
137
+ def native_pass(element, candidates, native_state)
138
+ positions = []
139
+ sels = []
140
+
141
+ candidates.each do |entry_idx|
142
+ @entries[entry_idx].selector_pairs.each {|ast, spec|
143
+ compiled = @compiled_by_ast[ast] or next
144
+
145
+ positions << [entry_idx, spec]
146
+ sels << compiled
147
+ }
148
+ end
149
+
150
+ best_by_entry = {}
151
+
152
+ return best_by_entry if sels.empty?
153
+
154
+ @snapshot.match_indices(element, sels, native_state).each {|i|
155
+ entry_idx, spec = positions[i]
156
+ cur = best_by_entry[entry_idx]
157
+
158
+ best_by_entry[entry_idx] = spec if cur.nil? || spec > cur
159
+ }
160
+
161
+ best_by_entry
162
+ end
163
+
164
+ # Run pure-Ruby matching for any selectors that didn't compile
165
+ # (pseudo-classes, etc.), merging results into best_by_entry.
166
+ def ruby_fallback_pass(element, candidates, best_by_entry, cache, state)
167
+ candidates.each do |entry_idx|
168
+ @entries[entry_idx].selector_pairs.each {|ast, spec|
169
+ next if @compiled_by_ast[ast]
170
+ next unless Selectors::Matcher.matches?(element, ast, cache: cache, state: state)
171
+
172
+ cur = best_by_entry[entry_idx]
173
+ best_by_entry[entry_idx] = spec if cur.nil? || spec > cur
174
+ }
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
data/lib/css/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module CSS
2
- VERSION = '0.1.9'
2
+ VERSION = '0.2.0.beta1'
3
3
  end
metadata CHANGED
@@ -1,24 +1,50 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: p_css
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.2.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ date: 2026-05-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rb_sys
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
12
27
  description: p_css is a Ruby implementation of the CSS Syntax Level 4 tokenizer and
13
28
  parser, including support for CSS nesting.
14
29
  email:
15
30
  - ursm@ursm.jp
16
31
  executables: []
17
- extensions: []
32
+ extensions:
33
+ - ext/css_native/extconf.rb
18
34
  extra_rdoc_files: []
19
35
  files:
36
+ - Cargo.lock
37
+ - Cargo.toml
20
38
  - LICENSE.txt
21
39
  - README.md
40
+ - ext/css_native/Cargo.toml
41
+ - ext/css_native/extconf.rb
42
+ - ext/css_native/src/lib.rs
43
+ - ext/css_native/src/matcher.rs
44
+ - ext/css_native/src/selectors.rs
45
+ - ext/css_native/src/snapshot.rs
46
+ - ext/css_native/src/state.rs
47
+ - ext/css_native/src/tokenizer.rs
22
48
  - lib/css.rb
23
49
  - lib/css/cascade.rb
24
50
  - lib/css/code_points.rb
@@ -28,6 +54,7 @@ files:
28
54
  - lib/css/media_queries/evaluator.rb
29
55
  - lib/css/media_queries/nodes.rb
30
56
  - lib/css/media_queries/parser.rb
57
+ - lib/css/native.rb
31
58
  - lib/css/nesting.rb
32
59
  - lib/css/nodes.rb
33
60
  - lib/css/parser.rb
@@ -59,6 +86,7 @@ metadata:
59
86
  changelog_uri: https://github.com/ursm/p_css/releases
60
87
  source_code_uri: https://github.com/ursm/p_css
61
88
  rubygems_mfa_required: 'true'
89
+ post_install_message:
62
90
  rdoc_options: []
63
91
  require_paths:
64
92
  - lib
@@ -73,7 +101,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
101
  - !ruby/object:Gem::Version
74
102
  version: '0'
75
103
  requirements: []
76
- rubygems_version: 3.6.9
104
+ rubygems_version: 3.5.22
105
+ signing_key:
77
106
  specification_version: 4
78
107
  summary: A CSS Syntax Level 4 parser for Ruby, with nesting support.
79
108
  test_files: []