diff --git a/playground/forms/custom-toolbar-radio-button/index.ts b/playground/forms/custom-toolbar-radio-button/index.ts new file mode 100644 index 0000000..dfd769b --- /dev/null +++ b/playground/forms/custom-toolbar-radio-button/index.ts @@ -0,0 +1,71 @@ +import type { Instance } from "@nutrient-sdk/viewer"; +import { baseOptions } from "../../shared/base-options"; + +let instance: Instance | null = null; + +const item = { + type: "custom", + id: "add-radio-group", + title: "Add Radio Group", + onPress: async () => { + const radioWidget1 = new window.NutrientViewer.Annotations.WidgetAnnotation( + { + id: window.NutrientViewer.generateInstantId(), + pageIndex: 0, + formFieldName: "MyFormField", + boundingBox: new window.NutrientViewer.Geometry.Rect({ + left: 100, + top: 100, + width: 20, + height: 20, + }), + }, + ); + const radioWidget2 = new window.NutrientViewer.Annotations.WidgetAnnotation( + { + id: window.NutrientViewer.generateInstantId(), + pageIndex: 0, + formFieldName: "MyFormField", + boundingBox: new window.NutrientViewer.Geometry.Rect({ + left: 130, + top: 100, + width: 20, + height: 20, + }), + }, + ); + const formField = new window.NutrientViewer.FormFields.RadioButtonFormField( + { + name: "MyFormField", + annotationIds: new window.NutrientViewer.Immutable.List([ + radioWidget1.id, + radioWidget2.id, + ]), + options: new window.NutrientViewer.Immutable.List([ + new window.NutrientViewer.FormOption({ + label: "Option 1", + value: "1", + }), + new window.NutrientViewer.FormOption({ + label: "Option 2", + value: "2", + }), + ]), + defaultValue: "1", + }, + ); + await instance!.create([radioWidget1, radioWidget2, formField]); + }, +}; + +window.NutrientViewer.load({ + ...baseOptions, + theme: window.NutrientViewer.Theme.DARK, + toolbarItems: [ + ...window.NutrientViewer.defaultToolbarItems, + { type: "form-creator" }, + ], +}).then((_instance: Instance) => { + instance = _instance; + instance.setToolbarItems((items) => [...items, item]); +}); diff --git a/playground/forms/custom-toolbar-radio-button/playground.mdx b/playground/forms/custom-toolbar-radio-button/playground.mdx new file mode 100644 index 0000000..6f6244e --- /dev/null +++ b/playground/forms/custom-toolbar-radio-button/playground.mdx @@ -0,0 +1,6 @@ +--- +category: forms +title: Add Grouped Radio Buttons with Custom Toolbar Button +description: Create a custom toolbar item that programmatically places pre-grouped radio buttons with a single click, avoiding manual renaming. +keywords: [forms, radio-button, custom-toolbar, toolbar-item, widget, form-field, radio-group, grouped radio-buttons] +--- diff --git a/playground/forms/custom-toolbar-radio-button/playground.url b/playground/forms/custom-toolbar-radio-button/playground.url new file mode 100644 index 0000000..dcaab85 --- /dev/null +++ b/playground/forms/custom-toolbar-radio-button/playground.url @@ -0,0 +1,2 @@ +[InternetShortcut] +URL=https://playground.pspdfkit.com/?p=eyJ2IjoxLCJjc3MiOiIvKiBBZGQgeW91ciBDU1MgaGVyZSAqL1xuIiwic2V0dGluZ3MiOnsiZmlsZU5hbWUiOiJiYXNpYy5wZGYifSwianMiOiJsZXQgaW5zdGFuY2UgPSBudWxsO1xuXG5jb25zdCBpdGVtID0ge1xuICB0eXBlOiBcImN1c3RvbVwiLFxuICBpZDogXCJhZGQtcmFkaW8tZ3JvdXBcIixcbiAgdGl0bGU6IFwiQWRkIFJhZGlvIEdyb3VwXCIsXG4gIG9uUHJlc3M6IGFzeW5jICgpID0%252BIHtcbiAgICBjb25zdCByYWRpb1dpZGdldDEgPSBuZXcgTnV0cmllbnRWaWV3ZXIuQW5ub3RhdGlvbnMuV2lkZ2V0QW5ub3RhdGlvbih7XG4gIGlkOiBOdXRyaWVudFZpZXdlci5nZW5lcmF0ZUluc3RhbnRJZCgpLFxuICBwYWdlSW5kZXg6IDAsXG4gIGZvcm1GaWVsZE5hbWU6IFwiTXlGb3JtRmllbGRcIixcbiAgYm91bmRpbmdCb3g6IG5ldyBOdXRyaWVudFZpZXdlci5HZW9tZXRyeS5SZWN0KHtcbiAgICBsZWZ0OiAxMDAsXG4gICAgdG9wOiAxMDAsXG4gICAgd2lkdGg6IDIwLFxuICAgIGhlaWdodDogMjBcbiAgfSlcbn0pO1xuY29uc3QgcmFkaW9XaWRnZXQyID0gbmV3IE51dHJpZW50Vmlld2VyLkFubm90YXRpb25zLldpZGdldEFubm90YXRpb24oe1xuICBpZDogTnV0cmllbnRWaWV3ZXIuZ2VuZXJhdGVJbnN0YW50SWQoKSxcbiAgcGFnZUluZGV4OiAwLFxuICBmb3JtRmllbGROYW1lOiBcIk15Rm9ybUZpZWxkXCIsXG4gIGJvdW5kaW5nQm94OiBuZXcgTnV0cmllbnRWaWV3ZXIuR2VvbWV0cnkuUmVjdCh7XG4gICAgbGVmdDogMTMwLFxuICAgIHRvcDogMTAwLFxuICAgIHdpZHRoOiAyMCxcbiAgICBoZWlnaHQ6IDIwXG4gIH0pXG59KTtcbmNvbnN0IGZvcm1GaWVsZCA9IG5ldyBOdXRyaWVudFZpZXdlci5Gb3JtRmllbGRzLlJhZGlvQnV0dG9uRm9ybUZpZWxkKHtcbiAgbmFtZTogXCJNeUZvcm1GaWVsZFwiLFxuICBhbm5vdGF0aW9uSWRzOiBuZXcgTnV0cmllbnRWaWV3ZXIuSW1tdXRhYmxlLkxpc3QoW3JhZGlvV2lkZ2V0MS5pZCwgcmFkaW9XaWRnZXQyLmlkXSksXG4gIG9wdGlvbnM6IG5ldyBOdXRyaWVudFZpZXdlci5JbW11dGFibGUuTGlzdChbXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMVwiLCB2YWx1ZTogXCIxXCIgfSksXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMlwiLCB2YWx1ZTogXCIyXCIgfSlcbiAgXSksXG4gIGRlZmF1bHRWYWx1ZTogXCIxXCJcbn0pO1xuYXdhaXQgaW5zdGFuY2UuY3JlYXRlKFtyYWRpb1dpZGdldDEsIHJhZGlvV2lkZ2V0MiwgZm9ybUZpZWxkXSk7XG4gIH1cbn07XG5cblxuXG5cblxuXG5cbk51dHJpZW50Vmlld2VyLmxvYWQoe1xuICAuLi5iYXNlT3B0aW9ucyxcbiAgdGhlbWU6IE51dHJpZW50Vmlld2VyLlRoZW1lLkRBUkssXG4gIHRvb2xiYXJJdGVtczogWy4uLk51dHJpZW50Vmlld2VyLmRlZmF1bHRUb29sYmFySXRlbXMsIHsgdHlwZTogXCJmb3JtLWNyZWF0b3JcIiB9XVxufSkudGhlbigoX2luc3RhbmNlKSA9PiB7XG4gICAgaW5zdGFuY2UgPSBfaW5zdGFuY2U7XG4gICAgaW5zdGFuY2Uuc2V0VG9vbGJhckl0ZW1zKChpdGVtcykgPT4gWy4uLml0ZW1zLCBpdGVtXSk7XG5cbn0pO1xuXHQifQ%253D%253D 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