diff --git a/.eslintrc.js b/.eslintrc.js index 160800d28..ae7c3d1e2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { parser: '@babel/eslint-parser', env: { browser: true, + es6: true, }, rules: { strict: 0, diff --git a/build-plugins/gatsby-transformer-nubook-bibliography/ieee.csl b/build-plugins/gatsby-transformer-nubook-bibliography/ieee.csl new file mode 100644 index 000000000..9c76dd26f --- /dev/null +++ b/build-plugins/gatsby-transformer-nubook-bibliography/ieee.csl @@ -0,0 +1,457 @@ + + + diff --git a/build-plugins/gatsby-transformer-nubook-bibliography/locales-en-GB.xml b/build-plugins/gatsby-transformer-nubook-bibliography/locales-en-GB.xml new file mode 100644 index 000000000..6a0fee22c --- /dev/null +++ b/build-plugins/gatsby-transformer-nubook-bibliography/locales-en-GB.xml @@ -0,0 +1,654 @@ + + + + + + Andrew Dunning + + + Sebastian Karcher + + + Rintze M. Zelle + + This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License + 2015-10-10T23:31:02+00:00 + + + + + + + + + + + + + + advance online publication + album + audio recording + film + henceforth + loc. cit. + no place + n.p. + no publisher + n.p. + on + op. cit. + original work published + personal communication + podcast + podcast episode + preprint + radio broadcast + radio series + radio series episode + special issue + special section + television broadcast + television series + television series episode + video + working paper + accessed + and + and others + anonymous + anon. + at + available at + by + circa + c. + cited + + edition + editions + + ed. + et al. + forthcoming + from + ibid. + in + in press + internet + interview + letter + no date + n.d. + online + presented at the + + reference + references + + + ref. + refs. + + retrieved + scale + version + + + preprint + journal article + magazine article + newspaper article + bill + book + broadcast + book chapter + classic + collection + dataset + document + entry + dictionary entry + encyclopedia entry + event + + graphic + hearing + interview + legal case + legislation + manuscript + map + video recording + musical score + pamphlet + conference paper + patent + performance + periodical + personal communication + post + blog post + regulation + report + review + book review + software + audio recording + presentation + standard + thesis + treaty + webpage + + + journal art. + mag. art. + newspaper art. + bk. + bk. chap. + doc. + + graph. + interv. + MS + video rec. + rep. + rev. + bk. rev. + audio rec. + + + AD + BC + BCE + CE + + + + + + + + : + , + ; + + + th + st + nd + rd + th + th + th + + + first + second + third + fourth + fifth + sixth + seventh + eighth + ninth + tenth + + + + act + acts + + + appendix + appendices + + + article + articles + + + canon + canons + + + location + locations + + + equation + equations + + + rule + rules + + + scene + scenes + + + table + tables + + + + + + + title + titles + + + book + books + + + chapter + chapters + + + column + columns + + + figure + figures + + + folio + folios + + + number + numbers + + + line + lines + + + note + notes + + + opus + opera + + + page + pages + + + page + pages + + + paragraph + paragraphs + + + part + parts + + + section + sections + + + sub verbo + sub verbis + + + verse + verses + + + volume + volumes + + + + + app. + apps. + + + art. + arts. + + + loc. + locs. + + + eq. + eqs. + + + r. + rr. + + + sc. + scs. + + + tbl. + tbls. + + + + + + + tit. + tits. + + + bk. + bks + + + chap. + chaps + + + col. + cols + + + fig. + figs + + + fol. + fols + + + no. + nos. + + + l. + ll. + + + n. + nn. + + + op. + opp. + + + p. + pp. + + + p. + pp. + + + para. + paras + + + pt. + pts + + + sec. + secs + + + s.v. + s.vv. + + + v. + vv. + + + vol. + vols + + + + + + ¶¶ + + + § + §§ + + + + + chair + chairs + + + compiler + compilers + + + contributor + contributors + + + curator + curators + + + executive producer + executive producers + + + guest + guests + + + host + hosts + + + narrator + narrators + + + organizer + organizers + + + performer + performers + + + producer + producers + + + writer + writers + + + series creator + series creators + + + director + directors + + + editor + editors + + + editor + editors + + + illustrator + illustrators + + + translator + translators + + + editor & translator + editors & translators + + + + + comp. + comps. + + + contrib. + contribs. + + + cur. + curs. + + + exec. prod. + exec. prods. + + + narr. + narrs. + + + org. + orgs. + + + perf. + perfs. + + + prod. + prods. + + + writ. + writs. + + + cre. + cres. + + + dir. + dirs. + + + ed. + eds. + + + ed. + eds. + + + ill. + ills. + + + tran. + trans. + + + ed. & tran. + eds. & trans. + + + + chaired by + compiled by + with + curated by + executive produced by + with guest + hosted by + narrated by + organized by + performed by + produced by + written by + created by + by + directed by + edited by + edited by + illustrated by + interview by + to + by + translated by + edited & translated by + + + comp. by + w. + cur. by + exec. prod. by + w. guest + hosted by + narr. by + org. by + perf. by + prod. by + writ. by + cre. by + dir. by + ed. by + ed. by + illus. by + trans. by + ed. & trans. by + + + January + February + March + April + May + June + July + August + September + October + November + December + + + Jan. + Feb. + Mar. + Apr. + May + Jun. + Jul. + Aug. + Sep. + Oct. + Nov. + Dec. + + + Spring + Summer + Autumn + Winter + + diff --git a/build-plugins/gatsby-transformer-nubook-bibliography/transformer.js b/build-plugins/gatsby-transformer-nubook-bibliography/transformer.js new file mode 100644 index 000000000..c6c5c3c4f --- /dev/null +++ b/build-plugins/gatsby-transformer-nubook-bibliography/transformer.js @@ -0,0 +1,179 @@ +/*eslint-env node*/ + +const fs = require('fs') +const path = require('path') +const Cite = require('citation-js') + +let enGbLocale = '' +let ieeeTemplate = '' + +/** + * Load the IEEE template and the en-GB locale and register them with Cite. + */ +async function loadIeeeTemplateAndGbLocale() { + if (enGbLocale.length > 0 && ieeeTemplate.length > 0) { + return + } + + try { + enGbLocale = await fs.promises.readFile( + path.join(__dirname, 'locales-en-GB.xml'), + 'utf8' + ) + } catch (e) { + throw new Error('unable to load en-GB locale for citation-js' + e) + } + + try { + ieeeTemplate = await fs.promises.readFile( + path.join(__dirname, 'ieee.csl'), + 'utf8' + ) + } catch (e) { + throw new Error('unable to load IEEE template for citation-js' + e) + } + + const config = Cite.plugins.config.get('@csl') + + config.locales.add('en-GB', enGbLocale) + config.templates.add('ieee', ieeeTemplate) +} + +// https://regexr.com/6ee9c +const bibtexEntrySplitRegex = /(@(?:.|\s)*?\{)/g + +/** + * Split the entire Bibtex string into individual entry strings: splits on `@some_entry_type{` + */ +function parseBibEntries(bibtex) { + const separator = '__separator__' + + const entries = bibtex + .replace(bibtexEntrySplitRegex, separator + '$1') // add separator before each entry + .split(separator) // split on the separator + .filter((entry) => entry.trim().length > 0) // remove empty entries + + return entries +} + +const ieeeTemplateRegexes = { + // https://regexr.com/6ee74 + entry: + /
\s*(.+?<\/div>)\s*<\/div>$/m, + entryLeftMargin: /
(.+?)<\/div>/g, + divOpen: /
/g, +} + +/** + * Extract the first entry from the given bibliography HTML. + */ +function extractSingleReference(bibliography) { + const match = bibliography + // The IEEE reference template includes the item index at the start in + // `div.csl-left-margin`. Remove this div from the HTML. + .replace(ieeeTemplateRegexes.entryLeftMargin, '') + .match(ieeeTemplateRegexes.entry) + + if (match) { + return match[2] + .replace(ieeeTemplateRegexes.divOpen, '') + } + + return '' +} + +/** + * Parse the given bibtex string into a list of bibliography entries in the IEEE format. + */ +async function parseIeeeBibliography(bibFileContent, bibFilePath) { + await loadIeeeTemplateAndGbLocale() + + // Parse the entire bibtex file as a pre-run, for accurate line numbers if there's an error + try { + new Cite(bibFileContent) + } catch (error) { + throw new Error(`unable to parse Bibtex file ${bibFilePath}: \n` + error) + } + + return parseBibEntries(bibFileContent).map((entry) => { + const cite = new Cite(entry) + + const data = JSON.parse(cite.format('data'))[0] ?? {} + + // .slice() removes the surrounding brackets + const citation = (cite.format('citation') ?? '').slice(1, -1) + + const html = cite.format('bibliography', { + format: 'html', + template: 'ieee', + lang: 'en-GB', + }) + + return { + id: data.id, + title: data.title, + citation, + reference: extractSingleReference(html), + } + }) +} + +async function createBibNode({ + node, + actions, + loadNodeContent, + createNodeId, + createContentDigest, +}) { + const content = await loadNodeContent(node) + + const bibliography = await parseIeeeBibliography(content, node.absolutePath) + + const bibFileRelativePath = path.posix.join(node.relativeDirectory, node.base) + + // The new custom NUbookBibliography node + const bibliographyNode = { + // Data needed for a Gatsby MDX node + id: createNodeId(`${node.id} >>> NUbookBibliography`), + parent: node.id, + internal: { + type: 'NUbookBibliography', + contentDigest: createContentDigest({ + bibFileRelativePath, + bibliography, + }), + }, + // The actual NUbook-specific data + bibFileRelativePath, + bibliography, + } + + // Create the NUbookBibliography node with Gatsby + actions.createNode(bibliographyNode) + + // Add the NUbookBibliography node as a child to the MDX page node + actions.createParentChildLink({ parent: node, child: bibliographyNode }) +} + +exports.createBibNode = createBibNode + +async function test() { + const filePath = path.join( + __dirname, + '..', + '..', + '/src/book/kitchen-sink/12-referencing.bib' + ) + + const file = await fs.promises.readFile(filePath, 'utf8') + + const bibliography = await parseIeeeBibliography(file, filePath) + + console.log(bibliography) +} + +if (require.main === module) { + test() +} diff --git a/gatsby-node.js b/gatsby-node.js index 31d394748..6432d1a29 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -2,21 +2,31 @@ /*eslint-env es6 */ const path = require('path') + +const nubookBibliography = require('./build-plugins/gatsby-transformer-nubook-bibliography/transformer.js') const nubookContributionsPlugin = require('./build-plugins/gatsby-transformer-nubook-contributions') const menu = [] const createdPages = new Set() /** - * Use the custom gatsby-transformer-nubook-contributions plugin to add - * NUbook contribution data (authors and last commit) to MDX page nodes + * Entry point for NUbook-specific GraphQL data extension and customisation. */ exports.onCreateNode = async ( { node, ...otherFirstParams }, ...otherParams ) => { + // Use the custom gatsby-transformer-nubook-contributions plugin to add + // NUbook contribution data (authors and last commit) to MDX page nodes if (node.internal.type === `Mdx`) { - nubookContributionsPlugin.onCreateNode( + await nubookContributionsPlugin.onCreateNode( + { node, ...otherFirstParams }, + ...otherParams + ) + } + // Create a bibliography node for each .bib file + else if (node.ext === `.bib`) { + await nubookBibliography.createBibNode( { node, ...otherFirstParams }, ...otherParams ) @@ -28,7 +38,7 @@ exports.onCreateNode = async ( */ exports.createPages = async ({ graphql, actions, reporter }) => { // Get the MDX pages sorted by file path (this is why we have the numeric prefixes) - const result = await graphql(` + const mdxResults = await graphql(` query { allMdx(sort: { fields: fileAbsolutePath }) { edges { @@ -40,6 +50,7 @@ exports.createPages = async ({ graphql, actions, reporter }) => { chapter title slug + references hidden } } @@ -67,12 +78,18 @@ exports.createPages = async ({ graphql, actions, reporter }) => { `) // Panic and abort on query error - if (result.errors) { - reporter.panicOnBuild('🚨 ERROR: Loading "createPages" query') + if (mdxResults.errors) { + reporter.panicOnBuild( + '🚨 ERROR: Loading "createPages" query for all MDX files' + ) } + // Get the Bibtex bibliographies, a map where the keys are MDX file paths + // (relative to src/book), and the values are the bibliography content + const allBibliographies = await getAllBibliographies(graphql, reporter) + // Get all MDX files - const posts = result.data.allMdx.edges + const posts = mdxResults.data.allMdx.edges // Populate the menu posts @@ -106,6 +123,8 @@ exports.createPages = async ({ graphql, actions, reporter }) => { const nextPage = getNext(next, posts, index) const previousPage = getPrevious(previous, posts, index) + const bibliography = getBibliographyForPage(allBibliographies, node) + actions.createPage({ path: node.frontmatter.slug, component: templatePath, @@ -118,6 +137,7 @@ exports.createPages = async ({ graphql, actions, reporter }) => { next: nextPage ? nextPage.frontmatter : null, previous: previousPage ? previousPage.frontmatter : null, menu, + references: bibliography, hidden: node.frontmatter.hidden, }, }) @@ -147,6 +167,80 @@ exports.onCreatePage = ({ page, actions }) => { } } +/** + * + * Get the Bibtex bibliography for all pages in NUbook. Returns a map where the + * keys are MDX file paths (relative to src/book), and the values are the + * bibliographies for the file, created in the custom plugin + * `gatsby-transformer-nubook-bibliography`. + */ +async function getAllBibliographies(graphql, reporter) { + // Get all the NUbookBibliography nodes via GraphQL: these are created in + // `gatsby-transformer-nubook-bibliography` and registered with Gatsby + // in `gatsby-node.js` + const bibResults = await graphql(` + query { + allNUbookBibliography { + nodes { + bibFileRelativePath + bibliography { + id + title + citation + reference + } + } + } + } + `) + + // Panic and abort on query error + if (bibResults.errors) { + reporter.panicOnBuild( + '🚨 ERROR: Loading query for all NUbookBibliography nodes' + ) + return {} + } + + // Create the map of bibliography files in Nubook: each key is the path + // to the bib file, and each value is the parsed bibliography content + const allBibliographies = {} + + for (const bibNode of bibResults.data.allNUbookBibliography.nodes) { + // Convert the list of bibliography entries into a map where the keys are + // the reference ids, and the values are the bibliography content + const bibliographyMap = {} + for (const bibliography of bibNode.bibliography) { + bibliographyMap[bibliography.id] = bibliography + } + + allBibliographies[bibNode.bibFileRelativePath] = bibliographyMap + } + + return allBibliographies +} + +/** + * Get the bibliography data for the given MDX page node from the + * given map containing the bibliographies of all pages in NUbook. + */ +function getBibliographyForPage(allBibliographies, pageNode) { + // Compute the path to this page's bibliography.bib file + // (relative to the `src/book` directory) + const basename = path + .basename(pageNode.fileAbsolutePath) + .replace(path.extname(pageNode.fileAbsolutePath), '') + + const bibFilePath = path.posix.join( + path.dirname(pageNode.fileAbsolutePath), + pageNode.frontmatter.references ?? `${basename}.bib` + ) + + const [, bibFileRelative] = bibFilePath.split('src/book/') + + return allBibliographies[bibFileRelative] ?? null +} + /** * Get the section in the given menu that the given node belongs to, * or create a new section if not found. diff --git a/package.json b/package.json index 77883b3f9..28760a98c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@mdx-js/react": "^1.6.22", "autoprefixer": "^10.2.4", "babel-preset-gatsby": "^1.12.0", + "citation-js": "^0.5.5", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.24.0", diff --git a/src/book/02-system/03-tools/02-nubook.mdx b/src/book/02-system/03-tools/02-nubook.mdx index 4e97b858b..639d69169 100644 --- a/src/book/02-system/03-tools/02-nubook.mdx +++ b/src/book/02-system/03-tools/02-nubook.mdx @@ -4,6 +4,8 @@ chapter: Tools title: NUbook description: The NUbots team handbook and documentation. slug: /system/tools/nubook +authors: + - Josephus Paye II (@JosephusPaye) --- NUbook is the handbook and high-level documentation for the NUbots team. What you are looking at right now is NUbook! This page has information on how NUbook works. @@ -296,6 +298,51 @@ qui. ``` +## Bibtex Referencing + +NUbook supports Bibtex referencing in Markdown. You can use it as follows. + +1. First, add the Bibtex file containing your references to the same folder that contains the MDX file you're editing. Give the Bibtex file the same name as the MDX file, but with a `.bib` extension. For example, if you're editing `01-introduction.mdx`, the Bibtex file should be `01-introduction.bib`. + +
+ If you need to reference a Bibtex file from multiple MDX files + + You can use the `references` key at the top of the MDX file to override the default Bibtex file path. + + ```yaml + --- + title: My page + # ... other data + references: ./shared-references.bib # Relative path to the shared references file + --- + ``` + +
+ +2. In the Markdown file, add in-text citations using `` tags. For example, if there's a citation in the Bibtex file with id `smith2018`, you can reference it in the Markdown file like so: + + ```md + Fuga ullam cumque accusamus doloremque rem smith2019. + ``` + + This will add an inline citation with an auto generated citation number, linked to the full citation reference in the list of references. + + You can add multiple in-text citations in the same place by including multiple comma-separated ids in the `` tag. For example, if there's a citation in the Bibtex file with id `smith2018` and a citation with id `brown2005`, you can reference both as follows: + + ```md + Fuga ullam cumque accusamus doloremque rem smith2019,brown2005. + ``` + +3. After adding your in-text citations, create a new section at the bottom of the MDX page to show the list of references: + + ```md + ## References + + + ``` + +See the [Referencing kitchen sink](/kitchen-sink/referencing) for an example of referencing. + ## Organising Pages Pages are written in MDX files and stored in section and chapter folders in the `src/book/` directory, and organised as follows: @@ -327,6 +374,7 @@ Pages are written in MDX files and stored in section and chapter folders in the | `description` | String | Required | A short, one-sentence description of the page content | | `slug` | String | Required | The page URL relative to the root of the site, starting with `/` | | `authors` | List | Required | A list of authors of the page, in the format `Name (@GitHubUsername)`, or just `Name`. Authors without a username will have the NUbots icon as a default avatar, without a link to GitHub. | + | `references` | String | Optional | Relative path to a `.bib` file with references for use on the page. Defaults to `[mdx-file-name].bib` if a matching Bibtex file with the same name as the MDX file is available. | | `keywords` | List | Optional | Keywords for the page content, used for SEO | | `hidden` | Boolean | Optional | When `true`, removes the page from menus and disables search indexing | diff --git a/src/book/kitchen-sink/12-referencing.bib b/src/book/kitchen-sink/12-referencing.bib new file mode 100644 index 000000000..cc7cfa4d5 --- /dev/null +++ b/src/book/kitchen-sink/12-referencing.bib @@ -0,0 +1,30 @@ +@inproceedings{wong2018robocup, + title={RoboCup junior in the hunter region: driving the future of robotic STEM education}, + author={Wong, Aaron SW and Jeffery, Ryan and Turner, Peter and Sleap, Scott and Chalup, Stephan K}, + booktitle={Robot World Cup}, + pages={362--373}, + year={2018}, + organization={Springer} +} + +@article{flomo2018, +Author = "Emmanuel Flomo", +Title = "The Practical Benefits of Unit Testing", +Year = 2008, +Url = "https://youshouldtest.com/benefits" +} + +@article{everton2020, +Author = "Maybelle Everton", +Title = "Software Engineering Best Practices for Teams", +Year = 2020, +Url = "https://meverton.dev/best-practices" +} + +@book{hawking1988, +Author = "Stephen Hawking", +Title = "A Brief History of Time", +Publisher = "Bantam Books", +Address = "Oxford", +Year = 1988 +} diff --git a/src/book/kitchen-sink/12-referencing.mdx b/src/book/kitchen-sink/12-referencing.mdx new file mode 100644 index 000000000..61c94a5ee --- /dev/null +++ b/src/book/kitchen-sink/12-referencing.mdx @@ -0,0 +1,33 @@ +--- +section: Kitchen Sink +chapter: Markdown +title: Referencing +description: BibTeX referencing in Markdown +keywords: + - markdown + - referencing + - bibtex +slug: /kitchen-sink/referencing +authors: + - Josephus Paye II (@JosephusPaye) + - Moosa Hassan (@moosahassanx) + - Samuel Dearie (@SinisterSamtaro94) +references: 12-referencing.bib +hidden: true +--- + +It is important to write automated tests when working on a module or piece of code in the NUbots system everton2020. Testing is not just an abstract "best practice" with no practical benefits, and it's not a waste of time everton2020. In fact, it'll actually save you and the other members of the team a lot of time going forward. + +The tests you write will serves as a formal guarantee that what you've written works everton2020,flomo2018,wong2018robocup, and will be a quick way to verify that it continues to work when someone else makes changes flomo2018. + +Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ab ducimus, ipsum libero quo quae blanditiis hic. Impedit dicta ex perferendis at unde iure, aut necessitatibus molestiae ad ipsam reiciendis facilis. + +Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ab ducimus, ipsum libero quo quae blanditiis hic. Impedit dicta ex perferendis at unde iure. + +Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ab ducimus, ipsum libero quo quae blanditiis hic wong2018robocup. Impedit dicta ex perferendis at unde iure, aut necessitatibus molestiae ad ipsam reiciendis facilis. + +Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ab ducimus, ipsum libero quo quae blanditiis hic. Impedit dicta ex perferendis at unde iure. + +## References + + diff --git a/src/components/markdown/markdown.jsx b/src/components/markdown/markdown.jsx index 6adb66124..70076c9d4 100644 --- a/src/components/markdown/markdown.jsx +++ b/src/components/markdown/markdown.jsx @@ -9,9 +9,12 @@ import TabbedImages from './tabbed-images' import Alert from './alert/alert' import Grid from './grid/grid' -import * as style from './markdown.module.css' +import Cite from './references/cite' +import References from './references/references' import { createHeading } from './heading/heading' +import * as style from './markdown.module.css' + const Img = ({ src, alt, title }) => ( {alt} {title} @@ -50,6 +53,8 @@ const MDXComponents = { a: A, pre: PreCode, table: Table, + cite: Cite, + References: References, h1: createHeading('h1'), h2: createHeading('h2'), h3: createHeading('h3'), diff --git a/src/components/markdown/references/cite.jsx b/src/components/markdown/references/cite.jsx new file mode 100644 index 000000000..f357b215f --- /dev/null +++ b/src/components/markdown/references/cite.jsx @@ -0,0 +1,76 @@ +import React, { useContext, useEffect } from 'react' +import PropTypes from 'prop-types' + +import { BibReferencesContext } from './context' +import { highlightElement } from './highlight-element' + +import * as style from './cite.module.css' + +const CiteSingle = ({ children }) => { + const { references, usedReferenceIds, addUsedReference } = + useContext(BibReferencesContext) + + if (!references) { + throw new Error( + 'Cannot use : no Bibtex references found for this page.' + ) + } + + const referenceId = String(children) + const referenceEntry = references[referenceId] + + if (referenceEntry === undefined) { + throw new Error( + `Cannot cite "${referenceId}": no such reference found in the Bibtex file` + ) + } + + const referenceIndex = Array.from(usedReferenceIds).indexOf(referenceId) + let referenceNumber = referenceIndex + 1 + + // Add the reference if it is not yet in the list of used references + if (referenceIndex === -1) { + useEffect(() => { + addUsedReference(referenceId) + }) + } + + return ( + + highlightElement( + document.querySelector('#reference-' + referenceId).parentElement + ) + } + > + {referenceNumber} + + ) +} + +CiteSingle.propTypes = { + children: PropTypes.node.isRequired, +} + +const Cite = ({ children }) => { + const referenceIds = String(children) + .split(',') + .map((id) => id.trim()) + + return ( + + {referenceIds.map((id, index) => ( + {id} + ))} + + ) +} + +Cite.propTypes = { + children: PropTypes.node.isRequired, +} + +export default Cite diff --git a/src/components/markdown/references/cite.module.css b/src/components/markdown/references/cite.module.css new file mode 100644 index 000000000..8f4999c7e --- /dev/null +++ b/src/components/markdown/references/cite.module.css @@ -0,0 +1,52 @@ +.citeWrapper { + display: inline-flex; +} + +.citeLink { + @apply inline-block align-top leading-none py-px; + @apply no-underline !important; + font-size: 0.85em; + padding-left: 0.4rem; + padding-right: 0.4rem; + padding-top: 2.5px; + padding-bottom: 2.5px; + font-weight: 600; + + &:first-child { + @apply rounded-l-full; + padding-left: 0.5rem; + } + + &:last-child { + @apply rounded-r-full; + padding-right: 0.5rem; + } + + & + .citeLink { + margin-left: 1px; + } +} + +:global(.light) { + .citeLink { + @apply text-primary !important; + background-color: rgba(0, 0, 0, 0.07); + } + + .citeLink:hover, + .citeLink:focus { + background-color: rgba(0, 0, 0, 0.1); + } +} + +:global(.dark) { + .citeLink { + @apply text-white !important; + @apply bg-gray-800; + } + + .citeLink:hover, + .citeLink:focus { + @apply bg-gray-700; + } +} diff --git a/src/components/markdown/references/context.js b/src/components/markdown/references/context.js new file mode 100644 index 000000000..02900b595 --- /dev/null +++ b/src/components/markdown/references/context.js @@ -0,0 +1,3 @@ +import React from 'react' + +export const BibReferencesContext = React.createContext('bibReferences') diff --git a/src/components/markdown/references/highlight-element.js b/src/components/markdown/references/highlight-element.js new file mode 100644 index 000000000..74d002d28 --- /dev/null +++ b/src/components/markdown/references/highlight-element.js @@ -0,0 +1,33 @@ +const defaultBackgroundColor = 'transparent' +const highlightDuration = '5s' + +export function highlightElement(element) { + if (element) { + const highlightColor = document.documentElement.classList.contains('dark') + ? 'rgba(255, 255, 0, 0.25)' + : '#FFC107' + + const oldBackground = element.style.backgroundColor + const oldTransition = element.style.transition + + element.style.transition = 'none' + + element.style.backgroundColor = highlightColor + + // Force CSS repaint + element.offsetHeight // eslint-disable-line no-unused-expressions + + element.style.transition = 'background-color ' + highlightDuration + element.style.backgroundColor = + oldBackground.length > 0 ? oldBackground : defaultBackgroundColor + + element.addEventListener( + 'transitionend', + () => { + element.style.transition = oldTransition + element.style.backgroundColor = oldBackground + }, + { once: true } + ) + } +} diff --git a/src/components/markdown/references/references.jsx b/src/components/markdown/references/references.jsx new file mode 100644 index 000000000..cbd6d4036 --- /dev/null +++ b/src/components/markdown/references/references.jsx @@ -0,0 +1,32 @@ +import React, { useContext } from 'react' + +import { BibReferencesContext } from './context' + +import * as style from './references.module.css' + +const References = () => { + const { references, usedReferenceIds } = useContext(BibReferencesContext) + + const referenceListItems = Array.from(usedReferenceIds).map((referenceId) => { + const referenceEntry = references[referenceId] + + return ( +
  • +
    +
    +
  • + ) + }) + + return referenceListItems.length > 0 ? ( +
      {referenceListItems}
    + ) : ( +

    + No references have been cited on this page yet. +

    + ) +} + +export default References diff --git a/src/components/markdown/references/references.module.css b/src/components/markdown/references/references.module.css new file mode 100644 index 000000000..794fe3ac8 --- /dev/null +++ b/src/components/markdown/references/references.module.css @@ -0,0 +1,8 @@ +.anchor { + position: absolute; + width: 1px; + height: 80px; + margin-top: -80px; + visibility: hidden; + pointer-events: none; +} diff --git a/src/components/page-template.jsx b/src/components/page-template.jsx index 206940d92..3a3b62320 100644 --- a/src/components/page-template.jsx +++ b/src/components/page-template.jsx @@ -1,19 +1,37 @@ -import React from 'react' +import React, { useState } from 'react' import PropTypes from 'prop-types' import { graphql } from 'gatsby' import { MDXRenderer } from 'gatsby-plugin-mdx' +import { BibReferencesContext } from './markdown/references/context' + import Layout from './layout' const PageTemplate = (props) => { + const [usedReferenceIds, setUsedReferenceIds] = useState(new Set()) + + function addUsedReference(referenceId) { + setUsedReferenceIds( + (previousReferences) => new Set([...previousReferences, referenceId]) + ) + } + + const referencesContext = { + references: props.pageContext.references, + usedReferenceIds, + addUsedReference, + } + return ( - - {props.data.mdx.body} - + + + {props.data.mdx.body} + + ) } diff --git a/yarn.lock b/yarn.lock index 3f235f55e..12b5d3fb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1081,6 +1081,89 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@citation-js/cli@0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@citation-js/cli/-/cli-0.5.5.tgz#61def791884b927657eb9ec807c9bb2078be928e" + integrity sha512-U+Xuy+Vzwq/H4X3NYvL3Uj2G+08mMTFuumszRX53HyHUzeBhhh+a42Uhw4dMzrh8SWRy6JDcsijEOww5jC6YHg== + dependencies: + "@citation-js/core" "^0.5.4" + "@citation-js/plugin-bibjson" "^0.5.4" + "@citation-js/plugin-bibtex" "^0.5.5" + "@citation-js/plugin-csl" "^0.5.5" + "@citation-js/plugin-doi" "^0.5.4" + "@citation-js/plugin-ris" "^0.5.5" + "@citation-js/plugin-wikidata" "^0.5.4" + commander "^5.1.0" + +"@citation-js/core@0.5.4", "@citation-js/core@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@citation-js/core/-/core-0.5.4.tgz#f44a1d4e69fd372b634773a12742a8ffdbde3234" + integrity sha512-84as1w5v24DDTlzaRUtEBWIcj5wNw9A2ELqQs2ngrbFsc1psg1FbmwJplZQDSsiJHt+MfO23+DRmtOXyn7qXsg== + dependencies: + "@citation-js/date" "^0.5.0" + "@citation-js/name" "^0.4.2" + isomorphic-fetch "^3.0.0" + sync-fetch "^0.3.0" + +"@citation-js/date@0.5.1", "@citation-js/date@^0.5.0": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@citation-js/date/-/date-0.5.1.tgz#5d4e3746ce6a66b467c94071284de1c38e9a4987" + integrity sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw== + +"@citation-js/name@0.4.2", "@citation-js/name@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@citation-js/name/-/name-0.4.2.tgz#8dc834b7e6c06998fc8d3b63632658754c94a875" + integrity sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ== + +"@citation-js/plugin-bibjson@0.5.4", "@citation-js/plugin-bibjson@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@citation-js/plugin-bibjson/-/plugin-bibjson-0.5.4.tgz#dc0075caa0b40d93c8307af0536d04768e9d4abc" + integrity sha512-5KT7c8NOU7X2/JUgL8GmtOZFJtMFyCsLgoRBwpgDZ1pGdhE4H0/2292iPQ6SDyf2KxWAKXEy3DV5oMCqX9nzzA== + dependencies: + "@citation-js/date" "^0.5.0" + "@citation-js/name" "^0.4.2" + +"@citation-js/plugin-bibtex@0.5.5", "@citation-js/plugin-bibtex@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@citation-js/plugin-bibtex/-/plugin-bibtex-0.5.5.tgz#4983aad6747cf34f34a86c45aeef62a28366d757" + integrity sha512-rJICtSMDUrIqHQlazMz6F8emftg4IA/ZGHFQkbZUYfebd+c5Ksid2byLwNFXGoe1hc/I92P2rKCKbz+UbHUeWQ== + dependencies: + "@citation-js/date" "^0.5.0" + "@citation-js/name" "^0.4.2" + moo "^0.5.1" + +"@citation-js/plugin-csl@0.5.5", "@citation-js/plugin-csl@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@citation-js/plugin-csl/-/plugin-csl-0.5.5.tgz#8617fbd13667229c51390e734336cb1f418869ac" + integrity sha512-ytid/o/wpOU9OZ1gecibcfIP3YfFHhi0zamkVEbFp9Zwlud1Imyj96Mw8xTCgibVRhoQ4N7QJ/BToXIq+ON4DQ== + dependencies: + "@citation-js/date" "^0.5.0" + citeproc "^2.4.6" + +"@citation-js/plugin-doi@0.5.4", "@citation-js/plugin-doi@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@citation-js/plugin-doi/-/plugin-doi-0.5.4.tgz#903807fb04c7d8de2aebb915f063019e73fce479" + integrity sha512-L5K58SWUYT/QFu6yOt7iB9tysSBeorX2hkE3g4rsgvayHKdmVvfmyaRoEVlxgoDRseLaReevJiYn+LOkrte9Hw== + dependencies: + "@citation-js/date" "^0.5.0" + +"@citation-js/plugin-ris@0.5.5", "@citation-js/plugin-ris@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@citation-js/plugin-ris/-/plugin-ris-0.5.5.tgz#8c86e8dfeeb8c56f38cece0cb037128e6b00f4e4" + integrity sha512-mP2unuquPA156VhUeLCQquMHpxcAd+jNHpRlMFv7Qz5SATM8z/VTRsBPLC6k2om6bkGok1r5fjpmUO168OGLKA== + dependencies: + "@citation-js/date" "^0.5.0" + "@citation-js/name" "^0.4.2" + +"@citation-js/plugin-wikidata@0.5.4", "@citation-js/plugin-wikidata@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@citation-js/plugin-wikidata/-/plugin-wikidata-0.5.4.tgz#c97d5fcd3f471fb306b0f4e37a1a96d35387af03" + integrity sha512-3DV0j3Rk7RjRjxgyat5lFyg+Uat6jniZAJnKPV+ryWW2N5mhdksBLWqRaTCcsN/CFZ0PEnCY1eRajfIjNKuGjg== + dependencies: + "@citation-js/date" "^0.5.0" + "@citation-js/name" "^0.4.2" + wikidata-sdk "7" + "@endemolshinegroup/cosmiconfig-typescript-loader@3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz#eea4635828dde372838b0909693ebd9aafeec22d" @@ -3713,6 +3796,28 @@ ci-info@2.0.0, ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +citation-js@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/citation-js/-/citation-js-0.5.5.tgz#8a2e2a78865f771652d21aff3f211a4c70bd3b13" + integrity sha512-jWaLw8A5jrOJ2SbJbqFfMwp89w+qojlsKORfjhL4pm2wqoXer7DKu1gWWU5FCgkO4vn9JFYOThGRioHWo/jYiQ== + dependencies: + "@citation-js/cli" "0.5.5" + "@citation-js/core" "0.5.4" + "@citation-js/date" "0.5.1" + "@citation-js/name" "0.4.2" + "@citation-js/plugin-bibjson" "0.5.4" + "@citation-js/plugin-bibtex" "0.5.5" + "@citation-js/plugin-csl" "0.5.5" + "@citation-js/plugin-doi" "0.5.4" + "@citation-js/plugin-ris" "0.5.5" + "@citation-js/plugin-wikidata" "0.5.4" + citeproc "^2.4.59" + +citeproc@^2.4.59, citeproc@^2.4.6: + version "2.4.62" + resolved "https://registry.yarnpkg.com/citeproc/-/citeproc-2.4.62.tgz#c2a27eafd7182b95b0812b92c53193b037840ed3" + integrity sha512-l3uFfSEwNZp/jlz/TpgyBs85kOww6VlQHbAth0cpbgOn6iulZd+QlFY43LrRelzcYt3FZHTZ3soDyd8lNmkqdw== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -3908,6 +4013,11 @@ commander@^2.20.0, commander@^2.20.3, commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + commander@^6.0.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -8432,6 +8542,14 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + isomorphic-ws@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" @@ -9671,6 +9789,11 @@ moment@^2.27.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== +moo@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" + integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== + mozjpeg@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/mozjpeg/-/mozjpeg-7.1.0.tgz#23f202f3e48e98f02ed84f415358d4cbfab66c19" @@ -13476,6 +13599,14 @@ sync-fetch@0.3.0: buffer "^5.7.0" node-fetch "^2.6.1" +sync-fetch@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/sync-fetch/-/sync-fetch-0.3.1.tgz#62aa82c4b4d43afd6906bfd7b5f92056458509f0" + integrity sha512-xj5qiCDap/03kpci5a+qc5wSJjc8ZSixgG2EUmH1B8Ea2sfWclQA7eH40hiHPCtkCn6MCk4Wb+dqcXdCy2PP3g== + dependencies: + buffer "^5.7.0" + node-fetch "^2.6.1" + table@^6.0.9, table@^6.6.0: version "6.7.1" resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" @@ -14715,6 +14846,11 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +whatwg-fetch@^3.4.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -14759,6 +14895,18 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +wikibase-sdk@^7.14.2: + version "7.14.2" + resolved "https://registry.yarnpkg.com/wikibase-sdk/-/wikibase-sdk-7.14.2.tgz#00392bcaa2817542f349b3d5c0eacec84d66aec6" + integrity sha512-s+U23nKhP+ZA9yfxH/fe5mjZRvcb8/YtinMoKUkr/05MNNWbofUPRZAgQa1B3i0xvQ37zbkhaOP5K3HkC1apwQ== + +wikidata-sdk@7: + version "7.14.2" + resolved "https://registry.yarnpkg.com/wikidata-sdk/-/wikidata-sdk-7.14.2.tgz#973eb57cff4adf5bda3d4ca9112b4a4d897ab244" + integrity sha512-+7cVBKAqaJAfnCTfJZ3rs55mn56TykNh7tm1V17jUtp6yMTt6PoS+GDW0sDaL60fIl+qOHnca4oknHMaTmcBVg== + dependencies: + wikibase-sdk "^7.14.2" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"