scarpe-components 0.2.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +85 -0
  4. data/README.md +2 -2
  5. data/assets/bootstrap-themes/bootstrap-cerulean.css +12229 -0
  6. data/assets/bootstrap-themes/bootstrap-cosmo.css +11810 -0
  7. data/assets/bootstrap-themes/bootstrap-cyborg.css +12210 -0
  8. data/assets/bootstrap-themes/bootstrap-darkly.css +12153 -0
  9. data/assets/bootstrap-themes/bootstrap-flatly.css +12126 -0
  10. data/assets/bootstrap-themes/bootstrap-icons.min.css +5 -0
  11. data/assets/bootstrap-themes/bootstrap-journal.css +12099 -0
  12. data/assets/bootstrap-themes/bootstrap-litera.css +12211 -0
  13. data/assets/bootstrap-themes/bootstrap-lumen.css +12369 -0
  14. data/assets/bootstrap-themes/bootstrap-lux.css +11928 -0
  15. data/assets/bootstrap-themes/bootstrap-materia.css +13184 -0
  16. data/assets/bootstrap-themes/bootstrap-minty.css +12177 -0
  17. data/assets/bootstrap-themes/bootstrap-morph.css +12750 -0
  18. data/assets/bootstrap-themes/bootstrap-pulse.css +11890 -0
  19. data/assets/bootstrap-themes/bootstrap-quartz.css +12622 -0
  20. data/assets/bootstrap-themes/bootstrap-sandstone.css +12201 -0
  21. data/assets/bootstrap-themes/bootstrap-simplex.css +12186 -0
  22. data/assets/bootstrap-themes/bootstrap-sketchy.css +12451 -0
  23. data/assets/bootstrap-themes/bootstrap-slate.css +12492 -0
  24. data/assets/bootstrap-themes/bootstrap-solar.css +12149 -0
  25. data/assets/bootstrap-themes/bootstrap-spacelab.css +12266 -0
  26. data/assets/bootstrap-themes/bootstrap-superhero.css +12216 -0
  27. data/assets/bootstrap-themes/bootstrap-united.css +12077 -0
  28. data/assets/bootstrap-themes/bootstrap-vapor.css +12549 -0
  29. data/assets/bootstrap-themes/bootstrap-yeti.css +12325 -0
  30. data/assets/bootstrap-themes/bootstrap-zephyr.css +12283 -0
  31. data/assets/bootstrap-themes/bootstrap.bundle.min.js +7 -0
  32. data/lib/scarpe/components/asset_server.rb +219 -0
  33. data/lib/scarpe/components/base64.rb +23 -5
  34. data/lib/scarpe/components/calzini/alert.rb +49 -0
  35. data/lib/scarpe/components/calzini/art_drawables.rb +227 -0
  36. data/lib/scarpe/components/calzini/border.rb +38 -0
  37. data/lib/scarpe/components/calzini/button.rb +37 -0
  38. data/lib/scarpe/components/calzini/misc.rb +136 -0
  39. data/lib/scarpe/components/calzini/para.rb +237 -0
  40. data/lib/scarpe/components/calzini/slots.rb +109 -0
  41. data/lib/scarpe/components/calzini.rb +236 -0
  42. data/lib/scarpe/components/errors.rb +24 -0
  43. data/lib/scarpe/components/file_helpers.rb +1 -0
  44. data/lib/scarpe/components/html.rb +134 -0
  45. data/lib/scarpe/components/minitest_export_reporter.rb +83 -0
  46. data/lib/scarpe/components/minitest_import_runnable.rb +98 -0
  47. data/lib/scarpe/components/minitest_result.rb +127 -0
  48. data/lib/scarpe/components/modular_logger.rb +5 -5
  49. data/lib/scarpe/components/print_logger.rb +22 -3
  50. data/lib/scarpe/components/process_helpers.rb +37 -0
  51. data/lib/scarpe/components/promises.rb +14 -14
  52. data/lib/scarpe/components/segmented_file_loader.rb +36 -17
  53. data/lib/scarpe/components/string_helpers.rb +10 -0
  54. data/lib/scarpe/components/tiranti.rb +167 -0
  55. data/lib/scarpe/components/unit_test_helpers.rb +48 -6
  56. data/lib/scarpe/components/version.rb +2 -2
  57. metadata +48 -4
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "json"
5
+ #require "json/add/exception"
6
+
7
+ module Scarpe; module Components; end; end
8
+
9
+ # A MinitestResult imports a JSON file from a minitest_export_reporter.
10
+ # But instead of creating a Minitest::Test to report the result, the
11
+ # MinitestResult is just a queryable Ruby object.
12
+ #
13
+ # MinitestResult assumes there will be only one class and one method
14
+ # in the JSON, which is true for Scarpe but not necessarily in general.
15
+ class Scarpe::Components::MinitestResult
16
+ attr_reader :assertions
17
+ attr_reader :method_name
18
+ attr_reader :class_name
19
+
20
+ def initialize(filename)
21
+ data = JSON.parse File.read(filename)
22
+
23
+ unless data.size == 1
24
+ # We would want a different interface to support this in general. For now we don't
25
+ # need it to work in general.
26
+ raise "Scarpe::Components::MinitestResult only supports one class and method in results!"
27
+ end
28
+
29
+ item = data.first
30
+
31
+ @assertions = item["assertions"]
32
+ @method_name = item["name"]
33
+ @class_name = item["klass"]
34
+ @time = item["time"]
35
+ @metadata = item.key?("metadata") ? item["metadata"]: {}
36
+
37
+ @skip = false
38
+ @exceptions = []
39
+ @failures = []
40
+ item["failures"].each do |f|
41
+ # JSON.parse ignores json_class and won't create an arbitrary object. That's good
42
+ # because Minitest::UnexpectedError seems to load in a bad way, so we don't want
43
+ # it to auto-instantiate.
44
+ d = JSON.parse f[1]
45
+ msg = d["m"]
46
+ case d["json_class"]
47
+ when "Minitest::UnexpectedError"
48
+ @exceptions << msg
49
+ when "Minitest::Skip"
50
+ @skip = msg
51
+ when "Minitest::Assertion"
52
+ @failures << msg
53
+ else
54
+ raise Scarpe::InternalError, "Didn't expect type #{t.inspect} as exception type when importing Minitest tests!"
55
+ end
56
+ end
57
+ end
58
+
59
+ def one_word_result
60
+ return "error" if self.error?
61
+ return "fail" if self.fail?
62
+ return "skip" if self.skip?
63
+ "success"
64
+ end
65
+
66
+ def result_and_message
67
+ return ["error", error_message] if self.error?
68
+ return ["fail", fail_message] if self.fail?
69
+ return ["skip", skip_message] if self.skip?
70
+ ["success", "OK"]
71
+ end
72
+
73
+ def console_summary
74
+ return "Error(s): #{@exceptions.inspect}" if self.error?
75
+ return "Failure: #{@failures.inspect}" if self.fail?
76
+ return "Skip: #{skip_message.inspect}" if self.skip?
77
+ "Success!"
78
+ end
79
+
80
+ def check(expect_result: :success, min_asserts: nil, max_asserts: nil)
81
+ unless [:error, :fail, :skip, :success].include?(expect_result)
82
+ raise Scarpe::InternalError, "Expected test result should be one of [:success, :fail, :error, :skip]!"
83
+ end
84
+
85
+ res, msg = result_and_message
86
+ if expect_result.to_s != res
87
+ return [false, "Expected #{expect_result} but got #{res}: #{msg}!"]
88
+ end
89
+
90
+ if min_asserts && @assertions < min_asserts
91
+ return [false, "Expected success with at least #{min_asserts} assertions but found only #{@assertions}!"]
92
+ end
93
+ if max_asserts && @assertions > max_asserts
94
+ return [false, "Expected success with no more than #{max_asserts} assertions but found only #{@assertions}!"]
95
+ end
96
+
97
+ [true, ""]
98
+ end
99
+
100
+ def error?
101
+ !@exceptions.empty?
102
+ end
103
+
104
+ def fail?
105
+ !@failures.empty?
106
+ end
107
+
108
+ def skip?
109
+ @skip ? true : false
110
+ end
111
+
112
+ def passed?
113
+ @exceptions.empty? && @failures.empty? && !@skip
114
+ end
115
+
116
+ def error_message
117
+ @exceptions[0]
118
+ end
119
+
120
+ def fail_message
121
+ @failures[0]
122
+ end
123
+
124
+ def skip_message
125
+ @skip
126
+ end
127
+ end
@@ -7,9 +7,9 @@ require "shoes/log"
7
7
 
