Image Test
Contents
1 Basic Markdown Syntax for Image:
!
Images have a similar syntax to links but include a preceding exclamation point. ❕
1.1 With Label:

1.2 Without Label:


1.3 With Label and Admonition:
{{< admonition type=info title="Click to fold" open=true >}}

{{< /admonition >}}
2 Using Image Shortcode
image shortcode is an alternative to figure shortcode. image shortcode can take full advantage of the dependent libraries of lazysizes and lightgallery.js.
{{< image src="images/stormtroopocat.jpg" caption="The Stormtroopocat" linked=true >}}
The rendered output looks like this:

2.1 Using Image Shortcode with Admonition:
{{< admonition type=info title="Click to fold" open=true >}}
{{< image src="images/stormtroopocat.jpg" caption="The Stormtroopocat" height="676" width="704" linked=true >}}
{{< /admonition >}}
The rendered output looks like this:
Question
I ran into a strange problem that I’m not sure how to resolve for the time being.
Images shortcode does not function properly in admonition shortcode and only displays a link to the image (see below). Surprisingly, only one computer (my laptop) has this issue. That is, somehow because Scale and Layout of :(fab fa-windows): Windows Display is set to 150 %, therefore the symptom occurs globally in all browsers, instead of just one. Fortunately, images load normally on other devices (desktop PC, phones, and tablets). :(far fa-question-circle):The image :(fas fa-image): below only shows a link, but changing the zooming level of the browser forces it to show, however.

