diff --git a/package.json b/package.json index 3555c74..0491e37 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "dependencies": { "compressing": "^1.5.1", "esbuild": "^0.14.34", - "replace-in-file": "^6.3.2" + "replace-in-file": "^6.3.2", + "tree-model": "^1.0.7" }, "devDependencies": { "release-it": "^14.14.0" diff --git a/src/events.ts b/src/events.ts index 566a617..ab10267 100644 --- a/src/events.ts +++ b/src/events.ts @@ -23,8 +23,23 @@ class AddonEvents extends AddonBase { }; } + public async addEditorEventListener( + instance: EditorInstance, + event: string, + message: EditorMessage + ) { + let editor: Element = await this._Addon.views.getEditor(instance); + editor.addEventListener(event, (e: XULEvent) => { + message.content.event = e; + message.content.editorInstance = instance; + this.onEditorEvent(message); + }); + } + public async onEditorEvent(message: EditorMessage) { - Zotero.debug(`Knowledge4Zotero: onEditorEvent\n${String(message)}`); + Zotero.debug( + `Knowledge4Zotero: onEditorEvent\n${message.type}\n${message.content}` + ); if (message.type === "addNoteInstance") { let mainKnowledgeID = parseInt( Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") @@ -51,6 +66,11 @@ class AddonEvents extends AddonBase { "Add Note Link to Knowledge Workspace", new EditorMessage("addToKnowledge", {}) ); + this.addEditorEventListener( + message.content.editorInstance, + "click", + new EditorMessage("noteEditorClick", {}) + ); } else if (message.type === "addToKnowledge") { /* message.content = { @@ -105,6 +125,24 @@ class AddonEvents extends AddonBase { } } } + } else if (message.type === "noteEditorClick") { + let el: XUL.Element = message.content.event.target; + if (el.children.length !== 0) { + return; + } + let urlIndex = el.innerHTML.search(/zotero:\/\/note\//g); + if (urlIndex >= 0) { + let noteID = parseInt( + el.innerHTML.substring(urlIndex + "zotero://note/".length) + ); + let note = Zotero.Items.get(noteID); + if (note && note.isNote()) { + // TODO: Open note + Zotero.debug(`Knowledge4Zotero: noteEditorClick ${note.id}`); + } + } + } else { + Zotero.debug(`Knowledge4Zotero: message not handled.`); } } diff --git a/src/index.ts b/src/index.ts index 35ea863..b614b16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ import Knowledge4Zotero from "./addon"; +import { Knowledge } from "./knowledge"; Zotero.Knowledge4Zotero = new Knowledge4Zotero(); +Zotero.Knowledge = Knowledge + window.addEventListener( "load", async function (e) { diff --git a/src/knowledge.ts b/src/knowledge.ts index e69de29..b8f4c50 100644 --- a/src/knowledge.ts +++ b/src/knowledge.ts @@ -0,0 +1,68 @@ +const TreeModel = require("./treemodel"); + +class Knowledge { + _note: ZoteroItem; + constructor(noteItem: ZoteroItem) { + this._note = noteItem; + // this.createKnowledgeItem(); + } + // createKnowledgeItem() { + // return; + // } + + addNoteLink(noteItem: ZoteroItem) {} + + getOutline(): Node { + let parser = Components.classes[ + "@mozilla.org/xmlextras/domparser;1" + ].createInstance(Components.interfaces.nsIDOMParser); + let doc = parser.parseFromString(this._note.getNote(), "text/html"); + + let metadataContainer: Element = doc.querySelector( + "body > div[data-schema-version]" + ); + + 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({ + rank: 0, + lineIndex: -1, + }); + let currentNode = root; + for (let i = 0; i < metadataContainer.children.length; i++) { + let currentRank = 7; + let lineElement = metadataContainer.children[i]; + if (lineElement.tagName[0] === "H" && lineElement.tagName.length === 2) { + let _rank = parseInt(lineElement.tagName[1]); + if (_rank >= 1 && _rank <= 6) { + currentRank = _rank; + } + while (currentNode.model.rank >= currentRank) { + currentNode = currentNode.parent; + } + currentNode.addChild( + tree.parse({ + rank: currentRank, + lineIndex: i, + }) + ); + } + } + return root; + } + + jumpToNote(noteLinke: string) {} + + export() {} +} + +export { Knowledge }; diff --git a/src/treemodel.js b/src/treemodel.js new file mode 100644 index 0000000..140e5bf --- /dev/null +++ b/src/treemodel.js @@ -0,0 +1,291 @@ +var mergeSort, findInsertIndex; +mergeSort = require('mergesort'); +findInsertIndex = require('find-insert-index'); + +module.exports = (function () { + 'use strict'; + + var walkStrategies; + + walkStrategies = {}; + + function k(result) { + return function () { + return result; + }; + } + + function TreeModel(config) { + config = config || {}; + this.config = config; + this.config.childrenPropertyName = config.childrenPropertyName || 'children'; + this.config.modelComparatorFn = config.modelComparatorFn; + } + + function addChildToNode(node, child) { + child.parent = node; + node.children.push(child); + return child; + } + + function Node(config, model) { + this.config = config; + this.model = model; + this.children = []; + } + + TreeModel.prototype.parse = function (model) { + var i, childCount, node; + + // if (!(model instanceof Object)) { + // throw new TypeError('Model must be of type object.'); + // } + + node = new Node(this.config, model); + if (model[this.config.childrenPropertyName] instanceof Array) { + if (this.config.modelComparatorFn) { + model[this.config.childrenPropertyName] = mergeSort( + this.config.modelComparatorFn, + model[this.config.childrenPropertyName]); + } + for (i = 0, childCount = model[this.config.childrenPropertyName].length; i < childCount; i++) { + addChildToNode(node, this.parse(model[this.config.childrenPropertyName][i])); + } + } + return node; + }; + + function hasComparatorFunction(node) { + return typeof node.config.modelComparatorFn === 'function'; + } + + Node.prototype.isRoot = function () { + return this.parent === undefined; + }; + + Node.prototype.hasChildren = function () { + return this.children.length > 0; + }; + + function addChild(self, child, insertIndex) { + var index; + + if (!(child instanceof Node)) { + throw new TypeError('Child must be of type Node.'); + } + + child.parent = self; + if (!(self.model[self.config.childrenPropertyName] instanceof Array)) { + self.model[self.config.childrenPropertyName] = []; + } + + if (hasComparatorFunction(self)) { + // Find the index to insert the child + index = findInsertIndex( + self.config.modelComparatorFn, + self.model[self.config.childrenPropertyName], + child.model); + + // Add to the model children + self.model[self.config.childrenPropertyName].splice(index, 0, child.model); + + // Add to the node children + self.children.splice(index, 0, child); + } else { + if (insertIndex === undefined) { + self.model[self.config.childrenPropertyName].push(child.model); + self.children.push(child); + } else { + if (insertIndex < 0 || insertIndex > self.children.length) { + throw new Error('Invalid index.'); + } + self.model[self.config.childrenPropertyName].splice(insertIndex, 0, child.model); + self.children.splice(insertIndex, 0, child); + } + } + return child; + } + + Node.prototype.addChild = function (child) { + return addChild(this, child); + }; + + Node.prototype.addChildAtIndex = function (child, index) { + if (hasComparatorFunction(this)) { + throw new Error('Cannot add child at index when using a comparator function.'); + } + + return addChild(this, child, index); + }; + + Node.prototype.setIndex = function (index) { + if (hasComparatorFunction(this)) { + throw new Error('Cannot set node index when using a comparator function.'); + } + + if (this.isRoot()) { + if (index === 0) { + return this; + } + throw new Error('Invalid index.'); + } + + if (index < 0 || index >= this.parent.children.length) { + throw new Error('Invalid index.'); + } + + var oldIndex = this.parent.children.indexOf(this); + + this.parent.children.splice(index, 0, this.parent.children.splice(oldIndex, 1)[0]); + + this.parent.model[this.parent.config.childrenPropertyName] + .splice(index, 0, this.parent.model[this.parent.config.childrenPropertyName].splice(oldIndex, 1)[0]); + + return this; + }; + + Node.prototype.getPath = function () { + var path = []; + (function addToPath(node) { + path.unshift(node); + if (!node.isRoot()) { + addToPath(node.parent); + } + })(this); + return path; + }; + + Node.prototype.getIndex = function () { + if (this.isRoot()) { + return 0; + } + return this.parent.children.indexOf(this); + }; + + /** + * Parse the arguments of traversal functions. These functions can take one optional + * first argument which is an options object. If present, this object will be stored + * in args.options. The only mandatory argument is the callback function which can + * appear in the first or second position (if an options object is given). This + * function will be saved to args.fn. The last optional argument is the context on + * which the callback function will be called. It will be available in args.ctx. + * + * @returns Parsed arguments. + */ + function parseArgs() { + var args = {}; + if (arguments.length === 1) { + if (typeof arguments[0] === 'function') { + args.fn = arguments[0]; + } else { + args.options = arguments[0]; + } + } else if (arguments.length === 2) { + if (typeof arguments[0] === 'function') { + args.fn = arguments[0]; + args.ctx = arguments[1]; + } else { + args.options = arguments[0]; + args.fn = arguments[1]; + } + } else { + args.options = arguments[0]; + args.fn = arguments[1]; + args.ctx = arguments[2]; + } + args.options = args.options || {}; + if (!args.options.strategy) { + args.options.strategy = 'pre'; + } + if (!walkStrategies[args.options.strategy]) { + throw new Error('Unknown tree walk strategy. Valid strategies are \'pre\' [default], \'post\' and \'breadth\'.'); + } + return args; + } + + Node.prototype.walk = function () { + var args; + args = parseArgs.apply(this, arguments); + walkStrategies[args.options.strategy].call(this, args.fn, args.ctx); + }; + + walkStrategies.pre = function depthFirstPreOrder(callback, context) { + var i, childCount, keepGoing; + keepGoing = callback.call(context, this); + for (i = 0, childCount = this.children.length; i < childCount; i++) { + if (keepGoing === false) { + return false; + } + keepGoing = depthFirstPreOrder.call(this.children[i], callback, context); + } + return keepGoing; + }; + + walkStrategies.post = function depthFirstPostOrder(callback, context) { + var i, childCount, keepGoing; + for (i = 0, childCount = this.children.length; i < childCount; i++) { + keepGoing = depthFirstPostOrder.call(this.children[i], callback, context); + if (keepGoing === false) { + return false; + } + } + keepGoing = callback.call(context, this); + return keepGoing; + }; + + walkStrategies.breadth = function breadthFirst(callback, context) { + var queue = [this]; + (function processQueue() { + var i, childCount, node; + if (queue.length === 0) { + return; + } + node = queue.shift(); + for (i = 0, childCount = node.children.length; i < childCount; i++) { + queue.push(node.children[i]); + } + if (callback.call(context, node) !== false) { + processQueue(); + } + })(); + }; + + Node.prototype.all = function () { + var args, all = []; + args = parseArgs.apply(this, arguments); + args.fn = args.fn || k(true); + walkStrategies[args.options.strategy].call(this, function (node) { + if (args.fn.call(args.ctx, node)) { + all.push(node); + } + }, args.ctx); + return all; + }; + + Node.prototype.first = function () { + var args, first; + args = parseArgs.apply(this, arguments); + args.fn = args.fn || k(true); + walkStrategies[args.options.strategy].call(this, function (node) { + if (args.fn.call(args.ctx, node)) { + first = node; + return false; + } + }, args.ctx); + return first; + }; + + Node.prototype.drop = function () { + var indexOfChild; + if (!this.isRoot()) { + indexOfChild = this.parent.children.indexOf(this); + this.parent.children.splice(indexOfChild, 1); + this.parent.model[this.config.childrenPropertyName].splice(indexOfChild, 1); + this.parent = undefined; + delete this.parent; + } + return this; + }; + + return TreeModel; +})(); diff --git a/src/views.ts b/src/views.ts index b939f38..6802e24 100644 --- a/src/views.ts +++ b/src/views.ts @@ -18,6 +18,15 @@ class AddonViews extends AddonBase { }; } + async getEditor(instance: EditorInstance) { + await instance._initPromise; + let editor = + instance._iframeWindow.document.getElementsByClassName( + "primary-editor" + )[0]; + return editor; + } + async addEditorButton( editorInstances: EditorInstance, id: string, @@ -48,6 +57,15 @@ class AddonViews extends AddonBase { button.setAttribute("title", title); } + async scrollToLine(instance: EditorInstance, lineIndex: number) { + let editor = await this.getEditor(instance); + if (lineIndex > editor.children.length) { + lineIndex = editor.children.length - 1; + } + // @ts-ignore + editor.parentNode.scrollTo(0, editor.children[lineIndex].offsetTop); + } + showProgressWindow( header: string, context: string, diff --git a/typing/global.d.ts b/typing/global.d.ts index dfecf7d..0cd60be 100644 --- a/typing/global.d.ts +++ b/typing/global.d.ts @@ -83,6 +83,7 @@ declare interface ZoteroItem { id: number; isRegularItem: () => boolean; isNote: () => boolean; + getNote: () => string; isAttachment: () => boolean; isAnnotation?: () => boolean; itemTypeID: number; @@ -126,6 +127,9 @@ declare const Zotero: { get: (key: string) => any; set: (key: string, value: any) => any; }; + Items: { + get: (key: string | number) => ZoteroItem; + }; Reader: Reader; Notes: Notes; Knowledge4Zotero: import("../src/addon"); @@ -161,7 +165,7 @@ declare class ReaderObj { declare class EditorInstance { _iframeWindow: XULWindow; _item: ZoteroItem; - _initPromise: Promise + _initPromise: Promise; } declare class Notes {