charming 0.1.1 → 0.1.3

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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/charming/application.rb +14 -3
  4. data/lib/charming/cli.rb +23 -0
  5. data/lib/charming/controller/class_methods.rb +115 -0
  6. data/lib/charming/controller/command_palette.rb +135 -0
  7. data/lib/charming/controller/component_dispatching.rb +81 -0
  8. data/lib/charming/controller/dispatching.rb +60 -0
  9. data/lib/charming/controller/focus_management.rb +30 -0
  10. data/lib/charming/controller/rendering.rb +127 -0
  11. data/lib/charming/controller/session_state.rb +41 -0
  12. data/lib/charming/controller/sidebar_navigation.rb +111 -0
  13. data/lib/charming/controller.rb +35 -559
  14. data/lib/charming/database_commands.rb +16 -0
  15. data/lib/charming/database_installer.rb +27 -0
  16. data/lib/charming/focus.rb +58 -2
  17. data/lib/charming/generators/app_file_generator.rb +13 -0
  18. data/lib/charming/generators/app_generator.rb +123 -47
  19. data/lib/charming/generators/base.rb +26 -0
  20. data/lib/charming/generators/component_generator.rb +10 -10
  21. data/lib/charming/generators/controller_generator.rb +22 -11
  22. data/lib/charming/generators/model_generator.rb +38 -29
  23. data/lib/charming/generators/name.rb +10 -0
  24. data/lib/charming/generators/screen_generator.rb +78 -32
  25. data/lib/charming/generators/templates/app/Gemfile.template +5 -0
  26. data/lib/charming/generators/templates/app/README.md.template +9 -0
  27. data/lib/charming/generators/templates/app/Rakefile.template +3 -0
  28. data/lib/charming/generators/templates/app/application.template +13 -0
  29. data/lib/charming/generators/templates/app/application_controller.template +19 -0
  30. data/lib/charming/generators/templates/app/application_record.template +7 -0
  31. data/lib/charming/generators/templates/app/application_state.template +6 -0
  32. data/lib/charming/generators/templates/app/database_config.template +12 -0
  33. data/lib/charming/generators/templates/app/executable.template +7 -0
  34. data/lib/charming/generators/templates/app/gemspec.template +6 -0
  35. data/lib/charming/generators/templates/app/home_controller.template +6 -0
  36. data/lib/charming/generators/templates/app/home_state.template +7 -0
  37. data/lib/charming/generators/templates/app/keep.template +0 -0
  38. data/lib/charming/generators/templates/app/layout.template +110 -0
  39. data/lib/charming/generators/templates/app/root_file.template +20 -0
  40. data/lib/charming/generators/templates/app/routes.template +5 -0
  41. data/lib/charming/generators/templates/app/seeds.template +1 -0
  42. data/lib/charming/generators/templates/app/spec_controller.template +17 -0
  43. data/lib/charming/generators/templates/app/spec_helper.template +3 -0
  44. data/lib/charming/generators/templates/app/spec_state.template +17 -0
  45. data/lib/charming/generators/templates/app/spec_view.template +16 -0
  46. data/lib/charming/generators/templates/app/version.template +5 -0
  47. data/lib/charming/generators/templates/app/view.template +21 -0
  48. data/lib/charming/generators/templates/component/component.rb.template +9 -0
  49. data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
  50. data/lib/charming/generators/templates/model/migration.rb.template +9 -0
  51. data/lib/charming/generators/templates/model/model.rb.template +6 -0
  52. data/lib/charming/generators/templates/model/spec.rb.template +9 -0
  53. data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
  54. data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
  55. data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
  56. data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
  57. data/lib/charming/generators/templates/screen/state.rb.template +7 -0
  58. data/lib/charming/generators/templates/screen/view.rb.template +11 -0
  59. data/lib/charming/generators/templates/view/view.rb.template +11 -0
  60. data/lib/charming/generators/view_generator.rb +19 -3
  61. data/lib/charming/internal/renderer/differential.rb +25 -2
  62. data/lib/charming/internal/renderer/full_repaint.rb +6 -0
  63. data/lib/charming/internal/terminal/adapter.rb +29 -3
  64. data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
  65. data/lib/charming/internal/terminal/memory_backend.rb +28 -1
  66. data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
  67. data/lib/charming/internal/terminal/tty_backend.rb +65 -115
  68. data/lib/charming/presentation/component.rb +3 -5
  69. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  70. data/lib/charming/presentation/components/command_palette.rb +94 -96
  71. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  72. data/lib/charming/presentation/components/empty_state.rb +47 -36
  73. data/lib/charming/presentation/components/form/builder.rb +52 -40
  74. data/lib/charming/presentation/components/form/confirm.rb +50 -39
  75. data/lib/charming/presentation/components/form/field.rb +94 -71
  76. data/lib/charming/presentation/components/form/input.rb +61 -49
  77. data/lib/charming/presentation/components/form/note.rb +27 -20
  78. data/lib/charming/presentation/components/form/select.rb +84 -63
  79. data/lib/charming/presentation/components/form/textarea.rb +67 -53
  80. data/lib/charming/presentation/components/form.rb +120 -93
  81. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  82. data/lib/charming/presentation/components/list.rb +123 -97
  83. data/lib/charming/presentation/components/markdown.rb +21 -17
  84. data/lib/charming/presentation/components/modal.rb +55 -43
  85. data/lib/charming/presentation/components/progressbar.rb +61 -50
  86. data/lib/charming/presentation/components/spinner.rb +35 -27
  87. data/lib/charming/presentation/components/table.rb +108 -85
  88. data/lib/charming/presentation/components/text_area.rb +219 -173
  89. data/lib/charming/presentation/components/text_input.rb +120 -98
  90. data/lib/charming/presentation/components/viewport.rb +218 -168
  91. data/lib/charming/presentation/layout/builder.rb +84 -0
  92. data/lib/charming/presentation/layout/overlay.rb +55 -0
  93. data/lib/charming/presentation/layout/pane.rb +149 -0
  94. data/lib/charming/presentation/layout/rect.rb +21 -0
  95. data/lib/charming/presentation/layout/screen_layout.rb +58 -0
  96. data/lib/charming/presentation/layout/split.rb +132 -0
  97. data/lib/charming/presentation/layout.rb +28 -30
  98. data/lib/charming/presentation/markdown/block_renderers.rb +118 -0
  99. data/lib/charming/presentation/markdown/inline_renderers.rb +66 -0
  100. data/lib/charming/presentation/markdown/render_context.rb +20 -0
  101. data/lib/charming/presentation/markdown/renderer.rb +82 -174
  102. data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -44
  103. data/lib/charming/presentation/markdown.rb +4 -3
  104. data/lib/charming/presentation/template_view.rb +22 -17
  105. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  106. data/lib/charming/presentation/templates.rb +47 -32
  107. data/lib/charming/presentation/ui/ansi_codes.rb +87 -0
  108. data/lib/charming/presentation/ui/ansi_slicer.rb +92 -0
  109. data/lib/charming/presentation/ui/border.rb +24 -26
  110. data/lib/charming/presentation/ui/border_painter.rb +56 -0
  111. data/lib/charming/presentation/ui/canvas.rb +80 -0
  112. data/lib/charming/presentation/ui/style.rb +170 -205
  113. data/lib/charming/presentation/ui/theme.rb +133 -135
  114. data/lib/charming/presentation/ui/width.rb +12 -14
  115. data/lib/charming/presentation/ui.rb +70 -213
  116. data/lib/charming/presentation/view.rb +107 -92
  117. data/lib/charming/runtime.rb +25 -10
  118. data/lib/charming/tasks/inline_executor.rb +9 -0
  119. data/lib/charming/tasks/task.rb +3 -0
  120. data/lib/charming/tasks/threaded_executor.rb +12 -0
  121. data/lib/charming/version.rb +1 -1
  122. data/lib/charming.rb +16 -2
  123. metadata +60 -10
  124. data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -90
  125. data/lib/charming/generators/app_generator/basic_templates.rb +0 -81
  126. data/lib/charming/generators/app_generator/component_templates.rb +0 -36
  127. data/lib/charming/generators/app_generator/controller_template.rb +0 -60
  128. data/lib/charming/generators/app_generator/database_templates.rb +0 -45
  129. data/lib/charming/generators/app_generator/layout_template.rb +0 -66
  130. data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -69
  131. data/lib/charming/generators/app_generator/state_templates.rb +0 -30
  132. data/lib/charming/generators/app_generator/view_template.rb +0 -84
