-
-
- `);
+ if (!newWindow) {
+ this.showNotification('Popup blocked: allow popups to view the workflow', 'warning');
+ return;
+ }
+ newWindow.document.write(ImageHelpers.workflowDocumentHtml(this.currentMetadata.workflow));
+ newWindow.document.close();
}
}
diff --git a/web/js/image-helpers.js b/web/js/image-helpers.js
new file mode 100644
index 0000000..9c29786
--- /dev/null
+++ b/web/js/image-helpers.js
@@ -0,0 +1,68 @@
+/**
+ * Pure helpers for image entries rendered by the admin and gallery pages.
+ *
+ * Image rows come from the database (`width`, `height`, `file_size` are stored
+ * from a POST body and may be strings) or from the output-folder scan (`id` is
+ * a path digest, `path` is the file). Everything these helpers return for HTML
+ * is already escaped, so callers can drop it into text or a quoted attribute.
+ *
+ * Loadable in the browser (window.ImageHelpers) and in Node for tests.
+ */
+(function (root) {
+ "use strict";
+
+ const escapeHtml =
+ typeof module !== "undefined" && module.exports
+ ? require("./html-escape.js").escapeHtml
+ : root.escapeHtml;
+
+ const UNKNOWN_SIZE = "Unknown size";
+
+ /**
+ * "640×480 | 1.2 MB" for a card body or a viewer caption, escaped.
+ *
+ * @param {{width?: unknown, height?: unknown, file_size?: unknown}|null|undefined} image
+ * @param {{formatFileSize?: (bytes: number) => string, separator?: string}} [options]
+ * @returns {string} escaped text; "Unknown size" when a dimension is missing
+ */
+ function formatImageCaption(image, { formatFileSize, separator = " | " } = {}) {
+ const entry = image || {};
+ const parts = [];
+ if (entry.width && entry.height) {
+ parts.push(`${escapeHtml(entry.width)}×${escapeHtml(entry.height)}`);
+ } else {
+ parts.push(UNKNOWN_SIZE);
+ }
+ const bytes = Number(entry.file_size);
+ if (formatFileSize && Number.isFinite(bytes) && bytes > 0) {
+ parts.push(escapeHtml(formatFileSize(bytes)));
+ }
+ return parts.join(escapeHtml(separator));
+ }
+
+ /**
+ * A complete standalone page showing a workflow as pretty-printed JSON.
+ * Written into a blank same-origin window, so the JSON must not be able
+ * to close the