diff --git a/front/dockerfile b/front/dockerfile index a541918d1..f9dcb9dae 100644 --- a/front/dockerfile +++ b/front/dockerfile @@ -2,6 +2,7 @@ FROM node:14-alpine as app-builder WORKDIR /app COPY gatsby/package*.json ./ +RUN apk add --update-cache git RUN npm ci --silent COPY gatsby ./ RUN npm run build diff --git a/front/gatsby/package-lock.json b/front/gatsby/package-lock.json index b16f9c4eb..835e6821e 100644 --- a/front/gatsby/package-lock.json +++ b/front/gatsby/package-lock.json @@ -4382,9 +4382,9 @@ } }, "biblatex-csl-converter": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/biblatex-csl-converter/-/biblatex-csl-converter-1.9.5.tgz", - "integrity": "sha512-ZOO67ojGF9NoNFUJ9ohFsEab35po0kqttxSCTphpmKH5QAhPqwG67khDwgnU15kKI4DeWrcj4J5ioZtMkdzkFQ==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/biblatex-csl-converter/-/biblatex-csl-converter-1.11.0.tgz", + "integrity": "sha512-s48XRWl3Ga0LiIqCmrwyPfUHBzeUKOjaNChMOnv3flHqlzYs0yqCIBrftPXBg5gGAvqYaC0kWNsmQeYF6gx4sA==" }, "big-integer": { "version": "1.6.48", @@ -4608,9 +4608,9 @@ "dev": true }, "codemirror": { - "version": "5.59.4", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.59.4.tgz", - "integrity": "sha512-achw5JBgx8QPcACDDn+EUUXmCYzx/zxEtOGXyjvLEvYY8GleUrnfm5D+Zb+UjShHggXKDT9AXrbkBZX6a0YSQg==" + "version": "5.61.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz", + "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==" }, "collect-v8-coverage": { "version": "1.0.1", diff --git a/front/gatsby/package.json b/front/gatsby/package.json index cad35e670..130efa57b 100644 --- a/front/gatsby/package.json +++ b/front/gatsby/package.json @@ -19,8 +19,8 @@ }, "dependencies": { "@rjsf/core": "^2.4.0", - "biblatex-csl-converter": "^1.9.5", - "codemirror": "^5.59.4", + "biblatex-csl-converter": "^1.11.0", + "codemirror": "^5.61.0", "diff-match-patch": "^1.0.5", "downshift": "^6.0.6", "http-link-header": "^1.0.2", diff --git a/front/gatsby/snowpack.config.cjs b/front/gatsby/snowpack.config.cjs index c64f765a3..481f611c2 100644 --- a/front/gatsby/snowpack.config.cjs +++ b/front/gatsby/snowpack.config.cjs @@ -22,6 +22,6 @@ module.exports = { '@snowpack/plugin-react-refresh', ], devOptions: { - port: 3000, + port: 3000 }, } diff --git a/front/gatsby/src/components/Write/Biblio.jsx b/front/gatsby/src/components/Write/Biblio.jsx index cb7dff049..b77987d62 100644 --- a/front/gatsby/src/components/Write/Biblio.jsx +++ b/front/gatsby/src/components/Write/Biblio.jsx @@ -8,7 +8,7 @@ import Bibliographe from './bibliographe/Bibliographe' import menuStyles from './menu.module.scss' import Button from '../Button' -export default (props) => { +export default function Biblio ({ bib, article, bibTeXEntries, handleBib, readOnly }) { const [expand, setExpand] = useState(true) const [modal, setModal] = useState(false) @@ -19,10 +19,10 @@ export default (props) => { {expand && ( <> - {!props.readOnly && ( + {!readOnly && ( )} - {props.bibTeXEntries.map((entry, index) => ( + {bibTeXEntries.map((entry, index) => ( ))} @@ -30,10 +30,10 @@ export default (props) => { {modal && ( setModal(false)}> setModal(false)} - article={props.article} + article={article} /> )} diff --git a/front/gatsby/src/components/Write/Sommaire.jsx b/front/gatsby/src/components/Write/Sommaire.jsx index 51b29b631..0a742d88c 100644 --- a/front/gatsby/src/components/Write/Sommaire.jsx +++ b/front/gatsby/src/components/Write/Sommaire.jsx @@ -1,28 +1,16 @@ import React, { useState } from 'react' -import { ChevronDown, ChevronRight, Bookmark } from 'react-feather' +import { ChevronDown, ChevronRight } from 'react-feather' import styles from './sommaire.module.scss' import menuStyles from './menu.module.scss' +import { connect } from 'react-redux' -export default function Sommaire (props) { - const [expand, setExpand] = useState(true) - // eslint-disable-next-line - const lines = props.md - .split('\n') - .map((l, i) => { - return { line: i, payload: l } - }) - .filter((l) => l.payload.match(/^##+\ /)) +const mapStateToProps = ({ articleStructure }) => ({ articleStructure }) - //arrow backspace \u21B3 - // right arrow \u2192 - // nbsp \xa0 - // down arrow \u2223 - // right tack \u22A2 - // bottom left box drawing \u2514 - // left drawing \u2502 - //23B8 +function Sommaire (props) { + const [expand, setExpand] = useState(true) + const { articleStructure } = props return (
@@ -30,19 +18,18 @@ export default function Sommaire (props) { {expand ? : } Table of contents {expand && (
    - {lines.map((l) => ( + {articleStructure.map((item) => (
  • props.setCodeMirrorCursor(l.line)} + key={`line-${item.index}-${item.line}`} + onClick={() => props.setCodeMirrorCursor(item.index)} > - {l.payload - .replace(/##/, '') - .replace(/#\s/g, '\u21B3') - .replace(/#/g, '\u00B7\xa0')} + {item.title}
  • ))}
)}
) } + +export default connect(mapStateToProps)(Sommaire) diff --git a/front/gatsby/src/components/Write/Stats.jsx b/front/gatsby/src/components/Write/Stats.jsx index dc10b5f6b..8dd36f5bf 100644 --- a/front/gatsby/src/components/Write/Stats.jsx +++ b/front/gatsby/src/components/Write/Stats.jsx @@ -3,23 +3,9 @@ import { ChevronDown, ChevronRight } from 'react-feather' import menuStyles from './menu.module.scss' -export default (props) => { +export default ({ stats }) => { const [expand, setExpand] = useState(true) - let value = props.md || '' - let regex = /\s+/gi - let citation = /\[@[\w-]+/gi - let noMarkDown = /[#_*]+\s?/gi - let wordCount = value - .trim() - .replace(noMarkDown, '') - .replace(regex, ' ') - .split(' ').length - let charCountNoSpace = value.replace(noMarkDown, '').replace(regex, '').length - let charCountPlusSpace = value.replace(noMarkDown, '').length - let citationNb = - value.replace(regex, '').replace(citation, ' ').split(' ').length - 1 - return (

setExpand(!expand)}> @@ -27,10 +13,10 @@ export default (props) => {

{expand && ( <> -

Words : {wordCount}

-

Characters : {charCountNoSpace}

-

Characters (with spaces) : {charCountPlusSpace}

-

Citations : {citationNb}

+

Words : {stats.wordCount}

+

Characters : {stats.charCountNoSpace}

+

Characters (with spaces) : {stats.charCountPlusSpace}

+

Citations : {stats.citationNb}

)}
diff --git a/front/gatsby/src/components/Write/Write.jsx b/front/gatsby/src/components/Write/Write.jsx index 8fba30260..06eaab018 100644 --- a/front/gatsby/src/components/Write/Write.jsx +++ b/front/gatsby/src/components/Write/Write.jsx @@ -1,7 +1,8 @@ -import React, { useEffect, useRef, useState } from 'react' -import { connect } from 'react-redux' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { connect, useDispatch } from 'react-redux' import 'codemirror/mode/markdown/markdown' import { Controlled as CodeMirror } from 'react-codemirror2' +import throttle from 'lodash/throttle' import askGraphQL from '../../helpers/graphQL' import styles from './write.module.scss' @@ -18,10 +19,19 @@ const mapStateToProps = ({ sessionToken, activeUser, applicationConfig }) => { return { sessionToken, activeUser, applicationConfig } } -const ConnectedWrite = (props) => { - const readOnly = Boolean(props.version) +function ConnectedWrite(props) { + const { version: currentVersion } = props + const [readOnly, setReadOnly] = useState(Boolean(currentVersion)) + const dispatch = useDispatch() + const deriveArticleStructureAndStats = useCallback( + throttle(({ md }) => { + dispatch({ type: 'UPDATE_ARTICLE_STATS', md }) + dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md }) + }, 1500, { leading: false, trailing: true }), + [] + ) - const fullQuery = `query($article:ID!, $readOnly: Boolean!, $version:ID!) { + const fullQuery = `query($article:ID!, $hasVersion: Boolean!, $version:ID!) { article(article:$article) { _id title @@ -41,7 +51,7 @@ const ConnectedWrite = (props) => { } } - live @skip (if: $readOnly) { + live @skip (if: $hasVersion) { md bib yaml @@ -52,7 +62,7 @@ const ConnectedWrite = (props) => { } } - version(version: $version) @include (if: $readOnly) { + version(version: $version) @include (if: $hasVersion) { _id md bib @@ -82,8 +92,8 @@ const ConnectedWrite = (props) => { const variables = { user: props.activeUser && props.activeUser._id, article: props.id, - version: props.version || '0123456789ab', - readOnly, + version: currentVersion || '0123456789ab', + hasVersion: typeof currentVersion === 'string' } const [graphqlError, setError] = useState() @@ -113,7 +123,29 @@ const ConnectedWrite = (props) => { const sendVersion = async (autosave = true, major = false, message = '') => { try { - const query = `mutation($user:ID!,$article:ID!,$md:String!,$bib:String!,$yaml:String!,$autosave:Boolean!,$major:Boolean!,$message:String){saveVersion(version:{article:$article,major:$major,auto:$autosave,md:$md,yaml:$yaml,bib:$bib,message:$message},user:$user){ _id version revision message autosave updatedAt owner{ displayName }} }` + const query = `mutation($user: ID!, $article: ID!, $md: String!, $bib: String!, $yaml: String!, $autosave: Boolean!, $major: Boolean!, $message: String) { + saveVersion(version: { + article: $article, + major: $major, + auto: $autosave, + md: $md, + yaml: $yaml, + bib: $bib, + message: $message + }, + user: $user + ) { + _id + version + revision + message + autosave + updatedAt + owner { + displayName + } + } +}` const response = await askGraphQL( { query, @@ -152,6 +184,8 @@ const ConnectedWrite = (props) => { }, [debouncedLive]) const handleMDCM = async (___, __, md) => { + deriveArticleStructureAndStats({ md }) + await setLive({ ...live, md: md }) } const handleYaml = async (yaml) => { @@ -164,32 +198,40 @@ const ConnectedWrite = (props) => { //Reload when version switching useEffect(() => { setIsLoading(true) + setReadOnly(currentVersion) ;(async () => { const data = await askGraphQL( { query: fullQuery, variables }, 'fetching Live version', props.sessionToken, props.applicationConfig - ) - .then(({ version, article }) => ({ version, article })) - .catch((error) => { - setError(error) - return {} - }) + ).then(({ version, article }) => ({ version, article }) + ).catch((error) => { + setError(error) + return {} + }) if (data?.article) { - setLive(props.version ? data.version : data.article.live) + const article = data.article + const version = currentVersion ? data.version : article.live + setLive(version) setArticleInfos({ - _id: data.article._id, - title: data.article.title, - zoteroLink: data.article.zoteroLink, - owners: data.article.owners.map((o) => o.displayName), + _id: article._id, + title: article.title, + zoteroLink: article.zoteroLink, + owners: article.owners.map((o) => o.displayName), }) - setVersions(data.article.versions) + + setVersions(article.versions) + + const md = version.md + dispatch({ type: 'UPDATE_ARTICLE_STATS', md }) + dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md }) } + setIsLoading(false) })() - }, [props.version]) + }, [currentVersion]) if (graphqlError) { return ( @@ -212,7 +254,7 @@ const ConnectedWrite = (props) => { article={articleInfos} {...live} compareTo={props.compareTo} - selectedVersion={props.version} + selectedVersion={currentVersion} versions={versions} readOnly={readOnly} sendVersion={sendVersion} @@ -229,7 +271,7 @@ const ConnectedWrite = (props) => { versions={versions} readOnly={readOnly} article={articleInfos} - selectedVersion={props.version} + selectedVersion={currentVersion} /> )} diff --git a/front/gatsby/src/components/Write/WriteLeft.jsx b/front/gatsby/src/components/Write/WriteLeft.jsx index 5e6b17ff3..7bdc2f7c3 100644 --- a/front/gatsby/src/components/Write/WriteLeft.jsx +++ b/front/gatsby/src/components/Write/WriteLeft.jsx @@ -1,4 +1,5 @@ -import React, { useMemo, useState } from 'react' +import React, { useState, useMemo } from 'react' +import { connect } from 'react-redux' import styles from './writeLeft.module.scss' import Stats from './Stats' @@ -7,9 +8,10 @@ import Sommaire from './Sommaire' import Versions from './Versions' import bib2key from './bibliographe/CitationsFilter' -export default (props) => { - const bibTeXEntries = useMemo(() => bib2key(props.bib), [props.bib]) +const mapStateToProps = ({ articleStats }) => ({ articleStats }) +function WriteLeft (props) { + const bibTeXEntries = useMemo(() => bib2key(props.bib), [props.bib]) const [expanded, setExpanded] = useState(true) return ( @@ -21,19 +23,19 @@ export default (props) => { {expanded ? 'close' : 'open'} {expanded && ( - <> -
-
-

{props.article.title}

-

by {props.article.owners.join(', ')}

-
- - - - -
- +
+
+

{props.article.title}

+

by {props.article.owners.join(', ')}

+
+ + + + +
)} ) } + +export default connect(mapStateToProps)(WriteLeft) diff --git a/front/gatsby/src/components/Write/bibliographe/Bibliographe.jsx b/front/gatsby/src/components/Write/bibliographe/Bibliographe.jsx index 1abbe48c9..0e19faf32 100644 --- a/front/gatsby/src/components/Write/bibliographe/Bibliographe.jsx +++ b/front/gatsby/src/components/Write/bibliographe/Bibliographe.jsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect } from 'react-redux' -import { debounce } from 'lodash' +import debounce from 'lodash/debounce' import styles from './bibliographe.module.scss' import etv from '../../../helpers/eventTargetValue' @@ -31,7 +31,7 @@ const mapDispatchToProps = (dispatch) => ({ ), }) -const ConnectedBibliographe = (props) => { +function ConnectedBibliographe (props) { const {backendEndpoint} = props.applicationConfig const defaultSuccess = (result) => console.log(result) const { refreshProfile } = props diff --git a/front/gatsby/src/components/Write/bibliographe/CitationsFilter.js b/front/gatsby/src/components/Write/bibliographe/CitationsFilter.js index 744eaaaa8..681efe496 100644 --- a/front/gatsby/src/components/Write/bibliographe/CitationsFilter.js +++ b/front/gatsby/src/components/Write/bibliographe/CitationsFilter.js @@ -21,6 +21,7 @@ export default (input) => { const parser = new BibLatexParser(input, { processUnexpected: true, processUnknown: true, + includeRawText: true, async: false, }) diff --git a/front/gatsby/src/components/Write/metadata/isidoreAuthor.jsx b/front/gatsby/src/components/Write/metadata/isidoreAuthor.jsx index 39481fbd9..09d518e75 100644 --- a/front/gatsby/src/components/Write/metadata/isidoreAuthor.jsx +++ b/front/gatsby/src/components/Write/metadata/isidoreAuthor.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react' -import { throttle } from 'lodash' +import throttle from 'lodash/throttle' import { searchAuthor as isidoreAuthorSearch } from '../../../helpers/isidore' import { useCombobox } from 'downshift' import Field from '../../Field' diff --git a/front/gatsby/src/components/Write/metadata/isidoreKeyword.jsx b/front/gatsby/src/components/Write/metadata/isidoreKeyword.jsx index e79cc82bb..e8b6cce85 100644 --- a/front/gatsby/src/components/Write/metadata/isidoreKeyword.jsx +++ b/front/gatsby/src/components/Write/metadata/isidoreKeyword.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react' -import { throttle } from 'lodash' +import throttle from 'lodash/throttle' import { searchKeyword as isidoreKeywordSearch } from '../../../helpers/isidore' import { useCombobox } from 'downshift' import Field from '../../Field' diff --git a/front/gatsby/src/createReduxStore.js b/front/gatsby/src/createReduxStore.js index 365946aa9..dbb1df6c6 100644 --- a/front/gatsby/src/createReduxStore.js +++ b/front/gatsby/src/createReduxStore.js @@ -1,87 +1,175 @@ -import { createStore as reduxCreateStore } from 'redux' +import { createStore } from 'redux' -// Définition du store Redux et de l'ensemble des actions +function createReducer(initialState, handlers) { + return function reducer(state = initialState, action) { + if (handlers.hasOwnProperty(action.type)) { + return handlers[action.type](state, action) + } else { + return state + } + } +} +// Définition du store Redux et de l'ensemble des actions const initialState = { logedIn: false, users: [], password: undefined, sessionToken: undefined, + articleStructure: [], + articleStats: { + wordCount: 0, + charCountNoSpace: 0, + charCountPlusSpace: 0, + citationNb: 0, + }, } -const reducer = (state = initialState, action) => { - if (action.type === 'APPLICATION_CONFIG') { - return { ...state, applicationConfig: action.applicationConfig } - } else if (action.type === 'PROFILE') { - if (!action.user) { - return { ...state, hasBooted: true } - } +const reducer = createReducer([], { + APPLICATION_CONFIG: setApplicationConfig, + PROFILE: setProfile, + CLEAR_ZOTERO_TOKEN: clearZoteroToken, + LOGIN: loginUser, + UPDATE_ACTIVE_USER: updateActiveUser, + RELOAD_USERS: reloadUsers, + SWITCH: switchUser, + LOGOUT: logoutUser, + REMOVE_MYSELF_ALLOWED_LOGIN: removeMyselfAllowedLogin, + + // article reducers + UPDATE_ARTICLE_STATS: updateArticleStats, + UPDATE_ARTICLE_STRUCTURE: updateArticleStructure, +}) + + +function setApplicationConfig (state, action) { + const applicationConfig = { + ...action.applicationConfig + } + + return { ...state, applicationConfig } +} - const { user: activeUser } = action +function setProfile (state, action) { + if (!action.user) { + return { ...state, hasBooted: true } + } + + const { user: activeUser } = action + + return Object.assign({}, state, { + hasBooted: true, + activeUser, + logedIn: true, + // it will allow password modification if logged with password, + // otherwise it means we use an external auth service + password: + activeUser.passwords.find((p) => p.email === activeUser.email) || {}, + users: [activeUser._id], + }) +} + +function clearZoteroToken (state) { + state.activeUser.zoteroToken = null + + return state +} - return Object.assign({}, state, { - hasBooted: true, - activeUser, +function loginUser (state, {login}) { + if (login.password && login.users && login.token) { + return { + ...state, logedIn: true, - // it will allow password modification if logged with password, - // otherwise it means we use an external auth service - password: - activeUser.passwords.find((p) => p.email === activeUser.email) || {}, - users: [activeUser._id], - }) - } else if (action.type === 'CLEAR_ZOTERO_TOKEN') { - state.activeUser.zoteroToken = null - return state - } else if (action.type === 'LOGIN') { - const login = action.login - if (login.password && login.users && login.token) { - return Object.assign({}, state, { - logedIn: true, - users: login.users, - activeUser: login.users[0], - password: login.password, - sessionToken: login.token, - }) + users: login.users, + activeUser: login.users[0], + password: login.password, + sessionToken: login.token, } - } else if (action.type === 'UPDATE_ACTIVE_USER') { - return Object.assign( - {}, - state, - { - activeUser: { ...state.activeUser, displayName: action.payload }, - }, - { - users: [...state.users].map((u) => { - if (state.activeUser._id === u._id) { - u.displayName = action.payload - } - return u - }), + } + + return state +} + +function updateActiveUser (state, action) { + return { + ...state, + activeUser: { ...state.activeUser, displayName: action.payload }, + users: [...state.users].map((u) => { + if (state.activeUser._id === u._id) { + u.displayName = action.payload } - ) - } else if (action.type === 'RELOAD_USERS') { - return Object.assign({}, state, { - users: action.payload, - }) - } else if (action.type === 'SWITCH') { - if (state.users.map((u) => u._id).includes(action.payload._id)) { - return Object.assign({}, state, { activeUser: action.payload }) - } - } else if (action.type === 'LOGOUT') { - return Object.assign({}, state, { - ...initialState, - }) - } else if (action.type === 'REMOVE_MYSELF_ALLOWED_LOGIN') { - const remainingUsers = state.users.filter((u) => u._id !== action.payload) - return Object.assign({}, state, { - users: remainingUsers, - activeUser: remainingUsers[0], + return u }) + + } +} + +function reloadUsers (state, {payload: users}) { + return { ...state, users } +} + +function switchUser (state, {payload: activeUser}) { + if (state.users.map((u) => u._id).includes(activeUser._id)) { + return { ...state, activeUser } + } + + return state +} + +function logoutUser (state) { + return { ...state, ...initialState } +} + +function removeMyselfAllowedLogin (state, {payload: userId}) { + const remainingUsers = state.users.filter((u) => u._id !== userId) + + return { ...state, + users: remainingUsers, + activeUser: remainingUsers[0], } +} + +const SPACE_RE = /\s+/gi +const CITATION_RE = /(\[@[\w-]+)/gi +const REMOVE_MARKDOWN_RE = /[#_*]+\s?/gi - return initialState +function updateArticleStats (state, { md }) { + const text = (md || '').trim() + + const textWithoutMarkdown = text.replace(REMOVE_MARKDOWN_RE, '') + const wordCount = textWithoutMarkdown + .replace(SPACE_RE, ' ') + .split(' ').length + + const charCountNoSpace = textWithoutMarkdown.replace(SPACE_RE, '').length + const charCountPlusSpace = textWithoutMarkdown.length + const citationNb = text.match(CITATION_RE)?.length || 0 + + return { ...state, articleStats: { + wordCount, + charCountNoSpace, + charCountPlusSpace, + citationNb + }} } -const createStore = () => reduxCreateStore(reducer, initialState) +function updateArticleStructure(state, { md }) { + const text = (md || '').trim() + const articleStructure = text + .split('\n') + .map((line, index) => ({ line, index })) + .filter((lineWithIndex) => lineWithIndex.line.match(/^##+\ /)) + .map((lineWithIndex) => { + const title = lineWithIndex.line + .replace(/##/, '') + //arrow backspace (\u21B3) + .replace(/#\s/g, '\u21B3') + // middle dot (\u00B7) + non-breaking space (\xa0) + .replace(/#/g, '\u00B7\xa0') + return {...lineWithIndex, title} + }) + + return { ...state, articleStructure } +} -export default createStore +export default () => createStore(reducer, initialState, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) diff --git a/front/gatsby/src/helpers/bibtex.js b/front/gatsby/src/helpers/bibtex.js index 168626049..25359c4fe 100644 --- a/front/gatsby/src/helpers/bibtex.js +++ b/front/gatsby/src/helpers/bibtex.js @@ -1,9 +1,10 @@ -import { BibLatexExporter, BibLatexParser } from 'biblatex-csl-converter' +import { BibLatexParser } from 'biblatex-csl-converter' export async function parse(bibtex, options = { expectOutput: false }) { const parser = new BibLatexParser(bibtex, { processUnexpected: true, processUnknown: true, + includeRawText: true, async: true, }) @@ -12,13 +13,7 @@ export async function parse(bibtex, options = { expectOutput: false }) { } export function toBibtex(entries) { - const bibDB = entries.reduce((db, entry, i) => { - return Object.assign(db, { [String(i)]: entry }) - }, {}) - - return new BibLatexExporter(bibDB, false, { - exportUnexpectedFields: true, - }).parse() + return entries.map((e) => e.raw_text).join('\n\n') } /**