@@ -1,244 +1,209 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module UI
6
- class Style
7
- ATTRIBUTES = {
8
- bold: 1,
9
- faint: 2,
10
- italic: 3,
11
- underline: 4,
12
- reverse: 7,
13
- strikethrough: 9
14
- }.freeze
15
-
16
- COLORS = {
17
- black: 30,
18
- red: 31,
19
- green: 32,
20
- yellow: 33,
21
- blue: 34,
22
- magenta: 35,
23
- cyan: 36,
24
- white: 37,
25
- bright_black: 90,
26
- bright_red: 91,
27
- bright_green: 92,
28
- bright_yellow: 93,
29
- bright_blue: 94,
30
- bright_magenta: 95,
31
- bright_cyan: 96,
32
- bright_white: 97
33
- }.freeze
34
-
35
- def initialize(options = {})
36
- @options = {
37
- attributes: [],
38
- padding: [0, 0, 0, 0],
39
- align: :left
40
- }.merge(options)
41
- end
42
-
43
- def foreground(color)
44
- with(foreground: color)
45
- end
46
- alias_method :fg, :foreground
47
-
48
- def background(color)
49
- with(background: color)
50
- end
51
- alias_method :bg, :background
52
-
53
- ATTRIBUTES.each_key do |attribute|
54
- define_method(attribute) do
55
- with(attributes: (@options.fetch(:attributes) + [attribute]).uniq)
56
- end
57
- end
58
-
59
- def padding(*values)
60
- with(padding: expand_box_values(values))
61
- end
62
-
63
- def border(style = :normal, sides: nil, foreground: nil)
64
- with(border: style, border_sides: sides, border_foreground: foreground)
65
- end
66
-
67
- def width(value)
68
- with(width: value)
69
- end
70
-
71
- def height(value)
72
- with(height: value)
73
- end
74
-
75
- def align(value)
76
- with(align: value)
77
- end
78
-
79
- def render(value)
80
- lines = apply_dimensions(value.to_s.lines(chomp: true))
81
- lines = apply_padding(lines)
82
- lines = apply_border(lines)
83
- apply_ansi(lines.join("\n"))
84
- end
85
-
86
- private
87
-
88
- def with(changes)
89
- self.class.new(@options.merge(changes))
90
- end
91
-
92
- def apply_dimensions(lines)
93
- content_width = target_content_width(lines)
94
- dimensioned = lines.map { |line| align_line(fit_line(line, content_width), content_width) }
95
- apply_height(dimensioned, content_width)
96
- end
97
-
98
- def target_content_width(lines)
99
- explicit_width = @options[:width]
100
- natural_width = lines.map { |line| Width.measure(line) }.max || 0
101
- explicit_width || natural_width
102
- end
103
-
104
- def fit_line(line, width)
105
- return line if Width.measure(line) <= width
4
+ module UI
5
+ # Style is an immutable builder for terminal text styling. Every method returns a new
6
+ # Style instance with the requested attribute added, so styles can be safely chained and
7
+ # shared across views. `render(value)` applies the accumulated style to a string.
8
+ class Style
9
+ ATTRIBUTES = ANSICodes::ATTRIBUTES
10
+
11
+ COLORS = ANSICodes::COLORS
12
+
13
+ # Initializes a new style with an optional options hash. Recognized keys: `:attributes`
14
+ # (array of attribute symbols), `:padding` ([top, right, bottom, left]), `:align`
15
+ # (`:left`/`:right`/`:center`), and any of `:foreground`, `:background`, `:border`,
16
+ # `:border_sides`, `:border_foreground`, `:width`, `:height`.
17
+ def initialize(options = {})
18
+ @options = {
19
+ attributes: [],
20
+ padding: [0, 0, 0, 0],
21
+ align: :left
22
+ }.merge(options)
23
+ end
106
24
 
