diff --git a/chrome/content/zotero/integration/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js index fd3bffbab8..4220a5b086 100644 --- a/chrome/content/zotero/integration/quickFormat.js +++ b/chrome/content/zotero/integration/quickFormat.js @@ -311,12 +311,16 @@ var Zotero_QuickFormat = new function () { // Exclude feeds Zotero.Feeds.getAll() .forEach(feed => s.addCondition("libraryID", "isNot", feed.libraryID)); - s.addCondition("quicksearch-titleCreatorYear", "contains", str); - s.addCondition("itemType", "isNot", "attachment"); - if (io.filterLibraryIDs) { - io.filterLibraryIDs.forEach(id => s.addCondition("libraryID", "is", id)); + if (io.allowCitingNotes) { + s.addCondition("quicksearch-titleCreatorYearNote", "contains", str); + } + else { + s.addCondition("quicksearch-titleCreatorYear", "contains", str); + s.addCondition("itemType", "isNot", "attachment"); + if (io.filterLibraryIDs) { + io.filterLibraryIDs.forEach(id => s.addCondition("libraryID", "is", id)); + } } - haveConditions = true; } } @@ -671,8 +675,12 @@ var Zotero_QuickFormat = new function () { var str = item.getField("firstCreator"); // Title, if no creator (getDisplayTitle in order to get case, e-mail, statute which don't have a title field) - if(!str) { - str = Zotero.getString("punctuation.openingQMark") + item.getDisplayTitle() + Zotero.getString("punctuation.closingQMark"); + title = item.getDisplayTitle(); + if (item.isNote()) { + title = title.substr(0, 24) + '…'; + } + if (!str) { + str = Zotero.getString("punctuation.openingQMark") + title + Zotero.getString("punctuation.closingQMark"); } // Date @@ -754,10 +762,27 @@ var Zotero_QuickFormat = new function () { */ var _bubbleizeSelected = Zotero.Promise.coroutine(function* () { if(!referenceBox.hasChildNodes() || !referenceBox.selectedItem) return false; - + + var ps = Services.prompt; var citationItem = {"id":referenceBox.selectedItem.getAttribute("zotero-item")}; + var item = Zotero.Cite.getItem(citationItem.id); + var nodes = Array.from(qfe.childNodes).filter(node => node.tagName == 'span'); + if (nodes[0] && nodes[0].dataset && JSON.parse(nodes[0].dataset.citationItem).isNote) { + ps.alert(null, + Zotero.getString('general.warning'), + Zotero.getString('integration.cannotInsertItemWithNote')); + return false; + } + if (item && item.isNote() && nodes.length) { + if (!ps.confirm(null, + Zotero.getString('general.warning'), + Zotero.getString('integration.noteCiteItemsRemoved'))) { + return false; + } + _clearCitation(); + citationItem.isNote = true; + } if (typeof citationItem.id === "string" && citationItem.id.indexOf("/") !== -1) { - var item = Zotero.Cite.getItem(citationItem.id); citationItem.uris = item.cslURIs; citationItem.itemData = item.cslItemData; } diff --git a/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js b/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js index 83e5725df2..080aac5a05 100644 --- a/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js +++ b/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js @@ -60,6 +60,7 @@ Zotero.HTTPIntegrationClient.Application = function() { this.outputFormat = 'html'; this.supportedNotes = ['footnotes']; this.supportsImportExport = false; + this.supportsTextInsertion = false; this.processorName = "HTTP Integration"; }; Zotero.HTTPIntegrationClient.Application.prototype = { @@ -68,6 +69,7 @@ Zotero.HTTPIntegrationClient.Application.prototype = { this.outputFormat = result.outputFormat || this.outputFormat; this.supportedNotes = result.supportedNotes || this.supportedNotes; this.supportsImportExport = result.supportsImportExport || this.supportsImportExport; + this.supportsTextInsertion = result.supportsTextInsertion || this.supportsTextInsertion; this.processorName = result.processorName || this.processorName; return new Zotero.HTTPIntegrationClient.Document(result.documentID); } @@ -80,7 +82,8 @@ Zotero.HTTPIntegrationClient.Document = function(documentID) { this._documentID = documentID; }; for (let method of ["activate", "canInsertField", "displayAlert", "getDocumentData", - "setDocumentData", "setBibliographyStyle", "importDocument", "exportDocument"]) { + "setDocumentData", "setBibliographyStyle", "importDocument", "exportDocument", + "insertText"]) { Zotero.HTTPIntegrationClient.Document.prototype[method] = async function() { return Zotero.HTTPIntegrationClient.sendCommand("Document."+method, [this._documentID].concat(Array.prototype.slice.call(arguments))); @@ -146,6 +149,10 @@ Zotero.HTTPIntegrationClient.Document.prototype.convert = async function(fields, fields = fields.map((f) => f._id); await Zotero.HTTPIntegrationClient.sendCommand("Document.convert", [this._documentID, fields, fieldType, noteTypes]); }; +Zotero.HTTPIntegrationClient.Document.prototype.convertPlaceholdersToFields = async function(codes, placeholderIDs, noteType) { + var retVal = await Zotero.HTTPIntegrationClient.sendCommand("Document.convertPlaceholdersToFields", [this._documentID, codes, placeholderIDs, noteType]); + return retVal.map(field => new Zotero.HTTPIntegrationClient.Field(this._documentID, field)); +} Zotero.HTTPIntegrationClient.Document.prototype.complete = async function() { Zotero.HTTPIntegrationClient.inProgress = false; Zotero.HTTPIntegrationClient.sendCommand("Document.complete", [this._documentID]); diff --git a/chrome/content/zotero/xpcom/data/search.js b/chrome/content/zotero/xpcom/data/search.js index 0470b2d495..f8e2d99379 100644 --- a/chrome/content/zotero/xpcom/data/search.js +++ b/chrome/content/zotero/xpcom/data/search.js @@ -313,12 +313,16 @@ Zotero.Search.prototype.addCondition = function (condition, operator, value, req this.addCondition('key', 'is', part.text, false); } - if (condition == 'quicksearch-titleCreatorYear') { + if (condition.startsWith('quicksearch-titleCreatorYear')) { this.addCondition('title', operator, part.text, false); this.addCondition('publicationTitle', operator, part.text, false); this.addCondition('shortTitle', operator, part.text, false); this.addCondition('court', operator, part.text, false); this.addCondition('year', operator, part.text, false); + + if (condition == 'quicksearch-titleCreatorYearNote') { + this.addCondition('note', operator, part.text, false); + } } else { this.addCondition('field', operator, part.text, false); diff --git a/chrome/content/zotero/xpcom/data/searchConditions.js b/chrome/content/zotero/xpcom/data/searchConditions.js index 11fd2c53a3..e24b369913 100644 --- a/chrome/content/zotero/xpcom/data/searchConditions.js +++ b/chrome/content/zotero/xpcom/data/searchConditions.js @@ -168,6 +168,17 @@ Zotero.SearchConditions = new function(){ noLoad: true }, + { + name: 'quicksearch-titleCreatorYearNote', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + { name: 'quicksearch-fields', operators: { diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 51ba061bf2..dd601f379e 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -61,6 +61,8 @@ const DELAYED_CITATION_HTML_STYLING_END = "" const EXPORTED_DOCUMENT_MARKER = "ZOTERO_TRANSFER_DOCUMENT"; +const NOTE_CITATION_PLACEHOLDER_LINK = 'https://www.zotero.org/?'; + Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); @@ -584,10 +586,14 @@ Zotero.Integration.Interface.prototype.addEditCitation = async function (docFiel await this._session.init(false, false); docField = docField || await this._doc.cursorInField(this._session.data.prefs['fieldType']); - let [idx, field, citation] = await this._session.cite(docField); - await this._session.addCitation(idx, await field.getNoteIndex(), citation); + let citations = await this._session.cite(docField); + for (let citation of citations) { + await this._session.addCitation(citation._fieldIndex, await citation._field.getNoteIndex(), citation); + } if (this._session.data.prefs.delayCitationUpdates) { - return this._session.writeDelayedCitation(field, citation); + for (let citation of citations) { + await this._session.writeDelayedCitation(citation._field, citation); + } } else { return this._session.updateDocument(FORCE_CITATIONS_FALSE, false, false); } @@ -843,7 +849,7 @@ Zotero.Integration.Session = function(doc, app) { * Checks that it is appropriate to add fields to the current document at the current * position, then adds one. */ -Zotero.Integration.Session.prototype.addField = async function(note) { +Zotero.Integration.Session.prototype.addField = async function(note, fieldIndex=-1) { // Get citation types if necessary if (!await this._doc.canInsertField(this.data.prefs['fieldType'])) { return Zotero.Promise.reject(new Zotero.Exception.Alert("integration.error.cannotInsertHere", @@ -867,12 +873,17 @@ Zotero.Integration.Session.prototype.addField = async function(note) { field.setCode('TEMP'); } // If fields already retrieved, further this.getFields() calls will returned the cached version - // So we append this field to that list + // So add this field to the cache if (this._fields) { - this._fields.push(field); + if (fieldIndex == -1) { + this._fields.push(field); + } + else { + this._fields.splice(fieldIndex, 0, field); + } } - return Zotero.Promise.resolve(field); + return field; } /** @@ -1241,9 +1252,9 @@ Zotero.Integration.Session.prototype._updateDocument = async function(forceCitat await this._fields[removeCodeFields[i]].removeCode(); } - var deleteFields = Object.keys(this._deleteFields).sort(); - for (var i=(deleteFields.length-1); i>=0; i--) { - this._fields[deleteFields[i]].delete(); + var deleteFields = Object.keys(this._deleteFields).sort((a, b) => b - a); + for (let fieldIndex of deleteFields) { + this._fields[fieldIndex].delete(); } this.processIndices = {} } @@ -1328,7 +1339,8 @@ Zotero.Integration.Session.prototype.cite = async function (field) { var io = new Zotero.Integration.CitationEditInterface( citation, this.style.opt.sort_citations, - fieldIndexPromise, citationsByItemIDPromise, previewFn + fieldIndexPromise, citationsByItemIDPromise, previewFn, + this._app.supportsTextInsertion ); Zotero.debug(`Editing citation:`); Zotero.debug(JSON.stringify(citation.toJSON())); @@ -1360,18 +1372,127 @@ Zotero.Integration.Session.prototype.cite = async function (field) { var fieldIndex = await fieldIndexPromise; // Make sure session is updated await citationsByItemIDPromise; - return [fieldIndex, field, io.citation]; + + let citations = await this._insertCitingResult(fieldIndex, field, io.citation); + // We need to re-update from document because we've inserted multiple fields. + // Don't worry, the field list and info is cached so this triggers no calls to the doc. + await this.updateFromDocument(FORCE_CITATIONS_FALSE); + for (let citation of citations) { + await this.addCitation(citation._fieldIndex, await citation._field.getNoteIndex(), citation); + } + return citations; +}; + +/** + * Inserts a citing result, where a citing result is either multiple Items or a Note item. + * Notes may contain Items in them, which means that + * a single citing result insert can produce multiple Citations. + * + * Returns an array of Citation objects which correspond to inserted citations. At least 1 Citation + * is always returned. + * + * @param fieldIndex + * @param field + * @param citation + * @returns {Promise<[]>} + * @private + */ +Zotero.Integration.Session.prototype._insertCitingResult = async function (fieldIndex, field, citation) { + await citation.loadItemData(); + + let firstItem = Zotero.Cite.getItem(citation.citationItems[0].id); + if (firstItem && firstItem.isNote()) { + return this._insertNoteIntoDocument(fieldIndex, field, firstItem); + } + else { + return [await this._insertItemsIntoDocument(fieldIndex++, field, citation)]; + } +}; + +/** + * Splits out cited items from the note text and converts them to placeholder links. + * + * Returns the modified note text and an array of citation objects and their corresponding + * placeholder IDs + * @param item {Zotero.Item} + */ +Zotero.Integration.Session.prototype._processNote = function (item) { + let text = item.getNote(); + let parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Components.interfaces.nsIDOMParser); + let doc = parser.parseFromString(text, "text/html"); + let citationsElems = doc.querySelectorAll('.citation[data-citation]'); + let citations = []; + let placeholderIDs = []; + for (let citationElem of citationsElems) { + try { + // Add the citation code object to citations array + let citation = JSON.parse(decodeURIComponent(citationElem.dataset.citation)); + delete citation.properties; + citations.push(citation); + let placeholderID = Zotero.Utilities.randomString(6); + // Add the placeholder we'll be using for the link to placeholder array + placeholderIDs.push(placeholderID); + let placeholderURL = NOTE_CITATION_PLACEHOLDER_LINK + placeholderID; + // Split out the citation element and replace with a placeholder link + text = text.split(citationElem.outerHTML) + .join(`${citationElem.textContent}`); + } + catch (e) { + e.message = `Failed to parse a citation from a note: ${decodeURIComponent(citationElem.dataset.citation)}`; + Zotero.debug(e, 1); + Zotero.logError(e); + } + } + // TODO: Later we'll need to convert note HTML to RDF. + // if (Zotero.Integration.currentSession._app.outputFormat == 'rtf') { + // text = return Zotero.RTFConverter.HTMLToRTF(text); + // }); + // } + return [text, citations, placeholderIDs]; +}; + +Zotero.Integration.Session.prototype._insertNoteIntoDocument = async function (fieldIndex, field, noteItem) { + let [text, citations, placeholderIDs] = this._processNote(noteItem); + await field.delete(); + await this._doc.insertText(text); + if (!citations.length) return []; + + // Do these in reverse order to ensure we don't get messy document edits + citations.reverse(); + placeholderIDs.reverse(); + let fields = await this._doc.convertPlaceholdersToFields(citations.map(() => 'TEMP'), + placeholderIDs, this.data.prefs.noteType); + + let insertedCitations = await Promise.all(fields.map(async (field, index) => { + let citation = new Zotero.Integration.Citation(new Zotero.Integration.CitationField(field, 'TEMP'), + citations[index]); + citation._fieldIndex = fieldIndex + fields.length - 1 - index; + return citation; + })); + return insertedCitations; +}; + +Zotero.Integration.Session.prototype._insertItemsIntoDocument = async function (fieldIndex, field, citation) { + if (!field) { + field = new Zotero.Integration.CitationField(await this.addField(true, fieldIndex)); + } + citation._field = field; + citation._fieldIndex = fieldIndex; + return citation; }; /** * Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js */ -Zotero.Integration.CitationEditInterface = function(citation, sortable, fieldIndexPromise, citationsByItemIDPromise, previewFn) { - this.citation = citation; +Zotero.Integration.CitationEditInterface = function(items, sortable, fieldIndexPromise, + citationsByItemIDPromise, previewFn, allowCitingNotes=false){ + this.citation = items; this.sortable = sortable; this.previewFn = previewFn; this._fieldIndexPromise = fieldIndexPromise; this._citationsByItemIDPromise = citationsByItemIDPromise; + this.allowCitingNotes = allowCitingNotes; // Not available in quickFormat.js if this unspecified this.wrappedJSObject = this; @@ -1717,10 +1838,10 @@ Zotero.Integration.Session.prototype.importDocument = async function() { /** * Adds a citation to the arrays representing the document */ -Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, citation) { - var index = parseInt(index, 10); +Zotero.Integration.Session.prototype.addCitation = async function (index, noteIndex, citation) { + index = parseInt(index, 10); - var action = yield citation.loadItemData(); + var action = await citation.loadItemData(); if (action == Zotero.Integration.REMOVE_CODE) { // Mark for removal and return @@ -1784,7 +1905,7 @@ Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(func } Zotero.debug("Integration: Adding citationID "+citation.citationID); this.documentCitationIDs[citation.citationID] = index; -}); +}; Zotero.Integration.Session.prototype.getCiteprocLists = function() { var citations = []; @@ -2340,7 +2461,7 @@ Zotero.Integration.Field = class { } this._field = field; this._code = rawCode; - this.type = INTEGRATION_TYPE_TEMP; + this.type = INTEGRATION_TYPE_TEMP; } async setCode(code) { @@ -2369,8 +2490,17 @@ Zotero.Integration.Field = class { async clearCode() { return await this.setCode('{}'); } + + async getText() { + if (this._text) { + return this._text; + } + this._text = await this._field.getText(); + return this._text; + } async setText(text) { + this._text = null; var isRich = false; // If RTF wrap with RTF tags if (Zotero.Integration.currentSession.outputFormat == "rtf" && text.includes("\\")) { @@ -2606,9 +2736,7 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { Zotero.Integration.Citation = class { constructor(citationField, data, noteIndex) { - if (!data) { - data = {citationItems: [], properties: {}}; - } + data = Object.assign({ citationItems: [], properties: {} }, data) this.citationID = data.citationID; this.citationItems = data.citationItems; this.properties = data.properties; @@ -2626,102 +2754,100 @@ Zotero.Integration.Citation = class { * - Zotero.Integration.REMOVE_CODE * - Zotero.Integration.DELETE */ - loadItemData() { - return Zotero.Promise.coroutine(function *(promptToReselect=true){ - let items = []; - var needUpdate = false; + async loadItemData(promptToReselect=true) { + let items = []; + var needUpdate = false; + + if (!this.citationItems.length) { + return Zotero.Integration.DELETE; + } + for (var i=0, n=this.citationItems.length; i { + let field = new DocumentPluginDummy.Field(this); + field.code = code; + this.fields.push(field); + return field; + }); + }, /** * Gets all fields present in the document. * @param {String} fieldType * @returns {DocumentPluginDummy.Field[]} */ - getFields: function(fieldType) {return Array.from(this.fields)}, + getFields: function (fieldType) {return Array.from(this.fields)}, /** * Sets the bibliography style, overwriting the current values for this document */