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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Smith, 2009}
+ Smith {2009}
+ {Smith et al., 2009}
+ {John Smith, 2009}
+ {Smith, 2009, 10-14}
+ {Smith, "Title," 2009}
+ {Jones, 2005; Smith, 2009}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/chrome/content/zotero/rtfScan.xul b/chrome/content/zotero/rtfScan.xul
deleted file mode 100644
index 311530d69f..0000000000
--- a/chrome/content/zotero/rtfScan.xul
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- &zotero.rtfScan.introPage.description;
-
-
-
-
-
-
-
- &zotero.rtfScan.introPage.description2;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- &zotero.rtfScan.scanPage.description;
-
-
-
-
- &zotero.rtfScan.citationsPage.description;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- &zotero.rtfScan.formatPage.description;
-
-
-
-
- &zotero.rtfScan.completePage.description;
-
-
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index 351dcb52fc..686ecf8bc2 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -702,7 +702,7 @@
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard();"
disabled="true"/>
-
+
diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl
index c725dfb585..0ac9e41b3d 100644
--- a/chrome/locale/en-US/zotero/zotero.ftl
+++ b/chrome/locale/en-US/zotero/zotero.ftl
@@ -50,8 +50,6 @@ file-interface-items-were-imported = { $numItems ->
import-mendeley-encrypted = The selected Mendeley database cannot be read, likely because it is encrypted.
See How do I import a Mendeley library into Zotero? for more information.
-
-file-interface-import-error = = An error occurred while trying to import the selected file. Please ensure that the file is valid and try again.
file-interface-import-error-translator = An error occurred importing the selected file with “{ $translator }”. Please ensure that the file is valid and try again.
# Variables:
@@ -61,4 +59,55 @@ import-online-intro=In the next step you will be asked to log in to { $targetApp
import-online-intro2={ -app-name } will never see or store your { $targetApp } password.
report-error =
- .label = Report Error…
\ No newline at end of file
+ .label = Report Error…
+
+rtfScan-wizard =
+ .title = RTF Scan
+
+rtfScan-introPage-description = Zotero can automatically extract and reformat citations and insert a bibliography into RTF files. To get started, choose an RTF file below.
+rtfScan-introPage-description2 = To get started, select an RTF input file and an output file below:
+
+rtfScan-input-file = Input File
+rtfScan-output-file = Output File
+
+zotero-file-none-selected =
+ .value = No file selected
+
+zotero-file-choose =
+ .label = Choose File…
+
+rtfScan-intro-page =
+ .label = Introduction
+
+rtfScan-scan-page =
+ .label = Scanning for Citations
+
+rtfScan-scanPage-description = Zotero is scanning your document for citations. Please be patient.
+
+rtfScan-citations-page =
+ .label = Verify Cited Items
+
+rtfScan-citations-page-description = Please review the list of recognized citations below to ensure that Zotero has selected the corresponding items correctly. Any unmapped or ambiguous citations must be resolved before proceeding to the next step.
+
+rtfScan-style-page =
+ .label = Document Formatting
+
+rtfScan-format-page =
+ .label = Formatting Citations
+
+rtfScan-format-page-description = Zotero is processing and formatting your RTF file. Please be patient.
+
+rtfScan-complete-page =
+ .label = RTF Scan Complete
+
+rtfScan-complete-page-description = Your document has now been scanned and processed. Please ensure that it is formatted correctly.
+
+
+bibliography-style-label = Citation Style:
+bibliography-locale-label = Language:
+
+integration-prefs-displayAs-label = Display Citations As:
+integration-prefs-footnotes =
+ .label = Footnotes
+integration-prefs-endnotes =
+ .label = Endnotes
diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss
index 3ee50b97a8..450bfcc016 100644
--- a/scss/_zotero-react-client.scss
+++ b/scss/_zotero-react-client.scss
@@ -37,6 +37,7 @@
@import "components/mainWindow";
@import "components/notesList";
@import "components/progressMeter";
+@import "components/rtfScan.scss";
@import "components/search";
@import "components/syncButtonTooltip";
@import "components/tabBar";
diff --git a/scss/components/_rtfScan.scss b/scss/components/_rtfScan.scss
new file mode 100644
index 0000000000..ff66ffa835
--- /dev/null
+++ b/scss/components/_rtfScan.scss
@@ -0,0 +1,85 @@
+.rtfscan-wizard {
+ wizardpage {
+ display: flex;
+ flex-direction: column;
+
+ > div {
+ display: block;
+ }
+ }
+
+ p {
+ display: inline;
+ }
+
+ .example, .page-start-1, .page-start-2, .file-input-label {
+ display: block;
+ }
+
+ .example {
+ line-height: 1.5em
+ }
+
+ .page-start-1 {
+ margin-bottom: 1em;
+ }
+
+ .page-start-2 {
+ margin-top: 1em;
+ }
+
+ .file-input-label {
+ margin: 1em $space-xs $space-min;
+ }
+
+ .file-input-container {
+ background-color: $input-group-background-color;
+ border-radius: $border-radius-base;
+ border: 1px solid $input-group-border-color;
+ display: flex;
+ margin: auto $space-min auto;
+ padding: .5em;
+
+ > input {
+ flex: 1 0 auto;
+ }
+
+ > button {
+ flex: 0 1 auto;
+ margin-left: 1em;
+ }
+ }
+
+ .citations-page-description {
+ margin-bottom: 1em;
+ }
+
+ .citations-page > .wizard-body {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .table-container {
+ display: flex;
+ height: 0;
+ flex-direction: column;
+ flex: 1 0 auto;
+ margin-top: 1.5em;
+
+ >div {
+ display: flex;
+ flex: 1 0 auto;
+ background-color: -moz-field;
+ overflow: hidden;
+ position: relative;
+ }
+
+ .virtualized-table-body {
+ display: flex;
+
+ .windowed-list {
+ flex: 1 0 auto;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/scss/elements/style-configurator.scss b/scss/elements/style-configurator.scss
new file mode 100644
index 0000000000..5e72da7805
--- /dev/null
+++ b/scss/elements/style-configurator.scss
@@ -0,0 +1,58 @@
+@import "../abstracts/variables";
+@import "../abstracts/functions";
+@import "../abstracts/mixins";
+@import "../abstracts/placeholders";
+@import "../abstracts/utilities";
+@import "../themes/light";
+
+:host {
+ display: block;
+ width: 100%;
+}
+
+richlistbox {
+ padding: var(--style-configurator-richlistbox-padding, 2px);
+ max-height: var(--style-configurator-richlistitem-max-height, 260px);
+ overflow: var(--style-configurator-richlistitem-overflow, auto scroll);
+
+ richlistitem {
+ line-height: var(--style-configurator-richlistitem-line-height, 1.5em);
+ }
+
+ @media (-moz-platform: macos) {
+ &:not(:focus) richlistitem[selected="true"] {
+ background-color: -moz-mac-secondaryhighlight;
+ }
+ }
+}
+
+#locale-selector-wrapper,
+#style-selector-wrapper,
+#display-as-wrapper {
+ background-color: var(--style-configurator-field-wrapper-background-color, $input-group-background-color);
+ border-radius: var(--style-configurator-field-wrapper-border-radius, $border-radius-base);
+ border: var(--style-configurator-field-wrapper-border, 1px solid $input-group-border-color);
+ margin: var(--style-configurator-field-margin, 1.5em 0 0 0);
+ padding: var(--style-configurator-field-padding, $space-xs);
+}
+
+label[for="style-selector"] {
+ margin: var(--style-configurator-label-margin, 1.5em 0 0 0);
+ font-size: var(--style-configurator-label-font-size, 13px);
+}
+
+#style-selector-wrapper {
+ margin: var(--style-configurator-style-field-margin, 0);
+}
+
+#locale-selector-wrapper {
+ display: flex;
+ align-items: center;
+}
+
+#display-as-wrapper {
+ radiogroup {
+ display: flex;
+ flex-direction: row;
+ }
+}
\ No newline at end of file
diff --git a/scss/themes/_light.scss b/scss/themes/_light.scss
index a6b7fdcab6..6476edac22 100644
--- a/scss/themes/_light.scss
+++ b/scss/themes/_light.scss
@@ -156,6 +156,8 @@ $input-bg: $body-bg;
$input-border-color: $shade-3;
$input-focus-color: $secondary;
$placeholder-color: $shade-5;
+$input-group-background-color: #e5e5e5;
+$input-group-border-color: darken($input-group-background-color, 5%);
// Editable
$editable-color: $text-color;