react-cheminfo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/lib/citation/core/clipboard.d.ts +14 -0
- package/lib/citation/core/clipboard.d.ts.map +1 -0
- package/lib/citation/core/clipboard.js +25 -0
- package/lib/citation/core/clipboard.js.map +1 -0
- package/lib/citation/core/download.d.ts +11 -0
- package/lib/citation/core/download.d.ts.map +1 -0
- package/lib/citation/core/download.js +20 -0
- package/lib/citation/core/download.js.map +1 -0
- package/lib/citation/core/formats.d.ts +91 -0
- package/lib/citation/core/formats.d.ts.map +1 -0
- package/lib/citation/core/formats.js +125 -0
- package/lib/citation/core/formats.js.map +1 -0
- package/lib/citation/core/index.d.ts +10 -0
- package/lib/citation/core/index.d.ts.map +1 -0
- package/lib/citation/core/index.js +7 -0
- package/lib/citation/core/index.js.map +1 -0
- package/lib/citation/core/reference.d.ts +30 -0
- package/lib/citation/core/reference.d.ts.map +1 -0
- package/lib/citation/core/reference.js +9 -0
- package/lib/citation/core/reference.js.map +1 -0
- package/lib/citation/core/render.d.ts +21 -0
- package/lib/citation/core/render.d.ts.map +1 -0
- package/lib/citation/core/render.js +74 -0
- package/lib/citation/core/render.js.map +1 -0
- package/lib/citation/core/segments.d.ts +47 -0
- package/lib/citation/core/segments.d.ts.map +1 -0
- package/lib/citation/core/segments.js +134 -0
- package/lib/citation/core/segments.js.map +1 -0
- package/lib/citation/ui/CitationMenu.d.ts +14 -0
- package/lib/citation/ui/CitationMenu.d.ts.map +1 -0
- package/lib/citation/ui/CitationMenu.js +63 -0
- package/lib/citation/ui/CitationMenu.js.map +1 -0
- package/lib/citation/ui/CitationPreview.d.ts +18 -0
- package/lib/citation/ui/CitationPreview.d.ts.map +1 -0
- package/lib/citation/ui/CitationPreview.js +55 -0
- package/lib/citation/ui/CitationPreview.js.map +1 -0
- package/lib/citation/ui/CiteButton.d.ts +22 -0
- package/lib/citation/ui/CiteButton.d.ts.map +1 -0
- package/lib/citation/ui/CiteButton.js +15 -0
- package/lib/citation/ui/CiteButton.js.map +1 -0
- package/lib/citation/ui/entries.d.ts +45 -0
- package/lib/citation/ui/entries.d.ts.map +1 -0
- package/lib/citation/ui/entries.js +52 -0
- package/lib/citation/ui/entries.js.map +1 -0
- package/lib/citation/ui/index.d.ts +7 -0
- package/lib/citation/ui/index.d.ts.map +1 -0
- package/lib/citation/ui/index.js +4 -0
- package/lib/citation/ui/index.js.map +1 -0
- package/lib/core.d.ts +3 -0
- package/lib/core.d.ts.map +1 -0
- package/lib/core.js +3 -0
- package/lib/core.js.map +1 -0
- package/lib/ecosystem/core/index.d.ts +3 -0
- package/lib/ecosystem/core/index.d.ts.map +1 -0
- package/lib/ecosystem/core/index.js +2 -0
- package/lib/ecosystem/core/index.js.map +1 -0
- package/lib/ecosystem/core/sites.d.ts +57 -0
- package/lib/ecosystem/core/sites.d.ts.map +1 -0
- package/lib/ecosystem/core/sites.js +105 -0
- package/lib/ecosystem/core/sites.js.map +1 -0
- package/lib/ecosystem/ui/EcosystemButton.d.ts +24 -0
- package/lib/ecosystem/ui/EcosystemButton.d.ts.map +1 -0
- package/lib/ecosystem/ui/EcosystemButton.js +14 -0
- package/lib/ecosystem/ui/EcosystemButton.js.map +1 -0
- package/lib/ecosystem/ui/EcosystemMenu.d.ts +17 -0
- package/lib/ecosystem/ui/EcosystemMenu.d.ts.map +1 -0
- package/lib/ecosystem/ui/EcosystemMenu.js +112 -0
- package/lib/ecosystem/ui/EcosystemMenu.js.map +1 -0
- package/lib/ecosystem/ui/index.d.ts +7 -0
- package/lib/ecosystem/ui/index.d.ts.map +1 -0
- package/lib/ecosystem/ui/index.js +4 -0
- package/lib/ecosystem/ui/index.js.map +1 -0
- package/lib/ecosystem/ui/marks.d.ts +18 -0
- package/lib/ecosystem/ui/marks.d.ts.map +1 -0
- package/lib/ecosystem/ui/marks.js +43 -0
- package/lib/ecosystem/ui/marks.js.map +1 -0
- package/lib/shared/ui/MenuButton.d.ts +34 -0
- package/lib/shared/ui/MenuButton.d.ts.map +1 -0
- package/lib/shared/ui/MenuButton.js +18 -0
- package/lib/shared/ui/MenuButton.js.map +1 -0
- package/lib/shared/ui/index.d.ts +2 -0
- package/lib/shared/ui/index.d.ts.map +1 -0
- package/lib/shared/ui/index.js +2 -0
- package/lib/shared/ui/index.js.map +1 -0
- package/lib/ui.d.ts +4 -0
- package/lib/ui.d.ts.map +1 -0
- package/lib/ui.js +4 -0
- package/lib/ui.js.map +1 -0
- package/package.json +79 -0
- package/src/citation/core/clipboard.ts +32 -0
- package/src/citation/core/download.ts +27 -0
- package/src/citation/core/formats.ts +169 -0
- package/src/citation/core/index.ts +23 -0
- package/src/citation/core/reference.ts +33 -0
- package/src/citation/core/render.ts +78 -0
- package/src/citation/core/segments.ts +170 -0
- package/src/citation/ui/CitationMenu.tsx +132 -0
- package/src/citation/ui/CitationPreview.tsx +84 -0
- package/src/citation/ui/CiteButton.tsx +39 -0
- package/src/citation/ui/entries.tsx +144 -0
- package/src/citation/ui/index.ts +6 -0
- package/src/core.ts +2 -0
- package/src/ecosystem/core/index.ts +7 -0
- package/src/ecosystem/core/sites.ts +164 -0
- package/src/ecosystem/ui/EcosystemButton.tsx +41 -0
- package/src/ecosystem/ui/EcosystemMenu.tsx +191 -0
- package/src/ecosystem/ui/index.ts +6 -0
- package/src/ecosystem/ui/marks.tsx +207 -0
- package/src/shared/ui/MenuButton.tsx +67 -0
- package/src/shared/ui/index.ts +1 -0
- package/src/ui.ts +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-cheminfo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Assembled React components shared by the cheminfo sites: a Cite button and an ecosystem menu",
|
|
6
|
+
"author": "Luc Patiny",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"react",
|
|
9
|
+
"cheminfo",
|
|
10
|
+
"citation",
|
|
11
|
+
"components"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
"./core": "./lib/core.js",
|
|
16
|
+
"./ui": "./lib/ui.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build-storybook": "storybook build",
|
|
24
|
+
"check-types": "tsc --noEmit",
|
|
25
|
+
"clean": "rimraf coverage lib storybook-static",
|
|
26
|
+
"dev": "storybook dev -p 10815",
|
|
27
|
+
"eslint": "eslint .",
|
|
28
|
+
"eslint-fix": "eslint . --fix",
|
|
29
|
+
"prepack": "npm run tsc",
|
|
30
|
+
"prettier": "prettier --check .",
|
|
31
|
+
"prettier-write": "prettier --write .",
|
|
32
|
+
"test": "npm run test-only && npm run check-types && npm run eslint && npm run prettier",
|
|
33
|
+
"test-e2e": "playwright test",
|
|
34
|
+
"test-e2e-ui": "playwright test --ui",
|
|
35
|
+
"test-only": "vitest run --coverage",
|
|
36
|
+
"tsc": "npm run clean && npm run tsc-build",
|
|
37
|
+
"tsc-build": "tsc --project tsconfig.build.json"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"cheminfo-font": "^1.28.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@blueprintjs/core": "^6.18.0",
|
|
44
|
+
"@playwright/test": "^1.62.1",
|
|
45
|
+
"@storybook/addon-docs": "^10.5.8",
|
|
46
|
+
"@storybook/react-vite": "^10.5.8",
|
|
47
|
+
"@types/node": "^26.2.0",
|
|
48
|
+
"@types/react": "^19.2.18",
|
|
49
|
+
"@types/react-dom": "^19.2.4",
|
|
50
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
51
|
+
"@zakodium/tsconfig": "^1.0.5",
|
|
52
|
+
"eslint": "^9.39.5",
|
|
53
|
+
"eslint-config-cheminfo-react": "^20.1.0",
|
|
54
|
+
"eslint-config-cheminfo-typescript": "^22.1.0",
|
|
55
|
+
"prettier": "^3.9.6",
|
|
56
|
+
"react": "^19.2.8",
|
|
57
|
+
"react-dom": "^19.2.8",
|
|
58
|
+
"react-science": "^23.0.0",
|
|
59
|
+
"rimraf": "^6.1.3",
|
|
60
|
+
"storybook": "^10.5.8",
|
|
61
|
+
"typescript": "^6.0.3",
|
|
62
|
+
"vite": "^8.2.1",
|
|
63
|
+
"vitest": "^4.1.10"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@blueprintjs/core": "^6.18.0",
|
|
67
|
+
"react": ">=19",
|
|
68
|
+
"react-dom": ">=19",
|
|
69
|
+
"react-science": "^21.0.0 || ^22.0.0 || ^23.0.0"
|
|
70
|
+
},
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "git+https://github.com/cheminfo/react-cheminfo.git"
|
|
74
|
+
},
|
|
75
|
+
"bugs": {
|
|
76
|
+
"url": "https://github.com/cheminfo/react-cheminfo/issues"
|
|
77
|
+
},
|
|
78
|
+
"homepage": "https://github.com/cheminfo/react-cheminfo#readme"
|
|
79
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CitationFormatId } from './formats.ts';
|
|
2
|
+
import { formatCitation } from './formats.ts';
|
|
3
|
+
import type { Reference } from './reference.ts';
|
|
4
|
+
import type { CitationStyleId } from './segments.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Put a reference on the clipboard. HTML goes on in both flavours, so a paste
|
|
8
|
+
* into Word or Google Docs keeps the emphasis of the style while a plain text
|
|
9
|
+
* editor still receives a readable line rather than the markup.
|
|
10
|
+
* @param reference - Reference to copy.
|
|
11
|
+
* @param format - Format to copy it in.
|
|
12
|
+
* @param style - Journal style, for the formats that have one.
|
|
13
|
+
* @returns Resolves once the clipboard holds the citation.
|
|
14
|
+
*/
|
|
15
|
+
export function copyCitation(
|
|
16
|
+
reference: Reference,
|
|
17
|
+
format: CitationFormatId,
|
|
18
|
+
style?: CitationStyleId,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const value = formatCitation(reference, format, style);
|
|
21
|
+
if (format !== 'html' || typeof ClipboardItem === 'undefined') {
|
|
22
|
+
return navigator.clipboard.writeText(value);
|
|
23
|
+
}
|
|
24
|
+
return navigator.clipboard.write([
|
|
25
|
+
new ClipboardItem({
|
|
26
|
+
'text/html': new Blob([value], { type: 'text/html' }),
|
|
27
|
+
'text/plain': new Blob([formatCitation(reference, 'text', style)], {
|
|
28
|
+
type: 'text/plain',
|
|
29
|
+
}),
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CitationDownload } from './formats.ts';
|
|
2
|
+
import { citationFilename, formatCitation } from './formats.ts';
|
|
3
|
+
import type { Reference } from './reference.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Save the reference as the file a reference manager imports. The MIME type
|
|
7
|
+
* is the one Zotero, Mendeley and EndNote recognise, so opening the saved
|
|
8
|
+
* file hands it to whichever of them is installed.
|
|
9
|
+
* @param reference - Reference to save.
|
|
10
|
+
* @param download - File to write, from `CITATION_DOWNLOADS`.
|
|
11
|
+
*/
|
|
12
|
+
export function downloadCitation(
|
|
13
|
+
reference: Reference,
|
|
14
|
+
download: CitationDownload,
|
|
15
|
+
): void {
|
|
16
|
+
const content = formatCitation(reference, download.format);
|
|
17
|
+
const url = URL.createObjectURL(
|
|
18
|
+
new Blob([content], { type: download.mimeType }),
|
|
19
|
+
);
|
|
20
|
+
const anchor = document.createElement('a');
|
|
21
|
+
anchor.href = url;
|
|
22
|
+
anchor.download = citationFilename(reference, download.extension);
|
|
23
|
+
document.body.append(anchor);
|
|
24
|
+
anchor.click();
|
|
25
|
+
anchor.remove();
|
|
26
|
+
URL.revokeObjectURL(url);
|
|
27
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Reference } from './reference.ts';
|
|
2
|
+
import { doiUrl } from './reference.ts';
|
|
3
|
+
import { renderHtml, renderMarkdown, renderText } from './render.ts';
|
|
4
|
+
import type { CitationStyleId } from './segments.ts';
|
|
5
|
+
import { citationSegments } from './segments.ts';
|
|
6
|
+
|
|
7
|
+
/** The formats the reference can be copied in. */
|
|
8
|
+
export type CitationFormatId =
|
|
9
|
+
'text' | 'html' | 'markdown' | 'bibtex' | 'ris' | 'doi';
|
|
10
|
+
|
|
11
|
+
/** One entry of the copy menu. */
|
|
12
|
+
export interface CitationFormat {
|
|
13
|
+
id: CitationFormatId;
|
|
14
|
+
/** Name of the format. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Where that format is pasted, shown on the right of the entry. */
|
|
17
|
+
hint: string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the format is written differently by each journal style, and so
|
|
20
|
+
* opens the submenu of styles rather than copying straight away.
|
|
21
|
+
*/
|
|
22
|
+
styled: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every format offered, in the order the menu lists them. Plain text is not
|
|
27
|
+
* one of them: an HTML copy carries it as its second flavour, so a paste into
|
|
28
|
+
* a plain editor already gives the unmarked line.
|
|
29
|
+
*/
|
|
30
|
+
export const CITATION_FORMATS = [
|
|
31
|
+
{ id: 'html', label: 'HTML', hint: 'Word, Docs, email', styled: true },
|
|
32
|
+
{ id: 'markdown', label: 'Markdown', hint: 'README, issue', styled: true },
|
|
33
|
+
{ id: 'bibtex', label: 'BibTeX', hint: 'LaTeX', styled: false },
|
|
34
|
+
{ id: 'ris', label: 'RIS', hint: 'EndNote, Zotero', styled: false },
|
|
35
|
+
{ id: 'doi', label: 'DOI link', hint: 'URL', styled: false },
|
|
36
|
+
] as const satisfies readonly CitationFormat[];
|
|
37
|
+
|
|
38
|
+
/** The style a format that has one is written in unless another is picked. */
|
|
39
|
+
export const DEFAULT_CITATION_STYLE: CitationStyleId = 'acs';
|
|
40
|
+
|
|
41
|
+
/** One entry of the download menu: a file a reference manager imports. */
|
|
42
|
+
export interface CitationDownload {
|
|
43
|
+
format: CitationFormatId;
|
|
44
|
+
label: string;
|
|
45
|
+
hint: string;
|
|
46
|
+
extension: string;
|
|
47
|
+
mimeType: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The files offered for download. Zotero, Mendeley and EndNote all import
|
|
52
|
+
* both, and their connectors recognise the MIME types below on their own.
|
|
53
|
+
*/
|
|
54
|
+
export const CITATION_DOWNLOADS = [
|
|
55
|
+
{
|
|
56
|
+
format: 'ris',
|
|
57
|
+
label: 'RIS file',
|
|
58
|
+
hint: 'Zotero, Mendeley, EndNote',
|
|
59
|
+
extension: 'ris',
|
|
60
|
+
mimeType: 'application/x-research-info-systems',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
format: 'bibtex',
|
|
64
|
+
label: 'BibTeX file',
|
|
65
|
+
hint: 'JabRef, Overleaf',
|
|
66
|
+
extension: 'bib',
|
|
67
|
+
mimeType: 'application/x-bibtex',
|
|
68
|
+
},
|
|
69
|
+
] as const satisfies readonly CitationDownload[];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Name the saved file carries, e.g. `Vanderveen2015.ris`.
|
|
73
|
+
* @param reference - Reference the file holds.
|
|
74
|
+
* @param extension - Extension of the file, without its dot.
|
|
75
|
+
* @returns The file name.
|
|
76
|
+
*/
|
|
77
|
+
export function citationFilename(
|
|
78
|
+
reference: Reference,
|
|
79
|
+
extension: string,
|
|
80
|
+
): string {
|
|
81
|
+
return `${bibTeXKey(reference)}.${extension}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render a reference in one of the citation formats.
|
|
86
|
+
* @param reference - Reference to render.
|
|
87
|
+
* @param format - Format to render it in.
|
|
88
|
+
* @param style - Journal style, for the formats that have one.
|
|
89
|
+
* @returns The citation, ready to be copied to the clipboard.
|
|
90
|
+
*/
|
|
91
|
+
export function formatCitation(
|
|
92
|
+
reference: Reference,
|
|
93
|
+
format: CitationFormatId,
|
|
94
|
+
style: CitationStyleId = DEFAULT_CITATION_STYLE,
|
|
95
|
+
): string {
|
|
96
|
+
switch (format) {
|
|
97
|
+
case 'text':
|
|
98
|
+
return renderText(citationSegments(reference, style));
|
|
99
|
+
case 'html':
|
|
100
|
+
return renderHtml(citationSegments(reference, style));
|
|
101
|
+
case 'markdown':
|
|
102
|
+
return renderMarkdown(citationSegments(reference, style));
|
|
103
|
+
case 'bibtex':
|
|
104
|
+
return formatBibTeX(reference);
|
|
105
|
+
case 'ris':
|
|
106
|
+
return formatRis(reference);
|
|
107
|
+
case 'doi':
|
|
108
|
+
return doiUrl(reference);
|
|
109
|
+
default:
|
|
110
|
+
throw new Error(`unknown citation format: ${String(format)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatBibTeX(reference: Reference): string {
|
|
115
|
+
const { title, journal, year, volume, issue, publisher, doi } = reference;
|
|
116
|
+
const { firstPage, lastPage } = reference;
|
|
117
|
+
const fields: Array<[string, string]> = [
|
|
118
|
+
['author', authorList(reference, ' and ')],
|
|
119
|
+
['title', title],
|
|
120
|
+
['journal', journal],
|
|
121
|
+
['year', String(year)],
|
|
122
|
+
['volume', volume],
|
|
123
|
+
['number', issue],
|
|
124
|
+
['pages', `${firstPage}--${lastPage}`],
|
|
125
|
+
['publisher', publisher],
|
|
126
|
+
['doi', doi],
|
|
127
|
+
['url', doiUrl(reference)],
|
|
128
|
+
];
|
|
129
|
+
const body = fields
|
|
130
|
+
.map(([name, value]) => ` ${name} = {${value}},`)
|
|
131
|
+
.join('\n');
|
|
132
|
+
return `@article{${bibTeXKey(reference)},\n${body}\n}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatRis(reference: Reference): string {
|
|
136
|
+
const { title, journal, year, volume, issue, publisher, doi } = reference;
|
|
137
|
+
const { authors, firstPage, lastPage } = reference;
|
|
138
|
+
const lines: Array<[string, string]> = [
|
|
139
|
+
['TY', 'JOUR'],
|
|
140
|
+
...authors.map((author): [string, string] => [
|
|
141
|
+
'AU',
|
|
142
|
+
`${author.family}, ${author.given}`,
|
|
143
|
+
]),
|
|
144
|
+
['TI', title],
|
|
145
|
+
['JO', journal],
|
|
146
|
+
['PY', String(year)],
|
|
147
|
+
['VL', volume],
|
|
148
|
+
['IS', issue],
|
|
149
|
+
['SP', firstPage],
|
|
150
|
+
['EP', lastPage],
|
|
151
|
+
['PB', publisher],
|
|
152
|
+
['DO', doi],
|
|
153
|
+
['UR', doiUrl(reference)],
|
|
154
|
+
['ER', ''],
|
|
155
|
+
];
|
|
156
|
+
return lines.map(([tag, value]) => `${tag} - ${value}`).join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function authorList(reference: Reference, separator: string): string {
|
|
160
|
+
return reference.authors
|
|
161
|
+
.map((author) => `${author.family}, ${author.given}`)
|
|
162
|
+
.join(separator);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function bibTeXKey(reference: Reference): string {
|
|
166
|
+
const first = reference.authors[0];
|
|
167
|
+
const family = first === undefined ? 'reference' : first.family;
|
|
168
|
+
return `${family.replaceAll(/[^a-zA-Z]/g, '')}${reference.year}`;
|
|
169
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { copyCitation } from './clipboard.ts';
|
|
2
|
+
export { downloadCitation } from './download.ts';
|
|
3
|
+
export type {
|
|
4
|
+
CitationDownload,
|
|
5
|
+
CitationFormat,
|
|
6
|
+
CitationFormatId,
|
|
7
|
+
} from './formats.ts';
|
|
8
|
+
export {
|
|
9
|
+
CITATION_DOWNLOADS,
|
|
10
|
+
CITATION_FORMATS,
|
|
11
|
+
DEFAULT_CITATION_STYLE,
|
|
12
|
+
citationFilename,
|
|
13
|
+
formatCitation,
|
|
14
|
+
} from './formats.ts';
|
|
15
|
+
export type { Reference, ReferenceAuthor } from './reference.ts';
|
|
16
|
+
export { doiUrl } from './reference.ts';
|
|
17
|
+
export { renderHtml, renderMarkdown, renderText } from './render.ts';
|
|
18
|
+
export type {
|
|
19
|
+
CitationSegment,
|
|
20
|
+
CitationStyle,
|
|
21
|
+
CitationStyleId,
|
|
22
|
+
} from './segments.ts';
|
|
23
|
+
export { CITATION_STYLES, citationSegments } from './segments.ts';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One author of a reference, as the citation styles need them. */
|
|
2
|
+
export interface ReferenceAuthor {
|
|
3
|
+
/** Initials or first name, e.g. `J. R.`. */
|
|
4
|
+
given: string;
|
|
5
|
+
/** Family name, e.g. `Vanderveen`. */
|
|
6
|
+
family: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A journal article, with everything the citation formats need. */
|
|
10
|
+
export interface Reference {
|
|
11
|
+
authors: readonly ReferenceAuthor[];
|
|
12
|
+
title: string;
|
|
13
|
+
/** Full journal name, e.g. `Green Chemistry`. */
|
|
14
|
+
journal: string;
|
|
15
|
+
/** Abbreviated journal name, e.g. `Green Chem.`. */
|
|
16
|
+
journalAbbreviation: string;
|
|
17
|
+
year: number;
|
|
18
|
+
volume: string;
|
|
19
|
+
issue: string;
|
|
20
|
+
firstPage: string;
|
|
21
|
+
lastPage: string;
|
|
22
|
+
doi: string;
|
|
23
|
+
publisher: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolvable address of a reference.
|
|
28
|
+
* @param reference - Reference to link to.
|
|
29
|
+
* @returns The `https://doi.org/…` URL.
|
|
30
|
+
*/
|
|
31
|
+
export function doiUrl(reference: Reference): string {
|
|
32
|
+
return `https://doi.org/${reference.doi}`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CitationSegment } from './segments.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write the segments as they read in an email or a manuscript.
|
|
5
|
+
* @param segments - Segments of the citation.
|
|
6
|
+
* @returns The citation, with no markup at all.
|
|
7
|
+
*/
|
|
8
|
+
export function renderText(segments: readonly CitationSegment[]): string {
|
|
9
|
+
let result = '';
|
|
10
|
+
for (const segment of segments) {
|
|
11
|
+
result += segment.kind === 'link' ? segment.href : segment.text;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write the segments as HTML, so a paste into Word or a web page keeps the
|
|
18
|
+
* italics and the bold the style asks for.
|
|
19
|
+
* @param segments - Segments of the citation.
|
|
20
|
+
* @returns The citation as one line of HTML.
|
|
21
|
+
*/
|
|
22
|
+
export function renderHtml(segments: readonly CitationSegment[]): string {
|
|
23
|
+
let result = '';
|
|
24
|
+
for (const segment of segments) {
|
|
25
|
+
switch (segment.kind) {
|
|
26
|
+
case 'italic':
|
|
27
|
+
result += `<em>${escapeHtml(segment.text)}</em>`;
|
|
28
|
+
break;
|
|
29
|
+
case 'bold':
|
|
30
|
+
result += `<strong>${escapeHtml(segment.text)}</strong>`;
|
|
31
|
+
break;
|
|
32
|
+
case 'link':
|
|
33
|
+
result += `<a href="${escapeHtml(segment.href)}">${escapeHtml(segment.text)}</a>`;
|
|
34
|
+
break;
|
|
35
|
+
case 'text':
|
|
36
|
+
result += escapeHtml(segment.text);
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write the segments as Markdown, for a README or an issue.
|
|
47
|
+
* @param segments - Segments of the citation.
|
|
48
|
+
* @returns The citation as one line of Markdown.
|
|
49
|
+
*/
|
|
50
|
+
export function renderMarkdown(segments: readonly CitationSegment[]): string {
|
|
51
|
+
let result = '';
|
|
52
|
+
for (const segment of segments) {
|
|
53
|
+
switch (segment.kind) {
|
|
54
|
+
case 'italic':
|
|
55
|
+
result += `*${segment.text}*`;
|
|
56
|
+
break;
|
|
57
|
+
case 'bold':
|
|
58
|
+
result += `**${segment.text}**`;
|
|
59
|
+
break;
|
|
60
|
+
case 'link':
|
|
61
|
+
result += `[${segment.text}](${segment.href})`;
|
|
62
|
+
break;
|
|
63
|
+
case 'text':
|
|
64
|
+
result += segment.text;
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function escapeHtml(value: string): string {
|
|
74
|
+
return value
|
|
75
|
+
.replaceAll('&', '&')
|
|
76
|
+
.replaceAll('<', '<')
|
|
77
|
+
.replaceAll('>', '>');
|
|
78
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Reference } from './reference.ts';
|
|
2
|
+
import { doiUrl } from './reference.ts';
|
|
3
|
+
|
|
4
|
+
/** One piece of a written reference, with the emphasis its style asks for. */
|
|
5
|
+
export type CitationSegment =
|
|
6
|
+
| { kind: 'text' | 'italic' | 'bold'; text: string }
|
|
7
|
+
| { kind: 'link'; text: string; href: string };
|
|
8
|
+
|
|
9
|
+
/** The journal styles the reference can be written in. */
|
|
10
|
+
export type CitationStyleId = 'acs' | 'nature' | 'rsc' | 'wiley';
|
|
11
|
+
|
|
12
|
+
/** One entry of the style submenu. */
|
|
13
|
+
export interface CitationStyle {
|
|
14
|
+
id: CitationStyleId;
|
|
15
|
+
/** Name of the style. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Journals that ask for it, shown on the right of the entry. */
|
|
18
|
+
hint: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Every style offered, in the order the menu lists them. */
|
|
22
|
+
export const CITATION_STYLES = [
|
|
23
|
+
{ id: 'acs', label: 'ACS', hint: 'JACS, Org. Lett.' },
|
|
24
|
+
{ id: 'nature', label: 'Nature', hint: 'Nature, Nat. Chem.' },
|
|
25
|
+
{ id: 'rsc', label: 'RSC', hint: 'Chem. Sci., Green Chem.' },
|
|
26
|
+
{ id: 'wiley', label: 'Wiley', hint: 'Angew. Chem., Chem. Eur. J.' },
|
|
27
|
+
] as const satisfies readonly CitationStyle[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Write a reference in one journal style, as the pieces every output format
|
|
31
|
+
* then renders with its own emphasis.
|
|
32
|
+
* @param reference - Reference to write.
|
|
33
|
+
* @param style - Style to write it in.
|
|
34
|
+
* @returns The segments of the citation, in reading order.
|
|
35
|
+
*/
|
|
36
|
+
export function citationSegments(
|
|
37
|
+
reference: Reference,
|
|
38
|
+
style: CitationStyleId,
|
|
39
|
+
): CitationSegment[] {
|
|
40
|
+
switch (style) {
|
|
41
|
+
case 'acs':
|
|
42
|
+
return acs(reference);
|
|
43
|
+
case 'nature':
|
|
44
|
+
return nature(reference);
|
|
45
|
+
case 'rsc':
|
|
46
|
+
return rsc(reference);
|
|
47
|
+
case 'wiley':
|
|
48
|
+
return wiley(reference);
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`unknown citation style: ${String(style)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function acs(reference: Reference): CitationSegment[] {
|
|
55
|
+
const { title, journalAbbreviation, year, volume } = reference;
|
|
56
|
+
return [
|
|
57
|
+
text(`${familyFirst(reference, '; ')} ${title}. `),
|
|
58
|
+
italic(journalAbbreviation),
|
|
59
|
+
text(' '),
|
|
60
|
+
bold(String(year)),
|
|
61
|
+
text(', '),
|
|
62
|
+
italic(volume),
|
|
63
|
+
text(`, ${pageRange(reference)}. `),
|
|
64
|
+
doiLink(reference),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nature(reference: Reference): CitationSegment[] {
|
|
69
|
+
const { title, journalAbbreviation, year, volume } = reference;
|
|
70
|
+
return [
|
|
71
|
+
text(`${familyFirstAmpersand(reference)} ${title}. `),
|
|
72
|
+
italic(journalAbbreviation),
|
|
73
|
+
text(' '),
|
|
74
|
+
bold(volume),
|
|
75
|
+
text(`, ${pageRange(reference)} (${year}). `),
|
|
76
|
+
doiLink(reference),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rsc(reference: Reference): CitationSegment[] {
|
|
81
|
+
const { journalAbbreviation, year, volume } = reference;
|
|
82
|
+
return [
|
|
83
|
+
text(`${initialsFirst(reference, ' and ')}, `),
|
|
84
|
+
italic(journalAbbreviation),
|
|
85
|
+
text(`, ${year}, `),
|
|
86
|
+
bold(volume),
|
|
87
|
+
text(`, ${pageRange(reference)}. `),
|
|
88
|
+
doiLink(reference),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function wiley(reference: Reference): CitationSegment[] {
|
|
93
|
+
const { journalAbbreviation, year, volume } = reference;
|
|
94
|
+
return [
|
|
95
|
+
text(`${initialsFirst(reference, ', ')}, `),
|
|
96
|
+
italic(journalAbbreviation),
|
|
97
|
+
text(' '),
|
|
98
|
+
bold(String(year)),
|
|
99
|
+
text(', '),
|
|
100
|
+
italic(volume),
|
|
101
|
+
text(`, ${pageRange(reference)}. `),
|
|
102
|
+
doiLink(reference),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function text(value: string): CitationSegment {
|
|
107
|
+
return { kind: 'text', text: value };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function italic(value: string): CitationSegment {
|
|
111
|
+
return { kind: 'italic', text: value };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function bold(value: string): CitationSegment {
|
|
115
|
+
return { kind: 'bold', text: value };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function doiLink(reference: Reference): CitationSegment {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'link',
|
|
121
|
+
text: `doi:${reference.doi}`,
|
|
122
|
+
href: doiUrl(reference),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `Vanderveen, J. R.; Patiny, L.` — the family name leads.
|
|
128
|
+
* @param reference - The work being written.
|
|
129
|
+
* @param separator - What goes between two authors.
|
|
130
|
+
* @returns The authors, as one string.
|
|
131
|
+
*/
|
|
132
|
+
function familyFirst(reference: Reference, separator: string): string {
|
|
133
|
+
return reference.authors
|
|
134
|
+
.map((author) => `${author.family}, ${author.given}`)
|
|
135
|
+
.join(separator);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The same, with the last author introduced by an ampersand.
|
|
140
|
+
* @param reference - The work being written.
|
|
141
|
+
* @returns The authors, as one string.
|
|
142
|
+
*/
|
|
143
|
+
function familyFirstAmpersand(reference: Reference): string {
|
|
144
|
+
const names = reference.authors.map(
|
|
145
|
+
(author) => `${author.family}, ${author.given}`,
|
|
146
|
+
);
|
|
147
|
+
const last = names.at(-1);
|
|
148
|
+
if (names.length < 2 || last === undefined) return names.join(', ');
|
|
149
|
+
return `${names.slice(0, -1).join(', ')} & ${last}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `J. R. Vanderveen, L. Patiny` — the initials lead.
|
|
154
|
+
* @param reference - The work being written.
|
|
155
|
+
* @param lastSeparator - What introduces the last author.
|
|
156
|
+
* @returns The authors, as one string.
|
|
157
|
+
*/
|
|
158
|
+
function initialsFirst(reference: Reference, lastSeparator: string): string {
|
|
159
|
+
const names = reference.authors.map(
|
|
160
|
+
(author) => `${author.given} ${author.family}`,
|
|
161
|
+
);
|
|
162
|
+
const last = names.at(-1);
|
|
163
|
+
if (names.length < 2 || last === undefined) return names.join(', ');
|
|
164
|
+
return `${names.slice(0, -1).join(', ')}${lastSeparator}${last}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function pageRange(reference: Reference): string {
|
|
168
|
+
// En dash: what every style asks for in a page range.
|
|
169
|
+
return `${reference.firstPage}–${reference.lastPage}`;
|
|
170
|
+
}
|