Module: Dommy::Internal::CSS::PropertyRegistry

Defined in:
lib/dommy/internal/css/calc.rb,
lib/dommy/internal/css/property_registry.rb

Overview

The curated property table driving the cascade: which properties inherit, their initial values, computed-value transforms, and shorthand expansion. Properties outside the table degrade gracefully: their cascaded value is returned as-is (no inheritance, no normalization) rather than raising.

Defined Under Namespace

Classes: CalcUnresolvable, Property

Constant Summary collapse

PROPERTIES =
{
  "display" => Property.new(inherited: false, initial: "inline"),
  "visibility" => Property.new(inherited: true, initial: "visible"),
  "opacity" => Property.new(inherited: false, initial: "1"),
  "color" => Property.new(inherited: true, initial: "rgb(0, 0, 0)"),
  "background-color" => Property.new(inherited: false, initial: "rgba(0, 0, 0, 0)"),
  "font-size" => Property.new(inherited: true, initial: "16px"),
  "font-family" => Property.new(inherited: true, initial: "sans-serif"),
  "font-weight" => Property.new(inherited: true, initial: "400"),
  "font-style" => Property.new(inherited: true, initial: "normal"),
  "line-height" => Property.new(inherited: true, initial: "normal"),
  "text-align" => Property.new(inherited: true, initial: "start"),
  "text-decoration-line" => Property.new(inherited: false, initial: "none"),
  "white-space" => Property.new(inherited: true, initial: "normal"),
  "letter-spacing" => Property.new(inherited: true, initial: "normal"),
  "word-spacing" => Property.new(inherited: true, initial: "normal"),
  "cursor" => Property.new(inherited: true, initial: "auto"),
  "direction" => Property.new(inherited: true, initial: "ltr"),
  "text-transform" => Property.new(inherited: true, initial: "none"),
  "text-indent" => Property.new(inherited: true, initial: "0px"),
  "font-variant" => Property.new(inherited: true, initial: "normal"),
  "list-style-type" => Property.new(inherited: true, initial: "disc"),
  "list-style-position" => Property.new(inherited: true, initial: "outside"),
  "list-style-image" => Property.new(inherited: true, initial: "none"),
  "tab-size" => Property.new(inherited: true, initial: "8"),
  "z-index" => Property.new(inherited: false, initial: "auto"),
  "position" => Property.new(inherited: false, initial: "static"),
  # Color longhands of the border/outline/text-decoration shorthands:
  # registered so they normalize to rgb() and resolve currentColor (the
  # initial of each). Width/style longhands stay unregistered (passed
  # through as specified — Dommy applies no style-dependent used-value
  # rule like "width is 0 when style is none").
  "border-top-color" => Property.new(inherited: false, initial: "currentColor"),
  "border-right-color" => Property.new(inherited: false, initial: "currentColor"),
  "border-bottom-color" => Property.new(inherited: false, initial: "currentColor"),
  "border-left-color" => Property.new(inherited: false, initial: "currentColor"),
  "outline-color" => Property.new(inherited: false, initial: "currentColor"),
  "text-decoration-color" => Property.new(inherited: false, initial: "currentColor"),
  "text-decoration-style" => Property.new(inherited: false, initial: "solid"),
  "flex-grow" => Property.new(inherited: false, initial: "0"),
  "flex-shrink" => Property.new(inherited: false, initial: "1"),
  "flex-basis" => Property.new(inherited: false, initial: "auto"),
  "flex-direction" => Property.new(inherited: false, initial: "row"),
  "flex-wrap" => Property.new(inherited: false, initial: "nowrap"),
}.freeze
COLOR_PROPERTIES =
%w[
  color background-color
  border-top-color border-right-color border-bottom-color border-left-color
  outline-color text-decoration-color
].freeze
FONT_WEIGHT_KEYWORDS =
{"normal" => "400", "bold" => "700"}.freeze
TEXT_DECORATION_LINES =