8
8
  # Requires the logging gem
9
9
 
10
- class Scarpe; end
10
+ module Scarpe; end
11
11
  module Scarpe::Components; end
12
- class Scarpe
12
+ module Scarpe
13
13
  class Components::ModularLogImpl
14
14
  include Shoes::Log # for constants
15
15
 
@@ -32,7 +32,7 @@ class Scarpe
32
32
  when "fatal"
33
33
  :fatal
34
34
  else
35
- raise "Don't know how to treat #{data.inspect} as a logger severity!"
35
+ raise Shoes::Errors::InvalidAttributeValueError, "Don't know how to treat #{data.inspect} as a logger severity!"
36
36
  end
37
37
  end
38
38
 
@@ -45,7 +45,7 @@ class Scarpe
45
45
  when String
46
46
  Logging.appenders.file data, layout: @custom_log_layout
47
47
  else
48
- raise "Don't know how to convert #{data.inspect} to an appender!"
48
+ raise Shoes::Errors::InvalidAttributeValueError, "Don't know how to convert #{data.inspect} to an appender!"
49
49
  end
50
50
  end
51
51
 
@@ -64,7 +64,7 @@ class Scarpe
64
64
 
65
65
  logger.level = name_to_severity(level)
66
66
  else
67
- raise "Don't know how to use #{data.inspect} to specify a logger!"
67
+ raise Shoes::Errors::InvalidAttributeValueError, "Don't know how to use #{data.inspect} to specify a logger!"
68
68
  end
