1+ const { Cite } = require ( '@citation-js/core' ) ;
2+ require ( '@citation-js/plugin-bibtex' ) ;
3+ require ( '@citation-js/plugin-csl' ) ;
4+ const fs = require ( 'fs' ) ;
5+
6+ const DEFAULT_INPUT = 'https://docs.google.com/document/d/1-KKsOYZWJ3LdgdO2b2uJsOG2AmUDaQBNqWVVTY2W4W8/edit?tab=t.0' ;
7+ const DEFAULT_OUTPUT = 'apa_lookup.json' ;
8+
9+ async function fetchBibtex ( input ) {
10+ if ( ! input . startsWith ( 'http' ) ) {
11+ return fs . readFileSync ( input , 'utf-8' ) ;
12+ }
13+
14+ if ( input . includes ( 'docs.google.com' ) ) {
15+ const match = input . match ( / \/ d \/ ( [ a - z A - Z 0 - 9 _ - ] + ) / ) ;
16+ if ( ! match ) throw new Error ( 'Invalid Google Doc URL' ) ;
17+ const exportUrl = `https://docs.google.com/document/d/${ match [ 1 ] } /export?format=txt` ;
18+ const response = await fetch ( exportUrl ) ;
19+ if ( ! response . ok ) throw new Error ( `Failed to fetch: ${ response . status } ` ) ;
20+ let text = await response . text ( ) ;
21+ return text . replace ( / \[ [ a - z ] + \] / gi, '' ) ; // Remove Google Docs comment markers
22+ }
23+
24+ const response = await fetch ( input ) ;
25+ if ( ! response . ok ) throw new Error ( `Failed to fetch: ${ response . status } ` ) ;
26+ return response . text ( ) ;
27+ }
28+
29+ function extractUrl ( entry ) {
30+ if ( entry . URL ) return entry . URL ;
31+ if ( entry . note ) {
32+ const match = entry . note . match ( / h t t p s ? : \/ \/ [ ^ \s ] + / ) ;
33+ if ( match ) return match [ 0 ] ;
34+ }
35+ return null ;
36+ }
37+
38+ function bibtexToApaJson ( bibtexContent , includeUrl = true ) {
39+ const cite = new Cite ( bibtexContent ) ;
40+ const result = { } ;
41+
42+ for ( const entry of cite . data ) {
43+ const key = entry . id || entry [ 'citation-key' ] ;
44+ let ref = new Cite ( entry ) . format ( 'bibliography' , {
45+ format : 'text' ,
46+ template : 'apa' ,
47+ lang : 'en-US'
48+ } ) . trim ( ) ;
49+
50+ if ( includeUrl ) {
51+ const url = extractUrl ( entry ) ;
52+ if ( url && ! url . includes ( 'doi.org' ) && ! ref . includes ( url ) ) {
53+ ref = ref . match ( / h t t p s ? : \/ \/ [ ^ \s ] + $ / )
54+ ? `${ ref } Retrieved from ${ url } `
55+ : ref . replace ( / \. ? $ / , `. Retrieved from ${ url } ` ) ;
56+ }
57+ }
58+
59+ result [ key ] = ref ;
60+ }
61+
62+ return result ;
63+ }
64+
65+ async function main ( ) {
66+ const args = process . argv . slice ( 2 ) ;
67+ let input = DEFAULT_INPUT ;
68+ let output = DEFAULT_OUTPUT ;
69+ let includeUrl = true ;
70+
71+ for ( let i = 0 ; i < args . length ; i ++ ) {
72+ if ( args [ i ] === '-i' || args [ i ] === '--input' ) input = args [ ++ i ] ;
73+ else if ( args [ i ] === '-o' || args [ i ] === '--output' ) output = args [ ++ i ] ;
74+ else if ( args [ i ] === '--no-url' ) includeUrl = false ;
75+ else if ( args [ i ] === '-h' || args [ i ] === '--help' ) {
76+ console . log ( `Usage: node bibtex_to_apa.js [-i INPUT] [-o OUTPUT] [--no-url]
77+ Options:
78+ -i, --input Input BibTeX (URL or file). Default: Google Doc
79+ -o, --output Output JSON file. Default: apa_lookup.json
80+ --no-url Don't append URLs to references` ) ;
81+ process . exit ( 0 ) ;
82+ }
83+ }
84+
85+ const bibtex = await fetchBibtex ( input ) ;
86+ const apaJson = bibtexToApaJson ( bibtex , includeUrl ) ;
87+ fs . writeFileSync ( output , JSON . stringify ( apaJson , null , 2 ) ) ;
88+ console . log ( `Wrote ${ Object . keys ( apaJson ) . length } references to ${ output } ` ) ;
89+ }
90+
91+ main ( ) . catch ( console . error ) ;
0 commit comments