From 4174b9af702225f0ef830b4d3cd91d386e1ea707 Mon Sep 17 00:00:00 2001 From: Alexandre Marcireau Date: Mon, 18 Dec 2017 10:29:58 +0100 Subject: [PATCH 1/2] Place non-action functions in their own files --- source/actions/manageMenu.js | 251 --------------------------------- source/actors/manageVersion.js | 2 +- source/app.js | 2 +- source/containers/Origami.js | 8 +- source/libraries/utilities.js | 165 ++++++++++++++++++++++ source/reducers/origami.js | 23 +-- source/state.js | 124 ++++++++++++++++ 7 files changed, 298 insertions(+), 277 deletions(-) create mode 100644 source/state.js diff --git a/source/actions/manageMenu.js b/source/actions/manageMenu.js index 565f775..c900819 100644 --- a/source/actions/manageMenu.js +++ b/source/actions/manageMenu.js @@ -1,11 +1,4 @@ import {ipcRenderer} from 'electron' -import { - convertLaTeX, - stringifyLaTeX -} from '../libraries/latex-to-unicode-converter' -import crossrefQueue from '../queues/crossrefQueue' -import doiQueue from '../queues/doiQueue' -import scholarQueue from '../queues/scholarQueue' import { RESET, OPEN_MENU_ITEM, @@ -22,16 +15,6 @@ import { SELECT_GRAPH_DISPLAY, SELECT_LIST_DISPLAY, } from '../constants/actionTypes' -import { - SCHOLAR_STATUS_IDLE, - SCHOLAR_STATUS_FETCHING, - SCHOLAR_STATUS_BLOCKED_HIDDEN, - SCHOLAR_STATUS_BLOCKED_VISIBLE, - SCHOLAR_STATUS_UNBLOCKING, - CROSSREF_REQUEST_TYPE_VALIDATION, - CROSSREF_REQUEST_TYPE_CITER_METADATA, - CROSSREF_REQUEST_TYPE_IMPORTED_METADATA, -} from '../constants/enums' export function reset(state) { return { @@ -128,237 +111,3 @@ export function selectGraphDisplay() { export function selectListDisplay() { return {type: SELECT_LIST_DISPLAY}; } - -export function stateToJson(state, expand) { - return `${JSON.stringify({ - appVersion: state.appVersion, - display: state.menu.display, - knownDois: Array.from(state.knownDois), - crossref: state.crossref.requests.filter(crossrefRequest => ( - crossrefRequest.type !== CROSSREF_REQUEST_TYPE_CITER_METADATA - || state.publications.has(crossrefRequest.parentDoi) - )), - doi: state.doi.requests.filter(doiRequest => state.publications.has(doiRequest.doi)), - graph: state.graph, - saveFilename: expand ? undefined : state.menu.saveFilename, - savable: expand ? undefined : state.menu.savedVersion < state.version, - publications: Array.from(state.publications.entries()), - scholar: { - requests: state.scholar.requests.filter(request => state.publications.has(request.doi)), - minimumRefractoryPeriod: state.scholar.minimumRefractoryPeriod, - maximumRefractoryPeriod: state.scholar.maximumRefractoryPeriod, - }, - search: state.search, - tabs: state.tabs.index, - warnings: state.warnings.list, - }, null, expand ? ' ' : null)}${expand ? '\n' : ''}`; -} - -export function jsonToState(json, saveFilename, previousState) { - try { - const state = JSON.parse(new TextDecoder('utf-8').decode(json)); - state.appVersion = previousState ? previousState.appVersion : undefined; - state.colors = previousState ? previousState.colors : undefined; - state.connected = previousState ? previousState.connected : undefined; - state.crossref = { - status: crossrefQueue.status.IDLE, - requests: state.crossref, - }; - state.doi = { - status: doiQueue.status.IDLE, - requests: state.doi, - }; - state.knownDois = new Set(state.knownDois); - state.menu = { - activeItem: null, - hash: previousState ? previousState.menu.hash + 1 : 0, - saveFilename: saveFilename ? saveFilename : state.saveFilename, - savedVersion: previousState ? previousState.version + 1 : 0, - display: state.display, - }; - delete state.saveFilename; - delete state.display; - state.publications = new Map(state.publications); - state.scholar.status = SCHOLAR_STATUS_IDLE; - state.scholar.beginOfRefractoryPeriod = null; - state.scholar.endOfRefractoryPeriod = null; - state.scholar.url = null; - state.tabs = { - index: state.tabs, - hash: previousState ? previousState.tabs.hash + 1 : 0, - }; - state.version = (previousState ? - previousState.version + 1 - : (state.savable ? - 1 - : 0 - ) - ); - delete state.savable; - state.warnings = { - list: state.warnings, - hash: previousState ? previousState.warnings.hash + 1 : 0, - }; - return [null, state]; - } catch(error) { - return [error, null]; - } -} - -export function bibtexToPublications(bibtex) { - const bibtexAsString = bibtex.toString('utf8'); - const publications = []; - let line = 1; - let position = 0; - let status = 'root'; - let nesting = 0; - let publication = {}; - let key = ''; - const throwError = character => { - throw new Error(`Unexpected character '${character}' on line ${line}:${position}`); - }; - const addPublication = () => { - if (publication.title != null && publication.author != null && publication.year != null) { - publications.push({ - title: convertLaTeX({ - onError: (error, latex) => stringifyLaTeX(latex) - }, publication.title), - authors: convertLaTeX({ - onError: (error, latex) => stringifyLaTeX(latex) - }, publication.author).split(' and ').map( - author => author.split(',').reverse().filter(name => name.length > 0).map(name => name.trim()).join(' ') - ), - dateAsString: convertLaTeX({ - onError: (error, latex) => stringifyLaTeX(latex) - }, publication.year), - }); - } - key = ''; - publication = {}; - status = 'root'; - nesting = 0; - } - try { - for (const character of bibtexAsString) { - ++position; - switch (status) { - case 'root': - if (character === '@') { - status = 'type'; - } else if (/\S/.test(character)) { - status = 'comment'; - } - break; - case 'comment': - if (character === '\n') { - status = 'root'; - } - break; - case 'type': - if (/(\w|\s)/.test(character)) { - break; - } else if (character === '{') { - status = 'label'; - nesting = 1; - } else { - throwError(character); - } - break; - case 'label': - if (character === ',') { - status = 'beforeKey'; - } else if (character === '{' || character === '}') { - throwError(character); - } - break; - case 'beforeKey': - if (character === '}') { - addPublication(); - } else if (/\w/.test(character)) { - status = 'key'; - key += character.toLowerCase(); - } else if (/\S/.test(character)) { - throwError(character); - } - break; - case 'key': - if (/(\w|:|-)/.test(character)) { - key += character.toLowerCase(); - } else if (character === '=') { - publication[key] = ''; - status = 'beforeValue'; - } else if (/\s/.test(character)) { - status = 'afterKey'; - } else { - throwError(character); - } - break; - case 'afterKey': - if (character === '=') { - publication[key] = ''; - status = 'beforeValue'; - } else if (/\S/.test(character)) { - throwError(character); - } - break; - case 'beforeValue': - if (/\s/.test(character)) { - break; - } else if (character === '}') { - throwError(character); - } else { - if (character === '{') { - ++nesting; - } - publication[key] += character; - status = 'value'; - } - break; - case 'value': - if (character === '}') { - --nesting; - if (nesting === 0) { - addPublication(); - } else { - publication[key] += character; - } - } else if (character === '{') { - ++nesting; - publication[key] += character; - } else if (nesting === 1) { - if (character === ',') { - key = ''; - status = 'beforeKey'; - } else if (/(\s)/.test(character)) { - status = 'afterValue'; - } else { - publication[key] += character; - } - } else { - publication[key] += character; - } - break; - case 'afterValue': { - if (character === ',') { - key = ''; - status = 'beforeKey'; - } else if (character === '}') { - addPublication(); - } else if (/\S/.test(character)) { - throwError(character); - } - break; - } - default: - break; - } - if (character === '\n') { - ++line; - position = 0; - } - } - return [null, publications]; - } catch (error) { - return [error, null]; - } -} diff --git a/source/actors/manageVersion.js b/source/actors/manageVersion.js index 3388f18..19bffae 100644 --- a/source/actors/manageVersion.js +++ b/source/actors/manageVersion.js @@ -1,5 +1,5 @@ import {ipcRenderer} from 'electron' -import {stateToJson} from '../actions/manageMenu' +import {stateToJson} from '../state' let inhibited = false; let bufferedState = null; diff --git a/source/app.js b/source/app.js index d0cad85..0e44eaf 100644 --- a/source/app.js +++ b/source/app.js @@ -11,7 +11,7 @@ import origamiReducers from './reducers/origami' import origamiActors from './actors/origami' import Origami from './containers/Origami' import {disconnect} from './actions/setConnection' -import {jsonToState} from './actions/manageMenu' +import {jsonToState} from './state' import {SCHOLAR_STATUS_IDLE} from './constants/enums' ipcRenderer.once('startWithState', (event, json, appVersion, colors) => { diff --git a/source/containers/Origami.js b/source/containers/Origami.js index 228be81..e34b6bb 100644 --- a/source/containers/Origami.js +++ b/source/containers/Origami.js @@ -15,6 +15,11 @@ import SetRefractoryPeriod from './SetRefractoryPeriod' import Tabs from './Tabs' import Warnings from './Warnings' import Tab from '../components/Tab' +import {bibtexToPublications} from '../libraries/utilities' +import { + stateToJson, + jsonToState, +} from '../state' import { reset, resolveSave, @@ -28,9 +33,6 @@ import { rejectImportBibtex, selectGraphDisplay, selectListDisplay, - stateToJson, - jsonToState, - bibtexToPublications, } from '../actions/manageMenu' import { PUBLICATION_STATUS_UNVALIDATED, diff --git a/source/libraries/utilities.js b/source/libraries/utilities.js index 436d26e..36a6c68 100644 --- a/source/libraries/utilities.js +++ b/source/libraries/utilities.js @@ -1,3 +1,8 @@ +import { + convertLaTeX, + stringifyLaTeX +} from './latex-to-unicode-converter' + /// smallestOfThreePlusOne is used to calculate the Levenshtein distance. export function smallestOfThreePlusOne(first, second, third, incrementSecondIfSmallest) { return (first < second || third < second ? @@ -109,3 +114,163 @@ export function pad(number) { /// doiPattern matches a Digital Object Identifier, with an optionnal 'https://doi.org/' prefix. export const doiPattern = /^\s*(?:https?:\/\/doi\.org\/)?(10\.[0-9]{4,}(?:\.[0-9]+)*\/(?:(?![%"#? ])\S)+)\s*$/; + +/// bibtexToPublications parses a BibTeX file and extracts publications to validate. +/// bibtex must be a buffer. +export function bibtexToPublications(bibtex) { + const bibtexAsString = bibtex.toString('utf8'); + const publications = []; + let line = 1; + let position = 0; + let status = 'root'; + let nesting = 0; + let publication = {}; + let key = ''; + const throwError = character => { + throw new Error(`Unexpected character '${character}' on line ${line}:${position}`); + }; + const addPublication = () => { + if (publication.title != null && publication.author != null && publication.year != null) { + publications.push({ + title: convertLaTeX({ + onError: (error, latex) => stringifyLaTeX(latex) + }, publication.title), + authors: convertLaTeX({ + onError: (error, latex) => stringifyLaTeX(latex) + }, publication.author).split(' and ').map( + author => author.split(',').reverse().filter(name => name.length > 0).map(name => name.trim()).join(' ') + ), + dateAsString: convertLaTeX({ + onError: (error, latex) => stringifyLaTeX(latex) + }, publication.year), + }); + } + key = ''; + publication = {}; + status = 'root'; + nesting = 0; + } + try { + for (const character of bibtexAsString) { + ++position; + switch (status) { + case 'root': + if (character === '@') { + status = 'type'; + } else if (/\S/.test(character)) { + status = 'comment'; + } + break; + case 'comment': + if (character === '\n') { + status = 'root'; + } + break; + case 'type': + if (/(\w|\s)/.test(character)) { + break; + } else if (character === '{') { + status = 'label'; + nesting = 1; + } else { + throwError(character); + } + break; + case 'label': + if (character === ',') { + status = 'beforeKey'; + } else if (character === '{' || character === '}') { + throwError(character); + } + break; + case 'beforeKey': + if (character === '}') { + addPublication(); + } else if (/\w/.test(character)) { + status = 'key'; + key += character.toLowerCase(); + } else if (/\S/.test(character)) { + throwError(character); + } + break; + case 'key': + if (/(\w|:|-)/.test(character)) { + key += character.toLowerCase(); + } else if (character === '=') { + publication[key] = ''; + status = 'beforeValue'; + } else if (/\s/.test(character)) { + status = 'afterKey'; + } else { + throwError(character); + } + break; + case 'afterKey': + if (character === '=') { + publication[key] = ''; + status = 'beforeValue'; + } else if (/\S/.test(character)) { + throwError(character); + } + break; + case 'beforeValue': + if (/\s/.test(character)) { + break; + } else if (character === '}') { + throwError(character); + } else { + if (character === '{') { + ++nesting; + } + publication[key] += character; + status = 'value'; + } + break; + case 'value': + if (character === '}') { + --nesting; + if (nesting === 0) { + addPublication(); + } else { + publication[key] += character; + } + } else if (character === '{') { + ++nesting; + publication[key] += character; + } else if (nesting === 1) { + if (character === ',') { + key = ''; + status = 'beforeKey'; + } else if (/(\s)/.test(character)) { + status = 'afterValue'; + } else { + publication[key] += character; + } + } else { + publication[key] += character; + } + break; + case 'afterValue': { + if (character === ',') { + key = ''; + status = 'beforeKey'; + } else if (character === '}') { + addPublication(); + } else if (/\S/.test(character)) { + throwError(character); + } + break; + } + default: + break; + } + if (character === '\n') { + ++line; + position = 0; + } + } + return [null, publications]; + } catch (error) { + return [error, null]; + } +} diff --git a/source/reducers/origami.js b/source/reducers/origami.js index 3b3cc91..0c8e61d 100644 --- a/source/reducers/origami.js +++ b/source/reducers/origami.js @@ -1,3 +1,4 @@ +import {resetState} from '../state' import {combineReducers} from 'redux' import connected from './connected' import crossref from './crossref' @@ -24,27 +25,7 @@ function reduceReducers(...reducers) { export default function(state, action) { if (action.type === RESET) { if (action.state == null) { - state = { - appVersion: state.appVersion, - colors: state.colors, - connected: state.connected, - menu: { - activeItem: null, - hash: state.menu.hash + 1, - saveFilename: null, - savedVersion: state.version + 1, - display: 0, - }, - tabs: { - index: 0, - hash: state.tabs.hash + 1, - }, - version: state.version + 1, - warnings: { - list: [], - hash: state.warnings.hash + 1, - }, - }; + state = resetState(state); } else { state = action.state; } diff --git a/source/state.js b/source/state.js new file mode 100644 index 0000000..bdac04d --- /dev/null +++ b/source/state.js @@ -0,0 +1,124 @@ +import crossrefQueue from './queues/crossrefQueue' +import doiQueue from './queues/doiQueue' +import scholarQueue from './queues/scholarQueue' +import { + SCHOLAR_STATUS_IDLE, + SCHOLAR_STATUS_FETCHING, + SCHOLAR_STATUS_BLOCKED_HIDDEN, + SCHOLAR_STATUS_BLOCKED_VISIBLE, + SCHOLAR_STATUS_UNBLOCKING, + CROSSREF_REQUEST_TYPE_VALIDATION, + CROSSREF_REQUEST_TYPE_CITER_METADATA, + CROSSREF_REQUEST_TYPE_IMPORTED_METADATA, +} from './constants/enums' + +/// stateToJson generates a JSON string from an app state. +/// expand must be a boolean: +/// if true, a user-save JSON is generated (pretty-printed) +/// otherwise, an auto-save JSON is generated +export function stateToJson(state, expand) { + return `${JSON.stringify({ + appVersion: state.appVersion, + display: state.menu.display, + knownDois: Array.from(state.knownDois), + crossref: state.crossref.requests.filter(crossrefRequest => ( + crossrefRequest.type !== CROSSREF_REQUEST_TYPE_CITER_METADATA + || state.publications.has(crossrefRequest.parentDoi) + )), + doi: state.doi.requests.filter(doiRequest => state.publications.has(doiRequest.doi)), + graph: state.graph, + saveFilename: expand ? undefined : state.menu.saveFilename, + savable: expand ? undefined : state.menu.savedVersion < state.version, + publications: Array.from(state.publications.entries()), + scholar: { + requests: state.scholar.requests.filter(request => state.publications.has(request.doi)), + minimumRefractoryPeriod: state.scholar.minimumRefractoryPeriod, + maximumRefractoryPeriod: state.scholar.maximumRefractoryPeriod, + }, + search: state.search, + tabs: state.tabs.index, + warnings: state.warnings.list, + }, null, expand ? ' ' : null)}${expand ? '\n' : ''}`; +} + +/// jsonToState generates an app state from a JSON buffer. +/// saveFilename must be a string: +/// if null (typically, for an auto-save JSON), the JSON's saveFilename is used +/// previousState must be an object: +/// if null (typically, for an auto-save JSON), some state parameters are left empty (and must be added manually) +export function jsonToState(json, saveFilename, previousState) { + try { + const state = JSON.parse(new TextDecoder('utf-8').decode(json)); + state.appVersion = previousState ? previousState.appVersion : undefined; + state.colors = previousState ? previousState.colors : undefined; + state.connected = previousState ? previousState.connected : undefined; + state.crossref = { + status: crossrefQueue.status.IDLE, + requests: state.crossref, + }; + state.doi = { + status: doiQueue.status.IDLE, + requests: state.doi, + }; + state.knownDois = new Set(state.knownDois); + state.menu = { + activeItem: null, + hash: previousState ? previousState.menu.hash + 1 : 0, + saveFilename: saveFilename ? saveFilename : state.saveFilename, + savedVersion: previousState ? previousState.version + 1 : 0, + display: state.display, + }; + delete state.saveFilename; + delete state.display; + state.publications = new Map(state.publications); + state.scholar.status = SCHOLAR_STATUS_IDLE; + state.scholar.beginOfRefractoryPeriod = null; + state.scholar.endOfRefractoryPeriod = null; + state.scholar.url = null; + state.tabs = { + index: state.tabs, + hash: previousState ? previousState.tabs.hash + 1 : 0, + }; + state.version = (previousState ? + previousState.version + 1 + : (state.savable ? + 1 + : 0 + ) + ); + delete state.savable; + state.warnings = { + list: state.warnings, + hash: previousState ? previousState.warnings.hash + 1 : 0, + }; + return [null, state]; + } catch(error) { + return [error, null]; + } +} + +/// resetState generates a reset state with incremented hashes. +/// state must be an app state. +export function resetState(state) { + return { + appVersion: state.appVersion, + colors: state.colors, + connected: state.connected, + menu: { + activeItem: null, + hash: state.menu.hash + 1, + saveFilename: null, + savedVersion: state.version + 1, + display: 0, + }, + tabs: { + index: 0, + hash: state.tabs.hash + 1, + }, + version: state.version + 1, + warnings: { + list: [], + hash: state.warnings.hash + 1, + }, + }; +} From b5d43fc16882d27344d1575ef115836496ff490f Mon Sep 17 00:00:00 2001 From: Alexandre Marcireau Date: Mon, 18 Dec 2017 10:34:15 +0100 Subject: [PATCH 2/2] Small edit to the hellip regex --- source/actions/manageScholar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/actions/manageScholar.js b/source/actions/manageScholar.js index 6a63616..b2b4976 100644 --- a/source/actions/manageScholar.js +++ b/source/actions/manageScholar.js @@ -258,7 +258,7 @@ export function resolveHtml(url, text) { dispatch(publicationFromCiterMetadata( state.scholar.requests[0].doi, titleCandidates[0].children[0].data, - matchedMetadata[1].replace(/\s*…\s*$/, '').split(/\s*,\s*/), + matchedMetadata[1].replace(/\s*(…|…)\s*$/, '').split(/\s*,\s*/), matchedMetadata[3] )); }