69
69
  end
70
70
 
@@ -3,29 +3,48 @@
3
3
  require "shoes/log"
4
4
  require "json"
5
5
 
6
- class Scarpe; end
6
+ module Scarpe; end
7
7
  module Scarpe::Components; end
8
8
  class Scarpe::Components::PrintLogImpl
9
9
  include Shoes::Log # for constants
10
10
 
11
11
  class PrintLogger
12
+ class << self
13
+ attr_accessor :silence
14
+ attr_accessor :min_level
15
+ end
16
+
17
+ LEVELS = {
18
+ :never => 1000,
19
+ :error => 4,
20
+ :warn => 3,
21
+ :info => 2,
22
+ :debug => 1,
23
+ :always => -1,
24
+ }
25
+ PrintLogger.min_level = LEVELS[:always]
26
+
12
27
  def initialize(component_name)
13
28
  @comp_name = component_name
14
29
  end
15
30
 
16
31
  def error(msg)
32
+ return if PrintLogger.silence || PrintLogger.min_level > LEVELS[:error]
17
33
  puts "#{@comp_name} error: #{msg}"
18
34
  end
19
35
 
20
36
  def warn(msg)
21
- puts "#{@comp_name} warn: #{msg}"
37
+ return if PrintLogger.silence || PrintLogger.min_level > LEVELS[:warn]
38
+ puts "#{@comp_name} warn: #{msg}" unless PrintLogger.silence
22
39
  end
23
40
 
24
41
  def debug(msg)
25
- puts "#{@comp_name} debug: #{msg}"
42
+ return if PrintLogger.silence || PrintLogger.min_level > LEVELS[:debug]
43
+ puts "#{@comp_name} debug: #{msg}" unless PrintLogger.silence
26
44
  end
27
45
 
28
46
  def info(msg)
47
+ return if PrintLogger.silence || PrintLogger.min_level > LEVELS[:info]
29
48
  puts "#{@comp_name} info: #{msg}"
30
49
  end
