add: two-path syncing (md<->note)

This commit is contained in:
xiangyu 2022-12-04 15:54:47 +08:00
parent 7b2768bcfb
commit 62b84a5e70
25 changed files with 2770 additions and 2192 deletions

View File

@ -0,0 +1,415 @@
<!DOCTYPE html>
<html lang="en">
<body>
<script src="chrome://__addonRef__/content/lib/js/jquery.min.js"></script>
<script src="chrome://__addonRef__/content/lib/js/dx.all.js"></script>
<link
rel="stylesheet"
type="text/css"
href="chrome://__addonRef__/content/lib/css/dx.light.css"
/>
<link rel="stylesheet" href="chrome://__addonRef__/skin/workspace.css" />
<link
rel="stylesheet"
type="text/css"
href="resource://zotero/note-editor/editor.css"
/>
<style>
html,
body,
.viewport {
padding: 0;
margin: 0;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
word-wrap: break-word;
background-color: #f0f0f0;
}
.viewport {
margin: 0 5px 0 5px;
}
.top-container {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
font-family: initial;
}
.header-container {
padding: 5px;
margin: 0;
height: 105px;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #f0f0f0;
}
.title-container {
padding: 5px;
margin: 0;
height: 35px;
width: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
background-image: linear-gradient(to top, #f8f8f8, #f9f9f9);
border: solid 0.5px #aaaaaa;
}
.title-text {
text-align: center;
}
.viewport-container {
padding: 0;
margin: 0;
height: calc(100% - 200px);
width: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
background-color: #f0f0f0;
}
.footer-container {
padding: 5px;
margin: 0;
height: 60px;
width: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
background-color: #f0f0f0;
}
.added {
background-color: rgb(230, 255, 236);
color: green;
}
.removed {
background-color: rgb(255, 235, 233);
color: red;
}
.added-list-item {
background-color: rgb(230, 255, 236);
}
.removed-list-item {
background-color: rgb(255, 235, 233);
}
.normal {
color: darkgray;
}
.selected-data,
.options {
margin-top: 20px;
padding: 20px;
background-color: rgba(191, 191, 191, 0.15);
}
.selected-data .caption {
font-weight: bold;
font-size: 115%;
}
.options .caption {
font-size: 18px;
font-weight: 500;
}
.option {
margin-top: 10px;
}
.option > span {
width: 120px;
display: inline-block;
}
.option > .dx-widget {
display: inline-block;
vertical-align: middle;
width: 100%;
max-width: 350px;
}
.dx-checkbox-checked .dx-checkbox-icon::before {
content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20t%3D%221670065400920%22%20class%3D%22icon%22%20viewBox%3D%220%200%201024%201024%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20p-id%3D%221452%22%20width%3D%2216%22%20height%3D%2216%22%3E%3Cpath%20d%3D%22M441.6%20812.8c-19.2%200-32-6.4-44.8-19.2L70.4%20467.2c-25.6-25.6-25.6-64%200-89.6%2025.6-25.6%2064-25.6%2089.6%200l281.6%20281.6%20441.6-428.8c25.6-25.6%2064-25.6%2089.6%200%2025.6%2025.6%2025.6%2064%200%2089.6l-486.4%20473.6C473.6%20806.4%20460.8%20812.8%20441.6%20812.8z%22%20p-id%3D%221453%22%20fill%3D%22%23337ab7%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E") !important;
}
img {
width: auto !important;
}
a {
color: blue;
text-decoration: underline;
}
.tool-button {
width: 75px;
max-width: 75px;
min-width: 75px;
padding: 5px;
text-align: center;
margin: 0 10px 0 10px;
}
</style>
<div class="top-container">
<div class="header-container">
<h2 style="margin: 0">Diff-Merger</h2>
<div style="display: flex">
<div style="width: -moz-fit-content; text-align: center">
<div>
<span>[Note]&nbsp;</span>
</div>
<div>
<span>[MarkDown]&nbsp;</span>
</div>
</div>
<div style="width: 100%">
<div>
<span id="note-name"></span>
</div>
<div>
<span id="md-name"></span>
</div>
</div>
<div style="width: -moz-available; text-align: right">
<div>
<span>Last Modified: </span>
<span id="note-modify"></span>
</div>
<div>
<span>Last Modified: </span>
<span id="md-modify"></span>
</div>
<div>
<span>Last Synced: </span>
<span id="sync-time"></span>
</div>
</div>
</div>
</div>
<div class="title-container">
<div class="title-text" style="width: 20%">Conflicted Changes</div>
<div class="title-text" style="width: 30%">Raw Note</div>
<div class="title-text" style="width: 50%">Rendered Note</div>
</div>
<div class="viewport-container">
<div
class="dx-viewport viewport"
id="outline-container"
style="background-color: #ffffff; width: calc(20% - 10px)"
>
<div class="demo-container">
<div id="list-demo">
<div class="widget-container">
<div id="simpleList"></div>
</div>
</div>
</div>
</div>
<div
class="diff-viewport viewport"
style="
background-color: #ffffff;
width: calc(30% - 10px);
padding: 10px 30px 10px 30px;
"
></div>
<div
class="render-viewport viewport primary-editor ProseMirror"
style="
background-color: #ffffff;
width: calc(50% - 10px);
padding: 10px 30px 10px 30px;
"
></div>
</div>
<div
class="footer-container"
style="justify-content: flex-start; padding: 10px"
>
<button
class="tool-button"
id="finish"
title="Confirm selected changes and merge. Will update MD and note."
>
Finish
</button>
<button
class="tool-button"
id="unsync"
title="Remove this note from syncing list."
>
Unsync
</button>
<button class="tool-button" id="skip" title="Skip merging this time.">
Skip
</button>
</div>
</div>
<script>
window.syncInfo = {};
window.diffData = [];
window.imageData = [];
window.io = {};
let listWidget;
let scrollRatio = 0;
function initSyncInfo() {
console.log(window.syncInfo);
document.querySelector("#note-name").innerText =
window.syncInfo.noteName;
document.querySelector("#note-modify").innerText =
window.syncInfo.noteModify;
document.querySelector("#md-name").innerText = window.syncInfo.mdName;
document.querySelector("#md-modify").innerText =
window.syncInfo.mdModify;
document.querySelector("#sync-time").innerText =
window.syncInfo.syncTime;
}
function initList() {
$(() => {
listWidget = $("#simpleList")
.dxList({
dataSource: new DevExpress.data.DataSource({
store: new DevExpress.data.ArrayStore({
key: "id",
data: window.diffData.filter(
(diff) => diff.added || diff.removed
),
}),
}),
showSelectionControls: true,
selectionMode: "all",
onItemRendered(e) {
console.log(e, e.itemElement);
if (e.itemData) {
e.itemElement.addClass(
e.itemData.added
? "added-list-item"
: e.itemData.removed
? "removed-list-item"
: ""
);
}
},
onSelectionChanged(e) {
const addedItems = e.addedItems;
const removedItems = e.removedItems;
updateDiffRender();
let target;
if (addedItems.length === 1) {
target = addedItems[0].element;
} else if (removedItems.length === 1) {
target = removedItems[0].element;
}
if (target) {
const diffViewer = document.querySelector(".diff-viewport");
diffViewer.scrollTo(
0,
target.offsetTop - diffViewer.offsetTop
);
}
},
})
.dxList("instance");
});
}
function getAcceptedChangeIdx() {
return listWidget.option("selectedItemKeys") || [];
}
function initDiffViewer() {
$(".diff-viewport").empty();
const frag = document.createDocumentFragment();
window.diffData.forEach((diff) => {
const span = document.createElement("span");
span.className = diff.added
? "added"
: diff.removed
? "removed"
: "normal";
span.innerText = diff.value;
frag.append(span);
diff.element = span;
});
$(".diff-viewport").append(frag);
}
function updateDiffRender(ids = undefined) {
console.log("update render");
ids = ids || getAcceptedChangeIdx();
const result = window.diffData
.filter((diff) => {
return (
(diff.added && ids.includes(diff.id)) ||
(diff.removed && !ids.includes(diff.id)) ||
(!diff.added && !diff.removed)
);
})
.map((diff) => diff.value)
.join("");
document.querySelector(".render-viewport").innerHTML = result;
document.querySelectorAll("img[data-attachment-key]").forEach((e) => {
e.src = window.imageData[e.getAttribute("data-attachment-key")];
});
window.io.result = result;
}
// https://juejin.cn/post/6844904020281147405
const syncScroller = function () {
let nodes = Array.prototype.filter.call(
arguments,
(item) => item instanceof HTMLElement
);
let max = nodes.length;
if (!max || max === 1) return;
let sign = 0;
nodes.forEach((ele, index) => {
ele.addEventListener("scroll", function () {
if (!sign) {
sign = max - 1;
let top =
this.scrollTop / (this.scrollHeight - this.clientHeight);
let left =
this.scrollLeft / (this.scrollWidth - this.clientWidth);
for (node of nodes) {
if (node == this) continue;
node.scrollTo(
left * (node.scrollWidth - node.clientWidth),
top * (node.scrollHeight - node.clientHeight)
);
}
} else --sign;
});
});
};
window.addEventListener("DOMContentLoaded", (e) => {
const diffViewer = document.querySelector(".diff-viewport");
const renderViewer = document.querySelector(".render-viewport");
syncScroller(diffViewer, renderViewer);
document.querySelector("#finish").addEventListener("click", (e) => {
window.io.type = "finish";
window.close();
});
document.querySelector("#unsync").addEventListener("click", (e) => {
if (confirm("This note will not be synced any more. Continue?")) {
window.io.type = "unsync";
window.close();
}
});
document.querySelector("#skip").addEventListener("click", (e) => {
window.io.type = "skip";
window.close();
});
});
</script>
</body>
</html>

View File

@ -22,12 +22,14 @@
<keyset>
<key id="key_close" key="W" modifiers="accel" command="cmd_close" />
<key id="key_selectall" key="A" modifiers="accel" command="cmd_selectall" />
<key id="key_selectall" keyCode="27" command="cmd_unselectall" />
</keyset>
<command id="cmd_close" oncommand="window.close();" />
<command id="cmd_selectall" oncommand="window.document.getElementById('sync-list').selectAll();" />
<command id="cmd_unselectall" oncommand="window.document.getElementById('sync-list').clearSelection();" />
<vbox flex="1">
<listbox id="sync-list" flex="1" seltype="multiple">
<listbox id="sync-list" flex="1" seltype="multiple" onselect="Zotero.Knowledge4Zotero.SyncListWindow.onSelect();">
<listhead>
<listheader id="icon" label="#" flex="1" />
<listheader id="notename" label="&zotero.__addonRef__.syncList.notename.label;" flex="1" />
@ -44,7 +46,6 @@
<button id="dosync" label="&zotero.__addonRef__.syncList.dosync.label;" oncommand="Zotero.Knowledge4Zotero.SyncListWindow.doSync();"></button>
<button id="changesync" label="&zotero.__addonRef__.syncList.changesync.label;" oncommand="Zotero.Knowledge4Zotero.SyncListWindow.changeSync();"></button>
<button id="removesync" label="&zotero.__addonRef__.syncList.removesync.label;" oncommand="Zotero.Knowledge4Zotero.SyncListWindow.removeSync();"></button>
<checkbox id="related" label="&zotero.__addonRef__.syncList.related.label;"></checkbox>
</hbox>
</vbox>

View File

@ -69,7 +69,7 @@
<!ENTITY zotero.__addonRef__.syncList.changesyncperiod.label "Sync Period:">
<!ENTITY zotero.__addonRef__.syncList.dosync.label "Sync">
<!ENTITY zotero.__addonRef__.syncList.changesync.label "Change Folder">
<!ENTITY zotero.__addonRef__.syncList.removesync.label "Remove This">
<!ENTITY zotero.__addonRef__.syncList.removesync.label "Remove">
<!ENTITY zotero.__addonRef__.syncList.related.label "Apply to Related Notes">
<!ENTITY zotero.__addonRef__.wizard.title "Welcomed to Zotero Better Notes">

View File

@ -35,6 +35,8 @@
background: #fbfbfb;
border: solid #c9c9c9 1px;
text-decoration: none;
user-select: none;
-moz-user-select: none;
}
.tool-button:hover {

View File

@ -1,6 +1,6 @@
pref("extensions.zotero.Knowledge4Zotero.recentMainNoteIds", "");
pref("extensions.zotero.Knowledge4Zotero.syncNoteIds", "");
pref("extensions.zotero.Knowledge4Zotero.syncPeriod", 10000);
pref("extensions.zotero.Knowledge4Zotero.syncPeriod", 30000);
pref("extensions.zotero.Knowledge4Zotero.autoAnnotation", false);
pref("extensions.zotero.Knowledge4Zotero.exportMD", true);
pref("extensions.zotero.Knowledge4Zotero.exportSubMD", false);

View File

@ -26,25 +26,40 @@
},
"homepage": "https://github.com/windingwind/zotero-better-notes#readme",
"dependencies": {
"@syncfusion/ej2-base": "^20.1.50",
"@syncfusion/ej2-navigations": "^20.1.51",
"asciidoctor": "^2.2.6",
"compressing": "^1.5.1",
"crypto-js": "^4.1.1",
"esbuild": "^0.14.34",
"diff": "^5.1.0",
"hast-util-to-html": "^8.0.3",
"hast-util-to-mdast": "^8.4.1",
"hast-util-to-text": "^3.1.1",
"hastscript": "^7.1.0",
"html-docx-js-typescript": "^0.1.5",
"prosemirror-model": "^1.18.3",
"prosemirror-transform": "^1.7.0",
"rehype-format": "^4.0.1",
"rehype-parse": "^8.0.4",
"rehype-remark": "^9.1.2",
"rehype-stringify": "^9.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"remark-stringify": "^10.0.2",
"replace-in-file": "^6.3.2",
"seedrandom": "^3.0.5",
"tree-model": "^1.0.7",
"turndown": "^7.1.1",
"turndown-plugin-gfm": "^1.0.2"
"unified": "^10.1.2",
"unist-util-visit": "^4.1.1",
"unist-util-visit-parents": "^5.1.1",
"yamljs": "^0.3.0"
},
"devDependencies": {
"@types/diff": "^5.0.2",
"@types/jquery": "^3.5.14",
"@types/node": "^17.0.31",
"@types/turndown": "^5.0.1",
"esbuild": "^0.14.34",
"compressing": "^1.5.1",
"release-it": "^14.14.0",
"zotero-types": "^0.0.4"
"zotero-types": "^0.0.8"
}
}