107
- UI.visible_slice(line, 0, width)
108
- end
25
+ # Returns a new Style with the foreground *color* set. *color* is a color name (":red"),
26
+ # 256-color index (integer), or hex string ("#rrggbb").
27
+ def foreground(color)
28
+ with(foreground: color)
29
+ end
30
+ alias_method :fg, :foreground
109
31
 
110
- def apply_height(lines, width)
111
- height = @options[:height]
112
- return lines unless height
32
+ # Returns a new Style with the background *color* set.
33
+ def background(color)
34
+ with(background: color)
35
+ end
36
+ alias_method :bg, :background
113
37
 
114
- visible = lines.first(height)
115
- visible + Array.new([height - visible.length, 0].max) { " " * width }
38
+ # Attribute methods (bold, italic, underline, …) are defined dynamically by the
39
+ # metaprogramming loop below. Each toggles a single text attribute on the style.
40
+ ATTRIBUTES.each_key do |attribute|
41
+ define_method(attribute) do
42
+ with(attributes: (@options.fetch(:attributes) + [attribute]).uniq)
116
43
  end
44
+ end
117
45
 
118
- def apply_padding(lines)
119
- top, right, bottom, left = @options.fetch(:padding)
120
- inner_width = lines.map { |line| Width.measure(line) }.max || 0
121
- empty = " " * (left + inner_width + right)
122
- padded = lines.map do |line|
123
- pad_line(line, inner_width, left, right)
124
- end
46
+ # Returns a new Style with the padding set. Accepts 1, 2, or 4 values following CSS-style
47
+ # shorthand: 1 → all sides, 2 → [vertical, horizontal], 4 → [top, right, bottom, left].
48
+ def padding(*values)
49
+ with(padding: expand_box_values(values))
50
+ end
125
51
 
