add: two-path syncing (md<->note)
This commit is contained in:
parent
7b2768bcfb
commit
62b84a5e70
|
|
@ -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] </span>
|
||||
</div>
|
||||
<div>
|
||||
<span>[MarkDown] </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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@
|
|||
background: #fbfbfb;
|
||||
border: solid #c9c9c9 1px;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.tool-button:hover {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
31
package.json
31
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class EditorController extends AddonBase {
|
|||
instance: Zotero.EditorInstance;
|
||||
time: number;
|
||||
}>;
|
||||
editorPromise: ZoteroPromise;
|
||||
editorPromise: _ZoteroPromise;
|
||||
activeEditor: Zotero.EditorInstance;
|
||||
|
||||
constructor(parent: Knowledge4Zotero) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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]",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 = `<!-- bn::${citationKey} -->`;
|
||||
const afterCitationDecorator = doc.createElement("span");
|
||||
afterCitationDecorator.innerHTML = `<!-- bn::${citationKey} -->`;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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) {
|
||||
|
|
|
|||
38
src/utils.ts
38
src/utils.ts
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue