@sdeverywhere/compile 0.7.30 → 0.7.32

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/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.30",
3
+ "version": "0.7.32",
4
4
  "description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "dependencies": {
8
- "@sdeverywhere/parse": "^0.1.4",
8
+ "@sdeverywhere/parse": "^0.1.5",
9
9
  "byline": "^5.0.0",
10
10
  "csv-parse": "^5.3.3",
11
+ "fflate": "^0.8.3",
11
12
  "ramda": "^0.27.0",
12
- "strip-bom": "^5.0.0",
13
- "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
13
+ "strip-bom": "^5.0.0"
14
14
  },
15
15
  "author": "Climate Interactive",
16
16
  "license": "MIT",
@@ -1,9 +1,9 @@
1
- import * as fs from 'node:fs'
2
1
  import util from 'util'
3
2
  import { parse as parseCsv } from 'csv-parse/sync'
4
3
  import * as R from 'ramda'
5
- import XLSX from 'xlsx'
4
+
6
5
  import B from './bufx.js'
6
+ import { readXlsx as readXlsxFile, resetXlsxCache } from './xlsx.js'
7
7
 
8
8
  import { canonicalId, canonicalVarId } from '@sdeverywhere/parse'
9
9
 
@@ -24,12 +24,6 @@ let nextLevelVarSeq = 1
24
24
  let nextAuxVarSeq = 1
25
25
  // parsed csv data cache
26
26
  let csvData = new Map()
27
- // parsed xlsx data cache
28
- let xlsxData = new Map()
29
-
30
- // Newer versions of the xlsx package require manually setting the `fs` instance
31
- // before using the `XLSX.readFile` function
32
- XLSX.set_fs(fs)
33
27
 
34
28
  // XXX: This is needed for tests due to sequence numbers being in module-level storage