text-decoration sub-property keyword sets (the shorthand classifies each token as a line, a style, or — failing both — a color).

%w[none underline overline line-through blink].freeze
TEXT_DECORATION_STYLES =
%w[solid double dotted dashed wavy].freeze
BORDER_SIDES =
%w[top right bottom left].freeze
BORDER_STYLE_KEYWORDS =
%w[none hidden dotted dashed solid double groove ridge inset outset].freeze
BORDER_WIDTH_KEYWORDS =
%w[thin medium thick].freeze
BOX_SHORTHANDS =
{
  "margin" => "margin-%s",
  "padding" => "padding-%s",
  "inset" => "%s",
}.freeze
BORDER_LONGHANDS =

Per-side longhands of the border family, in (width, style, color) order across the four sides — the targets a CSS-wide keyword on border expands to.

BORDER_SIDES.flat_map do |side|
  %w[width style color].map { |kind| "border-#{side}-#{kind}" }
end.freeze
EXPANSION_TARGETS =

The longhands each curated shorthand covers — what a CSS-wide keyword (or an invalid var() substitution) applies to when it appears as the shorthand's whole value.

{
  "margin" => %w[margin-top margin-right margin-bottom margin-left],
  "padding" => %w[padding-top padding-right padding-bottom padding-left],
  "inset" => %w[top right bottom left],
  "overflow" => %w[overflow-x overflow-y],
  "gap" => %w[row-gap column-gap],
  "background" => %w[background-color],
  "font" => %w[font-style font-weight font-size line-height font-family],
  "text-decoration" => %w[text-decoration-line text-decoration-style text-decoration-color],
  "border" => BORDER_LONGHANDS,
  "border-width" => %w[border-top-width border-right-width border-bottom-width border-left-width],
  "border-style" => %w[border-top-style border-right-style border-bottom-style border-left-style],
  "border-color" => %w[border-top-color border-right-color border-bottom-color border-left-color],
  "border-top" => %w[border-top-width border-top-style border-top-color],
  "border-right" => %w[border-right-width border-right-style border-right-color],
  "border-bottom" => %w[border-bottom-width border-bottom-style border-bottom-color],
  "border-left" => %w[border-left-width border-left-style border-left-color],
  "outline" => %w[outline-width outline-style outline-color],
  "flex" => %w[flex-grow flex-shrink flex-basis],
  "flex-flow" => %w[flex-direction flex-wrap],
  "list-style" => %w[list-style-type list-style-position list-style-image],
  "place-content" => %w[align-content justify-content],
  "place-items" => %w[align-items justify-items],
  "place-self" => %w[align-self justify-self],
}.freeze
ABSOLUTE_UNIT_PX =

px-per-unit for the absolute length units (CSS Values 4: 96px per inch). These need no layout, so a computed value can carry them as px.

{
  "px" => 1.0,
  "pt" => 96.0 / 72,
  "pc" => 16.0,
  "in" => 96.0,
  "cm" => 96.0 / 2.54,
  "mm" => 96.0 / 25.4,
  # 1Q = 1/40 of a centimetre (CSS Values 4 §5.2).
  "q" => 96.0 / 2.54 / 40,
}.freeze
LENGTH_PATTERN =
/\A(-?\d+(?:\.\d+)?)(px|em|rem|pt|pc|in|cm|mm|q|vw|vh|vmin|vmax)\z/i

Class Method Summary collapse

Class Method Details

.binary_operator_position?(previous) ⇒ Boolean

A +/- is a binary operator (vs a unary sign) when it follows a value: a number/dimension or a closing paren.

Returns:

  • (Boolean)


88
89
90
# File 'lib/dommy/internal/css/calc.rb', line 88

def binary_operator_position?(previous)
  previous == ")" || (previous.is_a?(Hash) && previous.key?(:num))
end

.border_triple(prefix, parts) ⇒ Object

