view_component_atomix 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +744 -0
- data/lib/view_component/atomix/accessors.rb +129 -0
- data/lib/view_component/atomix/attribute_helpers.rb +15 -0
- data/lib/view_component/atomix/attrs.rb +17 -0
- data/lib/view_component/atomix/base.rb +226 -0
- data/lib/view_component/atomix/builder.rb +77 -0
- data/lib/view_component/atomix/compiler.rb +147 -0
- data/lib/view_component/atomix/condition_resolver.rb +79 -0
- data/lib/view_component/atomix/configuration.rb +103 -0
- data/lib/view_component/atomix/declaration_queue.rb +50 -0
- data/lib/view_component/atomix/dynamic_properties/literal_key.rb +68 -0
- data/lib/view_component/atomix/dynamic_properties/source_cache.rb +31 -0
- data/lib/view_component/atomix/dynamic_properties/source_parser.rb +61 -0
- data/lib/view_component/atomix/dynamic_properties.rb +75 -0
- data/lib/view_component/atomix/dynamic_style.rb +109 -0
- data/lib/view_component/atomix/enumerated_style.rb +39 -0
- data/lib/view_component/atomix/hashing.rb +47 -0
- data/lib/view_component/atomix/helper.rb +31 -0
- data/lib/view_component/atomix/immutable.rb +20 -0
- data/lib/view_component/atomix/known_properties.rb +102 -0
- data/lib/view_component/atomix/owner_state.rb +48 -0
- data/lib/view_component/atomix/property.rb +183 -0
- data/lib/view_component/atomix/railtie.rb +46 -0
- data/lib/view_component/atomix/registry.rb +208 -0
- data/lib/view_component/atomix/resolution_session.rb +81 -0
- data/lib/view_component/atomix/rule.rb +42 -0
- data/lib/view_component/atomix/sheet.rb +98 -0
- data/lib/view_component/atomix/style.rb +80 -0
- data/lib/view_component/atomix/tasks/build.rake +17 -0
- data/lib/view_component/atomix/theme_compiler.rb +19 -0
- data/lib/view_component/atomix/theme_resolution.rb +66 -0
- data/lib/view_component/atomix/trait_classifier.rb +111 -0
- data/lib/view_component/atomix/vars.rb +97 -0
- data/lib/view_component/atomix/version.rb +7 -0
- data/lib/view_component/atomix/weak_collection.rb +62 -0
- data/lib/view_component/atomix.rb +301 -0
- data/lib/view_component_atomix.rb +3 -0
- metadata +162 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
module Property
|
|
6
|
+
KNOWN = %w[
|
|
7
|
+
accent-color align-content align-items align-self alignment-baseline all anchor-name anchor-scope
|
|
8
|
+
animation animation-composition animation-delay animation-direction animation-duration
|
|
9
|
+
animation-fill-mode animation-iteration-count animation-name animation-play-state
|
|
10
|
+
animation-range animation-range-end animation-range-start animation-timeline
|
|
11
|
+
animation-timing-function appearance aspect-ratio backdrop-filter backface-visibility
|
|
12
|
+
background background-attachment background-blend-mode background-clip background-color
|
|
13
|
+
background-image background-origin background-position background-position-x
|
|
14
|
+
background-position-y background-repeat background-size baseline-shift baseline-source
|
|
15
|
+
block-size border border-block border-block-color border-block-end border-block-end-color
|
|
16
|
+
border-block-end-style border-block-end-width border-block-start border-block-start-color
|
|
17
|
+
border-block-start-style border-block-start-width border-block-style border-block-width
|
|
18
|
+
border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius
|
|
19
|
+
border-bottom-style border-bottom-width border-collapse border-color border-end-end-radius
|
|
20
|
+
border-end-start-radius border-image border-image-outset border-image-repeat
|
|
21
|
+
border-image-slice border-image-source border-image-width border-inline border-inline-color
|
|
22
|
+
border-inline-end border-inline-end-color border-inline-end-style border-inline-end-width
|
|
23
|
+
border-inline-start border-inline-start-color border-inline-start-style
|
|
24
|
+
border-inline-start-width border-inline-style border-inline-width border-left
|
|
25
|
+
border-left-color border-left-style border-left-width border-radius border-right
|
|
26
|
+
border-right-color border-right-style border-right-width border-spacing
|
|
27
|
+
border-start-end-radius border-start-start-radius border-style border-top border-top-color
|
|
28
|
+
border-top-left-radius border-top-right-radius border-top-style border-top-width
|
|
29
|
+
border-width bottom box-decoration-break box-shadow box-sizing break-after break-before
|
|
30
|
+
break-inside caption-side caret caret-color caret-shape clear clip clip-path clip-rule
|
|
31
|
+
color color-interpolation color-interpolation-filters color-scheme column-count column-fill
|
|
32
|
+
column-gap column-rule column-rule-color column-rule-style column-rule-width column-span
|
|
33
|
+
column-width columns contain contain-intrinsic-block-size contain-intrinsic-height
|
|
34
|
+
contain-intrinsic-inline-size contain-intrinsic-size contain-intrinsic-width container
|
|
35
|
+
container-name container-type content content-visibility corner-shape counter-increment
|
|
36
|
+
counter-reset counter-set cursor direction display dominant-baseline empty-cells
|
|
37
|
+
field-sizing fill fill-opacity fill-rule filter flex flex-basis flex-direction flex-flow
|
|
38
|
+
flex-grow flex-shrink flex-wrap float flood-color flood-opacity font font-family
|
|
39
|
+
font-feature-settings font-kerning font-language-override font-optical-sizing font-palette
|
|
40
|
+
font-size font-size-adjust font-stretch font-style font-synthesis font-synthesis-small-caps
|
|
41
|
+
font-synthesis-style font-synthesis-weight font-variant font-variant-alternates
|
|
42
|
+
font-variant-caps font-variant-east-asian font-variant-emoji font-variant-ligatures
|
|
43
|
+
font-variant-numeric font-variant-position font-variation-settings font-weight
|
|
44
|
+
forced-color-adjust gap grid grid-area grid-auto-columns grid-auto-flow grid-auto-rows
|
|
45
|
+
grid-column grid-column-end grid-column-gap grid-column-start grid-gap grid-row grid-row-end
|
|
46
|
+
grid-row-gap grid-row-start grid-template grid-template-areas grid-template-columns
|
|
47
|
+
grid-template-rows hanging-punctuation height hyphenate-character hyphenate-limit-chars
|
|
48
|
+
hyphens image-orientation image-rendering initial-letter inline-size inset inset-block
|
|
49
|
+
inset-block-end inset-block-start inset-inline inset-inline-end inset-inline-start
|
|
50
|
+
interpolate-size isolation justify-content justify-items justify-self left letter-spacing
|
|
51
|
+
lighting-color line-break line-clamp line-height list-style list-style-image
|
|
52
|
+
list-style-position list-style-type margin margin-block margin-block-end margin-block-start
|
|
53
|
+
margin-bottom margin-inline margin-inline-end margin-inline-start margin-left margin-right
|
|
54
|
+
margin-top margin-trim marker marker-end marker-mid marker-start mask mask-border
|
|
55
|
+
mask-border-mode mask-border-outset mask-border-repeat mask-border-slice mask-border-source
|
|
56
|
+
mask-border-width mask-clip mask-composite mask-image mask-mode mask-origin mask-position
|
|
57
|
+
mask-repeat mask-size mask-type math-depth math-shift math-style max-block-size max-height
|
|
58
|
+
max-inline-size max-width min-block-size min-height min-inline-size min-width mix-blend-mode
|
|
59
|
+
object-fit object-position offset offset-anchor offset-distance offset-path offset-position
|
|
60
|
+
offset-rotate opacity order orphans outline outline-color outline-offset outline-style
|
|
61
|
+
outline-width overflow overflow-anchor overflow-block overflow-clip-margin overflow-inline
|
|
62
|
+
overflow-wrap overflow-x overflow-y overlay overscroll-behavior overscroll-behavior-block
|
|
63
|
+
overscroll-behavior-inline overscroll-behavior-x overscroll-behavior-y padding padding-block
|
|
64
|
+
padding-block-end padding-block-start padding-bottom padding-inline padding-inline-end
|
|
65
|
+
padding-inline-start padding-left padding-right padding-top page page-break-after
|
|
66
|
+
page-break-before page-break-inside paint-order perspective perspective-origin place-content
|
|
67
|
+
place-items place-self pointer-events position position-anchor position-area position-try
|
|
68
|
+
position-try-fallbacks position-try-order position-visibility print-color-adjust quotes
|
|
69
|
+
resize right rotate row-gap ruby-align ruby-position scale scroll-behavior
|
|
70
|
+
scroll-margin scroll-margin-block scroll-margin-block-end scroll-margin-block-start
|
|
71
|
+
scroll-margin-bottom scroll-margin-inline scroll-margin-inline-end scroll-margin-inline-start
|
|
72
|
+
scroll-margin-left scroll-margin-right scroll-margin-top scroll-padding scroll-padding-block
|
|
73
|
+
scroll-padding-block-end scroll-padding-block-start scroll-padding-bottom
|
|
74
|
+
scroll-padding-inline scroll-padding-inline-end scroll-padding-inline-start
|
|
75
|
+
scroll-padding-left scroll-padding-right scroll-padding-top scroll-snap-align
|
|
76
|
+
scroll-snap-stop scroll-snap-type scroll-timeline scroll-timeline-axis scroll-timeline-name
|
|
77
|
+
scrollbar-color scrollbar-gutter scrollbar-width shape-image-threshold shape-margin
|
|
78
|
+
shape-outside shape-rendering stop-color stop-opacity stroke stroke-dasharray
|
|
79
|
+
stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity
|
|
80
|
+
stroke-width tab-size table-layout text-align text-align-last text-anchor text-autospace
|
|
81
|
+
text-box text-box-edge text-box-trim text-combine-upright text-decoration
|
|
82
|
+
text-decoration-color text-decoration-line text-decoration-skip text-decoration-skip-ink
|
|
83
|
+
text-decoration-style text-decoration-thickness text-emphasis text-emphasis-color
|
|
84
|
+
text-emphasis-position text-emphasis-style text-indent text-justify text-orientation
|
|
85
|
+
text-overflow text-rendering text-shadow text-size-adjust text-spacing-trim text-transform
|
|
86
|
+
text-underline-offset text-underline-position text-wrap text-wrap-mode text-wrap-style top
|
|
87
|
+
touch-action transform transform-box transform-origin transform-style transition
|
|
88
|
+
transition-behavior transition-delay transition-duration transition-property
|
|
89
|
+
transition-timing-function translate unicode-bidi user-select vector-effect vertical-align
|
|
90
|
+
view-timeline view-timeline-axis view-timeline-inset view-timeline-name view-transition-class
|
|
91
|
+
view-transition-name visibility white-space white-space-collapse widows width will-change
|
|
92
|
+
word-break word-spacing word-wrap writing-mode z-index zoom
|
|
93
|
+
].to_set.freeze
|
|
94
|
+
|
|
95
|
+
module_function
|
|
96
|
+
|
|
97
|
+
def known?(property)
|
|
98
|
+
KNOWN.include?(property) || property.start_with?("--", "-webkit-", "-moz-", "-ms-", "-o-")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
# Resolution state is resettable. Installed accessor names and mixin
|
|
6
|
+
# metadata belong to Accessors and survive resets. Unload is terminal for
|
|
7
|
+
# this owner; Rails creates a new owner when it loads the constant again.
|
|
8
|
+
class OwnerState
|
|
9
|
+
attr_accessor :styles_own, :styles_resolved, :styles_breakpoints, :styles_resolving, :variables_resolving,
|
|
10
|
+
:theme_own, :theme_resolved, :theme_resolving, :companion_styles_module
|
|
11
|
+
attr_reader :styles, :variables, :themes
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
clear
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset!
|
|
18
|
+
ResolutionSession.stale!
|
|
19
|
+
clear
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unload!
|
|
23
|
+
@unloaded = true
|
|
24
|
+
invalidate!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def unloaded?
|
|
28
|
+
@unloaded == true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def invalidate!
|
|
32
|
+
ResolutionSession.stale!
|
|
33
|
+
@styles_resolved = @theme_resolved = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def clear
|
|
39
|
+
@styles_own = @styles_resolved = @styles_breakpoints = nil
|
|
40
|
+
@styles_resolving = @variables_resolving = @theme_resolving = false
|
|
41
|
+
@theme_own = @theme_resolved = @companion_styles_module = nil
|
|
42
|
+
@styles = DeclarationQueue.new
|
|
43
|
+
@variables = DeclarationQueue.new
|
|
44
|
+
@themes = DeclarationQueue.new
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
module Property
|
|
6
|
+
SHORTHANDS_OF_SHORTHANDS = %w[
|
|
7
|
+
animation background border font grid grid-area
|
|
8
|
+
inset margin mask padding scroll-margin scroll-padding
|
|
9
|
+
].to_set.freeze
|
|
10
|
+
|
|
11
|
+
TIME_PROPERTIES = %w[
|
|
12
|
+
animation-delay animation-duration transition-delay transition-duration voice-duration
|
|
13
|
+
].to_set.freeze
|
|
14
|
+
|
|
15
|
+
ADDITIONAL_UNITLESS = %w[
|
|
16
|
+
-webkit-line-clamp counter-set counter-reset hyphenate-limit-chars
|
|
17
|
+
mask-border-outset mask-border-slice mask-border-width rotate
|
|
18
|
+
shape-image-threshold math-depth
|
|
19
|
+
].to_set.freeze
|
|
20
|
+
|
|
21
|
+
# These shorthands sit between their parent shorthand and other shorthands.
|
|
22
|
+
INTERMEDIATE_SHORTHANDS = %w[border-block border-inline grid-template].to_set.freeze
|
|
23
|
+
|
|
24
|
+
SHORTHANDS_OF_LONGHANDS = %w[
|
|
25
|
+
animation-range background-position border-block-color border-block-end border-block-start
|
|
26
|
+
border-block-style border-block-width border-bottom border-color
|
|
27
|
+
border-image border-inline-color border-inline-end border-inline-start border-inline-style
|
|
28
|
+
border-inline-width border-left border-radius border-right border-style border-top border-width
|
|
29
|
+
caret column-rule columns
|
|
30
|
+
contain-intrinsic-size container corner-shape flex flex-flow font-synthesis font-variant gap grid-column
|
|
31
|
+
grid-gap grid-row inset-block inset-inline list-style margin-block
|
|
32
|
+
margin-inline mask-border offset outline overflow overscroll-behavior padding-block
|
|
33
|
+
padding-inline place-content place-items place-self position-try scroll-margin-block scroll-margin-inline
|
|
34
|
+
scroll-padding-block scroll-padding-inline scroll-timeline text-box text-decoration
|
|
35
|
+
text-emphasis text-wrap transition view-timeline white-space
|
|
36
|
+
].to_set.freeze
|
|
37
|
+
|
|
38
|
+
UNITLESS = %w[
|
|
39
|
+
animation-iteration-count aspect-ratio border-image-outset border-image-slice
|
|
40
|
+
border-image-width box-flex box-flex-group box-ordinal-group column-count columns fill-opacity
|
|
41
|
+
flex flex-grow flex-negative flex-order flex-positive flex-shrink flood-opacity font-weight
|
|
42
|
+
grid-area grid-column grid-column-end grid-column-span grid-column-start grid-row
|
|
43
|
+
grid-row-end grid-row-span grid-row-start line-clamp line-height opacity order orphans
|
|
44
|
+
scale stop-opacity stroke-dasharray stroke-dashoffset stroke-miterlimit stroke-opacity
|
|
45
|
+
stroke-width tab-size widows z-index zoom
|
|
46
|
+
].to_set.freeze
|
|
47
|
+
|
|
48
|
+
AT_RULE_PRIORITIES = {
|
|
49
|
+
"@supports" => 30,
|
|
50
|
+
"@media" => 200,
|
|
51
|
+
"@container" => 300
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
PSEUDO_CLASS_PRIORITIES = {
|
|
55
|
+
":is" => 40, ":where" => 40, ":not" => 40, ":has" => 45, ":dir" => 50, ":lang" => 51,
|
|
56
|
+
":first-child" => 52, ":first-of-type" => 53, ":last-child" => 54, ":last-of-type" => 55,
|
|
57
|
+
":only-child" => 56, ":only-of-type" => 57, ":nth-child" => 60, ":nth-last-child" => 61,
|
|
58
|
+
":nth-of-type" => 62, ":nth-last-of-type" => 63, ":empty" => 70, ":link" => 80,
|
|
59
|
+
":any-link" => 81, ":local-link" => 82, ":target-within" => 83, ":target" => 84,
|
|
60
|
+
":visited" => 85, ":enabled" => 91, ":disabled" => 92, ":required" => 93, ":optional" => 94,
|
|
61
|
+
":read-only" => 95, ":read-write" => 96, ":placeholder-shown" => 97, ":in-range" => 98,
|
|
62
|
+
":out-of-range" => 99, ":default" => 100, ":checked" => 101, ":indeterminate" => 101,
|
|
63
|
+
":blank" => 102, ":valid" => 103, ":invalid" => 104, ":user-invalid" => 105,
|
|
64
|
+
":autofill" => 110, ":picture-in-picture" => 120, ":modal" => 121, ":fullscreen" => 122,
|
|
65
|
+
":paused" => 123, ":playing" => 124, ":current" => 125, ":past" => 126, ":future" => 127,
|
|
66
|
+
":hover" => 130, ":focus-within" => 140, ":focus" => 150, ":focus-visible" => 160,
|
|
67
|
+
":active" => 170
|
|
68
|
+
}.freeze
|
|
69
|
+
|
|
70
|
+
DEFAULT_PSEUDO_CLASS_PRIORITY = 40
|
|
71
|
+
PSEUDO_ELEMENT_PRIORITY = 5000
|
|
72
|
+
LOGICAL_PATTERN = /(^|-)(block|inline|start|end)(-|$)/
|
|
73
|
+
|
|
74
|
+
STRUCTURAL_CHARACTERS = %r{[;{}()\[\]"'\\]|/\*}
|
|
75
|
+
PLAIN_TEXT = %r{[^;{}()\[\]"'\\/]+}
|
|
76
|
+
ESCAPE = /\\(?:\h{1,6}(?:\r\n|[ \t\n\r\f])?|.)/m
|
|
77
|
+
DOUBLE_QUOTED = /"(?:[^"\\\n\r\f]|\\.)*"/m
|
|
78
|
+
SINGLE_QUOTED = /'(?:[^'\\\n\r\f]|\\.)*'/m
|
|
79
|
+
COMMENT = %r{/\*.*?\*/}m
|
|
80
|
+
NAME = /\A(?:[a-zA-Z0-9_\-\x00]|[^\x00-\x7F])*\z/
|
|
81
|
+
URL_NAME = /(?<![a-zA-Z0-9_\-\x00]|[^\x00-\x7F])url\z/i
|
|
82
|
+
SPLIT_URL_NAME = /(?:[#@]|<!--)url\z/i
|
|
83
|
+
QUOTED_URL = /\([ \t\n\r\f]*["']/
|
|
84
|
+
UNQUOTED_URL = /\([ \t\n\r\f]*(?:[^"'()\\\x00-\x20\x7F]|\\[^\n\r\f])*[ \t\n\r\f]*\)/
|
|
85
|
+
CLOSERS = { "(" => ")", "[" => "]", "{" => "}" }.freeze
|
|
86
|
+
|
|
87
|
+
module_function
|
|
88
|
+
|
|
89
|
+
def normalize_key(key)
|
|
90
|
+
name = key.to_s
|
|
91
|
+
return name if name.start_with?("--")
|
|
92
|
+
|
|
93
|
+
name.gsub(/([a-z\d])([A-Z])/, '\1-\2').tr("_", "-").downcase
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def normalize_value(property, value)
|
|
97
|
+
case value
|
|
98
|
+
when Array then value.map { |v| normalize_value(property, v) }
|
|
99
|
+
when Integer, Float
|
|
100
|
+
return value.to_s if property.start_with?("--") || ADDITIONAL_UNITLESS.include?(property)
|
|
101
|
+
return "#{value}ms" if TIME_PROPERTIES.include?(property)
|
|
102
|
+
return "0" if value.zero?
|
|
103
|
+
UNITLESS.include?(property) ? value.to_s : "#{value}px"
|
|
104
|
+
when Symbol then validate_value!(property, value.to_s.tr("_", "-"))
|
|
105
|
+
else validate_value!(property, value.to_s)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_value!(property, value)
|
|
110
|
+
return value unless value.match?(STRUCTURAL_CHARACTERS)
|
|
111
|
+
return value if self_contained?(value)
|
|
112
|
+
|
|
113
|
+
raise ArgumentError,
|
|
114
|
+
"Unsafe CSS value for #{property}: #{value.inspect}. A value cannot contain a top-level ';' " \
|
|
115
|
+
"or unbalanced quotes, brackets or comments."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self_contained?(value)
|
|
119
|
+
open = []
|
|
120
|
+
scanner = StringScanner.new(value)
|
|
121
|
+
word = nil
|
|
122
|
+
escaped = false
|
|
123
|
+
until scanner.eos?
|
|
124
|
+
if (text = scanner.scan(PLAIN_TEXT))
|
|
125
|
+
word = text
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
if scanner.skip(ESCAPE)
|
|
129
|
+
word = nil
|
|
130
|
+
escaped = true
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
url = scanner.match?(/\(/) && url_token(scanner, word.to_s, escaped)
|
|
135
|
+
word = nil
|
|
136
|
+
escaped = false
|
|
137
|
+
return false if url == :malformed
|
|
138
|
+
next if url == :consumed || scanner.skip(DOUBLE_QUOTED) || scanner.skip(SINGLE_QUOTED) || scanner.skip(COMMENT)
|
|
139
|
+
|
|
140
|
+
if scanner.match?(%r{/\*}) then return false
|
|
141
|
+
elsif scanner.skip(%r{/}) then next
|
|
142
|
+
elsif (bracket = scanner.scan(/[(\[{]/)) then open << CLOSERS.fetch(bracket)
|
|
143
|
+
elsif (bracket = scanner.scan(/[)\]}]/)) then return false unless open.pop == bracket
|
|
144
|
+
elsif scanner.skip(/;/) then return false if open.empty?
|
|
145
|
+
else return false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
open.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def url_token(scanner, name, escaped)
|
|
152
|
+
return :malformed if (escaped && name.match?(NAME)) || name.match?(SPLIT_URL_NAME)
|
|
153
|
+
return unless name.match?(URL_NAME) && !scanner.match?(QUOTED_URL)
|
|
154
|
+
|
|
155
|
+
scanner.skip(UNQUOTED_URL) ? :consumed : :malformed
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def base_priority(property)
|
|
159
|
+
return 500 if property == "all"
|
|
160
|
+
return 1000 if SHORTHANDS_OF_SHORTHANDS.include?(property)
|
|
161
|
+
return 1500 if INTERMEDIATE_SHORTHANDS.include?(property)
|
|
162
|
+
return 2000 if SHORTHANDS_OF_LONGHANDS.include?(property)
|
|
163
|
+
return 3000 if property.match?(LOGICAL_PATTERN)
|
|
164
|
+
|
|
165
|
+
4000
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def condition_priority(condition)
|
|
169
|
+
if condition.start_with?("@")
|
|
170
|
+
AT_RULE_PRIORITIES.fetch(condition.split(/[\s(]/, 2).first, AT_RULE_PRIORITIES["@media"])
|
|
171
|
+
elsif condition.start_with?("::")
|
|
172
|
+
PSEUDO_ELEMENT_PRIORITY
|
|
173
|
+
else
|
|
174
|
+
PSEUDO_CLASS_PRIORITIES.fetch(condition.split("(", 2).first, DEFAULT_PSEUDO_CLASS_PRIORITY)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def priority(property, conditions = [])
|
|
179
|
+
conditions.sum(base_priority(property)) { |c| condition_priority(c) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module ViewComponent
|
|
6
|
+
module Atomix
|
|
7
|
+
class Railtie < Rails::Railtie
|
|
8
|
+
config.before_configuration do |app|
|
|
9
|
+
app.config.view_component.atomix = ViewComponent::Atomix.config
|
|
10
|
+
ViewComponent::Atomix.config.inline = !Rails.env.production?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "view_component.atomix.autoload_roots", before: :set_load_path do |app|
|
|
14
|
+
ViewComponent::Atomix.config.autoload_roots.each do |relative|
|
|
15
|
+
app.config.paths.add relative.to_s, eager_load: true
|
|
16
|
+
dir = app.root.join(relative.to_s).to_s
|
|
17
|
+
next unless File.directory?(dir)
|
|
18
|
+
|
|
19
|
+
effective = app.config.respond_to?(:all_eager_load_paths) ? app.config.all_eager_load_paths : app.config.eager_load_paths
|
|
20
|
+
app.config.eager_load_paths << dir unless effective.include?(dir)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
initializer "view_component.atomix.helper" do
|
|
25
|
+
ActiveSupport.on_load(:action_view) { include ViewComponent::Atomix::Helper }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
initializer "view_component.atomix.reloader" do
|
|
29
|
+
Rails.autoloaders.main.on_unload do |_name, owner, source|
|
|
30
|
+
ViewComponent::Atomix.unload!(owner, source)
|
|
31
|
+
end
|
|
32
|
+
ActiveSupport::Reloader.before_class_unload do
|
|
33
|
+
ViewComponent::Atomix.registry.clear_reloadable
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
config.to_prepare do
|
|
38
|
+
ViewComponent::Atomix.eager_load! if ViewComponent::Atomix.config.inline?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
rake_tasks do
|
|
42
|
+
load File.expand_path("tasks/build.rake", __dir__)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
class Registry
|
|
6
|
+
Registration = Struct.new(:rule, :source, keyword_init: true)
|
|
7
|
+
RootReplacement = Struct.new(:property, :source, keyword_init: true)
|
|
8
|
+
Transaction = Struct.new(:registrations, :callbacks, keyword_init: true) do
|
|
9
|
+
def initialize(registrations: [], callbacks: [])
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def absorb(other)
|
|
14
|
+
registrations.concat(other.registrations)
|
|
15
|
+
callbacks.concat(other.callbacks)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
@rules = {}
|
|
22
|
+
@root_rules = {}
|
|
23
|
+
@css = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add(rule)
|
|
27
|
+
entry = Registration.new(rule: rule, source: registration_source)
|
|
28
|
+
if (pending = transactions.last)
|
|
29
|
+
pending.registrations << entry
|
|
30
|
+
else
|
|
31
|
+
commit(Transaction.new(registrations: [entry]))
|
|
32
|
+
end
|
|
33
|
+
rule
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def after_commit(&block)
|
|
37
|
+
if (pending = transactions.last)
|
|
38
|
+
pending.callbacks << block
|
|
39
|
+
else
|
|
40
|
+
block.call
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Record the replacement in declaration order, in the same transaction as
|
|
45
|
+
# its new rules. Empty assignments must also remove the old branches.
|
|
46
|
+
def replace_root(property)
|
|
47
|
+
entry = RootReplacement.new(property: property, source: registration_source)
|
|
48
|
+
if (pending = transactions.last)
|
|
49
|
+
pending.registrations << entry
|
|
50
|
+
else
|
|
51
|
+
commit(Transaction.new(registrations: [entry]))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Nested operations join their declaration. A separately resolved owner
|
|
56
|
+
# uses independent: true because its successful result is cached even if
|
|
57
|
+
# the declaration which requested it subsequently fails.
|
|
58
|
+
def transaction(independent: false)
|
|
59
|
+
pending = Transaction.new
|
|
60
|
+
transactions << pending
|
|
61
|
+
result = yield
|
|
62
|
+
if transactions.length > 1 && !independent
|
|
63
|
+
transactions[-2].absorb(pending)
|
|
64
|
+
else
|
|
65
|
+
commit(pending)
|
|
66
|
+
end
|
|
67
|
+
result
|
|
68
|
+
ensure
|
|
69
|
+
stack = transactions
|
|
70
|
+
stack.pop
|
|
71
|
+
Thread.current[:view_component_atomix_transactions].delete(object_id) if stack.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def with_source(path)
|
|
75
|
+
previous = Thread.current[:view_component_atomix_source]
|
|
76
|
+
Thread.current[:view_component_atomix_source] = path
|
|
77
|
+
yield
|
|
78
|
+
ensure
|
|
79
|
+
Thread.current[:view_component_atomix_source] = previous
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def clear_reloadable
|
|
83
|
+
roots = reloadable_roots
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
@rules.delete_if do |_, sources|
|
|
86
|
+
sources.delete_if { |source| source && roots.any? { |root| source.start_with?(root) } }
|
|
87
|
+
sources.empty?
|
|
88
|
+
end
|
|
89
|
+
@root_rules.each_value { |rules| rules.select! { |rule| @rules.key?(rule) } }
|
|
90
|
+
@root_rules.delete_if { |_, rules| rules.empty? }
|
|
91
|
+
@css = nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def clear
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
@rules.clear
|
|
98
|
+
@root_rules.clear
|
|
99
|
+
@css = nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def rules
|
|
104
|
+
@mutex.synchronize { @rules.keys }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def size
|
|
108
|
+
@mutex.synchronize { @rules.size }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_css
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
@css ||= build_css(@rules.keys)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def transactions
|
|
120
|
+
Thread.current[:view_component_atomix_transactions] ||= {}
|
|
121
|
+
Thread.current[:view_component_atomix_transactions][object_id] ||= []
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def commit(transaction)
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
transaction.registrations.each do |entry|
|
|
127
|
+
if entry.is_a?(RootReplacement)
|
|
128
|
+
release_root(entry.property, entry.source)
|
|
129
|
+
else
|
|
130
|
+
register(entry.rule, entry.source)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
@css = nil unless transaction.registrations.empty?
|
|
134
|
+
end
|
|
135
|
+
transaction.callbacks.each(&:call)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def register(rule, source)
|
|
139
|
+
unless rule.selector == ":root"
|
|
140
|
+
(@rules[rule] ||= Set.new).add(source)
|
|
141
|
+
return
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
release_root(rule.property, source) { |existing| existing.conditions == rule.conditions }
|
|
145
|
+
@rules[rule] = (@rules.delete(rule) || Set.new).add(source)
|
|
146
|
+
(@root_rules[rule.property] ||= Set.new).add(rule)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Root variables are assignments, unlike content-addressed atoms. Remove
|
|
150
|
+
# only this source's previous assignment; other owners may still use it.
|
|
151
|
+
def release_root(property, source)
|
|
152
|
+
rules = @root_rules[property]
|
|
153
|
+
return unless rules
|
|
154
|
+
|
|
155
|
+
rules.delete_if do |rule|
|
|
156
|
+
next false if block_given? && !yield(rule)
|
|
157
|
+
|
|
158
|
+
sources = @rules.fetch(rule)
|
|
159
|
+
sources.delete(source)
|
|
160
|
+
next false unless sources.empty?
|
|
161
|
+
|
|
162
|
+
@rules.delete(rule)
|
|
163
|
+
true
|
|
164
|
+
end
|
|
165
|
+
@root_rules.delete(property) if rules.empty?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def reloadable_roots
|
|
169
|
+
return [] unless defined?(Rails) && Rails.respond_to?(:autoloaders) && Rails.application
|
|
170
|
+
|
|
171
|
+
Rails.autoloaders.main.dirs.map { |dir| "#{File.expand_path(dir)}/" }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def registration_source
|
|
175
|
+
source = Thread.current[:view_component_atomix_source]
|
|
176
|
+
return File.expand_path(source) if source
|
|
177
|
+
|
|
178
|
+
roots = reloadable_roots
|
|
179
|
+
caller_locations.each do |location|
|
|
180
|
+
path = File.expand_path(location.absolute_path || location.path)
|
|
181
|
+
return path if roots.any? { |root| path.start_with?(root) }
|
|
182
|
+
end
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def build_css(rules)
|
|
187
|
+
sorted = rules.sort_by.with_index { |rule, i| [rule.priority, i] }
|
|
188
|
+
|
|
189
|
+
unless Atomix.config.use_layers
|
|
190
|
+
return sorted.map(&:to_css).join("\n")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
groups = sorted.group_by(&:priority)
|
|
194
|
+
return "" if groups.empty?
|
|
195
|
+
|
|
196
|
+
header = "@layer #{groups.keys.map { |p| layer_name(p) }.join(',')};"
|
|
197
|
+
body = groups.map do |priority, group|
|
|
198
|
+
"@layer #{layer_name(priority)}{\n#{group.map(&:to_css).join("\n")}\n}"
|
|
199
|
+
end
|
|
200
|
+
[header, *body].join("\n")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def layer_name(priority)
|
|
204
|
+
"#{Atomix.config.class_prefix}#{priority}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
# One logical read, including nested owners, queue drains and cache rebuilds.
|
|
6
|
+
# Each frame tracks declarations made during its work (stale) and reads of
|
|
7
|
+
# work that has not finished yet (provisional). Neither may publish a cache.
|
|
8
|
+
class ResolutionSession
|
|
9
|
+
MAX_DEPTH = 64
|
|
10
|
+
MAX_STABILITY_PASSES = 32
|
|
11
|
+
Frame = Struct.new(:provisional, :stale)
|
|
12
|
+
|
|
13
|
+
def self.with
|
|
14
|
+
existing = Thread.current[:view_component_atomix_resolution]
|
|
15
|
+
return yield existing if existing
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
session = new
|
|
19
|
+
Thread.current[:view_component_atomix_resolution] = session
|
|
20
|
+
yield session
|
|
21
|
+
ensure
|
|
22
|
+
Thread.current[:view_component_atomix_resolution] = nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.stale!
|
|
27
|
+
Thread.current[:view_component_atomix_resolution]&.stale!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@frames = []
|
|
32
|
+
@depth = 0
|
|
33
|
+
@passes = 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolving?
|
|
37
|
+
!@frames.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def provisional!
|
|
41
|
+
@frames.each { |frame| frame.provisional = true }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stale!
|
|
45
|
+
@frames.each { |frame| frame.stale = true }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def frame
|
|
49
|
+
nested do
|
|
50
|
+
current = Frame.new(false, false)
|
|
51
|
+
@frames << current
|
|
52
|
+
begin
|
|
53
|
+
yield current
|
|
54
|
+
ensure
|
|
55
|
+
@frames.pop
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def nested
|
|
61
|
+
if @depth >= MAX_DEPTH
|
|
62
|
+
raise ArgumentError, "Style resolution exceeds the nesting limit (#{MAX_DEPTH})"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@depth += 1
|
|
66
|
+
begin
|
|
67
|
+
yield
|
|
68
|
+
ensure
|
|
69
|
+
@depth -= 1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def consume!
|
|
74
|
+
@passes += 1
|
|
75
|
+
if @passes > DeclarationQueue::MAX_DRAIN_PASSES
|
|
76
|
+
raise ArgumentError, "Style declarations keep adding work during resolution"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|