jekyll-svg-viewer 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/LICENSE.txt +22 -0
- data/README.md +110 -0
- data/assets/svg-viewer/css/svg-viewer.css +439 -0
- data/assets/svg-viewer/i18n/locales.json +78 -0
- data/assets/svg-viewer/js/svg-viewer.js +1604 -0
- data/assets/svg-viewer/preview/index.html +178 -0
- data/assets/svg-viewer/preview/preset-builder.css +338 -0
- data/assets/svg-viewer/preview/preset-builder.js +793 -0
- data/lib/jekyll/svg_viewer/asset_manager.rb +129 -0
- data/lib/jekyll/svg_viewer/config.rb +40 -0
- data/lib/jekyll/svg_viewer/preview_page.rb +54 -0
- data/lib/jekyll/svg_viewer/tag.rb +528 -0
- data/lib/jekyll/svg_viewer/version.rb +6 -0
- data/lib/jekyll/svg_viewer.rb +6 -0
- data/lib/jekyll-svg-viewer.rb +11 -0
- metadata +93 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
module SvgViewer
|
|
7
|
+
class Tag < Liquid::Tag
|
|
8
|
+
include ERB::Util
|
|
9
|
+
|
|
10
|
+
ATTRIBUTE_REGEX = /
|
|
11
|
+
(?<key>[a-zA-Z0-9_]+)
|
|
12
|
+
\s*=\s*
|
|
13
|
+
(?<value>
|
|
14
|
+
"(?:[^"\\]|\\.)*" |
|
|
15
|
+
'(?:[^'\\]|\\.)*' |
|
|
16
|
+
[^\s]+
|
|
17
|
+
)
|
|
18
|
+
/x.freeze
|
|
19
|
+
|
|
20
|
+
ATTR_ALIASES = {
|
|
21
|
+
"button_bg" => "button_fill",
|
|
22
|
+
"button_background" => "button_fill",
|
|
23
|
+
"button_fg" => "button_foreground",
|
|
24
|
+
"pan" => "pan_mode",
|
|
25
|
+
"zoom_behavior" => "zoom_mode",
|
|
26
|
+
"zoom_interaction" => "zoom_mode",
|
|
27
|
+
"initial_zoom" => "zoom"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
BUTTONS = {
|
|
31
|
+
"zoom_in" => {
|
|
32
|
+
"class" => "zoom-in-btn",
|
|
33
|
+
"icon" => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 176C258.7 176 248 186.7 248 200L248 248L200 248C186.7 248 176 258.7 176 272C176 285.3 186.7 296 200 296L248 296L248 344C248 357.3 258.7 368 272 368C285.3 368 296 357.3 296 344L296 296L344 296C357.3 296 368 285.3 368 272C368 258.7 357.3 248 344 248L296 248L296 200C296 186.7 285.3 176 272 176z"/></svg>',
|
|
34
|
+
"text" => "Zoom In",
|
|
35
|
+
"title" => "Zoom In (Ctrl +)",
|
|
36
|
+
"requires_show_coords" => false
|
|
37
|
+
},
|
|
38
|
+
"zoom_out" => {
|
|
39
|
+
"class" => "zoom-out-btn",
|
|
40
|
+
"icon" => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM200 248C186.7 248 176 258.7 176 272C176 285.3 186.7 296 200 296L344 296C357.3 296 368 285.3 368 272C368 258.7 357.3 248 344 248L200 248z"/></svg>',
|
|
41
|
+
"text" => "Zoom Out",
|
|
42
|
+
"title" => "Zoom Out (Ctrl -)",
|
|
43
|
+
"requires_show_coords" => false
|
|
44
|
+
},
|
|
45
|
+
"reset" => {
|
|
46
|
+
"class" => "reset-zoom-btn",
|
|
47
|
+
"icon" => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 416C351.5 416 416 351.5 416 272C416 192.5 351.5 128 272 128C192.5 128 128 192.5 128 272C128 351.5 192.5 416 272 416z"/></svg>',
|
|
48
|
+
"text" => "Reset Zoom",
|
|
49
|
+
"title" => "Reset Zoom",
|
|
50
|
+
"requires_show_coords" => false
|
|
51
|
+
},
|
|
52
|
+
"center" => {
|
|
53
|
+
"class" => "center-view-btn",
|
|
54
|
+
"icon" => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M320 48C337.7 48 352 62.3 352 80L352 98.3C450.1 112.3 527.7 189.9 541.7 288L560 288C577.7 288 592 302.3 592 320C592 337.7 577.7 352 560 352L541.7 352C527.7 450.1 450.1 527.7 352 541.7L352 560C352 577.7 337.7 592 320 592C302.3 592 288 577.7 288 560L288 541.7C189.9 527.7 112.3 450.1 98.3 352L80 352C62.3 352 48 337.7 48 320C48 302.3 62.3 288 80 288L98.3 288C112.3 189.9 189.9 112.3 288 98.3L288 80C288 62.3 302.3 48 320 48zM163.2 352C175.9 414.7 225.3 464.1 288 476.8L288 464C288 446.3 302.3 432 320 432C337.7 432 352 446.3 352 464L352 476.8C414.7 464.1 464.1 414.7 476.8 352L464 352C446.3 352 432 337.7 432 320C432 302.3 446.3 288 464 288L476.8 288C464.1 225.3 414.7 175.9 352 163.2L352 176C352 193.7 337.7 208 320 208C302.3 208 288 193.7 288 176L288 163.2C225.3 175.9 175.9 225.3 163.2 288L176 288C193.7 288 208 302.3 208 320C208 337.7 193.7 352 176 352L163.2 352zM320 272C346.5 272 368 293.5 368 320C368 346.5 346.5 368 320 368C293.5 368 272 346.5 272 320C272 293.5 293.5 272 320 272z"/></svg>',
|
|
55
|
+
"text" => "Center View",
|
|
56
|
+
"title" => "Center View",
|
|
57
|
+
"requires_show_coords" => false
|
|
58
|
+
},
|
|
59
|
+
"coords" => {
|
|
60
|
+
"class" => "coord-copy-btn",
|
|
61
|
+
"icon" => "📍",
|
|
62
|
+
"text" => "Copy Center",
|
|
63
|
+
"title" => "Copy current center coordinates",
|
|
64
|
+
"requires_show_coords" => true
|
|
65
|
+
}
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
MODE_OPTIONS = %w[icon text both].freeze
|
|
69
|
+
STYLE_OPTIONS = %w[compact labels-on-hover labels_on_hover].freeze
|
|
70
|
+
HIDDEN_OPTIONS = %w[hidden none].freeze
|
|
71
|
+
ALIGNMENT_OPTIONS = %w[alignleft aligncenter alignright].freeze
|
|
72
|
+
AVAILABLE_BUTTONS = BUTTONS.keys.freeze
|
|
73
|
+
|
|
74
|
+
def initialize(tag_name, markup, tokens)
|
|
75
|
+
super
|
|
76
|
+
@raw_markup = markup.to_s
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render(context)
|
|
80
|
+
site = context.registers[:site]
|
|
81
|
+
page = context.registers[:page]
|
|
82
|
+
config = Config.for(site)
|
|
83
|
+
|
|
84
|
+
attrs = merge_defaults(config["defaults"], parse_attributes(@raw_markup))
|
|
85
|
+
attrs["show_coords"] = truthy?(attrs["show_coords"])
|
|
86
|
+
|
|
87
|
+
validate_src!(attrs["src"])
|
|
88
|
+
|
|
89
|
+
viewer_id = "svg-viewer-#{SecureRandom.hex(6)}"
|
|
90
|
+
|
|
91
|
+
zoom_values = normalize_zoom_values(attrs)
|
|
92
|
+
interaction = resolve_interaction(attrs["pan_mode"], attrs["zoom_mode"])
|
|
93
|
+
controls = parse_controls_config(attrs["controls_position"], attrs["controls_buttons"], attrs["show_coords"])
|
|
94
|
+
controls_markup = render_controls_markup(
|
|
95
|
+
viewer_id,
|
|
96
|
+
controls,
|
|
97
|
+
zoom_values[:initial_percent],
|
|
98
|
+
zoom_values[:min_percent],
|
|
99
|
+
zoom_values[:max_percent],
|
|
100
|
+
zoom_values[:step_percent]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
wrapper_classes = build_wrapper_classes(attrs, controls, interaction)
|
|
104
|
+
main_classes = main_container_classes(controls)
|
|
105
|
+
wrapper_style = build_wrapper_style(attrs)
|
|
106
|
+
|
|
107
|
+
AssetManager.flag_page_for_assets(site, page)
|
|
108
|
+
|
|
109
|
+
interaction_caption = interaction[:messages].empty? ? "" : %(<div class="svg-viewer-caption svg-viewer-interaction-caption">#{interaction[:messages].map { |m| h(m) }.join("<br />")}</div>)
|
|
110
|
+
caption_markup = caption_html(attrs["caption"])
|
|
111
|
+
title_markup = title_html(attrs["title"])
|
|
112
|
+
|
|
113
|
+
<<~HTML
|
|
114
|
+
<div id="#{viewer_id}" class="#{wrapper_classes}" style="#{wrapper_style}">
|
|
115
|
+
#{title_markup}
|
|
116
|
+
<div class="#{main_classes}">
|
|
117
|
+
#{controls_markup}
|
|
118
|
+
<div class="svg-container" style="height: #{h(attrs['height'])}; width: 100%; max-width: 100%; min-width: 0;" data-viewer="#{viewer_id}">
|
|
119
|
+
<div class="svg-viewport" data-viewer="#{viewer_id}"></div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
#{interaction_caption}
|
|
123
|
+
#{caption_markup}
|
|
124
|
+
</div>
|
|
125
|
+
#{viewer_bootstrap(viewer_id, attrs, zoom_values, interaction, controls, site)}
|
|
126
|
+
HTML
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def parse_attributes(markup)
|
|
132
|
+
return {} if markup.strip.empty?
|
|
133
|
+
|
|
134
|
+
markup.scan(ATTRIBUTE_REGEX).each_with_object({}) do |match, attrs|
|
|
135
|
+
key = match[0]
|
|
136
|
+
value = unquote(match[1])
|
|
137
|
+
canonical = ATTR_ALIASES.fetch(key, key)
|
|
138
|
+
attrs[canonical] = value
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def unquote(value)
|
|
143
|
+
return "" if value.nil?
|
|
144
|
+
|
|
145
|
+
if value.start_with?('"') && value.end_with?('"')
|
|
146
|
+
value[1...-1]
|
|
147
|
+
elsif value.start_with?("'") && value.end_with?("'")
|
|
148
|
+
value[1...-1]
|
|
149
|
+
else
|
|
150
|
+
value
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def merge_defaults(defaults, overrides)
|
|
155
|
+
overrides = overrides.dup
|
|
156
|
+
defaults.each_with_object(overrides) do |(key, val), merged|
|
|
157
|
+
merged[key] = overrides.key?(key) ? overrides[key] : val
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def validate_src!(src)
|
|
162
|
+
raise Liquid::ArgumentError, "svg_viewer: src attribute is required" if src.to_s.strip.empty?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def normalize_zoom_values(attrs)
|
|
166
|
+
initial = safe_number(attrs["zoom"], 100.0)
|
|
167
|
+
min = safe_number(attrs["min_zoom"], 25.0)
|
|
168
|
+
max = [safe_number(attrs["max_zoom"], 800.0), initial].max
|
|
169
|
+
step = safe_number(attrs["zoom_step"], 10.0)
|
|
170
|
+
|
|
171
|
+
{
|
|
172
|
+
initial: initial / 100.0,
|
|
173
|
+
min: min / 100.0,
|
|
174
|
+
max: max / 100.0,
|
|
175
|
+
step: [step / 100.0, 0.001].max,
|
|
176
|
+
initial_percent: initial.round,
|
|
177
|
+
min_percent: min.round,
|
|
178
|
+
max_percent: max.round,
|
|
179
|
+
step_percent: [step.round, 1].max
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def resolve_interaction(pan_value, zoom_value)
|
|
184
|
+
pan_mode = normalize_pan(pan_value)
|
|
185
|
+
zoom_mode = normalize_zoom(zoom_value)
|
|
186
|
+
messages = []
|
|
187
|
+
|
|
188
|
+
pan_mode = "drag" if zoom_mode == "scroll" && pan_mode == "scroll"
|
|
189
|
+
|
|
190
|
+
case zoom_mode
|
|
191
|
+
when "click"
|
|
192
|
+
messages << "Cmd/Ctrl-click to zoom in, Option/Alt-click to zoom out."
|
|
193
|
+
when "scroll"
|
|
194
|
+
messages << "Scroll up to zoom in, scroll down to zoom out."
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if pan_mode == "drag"
|
|
198
|
+
if zoom_mode == "scroll"
|
|
199
|
+
messages << "Drag to pan around the image while scrolling zooms."
|
|
200
|
+
else
|
|
201
|
+
messages << "Drag to pan around the image."
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
{ pan_mode: pan_mode, zoom_mode: zoom_mode, messages: messages.uniq }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def parse_controls_config(position, buttons_setting, show_coords)
|
|
209
|
+
position = %w[top bottom left right].include?(position) ? position : "top"
|
|
210
|
+
normalized_setting = buttons_setting.to_s.strip
|
|
211
|
+
normalized_setting = "both" if normalized_setting.empty?
|
|
212
|
+
tokens = normalized_setting.downcase.tr(":", ",").split(",").map(&:strip).reject(&:empty?)
|
|
213
|
+
|
|
214
|
+
has_slider = tokens.include?("slider")
|
|
215
|
+
slider_explicit_zoom_in = tokens.include?("zoom_in")
|
|
216
|
+
slider_explicit_zoom_out = tokens.include?("zoom_out")
|
|
217
|
+
|
|
218
|
+
mode = "both"
|
|
219
|
+
styles = []
|
|
220
|
+
alignment = "alignleft"
|
|
221
|
+
buttons = default_buttons(show_coords)
|
|
222
|
+
is_custom = false
|
|
223
|
+
|
|
224
|
+
token = normalized_setting.downcase
|
|
225
|
+
if HIDDEN_OPTIONS.include?(token)
|
|
226
|
+
mode = "hidden"
|
|
227
|
+
buttons = []
|
|
228
|
+
elsif token == "minimal"
|
|
229
|
+
buttons = %w[zoom_in zoom_out center]
|
|
230
|
+
buttons << "coords" if show_coords
|
|
231
|
+
elsif token == "slider"
|
|
232
|
+
has_slider = true
|
|
233
|
+
buttons = default_buttons_without_zoom(show_coords)
|
|
234
|
+
elsif STYLE_OPTIONS.include?(token)
|
|
235
|
+
styles << token.tr("_", "-")
|
|
236
|
+
elsif ALIGNMENT_OPTIONS.include?(token)
|
|
237
|
+
alignment = token
|
|
238
|
+
elsif MODE_OPTIONS.include?(token)
|
|
239
|
+
mode = token
|
|
240
|
+
elsif token == "custom" || normalized_setting.include?(",")
|
|
241
|
+
is_custom = true
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
if is_custom
|
|
245
|
+
parts = normalized_setting.tr(":", ",").split(",").map(&:strip).reject(&:empty?)
|
|
246
|
+
parts.shift if parts.first&.casecmp?("custom")
|
|
247
|
+
|
|
248
|
+
custom_mode = nil
|
|
249
|
+
custom_styles = []
|
|
250
|
+
|
|
251
|
+
if parts.first&.casecmp?("slider")
|
|
252
|
+
has_slider = true
|
|
253
|
+
parts.shift
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
if parts.first && MODE_OPTIONS.include?(parts.first.downcase)
|
|
257
|
+
custom_mode = parts.shift.downcase
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if parts.first && ALIGNMENT_OPTIONS.include?(parts.first.downcase)
|
|
261
|
+
alignment = parts.shift.downcase
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
custom_styles += parts.select { |p| STYLE_OPTIONS.include?(p.downcase) }.map { |p| p.tr("_", "-") }
|
|
265
|
+
parts -= custom_styles
|
|
266
|
+
|
|
267
|
+
if mode != "hidden"
|
|
268
|
+
custom_buttons = parts.map(&:downcase).uniq.select do |key|
|
|
269
|
+
next false if key == "slider"
|
|
270
|
+
next false if key == "coords" && !show_coords
|
|
271
|
+
|
|
272
|
+
AVAILABLE_BUTTONS.include?(key)
|
|
273
|
+
end
|
|
274
|
+
if custom_buttons.any?
|
|
275
|
+
buttons = custom_buttons
|
|
276
|
+
elsif has_slider
|
|
277
|
+
buttons = default_buttons_without_zoom(show_coords)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
mode = custom_mode if custom_mode
|
|
282
|
+
styles.concat(custom_styles)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
if mode != "hidden"
|
|
286
|
+
if show_coords
|
|
287
|
+
buttons << "coords" unless buttons.include?("coords")
|
|
288
|
+
else
|
|
289
|
+
buttons = buttons.reject { |b| b == "coords" }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
buttons = default_buttons(show_coords) if buttons.empty?
|
|
293
|
+
else
|
|
294
|
+
buttons = []
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
styles = styles.map { |s| s.tr("_", "-").downcase }.uniq
|
|
298
|
+
|
|
299
|
+
if has_slider && !slider_explicit_zoom_in
|
|
300
|
+
buttons = buttons.reject { |b| b == "zoom_in" }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if has_slider && !slider_explicit_zoom_out
|
|
304
|
+
buttons = buttons.reject { |b| b == "zoom_out" }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
{
|
|
308
|
+
position: position,
|
|
309
|
+
mode: mode,
|
|
310
|
+
styles: styles,
|
|
311
|
+
alignment: ALIGNMENT_OPTIONS.include?(alignment) ? alignment : "alignleft",
|
|
312
|
+
buttons: buttons,
|
|
313
|
+
has_slider: has_slider
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def default_buttons(show_coords)
|
|
318
|
+
list = %w[zoom_in zoom_out reset center]
|
|
319
|
+
list << "coords" if show_coords
|
|
320
|
+
list
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def default_buttons_without_zoom(show_coords)
|
|
324
|
+
default_buttons(show_coords).reject { |b| %w[zoom_in zoom_out].include?(b) }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def render_controls_markup(viewer_id, config, initial_percent, min_percent, max_percent, step_percent)
|
|
328
|
+
return "" if config[:mode] == "hidden"
|
|
329
|
+
|
|
330
|
+
classes = ["svg-controls", "controls-mode-#{config[:mode]}", "controls-align-#{config[:alignment]}"]
|
|
331
|
+
classes.concat(config[:styles].map { |s| "controls-style-#{s}" })
|
|
332
|
+
classes << "controls-vertical" if %w[left right].include?(config[:position])
|
|
333
|
+
class_attribute = classes.map { |cls| h(cls) }.uniq.join(" ")
|
|
334
|
+
|
|
335
|
+
buttons = config[:buttons].map { |key| BUTTONS[key] }.compact
|
|
336
|
+
has_coords_button = config[:buttons].include?("coords")
|
|
337
|
+
|
|
338
|
+
slider_markup = ""
|
|
339
|
+
if config[:has_slider]
|
|
340
|
+
slider_markup = <<~HTML
|
|
341
|
+
<div class="zoom-slider-wrapper">
|
|
342
|
+
<input type="range" class="zoom-slider" data-viewer="#{h(viewer_id)}"
|
|
343
|
+
min="#{min_percent}" max="#{max_percent}"
|
|
344
|
+
step="#{step_percent}" value="#{initial_percent}"
|
|
345
|
+
aria-label="Zoom level"
|
|
346
|
+
aria-valuemin="#{min_percent}"
|
|
347
|
+
aria-valuemax="#{max_percent}"
|
|
348
|
+
aria-valuenow="#{initial_percent}" />
|
|
349
|
+
</div>
|
|
350
|
+
HTML
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
buttons_markup = buttons.map do |definition|
|
|
354
|
+
<<~HTML
|
|
355
|
+
<button type="button"
|
|
356
|
+
class="svg-viewer-btn #{h(definition['class'])}"
|
|
357
|
+
data-viewer="#{h(viewer_id)}"
|
|
358
|
+
title="#{h(definition['title'])}"
|
|
359
|
+
aria-label="#{h(definition['text'])}">
|
|
360
|
+
<span class="btn-icon" aria-hidden="true">#{definition['icon']}</span>
|
|
361
|
+
<span class="btn-text">#{h(definition['text'])}</span>
|
|
362
|
+
</button>
|
|
363
|
+
HTML
|
|
364
|
+
end.join
|
|
365
|
+
|
|
366
|
+
coord_output =
|
|
367
|
+
if has_coords_button
|
|
368
|
+
%(<span class="coord-output" data-viewer="#{h(viewer_id)}" aria-live="polite"></span>)
|
|
369
|
+
else
|
|
370
|
+
""
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
<<~HTML
|
|
374
|
+
<div class="#{class_attribute}" data-viewer="#{h(viewer_id)}">
|
|
375
|
+
#{slider_markup}
|
|
376
|
+
#{buttons_markup}
|
|
377
|
+
#{coord_output}
|
|
378
|
+
<div class="divider"></div>
|
|
379
|
+
<span class="zoom-display">
|
|
380
|
+
<span class="zoom-percentage" data-viewer="#{h(viewer_id)}">#{initial_percent}</span>%
|
|
381
|
+
</span>
|
|
382
|
+
</div>
|
|
383
|
+
HTML
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def build_wrapper_classes(attrs, controls, interaction)
|
|
387
|
+
classes = ["svg-viewer-wrapper", "controls-position-#{controls[:position]}", "controls-mode-#{controls[:mode]}", "pan-mode-#{interaction[:pan_mode]}", "zoom-mode-#{interaction[:zoom_mode]}"]
|
|
388
|
+
classes.concat(controls[:styles].map { |s| "controls-style-#{s}" })
|
|
389
|
+
custom = attrs["class"].to_s.strip
|
|
390
|
+
classes << custom unless custom.empty?
|
|
391
|
+
classes.map { |cls| h(cls) }.uniq.join(" ")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def main_container_classes(controls)
|
|
395
|
+
classes = ["svg-viewer-main", "controls-position-#{controls[:position]}", "controls-align-#{controls[:alignment]}"]
|
|
396
|
+
classes.concat(controls[:styles].map { |s| "controls-style-#{s}" })
|
|
397
|
+
classes.map { |cls| h(cls) }.uniq.join(" ")
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def build_wrapper_style(attrs)
|
|
401
|
+
declarations = ["width: 100%", "max-width: 100%", "min-width: 0"]
|
|
402
|
+
declarations.concat(button_color_custom_properties(attrs["button_fill"], attrs["button_border"], attrs["button_foreground"]))
|
|
403
|
+
declarations.join("; ")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def button_color_custom_properties(fill, border, foreground)
|
|
407
|
+
declarations = []
|
|
408
|
+
|
|
409
|
+
fill_color = sanitize_hex_color(fill)
|
|
410
|
+
border_color = sanitize_hex_color(border)
|
|
411
|
+
foreground_color = sanitize_hex_color(foreground)
|
|
412
|
+
|
|
413
|
+
if fill_color
|
|
414
|
+
declarations << "--svg-viewer-button-fill: #{fill_color}"
|
|
415
|
+
if (hover = adjust_color_brightness(fill_color, -12))
|
|
416
|
+
declarations << "--svg-viewer-button-hover: #{hover}"
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
border_color ||= fill_color
|
|
421
|
+
declarations << "--svg-viewer-button-border: #{border_color}" if border_color
|
|
422
|
+
declarations << "--svg-viewer-button-text: #{foreground_color}" if foreground_color
|
|
423
|
+
declarations
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def sanitize_hex_color(color)
|
|
427
|
+
return nil if color.to_s.strip.empty?
|
|
428
|
+
hex = color.to_s.strip
|
|
429
|
+
hex = "##{hex}" unless hex.start_with?("#")
|
|
430
|
+
return nil unless hex.match?(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)
|
|
431
|
+
hex.downcase
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def adjust_color_brightness(hex_color, percentage)
|
|
435
|
+
hex = hex_color.delete_prefix("#")
|
|
436
|
+
hex = hex.chars.map { |ch| ch * 2 }.join if hex.length == 3
|
|
437
|
+
return nil unless hex.length == 6
|
|
438
|
+
|
|
439
|
+
components = [hex[0..1], hex[2..3], hex[4..5]].map { |pair| pair.to_i(16) }
|
|
440
|
+
factor = percentage / 100.0
|
|
441
|
+
components.map! do |component|
|
|
442
|
+
value = component + (percentage.positive? ? (255 - component) * factor : component * factor)
|
|
443
|
+
[[value.round, 0].max, 255].min
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
format("#%02x%02x%02x", *components)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def viewer_bootstrap(viewer_id, attrs, zoom_values, interaction, controls, site)
|
|
450
|
+
config_hash = site.config.fetch("svg_viewer", {})
|
|
451
|
+
locale = config_hash.is_a?(Hash) ? config_hash["locale"] : nil
|
|
452
|
+
i18n_payload = AssetManager.locale_payload(site)
|
|
453
|
+
|
|
454
|
+
options = {
|
|
455
|
+
viewerId: viewer_id,
|
|
456
|
+
svgUrl: attrs["src"],
|
|
457
|
+
initialZoom: zoom_values[:initial],
|
|
458
|
+
minZoom: zoom_values[:min],
|
|
459
|
+
maxZoom: zoom_values[:max],
|
|
460
|
+
zoomStep: zoom_values[:step],
|
|
461
|
+
centerX: numeric_value_or_nil(attrs["center_x"]),
|
|
462
|
+
centerY: numeric_value_or_nil(attrs["center_y"]),
|
|
463
|
+
showCoordinates: attrs["show_coords"],
|
|
464
|
+
panMode: interaction[:pan_mode],
|
|
465
|
+
zoomMode: interaction[:zoom_mode],
|
|
466
|
+
controlsConfig: controls,
|
|
467
|
+
buttonFill: attrs["button_fill"],
|
|
468
|
+
buttonBorder: attrs["button_border"],
|
|
469
|
+
buttonForeground: attrs["button_foreground"],
|
|
470
|
+
locale: locale || "en",
|
|
471
|
+
i18n: i18n_payload
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
<<~HTML
|
|
475
|
+
<script>
|
|
476
|
+
window.svgViewerInstances ||= {};
|
|
477
|
+
window.__SVG_VIEWER_I18N__ = #{JSON.generate(i18n_payload)};
|
|
478
|
+
(function init() {
|
|
479
|
+
if (typeof SVGViewer === "undefined") {
|
|
480
|
+
return setTimeout(init, 50);
|
|
481
|
+
}
|
|
482
|
+
var options = #{JSON.generate(options)};
|
|
483
|
+
window.svgViewerInstances["#{viewer_id}"] = new SVGViewer(options);
|
|
484
|
+
})();
|
|
485
|
+
</script>
|
|
486
|
+
HTML
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def truthy?(value)
|
|
490
|
+
return value if value == true || value == false
|
|
491
|
+
%w[true 1 yes on].include?(value.to_s.strip.downcase)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def safe_number(value, default)
|
|
495
|
+
Float(value.to_s.strip)
|
|
496
|
+
rescue ArgumentError, TypeError
|
|
497
|
+
default
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def numeric_value_or_nil(value)
|
|
501
|
+
Float(value.to_s.strip)
|
|
502
|
+
rescue ArgumentError, TypeError
|
|
503
|
+
nil
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def normalize_pan(value)
|
|
507
|
+
value.to_s.strip.downcase == "drag" ? "drag" : "scroll"
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def normalize_zoom(value)
|
|
511
|
+
normalized = value.to_s.strip.downcase.tr(" -", "_")
|
|
512
|
+
return normalized if %w[click scroll].include?(normalized)
|
|
513
|
+
"super_scroll"
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def title_html(title)
|
|
517
|
+
return "" if title.to_s.strip.empty?
|
|
518
|
+
%(<div class="svg-viewer-title">#{title}</div>)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def caption_html(caption)
|
|
522
|
+
return "" if caption.to_s.strip.empty?
|
|
523
|
+
%(<div class="svg-viewer-caption">#{caption}</div>)
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "jekyll"
|
|
2
|
+
|
|
3
|
+
require_relative "jekyll/svg_viewer"
|
|
4
|
+
|
|
5
|
+
Jekyll::Hooks.register :site, :after_init do |site|
|
|
6
|
+
Jekyll::SvgViewer::AssetManager.register(site)
|
|
7
|
+
Jekyll::SvgViewer::PreviewPage.register(site)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Liquid::Template.register_tag("svg_viewer", Jekyll::SvgViewer::Tag)
|
|
11
|
+
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jekyll-svg-viewer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Brett Terpstra
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: jekyll
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '5.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '4.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '5.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: liquid
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '4.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '4.0'
|
|
46
|
+
description: Port of the WP SVG Viewer frontend packaged as a Jekyll plugin gem with
|
|
47
|
+
configurable defaults, localized UI strings, and an optional preset builder page.
|
|
48
|
+
email:
|
|
49
|
+
- brett@brettterpstra.com
|
|
50
|
+
executables: []
|
|
51
|
+
extensions: []
|
|
52
|
+
extra_rdoc_files: []
|
|
53
|
+
files:
|
|
54
|
+
- LICENSE.txt
|
|
55
|
+
- README.md
|
|
56
|
+
- assets/svg-viewer/css/svg-viewer.css
|
|
57
|
+
- assets/svg-viewer/i18n/locales.json
|
|
58
|
+
- assets/svg-viewer/js/svg-viewer.js
|
|
59
|
+
- assets/svg-viewer/preview/index.html
|
|
60
|
+
- assets/svg-viewer/preview/preset-builder.css
|
|
61
|
+
- assets/svg-viewer/preview/preset-builder.js
|
|
62
|
+
- lib/jekyll-svg-viewer.rb
|
|
63
|
+
- lib/jekyll/svg_viewer.rb
|
|
64
|
+
- lib/jekyll/svg_viewer/asset_manager.rb
|
|
65
|
+
- lib/jekyll/svg_viewer/config.rb
|
|
66
|
+
- lib/jekyll/svg_viewer/preview_page.rb
|
|
67
|
+
- lib/jekyll/svg_viewer/tag.rb
|
|
68
|
+
- lib/jekyll/svg_viewer/version.rb
|
|
69
|
+
homepage: https://github.com/ttscoff/wp-svg-viewer
|
|
70
|
+
licenses:
|
|
71
|
+
- MIT
|
|
72
|
+
metadata:
|
|
73
|
+
homepage_uri: https://github.com/ttscoff/wp-svg-viewer
|
|
74
|
+
source_code_uri: https://github.com/ttscoff/wp-svg-viewer
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.6.7
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Liquid tag and preset builder for embedding interactive SVG viewers in Jekyll
|
|
92
|
+
sites.
|
|
93
|
+
test_files: []
|