126
- Array.new(top, empty) + padded + Array.new(bottom, empty)
127
- end
52
+ # Returns a new Style with the border set. *style* is a border name (e.g., :normal,
53
+ # :rounded). *sides* optionally restricts the border to specific sides. *foreground*
54
+ # sets the border color.
55
+ def border(style = :normal, sides: nil, foreground: nil)
56
+ with(border: style, border_sides: sides, border_foreground: foreground)
57
+ end
128
58
 
129
- def apply_border(lines)
130
- border_name = @options[:border]
131
- return lines unless border_name
59
+ # Returns a new Style that fixes the rendered width to *value* (in display columns).
60
+ def width(value)
61
+ with(width: value)
62
+ end
132
63
 
133
- border = Border.fetch(border_name)
134
- sides = Array(@options[:border_sides] || %i[top right bottom left]).map(&:to_sym)
135
- width = lines.map { |line| Width.measure(line) }.max || 0
136
- horizontal = border.horizontal * width
137
- body = lines.map { |line| border_line(line, width, border, sides) }
64
+ # Returns a new Style that fixes the rendered height to *value* (in rows).
65
+ def height(value)
66
+ with(height: value)
67
+ end
138
68
 
139
- [top_border(border, horizontal, sides), *body, bottom_border(border, horizontal, sides)].compact
140
- end
69
+ # Returns a new Style with horizontal alignment set (`:left`, `:right`, or `:center`).
70
+ def align(value)
71
+ with(align: value)
72
+ end
141
73
 
142
- def pad_line(line, inner_width, left, right)
143
- (" " * left) + line + (" " * (inner_width - Width.measure(line) + right))
144
- end
74
+ # Applies the configured style to *value* and returns the styled string. Steps:
75
+ # 1. wrap to `:width`, 2. align horizontally, 3. expand to `:height`, 4. apply padding,
76
+ # 5. paint border, 6. emit ANSI attribute/foreground/background escapes.
77
+ def render(value)
78
+ lines = apply_dimensions(value.to_s.lines(chomp: true))
79
+ lines = apply_padding(lines)
80
+ lines = apply_border(lines)
81
+ apply_ansi(lines.join("\n"))
82
+ end
145
83
 
146
- def border_line(line, width, border, sides)
147
- left = sides.include?(:left) ? render_border(border.vertical) : ""
148
- right = sides.include?(:right) ? render_border(border.vertical) : ""
84
+ private
149
85
 
