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 = ``; 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(``); 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 { if (lineIndex < 0) { lineIndex = this.currentLine; } let nodes = this.getNoteTreeAsList(note); if (!nodes.length || nodes[0].model.lineIndex > lineIndex) { // There is no parent node return undefined; } else if (nodes[nodes.length - 1].model.lineIndex <= lineIndex) { return nodes[nodes.length - 1]; } else { for (let i = 0; i < nodes.length - 1; i++) { if ( nodes[i].model.lineIndex <= lineIndex && nodes[i + 1].model.lineIndex > lineIndex ) { return nodes[i]; } } } } async scrollWithRefresh(lineIndex: number) { await Zotero.Promise.delay(500); let editorInstance = await this.getWorkspaceEditorInstance(); this._Addon.views.scrollToLine( editorInstance, // Scroll to 6 lines before the inserted line lineIndex - 1 ); this._Addon.events.onEditorEvent( new EditorMessage("enterWorkspace", { editorInstance: editorInstance, params: "main", }) ); } private async addLineToNote( note: ZoteroItem, text: string, lineIndex: number, newLine: boolean ) { 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; } Zotero.debug( `insert to ${lineIndex}, it used to be ${noteLines[lineIndex]}` ); // Force links not to appear in one line if (lineIndex > 0 && noteLines[lineIndex - 1].search(/

\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, targetNode: TreeModel.Node, as: "child" | "before" | "after" ) { note = note || this.getWorkspaceNote(); if (!note || targetNode.getPath().indexOf(currentNode) >= 0) { return undefined; } let targetIndex = 0; let targetRank = 1; let lines = this.getLinesInNote(note); if (as === "child") { targetIndex = targetNode.model.endIndex; targetRank = targetNode.model.rank === 6 ? 6 : targetNode.model.rank + 1; } else if (as === "before") { targetIndex = targetNode.model.lineIndex; targetRank = targetNode.model.rank === 7 ? targetNode.parent.model.rank === 6 ? 6 : targetNode.parent.model.rank + 1 : targetNode.model.rank; } else if (as === "after") { targetIndex = targetNode.model.endIndex; targetRank = targetNode.model.rank === 7 ? targetNode.parent.model.rank === 6 ? 6 : targetNode.parent.model.rank + 1 : targetNode.model.rank; } let rankChange = targetRank - currentNode.model.rank; Zotero.debug(`move to ${targetIndex}`); let movedLines = lines.splice( currentNode.model.lineIndex, currentNode.model.endIndex - currentNode.model.lineIndex ); let headerReg = /<\/?h[1-6]>/g; for (const i in movedLines) { movedLines[i] = movedLines[i].replace(headerReg, (e) => { let rank = parseInt(e.slice(-2, -1)); rank += rankChange; if (rank > 6) { rank = 6; } if (rank < 1) { rank = 1; } return `${e.slice(0, -2)}${rank}>`; }); } let newLines = lines .slice(0, targetIndex) .concat(movedLines, lines.slice(targetIndex)); this.setLinesToNote(note, newLines); } getNoteTree(note: ZoteroItem = undefined): TreeModel.Node { // See http://jnuno.com/tree-model-js note = note || this.getWorkspaceNote(); if (!note) { return undefined; } const noteLines = this.getLinesInNote(note); let tree = new TreeModel(); /* tree-model/index.js: line 40 TreeModel.prototype.parse = function (model) { var i, childCount, node; Annotate the line 40 of: // if (!(model instanceof Object)) { // throw new TypeError('Model must be of type object.'); // } */ let root = tree.parse({ id: -1, rank: 0, lineIndex: -1, endIndex: -1, }); let id = 0; let lastNode = root; let headerStartReg = new RegExp(""); let headerStopReg = new RegExp(""); for (let i in noteLines) { let currentRank = 7; let lineElement = noteLines[i]; const isHeading = lineElement.search(headerStartReg) !== -1; const isLink = lineElement.search(/zotero:\/\/note\//g) !== -1; if (isHeading || isLink) { let name = ""; let link = ""; if (isHeading) { const startIndex = lineElement.search(headerStartReg); currentRank = parseInt( lineElement.slice(startIndex + 2, startIndex + 3) ); } else { link = lineElement.slice(lineElement.search(/href="/g) + 6); link = link.slice(0, link.search(/"/g)); } name = this.parseLineText(lineElement); // Find parent node let parentNode = lastNode; while (parentNode.model.rank >= currentRank) { parentNode = parentNode.parent; } const currentNode = tree.parse({ id: id++, rank: currentRank, name: name, lineIndex: parseInt(i), endIndex: noteLines.length, link: link, }); parentNode.addChild(currentNode); const currentIndex = parentNode.children.indexOf(currentNode); if (currentIndex > 0) { const previousNode = parentNode.children[currentIndex - 1]; // Traverse the previous node tree and set the end index previousNode.walk((node) => { if (node.model.endIndex > parseInt(i)) { node.model.endIndex = parseInt(i); } return true; }); previousNode.model.endIndex = parseInt(i); } lastNode = currentNode; } } return root; } getNoteTreeAsList( note: ZoteroItem, filterRoot: boolean = true, filterLink: boolean = true ): TreeModel.Node[] { note = note || this.getWorkspaceNote(); if (!note) { return; } return this.getNoteTree(note).all( (node) => (!filterRoot || node.model.lineIndex >= 0) && (!filterLink || node.model.rank <= 6) ); } getNoteTreeNodeById( note: ZoteroItem, id: number, root: TreeModel.Node = undefined ) { root = root || this.getNoteTree(note); return root.first(function (node) { return node.model.id === id; }); } getNoteTreeNodesByRank( note: ZoteroItem, rank: number, root: TreeModel.Node = undefined ) { root = root || this.getNoteTree(note); return root.all(function (node) { return node.model.rank === rank; }); } async exportNoteToFile( note: ZoteroItem, convertNoteLinks: boolean = true, saveFile: boolean = true, saveNote: boolean = false, saveCopy: boolean = false, savePDF: boolean = false ) { if (!saveFile && !saveNote && !saveCopy && !savePDF) { return; } this._exportFileDict = []; note = note || this.getWorkspaceNote(); let newNote: ZoteroItem; if (convertNoteLinks || saveNote) { const noteID = await ZoteroPane_Local.newNote(); newNote = Zotero.Items.get(noteID); const rootNoteIds = [note.id]; const convertResult = await this.convertNoteLines( note, rootNoteIds, convertNoteLinks ); this.setLinesToNote(newNote, convertResult.lines); Zotero.debug(convertResult.subNotes); await Zotero.DB.executeTransaction(async () => { await Zotero.Notes.copyEmbeddedImages(note, newNote); for (const subNote of convertResult.subNotes) { await Zotero.Notes.copyEmbeddedImages(subNote, newNote); } }); } else { newNote = note; } if (saveFile) { await loadTranslator(TRANSLATOR_ID_BETTER_MARKDOWN); const filename = await pick( Zotero.getString("fileInterface.export"), "save", [["MarkDown File(*.md)", "*.md"]], `${newNote.getNoteTitle()}.md` ); if (filename) { this._exportNote = newNote; this._exportPath = Zotero.File.pathToFile(filename).parent.path + "/attachments"; // Convert to unix format this._exportPath = this._exportPath.replace(/\\/g, "/"); Components.utils.import("resource://gre/modules/osfile.jsm"); const hasImage = newNote.getNote().includes(" e._item.id === newNote.id ); await editor._initPromise; const noteEditor = _w.document.getElementById( "zotero-note-editor" ) as any; let _editor: EditorInstance = noteEditor.getCurrentInstance(); t = 0; while (!_editor && t < 500) { t += 1; await Zotero.Promise.delay(10); _editor = noteEditor.getCurrentInstance(); } await Zotero.Promise.delay(3000); await this._Addon.events.onEditorEvent( new EditorMessage("switchEditorTex", { editorInstance: _editor, params: { viewTex: true, }, }) ); await Zotero.Promise.delay(3000); _editor._iframeWindow.document .querySelectorAll(".toolbar") .forEach((e) => e.remove()); _editor._iframeWindow.document .getElementById("texView") .removeAttribute("style"); const fullPageStyle = _editor._iframeWindow.document.createElement("style"); fullPageStyle.innerHTML = "@page { margin: 0; } @media print{ body { height : auto}}"; _editor._iframeWindow.document .getElementById("texView") .before(fullPageStyle); _editor._iframeWindow.print(); // const doc = new jsPDF(); // const source = editor._iframeWindow.document.getElementById("texView"); // console.log(source); // doc.html(source, { // callback: function (doc) { // doc.save(); // }, // x: 10, // y: 10, // }); // // doc.text("123", 10, 10); // const blob = doc.output("blob"); // console.log(blob); // let filename = await pick( // Zotero.getString("fileInterface.export"), // "save", // [["PDF File(*.pdf)", "*.pdf"]], // `${newNote.getNoteTitle()}.pdf` // ); // if (filename) { // filename = Zotero.File.pathToFile(filename).path.replace(/\\/g, "/"); // const file = OS.Path.join(...filename.split(/\//)); // // const d = document.createElement("a"); // // d.href = window.URL.createObjectURL(blob); // // d.download = file; // // d.style.display = "none"; // // document.appendChild(d); // // d.click(); // // d.remove(); // // window.URL.revokeObjectURL(d.href); // await Zotero.File.putContentsAsync(file, blob); // await Zotero.File.setNormalFilePermissions(file); // } } if (!saveNote) { if (newNote.id !== note.id) { const _w: Window = ZoteroPane.findNoteWindow(newNote.id); if (_w) { _w.close(); } await Zotero.Items.erase(newNote.id); } } else { ZoteroPane.openNoteWindow(newNote.id); } } async exportNotesToFile(notes: ZoteroItem[], useEmbed: boolean) { Components.utils.import("resource://gre/modules/osfile.jsm"); this._exportFileDict = []; const filepath = await pick( Zotero.getString("fileInterface.export"), "folder" ); if (!filepath) { return; } await loadTranslator(TRANSLATOR_ID_BETTER_MARKDOWN); this._exportPath = Zotero.File.pathToFile(filepath).path + "/attachments"; // Convert to unix format this._exportPath = this._exportPath.replace(/\\/g, "/"); if (useEmbed) { for (const note of notes) { let newNote: ZoteroItem; if (this.getLinkFromText(note.getNote())) { const noteID = await ZoteroPane_Local.newNote(); newNote = Zotero.Items.get(noteID); const rootNoteIds = [note.id]; const convertResult = await this.convertNoteLines( note, rootNoteIds, true ); this.setLinesToNote(newNote, convertResult.lines); Zotero.debug(convertResult.subNotes); await Zotero.DB.executeTransaction(async () => { await Zotero.Notes.copyEmbeddedImages(note, newNote); for (const subNote of convertResult.subNotes) { await Zotero.Notes.copyEmbeddedImages(subNote, newNote); } }); } else { newNote = note; } this._exportNote = newNote; let filename = `${ Zotero.File.pathToFile(filepath).path }/${this._getFileName(note)}`; filename = filename.replace(/\\/g, "/"); this._export(newNote, filename, newNote.id !== note.id); } } else { // Export every linked note as a markdown file // Find all linked notes that need to be exported let allNoteIds: number[] = [].concat(notes.map((n) => n.id)); for (const note of notes) { const subNoteIds = ( await Promise.all( note .getNote() .match(/zotero:\/\/note\/\w+\/\w+\//g) .map(async (link) => this.getNoteFromLink(link)) ) ) .filter((res) => res.item) .map((res) => res.item.id); allNoteIds = allNoteIds.concat(subNoteIds); } allNoteIds = new Array(...new Set(allNoteIds)); // console.log(allNoteIds); const allNoteItems: ZoteroItem[] = Zotero.Items.get(allNoteIds); const noteLinkDict = allNoteItems.map((_note) => { return { link: this.getNoteLink(_note), id: _note.id, note: _note, filename: this._getFileName(_note), }; }); this._exportFileDict = noteLinkDict; for (const noteInfo of noteLinkDict) { this._exportNote = noteInfo.note; this._export( noteInfo.note, `${Zotero.File.pathToFile(filepath).path}/${noteInfo.filename}`, false ); } } } private async _export( note: ZoteroItem, filename: string, deleteAfterExport: boolean ) { const hasImage = note.getNote().includes(" { Zotero.debug(`convert note ${currentNote.id}`); let subNotes = []; const [..._rootNoteIds] = rootNoteIds; _rootNoteIds.push(currentNote.id); let newLines = []; const noteLines = this.getLinesInNote(currentNote); for (let i in noteLines) { newLines.push(noteLines[i]); // Convert Link if (convertNoteLinks) { let link = this.getLinkFromText(noteLines[i]); while (link) { const params = this.getParamsFromLink(link); if (params.ignore) { Zotero.debug("ignore link"); 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]); 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;