bandoola 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/.tool-versions +2 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/Rakefile +10 -0
- data/exe/bandoola +5 -0
- data/lib/bandoola/cli.rb +44 -0
- data/lib/bandoola/font.rb +204 -0
- data/lib/bandoola/jpeg.rb +104 -0
- data/lib/bandoola/standard_metrics.rb +113 -0
- data/lib/bandoola/svg.rb +349 -0
- data/lib/bandoola/true_type_font.rb +206 -0
- data/lib/bandoola/true_type_subset.rb +302 -0
- data/lib/bandoola/version.rb +3 -0
- data/lib/bandoola/view/element.rb +431 -0
- data/lib/bandoola/view/file.rb +16 -0
- data/lib/bandoola/view/header.rb +110 -0
- data/lib/bandoola/view/resources.rb +44 -0
- data/lib/bandoola/view/style/borders.rb +66 -0
- data/lib/bandoola/view/style/colors.rb +167 -0
- data/lib/bandoola/view/style/spacing.rb +42 -0
- data/lib/bandoola/view/style/typography.rb +72 -0
- data/lib/bandoola/view/style.rb +139 -0
- data/lib/bandoola/view.rb +364 -0
- data/lib/bandoola.rb +14 -0
- data/sig/bandoola.rbs +4 -0
- metadata +73 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module Bandoola
|
|
2
|
+
class View
|
|
3
|
+
class Style
|
|
4
|
+
# Background and border colors, named with Tailwind classes:
|
|
5
|
+
# bg-<color>-<shade> fills a div; border-<color>-<shade> colors its
|
|
6
|
+
# border (which still needs a border-N width to be drawn). Mixed into
|
|
7
|
+
# Style.
|
|
8
|
+
#
|
|
9
|
+
# PALETTE is Tailwind CSS v3.4 default colors (MIT-licensed): each
|
|
10
|
+
# family mapped to its 50..950 shades as hex.
|
|
11
|
+
module Colors # rubocop:disable Metrics/ModuleLength -- mostly the palette data table
|
|
12
|
+
PALETTE = {
|
|
13
|
+
"red" => {
|
|
14
|
+
50 => "#fef2f2", 100 => "#fee2e2", 200 => "#fecaca", 300 => "#fca5a5",
|
|
15
|
+
400 => "#f87171", 500 => "#ef4444", 600 => "#dc2626", 700 => "#b91c1c",
|
|
16
|
+
800 => "#991b1b", 900 => "#7f1d1d", 950 => "#450a0a"
|
|
17
|
+
},
|
|
18
|
+
"orange" => {
|
|
19
|
+
50 => "#fff7ed", 100 => "#ffedd5", 200 => "#fed7aa", 300 => "#fdba74",
|
|
20
|
+
400 => "#fb923c", 500 => "#f97316", 600 => "#ea580c", 700 => "#c2410c",
|
|
21
|
+
800 => "#9a3412", 900 => "#7c2d12", 950 => "#431407"
|
|
22
|
+
},
|
|
23
|
+
"amber" => {
|
|
24
|
+
50 => "#fffbeb", 100 => "#fef3c7", 200 => "#fde68a", 300 => "#fcd34d",
|
|
25
|
+
400 => "#fbbf24", 500 => "#f59e0b", 600 => "#d97706", 700 => "#b45309",
|
|
26
|
+
800 => "#92400e", 900 => "#78350f", 950 => "#451a03"
|
|
27
|
+
},
|
|
28
|
+
"yellow" => {
|
|
29
|
+
50 => "#fefce8", 100 => "#fef9c3", 200 => "#fef08a", 300 => "#fde047",
|
|
30
|
+
400 => "#facc15", 500 => "#eab308", 600 => "#ca8a04", 700 => "#a16207",
|
|
31
|
+
800 => "#854d0e", 900 => "#713f12", 950 => "#422006"
|
|
32
|
+
},
|
|
33
|
+
"lime" => {
|
|
34
|
+
50 => "#f7fee7", 100 => "#ecfccb", 200 => "#d9f99d", 300 => "#bef264",
|
|
35
|
+
400 => "#a3e635", 500 => "#84cc16", 600 => "#65a30d", 700 => "#4d7c0f",
|
|
36
|
+
800 => "#3f6212", 900 => "#365314", 950 => "#1a2e05"
|
|
37
|
+
},
|
|
38
|
+
"green" => {
|
|
39
|
+
50 => "#f0fdf4", 100 => "#dcfce7", 200 => "#bbf7d0", 300 => "#86efac",
|
|
40
|
+
400 => "#4ade80", 500 => "#22c55e", 600 => "#16a34a", 700 => "#15803d",
|
|
41
|
+
800 => "#166534", 900 => "#14532d", 950 => "#052e16"
|
|
42
|
+
},
|
|
43
|
+
"emerald" => {
|
|
44
|
+
50 => "#ecfdf5", 100 => "#d1fae5", 200 => "#a7f3d0", 300 => "#6ee7b7",
|
|
45
|
+
400 => "#34d399", 500 => "#10b981", 600 => "#059669", 700 => "#047857",
|
|
46
|
+
800 => "#065f46", 900 => "#064e3b", 950 => "#022c22"
|
|
47
|
+
},
|
|
48
|
+
"teal" => {
|
|
49
|
+
50 => "#f0fdfa", 100 => "#ccfbf1", 200 => "#99f6e4", 300 => "#5eead4",
|
|
50
|
+
400 => "#2dd4bf", 500 => "#14b8a6", 600 => "#0d9488", 700 => "#0f766e",
|
|
51
|
+
800 => "#115e59", 900 => "#134e4a", 950 => "#042f2e"
|
|
52
|
+
},
|
|
53
|
+
"cyan" => {
|
|
54
|
+
50 => "#ecfeff", 100 => "#cffafe", 200 => "#a5f3fc", 300 => "#67e8f9",
|
|
55
|
+
400 => "#22d3ee", 500 => "#06b6d4", 600 => "#0891b2", 700 => "#0e7490",
|
|
56
|
+
800 => "#155e75", 900 => "#164e63", 950 => "#083344"
|
|
57
|
+
},
|
|
58
|
+
"sky" => {
|
|
59
|
+
50 => "#f0f9ff", 100 => "#e0f2fe", 200 => "#bae6fd", 300 => "#7dd3fc",
|
|
60
|
+
400 => "#38bdf8", 500 => "#0ea5e9", 600 => "#0284c7", 700 => "#0369a1",
|
|
61
|
+
800 => "#075985", 900 => "#0c4a6e", 950 => "#082f49"
|
|
62
|
+
},
|
|
63
|
+
"blue" => {
|
|
64
|
+
50 => "#eff6ff", 100 => "#dbeafe", 200 => "#bfdbfe", 300 => "#93c5fd",
|
|
65
|
+
400 => "#60a5fa", 500 => "#3b82f6", 600 => "#2563eb", 700 => "#1d4ed8",
|
|
66
|
+
800 => "#1e40af", 900 => "#1e3a8a", 950 => "#172554"
|
|
67
|
+
},
|
|
68
|
+
"indigo" => {
|
|
69
|
+
50 => "#eef2ff", 100 => "#e0e7ff", 200 => "#c7d2fe", 300 => "#a5b4fc",
|
|
70
|
+
400 => "#818cf8", 500 => "#6366f1", 600 => "#4f46e5", 700 => "#4338ca",
|
|
71
|
+
800 => "#3730a3", 900 => "#312e81", 950 => "#1e1b4b"
|
|
72
|
+
},
|
|
73
|
+
"violet" => {
|
|
74
|
+
50 => "#f5f3ff", 100 => "#ede9fe", 200 => "#ddd6fe", 300 => "#c4b5fd",
|
|
75
|
+
400 => "#a78bfa", 500 => "#8b5cf6", 600 => "#7c3aed", 700 => "#6d28d9",
|
|
76
|
+
800 => "#5b21b6", 900 => "#4c1d95", 950 => "#2e1065"
|
|
77
|
+
},
|
|
78
|
+
"purple" => {
|
|
79
|
+
50 => "#faf5ff", 100 => "#f3e8ff", 200 => "#e9d5ff", 300 => "#d8b4fe",
|
|
80
|
+
400 => "#c084fc", 500 => "#a855f7", 600 => "#9333ea", 700 => "#7e22ce",
|
|
81
|
+
800 => "#6b21a8", 900 => "#581c87", 950 => "#3b0764"
|
|
82
|
+
},
|
|
83
|
+
"fuchsia" => {
|
|
84
|
+
50 => "#fdf4ff", 100 => "#fae8ff", 200 => "#f5d0fe", 300 => "#f0abfc",
|
|
85
|
+
400 => "#e879f9", 500 => "#d946ef", 600 => "#c026d3", 700 => "#a21caf",
|
|
86
|
+
800 => "#86198f", 900 => "#701a75", 950 => "#4a044e"
|
|
87
|
+
},
|
|
88
|
+
"pink" => {
|
|
89
|
+
50 => "#fdf2f8", 100 => "#fce7f3", 200 => "#fbcfe8", 300 => "#f9a8d4",
|
|
90
|
+
400 => "#f472b6", 500 => "#ec4899", 600 => "#db2777", 700 => "#be185d",
|
|
91
|
+
800 => "#9d174d", 900 => "#831843", 950 => "#500724"
|
|
92
|
+
},
|
|
93
|
+
"rose" => {
|
|
94
|
+
50 => "#fff1f2", 100 => "#ffe4e6", 200 => "#fecdd3", 300 => "#fda4af",
|
|
95
|
+
400 => "#fb7185", 500 => "#f43f5e", 600 => "#e11d48", 700 => "#be123c",
|
|
96
|
+
800 => "#9f1239", 900 => "#881337", 950 => "#4c0519"
|
|
97
|
+
},
|
|
98
|
+
"slate" => {
|
|
99
|
+
50 => "#f8fafc", 100 => "#f1f5f9", 200 => "#e2e8f0", 300 => "#cbd5e1",
|
|
100
|
+
400 => "#94a3b8", 500 => "#64748b", 600 => "#475569", 700 => "#334155",
|
|
101
|
+
800 => "#1e293b", 900 => "#0f172a", 950 => "#020617"
|
|
102
|
+
},
|
|
103
|
+
"gray" => {
|
|
104
|
+
50 => "#f9fafb", 100 => "#f3f4f6", 200 => "#e5e7eb", 300 => "#d1d5db",
|
|
105
|
+
400 => "#9ca3af", 500 => "#6b7280", 600 => "#4b5563", 700 => "#374151",
|
|
106
|
+
800 => "#1f2937", 900 => "#111827", 950 => "#030712"
|
|
107
|
+
},
|
|
108
|
+
"zinc" => {
|
|
109
|
+
50 => "#fafafa", 100 => "#f4f4f5", 200 => "#e4e4e7", 300 => "#d4d4d8",
|
|
110
|
+
400 => "#a1a1aa", 500 => "#71717a", 600 => "#52525b", 700 => "#3f3f46",
|
|
111
|
+
800 => "#27272a", 900 => "#18181b", 950 => "#09090b"
|
|
112
|
+
},
|
|
113
|
+
"neutral" => {
|
|
114
|
+
50 => "#fafafa", 100 => "#f5f5f5", 200 => "#e5e5e5", 300 => "#d4d4d4",
|
|
115
|
+
400 => "#a3a3a3", 500 => "#737373", 600 => "#525252", 700 => "#404040",
|
|
116
|
+
800 => "#262626", 900 => "#171717", 950 => "#0a0a0a"
|
|
117
|
+
},
|
|
118
|
+
"stone" => {
|
|
119
|
+
50 => "#fafaf9", 100 => "#f5f5f4", 200 => "#e7e5e4", 300 => "#d6d3d1",
|
|
120
|
+
400 => "#a8a29e", 500 => "#78716c", 600 => "#57534e", 700 => "#44403c",
|
|
121
|
+
800 => "#292524", 900 => "#1c1917", 950 => "#0c0a09"
|
|
122
|
+
}
|
|
123
|
+
}.freeze
|
|
124
|
+
|
|
125
|
+
# Colors without a shade: black/white. A shade is optional, so
|
|
126
|
+
# bg-black works, and a bare family (bg-cyan) falls back to 500.
|
|
127
|
+
NAMED = { "black" => "#000000", "white" => "#ffffff" }.freeze
|
|
128
|
+
DEFAULT_SHADE = 500
|
|
129
|
+
|
|
130
|
+
BACKGROUND = /\Abg-([a-z]+)(?:-(\d+))?\z/
|
|
131
|
+
BORDER_COLOR = /\Aborder-([a-z]+)(?:-(\d+))?\z/
|
|
132
|
+
TEXT_COLOR = /\Atext-([a-z]+)(?:-(\d+))?\z/
|
|
133
|
+
|
|
134
|
+
attr_reader :background, :border_color, :text_color
|
|
135
|
+
|
|
136
|
+
# [r, g, b] floats in 0..1 for a color name and optional shade, or nil
|
|
137
|
+
# if unknown. Named colors (black/white) ignore the shade.
|
|
138
|
+
def self.rgb(family, shade)
|
|
139
|
+
hex = NAMED[family] || PALETTE.dig(family, (shade || DEFAULT_SHADE).to_i)
|
|
140
|
+
return nil unless hex
|
|
141
|
+
|
|
142
|
+
digits = hex.delete("#")
|
|
143
|
+
[0, 2, 4].map { |offset| digits[offset, 2].to_i(16) / 255.0 }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def init_colors
|
|
147
|
+
@background = nil
|
|
148
|
+
@border_color = nil
|
|
149
|
+
@text_color = nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Only assigns when the token resolves to a real color, so an unknown
|
|
153
|
+
# color (or a text-<size> token that matches the text- shape but isn't
|
|
154
|
+
# a color) doesn't clobber a color set by an earlier class.
|
|
155
|
+
def apply_color_token(token)
|
|
156
|
+
if (match = token.match(BACKGROUND))
|
|
157
|
+
@background = Colors.rgb(match[1], match[2]) || @background
|
|
158
|
+
elsif (match = token.match(BORDER_COLOR))
|
|
159
|
+
@border_color = Colors.rgb(match[1], match[2]) || @border_color
|
|
160
|
+
elsif (match = token.match(TEXT_COLOR))
|
|
161
|
+
@text_color = Colors.rgb(match[1], match[2]) || @text_color
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Bandoola
|
|
2
|
+
class View
|
|
3
|
+
class Style
|
|
4
|
+
# Padding and margin: the p-* and m-* classes (p-4, px-2, mt-1, …). Both
|
|
5
|
+
# use the shared spacing SCALE and the same side letters, so they live
|
|
6
|
+
# together. Mixed into Style.
|
|
7
|
+
module Spacing
|
|
8
|
+
SPACING = /\A([mp])([trblxy]?)-(\d+)\z/
|
|
9
|
+
|
|
10
|
+
# Which edges each side letter touches (empty = all four, Tailwind's
|
|
11
|
+
# x/y axes, or a single t/r/b/l edge).
|
|
12
|
+
SIDES = {
|
|
13
|
+
"" => %i[top right bottom left],
|
|
14
|
+
"x" => %i[left right],
|
|
15
|
+
"y" => %i[top bottom],
|
|
16
|
+
"t" => %i[top],
|
|
17
|
+
"r" => %i[right],
|
|
18
|
+
"b" => %i[bottom],
|
|
19
|
+
"l" => %i[left]
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :margin, :padding
|
|
23
|
+
|
|
24
|
+
def init_spacing
|
|
25
|
+
@margin = Inset.new(0, 0, 0, 0)
|
|
26
|
+
@padding = Inset.new(0, 0, 0, 0)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def apply_spacing_token(token)
|
|
30
|
+
match = token.match(SPACING)
|
|
31
|
+
return unless match
|
|
32
|
+
|
|
33
|
+
points = SCALE[match[3].to_i]
|
|
34
|
+
return unless points # steps outside Tailwind's scale have no effect
|
|
35
|
+
|
|
36
|
+
inset = match[1] == "m" ? @margin : @padding
|
|
37
|
+
SIDES.fetch(match[2]).each { |edge| inset[edge] = points }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Bandoola
|
|
2
|
+
class View
|
|
3
|
+
class Style
|
|
4
|
+
# Typography classes:
|
|
5
|
+
# font-sans / font-serif / font-mono / font-<name> -> font family
|
|
6
|
+
# font-light/normal/medium/semibold/bold/... -> font weight
|
|
7
|
+
# text-xs … text-9xl -> font size (pt)
|
|
8
|
+
#
|
|
9
|
+
# The family is recorded by name (the View resolves it to an actual font
|
|
10
|
+
# at render time). The weight is a number; since we don't carry separate
|
|
11
|
+
# weight files, the renderer synthesises heavier weights with a stroke
|
|
12
|
+
# ("faux bold"), so anything below normal renders like normal. A name
|
|
13
|
+
# that is a known weight keyword can't double as a family name.
|
|
14
|
+
#
|
|
15
|
+
# text- also covers font size (text-xs … text-9xl) and alignment
|
|
16
|
+
# (text-left/center/right). These share the prefix with colors
|
|
17
|
+
# (text-cyan-500); each handler recognises only its own keywords, so they
|
|
18
|
+
# never claim each other's tokens.
|
|
19
|
+
# Mixed into Style.
|
|
20
|
+
module Typography
|
|
21
|
+
FONT = /\Afont-([a-z0-9-]+)\z/
|
|
22
|
+
|
|
23
|
+
WEIGHTS = {
|
|
24
|
+
"thin" => 100, "extralight" => 200, "light" => 300, "normal" => 400,
|
|
25
|
+
"medium" => 500, "semibold" => 600, "bold" => 700, "extrabold" => 800,
|
|
26
|
+
"black" => 900
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Tailwind's font-size scale, in points (1px ≈ 1pt).
|
|
30
|
+
SIZES = {
|
|
31
|
+
"xs" => 12, "sm" => 14, "base" => 16, "lg" => 18, "xl" => 20,
|
|
32
|
+
"2xl" => 24, "3xl" => 30, "4xl" => 36, "5xl" => 48, "6xl" => 60,
|
|
33
|
+
"7xl" => 72, "8xl" => 96, "9xl" => 128
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
ALIGNMENTS = %w[left center right].freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :font, :font_weight, :text_size, :text_align
|
|
39
|
+
|
|
40
|
+
def init_typography
|
|
41
|
+
@font = nil
|
|
42
|
+
@font_weight = nil
|
|
43
|
+
@text_size = nil
|
|
44
|
+
@text_align = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def apply_font_token(token)
|
|
48
|
+
match = token.match(FONT)
|
|
49
|
+
return unless match
|
|
50
|
+
|
|
51
|
+
name = match[1]
|
|
52
|
+
if WEIGHTS.key?(name)
|
|
53
|
+
@font_weight = WEIGHTS[name]
|
|
54
|
+
else
|
|
55
|
+
@font = name
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply_text_token(token)
|
|
60
|
+
return unless token.start_with?("text-")
|
|
61
|
+
|
|
62
|
+
key = token.delete_prefix("text-")
|
|
63
|
+
if (size = SIZES[key])
|
|
64
|
+
@text_size = size
|
|
65
|
+
elsif ALIGNMENTS.include?(key)
|
|
66
|
+
@text_align = key.to_sym
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require_relative "style/spacing"
|
|
2
|
+
require_relative "style/borders"
|
|
3
|
+
require_relative "style/colors"
|
|
4
|
+
require_relative "style/typography"
|
|
5
|
+
|
|
6
|
+
module Bandoola
|
|
7
|
+
class View
|
|
8
|
+
# A box's spacing on each edge, in PDF points.
|
|
9
|
+
Inset = Struct.new(:top, :right, :bottom, :left)
|
|
10
|
+
|
|
11
|
+
# The visual style parsed from an element's `class` keyword. The vocabulary
|
|
12
|
+
# borrows from Tailwind. This file keeps the shared scale and the sizing
|
|
13
|
+
# classes; the other concerns live in mixins beside it:
|
|
14
|
+
#
|
|
15
|
+
# Spacing p-4 px-2 mt-1 padding / margin (style/spacing.rb)
|
|
16
|
+
# Borders border-2 border width (style/borders.rb)
|
|
17
|
+
# Colors bg-cyan-100 border-... colors (style/colors.rb)
|
|
18
|
+
#
|
|
19
|
+
# Sizing (kept here):
|
|
20
|
+
# w-2 w-64 h-8 fixed width / height (the shared SCALE)
|
|
21
|
+
# w-1/2 w-6/7 h-3/4 fractional width / height
|
|
22
|
+
# w-full h-full fill the container
|
|
23
|
+
#
|
|
24
|
+
# Spacing and fixed sizes share one scale: `n` maps to `n * SPACING_UNIT`
|
|
25
|
+
# points. The allowed steps are Tailwind's — close together at the low end
|
|
26
|
+
# and increasingly sparse higher up (…12, 14, 16, 20, 24…), so `p-6` and
|
|
27
|
+
# `w-64` are valid but `w-31` is not. Unknown classes are ignored, so
|
|
28
|
+
# decorative names can sit alongside layout ones with no effect yet.
|
|
29
|
+
class Style
|
|
30
|
+
SPACING_UNIT = 4
|
|
31
|
+
|
|
32
|
+
# Tailwind's spacing scale (the integer steps): dense low, sparse high.
|
|
33
|
+
SCALE = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28,
|
|
34
|
+
32, 36, 40, 44, 48, 52, 56, 60, 64].to_h { |step| [step, step * SPACING_UNIT] }.freeze
|
|
35
|
+
|
|
36
|
+
SIZE_FRACTION = %r{\A([wh])-(\d+)/(\d+)\z}
|
|
37
|
+
SIZE_FIXED = /\A([wh])-(\d+)\z/
|
|
38
|
+
|
|
39
|
+
include Spacing
|
|
40
|
+
include Borders
|
|
41
|
+
include Colors
|
|
42
|
+
include Typography
|
|
43
|
+
|
|
44
|
+
attr_reader :width, :width_fraction, :height, :height_fraction
|
|
45
|
+
|
|
46
|
+
def self.none
|
|
47
|
+
new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Parse a `class` value into a Style. Accepts a String (split on
|
|
51
|
+
# whitespace), an Array of tokens, or nil.
|
|
52
|
+
def self.parse(value)
|
|
53
|
+
new.apply(tokens(value))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Normalize a `class` value to an array of tokens: a String is split on
|
|
57
|
+
# whitespace, an Array is flattened (its entries normalized in turn),
|
|
58
|
+
# and nil yields none.
|
|
59
|
+
def self.tokens(value)
|
|
60
|
+
case value
|
|
61
|
+
when Array then value.flat_map { |item| tokens(item) }
|
|
62
|
+
when String then value.split(/\s+/).reject(&:empty?)
|
|
63
|
+
when nil then []
|
|
64
|
+
else [value.to_s]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize
|
|
69
|
+
init_spacing
|
|
70
|
+
init_sizing
|
|
71
|
+
init_borders
|
|
72
|
+
init_colors
|
|
73
|
+
init_typography
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def apply(tokens)
|
|
77
|
+
tokens.each { |token| apply_token(token) }
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Try each concern in turn. A token matches at most one, so the others
|
|
84
|
+
# simply do nothing — no branching, no precedence to reason about.
|
|
85
|
+
def apply_token(token)
|
|
86
|
+
apply_spacing_token(token)
|
|
87
|
+
apply_sizing_token(token)
|
|
88
|
+
apply_border_token(token)
|
|
89
|
+
apply_color_token(token)
|
|
90
|
+
apply_font_token(token)
|
|
91
|
+
apply_text_token(token)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def init_sizing
|
|
95
|
+
@width = nil # fixed width in points, or nil
|
|
96
|
+
@width_fraction = nil # [numerator, denominator], or nil
|
|
97
|
+
@height = nil
|
|
98
|
+
@height_fraction = nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def apply_sizing_token(token)
|
|
102
|
+
if (match = token.match(SIZE_FRACTION))
|
|
103
|
+
apply_fraction(match[1], [match[2].to_i, match[3].to_i])
|
|
104
|
+
elsif (match = token.match(SIZE_FIXED))
|
|
105
|
+
apply_fixed(match[1], SCALE[match[2].to_i])
|
|
106
|
+
elsif token == "w-full"
|
|
107
|
+
# Width already defaults to full, so "full" just clears any size.
|
|
108
|
+
@width = @width_fraction = nil
|
|
109
|
+
elsif token == "h-full"
|
|
110
|
+
# Height defaults to auto, so "full" means fill the available height.
|
|
111
|
+
@height = nil
|
|
112
|
+
@height_fraction = [1, 1]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def apply_fraction(axis, fraction)
|
|
117
|
+
if axis == "w"
|
|
118
|
+
@width = nil
|
|
119
|
+
@width_fraction = fraction
|
|
120
|
+
else
|
|
121
|
+
@height = nil
|
|
122
|
+
@height_fraction = fraction
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def apply_fixed(axis, points)
|
|
127
|
+
return unless points # steps outside Tailwind's scale have no effect
|
|
128
|
+
|
|
129
|
+
if axis == "w"
|
|
130
|
+
@width = points
|
|
131
|
+
@width_fraction = nil
|
|
132
|
+
else
|
|
133
|
+
@height = points
|
|
134
|
+
@height_fraction = nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|