@citestyle/compiler 0.0.2
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 +138 -0
- package/locales/locales-de-DE.xml +734 -0
- package/locales/locales-en-US.xml +864 -0
- package/locales/locales-pt-BR.xml +746 -0
- package/package.json +36 -0
- package/src/cli.js +322 -0
- package/src/codegen.js +992 -0
- package/src/compile.js +43 -0
- package/src/index.js +9 -0
- package/src/locale.js +235 -0
- package/src/parser.js +606 -0
- package/types.d.ts +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@citestyle/compiler",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Build-time compiler: CSL XML → JavaScript modules",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./types.d.ts",
|
|
9
|
+
"import": "./src/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"citestyle": "./src/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"locales",
|
|
18
|
+
"types.d.ts",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@citestyle/types": "0.0.2",
|
|
23
|
+
"@xmldom/xmldom": "^0.8.11"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"vitest": "^3.1.1"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.19.0"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"lint": "echo 'TODO: configure linter'"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for the CSL compiler.
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* citestyle compile <input.csl> [options] — Compile CSL to JS module
|
|
8
|
+
* citestyle check <input.csl> — Validate CSL without compiling
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* -o, --output <file|dir> Output file or directory (default: stdout)
|
|
12
|
+
* --locale <lang[,lang]> Target locale(s) (default: style's default or en-US)
|
|
13
|
+
* --format <esm|cjs> Output format (default: esm)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs'
|
|
17
|
+
import { join, basename, extname, resolve, dirname } from 'node:path'
|
|
18
|
+
import { compile } from './compile.js'
|
|
19
|
+
import { parse } from './parser.js'
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2)
|
|
22
|
+
|
|
23
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
24
|
+
printHelp()
|
|
25
|
+
process.exit(0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const command = args[0]
|
|
29
|
+
|
|
30
|
+
if (command === 'compile') {
|
|
31
|
+
runCompile(args.slice(1))
|
|
32
|
+
} else if (command === 'check') {
|
|
33
|
+
runCheck(args.slice(1))
|
|
34
|
+
} else {
|
|
35
|
+
console.error(`Unknown command: ${command}. Use "citestyle compile" or "citestyle check".`)
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printHelp() {
|
|
40
|
+
console.log(`Usage: citestyle <command> [options]
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
compile <input...> Compile CSL XML to JavaScript module(s)
|
|
44
|
+
check <input...> Validate CSL XML without compiling
|
|
45
|
+
|
|
46
|
+
Options (compile):
|
|
47
|
+
-o, --output <path> Output file (single input) or directory (batch mode)
|
|
48
|
+
--locale <lang> Target locale, comma-separated for multi-locale (default: en-US)
|
|
49
|
+
--format <esm|cjs> Output format (default: esm)
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
citestyle compile apa.csl -o apa.js
|
|
53
|
+
citestyle compile apa.csl --locale fr-FR -o apa-fr.js
|
|
54
|
+
citestyle compile apa.csl --locale en-US,fr-FR -o dist/
|
|
55
|
+
citestyle compile styles/*.csl -o dist/
|
|
56
|
+
citestyle compile apa.csl --format cjs -o apa.cjs
|
|
57
|
+
citestyle check apa.csl
|
|
58
|
+
citestyle check styles/*.csl`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Compile command ─────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function runCompile(args) {
|
|
64
|
+
const { inputs, output, locales, format } = parseArgs(args)
|
|
65
|
+
|
|
66
|
+
if (inputs.length === 0) {
|
|
67
|
+
console.error('Error: No input file(s) specified.')
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Resolve input files (handle globs already expanded by shell)
|
|
72
|
+
const files = resolveInputFiles(inputs)
|
|
73
|
+
|
|
74
|
+
if (files.length === 0) {
|
|
75
|
+
console.error('Error: No .csl files found.')
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isBatch = files.length > 1 || (output && isDirectory(output))
|
|
80
|
+
let exitCode = 0
|
|
81
|
+
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
for (const locale of locales) {
|
|
84
|
+
try {
|
|
85
|
+
const cslXml = readFileSync(file, 'utf-8')
|
|
86
|
+
const options = { locale, format }
|
|
87
|
+
const { code, meta } = compile(cslXml, options)
|
|
88
|
+
|
|
89
|
+
let finalCode = code
|
|
90
|
+
if (format === 'cjs') {
|
|
91
|
+
finalCode = esmToCjs(code)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (output) {
|
|
95
|
+
let outPath
|
|
96
|
+
if (isBatch) {
|
|
97
|
+
mkdirSync(output, { recursive: true })
|
|
98
|
+
let outName = basename(file, extname(file))
|
|
99
|
+
if (locales.length > 1) outName += `-${locale}`
|
|
100
|
+
outName += format === 'cjs' ? '.cjs' : '.js'
|
|
101
|
+
outPath = join(output, outName)
|
|
102
|
+
} else {
|
|
103
|
+
mkdirSync(dirname(resolve(output)), { recursive: true })
|
|
104
|
+
outPath = output
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writeFileSync(outPath, finalCode)
|
|
108
|
+
const size = (finalCode.length / 1024).toFixed(1)
|
|
109
|
+
console.log(`✓ ${meta.title || basename(file)} → ${outPath} (${size}KB)`)
|
|
110
|
+
} else {
|
|
111
|
+
process.stdout.write(finalCode)
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(`✗ ${basename(file)}: ${formatError(err, file)}`)
|
|
115
|
+
exitCode = 1
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (exitCode) process.exit(exitCode)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Check command ───────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
function runCheck(args) {
|
|
126
|
+
const { inputs } = parseArgs(args)
|
|
127
|
+
|
|
128
|
+
if (inputs.length === 0) {
|
|
129
|
+
console.error('Error: No input file(s) specified.')
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const files = resolveInputFiles(inputs)
|
|
134
|
+
|
|
135
|
+
if (files.length === 0) {
|
|
136
|
+
console.error('Error: No .csl files found.')
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let exitCode = 0
|
|
141
|
+
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
try {
|
|
144
|
+
const cslXml = readFileSync(file, 'utf-8')
|
|
145
|
+
const ast = parse(cslXml)
|
|
146
|
+
|
|
147
|
+
// Basic validation
|
|
148
|
+
const warnings = validateAst(ast)
|
|
149
|
+
|
|
150
|
+
if (warnings.length === 0) {
|
|
151
|
+
console.log(`✓ ${basename(file)}: valid (${ast.info?.title || 'untitled'})`)
|
|
152
|
+
} else {
|
|
153
|
+
console.log(`⚠ ${basename(file)}: ${warnings.length} warning(s)`)
|
|
154
|
+
for (const w of warnings) console.log(` ${w}`)
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(`✗ ${basename(file)}: ${formatError(err, file)}`)
|
|
158
|
+
exitCode = 1
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (exitCode) process.exit(exitCode)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Argument parsing ────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
function parseArgs(args) {
|
|
168
|
+
const inputs = []
|
|
169
|
+
let output = null
|
|
170
|
+
let localeStr = null
|
|
171
|
+
let format = 'esm'
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < args.length; i++) {
|
|
174
|
+
if (args[i] === '-o' || args[i] === '--output') {
|
|
175
|
+
output = args[++i]
|
|
176
|
+
} else if (args[i] === '--locale') {
|
|
177
|
+
localeStr = args[++i]
|
|
178
|
+
} else if (args[i] === '--format') {
|
|
179
|
+
format = args[++i]
|
|
180
|
+
if (format !== 'esm' && format !== 'cjs') {
|
|
181
|
+
console.error(`Error: Unknown format "${format}". Use "esm" or "cjs".`)
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
} else if (!args[i].startsWith('-')) {
|
|
185
|
+
inputs.push(args[i])
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const locales = localeStr ? localeStr.split(',').map(s => s.trim()) : ['en-US']
|
|
190
|
+
|
|
191
|
+
return { inputs, output, locales, format }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveInputFiles(inputs) {
|
|
195
|
+
const files = []
|
|
196
|
+
for (const input of inputs) {
|
|
197
|
+
try {
|
|
198
|
+
const stat = statSync(input)
|
|
199
|
+
if (stat.isDirectory()) {
|
|
200
|
+
const entries = readdirSync(input).filter(f => f.endsWith('.csl')).sort()
|
|
201
|
+
files.push(...entries.map(f => join(input, f)))
|
|
202
|
+
} else {
|
|
203
|
+
files.push(input)
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Could be a file that doesn't exist
|
|
207
|
+
files.push(input)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return files
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isDirectory(path) {
|
|
214
|
+
try {
|
|
215
|
+
return statSync(path).isDirectory()
|
|
216
|
+
} catch {
|
|
217
|
+
// If it doesn't exist and ends with / or has no extension, treat as directory
|
|
218
|
+
return path.endsWith('/') || !extname(path)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── ESM → CJS conversion ───────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function esmToCjs(code) {
|
|
225
|
+
// Convert import statements
|
|
226
|
+
let result = code.replace(
|
|
227
|
+
/import\s*\{([^}]+)\}\s*from\s*'([^']+)'/g,
|
|
228
|
+
(_, imports, source) => {
|
|
229
|
+
const names = imports.split(',').map(s => s.trim()).filter(Boolean)
|
|
230
|
+
return `const { ${names.join(', ')} } = require('${source}')`
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Convert export function to module.exports
|
|
235
|
+
result = result.replace(
|
|
236
|
+
/export\s+function\s+(\w+)/g,
|
|
237
|
+
'function $1'
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Convert export const/let
|
|
241
|
+
result = result.replace(
|
|
242
|
+
/export\s+(const|let)\s+(\w+)/g,
|
|
243
|
+
'$1 $2'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// Add module.exports at the end — find all exported names
|
|
247
|
+
const exportedNames = []
|
|
248
|
+
const origExports = code.matchAll(/export\s+(?:function|const|let)\s+(\w+)/g)
|
|
249
|
+
for (const m of origExports) exportedNames.push(m[1])
|
|
250
|
+
|
|
251
|
+
if (exportedNames.length) {
|
|
252
|
+
result += `\nmodule.exports = { ${exportedNames.join(', ')} };\n`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
function validateAst(ast, file) {
|
|
261
|
+
const warnings = []
|
|
262
|
+
|
|
263
|
+
if (!ast.class) warnings.push('Missing style class (in-text or note)')
|
|
264
|
+
if (!ast.version) warnings.push('Missing CSL version')
|
|
265
|
+
if (!ast.info?.title) warnings.push('Missing <info><title>')
|
|
266
|
+
if (!ast.info?.id) warnings.push('Missing <info><id>')
|
|
267
|
+
|
|
268
|
+
if (!ast.bibliography && !ast.citation) {
|
|
269
|
+
warnings.push('No <citation> or <bibliography> element found')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (ast.citation && !ast.citation.layout) {
|
|
273
|
+
warnings.push('<citation> has no <layout> child')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (ast.bibliography && !ast.bibliography.layout) {
|
|
277
|
+
warnings.push('<bibliography> has no <layout> child')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for undefined macros (ast.macros is an object keyed by name)
|
|
281
|
+
const definedMacros = new Set(Object.keys(ast.macros || {}))
|
|
282
|
+
const usedMacros = findMacroRefs(ast)
|
|
283
|
+
for (const name of usedMacros) {
|
|
284
|
+
if (!definedMacros.has(name)) {
|
|
285
|
+
warnings.push(`Reference to undefined macro "${name}"`)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return warnings
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function findMacroRefs(node, refs = new Set()) {
|
|
293
|
+
if (!node || typeof node !== 'object') return refs
|
|
294
|
+
if (node.type === 'text' && node.macro) refs.add(node.macro)
|
|
295
|
+
if (Array.isArray(node.children)) node.children.forEach(c => findMacroRefs(c, refs))
|
|
296
|
+
if (node.layout) findMacroRefs(node.layout, refs)
|
|
297
|
+
if (node.citation) findMacroRefs(node.citation, refs)
|
|
298
|
+
if (node.bibliography) findMacroRefs(node.bibliography, refs)
|
|
299
|
+
if (node.macros && typeof node.macros === 'object' && !Array.isArray(node.macros)) {
|
|
300
|
+
Object.values(node.macros).forEach(m => findMacroRefs(m, refs))
|
|
301
|
+
}
|
|
302
|
+
if (Array.isArray(node.branches)) node.branches.forEach(b => findMacroRefs(b, refs))
|
|
303
|
+
if (node.else) findMacroRefs(node.else, refs)
|
|
304
|
+
return refs
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Error formatting ────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
function formatError(err, file) {
|
|
310
|
+
const msg = err.message || String(err)
|
|
311
|
+
|
|
312
|
+
// Try to extract line/column info from XML parse errors
|
|
313
|
+
const lineMatch = msg.match(/line[:\s]+(\d+)/i)
|
|
314
|
+
const colMatch = msg.match(/col(?:umn)?[:\s]+(\d+)/i)
|
|
315
|
+
|
|
316
|
+
if (lineMatch) {
|
|
317
|
+
const loc = colMatch ? `${lineMatch[1]}:${colMatch[1]}` : lineMatch[1]
|
|
318
|
+
return `${msg} (at ${basename(file)}:${loc})`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return msg
|
|
322
|
+
}
|