change: export md

fix: reader note editor not initialized
add: export md with citation wrapped
fix: MDFileName template bug
This commit is contained in:
xiangyu 2022-09-22 16:05:25 +08:00
parent d55d03edb8
commit fa193ad963
8 changed files with 326 additions and 2007 deletions

File diff suppressed because it is too large Load Diff

View File

@ -32,11 +32,15 @@
"compressing": "^1.5.1",
"esbuild": "^0.14.34",
"replace-in-file": "^6.3.2",
"tree-model": "^1.0.7"
"seedrandom": "^3.0.5",
"tree-model": "^1.0.7",
"turndown": "^7.1.1",
"turndown-plugin-gfm": "^1.0.2"
},
"devDependencies": {
"@types/jquery": "^3.5.14",
"@types/node": "^17.0.31",
"@types/turndown": "^5.0.1",
"release-it": "^14.14.0",
"zotero-types": "^0.0.4"
}

View File

@ -76,7 +76,7 @@ class AddonEvents extends AddonBase {
t < 100
) {
t += 1;
this._Addon.knowledge.setWorkspaceNote();
this._Addon.knowledge.setWorkspaceNote("main", undefined, false);
await Zotero.Promise.delay(100);
}
@ -500,15 +500,15 @@ class AddonEvents extends AddonBase {
editor._knowledgeUIInitialized = false;
const currentID = editor._item.id;
const noteItem = editor._item;
// item.getNote may not be initialized yet
if (Zotero.ItemTypes.getID("note") !== noteItem.itemTypeID) {
return;
}
// Check if this is a window for print
const isPrint = this._Addon.knowledge._pdfNoteId === currentID;
const noteItem = Zotero.Items.get(currentID) as Zotero.Item;
if (!noteItem.isNote()) {
return;
}
const mainNoteID = parseInt(
Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") as string
);
@ -1843,6 +1843,9 @@ class AddonEvents extends AddonBase {
await io.deferred.promise;
const options = io.dataOut as any;
if (!options) {
return;
}
if (options.exportFile && options.exportSingleFile) {
await this._Addon.knowledge.exportNotesToFile(
[item],

View File

@ -1,34 +0,0 @@
const TRANSLATOR_ID_BETTER_MARKDOWN = "1412e9e2-51e1-42ec-aa35-e036a895534c";
const configs = {};
configs[TRANSLATOR_ID_BETTER_MARKDOWN] = {
translatorID: TRANSLATOR_ID_BETTER_MARKDOWN,
label: "Better Note Markdown",
creator: "Martynas Bagdonas; Winding",
target: "md",
minVersion: "5.0.97",
maxVersion: "",
priority: 50,
configOptions: {
noteTranslator: true,
},
displayOptions: {
includeAppLinks: true,
},
inRepository: true,
translatorType: 2,
lastUpdated: "2022-06-01 23:26:46",
_codePath:
"chrome://Knowledge4Zotero/content/translators/Better Note Markdown.js",
};
async function loadTranslator(id) {
const config = configs[id];
const code = (await Zotero.File.getContentsAsync(config._codePath)).response;
Zotero.debug(code);
await Zotero.Translators.save(config, code);
await Zotero.Translators.reinit();
}
export { TRANSLATOR_ID_BETTER_MARKDOWN, loadTranslator };

View File

@ -1,6 +1,5 @@
import Knowledge4Zotero from "./addon";
import { OutlineType } from "./base";
import { loadTranslator, TRANSLATOR_ID_BETTER_MARKDOWN } from "./exportMD";
import { pick } from "./file_picker";
import AddonBase from "./module";
@ -15,7 +14,6 @@ class Knowledge extends AddonBase {
_workspacePromise: any;
_exportPath: string;
_exportFileDict: object;
_exportPromise: any;
_pdfNoteId: number;
_pdfPrintPromise: any;
constructor(parent: Knowledge4Zotero) {
@ -191,7 +189,8 @@ class Knowledge extends AddonBase {
async setWorkspaceNote(
type: "main" | "preview" = "main",
note: Zotero.Item | undefined = undefined
note: Zotero.Item | undefined = undefined,
showPopup: boolean = true
) {
let _window = this.getWorkspaceWindow() as Window;
note = note || this.getWorkspaceNote();
@ -273,10 +272,12 @@ class Knowledge extends AddonBase {
.filter((id) => id)
.join(",")
);
this._Addon.views.showProgressWindow(
"Better Notes",
`Set main Note to: ${note.getNoteTitle()}`
);
if (showPopup) {
this._Addon.views.showProgressWindow(
"Better Notes",
`Set main Note to: ${note.getNoteTitle()}`
);
}
}
}
@ -786,12 +787,12 @@ class Knowledge extends AddonBase {
async exportNoteToFile(
note: Zotero.Item,
convertNoteLinks: boolean = true,
saveFile: boolean = true,
saveMD: boolean = true,
saveNote: boolean = false,
saveCopy: boolean = false,
doCopy: boolean = false,
savePDF: boolean = false
) {
if (!saveFile && !saveNote && !saveCopy && !savePDF) {
if (!saveMD && !saveNote && !doCopy && !savePDF) {
return;
}
this._exportFileDict = [];
@ -822,9 +823,7 @@ class Knowledge extends AddonBase {
newNote = note;
}
if (saveFile) {
await loadTranslator(TRANSLATOR_ID_BETTER_MARKDOWN);
if (saveMD) {
const filename = await pick(
Zotero.getString("fileInterface.export"),
"save",
@ -836,30 +835,10 @@ class Knowledge extends AddonBase {
Zotero.File.pathToFile(filename).parent.path + "/attachments";
// Convert to unix format
this._exportPath = this._exportPath.replace(/\\/g, "/");
Components.utils.import("resource://gre/modules/osfile.jsm");
const hasImage = newNote.getNote().includes("<img");
if (hasImage) {
await Zotero.File.createDirectoryIfMissingAsync(
OS.Path.join(...this._exportPath.split(/\//))
);
}
const translator = new Zotero.Translate.Export();
translator.setItems([newNote]);
translator.setLocation(Zotero.File.pathToFile(filename));
this._exportPromise = Zotero.Promise.defer();
translator.setTranslator(TRANSLATOR_ID_BETTER_MARKDOWN);
translator.translate();
await this._exportPromise.promise;
this._Addon.views.showProgressWindow(
"Better Notes",
`Note Saved to ${filename}`
);
await this._export(newNote, filename, false);
}
}
if (saveCopy) {
if (doCopy) {
if (!convertNoteLinks) {
Zotero_File_Interface.exportItemsToClipboard(
[newNote],
@ -944,8 +923,6 @@ class Knowledge extends AddonBase {
return;
}
await loadTranslator(TRANSLATOR_ID_BETTER_MARKDOWN);
this._exportPath = Zotero.File.pathToFile(filepath).path + "/attachments";
// Convert to unix format
this._exportPath = this._exportPath.replace(/\\/g, "/");
@ -1007,7 +984,6 @@ class Knowledge extends AddonBase {
allNoteIds = allNoteIds.concat(subNoteIds);
}
allNoteIds = Array.from(new Set(allNoteIds));
console.log(allNoteIds);
const allNoteItems: Zotero.Item[] = Zotero.Items.get(
allNoteIds
) as Zotero.Item[];
@ -1101,13 +1077,11 @@ class Knowledge extends AddonBase {
if (!Zotero.isWin && filename.charAt(0) !== "/") {
filename = "/" + filename;
}
const translator = new Zotero.Translate.Export();
translator.setItems([note]);
translator.setLocation(Zotero.File.pathToFile(filename));
translator.setTranslator(TRANSLATOR_ID_BETTER_MARKDOWN);
this._exportPromise = Zotero.Promise.defer();
translator.translate();
await this._exportPromise.promise;
const content: string = await this._Addon.parse.parseNoteToMD(note);
console.log(
`Exporting MD file: ${filename}, content length: ${content.length}`
);
await Zotero.File.putContentsAsync(filename, content);
this._Addon.views.showProgressWindow(
"Better Notes",
`Note Saved to ${filename}`
@ -1122,11 +1096,11 @@ class Knowledge extends AddonBase {
}
private _getFileName(noteItem: Zotero.Item) {
return this._Addon.template.renderTemplate(
"[ExportMDFileName]",
"noteItem",
[noteItem]
);
return (
this._Addon.template.renderTemplate("[ExportMDFileName]", "noteItem", [
noteItem,
]) as string
).replace(/\\/g, "-");
}
async convertNoteLines(

View File

@ -1,9 +1,28 @@
import AddonBase from "./module";
import { HTML2Markdown, Markdown2HTML } from "./convertMD";
import TurndownService = require("turndown");
const turndownPluginGfm = require("turndown-plugin-gfm");
const TreeModel = require("./treemodel");
const asciidoctor = require("asciidoctor")();
const seedrandom = require("seedrandom");
class AddonParse extends AddonBase {
// 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;
}
public parseNoteTree(note: Zotero.Item): TreeModel.Node<object> {
const noteLines = this._Addon.knowledge.getLinesInNote(note);
let tree = new TreeModel();
@ -431,6 +450,272 @@ class AddonParse extends AddonBase {
parseAsciiDocToHTML(str: string): string {
return asciidoctor.convert(str);
}
// A realization of Markdown Note.js translator
async parseNoteToMD(noteItem: Zotero.Item, options: any = {}) {
const doc = new DOMParser().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);
}
});
// Turndown wants pre content inside additional code block
doc.querySelectorAll("pre").forEach(function (pre) {
let code = doc.createElement("code");
code.append(...pre.childNodes);
pre.append(code);
});
// Insert a PDF link for highlight and image annotation nodes
doc
.querySelectorAll('span[class="highlight"], img[data-annotation]')
.forEach((node) => {
Zotero.debug(node.outerHTML);
try {
var annotation = JSON.parse(
decodeURIComponent(node.getAttribute("data-annotation"))
);
} catch (e) {
Zotero.debug(e);
}
if (annotation) {
// annotation.uri was used before note-editor v4
let uri = annotation.attachmentURI || annotation.uri;
let position = annotation.position;
Zotero.debug("----Debug Link----");
Zotero.debug(annotation);
if (typeof uri === "string" && typeof position === "object") {
Zotero.debug(uri);
let openURI;
let uriParts = uri.split("/");
let libraryType = uriParts[3];
let key = uriParts[uriParts.length - 1];
Zotero.debug(key);
if (libraryType === "users") {
openURI = "zotero://open-pdf/library/items/" + key;
}
// groups
else {
let groupID = uriParts[4];
openURI = "zotero://open-pdf/groups/" + groupID + "/items/" + key;
}
openURI +=
"?page=" +
(position.pageIndex + 1) +
(annotation.annotationKey
? "&annotation=" + annotation.annotationKey
: "");
let a = doc.createElement("a");
a.href = openURI;
a.append("pdf");
let fragment = doc.createDocumentFragment();
fragment.append(" (", a, ") ");
if (options.wrapCitation) {
const citationKey = annotation.annotationKey
? annotation.annotationKey
: this.randomString(
8,
Zotero.Utilities.allowedKeyChars,
Zotero.Utilities.Internal.md5(
node.getAttribute("data-annotation")
)
);
Zotero.Utilities.Internal.md5(
node.getAttribute("data-annotation")
);
const beforeCitationDecorator = doc.createElement("span");
beforeCitationDecorator.innerHTML = `&lt;!-- bn::${citationKey} --&gt;`;
const afterCitationDecorator = doc.createElement("span");
afterCitationDecorator.innerHTML = `&lt;!-- bn::${citationKey} --&gt;`;
node.before(beforeCitationDecorator);
fragment.append(afterCitationDecorator);
}
let nextNode = node.nextElementSibling;
if (nextNode && nextNode.classList.contains("citation")) {
nextNode.parentNode.insertBefore(fragment, nextNode.nextSibling);
} else {
node.parentNode.insertBefore(fragment, node.nextSibling);
}
}
}
});
console.log(doc);
for (const img of doc.querySelectorAll("img[data-attachment-key]")) {
let imgKey = img.getAttribute("data-attachment-key");
const attachmentItem = await Zotero.Items.getByLibraryAndKeyAsync(
noteItem.libraryID,
imgKey
);
Zotero.debug(attachmentItem);
let oldFile = String(await attachmentItem.getFilePathAsync());
Zotero.debug(oldFile);
let ext = oldFile.split(".").pop();
let newAbsPath = OS.Path.join(
...`${this._Addon.knowledge._exportPath}/${imgKey}.${ext}`.split(/\//)
);
if (!Zotero.isWin && newAbsPath.charAt(0) !== "/") {
newAbsPath = "/" + newAbsPath;
}
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: function (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
var _Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
Components.interfaces.nsISupports
).wrappedJSObject;
const noteInfo =
_Zotero.Knowledge4Zotero.knowledge._exportFileDict &&
_Zotero.Knowledge4Zotero.knowledge._exportFileDict.find((i) =>
href.includes(i.link)
);
if (noteInfo) {
href = `./${noteInfo.filename}`;
}
}
return "[" + content + "](" + href + title + ")";
},
});
return turndownService.turndown(doc.body);
}
}
export default AddonParse;

View File

@ -38,7 +38,7 @@ class AddonTemplate extends AddonBase {
},
{
name: "[ExportMDFileName]",
text: '${(noteItem.getNoteTitle ? noteItem.getNoteTitle().replace(/[/\\?%*:|"<> ]/g, "-") + "-" : "")}${noteItem.key}.md',
text: '${(noteItem.getNoteTitle ? noteItem.getNoteTitle().replace(/[/\\\\?%*:|"<> ]/g, "-") + "-" : "")}${noteItem.key}.md',
disabled: false,
},
{

View File

@ -6,7 +6,8 @@
"include": [
"src",
"typing",
"node_modules/zotero-types"
"node_modules/zotero-types",
],
"exclude": [
"builds",