150
- "#{left}#{line}#{" " * (width - Width.measure(line))}#{right}"
151
- end
86
+ # Returns a copy of self with *changes* merged into the options hash.
87
+ def with(changes)
88
+ self.class.new(@options.merge(changes))
89
+ end
152
90
 
153
- def top_border(border, horizontal, sides)
154
- return unless sides.include?(:top)
155
- return render_border(horizontal) unless full_horizontal_border?(sides)
91
+ # Wraps each line to the target width and applies horizontal alignment, then expands
92
+ # to the target height.
93
+ def apply_dimensions(lines)
94
+ content_width = target_content_width(lines)
95
+ dimensioned = lines.map { |line| align_line(fit_line(line, content_width), content_width) }
96
+ apply_height(dimensioned, content_width)
97
+ end
156
98
 
157
- render_border("#{border.top_left}#{horizontal}#{border.top_right}")
158
- end
99
+ # Returns the target content width: the explicit :width if set, otherwise the natural
100
+ # max display width of the lines.
101
+ def target_content_width(lines)
102
+ explicit_width = @options[:width]
103
+ natural_width = lines.map { |line| Width.measure(line) }.max || 0
104
+ explicit_width || natural_width
105
+ end
159
106
 
160
- def bottom_border(border, horizontal, sides)
161
- return unless sides.include?(:bottom)
162
- return render_border(horizontal) unless full_horizontal_border?(sides)
107
+ # Clips *line* to *width* display columns, preserving ANSI styling where possible.
108
+ def fit_line(line, width)
109
+ return line if Width.measure(line) <= width
163
110
 
164
- render_border("#{border.bottom_left}#{horizontal}#{border.bottom_right}")
165
- end
111
+ UI.visible_slice(line, 0, width)
112
+ end
166
113
 
167
- def full_horizontal_border?(sides)
168
- sides.include?(:left) && sides.include?(:right)
169
- end
114
+ # Truncates or pads the lines array to *height* rows, filling with blank rows.
115
+ def apply_height(lines, width)
116
+ height = @options[:height]
117
+ return lines unless height
170
118
 
171
- def render_border(value)
172
- border_foreground = @options[:border_foreground]
173
- return value unless border_foreground
119
+ visible = lines.first(height)
120
+ visible + Array.new([height - visible.length, 0].max) { " " * width }
121
+ end
174
122
 
175
- Style.new(foreground: border_foreground, background: @options[:background]).render(value)
123
+ # Applies padding by prepending/appending blank rows (vertical) and indenting each
124
+ # line (horizontal).
125
+ def apply_padding(lines)
126
+ top, right, bottom, left = @options.fetch(:padding)
127
+ inner_width = lines.map { |line| Width.measure(line) }.max || 0
128
+ empty = " " * (left + inner_width + right)
129
+ padded = lines.map do |line|
130
+ pad_line(line, inner_width, left, right)
176
131
  end
177
132
 
178
- def apply_ansi(value)
179
- codes = ansi_codes
180
- return value if codes.empty?
133
+ Array.new(top, empty) + padded + Array.new(bottom, empty)
134
+ end
181
135
 
