diff --git a/playground/viewer/print-selected-pages/index.ts b/playground/viewer/print-selected-pages/index.ts new file mode 100644 index 0000000..6278ff3 --- /dev/null +++ b/playground/viewer/print-selected-pages/index.ts @@ -0,0 +1,228 @@ +import type { Instance } from "@nutrient-sdk/viewer"; +import { baseOptions } from "../../shared/base-options"; + +window.NutrientViewer.load({ + ...baseOptions, + theme: window.NutrientViewer.Theme.DARK, +}).then((instance: Instance) => { + const panel = document.createElement("div"); + panel.style.cssText = + "position:fixed;top:10px;right:10px;z-index:99999;background:#2d2d2d;color:#e0e0e0;padding:12px;border:1px solid #555;border-radius:8px;font:12px system-ui;max-width:280px"; + panel.innerHTML = ` +
Print selected pages
+ + + + + +
+`; + document.body.appendChild(panel); + + const logEl = panel.querySelector("#log") as HTMLElement; + const log = (msg: string, level: "info" | "error" | "success" = "info") => { + const c = + level === "error" + ? "#ff3b30" + : level === "success" + ? "#34c759" + : "#4aa3ff"; + const line = document.createElement("div"); + line.style.color = c; + line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + logEl.appendChild(line); + logEl.scrollTop = logEl.scrollHeight; + console.log("[PrintPages]", msg); + }; + + async function getTotalPages(): Promise { + if (typeof instance.totalPageCount === "number") + return instance.totalPageCount; + if ( + instance.document && + typeof instance.document.getPageCount === "function" + ) + return await instance.document.getPageCount(); + throw new Error("Unable to determine page count from SDK instance."); + } + + function parseRange(str: string, totalPages: number): number[] { + const parts = str + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + const set = new Set(); + + for (const part of parts) { + if (part.includes("-")) { + const [a, b] = part + .split("-") + .map((s) => Number.parseInt(s.trim(), 10)); + if (!Number.isInteger(a) || !Number.isInteger(b)) + throw new Error(`Invalid range: "${part}"`); + if (a > b) throw new Error(`Invalid range (start > end): "${part}"`); + for (let p = a; p <= b; p++) { + if (p < 1 || p > totalPages) + throw new Error(`Page ${p} out of bounds (1-${totalPages})`); + set.add(p - 1); + } + } else { + const p = Number.parseInt(part, 10); + if (!Number.isInteger(p)) throw new Error(`Invalid page: "${part}"`); + if (p < 1 || p > totalPages) + throw new Error(`Page ${p} out of bounds (1-${totalPages})`); + set.add(p - 1); + } + } + + return Array.from(set).sort((x, y) => x - y); + } + + async function exportSubset(pageIndexes: number[]): Promise { + log( + `Exporting pages (1-based): ${pageIndexes.map((i) => i + 1).join(", ")}`, + ); + const buf = await instance.exportPDFWithOperations([ + { type: "keepPages", pageIndexes }, + ]); + log(`Export complete (${Math.round(buf.byteLength / 1024)} KB)`, "success"); + return buf; + } + + function printNewTabWithPopupSafeNavigation( + buf: ArrayBuffer, + preOpenedWindow: Window | null, + ): void { + const blob = new Blob([buf], { type: "application/pdf" }); + const url = URL.createObjectURL(blob); + + // If we successfully opened a window synchronously, navigate it now. + if (preOpenedWindow && !preOpenedWindow.closed) { + preOpenedWindow.location.href = url; + log("Opened subset PDF in the pre-opened tab.", "success"); + } else { + // Fallback attempt (may be blocked depending on browser) + const w = window.open(url, "_blank", "noopener,noreferrer"); + if (!w) { + log( + "Popup blocked. Please allow popups, or switch to iframe method.", + "error", + ); + } else { + log("Opened subset PDF in a new tab.", "success"); + } + } + + // Conservative cleanup: do not revoke quickly. + setTimeout(() => { + try { + URL.revokeObjectURL(url); + } catch (_) {} + log("Blob URL revoked (cleanup)."); + }, 120000); + } + + function printViaIframe(buf: ArrayBuffer): void { + const blob = new Blob([buf], { type: "application/pdf" }); + const url = URL.createObjectURL(blob); + + const iframe = document.createElement("iframe"); + iframe.style.cssText = + "position:fixed;left:-9999px;top:0;width:1px;height:1px;border:0"; + iframe.src = url; + + const cleanup = () => { + setTimeout(() => { + try { + URL.revokeObjectURL(url); + } catch (_) {} + try { + iframe.remove(); + } catch (_) {} + log("Iframe cleaned up."); + }, 120000); + }; + + iframe.onload = () => { + log("PDF loaded in iframe. Trying print()..."); + try { + iframe.contentWindow?.focus(); + iframe.contentWindow?.print(); + log("Print dialog triggered (iframe).", "success"); + } catch (e) { + log(`Iframe print failed: ${(e as Error)?.message || e}`, "error"); + } finally { + cleanup(); + } + }; + + iframe.onerror = () => { + log("Iframe failed to load PDF.", "error"); + cleanup(); + }; + + document.body.appendChild(iframe); + } + + async function runPrint( + pageIndexes: number[], + clickEvent: MouseEvent, + ): Promise { + // Determine method and pre-open tab synchronously if needed (avoids popup blockers) + const method = panel.querySelector( + 'input[name="m"]:checked', + )?.value; + + let preOpenedWindow: Window | null = null; + if (method === "tab") { + // Must happen synchronously during the click handler call stack + preOpenedWindow = window.open("about:blank", "_blank"); + if (!preOpenedWindow) + log( + "Popup blocked when opening blank tab. Will try direct open later.", + "error", + ); + } + + try { + const buf = await exportSubset(pageIndexes); + if (method === "tab") { + printNewTabWithPopupSafeNavigation(buf, preOpenedWindow); + } else { + printViaIframe(buf); + } + } catch (e) { + log((e as Error)?.message || String(e), "error"); + try { + if (preOpenedWindow && !preOpenedWindow.closed) preOpenedWindow.close(); + } catch (_) {} + } + } + + // Wire buttons + panel + .querySelector("#btn-print-current") + ?.addEventListener("click", async (ev) => { + const idx = instance.viewState?.currentPageIndex; + if (typeof idx !== "number") + return log("Could not read currentPageIndex from viewState.", "error"); + await runPrint([idx], ev as MouseEvent); + }); + + panel + .querySelector("#btn-print-range") + ?.addEventListener("click", async (ev) => { + const input = ( + panel.querySelector("#inp-range") as HTMLInputElement + )?.value.trim(); + if (!input) return log("Enter a page list, e.g. 1,3-5,8", "error"); + + const total = await getTotalPages(); + const indexes = parseRange(input, total); + if (!indexes.length) + return log("No pages selected after parsing.", "error"); + await runPrint(indexes, ev as MouseEvent); + }); + + log("Print controls ready.", "success"); +}); diff --git a/playground/viewer/print-selected-pages/playground.mdx b/playground/viewer/print-selected-pages/playground.mdx new file mode 100644 index 0000000..13ba834 --- /dev/null +++ b/playground/viewer/print-selected-pages/playground.mdx @@ -0,0 +1,6 @@ +--- +category: viewer +title: Print Selected Pages +description: Adds a floating panel that allows printing the current page or custom page ranges by exporting selected pages and opening them in a new tab or triggering auto-print via iframe. +keywords: [print, export, pages, range, current, selected, custom, subset, iframe, tab, printDialog, pageRange, singlePage, multiPage] +--- diff --git a/playground/viewer/print-selected-pages/playground.url b/playground/viewer/print-selected-pages/playground.url new file mode 100644 index 0000000..75ae482 --- /dev/null +++ b/playground/viewer/print-selected-pages/playground.url @@ -0,0 +1,2 @@ +[InternetShortcut] +URL=https://www.nutrient.io/playground?p=eyJ2IjoxLCJqcyI6Ik51dHJpZW50Vmlld2VyLmxvYWQoeyAuLi5iYXNlT3B0aW9ucywgdGhlbWU6IE51dHJpZW50Vmlld2VyLlRoZW1lLkRBUksgfSkudGhlbihcbiAgKGluc3RhbmNlKSA9PiB7XG4gICAgY29uc3QgcGFuZWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiZGl2XCIpO1xuICAgIHBhbmVsLnN0eWxlLmNzc1RleHQgPVxuICAgICAgXCJwb3NpdGlvbjpmaXhlZDt0b3A6MTBweDtyaWdodDoxMHB4O3otaW5kZXg6OTk5OTk7YmFja2dyb3VuZDojMmQyZDJkO2NvbG9yOiNlMGUwZTA7cGFkZGluZzoxMnB4O2JvcmRlcjoxcHggc29saWQgIzU1NTtib3JkZXItcmFkaXVzOjhweDtmb250OjEycHggc3lzdGVtLXVpO21heC13aWR0aDoyODBweFwiO1xuICAgIHBhbmVsLmlubmVySFRNTCA9IGBcbiAgPGRpdiBzdHlsZT1cImZvbnQtd2VpZ2h0OjYwMDttYXJnaW4tYm90dG9tOjhweFwiPlByaW50IHNlbGVjdGVkIHBhZ2VzPC9kaXY%252BXG4gIDxidXR0b24gaWQ9XCJidG4tcHJpbnQtY3VycmVudFwiIHN0eWxlPVwid2lkdGg6MTAwJTtwYWRkaW5nOjhweDttYXJnaW4tYm90dG9tOjhweDtiYWNrZ3JvdW5kOiMwMDdhZmY7Y29sb3I6I2ZmZjtib3JkZXI6MDtib3JkZXItcmFkaXVzOjRweDtjdXJzb3I6cG9pbnRlclwiPlByaW50IGN1cnJlbnQgcGFnZTwvYnV0dG9uPlxuICA8aW5wdXQgaWQ9XCJpbnAtcmFuZ2VcIiBwbGFjZWhvbGRlcj1cImUuZy4gMSwzLTUsOFwiIHN0eWxlPVwid2lkdGg6MTAwJTtib3gtc2l6aW5nOmJvcmRlci1ib3g7cGFkZGluZzo3cHg7Ym9yZGVyLXJhZGl1czo0cHg7Ym9yZGVyOjFweCBzb2xpZCAjNTU1O2JhY2tncm91bmQ6IzFhMWExYTtjb2xvcjojZTBlMGUwO21hcmdpbi1ib3R0b206NnB4XCIgLz5cbiAgPGJ1dHRvbiBpZD1cImJ0bi1wcmludC1yYW5nZVwiIHN0eWxlPVwid2lkdGg6MTAwJTtwYWRkaW5nOjhweDttYXJnaW4tYm90dG9tOjhweDtiYWNrZ3JvdW5kOiMzNGM3NTk7Y29sb3I6I2ZmZjtib3JkZXI6MDtib3JkZXItcmFkaXVzOjRweDtjdXJzb3I6cG9pbnRlclwiPlByaW50IHNlbGVjdGVkIHBhZ2VzPC9idXR0b24%252BXG4gIDxsYWJlbCBzdHlsZT1cImRpc3BsYXk6YmxvY2s7bWFyZ2luLWJvdHRvbTo0cHhcIj48aW5wdXQgdHlwZT1cInJhZGlvXCIgbmFtZT1cIm1cIiB2YWx1ZT1cInRhYlwiIGNoZWNrZWQgLz4gTmV3IHRhYjwvbGFiZWw%252BXG4gIDxsYWJlbCBzdHlsZT1cImRpc3BsYXk6YmxvY2s7bWFyZ2luLWJvdHRvbTo4cHhcIj48aW5wdXQgdHlwZT1cInJhZGlvXCIgbmFtZT1cIm1cIiB2YWx1ZT1cImlmcmFtZVwiIC8%252BIElmcmFtZSBhdXRvLXByaW50IChiZXN0IGVmZm9ydCk8L2xhYmVsPlxuICA8ZGl2IGlkPVwibG9nXCIgc3R5bGU9XCJiYWNrZ3JvdW5kOiMxYTFhMWE7Ym9yZGVyLXJhZGl1czo0cHg7cGFkZGluZzo4cHg7Zm9udDoxMXB4IHVpLW1vbm9zcGFjZSwgU0ZNb25vLVJlZ3VsYXIsIE1lbmxvLCBtb25vc3BhY2U7bWF4LWhlaWdodDoxNDBweDtvdmVyZmxvdzphdXRvO2NvbG9yOiNhYWFcIj48L2Rpdj5cbmA7XG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChwYW5lbCk7XG5cbiAgICBjb25zdCBsb2dFbCA9IHBhbmVsLnF1ZXJ5U2VsZWN0b3IoXCIjbG9nXCIpO1xuICAgIGNvbnN0IGxvZyA9IChtc2csIGxldmVsID0gXCJpbmZvXCIpID0%252BIHtcbiAgICAgIGNvbnN0IGMgPVxuICAgICAgICBsZXZlbCA9PT0gXCJlcnJvclwiXG4gICAgICAgICAgPyBcIiNmZjNiMzBcIlxuICAgICAgICAgIDogbGV2ZWwgPT09IFwic3VjY2Vzc1wiXG4gICAgICAgICAgPyBcIiMzNGM3NTlcIlxuICAgICAgICAgIDogXCIjNGFhM2ZmXCI7XG4gICAgICBjb25zdCBsaW5lID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImRpdlwiKTtcbiAgICAgIGxpbmUuc3R5bGUuY29sb3IgPSBjO1xuICAgICAgbGluZS50ZXh0Q29udGVudCA9IGBbJHtuZXcgRGF0ZSgpLnRvTG9jYWxlVGltZVN0cmluZygpfV0gJHttc2d9YDtcbiAgICAgIGxvZ0VsLmFwcGVuZENoaWxkKGxpbmUpO1xuICAgICAgbG9nRWwuc2Nyb2xsVG9wID0gbG9nRWwuc2Nyb2xsSGVpZ2h0O1xuICAgICAgY29uc29sZS5sb2coXCJbUHJpbnRQYWdlc11cIiwgbXNnKTtcbiAgICB9O1xuXG4gICAgYXN5bmMgZnVuY3Rpb24gZ2V0VG90YWxQYWdlcygpIHtcbiAgICAgIGlmICh0eXBlb2YgaW5zdGFuY2UudG90YWxQYWdlQ291bnQgPT09IFwibnVtYmVyXCIpXG4gICAgICAgIHJldHVybiBpbnN0YW5jZS50b3RhbFBhZ2VDb3VudDtcbiAgICAgIGlmIChcbiAgICAgICAgaW5zdGFuY2UuZG9jdW1lbnQgJiZcbiAgICAgICAgdHlwZW9mIGluc3RhbmNlLmRvY3VtZW50LmdldFBhZ2VDb3VudCA9PT0gXCJmdW5jdGlvblwiXG4gICAgICApXG4gICAgICAgIHJldHVybiBhd2FpdCBpbnN0YW5jZS5kb2N1bWVudC5nZXRQYWdlQ291bnQoKTtcbiAgICAgIHRocm93IG5ldyBFcnJvcihcIlVuYWJsZSB0byBkZXRlcm1pbmUgcGFnZSBjb3VudCBmcm9tIFNESyBpbnN0YW5jZS5cIik7XG4gICAgfVxuXG4gICAgZnVuY3Rpb24gcGFyc2VSYW5nZShzdHIsIHRvdGFsUGFnZXMpIHtcbiAgICAgIGNvbnN0IHBhcnRzID0gc3RyXG4gICAgICAgIC5zcGxpdChcIixcIilcbiAgICAgICAgLm1hcCgocykgPT4gcy50cmltKCkpXG4gICAgICAgIC5maWx0ZXIoQm9vbGVhbik7XG4gICAgICBjb25zdCBzZXQgPSBuZXcgU2V0KCk7XG5cbiAgICAgIGZvciAoY29uc3QgcGFydCBvZiBwYXJ0cykge1xuICAgICAgICBpZiAocGFydC5pbmNsdWRlcyhcIi1cIikpIHtcbiAgICAgICAgICBjb25zdCBbYSwgYl0gPSBwYXJ0LnNwbGl0KFwiLVwiKS5tYXAoKHMpID0%252BIHBhcnNlSW50KHMudHJpbSgpLCAxMCkpO1xuICAgICAgICAgIGlmICghTnVtYmVyLmlzSW50ZWdlcihhKSB8fCAhTnVtYmVyLmlzSW50ZWdlcihiKSlcbiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCByYW5nZTogXCIke3BhcnR9XCJgKTtcbiAgICAgICAgICBpZiAoYSA%252BIGIpIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCByYW5nZSAoc3RhcnQgPiBlbmQpOiBcIiR7cGFydH1cImApO1xuICAgICAgICAgIGZvciAobGV0IHAgPSBhOyBwIDw9IGI7IHArKykge1xuICAgICAgICAgICAgaWYgKHAgPCAxIHx8IHAgPiB0b3RhbFBhZ2VzKVxuICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFBhZ2UgJHtwfSBvdXQgb2YgYm91bmRzICgxLSR7dG90YWxQYWdlc30pYCk7XG4gICAgICAgICAgICBzZXQuYWRkKHAgLSAxKTtcbiAgICAgICAgICB9XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgY29uc3QgcCA9IHBhcnNlSW50KHBhcnQsIDEwKTtcbiAgICAgICAgICBpZiAoIU51bWJlci5pc0ludGVnZXIocCkpIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBwYWdlOiBcIiR7cGFydH1cImApO1xuICAgICAgICAgIGlmIChwIDwgMSB8fCBwID4gdG90YWxQYWdlcylcbiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcihgUGFnZSAke3B9IG91dCBvZiBib3VuZHMgKDEtJHt0b3RhbFBhZ2VzfSlgKTtcbiAgICAgICAgICBzZXQuYWRkKHAgLSAxKTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICByZXR1cm4gQXJyYXkuZnJvbShzZXQpLnNvcnQoKHgsIHkpID0%252BIHggLSB5KTtcbiAgICB9XG5cbiAgICBhc3luYyBmdW5jdGlvbiBleHBvcnRTdWJzZXQocGFnZUluZGV4ZXMpIHtcbiAgICAgIGxvZyhcbiAgICAgICAgYEV4cG9ydGluZyBwYWdlcyAoMS1iYXNlZCk6ICR7cGFnZUluZGV4ZXMubWFwKChpKSA9PiBpICsgMSkuam9pbihcIiwgXCIpfWBcbiAgICAgICk7XG4gICAgICBjb25zdCBidWYgPSBhd2FpdCBpbnN0YW5jZS5leHBvcnRQREZXaXRoT3BlcmF0aW9ucyhbXG4gICAgICAgIHsgdHlwZTogXCJrZWVwUGFnZXNcIiwgcGFnZUluZGV4ZXMgfSxcbiAgICAgIF0pO1xuICAgICAgbG9nKFxuICAgICAgICBgRXhwb3J0IGNvbXBsZXRlICgke01hdGgucm91bmQoYnVmLmJ5dGVMZW5ndGggLyAxMDI0KX0gS0IpYCxcbiAgICAgICAgXCJzdWNjZXNzXCJcbiAgICAgICk7XG4gICAgICByZXR1cm4gYnVmO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIHByaW50TmV3VGFiV2l0aFBvcHVwU2FmZU5hdmlnYXRpb24oYnVmLCBwcmVPcGVuZWRXaW5kb3cpIHtcbiAgICAgIGNvbnN0IGJsb2IgPSBuZXcgQmxvYihbYnVmXSwgeyB0eXBlOiBcImFwcGxpY2F0aW9uL3BkZlwiIH0pO1xuICAgICAgY29uc3QgdXJsID0gVVJMLmNyZWF0ZU9iamVjdFVSTChibG9iKTtcblxuICAgICAgLy8gSWYgd2Ugc3VjY2Vzc2Z1bGx5IG9wZW5lZCBhIHdpbmRvdyBzeW5jaHJvbm91c2x5LCBuYXZpZ2F0ZSBpdCBub3cuXG4gICAgICBpZiAocHJlT3BlbmVkV2luZG93ICYmICFwcmVPcGVuZWRXaW5kb3cuY2xvc2VkKSB7XG4gICAgICAgIHByZU9wZW5lZFdpbmRvdy5sb2NhdGlvbiA9IHVybDtcbiAgICAgICAgbG9nKFwiT3BlbmVkIHN1YnNldCBQREYgaW4gdGhlIHByZS1vcGVuZWQgdGFiLlwiLCBcInN1Y2Nlc3NcIik7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICAvLyBGYWxsYmFjayBhdHRlbXB0IChtYXkgYmUgYmxvY2tlZCBkZXBlbmRpbmcgb24gYnJvd3NlcilcbiAgICAgICAgY29uc3QgdyA9IHdpbmRvdy5vcGVuKHVybCwgXCJfYmxhbmtcIiwgXCJub29wZW5lcixub3JlZmVycmVyXCIpO1xuICAgICAgICBpZiAoIXcpIHtcbiAgICAgICAgICBsb2coXG4gICAgICAgICAgICBcIlBvcHVwIGJsb2NrZWQuIFBsZWFzZSBhbGxvdyBwb3B1cHMsIG9yIHN3aXRjaCB0byBpZnJhbWUgbWV0aG9kLlwiLFxuICAgICAgICAgICAgXCJlcnJvclwiXG4gICAgICAgICAgKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBsb2coXCJPcGVuZWQgc3Vic2V0IFBERiBpbiBhIG5ldyB0YWIuXCIsIFwic3VjY2Vzc1wiKTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICAvLyBDb25zZXJ2YXRpdmUgY2xlYW51cDogZG8gbm90IHJldm9rZSBxdWlja2x5LlxuICAgICAgc2V0VGltZW91dCgoKSA9PiB7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgVVJMLnJldm9rZU9iamVjdFVSTCh1cmwpO1xuICAgICAgICB9IGNhdGNoIChfKSB7fVxuICAgICAgICBsb2coXCJCbG9iIFVSTCByZXZva2VkIChjbGVhbnVwKS5cIik7XG4gICAgICB9LCAxMjAwMDApO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIHByaW50VmlhSWZyYW1lKGJ1Zikge1xuICAgICAgY29uc3QgYmxvYiA9IG5ldyBCbG9iKFtidWZdLCB7IHR5cGU6IFwiYXBwbGljYXRpb24vcGRmXCIgfSk7XG4gICAgICBjb25zdCB1cmwgPSBVUkwuY3JlYXRlT2JqZWN0VVJMKGJsb2IpO1xuXG4gICAgICBjb25zdCBpZnJhbWUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiaWZyYW1lXCIpO1xuICAgICAgaWZyYW1lLnN0eWxlLmNzc1RleHQgPVxuICAgICAgICBcInBvc2l0aW9uOmZpeGVkO2xlZnQ6LTk5OTlweDt0b3A6MDt3aWR0aDoxcHg7aGVpZ2h0OjFweDtib3JkZXI6MFwiO1xuICAgICAgaWZyYW1lLnNyYyA9IHVybDtcblxuICAgICAgY29uc3QgY2xlYW51cCA9ICgpID0%252BIHtcbiAgICAgICAgc2V0VGltZW91dCgoKSA9PiB7XG4gICAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIFVSTC5yZXZva2VPYmplY3RVUkwodXJsKTtcbiAgICAgICAgICB9IGNhdGNoIChfKSB7fVxuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICBpZnJhbWUucmVtb3ZlKCk7XG4gICAgICAgICAgfSBjYXRjaCAoXykge31cbiAgICAgICAgICBsb2coXCJJZnJhbWUgY2xlYW5lZCB1cC5cIik7XG4gICAgICAgIH0sIDEyMDAwMCk7XG4gICAgICB9O1xuXG4gICAgICBpZnJhbWUub25sb2FkID0gKCkgPT4ge1xuICAgICAgICBsb2coXCJQREYgbG9hZGVkIGluIGlmcmFtZS4gVHJ5aW5nIHByaW50KCkuLi5cIik7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgaWZyYW1lLmNvbnRlbnRXaW5kb3cuZm9jdXMoKTtcbiAgICAgICAgICBpZnJhbWUuY29udGVudFdpbmRvdy5wcmludCgpO1xuICAgICAgICAgIGxvZyhcIlByaW50IGRpYWxvZyB0cmlnZ2VyZWQgKGlmcmFtZSkuXCIsIFwic3VjY2Vzc1wiKTtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIGxvZyhgSWZyYW1lIHByaW50IGZhaWxlZDogJHtlPy5tZXNzYWdlIHx8IGV9YCwgXCJlcnJvclwiKTtcbiAgICAgICAgfSBmaW5hbGx5IHtcbiAgICAgICAgICBjbGVhbnVwKCk7XG4gICAgICAgIH1cbiAgICAgIH07XG5cbiAgICAgIGlmcmFtZS5vbmVycm9yID0gKCkgPT4ge1xuICAgICAgICBsb2coXCJJZnJhbWUgZmFpbGVkIHRvIGxvYWQgUERGLlwiLCBcImVycm9yXCIpO1xuICAgICAgICBjbGVhbnVwKCk7XG4gICAgICB9O1xuXG4gICAgICBkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKGlmcmFtZSk7XG4gICAgfVxuXG4gICAgYXN5bmMgZnVuY3Rpb24gcnVuUHJpbnQocGFnZUluZGV4ZXMsIGNsaWNrRXZlbnQpIHtcbiAgICAgIC8vIERldGVybWluZSBtZXRob2QgYW5kIHByZS1vcGVuIHRhYiBzeW5jaHJvbm91c2x5IGlmIG5lZWRlZCAoYXZvaWRzIHBvcHVwIGJsb2NrZXJzKVxuICAgICAgY29uc3QgbWV0aG9kID0gcGFuZWwucXVlcnlTZWxlY3RvcignaW5wdXRbbmFtZT1cIm1cIl06Y2hlY2tlZCcpLnZhbHVlO1xuXG4gICAgICBsZXQgcHJlT3BlbmVkV2luZG93ID0gbnVsbDtcbiAgICAgIGlmIChtZXRob2QgPT09IFwidGFiXCIpIHtcbiAgICAgICAgLy8gTXVzdCBoYXBwZW4gc3luY2hyb25vdXNseSBkdXJpbmcgdGhlIGNsaWNrIGhhbmRsZXIgY2FsbCBzdGFja1xuICAgICAgICBwcmVPcGVuZWRXaW5kb3cgPSB3aW5kb3cub3BlbihcImFib3V0OmJsYW5rXCIsIFwiX2JsYW5rXCIpO1xuICAgICAgICBpZiAoIXByZU9wZW5lZFdpbmRvdylcbiAgICAgICAgICBsb2coXG4gICAgICAgICAgICBcIlBvcHVwIGJsb2NrZWQgd2hlbiBvcGVuaW5nIGJsYW5rIHRhYi4gV2lsbCB0cnkgZGlyZWN0IG9wZW4gbGF0ZXIuXCIsXG4gICAgICAgICAgICBcImVycm9yXCJcbiAgICAgICAgICApO1xuICAgICAgfVxuXG4gICAgICB0cnkge1xuICAgICAgICBjb25zdCBidWYgPSBhd2FpdCBleHBvcnRTdWJzZXQocGFnZUluZGV4ZXMpO1xuICAgICAgICBpZiAobWV0aG9kID09PSBcInRhYlwiKSB7XG4gICAgICAgICAgcHJpbnROZXdUYWJXaXRoUG9wdXBTYWZlTmF2aWdhdGlvbihidWYsIHByZU9wZW5lZFdpbmRvdyk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgcHJpbnRWaWFJZnJhbWUoYnVmKTtcbiAgICAgICAgfVxuICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBsb2coZT8ubWVzc2FnZSB8fCBTdHJpbmcoZSksIFwiZXJyb3JcIik7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgaWYgKHByZU9wZW5lZFdpbmRvdyAmJiAhcHJlT3BlbmVkV2luZG93LmNsb3NlZClcbiAgICAgICAgICAgIHByZU9wZW5lZFdpbmRvdy5jbG9zZSgpO1xuICAgICAgICB9IGNhdGNoIChfKSB7fVxuICAgICAgfVxuICAgIH1cblxuICAgIC8vIFdpcmUgYnV0dG9uc1xuICAgIHBhbmVsXG4gICAgICAucXVlcnlTZWxlY3RvcihcIiNidG4tcHJpbnQtY3VycmVudFwiKVxuICAgICAgLmFkZEV2ZW50TGlzdGVuZXIoXCJjbGlja1wiLCBhc3luYyAoZXYpID0%252BIHtcbiAgICAgICAgY29uc3QgaWR4ID0gaW5zdGFuY2Uudmlld1N0YXRlPy5jdXJyZW50UGFnZUluZGV4O1xuICAgICAgICBpZiAodHlwZW9mIGlkeCAhPT0gXCJudW1iZXJcIilcbiAgICAgICAgICByZXR1cm4gbG9nKFxuICAgICAgICAgICAgXCJDb3VsZCBub3QgcmVhZCBjdXJyZW50UGFnZUluZGV4IGZyb20gdmlld1N0YXRlLlwiLFxuICAgICAgICAgICAgXCJlcnJvclwiXG4gICAgICAgICAgKTtcbiAgICAgICAgYXdhaXQgcnVuUHJpbnQoW2lkeF0sIGV2KTtcbiAgICAgIH0pO1xuXG4gICAgcGFuZWxcbiAgICAgIC5xdWVyeVNlbGVjdG9yKFwiI2J0bi1wcmludC1yYW5nZVwiKVxuICAgICAgLmFkZEV2ZW50TGlzdGVuZXIoXCJjbGlja1wiLCBhc3luYyAoZXYpID0%252BIHtcbiAgICAgICAgY29uc3QgaW5wdXQgPSBwYW5lbC5xdWVyeVNlbGVjdG9yKFwiI2lucC1yYW5nZVwiKS52YWx1ZS50cmltKCk7XG4gICAgICAgIGlmICghaW5wdXQpIHJldHVybiBsb2coXCJFbnRlciBhIHBhZ2UgbGlzdCwgZS5nLiAxLDMtNSw4XCIsIFwiZXJyb3JcIik7XG5cbiAgICAgICAgY29uc3QgdG90YWwgPSBhd2FpdCBnZXRUb3RhbFBhZ2VzKCk7XG4gICAgICAgIGNvbnN0IGluZGV4ZXMgPSBwYXJzZVJhbmdlKGlucHV0LCB0b3RhbCk7XG4gICAgICAgIGlmICghaW5kZXhlcy5sZW5ndGgpXG4gICAgICAgICAgcmV0dXJuIGxvZyhcIk5vIHBhZ2VzIHNlbGVjdGVkIGFmdGVyIHBhcnNpbmcuXCIsIFwiZXJyb3JcIik7XG4gICAgICAgIGF3YWl0IHJ1blByaW50KGluZGV4ZXMsIGV2KTtcbiAgICAgIH0pO1xuXG4gICAgbG9nKFwiUHJpbnQgY29udHJvbHMgcmVhZHkuXCIsIFwic3VjY2Vzc1wiKTtcbiAgfVxuKTtcbiIsImNzcyI6Ii8qIEFkZCB5b3VyIENTUyBoZXJlICovXG4ifQ%253D%253D