From c65e8f1621ae8780fc4c98ed295b227efa39b641 Mon Sep 17 00:00:00 2001 From: Tom Najdek Date: Mon, 19 Sep 2022 14:21:49 +0200 Subject: [PATCH] fx-compat: Convert rtfScan to use CE wizards Also: * Adds Style Configurator CE * Extends "base" CE to enable fluent l10n --- chrome/content/zotero/bibliography.js | 2 +- chrome/content/zotero/elements/base.js | 5 + .../zotero/elements/styleConfigurator.js | 262 ++++++ chrome/content/zotero/rtfScan.js | 758 +++++++++++++++++ chrome/content/zotero/rtfScan.jsx | 779 ------------------ chrome/content/zotero/rtfScan.xhtml | 72 ++ chrome/content/zotero/rtfScan.xul | 95 --- chrome/content/zotero/zoteroPane.xhtml | 2 +- chrome/locale/en-US/zotero/zotero.ftl | 55 +- scss/_zotero-react-client.scss | 1 + scss/components/_rtfScan.scss | 85 ++ scss/elements/style-configurator.scss | 58 ++ scss/themes/_light.scss | 2 + 13 files changed, 1297 insertions(+), 879 deletions(-) create mode 100644 chrome/content/zotero/elements/styleConfigurator.js create mode 100644 chrome/content/zotero/rtfScan.js delete mode 100644 chrome/content/zotero/rtfScan.jsx create mode 100644 chrome/content/zotero/rtfScan.xhtml delete mode 100644 chrome/content/zotero/rtfScan.xul create mode 100644 scss/components/_rtfScan.scss create mode 100644 scss/elements/style-configurator.scss diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index 431f0e75b1..a7df980cd0 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -225,7 +225,7 @@ var Zotero_File_Interface_Bibliography = new function() { updateLocaleMenu(selectedStyleObj); // - // For integrationDocPrefs.xul and rtfScan.xul + // For integrationDocPrefs.xul and rtfScan.xhtml // if (isDocPrefs) { // update status of displayAs box based on style class diff --git a/chrome/content/zotero/elements/base.js b/chrome/content/zotero/elements/base.js index 35ed5839bc..ebe1a5049c 100644 --- a/chrome/content/zotero/elements/base.js +++ b/chrome/content/zotero/elements/base.js @@ -58,6 +58,11 @@ class XULElementBase extends XULElement { shadow.append(content); } + MozXULElement.insertFTLIfNeeded("zotero.ftl"); + if (document.l10n) { + document.l10n.connectRoot(this.shadowRoot); + } + this.init(); } diff --git a/chrome/content/zotero/elements/styleConfigurator.js b/chrome/content/zotero/elements/styleConfigurator.js new file mode 100644 index 0000000000..3ac1e52b3a --- /dev/null +++ b/chrome/content/zotero/elements/styleConfigurator.js @@ -0,0 +1,262 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2022 Corporation for Digital Scholarship + Vienna, Virginia, USA + https://www.zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +/* global XULElementBase: false */ + +{ + Services.scriptloader.loadSubScript("chrome://zotero/content/elements/base.js", this); + + class StyleSelector extends XULElementBase { + get stylesheets() { + return [ + 'chrome://global/skin/global.css', + 'chrome://zotero/skin/elements/style-configurator.css' + ]; + } + + content = MozXULElement.parseXULToFragment(` +
+
+ +
+
+ `); + + set value(val) { + this.shadowRoot.getElementById('style-list').value = val; + } + + get value() { + return this.shadowRoot.getElementById('style-list').value; + } + + async init() { + await Zotero.Styles.init(); + const styleListEl = this.shadowRoot.getElementById('style-list'); + + Zotero.Styles.getVisible().forEach((so) => { + const value = so.styleID; + // Add acronyms to APA and ASA to avoid confusion + // https://forums.zotero.org/discussion/comment/357135/#Comment_357135 + const label = so.title + .replace(/^American Psychological Association/, "American Psychological Association (APA)") + .replace(/^American Sociological Association/, "American Sociological Association (ASA)"); + + styleListEl.appendChild(MozXULElement.parseXULToFragment(` + ${label} + `)); + }); + this.value = this.getAttribute('value'); + this.shadowRoot.getElementById('style-list').addEventListener("select", () => { + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + } + } + + class LocaleSelector extends XULElementBase { + content = MozXULElement.parseXULToFragment(` +
+
+ + + +
+
+ `); + + get stylesheets() { + return [ + 'chrome://global/skin/global.css', + 'chrome://zotero/skin/elements/style-configurator.css' + ]; + } + + get value() { + return this.localeListEl.value; + } + + set value(val) { + this._value = val; + const styleData = this._style ? Zotero.Styles.get(this._style) : null; + this.localeListEl.value = styleData && styleData.locale || this._value; + } + + get style() { + return this.style; + } + + set style(style) { + this._style = style; + const styleData = style ? Zotero.Styles.get(style) : null; + this.localeListEl.disabled = !style || !!styleData.locale; + this.localeListEl.value = styleData && styleData.locale || this._value || this.fallbackLocale; + } + + connectedCallback() { + super.connectedCallback(); + this.localeListEl = this.shadowRoot.getElementById('locale-list'); + this.localePopupEl = this.shadowRoot.querySelector('#locale-list > menupopup'); + } + + async init() { + this._style = this.getAttribute('style'); + this._value = this.getAttribute('value'); + + await Zotero.Styles.init(); + this.fallbackLocale = Zotero.Styles?.primaryDialects[Zotero.locale] || Zotero.locale; + + const menuLocales = Zotero.Utilities.deepCopy(Zotero.Styles.locales); + const menuLocalesKeys = Object.keys(menuLocales).sort(); + + // Make sure that client locale is always available as a choice + if (this.fallbackLocale && !(this.fallbackLocale in menuLocales)) { + menuLocales[this.fallbackLocale] = this.fallbackLocale; + menuLocalesKeys.unshift(this.fallbackLocale); + } + + menuLocalesKeys.forEach((key) => { + const label = menuLocales[key]; + + this.localePopupEl.appendChild(MozXULElement.parseXULToFragment(` + + `)); + }); + + this.value = this._value; + this.style = this._style; + + this.localeListEl.addEventListener("command", (_event) => { + this._value = this.localeListEl.value; + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + } + } + + class StyleConfigurator extends XULElementBase { + content = MozXULElement.parseXULToFragment(` +
+
+ `); + + get stylesheets() { + return [ + 'chrome://global/skin/global.css', + 'chrome://zotero/skin/elements/style-configurator.css' + ]; + } + + set style(val) { + this.shadowRoot.getElementById('style-selector').value = val; + this.handleStyleChanged(val); + } + + get style() { + return this.shadowRoot.getElementById('style-selector').value; + } + + set locale(val) { + this.shadowRoot.getElementById('locale-selector').value = val; + } + + get locale() { + return this.shadowRoot.getElementById('locale-selector').value; + } + + set displayAs(val) { + this.shadowRoot.getElementById('display-as').value = val; + } + + get displayAs() { + return this.shadowRoot.getElementById('display-as').value; + } + + async init() { + this.shadowRoot.getElementById('style-configurator').style.display = 'none'; + await Zotero.Styles.init(); + this.shadowRoot.getElementById('style-configurator').style.display = ''; + this.shadowRoot.getElementById('style-selector').addEventListener('select', (_event) => { + this.handleStyleChanged(_event.target.value); + + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + + this.shadowRoot.getElementById('locale-selector').addEventListener('select', (_event) => { + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + + this.shadowRoot.getElementById('display-as').addEventListener('select', (_event) => { + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + } + + handleStyleChanged(style) { + this.shadowRoot.getElementById('locale-selector').style = style; + const styleData = style ? Zotero.Styles.get(style) : null; + const isNoteStyle = (styleData || {}).class === 'note'; + this.shadowRoot.getElementById('display-as-wrapper').style.display = isNoteStyle ? '' : 'none'; + } + } + + customElements.define('locale-selector', LocaleSelector); + customElements.define('style-selector', StyleSelector); + customElements.define('style-configurator', StyleConfigurator); +} diff --git a/chrome/content/zotero/rtfScan.js b/chrome/content/zotero/rtfScan.js new file mode 100644 index 0000000000..793b8bcb0c --- /dev/null +++ b/chrome/content/zotero/rtfScan.js @@ -0,0 +1,758 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import FilePicker from 'zotero/modules/filePicker'; +import VirtualizedTable from 'components/virtualized-table'; +import { getDOMElement } from 'components/icons'; + +var { Services } = ChromeUtils.import('resource://gre/modules/Services.jsm'); +Services.scriptloader.loadSubScript('chrome://zotero/content/elements/styleConfigurator.js', this); + +function _generateItem(citationString, itemName, action) { + return { + rtf: citationString, + item: itemName, + action + }; +} + +function _matchesItemCreators(creators, item, etAl) { + var itemCreators = item.getCreators(); + var primaryCreators = []; + var primaryCreatorTypeID = Zotero.CreatorTypes.getPrimaryIDForType(item.itemTypeID); + + // use only primary creators if primary creators exist + for (let i = 0; i < itemCreators.length; i++) { + if (itemCreators[i].creatorTypeID == primaryCreatorTypeID) { + primaryCreators.push(itemCreators[i]); + } + } + // if primaryCreators matches the creator list length, or if et al is being used, use only + // primary creators + if (primaryCreators.length == creators.length || etAl) itemCreators = primaryCreators; + + // for us to have an exact match, either the citation creator list length has to match the + // item creator list length, or et al has to be used + if (itemCreators.length == creators.length || (etAl && itemCreators.length > creators.length)) { + var matched = true; + for (let i = 0; i < creators.length; i++) { + // check each item creator to see if it matches + matched = matched && _matchesItemCreator(creators[i], itemCreators[i]); + if (!matched) break; + } + return matched; + } + + return false; +} + +function _matchesItemCreator(creator, itemCreator) { + // make sure last name matches + var lowerLast = itemCreator.lastName.toLowerCase(); + if (lowerLast != creator.substr(-lowerLast.length).toLowerCase()) return false; + + // make sure first name matches, if it exists + if (creator.length > lowerLast.length) { + var firstName = Zotero.Utilities.trim(creator.substr(0, creator.length - lowerLast.length)); + if (firstName.length) { + // check to see whether the first name is all initials + const initialRe = /^(?:[A-Z]\.? ?)+$/; + var m = initialRe.exec(firstName); + if (m) { + var initials = firstName.replace(/[^A-Z]/g, ""); + var itemInitials = itemCreator.firstName.split(/ +/g) + .map(name => name[0].toUpperCase()) + .join(""); + if (initials != itemInitials) return false; + } + else { + // not all initials; verify that the first name matches + var firstWord = firstName.substr(0, itemCreator.firstName).toLowerCase(); + var itemFirstWord = itemCreator.firstName.substr(0, itemCreator.firstName.indexOf(" ")).toLowerCase(); + if (firstWord != itemFirstWord) return false; + } + } + } + + return true; +} + + +const columns = [ + { dataKey: 'rtf', label: "zotero.rtfScan.citation.label", primary: true, flex: 4 }, + { dataKey: 'item', label: "zotero.rtfScan.itemName.label", flex: 5 }, + { dataKey: 'action', label: "", fixedWidth: true, width: "26px" }, +]; + +const BIBLIOGRAPHY_PLACEHOLDER = "\\{Bibliography\\}"; + +const initialRows = [ + { id: 'unmapped', rtf: Zotero.Intl.strings['zotero.rtfScan.unmappedCitations.label'], collapsed: false }, + { id: 'ambiguous', rtf: Zotero.Intl.strings['zotero.rtfScan.ambiguousCitations.label'], collapsed: false }, + { id: 'mapped', rtf: Zotero.Intl.strings['zotero.rtfScan.mappedCitations.label'], collapsed: false }, +]; +Object.freeze(initialRows); + +// const initialRowMap = {}; +// initialRows.forEach((row, index) => initialRowMap[row.id] = index); +const initialRowMap = initialRows.reduce((aggr, row, index) => { + aggr[row.id] = index; + return aggr; +}, {}); +Object.freeze(initialRowMap); + + +const Zotero_RTFScan = { // eslint-disable-line no-unused-vars, camelcase + wizard: null, + inputFile: null, + outputFile: null, + contents: null, + tree: null, + styleConfig: null, + citations: null, + citationItemIDs: null, + ids: 0, + rows: [...initialRows], + rowMap: { ...initialRowMap }, + + + async init() { + this.wizard = document.getElementById('rtfscan-wizard'); + + this.wizard.getPageById('page-start') + .addEventListener('pageshow', this.onIntroShow.bind(this)); + this.wizard.getPageById('page-start') + .addEventListener('pageadvanced', this.onIntroAdvanced.bind(this)); + this.wizard.getPageById('scan-page') + .addEventListener('pageshow', this.onScanPageShow.bind(this)); + this.wizard.getPageById('style-page') + .addEventListener('pageadvanced', this.onStylePageAdvanced.bind(this)); + this.wizard.getPageById('style-page') + .addEventListener('pagerewound', this.onStylePageRewound.bind(this)); + this.wizard.getPageById('format-page') + .addEventListener('pageshow', this.onFormatPageShow.bind(this)); + this.wizard.getPageById('citations-page') + .addEventListener('pageshow', this.onCitationsPageShow.bind(this)); + this.wizard.getPageById('citations-page') + .addEventListener('pagerewound', this.onCitationsPageRewound.bind(this)); + this.wizard.getPageById('complete-page') + .addEventListener('pageshow', this.onCompletePageShow.bind(this)); + + document + .getElementById('choose-input-file') + .addEventListener('click', this.onChooseInputFile.bind(this)); + document + .getElementById('choose-output-file') + .addEventListener('click', this.onChooseOutputFile.bind(this)); + + ReactDOM.render(( + this.rows.length} + id="rtfScan-table" + ref={ref => this.tree = ref} + renderItem={this.renderItem.bind(this)} + showHeader={true} + columns={columns} + containerWidth={document.getElementById('tree').clientWidth} + disableFontSizeScaling={true} + /> + ), document.getElementById('tree')); + + const lastInputFile = Zotero.Prefs.get("rtfScan.lastInputFile"); + if (lastInputFile) { + document.getElementById('input-path').value = lastInputFile; + this.inputFile = Zotero.File.pathToFile(lastInputFile); + } + const lastOutputFile = Zotero.Prefs.get("rtfScan.lastOutputFile"); + if (lastOutputFile) { + document.getElementById('output-path').value = lastOutputFile; + this.outputFile = Zotero.File.pathToFile(lastOutputFile); + } + + // wizard.shadowRoot content isn't exposed to our css + this.wizard.shadowRoot + .querySelector('.wizard-header-label').style.fontSize = '16px'; + + this.updatePath(); + document.getElementById("choose-input-file").focus(); + }, + + async onChooseInputFile(ev) { + if (ev.type === 'keydown' && ev.key !== ' ') { + return; + } + ev.stopPropagation(); + const fp = new FilePicker(); + fp.init(window, Zotero.getString("rtfScan.openTitle"), fp.modeOpen); + fp.appendFilters(fp.filterAll); + fp.appendFilter(Zotero.getString("rtfScan.rtf"), "*.rtf"); + const rv = await fp.show(); + + if (rv == fp.returnOK || rv == fp.returnReplace) { + this.inputFile = Zotero.File.pathToFile(fp.file); + this.updatePath(); + } + }, + + async onChooseOutputFile(ev) { + if (ev.type === 'keydown' && ev.key !== ' ') { + return; + } + ev.stopPropagation(); + const fp = new FilePicker(); + fp.init(window, Zotero.getString("rtfScan.saveTitle"), fp.modeSave); + fp.appendFilter(Zotero.getString("rtfScan.rtf"), "*.rtf"); + if (this.inputFile) { + let leafName = this.inputFile.leafName; + let dotIndex = leafName.lastIndexOf("."); + if (dotIndex !== -1) { + leafName = leafName.substr(0, dotIndex); + } + fp.defaultString = leafName + " " + Zotero.getString("rtfScan.scannedFileSuffix") + ".rtf"; + } + else { + fp.defaultString = "Untitled.rtf"; + } + + var rv = await fp.show(); + + if (rv == fp.returnOK || rv == fp.returnReplace) { + this.outputFile = Zotero.File.pathToFile(fp.file); + this.updatePath(); + } + }, + + onIntroShow() { + this.wizard.canRewind = false; + this.updatePath(); + }, + + onIntroAdvanced() { + Zotero.Prefs.set("rtfScan.lastInputFile", this.inputFile.path); + Zotero.Prefs.set("rtfScan.lastOutputFile", this.outputFile.path); + }, + + async onScanPageShow() { + this.wizard.canRewind = false; + this.wizard.canAdvance = false; + + // wait a ms so that UI thread gets updated + try { + await this.scanRTF(); + this.tree.invalidate(); + this.wizard.canRewind = true; + this.wizard.canAdvance = true; + this.wizard.advance(); + } + catch (e) { + Zotero.logError(e); + Zotero.debug(e); + } + }, + + onStylePageRewound(ev) { + ev.preventDefault(); + this.rows = [...initialRows]; + this.rowMap = { ...initialRowMap }; + this.wizard.goTo('page-start'); + }, + + onStylePageAdvanced() { + const styleConfigurator = document.getElementById('style-configurator'); + this.styleConfig = { + style: styleConfigurator.style, + locale: styleConfigurator.locale, + displayAs: styleConfigurator.displayAs + }; + Zotero.Prefs.set("export.lastStyle", this.styleConfig.style); + }, + + onCitationsPageShow() { + this.refreshCanAdvanceIfCitationsReady(); + }, + + onCitationsPageRewound(ev) { + ev.preventDefault(); + this.rows = [...initialRows]; + this.rowMap = { ...initialRowMap }; + this.wizard.goTo('page-start'); + }, + + onFormatPageShow() { + this.wizard.canAdvance = false; + this.wizard.canRewind = false; + + window.setTimeout(() => { + this.formatRTF(); + this.wizard.canRewind = true; + this.wizard.canAdvance = true; + this.wizard.advance(); + }, 0); + }, + + onCompletePageShow() { + this.wizard.canRewind = false; + }, + + onRowTwistyMouseUp(event, index) { + const row = this.rows[index]; + if (!row.collapsed) { + // Store children rows on the parent when collapsing + row.children = []; + const depth = this.getRowLevel(index); + for (let childIndex = index + 1; childIndex < this.rows.length && this.getRowLevel(this.rows[childIndex]) > depth; childIndex++) { + row.children.push(this.rows[childIndex]); + } + // And then remove them + this.removeRows(row.children.map((_, childIndex) => index + 1 + childIndex)); + } + else { + // Insert children rows from the ones stored on the parent + this.insertRows(row.children, index + 1); + delete row.children; + } + row.collapsed = !row.collapsed; + this.tree.invalidate(); + }, + + onActionMouseUp(event, index) { + let row = this.rows[index]; + if (!row.parent) return; + let level = this.getRowLevel(row); + if (level == 2) { // ambiguous citation item + let parentIndex = this.rowMap[row.parent.id]; + // Update parent item + row.parent.item = row.item; + + // Remove children + let children = []; + for (let childIndex = parentIndex + 1; childIndex < this.rows.length && this.getRowLevel(this.rows[childIndex]) >= level; childIndex++) { + children.push(this.rows[childIndex]); + } + this.removeRows(children.map((_, childIndex) => parentIndex + 1 + childIndex)); + + // Move citation to mapped rows + row.parent.parent = this.rows[this.rowMap.mapped]; + this.removeRows(parentIndex); + this.insertRows(row.parent, this.rows.length); + + // update array + this.citationItemIDs[row.parent.rtf] = [this.citationItemIDs[row.parent.rtf][index - parentIndex - 1]]; + } + else { // mapped or unmapped citation, or ambiguous citation parent + var citation = row.rtf; + var io = { singleSelection: true }; + if (this.citationItemIDs[citation] && this.citationItemIDs[citation].length == 1) { // mapped citation + // specify that item should be selected in window + io.select = this.citationItemIDs[citation][0]; + } + + window.openDialog('chrome://zotero/content/selectItemsDialog.xhtml', '', 'chrome,modal', io); + + if (io.dataOut && io.dataOut.length) { + var selectedItemID = io.dataOut[0]; + var selectedItem = Zotero.Items.get(selectedItemID); + // update item name + row.item = selectedItem.getField("title"); + + // Remove children + let children = []; + for (let childIndex = index + 1; childIndex < this.rows.length && this.getRowLevel(this.rows[childIndex]) > level; childIndex++) { + children.push(this.rows[childIndex]); + } + this.removeRows(children.map((_, childIndex) => index + 1 + childIndex)); + + if (row.parent.id != 'mapped') { + // Move citation to mapped rows + row.parent = this.rows[this.rowMap.mapped]; + this.removeRows(index); + this.insertRows(row, this.rows.length); + } + + // update array + this.citationItemIDs[citation] = [selectedItemID]; + } + } + this.tree.invalidate(); + this.refreshCanAdvanceIfCitationsReady(); + }, + + async scanRTF() { + // set up globals + this.citations = []; + this.citationItemIDs = {}; + + let unmappedRow = this.rows[this.rowMap.unmapped]; + let ambiguousRow = this.rows[this.rowMap.ambiguous]; + let mappedRow = this.rows[this.rowMap.mapped]; + + // set up regular expressions + // this assumes that names are >=2 chars or only capital initials and that there are no + // more than 4 names + const nameRe = "(?:[^ .,;]{2,} |[A-Z].? ?){0,3}[A-Z][^ .,;]+"; + const creatorRe = '((?:(?:' + nameRe + ', )*' + nameRe + '(?:,? and|,? \\&|,) )?' + nameRe + ')(,? et al\\.?)?'; + // TODO: localize "and" term + const creatorSplitRe = /(?:,| *(?:and|&)) +/g; + var citationRe = new RegExp('(\\\\\\{|; )(' + creatorRe + ',? (?:"([^"]+)(?:,"|",) )?([0-9]{4})[a-z]?)(?:,(?: pp?.?)? ([^ )]+))?(?=;|\\\\\\})|(([A-Z][^ .,;]+)(,? et al\\.?)? (\\\\\\{([0-9]{4})[a-z]?\\\\\\}))', "gm"); + + // read through RTF file and display items as they're found + // we could read the file in chunks, but unless people start having memory issues, it's + // probably faster and definitely simpler if we don't + this.contents = Zotero.File.getContents(this.inputFile) + .replace(/([^\\\r])\r?\n/, "$1 ") + .replace("\\'92", "'", "g") + .replace("\\rquote ", "’"); + var m; + var lastCitation = false; + while ((m = citationRe.exec(this.contents))) { + // determine whether suppressed or standard regular expression was used + if (m[2]) { // standard parenthetical + var citationString = m[2]; + var creators = m[3]; + // var etAl = !!m[4]; + var title = m[5]; + var date = m[6]; + var pages = m[7]; + var start = citationRe.lastIndex - m[0].length; + var end = citationRe.lastIndex + 2; + } + else { // suppressed + citationString = m[8]; + creators = m[9]; + // etAl = !!m[10]; + title = false; + date = m[12]; + pages = false; + start = citationRe.lastIndex - m[11].length; + end = citationRe.lastIndex; + } + citationString = citationString.replace("\\{", "{", "g").replace("\\}", "}", "g"); + var suppressAuthor = !m[2]; + + if (lastCitation && lastCitation.end >= start) { + // if this citation is just an extension of the last, add items to it + lastCitation.citationStrings.push(citationString); + lastCitation.pages.push(pages); + lastCitation.end = end; + } + else { + // otherwise, add another citation + lastCitation = { + citationStrings: [citationString], pages: [pages], + start, end, suppressAuthor + }; + this.citations.push(lastCitation); + } + + // only add each citation once + if (this.citationItemIDs[citationString]) continue; + Zotero.debug("Found citation " + citationString); + + // for each individual match, look for an item in the database + var s = new Zotero.Search; + creators = creators.replace(".", ""); + // TODO: localize "et al." term + creators = creators.split(creatorSplitRe); + + for (let i = 0; i < creators.length; i++) { + if (!creators[i]) { + if (i == creators.length - 1) { + break; + } + else { + creators.splice(i, 1); + } + } + + var spaceIndex = creators[i].lastIndexOf(" "); + var lastName = spaceIndex == -1 ? creators[i] : creators[i].substr(spaceIndex + 1); + s.addCondition("lastName", "contains", lastName); + } + if (title) s.addCondition("title", "contains", title); + s.addCondition("date", "is", date); + var ids = await s.search(); // eslint-disable-line no-await-in-loop + Zotero.debug("Mapped to " + ids); + this.citationItemIDs[citationString] = ids; + + if (!ids) { // no mapping found + let row = _generateItem(citationString, ""); + row.parent = unmappedRow; + this.insertRows(row, this.rowMap.ambiguous); + } + else { // some mapping found + var items = await Zotero.Items.getAsync(ids); // eslint-disable-line no-await-in-loop + if (items.length > 1) { + // check to see how well the author list matches the citation + var matchedItems = []; + for (let item of items) { + await item.loadAllData(); // eslint-disable-line no-await-in-loop + if (_matchesItemCreators(creators, item)) matchedItems.push(item); + } + + if (matchedItems.length != 0) items = matchedItems; + } + + if (items.length == 1) { // only one mapping + await items[0].loadAllData(); // eslint-disable-line no-await-in-loop + let row = _generateItem(citationString, items[0].getField("title")); + row.parent = mappedRow; + this.insertRows(row, this.rows.length); + this.citationItemIDs[citationString] = [items[0].id]; + } + else { // ambiguous mapping + let row = _generateItem(citationString, ""); + row.parent = ambiguousRow; + this.insertRows(row, this.rowMap.mapped); + + // generate child items + let children = []; + for (let item of items) { + let childRow = _generateItem("", item.getField("title"), true); + childRow.parent = row; + children.push(childRow); + } + this.insertRows(children, this.rowMap[row.id] + 1); + } + } + } + }, + + formatRTF() { + // load style and create ItemSet with all items + var zStyle = Zotero.Styles.get(this.styleConfig.style); + var cslEngine = zStyle.getCiteProc(this.styleConfig.locale, 'rtf'); + var isNote = zStyle.class == "note"; + + // create citations + // var k = 0; + var cslCitations = []; + var itemIDs = {}; + // var shouldBeSubsequent = {}; + for (let i = 0; i < this.citations.length; i++) { + let citation = this.citations[i]; + var cslCitation = { citationItems: [], properties: {} }; + if (isNote) { + cslCitation.properties.noteIndex = i; + } + + // create citation items + for (var j = 0; j < citation.citationStrings.length; j++) { + var citationItem = {}; + citationItem.id = this.citationItemIDs[citation.citationStrings[j]][0]; + itemIDs[citationItem.id] = true; + citationItem.locator = citation.pages[j]; + citationItem.label = "page"; + citationItem["suppress-author"] = citation.suppressAuthor && !isNote; + cslCitation.citationItems.push(citationItem); + } + + cslCitations.push(cslCitation); + } + Zotero.debug(cslCitations); + + itemIDs = Object.keys(itemIDs); + Zotero.debug(itemIDs); + + // prepare the list of rendered citations + var citationResults = cslEngine.rebuildProcessorState(cslCitations, "rtf"); + + // format citations + var contentArray = []; + var lastEnd = 0; + for (let i = 0; i < this.citations.length; i++) { + let citation = citationResults[i][2]; + Zotero.debug("Formatted " + citation); + + // if using notes, we might have to move the note after the punctuation + if (isNote && this.citations[i].start != 0 && this.contents[this.citations[i].start - 1] == " ") { + contentArray.push(this.contents.substring(lastEnd, this.citations[i].start - 1)); + } + else { + contentArray.push(this.contents.substring(lastEnd, this.citations[i].start)); + } + + lastEnd = this.citations[i].end; + if (isNote && this.citations[i].end < this.contents.length && ".,!?".indexOf(this.contents[this.citations[i].end]) !== -1) { + contentArray.push(this.contents[this.citations[i].end]); + lastEnd++; + } + + if (isNote) { + if (this.styleConfig.displayAs === 'endnotes') { + contentArray.push("{\\super\\chftn}\\ftnbj {\\footnote\\ftnalt {\\super\\chftn } " + citation + "}"); + } + else { // footnotes + contentArray.push("{\\super\\chftn}\\ftnbj {\\footnote {\\super\\chftn } " + citation + "}"); + } + } + else { + contentArray.push(citation); + } + } + contentArray.push(this.contents.substring(lastEnd)); + this.contents = contentArray.join(""); + + // add bibliography + if (zStyle.hasBibliography) { + var bibliography = Zotero.Cite.makeFormattedBibliography(cslEngine, "rtf"); + bibliography = bibliography.substring(5, bibliography.length - 1); + // fix line breaks + var linebreak = "\r\n"; + if (this.contents.indexOf("\r\n") == -1) { + bibliography = bibliography.replace("\r\n", "\n", "g"); + linebreak = "\n"; + } + + if (this.contents.indexOf(BIBLIOGRAPHY_PLACEHOLDER) !== -1) { + this.contents = this.contents.replace(BIBLIOGRAPHY_PLACEHOLDER, bibliography); + } + else { + // add two newlines before bibliography + bibliography = linebreak + "\\" + linebreak + "\\" + linebreak + bibliography; + + // add bibliography automatically inside last set of brackets closed + const bracketRe = /^\{+/; + var m = bracketRe.exec(this.contents); + if (m) { + var closeBracketRe = new RegExp("(\\}{" + m[0].length + "}\\s*)$"); + this.contents = this.contents.replace(closeBracketRe, bibliography + "$1"); + } + else { + this.contents += bibliography; + } + } + } + + cslEngine.free(); + + Zotero.File.putContents(this.outputFile, this.contents); + + // save locale + if (!zStyle.locale && this.styleConfig.locale) { + Zotero.Prefs.set("export.lastLocale", this.styleConfig.locale); + } + }, + + refreshCanAdvanceIfCitationsReady() { + let newCanAdvance = true; + for (let i in this.citationItemIDs) { + let itemList = this.citationItemIDs[i]; + if (itemList.length !== 1) { + newCanAdvance = false; + break; + } + } + this.wizard.canAdvance = newCanAdvance; + }, + + updatePath() { + this.wizard.canAdvance = this.inputFile && this.outputFile; + document.getElementById('input-path').value = this.inputFile ? this.inputFile.path : ''; + document.getElementById('output-path').value = this.outputFile ? this.outputFile.path : ''; + }, + + insertRows(newRows, beforeRow) { + if (!Array.isArray(newRows)) { + newRows = [newRows]; + } + this.rows.splice(beforeRow, 0, ...newRows); + newRows.forEach(row => row.id = this.ids++); + + // Refresh the row map + this.rowMap = {}; + this.rows.forEach((row, index) => this.rowMap[row.id] = index); + }, + + removeRows(indices) { + if (!Array.isArray(indices)) { + indices = [indices]; + } + // Reverse sort so we can safely splice out the entries from the rows array + indices.sort((a, b) => b - a); + for (const index of indices) { + this.rows.splice(index, 1); + } + // Refresh the row map + this.rowMap = {}; + this.rows.forEach((row, index) => this.rowMap[row.id] = index); + }, + + getRowLevel(row, depth = 0) { + if (typeof row == 'number') { + row = this.rows[row]; + } + if (!row.parent) { + return depth; + } + return this.getRowLevel(row.parent, depth + 1); + }, + + renderItem(index, selection, oldDiv = null, columns) { + const row = this.rows[index]; + let div; + if (oldDiv) { + div = oldDiv; + div.innerHTML = ""; + } + else { + div = document.createElement('div'); + div.className = "row"; + } + + for (const column of columns) { + if (column.primary) { + let twisty; + if (row.children || (this.rows[index + 1] && this.rows[index + 1].parent == row)) { + twisty = getDOMElement("IconTwisty"); + twisty.classList.add('twisty'); + if (!row.collapsed) { + twisty.classList.add('open'); + } + twisty.style.pointerEvents = 'auto'; + twisty.addEventListener('mousedown', event => event.stopPropagation()); + twisty.addEventListener('mouseup', event => this.onRowTwistyMouseUp(event, index), + { passive: true }); + } + else { + twisty = document.createElement('span'); + twisty.classList.add("spacer-twisty"); + } + + let textSpan = document.createElement('span'); + textSpan.className = "cell-text"; + textSpan.innerText = row[column.dataKey] || ""; + textSpan.style.paddingLeft = (5 + 20 * this.getRowLevel(row)) + 'px'; + + let span = document.createElement('span'); + span.className = `cell primary ${column.className}`; + span.appendChild(twisty); + span.appendChild(textSpan); + div.appendChild(span); + } + else if (column.dataKey == 'action') { + let span = document.createElement('span'); + span.className = `cell action ${column.className}`; + if (row.parent) { + if (row.action) { + span.appendChild(getDOMElement('IconRTFScanAccept')); + } + else { + span.appendChild(getDOMElement('IconRTFScanLink')); + } + span.addEventListener('mouseup', e => this.onActionMouseUp(e, index), { passive: true }); + span.style.pointerEvents = 'auto'; + } + + div.appendChild(span); + } + else { + let span = document.createElement('span'); + span.className = `cell ${column.className}`; + span.innerText = row[column.dataKey] || ""; + div.appendChild(span); + } + } + return div; + }, + +}; diff --git a/chrome/content/zotero/rtfScan.jsx b/chrome/content/zotero/rtfScan.jsx deleted file mode 100644 index 3cf30bd8ae..0000000000 --- a/chrome/content/zotero/rtfScan.jsx +++ /dev/null @@ -1,779 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright © 2009 Center for History and New Media - George Mason University, Fairfax, Virginia, USA - http://zotero.org - - This file is part of Zotero. - - Zotero is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Zotero is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Zotero. If not, see . - - ***** END LICENSE BLOCK ***** -*/ - -/** - * @fileOverview Tools for automatically retrieving a citation for the given PDF - */ - -import FilePicker from 'zotero/modules/filePicker'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import VirtualizedTable from 'components/virtualized-table'; -import { getDOMElement } from 'components/icons'; - -/** - * Front end for recognizing PDFs - * @namespace - */ -var Zotero_RTFScan = new function() { - const ACCEPT_ICON = "chrome://zotero/skin/rtfscan-accept.png"; - const LINK_ICON = "chrome://zotero/skin/rtfscan-link.png"; - const BIBLIOGRAPHY_PLACEHOLDER = "\\{Bibliography\\}"; - - const columns = [ - { dataKey: 'rtf', label: "zotero.rtfScan.citation.label", primary: true, flex: 4 }, - { dataKey: 'item', label: "zotero.rtfScan.itemName.label", flex: 5 }, - { dataKey: 'action', label: "", fixedWidth: true, width: "26px" }, - ]; - var ids = 0; - var tree; - this._rows = [ - { id: 'unmapped', rtf: Zotero.getString('zotero.rtfScan.unmappedCitations.label'), collapsed: false }, - { id: 'ambiguous', rtf: Zotero.getString('zotero.rtfScan.ambiguousCitations.label'), collapsed: false }, - { id: 'mapped', rtf: Zotero.getString('zotero.rtfScan.mappedCitations.label'), collapsed: false }, - ]; - this._rowMap = {}; - this._rows.forEach((row, index) => this._rowMap[row.id] = index); - - var inputFile = null, outputFile = null; - var citations, citationItemIDs, contents; - - /** INTRO PAGE UI **/ - - /** - * Called when the first page is shown; loads target file from preference, if one is set - */ - this.introPageShowing = function() { - var path = Zotero.Prefs.get("rtfScan.lastInputFile"); - if(path) { - inputFile = Zotero.File.pathToFile(path); - } - var path = Zotero.Prefs.get("rtfScan.lastOutputFile"); - if(path) { - outputFile = Zotero.File.pathToFile(path); - } - _updatePath(); - document.getElementById("choose-input-file").focus(); - } - - /** - * Called when the first page is hidden - */ - this.introPageAdvanced = function() { - Zotero.Prefs.set("rtfScan.lastInputFile", inputFile.path); - Zotero.Prefs.set("rtfScan.lastOutputFile", outputFile.path); - } - - /** - * Called to select the file to be processed - */ - this.chooseInputFile = async function () { - // display file picker - var fp = new FilePicker(); - fp.init(window, Zotero.getString("rtfScan.openTitle"), fp.modeOpen); - - fp.appendFilters(fp.filterAll); - fp.appendFilter(Zotero.getString("rtfScan.rtf"), "*.rtf"); - - var rv = await fp.show(); - if (rv == fp.returnOK || rv == fp.returnReplace) { - inputFile = Zotero.File.pathToFile(fp.file); - _updatePath(); - } - } - - /** - * Called to select the output file - */ - this.chooseOutputFile = async function () { - var fp = new FilePicker(); - fp.init(window, Zotero.getString("rtfScan.saveTitle"), fp.modeSave); - fp.appendFilter(Zotero.getString("rtfScan.rtf"), "*.rtf"); - if(inputFile) { - var leafName = inputFile.leafName; - var dotIndex = leafName.lastIndexOf("."); - if(dotIndex != -1) { - leafName = leafName.substr(0, dotIndex); - } - fp.defaultString = leafName+" "+Zotero.getString("rtfScan.scannedFileSuffix")+".rtf"; - } else { - fp.defaultString = "Untitled.rtf"; - } - - var rv = await fp.show(); - if (rv == fp.returnOK || rv == fp.returnReplace) { - outputFile = Zotero.File.pathToFile(fp.file); - _updatePath(); - } - } - - /** - * Called to update the path label in the dialog box - * @private - */ - function _updatePath() { - document.documentElement.canAdvance = inputFile && outputFile; - if(inputFile) document.getElementById("input-path").value = inputFile.path; - if(outputFile) document.getElementById("output-path").value = outputFile.path; - } - - /** SCAN PAGE UI **/ - - /** - * Called when second page is shown. - */ - this.scanPageShowing = async function () { - // can't advance - document.documentElement.canAdvance = false; - - // wait a ms so that UI thread gets updated - try { - await this._scanRTF(); - } - catch (e) { - Zotero.logError(e); - Zotero.debug(e); - } - }; - - /** - * Scans file for citations, then proceeds to next wizard page. - */ - this._scanRTF = async () => { - // set up globals - citations = []; - citationItemIDs = {}; - - let unmappedRow = this._rows[this._rowMap['unmapped']]; - let ambiguousRow = this._rows[this._rowMap['ambiguous']]; - let mappedRow = this._rows[this._rowMap['mapped']]; - - // set up regular expressions - // this assumes that names are >=2 chars or only capital initials and that there are no - // more than 4 names - const nameRe = "(?:[^ .,;]{2,} |[A-Z].? ?){0,3}[A-Z][^ .,;]+"; - const creatorRe = '((?:(?:'+nameRe+', )*'+nameRe+'(?:,? and|,? \\&|,) )?'+nameRe+')(,? et al\\.?)?'; - // TODO: localize "and" term - const creatorSplitRe = /(?:,| *(?:and|\&)) +/g; - var citationRe = new RegExp('(\\\\\\{|; )('+creatorRe+',? (?:"([^"]+)(?:,"|",) )?([0-9]{4})[a-z]?)(?:,(?: pp?\.?)? ([^ )]+))?(?=;|\\\\\\})|(([A-Z][^ .,;]+)(,? et al\\.?)? (\\\\\\{([0-9]{4})[a-z]?\\\\\\}))', "gm"); - - // read through RTF file and display items as they're found - // we could read the file in chunks, but unless people start having memory issues, it's - // probably faster and definitely simpler if we don't - contents = Zotero.File.getContents(inputFile).replace(/([^\\\r])\r?\n/, "$1 ").replace("\\'92", "'", "g").replace("\\rquote ", "’"); - var m; - var lastCitation = false; - while ((m = citationRe.exec(contents))) { - // determine whether suppressed or standard regular expression was used - if (m[2]) { // standard parenthetical - var citationString = m[2]; - var creators = m[3]; - var etAl = !!m[4]; - var title = m[5]; - var date = m[6]; - var pages = m[7]; - var start = citationRe.lastIndex - m[0].length; - var end = citationRe.lastIndex + 2; - } - else { // suppressed - citationString = m[8]; - creators = m[9]; - etAl = !!m[10]; - title = false; - date = m[12]; - pages = false; - start = citationRe.lastIndex - m[11].length; - end = citationRe.lastIndex; - } - citationString = citationString.replace("\\{", "{", "g").replace("\\}", "}", "g"); - var suppressAuthor = !m[2]; - - if (lastCitation && lastCitation.end >= start) { - // if this citation is just an extension of the last, add items to it - lastCitation.citationStrings.push(citationString); - lastCitation.pages.push(pages); - lastCitation.end = end; - } - else { - // otherwise, add another citation - lastCitation = { citationStrings: [citationString], pages: [pages], - start, end, suppressAuthor }; - citations.push(lastCitation); - } - - // only add each citation once - if (citationItemIDs[citationString]) continue; - Zotero.debug("Found citation " + citationString); - - // for each individual match, look for an item in the database - var s = new Zotero.Search; - creators = creators.replace(".", ""); - // TODO: localize "et al." term - creators = creators.split(creatorSplitRe); - - for (let i = 0; i < creators.length; i++) { - if (!creators[i]) { - if (i == creators.length - 1) { - break; - } - else { - creators.splice(i, 1); - } - } - - var spaceIndex = creators[i].lastIndexOf(" "); - var lastName = spaceIndex == -1 ? creators[i] : creators[i].substr(spaceIndex+1); - s.addCondition("lastName", "contains", lastName); - } - if (title) s.addCondition("title", "contains", title); - s.addCondition("date", "is", date); - var ids = await s.search(); - Zotero.debug("Mapped to " + ids); - citationItemIDs[citationString] = ids; - - if (!ids) { // no mapping found - let row = _generateItem(citationString, ""); - row.parent = unmappedRow; - this._insertRows(row, this._rowMap.ambiguous); - } - else { // some mapping found - var items = await Zotero.Items.getAsync(ids); - if (items.length > 1) { - // check to see how well the author list matches the citation - var matchedItems = []; - for (let item of items) { - await item.loadAllData(); - if (_matchesItemCreators(creators, item)) matchedItems.push(item); - } - - if (matchedItems.length != 0) items = matchedItems; - } - - if (items.length == 1) { // only one mapping - await items[0].loadAllData(); - let row = _generateItem(citationString, items[0].getField("title")); - row.parent = mappedRow; - this._insertRows(row, this._rows.length); - citationItemIDs[citationString] = [items[0].id]; - } - else { // ambiguous mapping - let row = _generateItem(citationString, ""); - row.parent = ambiguousRow; - this._insertRows(row, this._rowMap.mapped); - - // generate child items - let children = []; - for (let item of items) { - let childRow = _generateItem("", item.getField("title"), true); - childRow.parent = row; - children.push(childRow); - } - this._insertRows(children, this._rowMap[row.id] + 1); - } - } - } - tree.invalidate(); - - // when scanning is complete, go to citations page - document.documentElement.canAdvance = true; - document.documentElement.advance(); - }; - - function _generateItem(citationString, itemName, action) { - return { - rtf: citationString, - item: itemName, - action - }; - } - - function _matchesItemCreators(creators, item, etAl) { - var itemCreators = item.getCreators(); - var primaryCreators = []; - var primaryCreatorTypeID = Zotero.CreatorTypes.getPrimaryIDForType(item.itemTypeID); - - // use only primary creators if primary creators exist - for(var i=0; i creators.length)) { - var matched = true; - for(var i=0; i lowerLast.length) { - var firstName = Zotero.Utilities.trim(creator.substr(0, creator.length-lowerLast.length)); - if(firstName.length) { - // check to see whether the first name is all initials - const initialRe = /^(?:[A-Z]\.? ?)+$/; - var m = initialRe.exec(firstName); - if(m) { - var initials = firstName.replace(/[^A-Z]/g, ""); - var itemInitials = itemCreator.firstName.split(/ +/g) - .map(name => name[0].toUpperCase()) - .join(""); - if(initials != itemInitials) return false; - } else { - // not all initials; verify that the first name matches - var firstWord = firstName.substr(0, itemCreator.firstName).toLowerCase(); - var itemFirstWord = itemCreator.firstName.substr(0, itemCreator.firstName.indexOf(" ")).toLowerCase(); - if(firstWord != itemFirstWord) return false; - } - } - } - - return true; - } - - /** CITATIONS PAGE UI **/ - - /** - * Called when citations page is shown to determine whether user can immediately advance. - */ - this.citationsPageShowing = function() { - _refreshCanAdvance(); - } - - /** - * Called when the citations page is rewound. Removes all citations from the list, clears - * globals, and returns to intro page. - */ - this.citationsPageRewound = function () { - // skip back to intro page - document.documentElement.currentPage = document.getElementById('intro-page'); - - this._rows = [ - { id: 'unmapped', rtf: Zotero.getString('zotero.rtfScan.unmappedCitations.label'), collapsed: false }, - { id: 'ambiguous', rtf: Zotero.getString('zotero.rtfScan.ambiguousCitations.label'), collapsed: false }, - { id: 'mapped', rtf: Zotero.getString('zotero.rtfScan.mappedCitations.label'), collapsed: false }, - ]; - this._rowMap = {}; - this._rows.forEach((row, index) => this._rowMap[row.id] = index); - - return false; - } - - /** - * Called when a tree item is clicked to remap a citation, or accept a suggestion for an - * ambiguous citation - */ - this.treeClick = function(event) { - var tree = document.getElementById("tree"); - - // get clicked cell - var { row, col } = tree.getCellAt(event.clientX, event.clientY); - - // figure out which item this corresponds to - var level = tree.view.getLevel(row); - } - - /** - * Determines whether the button to advance the wizard should be enabled or not based on whether - * unmapped citations exist, and sets the status appropriately - */ - function _refreshCanAdvance() { - var canAdvance = true; - for (let i in citationItemIDs) { - let itemList = citationItemIDs[i]; - if(itemList.length != 1) { - canAdvance = false; - break; - } - } - - document.documentElement.canAdvance = canAdvance; - } - - /** STYLE PAGE UI **/ - - /** - * Called when style page is shown to add styles to listbox. - */ - this.stylePageShowing = async function() { - await Zotero.Styles.init(); - Zotero_File_Interface_Bibliography.init({ - supportedNotes: ['footnotes', 'endnotes'] - }); - } - - /** - * Called when style page is hidden to save preferences. - */ - this.stylePageAdvanced = function() { - Zotero.Prefs.set("export.lastStyle", document.getElementById("style-listbox").selectedItem.value); - } - - /** FORMAT PAGE UI **/ - - this.formatPageShowing = function() { - // can't advance - document.documentElement.canAdvance = false; - - // wait a ms so that UI thread gets updated - window.setTimeout(function() { _formatRTF() }, 1); - } - - function _formatRTF() { - // load style and create ItemSet with all items - var zStyle = Zotero.Styles.get(document.getElementById("style-listbox").value) - var locale = document.getElementById("locale-menu").value; - var cslEngine = zStyle.getCiteProc(locale, 'rtf'); - var isNote = zStyle.class == "note"; - - // create citations - var k = 0; - var cslCitations = []; - var itemIDs = {}; - var shouldBeSubsequent = {}; - for(var i=0; i { - const row = this._rows[index]; - if (!row.collapsed) { - // Store children rows on the parent when collapsing - row.children = []; - const depth = this._getRowLevel(index); - for (let childIndex = index + 1; childIndex < this._rows.length && this._getRowLevel(this._rows[childIndex]) > depth; childIndex++) { - row.children.push(this._rows[childIndex]); - } - // And then remove them - this._removeRows(row.children.map((_, childIndex) => index + 1 + childIndex)); - } - else { - // Insert children rows from the ones stored on the parent - this._insertRows(row.children, index + 1); - delete row.children; - } - row.collapsed = !row.collapsed; - tree.invalidate(); - }; - - this._onActionMouseUp = (event, index) => { - let row = this._rows[index]; - if (!row.parent) return; - let level = this._getRowLevel(row); - if (level == 2) { // ambiguous citation item - let parentIndex = this._rowMap[row.parent.id]; - // Update parent item - row.parent.item = row.item; - - // Remove children - let children = []; - for (let childIndex = parentIndex + 1; childIndex < this._rows.length && this._getRowLevel(this._rows[childIndex]) >= level; childIndex++) { - children.push(this._rows[childIndex]); - } - this._removeRows(children.map((_, childIndex) => parentIndex + 1 + childIndex)); - - // Move citation to mapped rows - row.parent.parent = this._rows[this._rowMap.mapped]; - this._removeRows(parentIndex); - this._insertRows(row.parent, this._rows.length); - - // update array - citationItemIDs[row.parent.rtf] = [citationItemIDs[row.parent.rtf][index-parentIndex-1]]; - } - else { // mapped or unmapped citation, or ambiguous citation parent - var citation = row.rtf; - var io = { singleSelection: true }; - if (citationItemIDs[citation] && citationItemIDs[citation].length == 1) { // mapped citation - // specify that item should be selected in window - io.select = citationItemIDs[citation][0]; - } - - window.openDialog('chrome://zotero/content/selectItemsDialog.xul', '', 'chrome,modal', io); - - if (io.dataOut && io.dataOut.length) { - var selectedItemID = io.dataOut[0]; - var selectedItem = Zotero.Items.get(selectedItemID); - // update item name - row.item = selectedItem.getField("title"); - - // Remove children - let children = []; - for (let childIndex = index + 1; childIndex < this._rows.length && this._getRowLevel(this._rows[childIndex]) > level; childIndex++) { - children.push(this._rows[childIndex]); - } - this._removeRows(children.map((_, childIndex) => index + 1 + childIndex)); - - if (row.parent.id != 'mapped') { - // Move citation to mapped rows - row.parent = this._rows[this._rowMap.mapped]; - this._removeRows(index); - this._insertRows(row, this._rows.length); - } - - // update array - citationItemIDs[citation] = [selectedItemID]; - } - } - tree.invalidate(); - _refreshCanAdvance(); - }; - - this._insertRows = (rows, beforeRow) => { - if (!Array.isArray(rows)) { - rows = [rows]; - } - this._rows.splice(beforeRow, 0, ...rows); - rows.forEach(row => row.id = ids++); - for (let row of rows) { - row.id = ids++; - } - // Refresh the row map - this._rowMap = {}; - this._rows.forEach((row, index) => this._rowMap[row.id] = index); - }; - - this._removeRows = (indices) => { - if (!Array.isArray(indices)) { - indices = [indices]; - } - // Reverse sort so we can safely splice out the entries from the rows array - indices.sort((a, b) => b - a); - for (const index of indices) { - this._rows.splice(index, 1); - } - // Refresh the row map - this._rowMap = {}; - this._rows.forEach((row, index) => this._rowMap[row.id] = index); - }; - - this._getRowLevel = (row, depth=0) => { - if (typeof row == 'number') { - row = this._rows[row]; - } - if (!row.parent) { - return depth; - } - return this._getRowLevel(row.parent, depth+1); - } - - this._renderItem = (index, selection, oldDiv=null, columns) => { - const row = this._rows[index]; - let div; - if (oldDiv) { - div = oldDiv; - div.innerHTML = ""; - } - else { - div = document.createElement('div'); - div.className = "row"; - } - - for (const column of columns) { - if (column.primary) { - let twisty; - if (row.children || (this._rows[index + 1] && this._rows[index + 1].parent == row)) { - twisty = getDOMElement("IconTwisty"); - twisty.classList.add('twisty'); - if (!row.collapsed) { - twisty.classList.add('open'); - } - twisty.style.pointerEvents = 'auto'; - twisty.addEventListener('mousedown', event => event.stopPropagation()); - twisty.addEventListener('mouseup', event => this._onTwistyMouseUp(event, index), - { passive: true }); - } - else { - twisty = document.createElement('span'); - twisty.classList.add("spacer-twisty"); - } - - let textSpan = document.createElement('span'); - textSpan.className = "cell-text"; - textSpan.innerText = row[column.dataKey] || ""; - - let span = document.createElement('span'); - span.className = `cell primary ${column.className}`; - span.appendChild(twisty); - span.appendChild(textSpan); - span.style.paddingLeft = (5 + 20 * this._getRowLevel(row)) + 'px'; - div.appendChild(span); - } - else if (column.dataKey == 'action') { - let span = document.createElement('span'); - span.className = `cell action ${column.className}`; - if (row.parent) { - if (row.action) { - span.appendChild(getDOMElement('IconRTFScanAccept')); - } - else { - span.appendChild(getDOMElement('IconRTFScanLink')); - } - span.addEventListener('mouseup', e => this._onActionMouseUp(e, index), { passive: true }); - span.style.pointerEvents = 'auto'; - } - - div.appendChild(span); - } - else { - let span = document.createElement('span'); - span.className = `cell ${column.className}`; - span.innerText = row[column.dataKey] || ""; - div.appendChild(span); - } - } - return div; - }; - - this._initCitationTree = function () { - const domEl = document.querySelector('#tree'); - const elem = ( - this._rows.length} - id="rtfScan-table" - ref={ref => tree = ref} - renderItem={this._renderItem} - showHeader={true} - columns={columns} - disableFontSizeScaling={true} - /> - ); - return new Promise(resolve => ReactDOM.render(elem, domEl, resolve)); - }; -} diff --git a/chrome/content/zotero/rtfScan.xhtml b/chrome/content/zotero/rtfScan.xhtml new file mode 100644 index 0000000000..9cfcb6527b --- /dev/null +++ b/chrome/content/zotero/rtfScan.xhtml @@ -0,0 +1,72 @@ + + + + + + + + + + + + +