35
29
  export function resetHelperState() {
@@ -40,7 +34,7 @@ export function resetHelperState() {
40
34
  nextLevelVarSeq = 1
41
35
  nextAuxVarSeq = 1
42
36
  csvData.clear()
43
- xlsxData.clear()
37
+ resetXlsxCache()
44
38
  }
45
39
 
46
40
  export let canonicalName = name => {
@@ -203,15 +197,10 @@ export let isIterable = obj => {
203
197
  }
204
198
  // Command helpers
205
199
  export let readXlsx = pathname => {
206
- // Read the XLSX file at the pathname and parse it.
207
- // Return a `XLSX.WorkBook` object that can be used to access the data.
208
- // Cache parsed files to support multiple reads from different equations.
209
- let xlsx = xlsxData.get(pathname)
210
- if (!xlsx) {
211
- xlsx = XLSX.readFile(pathname, { cellDates: true })
212
- xlsxData.set(pathname, xlsx)
213
- }
214
- return xlsx
200
+ // Read the XLSX file at the pathname and parse it. Return a workbook
201
+ // object ({ SheetNames, Sheets }) that can be used to access the data.
202
+ // The underlying reader caches parsed files by path.
203
+ return readXlsxFile(pathname)
215
204
  }
216
205
  export let readCsv = (pathname, delimiter = ',') => {
217
206
  // Read the CSV file at the pathname and parse it with the given delimiter.
@@ -0,0 +1,517 @@
1
+ // Copyright (c) 2026 Climate Interactive / New Venture Fund
2
+
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ import { strFromU8, unzipSync } from 'fflate'
6
+
7
+ //
8
+ // Minimal xlsx reader and cell-address utilities used by the `GET DIRECT ...`
9
+ // code paths in the compile pipeline. We read numeric cell values only; strings
10
+ // are surfaced where present but are not the focus of this module.
11
+ //
12
+
13
+ const A_UPPER = 65 // 'A'
14
+ const A_LOWER = 97 // 'a'
15
+
16
+ // Workbook cache, mirroring the previous SheetJS readXlsx behavior in
17
+ // helpers.js. Each xlsx file is parsed at most once per process.
18
+ const workbookCache = new Map()
19
+
20
+ /**
21
+ * Reset the workbook cache. Intended for tests that load the same path with
22
+ * different contents across runs.
23
+ */
24
+ export function resetXlsxCache() {
25
+ workbookCache.clear()
26
+ }
27
+
28
+ /**
29
+ * Read the xlsx file at the given path and return a workbook shaped like the
30
+ * SheetJS `WorkBook`:
31
+ *
32
+ * ```
33
+ * { SheetNames: string[],
34
+ * Sheets: { [name]: { [cellRef]: { v }, '!ref': 'A1:Z99' } } }
35
+ * ```
36
+ *
37
+ * Sheets are materialized lazily — the first time a sheet name is read from
38
+ * `Sheets`, its XML is parsed; subsequent reads of the same sheet return the
39
+ * cached map. Workbooks are also cached by path.
40
+ *
41
+ * @param {string} pathname The absolute path to the xlsx file.
42
+ * @returns The parsed workbook.
43
+ */
44
+ export function readXlsx(pathname) {
45
+ // Return the cached workbook if we've already parsed this file
46
+ const cached = workbookCache.get(pathname)
47
+ if (cached) {
48
+ return cached
49
+ }
50
+
51
+ // Decompress the file, keeping only the entries we actually read from
52
+ const buf = readFileSync(pathname)
53
+ const unzipped = unzipSync(buf, {
54
+ filter: file => {
55
+ const n = file.name
56
+ return (
57
+ n === 'xl/workbook.xml' ||
58
+ n === 'xl/sharedStrings.xml' ||
59
+ n === 'xl/_rels/workbook.xml.rels' ||
60
+ (n.startsWith('xl/worksheets/sheet') && n.endsWith('.xml'))
61
+ )
62
+ }
63
+ })
64
+
65
+ // Pull out the workbook, rels, and (optional) sharedStrings parts as text
66
+ const wbBytes = unzipped['xl/workbook.xml']
67
+ const relsBytes = unzipped['xl/_rels/workbook.xml.rels']
68
+ if (!wbBytes || !relsBytes) {
69
+ throw new Error(`Failed to read xlsx file (missing workbook or rels): ${pathname}`)
70
+ }
71
+ const wbXml = strFromU8(wbBytes)
72
+ const relsXml = strFromU8(relsBytes)
73
+ const ssBytes = unzipped['xl/sharedStrings.xml']
74
+
75
+ // Build the sheet definitions
76
+ const sheetDefs = parseWorkbookXml(wbXml)
77
+
78
+ // Build the rid-to-target map
79
+ const rels = parseWorkbookRels(relsXml)
80
+
81
+ // Build the shared-string table
82
+ const sharedStrings = ssBytes ? parseSharedStrings(strFromU8(ssBytes)) : []
83
+
84
+ // Resolve each sheet's rid to its worksheet XML bytes in the zip
85
+ const sheetNames = []
86
+ const sheetXmls = Object.create(null)
87
+ for (const { name, rid } of sheetDefs) {
88
+ let target = rels[rid]
89
+ if (!target) {
90
+ continue
91
+ }
92
+ // Targets are workbook-relative; normalize to the zip entry path
93
+ target = target.startsWith('/') ? target.slice(1) : 'xl/' + target
94
+ const bytes = unzipped[target]
95
+ if (!bytes) {
96
+ continue
97
+ }
98
+ sheetNames.push(name)
99
+ sheetXmls[name] = bytes
100
+ }
101
+
102
+ // Lazy materialization: parse a sheet only when first accessed
103
+ const parsedSheets = Object.create(null)
104
+ const sheetsProxy = new Proxy(
105
+ {},
106
+ {
107
+ get(_, name) {
108
+ if (typeof name !== 'string') {
109
+ return undefined
110
+ }
111
+ if (parsedSheets[name]) {
112
+ return parsedSheets[name]
113
+ }
114
+ const bytes = sheetXmls[name]
115
+ if (!bytes) {
116
+ return undefined
117
+ }
118
+ const parsed = parseSheetXml(strFromU8(bytes), sharedStrings)
119
+ parsedSheets[name] = parsed
120
+ return parsed
121
+ },
122
+ has(_, name) {
123
+ return typeof name === 'string' && name in sheetXmls
124
+ },
125
+ ownKeys() {
126
+ return sheetNames.slice()
127
+ },
128
+ getOwnPropertyDescriptor(_, name) {
129
+ if (typeof name !== 'string' || !(name in sheetXmls)) {
130
+ return undefined
131
+ }
132
+ const bytes = sheetXmls[name]
133
+ if (!parsedSheets[name]) {
134
+ parsedSheets[name] = parseSheetXml(strFromU8(bytes), sharedStrings)
135
+ }
136
+ return { enumerable: true, configurable: true, value: parsedSheets[name], writable: false }
137
+ }
138
+ }
139
+ )
140
+
141
+ // Cache the workbook by path so subsequent reads are free
142
+ const workbook = { SheetNames: sheetNames, Sheets: sheetsProxy }
143
+ workbookCache.set(pathname, workbook)
144
+ return workbook
145
+ }
146
+
147
+ //
148
+ // Cell address utilities
149
+ //
150
+
151
+ /**
152
+ * Decode an A1-style cell ref to a zero-indexed `{c, r}`.
153
+ *
154
+ * Returns `{c: -1, r: -1}` for invalid input, matching the behavior of
155
+ * `XLSX.utils.decode_cell` from SheetJS.
156
+ *
157
+ * @param {string} ref The cell reference (e.g. 'A1', 'AZ100').
158
+ * @returns The zero-indexed column and row.
159
+ */
160
+ export function decodeCell(ref) {
161
+ // Walk the leading run of letters, accumulating the 1-based column index
162
+ // in bijective base 26 (A=1, B=2, ..., Z=26, AA=27, AB=28, ...)
163
+ let c = 0
164
+ let i = 0
165
+ const len = ref.length
166
+ while (i < len) {
167
+ const code = ref.charCodeAt(i)
168
+ if (code >= 65 && code <= 90) {
169
+ c = c * 26 + (code - A_UPPER + 1)
170
+ } else if (code >= 97 && code <= 122) {
171
+ c = c * 26 + (code - A_LOWER + 1)
172
+ } else {
173
+ break
174
+ }
175
+ i++
176
+ }
177
+
178
+ // Reject input that didn't start with at least one letter
179
+ if (i === 0) {
180
+ return { c: -1, r: -1 }
181
+ }
182
+
183
+ // Parse the trailing row digits; rows are 1-based, so reject 0/non-numeric
184
+ const r = parseInt(ref.slice(i), 10)
185
+ if (!Number.isFinite(r) || r < 1) {
186
+ return { c: -1, r: -1 }
187
+ }
188
+
189
+ // Convert column and row to the zero-indexed form callers expect
190
+ return { c: c - 1, r: r - 1 }
191
+ }
192
+
193
+ /**
194
+ * Encode a zero-indexed `{c, r}` to an A1-style cell ref.
195
+ *
196
+ * @param {{c: number, r: number}} addr The zero-indexed column and row.
197
+ * @returns The A1-style cell reference (e.g. 'B7').
198
+ */
199
+ export function encodeCell({ c, r }) {
200
+ // Convert the 1-based column index to bijective base-26 letters, building
201
+ // the string right-to-left (one letter per iteration, least-significant first)
202
+ let col = ''
203
+ let n = c + 1
204
+ while (n > 0) {
205
+ const rem = (n - 1) % 26
206
+ col = String.fromCharCode(A_UPPER + rem) + col
207
+ n = Math.floor((n - 1) / 26)
208
+ }
209
+
210
+ // Rows in A1 notation are 1-based
211
+ return col + (r + 1)
212
+ }
213
+
214
+ /**
215
+ * Decode a column ref (e.g. 'AB') to a zero-indexed column number.
216
+ *
217
+ * @param {string} ref The column reference.
218
+ * @returns The zero-indexed column number, or -1 if the input is invalid.
219
+ */
220
+ export function decodeCol(ref) {
221
+ // Empty input has no valid column
222
+ if (ref.length === 0) {
223
+ return -1
224
+ }
225
+
226
+ // Same bijective base-26 walk as decodeCell, but every character must be a
227
+ // letter — anything else (including a row digit) is rejected
228
+ let c = 0
229
+ for (let i = 0; i < ref.length; i++) {
230
+ const code = ref.charCodeAt(i)
231
+ if (code >= 65 && code <= 90) {
232
+ c = c * 26 + (code - A_UPPER + 1)
233
+ } else if (code >= 97 && code <= 122) {
234
+ c = c * 26 + (code - A_LOWER + 1)
235
+ } else {
236
+ return -1
237
+ }
238
+ }
239
+
240
+ // Convert from 1-based to zero-indexed
241
+ return c - 1
242
+ }
243
+
244
+ /**
245
+ * Decode a row ref (e.g. '13') to a zero-indexed row number.
246
+ *
247
+ * @param {string} ref The row reference.
248
+ * @returns The zero-indexed row number, or -1 if the input is invalid.
249
+ */
250
+ export function decodeRow(ref) {
251
+ // Parse the 1-based row number and convert to zero-indexed; -1 on invalid input
252
+ const r = parseInt(ref, 10)
253
+ return Number.isFinite(r) && r >= 1 ? r - 1 : -1
254
+ }
255
+
256
+ //
257
+ // XML helpers
258
+ //
259
+
260
+ /**
261
+ * Pull one attribute value out of a tag's attribute list. Cheaper than a full
262
+ * attribute parser when we only need a few specific keys.
263
+ *
264
+ * @param {string} attrs The portion of a start tag containing the attributes.
265
+ * @param {string} name The attribute name to look up.
266
+ * @returns The attribute value, or undefined if the attribute is not present.
267
+ */
268
+ function getAttr(attrs, name) {
269
+ // Look for `name="` — every attribute we care about uses double quotes
270
+ const i = attrs.indexOf(name + '="')
271
+ if (i < 0) {
272
+ return undefined
273
+ }
274
+
275
+ // Slice out everything between the opening and closing quote
276
+ const start = i + name.length + 2
277
+ const end = attrs.indexOf('"', start)
278
+ return end < 0 ? undefined : attrs.slice(start, end)
279
+ }
280
+
281
+ /**
282
+ * Normalize line endings the way a conformant XML parser (and SheetJS) does:
283
+ * `\r\n` and lone `\r` both become `\n`. Applied after entity decoding so a
284
+ * CR encoded as `&#13;` is normalized too.
285
+ *
286
+ * @param {string} s The text to normalize.
287
+ * @returns The normalized string.
288
+ */
289
+ function normalizeEol(s) {
290
+ return s.indexOf('\r') === -1 ? s : s.replace(/\r\n?/g, '\n')
291
+ }
292
+
293
+ /**
294
+ * Decode the standard XML entities (`&lt;`, `&gt;`, `&amp;`, `&quot;`,
295
+ * `&apos;`) along with numeric character references (`&#NN;` and `&#xNN;`)
296
+ * in the given text, and normalize line endings to `\n`.
297
+ *
298
+ * @param {string} s The raw text from an XML element body or attribute.
299
+ * @returns The decoded string.
300
+ */
301
+ function decodeXmlText(s) {
302
+ // Fast path: most cell text contains no entities, so skip the regex chain
303
+ if (s.indexOf('&') === -1) {
304
+ return normalizeEol(s)
305
+ }
306
+
307
+ // Decode the named entities, then decimal and hex numeric refs, and finally
308
+ // `&amp;` — leaving `&amp;` last avoids accidentally producing `&lt;` etc.
309
+ // from a literal `&amp;lt;` in the source
310
+ return normalizeEol(
311
+ s
312
+ .replace(/&lt;/g, '<')
313
+ .replace(/&gt;/g, '>')
314
+ .replace(/&quot;/g, '"')
315
+ .replace(/&apos;/g, "'")
316
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
317
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)))
318
+ .replace(/&amp;/g, '&')
319
+ )
320
+ }
321
+
322
+ //
323
+ // Parsers
324
+ //
325
+
326
+ /**
327
+ * Parse the contents of `xl/sharedStrings.xml` into a positional array of
328
+ * decoded strings. A shared string may contain rich-text runs
329
+ * (`<si><r><t>foo</t></r><r><t>bar</t></r></si>`); concatenate all `<t>`
330
+ * elements within each `<si>` so the result comes back as a single string.
331
+ *
332
+ * @param {string} xml The contents of the `xl/sharedStrings.xml` file.
333
+ * @returns An array indexed by shared-string position.
334
+ */
335
+ function parseSharedStrings(xml) {
336
+ // Outer regex finds each <si> entry; inner regex finds <t> runs within it.
337
+ // The two are stateful (global) regexes, so reset the inner one per <si>.
338
+ const strings = []
339
+ const siRe = /<si\b[^>]*>([\s\S]*?)<\/si>/g
340
+ const tRe = /<t\b[^>]*>([\s\S]*?)<\/t>/g
341
+ let m
342
+ while ((m = siRe.exec(xml)) !== null) {
343
+ // Concatenate every <t> chunk inside this <si> — rich-text runs collapse
344
+ // into a single plain string
345
+ const inner = m[1]
346
+ let s = ''
347
+ let tm
348
+ tRe.lastIndex = 0
349
+ while ((tm = tRe.exec(inner)) !== null) {
350
+ s += decodeXmlText(tm[1])
351
+ }
352
+ strings.push(s)
353
+ }
354
+ return strings
355
+ }
356
+
357
+ /**
358
+ * Parse the contents of `xl/workbook.xml` into the list of sheet definitions,
359
+ * each with the user-visible sheet name and the relationship id that maps to
360
+ * the sheet's XML part. Attribute spans can contain `/` (in URLs), so the
361
+ * regex matches up to the self-closing `/>` non-greedily rather than excluding
362
+ * `/` from the attribute span.
363
+ *
364
+ * @param {string} xml The contents of the `xl/workbook.xml` file.
365
+ * @returns An array of `{ name, rid }` objects in workbook order.
366
+ */
367
+ function parseWorkbookXml(xml) {
368
+ // Iterate every <sheet .../> element in workbook order
369
+ const sheets = []
370
+ const re = /<sheet\b([\s\S]*?)\/>/g
371
+ let m
372
+ while ((m = re.exec(xml)) !== null) {
373
+ // Each sheet is identified by its user-visible name and its rid pointer;
374
+ // the rid attribute is conventionally lowercase but accept both forms
375
+ const attrs = m[1]
376
+ const name = getAttr(attrs, 'name')
377
+ const rid = getAttr(attrs, 'r:id') ?? getAttr(attrs, 'r:Id')
378
+ if (name && rid) {
379
+ sheets.push({ name, rid })
380
+ }
381
+ }
382
+ return sheets
383
+ }
384
+
385
+ /**
386
+ * Parse the contents of `xl/_rels/workbook.xml.rels` into a map from
387
+ * relationship id to its target path (the part within the xlsx zip).
388
+ *
389
+ * @param {string} xml The contents of the `xl/_rels/workbook.xml.rels` file.
390
+ * @returns An object mapping relationship id to target path.
391
+ */
392
+ function parseWorkbookRels(xml) {
393
+ // Iterate every <Relationship .../> element
394
+ const rels = Object.create(null)
395
+ const re = /<Relationship\b([\s\S]*?)\/>/g
396
+ let m
397
+ while ((m = re.exec(xml)) !== null) {
398
+ // Record the Id -> Target mapping; ignore the Type and other attributes
399
+ const attrs = m[1]
400
+ const id = getAttr(attrs, 'Id')
401
+ const target = getAttr(attrs, 'Target')
402
+ if (id && target) {
403
+ rels[id] = target
404
+ }
405
+ }
406
+ return rels
407
+ }
408
+
409
+ /**
410
+ * Extract the text content of the `<v>` element in a cell body, tolerating
411
+ * attributes on the tag (e.g. `<v xml:space="preserve">`). Returns undefined
412
+ * when there is no `<v>` element (e.g. an uncalculated formula cell).
413
+ *
414
+ * @param {string} body The inner XML of a `<c>` element.
415
+ * @returns The raw text between `<v...>` and `</v>`, or undefined.
416
+ */
417
+ function getVText(body) {
418
+ const m = /<v\b[^>]*>([\s\S]*?)<\/v>/.exec(body)
419
+ return m ? m[1] : undefined
420
+ }
421
+
422
+ /**
423
+ * Scan a worksheet's XML and build a sparse cell map shaped like the SheetJS
424
+ * worksheet object: `{ [cellRef]: { v }, '!ref': 'A1:Z99' }`. Skips empty
425
+ * cells, error cells (`t='e'`), and numeric cells whose cached `<v>` value
426
+ * is missing.
427
+ *
428
+ * @param {string} xml The contents of a `xl/worksheets/sheet*.xml` file.
429
+ * @param {string[]} sharedStrings The shared-string table for resolving `t='s'` cells.
430
+ * @returns The sparse cell map, including a `!ref` key if any cells were read.
431
+ */
432
+ function parseSheetXml(xml, sharedStrings) {
433
+ const cells = Object.create(null)
434
+
435
+ // Match each <c .../> or <c ...>...</c> block. The attribute span is
436
+ // non-greedy so self-closing cells (e.g. <c r="I4" s="1"/>) don't accidentally
437
+ // swallow following cells.
438
+ const cRe = /<c\b([^>]*?)(\/>|>([\s\S]*?)<\/c>)/g
439
+ let maxRow = -1
440
+ let maxCol = -1
441
+ let m
442
+ while ((m = cRe.exec(xml)) !== null) {
443
+ const attrs = m[1]
444
+ const ref = getAttr(attrs, 'r')
445
+ if (!ref) {
446
+ continue
447
+ }
448
+ if (m[2] === '/>') {
449
+ // empty cell
450
+ continue
451
+ }
452
+ const body = m[3]
453
+ if (!body) {
454
+ continue
455
+ }
456
+ const t = getAttr(attrs, 't')
457
+
458
+ let value
459
+ if (t === 'inlineStr') {
460
+ // Inline string: <is><t>...</t></is>
461
+ const tStart = body.indexOf('<t')
462
+ if (tStart < 0) {
463
+ continue
464
+ }
465
+ const tOpenEnd = body.indexOf('>', tStart)
466
+ const tEnd = body.indexOf('</t>', tOpenEnd)
467
+ value = decodeXmlText(body.slice(tOpenEnd + 1, tEnd))
468
+ } else if (t === 'e') {
469
+ // Error cell, skip
470
+ continue
471
+ } else {
472
+ // The remaining cell types carry their value in a <v> element, which
473
+ // may have attributes (e.g. <v xml:space="preserve">). If <v> is
474
+ // missing (e.g. an uncalculated formula), skip the cell so the
475
+ // caller's missing-cell handling kicks in.
476
+ const vText = getVText(body)
477
+ if (vText === undefined) {
478
+ continue
479
+ }
480
+ if (t === 's') {
481
+ // Shared string: <v>N</v> where N indexes sharedStrings
482
+ value = sharedStrings[parseInt(vText, 10)]
483
+ } else if (t === 'str') {
484
+ // Formula result as string: <v>...</v>
485
+ value = decodeXmlText(vText)
486
+ } else if (t === 'b') {
487
+ // Boolean: <v>0</v> or <v>1</v>
488
+ value = vText.charCodeAt(0) === 49 // '1'
489
+ } else {
490
+ // Numeric (t === 'n' or absent)
491
+ const num = +vText
492
+ if (Number.isNaN(num)) {
493
+ continue
494
+ }
495
+ value = num
496
+ }
497
+ }
498
+
499
+ // Store the cell under its A1 ref, matching the SheetJS sheet shape
500
+ cells[ref] = { v: value }
501
+
502
+ // Track the bounding row/col so we can synthesize the !ref range below
503
+ const addr = decodeCell(ref)
504
+ if (addr.r > maxRow) {
505
+ maxRow = addr.r
506
+ }
507
+ if (addr.c > maxCol) {
508
+ maxCol = addr.c
509
+ }
510
+ }
511
+
512
+ // Expose the sheet's bounding range as !ref when any cells were read
513
+ if (maxRow >= 0) {
514
+ cells['!ref'] = `A1:${encodeCell({ c: maxCol, r: maxRow })}`
515
+ }
516
+ return cells
517
+ }
@@ -1,8 +1,7 @@
1
1
  import path from 'node:path'
