@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/src/compile.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a CSL XML string into a JavaScript module string.
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: XML → parse → resolve locale → codegen → JS module
|
|
5
|
+
*
|
|
6
|
+
* @param {string} cslXml - CSL XML source
|
|
7
|
+
* @param {object} [options]
|
|
8
|
+
* @param {string} [options.locale='en-US'] - Target locale
|
|
9
|
+
* @param {string} [options.format='esm'] - Output format ('esm' | 'cjs')
|
|
10
|
+
* @returns {{ code: string, meta: object }}
|
|
11
|
+
*/
|
|
12
|
+
import { parse } from './parser.js'
|
|
13
|
+
import { resolveLocale } from './locale.js'
|
|
14
|
+
import { generate } from './codegen.js'
|
|
15
|
+
|
|
16
|
+
export function compile(cslXml, options = {}) {
|
|
17
|
+
const { locale = 'en-US' } = options
|
|
18
|
+
|
|
19
|
+
// 1. Parse CSL XML → AST
|
|
20
|
+
const ast = parse(cslXml)
|
|
21
|
+
|
|
22
|
+
// 2. Resolve locale: style overrides → locale file → en-US fallback
|
|
23
|
+
const targetLocale = ast.defaultLocale || locale
|
|
24
|
+
const resolvedLocale = resolveLocale(targetLocale, ast.localeOverrides)
|
|
25
|
+
|
|
26
|
+
// 3. Merge inheritable name options from the style root into the AST
|
|
27
|
+
// (the codegen handles this per-section)
|
|
28
|
+
|
|
29
|
+
// 4. Generate JavaScript module
|
|
30
|
+
const code = generate(ast, resolvedLocale, options)
|
|
31
|
+
|
|
32
|
+
// 5. Build meta for the return value
|
|
33
|
+
const info = ast.info || {}
|
|
34
|
+
const meta = {
|
|
35
|
+
id: (info.id || '').replace(/^.*\//, ''),
|
|
36
|
+
title: info.title || '',
|
|
37
|
+
class: ast.class,
|
|
38
|
+
version: ast.version,
|
|
39
|
+
defaultLocale: targetLocale,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { code, meta }
|
|
43
|
+
}
|
package/src/index.js
ADDED
package/src/locale.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSL Locale Resolution.
|
|
3
|
+
*
|
|
4
|
+
* Merges locale terms from: style locale overrides → locale file → en-US fallback.
|
|
5
|
+
* All resolved at compile time — no runtime locale loading.
|
|
6
|
+
*/
|
|
7
|
+
import { DOMParser } from '@xmldom/xmldom'
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { join, dirname } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const LOCALES_DIR = join(__dirname, '..', 'locales')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load and parse a locale XML file.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} lang - Locale identifier (e.g., 'en-US')
|
|
19
|
+
* @returns {LocaleData | null}
|
|
20
|
+
*/
|
|
21
|
+
export function loadLocaleFile(lang) {
|
|
22
|
+
try {
|
|
23
|
+
const path = join(LOCALES_DIR, `locales-${lang}.xml`)
|
|
24
|
+
const xml = readFileSync(path, 'utf-8')
|
|
25
|
+
return parseLocaleXml(xml)
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a locale XML string.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} xml
|
|
35
|
+
* @returns {LocaleData}
|
|
36
|
+
*/
|
|
37
|
+
export function parseLocaleXml(xml) {
|
|
38
|
+
const doc = new DOMParser().parseFromString(xml, 'text/xml')
|
|
39
|
+
const root = doc.documentElement
|
|
40
|
+
|
|
41
|
+
const data = {
|
|
42
|
+
lang: attr(root, 'xml:lang') || attr(root, 'lang') || '',
|
|
43
|
+
terms: {},
|
|
44
|
+
dateFormats: {},
|
|
45
|
+
styleOptions: {},
|
|
46
|
+
ordinalTerms: {},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const child of childElements(root)) {
|
|
50
|
+
const name = localName(child)
|
|
51
|
+
|
|
52
|
+
if (name === 'terms') {
|
|
53
|
+
parseTerms(child, data)
|
|
54
|
+
} else if (name === 'date') {
|
|
55
|
+
const form = attr(child, 'form')
|
|
56
|
+
data.dateFormats[form] = parseDateFormat(child)
|
|
57
|
+
} else if (name === 'style-options') {
|
|
58
|
+
data.styleOptions = {
|
|
59
|
+
punctuationInQuote: attr(child, 'punctuation-in-quote') === 'true',
|
|
60
|
+
limitDayOrdinalsToDay1: attr(child, 'limit-day-ordinals-to-day-1') === 'true',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return data
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse <terms> element.
|
|
70
|
+
*/
|
|
71
|
+
function parseTerms(termsEl, data) {
|
|
72
|
+
for (const termEl of childElements(termsEl)) {
|
|
73
|
+
if (localName(termEl) !== 'term') continue
|
|
74
|
+
|
|
75
|
+
const name = attr(termEl, 'name')
|
|
76
|
+
const form = attr(termEl, 'form') || 'long'
|
|
77
|
+
const gender = attr(termEl, 'gender')
|
|
78
|
+
const genderForm = attr(termEl, 'gender-form')
|
|
79
|
+
|
|
80
|
+
// Build key: "term-name" for long, "term-name:form" for others
|
|
81
|
+
let key = name
|
|
82
|
+
if (form !== 'long') key = name + ':' + form
|
|
83
|
+
|
|
84
|
+
// Check for single/multiple child elements
|
|
85
|
+
const single = findChild(termEl, 'single')
|
|
86
|
+
const multiple = findChild(termEl, 'multiple')
|
|
87
|
+
|
|
88
|
+
if (single && multiple) {
|
|
89
|
+
data.terms[key] = {
|
|
90
|
+
single: textContent(single),
|
|
91
|
+
multiple: textContent(multiple),
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
data.terms[key] = textContent(termEl)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Track ordinal terms separately for number formatting
|
|
98
|
+
if (name.startsWith('ordinal')) {
|
|
99
|
+
data.ordinalTerms[name] = typeof data.terms[key] === 'string'
|
|
100
|
+
? data.terms[key]
|
|
101
|
+
: data.terms[key].single || ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Track gender
|
|
105
|
+
if (gender) {
|
|
106
|
+
data.terms[key + ':gender'] = gender
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse a locale <date> format.
|
|
113
|
+
*/
|
|
114
|
+
function parseDateFormat(dateEl) {
|
|
115
|
+
const parts = []
|
|
116
|
+
for (const child of childElements(dateEl)) {
|
|
117
|
+
if (localName(child) === 'date-part') {
|
|
118
|
+
parts.push({
|
|
119
|
+
name: attr(child, 'name'),
|
|
120
|
+
form: attr(child, 'form'),
|
|
121
|
+
prefix: attr(child, 'prefix') || '',
|
|
122
|
+
suffix: attr(child, 'suffix') || '',
|
|
123
|
+
rangeDelimiter: attr(child, 'range-delimiter'),
|
|
124
|
+
textCase: attr(child, 'text-case'),
|
|
125
|
+
stripPeriods: attr(child, 'strip-periods') === 'true',
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { parts, delimiter: attr(dateEl, 'delimiter') || '' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the complete locale for a given language.
|
|
134
|
+
*
|
|
135
|
+
* Merge order (later overrides earlier):
|
|
136
|
+
* 1. en-US fallback (always loaded)
|
|
137
|
+
* 2. Primary language locale file (e.g., fr-FR)
|
|
138
|
+
* 3. Style-level locale overrides
|
|
139
|
+
*
|
|
140
|
+
* @param {string} lang - Target language
|
|
141
|
+
* @param {Array} styleLocaleOverrides - From the parsed CSL AST
|
|
142
|
+
* @returns {ResolvedLocale}
|
|
143
|
+
*/
|
|
144
|
+
export function resolveLocale(lang, styleLocaleOverrides = []) {
|
|
145
|
+
const resolved = {
|
|
146
|
+
terms: {},
|
|
147
|
+
dateFormats: {},
|
|
148
|
+
styleOptions: {},
|
|
149
|
+
ordinalTerms: {},
|
|
150
|
+
monthTerms: {},
|
|
151
|
+
seasonTerms: {},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 1. Load en-US as fallback
|
|
155
|
+
const enUS = loadLocaleFile('en-US')
|
|
156
|
+
if (enUS) mergeLocale(resolved, enUS)
|
|
157
|
+
|
|
158
|
+
// 2. Load target locale (if not en-US)
|
|
159
|
+
if (lang && lang !== 'en-US') {
|
|
160
|
+
const target = loadLocaleFile(lang)
|
|
161
|
+
if (target) mergeLocale(resolved, target)
|
|
162
|
+
|
|
163
|
+
// Try base language fallback (e.g., 'fr' for 'fr-FR')
|
|
164
|
+
if (!target && lang.includes('-')) {
|
|
165
|
+
const base = loadLocaleFile(lang.split('-')[0])
|
|
166
|
+
if (base) mergeLocale(resolved, base)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Apply style-level locale overrides
|
|
171
|
+
for (const override of styleLocaleOverrides) {
|
|
172
|
+
if (!override.lang || override.lang === lang) {
|
|
173
|
+
if (override.terms) Object.assign(resolved.terms, override.terms)
|
|
174
|
+
if (override.dateFormats) Object.assign(resolved.dateFormats, override.dateFormats)
|
|
175
|
+
if (override.styleOptions) Object.assign(resolved.styleOptions, override.styleOptions)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Extract month and season terms for the date formatter
|
|
180
|
+
for (let i = 1; i <= 12; i++) {
|
|
181
|
+
resolved.monthTerms[String(i)] = resolved.terms['month-' + String(i).padStart(2, '0')] || ''
|
|
182
|
+
}
|
|
183
|
+
for (let i = 1; i <= 4; i++) {
|
|
184
|
+
resolved.seasonTerms[String(i)] = resolved.terms['season-0' + i] || ''
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return resolved
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function mergeLocale(target, source) {
|
|
191
|
+
Object.assign(target.terms, source.terms)
|
|
192
|
+
Object.assign(target.dateFormats, source.dateFormats)
|
|
193
|
+
Object.assign(target.styleOptions, source.styleOptions)
|
|
194
|
+
Object.assign(target.ordinalTerms, source.ordinalTerms)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── DOM helpers ──────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function localName(el) {
|
|
200
|
+
return el.localName || el.nodeName?.replace(/^.*:/, '')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function attr(el, name) {
|
|
204
|
+
if (!el.hasAttribute?.(name)) return null
|
|
205
|
+
return el.getAttribute(name)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function textContent(el) {
|
|
209
|
+
let text = ''
|
|
210
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
211
|
+
const child = el.childNodes[i]
|
|
212
|
+
if (child.nodeType === 3 || child.nodeType === 4) {
|
|
213
|
+
text += child.nodeValue || ''
|
|
214
|
+
} else if (child.nodeType === 1) {
|
|
215
|
+
text += textContent(child)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return text.trim()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function childElements(el) {
|
|
222
|
+
const result = []
|
|
223
|
+
if (!el.childNodes) return result
|
|
224
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
225
|
+
if (el.childNodes[i].nodeType === 1) result.push(el.childNodes[i])
|
|
226
|
+
}
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function findChild(el, name) {
|
|
231
|
+
for (const child of childElements(el)) {
|
|
232
|
+
if (localName(child) === name) return child
|
|
233
|
+
}
|
|
234
|
+
return null
|
|
235
|
+
}
|