UNPKG

rehype-citation

Version:

rehype plugin to add citation and bibliography from bibtex files

275 lines (274 loc) 9.28 kB
import fetch from 'cross-fetch'; export const isNode = typeof window === 'undefined'; export const readFile = async (path) => { if (isValidHttpUrl(path)) { try { const response = await fetch(path); return await response.text(); } catch (error) { throw new Error(`Cannot fetch bibliography URL: ${error}.`); } } else { if (isNode) { try { return import('fs').then((fs) => fs.readFileSync(path, 'utf8')); } catch (error) { throw new Error(`Cannot read non valid URL in node env.`); } } } }; /** * Check if valid URL * https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url * * @param {string} str * @return {boolean} */ export const isValidHttpUrl = (str) => { let url; try { url = new URL(str); } catch (_) { return false; } return url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'blob:'; }; /** * Get bibliography by merging options and vfile data * * @param {import('./generator.js').Options} options * @param {import('vfile').VFile} file */ export const getBibliography = async (options, file) => { /** @type {string[]} */ let bibliography = []; const frontmatterBibliography = getFrontmatterField(file, 'bibliography'); if (options.bibliography) { bibliography = typeof options.bibliography === 'string' ? [options.bibliography] : options.bibliography; } else if (frontmatterBibliography) { bibliography = typeof frontmatterBibliography === 'string' ? [frontmatterBibliography] : frontmatterBibliography; } // If local path, get absolute path for (let i = 0; i < bibliography.length; i++) { if (!isValidHttpUrl(bibliography[i])) { // Case options.path is provided and non empty if (options.path) { // if node env we construct the full path using options.path if (isNode) { bibliography[i] = await import('path').then((path) => path.join(options.path, bibliography[i])); // else we throw as it's non valid http url } else { throw new Error(`Cannot read non valid bibliography URL.`); } // Case options.path is empt } else { // if node env we construct the full path using default `process.cwd` if (isNode) { bibliography[i] = await import('path').then((path) => path.join(file.cwd, bibliography[i])); // else as it's a non valid http url we throw as a base url must be provided using options.path } else { throw new Error(`Non valid bibliography URL: Provide a full valid path for biblio ${bibliography[i]} or set an appropriate "options.path"`); } } } } return bibliography; }; /** * Load CSL - supports predefined name from config.templates.data or http, file path (nodejs) * * @param {*} Cite cite object from citation-js * @param {string} format CSL name e.g. apa or file path to CSL file * @param {string} root optional root path */ export const loadCSL = async (Cite, format, root = '') => { const config = Cite.plugins.config.get('@csl'); if (!Object.keys(config.templates.data).includes(format)) { const cslName = `customCSL-${Math.random().toString(36).slice(2, 7)}`; let cslPath = ''; if (isValidHttpUrl(format)) cslPath = format; else { if (isNode) cslPath = await import('path').then((path) => path.join(root, format)); } try { config.templates.add(cslName, await readFile(cslPath)); } catch (err) { throw new Error(`Input CSL option, ${format}, is invalid or is an unknown file.`); } return cslName; } else { return format; } }; /** * Load locale - supports predefined name from config.locales.data or http, file path (nodejs) * * @param {*} Cite cite object from citation-js * @param {string} format locale name * @param {string} root optional root path */ export const loadLocale = async (Cite, format, root = '') => { const config = Cite.plugins.config.get('@csl'); if (!Object.keys(config.locales.data).includes(format)) { let localePath = ''; if (isValidHttpUrl(format)) localePath = format; else { if (isNode) localePath = await import('path').then((path) => path.join(root, format)); } try { const file = await readFile(localePath); const xmlLangRe = /xml:lang="(.+)"/; const localeName = file.match(xmlLangRe)[1]; config.locales.add(localeName, file); return localeName; } catch (err) { throw new Error(`Input locale option, ${format}, is invalid or is an unknown file.`); } } else { return format; } }; /** * Get citation format * * @param {*} citeproc citeproc * @returns string */ export const getCitationFormat = (citeproc) => { const info = citeproc.cslXml.dataObj.children[0]; const node = info.children.find((x) => x['attrs'] && x['attrs']['citation-format']); // citation-format takes 5 possible values // https://docs.citationstyles.org/en/stable/specification.html#toc-entry-14 /** @type {'author-date' | 'author' | 'numeric' | 'note' | 'label'} */ const citationFormat = node['attrs']['citation-format']; return citationFormat; }; /** * Get registry objects that matches a list of relevantIds * If sorted is false, retrieve registry item in the order of the given relevantIds * * @param {*} citeproc citeproc * @param {string[]} relevantIds * @param {boolean} sorted * @return {*} registry objects that matches Ids, in the correct order */ export const getSortedRelevantRegistryItems = (citeproc, relevantIds, sorted) => { const res = []; if (sorted) { // If sorted follow registry order for (const item of citeproc.registry.reflist) { if (relevantIds.includes(item.id)) res.push(item); } } else { // Otherwise follow the relevantIds for (const id of relevantIds) { res.push(citeproc.registry.reflist.find((x) => x.id === id)); } } return res; }; /** * Split a string into two parts based on a given index position * * @param {string} str * @param {number} index * @return {string[]} */ export const split = (str, index) => { return [str.slice(0, index), str.slice(index)]; }; /** * Check if two registry objects belong to the same author * Currently only checks on family name * * @param {*} item registry object * @param {*} item2 registry object * @return {boolean} */ export const isSameAuthor = (item, item2) => { const authorList = item.ref.author; const authorList2 = item2.ref.author; if (authorList.length !== authorList2.length) return false; for (let i = 0; i < authorList.length; i++) { if (authorList[i].family !== authorList2[i].family) return false; } return true; }; /** * @typedef {Object} FrontmatterSource * @property {Record<string, any>} [matter] * @property {Record<string, any>} [frontmatter] * @property {{ frontmatter?: Record<string, any> }} [astro] */ /** * @param {{ data?: FrontmatterSource }} file * @param {string} fieldName * @returns {any} */ export const getFrontmatterField = (file, fieldName) => { if (!file || !file.data) { return undefined; } const sources = [file.data.matter, file.data.frontmatter, file.data.astro?.frontmatter]; for (const source of sources) { if (source && fieldName in source) { return source[fieldName]; } } return undefined; }; /** * Get bibliography entry text for a citation ID * * @param {*} citeproc citeproc engine * @param {string} id citation ID * @return {string} formatted bibliography entry without HTML tags */ export const getBibliographyEntryText = (citeproc, id) => { try { // Save the current state const originalItemIds = [...citeproc.registry.mylist]; // Since creating bibliography affects the state we need to save the current state and restore it citeproc.updateItems([id]); const bibOutput = citeproc.makeBibliography([id]); if (!bibOutput || !bibOutput[1] || bibOutput[1].length === 0) { citeproc.updateItems(originalItemIds); return ''; } // Get the text let entryText = bibOutput[1][0].replace(/<[^>]*>/g, ''); entryText = entryText.replace(/\s+/g, ' ').trim(); // Restore the original state citeproc.updateItems(originalItemIds); return entryText; } catch (error) { console.error('Error getting bibliography entry text:', error); return ''; } };