View File

@ -17,10 +17,13 @@ import NoteUtils from "./note/noteUtils";
import NoteParse from "./note/noteParse";
import NoteExportWindow from "./note/noteExportWindow";
import NoteExport from "./note/noteExportController";
import NoteImport from "./note/noteImportController";
import SyncDiffWindow from "./sync/syncDiffWindow";
import EditorViews from "./editor/editorViews";
import EditorController from "./editor/editorController";
import EditorImageViewer from "./editor/imageViewerWindow";
import TemplateWindow from "./template/templateWindow";
import { SyncUtils } from "./sync/syncUtils";
class Knowledge4Zotero {
public ZoteroEvents: ZoteroEvents;
@ -35,6 +38,7 @@ class Knowledge4Zotero {
// First-run wizard
public WizardWindow: WizardWindow;
// Sync tools
public SyncUtils: SyncUtils;
public SyncInfoWindow: SyncInfoWindow;
public SyncListWindow: SyncListWindow;
public SyncController: SyncController;
@ -46,6 +50,8 @@ class Knowledge4Zotero {
// Note tools
public NoteUtils: NoteUtils;
public NoteExport: NoteExport;
public NoteImport: NoteImport;
public SyncDiffWindow: SyncDiffWindow;
public NoteExportWindow: NoteExportWindow;
public NoteParse: NoteParse;
public EditorViews: EditorViews;
@ -63,13 +69,16 @@ class Knowledge4Zotero {
this.EditorController = new EditorController(this);
this.EditorImageViewer = new EditorImageViewer(this);
this.WizardWindow = new WizardWindow(this);
this.SyncUtils = new SyncUtils(this);
this.SyncInfoWindow = new SyncInfoWindow(this);
this.SyncListWindow = new SyncListWindow(this);
this.SyncController = new SyncController(this);
this.SyncDiffWindow = new SyncDiffWindow(this);
this.TemplateWindow = new TemplateWindow(this);
this.TemplateController = new TemplateController(this);
this.NoteUtils = new NoteUtils(this);
this.NoteExport = new NoteExport(this);
this.NoteImport = new NoteImport(this);
this.NoteExportWindow = new NoteExportWindow(this);
this.NoteParse = new NoteParse(this);
this.knowledge = new TemplateAPI(this);

View File

@ -10,7 +10,7 @@ class EditorController extends AddonBase {
instance: Zotero.EditorInstance;
time: number;
}>;
editorPromise: ZoteroPromise;
editorPromise: _ZoteroPromise;
activeEditor: Zotero.EditorInstance;
constructor(parent: Knowledge4Zotero) {

View File

@ -341,6 +341,13 @@ class EditorViews extends AddonBase {
refreshButton.classList.add("option");
refreshButton.innerText = "Refresh Editor";
refreshButton.addEventListener("click", (e) => {
if (
!confirm(
"Refresh before content is saved may cause note data loss. Only do refresh if tables are uneditable.\nAre you sure to continue?"
)
) {
return;
}
instance.init({
item: instance._item,
viewMode: instance._viewMode,
@ -407,11 +414,21 @@ class EditorViews extends AddonBase {
} Copied`
);
});
const importButton = _window.document.createElement("button");
importButton.classList.add("option");
importButton.innerText = "Import from MarkDown";
importButton.addEventListener("click", async (e) => {
await this._Addon.NoteImport.doImport(noteItem, {
ignoreVersion: true,
append: true,
});
});
dropdownPopup.append(
previewButton,
refreshButton,
copyLinkButton,
copyLinkAtLineButton
copyLinkAtLineButton,
importButton
);
}
}
@ -611,7 +628,7 @@ class EditorViews extends AddonBase {
);
newLines.push(templateText);
const newLineString = newLines.join("\n");
const notifyFlag: ZoteroPromise = Zotero.Promise.defer();
const notifyFlag: _ZoteroPromise = Zotero.Promise.defer();
const notifierName = "insertLinkWait";
this._Addon.ZoteroEvents.addNotifyListener(
notifierName,

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,8 @@ class NoteExport extends AddonBase {
note: Zotero.Item;
filename: string;
}>;
_pdfPrintPromise: ZoteroPromise;
_docxPromise: ZoteroPromise;
_pdfPrintPromise: _ZoteroPromise;
_docxPromise: _ZoteroPromise;
_docxBlob: Blob;
constructor(parent: Knowledge4Zotero) {
@ -182,28 +182,37 @@ class NoteExport extends AddonBase {
async exportNotesToMDFiles(
notes: Zotero.Item[],
useEmbed: boolean,
useSync: boolean = false
options: {
useEmbed?: boolean;
useSync?: boolean;
filedir?: string;
} = {}
) {
Components.utils.import("resource://gre/modules/osfile.jsm");
this._exportFileInfo = [];
const filepath = await pick(
Zotero.getString(useSync ? "sync.sync" : "fileInterface.export") +
" MarkDown",
"folder"
);
let filedir =
options.filedir ||
(await pick(
Zotero.getString(
options.useSync ? "sync.sync" : "fileInterface.export"
) + " MarkDown",
"folder"
));
if (!filepath) {
filedir = Zotero.File.normalizeToUnix(filedir);
if (!filedir) {
Zotero.debug("BN:export, filepath invalid");
return;
}
this._exportPath = this._Addon.NoteUtils.formatPath(
Zotero.File.pathToFile(filepath).path + "/attachments"
OS.Path.join(filedir, "attachments")
);
notes = notes.filter((n) => n && n.getNote);
if (useEmbed) {
if (options.useEmbed) {
for (const note of notes) {
let newNote: Zotero.Item;
if (this._Addon.NoteParse.parseLinkInText(note.getNote())) {
@ -233,9 +242,7 @@ class NoteExport extends AddonBase {
newNote = note;
}
let filename = `${
Zotero.File.pathToFile(filepath).path
}/${await this._getFileName(note)}`;
let filename = OS.Path.join(filedir, await this._getFileName(note));
filename = filename.replace(/\\/g, "/");
await this._exportMD(newNote, filename, newNote.id !== note.id);
@ -243,6 +250,7 @@ class NoteExport extends AddonBase {
} else {
// Export every linked note as a markdown file
// Find all linked notes that need to be exported
const inputIds = notes.map((n) => n.id);
let allNoteIds: number[] = notes.map((n) => n.id);
for (const note of notes) {
const linkMatches = note
@ -272,77 +280,38 @@ class NoteExport extends AddonBase {
link: this._Addon.NoteUtils.getNoteLink(_note),
id: _note.id,
note: _note,
filename: await this._getFileName(_note),
filename: await this._getFileName(_note, filedir),
});
}
this._exportFileInfo = noteLinkDict;
for (const noteInfo of noteLinkDict) {
let exportPath = `${Zotero.File.pathToFile(filepath).path}/${
noteInfo.filename
}`;
await this._exportMD(noteInfo.note, exportPath, false);
if (useSync) {
this._Addon.SyncController.updateNoteSyncStatus(
noteInfo.note,
Zotero.File.pathToFile(filepath).path,
noteInfo.filename
);
let exportPath = OS.Path.join(filedir, noteInfo.filename);
if (
options.useSync &&
!inputIds.includes(noteInfo.id) &&
(await OS.File.exists(exportPath))
) {
// Avoid overwrite existing notes that are waiting to be synced.
continue;
}
const content = await this._exportMD(noteInfo.note, exportPath, false);
if (options.useSync) {
this._Addon.SyncController.updateNoteSyncStatus(noteInfo.note, {
path: filedir,
filename: noteInfo.filename,
md5: Zotero.Utilities.Internal.md5(
this._Addon.SyncUtils.getMDStatusFromContent(content).content,
false
),
lastsync: new Date().getTime(),
itemID: noteInfo.id,
});
}
}
}
}
async syncNotesToMDFiles(notes: Zotero.Item[], filepath: string) {
this._exportPath = this._Addon.NoteUtils.formatPath(
Zotero.File.pathToFile(filepath).path + "/attachments"
);
// Export every linked note as a markdown file
// Find all linked notes that need to be exported
let allNoteIds: number[] = notes.map((n) => n.id);
for (const note of notes) {
const linkMatches = note.getNote().match(/zotero:\/\/note\/\w+\/\w+\//g);
if (!linkMatches) {
continue;
}
const subNoteIds = (
await Promise.all(
linkMatches.map(async (link) =>
this._Addon.NoteUtils.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: Zotero.Item[] = Zotero.Items.get(
allNoteIds
) as Zotero.Item[];
const noteLinkDict = [];
for (const _note of allNoteItems) {
noteLinkDict.push({
link: this._Addon.NoteUtils.getNoteLink(_note),
id: _note.id,
note: _note,
filename: await this._getFileName(_note),
});
}
this._exportFileInfo = noteLinkDict;
for (const note of notes) {
const syncInfo = this._Addon.SyncController.getNoteSyncStatus(note);
let exportPath = `${decodeURIComponent(
syncInfo.path
)}/${decodeURIComponent(syncInfo.filename)}`;
await this._exportMD(note, exportPath, false);
this._Addon.SyncController.updateNoteSyncStatus(note);
}
}
private async _exportDocx(filename: string) {
await Zotero.File.putContentsAsync(filename, this._docxBlob);
this._Addon.ZoteroViews.showProgressWindow(
@ -362,7 +331,9 @@ class NoteExport extends AddonBase {
}
filename = this._Addon.NoteUtils.formatPath(filename);
const content: string = await this._Addon.NoteParse.parseNoteToMD(note);
const content: string = await this._Addon.NoteParse.parseNoteToMD(note, {
withMeta: true,
});
console.log(
`Exporting MD file: ${filename}, content length: ${content.length}`
);
@ -378,6 +349,7 @@ class NoteExport extends AddonBase {
}
await Zotero.Items.erase(note.id);
}
return content;
}
private async _exportFreeMind(noteItem: Zotero.Item, filename: string) {
@ -392,7 +364,35 @@ class NoteExport extends AddonBase {
);
}
private async _getFileName(noteItem: Zotero.Item) {
private async _getFileName(
noteItem: Zotero.Item,
filedir: string = undefined
) {
if (filedir !== undefined && (await OS.File.exists(filedir))) {
const mdRegex = /\.(md|MD|Md|mD)$/;
let matchedFileName = null;
let matchedDate = new Date(0);
await Zotero.File.iterateDirectory(
filedir,
async (entry: OS.File.Entry) => {
if (entry.isDir) return;
if (mdRegex.test(entry.name)) {
if (
entry.name.split(".").shift().split("-").pop() === noteItem.key
) {
const stat = await OS.File.stat(entry.path);
if (stat.lastModificationDate > matchedDate) {
matchedFileName = entry.name;
matchedDate = stat.lastModificationDate;
}
}
}
}
);
if (matchedFileName) {
return matchedFileName;
}
}
return (
(await this._Addon.TemplateController.renderTemplateAsync(
"[ExportMDFileName]",

View File

@ -0,0 +1,99 @@
/*
* This file realizes md import.
*/
import Knowledge4Zotero from "../addon";
import AddonBase from "../module";
import { pick } from "../utils";
class NoteImport extends AddonBase {
constructor(parent: Knowledge4Zotero) {
super(parent);
}
async doImport(
noteItem: Zotero.Item = undefined,
options: {
ignoreVersion?: boolean;
append?: boolean;
} = {}
) {
const filepath = await pick(
`${Zotero.getString("fileInterface.import")} MarkDown Document`,
"open",
[["MarkDown File(*.md)", "*.md"]]
);
if (filepath) {
await this.importMDFileToNote(filepath, noteItem, options);
}
}
async importMDFileToNote(
file: string,
noteItem: Zotero.Item = undefined,
options: {
ignoreVersion?: boolean;
append?: boolean;
} = {}
) {
let mdStatus: MDStatus;
try {
mdStatus = await this._Addon.SyncUtils.getMDStatus(file);
} catch (e) {
Zotero.debug(`BN Import: ${String(e)}`);
}
if (!options.ignoreVersion && mdStatus.meta?.version < noteItem?._version) {
if (
!confirm(
`The target note seems to be newer than the file ${file}. Are you sure you want to import it anyway?`
)
) {
return;
}
}
const noteStatus = noteItem
? this._Addon.SyncUtils.getNoteStatus(noteItem)
: {
meta: '<div data-schema-version="9">',
content: "",
tail: "</div>",
};
if (!noteItem) {
noteItem = new Zotero.Item("note");
noteItem.libraryID = ZoteroPane.getSelectedLibraryID();
if (ZoteroPane.getCollectionTreeRow().isCollection()) {
noteItem.addToCollection(ZoteroPane.getCollectionTreeRow().ref.id);
}
await noteItem.saveTx({
notifierData: {
autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
},
});
}
const parsedContent = await this._Addon.NoteParse.parseMDToNote(
mdStatus,
noteItem,
true
);
console.log("bn import", noteStatus);
if (options.append) {
await this._Addon.NoteUtils.addLineToNote(
noteItem,
parsedContent,
Number.MAX_VALUE
);
} else {
noteItem.setNote(noteStatus.meta + parsedContent + noteStatus.tail);
await noteItem.saveTx({
notifierData: {
autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
},
});
}
return noteItem;
}
}
export default NoteImport;

View File

@ -2,40 +2,18 @@
* This file realizes note parse (md, html, rich-text).
*/
import AddonBase from "../module";
import { HTML2Markdown, Markdown2HTML } from "./convertMD";
import TurndownService = require("turndown");
const turndownPluginGfm = require("turndown-plugin-gfm");
import TreeModel = require("tree-model");
const asciidoctor = require("asciidoctor")();
const seedrandom = require("seedrandom");
import YAML = require("yamljs");
import AddonBase from "../module";
import Knowledge4Zotero from "../addon";
import { getDOMParser } from "../utils";
import { NodeMode } from "../sync/syncUtils";
class NoteParse extends AddonBase {
private getDOMParser(): DOMParser {
if (Zotero.platformMajorVersion > 60) {
return new DOMParser();
} else {
return Components.classes[
"@mozilla.org/xmlextras/domparser;1"
].createInstance(Components.interfaces.nsIDOMParser);
}
}
// A seedable version of Zotero.Utilities.randomString
private randomString(len: number, chars: string, seed: string) {
if (!chars) {
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
}
if (!len) {
len = 8;
}
let randomstring = "";
const random: Function = seedrandom(seed);
for (let i = 0; i < len; i++) {
const rnum = Math.floor(random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
tools: any;
constructor(parent: Knowledge4Zotero) {
super(parent);
}
public parseNoteTree(
@ -107,7 +85,7 @@ class NoteParse extends AddonBase {
return root;
}
public parseHTMLLines(html: string): string[] {
let containerIndex = html.search(/data-schema-version="8">/g);
let containerIndex = html.search(/data-schema-version="[0-9]*">/g);
if (containerIndex != -1) {
html = html.substring(
containerIndex + 'data-schema-version="8">'.length,
@ -256,7 +234,9 @@ class NoteParse extends AddonBase {
if (!annotationItem || !annotationItem.isAnnotation()) {
return null;
}
let json = await Zotero.Annotations.toJSON(annotationItem);
let json: AnnotationJson = await Zotero.Annotations.toJSON(
annotationItem
);
json.id = annotationItem.key;
json.attachmentItemID = annotationItem.parentItem.id;
delete json.key;
@ -271,15 +251,180 @@ class NoteParse extends AddonBase {
}
}
async parseAnnotationHTML(
note: Zotero.Item,
annotations: Zotero.Item[],
ignoreComment: boolean = false
// Zotero.EditorInstanceUtilities.serializeAnnotations
serializeAnnotations(
annotations: AnnotationJson[],
skipEmbeddingItemData: boolean = false,
skipCitation: boolean = false
) {
if (!note) {
return;
let storedCitationItems = [];
let html = "";
for (let annotation of annotations) {
let attachmentItem = Zotero.Items.get(annotation.attachmentItemID);
if (!attachmentItem) {
continue;
}
if (
(!annotation.text &&
!annotation.comment &&
!annotation.imageAttachmentKey) ||
annotation.type === "ink"
) {
continue;
}
let citationHTML = "";
let imageHTML = "";
let highlightHTML = "";
let quotedHighlightHTML = "";
let commentHTML = "";
let storedAnnotation: any = {
attachmentURI: Zotero.URI.getItemURI(attachmentItem),
annotationKey: annotation.id,
color: annotation.color,
pageLabel: annotation.pageLabel,
position: annotation.position,
};
// Citation
let parentItem = skipCitation
? undefined
: attachmentItem.parentID && Zotero.Items.get(attachmentItem.parentID);
if (parentItem) {
let uris = [Zotero.URI.getItemURI(parentItem)];
let citationItem: any = {
uris,
locator: annotation.pageLabel,
};
// Note: integration.js` uses `Zotero.Cite.System.prototype.retrieveItem`,
// which produces a little bit different CSL JSON
let itemData = Zotero.Utilities.Item.itemToCSLJSON(parentItem);
if (!skipEmbeddingItemData) {
citationItem.itemData = itemData;
}
let item = storedCitationItems.find((item) =>
item.uris.some((uri) => uris.includes(uri))
);
if (!item) {
storedCitationItems.push({ uris, itemData });
}
storedAnnotation.citationItem = citationItem;
let citation = {
citationItems: [citationItem],
properties: {},
};
let citationWithData = JSON.parse(JSON.stringify(citation));
citationWithData.citationItems[0].itemData = itemData;
let formatted =
Zotero.EditorInstanceUtilities.formatCitation(citationWithData);
citationHTML = `<span class="citation" data-citation="${encodeURIComponent(
JSON.stringify(citation)
)}">${formatted}</span>`;
}
// Image
if (annotation.imageAttachmentKey) {
// // let imageAttachmentKey = await this._importImage(annotation.image);
// delete annotation.image;
// Normalize image dimensions to 1.25 of the print size
let rect = annotation.position.rects[0];
let rectWidth = rect[2] - rect[0];
let rectHeight = rect[3] - rect[1];
// Constants from pdf.js
const CSS_UNITS = 96.0 / 72.0;
const PDFJS_DEFAULT_SCALE = 1.25;
let width = Math.round(rectWidth * CSS_UNITS * PDFJS_DEFAULT_SCALE);
let height = Math.round((rectHeight * width) / rectWidth);
imageHTML = `<img data-attachment-key="${
annotation.imageAttachmentKey
}" width="${width}" height="${height}" data-annotation="${encodeURIComponent(
JSON.stringify(storedAnnotation)
)}"/>`;
}
// Text
if (annotation.text) {
let text = Zotero.EditorInstanceUtilities._transformTextToHTML.call(
Zotero.EditorInstanceUtilities,
annotation.text.trim()
);
highlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(
JSON.stringify(storedAnnotation)
)}">${text}</span>`;
quotedHighlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(
JSON.stringify(storedAnnotation)
)}">${Zotero.getString(
"punctuation.openingQMark"
)}${text}${Zotero.getString("punctuation.closingQMark")}</span>`;
}
// Note
if (annotation.comment) {
commentHTML = Zotero.EditorInstanceUtilities._transformTextToHTML.call(
Zotero.EditorInstanceUtilities,
annotation.comment.trim()
);
}
let template;
if (annotation.type === "highlight") {
template = Zotero.Prefs.get("annotations.noteTemplates.highlight");
} else if (annotation.type === "note") {
template = Zotero.Prefs.get("annotations.noteTemplates.note");
} else if (annotation.type === "image") {
template = "<p>{{image}}<br/>{{citation}} {{comment}}</p>";
}
Zotero.debug("Using note template:");
Zotero.debug(template);
template = template.replace(
/(<blockquote>[^<>]*?)({{highlight}})([\s\S]*?<\/blockquote>)/g,
(match, p1, p2, p3) => p1 + "{{highlight quotes='false'}}" + p3
);
let vars = {
color: annotation.color || "",
// Include quotation marks by default, but allow to disable with `quotes='false'`
highlight: (attrs) =>
attrs.quotes === "false" ? highlightHTML : quotedHighlightHTML,
comment: commentHTML,
citation: citationHTML,
image: imageHTML,
tags: (attrs) =>
(
(annotation.tags && annotation.tags.map((tag) => tag.name)) ||
[]
).join(attrs.join || " "),
};
let templateHTML = Zotero.Utilities.Internal.generateHTMLFromTemplate(
template,
vars
);
// Remove some spaces at the end of paragraph
templateHTML = templateHTML.replace(/([\s]*)(<\/p)/g, "$2");
// Remove multiple spaces
templateHTML = templateHTML.replace(/\s\s+/g, " ");
html += templateHTML;
}
let annotationJSONList = [];
return { html, citationItems: storedCitationItems };
}
async parseAnnotationHTML(
note: Zotero.Item, // If you are sure there are no image annotations, note is not required.
annotations: Zotero.Item[],
ignoreComment: boolean = false,
skipCitation: boolean = false
) {
let annotationJSONList: AnnotationJson[] = [];
for (const annot of annotations) {
const annotJson = await this._Addon.NoteParse.parseAnnotation(annot);
if (ignoreComment && annotJson.comment) {
@ -288,10 +433,45 @@ class NoteParse extends AddonBase {
annotationJSONList.push(annotJson);
}
await this._Addon.NoteUtils.importImagesToNote(note, annotationJSONList);
const html =
Zotero.EditorInstanceUtilities.serializeAnnotations(
annotationJSONList
).html;
const html = this.serializeAnnotations(
annotationJSONList,
false,
skipCitation
).html;
return html;
}
async parseCitationHTML(citationIds: number[]) {
let html = "";
let items = await Zotero.Items.getAsync(citationIds);
for (let item of items) {
if (
item.isNote() &&
!(await Zotero.Notes.ensureEmbeddedImagesAreAvailable(item)) &&
!Zotero.Notes.promptToIgnoreMissingImage()
) {
return null;
}
}
for (let item of items) {
if (item.isRegularItem()) {
let itemData = Zotero.Utilities.Item.itemToCSLJSON(item);
let citation = {
citationItems: [
{
uris: [Zotero.URI.getItemURI(item)],
itemData,
},
],
properties: {},
};
let formatted = Zotero.EditorInstanceUtilities.formatCitation(citation);
html += `<p><span class="citation" data-citation="${encodeURIComponent(
JSON.stringify(citation)
)}">${formatted}</span></p>`;
}
}
return html;
}
@ -306,7 +486,7 @@ class NoteParse extends AddonBase {
.join("\n")}</div>`;
console.log(this.parseHTMLLines(item.getNote()).slice(0, lineCount));
let parser = this.getDOMParser();
let parser = getDOMParser();
let doc = parser.parseFromString(note, "text/html");
// Make sure this is the new note
@ -419,7 +599,7 @@ class NoteParse extends AddonBase {
if (noteText.search(/data-schema-version/g) === -1) {
noteText = `<div data-schema-version="8">${noteText}\n</div>`;
}
let parser = this.getDOMParser();
let parser = getDOMParser();
let doc = parser.parseFromString(noteText, "text/html");
let metadataContainer: HTMLElement = doc.querySelector(
@ -429,7 +609,7 @@ class NoteParse extends AddonBase {
}
parseLineText(line: string): string {
const parser = this.getDOMParser();
const parser = getDOMParser();
try {
if (line.search(/data-schema-version/g) === -1) {
line = `<div data-schema-version="8">${line}</div>`;
@ -444,12 +624,12 @@ class NoteParse extends AddonBase {
}
}
parseMDToHTML(str: string): string {
return Markdown2HTML(str.replace(/\u00A0/gu, " "));
async parseMDToHTML(str: string): Promise<string> {
return await this._Addon.SyncUtils.md2note(str.replace(/\u00A0/gu, " "));
}
parseHTMLToMD(str: string): string {
return HTML2Markdown(str);
async parseHTMLToMD(str: string): Promise<string> {
return await this._Addon.SyncUtils.note2md(str);
}
parseAsciiDocToHTML(str: string): string {
@ -537,280 +717,172 @@ class NoteParse extends AddonBase {
return mmXML;
}
// A realization of Markdown Note.js translator
async parseNoteToMD(
noteItem: Zotero.Item,
options: { wrapCitation?: boolean } = {}
options: {
withMeta?: boolean;
skipSavingImages?: boolean;
backend?: "turndown" | "unified";
} = {}
) {
const parser = this.getDOMParser();
const doc = parser.parseFromString(noteItem.getNote() || "", "text/html");
Components.utils.import("resource://gre/modules/osfile.jsm");
doc.querySelectorAll("span").forEach(function (span) {
if (span.style.textDecoration === "line-through") {
let s = doc.createElement("s");
s.append(...span.childNodes);
span.replaceWith(s);
}
});
const noteStatus = this._Addon.SyncUtils.getNoteStatus(noteItem);
const rehype = this._Addon.SyncUtils.note2rehype(noteStatus.content);
console.log(rehype);
this._Addon.SyncUtils.processN2MRehypeHighlightNodes(
this._Addon.SyncUtils.getN2MRehypeHighlightNodes(rehype),
NodeMode.direct
);
this._Addon.SyncUtils.processN2MRehypeCitationNodes(
this._Addon.SyncUtils.getN2MRehypeCitationNodes(rehype),
NodeMode.direct
);
this._Addon.SyncUtils.processN2MRehypeNoteLinkNodes(
this._Addon.SyncUtils.getN2MRehypeNoteLinkNodes(rehype),
this._Addon.NoteExport._exportFileInfo,
NodeMode.direct
);
await this._Addon.SyncUtils.processN2MRehypeImageNodes(
this._Addon.SyncUtils.getN2MRehypeImageNodes(rehype),
noteItem.libraryID,
this._Addon.NoteExport._exportPath,
options.skipSavingImages,
true,
NodeMode.direct
);
console.log("rehype", rehype);
const remark = await this._Addon.SyncUtils.rehype2remark(rehype);
console.log("remark", remark);
let md = this._Addon.SyncUtils.remark2md(remark);
// Turndown wants pre content inside additional code block
doc.querySelectorAll("pre").forEach(function (pre) {
let code = doc.createElement("code");
code.append(...pre.childNodes);
pre.append(code);
});
// Insert a PDF link for highlight and image annotation nodes
doc
.querySelectorAll('span[class="highlight"], img[data-annotation]')
.forEach((node) => {
Zotero.debug(node.outerHTML);
try {
var annotation = JSON.parse(
decodeURIComponent(node.getAttribute("data-annotation"))
);
} catch (e) {
Zotero.debug(e);
}
if (annotation) {
// annotation.uri was used before note-editor v4
let uri = annotation.attachmentURI || annotation.uri;
let position = annotation.position;
Zotero.debug("----Debug Link----");
Zotero.debug(annotation);
if (typeof uri === "string" && typeof position === "object") {
Zotero.debug(uri);
let openURI;
let uriParts = uri.split("/");
let libraryType = uriParts[3];
let key = uriParts[uriParts.length - 1];
Zotero.debug(key);
if (libraryType === "users") {
openURI = "zotero://open-pdf/library/items/" + key;
}
// groups
else {
let groupID = uriParts[4];
openURI = "zotero://open-pdf/groups/" + groupID + "/items/" + key;
}
openURI +=
"?page=" +
(position.pageIndex + 1) +
(annotation.annotationKey
? "&annotation=" + annotation.annotationKey
: "");
let a = doc.createElement("a");
a.href = openURI;
a.append("pdf");
let fragment = doc.createDocumentFragment();
fragment.append(" (", a, ") ");
if (options.wrapCitation) {
const citationKey = annotation.annotationKey
? annotation.annotationKey
: this.randomString(
8,
Zotero.Utilities.allowedKeyChars,
Zotero.Utilities.Internal.md5(
node.getAttribute("data-annotation")
)
);
Zotero.Utilities.Internal.md5(
node.getAttribute("data-annotation")
);
const beforeCitationDecorator = doc.createElement("span");
beforeCitationDecorator.innerHTML = `&lt;!-- bn::${citationKey} --&gt;`;
const afterCitationDecorator = doc.createElement("span");
afterCitationDecorator.innerHTML = `&lt;!-- bn::${citationKey} --&gt;`;
node.before(beforeCitationDecorator);
fragment.append(afterCitationDecorator);
}
let nextNode = node.nextElementSibling;
if (nextNode && nextNode.classList.contains("citation")) {
nextNode.parentNode.insertBefore(fragment, nextNode.nextSibling);
} else {
node.parentNode.insertBefore(fragment, node.nextSibling);
}
}
}
});
console.log(doc);
for (const img of doc.querySelectorAll("img[data-attachment-key]")) {
let imgKey = img.getAttribute("data-attachment-key");
const attachmentItem = await Zotero.Items.getByLibraryAndKeyAsync(
noteItem.libraryID,
imgKey
);
Zotero.debug(attachmentItem);
let oldFile = String(await attachmentItem.getFilePathAsync());
Zotero.debug(oldFile);
let ext = oldFile.split(".").pop();
let newAbsPath = this._Addon.NoteUtils.formatPath(
`${this._Addon.NoteExport._exportPath}/${imgKey}.${ext}`
);
Zotero.debug(newAbsPath);
let newFile = oldFile;
try {
// Don't overwrite
if (await OS.File.exists(newAbsPath)) {
newFile = newAbsPath.replace(/\\/g, "/");
} else {
newFile = Zotero.File.copyToUnique(oldFile, newAbsPath).path;
newFile = newFile.replace(/\\/g, "/");
}
newFile = `attachments/${newFile.split(/\//).pop()}`;
} catch (e) {
Zotero.debug(e);
}
Zotero.debug(newFile);
img.setAttribute("src", newFile ? newFile : oldFile);
img.setAttribute("alt", "image");
}
// Transform citations to links
doc.querySelectorAll('span[class="citation"]').forEach(function (span) {
try {
var citation = JSON.parse(
decodeURIComponent(span.getAttribute("data-citation"))
);
} catch (e) {}
if (citation && citation.citationItems && citation.citationItems.length) {
let uris = [];
for (let citationItem of citation.citationItems) {
let uri = citationItem.uris[0];
if (typeof uri === "string") {
let uriParts = uri.split("/");
let libraryType = uriParts[3];
let key = uriParts[uriParts.length - 1];
Zotero.debug(key);
if (libraryType === "users") {
uris.push("zotero://select/library/items/" + key);
}
// groups
else {
let groupID = uriParts[4];
uris.push("zotero://select/groups/" + groupID + "/items/" + key);
}
}
}
let items = Array.from(span.querySelectorAll(".citation-item")).map(
(x) => x.textContent
);
// Fallback to pre v5 note-editor schema that was serializing citations as plain text i.e.:
// <span class="citation" data-citation="...">(Jang et al., 2005, p. 14; Kongsgaard et al., 2009, p. 790)</span>
if (!items.length) {
items = span.textContent.slice(1, -1).split("; ");
}
span.innerHTML =
"(" +
items
.map((item, i) => {
return `<a href="${uris[i]}">${item}</a>`;
})
.join("; ") +
")";
}
});
// Overwrite escapes
const escapes: [RegExp, string][] = [
// [/\\/g, '\\\\'],
// [/\*/g, '\\*'],
// [/^-/g, "\\-"],
[/^\+ /g, "\\+ "],
[/^(=+)/g, "\\$1"],
[/^(#{1,6}) /g, "\\$1 "],
[/`/g, "\\`"],
[/^~~~/g, "\\~~~"],
// [/^>/g, "\\>"],
// [/_/g, "\\_"],
[/^(\d+)\. /g, "$1\\. "],
];
if (Zotero.Prefs.get("Knowledge4Zotero.convertSquare")) {
escapes.push([/\[/g, "\\["]);
escapes.push([/\]/g, "\\]"]);
}
TurndownService.prototype.escape = function (string) {
return escapes.reduce(function (accumulator, escape) {
return accumulator.replace(escape[0], escape[1]);
}, string);
};
// Initialize Turndown Service
let turndownService = new TurndownService({
headingStyle: "atx",
bulletListMarker: "-",
emDelimiter: "*",
codeBlockStyle: "fenced",
});
turndownService.use(turndownPluginGfm.gfm);
// Add math block rule
turndownService.addRule("mathBlock", {
filter: function (node) {
return node.nodeName === "PRE" && node.className === "math";
},
replacement: function (content, node, options) {
return (
"\n\n$$\n" + node.firstChild.textContent.slice(2, -2) + "\n$$\n\n"
);
},
});
turndownService.addRule("inlineLinkCustom", {
filter: function (node, options) {
return (
options.linkStyle === "inlined" &&
node.nodeName === "A" &&
node.getAttribute("href").length > 0
);
},
replacement: (content, node: HTMLElement, options) => {
var href = node.getAttribute("href");
const cleanAttribute = (attribute) =>
attribute ? attribute.replace(/(\n+\s*)+/g, "\n") : "";
var title = cleanAttribute(node.getAttribute("title"));
if (title) title = ' "' + title + '"';
if (href.search(/zotero:\/\/note\/\w+\/\w+\//g) !== -1) {
// A note link should be converted if it is in the _exportFileDict
const noteInfo = this._Addon.NoteExport._exportFileInfo.find((i) =>
href.includes(i.link)
);
if (noteInfo) {
href = `./${noteInfo.filename}`;
}
}
return "[" + content + "](" + href + title + ")";
},
});
if (Zotero.Prefs.get("Knowledge4Zotero.exportHighlight")) {
turndownService.addRule("backgroundColor", {
filter: function (node, options) {
return node.nodeName === "SPAN" && node.style["background-color"];
if (options.withMeta) {
let yamlFrontMatter = `---\n${YAML.stringify(
{
version: noteItem._version,
// "data-citation-items": JSON.parse(
// decodeURIComponent(
// doc
// .querySelector("div[data-citation-items]")
// .getAttribute("data-citation-items")
// )
// ),
},
replacement: function (content, node) {
return `<span style="background-color: ${
(node as HTMLElement).style["background-color"]
}">${content}</span>`;
},
});
10
)}\n---`;
md = `${yamlFrontMatter}\n${md}`;
}
console.log(md);
return md;
}
const parsedMD = turndownService.turndown(doc.body);
console.log(parsedMD);
return parsedMD;
async parseMDToNote(
mdStatus: MDStatus,
noteItem: Zotero.Item,
isImport: boolean = false
) {
// let editorInstance =
// this._Addon.WorkspaceWindow.getEditorInstance(noteItem);
// if (!editorInstance) {
// ZoteroPane.openNoteWindow(noteItem.id);
// editorInstance = this._Addon.WorkspaceWindow.getEditorInstance(noteItem);
// let t = 0;
// // Wait for editor instance
// while (t < 10 && !editorInstance) {
// await Zotero.Promise.delay(500);
// t += 1;
// editorInstance =
// this._Addon.WorkspaceWindow.getEditorInstance(noteItem);
// }
// }
// if (!editorInstance) {
// Zotero.debug("BN:Import: failed to open note.");
// return;
// }
console.log("md", mdStatus);
const remark = this._Addon.SyncUtils.md2remark(mdStatus.content);
console.log("remark", remark);
const _rehype = await this._Addon.SyncUtils.remark2rehype(remark);
console.log("_rehype", _rehype);
const _note = this._Addon.SyncUtils.rehype2note(_rehype);
console.log("_note", _note);
const rehype = this._Addon.SyncUtils.note2rehype(_note);
console.log("rehype", rehype);
// Import highlight to note meta
// Annotations don't need to be processed.
// Image annotations are imported with normal images.
// const annotationNodes = getM2NRehypeAnnotationNodes(mdRehype);
// for (const node of annotationNodes) {
// try {
// // {
// // "attachmentURI": "http://zotero.org/users/uid/items/itemkey",
// // "annotationKey": "4FLVQRDG",
// // "color": "#5fb236",
// // "pageLabel": "2503",
// // "position": {
// // "pageIndex": 0,
// // "rects": [
// // [
// // 101.716,
// // 298.162,
// // 135.469,
// // 307.069
// // ]
// // ]
// // },
// // "citationItem": {
// // "uris": [
// // "http://zotero.org/users/uid/items/itemkey"
// // ],
// // "locator": "2503"
// // }
// // }
// const dataAnnotation = JSON.parse(
// decodeURIComponent(node.properties.dataAnnotation)
// );
// const id = dataAnnotation.citationItems.map((c) =>
// Zotero.URI.getURIItemID(dataAnnotation.attachmentURI)
// );
// const html = await this.parseAnnotationHTML(noteItem, []);
// const newNode = note2rehype(html);
// // root -> p -> span(cite, this is what we actually want)
// replace(node, (newNode.children[0] as any).children[0]);
// } catch (e) {
// Zotero.debug(e);
// console.log(e);
// continue;
// }
// }
// Check if image already belongs to note
this._Addon.SyncUtils.processM2NRehypeHighlightNodes(
this._Addon.SyncUtils.getM2NRehypeHighlightNodes(rehype)
);
await this._Addon.SyncUtils.processM2NRehypeCitationNodes(
this._Addon.SyncUtils.getM2NRehypeCitationNodes(rehype),
isImport
);
this._Addon.SyncUtils.processM2NRehypeNoteLinkNodes(
this._Addon.SyncUtils.getM2NRehypeNoteLinkNodes(rehype)
);
await this._Addon.SyncUtils.processM2NRehypeImageNodes(
this._Addon.SyncUtils.getM2NRehypeImageNodes(rehype),
noteItem,
mdStatus.filedir,
isImport
);
console.log(rehype);
const noteContent = this._Addon.SyncUtils.rehype2note(rehype);
return noteContent;
}
async parseNoteForDiff(noteItem: Zotero.Item) {
const noteStatus = this._Addon.SyncUtils.getNoteStatus(noteItem);
const rehype = this._Addon.SyncUtils.note2rehype(noteStatus.content);
await this._Addon.SyncUtils.processM2NRehypeCitationNodes(
this._Addon.SyncUtils.getM2NRehypeCitationNodes(rehype),
true
);
// Prse content like ciations
return this._Addon.SyncUtils.rehype2note(rehype);
}
}

View File

@ -10,7 +10,7 @@ class NoteUtils extends AddonBase {
public currentLine: any;
constructor(parent: Knowledge4Zotero) {
super(parent);
this.currentLine = [];
this.currentLine = {};
}
public getLinesInNote(note: Zotero.Item): string[] {
@ -26,7 +26,7 @@ class NoteUtils extends AddonBase {
return [];
}
let noteText: string = note.getNote();
let containerIndex = noteText.search(/data-schema-version="8">/g);
let containerIndex = noteText.search(/data-schema-version="[0-9]*/g);
if (containerIndex === -1) {
note.setNote(
`<div data-schema-version="8">${noteLines.join("\n")}</div>`
@ -78,9 +78,28 @@ class NoteUtils extends AddonBase {
while (temp.firstChild) {
frag.appendChild(temp.firstChild);
}
const defer = Zotero.Promise.defer();
const notifyName = `addLineToNote-${note.id}`;
this._Addon.ZoteroEvents.addNotifyListener(
notifyName,
(
event: string,
type: string,
ids: Array<number | string>,
extraData: object
) => {
if (event === "modify" && type === "item" && ids.includes(note.id)) {
this._Addon.ZoteroEvents.removeNotifyListener(notifyName);
defer.resolve();
}
}
);
position === "after"
? currentElement.after(frag)
: currentElement.before(frag);
await defer.promise;
this._Addon.EditorViews.scrollToPosition(
editorInstance,
currentElement.offsetTop
@ -121,19 +140,52 @@ class NoteUtils extends AddonBase {
return null;
}
private async _importImage(note: Zotero.Item, src, download = false) {
let blob;
public async getAttachmentKeyFromFileName(
libraryID: number,
path: string
): Promise<false | _ZoteroItem> {
return await Zotero.Items.getByLibraryAndKeyAsync(
libraryID,
Zotero.File.normalizeToUnix(path).split("/").pop().split(".").shift()
);
}
public async _importImage(
note: Zotero.Item,
src: string,
type: "b64" | "url" | "file" = "b64"
): Promise<string | void> {
if (!note || !note.isNote()) {
return "";
}
let blob: Blob;
if (src.startsWith("data:")) {
blob = this._dataURLtoBlob(src);
} else if (download) {
} else if (type === "url") {
let res;
try {
res = await Zotero.HTTP.request("GET", src, { responseType: "blob" });
} catch (e) {
return;
}
blob = res.response;
} else if (type === "file") {
src = Zotero.File.normalizeToUnix(src);
const noteAttachmentKeys = Zotero.Items.get(note.getAttachments()).map(
(_i) => _i.key
);
const filename = src.split("/").pop().split(".").shift();
// The exported image is KEY.png by default.
// If it is already an attachment, just keep it.
if (noteAttachmentKeys.includes(filename)) {
return filename;
}
const imageData = await Zotero.File.getBinaryContentsAsync(src);
const array = new Uint8Array(imageData.length);
for (let i = 0; i < imageData.length; i++) {
array[i] = imageData.charCodeAt(i);
}
blob = new Blob([array], { type: "image/png" });
} else {
return;
}
@ -150,10 +202,8 @@ class NoteUtils extends AddonBase {
public async importImagesToNote(note: Zotero.Item, annotations: any) {
for (let annotation of annotations) {
if (annotation.image) {
annotation.imageAttachmentKey = await this._importImage(
note,
annotation.image
);
annotation.imageAttachmentKey =
(await this._importImage(note, annotation.image)) || "";
}
delete annotation.image;
}
@ -320,7 +370,24 @@ class NoteUtils extends AddonBase {
while (temp.firstChild) {
frag.appendChild(temp.firstChild);
}
const defer = Zotero.Promise.defer();
const notifyName = `modifyLineInNote-${note.id}`;
this._Addon.ZoteroEvents.addNotifyListener(
notifyName,
(
event: string,
type: string,
ids: Array<number | string>,
extraData: object
) => {
if (event === "modify" && type === "item" && ids.includes(note.id)) {
this._Addon.ZoteroEvents.removeNotifyListener(notifyName);
defer.resolve();
}
}
);
currentElement.replaceWith(frag);
await defer.promise;
this._Addon.EditorViews.scrollToPosition(
editorInstance,
currentElement.offsetTop
@ -713,7 +780,7 @@ class NoteUtils extends AddonBase {
// `Current Element: ${focusNode.outerHTML}; Real Element: ${realElement.outerHTML}`
// );
this.currentLine[editor._item.id] = currentLineIndex;
console.log(realElement);
// console.log(realElement);
if (realElement.tagName === "A") {
let link = (realElement as HTMLLinkElement).href;
let linkedNote = (await this.getNoteFromLink(link)).item;

View File

@ -4,12 +4,14 @@
import Knowledge4Zotero from "../addon";
import AddonBase from "../module";
import { SyncCode } from "../utils";
class SyncController extends AddonBase {
triggerTime: number;
sycnLock: boolean;
constructor(parent: Knowledge4Zotero) {
super(parent);
this.sycnLock = false;
}
getSyncNoteIds(): number[] {
@ -65,103 +67,208 @@ class SyncController extends AddonBase {
"Knowledge4Zotero.syncNoteIds",
ids.filter((id) => id !== noteItem.id).join(",")
);
const sycnTag = noteItem.getTags().find((t) => t.tag.includes("sync://"));
if (sycnTag) {
noteItem.removeTag(sycnTag.tag);
}
await noteItem.saveTx();
Zotero.Prefs.clear(`Knowledge4Zotero.syncDetail-${noteItem.id}`);
}
getNoteSyncStatus(noteItem: Zotero.Item): any {
const sycnInfo = noteItem.getTags().find((t) => t.tag.includes("sync://"));
if (!sycnInfo) {
return false;
async doCompare(noteItem: Zotero.Item): Promise<SyncCode> {
const syncStatus = this._Addon.SyncUtils.getSyncStatus(noteItem);
const MDStatus = await this._Addon.SyncUtils.getMDStatus(noteItem);
// No file found
if (!MDStatus.meta) {
return SyncCode.NoteAhead;
}
// File meta is unavailable
if (MDStatus.meta.version < 0) {
return SyncCode.NeedDiff;
}
let MDAhead = false;
let noteAhead = false;
const md5 = Zotero.Utilities.Internal.md5(MDStatus.content, false);
// MD5 doesn't match (md side change)
if (md5 !== syncStatus.md5) {
MDAhead = true;
}
// Note version doesn't match (note side change)
if (Number(MDStatus.meta.version) !== noteItem._version) {
noteAhead = true;
}
if (noteAhead && MDAhead) {
return SyncCode.NeedDiff;
} else if (noteAhead) {
return SyncCode.NoteAhead;
} else if (MDAhead) {
return SyncCode.MDAhead;
} else {
return SyncCode.UpToDate;
}
const params = {};
sycnInfo.tag
.split("?")
.pop()
.split("&")
.forEach((p) => {
params[p.split("=")[0]] = p.split("=")[1];
});
return params;
}
async updateNoteSyncStatus(
noteItem: Zotero.Item,
path: string = "",
filename: string = ""
) {
async updateNoteSyncStatus(noteItem: Zotero.Item, status: SyncStatus) {
this.addSyncNote(noteItem);
const syncInfo = this.getNoteSyncStatus(noteItem);
const sycnTag = noteItem.getTags().find((t) => t.tag.includes("sync://"));
if (sycnTag) {
noteItem.removeTag(sycnTag.tag);
}
noteItem.addTag(
`sync://note/?version=${noteItem._version + 1}&path=${
path ? encodeURIComponent(path) : syncInfo["path"]
}&filename=${
filename ? encodeURIComponent(filename) : syncInfo["filename"]
}&lastsync=${new Date().getTime()}`,
undefined
Zotero.Prefs.set(
`Knowledge4Zotero.syncDetail-${noteItem.id}`,
JSON.stringify(status)
);
await noteItem.saveTx();
}
setSync() {
const _t = new Date().getTime();
this.triggerTime = _t;
const syncPeriod = Number(Zotero.Prefs.get("Knowledge4Zotero.syncPeriod"));
const syncPeriod = Zotero.Prefs.get(
"Knowledge4Zotero.syncPeriod"
) as number;
if (syncPeriod > 0) {
setTimeout(() => {
if (this.triggerTime === _t) {
setInterval(() => {
// Only when Zotero is active and focused
if (document.hasFocus()) {
this.doSync();
}
}, syncPeriod);
}
}
async doSync(
items: Zotero.Item[] = null,
force: boolean = false,
useIO: boolean = true
) {
Zotero.debug("Better Notes: sync start");
items = items || (Zotero.Items.get(this.getSyncNoteIds()) as Zotero.Item[]);
const toExport = {};
const forceNoteIds = force
? await this.getRelatedNoteIdsFromNotes(
useIO ? [this._Addon.SyncInfoWindow.io.dataIn] : items
)
: [];
for (const item of items) {
const syncInfo = this.getNoteSyncStatus(item);
const filepath = decodeURIComponent(syncInfo.path);
const filename = decodeURIComponent(syncInfo.filename);
if (
Number(syncInfo.version) < item._version ||
!(await OS.File.exists(`${filepath}/${filename}`)) ||
forceNoteIds.includes(item.id)
) {
if (Object.keys(toExport).includes(filepath)) {
toExport[filepath].push(item);
} else {
toExport[filepath] = [item];
}
// We set quiet false by default in pre-releases
// to test the syncing
async doSync(items: Zotero.Item[] = null, quiet: boolean = false) {
if (this.sycnLock) {
// Only allow one task
return;
}
// Wrap the code in try...catch so that the lock can be released anyway
try {
Zotero.debug("Better Notes: sync start");
this.sycnLock = true;
if (!items || !items.length) {
items = Zotero.Items.get(this.getSyncNoteIds());
}
console.log("BN:Sync", items);
let progress;
if (!quiet) {
progress = this._Addon.ZoteroViews.showProgressWindow(
"[Syncing] Better Notes",
`[Check Status] 0/${items.length} ...`,
"default",
-1
);
progress.progress.setProgress(1);
await this._Addon.ZoteroViews.waitProgressWindow(progress);
}
// Export items of same dir in batch
const toExport = {};
const toImport: SyncStatus[] = [];
const toDiff: SyncStatus[] = [];
let i = 1;
for (const item of items) {
const syncStatus = this._Addon.SyncUtils.getSyncStatus(item);
const filepath = decodeURIComponent(syncStatus.path);
let compareResult = await this.doCompare(item);
switch (compareResult) {
case SyncCode.NoteAhead:
if (Object.keys(toExport).includes(filepath)) {
toExport[filepath].push(item);
} else {
toExport[filepath] = [item];
}
break;
case SyncCode.MDAhead:
toImport.push(syncStatus);
break;
case SyncCode.NeedDiff:
toDiff.push(syncStatus);
break;
default:
break;
}
if (progress) {
this._Addon.ZoteroViews.changeProgressWindowDescription(
progress,
`[Check Status] ${i}/${items.length} ...`
);
progress.progress.setProgress((i / items.length) * 100);
}
i += 1;
}
console.log(toExport, toImport, toDiff);
i = 1;
let totalCount = Object.keys(toExport).length;
for (const filepath of Object.keys(toExport)) {
if (progress) {
this._Addon.ZoteroViews.changeProgressWindowDescription(
progress,
`[Update MD] ${i}/${totalCount}, ${
toImport.length + toDiff.length
} queuing...`
);
progress.progress.setProgress(((i - 1) / totalCount) * 100);
}
await this._Addon.NoteExport.exportNotesToMDFiles(toExport[filepath], {
useEmbed: false,
useSync: true,
filedir: filepath,
});
i += 1;
}
i = 1;
totalCount = toImport.length;
for (const syncStatus of toImport) {
if (progress) {
this._Addon.ZoteroViews.changeProgressWindowDescription(
progress,
`[Update Note] ${i}/${totalCount}, ${toDiff.length} queuing...`
);
progress.progress.setProgress(((i - 1) / totalCount) * 100);
}
const item = Zotero.Items.get(syncStatus.itemID);
const filepath = OS.Path.join(syncStatus.path, syncStatus.filename);
await this._Addon.NoteImport.importMDFileToNote(filepath, item, {});
await this._Addon.NoteExport.exportNotesToMDFiles([item], {
useEmbed: false,
useSync: true,
filedir: syncStatus.path,
});
i += 1;
}
i = 1;
totalCount = toDiff.length;
for (const syncStatus of toDiff) {
if (progress) {
this._Addon.ZoteroViews.changeProgressWindowDescription(
progress,
`[Compare Diff] ${i}/${totalCount}...`
);
progress.progress.setProgress(((i - 1) / totalCount) * 100);
}
const item = Zotero.Items.get(syncStatus.itemID);
await this._Addon.SyncDiffWindow.doDiff(
item,
OS.Path.join(syncStatus.path, syncStatus.filename)
);
i += 1;
}
if (
this._Addon.SyncInfoWindow._window &&
!this._Addon.SyncInfoWindow._window.closed
) {
this._Addon.SyncInfoWindow.doUpdate();
}
if (progress) {
const syncCount =
Object.keys(toExport).length + toImport.length + toDiff.length;
this._Addon.ZoteroViews.changeProgressWindowDescription(
progress,
syncCount
? `[Finish] Sync ${syncCount} notes successfully`
: "[Finish] Already up to date"
);
progress.progress.setProgress(100);
progress.startCloseTimer(5000);
}
} catch (e) {
Zotero.debug(e);
console.log(e);
}
console.log(toExport);
for (const filepath of Object.keys(toExport)) {
await this._Addon.NoteExport.syncNotesToMDFiles(
toExport[filepath],
filepath
);
}
if (this._Addon.SyncInfoWindow._window && !this._Addon.SyncInfoWindow._window.closed) {
this._Addon.SyncInfoWindow.doUpdate();
}
this.sycnLock = false;
}
}

164
src/sync/syncDiffWindow.ts Normal file
View File

@ -0,0 +1,164 @@
/*
* This file realizes note diff with markdown file.
*/
import Knowledge4Zotero from "../addon";
import AddonBase from "../module";
import { diffChars } from "diff";
class SyncDiffWindow extends AddonBase {
_window: any | Window;
constructor(parent: Knowledge4Zotero) {
super(parent);
}
async doDiff(noteItem: Zotero.Item, mdPath: string) {
const noteStatus = this._Addon.SyncUtils.getNoteStatus(noteItem);
mdPath = Zotero.File.normalizeToUnix(mdPath);
if (!noteItem || !noteItem.isNote() || !(await OS.File.exists(mdPath))) {
return;
}
const mdStatus = await this._Addon.SyncUtils.getMDStatus(mdPath);
if (!mdStatus.meta) {
return;
}
const mdNoteContent = await this._Addon.NoteParse.parseMDToNote(
mdStatus,
noteItem,
true
);
const noteContent = await this._Addon.NoteParse.parseNoteForDiff(noteItem);
console.log(mdNoteContent, noteContent);
const changes = diffChars(noteContent, mdNoteContent);
console.log("changes", changes);
const io = {
defer: Zotero.Promise.defer(),
result: "",
type: "skip",
};
const addedCount = changes.filter((c) => c.added).length;
const removedCount = changes.filter((c) => c.removed).length;
if (addedCount === 0 || removedCount === 0) {
// If only one kind of changes, merge automatically
if (noteStatus.lastmodify >= mdStatus.lastmodify) {
// refuse all, keep note
io.result = changes
.filter((diff) => (!diff.added && !diff.removed) || diff.removed)
.map((diff) => diff.value)
.join("");
} else {
// accept all, keep md
io.result = changes
.filter((diff) => (!diff.added && !diff.removed) || diff.added)
.map((diff) => diff.value)
.join("");
}
io.type = "finish";
} else {
// Otherwise, merge manually
const imageAttachemnts = Zotero.Items.get(
noteItem.getAttachments()
).filter((attch) => attch.isEmbeddedImageAttachment());
const imageData = {};
for (const image of imageAttachemnts) {
try {
const b64 = await this._Addon.SyncUtils._getDataURL(image);
imageData[image.key] = b64;
} catch (e) {
Zotero.debug(e);
}
}
if (!this._window || this._window.closed) {
this._window = window.open(
"chrome://Knowledge4Zotero/content/diff.html",
"betternotes-note-syncdiff",
`chrome,centerscreen,resizable,status,width=900,height=550`
);
const defer = Zotero.Promise.defer();
this._window.addEventListener("DOMContentLoaded", (e) => {
defer.resolve();
});
// Incase we missed the content loaded event
setTimeout(() => {
if (this._window.document.readyState === "complete") {
defer.resolve();
}
}, 500);
await defer.promise;
}
this._window.document.title = `[Better Notes Sycing] Diff Merge of ${noteItem.getNoteTitle()}`;
this._window.syncInfo = {
noteName: noteItem.getNoteTitle(),
noteModify: noteStatus.lastmodify.toISOString(),
mdName: mdPath,
mdModify: mdStatus.lastmodify.toISOString(),
syncTime: new Date(
this._Addon.SyncUtils.getSyncStatus(noteItem).lastsync
).toISOString(),
};
this._window.diffData = changes.map((change, id) =>
Object.assign(change, {
id: id,
text: change.value,
})
);
this._window.imageData = imageData;
this._window.io = io;
this._window.initSyncInfo();
this._window.initList();
this._window.initDiffViewer();
this._window.updateDiffRender([]);
const abort = () => {
console.log("unloaded");
io.defer.resolve();
};
// If closed by user, abort syncing
this._window.addEventListener("beforeunload", abort);
this._window.addEventListener("unload", abort);
this._window.addEventListener("close", abort);
this._window.onclose = abort;
this._window.onbeforeunload = abort;
this._window.onunload = abort;
await io.defer.promise;
}
switch (io.type) {
case "skip":
alert(
`Syncing of "${noteItem.getNoteTitle()}" is skipped.\nTo sync manually, go to File->Better Notes Sync Manager.`
);
this._window.closed || this._window.close();
break;
case "unsync":
Zotero.debug("remove synce" + noteItem.getNote());
await this._Addon.SyncController.removeSyncNote(noteItem);
break;
case "finish":
Zotero.debug("Diff result:" + io.result);
console.log("Diff result:", io.result);
// return io.result;
noteItem.setNote(noteStatus.meta + io.result + noteStatus.tail);
await noteItem.saveTx({
notifierData: {
autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
},
});
await this._Addon.NoteExport.exportNotesToMDFiles([noteItem], {
useEmbed: false,
useSync: true,
filedir: mdStatus.filedir,
});
break;
default:
break;
}
}
}
export default SyncDiffWindow;

View File

@ -27,9 +27,7 @@ class SyncInfoWindow extends AddonBase {
}
doUpdate() {
const syncInfo = this._Addon.SyncController.getNoteSyncStatus(
this.io.dataIn
);
const syncInfo = this._Addon.SyncUtils.getSyncStatus(this.io.dataIn);
const syncPathLable = this._window.document.getElementById(
"Knowledge4Zotero-sync-path"
);

View File

@ -44,7 +44,7 @@ class SyncListWindow extends AddonBase {
e.parentElement.removeChild(e);
}
for (const note of notes) {
const syncInfo = this._Addon.SyncController.getNoteSyncStatus(note);
const syncInfo = this._Addon.SyncUtils.getSyncStatus(note);
const listitem: XUL.ListItem =
this._window.document.createElement("listitem");
listitem.setAttribute("id", note.id);
@ -101,6 +101,7 @@ class SyncListWindow extends AddonBase {
(period > 0 ? period + "s" : "disabled")
);
this._window.focus();
this.onSelect();
}
getSelectedItems(): Zotero.Item[] {
@ -109,21 +110,39 @@ class SyncListWindow extends AddonBase {
(this._window.document.getElementById("sync-list") as any)
.selectedItems,
(node) => node.id
)
) as Zotero.Item[];
) as string[]
);
}
onSelect() {
const selected =
(this._window.document.getElementById("sync-list") as any).selectedItems
.length > 0;
if (selected) {
this._window.document
.querySelector("#changesync")
.removeAttribute("disabled");
this._window.document
.querySelector("#removesync")
.removeAttribute("disabled");
} else {
this._window.document
.querySelector("#changesync")
.setAttribute("disabled", "true");
this._window.document
.querySelector("#removesync")
.setAttribute("disabled", "true");
}
}
useRelated(): Boolean {
return (this._window.document.getElementById("related") as XUL.Checkbox)
.checked;
return confirm(
"Apply changes to:\n[Yes] Selected note and it's linked notes\n[No] Only selected note"
);
}
async doSync() {
const selectedItems = this.getSelectedItems();
if (selectedItems.length === 0) {
return;
}
await this._Addon.SyncController.doSync(selectedItems, true, false);
await this._Addon.SyncController.doSync(this.getSelectedItems(), false);
this.doUpdate();
}
@ -132,7 +151,10 @@ class SyncListWindow extends AddonBase {
if (selectedItems.length === 0) {
return;
}
await this._Addon.NoteExport.exportNotesToMDFiles(selectedItems, false, true);
await this._Addon.NoteExport.exportNotesToMDFiles(selectedItems, {
useEmbed: false,
useSync: true,
});
this.doUpdate();
}

1084
src/sync/syncUtils.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -137,8 +137,8 @@ class TemplateWindow extends AddonBase {
);
await io.deferred.promise;
const ids = io.dataOut;
const note: Zotero.Item = (Zotero.Items.get(ids) as Zotero.Item[]).filter(
const ids = io.dataOut as number[];
const note: Zotero.Item = Zotero.Items.get(ids).filter(
(item: Zotero.Item) => item.isNote()
)[0];
if (!note) {

View File

@ -109,7 +109,7 @@ async function pick(
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return new Zotero.Promise((resolve) => {
return new Promise((resolve) => {
fp.open((userChoice) => {
switch (userChoice) {
case Components.interfaces.nsIFilePicker.returnOK:
@ -124,4 +124,38 @@ async function pick(
});
});
}
export { EditorMessage, OutlineType, NoteTemplate, CopyHelper, pick };
enum SyncCode {
UpToDate = 0,
NoteAhead,
MDAhead,
NeedDiff,
}
enum NodeMode {
default = 0,
wrap,
replace,
direct,
}
function getDOMParser(): DOMParser {
if (Zotero.platformMajorVersion > 60) {
return new DOMParser();
} else {
return Components.classes[
"@mozilla.org/xmlextras/domparser;1"
].createInstance(Components.interfaces.nsIDOMParser);
}
}
export {
EditorMessage,
OutlineType,
NoteTemplate,
CopyHelper,
pick,
SyncCode,
NodeMode,
getDOMParser,
};

View File

@ -7,13 +7,13 @@ import { EditorMessage, OutlineType, pick } from "../utils";
import AddonBase from "../module";
class WorkspaceWindow extends AddonBase {
private _initIframe: ZoteroPromise;
private _initIframe: _ZoteroPromise;
public workspaceWindow: Window;
public workspaceTabId: string;
public workspaceNoteEditor: Zotero.EditorInstance | undefined;
public previewItemID: number;
private _firstInit: boolean;
public _workspacePromise: ZoteroPromise;
public _workspacePromise: _ZoteroPromise;
private _DOMParser: any;
constructor(parent: Knowledge4Zotero) {

View File

@ -30,10 +30,14 @@ class ZoteroEvents extends AddonBase {
this._Addon.ZoteroViews.updateWordCount();
}
// Check Note Sync
const syncIds =
this._Addon.SyncController.getSyncNoteIds() as number[];
if (ids.filter((id) => syncIds.includes(id as number)).length > 0) {
this._Addon.SyncController.setSync();
const syncIds = this._Addon.SyncController.getSyncNoteIds();
const modifiedSyncIds = ids.filter((id) =>
syncIds.includes(id as number)
) as number[];
if (modifiedSyncIds.length > 0) {
this._Addon.SyncController.doSync(
Zotero.Items.get(modifiedSyncIds)
);
Zotero.debug("Better Notes: sync planned.");
}
}
@ -323,6 +327,7 @@ class ZoteroEvents extends AddonBase {
instance._iframeWindow.document.addEventListener(
"selectionchange",
async (e) => {
e.stopPropagation();
await this._Addon.NoteUtils.onSelectionChange(instance);
}
);
@ -924,7 +929,10 @@ class ZoteroEvents extends AddonBase {
console.log(html);
new CopyHelper()
.addText(html, "text/html")
.addText(this._Addon.NoteParse.parseHTMLToMD(html), "text/unicode")
.addText(
await this._Addon.NoteParse.parseHTMLToMD(html),
"text/unicode"
)
.copy();
progressWindow.changeHeadline("Template Copied");
} else {
@ -1040,7 +1048,10 @@ class ZoteroEvents extends AddonBase {
new CopyHelper()
.addText(html, "text/html")
.addText(this._Addon.NoteParse.parseHTMLToMD(html), "text/unicode")
.addText(
await this._Addon.NoteParse.parseHTMLToMD(html),
"text/unicode"
)
.copy();
progressWindow.changeHeadline("Template Copied");
} else {
@ -1119,11 +1130,10 @@ class ZoteroEvents extends AddonBase {
return;
}
if (options.exportMD && options.exportSubMD) {
await this._Addon.NoteExport.exportNotesToMDFiles(
[item],
false,
options.exportAutoSync
);
await this._Addon.NoteExport.exportNotesToMDFiles([item], {
useEmbed: false,
useSync: options.exportAutoSync,
});
} else {
await this._Addon.NoteExport.exportNote(item, options);
}
@ -1156,10 +1166,9 @@ class ZoteroEvents extends AddonBase {
);
} else {
const useSingleFile = confirm("Export linked notes to markdown files?");
await this._Addon.NoteExport.exportNotesToMDFiles(
noteItems,
!useSingleFile
);
await this._Addon.NoteExport.exportNotesToMDFiles(noteItems, {
useEmbed: !useSingleFile,
});
}
} else if (message.type === "sync") {
/*
@ -1169,9 +1178,12 @@ class ZoteroEvents extends AddonBase {
*/
const note = this._Addon.WorkspaceWindow.getWorkspaceNote();
if (this._Addon.SyncController.isSyncNote(note)) {
this._Addon.SyncController.doSync([note], true, false);
this._Addon.SyncController.doSync([note]);
} else {
await this._Addon.NoteExport.exportNotesToMDFiles([note], false, true);
await this._Addon.NoteExport.exportNotesToMDFiles([note], {
useEmbed: false,
useSync: true,
});
}
} else if (message.type === "openAttachment") {
/*
@ -1262,7 +1274,7 @@ class ZoteroEvents extends AddonBase {
);
return;
}
const html = this._Addon.NoteParse.parseMDToHTML(source);
const html = await this._Addon.NoteParse.parseMDToHTML(source);
console.log(source, html);
new CopyHelper().addText(html, "text/html").copy();

View File

@ -68,20 +68,56 @@ class ZoteroViews extends AddonBase {
let addNoteItem = document
.getElementById("zotero-tb-note-add")
.getElementsByTagName("menuitem")[1];
let button = document.createElement("menuitem");
button.setAttribute("id", "zotero-tb-knowledge-openwindow");
button.setAttribute("label", "New Main Note");
button.addEventListener("click", (e) => {
this._Addon.ZoteroEvents.onEditorEvent(
new EditorMessage("createWorkspace", {})
);
let buttons = this.createXULElement(document, {
tag: "fragment",
subElementOptions: [
{
tag: "menuitem",
id: "zotero-tb-knowledge-create-mainnote",
attributes: [
["label", "New Main Note"],
["class", "menuitem-iconic"],
[
"style",
"list-style-image: url('chrome://Knowledge4Zotero/skin/favicon.png');",
],
],
listeners: [
[
"click",
(e) => {
this._Addon.ZoteroEvents.onEditorEvent(
new EditorMessage("createWorkspace", {})
);
},
false,
],
],
},
{
tag: "menuitem",
id: "zotero-tb-knowledge-import-md",
attributes: [
["label", "Import MarkDown as Note"],
["class", "menuitem-iconic"],
[
"style",
"list-style-image: url('chrome://Knowledge4Zotero/skin/favicon.png');",
],
],
listeners: [
[
"click",
async (e) => {
await this._Addon.NoteImport.doImport();
},
false,
],
],
},
],
});
button.setAttribute("class", "menuitem-iconic");
button.setAttribute(
"style",
"list-style-image: url('chrome://Knowledge4Zotero/skin/favicon.png');"
);
addNoteItem.after(button);
addNoteItem.after(buttons);
}
public addOpenWorkspaceButton() {
@ -111,10 +147,12 @@ class ZoteroViews extends AddonBase {
: "Open Workspace";
span1.append(span2, span3, span4);
treeRow.append(span1);
treeRow.addEventListener("click", (e) => {
this._Addon.ZoteroEvents.onEditorEvent(
new EditorMessage("openWorkspace", { event: e })
);
treeRow.addEventListener("click", async (e) => {
if (e.shiftKey) {
await this._Addon.WorkspaceWindow.openWorkspaceWindow("window", true);
} else {
await this._Addon.WorkspaceWindow.openWorkspaceWindow();
}
});
treeRow.addEventListener("mouseover", (e: XUL.XULEvent) => {
treeRow.setAttribute(
@ -361,6 +399,16 @@ class ZoteroViews extends AddonBase {
progressWindow.progress._itemText.innerHTML = context;
}
public async waitProgressWindow(progressWindow) {
let t = 0;
// Wait for ready
while (!progressWindow.progress._itemText && t < 100) {
t += 1;
await Zotero.Promise.delay(10);
}
return;
}
public createXULElement(doc: Document, options: XULElementOptions) {
const createElement: () => XUL.Element =
options.tag === "fragment"

54
typing/global.d.ts vendored
View File

@ -1,8 +1,3 @@
declare interface ZoteroPromise {
promise: Promise<void>;
resolve: () => void;
}
declare interface XULElementOptions {
tag: string;
id?: string;
@ -22,3 +17,52 @@ declare interface XULElementOptions {
customCheck?: () => boolean;
subElementOptions?: Array<XULElementOptions>;
}
declare interface SyncStatus {
path: string;
filename: string;
md5: string;
lastsync: number;
itemID: number;
}
declare interface MDStatus {
meta: {
version: number;
} | null;
content: string;
filedir: string;
filename: string;
lastmodify: Date;
}
declare interface NoteStatus {
meta: string;
content: string;
tail: string;
lastmodify: Date;
}
declare interface AnnotationJson {
authorName: string;
color: string;
comment: string;
dateModified: string;
image: string;
imageAttachmentKey: string;
isAuthorNameAuthoritative: boolean;
isExternal: boolean;
id: string;
key: string;
lastModifiedByUser: string;
pageLabel: string;
position: {
rects: number[];
};
readOnly: boolean;
sortIndex: any;
tags: { name: string }[];
text: string;
type: string;
attachmentItemID: number;
}