31
50
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # These can be used for unit tests, but also more generally.
4
+
5
+ require_relative "file_helpers"
6
+
7
+ module Scarpe::Components::ProcessHelpers
8
+ include Scarpe::Components::FileHelpers
9
+
10
+ # Run the command and capture its stdout and stderr output, and whether
11
+ # it succeeded or failed. Return after the command has completed.
12
+ # The awkward name is because this is normally a component of another
13
+ # library. Ordinarily you'd want to raise a library-specific exception
14
+ # on failure, print a library-specific message or delimiter, or otherwise
15
+ # handle success and failure. This is too general as-is.
16
+ #
17
+ # @param cmd [String,Array<String>] the command to run in Kernel#spawn format
18
+ # @return [Array(String,String,bool)] the stdout output, stderr output and success/failure of the command in a 3-element Array
19
+ def run_out_err_result(cmd)
20
+ out_str = ""
21
+ err_str = ""
22
+ success = nil
23
+
24
+ with_tempfiles([
25
+ ["scarpe_cmd_stdout", ""],
26
+ ["scarpe_cmd_stderr", ""],
27
+ ]) do |stdout_file, stderr_file|
28
+ pid = Kernel.spawn(cmd, out: stdout_file, err: stderr_file)
29
+ Process.wait(pid)
30
+ success = $?.success?
31
+ out_str = File.read stdout_file
32
+ err_str = File.read stderr_file
33
+ end
34
+
35
+ [out_str, err_str, success]
36
+ end
37
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Scarpe; end
3
+ module Scarpe; end
4
4
  module Scarpe::Components; end
5
- class Scarpe
5
+ module Scarpe
6
6
  # Scarpe::Promise is a promises library, but one with no form of built-in
7
7
  # concurrency. Instead, promise callbacks are executed synchronously.
8
8
  # Even execution is usually synchronous, but can also be handled manually
@@ -198,7 +198,7 @@ class Scarpe
198
198
  case @state
199
199
  when :fulfilled
200
200
  # Should this be a no-op instead?
201
- raise "Registering an executor on an already fulfilled promise means it will never run!"
201
+ raise Scarpe::NoOperationError, "Registering an executor on an already fulfilled promise means it will never run!"
202
202
  when :rejected
203
203
  return
204
204
  when :unscheduled
@@ -207,7 +207,7 @@ class Scarpe
207
207
  @executor = block
208
208
  call_executor
209
209
  else
210
- raise "Internal error, illegal state!"
210
+ raise Scarpe::InternalError, "Internal error, illegal state!"
211
211
  end
212
212
 
213
213
  self
@@ -222,16 +222,16 @@ class Scarpe
222
222
 
223
223
  # First, filter out illegal input
224
224
  unless PROMISE_STATES.include?(old_state)
225
- raise "Internal Promise error! Internal state was #{old_state.inspect}! Legal states: #{PROMISE_STATES.inspect}"
225
+ raise Scarpe::InternalError, "Internal Promise error! Internal state was #{old_state.inspect}! Legal states: #{PROMISE_STATES.inspect}"
226
226
  end
227
227
 
228
228
  unless PROMISE_STATES.include?(new_state)
229
- raise "Internal Promise error! Internal state was set to #{new_state.inspect}! " +
229
+ raise Scarpe::InternalError, "Internal Promise error! Internal state was set to #{new_state.inspect}! " +
230
230
  "Legal states: #{PROMISE_STATES.inspect}"
231
231
  end
232
232
 
233
233
  if new_state != :fulfilled && new_state != :rejected && !value_or_reason.nil?
234
- raise "Internal promise error! Non-completed state transitions should not specify a value or reason!"
234
+ raise Scarpe::InternalError, "Internal promise error! Non-completed state transitions should not specify a value or reason!"
235
235
  end
236
236
 
237
237
  # Here's our state-transition grid for what we're doing here.
@@ -254,11 +254,11 @@ class Scarpe
254
254
 
255
255
  # Transitioning to any *different* state after being fulfilled or rejected? Nope. Those states are final.
256
256
  if complete?
