2026-06-14 16:20:41 +00:00
|
|
|
"use strict";
|
|
|
|
|
var __defProp = Object.defineProperty;
|
|
|
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
|
|
var __export = (target, all) => {
|
|
|
|
|
for (var name in all)
|
|
|
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
|
|
|
};
|
|
|
|
|
var __copyProps = (to, from, except, desc) => {
|
|
|
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
|
|
|
for (let key of __getOwnPropNames(from))
|
|
|
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
|
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
|
|
|
}
|
|
|
|
|
return to;
|
|
|
|
|
};
|
|
|
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
|
|
|
|
|
|
// src/main.ts
|
|
|
|
|
var main_exports = {};
|
|
|
|
|
__export(main_exports, {
|
|
|
|
|
default: () => CiteEnginePlugin
|
|
|
|
|
});
|
|
|
|
|
module.exports = __toCommonJS(main_exports);
|
|
|
|
|
var import_obsidian = require("obsidian");
|
|
|
|
|
|
|
|
|
|
// src/core.ts
|
|
|
|
|
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").split("-").slice(0, 4).join("-");
|
|
|
|
|
var surname = (author) => {
|
|
|
|
|
const a = author.trim();
|
|
|
|
|
if (a.includes(",")) return slug(a.split(",")[0]);
|
|
|
|
|
const parts = a.split(/\s+/);
|
|
|
|
|
return slug(parts[parts.length - 1] || a);
|
|
|
|
|
};
|
|
|
|
|
function generateCitekey(meta) {
|
|
|
|
|
return [surname(meta.author), meta.year, slug(meta.title)].filter(Boolean).join("-");
|
|
|
|
|
}
|
|
|
|
|
function newBlockId(existing) {
|
|
|
|
|
let id;
|
|
|
|
|
do {
|
|
|
|
|
id = "blk-" + Math.random().toString(36).slice(2, 8);
|
|
|
|
|
} while (existing.has(id));
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
var BLOCK_REF_RE = /\s\^([A-Za-z0-9-]+)\s*$/;
|
|
|
|
|
function stampNote(body) {
|
|
|
|
|
body = body.replace(/\r\n/g, "\n");
|
|
|
|
|
const paras = body.split(/\n{2,}/);
|
|
|
|
|
const existing = /* @__PURE__ */ new Set();
|
|
|
|
|
for (const p of paras) {
|
|
|
|
|
const m = p.match(BLOCK_REF_RE);
|
|
|
|
|
if (m) existing.add(m[1]);
|
|
|
|
|
}
|
|
|
|
|
const blocks = {};
|
|
|
|
|
const out = paras.map((p) => {
|
|
|
|
|
const trimmed = p.trim();
|
|
|
|
|
if (!trimmed) return p;
|
|
|
|
|
const m = p.match(BLOCK_REF_RE);
|
|
|
|
|
if (m) {
|
|
|
|
|
blocks[m[1]] = p.replace(BLOCK_REF_RE, "").trim();
|
|
|
|
|
return p;
|
|
|
|
|
}
|
|
|
|
|
const id = newBlockId(existing);
|
|
|
|
|
existing.add(id);
|
|
|
|
|
blocks[id] = trimmed;
|
|
|
|
|
return `${trimmed} ^${id}`;
|
|
|
|
|
});
|
|
|
|
|
return { content: out.join("\n\n"), blocks };
|
|
|
|
|
}
|
|
|
|
|
function insertCitation(reg, c) {
|
|
|
|
|
const entry = reg[c.citekey];
|
|
|
|
|
if (!entry) throw new Error(`unknown source: ${c.citekey}`);
|
|
|
|
|
if (!(c.blockId in entry.blocks)) throw new Error(`unknown block ${c.blockId} in ${c.citekey}`);
|
|
|
|
|
const ref = `[[${c.citekey}#^${c.blockId}|${c.locator}]]`;
|
|
|
|
|
return c.quote ? `"${c.quote}" ${ref}` : ref;
|
|
|
|
|
}
|
|
|
|
|
var CITE_RE = /(?:"([^"]*)"\s*)?\[\[([^#\]|]+)#\^([A-Za-z0-9-]+)\|([^\]]*)\]\]/g;
|
|
|
|
|
function checkIntegrity(text, reg) {
|
|
|
|
|
const issues = [];
|
|
|
|
|
for (const m of text.matchAll(CITE_RE)) {
|
|
|
|
|
const raw = m[0];
|
|
|
|
|
const quote = m[1];
|
|
|
|
|
const citekey = m[2];
|
|
|
|
|
const blockId = m[3];
|
|
|
|
|
const entry = reg[citekey];
|
|
|
|
|
if (!entry) {
|
|
|
|
|
issues.push({ kind: "unresolved-source", citekey, raw });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!(blockId in entry.blocks)) {
|
|
|
|
|
issues.push({ kind: "unresolved-block", citekey, blockId, raw });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (quote !== void 0) {
|
|
|
|
|
const actual = entry.blocks[blockId];
|
|
|
|
|
if (quote.trim() !== actual.trim()) {
|
|
|
|
|
issues.push({ kind: "drift", citekey, blockId, expected: quote, actual, raw });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return issues;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// src/main.ts
|
|
|
|
|
var FM_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
|
|
|
function splitFrontmatter(text) {
|
2026-06-16 00:07:32 +00:00
|
|
|
text = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
2026-06-14 16:20:41 +00:00
|
|
|
const m = text.match(FM_RE);
|
|
|
|
|
if (!m) return { fm: {}, body: text };
|
|
|
|
|
return { fm: (0, import_obsidian.parseYaml)(m[1]) ?? {}, body: text.slice(m[0].length) };
|
|
|
|
|
}
|
|
|
|
|
function joinFrontmatter(fm, body) {
|
|
|
|
|
return `---
|
|
|
|
|
${(0, import_obsidian.stringifyYaml)(fm)}---
|
|
|
|
|
${body}`;
|
|
|
|
|
}
|
|
|
|
|
async function buildRegistry(app) {
|
|
|
|
|
const reg = {};
|
|
|
|
|
for (const file of app.vault.getMarkdownFiles()) {
|
|
|
|
|
const cache = app.metadataCache.getFileCache(file);
|
|
|
|
|
const citekey = cache?.frontmatter?.citekey;
|
|
|
|
|
if (!citekey) continue;
|
|
|
|
|
const text = await app.vault.cachedRead(file);
|
|
|
|
|
const { fm, body } = splitFrontmatter(text);
|
|
|
|
|
const { blocks } = stampNote(body);
|
|
|
|
|
reg[citekey] = {
|
|
|
|
|
meta: {
|
|
|
|
|
citekey,
|
|
|
|
|
title: String(fm.title ?? file.basename),
|
|
|
|
|
author: String(fm.author ?? ""),
|
|
|
|
|
year: String(fm.year ?? ""),
|
|
|
|
|
url: fm.url ? String(fm.url) : void 0
|
|
|
|
|
},
|
|
|
|
|
blocks
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return reg;
|
|
|
|
|
}
|
|
|
|
|
var BlockPicker = class extends import_obsidian.FuzzySuggestModal {
|
|
|
|
|
constructor(app, items, onPick) {
|
|
|
|
|
super(app);
|
|
|
|
|
this.items = items;
|
|
|
|
|
this.onPick = onPick;
|
|
|
|
|
this.setPlaceholder("Cite a passage from your sources\u2026");
|
|
|
|
|
}
|
|
|
|
|
getItems() {
|
|
|
|
|
return this.items;
|
|
|
|
|
}
|
|
|
|
|
getItemText(c) {
|
|
|
|
|
return `${c.citekey} \u2014 ${c.text.slice(0, 80)}`;
|
|
|
|
|
}
|
|
|
|
|
onChooseItem(c) {
|
|
|
|
|
this.onPick(c);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var LocatorModal = class extends import_obsidian.Modal {
|
|
|
|
|
constructor(app, onSubmit) {
|
|
|
|
|
super(app);
|
|
|
|
|
this.onSubmit = onSubmit;
|
|
|
|
|
this.value = "";
|
|
|
|
|
}
|
|
|
|
|
onOpen() {
|
|
|
|
|
this.titleEl.setText("Locator (page / section)");
|
|
|
|
|
new import_obsidian.Setting(this.contentEl).setName("Page").addText(
|
|
|
|
|
(t) => t.setPlaceholder("p. 12").onChange((v) => this.value = v)
|
|
|
|
|
);
|
|
|
|
|
new import_obsidian.Setting(this.contentEl).addButton(
|
|
|
|
|
(b) => b.setButtonText("Insert").setCta().onClick(() => {
|
|
|
|
|
this.close();
|
|
|
|
|
this.onSubmit(this.value || "n.p.");
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
onClose() {
|
|
|
|
|
this.contentEl.empty();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var CiteEnginePlugin = class extends import_obsidian.Plugin {
|
|
|
|
|
async onload() {
|
2026-06-15 22:26:35 +00:00
|
|
|
this.addRibbonIcon("quote-glyph", "Cite Engine: cite a passage", () => this.cite());
|
2026-06-14 16:20:41 +00:00
|
|
|
this.addCommand({
|
|
|
|
|
id: "stamp-source",
|
|
|
|
|
name: "Stamp source (assign citekey + block ids)",
|
2026-06-15 22:26:35 +00:00
|
|
|
callback: () => this.stampSource()
|
2026-06-14 16:20:41 +00:00
|
|
|
});
|
|
|
|
|
this.addCommand({
|
|
|
|
|
id: "cite",
|
|
|
|
|
name: "Cite a passage",
|
2026-06-15 22:26:35 +00:00
|
|
|
callback: () => this.cite()
|
2026-06-14 16:20:41 +00:00
|
|
|
});
|
|
|
|
|
this.addCommand({
|
|
|
|
|
id: "integrity-check",
|
|
|
|
|
name: "Integrity check (this note)",
|
2026-06-15 22:26:35 +00:00
|
|
|
callback: () => this.integrityCheck()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/** Active markdown editor + its file, or null with a guiding notice. */
|
|
|
|
|
activeNote() {
|
|
|
|
|
const ae = this.app.workspace.activeEditor;
|
|
|
|
|
if (ae?.editor && ae.file) return { editor: ae.editor, file: ae.file };
|
|
|
|
|
new import_obsidian.Notice("Open a markdown note first, then run this Cite Engine command.");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
async stampSource() {
|
|
|
|
|
const ctx = this.activeNote();
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
const { file } = ctx;
|
|
|
|
|
const text = await this.app.vault.read(file);
|
|
|
|
|
const { fm, body } = splitFrontmatter(text);
|
|
|
|
|
if (!fm.citekey) {
|
|
|
|
|
fm.citekey = generateCitekey({
|
|
|
|
|
author: String(fm.author ?? ""),
|
|
|
|
|
year: String(fm.year ?? ""),
|
|
|
|
|
title: String(fm.title ?? file.basename)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const { content } = stampNote(body);
|
|
|
|
|
await this.app.vault.modify(file, joinFrontmatter(fm, content));
|
|
|
|
|
new import_obsidian.Notice(`Stamped as ${fm.citekey}`);
|
|
|
|
|
}
|
|
|
|
|
async cite() {
|
|
|
|
|
const ctx = this.activeNote();
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
const { editor } = ctx;
|
|
|
|
|
const reg = await buildRegistry(this.app);
|
|
|
|
|
const items = [];
|
|
|
|
|
for (const [citekey, entry] of Object.entries(reg)) {
|
|
|
|
|
for (const [blockId, text] of Object.entries(entry.blocks)) {
|
|
|
|
|
items.push({ citekey, blockId, text });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
new import_obsidian.Notice("No stamped sources yet. Run \u201CStamp source\u201D on a clipped note first.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
new BlockPicker(this.app, items, (choice) => {
|
|
|
|
|
new LocatorModal(this.app, (locator) => {
|
|
|
|
|
const cite = insertCitation(reg, {
|
|
|
|
|
citekey: choice.citekey,
|
|
|
|
|
blockId: choice.blockId,
|
|
|
|
|
locator,
|
|
|
|
|
quote: choice.text
|
|
|
|
|
});
|
|
|
|
|
editor.replaceSelection(cite);
|
|
|
|
|
}).open();
|
|
|
|
|
}).open();
|
|
|
|
|
}
|
|
|
|
|
async integrityCheck() {
|
|
|
|
|
const ctx = this.activeNote();
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
const { file } = ctx;
|
|
|
|
|
const reg = await buildRegistry(this.app);
|
|
|
|
|
const text = await this.app.vault.read(file);
|
|
|
|
|
const issues = checkIntegrity(text, reg);
|
|
|
|
|
if (issues.length === 0) {
|
|
|
|
|
new import_obsidian.Notice("\u2713 All citations resolve. No drift.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const lines = issues.map((i) => {
|
|
|
|
|
if (i.kind === "drift") return `DRIFT ${i.citekey}#^${i.blockId}
|
2026-06-14 16:20:41 +00:00
|
|
|
quoted: ${i.expected}
|
|
|
|
|
source: ${i.actual}`;
|
2026-06-15 22:26:35 +00:00
|
|
|
if (i.kind === "unresolved-block") return `MISSING BLOCK ${i.citekey}#^${i.blockId}`;
|
|
|
|
|
return `UNKNOWN SOURCE ${i.citekey}`;
|
|
|
|
|
});
|
|
|
|
|
const report = `# Integrity report \u2014 ${file.basename}
|
2026-06-14 16:20:41 +00:00
|
|
|
|
|
|
|
|
${lines.join("\n\n")}
|
|
|
|
|
`;
|
2026-06-15 23:50:13 +00:00
|
|
|
const reportPath = `Integrity report \u2014 ${file.basename}.md`;
|
|
|
|
|
const existingReport = this.app.vault.getAbstractFileByPath(reportPath);
|
|
|
|
|
if (existingReport instanceof import_obsidian.TFile) {
|
|
|
|
|
await this.app.vault.modify(existingReport, report);
|
|
|
|
|
} else {
|
|
|
|
|
await this.app.vault.create(reportPath, report);
|
|
|
|
|
}
|
2026-06-15 22:26:35 +00:00
|
|
|
new import_obsidian.Notice(`${issues.length} issue(s). See integrity report.`);
|
2026-06-14 16:20:41 +00:00
|
|
|
}
|
|
|
|
|
};
|
2026-06-15 23:50:13 +00:00
|
|
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL21haW4udHMiLCAic3JjL2NvcmUudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImltcG9ydCB7XG4gIEFwcCxcbiAgRWRpdG9yLFxuICBGdXp6eVN1Z2dlc3RNb2RhbCxcbiAgTW9kYWwsXG4gIE5vdGljZSxcbiAgUGx1Z2luLFxuICBTZXR0aW5nLFxuICBURmlsZSxcbiAgcGFyc2VZYW1sLFxuICBzdHJpbmdpZnlZYW1sLFxufSBmcm9tIFwib2JzaWRpYW5cIjtcbmltcG9ydCB7XG4gIGdlbmVyYXRlQ2l0ZWtleSxcbiAgc3RhbXBOb3RlLFxuICBpbnNlcnRDaXRhdGlvbixcbiAgY2hlY2tJbnRlZ3JpdHksXG4gIHR5cGUgUmVnaXN0cnksXG59IGZyb20gXCIuL2NvcmVcIjtcblxuY29uc3QgRk1fUkUgPSAvXi0tLVxcbihbXFxzXFxTXSo/KVxcbi0tLVxcbj8vO1xuXG5mdW5jdGlvbiBzcGxpdEZyb250bWF0dGVyKHRleHQ6IHN0cmluZyk6IHsgZm06IFJlY29yZDxzdHJpbmcsIHVua25vd24+OyBib2R5OiBzdHJpbmcgfSB7XG4gIHRleHQgPSB0ZXh0LnJlcGxhY2UoL15cdUZFRkYvLCBcIlwiKS5yZXBsYWNlKC9cXHJcXG4vZywgXCJcXG5cIik7XG4gIGNvbnN0IG0gPSB0ZXh0Lm1hdGNoKEZNX1JFKTtcbiAgaWYgKCFtKSByZXR1cm4geyBmbToge30sIGJvZHk6IHRleHQgfTtcbiAgcmV0dXJuIHsgZm06IChwYXJzZVlhbWwobVsxXSkgYXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj4pID8/IHt9LCBib2R5OiB0ZXh0LnNsaWNlKG1bMF0ubGVuZ3RoKSB9O1xufVxuXG5mdW5jdGlvbiBqb2luRnJvbnRtYXR0ZXIoZm06IFJlY29yZDxzdHJpbmcsIHVua25vd24+LCBib2R5OiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gYC0tLVxcbiR7c3RyaW5naWZ5WWFtbChmbSl9LS0tXFxuJHtib2R5fWA7XG59XG5cbi8qKiBCdWlsZCB0aGUgY2xvc2VkLWNvcnB1cyByZWdpc3RyeSBmcm9tIGV2ZXJ5IG5vdGUgY2FycnlpbmcgYSBjaXRla2V5LiAqL1xuYXN5bmMgZnVuY3Rpb24gYnVpbGRSZWdpc3RyeShhcHA6IEFwcCk6IFByb21pc2U8UmVnaXN0cnk+IHtcbiAgY29uc3QgcmVnOiBSZWdpc3RyeSA9IHt9O1xuICBmb3IgKGNvbnN0IGZpbGUgb2YgYXBwLnZhdWx0LmdldE1hcmtkb3duRmlsZXMoKSkge1xuICAgIGNvbnN0IGNhY2hlID0gYXBwLm1ldGFkYXRhQ2FjaGUuZ2V0RmlsZUNhY2hlKGZpbGUpO1xuICAgIGNvbnN0IGNpdGVrZXkgPSBjYWNoZT8uZnJvbnRtYXR0ZXI/LmNpdGVrZXkgYXMgc3RyaW5nIHwgdW5kZWZpbmVkO1xuICAgIGlmICghY2l0ZWtleSkgY29udGludWU7XG4gICAgY29uc3QgdGV4dCA9IGF3YWl0IGFwcC52YXVsdC5jYWNoZWRSZWFkKGZpbGUpO1xuICAgIGNvbnN0IHsgZm0sIGJvZHkgfSA9IHNwbGl0RnJvbnRtYXR0ZXIodGV4dCk7XG4gICAgY29uc3QgeyBibG9ja3MgfSA9IHN0YW1wTm90ZShib2R5KTtcbiAgICByZWdbY2l0ZWtleV0gPSB7XG4gICAgICBtZXRhOiB7XG4gICAgICAgIGNpdGVrZXksXG4gICAgICAgIHRpdGxlOiBTdHJpbmcoZm0udGl0bGUgPz8gZmlsZS5iYXNlbmFtZSksXG4gICAgICAgIGF1dGhvcjogU3RyaW5nKGZtLmF1dGhvciA/PyBcIlwiKSxcbiAgICAgICAgeWVhcjogU3RyaW5nKGZtLnllYXIgPz8gXCJcIiksXG4gICAgICAgIHVybDogZm0udXJsID8gU3RyaW5nKGZtLnVybCkgOiB1bmRlZmluZWQsXG4gICAgICB9LFxuICAgICAgYmxvY2tzLFxuICAgIH07XG4gIH1cbiAgcmV0dXJuIHJlZztcbn1cblxuaW50ZXJmYWNlIEJsb2NrQ2hvaWNlIHtcbiAgY2l0ZWtleTogc3RyaW5nO1xuICBibG9ja0lkOiBzdHJpbmc7XG4gIHRleHQ6IHN0cmluZztcbn1cblxuY2xhc3MgQmxvY2tQaWNrZXIgZXh0ZW5kcyBGdXp6eVN1Z2dlc3RNb2RhbDxCbG9ja0Nob2ljZT4ge1xuICBjb25zdHJ1Y3RvcihhcHA6IEFwcCwgcHJpdmF0ZSBpdGVtczogQmxvY2tDaG9pY2VbXSwgcHJpdmF0ZSBvblBpY2s6IChjOiBCbG9ja0Nob2ljZSkgPT4gdm9pZCkge1xuICAgIHN1cGVyKGFwcCk7XG4gICAgdGhpcy5zZXRQbGFjZWhvbGRlcihcIkNpdGUgYSBwYXNzYWdlIGZyb20geW91ciBzb3VyY2VzXHUyMDI2XCIpO1xuICB9XG4gIGdldEl0ZW1zKCk6IEJsb2NrQ2hvaWNlW10ge1xuICAgIHJldHVybiB0aGlzLml0ZW1zO1xuICB9XG4gIGdldEl0ZW1UZXh0KGM6IEJsb2NrQ2hvaWNlKTogc3RyaW5nIHtcbiAgICByZXR1cm4gYCR7Yy5jaXRla2V5fSBcdTIwMTQgJHtjLnRleHQuc2xpY2UoMCwgODApfWA7XG4gIH1cbiAgb25DaG9vc2VJdGVtKGM6IEJsb2NrQ2hvaWNlKTogdm9pZCB7XG4gICAgdGhpcy5vblBpY2soYyk7XG4gIH1cbn1cblxuY2xhc3MgTG9jYXRvck1vZGFsIGV4dGVuZHMgTW9kYWwge1xuICBwcml2YXRlIHZhbHVlID0gXCJcIjtcbiAgY29uc3RydWN0b3IoYXBwOiBBcHAsIHByaXZhdGUgb25TdWJtaXQ6IChsb2NhdG9yOiBzdHJpbmcpID0+IHZvaWQpIHtcbiAgICBzdXBlcihhcHApO1xuICB9XG4gIG9uT3BlbigpOiB2b2lkIHtcbiAgICB0aGlzLnRpdGxlRWwuc2V0VGV4dChcIkxvY2F0b3IgKHBhZ2UgLyBzZWN0aW9uKVwiKTtcbiAgICBuZXcgU2V0dGluZyh0aGlzLmNvbnRlbnRFbCkuc2V0TmFtZShcIlBhZ2VcIikuYWRkVGV4dCgodCkgPT5cbiAgICAgIHQuc2V0UGxhY2Vob2xkZXIoXCJwLiAxMlwiKS5vbkNoYW5nZSgodikgPT4gKHRoaXMudmFsdWUgPSB2KSksXG4gICAgKTtcbiAgICBuZXcgU2V0dGluZyh0aGlzLmNvbnRlbnRFbCkuYWRkQnV0dG9uKChiKSA9PlxuICAgICAgYlxuICAgICAgICAuc2V0QnV0dG9uVGV4dChcIkluc2VydFwiKVxuICAgICAgICAuc2V0Q3RhKClcbiAgICAgICAgLm9uQ2xpY2soKCkgPT4ge1xuICAgICAgICAgIHRoaXMuY2xvc2UoKTtcbiAgICAgICAgICB0aGlzLm9uU3VibWl0KHRoaXMudmFsdWUgfHwgXCJuLnAuXCIpO1xuICAgICAgICB9KSxcbiAgICApO1xuICB9XG4gIG9uQ2xvc2UoKTogdm9pZCB7XG4gICAgdGhpcy5jb250ZW50RWwuZW1wdHkoKTtcbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBDaXRlRW5naW5lUGx1Z2luIGV4dGVuZHMgUGx1Z2luIHtcbiAgYXN5bmMgb25sb2FkK
|