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.
@@ -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,6 @@
1
+ module Jekyll
2
+ module SvgViewer
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
6
+
@@ -0,0 +1,6 @@
1
+ require_relative "svg_viewer/version"
2
+ require_relative "svg_viewer/config"
3
+ require_relative "svg_viewer/tag"
4
+ require_relative "svg_viewer/asset_manager"
5
+ require_relative "svg_viewer/preview_page"
6
+
@@ -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: []