import { parse } from "wmap-parser"; import { renderToCanvas, StageType } from "wmap-renderer-canvas"; import DOMPurify from "isomorphic-dompurify"; const internals = { kStyleSheet: "/plugins/wmap/wmap.css", kDimensions: { full: { map: { mapWidth: 1300, mapHeight: 1000, padding: 42, }, fonts: {}, }, thumbnail: { map: { mapWidth: 403, mapHeight: 310, padding: 15, vertexWidth: 8, vertexHeight: 8, arrowHeadSize: 3, stageHeight: 50, }, fonts: { axisLabel: 8, vertexLabel: 9, note: 9, }, }, }, // Returns HTML that shows a message in the wiki. message(text) { return `
${text}
`; }, // Creates a wmap PNG from text. async renderImage(source, dimensions) { const parsedText = parse(source); const canvas = document.createElement("canvas"); canvas.width = dimensions.map.mapWidth + 2 * dimensions.map.padding; canvas.height = dimensions.map.mapHeight + 2 * dimensions.map.padding; // Further Improvements. The zoomed version could have a drop down // to show the different StageTypes and allow one to see the image // with the different types. // This could also potentially be in the toolbar that appears // on hover to also show this in the download. It requires some // thought on what the right interaction model would be. await renderToCanvas(parsedText, canvas, StageType.ACTIVITIES, { theme: { sizes: { ...dimensions.map, }, fonts: { ...dimensions.fonts, }, }, }); return canvas.toDataURL(); }, // Starts a download of the image. download(filename, text) { const element = document.createElement("a"); element.setAttribute("href", text); element.setAttribute("download", filename); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); }, // Replaces the image in a window. Intended for use in a dialog. replaceImage(window, imageData) { try { const $container = window.document.querySelector("main"); $container.querySelectorAll("img").forEach((img) => img.remove()); const image = document.createElement("img"); image.src = imageData; image.alt = "Wardley map rendered from source."; $container.appendChild(image); } catch (error) { console.error( "wmap: Could not replace image. The DOM wasn't ready!", error, ); } }, }; /** * Runs once for each block. Do setup for the block before rendering. */ const emit = function emit($item) { const hasStyleSheet = [...document.styleSheets].some((styleSheet) => styleSheet.href?.endsWith(internals.kStyleSheet), ); if (!hasStyleSheet) { console.log("Adding wmap stylesheet."); const link = document.createElement("link"); link.rel = "stylesheet"; link.href = internals.kStyleSheet; link.type = "text/css"; document.getElementsByTagName("head")[0].appendChild(link); } return $item.append(internals.message("Loading Wardley Map")); }; /** * Bind to events on the rendered block. Called once per block. * Convention is that double click opens the editor. * * @param {JQuery} $item the HTML element representing the block. * @param {Object} item the configuration for that particular object. */ const bind = async function bind($item, item) { $item.on("dblclick", () => { return wiki.textEditor($item, item); }); $item.on("click", "a", async (event) => { const { currentTarget } = event; const action = currentTarget.dataset?.action; if (!action) { return; } event.stopPropagation(); event.preventDefault(); switch (action) { case "download": { let imageData = await internals.renderImage( item.text, internals.kDimensions.full, ); const slug = $item.parents(".page").attr("id"); internals.download(`${slug}.png`, imageData); } break; case "zoom": { const shouldOpenStandalone = !!event.shiftKey; const target = shouldOpenStandalone ? "_blank" : "wmap"; const dialog = window.open( "/plugins/wmap/dialog/#", target, "popup,height=600,width=800", ); if (!dialog || dialog.closed) { console.error("wmap: Failed to open dialog."); return; } let imageData = await internals.renderImage( item.text, internals.kDimensions.full, ); // We *MUST* check both readyState and href, because // readyState will be complete when it first loads, // and this will be an empty window. if ( dialog.document.readyState === "complete" && dialog.document.location.href.includes( "/plugins/wmap/dialog/", ) ) { internals.replaceImage(dialog, imageData); } else { console.info("We not loaded"); dialog.addEventListener( "load", () => { internals.replaceImage(dialog, imageData); }, { once: true }, ); } } break; } }); try { let thumbnailData = await internals.renderImage( item.text, internals.kDimensions.thumbnail, ); $item.find(".viewer").html(`
`); } catch (err) { console.log("Failed to parse wardley map: ", err); $item.html(internals.message(err.message)); } }; /** * On load, attempt to add the emit and bind functions to window, as these * are called by the wiki plugin manager. */ if (typeof window !== "undefined" && window !== null) { if (!window.plugins.wmap) { window.plugins.wmap = { emit, bind }; } } const expand = function (text) { return DOMPurify.sanitize(text); }; export const wmap = typeof window === "undefined" ? { expand } : undefined;