2
2
 
3
- import XLSX from 'xlsx'
4
-
5
3
  import { cdbl, readCsv, readXlsx } from '../_shared/helpers.js'
4
+ import { encodeCell } from '../_shared/xlsx.js'
6
5
 
7
6
  /**
8
7
  * Return a `getCellValue` function that reads the CSV or XLS[X] content.
@@ -47,7 +46,7 @@ function handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, dataSource) {
47
46
  let sheet = workbook.Sheets[tab]
48
47
  if (sheet) {
49
48
  return (c, r) => {
50
- let cell = sheet[XLSX.utils.encode_cell({ c, r })]
49
+ let cell = sheet[encodeCell({ c, r })]
51
50
  if (cell == null || cell.v === '') {
52
51
  return null
53
52
  }
@@ -1,7 +1,6 @@
1
- import XLSX from 'xlsx'
2
-
3
1
  import { cartesianProductOf } from '../_shared/helpers.js'
4
2
  import { indexInSepDim, isDimension, sub } from '../_shared/subscript.js'
3
+ import { decodeCell } from '../_shared/xlsx.js'
5
4
 
6
5
  import { handleExcelOrCsvFile } from './direct-data-helpers.js'
7
6
 
@@ -73,7 +72,7 @@ export function generateDirectConstInit(variable, directData, modelDir) {
73
72
  // Read tabular data into an indexed variable for each cell.
74
73
  let numericSubscripts = lhsIndexSubscripts.map(idx => idx.map(s => sub(s).value))
75
74
  let lhsSubscripts = numericSubscripts.map(s => s.reduce((a, v) => a.concat(`[${v}]`), ''))
76
- let dataAddress = XLSX.utils.decode_cell(startCell.toUpperCase())
75
+ let dataAddress = decodeCell(startCell.toUpperCase())
77
76
  let startCol = dataAddress.c
78
77
  let startRow = dataAddress.r
79
78
  if (startCol < 0 || startRow < 0) {
@@ -128,6 +128,9 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
128
128
  }
129
129
  }
130
130
 
131
+ // Keep a buffer of code that will be included before all subscript loops
132
+ const preLoopLines = []
133
+
131
134
  // Keep a buffer of code that will be included before the innermost loop
132
135
  const preInnerLoopLines = []
133
136
 
@@ -145,6 +148,7 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
145
148
  cLhs,
146
149
  loopIndexVars,
147
150
  arrayIndexVars,
151
+ emitPreLoop: s => preLoopLines.push(s),
148
152
  emitPreInnerLoop: s => preInnerLoopLines.push(s),
149
153
  emitPreFormula: s => preFormulaLines.push(s),
150
154
  emitPostFormula: s => postFormulaLines.push(s),
@@ -161,7 +165,7 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
161
165
  }
162
166
 
163
167
  // Combine all lines of comments and code into a single array
164
- return [comment, ...openLoops, ...preFormulaLines, formula, ...postFormulaLines, ...closeLoops]
168
+ return [comment, ...preLoopLines, ...openLoops, ...preFormulaLines, formula, ...postFormulaLines, ...closeLoops]
165
169
  }
166
170
 
167
171
  /**