Omitted border sub-properties reset to their initial (shorthand semantics): width medium, style none, color currentColor.



405
406
407
408
409
# File 'lib/dommy/internal/css/property_registry.rb', line 405

def border_triple(prefix, parts)
  [["#{prefix}-width", parts[:width] || "medium"],
   ["#{prefix}-style", parts[:style] || "none"],
   ["#{prefix}-color", parts[:color] || "currentColor"]]
end

.border_width_token?(low) ⇒ Boolean

Returns:

  • (Boolean)


399
400
401
# File 'lib/dommy/internal/css/property_registry.rb', line 399

def border_width_token?(low)
  BORDER_WIDTH_KEYWORDS.include?(low) || low.match?(/\A[.\-\d]/)
end

.box_values(tokens) ⇒ Object

1 value -> all sides, 2 -> v/h, 3 -> t/h/b, 4 -> t/r/b/l.



373
374
375
376
377
378
379
380
# File 'lib/dommy/internal/css/property_registry.rb', line 373

def box_values(tokens)
  case tokens.size
  when 1 then [tokens[0]] * 4
  when 2 then [tokens[0], tokens[1], tokens[0], tokens[1]]
  when 3 then [tokens[0], tokens[1], tokens[2], tokens[1]]
  else tokens[0, 4]
  end
end

.calc_tokenize(str) ⇒ Object