257
- raise "Internal Promise error! Trying to change state from #{old_state.inspect} to #{new_state.inspect}!"
257
+ raise Scarpe::InternalError, "Internal Promise error! Trying to change state from #{old_state.inspect} to #{new_state.inspect}!"
258
258
  end
259
259
 
260
260
  if old_state == :pending && new_state == :unscheduled
261
- raise "Can't change state from :pending to :unscheduled! Scheduling is not reversible!"
261
+ raise Shoes::Errors::InvalidAttributeValueError, "Can't change state from :pending to :unscheduled! Scheduling is not reversible!"
262
262
  end
263
263
 
264
264
  # The next three checks should all be followed by calling handlers for the newly-changed state.
@@ -341,7 +341,7 @@ class Scarpe
341
341
  @on_scheduled.each { |h| h.call(*@parents.map(&:returned_value)) }
342
342
  @on_scheduled = []
343
343
  else
344
- raise "Internal error! Trying to call handlers for #{state.inspect}!"
344
+ raise Scarpe::InternalError, "Internal error! Trying to call handlers for #{state.inspect}!"
345
345
  end
346
346
  end
347
347
 
@@ -367,7 +367,7 @@ class Scarpe
367
367
  end
368
368
 
369
369
  def call_executor
370
- raise("Internal error! Should not call_executor with no executor!") unless @executor
370
+ raise(Scarpe::InternalError, "Internal error! Should not call_executor with no executor!") unless @executor
371
371
 
372
372
  begin
373
373
  result = @executor.call(*@parents.map(&:returned_value))
@@ -389,7 +389,7 @@ class Scarpe
389
389
  # @return [Scarpe::Promise] self
390
390
  def on_fulfilled(&handler)
391
391
  unless handler
392
- raise "You must pass a block to on_fulfilled!"
392
+ raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_fulfilled!"
393
393
  end
394
394
 
395
395
  case @state
@@ -411,7 +411,7 @@ class Scarpe
411
411
  # @return [Scarpe::Promise] self
412
412
  def on_rejected(&handler)
413
413
  unless handler
414
- raise "You must pass a block to on_rejected!"
414
+ raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_rejected!"
415
415
  end
416
416
 
417
417
  case @state
@@ -434,7 +434,7 @@ class Scarpe
434
434
  # @return [Scarpe::Promise] self
435
435
  def on_scheduled(&handler)
436
436
  unless handler
437
- raise "You must pass a block to on_scheduled!"
437
+ raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_scheduled!"
438
438
  end
439
439
 
440
440
  # Add a pending handler or call it now
@@ -14,7 +14,7 @@ module Scarpe::Components
14
14
  # @return <void>
15
15
  def add_segment_type(type, handler)
16
16
  if segment_type_hash.key?(type)
17
- raise "Segment type #{type.inspect} already exists!"
17
+ raise Shoes::Errors::InvalidAttributeValueError, "Segment type #{type.inspect} already exists!"
18
18
  end
19
19
 
20
20
  segment_type_hash[type] = handler
@@ -22,11 +22,23 @@ module Scarpe::Components
22
22
 
23
23
  # Return an Array of segment type labels, such as "code" and "app_test".
24
24
  #
25
- # @return Array<String> the segment type labels
25
+ # @return [Array<String>] the segment type labels
26
26
  def segment_types
27
27
  segment_type_hash.keys
28
28
  end
29
29
 
30
+ # Normally a Shoes application will want to keep the default segment types,
31
+ # which allow loading a Shoes app and running a test inside. But sometimes
32
+ # the default handler will be wrong and a library will want to register
33
+ # its own "shoes" and "app_test" segment handlers, or not have any at all.
34
+ # For those applications, it makes sense to clear all segment types before
35
+ # registering its own.
36
+ #
37
+ # @return <void>
38
+ def remove_all_segment_types!
39
+ @segment_type_hash = {}
40
+ end
41
+
30
42
  # Load a .sca file with an optional YAML frontmatter prefix and
31
43
  # multiple file sections which can be treated differently.
32
44
  #
