flyingmiata-droid_cite-engine/main.js

282 lines
32 KiB
JavaScript
Raw Normal View History

"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) {
text = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
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() {
this.addRibbonIcon("quote-glyph", "Cite Engine: cite a passage", () => this.cite());
this.addCommand({
id: "stamp-source",
name: "Stamp source (assign citekey + block ids)",
callback: () => this.stampSource()
});
this.addCommand({
id: "cite",
name: "Cite a passage",
callback: () => this.cite()
});
this.addCommand({
id: "integrity-check",
name: "Integrity check (this note)",
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}
quoted: ${i.expected}
source: ${i.actual}`;
if (i.kind === "unresolved-block") return `MISSING BLOCK ${i.citekey}#^${i.blockId}`;
return `UNKNOWN SOURCE ${i.citekey}`;
});
const report = `# Integrity report \u2014 ${file.basename}
${lines.join("\n\n")}
`;
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);
}
new import_obsidian.Notice(`${issues.length} issue(s). See integrity report.`);
}
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL21haW4udHMiLCAic3JjL2NvcmUudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImltcG9ydCB7XG4gIEFwcCxcbiAgRWRpdG9yLFxuICBGdXp6eVN1Z2dlc3RNb2RhbCxcbiAgTW9kYWwsXG4gIE5vdGljZSxcbiAgUGx1Z2luLFxuICBTZXR0aW5nLFxuICBURmlsZSxcbiAgcGFyc2VZYW1sLFxuICBzdHJpbmdpZnlZYW1sLFxufSBmcm9tIFwib2JzaWRpYW5cIjtcbmltcG9ydCB7XG4gIGdlbmVyYXRlQ2l0ZWtleSxcbiAgc3RhbXBOb3RlLFxuICBpbnNlcnRDaXRhdGlvbixcbiAgY2hlY2tJbnRlZ3JpdHksXG4gIHR5cGUgUmVnaXN0cnksXG59IGZyb20gXCIuL2NvcmVcIjtcblxuY29uc3QgRk1fUkUgPSAvXi0tLVxcbihbXFxzXFxTXSo/KVxcbi0tLVxcbj8vO1xuXG5mdW5jdGlvbiBzcGxpdEZyb250bWF0dGVyKHRleHQ6IHN0cmluZyk6IHsgZm06IFJlY29yZDxzdHJpbmcsIHVua25vd24+OyBib2R5OiBzdHJpbmcgfSB7XG4gIHRleHQgPSB0ZXh0LnJlcGxhY2UoL15cdUZFRkYvLCBcIlwiKS5yZXBsYWNlKC9cXHJcXG4vZywgXCJcXG5cIik7XG4gIGNvbnN0IG0gPSB0ZXh0Lm1hdGNoKEZNX1JFKTtcbiAgaWYgKCFtKSByZXR1cm4geyBmbToge30sIGJvZHk6IHRleHQgfTtcbiAgcmV0dXJuIHsgZm06IChwYXJzZVlhbWwobVsxXSkgYXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj4pID8/IHt9LCBib2R5OiB0ZXh0LnNsaWNlKG1bMF0ubGVuZ3RoKSB9O1xufVxuXG5mdW5jdGlvbiBqb2luRnJvbnRtYXR0ZXIoZm06IFJlY29yZDxzdHJpbmcsIHVua25vd24+LCBib2R5OiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gYC0tLVxcbiR7c3RyaW5naWZ5WWFtbChmbSl9LS0tXFxuJHtib2R5fWA7XG59XG5cbi8qKiBCdWlsZCB0aGUgY2xvc2VkLWNvcnB1cyByZWdpc3RyeSBmcm9tIGV2ZXJ5IG5vdGUgY2FycnlpbmcgYSBjaXRla2V5LiAqL1xuYXN5bmMgZnVuY3Rpb24gYnVpbGRSZWdpc3RyeShhcHA6IEFwcCk6IFByb21pc2U8UmVnaXN0cnk+IHtcbiAgY29uc3QgcmVnOiBSZWdpc3RyeSA9IHt9O1xuICBmb3IgKGNvbnN0IGZpbGUgb2YgYXBwLnZhdWx0LmdldE1hcmtkb3duRmlsZXMoKSkge1xuICAgIGNvbnN0IGNhY2hlID0gYXBwLm1ldGFkYXRhQ2FjaGUuZ2V0RmlsZUNhY2hlKGZpbGUpO1xuICAgIGNvbnN0IGNpdGVrZXkgPSBjYWNoZT8uZnJvbnRtYXR0ZXI/LmNpdGVrZXkgYXMgc3RyaW5nIHwgdW5kZWZpbmVkO1xuICAgIGlmICghY2l0ZWtleSkgY29udGludWU7XG4gICAgY29uc3QgdGV4dCA9IGF3YWl0IGFwcC52YXVsdC5jYWNoZWRSZWFkKGZpbGUpO1xuICAgIGNvbnN0IHsgZm0sIGJvZHkgfSA9IHNwbGl0RnJvbnRtYXR0ZXIodGV4dCk7XG4gICAgY29uc3QgeyBibG9ja3MgfSA9IHN0YW1wTm90ZShib2R5KTtcbiAgICByZWdbY2l0ZWtleV0gPSB7XG4gICAgICBtZXRhOiB7XG4gICAgICAgIGNpdGVrZXksXG4gICAgICAgIHRpdGxlOiBTdHJpbmcoZm0udGl0bGUgPz8gZmlsZS5iYXNlbmFtZSksXG4gICAgICAgIGF1dGhvcjogU3RyaW5nKGZtLmF1dGhvciA/PyBcIlwiKSxcbiAgICAgICAgeWVhcjogU3RyaW5nKGZtLnllYXIgPz8gXCJcIiksXG4gICAgICAgIHVybDogZm0udXJsID8gU3RyaW5nKGZtLnVybCkgOiB1bmRlZmluZWQsXG4gICAgICB9LFxuICAgICAgYmxvY2tzLFxuICAgIH07XG4gIH1cbiAgcmV0dXJuIHJlZztcbn1cblxuaW50ZXJmYWNlIEJsb2NrQ2hvaWNlIHtcbiAgY2l0ZWtleTogc3RyaW5nO1xuICBibG9ja0lkOiBzdHJpbmc7XG4gIHRleHQ6IHN0cmluZztcbn1cblxuY2xhc3MgQmxvY2tQaWNrZXIgZXh0ZW5kcyBGdXp6eVN1Z2dlc3RNb2RhbDxCbG9ja0Nob2ljZT4ge1xuICBjb25zdHJ1Y3RvcihhcHA6IEFwcCwgcHJpdmF0ZSBpdGVtczogQmxvY2tDaG9pY2VbXSwgcHJpdmF0ZSBvblBpY2s6IChjOiBCbG9ja0Nob2ljZSkgPT4gdm9pZCkge1xuICAgIHN1cGVyKGFwcCk7XG4gICAgdGhpcy5zZXRQbGFjZWhvbGRlcihcIkNpdGUgYSBwYXNzYWdlIGZyb20geW91ciBzb3VyY2VzXHUyMDI2XCIpO1xuICB9XG4gIGdldEl0ZW1zKCk6IEJsb2NrQ2hvaWNlW10ge1xuICAgIHJldHVybiB0aGlzLml0ZW1zO1xuICB9XG4gIGdldEl0ZW1UZXh0KGM6IEJsb2NrQ2hvaWNlKTogc3RyaW5nIHtcbiAgICByZXR1cm4gYCR7Yy5jaXRla2V5fSBcdTIwMTQgJHtjLnRleHQuc2xpY2UoMCwgODApfWA7XG4gIH1cbiAgb25DaG9vc2VJdGVtKGM6IEJsb2NrQ2hvaWNlKTogdm9pZCB7XG4gICAgdGhpcy5vblBpY2soYyk7XG4gIH1cbn1cblxuY2xhc3MgTG9jYXRvck1vZGFsIGV4dGVuZHMgTW9kYWwge1xuICBwcml2YXRlIHZhbHVlID0gXCJcIjtcbiAgY29uc3RydWN0b3IoYXBwOiBBcHAsIHByaXZhdGUgb25TdWJtaXQ6IChsb2NhdG9yOiBzdHJpbmcpID0+IHZvaWQpIHtcbiAgICBzdXBlcihhcHApO1xuICB9XG4gIG9uT3BlbigpOiB2b2lkIHtcbiAgICB0aGlzLnRpdGxlRWwuc2V0VGV4dChcIkxvY2F0b3IgKHBhZ2UgLyBzZWN0aW9uKVwiKTtcbiAgICBuZXcgU2V0dGluZyh0aGlzLmNvbnRlbnRFbCkuc2V0TmFtZShcIlBhZ2VcIikuYWRkVGV4dCgodCkgPT5cbiAgICAgIHQuc2V0UGxhY2Vob2xkZXIoXCJwLiAxMlwiKS5vbkNoYW5nZSgodikgPT4gKHRoaXMudmFsdWUgPSB2KSksXG4gICAgKTtcbiAgICBuZXcgU2V0dGluZyh0aGlzLmNvbnRlbnRFbCkuYWRkQnV0dG9uKChiKSA9PlxuICAgICAgYlxuICAgICAgICAuc2V0QnV0dG9uVGV4dChcIkluc2VydFwiKVxuICAgICAgICAuc2V0Q3RhKClcbiAgICAgICAgLm9uQ2xpY2soKCkgPT4ge1xuICAgICAgICAgIHRoaXMuY2xvc2UoKTtcbiAgICAgICAgICB0aGlzLm9uU3VibWl0KHRoaXMudmFsdWUgfHwgXCJuLnAuXCIpO1xuICAgICAgICB9KSxcbiAgICApO1xuICB9XG4gIG9uQ2xvc2UoKTogdm9pZCB7XG4gICAgdGhpcy5jb250ZW50RWwuZW1wdHkoKTtcbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBDaXRlRW5naW5lUGx1Z2luIGV4dGVuZHMgUGx1Z2luIHtcbiAgYXN5bmMgb25sb2FkK