Upgrade note schema to v2:

- Automatically upgrade all editable v1 notes when note is being opened
- Pull `itemData` from existing citations and annotations into metadata container attribute `data-citation-items`
- Strip `abstract` from `itemData`
- Fill `citationItem` (in citations and annotations) with `itemData` when passing an HTML fragment for further processing in `note-editor`
- Keep only `uri`, `text`, `color`, `pageLabel`, `position`, `citationItem` annotation properties
This commit is contained in:
Martynas Bagdonas 2021-04-02 18:33:02 +03:00
parent 26bf507fe2
commit 415e644211
5 changed files with 274 additions and 49 deletions

View file

@ -95,6 +95,13 @@
if (this._editorInstance) { if (this._editorInstance) {
this._editorInstance.uninit(); this._editorInstance.uninit();
} }
// Automatically upgrade editable v1 note before it's loaded
// TODO: Remove this at some point
if (this.editable) {
await Zotero.Notes.upgradeSchemaV1(this._item);
}
this._editorInstance = new Zotero.EditorInstance(); this._editorInstance = new Zotero.EditorInstance();
await this._editorInstance.init({ await this._editorInstance.init({
state, state,

View file

@ -125,35 +125,57 @@ Zotero.Notes = new function() {
if (!item.isNote()) { if (!item.isNote()) {
throw new Error('Item is not a note'); throw new Error('Item is not a note');
} }
var note = item.getNote(); let note = item.getNote();
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] let parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser); .createInstance(Components.interfaces.nsIDOMParser);
var doc = parser.parseFromString(note, 'text/html'); let doc = parser.parseFromString(note, 'text/html');
var nodes = doc.querySelectorAll('img[data-attachment-key]'); // Make sure this is the new note
for (var node of nodes) { let metadataContainer = doc.querySelector('body > div[data-schema-version]');
var attachmentKey = node.getAttribute('data-attachment-key'); if (metadataContainer) {
// Load base64 image data into src
let nodes = doc.querySelectorAll('img[data-attachment-key]');
for (let node of nodes) {
let attachmentKey = node.getAttribute('data-attachment-key');
if (attachmentKey) { if (attachmentKey) {
var attachment = Zotero.Items.getByLibraryAndKey(item.libraryID, attachmentKey); let attachment = Zotero.Items.getByLibraryAndKey(item.libraryID, attachmentKey);
if (attachment && attachment.parentID == item.id) { if (attachment && attachment.parentID == item.id) {
var dataURI = await attachment.attachmentDataURI; let dataURI = await attachment.attachmentDataURI;
node.setAttribute('src', dataURI); node.setAttribute('src', dataURI);
} }
} }
node.removeAttribute('data-attachment-key'); node.removeAttribute('data-attachment-key');
} }
var nodes = doc.querySelectorAll('.citation[data-citation]'); // Set itemData for each citation citationItem
for (var node of nodes) { nodes = doc.querySelectorAll('.citation[data-citation]');
var citation = node.getAttribute('data-citation'); for (let node of nodes) {
let citation = node.getAttribute('data-citation');
try { try {
citation = JSON.parse(decodeURIComponent(citation)); citation = JSON.parse(decodeURIComponent(citation));
for (var citationItem of citation.citationItems) { for (let citationItem of citation.citationItems) {
var item = await Zotero.EditorInstance.getItemFromURIs(citationItem.uris); // Get itemData from existing item
let item = await Zotero.EditorInstance.getItemFromURIs(citationItem.uris);
if (item) { if (item) {
citationItem.itemData = Zotero.Cite.System.prototype.retrieveItem(item); citationItem.itemData = Zotero.Cite.System.prototype.retrieveItem(item);
} }
// Get itemData from note metadata container
else {
try {
let items = JSON.parse(decodeURIComponent(metadataContainer.getAttribute('data-citation-items')));
let item = items.find(item => item.uris.some(uri => citationItem.uris.includes(uri)));
if (item) {
citationItem.itemData = item.itemData;
}
}
catch (e) {
}
}
if (!citationItem.itemData) {
node.replaceWith('(MISSING CITATION)');
break;
}
} }
citation = encodeURIComponent(JSON.stringify(citation)); citation = encodeURIComponent(JSON.stringify(citation));
node.setAttribute('data-citation', citation); node.setAttribute('data-citation', citation);
@ -162,6 +184,7 @@ Zotero.Notes = new function() {
Zotero.logError(e); Zotero.logError(e);
} }
} }
}
return doc.body.innerHTML; return doc.body.innerHTML;
}; };
@ -171,6 +194,112 @@ Zotero.Notes = new function() {
let doc = parser.parseFromString(note, 'text/html'); let doc = parser.parseFromString(note, 'text/html');
return !!doc.querySelector('body > div[data-schema-version]'); return !!doc.querySelector('body > div[data-schema-version]');
}; };
/**
* Upgrade v1 notes:
* - Pull itemData from citations, highlights, images into metadata container
* - Strip abstract field from itemData
* - For `data-annotation` keep only the following fields:
* - uri
* - text
* - color
* - pageLabel
* - position
* - citationItem
* - Increase schema version number
*
* @param item
* @returns {Promise<boolean>}
*/
this.upgradeSchemaV1 = async function (item) {
let note = item.note;
let parser = Components.classes['@mozilla.org/xmlextras/domparser;1']
.createInstance(Components.interfaces.nsIDOMParser);
let doc = parser.parseFromString(note, 'text/html');
let metadataContainer = doc.querySelector('body > div[data-schema-version]');
if (!metadataContainer) {
return false;
}
let schemaVersion = parseInt(metadataContainer.getAttribute('data-schema-version'));
if (schemaVersion !== 1) {
return false;
}
let storedCitationItems = [];
try {
let data = JSON.parse(decodeURIComponent(metadataContainer.getAttribute('data-citation-items')));
if (Array.isArray(data)) {
storedCitationItems = data;
}
} catch (e) {
}
function pullItemData(citationItem) {
let { uris, itemData } = citationItem;
if (itemData) {
delete citationItem.itemData.abstract;
delete citationItem.itemData;
let item = storedCitationItems.find(item => item.uris.some(uri => uris.includes(uri)));
if (!item) {
storedCitationItems.push({ uris, itemData });
}
}
}
let nodes = doc.querySelectorAll('.citation[data-citation]');
for (let node of nodes) {
let citation = node.getAttribute('data-citation');
try {
citation = JSON.parse(decodeURIComponent(citation));
citation.citationItems.forEach(citationItem => pullItemData(citationItem));
citation = encodeURIComponent(JSON.stringify(citation));
node.setAttribute('data-citation', citation);
}
catch (e) {
Zotero.logError(e);
}
}
// img[data-annotation] and div.highlight[data-annotation]
nodes = doc.querySelectorAll('*[data-annotation]');
for (let node of nodes) {
let annotation = node.getAttribute('data-annotation');
try {
annotation = JSON.parse(decodeURIComponent(annotation));
if (annotation.citationItem) {
pullItemData(annotation.citationItem);
}
annotation = {
uri: annotation.uri,
text: annotation.text,
color: annotation.color,
pageLabel: annotation.pageLabel,
position: annotation.position,
citationItem: annotation.citationItem
};
annotation = encodeURIComponent(JSON.stringify(annotation));
node.setAttribute('data-annotation', annotation);
}
catch (e) {
Zotero.logError(e);
}
}
if (storedCitationItems.length) {
storedCitationItems = encodeURIComponent(JSON.stringify(storedCitationItems));
metadataContainer.setAttribute('data-citation-items', storedCitationItems);
}
schemaVersion++;
metadataContainer.setAttribute('data-schema-version', schemaVersion);
note = doc.body.innerHTML;
note = note.trim();
item.setNote(note);
await item.saveTx({ skipDateModifiedUpdate: true });
return true;
};
}; };
if (typeof process === 'object' && process + '' === '[object process]') { if (typeof process === 'object' && process + '' === '[object process]') {

View file

@ -36,7 +36,7 @@ const DOWNLOADED_IMAGE_TYPE = [
]; ];
// Schema version here has to be the same as in note-editor! // Schema version here has to be the same as in note-editor!
const SCHEMA_VERSION = 1; const SCHEMA_VERSION = 2;
class EditorInstance { class EditorInstance {
constructor() { constructor() {
@ -159,7 +159,7 @@ class EditorInstance {
async insertAnnotations(annotations) { async insertAnnotations(annotations) {
await this._ensureNoteCreated(); await this._ensureNoteCreated();
let html = await this._serializeAnnotations(annotations); let [html] = await this._serializeAnnotations(annotations);
if (html) { if (html) {
this._postMessage({ action: 'insertHTML', pos: -1, html }); this._postMessage({ action: 'insertHTML', pos: -1, html });
} }
@ -197,9 +197,11 @@ class EditorInstance {
/** /**
* @param {Zotero.Item[]} annotations * @param {Zotero.Item[]} annotations
* @param {Boolean} skipEmbeddingItemData Do not add itemData to citation items
* @return {String} - HTML string * @return {String} - HTML string
*/ */
async _serializeAnnotations(annotations) { async _serializeAnnotations(annotations, skipEmbeddingItemData) {
let storedCitationItems = [];
let html = ''; let html = '';
for (let annotation of annotations) { for (let annotation of annotations) {
let attachmentItem = await Zotero.Items.getAsync(annotation.attachmentItemID); let attachmentItem = await Zotero.Items.getAsync(annotation.attachmentItemID);
@ -218,20 +220,36 @@ class EditorInstance {
let highlightHTML = ''; let highlightHTML = '';
let commentHTML = ''; let commentHTML = '';
annotation.uri = Zotero.URI.getItemURI(attachmentItem); let storedAnnotation = {
uri: Zotero.URI.getItemURI(attachmentItem),
text: annotation.text,
color: annotation.color,
pageLabel: annotation.pageLabel,
position: annotation.position
};
// Citation // Citation
let parentItem = attachmentItem.parentID && await Zotero.Items.getAsync(attachmentItem.parentID); let parentItem = attachmentItem.parentID && await Zotero.Items.getAsync(attachmentItem.parentID);
if (parentItem) { if (parentItem) {
let uris = [Zotero.URI.getItemURI(parentItem)];
let citationItem = {
uris,
locator: annotation.pageLabel
};
// TODO: Find a more elegant way to call this // TODO: Find a more elegant way to call this
let itemData = Zotero.Cite.System.prototype.retrieveItem(parentItem); let itemData = Zotero.Cite.System.prototype.retrieveItem(parentItem);
delete itemData.abstract; delete itemData.abstract;
let citationItem = { if (!skipEmbeddingItemData) {
uris: [Zotero.URI.getItemURI(parentItem)], citationItem.itemData = itemData;
itemData, }
locator: annotation.pageLabel
}; let item = storedCitationItems.find(item => item.uris.some(uri => uris.includes(uri)));
annotation.citationItem = citationItem; if (!item) {
storedCitationItems.push({ uris, itemData });
}
storedAnnotation.citationItem = citationItem;
let citation = { let citation = {
citationItems: [citationItem], citationItems: [citationItem],
properties: {} properties: {}
@ -255,12 +273,12 @@ class EditorInstance {
const PDFJS_DEFAULT_SCALE = 1.25; const PDFJS_DEFAULT_SCALE = 1.25;
let width = Math.round(rectWidth * CSS_UNITS * PDFJS_DEFAULT_SCALE); let width = Math.round(rectWidth * CSS_UNITS * PDFJS_DEFAULT_SCALE);
let height = Math.round(rectHeight * width / rectWidth); let height = Math.round(rectHeight * width / rectWidth);
imageHTML = `<img data-attachment-key="${imageAttachmentKey}" width="${width}" height="${height}" data-annotation="${encodeURIComponent(JSON.stringify(annotation))}"/>`; imageHTML = `<img data-attachment-key="${imageAttachmentKey}" width="${width}" height="${height}" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}"/>`;
} }
// Text // Text
if (annotation.text) { if (annotation.text) {
highlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(JSON.stringify(annotation))}">“${annotation.text}”</span>`; highlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}">“${annotation.text}”</span>`;
} }
// Note // Note
@ -274,7 +292,7 @@ class EditorInstance {
} }
html += '<p>' + imageHTML + otherHTML + '</p>\n'; html += '<p>' + imageHTML + otherHTML + '</p>\n';
} }
return html; return [html, storedCitationItems];
} }
async _digestItems(ids) { async _digestItems(ids) {
@ -299,6 +317,66 @@ class EditorInstance {
} }
else if (item.isNote()) { else if (item.isNote()) {
let note = item.note; let note = item.note;
let parser = Components.classes['@mozilla.org/xmlextras/domparser;1']
.createInstance(Components.interfaces.nsIDOMParser);
let doc = parser.parseFromString(note, 'text/html');
// Get citationItems with itemData from note metadata
let storedCitationItems = [];
let containerNode = doc.querySelector('body > div[data-schema-version]');
if (containerNode) {
try {
let data = JSON.parse(decodeURIComponent(containerNode.getAttribute('data-citation-items')));
if (Array.isArray(data)) {
storedCitationItems = data;
}
}
catch (e) {
}
}
if (storedCitationItems.length) {
let fillWithItemData = (citationItems) => {
for (let citationItem of citationItems) {
let item = storedCitationItems.find(item => item.uris.some(uri => citationItem.uris.includes(uri)));
if (item) {
citationItem.itemData = item.itemData;
}
}
};
let nodes = doc.querySelectorAll('.citation[data-citation]');
for (let node of nodes) {
let citation = node.getAttribute('data-citation');
try {
citation = JSON.parse(decodeURIComponent(citation));
fillWithItemData(citation.citationItems);
citation = encodeURIComponent(JSON.stringify(citation));
node.setAttribute('data-citation', citation);
}
catch (e) {
Zotero.logError(e);
}
}
// img[data-annotation] and div.highlight[data-annotation]
nodes = doc.querySelectorAll('*[data-annotation]');
for (let node of nodes) {
let annotation = node.getAttribute('data-annotation');
try {
annotation = JSON.parse(decodeURIComponent(annotation));
fillWithItemData([annotation.citationItem]);
annotation = encodeURIComponent(JSON.stringify(annotation));
node.setAttribute('data-annotation', annotation);
}
catch (e) {
Zotero.logError(e);
}
}
}
// Clone all note image attachments and replace keys in the new note
let attachments = await Zotero.Items.getAsync(item.getAttachments()); let attachments = await Zotero.Items.getAsync(item.getAttachments());
for (let attachment of attachments) { for (let attachment of attachments) {
let path = await attachment.getFilePathAsync(); let path = await attachment.getFilePathAsync();
@ -315,9 +393,14 @@ class EditorInstance {
} }
} }
}); });
note = note.replace(attachment.key, clonedAttachment.key);
let node = doc.querySelector(`img[data-attachment-key=${attachment.key}]`);
if (node) {
node.setAttribute('data-attachment-key', clonedAttachment.key);
} }
html += `<p></p>${note}<p></p>`; }
html += `<p></p>${doc.body.innerHTML}<p></p>`;
} }
} }
return html; return html;
@ -343,7 +426,7 @@ class EditorInstance {
} }
else if (type === 'zotero/annotation') { else if (type === 'zotero/annotation') {
let annotations = JSON.parse(data); let annotations = JSON.parse(data);
html = await this._serializeAnnotations(annotations); [html] = await this._serializeAnnotations(annotations);
} }
if (html) { if (html) {
this._postMessage({ action: 'insertHTML', pos, html }); this._postMessage({ action: 'insertHTML', pos, html });
@ -462,6 +545,7 @@ class EditorInstance {
availableCitationItems.push({ ...citationItem, id: item.id }); availableCitationItems.push({ ...citationItem, id: item.id });
} }
} }
// Notice: Citation items that don't exist in the library aren't shown in the popup
citation.citationItems = availableCitationItems; citation.citationItems = availableCitationItems;
let libraryID = this._item.libraryID; let libraryID = this._item.libraryID;
this._openQuickFormatDialog(nodeID, citation, [libraryID]); this._openQuickFormatDialog(nodeID, citation, [libraryID]);
@ -857,6 +941,7 @@ class EditorInstance {
delete citationItem.id; delete citationItem.id;
citationItem.uris = [Zotero.URI.getItemURI(item)]; citationItem.uris = [Zotero.URI.getItemURI(item)];
citationItem.itemData = Zotero.Cite.System.prototype.retrieveItem(item); citationItem.itemData = Zotero.Cite.System.prototype.retrieveItem(item);
delete citationItem.itemData.abstract;
} }
let formattedCitation = (await that._getFormattedCitationParts(citation)).join(';'); let formattedCitation = (await that._getFormattedCitationParts(citation)).join(';');
@ -1019,8 +1104,12 @@ class EditorInstance {
jsonAnnotations.push(jsonAnnotation); jsonAnnotations.push(jsonAnnotation);
} }
let html = `<h1>${Zotero.getString('note.annotationsWithDate', new Date().toLocaleString())}</h1>\n`; let html = `<h1>${Zotero.getString('note.annotationsWithDate', new Date().toLocaleString())}</h1>\n`;
html += await editorInstance._serializeAnnotations(jsonAnnotations); let [serializedHTML, storedCitationItems] = await editorInstance._serializeAnnotations(jsonAnnotations, true);
html = `<div data-schema-version="${SCHEMA_VERSION}">${html}</div>`;
html += serializedHTML;
storedCitationItems = encodeURIComponent(JSON.stringify(storedCitationItems));
html = `<div data-citation-items="${storedCitationItems}" data-schema-version="${SCHEMA_VERSION}">${html}</div>`;
note.setNote(html); note.setNote(html);
await note.saveTx(); await note.saveTx();
return note; return note;

@ -1 +1 @@
Subproject commit f649d1a4e6f74715a8b57ac298c058a07d8ea4e0 Subproject commit c5dfc982c09915e5a2fb6103edf0395dc17ce728

@ -1 +1 @@
Subproject commit 310698f3a7ebadad8557db1f11e8aa5decaae432 Subproject commit 047f753389e30e38e967cd48828ab8ea0441575c