diff --git a/chrome/content/zotero/xpcom/editorInstance.js b/chrome/content/zotero/xpcom/editorInstance.js index e9965017fd..1ae1854251 100644 --- a/chrome/content/zotero/xpcom/editorInstance.js +++ b/chrome/content/zotero/xpcom/editorInstance.js @@ -178,7 +178,8 @@ class EditorInstance { async insertAnnotations(annotations) { await this._ensureNoteCreated(); - let { html } = await this._serializeAnnotations(annotations); + await this.importImages(annotations); + let { html } = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations); if (html) { this._postMessage({ action: 'insertHTML', pos: -1, html }); } @@ -235,198 +236,13 @@ class EditorInstance { } } - /** - * Transform plain text, containing some supported HTML tags, into actual HTML. - * A similar code is also used in pdf-reader mini editor for annotation text and comments. - * It basically creates a text node and then parses and wraps specific parts - * of it into supported HTML tags - * - * @param text Plain text flavored with some HTML tags - * @returns {string} HTML - * @private - */ - _transformTextToHTML(text) { - const supportedFormats = ['i', 'b', 'sub', 'sup']; - - function getFormatter(str) { - let results = supportedFormats.map(format => str.toLowerCase().indexOf('<' + format + '>')); - results = results.map((offset, idx) => [supportedFormats[idx], offset]); - results.sort((a, b) => a[1] - b[1]); - for (let result of results) { - let format = result[0]; - let offset = result[1]; - if (offset < 0) continue; - let lastIndex = str.toLowerCase().indexOf('', offset); - if (lastIndex >= 0) { - let parts = []; - parts.push(str.slice(0, offset)); - parts.push(str.slice(offset + format.length + 2, lastIndex)); - parts.push(str.slice(lastIndex + format.length + 3)); - return { - format, - parts - }; - } - } - return null; - } - - function walkFormat(parent) { - let child = parent.firstChild; - while (child) { - if (child.nodeType === 3) { - let text = child.nodeValue; - let formatter = getFormatter(text); - if (formatter) { - let nodes = []; - nodes.push(doc.createTextNode(formatter.parts[0])); - let midNode = doc.createElement(formatter.format); - midNode.appendChild(doc.createTextNode(formatter.parts[1])); - nodes.push(midNode); - nodes.push(doc.createTextNode(formatter.parts[2])); - child.replaceWith(...nodes); - child = midNode; - } - } - walkFormat(child); - child = child.nextSibling; - } - } - - let parser = Components.classes['@mozilla.org/xmlextras/domparser;1'] - .createInstance(Components.interfaces.nsIDOMParser); - let doc = parser.parseFromString('', 'text/html'); - - // innerText transforms \n into
- doc.body.innerText = text; - walkFormat(doc.body); - return doc.body.innerHTML; - } - - /** - * @param {Object[]} annotations JSON annotations - * @param {Boolean} skipEmbeddingItemData Do not add itemData to citation items - * @return {Object} Object with `html` string and `citationItems` array to embed into metadata container - */ - async _serializeAnnotations(annotations, skipEmbeddingItemData) { - let storedCitationItems = []; - let html = ''; + async importImages(annotations) { for (let annotation of annotations) { - let attachmentItem = await Zotero.Items.getAsync(annotation.attachmentItemID); - if (!attachmentItem) { - continue; - } - - if (!annotation.text - && !annotation.comment - && !annotation.image) { - continue; - } - - let citationHTML = ''; - let imageHTML = ''; - let highlightHTML = ''; - let quotedHighlightHTML = ''; - let commentHTML = ''; - - let storedAnnotation = { - attachmentURI: Zotero.URI.getItemURI(attachmentItem), - annotationKey: annotation.id, - color: annotation.color, - pageLabel: annotation.pageLabel, - position: annotation.position - }; - - // Citation - let parentItem = attachmentItem.parentID && await Zotero.Items.getAsync(attachmentItem.parentID); - if (parentItem) { - let uris = [Zotero.URI.getItemURI(parentItem)]; - let citationItem = { - 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 = this._formatCitation(citationWithData); - citationHTML = `${formatted}`; - } - - // Image if (annotation.image && !this._filesReadOnly) { - // We assume that annotation.image is always PNG - 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 = ``; + annotation.imageAttachmentKey = await this._importImage(annotation.image); } - - // Text - if (annotation.text) { - let text = this._transformTextToHTML(annotation.text.trim()); - highlightHTML = `${text}`; - quotedHighlightHTML = `${Zotero.getString('punctuation.openingQMark')}${text}${Zotero.getString('punctuation.closingQMark')}`; - } - - // Note - if (annotation.comment) { - commentHTML = this._transformTextToHTML(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 = '

{{image}}
{{citation}} {{comment}}

'; - } - - let vars = { - color: annotation.color, - highlight: (attrs) => attrs.quotes === 'true' ? quotedHighlightHTML : highlightHTML, - 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; + delete annotation.image; } - return { html, citationItems: storedCitationItems }; } async _digestItems(ids) { @@ -450,7 +266,7 @@ class EditorInstance { }], properties: {} }; - let formatted = this._formatCitation(citation); + let formatted = Zotero.EditorInstanceUtilities.formatCitation(citation); html += `

${formatted}

`; } else if (item.isNote()) { @@ -576,7 +392,8 @@ class EditorInstance { } else if (type === 'zotero/annotation') { let annotations = JSON.parse(data); - let { html: serializedHTML } = await this._serializeAnnotations(annotations); + await this.importImages(annotations); + let { html: serializedHTML } = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations); html = serializedHTML; } if (html) { @@ -1077,86 +894,6 @@ class EditorInstance { } } - /** - * Build citation item preview string (based on _buildBubbleString in quickFormat.js) - * TODO: Try to avoid duplicating this code here and inside note-editor - */ - _formatCitationItemPreview(citationItem) { - const STARTSWITH_ROMANESQUE_REGEXP = /^[&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]/; - const ENDSWITH_ROMANESQUE_REGEXP = /[.;:&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]$/; - - let { itemData } = citationItem; - let str = ''; - - // Authors - let authors = itemData.author; - if (authors) { - if (authors.length === 1) { - str = authors[0].family || authors[0].literal; - } - else if (authors.length === 2) { - let a = authors[0].family || authors[0].literal; - let b = authors[1].family || authors[1].literal; - str = a + ' ' + Zotero.getString('general.and') + ' ' + b; - } - else if (authors.length >= 3) { - str = (authors[0].family || authors[0].literal) + ' ' + Zotero.getString('general.etAl'); - } - } - - // Title - if (!str && itemData.title) { - str = `“${itemData.title}”`; - } - - // Date - if (itemData.issued - && itemData.issued['date-parts'] - && itemData.issued['date-parts'][0]) { - let year = itemData.issued['date-parts'][0][0]; - if (year && year != '0000') { - str += ', ' + year; - } - } - - // Locator - if (citationItem.locator) { - if (citationItem.label) { - // TODO: Localize and use short forms - var label = citationItem.label; - } - else if (/[\-–,]/.test(citationItem.locator)) { - var label = 'pp.'; - } - else { - var label = 'p.'; - } - - str += ', ' + label + ' ' + citationItem.locator; - } - - // Prefix - if (citationItem.prefix && ENDSWITH_ROMANESQUE_REGEXP) { - str = citationItem.prefix - + (ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? ' ' : '') - + str; - } - - // Suffix - if (citationItem.suffix && STARTSWITH_ROMANESQUE_REGEXP) { - str += (STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? ' ' : '') - + citationItem.suffix; - } - - return str; - } - - _formatCitation(citation) { - return '(' + citation.citationItems.map((x) => { - return `${this._formatCitationItemPreview(x)}`; - }).join('; ') + ')'; - } - _arrayBufferToBase64(buffer) { var binary = ''; var bytes = new Uint8Array(buffer); @@ -1494,7 +1231,8 @@ class EditorInstance { // New line is needed for note title parser html += '\n'; - let { html: serializedHTML, citationItems } = await editorInstance._serializeAnnotations(jsonAnnotations, true); + await editorInstance.importImages(jsonAnnotations); + let { html: serializedHTML, citationItems } = Zotero.EditorInstanceUtilities.serializeAnnotations(jsonAnnotations, true); html += serializedHTML; citationItems = encodeURIComponent(JSON.stringify(citationItems)); html = `
${html}
`; @@ -1504,5 +1242,283 @@ class EditorInstance { } } +class EditorInstanceUtilities { + /** + * Serialize annotations into HTML + * + * @param {Object[]} annotations JSON annotations + * @param {Boolean} skipEmbeddingItemData Do not add itemData to citation items + * @return {Object} Object with `html` string and `citationItems` array to embed into metadata container + */ + serializeAnnotations(annotations, skipEmbeddingItemData) { + 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) { + continue; + } + + let citationHTML = ''; + let imageHTML = ''; + let highlightHTML = ''; + let quotedHighlightHTML = ''; + let commentHTML = ''; + + let storedAnnotation = { + attachmentURI: Zotero.URI.getItemURI(attachmentItem), + annotationKey: annotation.id, + color: annotation.color, + pageLabel: annotation.pageLabel, + position: annotation.position + }; + + // Citation + let parentItem = attachmentItem.parentID && Zotero.Items.get(attachmentItem.parentID); + if (parentItem) { + let uris = [Zotero.URI.getItemURI(parentItem)]; + let citationItem = { + 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 = `${formatted}`; + } + + // 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 = ``; + } + + // Text + if (annotation.text) { + let text = this._transformTextToHTML(annotation.text.trim()); + highlightHTML = `${text}`; + quotedHighlightHTML = `${Zotero.getString('punctuation.openingQMark')}${text}${Zotero.getString('punctuation.closingQMark')}`; + } + + // Note + if (annotation.comment) { + commentHTML = this._transformTextToHTML(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 = '

{{image}}
{{citation}} {{comment}}

'; + } + + let vars = { + color: annotation.color, + highlight: (attrs) => attrs.quotes === 'true' ? quotedHighlightHTML : highlightHTML, + 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; + } + return { html, citationItems: storedCitationItems }; + } + + /** + * Transform plain text, containing some supported HTML tags, into actual HTML. + * A similar code is also used in pdf-reader mini editor for annotation text and comments. + * It basically creates a text node and then parses and wraps specific parts + * of it into supported HTML tags + * + * @param text Plain text flavored with some HTML tags + * @returns {string} HTML + * @private + */ + _transformTextToHTML(text) { + const supportedFormats = ['i', 'b', 'sub', 'sup']; + + function getFormatter(str) { + let results = supportedFormats.map(format => str.toLowerCase().indexOf('<' + format + '>')); + results = results.map((offset, idx) => [supportedFormats[idx], offset]); + results.sort((a, b) => a[1] - b[1]); + for (let result of results) { + let format = result[0]; + let offset = result[1]; + if (offset < 0) continue; + let lastIndex = str.toLowerCase().indexOf('', offset); + if (lastIndex >= 0) { + let parts = []; + parts.push(str.slice(0, offset)); + parts.push(str.slice(offset + format.length + 2, lastIndex)); + parts.push(str.slice(lastIndex + format.length + 3)); + return { + format, + parts + }; + } + } + return null; + } + + function walkFormat(parent) { + let child = parent.firstChild; + while (child) { + if (child.nodeType === 3) { + let text = child.nodeValue; + let formatter = getFormatter(text); + if (formatter) { + let nodes = []; + nodes.push(doc.createTextNode(formatter.parts[0])); + let midNode = doc.createElement(formatter.format); + midNode.appendChild(doc.createTextNode(formatter.parts[1])); + nodes.push(midNode); + nodes.push(doc.createTextNode(formatter.parts[2])); + child.replaceWith(...nodes); + child = midNode; + } + } + walkFormat(child); + child = child.nextSibling; + } + } + + let parser = Components.classes['@mozilla.org/xmlextras/domparser;1'] + .createInstance(Components.interfaces.nsIDOMParser); + let doc = parser.parseFromString('', 'text/html'); + + // innerText transforms \n into
+ doc.body.innerText = text; + walkFormat(doc.body); + return doc.body.innerHTML; + } + + /** + * Build citation item preview string (based on _buildBubbleString in quickFormat.js) + * TODO: Try to avoid duplicating this code here and inside note-editor + */ + _formatCitationItemPreview(citationItem) { + const STARTSWITH_ROMANESQUE_REGEXP = /^[&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]/; + const ENDSWITH_ROMANESQUE_REGEXP = /[.;:&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]$/; + + let { itemData } = citationItem; + let str = ''; + + // Authors + let authors = itemData.author; + if (authors) { + if (authors.length === 1) { + str = authors[0].family || authors[0].literal; + } + else if (authors.length === 2) { + let a = authors[0].family || authors[0].literal; + let b = authors[1].family || authors[1].literal; + str = a + ' ' + Zotero.getString('general.and') + ' ' + b; + } + else if (authors.length >= 3) { + str = (authors[0].family || authors[0].literal) + ' ' + Zotero.getString('general.etAl'); + } + } + + // Title + if (!str && itemData.title) { + str = `“${itemData.title}”`; + } + + // Date + if (itemData.issued + && itemData.issued['date-parts'] + && itemData.issued['date-parts'][0]) { + let year = itemData.issued['date-parts'][0][0]; + if (year && year != '0000') { + str += ', ' + year; + } + } + + // Locator + if (citationItem.locator) { + if (citationItem.label) { + // TODO: Localize and use short forms + var label = citationItem.label; + } + else if (/[\-–,]/.test(citationItem.locator)) { + var label = 'pp.'; + } + else { + var label = 'p.'; + } + + str += ', ' + label + ' ' + citationItem.locator; + } + + // Prefix + if (citationItem.prefix && ENDSWITH_ROMANESQUE_REGEXP) { + str = citationItem.prefix + + (ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? ' ' : '') + + str; + } + + // Suffix + if (citationItem.suffix && STARTSWITH_ROMANESQUE_REGEXP) { + str += (STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? ' ' : '') + + citationItem.suffix; + } + + return str; + } + + formatCitation(citation) { + return '(' + citation.citationItems.map((x) => { + return `${this._formatCitationItemPreview(x)}`; + }).join('; ') + ')'; + } +} + Zotero.EditorInstance = EditorInstance; Zotero.EditorInstance.SCHEMA_VERSION = SCHEMA_VERSION; +Zotero.EditorInstanceUtilities = new EditorInstanceUtilities(); diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 320c9325b0..6904345e00 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -838,6 +838,60 @@ class ReaderTab extends ReaderInstance { Zotero.logError(event.error); }); + this._iframeWindow.wrappedJSObject.zoteroSetDataTransferAnnotations = (dataTransfer, annotations) => { + let res = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations); + let tmpNote = new Zotero.Item('note'); + tmpNote.libraryID = Zotero.Libraries.userLibraryID; + tmpNote.setNote(res.html); + let items = [tmpNote]; + let format = Zotero.QuickCopy.getNoteFormat(); + Zotero.debug('Copying/dragging annotation(s) with ' + format); + format = Zotero.QuickCopy.unserializeSetting(format); + // Basically the same code is used in itemTree.jsx onDragStart + try { + if (format.mode === 'export') { + // If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown + // and Note HTML translators instead + if (format.id === Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) { + let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN }; + let htmlFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML }; + Zotero.QuickCopy.getContentFromItems(items, markdownFormat, (obj, worked) => { + if (!worked) { + return; + } + Zotero.QuickCopy.getContentFromItems(items, htmlFormat, (obj2, worked) => { + if (!worked) { + return; + } + dataTransfer.setData('text/plain', obj.string.replace(/\r\n/g, '\n')); + dataTransfer.setData('text/html', obj2.string.replace(/\r\n/g, '\n')); + }); + }); + } + else { + Zotero.QuickCopy.getContentFromItems(items, format, (obj, worked) => { + if (!worked) { + return; + } + var text = obj.string.replace(/\r\n/g, '\n'); + // For Note HTML translator use body content only + if (format.id === Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) { + // Use body content only + let parser = Cc['@mozilla.org/xmlextras/domparser;1'] + .createInstance(Ci.nsIDOMParser); + let doc = parser.parseFromString(text, 'text/html'); + text = doc.body.innerHTML; + } + dataTransfer.setData('text/plain', text); + }); + } + } + } + catch (e) { + Zotero.debug(e); + } + }; + this._iframeWindow.wrappedJSObject.zoteroConfirmDeletion = function (plural) { let ps = Services.prompt; let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING diff --git a/pdf-reader b/pdf-reader index 826cce2fa0..b97d62e02b 160000 --- a/pdf-reader +++ b/pdf-reader @@ -1 +1 @@ -Subproject commit 826cce2fa0c754b9de456aa2d52f9bfc95dfd3ba +Subproject commit b97d62e02bdba96ef95a74dcefeab23ce2520eae