jekyll-image-links 0.2.0 → 0.3.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9d85919e65b8d1b2cf32c8d1ffccc9b149f8d84e7950020f5d291e0262dee60
|
|
4
|
+
data.tar.gz: f933adffe5a1970b78669ae40edd0f5595342c01e43e7f1f53c50072a1f5236b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4454cc915358b308486232099c78a6db594d6c86318e4608cf490ba9a2b6de954a76f276c0e2b91a61127115535ca1eb550ab901a2218cd42be86acd5b7ac83d
|
|
7
|
+
data.tar.gz: 59a5bbac2e96705896e0ddbcedefe36b215f3fc6fbe541c562b4e3a3b91f8f604df03fd0570daf86bcc0898059715eae29a22fb95c7c489457c27a603ff6e76f
|
|
@@ -17,42 +17,140 @@
|
|
|
17
17
|
return !!getHoverPopup();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function logError(message, error) {
|
|
21
|
+
if (error) console.error(`[jil] ${message}`, error);
|
|
22
|
+
else console.error(`[jil] ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function reportMapProblems(img, mapData) {
|
|
26
|
+
const src = mapData.src || img.getAttribute("src") || "image map";
|
|
27
|
+
const width = Number(mapData.width);
|
|
28
|
+
const height = Number(mapData.height);
|
|
29
|
+
|
|
30
|
+
if (!width || !height) {
|
|
31
|
+
logError(`${src}: map data is missing width or height`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const regions = mapData.regions;
|
|
35
|
+
if (!Array.isArray(regions) || regions.length === 0) {
|
|
36
|
+
logError(`${src}: map data has no regions`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const checkSize = () => {
|
|
40
|
+
const naturalWidth = img.naturalWidth;
|
|
41
|
+
const naturalHeight = img.naturalHeight;
|
|
42
|
+
if (!naturalWidth || !naturalHeight) {
|
|
43
|
+
logError(`${src}: image failed to load`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!width || !height) return;
|
|
47
|
+
if (naturalWidth === width && naturalHeight === height) return;
|
|
48
|
+
logError(`${src}: image is ${naturalWidth}×${naturalHeight} but map data is ${width}×${height}`);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
img.addEventListener("error", () => {
|
|
52
|
+
logError(`${src}: image failed to load`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (img.complete) checkSize();
|
|
56
|
+
else img.addEventListener("load", checkSize);
|
|
57
|
+
|
|
58
|
+
(regions || []).forEach((region, index) => {
|
|
59
|
+
const name = region.title || region.href || `region ${index + 1}`;
|
|
60
|
+
const points = region.points;
|
|
61
|
+
if (!region.href) logError(`${src}: "${name}" is missing href`);
|
|
62
|
+
if (!Array.isArray(points) || points.length < 3) {
|
|
63
|
+
logError(`${src}: "${name}" needs at least 3 points`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!width || !height) return;
|
|
67
|
+
|
|
68
|
+
const xs = points.map((point) => point[0]);
|
|
69
|
+
const ys = points.map((point) => point[1]);
|
|
70
|
+
const outside = xs.some((x) => x < 0 || x > width) || ys.some((y) => y < 0 || y > height);
|
|
71
|
+
if (!outside) return;
|
|
72
|
+
logError(
|
|
73
|
+
`${src}: "${name}" is outside ${width}×${height} (x ${Math.min(...xs)}–${Math.max(...xs)}, y ${Math.min(...ys)}–${Math.max(...ys)})`
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
20
78
|
function init() {
|
|
21
|
-
document.querySelectorAll("[data-jil-
|
|
79
|
+
document.querySelectorAll("img.jil-map-image[data-jil-regions]").forEach((img) => {
|
|
80
|
+
if (img.closest("[data-jil-map]")) return;
|
|
81
|
+
logError(
|
|
82
|
+
`${img.getAttribute("src") || "image"} was not enhanced (${img.getAttribute("data-jil-regions")})`
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
document.querySelectorAll("[data-jil-map]").forEach((host) => {
|
|
87
|
+
try {
|
|
88
|
+
initMapHost(host);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logError(`Failed to init ${host.id || "image map"}`, error);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
22
93
|
}
|
|
23
94
|
|
|
24
95
|
function initMapHost(host) {
|
|
25
96
|
if (host.dataset.jilReady === "true") return;
|
|
26
97
|
host.dataset.jilReady = "true";
|
|
27
98
|
|
|
28
|
-
|
|
99
|
+
let mapData;
|
|
100
|
+
try {
|
|
101
|
+
mapData = JSON.parse(host.dataset.jilMap);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logError(`Invalid map JSON on ${host.id || "image map"}`, error);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
29
107
|
const inline = host.dataset.jilInline !== "false";
|
|
30
108
|
const viewer = host.dataset.jilViewer !== "false";
|
|
31
109
|
const labels = host.dataset.jilLabels === "true";
|
|
32
110
|
const img = host.querySelector(".jil-map-image");
|
|
33
111
|
|
|
34
|
-
if (!img)
|
|
112
|
+
if (!img) {
|
|
113
|
+
logError(`${mapData.src || host.id || "image map"}: missing .jil-map-image`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
35
116
|
|
|
117
|
+
reportMapProblems(img, mapData);
|
|
36
118
|
if (labels) renderLabels(host, mapData, img);
|
|
37
119
|
if (inline) bindInlineClicks(host, mapData, img);
|
|
38
120
|
if (viewer) addViewerButton(host, mapData);
|
|
39
121
|
}
|
|
40
122
|
|
|
123
|
+
function bindOverlayGeometry(overlay, host, mapData, img) {
|
|
124
|
+
const update = () => {
|
|
125
|
+
const geom = getMapGeometry(img, mapData);
|
|
126
|
+
if (!geom || !geom.width || !geom.height) return;
|
|
127
|
+
overlay.style.left = `${geom.offsetX}px`;
|
|
128
|
+
overlay.style.top = `${geom.offsetY}px`;
|
|
129
|
+
overlay.style.width = `${geom.width}px`;
|
|
130
|
+
overlay.style.height = `${geom.height}px`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (img.complete) update();
|
|
134
|
+
img.addEventListener("load", update);
|
|
135
|
+
window.addEventListener("resize", update);
|
|
136
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
137
|
+
const observer = new ResizeObserver(update);
|
|
138
|
+
observer.observe(host);
|
|
139
|
+
observer.observe(img);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
41
143
|
function renderLabels(host, mapData, img) {
|
|
42
144
|
const overlay = document.createElement("div");
|
|
43
145
|
overlay.className = "jil-label-layer";
|
|
44
146
|
host.appendChild(overlay);
|
|
147
|
+
bindOverlayGeometry(overlay, host, mapData, img);
|
|
45
148
|
|
|
46
149
|
const update = () => {
|
|
47
150
|
overlay.innerHTML = "";
|
|
48
151
|
const geom = getMapGeometry(img, mapData);
|
|
49
152
|
if (!geom || !geom.width || !geom.height) return;
|
|
50
153
|
|
|
51
|
-
overlay.style.left = `${geom.offsetX}px`;
|
|
52
|
-
overlay.style.top = `${geom.offsetY}px`;
|
|
53
|
-
overlay.style.width = `${geom.width}px`;
|
|
54
|
-
overlay.style.height = `${geom.height}px`;
|
|
55
|
-
|
|
56
154
|
mapData.regions.forEach((region) => {
|
|
57
155
|
const center = polygonCenter(region.points);
|
|
58
156
|
if (!center) return;
|
|
@@ -329,6 +427,9 @@
|
|
|
329
427
|
state.image = image;
|
|
330
428
|
fitZoom();
|
|
331
429
|
};
|
|
430
|
+
image.onerror = () => {
|
|
431
|
+
logError(`${mapData.src || "viewer image"}: viewer image failed to load`);
|
|
432
|
+
};
|
|
332
433
|
image.src = mapData.src;
|
|
333
434
|
|
|
334
435
|
return {
|
|
@@ -354,13 +455,22 @@
|
|
|
354
455
|
const jhp = useHoverPopup() ? getHoverPopup() : null;
|
|
355
456
|
if (jhp) {
|
|
356
457
|
event.preventDefault();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
458
|
+
try {
|
|
459
|
+
await jhp.openLink({
|
|
460
|
+
href: region.href,
|
|
461
|
+
title: region.title || region.label || region.href,
|
|
462
|
+
clientX: event.clientX,
|
|
463
|
+
clientY: event.clientY,
|
|
464
|
+
isPermanent: true,
|
|
465
|
+
});
|
|
466
|
+
} catch (error) {
|
|
467
|
+
logError(`Failed to open ${region.href}`, error);
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!region.href) {
|
|
473
|
+
logError("Region is missing href");
|
|
364
474
|
return;
|
|
365
475
|
}
|
|
366
476
|
|
|
@@ -478,7 +588,10 @@
|
|
|
478
588
|
}
|
|
479
589
|
|
|
480
590
|
function start() {
|
|
481
|
-
if (!document.querySelector(ROOT_SELECTOR))
|
|
591
|
+
if (!document.querySelector(ROOT_SELECTOR)) {
|
|
592
|
+
logError("image_links.js loaded without data-jil-root");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
482
595
|
|
|
483
596
|
const run = () => init();
|
|
484
597
|
|
|
@@ -17,7 +17,10 @@ module Jekyll
|
|
|
17
17
|
html = MapRenderer.enhance_html(html, site: site, page: doc, cfg: cfg)
|
|
18
18
|
doc.output = inject_assets(html, assets_path: assets_path, site: site, cfg: cfg)
|
|
19
19
|
rescue StandardError => e
|
|
20
|
-
Jekyll.logger.
|
|
20
|
+
Jekyll.logger.error(
|
|
21
|
+
"jekyll-image-links:",
|
|
22
|
+
"Failed to process #{doc.relative_path}: #{e.class}: #{e.message}"
|
|
23
|
+
)
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
end
|
|
@@ -23,7 +23,7 @@ module Jekyll
|
|
|
23
23
|
html = html.gsub(PORTABLE_FIGURE) do |figure_html|
|
|
24
24
|
enhance_portable_markup(figure_html, site: site, page: page, cfg: cfg)
|
|
25
25
|
rescue StandardError => e
|
|
26
|
-
|
|
26
|
+
log_error("#{page_label(page)} #{markup_context(figure_html)}: #{e.class}: #{e.message}")
|
|
27
27
|
figure_html
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ module Jekyll
|
|
|
32
32
|
|
|
33
33
|
enhance_portable_markup(image_html, site: site, page: page, cfg: cfg)
|
|
34
34
|
rescue StandardError => e
|
|
35
|
-
|
|
35
|
+
log_error("#{page_label(page)} #{markup_context(image_html)}: #{e.class}: #{e.message}")
|
|
36
36
|
image_html
|
|
37
37
|
end
|
|
38
38
|
end
|
|
@@ -170,6 +170,8 @@ module Jekyll
|
|
|
170
170
|
"title" => title,
|
|
171
171
|
"regions" => normalize_regions(regions, site: site, page: page, liquid_context: liquid_context),
|
|
172
172
|
}
|
|
173
|
+
warn_if_regions_out_of_bounds!(map_data["regions"], width, height, src)
|
|
174
|
+
warn_if_image_dimensions_differ!(site, page, attrs["src"], width, height)
|
|
173
175
|
|
|
174
176
|
render_interactive(
|
|
175
177
|
map_data,
|
|
@@ -300,6 +302,170 @@ module Jekyll
|
|
|
300
302
|
end
|
|
301
303
|
end
|
|
302
304
|
|
|
305
|
+
def warn_if_regions_out_of_bounds!(regions, width, height, src)
|
|
306
|
+
regions.each do |region|
|
|
307
|
+
points = region["points"] || []
|
|
308
|
+
next if points.empty?
|
|
309
|
+
|
|
310
|
+
xs = points.map { |point| point[0] }
|
|
311
|
+
ys = points.map { |point| point[1] }
|
|
312
|
+
outside = xs.any? { |x| x.negative? || x > width } || ys.any? { |y| y.negative? || y > height }
|
|
313
|
+
next unless outside
|
|
314
|
+
|
|
315
|
+
name = region["title"] || region["href"]
|
|
316
|
+
log_error(
|
|
317
|
+
"#{src}: region \"#{name}\" is outside #{width}×#{height} (x #{xs.min}–#{xs.max}, y #{ys.min}–#{ys.max})"
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def warn_if_image_dimensions_differ!(site, page, src, width, height)
|
|
323
|
+
path = local_image_path(site, page, src)
|
|
324
|
+
if path.nil?
|
|
325
|
+
log_error("#{src}: image file not found") unless remote_url?(src)
|
|
326
|
+
return
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
actual = read_image_size(path)
|
|
330
|
+
return unless actual
|
|
331
|
+
|
|
332
|
+
actual_width, actual_height = actual
|
|
333
|
+
return if actual_width == width && actual_height == height
|
|
334
|
+
|
|
335
|
+
log_error("#{src}: image file is #{actual_width}×#{actual_height} but map data is #{width}×#{height}")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def local_image_path(site, page, src)
|
|
339
|
+
return nil if src.to_s.strip.empty? || remote_url?(src)
|
|
340
|
+
|
|
341
|
+
path = src.to_s
|
|
342
|
+
baseurl = site.baseurl.to_s
|
|
343
|
+
baseurl = "" if baseurl == "/"
|
|
344
|
+
path = path.delete_prefix(baseurl) if !baseurl.empty? && (path == baseurl || path.start_with?("#{baseurl}/"))
|
|
345
|
+
|
|
346
|
+
candidates = [path]
|
|
347
|
+
if !path.start_with?("/") && page&.respond_to?(:relative_path) && page.relative_path
|
|
348
|
+
candidates << File.join(File.dirname(page.relative_path), path)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
candidates.each do |candidate|
|
|
352
|
+
full_path = Jekyll.sanitized_path(site.source, candidate)
|
|
353
|
+
return full_path if File.file?(full_path)
|
|
354
|
+
end
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def remote_url?(src)
|
|
359
|
+
src.to_s.start_with?("http://", "https://", "data:")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def read_image_size(path)
|
|
363
|
+
File.open(path, "rb") do |file|
|
|
364
|
+
header = file.read(32)
|
|
365
|
+
return nil unless header && header.bytesize >= 24
|
|
366
|
+
|
|
367
|
+
header = header.b
|
|
368
|
+
if header.start_with?("\x89PNG\r\n\x1A\n".b)
|
|
369
|
+
return [header[16, 4].unpack1("N"), header[20, 4].unpack1("N")]
|
|
370
|
+
end
|
|
371
|
+
if header.start_with?("GIF87a".b, "GIF89a".b)
|
|
372
|
+
return header[6, 4].unpack("v2")
|
|
373
|
+
end
|
|
374
|
+
if header.start_with?("RIFF".b) && header[8, 4] == "WEBP".b
|
|
375
|
+
return read_webp_size(file, header)
|
|
376
|
+
end
|
|
377
|
+
return read_jpeg_size(file) if header.start_with?("\xFF\xD8".b)
|
|
378
|
+
end
|
|
379
|
+
nil
|
|
380
|
+
rescue StandardError => e
|
|
381
|
+
log_error("#{path}: could not read image dimensions: #{e.class}: #{e.message}")
|
|
382
|
+
nil
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def read_webp_size(file, header)
|
|
386
|
+
format = header[12, 4]
|
|
387
|
+
if format == "VP8X".b
|
|
388
|
+
file.seek(24)
|
|
389
|
+
data = file.read(6)
|
|
390
|
+
return nil unless data&.bytesize == 6
|
|
391
|
+
|
|
392
|
+
bytes = data.bytes
|
|
393
|
+
width = 1 + (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16))
|
|
394
|
+
height = 1 + (bytes[3] | (bytes[4] << 8) | (bytes[5] << 16))
|
|
395
|
+
[width, height]
|
|
396
|
+
elsif format == "VP8 ".b
|
|
397
|
+
file.seek(26)
|
|
398
|
+
data = file.read(4)
|
|
399
|
+
return nil unless data&.bytesize == 4
|
|
400
|
+
|
|
401
|
+
width = data[0, 2].unpack1("v") & 0x3fff
|
|
402
|
+
height = data[2, 2].unpack1("v") & 0x3fff
|
|
403
|
+
[width, height]
|
|
404
|
+
elsif format == "VP8L".b
|
|
405
|
+
file.seek(21)
|
|
406
|
+
data = file.read(4)
|
|
407
|
+
return nil unless data&.bytesize == 4
|
|
408
|
+
|
|
409
|
+
b0, b1, b2, b3 = data.bytes
|
|
410
|
+
width = 1 + (((b1 & 0x3F) << 8) | b0)
|
|
411
|
+
height = 1 + (((b3 & 0x0F) << 10) | (b2 << 2) | ((b1 & 0xC0) >> 6))
|
|
412
|
+
[width, height]
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def read_jpeg_size(file)
|
|
417
|
+
file.seek(2)
|
|
418
|
+
loop do
|
|
419
|
+
byte = file.read(1)
|
|
420
|
+
return nil unless byte == "\xFF".b
|
|
421
|
+
|
|
422
|
+
marker = nil
|
|
423
|
+
loop do
|
|
424
|
+
byte = file.read(1)
|
|
425
|
+
return nil unless byte
|
|
426
|
+
next if byte == "\xFF".b
|
|
427
|
+
|
|
428
|
+
marker = byte.ord
|
|
429
|
+
break
|
|
430
|
+
end
|
|
431
|
+
next if marker == 0xD8 || marker == 0xD9
|
|
432
|
+
return nil if marker == 0xDA
|
|
433
|
+
|
|
434
|
+
length_bytes = file.read(2)
|
|
435
|
+
return nil unless length_bytes&.bytesize == 2
|
|
436
|
+
|
|
437
|
+
length = length_bytes.unpack1("n")
|
|
438
|
+
return nil if length < 2
|
|
439
|
+
|
|
440
|
+
if [0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF].include?(marker)
|
|
441
|
+
data = file.read(5)
|
|
442
|
+
return nil unless data&.bytesize == 5
|
|
443
|
+
|
|
444
|
+
_precision, height, width = data.unpack("Cnn")
|
|
445
|
+
return [width, height]
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
file.seek(length - 2, IO::SEEK_CUR)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def page_label(page)
|
|
453
|
+
return page.relative_path if page.respond_to?(:relative_path) && page.relative_path
|
|
454
|
+
return page.path if page.respond_to?(:path) && page.path
|
|
455
|
+
|
|
456
|
+
"page"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def markup_context(markup)
|
|
460
|
+
src = markup[/src="([^"]*)"/, 1]
|
|
461
|
+
regions = markup[/data-jil-regions="([^"]*)"/, 1]
|
|
462
|
+
[regions, src].compact.reject(&:empty?).join(" ")
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def log_error(message)
|
|
466
|
+
Jekyll.logger.error("jekyll-image-links:", message)
|
|
467
|
+
end
|
|
468
|
+
|
|
303
469
|
def validate_dimensions!(width, height, src)
|
|
304
470
|
unless width.positive? && height.positive?
|
|
305
471
|
raise ArgumentError, "image_map requires native width and height in the YAML file or numeric width/height attributes"
|
|
@@ -15,14 +15,22 @@ module Jekyll
|
|
|
15
15
|
cfg = (site.config["image_links"] || {})
|
|
16
16
|
regions_body = @attrs["file"] ? nil : super
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
begin
|
|
19
|
+
figure = MapRenderer.render_portable_figure(
|
|
20
|
+
@attrs,
|
|
21
|
+
site: site,
|
|
22
|
+
page: context.registers[:page],
|
|
23
|
+
liquid_context: context,
|
|
24
|
+
cfg: cfg,
|
|
25
|
+
regions_body: regions_body
|
|
26
|
+
)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
page = context.registers[:page]
|
|
29
|
+
page_label = page.respond_to?(:relative_path) ? page.relative_path : "page"
|
|
30
|
+
src = @attrs["src"] || @attrs["file"]
|
|
31
|
+
Jekyll.logger.error("jekyll-image-links:", "#{page_label} #{src}: #{e.class}: #{e.message}")
|
|
32
|
+
raise
|
|
33
|
+
end
|
|
26
34
|
|
|
27
35
|
<<~HTML
|
|
28
36
|
{::nomarkdown}
|