182
- start = "\e[#{codes.join(";")}m"
183
- value.split("\n", -1).map { |line| "#{start}#{line.gsub("\e[0m", "\e[0m#{start}")}\e[0m" }.join("\n")
184
- end
136
+ # Paints the configured border around the lines, when :border is set.
137
+ def apply_border(lines)
138
+ border_name = @options[:border]
139
+ return lines unless border_name
185
140
 
186
- def ansi_codes
187
- @options.fetch(:attributes).map { |attribute| ATTRIBUTES.fetch(attribute) } +
188
- color_codes(@options[:foreground], foreground: true) +
189
- color_codes(@options[:background], foreground: false)
190
- end
141
+ border_painter(border_name).paint(lines, content_width(lines))
142
+ end
191
143
 
192
- def color_codes(color, foreground:)
193
- return [] unless color
194
- return indexed_color_code(color, foreground: foreground) if color.is_a?(Integer)
195
- return named_color_code(color, foreground: foreground) if COLORS.key?(color.to_sym)
196
- return truecolor_codes(color, foreground: foreground) if color.to_s.start_with?("#")
144
+ # Pads a single line to *inner_width*, with *left* and *right* padding spaces.
145
+ def pad_line(line, inner_width, left, right)
146
+ (" " * left) + line + (" " * (inner_width - Width.measure(line) + right))
147
+ end
197
148
 
198
- raise ArgumentError, "unknown color: #{color.inspect}"
199
- end
149
+ # Builds a BorderPainter configured for the current border options.
150
+ def border_painter(border_name)
151
+ BorderPainter.new(
152
+ border: Border.fetch(border_name),
153
+ sides: @options[:border_sides],
154
+ foreground: @options[:border_foreground],
155
+ background: @options[:background]
156
+ )
157
+ end
200
158
 
201
- def named_color_code(color, foreground:)
202
- code = COLORS.fetch(color.to_sym)
203
- [foreground ? code : code + 10]
204
- end
159
+ # Returns the natural display width of the longest line in *lines*.
160
+ def content_width(lines)
161
+ lines.map { |line| Width.measure(line) }.max || 0
162
+ end
205
163
 
206
- def indexed_color_code(color, foreground:)
207
- raise ArgumentError, "indexed color must be between 0 and 255" unless color.between?(0, 255)
164
+ # Applies the active ANSI attribute/foreground/background codes to *value*.
165
+ def apply_ansi(value)
166
+ ansi_codes_obj.apply(value)
167
+ end
208
168
 
209
- [foreground ? 38 : 48, 5, color]
210
- end
169
+ # The list of active ANSI escape sequence strings (attribute + foreground + background).
170
+ def ansi_codes
171
+ ansi_codes_obj.codes
172
+ end
211
173
 
212
- def truecolor_codes(color, foreground:)
213
- hex = color.to_s.delete_prefix("#")
214
- raise ArgumentError, "truecolor must be #rrggbb" unless hex.match?(/\A[0-9a-fA-F]{6}\z/)
174
+ # Builds an ANSICodes object from the active attributes, foreground, and background.
175
+ def ansi_codes_obj
176
+ ANSICodes.new(
177
+ attributes: @options.fetch(:attributes),
178
+ foreground: @options[:foreground],
179
+ background: @options[:background]
180
+ )
181
+ end
215
182
 
216
- [foreground ? 38 : 48, 2, hex[0..1].to_i(16), hex[2..3].to_i(16), hex[4..5].to_i(16)]
217
- end
183
+ # Pads *line* on the left or right (or both, for :center) according to :align.
184
+ def align_line(line, width)
185
+ remaining = width - Width.measure(line)
186
+ return line if remaining <= 0
218
187
 
219
- def align_line(line, width)
220
- remaining = width - Width.measure(line)
221
- return line if remaining <= 0
222
-
223
- case @options.fetch(:align)
224
- when :right
225
- (" " * remaining) + line
226
- when :center
227
- left = remaining / 2
228
- (" " * left) + line + (" " * (remaining - left))
229
- else
230
- line + (" " * remaining)
231
- end
188
+ case @options.fetch(:align)
189
+ when :right
190
+ (" " * remaining) + line
191
+ when :center
192
+ left = remaining / 2
193
+ (" " * left) + line + (" " * (remaining - left))
194
+ else
195
+ line + (" " * remaining)
232
196
  end
197
+ end
233
198
 
234
- def expand_box_values(values)
235
- case values.length
236
- when 1 then [values[0], values[0], values[0], values[0]]
237
- when 2 then [values[0], values[1], values[0], values[1]]
238
- when 4 then values
239
- else
240
- raise ArgumentError, "padding expects 1, 2, or 4 values"
241
- end
199
+ # Normalizes 1/2/4 padding value arguments into a [top, right, bottom, left] array.
200
+ def expand_box_values(values)
201
+ case values.length
202
+ when 1 then [values[0], values[0], values[0], values[0]]
203
+ when 2 then [values[0], values[1], values[0], values[1]]
204
+ when 4 then values
205
+ else
206
+ raise ArgumentError, "padding expects 1, 2, or 4 values"
242
207
  end
243
208
  end
244
209
  end