import { AddonBase, EditorMessage, OutlineType } from "./base";
import { loadTranslator, TRANSLATOR_ID_BETTER_MARKDOWN } from "./exportMD";
import { pick } from "./file_picker";
const TreeModel = require("./treemodel");
class Knowledge extends AddonBase {
currentLine: number;
currentNodeID: number;
workspaceWindow: Window;
workspaceTabId: string;
_exportNote: ZoteroItem;
_exportPath: string;
_exportFileDict: object;
constructor(parent: Knowledge4Zotero) {
super(parent);
this.currentLine = -1;
this.currentNodeID = -1;
}
getWorkspaceNote(): ZoteroItem {
return Zotero.Items.get(
Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID")
);
}
getWorkspaceWindow(): Window | boolean {
if (this.workspaceWindow && !this.workspaceWindow.closed) {
return this.workspaceWindow;
}
return false;
}
async openWorkspaceWindow(
type: "window" | "tab" = "tab",
reopen: boolean = false,
select: boolean = true
) {
if (this.getWorkspaceWindow()) {
if (!reopen) {
Zotero.debug("openWorkspaceWindow: focus");
if (this.workspaceTabId) {
Zotero_Tabs.select(this.workspaceTabId);
} else {
(this.getWorkspaceWindow() as Window).focus();
}
return;
} else {
Zotero.debug("openWorkspaceWindow: reopen");
this.closeWorkspaceWindow();
}
}
if (type === "window") {
Zotero.debug("openWorkspaceWindow: as window");
this._Addon.views._initIframe = Zotero.Promise.defer();
let win = window.open(
"chrome://Knowledge4Zotero/content/workspace.xul",
"_blank",
"chrome,extrachrome,menubar,resizable,scrollbars,status,width=1000,height=600"
);
this.workspaceWindow = win;
this.workspaceTabId = "";
await this.waitWorkspaceReady();
this.setWorkspaceNote("main");
this.currentLine = -1;
this._Addon.views.initKnowledgeWindow(win);
this._Addon.views.switchView(OutlineType.treeView);
this._Addon.views.updateOutline();
} else {
Zotero.debug("openWorkspaceWindow: as tab");
this._Addon.views._initIframe = Zotero.Promise.defer();
// Avoid sidebar show up
Zotero_Tabs.jump(0);
let { id, container } = Zotero_Tabs.add({
type: "betternotes",
title: Zotero.locale.includes("zh") ? "工作区" : "Workspace",
index: 1,
data: {},
select: select,
onClose: undefined,
});
this.workspaceTabId = id;
const _iframe = window.document.createElement("browser");
_iframe.setAttribute("class", "reader");
_iframe.setAttribute("flex", "1");
_iframe.setAttribute("type", "content");
_iframe.setAttribute(
"src",
"chrome://Knowledge4Zotero/content/workspace.xul"
);
container.appendChild(_iframe);
// @ts-ignore
this.workspaceWindow = _iframe.contentWindow;
await this.waitWorkspaceReady();
this._Addon.views.hideMenuBar(this.workspaceWindow.document);
this.currentLine = -1;
this._Addon.views.initKnowledgeWindow(this.workspaceWindow);
this._Addon.views.switchView(OutlineType.treeView);
this._Addon.views.updateOutline();
}
}
closeWorkspaceWindow() {
if (this.getWorkspaceWindow()) {
if (this.workspaceTabId) {
Zotero_Tabs.close(this.workspaceTabId);
} else {
(this.getWorkspaceWindow() as Window).close();
}
}
this.workspaceTabId = "";
}
async waitWorkspaceReady() {
let _window = this.getWorkspaceWindow() as Window;
if (!_window) {
return false;
}
let t = 0;
while (_window.document.readyState !== "complete" && t < 500) {
t += 1;
await Zotero.Promise.delay(10);
}
return t < 500;
}
async getWorkspaceEditor(type: "main" | "preview" = "main") {
let _window = this.getWorkspaceWindow() as Window;
if (!_window) {
return;
}
await this.waitWorkspaceReady();
return _window.document.getElementById(`zotero-note-editor-${type}`);
}
async getWorkspaceEditorInstance(
type: "main" | "preview" = "main",
wait: boolean = true
): Promise {
let noteEditor = (await this.getWorkspaceEditor(type)) as any;
let t = 0;
while (wait && !noteEditor.getCurrentInstance() && t < 500) {
t += 1;
await Zotero.Promise.delay(10);
}
return noteEditor.getCurrentInstance() as EditorInstance;
}
async setWorkspaceNote(
type: "main" | "preview" = "main",
note: ZoteroItem = undefined
) {
let _window = this.getWorkspaceWindow() as Window;
note = note || this.getWorkspaceNote();
if (!_window) {
return;
}
if (type === "preview") {
_window.document
.getElementById("preview-splitter")
.setAttribute("state", "open");
} else {
// Set line to default
this.currentLine = -1;
}
await this.waitWorkspaceReady();
let noteEditor: any = await this.getWorkspaceEditor(type);
noteEditor.mode = "edit";
noteEditor.viewMode = "library";
noteEditor.parent = null;
noteEditor.item = note;
if (!noteEditor || !noteEditor.getCurrentInstance()) {
noteEditor.initEditor();
}
await noteEditor._initPromise;
let t = 0;
while (!noteEditor.getCurrentInstance() && t < 500) {
t += 1;
await Zotero.Promise.delay(10);
}
await this._Addon.events.onEditorEvent(
new EditorMessage("enterWorkspace", {
editorInstance: noteEditor.getCurrentInstance(),
params: type,
})
);
if (type === "main") {
this._Addon.views.updateOutline();
}
}
getLinesInNote(note: ZoteroItem): string[] {
note = note || this.getWorkspaceNote();
if (!note) {
return [];
}
let noteText: string = note.getNote();
let containerIndex = noteText.search(/data-schema-version="8">/g);
if (containerIndex != -1) {
noteText = noteText.substring(
containerIndex + 'data-schema-version="8">'.length,
noteText.length - "".length
);
}
let noteLines = noteText.split("\n").filter((e) => e);
let tagStack = [];
let toPush = [];
let toRemove = 0;
let nextAppend = false;
const forceInline = ["table", "blockquote", "pre", "li"];
const selfInline = ["ol", "ul"];
const parsedLines = [];
for (let line of noteLines) {
for (const tag of forceInline) {
const startReg = `<${tag}>`;
const endReg = `${tag}>`;
const startIdx = line.search(startReg);
const endIdx = line.search(endReg);
if (startIdx !== -1 && endIdx === -1) {
toPush.push(tag);
} else if (endIdx !== -1) {
toRemove += 1;
}
}
if (tagStack.filter((e) => forceInline.indexOf(e) !== -1).length === 0) {
let nextLoop = false;
for (const tag of selfInline) {
const startReg = new RegExp(`<${tag}>`);
const endReg = new RegExp(`${tag}>`);
const startIdx = line.search(startReg);
const endIdx = line.search(endReg);
if (startIdx !== -1 && endIdx === -1) {
nextAppend = true;
nextLoop = true;
parsedLines.push(line);
break;
}
if (endIdx !== -1) {
parsedLines[parsedLines.length - 1] += `\n${line}`;
nextLoop = true;
break;
}
}
if (nextLoop) {
continue;
}
}
if (tagStack.length === 0 && !nextAppend) {
parsedLines.push(line);
} else {
parsedLines[parsedLines.length - 1] += `\n${line}`;
nextAppend = false;
}
if (toPush.length > 0) {
tagStack = tagStack.concat(toPush);
toPush = [];
}
while (toRemove > 0) {
tagStack.pop();
toRemove -= 1;
}
}
return parsedLines;
}
setLinesToNote(note: ZoteroItem, noteLines: string[]) {
note = note || this.getWorkspaceNote();
if (!note) {
return [];
}
let noteText: string = note.getNote();
let containerIndex = noteText.search(/data-schema-version="8">/g);
if (containerIndex === -1) {
note.setNote(
`${noteLines.join("\n")}
`
);
} else {
let noteHead = noteText.substring(0, containerIndex);
note.setNote(
`${noteHead}data-schema-version="8">${noteLines.join("\n")}`
);
}
note.saveTx();
}
getLineParentInNote(
note: ZoteroItem,
lineIndex: number = -1
): TreeModel.Node
\n${text}`;
}
if (
lineIndex < noteLines.length &&
noteLines[lineIndex].search(/ `;
}
noteLines.splice(lineIndex, 0, text);
this.setLinesToNote(note, noteLines);
await this.scrollWithRefresh(lineIndex);
}
async addLinesToNote(
note: ZoteroItem,
newLines: string[],
lineIndex: number
) {
note = note || this.getWorkspaceNote();
if (!note) {
return;
}
let noteLines = this.getLinesInNote(note);
if (lineIndex < 0) {
lineIndex =
this.getWorkspaceNote().id === note.id && this.currentLine >= 0
? this.currentLine
: noteLines.length;
} else if (lineIndex >= noteLines.length) {
lineIndex = noteLines.length;
}
this.setLinesToNote(
note,
noteLines.slice(0, lineIndex).concat(newLines, noteLines.slice(lineIndex))
);
await this.scrollWithRefresh(lineIndex);
}
addLinkToNote(
targetNote: ZoteroItem,
lineIndex: number,
linkedNoteID: number
) {
targetNote = targetNote || this.getWorkspaceNote();
if (!targetNote) {
return;
}
let linkedNote = Zotero.Items.get(linkedNoteID);
if (!linkedNote.isNote()) {
this._Addon.views.showProgressWindow("Better Notes", "Not a note item");
return;
}
const link = this.getNoteLink(linkedNote);
const linkText = linkedNote.getNoteTitle().trim();
const linkTemplate = this._Addon.template.renderTemplate(
"[QuickInsert]",
"link, subNoteItem, noteItem",
[link, linkedNote, targetNote]
);
this.addLineToNote(targetNote, linkTemplate, lineIndex, true);
const backLinkTemplate = this._Addon.template.renderTemplate(
"[QuickBackLink]",
"subNoteItem, noteItem",
[linkedNote, targetNote],
false
);
if (backLinkTemplate) {
this.addLineToNote(linkedNote, backLinkTemplate, -1, true);
}
this._Addon.views.showProgressWindow(
"Better Notes",
"Link is added to workspace"
);
}
getNoteLink(note: ZoteroItem) {
let libraryID = note.libraryID;
let library = Zotero.Libraries.get(libraryID);
let groupID: string;
if (library.libraryType === "user") {
groupID = "u";
} else if (library.libraryType === "group") {
groupID = `${library.id}`;
}
let noteKey = note.key;
return `zotero://note/${groupID}/${noteKey}/`;
}
getAnnotationLink(annotation: ZoteroItem) {
let position = JSON.parse(annotation.annotationPosition);
let openURI: string;
const attachment = annotation.parentItem;
let libraryID = attachment.libraryID;
let library = Zotero.Libraries.get(libraryID);
if (library.libraryType === "user") {
openURI = `zotero://open-pdf/library/items/${attachment.key}`;
} else if (library.libraryType === "group") {
openURI = `zotero://open-pdf/groups/${library.id}/items/${attachment.key}`;
}
openURI +=
"?page=" +
(position.pageIndex + 1) +
(annotation.key ? "&annotation=" + annotation.key : "");
return openURI;
}
async modifyLineInNote(
note: ZoteroItem,
text: string | Function,
lineIndex: number
) {
note = note || this.getWorkspaceNote();
if (!note) {
return;
}
let noteLines = this.getLinesInNote(note);
if (lineIndex < 0 || lineIndex >= noteLines.length) {
return;
}
if (typeof text === "string") {
noteLines[lineIndex] = text;
} else if (typeof text === "function") {
noteLines[lineIndex] = text(noteLines[lineIndex]);
}
this.setLinesToNote(note, noteLines);
await this.scrollWithRefresh(lineIndex);
}
async changeHeadingLineInNote(
note: ZoteroItem,
rankChange: number,
lineIndex: number
) {
note = note || this.getWorkspaceNote();
if (!note) {
return;
}
const noteLines = this.getLinesInNote(note);
if (lineIndex < 0 || lineIndex >= noteLines.length) {
return;
}
const headerStartReg = new RegExp("");
const headerStopReg = new RegExp("");
let headerStart = noteLines[lineIndex].search(headerStartReg);
if (headerStart === -1) {
return;
}
let lineRank = parseInt(noteLines[lineIndex][headerStart + 2]) + rankChange;
if (lineRank > 6) {
lineRank = 6;
} else if (lineRank < 1) {
lineRank = 1;
}
noteLines[lineIndex] = noteLines[lineIndex]
.replace(headerStartReg, ``)
.replace(headerStopReg, ``);
this.setLinesToNote(note, noteLines);
await this.scrollWithRefresh(lineIndex);
}
moveHeaderLineInNote(
note: ZoteroItem,
currentNode: TreeModel.Node".length
);
link = this.getLinkFromText(noteLines[i]);
continue;
}
Zotero.debug("convert link");
let res = await this.getNoteFromLink(link);
const subNote = res.item;
if (subNote && _rootNoteIds.indexOf(subNote.id) === -1) {
Zotero.debug(`Knowledge4Zotero: Exporting sub-note ${link}`);
const convertResult = await this.convertNoteLines(
subNote,
_rootNoteIds,
convertNoteLinks
);
const subNoteLines = convertResult.lines;
const templateText = this._Addon.template.renderTemplate(
"[QuickImport]",
"subNoteLines, subNoteItem, noteItem",
[subNoteLines, subNote, currentNote]
);
newLines.push(templateText);
subNotes.push(subNote);
subNotes = subNotes.concat(convertResult.subNotes);
}
noteLines[i] = noteLines[i].substring(
noteLines[i].search(/zotero:\/\/note\//g)
);
noteLines[i] = noteLines[i].substring(
noteLines[i].search(/<\/a>/g) + "".length
);
link = this.getLinkFromText(noteLines[i]);
}
}
}
Zotero.debug(subNotes);
return { lines: newLines, subNotes: subNotes };
}
getLinkFromText(text: string) {
// Must end with "
const linkIndex = text.search(/zotero:\/\/note\//g);
if (linkIndex === -1) {
return "";
}
let link = text.substring(linkIndex);
link = link.substring(0, link.search('"'));
return link;
}
getLinkIndexFromText(text: string): [number, number] {
// Must end with "
const linkIndex = text.search(/zotero:\/\/note\//g);
if (linkIndex === -1) {
return [-1, -1];
}
let link = text.substring(linkIndex);
return [linkIndex, linkIndex + link.search('"')];
}
getParamsFromLink(uri: string) {
uri = uri.split("//").pop();
const extraParams = {};
uri
.split("?")
.pop()
.split("&")
.forEach((p) => {
extraParams[p.split("=")[0]] = p.split("=")[1];
});
uri = uri.split("?")[0];
let params: any = {
libraryID: "",
noteKey: 0,
};
Object.assign(params, extraParams);
const router = new Zotero.Router(params);
router.add("note/:libraryID/:noteKey", function () {
if (params.libraryID === "u") {
params.libraryID = Zotero.Libraries.userLibraryID;
} else {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(
params.libraryID
);
}
});
router.run(uri);
return params;
}
async getNoteFromLink(uri: string) {
const params = this.getParamsFromLink(uri);
if (!params.libraryID) {
return {
item: false,
infoText: "Library does not exist or access denied.",
};
}
Zotero.debug(params);
let item = await Zotero.Items.getByLibraryAndKeyAsync(
params.libraryID,
params.noteKey
);
if (!item || !item.isNote()) {
return {
item: false,
infoText: "Note does not exist or is not a note.",
};
}
return {
item: item,
infoText: "OK",
};
}
parseNoteHTML(note: ZoteroItem): Element {
note = note || this.getWorkspaceNote();
if (!note) {
return undefined;
}
let noteText = note.getNote();
if (noteText.search(/data-schema-version/g) === -1) {
noteText = `${noteText}\n
`;
}
let parser = Components.classes[
"@mozilla.org/xmlextras/domparser;1"
].createInstance(Components.interfaces.nsIDOMParser);
let doc = parser.parseFromString(noteText, "text/html");
let metadataContainer: Element = doc.querySelector(
"body > div[data-schema-version]"
);
return metadataContainer;
}
parseLineText(line: string): string {
const parser = Components.classes[
"@mozilla.org/xmlextras/domparser;1"
].createInstance(Components.interfaces.nsIDOMParser);
try {
if (line.search(/data-schema-version/g) === -1) {
line = `${line}
`;
}
return parser
.parseFromString(line, "text/html")
.querySelector("body > div[data-schema-version]")
.innerText.trim();
} catch (e) {
return "";
}
}
}
export default Knowledge;