From 5409ae331325f43ff2bd26b70a043bd3251207fd Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Tue, 28 Dec 2021 17:32:56 -0800 Subject: [PATCH 1/3] Overhaul Scaffold (#2293) --- chrome/content/scaffold/load.js | 4 + chrome/content/scaffold/monaco/monaco.html | 64 + chrome/content/scaffold/scaffold.js | 1896 +++++++++++------ chrome/content/scaffold/scaffold.xul | 483 +++-- chrome/content/scaffold/templates/newWeb.js | 54 +- .../scaffold/templates/scrapeBibTeX.js | 27 +- chrome/content/scaffold/templates/scrapeEM.js | 23 +- .../content/scaffold/templates/scrapeMARC.js | 65 +- .../content/scaffold/templates/scrapeRIS.js | 48 +- .../content/scaffold/templates/shortcuts.txt | 11 - chrome/content/scaffold/translators.js | 30 +- chrome/locale/en-US/scaffold/scaffold.dtd | 133 +- chrome/skin/default/scaffold/checkSyntax.png | Bin 537 -> 0 bytes chrome/skin/default/scaffold/detectImport.png | Bin 3674 -> 0 bytes chrome/skin/default/scaffold/detectWeb.png | Bin 750 -> 1247 bytes chrome/skin/default/scaffold/doImport.png | Bin 2732 -> 0 bytes chrome/skin/default/scaffold/new.png | Bin 0 -> 1532 bytes chrome/skin/default/scaffold/reference.png | Bin 825 -> 0 bytes chrome/skin/default/scaffold/scaffold.css | 92 +- defaults/preferences/zotero.js | 3 + package-lock.json | 11 + package.json | 1 + resource/vs | 1 + scripts/config.js | 3 +- 24 files changed, 1936 insertions(+), 1013 deletions(-) create mode 100644 chrome/content/scaffold/monaco/monaco.html delete mode 100644 chrome/content/scaffold/templates/shortcuts.txt delete mode 100644 chrome/skin/default/scaffold/checkSyntax.png delete mode 100644 chrome/skin/default/scaffold/detectImport.png delete mode 100644 chrome/skin/default/scaffold/doImport.png create mode 100644 chrome/skin/default/scaffold/new.png delete mode 100644 chrome/skin/default/scaffold/reference.png create mode 120000 resource/vs diff --git a/chrome/content/scaffold/load.js b/chrome/content/scaffold/load.js index 6308178689..62067c1455 100644 --- a/chrome/content/scaffold/load.js +++ b/chrome/content/scaffold/load.js @@ -47,6 +47,10 @@ var Scaffold_Load = new function() { .sort((a, b) => a.label.localeCompare(b.label)); translators["Import Translators"] = (yield translatorProvider.getAllForType("import")) .sort((a, b) => a.label.localeCompare(b.label)); + translators["Export Translators"] = (yield translatorProvider.getAllForType("export")) + .sort((a, b) => a.label.localeCompare(b.label)); + translators["Search Translators"] = (yield translatorProvider.getAllForType("search")) + .sort((a, b) => a.label.localeCompare(b.label)); for (set in translators) { // Make a separator diff --git a/chrome/content/scaffold/monaco/monaco.html b/chrome/content/scaffold/monaco/monaco.html new file mode 100644 index 0000000000..22ccab4a1f --- /dev/null +++ b/chrome/content/scaffold/monaco/monaco.html @@ -0,0 +1,64 @@ + + + + + + Monaco + + + + + +
+ + + + + + \ No newline at end of file diff --git a/chrome/content/scaffold/scaffold.js b/chrome/content/scaffold/scaffold.js index 05ae6734b6..3d33a06ea1 100644 --- a/chrome/content/scaffold/scaffold.js +++ b/chrome/content/scaffold/scaffold.js @@ -52,37 +52,46 @@ function fix2028(str) { return str; } -var Scaffold = new function() { +var Scaffold = new function () { var _browser, _frames, _document; var _translatorsLoadedPromise; - var _translatorProvider = null + var _translatorProvider = null; + var _lastModifiedTime = 0; + var _lastHadFocus = true; var _editors = {}; var _propertyMap = { - 'textbox-translatorID':'translatorID', - 'textbox-label':'label', - 'textbox-creator':'creator', - 'textbox-target':'target', - 'textbox-minVersion':'minVersion', - 'textbox-maxVersion':'maxVersion', - 'textbox-priority':'priority', - 'textbox-target-all':'targetAll', - 'textbox-hidden-prefs':'hiddenPrefs' + 'textbox-translatorID': 'translatorID', + 'textbox-label': 'label', + 'textbox-creator': 'creator', + 'textbox-target': 'target', + 'textbox-minVersion': 'minVersion', + 'textbox-priority': 'priority', + 'textbox-target-all': 'targetAll', + 'textbox-hidden-prefs': 'hiddenPrefs' }; + var _linesOfMetadata = 15; + this.onLoad = async function (e) { - if(e.target !== document) return; + if (e.target !== document) return; _document = document; + + if (Zotero.isWin) { + // Hack to fix Windows toolbar + let toolbar = document.getElementById('zotero-toolbar'); + toolbar.className = ''; + } - _browser = document.getElementsByTagName('browser')[0]; + _browser = document.getElementById('browser'); _browser.addEventListener("pageshow", _updateFrames, true); _updateFrames(); let browserUrl = document.getElementById("browser-url"); - browserUrl.addEventListener('keypress', function(e) { + browserUrl.addEventListener('keypress', function (e) { if (e.keyCode == e.DOM_VK_RETURN) { _browser.loadURIWithFlags( browserUrl.value, @@ -90,31 +99,7 @@ var Scaffold = new function() { ); } }); - - var importWin = document.getElementById("editor-import").contentWindow; - var codeWin = document.getElementById("editor-code").contentWindow; - var testsWin = document.getElementById("editor-tests").contentWindow; - - _editors.import = importWin.editor; - _editors.code = codeWin.editor; - _editors.tests = testsWin.editor; - - for (let i in _editors) { - _editors[i].setTheme('ace/theme/monokai'); - } - - _editors.code.getSession().setMode(new codeWin.JavaScriptMode); - _editors.code.getSession().setUseSoftTabs(false); - // The first code line is preceeded by some metadata lines, such that - // the code lines start (usually) at line 15. - _editors.code.getSession().setOption("firstLineNumber", 15); - - _editors.tests.getSession().setUseWorker(false); - _editors.tests.getSession().setMode(new testsWin.JavaScriptMode); - _editors.tests.getSession().setUseSoftTabs(false); - - _editors.import.getSession().setMode(new importWin.TextMode); - + // Set font size from general pref Zotero.setFontSize(document.getElementById('scaffold-pane')); @@ -123,18 +108,7 @@ var Scaffold = new function() { if (size) { this.setFontSize(size); } - - // Set resize handler - _document.addEventListener("resize", this.onResize, false); - // Disable editing if external editor is enabled, enable when it is disabled - document.getElementById('checkbox-editor-external').addEventListener("command", - function() { - var external = document.getElementById('checkbox-editor-external').checked; - _editors.code.setReadOnly(external); - _editors.tests.setReadOnly(external); - }, true); - this.generateTranslatorID(); // Add List fields help menu entries for all other item types @@ -145,7 +119,9 @@ var Scaffold = new function() { if (primaryTypes.includes(type)) continue; var menuitem = document.createElement('menuitem'); menuitem.setAttribute('label', type); - menuitem.addEventListener('command', () => { Scaffold.addTemplate('templateNewItem', type) }); + menuitem.addEventListener('command', () => { + Scaffold.addTemplate('templateNewItem', type); + }); morePopup.appendChild(menuitem); } @@ -155,6 +131,48 @@ var Scaffold = new function() { return; } } + + var importWin = document.getElementById("editor-import").contentWindow; + var codeWin = document.getElementById("editor-code").contentWindow; + var testsWin = document.getElementById("editor-tests").contentWindow; + + _editors.import = importWin.editor; + _editors.importGlobal = importWin.globalEditor; + _editors.code = codeWin.editor; + _editors.codeGlobal = codeWin.globalEditor; + _editors.tests = testsWin.editor; + _editors.testsGlobal = testsWin.globalEditor; + + this.initImportEditor(); + this.initCodeEditor(); + this.initTestsEditor(); + + // Listen for Scaffold coming to the foreground and reload translators. + // We can't just set a focus listener on the because it'll fire + // when focus switches between the root window and any of the iframes. + setInterval(() => { + let hasFocus = document.hasFocus(); + if (_lastHadFocus == hasFocus) { + return; + } + + if (hasFocus) { + this.reloadTranslators(); + } + + _lastHadFocus = hasFocus; + }, 1000); + + + Scaffold_Translators.setLoadListener({ + onLoadBegin: () => { + document.getElementById('cmd_load').setAttribute('disabled', true); + }, + + onLoadComplete: () => { + document.getElementById('cmd_load').removeAttribute('disabled'); + } + }); _translatorsLoadedPromise = Scaffold_Translators.load(); _translatorProvider = Scaffold_Translators.getProvider(); @@ -210,89 +228,410 @@ var Scaffold = new function() { Scaffold_Translators.load(true); // async return path; }; - - this.onResize = function() { - // We try to let ACE resize itself - _editors.import.resize(); - _editors.code.resize(); - _editors.tests.resize(); - return true; - } + this.reloadTranslators = async function () { + Zotero.debug('Reloading translators quietly'); + let { numLoaded, numDeleted } = await Scaffold_Translators.load(true); + if (numLoaded) { + _logOutput(`${numLoaded} ${Zotero.Utilities.pluralize(numLoaded, 'translator')} updated.`); + } + if (numDeleted) { + _logOutput(`${numDeleted} ${Zotero.Utilities.pluralize(numDeleted, 'translator')} deleted.`); + } - this.setFontSize = function(size) { + let translatorID = document.getElementById('textbox-translatorID').value; + let modifiedTime = Scaffold_Translators.getModifiedTime(translatorID); + if (modifiedTime && modifiedTime > _lastModifiedTime) { + let ps = Services.prompt; + let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; + var index = ps.confirmEx(null, + "Scaffold", + "Translator code changed externally. Discard unsaved changes and reload?", + buttonFlags, + Zotero.getString('general.no'), + Zotero.getString('general.yes'), + null, null, {} + ); + if (index == 1) { + this.load(translatorID); + } + else { + _lastModifiedTime = modifiedTime; + } + } + }; + + this.initImportEditor = function () { + let monaco = _editors.importGlobal, editor = _editors.import; + monaco.editor.setModelLanguage(editor.getModel(), 'plaintext'); + }; + + this.initCodeEditor = async function () { + let monaco = _editors.codeGlobal, editor = _editors.code; + + editor.getModel().updateOptions({ + insertSpaces: false + }); + + editor.updateOptions({ + lineNumbers: num => num + _linesOfMetadata - 1, + // clicking links doesn't actually work, so disable them (for now) + links: false + }); + + monaco.languages.registerCodeLensProvider('javascript', this.createRunCodeLensProvider(monaco, editor)); + monaco.languages.registerHoverProvider('javascript', this.createHoverProvider(monaco, editor)); + + let tsLib = await Zotero.File.getContentsAsync( + OS.Path.join(Scaffold_Translators.getDirectory(), 'index.d.ts')); + let tsLibPath = 'ts:filename/index.d.ts'; + monaco.languages.typescript.javascriptDefaults.addExtraLib(tsLib, tsLibPath); + // this would allow peeking: + // monaco.editor.createModel(tsLib, 'typescript', monaco.Uri.parse(tsLibPath)); + // but it doesn't currently seem to work + }; + + this.initTestsEditor = function () { + let monaco = _editors.testsGlobal, editor = _editors.tests; + + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + allowComments: false, + trailingCommas: false, + schemaValidation: 'error' + }); + + monaco.editor.setModelLanguage(editor.getModel(), 'json'); + + editor.getModel().updateOptions({ + insertSpaces: false + }); + + editor.getModel().onDidChangeContent((_) => { + this.populateTests(); + }); + + editor.updateOptions({ + links: false + }); + + monaco.languages.registerCodeLensProvider('json', this.createTestCodeLensProvider(monaco, editor)); + }; + + this.createRunCodeLensProvider = function (monaco, editor) { + let runMethod = editor.addCommand(0, (_ctx, method) => this.run(method), ''); + + return { + provideCodeLenses: (model, _token) => { + let methodRe = '(async\\s+)?\\bfunction\\s+(detect|do)(Web|Import|Export|Search)\\s*\\('; + let lenses = []; + + let matches = model.findMatches( + methodRe, + /* searchOnlyEditableRange: */ false, + /* isRegex: */ true, + /* matchCase: */ true, + /* wordSeparators: */ null, + /* captureMatches: */ true + ); + + for (let match of matches) { + let line = match.matches[0]; + let methodName = line.match(/function\s+(\w*)/)[1]; + lenses.push({ + range: match.range, + command: { + id: runMethod, + title: `Run ${methodName}`, + arguments: [methodName] + } + }); + } + + return lenses; + }, + resolveCodeLens: (_model, codeLens, _token) => codeLens + }; + }; + + this.createHoverProvider = function (monaco, _editor) { + let uuidRe = `(["'])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\1`; + let types = Zotero.ItemTypes.getTypes().map(t => t.name); + let itemTypeRe = `(["'])(${types.join('|')})\\1`; + + return { + provideHover: (model, position) => { + let lineRange = new monaco.Range( + position.lineNumber, + model.getLineMinColumn(position.lineNumber), + position.lineNumber, + model.getLineMaxColumn(position.lineNumber) + ); + + let matches = model.findMatches( + uuidRe, + /* searchScope: */ lineRange, + /* isRegex: */ true, + /* matchCase: */ true, + /* wordSeparators: */ null, + /* captureMatches: */ true + ); + + for (let uuidMatch of matches) { + if (!uuidMatch.range.containsPosition(position)) continue; + let translator = _translatorProvider.get(uuidMatch.matches[2]); + + if (translator) { + let metadataJSON = JSON.stringify(translator.metadata, null, '\t'); + + return { + range: uuidMatch.range, + contents: [ + { value: `**${translator.label}**` }, + { value: '```json\n' + metadataJSON + '\n```' } + ] + }; + } + } + + matches = model.findMatches( + itemTypeRe, + /* searchScope: */ lineRange, + /* isRegex: */ true, + /* matchCase: */ true, + /* wordSeparators: */ null, + /* captureMatches: */ true + ); + + for (let itemTypeMatch of matches) { + if (!itemTypeMatch.range.containsPosition(position)) continue; + + let fieldsJSON = this.listFieldsForItemType(itemTypeMatch.matches[2]); + return { + range: itemTypeMatch.range, + contents: [ + { value: '```json\n' + fieldsJSON + '\n```' } + ] + }; + } + + return null; + } + }; + }; + + this.createTestCodeLensProvider = function (monaco, editor) { + let runTestsCommand = editor.addCommand( + 0, + (_ctx, testIndices) => { + let tests; + try { + tests = JSON.parse(editor.getValue()); + } + catch (e) { + _logOutput('Error parsing tests:\n' + e); + } + + if (testIndices) { + tests = testIndices.map(index => tests[index]); + } + + this.runTests(tests); + }, + ''); + + let updateTestsCommand = editor.addCommand( + 0, + async (_ctx, testIndices) => { + testIndices = testIndices || Object.keys(allTests); + + try { + var allTests = JSON.parse(editor.getValue()); + } + catch (e) { + _logOutput('Error parsing tests:\n' + e); + return; + } + + let tests = testIndices.map(index => allTests[index]); + + await this.updateTests(tests, + (newTest) => { + allTests[testIndices.shift()] = newTest; + }); + + _writeTestsToPane(allTests); + }, + ''); + + return { + provideCodeLenses: (model, _token) => { + let lenses = []; + + let firstChar = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1 + }; + lenses.push({ + range: firstChar, + command: { + id: runTestsCommand, + title: 'Run All' + } + }); + lenses.push({ + range: firstChar, + command: { + id: updateTestsCommand, + title: 'Run and Update All' + } + }); + + for (let [testIndex, range] of _findTestObjectTops(monaco, model).entries()) { + lenses.push({ + range: range, + command: { + id: runTestsCommand, + title: 'Run', + arguments: [[testIndex]] + } + }); + + lenses.push({ + range: range, + command: { + id: updateTestsCommand, + title: 'Run and Update', + arguments: [[testIndex]] + } + }); + } + + return lenses; + }, + resolveCodeLens: (_model, codeLens, _token) => codeLens + }; + }; + + this.updateModelMarkers = function (translatorPath) { + runESLint(translatorPath) + .then(eslintOutputToModelMarkers) + .then(markers => _editors.codeGlobal.editor.setModelMarkers(_editors.code.getModel(), 'eslint', markers)); + }; + + this.setFontSize = function (size) { var sizeWithPX = size + 'px'; - _editors.import.setOptions({fontSize: sizeWithPX}); - _editors.code.setOptions({fontSize: sizeWithPX}); - _editors.tests.setOptions({fontSize: sizeWithPX}); + _editors.import.updateOptions({ fontSize: size + 1 }); // editor font needs to be a little bigger + _editors.code.updateOptions({ fontSize: size + 1 }); + _editors.tests.updateOptions({ fontSize: size + 1 }); document.getElementById("scaffold-pane").style.fontSize = sizeWithPX; - if (size==11) { + if (size == 11) { // for the default value 11, clear the prefs Zotero.Prefs.clear('scaffold.fontSize'); - } else { + } + else { Zotero.Prefs.set("scaffold.fontSize", size); } - } + }; - this.increaseFontSize = function() { + this.increaseFontSize = function () { var currentSize = Zotero.Prefs.get("scaffold.fontSize") || 11; - this.setFontSize(currentSize+2); - } - this.decreaseFontSize = function() { + this.setFontSize(currentSize + 2); + }; + this.decreaseFontSize = function () { var currentSize = Zotero.Prefs.get("scaffold.fontSize") || 11; - this.setFontSize(currentSize-2); - } + this.setFontSize(currentSize - 2); + }; + + this.newTranslator = async function () { + _logOutput('Saving translator and resetting...'); + await this.save(); + + this.generateTranslatorID(); + document.getElementById('textbox-label').value = 'Untitled'; + document.getElementById('textbox-creator').value + = document.getElementById('textbox-target').value + = document.getElementById('textbox-target-all').value + = document.getElementById('textbox-configOptions').value + = document.getElementById('textbox-displayOptions').value + = document.getElementById('textbox-hidden-prefs').value + = ''; + document.getElementById('textbox-minVersion').value = '5.0'; + document.getElementById('textbox-priority').value = '100'; + document.getElementById('checkbox-import').checked = false; + document.getElementById('checkbox-export').checked = false; + document.getElementById('checkbox-web').checked = true; + document.getElementById('checkbox-search').checked = false; + + _editors.code.setValue(''); + _editors.tests.setValue(''); + + this.populateTests(); + + document.getElementById('textbox-label').focus(); + _showTab('metadata'); + }; /* * load translator */ - this.load = Zotero.Promise.coroutine(function* (translatorID) { + this.load = async function (translatorID) { + await _translatorsLoadedPromise; + var translator = false; if (translatorID === undefined) { var io = {}; io.translatorProvider = _translatorProvider; - io.url = _getDocument().location.href; + io.url = _getDocument()?.location.href || 'about:blank'; io.rootUrl = _browser.contentDocument.location.href; window.openDialog("chrome://scaffold/content/load.xul", - "_blank","chrome,modal", io); + "_blank", "chrome,modal", io); translator = io.dataOut; - } else { - yield _translatorsLoadedPromise; + } + else { translator = _translatorProvider.get(translatorID); } // No translator was selected in the dialog. - if (!translator) return false; + if (!translator) return; - for(var id in _propertyMap) { + for (var id in _propertyMap) { document.getElementById(id).value = translator[_propertyMap[id]] || ""; } //Strip JSON metadata - var code = yield _translatorProvider.getCodeForTranslator(translator); + var code = await _translatorProvider.getCodeForTranslator(translator); var lastUpdatedIndex = code.indexOf('"lastUpdated"'); var header = code.substr(0, lastUpdatedIndex + 50); var m = /^\s*{[\S\s]*?}\s*?[\r\n]+/.exec(header); - var fixedCode = code.substr(m[0].length); + var fixedCode = code.substr(m[0].length); // adjust the first line number when there are an unusual number of metadata lines - var linesOfMetadata = m[0].split('\n').length; - _editors.code.getSession().setOption("firstLineNumber", linesOfMetadata); - // load tests into test editing pane, but clear it first - _editors["tests"].getSession().setValue(''); - _loadTests(fixedCode); - // and remove them from the translator code - var testStart = fixedCode.indexOf("/** BEGIN TEST CASES **/"); - var testEnd = fixedCode.indexOf("/** END TEST CASES **/"); - if (testStart !== -1 && testEnd !== -1) - fixedCode = fixedCode.substr(0,testStart) + fixedCode.substr(testEnd+23); - + _linesOfMetadata = m[0].split('\n').length; + // load tests into test editing pane + _loadTestsFromTranslator(fixedCode); + // clear selection + _editors.tests.setSelection({ + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: 1 + }); + // Set up the test running pane this.populateTests(); + // remove tests from the translator code before loading into the code editor + var testStart = fixedCode.indexOf("/** BEGIN TEST CASES **/"); + var testEnd = fixedCode.indexOf("/** END TEST CASES **/"); + if (testStart !== -1 && testEnd !== -1) fixedCode = fixedCode.substr(0, testStart) + fixedCode.substr(testEnd + 23); + // Convert whitespace to tabs - _editors.code.getSession().setValue(normalizeWhitespace(fixedCode)); + _editors.code.setValue(normalizeWhitespace(fixedCode)); // Then go to line 1 - _editors.code.gotoLine(1); + _editors.code.setPosition({ lineNumber: 1, column: 1 }); // Reset configOptions and displayOptions before loading document.getElementById('textbox-configOptions').value = ''; @@ -303,7 +642,7 @@ var Scaffold = new function() { if (configOptions != '{}') { document.getElementById('textbox-configOptions').value = configOptions; } - } + } if (translator.displayOptions) { let displayOptions = JSON.stringify(translator.displayOptions); if (displayOptions != '{}') { @@ -314,21 +653,15 @@ var Scaffold = new function() { // get translator type; might as well have some fun here var type = translator.translatorType; var types = ["import", "export", "web", "search"]; - for(var i=2; i<=16; i*=2) { + for (var i = 2; i <= 16; i *= 2) { var mod = type % i; - document.getElementById('checkbox-'+types.shift()).checked = !!mod; - if(mod) type -= mod; - } - - // get browser support - var browserSupport = translator.browserSupport; - if(!browserSupport) browserSupport = "g"; - const browsers = {gecko:"g", chrome:"c", safari:"s", ie:"i", bookmarklet:"b", server:"v"}; - for (var browser in browsers) { - document.getElementById('checkbox-'+browser).checked = browserSupport.indexOf(browsers[browser]) !== -1; + document.getElementById('checkbox-' + types.shift()).checked = !!mod; + if (mod) type -= mod; } - }); + this.updateModelMarkers(translator.path); + _lastModifiedTime = new Date().getTime(); + }; function _getMetadataObject() { var metadata = { @@ -337,7 +670,7 @@ var Scaffold = new function() { creator: document.getElementById('textbox-creator').value, target: document.getElementById('textbox-target').value, minVersion: document.getElementById('textbox-minVersion').value, - maxVersion: document.getElementById('textbox-maxVersion').value, + maxVersion: '', priority: parseInt(document.getElementById('textbox-priority').value) }; @@ -350,59 +683,41 @@ var Scaffold = new function() { } if (document.getElementById('textbox-configOptions').value) { - metadata.configOptions = JSON.parse(document.getElementById('textbox-configOptions').value); + metadata.configOptions = JSON.parse(document.getElementById('textbox-configOptions').value); } if (document.getElementById('textbox-displayOptions').value) { - metadata.displayOptions = JSON.parse(document.getElementById('textbox-displayOptions').value); + metadata.displayOptions = JSON.parse(document.getElementById('textbox-displayOptions').value); } // no option for this metadata.inRepository = true; metadata.translatorType = 0; - if(document.getElementById('checkbox-import').checked) { + if (document.getElementById('checkbox-import').checked) { metadata.translatorType += 1; } - if(document.getElementById('checkbox-export').checked) { + if (document.getElementById('checkbox-export').checked) { metadata.translatorType += 2; } - if(document.getElementById('checkbox-web').checked) { + if (document.getElementById('checkbox-web').checked) { metadata.translatorType += 4; } - if(document.getElementById('checkbox-search').checked) { + if (document.getElementById('checkbox-search').checked) { metadata.translatorType += 8; } - if (document.getElementById('checkbox-web').checked) { - // save browserSupport only for web tranlsators - metadata.browserSupport = ""; - if(document.getElementById('checkbox-gecko').checked) { - metadata.browserSupport += "g"; - } - if(document.getElementById('checkbox-chrome').checked) { - metadata.browserSupport += "c"; - } - if(document.getElementById('checkbox-safari').checked) { - metadata.browserSupport += "s"; - } - if(document.getElementById('checkbox-ie').checked) { - metadata.browserSupport += "i"; - } - if(document.getElementById('checkbox-bookmarklet').checked) { - metadata.browserSupport += "b"; - } - if(document.getElementById('checkbox-server').checked) { - metadata.browserSupport += "v"; - } - } + if (document.getElementById('checkbox-web').checked) { + // save browserSupport only for web tranlsators + metadata.browserSupport = "gcsibv"; + } var date = new Date(); metadata.lastUpdated = date.getUTCFullYear() - +"-"+Zotero.Utilities.lpad(date.getUTCMonth()+1, '0', 2) - +"-"+Zotero.Utilities.lpad(date.getUTCDate(), '0', 2) - +" "+Zotero.Utilities.lpad(date.getUTCHours(), '0', 2) - +":"+Zotero.Utilities.lpad(date.getUTCMinutes(), '0', 2) - +":"+Zotero.Utilities.lpad(date.getUTCSeconds(), '0', 2); + + "-" + Zotero.Utilities.lpad(date.getUTCMonth() + 1, '0', 2) + + "-" + Zotero.Utilities.lpad(date.getUTCDate(), '0', 2) + + " " + Zotero.Utilities.lpad(date.getUTCHours(), '0', 2) + + ":" + Zotero.Utilities.lpad(date.getUTCMinutes(), '0', 2) + + ":" + Zotero.Utilities.lpad(date.getUTCSeconds(), '0', 2); return metadata; } @@ -410,10 +725,12 @@ var Scaffold = new function() { /* * save translator to database */ - this.save = Zotero.Promise.coroutine(function* (updateZotero) { - var code = _editors.code.getSession().getValue(); - var tests = _editors.tests.getSession().getValue(); - code += tests; + this.save = async function (updateZotero) { + var code = _editors.code.getValue(); + var tests = _editors.tests.getValue().trim(); + if (!tests || tests == '[]') tests = '[\n]'; // eslint wants a line break between the brackets + + code += '/** BEGIN TEST CASES **/\nvar testCases = ' + tests + '\n/** END TEST CASES **/'; var metadata = _getMetadataObject(); if (metadata.label === "Untitled") { @@ -421,14 +738,34 @@ var Scaffold = new function() { return; } - yield _translatorProvider.save(metadata, code); + var path = await _translatorProvider.save(metadata, code); if (updateZotero) { - yield Zotero.Translators.save(metadata, code); - yield Zotero.Translators.reinit(); + await Zotero.Translators.save(metadata, code); + await Zotero.Translators.reinit(); } - }); - + + _lastModifiedTime = new Date().getTime(); + + this.updateModelMarkers(path); + this.reloadTranslators(); + }; + + /** + * If an editor is focused, trigger `editorTrigger` in it. + * Otherwise, run `fallbackCommand`. + */ + this.trigger = function (editorTrigger, fallbackCommand) { + let activeEditor = _editors[_getActiveEditorName()]; + if (activeEditor) { + activeEditor.trigger('Scaffold.trigger', editorTrigger); + } + else { + // editMenuOverlay.js + goDoCommand(fallbackCommand); + } + }; + this.handleTabSelect = function (event) { // Focus editor when switching to tab var tab = event.target.selectedItem.id.match(/^tab-(.+)$/)[1]; @@ -436,92 +773,153 @@ var Scaffold = new function() { case 'import': case 'code': case 'tests': - _editors[tab].focus(); + // the select event's default behavior is to focus the selected tab. + // we don't want to prevent *all* of the event's default behavior, + // but we do want to focus the editor instead of the tab. + // so this stupid hack waits 10 ms for event processing to finish + // before focusing the editor. + setTimeout(() => { + document.getElementById(`editor-${tab}`).focus(); + _editors[tab].focus(); + }, 10); break; } + + let codeTabBroadcaster = document.getElementById('code-tab-only'); + if (tab == 'code') { + codeTabBroadcaster.removeAttribute('disabled'); + } + else { + codeTabBroadcaster.setAttribute('disabled', true); + } + }; + + this.handleTestSelect = function (event) { + let selected = event.target.selectedItems[0]; + if (!selected) return; + + let editImport = document.getElementById('testing_editImport'); + let openURL = document.getElementById('testing_openURL'); + if (selected.getUserData('test-type') == 'web') { + editImport.setAttribute('disabled', true); + openURL.removeAttribute('disabled'); + } + else { + editImport.removeAttribute('disabled'); + openURL.setAttribute('disabled', true); + } + }; + + this.listFieldsForItemType = function (itemType) { + var outputObject = {}; + outputObject.itemType = Zotero.ItemTypes.getName(itemType); + var typeID = Zotero.ItemTypes.getID(itemType); + var fieldList = Zotero.ItemFields.getItemTypeFields(typeID); + for (let field of fieldList) { + var key = Zotero.ItemFields.getName(field); + outputObject[key] = ""; + } + var creatorList = Zotero.CreatorTypes.getTypesForItemType(typeID); + var creators = []; + for (let creatorType of creatorList) { + creators.push({ firstName: "", lastName: "", creatorType: creatorType.name, fieldMode: true }); + } + outputObject.creators = creators; + outputObject.attachments = [{ url: "", document: "", title: "", mimeType: "" }]; + outputObject.tags = [{ tag: "" }]; + outputObject.notes = [{ note: "" }]; + outputObject.seeAlso = []; + return JSON.stringify(outputObject, null, '\t'); }; /* * add template code */ - this.addTemplate = Zotero.Promise.coroutine(function* (template, second) { - switch(template) { + this.addTemplate = async function (template, second) { + switch (template) { case "templateNewItem": - var outputObject = {}; - outputObject.itemType = Zotero.ItemTypes.getName(second); - var typeID = Zotero.ItemTypes.getID(second); - var fieldList = Zotero.ItemFields.getItemTypeFields(typeID); - for (var i=0; i t.name); document.getElementById('output').value = JSON.stringify(typeNames, null, '\t'); break; - case "shortcuts": - var value = Zotero.File.getContentsFromURL(`chrome://scaffold/content/templates/shortcuts.txt`); - document.getElementById('output').value = value; - break - default: + default: { //newWeb, scrapeEM, scrapeRIS, scrapeBibTeX, scrapeMARC //These names in the XUL file have to match the file names in template folder. - var cursorPos = _editors.code.getSession().selection.getCursor(); - var value = Zotero.File.getContentsFromURL(`chrome://scaffold/content/templates/${template}.js`); - _editors.code.getSession().insert(cursorPos, value); - break + let value = Zotero.File.getContentsFromURL(`chrome://scaffold/content/templates/${template}.js`); + let cursorOffset = value.indexOf('$$CURSOR$$'); + value = value.replace('$$CURSOR$$', ''); + + var selection = _editors.code.getSelection(); + var id = { major: 1, minor: 1 }; + var op = { identifier: id, range: selection, text: value, forceMoveMarkers: true }; + _editors.code.executeEdits("addTemplate", [op]); + + if (cursorOffset != -1) { + _editors.code.setPosition(_editors.code.getModel().getPositionAt(cursorOffset)); + } + + break; + } } - }); + }; /* * run translator */ - this.run = Zotero.Promise.coroutine(function* (functionToRun) { + this.run = async function (functionToRun) { if (document.getElementById('textbox-label').value == 'Untitled') { - alert("Translator title not set"); + _logOutput("Translator title not set"); return; } _clearOutput(); - if(document.getElementById('checkbox-editor-external').checked) { - // We don't save the translator-- we reload it instead - var translatorID = document.getElementById('textbox-translatorID').value; - yield this.load(translatorID); - } - // Handle generic call run('detect'), run('do') if (functionToRun == "detect" || functionToRun == "do") { - var isWeb = document.getElementById('checkbox-web').checked; - functionToRun += isWeb ? "Web" : "Import"; + if (document.getElementById('checkbox-web').checked + && _browser.contentWindow.location.href != 'about:blank') { + functionToRun += 'Web'; + } + else if (document.getElementById('checkbox-import').checked + && _editors.import.getValue().trim()) { + functionToRun += 'Import'; + } + else if (document.getElementById('checkbox-export').checked + && functionToRun == 'do') { + functionToRun += 'Export'; + } + else if (document.getElementById('checkbox-search').checked + && _editors.import.getValue().trim()) { + functionToRun += 'Search'; + } + else { + _logOutput('No appropriate detect/do function to run'); + return; + } } + + _logOutput(`Running ${functionToRun}`); - if (functionToRun == "detectWeb" || functionToRun == "doWeb") { - _run(functionToRun, _getDocument(), _selectItems, _myItemDone, _translators); - } else if (functionToRun == "detectImport" || functionToRun == "doImport") { - _run(functionToRun, _getImport(), _selectItems, _myItemDone, _translatorsImport); + let input = _getInput(functionToRun); + + if (functionToRun.endsWith('Export')) { + let numItems = Zotero.getActiveZoteroPane().getSelectedItems().length; + _logOutput(`Exporting ${numItems} item${numItems == 1 ? '' : 's'} selected in library`); + _run(functionToRun, input, _selectItems, () => {}, _getTranslatorsHandler(functionToRun), _myExportDone); } - }); + else { + _run(functionToRun, input, _selectItems, _myItemDone, _getTranslatorsHandler(functionToRun)); + } + }; /* * run translator in given mode with given input */ - async function _run(functionToRun, input, selectItems, itemDone, detectHandler, done) { + function _run(functionToRun, input, selectItems, itemDone, detectHandler, done) { if (functionToRun == "detectWeb" || functionToRun == "doWeb") { var translate = new Zotero.Translate.Web(); - var utilities = new Zotero.Utilities.Translate(translate); if (!_testTargetRegex(input)) { _logOutput("Target did not match " + _getDocumentURL(input)); if (done) { @@ -537,20 +935,29 @@ var Scaffold = new function() { _getDocumentURL(input), input.cookie )); - } else if (functionToRun == "detectImport" || functionToRun == "doImport") { - var translate = new Zotero.Translate.Import(); + } + else if (functionToRun == "detectImport" || functionToRun == "doImport") { + translate = new Zotero.Translate.Import(); translate.setString(input); } + else if (functionToRun == "doExport") { + translate = new Zotero.Translate.Export(); + translate.setItems(input); + } + else if (functionToRun == "detectSearch" || functionToRun == "doSearch") { + translate = new Zotero.Translate.Search(); + translate.setSearch(input); + } translate.setTranslatorProvider(_translatorProvider); translate.setHandler("error", _error); translate.setHandler("debug", _debug); if (done) { translate.setHandler("done", done); } - - if (functionToRun == "detectWeb") { - // get translator - var translator = _getTranslatorFromPane(); + + // get translator + var translator = _getTranslatorFromPane(); + if (functionToRun.startsWith('detect')) { // don't let target prevent translator from operating translator.target = null; // generate sandbox @@ -560,42 +967,17 @@ var Scaffold = new function() { translate._foundTranslators = []; translate._currentState = "detect"; translate._detect(); - } else if (functionToRun == "doWeb") { - // get translator - var translator = _getTranslatorFromPane(); + } + else { // don't let the detectCode prevent the translator from operating translator.detectCode = null; translate.setTranslator(translator); translate.setHandler("select", selectItems); translate.clearHandlers("itemDone"); - translate.setHandler("itemDone", itemDone); - translate.translate({ - // disable saving to database - libraryID: false - }); - } else if (functionToRun == "detectImport") { - // get translator - var translator = _getTranslatorFromPane(); - // don't let target prevent translator from operating - translator.target = null; - // generate sandbox - translate.setHandler("translators", detectHandler); - // internal hack to call detect on this translator - translate._potentialTranslators = [translator]; - translate._foundTranslators = []; - translate._currentState = "detect"; - translate._detect(); - } else if (functionToRun == "doImport") { - // get translator - var translator = _getTranslatorFromPane(); - // don't let the detectCode prevent the translator from operating - translator.detectCode = null; - translate.setTranslator(translator); - translate.clearHandlers("itemDone"); translate.clearHandlers("collectionDone"); translate.setHandler("itemDone", itemDone); - translate.setHandler("collectionDone", function(obj, collection) { - _logOutput("Collection: "+ collection.name + ", "+collection.children.length+" items"); + translate.setHandler("collectionDone", function (obj, collection) { + _logOutput("Collection: " + collection.name + ", " + collection.children.length + " items"); }); translate.translate({ // disable saving to database @@ -605,21 +987,21 @@ var Scaffold = new function() { } this.runTranslatorOrTests = async function () { - var tabs = document.getElementById('tabs'); - if (tabs.selectedItem.id == 'tab-testing') { + if (document.getElementById('tabs').selectedItem.id == 'tab-tests' + && document.activeElement.id == 'testing-listbox') { this.runSelectedTests(); } else { this.run('do'); } - } + }; /* * generate translator GUID */ - this.generateTranslatorID = function() { + this.generateTranslatorID = function () { document.getElementById("textbox-translatorID").value = _generateGUID(); - } + }; /** * Test target regular expression against document URL and log the result @@ -648,9 +1030,9 @@ var Scaffold = new function() { * called to select items */ function _selectItems(obj, itemList) { - var io = { dataIn:itemList, dataOut:null } - var newDialog = window.openDialog("chrome://zotero/content/ingester/selectitems.xul", - "_blank","chrome,modal,centerscreen,resizable=yes", io); + var io = { dataIn: itemList, dataOut: null }; + window.openDialog("chrome://zotero/content/ingester/selectitems.xul", + "_blank", "chrome,modal,centerscreen,resizable=yes", io); return io.dataOut; } @@ -658,12 +1040,9 @@ var Scaffold = new function() { /* * called if an error occurs */ - function _error(obj, error) { - if(error && error.lineNumber && - error.fileName == obj.translator[0].label ) { - var lines = _editors.code.getSession().getOption("firstLineNumber"); - _editors.code.gotoLine(error.lineNumber-lines+1); // subtract the metadata lines - } + function _error(_obj, _error) { + // stub: this handler doesn't actually seem to get called by the current + // translation architecture when a translator throws } /* @@ -677,35 +1056,42 @@ var Scaffold = new function() { * logs item output */ function _myItemDone(obj, item) { - Zotero.debug("Item returned"); - item = _sanitizeItem(item); - _logOutput("Returned item:\n"+Zotero_TranslatorTester._generateDiff(item, Zotero_TranslatorTester._sanitizeItem(item, true))); + _logOutput("Returned item:\n" + Zotero_TranslatorTester._generateDiff(item, Zotero_TranslatorTester._sanitizeItem(item, true))); } /* - * prints information from detectCode to window + * logs string output */ - function _translators(obj, translators) { - if(translators && translators.length != 0) { - _logOutput('detectWeb returned type "'+translators[0].itemType+'"'); - } else { - _logOutput('detectWeb did not match'); + function _myExportDone({ string }, worked) { + if (worked) { + Zotero.debug("Export successful"); + _logOutput("Returned string:\n" + string); } - - } + else { + Zotero.debug("Export failed"); + } + } /* - * prints information from detectCode to window, for import + * returns a 'translators' handler that prints information from detectCode to window */ - function _translatorsImport(obj, translators) { - if(translators && translators.length != 0 && translators[0].itemType) { - _logOutput('detectImport matched'); - } else { - _logOutput('detectImport did not match'); - } - } + function _getTranslatorsHandler(fnName) { + return (obj, translators) => { + if (translators && translators.length != 0) { + if (translators[0].itemType === true) { + _logOutput(`${fnName} matched`); + } + else { + _logOutput(`${fnName} returned type "${translators[0].itemType}"`); + } + } + else { + _logOutput(`${fnName} did not match`); + } + }; + } /* * logs debug info (instead of console) @@ -714,15 +1100,15 @@ var Scaffold = new function() { var date = new Date(); var output = document.getElementById('output'); - if(typeof string != "string") { + if (typeof string != "string") { string = fix2028(Zotero.Utilities.varDump(string)); } - if(output.value) output.value += "\n"; + if (output.value) output.value += "\n"; output.value += Zotero.Utilities.lpad(date.getHours(), '0', 2) - +":"+Zotero.Utilities.lpad(date.getMinutes(), '0', 2) - +":"+Zotero.Utilities.lpad(date.getSeconds(), '0', 2) - +" "+string.replace(/\n/g, "\n "); + + ":" + Zotero.Utilities.lpad(date.getMinutes(), '0', 2) + + ":" + Zotero.Utilities.lpad(date.getSeconds(), '0', 2) + + " " + string.replace(/\n/g, "\n "); // move to end output.inputField.scrollTop = output.inputField.scrollHeight; } @@ -731,25 +1117,71 @@ var Scaffold = new function() { * gets import text for import translator */ function _getImport() { - var text = _editors.import.getSession().getValue(); + var text = _editors.import.getValue(); return text; } + /* + * gets items to export for export translator + */ + function _getExport() { + return Zotero.getActiveZoteroPane().getSelectedItems(); + } + + /* + * gets search JSON object for search translator + */ + function _getSearch() { + return JSON.parse(_getImport()); + } + + /* + * gets appropriate input for the given type/method + */ + function _getInput(typeOrMethod) { + typeOrMethod = typeOrMethod.toLowerCase(); + if (typeOrMethod.endsWith('web')) { + return _getDocument(); + } + else if (typeOrMethod.endsWith('import')) { + return _getImport(); + } + else if (typeOrMethod.endsWith('export')) { + return _getExport(); + } + else if (typeOrMethod.endsWith('search')) { + return _getSearch(); + } + return null; + } + /* * transfers metadata to the translator object * Replicated from translator.js */ function _metaToTranslator(translator, metadata) { - var props = ["translatorID", "translatorType", "label", "creator", "target", - "minVersion", "maxVersion", "priority", "lastUpdated", "inRepository", "configOptions", - "displayOptions", "browserSupport", "targetAll", "hiddenPrefs"]; - for (var i=0; i2.1 - if(Zotero.Translator.RUN_MODE_IN_BROWSER) { + if (Zotero.Translator.RUN_MODE_IN_BROWSER) { translator.runMode = Zotero.Translator.RUN_MODE_IN_BROWSER; } return translator; } + /* + * loads the translator's tests from the translator code + */ + function _loadTestsFromTranslator(code) { + var testStart = code.indexOf("/** BEGIN TEST CASES **/"); + var testEnd = code.indexOf("/** END TEST CASES **/"); + if (testStart !== -1 && testEnd !== -1) { + code = code.substring(testStart + 24, testEnd); + } + + code = code.replace(/var testCases = /, '').trim(); + // The JSON parser doesn't like final semicolons + if (code.lastIndexOf(';') == code.length - 1) { + code = code.slice(0, -1); + } + + try { + var testObject = JSON.parse(code); + } + catch (e) { + testObject = []; + } + + // We don't use _writeTestsToPane here because we want to avoid _stringifyTests, + // which assumes valid test data and will choke on/incorrectly "fix" + // weird inputs that the user might want to fix manually. + _writeToEditor(_editors.tests, JSON.stringify(testObject, null, "\t")); + } + /* * loads the translator's tests from the pane */ - function _loadTests(code) { - var testStart = code.indexOf("/** BEGIN TEST CASES **/"); - var testEnd = code.indexOf("/** END TEST CASES **/"); - if (testStart !== -1 && testEnd !== -1) { - test = code.substring(testStart + 24, testEnd); - test = test.replace(/var testCases = /,'').trim(); - // The JSON parser doesn't like final semicolons - if (test.lastIndexOf(';') == (test.length-1)) - test = test.slice(0,-1); - try { - var testObject = JSON.parse(test); - _writeTests(JSON.stringify(testObject, null, "\t")); // Don't modify current tests - return testObject; - } catch (e) { - _logOutput("Exception parsing test JSON:\n\n" + e); - return false; - } - } else { - return false; + function _loadTestsFromPane() { + try { + return JSON.parse(_editors.tests.getValue().trim() || '[]'); } + catch (e) { + return null; + } + } + + /** + * Write text to an editor, overwriting its current value. + * This operation can be undone. + */ + function _writeToEditor(editor, text) { + editor.executeEdits('_writeToEditor', [{ + range: editor.getModel().getFullModelRange(), + text + }]); } /* * writes tests back into the translator */ - function _writeTests(testString) { - var code = "/** BEGIN TEST CASES **/\nvar testCases = " - + testString + "\n/** END TEST CASES **/"; - _editors["tests"].getSession().setValue(code); - } - - /* clear tests pane */ - function _clearTests() { - var listbox = document.getElementById("testing-listbox"); - var count = listbox.itemCount; - while(count-- > 0){ - listbox.removeItemAt(0); - } + function _writeTestsToPane(tests) { + _writeToEditor(_editors.tests, _stringifyTests(tests)); } /* turns an item into a test-safe item @@ -824,9 +1272,8 @@ var Scaffold = new function() { function _sanitizeItem(item) { // Clear attachment document objects if (item && item.attachments && item.attachments.length) { - for (var i=0; i 1 ? "\n]" : ']'); } - if(!value.itemType) { + if (!value.itemType) { // Not a Zotero.Item object let str = '{'; - - function processRow(key, value) { - let val = _stringifyTests(value, level+1); - if(val === undefined) return; - - val = val.replace(/\n/g, "\n\t"); - return JSON.stringify(''+key) + ': ' + val; - } - + if (level < 2 && value.items) { // Test object. Arrange properties in set order let order = ['type', 'url', 'input', 'defer', 'items']; - for (let i=0; i 1 ? ',' : '') + '\n\t' + val; } - } else { + } + else { for (let i in value) { let val = processRow(i, value[i]); if (val === undefined) continue; @@ -910,35 +1358,44 @@ var Scaffold = new function() { } // Zotero.Item object - const topFields = ['itemType', 'title', 'caseName', 'nameOfAct', 'subject', - 'creators', 'date', 'dateDecided', 'issueDate', 'dateEnacted']; + const topFields = ['itemType', + 'title', + 'caseName', + 'nameOfAct', + 'subject', + 'creators', + 'date', + 'dateDecided', + 'issueDate', + 'dateEnacted']; const bottomFields = ['attachments', 'tags', 'notes', 'seeAlso']; let otherFields = Object.keys(value); let presetFields = topFields.concat(bottomFields); - for(let i=0; i 1 ? ',':'') + "\n\t" + JSON.stringify(fields[i]) + ': ' + val; + str += (str.length > 1 ? ',' : '') + "\n\t" + JSON.stringify(fields[i]) + ': ' + val; } return str + "\n}"; @@ -948,170 +1405,191 @@ var Scaffold = new function() { * adds a new test from the current input/translator * web or import only for now */ - this.newTestFromCurrent = function(type) { + this.saveTestFromCurrent = async function (type) { + _logOutput(`Creating ${type} test...`); + + try { + let test = await this.constructTestFromCurrent(type); + _writeTestsToPane([..._loadTestsFromPane(), test]); + } + catch (e) { + _logOutput('Creation failed'); + return; + } + + _showTab('tests'); + let listBox = document.getElementById('testing-listbox'); + listBox.selectedIndex = listBox.getRowCount() - 1; + listBox.focus(); + }; + + this.constructTestFromCurrent = async function (type) { _clearOutput(); - var input, label; - if (type == "web" && !document.getElementById('checkbox-web').checked) { - _logOutput("Current translator isn't a web translator"); - return false; - } else if (type == "import" && !document.getElementById('checkbox-import').checked) { - _logOutput("Current translator isn't an import translator"); - return false; + if ((type === "web" && !document.getElementById('checkbox-web').checked) + || (type === "import" && !document.getElementById('checkbox-import').checked) + || (type === "search" && !document.getElementById('checkbox-search').checked)) { + _logOutput(`Translator does not support ${type} tests`); + return Promise.reject(new Error()); } - if (type == "web") { - input = _getDocument(); - label = Zotero.Proxies.proxyToProper(input.location.href); - } else if (type == "import") { - input = _getImport(); - label = input; - } else { - return false; + if (type == 'export') { + return Promise.reject(new Error(`Test of type export cannot be created`)); } - var listbox = document.getElementById("testing-listbox"); - var listitem = document.createElement("listitem"); - var listcell = document.createElement("listcell"); - listcell.setAttribute("label", label); - listitem.appendChild(listcell); - listcell = document.createElement("listcell"); - listcell.setAttribute("label", "Creating..."); - listitem.appendChild(listcell); - listbox.appendChild(listitem); + let input = _getInput(type); if (type == "web") { - // Creates the test. The test isn't saved yet! let tester = new Zotero_TranslatorTester( _getTranslatorFromPane(), type, _debug, _translatorProvider ); - tester.newTest(input, function (obj, newTest) { // "done" handler for do - if(newTest) { - listcell.setAttribute("label", "New unsaved test"); - listitem.setUserData("test-string", JSON.stringify(_sanitizeItemsInTest(newTest)), null); - } else { - listcell.setAttribute("label", "Creation failed"); - } - }); + return new Promise( + (resolve, reject) => tester.newTest(input, function (obj, newTest) { // "done" handler for do + if (newTest) { + resolve(_sanitizeItemsInTest(newTest)); + } + else { + reject(new Error('Creation failed')); + } + }) + ); } + else if (type == "import" || type == "search") { + let test = { type, input: input, items: [] }; - if (type == "import") { - var test = {"type" : "import", "input" : input, "items" : []}; - - // Creates the test. The test isn't saved yet! // TranslatorTester doesn't handle these correctly, so we do it manually - _run("doImport", input, null, function(obj, item) { - if(item) { - test.items.push(Zotero_TranslatorTester._sanitizeItem(item)); - } - }, null, function(){ - listcell.setAttribute("label", "New unsaved test"); - listitem.setUserData("test-string", JSON.stringify(test), null); - }); + return new Promise( + resolve => _run(`do${type == 'import' ? 'Import' : 'Search'}`, input, null, function (obj, item) { + if (item) { + test.items.push(Zotero_TranslatorTester._sanitizeItem(item)); + } + }, null, function () { + resolve(test); + }) + ); } - } + + return Promise.reject(new Error('Invalid type: ' + type)); + }; /* * populate tests pane and url options in browser pane */ - this.populateTests = function() { - _clearTests(); - // Clear entries (but not value) in the url dropdown in the browser tab - var browserURL = document.getElementById("browser-url"); - var currentURL = browserURL.value; + this.populateTests = function () { + let tests = _loadTestsFromPane(); + let validateTestsBroadcaster = document.getElementById('validate-tests'); + if (tests === null) { + validateTestsBroadcaster.setAttribute('disabled', true); + return; + } + else { + validateTestsBroadcaster.removeAttribute('disabled'); + } + + let browserURL = document.getElementById("browser-url"); + let currentURL = browserURL.value; browserURL.removeAllItems(); browserURL.value = currentURL; - - var tests = _loadTests(_editors["tests"].getSession().getValue()); - // We've got tests, let's display them - var listbox = document.getElementById("testing-listbox"); - for (var i=0; i= count) { + listBox.appendChild(item); } - var test = item.getUserData("test-string"); - if(test) tests.push(JSON.parse(test)); - i++; + + if (test.type == 'web') { + browserURL.appendItem(test.url); + } + + testIndex++; } - _writeTests(_stringifyTests(tests)); - }); + + // remove old rows that we didn't reuse + while (listBox.getItemAtIndex(testIndex)) { + listBox.removeItemAt(testIndex); + } + }; /* - * Delete selected test(s), from UI + * Delete selected test(s) */ - this.deleteSelectedTests = function() { + this.deleteSelectedTests = function () { var listbox = document.getElementById("testing-listbox"); - var count = listbox.selectedCount; - while (count--) { - var item = listbox.selectedItems[0]; - listbox.removeItemAt(listbox.getIndexOfItem(item)); - } - } + var indicesToRemove = [...listbox.selectedItems].map(item => listbox.getIndexOfItem(item)); + + let tests = _loadTestsFromPane(); + indicesToRemove.forEach(i => tests.splice(i, 1)); + _writeTestsToPane(tests); + + this.populateTests(); + }; /* * Load the import input for the first selected test in the import pane, * from the UI. - */ - this.editImportFromTest = function() { + */ + this.editImportFromTest = function () { var listbox = document.getElementById("testing-listbox"); var item = listbox.selectedItems[0]; var test = JSON.parse(item.getUserData("test-string")); if (test.input === undefined) { - _logOutput("Can't edit import data for a non-import test."); + _logOutput("Can't edit input of a non-import/search test."); } - _editors.import.getSession().setValue(test.input); - } + _writeToEditor(_editors.import, + test.type == 'import' + ? test.input + : JSON.stringify(test.input, null, '\t')); + _editors.import.setSelection({ + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: 1 + }); + _showTab('import'); + }; /* * Copy the url or data of the first selected test to the clipboard. - */ - this.copyToClipboard = function() { + */ + this.copyToClipboard = function () { var listbox = document.getElementById("testing-listbox"); var item = listbox.selectedItems[0]; var url = item.getElementsByTagName("listcell")[0].getAttribute("label"); var test = JSON.parse(item.getUserData("test-string")); var urlOrData = (test.input !== undefined) ? test.input : url; Zotero.Utilities.Internal.copyTextToClipboard(urlOrData); - } + }; /** * Open the url of the first selected test in the browser (Browser tab or @@ -1126,101 +1604,137 @@ var Scaffold = new function() { Zotero.launchURL(url); } else { - var tabs = document.getElementById('tabs'); _browser.loadURIWithFlags( url, Components.interfaces.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE ); - tabs.selectedItem = document.getElementById('tab-browser'); + _showTab('browser'); } - } + }; + + this.runTests = function (tests, callback) { + callback = callback || (() => {}); + + _clearOutput(); + + let testsByType = { + import: [], + export: [], + web: [], + search: [] + }; + + for (let test of tests) { + testsByType[test.type].push(test); + } + + for (let [type, testsOfType] of Object.entries(testsByType)) { + if (testsOfType.length) { + let tester = new Zotero_TranslatorTester( + _getTranslatorFromPane(), + type, + _debug, + _translatorProvider + ); + tester.setTests(testsOfType); + tester.runTests(callback); + } + } + }; /* * Run selected test(s) */ - this.runSelectedTests = function() { - _clearOutput(); - + this.runSelectedTests = function () { var listbox = document.getElementById("testing-listbox"); var items = listbox.selectedItems; - if(!items || items.length == 0) return false; // No action if nothing selected - var webtests = []; - var importtests = []; - for (var i=0; i 0) { - let webtester = new Zotero_TranslatorTester( - _getTranslatorFromPane(), - "web", - _debug, - _translatorProvider - ); - webtester.setTests(webtests); - webtester.runTests(function(obj, test, status, message) { - test["ui-item"].getElementsByTagName("listcell")[1].setAttribute("label", message); - }); - } - - if (importtests.length > 0 ) { - let importtester = new Zotero_TranslatorTester( - _getTranslatorFromPane(), - "import", - _debug, - _translatorProvider - ); - importtester.setTests(importtests); - importtester.runTests(function(obj, test, status, message) { - test["ui-item"].getElementsByTagName("listcell")[1].setAttribute("label", message); - }); - } - } + + this.runTests(tests, (obj, test, status, message) => { + test["ui-item"].getElementsByTagName("listcell")[1].setAttribute("label", message); + }); + }; + + this.updateTests = function (tests, testUpdatedCallback) { + _clearOutput(); + + var updater = new TestUpdater(tests); + return new Promise(resolve => updater.updateTests( + testUpdatedCallback, + resolve + )); + }; /* * Update selected test(s) */ - this.updateSelectedTests = function () { - _clearOutput(); + this.updateSelectedTests = async function () { var listbox = document.getElementById("testing-listbox"); var items = [...listbox.selectedItems]; - if(!items || items.length == 0) return false; // No action if nothing selected + if (!items || items.length == 0) return; // No action if nothing selected + var itemIndices = items.map(item => listbox.getIndexOfItem(item)); var tests = []; - for (var i=0; i { + let message; // Assume sequential. TODO: handle this properly via test ID of some sort - if(newTest) { + if (newTest) { message = "Test updated"; - //Zotero.debug(newTest[testsDone]); - items[testsDone].setUserData("test-string", JSON.stringify(newTest), null); - } else { - message = "Update failed" + items[testsDone].setUserData("test-string", _stringifyTests(newTest, 1)); + tests[testsDone] = newTest; + } + else { + message = "Update failed"; } items[testsDone].getElementsByTagName("listcell")[1].setAttribute("label", message); testsDone++; - }, - () => { - _logOutput("Tests updated."); - // Save tests - _logOutput("Saving tests and translator."); - this.saveTests(); - } - ); - } + }); + + let allTests = _loadTestsFromPane(); + for (let [i, test] of Object.entries(tests)) { + allTests[itemIndices[i]] = test; + } + _writeTestsToPane(allTests); + _logOutput("Tests updated."); + }; + + this.populateLinterMenu = function () { + let status = 'Path: ' + getDefaultESLintPath(); + let toggle = Zotero.Prefs.get('scaffold.eslint.enabled') ? 'Disable' : 'Enable'; + document.getElementById('menu_eslintStatus').label = status; + document.getElementById('menu_toggleESLint').label = toggle; + }; + + this.toggleESLint = async function () { + Zotero.Prefs.set('scaffold.eslint.enabled', !Zotero.Prefs.get('scaffold.eslint.enabled')); + await getESLintPath(); + }; + + this.showTabNumbered = function (tabNumber) { + let tabBox = document.getElementById('left-tabbox'); + let numTabs = tabBox.querySelectorAll('tabs > tab').length; + if (tabNumber > numTabs) { + tabNumber = numTabs; + } + + tabBox.selectedIndex = tabNumber - 1; + }; - var TestUpdater = function(tests) { + var TestUpdater = function (tests) { this.testsToUpdate = tests.slice(); this.numTestsTotal = this.testsToUpdate.length; this.newTests = []; @@ -1230,17 +1744,17 @@ var Scaffold = new function() { _debug, _translatorProvider ); - } + }; - TestUpdater.prototype.updateTests = function(testDoneCallback, doneCallback) { - this.testDoneCallback = testDoneCallback || function() { /* no-op */}; - this.doneCallback = doneCallback || function() { /* no-op */}; + TestUpdater.prototype.updateTests = function (testDoneCallback, doneCallback) { + this.testDoneCallback = testDoneCallback || function () { /* no-op */ }; + this.doneCallback = doneCallback || function () { /* no-op */ }; this._updateTests(); - } + }; - TestUpdater.prototype._updateTests = function() { - if(!this.testsToUpdate.length) { + TestUpdater.prototype._updateTests = function () { + if (!this.testsToUpdate.length) { this.doneCallback(this.newTests); return; } @@ -1250,74 +1764,79 @@ var Scaffold = new function() { var me = this; - if (test.type == "import") { + if (test.type == 'web') { + _logOutput("Loading web page from " + test.url); + var hiddenBrowser = Zotero.HTTP.loadDocuments( + test.url, + function (doc) { + _logOutput("Page loaded"); + if (test.defer) { + _logOutput("Waiting " + (Zotero_TranslatorTester.DEFER_DELAY / 1000) + + " second(s) for page content to settle" + ); + } + Zotero.setTimeout( + function () { + doc = hiddenBrowser.contentDocument; + if (doc.location.href != test.url) { + _logOutput("Page URL differs from test. Will be updated. " + doc.location.href); + } + me.tester.newTest(doc, function (obj, newTest) { + Zotero.Browser.deleteHiddenBrowser(hiddenBrowser); + if (test.defer) { + newTest.defer = true; + } + newTest = _sanitizeItemsInTest(newTest); + me.newTests.push(newTest); + me.testDoneCallback(newTest); + me._updateTests(); + }); + }, + test.defer ? Zotero_TranslatorTester.DEFER_DELAY : 0, + true + ); + }, + null, + function (e) { + Zotero.logError(e); + me.newTests.push(false); + me.testDoneCallback(false); + me._updateTests(); + }, + true + ); + + hiddenBrowser.docShell.allowMetaRedirects = true; + } + else { test.items = []; + const methods = { + import: 'doImport', + export: 'doExport', // not supported, will error + search: 'doSearch' + }; + // Re-runs the test. // TranslatorTester doesn't handle these correctly, so we do it manually - _run("doImport", test.input, null, function(obj, item) { - if(item) { + _run(methods[test.type], test.input, null, function (obj, item) { + if (item) { test.items.push(Zotero_TranslatorTester._sanitizeItem(item)); - } - }, null, function() { + } + }, null, function () { if (!test.items.length) test = false; me.newTests.push(test); me.testDoneCallback(test); me._updateTests(); }); - // Don't want to run the web portion - return true; } - - _logOutput("Loading web page from " + test.url); - var hiddenBrowser = Zotero.HTTP.loadDocuments( - test.url, - function (doc) { - _logOutput("Page loaded"); - if (test.defer) { - _logOutput("Waiting " + (Zotero_TranslatorTester.DEFER_DELAY/1000) - + " second(s) for page content to settle" - ); - } - Zotero.setTimeout( - function() { - doc = hiddenBrowser.contentDocument; - if (doc.location.href != test.url) { - _logOutput("Page URL differs from test. Will be updated. "+ doc.location.href); - } - me.tester.newTest(doc, function(obj, newTest) { - Zotero.Browser.deleteHiddenBrowser(hiddenBrowser); - if (test.defer) { - newTest.defer = true; - } - newTest = _sanitizeItemsInTest(newTest); - me.newTests.push(newTest); - me.testDoneCallback(newTest); - me._updateTests(); - }); - }, - test.defer ? Zotero_TranslatorTester.DEFER_DELAY : 0, - true - ) - }, - null, - function(e) { - Zotero.logError(e); - me.newTests.push(false); - me.testDoneCallback(false); - me._updateTests(); - }, - true - ); - - hiddenBrowser.docShell.allowMetaRedirects = true; - } + }; /* * Normalize whitespace to the Zotero norm of tabs */ function normalizeWhitespace(text) { - return text.replace(/^[ \t]+/gm, function(str) { + return text.replace(/^[ \t]+/gm, function (str) { return str.replace(/ {4}/g, "\t"); }); } @@ -1334,16 +1853,16 @@ var Scaffold = new function() { */ function _generateGUID() { var guid = ""; - for(var i=0; i<16; i++) { + for (var i = 0; i < 16; i++) { var bite = Math.floor(Math.random() * 255); - if(i == 4 || i == 6 || i == 8 || i == 10) { + if (i == 4 || i == 6 || i == 8 || i == 10) { guid += "-"; // version - if(i == 6) bite = bite & 0x0f | 0x40; + if (i == 6) bite = bite & 0x0f | 0x40; // variant - if(i == 8) bite = bite & 0x3f | 0x80; + if (i == 8) bite = bite & 0x3f | 0x80; } var str = bite.toString(16); guid += str.length == 1 ? '0' + str : str; @@ -1352,7 +1871,7 @@ var Scaffold = new function() { } /* - * updates list of available frames and show URL of active tab + * updates list of available frames and show URL of active tab */ function _updateFrames() { var doc = _browser.contentDocument; @@ -1362,18 +1881,19 @@ var Scaffold = new function() { // No need to run if Scaffold isn't open var menulist = _document.getElementById("menulist-testFrame"); - if (!_document || !menulist) return true; + if (!_document || !menulist) return; menulist.removeAllItems(); var popup = _document.createElement("menupopup"); menulist.appendChild(popup); - _frames = new Array(); + _frames = []; var frames = doc.getElementsByTagName("frame"); - if(frames.length) { + if (frames.length) { _getFrames(frames, popup); - } else { + } + else { var item = _document.createElement("menuitem"); item.setAttribute("label", "Default"); popup.appendChild(item); @@ -1388,16 +1908,18 @@ var Scaffold = new function() { * recursively searches for frames */ function _getFrames(frames, popup) { - for (var i=0; i ({ + startLineNumber: message.line - _linesOfMetadata + 1, + startColumn: message.column, + endLineNumber: message.endLine - _linesOfMetadata + 1, + endColumn: message.endColumn, + message: message.message, + severity: message.severity * 4, + source: 'ESLint', + tags: [ + message.ruleId + ] + })); + } + + function getTestLabel(test) { + switch (test.type) { + case 'import': + return test.input.substr(0, 80); + case 'web': + return test.url; + case 'search': + return JSON.stringify(test.input).substr(0, 80); + default: + return `Unknown type: ${test.type}`; + } + } + + function _showTab(tab) { + document.getElementById('tabs').selectedItem = document.getElementById(`tab-${tab}`); + } + + function _getActiveEditorName() { + let activeElement = document.activeElement; + if (activeElement && activeElement.id && activeElement.id.startsWith('editor-')) { + return activeElement.id.substring(7); + } + return null; + } +}; + +window.addEventListener("load", function (e) { + Scaffold.onLoad(e); +}, false); diff --git a/chrome/content/scaffold/scaffold.xul b/chrome/content/scaffold/scaffold.xul index ecc7d6e450..3e1fbf80d1 100644 --- a/chrome/content/scaffold/scaffold.xul +++ b/chrome/content/scaffold/scaffold.xul @@ -1,58 +1,113 @@ + + - + + + - %globalDTD; + %textcontextDTD; + %standaloneDTD; + %editMenuOverlayDTD; + %brandDTD; + %zoteroDTD; + %scaffoldDTD; +]> + +