Tokens: "(" ")" "," operators, unit:, fn: (an ident, only valid before "("). Returns nil on an unexpected character — or when a binary +/- lacks the whitespace CSS Values 4 §10.1 requires on both sides (1px+1px is invalid; a unary sign after (/,/operator is fine).



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
# File 'lib/dommy/internal/css/calc.rb', line 47

def calc_tokenize(str)
  tokens = []
  index = 0
  length = str.length
  space_before = true # the opening boundary counts as whitespace
  while index < length
    char = str[index]
    if char.match?(/\s/)
      space_before = true
      index += 1
      next
    end

    if (char == "+" || char == "-") && binary_operator_position?(tokens.last)
      space_after = index + 1 < length && str[index + 1].match?(/\s/)
      return nil unless space_before && space_after
      tokens << char
    elsif "+-*/(),".include?(char)
      tokens << char
    elsif (match = str[index..].match(/\A(\d*\.\d+|\d+\.?\d*)([a-z%]*)/i))
      tokens << {num: match[1].to_f, unit: match[2].downcase}
      index += match[0].length
      space_before = false
      next
    elsif (match = str[index..].match(/\A[a-z]+/i))
      tokens << {fn: match[0].downcase}
      index += match[0].length
      space_before = false
      next
    else
      return nil
    end

    space_before = false
    index += 1
  end
  tokens
end

.clamp_opacity(value) ⇒ Object

opacity computes to a number clamped to [0, 1]; a percentage maps to that fraction (css-color-4 §13). Unparseable values pass through.



213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/dommy/internal/css/property_registry.rb', line 213

def clamp_opacity(value)
  text = value.to_s.strip
  number =
    if text.end_with?("%")
      Float(text[0..-2]) / 100.0
    else
      Float(text)
    end
  format_number(number.clamp(0.0, 1.0))
rescue ArgumentError, TypeError
  value
end

.computed_value(name, value, font_size:, root_font_size:, viewport_width: nil, viewport_height: nil) ⇒ Object

The computed-value transform for one property. font_size / root_font_size are the element's / root's computed font-size in px; viewport_width / viewport_height the viewport in px. Together they let a bare length resolve to px without layout (em/rem/absolute/vw/vh).



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/dommy/internal/css/property_registry.rb', line 194

def computed_value(name, value, font_size:, root_font_size:, viewport_width: nil, viewport_height: nil)
  return Color.normalize(value) if COLOR_PROPERTIES.include?(name)
  return FONT_WEIGHT_KEYWORDS.fetch(value.downcase, value) if name == "font-weight"
  return clamp_opacity(value) if name == "opacity"
  # A <percentage> line-height computes to px against the element's own
  # font-size (CSS2 §10.8.1), inherited as that length. A unitless
  # <number> is kept as the number — its whole point is to inherit
  # unitless and scale to each descendant's font-size.
  if name == "line-height" && (pct = percentage_of(value, font_size))
    return pct
  end

  ctx = {font_size: font_size, root_font_size: root_font_size,
         viewport_width: viewport_width, viewport_height: viewport_height}
  evaluate_calc(value, **ctx) || resolve_length(value, **ctx) || value
end

.evaluate_calc(value, **ctx) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/dommy/internal/css/calc.rb', line 20

def evaluate_calc(value, **ctx)
  value = value.to_s.strip
  return nil unless value.match?(/\A(?:calc|min|max|clamp)\(/i)

  tokens = calc_tokenize(value)
  return nil unless tokens

  parser = CalcParser.new(tokens, ctx)
  kind, number = parser.parse_value
  return nil unless parser.done?

  kind == :length ? format_px(number) : format_number(number)
rescue CalcUnresolvable
  nil
end

.expand(name, value) ⇒ Object

Expand a (possibly shorthand) declaration into [[name, value], ...] longhand pairs. Cascade correctness is the point: a later shorthand must reset the longhands it covers (e.g. background: url(x) resets background-color to its initial). Shorthands outside this curated set pass through unexpanded (documented divergence).



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/dommy/internal/css/property_registry.rb', line 139

def expand(name, value)
  value = value.to_s.strip
  case name
  when "margin", "padding", "inset"
    expand_box(BOX_SHORTHANDS[name], value) || [[name, value]]
  when "overflow"
    expand_overflow(value)
  when "gap"
    expand_gap(value)
  when "background"
    [["background-color", Color.extract(value) || initial("background-color")]]
  when "font"
    expand_font(value) || [[name, value]]
  when "text-decoration"
    expand_text_decoration(value)
  when "border"
    expand_border_all(value)
  when "border-top", "border-right", "border-bottom", "border-left"
    expand_border_side(name.delete_prefix("border-"), value)
  when "border-width", "border-style", "border-color"
    expand_border_box(name.delete_prefix("border-"), value) || [[name, value]]
  when "outline"
    expand_outline(value)
  when "flex"
    expand_flex(value)
  when "flex-flow"
    expand_flex_flow(value)
  when "list-style"
    expand_list_style(value)
  when "place-content", "place-items", "place-self"
    expand_place(name.delete_prefix("place-"), value)
  else
    [[name, value]]
  end
end

.expand_border_all(value) ⇒ Object



411
412
413
414
# File 'lib/dommy/internal/css/property_registry.rb', line 411

def expand_border_all(value)
  parts = parse_border_shorthand(value)
  BORDER_SIDES.flat_map { |side| border_triple("border-#{side}", parts) }
end

.expand_border_box(kind, value) ⇒ Object

border-width|style|color: <box> — one kind across the four sides.



421
422
423
424
425
426
# File 'lib/dommy/internal/css/property_registry.rb', line 421

def expand_border_box(kind, value)
  tokens = split_tokens(value)
  return nil unless (1..4).cover?(tokens.size)

  BORDER_SIDES.zip(box_values(tokens)).map { |side, v| ["border-#{side}-#{kind}", v] }
end

.expand_border_side(side, value) ⇒ Object



416
417
418
# File 'lib/dommy/internal/css/property_registry.rb', line 416

def expand_border_side(side, value)
  border_triple("border-#{side}", parse_border_shorthand(value))
end

.expand_box(pattern, value) ⇒ Object

CSS box expansion: 1 value -> all sides, 2 -> v/h, 3 -> t/h/b, 4 -> t/r/b/l. Returns nil for token counts outside 1..4 (or values with nested whitespace we can't split safely).



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/dommy/internal/css/property_registry.rb', line 272

def expand_box(pattern, value)
  tokens = value.split(/\s+/)
  return nil unless (1..4).cover?(tokens.size) && tokens.none? { |t| t.include?("(") }

  top, right, bottom, left =
    case tokens.size
    when 1 then [tokens[0]] * 4
    when 2 then [tokens[0], tokens[1], tokens[0], tokens[1]]
    when 3 then [tokens[0], tokens[1], tokens[2], tokens[1]]
    else tokens
    end
  %w[top right bottom left].zip([top, right, bottom, left]).map do |side, v|
    [format(pattern, side), v]
  end
end

.expand_flex(value) ⇒ Object

flex per css-flexbox-1 §7.1.1: none -> 0 0 auto, auto -> 1 1 auto, a bare number -> grow 1 0%, etc. Omitted parts take the shorthand's reset values, not the longhand initials.



438
439
440
441
442
443
444
445
446
447
# File 'lib/dommy/internal/css/property_registry.rb', line 438

def expand_flex(value)
  tokens = split_tokens(value)
  grow, shrink, basis =
    case tokens.size
    when 1 then flex_one(tokens[0])
    when 2 then flex_two(tokens[0], tokens[1])
    else tokens[0, 3]
    end
  [["flex-grow", grow], ["flex-shrink", shrink], ["flex-basis", basis]]
end

.expand_flex_flow(value) ⇒ Object



464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/dommy/internal/css/property_registry.rb', line 464

def expand_flex_flow(value)
  direction = nil
  wrap = nil
  split_tokens(value).each do |token|
    low = token.downcase
    if %w[row row-reverse column column-reverse].include?(low)
      direction = token
    elsif %w[nowrap wrap wrap-reverse].include?(low)
      wrap = token
    end
  end
  [["flex-direction", direction || "row"], ["flex-wrap", wrap || "nowrap"]]
end

.expand_font(value) ⇒ Object

Minimal font shorthand: [style] [weight] [/] <family...>. Omitted sub-properties reset to their initial value (shorthand semantics). Returns nil when no size token is found (e.g. system font keywords) so the caller passes it through.



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/dommy/internal/css/property_registry.rb', line 302

def expand_font(value)
  tokens = value.split(/\s+/)
  size_index = tokens.index { |t| t.match?(%r{\A\d+(?:\.\d+)?(px|em|rem|%)(/|\z)}i) }
  return nil unless size_index

  style = "normal"
  weight = "400"
  tokens[0...size_index].each do |token|
    case token.downcase
    when "italic", "oblique" then style = token.downcase
    when "bold", /\A[1-9]00\z/ then weight = FONT_WEIGHT_KEYWORDS.fetch(token.downcase, token)
    end
  end

  size, line_height = tokens[size_index].split("/", 2)
  family = tokens[(size_index + 1)..].join(" ")

  pairs = [
    ["font-style", style],
    ["font-weight", weight],
    ["font-size", size],
    ["line-height", line_height || "normal"],
  ]
  pairs << ["font-family", family] unless family.empty?
  pairs
end

.expand_gap(value) ⇒ Object



293
294
295
296
# File 'lib/dommy/internal/css/property_registry.rb', line 293

def expand_gap(value)
  row, column = value.split(/\s+/)
  [["row-gap", row], ["column-gap", column || row]]
end

.expand_list_style(value) ⇒ Object

list-style: <type> || <position> || <image> (any order). A none sets both type and image to none (css-lists-3).



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/dommy/internal/css/property_registry.rb', line 480

def expand_list_style(value)
  type = position = image = nil
  none = false
  split_tokens(value).each do |token|
    low = token.downcase
    if %w[inside outside].include?(low)
      position = token
    elsif low.start_with?("url(")
      image = token
    elsif low == "none"
      none = true
    else
      type = token
    end
  end
  type ||= "none" if none
  image ||= "none" if none
  [["list-style-type", type || "disc"],
   ["list-style-position", position || "outside"],
   ["list-style-image", image || "none"]]
end

.expand_outline(value) ⇒ Object



428
429
430
431
432
433
# File 'lib/dommy/internal/css/property_registry.rb', line 428

def expand_outline(value)
  parts = parse_border_shorthand(value)
  [["outline-width", parts[:width] || "medium"],
   ["outline-style", parts[:style] || "none"],
   ["outline-color", parts[:color] || "currentColor"]]
end

.expand_overflow(value) ⇒ Object



288
289
290
291
# File 'lib/dommy/internal/css/property_registry.rb', line 288

def expand_overflow(value)
  x, y = value.split(/\s+/)
  [["overflow-x", x], ["overflow-y", y || x]]
end

.expand_place(suffix, value) ⇒ Object

place-content/items/self: <align> [<justify>]; a single value applies to both. (Multi-keyword alignment values like safe center are not split — a documented simplification.)



505
506
507
508
# File 'lib/dommy/internal/css/property_registry.rb', line 505

def expand_place(suffix, value)
  tokens = split_tokens(value)
  [["align-#{suffix}", tokens[0]], ["justify-#{suffix}", tokens[1] || tokens[0]]]
end

.expand_text_decoration(value) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/dommy/internal/css/property_registry.rb', line 329

def expand_text_decoration(value)
  lines = []
  style = nil
  color = nil
  split_tokens(value).each do |token|
    low = token.downcase
    if TEXT_DECORATION_LINES.include?(low)
      lines << token
    elsif TEXT_DECORATION_STYLES.include?(low)
      style = token
    else
      color ||= token
    end
  end
  [["text-decoration-line", lines.empty? ? "none" : lines.join(" ")],
   ["text-decoration-style", style || "solid"],
   ["text-decoration-color", color || "currentColor"]]
end

.expansion_targets(name) ⇒ Object



126
# File 'lib/dommy/internal/css/property_registry.rb', line 126

def expansion_targets(name) = EXPANSION_TARGETS.fetch(name, [name])

.flex_one(token) ⇒ Object



449
450
451
452
453
454
455
456
# File 'lib/dommy/internal/css/property_registry.rb', line 449

def flex_one(token)
  case token.downcase
  when "none" then ["0", "0", "auto"]
  when "auto" then ["1", "1", "auto"]
  when "initial" then ["0", "1", "auto"]
  else number?(token) ? [token, "1", "0%"] : ["1", "1", token]
  end
end

.flex_two(first, second) ⇒ Object



458
459
460
# File 'lib/dommy/internal/css/property_registry.rb', line 458

def flex_two(first, second)
  number?(second) ? [first, second, "0%"] : [first, "1", second]
end

.format_number(number) ⇒ Object

A unitless calc result (e.g. line-height: calc(1 + 0.5)), serialized without a unit and without a trailing ".0".



38
39
40
41
# File 'lib/dommy/internal/css/calc.rb', line 38

def format_number(number)
  rounded = number.round(5)
  rounded == rounded.to_i ? rounded.to_i.to_s : rounded.to_s
end

.format_px(number) ⇒ Object



262
263
264
265
# File 'lib/dommy/internal/css/property_registry.rb', line 262

def format_px(number)
  rounded = number.round(3)
  rounded == rounded.to_i ? "#{rounded.to_i}px" : "#{rounded}px"
end

.inherited?(name) ⇒ Boolean

Custom properties inherit by default (css-variables-1 §2); their initial value is the guaranteed-invalid value (nil here).

Returns:

  • (Boolean)


130
# File 'lib/dommy/internal/css/property_registry.rb', line 130

def inherited?(name) = name.start_with?("--") || PROPERTIES[name]&.inherited || false

.initial(name) ⇒ Object



132
# File 'lib/dommy/internal/css/property_registry.rb', line 132

def initial(name) = PROPERTIES[name]&.initial

.known?(name) ⇒ Boolean

Returns:

  • (Boolean)


124
# File 'lib/dommy/internal/css/property_registry.rb', line 124

def known?(name) = PROPERTIES.key?(name)

.number?(token) ⇒ Boolean

Returns:

  • (Boolean)


462
# File 'lib/dommy/internal/css/property_registry.rb', line 462

def number?(token) = token.match?(/\A-?\d+(?:\.\d+)?\z/)

.parse_border_shorthand(value) ⇒ Object

Classify the tokens of a border/border-<side>/outline value into style:, color: (order-independent, omitted parts nil).



384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/dommy/internal/css/property_registry.rb', line 384

def parse_border_shorthand(value)
  parts = {width: nil, style: nil, color: nil}
  split_tokens(value).each do |token|
    low = token.downcase
    if parts[:style].nil? && BORDER_STYLE_KEYWORDS.include?(low)
      parts[:style] = token
    elsif parts[:width].nil? && border_width_token?(low)
      parts[:width] = token
    else
      parts[:color] ||= token
    end
  end
  parts
end

.percentage_of(value, base) ⇒ Object

px for a "%" value against base px, or nil when not a percentage (or no base available).



228
229
230
231
232
233
# File 'lib/dommy/internal/css/property_registry.rb', line 228

def percentage_of(value, base)
  return nil unless base

  match = value.to_s.strip.match(/\A(-?\d+(?:\.\d+)?)%\z/)
  match && format_px(match[1].to_f / 100.0 * base)
end

.resolve_length(value, **ctx) ⇒ Object

Resolve a bare "" length to a px string, or nil when it isn't a single resolvable length. See resolve_length_px for the units.



237
238
239
240
# File 'lib/dommy/internal/css/property_registry.rb', line 237

def resolve_length(value, **ctx)
  px = resolve_length_px(value, **ctx)
  px && format_px(px)
end

.resolve_length_px(value, font_size:, root_font_size:, viewport_width: nil, viewport_height: nil) ⇒ Object

The px magnitude (Float) of a single "" length, or nil when the value isn't such a length or its base is unavailable. Handles font-relative (em/rem), absolute (px/pt/pc/in/cm/mm/Q) and viewport (vw/vh/vmin/vmax) units — everything resolvable without layout.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/dommy/internal/css/property_registry.rb', line 246

def resolve_length_px(value, font_size:, root_font_size:, viewport_width: nil, viewport_height: nil)
  match = value.to_s.match(LENGTH_PATTERN)
  return nil unless match

  number = match[1].to_f
  case match[2].downcase
  when "em" then font_size && number * font_size
  when "rem" then root_font_size && number * root_font_size
  when "vw" then viewport_width && number * viewport_width / 100.0
  when "vh" then viewport_height && number * viewport_height / 100.0
  when "vmin" then viewport_width && viewport_height && number * [viewport_width, viewport_height].min / 100.0
  when "vmax" then viewport_width && viewport_height && number * [viewport_width, viewport_height].max / 100.0
  else number * ABSOLUTE_UNIT_PX[match[2].downcase]
  end
end

.split_tokens(value) ⇒ Object

Whitespace split that keeps parenthesized groups intact, so a color rgb(0, 0, 0) or an url(...) stays one token.



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/dommy/internal/css/property_registry.rb', line 350

def split_tokens(value)
  tokens = []
  current = +""
  depth = 0
  value.each_char do |char|
    if char == "("
      depth += 1
      current << char
    elsif char == ")"
      depth -= 1 if depth.positive?
      current << char
    elsif char.match?(/\s/) && depth.zero?
      tokens << current unless current.empty?
      current = +""
    else
      current << char
    end
  end
  tokens << current unless current.empty?
  tokens
end