@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/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
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @citestyle/compiler
3
+ *
4
+ * CSL XML → JavaScript module compiler.
5
+ */
6
+ export { compile } from './compile.js'
7
+ export { parse } from './parser.js'
8
+ export { resolveLocale } from './locale.js'
9
+ export { generate } from './codegen.js'
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
+ }