scarpe-components 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +86 -0
- data/lib/scarpe/components/base64.rb +3 -7
- data/lib/scarpe/components/calzini/alert.rb +49 -0
- data/lib/scarpe/components/calzini/art_widgets.rb +203 -0
- data/lib/scarpe/components/calzini/button.rb +39 -0
- data/lib/scarpe/components/calzini/misc.rb +146 -0
- data/lib/scarpe/components/calzini/para.rb +35 -0
- data/lib/scarpe/components/calzini/slots.rb +155 -0
- data/lib/scarpe/components/calzini/text_widgets.rb +65 -0
- data/lib/scarpe/components/calzini.rb +149 -0
- data/lib/scarpe/components/errors.rb +20 -0
- data/lib/scarpe/components/file_helpers.rb +1 -0
- data/lib/scarpe/components/html.rb +131 -0
- data/lib/scarpe/components/minitest_export_reporter.rb +75 -0
- data/lib/scarpe/components/minitest_import_runnable.rb +98 -0
- data/lib/scarpe/components/minitest_result.rb +86 -0
- data/lib/scarpe/components/modular_logger.rb +5 -5
- data/lib/scarpe/components/print_logger.rb +9 -5
- data/lib/scarpe/components/promises.rb +14 -14
- data/lib/scarpe/components/segmented_file_loader.rb +36 -17
- data/lib/scarpe/components/string_helpers.rb +10 -0
- data/lib/scarpe/components/tiranti.rb +225 -0
- data/lib/scarpe/components/unit_test_helpers.rb +45 -5
- data/lib/scarpe/components/version.rb +2 -2
- metadata +18 -2
@@ -0,0 +1,86 @@
|
|
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 error?
|
60
|
+
!@exceptions.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
def fail?
|
64
|
+
!@failures.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def skip?
|
68
|
+
@skip ? true : false
|
69
|
+
end
|
70
|
+
|
71
|
+
def passed?
|
72
|
+
@exceptions.empty? && @failures.empty? && !@skip
|
73
|
+
end
|
74
|
+
|
75
|
+
def error_message
|
76
|
+
@exceptions[0]
|
77
|
+
end
|
78
|
+
|
79
|
+
def fail_message
|
80
|
+
@failures[0]
|
81
|
+
end
|
82
|
+
|
83
|
+
def skip_message
|
84
|
+
@skip
|
85
|
+
end
|
86
|
+
end
|
@@ -7,9 +7,9 @@ require "shoes/log"
|
|
7
7
|
|
8
8
|
# Requires the logging gem
|
9
9
|
|
10
|
-
|
10
|
+
module Scarpe; end
|
11
11
|
module Scarpe::Components; end
|
12
|
-
|
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,30 +3,34 @@
|
|
3
3
|
require "shoes/log"
|
4
4
|
require "json"
|
5
5
|
|
6
|
-
|
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
|
+
end
|
15
|
+
|
12
16
|
def initialize(component_name)
|
13
17
|
@comp_name = component_name
|
14
18
|
end
|
15
19
|
|
16
20
|
def error(msg)
|
17
|
-
puts "#{@comp_name} error: #{msg}"
|
21
|
+
puts "#{@comp_name} error: #{msg}" unless PrintLogger.silence
|
18
22
|
end
|
19
23
|
|
20
24
|
def warn(msg)
|
21
|
-
puts "#{@comp_name} warn: #{msg}"
|
25
|
+
puts "#{@comp_name} warn: #{msg}" unless PrintLogger.silence
|
22
26
|
end
|
23
27
|
|
24
28
|
def debug(msg)
|
25
|
-
puts "#{@comp_name} debug: #{msg}"
|
29
|
+
puts "#{@comp_name} debug: #{msg}" unless PrintLogger.silence
|
26
30
|
end
|
27
31
|
|
28
32
|
def info(msg)
|
29
|
-
puts "#{@comp_name} info: #{msg}"
|
33
|
+
puts "#{@comp_name} info: #{msg}" unless PrintLogger.silence
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module Scarpe; end
|
4
4
|
module Scarpe::Components; end
|
5
|
-
|
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
|
-
|
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
|
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
|
-
|
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 =
|
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
|
-
#
|
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
|
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,225 @@
|
|
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 would ordinarily set either Calzini or Tiranti as the top-level HTML renderer, not both.
|
6
|
+
# You'll include both if you use Tiranti, 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 = [
|
22
|
+
"cerulean",
|
23
|
+
"cosmo",
|
24
|
+
"cyborg",
|
25
|
+
"darkly",
|
26
|
+
"flatly",
|
27
|
+
"journal",
|
28
|
+
"litera",
|
29
|
+
"lumen",
|
30
|
+
"lux",
|
31
|
+
"materia",
|
32
|
+
"minty",
|
33
|
+
"morph",
|
34
|
+
"pulse",
|
35
|
+
"quartz",
|
36
|
+
"sandstone",
|
37
|
+
"simplex",
|
38
|
+
"sketchy",
|
39
|
+
"slate",
|
40
|
+
"solar",
|
41
|
+
"spacelab",
|
42
|
+
"superhero",
|
43
|
+
"united",
|
44
|
+
"vapor",
|
45
|
+
"yeti",
|
46
|
+
"zephyr",
|
47
|
+
]
|
48
|
+
|
49
|
+
BOOTSWATCH_THEME = ENV["SCARPE_BOOTSTRAP_THEME"] || "sketchy"
|
50
|
+
|
51
|
+
def empty_page_element
|
52
|
+
<<~HTML
|
53
|
+
<html>
|
54
|
+
<head id='head-wvroot'>
|
55
|
+
<meta charset="utf-8">
|
56
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
57
|
+
<link rel="stylesheet" href="https://bootswatch.com/5/#{BOOTSWATCH_THEME}/bootstrap.css">
|
58
|
+
<link rel="stylesheet" href="https://bootswatch.com/_vendor/bootstrap-icons/font/bootstrap-icons.min.css">
|
59
|
+
<style id='style-wvroot'>
|
60
|
+
/** Style resets **/
|
61
|
+
body {
|
62
|
+
height: 100%;
|
63
|
+
overflow: hidden;
|
64
|
+
}
|
65
|
+
</style>
|
66
|
+
</head>
|
67
|
+
<body id='body-wvroot'>
|
68
|
+
<div id='wrapper-wvroot'></div>
|
69
|
+
|
70
|
+
<script src="https://bootswatch.com/_vendor/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
71
|
+
</body>
|
72
|
+
</html>
|
73
|
+
HTML
|
74
|
+
end
|
75
|
+
|
76
|
+
# def render_stack
|
77
|
+
# end
|
78
|
+
# def render_flow
|
79
|
+
# end
|
80
|
+
|
81
|
+
# How do we want to handle theme-specific colours and primary/secondary buttons in Bootstrap?
|
82
|
+
# "Disabled" could be checked in properties. Is there any way we can/should use "outline" buttons?
|
83
|
+
def button_element(props)
|
84
|
+
HTML.render do |h|
|
85
|
+
h.button(id: html_id, type: "button", class: "btn btn-primary", onclick: handler_js_code("click"), style: button_style(props)) do
|
86
|
+
props["text"]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def button_style(props)
|
94
|
+
styles = drawable_style(props)
|
95
|
+
|
96
|
+
styles[:"background-color"] = props["color"] if props["color"]
|
97
|
+
styles[:"padding-top"] = props["padding_top"] if props["padding_top"]
|
98
|
+
styles[:"padding-bottom"] = props["padding_bottom"] if props["padding_bottom"]
|
99
|
+
styles[:color] = props["text_color"] if props["text_color"]
|
100
|
+
styles[:width] = dimensions_length(props["width"]) if props["width"]
|
101
|
+
styles[:height] = dimensions_length(props["height"]) if props["height"]
|
102
|
+
styles[:"font-size"] = props["font_size"] if props["font_size"]
|
103
|
+
|
104
|
+
styles[:top] = dimensions_length(props["top"]) if props["top"]
|
105
|
+
styles[:left] = dimensions_length(props["left"]) if props["left"]
|
106
|
+
styles[:position] = "absolute" if props["top"] || props["left"]
|
107
|
+
styles[:"font-size"] = dimensions_length(text_size(props["size"])) if props["size"]
|
108
|
+
styles[:"font-family"] = props["font"] if props["font"]
|
109
|
+
|
110
|
+
styles
|
111
|
+
end
|
112
|
+
|
113
|
+
public
|
114
|
+
|
115
|
+
def alert_element(props)
|
116
|
+
onclick = handler_js_code(props["event_name"] || "click")
|
117
|
+
|
118
|
+
HTML.render do |h|
|
119
|
+
h.div(id: html_id, class: "modal", tabindex: -1, role: "dialog", style: alert_overlay_style(props)) do
|
120
|
+
h.div(class: "modal-dialog", role: "document") do
|
121
|
+
h.div(class: "modal-content", style: alert_modal_style) do
|
122
|
+
h.div(class: "modal-header") do
|
123
|
+
h.h5(class: "modal-title") { "Alert" }
|
124
|
+
h.button(type: "button", class: "close", data_dismiss: "modal", aria_label: "Close") do
|
125
|
+
h.span(aria_hidden: "true") { "×" }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
h.div(class: "modal-body") do
|
129
|
+
h.p { props["text"] }
|
130
|
+
end
|
131
|
+
h.div(class: "modal-footer") do
|
132
|
+
h.button(type: "button", onclick:, class: "btn btn-primary") { "OK" }
|
133
|
+
#h.button(type: "button", class: "btn btn-secondary") { "Close" }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def check_element(props)
|
142
|
+
HTML.render do |h|
|
143
|
+
h.div class: "form-check" do
|
144
|
+
h.input type: :checkbox,
|
145
|
+
id: html_id,
|
146
|
+
class: "form-check-input",
|
147
|
+
onclick: handler_js_code("click"),
|
148
|
+
value: props["text"],
|
149
|
+
checked: props["checked"],
|
150
|
+
style: drawable_style(props)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def progress_element(props)
|
156
|
+
HTML.render do |h|
|
157
|
+
h.div(class: "progress", style: "width: 90%") do
|
158
|
+
pct = "%.1f" % ((props["fraction"] || 0.0) * 100.0)
|
159
|
+
h.div(
|
160
|
+
class: "progress-bar progress-bar-striped progress-bar-animated",
|
161
|
+
role: "progressbar",
|
162
|
+
"aria-valuenow": pct,
|
163
|
+
"aria-valuemin": 0,
|
164
|
+
"aria-valuemax": 100,
|
165
|
+
style: "width: #{pct}%",
|
166
|
+
)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# para_element is a bit of a hard one, since it does not-entirely-trivial
|
172
|
+
# mapping between display objects and IDs. But we don't want Calzini
|
173
|
+
# messing with the display service or display objects.
|
174
|
+
def para_element(props, &block)
|
175
|
+
tag, opts = para_elt_and_opts(props)
|
176
|
+
|
177
|
+
HTML.render do |h|
|
178
|
+
h.send(tag, **opts, &block)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
ELT_AND_SIZE = {
|
185
|
+
inscription: [:p, 10],
|
186
|
+
ins: [:p, 10],
|
187
|
+
para: [:p, 12],
|
188
|
+
caption: [:p, 14],
|
189
|
+
tagline: [:p, 18],
|
190
|
+
subtitle: [:h3, 26],
|
191
|
+
title: [:h2, 34],
|
192
|
+
banner: [:h1, 48],
|
193
|
+
}.freeze
|
194
|
+
|
195
|
+
def para_elt_and_opts(props)
|
196
|
+
elt, size = para_elt_and_size(props)
|
197
|
+
size = dimensions_length(size)
|
198
|
+
|
199
|
+
para_style = drawable_style(props).merge({
|
200
|
+
color: rgb_to_hex(props["stroke"]),
|
201
|
+
"font-size": para_font_size(props),
|
202
|
+
"font-family": props["font"],
|
203
|
+
}.compact)
|
204
|
+
|
205
|
+
opts = (props["html_attributes"] || {}).merge(id: html_id, style: para_style)
|
206
|
+
|
207
|
+
[elt, opts]
|
208
|
+
end
|
209
|
+
|
210
|
+
def para_elt_and_size(props)
|
211
|
+
return [:p, nil] unless props["size"]
|
212
|
+
|
213
|
+
ps = props["size"].to_s.to_sym
|
214
|
+
if ELT_AND_SIZE.key?(ps)
|
215
|
+
ELT_AND_SIZE[ps]
|
216
|
+
else
|
217
|
+
sz = props["size"].to_i
|
218
|
+
if sz > 18
|
219
|
+
[:h2, sz]
|
220
|
+
else
|
221
|
+
[:p, sz]
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|