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