2.2 Image with Hover Text Boxes: (experimental)
(function () {
function isMeasurable(el) {
// Visible and non-zero rendered size
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && el.offsetParent !== null;
}
function clamp(v, min, max) {
return Math.min(Math.max(v, min), max);
}
// --- New helpers for symbol + toggle integration ---
const SYMBOL_CLASS = 'symbol';
const TOGGLE_BTN_CLASS = 'note-toggle-btn';
const SYMBOL_W = 22;
const SYMBOL_H = 22;
function disposeTooltips(nodes) {
nodes.forEach(el => {
// Bootstrap 5 API: get existing instance and dispose if present
if (window.bootstrap && bootstrap.Tooltip) {
const inst = bootstrap.Tooltip.getInstance(el);
if (inst) inst.dispose();
}
});
}
function clearSymbolsAndButton(container) {
const symbols = Array.from(container.querySelectorAll(`.${SYMBOL_CLASS}`));
disposeTooltips(symbols);
symbols.forEach(el => el.remove());
const btn = container.querySelector(`.${TOGGLE_BTN_CLASS}`);
if (btn) btn.remove();
}
function makeSymbol(container, text, styleObj) {
const el = document.createElement('div');
el.className = SYMBOL_CLASS;
el.setAttribute('title', text || '');
el.setAttribute('data-bs-toggle', 'tooltip');
el.setAttribute('data-bs-placement', 'top');
Object.assign(el.style, { position: 'absolute', ...styleObj });
container.appendChild(el);
if (window.bootstrap && bootstrap.Tooltip) {
new bootstrap.Tooltip(el);
}
// Prevent default navigation on clicks, matching second script
el.addEventListener('click', (e) => { e.preventDefault(); });
return el;
}
function createToggle(container, onClick) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `btn btn-outline-success ${TOGGLE_BTN_CLASS}`;
btn.textContent = 'Toggle';
Object.assign(btn.style, {
position: 'absolute',
bottom: '10px',
right: '10px',
zIndex: '101',
});
btn.addEventListener('click', (e) => {
e.preventDefault();
onClick();
});
container.appendChild(btn);
return btn;
}
function setupHoverUI(container) {
const hoverTooltip = container.getAttribute('hover-tooltip');
// Always clean first to be idempotent
clearSymbolsAndButton(container);
if (hoverTooltip !== 'true') {
// Base: show original notes; remove symbols and button
container.querySelectorAll('.note-box.movable').forEach(n => { n.style.display = ''; });
return;
}
if (!isMeasurable(container)) return;
const screenWidth = window.innerWidth || document.documentElement.clientWidth;
const notes = Array.from(container.querySelectorAll('.note-box.movable'));
// Utility for percent clamping like second script
function clampPercent(v) {
return Math.min(Math.max(v, 0), 100);
}
// Small: <= 600px — show symbols only (notes hidden), symbols placed by percent
if (screenWidth <= 600) {
notes.forEach(note => { note.style.display = 'none'; });
notes.forEach(note => {
const px = parseFloat(note.getAttribute('data-initial-x')) || 0;
const py = parseFloat(note.getAttribute('data-initial-y')) || 0;
const xPct = clampPercent(px);
const yPct = clampPercent(py);
makeSymbol(container, note.textContent || '', {
left: `${xPct}%`,
top: `${yPct}%`,
zIndex: '100',
});
});
// Prevent default on container clicks, similar to second script
container.addEventListener('click', (e) => { e.preventDefault(); }, { passive: false });
return;
}
// Medium: <= 1200px — symbols only by default + a Toggle button to switch
if (screenWidth <= 1200) {
notes.forEach(note => { note.style.display = 'none'; });
notes.forEach(note => {
const px = parseFloat(note.getAttribute('data-initial-x')) || 0;
const py = parseFloat(note.getAttribute('data-initial-y')) || 0;
const xPct = clampPercent(px);
const yPct = clampPercent(py);
makeSymbol(container, note.textContent || '', {
left: `${xPct}%`,
top: `${yPct}%`,
zIndex: '100',
});
});
const toggle = createToggle(container, () => {
const anyNoteVisible = notes.some(n => n.style.display !== 'none');
if (anyNoteVisible) {
// Hide notes, show symbols
notes.forEach(n => { n.style.display = 'none'; });
container.querySelectorAll(`.${SYMBOL_CLASS}`).forEach(s => { s.style.display = ''; });
} else {
// Show notes, hide symbols
notes.forEach(n => { n.style.display = ''; });
container.querySelectorAll(`.${SYMBOL_CLASS}`).forEach(s => { s.style.display = 'none'; });
}
});
container.addEventListener('click', (e) => { e.preventDefault(); }, { passive: false });
return;
}
// Large: > 1200px — show notes by default, also create centered symbols hidden initially + Toggle
notes.forEach(note => { note.style.display = ''; });
const cpos = container.getBoundingClientRect();
notes.forEach(note => {
const npos = note.getBoundingClientRect();
const centerLeft = (npos.left - cpos.left) + (npos.width / 2) - (SYMBOL_W / 2);
const centerTop = (npos.top - cpos.top) + (npos.height / 2) - (SYMBOL_H / 2);
const sym = makeSymbol(container, note.textContent || '', {
left: `${centerLeft}px`,
top: `${centerTop}px`,
zIndex: '100',
});
// Hidden initially on large
sym.style.display = 'none';
});
createToggle(container, () => {
const symbols = Array.from(container.querySelectorAll(`.${SYMBOL_CLASS}`));
const visible = symbols.some(s => s.style.display !== 'none');
if (visible) {
// Hide symbols, show notes
symbols.forEach(s => { s.style.display = 'none'; });
notes.forEach(n => { n.style.display = ''; });
} else {
// Show symbols, hide notes
symbols.forEach(s => { s.style.display = ''; });
notes.forEach(n => { n.style.display = 'none'; });
}
});
container.addEventListener('click', (e) => { e.preventDefault(); }, { passive: false });
}
// --- End new helpers ---
function layout(container) {
// Ensure container is the positioning context
const computed = getComputedStyle(container);
if (computed.position === 'static') {
container.style.position = 'relative';
}
// Require measurable size
if (!isMeasurable(container)) return;
const crect = container.getBoundingClientRect(); // rendered size
const cw = crect.width;
const ch = crect.height;
// Place each note from % to px, then keep pixel math for drag
const notes = container.querySelectorAll('.note-box.movable');
notes.forEach(note => {
// Make sure note is absolutely positioned above the image
note.style.position = 'absolute';
note.style.zIndex = '100';
note.style.cursor = 'move';
note.style.touchAction = 'none'; // prevent touch panning during drag
const px = parseFloat(note.getAttribute('data-initial-x')) || 0;
const py = parseFloat(note.getAttribute('data-initial-y')) || 0;
// First pass: place near target to measure size
let left = (px / 100) * cw;
let top = (py / 100) * ch;
note.style.left = `${left}px`;
note.style.top = `${top}px`;
// Measure outer size after initial placement
const nrect = note.getBoundingClientRect();
const nw = nrect.width;
const nh = nrect.height;
// Clamp so the entire note remains visible
left = clamp(left, 0, Math.max(cw - nw, 0));
top = clamp(top, 0, Math.max(ch - nh, 0));
note.style.left = `${left}px`;
note.style.top = `${top}px`;
});
}
function enableDrag(container) {
const notes = container.querySelectorAll('.note-box.movable');
notes.forEach(note => {
// Remove previous handlers by replacing element listeners via pointer events
let dragging = false;
let startX = 0, startY = 0, startLeft = 0, startTop = 0;
const onPointerDown = (e) => {
// Only start if container is measurable
if (!isMeasurable(container)) return;
dragging = true;
note.setPointerCapture(e.pointerId);
const npos = note.getBoundingClientRect();
const cpos = container.getBoundingClientRect();
startLeft = npos.left - cpos.left;
startTop = npos.top - cpos.top;
startX = e.clientX;
startY = e.clientY;
e.preventDefault();
};
const onPointerMove = (e) => {
if (!dragging) return;
const cpos = container.getBoundingClientRect();
const dx = e.clientX - startX;
const dy = e.clientY - startY;
// Use rendered sizes for clamping
const nrect = note.getBoundingClientRect();
const cw = cpos.width;
const ch = cpos.height;
const nw = nrect.width;
const nh = nrect.height;
const nextLeft = clamp(startLeft + dx, 0, Math.max(cw - nw, 0));
const nextTop = clamp(startTop + dy, 0, Math.max(ch - nh, 0));
note.style.left = `${nextLeft}px`;
note.style.top = `${nextTop}px`;
e.preventDefault();
};
const onPointerUp = (e) => {
if (!dragging) return;
dragging = false;
try { note.releasePointerCapture(e.pointerId); } catch (_) {}
e.preventDefault();
};
// Replace any prior listeners
note.onpointerdown = onPointerDown;
note.onpointermove = onPointerMove;
note.onpointerup = onPointerUp;
note.onpointercancel = onPointerUp;
});
}
function initContainer(container) {
// Try to layout now
layout(container);
enableDrag(container);
setupHoverUI(container);
// Observe size changes for relayout
if ('ResizeObserver' in window) {
const ro = new ResizeObserver(() => {
layout(container);
enableDrag(container);
setupHoverUI(container);
});
ro.observe(container);
// If an <img> exists, observe it as well (covers lazy/eager reloads)
const img = container.querySelector('img');
if (img) ro.observe(img);
// Also respond to viewport size changes (breakpoints)
window.addEventListener('resize', () => {
layout(container);
enableDrag(container);
setupHoverUI(container);
}, { passive: true });
} else {
// Fallback: re-check a few times while becoming visible
let tries = 0;
const id = setInterval(() => {
layout(container);
enableDrag(container);
setupHoverUI(container);
if (++tries >= 20) clearInterval(id);
}, 100);
window.addEventListener('resize', () => {
layout(container);
enableDrag(container);
setupHoverUI(container);
}, { passive: true });
}
// Also relayout on image load for browsers that delay sizing until onload
const img = container.querySelector('img');
if (img && !(img.complete && img.naturalWidth > 0)) {
img.addEventListener('load', () => {
layout(container);
enableDrag(container);
setupHoverUI(container);
}, { once: true });
}
// If content is inside a collapsible section, react to visibility changes
const mo = new MutationObserver(() => {
if (isMeasurable(container)) {
layout(container);
enableDrag(container);
setupHoverUI(container);
}
});
mo.observe(container, { attributes: true, attributeFilter: ['style', 'class'] });
const parent = container.parentElement;
if (parent) {
mo.observe(parent, { attributes: true, attributeFilter: ['style', 'class', 'open', 'hidden'] });
}
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.image-container').forEach(initContainer);
});
})();
{{< image2 src="images/stormtroopocat.jpg" caption="The Stormtroopocat" height="676" width="704" linked="true" data-initial-x="50" data-initial-y="50" textbox_content="This is the <b>first line</b> | This is the second line." >}}