@@ -38,7 +50,7 @@ module Scarpe::Components
38
50
  # @param path [String] the file or directory to treat as a Scarpe app
39
51
  # @return [Boolean] return true if the file is loaded as a segmented Scarpe app file
40
52
  def call(path)
41
- return false unless path.end_with?(".scas")
53
+ return false unless path.end_with?(".scas") || path.end_with?(".sspec")
42
54
 
43
55
  file_load(path)
44
56
  true
@@ -55,14 +67,12 @@ module Scarpe::Components
55
67
  @after_load << block
56
68
  end
57
69
 
58
- private
59
-
60
- def gen_name(segmap)
70
+ def self.gen_name(segmap)
61
71
  ctr = (1..10_000).detect { |i| !segmap.key?("%5d" % i) }
62
72
  "%5d" % ctr
63
73
  end
64
74
 
65
- def tokenize_segments(contents)
75
+ def self.front_matter_and_segments_from_file(contents)
66
76
  require "yaml" # Only load when needed
67
77
  require "English"
68
78
 
@@ -90,12 +100,16 @@ module Scarpe::Components
90
100
  segments.each do |segment|
91
101
  if segment =~ /\A-* +(.*?)\n/
92
102
  # named segment with separator
93
- segmap[::Regexp.last_match(1)] = ::Regexp.last_match.post_match
103
+ name = ::Regexp.last_match(1)
104
+
105
+ raise("Duplicate segment name: #{name.inspect}!") if segmap.key?(name)
106
+
107
+ segmap[name] = ::Regexp.last_match.post_match
94
108
  elsif segment =~ /\A-* *\n/
95
109
  # unnamed segment with separator
96
110
  segmap[gen_name(segmap)] = ::Regexp.last_match.post_match
97
111
  else
98
- raise "Internal error when parsing segments in segmented app file! seg: #{segment.inspect}"
112
+ raise Scarpe::InternalError, "Internal error when parsing segments in segmented app file! seg: #{segment.inspect}"
99
113
  end
100
114
  end
101
115
 
@@ -105,19 +119,19 @@ module Scarpe::Components
105
119
  def file_load(path)
106
120
  contents = File.read(path)
107
121
 
108
- front_matter, segmap = tokenize_segments(contents)
122
+ front_matter, segmap = self.class.front_matter_and_segments_from_file(contents)
109
123
 
110
124
  if segmap.empty?
111
- raise "Illegal segmented Scarpe file: must have at least one code segment, not just front matter!"
125
+ raise Scarpe::FileContentError, "Illegal segmented Scarpe file: must have at least one code segment, not just front matter!"
112
126
  end
113
127
 
114
128
  if front_matter[:segments]
115
129
  if front_matter[:segments].size != segmap.size
116
- raise "Number of front matter :segments must equal number of file segments!"
130
+ raise Scarpe::FileContentError, "Number of front matter :segments must equal number of file segments!"
117
131
  end
118
132
  else
119
133
  if segmap.size > 2
120
- raise "Segmented files with more than two segments have to specify what they're for!"
134
+ raise Scarpe::FileContentError, "Segmented files with more than two segments have to specify what they're for!"
121
135
  end
122
136
 
123
137
  # Set to default of shoes code only or shoes code and app test code.
@@ -132,7 +146,7 @@ module Scarpe::Components
132
146
  tf_specs = []
133
147
  front_matter[:segments].each.with_index do |seg_type, idx|
134
148
  unless sth.key?(seg_type)
135
- raise "Unrecognized segment type #{seg_type.inspect}! No matching segment type available!"
149
+ raise Scarpe::FileContentError, "Unrecognized segment type #{seg_type.inspect}! No matching segment type available!"
136
150
  end
137
151
 
138
152
  tf_specs << ["scarpe_#{seg_type}_segment_contents", sv[idx]]
@@ -147,18 +161,23 @@ module Scarpe::Components
147
161
  # Need to call @after_load hooks while tempfiles still exist
148
162
  if @after_load && !@after_load.empty?
149
163
  @after_load.each(&:call)
164
+ @after_load = []
150
165
  end
151
166
  end
152
167
  end
