Module: Dommy::Internal::CSSRuleText

Defined in:
lib/dommy/internal/css_rule_text.rb

Overview

Splits raw stylesheet text into individual rule slices and pulls a single rule apart into its prelude (selector / at-rule condition) and body — enough to back the CSSOM CSSRule accessors (selectorText, style, nested cssRules) without re-tokenizing the whole grammar. Each slice is kept verbatim so CSSRule#cssText round-trips the source until mutated.

This is deliberately a lightweight scanner (it tracks brace depth, string literals and /* */ comments); the cascade's correctness still comes from lexbor. It exists only to give JS-visible CSSOM introspection.

Class Method Summary collapse

Class Method Details

.at_keyword(prelude) ⇒ Object

The at-rule keyword (lowercased, without @) of a prelude, or nil for a plain style rule.



108
109
110
111
# File 'lib/dommy/internal/css_rule_text.rb', line 108

def at_keyword(prelude)
  match = prelude.to_s.match(/\A@(-?[a-z][a-z-]*)/i)
  match && match[1].downcase
end

.matching_brace(text, open) ⇒ Object

Index of the } matching the { at open, skipping nested braces, strings and comments. nil when unterminated.



121
122
123
124
125
126
127
128
129
130
131
# File 'lib/dommy/internal/css_rule_text.rb', line 121

def matching_brace(text, open)
  depth = 0
  scan_braces(text, open) do |ch, i|
    depth += 1 if ch == "{"
    if ch == "}"
      depth -= 1
      return i if depth.zero?
    end
  end
  nil
end

.scan_braces(text, from) ⇒ Object

Walk text from from, yielding [char, index] for each {`/`} that lies outside string literals and /* */ comments.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/dommy/internal/css_rule_text.rb', line 135

def scan_braces(text, from)
  string = nil
  i = from
  len = text.length
  while i < len
    ch = text[i]
    if string
      if ch == "\\"
        i += 2
        next
      end
      string = nil if ch == string
    elsif ch == "/" && text[i + 1] == "*"
      i = (text.index("*/", i + 2) || len - 2) + 2
      next
    elsif ch == '"' || ch == "'"
      string = ch
    elsif ch == "{" || ch == "}"
      yield ch, i
    end
    i += 1
  end
end

.split_rule(text) ⇒ Object

[prelude, body] for one rule slice. body is the text between the outermost braces, or nil for a braceless statement at-rule. prelude is the selector list (style rule) or the at-rule keyword + condition.

Walks the BINARY bytes for the same reason as #split_rules: the brace scan is O(1) per byte instead of O(i) per UTF-8 character. Brace indices land on the 7-bit {`/`}, so every byteslice is a whole-character substring; re-tag the pieces UTF-8.



95
96
97
98
99
100
101
102
103
104
# File 'lib/dommy/internal/css_rule_text.rb', line 95

def split_rule(text)
  bytes = text.to_s.b
  brace = top_level_brace(bytes)
  return [bytes.force_encoding(Encoding::UTF_8).sub(/;\s*\z/, "").strip, nil] unless brace

  prelude = bytes[0...brace].force_encoding(Encoding::UTF_8).strip
  close = matching_brace(bytes, brace)
  body = bytes[(brace + 1)...(close || bytes.length)].to_s.force_encoding(Encoding::UTF_8)
  [prelude, body]
end

.split_rules(text) ⇒ Object

Top-level rule slices in source order, each stripped of surrounding whitespace. Block rules (sel { ... }, @media ... { ... }) span to their matching }; statement at-rules (@import ...;) span to ;.

Scans over the BINARY bytes, not the UTF-8 characters: every byte we branch on ({ } " ' / * ; \ and ASCII whitespace) is 7-bit, and a UTF-8 multibyte sequence is wholly bytes >= 0x80 — none of which collide with those, so the structure is read identically while bytes[i] stays O(1) (UTF-8 String#[i] is O(i), making the whole scan O(n^2) — the dominant cost on a large stylesheet). Slice boundaries always land on a 7-bit byte (a delimiter) or a UTF-8 lead byte (the first non-space of a prelude), so each byteslice is a whole-character substring; re-tag it UTF-8 on the way out so the verbatim rule text round-trips unchanged.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/dommy/internal/css_rule_text.rb', line 30

def split_rules(text)
  bytes = text.to_s.b
  rules = []
  start = nil
  depth = 0
  string = nil
  i = 0
  len = bytes.length

  while i < len
    ch = bytes[i]

    if string
      if ch == "\\"
        i += 2
        next
      end
      string = nil if ch == string
      i += 1
      next
    end

    if ch == "/" && bytes[i + 1] == "*"
      close = bytes.index("*/", i + 2)
      i = (close || len - 2) + 2
      next
    end

    case ch
    when '"', "'"
      start ||= i
      string = ch
    when "{"
      start ||= i
      depth += 1
    when "}"
      depth -= 1 if depth.positive?
      if depth.zero? && start
        rules << bytes[start..i]
        start = nil
      end
    when ";"
      if depth.zero? && start
        rules << bytes[start..i]
        start = nil
      end
    else
      start ||= i unless ch.match?(/\s/)
    end

    i += 1
  end

  rules << bytes[start..] if start
  rules.map { |slice| slice.force_encoding(Encoding::UTF_8).strip }.reject(&:empty?)
end

.top_level_brace(text) ⇒ Object

Index of the first { at brace depth 0, skipping strings and comments.



114
115
116
117
# File 'lib/dommy/internal/css_rule_text.rb', line 114

def top_level_brace(text)
  scan_braces(text, 0) { |ch, i| return i if ch == "{" }
  nil
end