silk_layout 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +36 -1
- data/README.md +74 -29
- data/lib/silk_layout/css/cascade.rb +5 -2
- data/lib/silk_layout/css/color.rb +415 -0
- data/lib/silk_layout/css/computed_style.rb +43 -11
- data/lib/silk_layout/css/page_rule.rb +104 -0
- data/lib/silk_layout/css/parser.rb +122 -27
- data/lib/silk_layout/css/properties.rb +301 -3
- data/lib/silk_layout/css/selector.rb +246 -30
- data/lib/silk_layout/css/values.rb +169 -0
- data/lib/silk_layout/html/node.rb +1 -0
- data/lib/silk_layout/html/parser.rb +17 -3
- data/lib/silk_layout/layout/block_layout.rb +131 -12
- data/lib/silk_layout/layout/box.rb +39 -1
- data/lib/silk_layout/layout/context.rb +7 -2
- data/lib/silk_layout/layout/engine.rb +12 -2
- data/lib/silk_layout/layout/flex_layout.rb +1 -0
- data/lib/silk_layout/layout/formatting_builder.rb +73 -19
- data/lib/silk_layout/layout/inline_formatter.rb +88 -10
- data/lib/silk_layout/layout/root.rb +4 -5
- data/lib/silk_layout/render/painter.rb +17 -23
- data/lib/silk_layout/render/pdf_renderer.rb +39 -5
- data/lib/silk_layout/resource/image.rb +151 -0
- data/lib/silk_layout/version.rb +1 -1
- data/lib/silk_layout.rb +54 -4
- metadata +5 -1
|
@@ -7,7 +7,7 @@ module SilkLayout
|
|
|
7
7
|
@values = {}
|
|
8
8
|
@explicit_properties = {}
|
|
9
9
|
|
|
10
|
-
apply_rules(rules)
|
|
10
|
+
apply_rules(rules, parent_style)
|
|
11
11
|
|
|
12
12
|
apply_inheritance(parent_style)
|
|
13
13
|
apply_defaults
|
|
@@ -39,29 +39,31 @@ module SilkLayout
|
|
|
39
39
|
|
|
40
40
|
INLINE_SPECIFICITY = [1000, 0, 0].freeze
|
|
41
41
|
|
|
42
|
-
def apply_rules(rules)
|
|
42
|
+
def apply_rules(rules, parent_style)
|
|
43
43
|
winners = {}
|
|
44
44
|
|
|
45
45
|
rules.each do |rule|
|
|
46
46
|
spec = rule.specificity || INLINE_SPECIFICITY
|
|
47
47
|
order = rule.order || 0
|
|
48
48
|
|
|
49
|
-
rule.declarations.
|
|
49
|
+
each_declaration(rule.declarations).each_with_index do |(property, decl), declaration_index|
|
|
50
50
|
value = decl.is_a?(Declaration) ? decl.value : decl
|
|
51
51
|
important = decl.is_a?(Declaration) ? decl.important : false
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
Properties.expand_declaration(property, value).each_with_index do |(expanded_property, expanded_value), expansion_index|
|
|
54
|
+
key = [important ? 1 : 0, spec, order, declaration_index, expansion_index]
|
|
55
|
+
current = winners[expanded_property]
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if current.nil? || (key <=> current[:key]) == 1
|
|
58
|
+
winners[expanded_property] = {key: key, value: expanded_value}
|
|
59
|
+
@explicit_properties[expanded_property] = true
|
|
60
|
+
end
|
|
59
61
|
end
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
winners.each do |prop, data|
|
|
64
|
-
@values[prop] = data[:value]
|
|
66
|
+
@values[prop] = resolve_css_wide(prop, data[:value], parent_style)
|
|
65
67
|
end
|
|
66
68
|
end
|
|
67
69
|
|
|
@@ -69,15 +71,45 @@ module SilkLayout
|
|
|
69
71
|
return unless parent
|
|
70
72
|
|
|
71
73
|
Properties::INHERITED.each do |prop|
|
|
72
|
-
@values[prop]
|
|
74
|
+
@values[prop] = parent[prop] unless @values.key?(prop)
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
def apply_defaults
|
|
77
79
|
Properties::DEFAULTS.each do |prop, value|
|
|
78
|
-
@values[prop]
|
|
80
|
+
@values[prop] = value unless @values.key?(prop)
|
|
79
81
|
end
|
|
80
82
|
end
|
|
83
|
+
|
|
84
|
+
def each_declaration(declarations)
|
|
85
|
+
return enum_for(:each_declaration, declarations) unless block_given?
|
|
86
|
+
|
|
87
|
+
declarations.each do |property, declaration|
|
|
88
|
+
yield property, declaration
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_css_wide(property, value, parent)
|
|
93
|
+
return value unless Properties.css_wide_keyword?(value)
|
|
94
|
+
return value unless Properties.known?(property)
|
|
95
|
+
|
|
96
|
+
case value.to_s.strip.downcase
|
|
97
|
+
when "inherit"
|
|
98
|
+
inherited_value(property, parent)
|
|
99
|
+
when "initial"
|
|
100
|
+
Properties.initial_value(property)
|
|
101
|
+
when "unset"
|
|
102
|
+
if Properties.inherited?(property)
|
|
103
|
+
inherited_value(property, parent)
|
|
104
|
+
else
|
|
105
|
+
Properties.initial_value(property)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inherited_value(property, parent)
|
|
111
|
+
parent&.[](property) || Properties.initial_value(property)
|
|
112
|
+
end
|
|
81
113
|
end
|
|
82
114
|
end
|
|
83
115
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SilkLayout
|
|
4
|
+
module CSS
|
|
5
|
+
class PageRule
|
|
6
|
+
CSS_PX_PER_IN = 96.0
|
|
7
|
+
MM_PER_IN = 25.4
|
|
8
|
+
A4 = [(210.0 / MM_PER_IN) * CSS_PX_PER_IN, (297.0 / MM_PER_IN) * CSS_PX_PER_IN].freeze
|
|
9
|
+
NAMED_SIZES = {
|
|
10
|
+
"a4" => A4
|
|
11
|
+
}.freeze
|
|
12
|
+
ORIENTATIONS = %w[
|
|
13
|
+
landscape
|
|
14
|
+
portrait
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :selector, :declarations, :order
|
|
18
|
+
|
|
19
|
+
def self.resolve_page_size(page_rules)
|
|
20
|
+
page_rules
|
|
21
|
+
.select { |rule| rule.selector.to_s.empty? }
|
|
22
|
+
.sort_by(&:order)
|
|
23
|
+
.filter_map(&:size)
|
|
24
|
+
.last
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.parse_size(value)
|
|
28
|
+
tokens = value.to_s.strip.split(/\s+/)
|
|
29
|
+
return nil if tokens.empty?
|
|
30
|
+
|
|
31
|
+
orientation = tokens.find { |token| ORIENTATIONS.include?(token.downcase) }&.downcase
|
|
32
|
+
size_tokens = tokens.reject { |token| ORIENTATIONS.include?(token.downcase) }
|
|
33
|
+
dimensions = named_dimensions(size_tokens) || length_dimensions(size_tokens)
|
|
34
|
+
return nil unless dimensions
|
|
35
|
+
|
|
36
|
+
orient(dimensions, orientation)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.named_dimensions(tokens)
|
|
40
|
+
return nil unless tokens.length == 1
|
|
41
|
+
|
|
42
|
+
named = NAMED_SIZES[tokens[0].downcase]
|
|
43
|
+
named&.dup
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.length_dimensions(tokens)
|
|
47
|
+
return nil unless tokens.length.between?(1, 2)
|
|
48
|
+
|
|
49
|
+
lengths = tokens.map { |token| length_to_px(token) }
|
|
50
|
+
return nil if lengths.any?(&:nil?)
|
|
51
|
+
|
|
52
|
+
[lengths[0], lengths[1] || lengths[0]]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.length_to_px(token)
|
|
56
|
+
match = token.to_s.match(/\A([-+]?\d*\.?\d+)(px|in)\z/i)
|
|
57
|
+
return nil unless match
|
|
58
|
+
|
|
59
|
+
value = match[1].to_f
|
|
60
|
+
return nil unless value.positive?
|
|
61
|
+
|
|
62
|
+
(match[2].downcase == "in") ? value * CSS_PX_PER_IN : value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.orient(dimensions, orientation)
|
|
66
|
+
case orientation
|
|
67
|
+
when "landscape"
|
|
68
|
+
(dimensions[0] > dimensions[1]) ? dimensions : dimensions.reverse
|
|
69
|
+
when "portrait"
|
|
70
|
+
(dimensions[0] < dimensions[1]) ? dimensions : dimensions.reverse
|
|
71
|
+
else
|
|
72
|
+
dimensions
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize(selector:, declarations:, order:)
|
|
77
|
+
@selector = selector
|
|
78
|
+
@declarations = declarations
|
|
79
|
+
@order = order
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def size
|
|
83
|
+
declaration = last_declaration_named("size")
|
|
84
|
+
return nil unless declaration
|
|
85
|
+
|
|
86
|
+
self.class.parse_size(declaration[1].value)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def last_declaration_named(name)
|
|
92
|
+
index = declarations.length - 1
|
|
93
|
+
while index >= 0
|
|
94
|
+
declaration = declarations[index]
|
|
95
|
+
return declaration if declaration[0] == name
|
|
96
|
+
|
|
97
|
+
index -= 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -5,45 +5,140 @@ require "crass"
|
|
|
5
5
|
module SilkLayout
|
|
6
6
|
module CSS
|
|
7
7
|
class Parser
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
attr_reader :rules, :page_rules
|
|
9
|
+
|
|
10
|
+
def self.parse_all(stylesheets, media: :print)
|
|
11
|
+
parse_stylesheets(stylesheets, media: media).rules
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.parse_page_rules(stylesheets, media: :print)
|
|
15
|
+
parse_stylesheets(stylesheets, media: media).page_rules
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.parse_stylesheets(stylesheets, media: :print)
|
|
19
|
+
parser = new(media: media)
|
|
11
20
|
|
|
12
21
|
stylesheets.each do |css|
|
|
13
|
-
Crass.parse(css)
|
|
14
|
-
|
|
22
|
+
parser.parse_nodes(Crass.parse(css))
|
|
23
|
+
end
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
parser
|
|
26
|
+
end
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
def initialize(media:)
|
|
29
|
+
@media = media.to_s.downcase
|
|
30
|
+
@rules = []
|
|
31
|
+
@page_rules = []
|
|
32
|
+
@order = 0
|
|
33
|
+
@page_order = 0
|
|
34
|
+
end
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
36
|
+
def parse_nodes(nodes)
|
|
37
|
+
nodes.each do |node|
|
|
38
|
+
case node[:node]
|
|
39
|
+
when :style_rule
|
|
40
|
+
append_style_rules(node)
|
|
41
|
+
when :at_rule
|
|
42
|
+
append_at_rule(node)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
value = child[:value]
|
|
47
|
+
private
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
def append_at_rule(node)
|
|
50
|
+
case node[:name].to_s.downcase
|
|
51
|
+
when "media"
|
|
52
|
+
parse_nodes(Crass.parse(tokens_to_css(node[:block]))) if media_matches?(node[:prelude])
|
|
53
|
+
when "page"
|
|
54
|
+
append_page_rule(node) if print_media?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
def append_style_rules(node)
|
|
59
|
+
selector_text = node[:selector][:value].to_s.strip
|
|
60
|
+
selectors = selector_text.split(",").map(&:strip).reject(&:empty?)
|
|
61
|
+
declarations = declarations_from_children(node[:children])
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
declarations: declarations,
|
|
36
|
-
specificity: selector.specificity,
|
|
37
|
-
order: order,
|
|
38
|
-
origin: :author
|
|
39
|
-
)
|
|
63
|
+
selectors.each do |raw_selector|
|
|
64
|
+
selector = Selector.new(raw_selector)
|
|
40
65
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
rules << Rule.new(
|
|
67
|
+
selector: selector,
|
|
68
|
+
declarations: declarations,
|
|
69
|
+
specificity: selector.specificity,
|
|
70
|
+
order: @order,
|
|
71
|
+
origin: :author
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@order += 1
|
|
44
75
|
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def append_page_rule(node)
|
|
79
|
+
declarations = declarations_from_block(node[:block])
|
|
80
|
+
return if declarations.empty?
|
|
81
|
+
|
|
82
|
+
page_rules << PageRule.new(
|
|
83
|
+
selector: tokens_to_css(node[:prelude]).strip,
|
|
84
|
+
declarations: declarations,
|
|
85
|
+
order: @page_order
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@page_order += 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def declarations_from_children(children)
|
|
92
|
+
children.each_with_object([]) do |child, declarations|
|
|
93
|
+
next unless child[:node] == :property
|
|
94
|
+
|
|
95
|
+
declarations << declaration_for(child)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def declarations_from_block(block)
|
|
100
|
+
declarations_from_children(Crass.parse_properties(tokens_to_css(block)))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def declaration_for(node)
|
|
104
|
+
[
|
|
105
|
+
node[:name].to_s.downcase,
|
|
106
|
+
Declaration.new(value: node[:value].to_s.strip, important: node[:important] ? true : false)
|
|
107
|
+
]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def media_matches?(prelude)
|
|
111
|
+
tokens_to_css(prelude).split(",").any? do |query|
|
|
112
|
+
media_query_matches?(query)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def media_query_matches?(query)
|
|
117
|
+
raw = query.to_s.strip.downcase
|
|
118
|
+
return false if raw.empty?
|
|
119
|
+
|
|
120
|
+
raw = raw.sub(/\Aonly\s+/, "")
|
|
121
|
+
negated = raw.start_with?("not ")
|
|
122
|
+
raw = raw.sub(/\Anot\s+/, "") if negated
|
|
123
|
+
|
|
124
|
+
media_type = raw[/\A[a-z][a-z0-9_-]*/]
|
|
125
|
+
matched = media_type.nil? || media_type == "all" || media_type == @media
|
|
126
|
+
|
|
127
|
+
negated ? !matched : matched
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def print_media?
|
|
131
|
+
@media == "print" || @media == "all"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def tokens_to_css(tokens)
|
|
135
|
+
Array(tokens).map { |token| token_to_css(token) }.join
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def token_to_css(token)
|
|
139
|
+
return token[:tokens].map { |child| token_to_css(child) }.join if token[:tokens]
|
|
45
140
|
|
|
46
|
-
|
|
141
|
+
token[:raw].to_s
|
|
47
142
|
end
|
|
48
143
|
end
|
|
49
144
|
end
|
|
@@ -3,17 +3,315 @@
|
|
|
3
3
|
module SilkLayout
|
|
4
4
|
module CSS
|
|
5
5
|
module Properties
|
|
6
|
+
EDGES = %w[
|
|
7
|
+
top
|
|
8
|
+
right
|
|
9
|
+
bottom
|
|
10
|
+
left
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
BORDER_PARTS = %w[
|
|
14
|
+
width
|
|
15
|
+
style
|
|
16
|
+
color
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
CSS_WIDE_KEYWORDS = %w[
|
|
20
|
+
inherit
|
|
21
|
+
initial
|
|
22
|
+
unset
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
6
25
|
INHERITED = %w[
|
|
7
26
|
color
|
|
8
27
|
font-size
|
|
9
28
|
font-family
|
|
29
|
+
font-weight
|
|
30
|
+
font-style
|
|
31
|
+
line-height
|
|
10
32
|
].freeze
|
|
11
33
|
|
|
12
|
-
|
|
34
|
+
SPACING_INITIALS = %w[
|
|
35
|
+
margin
|
|
36
|
+
padding
|
|
37
|
+
].flat_map do |property|
|
|
38
|
+
EDGES.map { |side| ["#{property}-#{side}", "0"] }
|
|
39
|
+
end.to_h.freeze
|
|
40
|
+
|
|
41
|
+
BORDER_INITIALS = EDGES.flat_map do |side|
|
|
42
|
+
[
|
|
43
|
+
["border-#{side}-width", "medium"],
|
|
44
|
+
["border-#{side}-style", "none"],
|
|
45
|
+
["border-#{side}-color", "black"]
|
|
46
|
+
]
|
|
47
|
+
end.to_h.freeze
|
|
48
|
+
|
|
49
|
+
INITIAL_VALUES = {
|
|
13
50
|
"color" => "black",
|
|
14
51
|
"font-size" => "16px",
|
|
15
|
-
"
|
|
16
|
-
|
|
52
|
+
"font-family" => "Helvetica",
|
|
53
|
+
"font-weight" => "normal",
|
|
54
|
+
"font-style" => "normal",
|
|
55
|
+
"line-height" => "normal",
|
|
56
|
+
"display" => "inline",
|
|
57
|
+
"width" => "auto",
|
|
58
|
+
"height" => "auto",
|
|
59
|
+
"box-sizing" => "content-box",
|
|
60
|
+
"min-width" => "0",
|
|
61
|
+
"max-width" => "none",
|
|
62
|
+
"background-color" => "transparent",
|
|
63
|
+
"flex-direction" => "row",
|
|
64
|
+
"flex-wrap" => "nowrap",
|
|
65
|
+
"justify-content" => "flex-start",
|
|
66
|
+
"align-items" => "stretch",
|
|
67
|
+
"flex-grow" => "0",
|
|
68
|
+
"flex-shrink" => "1",
|
|
69
|
+
"flex-basis" => "auto",
|
|
70
|
+
"row-gap" => "0",
|
|
71
|
+
"column-gap" => "0"
|
|
72
|
+
}.merge(SPACING_INITIALS).merge(BORDER_INITIALS).freeze
|
|
73
|
+
|
|
74
|
+
DEFAULTS = INITIAL_VALUES
|
|
75
|
+
KNOWN = INITIAL_VALUES.keys.freeze
|
|
76
|
+
|
|
77
|
+
def self.expand_declaration(property, value)
|
|
78
|
+
property = property.to_s.downcase
|
|
79
|
+
value = value.to_s.strip
|
|
80
|
+
expanded = [[property, value]]
|
|
81
|
+
|
|
82
|
+
expanded.concat(
|
|
83
|
+
if css_wide_keyword?(value)
|
|
84
|
+
css_wide_expansion(property, value)
|
|
85
|
+
else
|
|
86
|
+
shorthand_expansion(property, value)
|
|
87
|
+
end
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.css_wide_keyword?(value)
|
|
92
|
+
CSS_WIDE_KEYWORDS.include?(value.to_s.strip.downcase)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.initial_value(property)
|
|
96
|
+
INITIAL_VALUES[property]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.known?(property)
|
|
100
|
+
KNOWN.include?(property)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.inherited?(property)
|
|
104
|
+
INHERITED.include?(property)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.css_wide_expansion(property, value)
|
|
108
|
+
case property
|
|
109
|
+
when "margin", "padding"
|
|
110
|
+
EDGES.map { |side| ["#{property}-#{side}", value] }
|
|
111
|
+
when "border"
|
|
112
|
+
all_border_longhands(value)
|
|
113
|
+
when "border-width", "border-style", "border-color"
|
|
114
|
+
border_edge_longhands(property.delete_prefix("border-"), value)
|
|
115
|
+
when /\Aborder-(top|right|bottom|left)\z/
|
|
116
|
+
border_side_longhands(Regexp.last_match(1), value)
|
|
117
|
+
when "background"
|
|
118
|
+
[["background-color", value]]
|
|
119
|
+
when "flex"
|
|
120
|
+
flex_longhands(value, value, value)
|
|
121
|
+
when "flex-flow"
|
|
122
|
+
flex_flow_longhands(value, value)
|
|
123
|
+
when "gap"
|
|
124
|
+
gap_longhands(value, value)
|
|
125
|
+
else
|
|
126
|
+
[]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.shorthand_expansion(property, value)
|
|
131
|
+
case property
|
|
132
|
+
when "margin", "padding"
|
|
133
|
+
edge_values(value).map { |side, side_value| ["#{property}-#{side}", side_value] }
|
|
134
|
+
when "border"
|
|
135
|
+
parsed = border_shorthand(value)
|
|
136
|
+
all_border_longhands do |side, part|
|
|
137
|
+
parsed[part] || INITIAL_VALUES.fetch("border-#{side}-#{part}")
|
|
138
|
+
end
|
|
139
|
+
when "border-width", "border-style", "border-color"
|
|
140
|
+
part = property.delete_prefix("border-")
|
|
141
|
+
edge_values(value).map { |side, side_value| ["border-#{side}-#{part}", side_value] }
|
|
142
|
+
when /\Aborder-(top|right|bottom|left)\z/
|
|
143
|
+
side = Regexp.last_match(1)
|
|
144
|
+
parsed = border_shorthand(value)
|
|
145
|
+
border_side_longhands(side) do |part|
|
|
146
|
+
parsed[part] || INITIAL_VALUES.fetch("border-#{side}-#{part}")
|
|
147
|
+
end
|
|
148
|
+
when "background"
|
|
149
|
+
[["background-color", background_color(value)]]
|
|
150
|
+
when "flex"
|
|
151
|
+
flex_shorthand(value).map { |part, part_value| ["flex-#{part}", part_value] }
|
|
152
|
+
when "flex-flow"
|
|
153
|
+
flex_flow(value).map { |part, part_value| ["flex-#{part}", part_value] }
|
|
154
|
+
when "gap"
|
|
155
|
+
gap_values(value)
|
|
156
|
+
else
|
|
157
|
+
[]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.edge_values(value)
|
|
162
|
+
tokens = split_tokens(value)
|
|
163
|
+
values =
|
|
164
|
+
case tokens.length
|
|
165
|
+
when 0
|
|
166
|
+
[]
|
|
167
|
+
when 1
|
|
168
|
+
[tokens[0], tokens[0], tokens[0], tokens[0]]
|
|
169
|
+
when 2
|
|
170
|
+
[tokens[0], tokens[1], tokens[0], tokens[1]]
|
|
171
|
+
when 3
|
|
172
|
+
[tokens[0], tokens[1], tokens[2], tokens[1]]
|
|
173
|
+
else
|
|
174
|
+
[tokens[0], tokens[1], tokens[2], tokens[3]]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
EDGES.zip(values).to_h
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.all_border_longhands(value = nil)
|
|
181
|
+
EDGES.flat_map do |side|
|
|
182
|
+
BORDER_PARTS.map do |part|
|
|
183
|
+
["border-#{side}-#{part}", block_given? ? yield(side, part) : value]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def self.border_edge_longhands(part, value)
|
|
189
|
+
EDGES.map { |side| ["border-#{side}-#{part}", value] }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.border_side_longhands(side, value = nil)
|
|
193
|
+
BORDER_PARTS.map do |part|
|
|
194
|
+
["border-#{side}-#{part}", block_given? ? yield(part) : value]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.border_shorthand(value)
|
|
199
|
+
split_tokens(value).each_with_object({}) do |token, parsed|
|
|
200
|
+
if border_width?(token)
|
|
201
|
+
parsed["width"] ||= token
|
|
202
|
+
elsif border_style?(token)
|
|
203
|
+
parsed["style"] ||= token
|
|
204
|
+
else
|
|
205
|
+
parsed["color"] ||= token
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.background_color(value)
|
|
211
|
+
split_tokens(value).find { |token| color_token?(token) } || INITIAL_VALUES.fetch("background-color")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.flex_shorthand(value)
|
|
215
|
+
tokens = split_tokens(value)
|
|
216
|
+
|
|
217
|
+
case tokens.join(" ")
|
|
218
|
+
when "none"
|
|
219
|
+
{"grow" => "0", "shrink" => "0", "basis" => "auto"}
|
|
220
|
+
when "auto"
|
|
221
|
+
{"grow" => "1", "shrink" => "1", "basis" => "auto"}
|
|
222
|
+
else
|
|
223
|
+
numbers = tokens.select { |token| numeric?(token) }
|
|
224
|
+
basis = tokens.find { |token| !numeric?(token) }
|
|
225
|
+
|
|
226
|
+
{
|
|
227
|
+
"grow" => numbers[0] || "1",
|
|
228
|
+
"shrink" => numbers[1] || "1",
|
|
229
|
+
"basis" => basis || (numbers.any? ? "0px" : "auto")
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def self.flex_longhands(grow, shrink, basis)
|
|
235
|
+
[
|
|
236
|
+
["flex-grow", grow],
|
|
237
|
+
["flex-shrink", shrink],
|
|
238
|
+
["flex-basis", basis]
|
|
239
|
+
]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.flex_flow(value)
|
|
243
|
+
parsed = split_tokens(value).each_with_object({}) do |token, acc|
|
|
244
|
+
if %w[row row-reverse column column-reverse].include?(token)
|
|
245
|
+
acc["direction"] = token
|
|
246
|
+
elsif %w[nowrap wrap wrap-reverse].include?(token)
|
|
247
|
+
acc["wrap"] = token
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
"direction" => parsed["direction"] || INITIAL_VALUES.fetch("flex-direction"),
|
|
253
|
+
"wrap" => parsed["wrap"] || INITIAL_VALUES.fetch("flex-wrap")
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.flex_flow_longhands(direction, wrap)
|
|
258
|
+
[
|
|
259
|
+
["flex-direction", direction],
|
|
260
|
+
["flex-wrap", wrap]
|
|
261
|
+
]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def self.gap_values(value)
|
|
265
|
+
values = split_tokens(value)
|
|
266
|
+
row = values[0] || INITIAL_VALUES.fetch("row-gap")
|
|
267
|
+
column = values[1] || row
|
|
268
|
+
|
|
269
|
+
gap_longhands(row, column)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def self.gap_longhands(row, column)
|
|
273
|
+
[
|
|
274
|
+
["row-gap", row],
|
|
275
|
+
["column-gap", column]
|
|
276
|
+
]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def self.split_tokens(value)
|
|
280
|
+
Values.split_tokens(value)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def self.numeric?(value)
|
|
284
|
+
value.to_s.match?(/\A[-+]?\d*\.?\d+\z/)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def self.border_width?(value)
|
|
288
|
+
value.to_s.match?(/\A[-+]?\d*\.?\d+(?:px)?\z/) || %w[thin medium thick].include?(value.to_s)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def self.border_style?(value)
|
|
292
|
+
%w[
|
|
293
|
+
none
|
|
294
|
+
hidden
|
|
295
|
+
dotted
|
|
296
|
+
dashed
|
|
297
|
+
solid
|
|
298
|
+
double
|
|
299
|
+
groove
|
|
300
|
+
ridge
|
|
301
|
+
inset
|
|
302
|
+
outset
|
|
303
|
+
].include?(value.to_s)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def self.color_token?(value)
|
|
307
|
+
raw = value.to_s.strip
|
|
308
|
+
return false if raw.empty?
|
|
309
|
+
return true if raw.start_with?("#")
|
|
310
|
+
return true if SilkLayout::CSS::Color.parse(raw)
|
|
311
|
+
return false if %w[none transparent inherit initial unset].include?(raw)
|
|
312
|
+
|
|
313
|
+
!border_width?(raw) && !border_style?(raw) && !raw.include?("(") && !raw.include?("/")
|
|
314
|
+
end
|
|
17
315
|
end
|
|
18
316
|
end
|
|
19
317
|
end
|