153
168
 
154
169
  # The hash of segment type labels mapped to handlers which will be called.
155
- # Normal client code shouldn't ever call this.
170
+ # This could be called by a display service, a test framework or similar
171
+ # code that wants to define a non-Scarpe-standard file format.
156
172
  #
157
- # @return Hash<String, Object> the name/handler pairs
173
+ # @return [Hash<String, Object>] the name/handler pairs
158
174
  def segment_type_hash
159
175
  @segment_handlers ||= {
160
176
  "shoes" => proc { |seg_file| after_load { load seg_file } },
161
- "app_test" => proc { |seg_file| ENV["SCARPE_APP_TEST"] = seg_file },
177
+ "app_test" => proc do |seg_file|
178
+ ENV["SHOES_SPEC_TEST"] = seg_file
179
+ ENV["SHOES_MINITEST_EXPORT_FILE"] ||= "sspec.json"
180
+ end,
162
181
  }
163
182
  end
164
183
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scarpe; module Components; end; end
4
+ module Scarpe::Components::StringHelpers
5
+ # Cut down from Rails camelize
6
+ def self.camelize(string)
7
+ string = string.sub(/^[a-z\d]*/, &:capitalize)
8
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{::Regexp.last_match(1)}#{::Regexp.last_match(2).capitalize}" }
9
+ end
10
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ # In Italian, tiranti are bootstraps -- the literal pull-on-a-boot kind, not a step to something better.
4
+ # Tiranti.rb builds on calzini.rb, but renders a Bootstrap-decorated version of the HTML output.
5
+ # You can set Tiranti as your HTML renderer and you'll get Bootstrap versions of all the drawables.
6
+ # Tiranti requires Calzini's files because it falls back to Calzini for a lot of its rendering.
7
+
8
+ require "scarpe/components/calzini"
9
+
10
+ # The Tiranti module expects to be included by a class defining
11
+ # the following methods:
12
+ #
13
+ # * html_id - the HTML ID for the specific rendered DOM object
14
+ # * handler_js_code(event_name) - the JS handler code for this DOM object and event name
15
+ # * (optional) display_properties - the display properties for this object, unless overridden in render()
16
+ module Scarpe::Components::Tiranti
17
+ include Scarpe::Components::Calzini
18
+ extend self
19
+
20
+ # Currently we're using Bootswatch 5.
21
+ # Bootswatch themes downloaded from https://bootswatch.com/5/THEME_NAME/bootstrap.css
22
+
23
+ def empty_page_element(theme: ENV["SCARPE_BOOTSTRAP_THEME"] || "sketchy")
24
+ comp_dir = File.expand_path("#{__dir__}/../../..")
25
+ bootstrap_js_url = Scarpe::Webview.asset_server.asset_url("#{comp_dir}/assets/bootstrap-themes/bootstrap.bundle.min.js", url_type: :asset)
26
+ theme_url = Scarpe::Webview.asset_server.asset_url("#{comp_dir}/assets/bootstrap-themes/bootstrap-#{theme}.css", url_type: :asset)
27
+ icons_url = Scarpe::Webview.asset_server.asset_url("#{comp_dir}/assets/bootstrap-themes/bootstrap-icons.min.css", url_type: :asset)
28
+
29
+ <<~HTML
30
+ <html>
31
+ <head id='head-wvroot'>
32
+ <meta charset="utf-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
34
+ <link rel="stylesheet" href=#{theme_url.inspect}>
35
+ <link rel="stylesheet" href=#{icons_url.inspect}>
36
+ <style id='style-wvroot'>
37
+ /** Style resets **/
38
+ body {
39
+ height: 100%;
40
+ overflow: hidden;
41
+ }
42
+ </style>
43
+ </head>
44
+ <body id='body-wvroot'>
45
+ <div id='wrapper-wvroot'></div>
46
+
47
+ <script src=#{bootstrap_js_url}></script>
48
+ </body>
49
+ </html>
50
+ HTML
51
+ end
52
+
53
+ # How do we want to handle theme-specific colours and primary/secondary buttons in Bootstrap?
54
+ # "Disabled" could be checked in properties. Is there any way we can/should use "outline" buttons?
55
+ def button_element(props)
56
+ HTML.render do |h|
57
+ h.button(
58
+ id: html_id,
59
+ type: "button",
60
+ class: props["html_class"] ? "btn #{props["html_class"]}" : "btn btn-primary",
61
+ onclick: handler_js_code("click"), style: button_style(props)
62
+ ) do
63
+ props["text"]
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def button_style(props)
71
+ styles = drawable_style(props)
72
+
73
+ styles[:"background-color"] = props["color"] if props["color"]
74
+ styles[:"padding-top"] = props["padding_top"] if props["padding_top"]
75
+ styles[:"padding-bottom"] = props["padding_bottom"] if props["padding_bottom"]
76
+ styles[:color] = props["text_color"] if props["text_color"]
77
+
78
+ # How do we want to handle font size?
79
+ styles[:"font-size"] = props["font_size"] if props["font_size"]
80
+ styles[:"font-size"] = dimensions_length(text_size(props["size"])) if props["size"]
81
+
82
+ styles[:"font-family"] = props["font"] if props["font"]
83
+
84
+ styles
85
+ end
86
+
87
+ public
88
+
89
+ def alert_element(props)
90
+ onclick = handler_js_code(props["event_name"] || "click")
91
+
92
+ HTML.render do |h|
93
+ h.div(id: html_id, class: "modal", tabindex: -1, role: "dialog", style: alert_overlay_style(props)) do
94
+ h.div(class: "modal-dialog", role: "document") do
95
+ h.div(class: "modal-content", style: alert_modal_style) do
96
+ h.div(class: "modal-header") do
97
+ h.h5(class: "modal-title") { "Alert" }
98
+ h.button(type: "button", class: "close", data_dismiss: "modal", aria_label: "Close") do
99
+ h.span(aria_hidden: "true") { "&times;" }
100
+ end
101
+ end
102
+ h.div(class: "modal-body") do
103
+ h.p { props["text"] }
104
+ end
105
+ h.div(class: "modal-footer") do
106
+ h.button(type: "button", onclick:, class: "btn btn-primary") { "OK" }
107
+ #h.button(type: "button", class: "btn btn-secondary") { "Close" }
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def check_element(props)
116
+ HTML.render do |h|
117
+ h.div class: "form-check" do
118
+ h.input type: :checkbox,
119
+ id: html_id,
120
+ class: "form-check-input",
121
+ onclick: handler_js_code("click"),
122
+ value: props["text"],
123
+ checked: props["checked"],
124
+ style: drawable_style(props)
125
+ end
126
+ end
127
+ end
128
+
129
+ def progress_element(props)
130
+ progress_style = drawable_style(props).merge({
131
+ width: "90%",
132
+ })
133
+ HTML.render do |h|
134
+ h.div(id: html_id, class: "progress", style: progress_style) do
135
+ pct = "%.1f" % ((props["fraction"] || 0.0) * 100.0)
136
+ h.div(
137
+ class: "progress-bar progress-bar-striped progress-bar-animated",
138
+ role: "progressbar",
139
+ "aria-valuenow": pct,
140
+ "aria-valuemin": 0,
141
+ "aria-valuemax": 100,
142
+ style: "width: #{pct}%",
143
+ )
144
+ end
145
+ end
146
+ end
147
+
148
+ def para_element(props, &block)
149
+ ps, _extra = para_style(props)
150
+ size = ps[:"font-size"] || "12px"
151
+ size_int = size.to_i # Mostly useful if it's something like "12px"
152
+ if size.include?("calc") || size.end_with?("%")
153
+ # Very big text!
154
+ props["tag"] = "h2"
155
+ elsif size_int >= 48
156
+ props["tag"] = "h1"
157
+ elsif size_int >= 34
158
+ props["tag"] = "h2"
159
+ elsif size_int >= 26
160
+ props["tag"] = "h3"
161
+ else
162
+ props["tag"] = "p"
163
+ end
164
+
165
+ super
166
+ end
167
+ end