@sdeverywhere/plugin-config 0.2.12 → 0.2.14

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/dist/index.js CHANGED
@@ -1,882 +1,793 @@
1
- // src/processor.ts
2
- import { existsSync } from "fs";
3
- import { join as joinPath2 } from "path";
4
-
5
- // src/context.ts
6
- import { readFileSync } from "fs";
7
- import { join as joinPath, relative } from "path";
8
- import { parse as parseCsv } from "csv-parse/sync";
9
-
10
- // src/strings.ts
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { dirname, join, relative, resolve } from "path";
3
+ import { parse } from "csv-parse/sync";
11
4
  import sanitizeHtml from "sanitize-html";
5
+ import { fileURLToPath } from "url";
6
+ //#region src/strings.ts
12
7
  var Strings = class {
13
- constructor() {
14
- this.records = /* @__PURE__ */ new Map();
15
- }
16
- add(key, str, layout2, context, grouping, appendedStringKeys) {
17
- checkInvisibleCharacters(str || "");
18
- if (!key) {
19
- throw new Error(`Must provide a key for the string: ${str}`);
20
- }
21
- const validKey = /^[0-9a-z_]+$/.test(key);
22
- if (!validKey) {
23
- throw new Error(`String key contains undesirable characters: ${key}`);
24
- }
25
- if (!layout2) {
26
- throw new Error(`Must provide a layout (e.g., 'layout1' or 'not-translated')`);
27
- }
28
- if (!context) {
29
- throw new Error(`Must provide a context string: key=${key}, string=${str}`);
30
- }
31
- if (context === "Core") {
32
- context = void 0;
33
- }
34
- if (!grouping) {
35
- grouping = "primary";
36
- }
37
- if (str) {
38
- str = str.trim();
39
- str = htmlToUtf8(str);
40
- }
41
- if (!str) {
42
- return "";
43
- }
44
- if (this.records.has(key)) {
45
- const prefix = key.substring(0, key.indexOf("__"));
46
- switch (prefix) {
47
- case "graph_dataset_label":
48
- case "graph_xaxis_label":
49
- case "graph_yaxis_label":
50
- case "input_group_title":
51
- case "input_range":
52
- case "input_units":
53
- break;
54
- default:
55
- throw new Error(`More than one string with key=${key}`);
56
- }
57
- }
58
- this.records.set(key, {
59
- key,
60
- str,
61
- layout: layout2,
62
- context,
63
- grouping,
64
- appendedStringKeys
65
- });
66
- return key;
67
- }
68
- /**
69
- * Write a `<lang>.js` file containing translated strings for each supported language.
70
- *
71
- * @param context The build context.
72
- * @param dstDir The `strings` directory in the core package.
73
- * @param xlatLangs The set of languages that are configured for translation.
74
- */
75
- writeJsFiles(context, dstDir) {
76
- writeLangJsFiles(
77
- context,
78
- dstDir,
79
- this.records
80
- /*, xlatLangs*/
81
- );
82
- }
8
+ constructor() {
9
+ this.records = /* @__PURE__ */ new Map();
10
+ }
11
+ add(key, str, layout, context, grouping, appendedStringKeys) {
12
+ checkInvisibleCharacters(str || "");
13
+ if (!key) throw new Error(`Must provide a key for the string: ${str}`);
14
+ if (!/^[0-9a-z_]+$/.test(key)) throw new Error(`String key contains undesirable characters: ${key}`);
15
+ if (!layout) throw new Error(`Must provide a layout (e.g., 'layout1' or 'not-translated')`);
16
+ if (!context) throw new Error(`Must provide a context string: key=${key}, string=${str}`);
17
+ if (context === "Core") context = void 0;
18
+ if (!grouping) grouping = "primary";
19
+ if (str) {
20
+ str = str.trim();
21
+ str = htmlToUtf8(str);
22
+ }
23
+ if (!str) return "";
24
+ if (this.records.has(key)) switch (key.substring(0, key.indexOf("__"))) {
25
+ case "graph_dataset_label":
26
+ case "graph_xaxis_label":
27
+ case "graph_yaxis_label":
28
+ case "input_group_title":
29
+ case "input_range":
30
+ case "input_units": break;
31
+ default: throw new Error(`More than one string with key=${key}`);
32
+ }
33
+ this.records.set(key, {
34
+ key,
35
+ str,
36
+ layout,
37
+ context,
38
+ grouping,
39
+ appendedStringKeys
40
+ });
41
+ return key;
42
+ }
43
+ /**
44
+ * Write a `<lang>.js` file containing translated strings for each supported language.
45
+ *
46
+ * @param context The build context.
47
+ * @param dstDir The `strings` directory in the core package.
48
+ * @param xlatLangs The set of languages that are configured for translation.
49
+ */
50
+ writeJsFiles(context, dstDir) {
51
+ writeLangJsFiles(context, dstDir, this.records);
52
+ }
83
53
  };
84
54
  function getSortedRecords(records) {
85
- return Array.from(records.values()).sort((a, b) => {
86
- return a.key > b.key ? 1 : b.key > a.key ? -1 : 0;
87
- });
55
+ return Array.from(records.values()).sort((a, b) => {
56
+ return a.key > b.key ? 1 : b.key > a.key ? -1 : 0;
57
+ });
88
58
  }
89
59
  function checkInvisibleCharacters(s) {
90
- if (s.includes("\xA0")) {
91
- const e = s.replace(/\u00a0/g, "HERE");
92
- throw new Error(
93
- `String contains one or more non-breaking space characters (to fix, replace "HERE" with a normal space):
94
- ${e}`
95
- );
96
- }
60
+ if (s.includes("\xA0")) {
61
+ const e = s.replace(/\u00a0/g, "HERE");
62
+ throw new Error(`String contains one or more non-breaking space characters (to fix, replace "HERE" with a normal space):\n ${e}`);
63
+ }
97
64
  }
98
65
  function utf8SubscriptToHtml(key, s) {
99
- if (key.includes("graph_yaxis_label")) {
100
- s = s.replace(/₂/gi, "2");
101
- s = s.replace(/₃/gi, "3");
102
- s = s.replace(/₄/gi, "4");
103
- s = s.replace(/₆/gi, "6");
104
- return s;
105
- }
106
- s = s.replace(/₂/gi, "<sub>2</sub>");
107
- s = s.replace(/₃/gi, "<sub>3</sub>");
108
- s = s.replace(/₄/gi, "<sub>4</sub>");
109
- s = s.replace(/₆/gi, "<sub>6</sub>");
110
- s = s.replace(/<\/sub> /gi, "</sub>&nbsp;");
111
- return s;
66
+ if (key.includes("graph_yaxis_label")) {
67
+ s = s.replace(/₂/gi, "2");
68
+ s = s.replace(/₃/gi, "3");
69
+ s = s.replace(/₄/gi, "4");
70
+ s = s.replace(/₆/gi, "6");
71
+ return s;
72
+ }
73
+ s = s.replace(/₂/gi, "<sub>2</sub>");
74
+ s = s.replace(/₃/gi, "<sub>3</sub>");
75
+ s = s.replace(/₄/gi, "<sub>4</sub>");
76
+ s = s.replace(/₆/gi, "<sub>6</sub>");
77
+ s = s.replace(/<\/sub> /gi, "</sub>&nbsp;");
78
+ return s;
112
79
  }
113
80
  function htmlSubscriptAndSuperscriptToUtf8(s) {
114
- s = s.replace(/<sub>(\d)<\/sub>/gi, (_match, p1) => String.fromCharCode(8320 + Number(p1)));
115
- s = s.replace(/<sup>6<\/sup>/gi, "\u2076");
116
- s = s.replace(/<sup>9<\/sup>/gi, "\u2079");
117
- return s;
81
+ s = s.replace(/<sub>(\d)<\/sub>/gi, (_match, p1) => String.fromCharCode(8320 + Number(p1)));
82
+ s = s.replace(/<sup>6<\/sup>/gi, "");
83
+ s = s.replace(/<sup>9<\/sup>/gi, "");
84
+ return s;
118
85
  }
119
86
  function htmlToUtf8(orig) {
120
- let s = orig;
121
- s = htmlSubscriptAndSuperscriptToUtf8(s);
122
- let clean = sanitizeHtml(s, {
123
- allowedTags: ["a", "b", "br", "i", "em", "li", "p", "strong", "sub", "sup", "ul"],
124
- allowedAttributes: {
125
- a: ["href", "target", "rel"]
126
- }
127
- });
128
- clean = clean.replace(/\u00a0/gi, "&nbsp;");
129
- return clean;
87
+ let s = orig;
88
+ s = htmlSubscriptAndSuperscriptToUtf8(s);
89
+ let clean = sanitizeHtml(s, {
90
+ allowedTags: [
91
+ "a",
92
+ "b",
93
+ "br",
94
+ "i",
95
+ "em",
96
+ "li",
97
+ "p",
98
+ "strong",
99
+ "sub",
100
+ "sup",
101
+ "ul"
102
+ ],
103
+ allowedAttributes: { a: [
104
+ "href",
105
+ "target",
106
+ "rel"
107
+ ] }
108
+ });
109
+ clean = clean.replace(/\u00a0/gi, "&nbsp;");
110
+ return clean;
130
111
  }
112
+ /**
113
+ * Generate a string key for the given string by replacing special characters with
114
+ * underscores and converting other characters to lowercase.
115
+ */
131
116
  function genStringKey(prefix, s) {
132
- checkInvisibleCharacters(s);
133
- let key = s.toLowerCase();
134
- key = key.replace(/ – /g, "_");
135
- key = key.replace(/ \/ /g, "_per_");
136
- key = key.replace(/ /g, "_");
137
- key = key.replace("\u2082", "2");
138
- key = key.replace("<sub>2</sub>", "2");
139
- key = key.replace(/\$\//g, "dollars_per_");
140
- key = key.replace(/%\//g, "pct_per_");
141
- key = key.replace(/\//g, "_per_");
142
- key = key.replace(/º/g, "degrees_");
143
- key = key.replace(/\*/g, "_");
144
- key = key.replace(/%/g, "pct");
145
- key = key.replace(/\$/g, "dollars");
146
- key = key.replace(/&/g, "and");
147
- key = key.replace(/\//g, "per");
148
- key = key.replace(/:/g, "");
149
- key = key.replace(/\./g, "");
150
- key = key.replace(/-/g, "_");
151
- key = key.replace(/—/g, "_");
152
- key = key.replace(/–/g, "_");
153
- key = key.replace(/,/g, "");
154
- key = key.replace(/\(/g, "");
155
- key = key.replace(/\)/g, "");
156
- key = key.replace(/\\n/g, "");
157
- key = key.replace(/<br>/g, "_");
158
- return `${prefix}__${key}`;
117
+ checkInvisibleCharacters(s);
118
+ let key = s.toLowerCase();
119
+ key = key.replace(/ – /g, "_");
120
+ key = key.replace(/ \/ /g, "_per_");
121
+ key = key.replace(/ /g, "_");
122
+ key = key.replace("", "2");
123
+ key = key.replace("<sub>2</sub>", "2");
124
+ key = key.replace(/\$\//g, "dollars_per_");
125
+ key = key.replace(/%\//g, "pct_per_");
126
+ key = key.replace(/\//g, "_per_");
127
+ key = key.replace(/º/g, "degrees_");
128
+ key = key.replace(/\*/g, "_");
129
+ key = key.replace(/%/g, "pct");
130
+ key = key.replace(/\$/g, "dollars");
131
+ key = key.replace(/&/g, "and");
132
+ key = key.replace(/\//g, "per");
133
+ key = key.replace(/:/g, "");
134
+ key = key.replace(/\./g, "");
135
+ key = key.replace(/-/g, "_");
136
+ key = key.replace(/—/g, "_");
137
+ key = key.replace(/–/g, "_");
138
+ key = key.replace(/,/g, "");
139
+ key = key.replace(/\(/g, "");
140
+ key = key.replace(/\)/g, "");
141
+ key = key.replace(/\\n/g, "");
142
+ key = key.replace(/<br>/g, "_");
143
+ return `${prefix}__${key}`;
159
144
  }
145
+ /**
146
+ * Write a `<lang>.js` file containing translated strings for each supported language.
147
+ *
148
+ * These files are currently saved as plain JS (ES6) files. The only difference compared
149
+ * to JSON files is these JS files start with `export default`, so converting to JSON is
150
+ * as trivial as stripping those two words, if needed.
151
+ *
152
+ * @param context The build context.
153
+ * @param dstDir The `strings` directory in the core package.
154
+ * @param records The string records.
155
+ * //@param xlatLangs The set of languages that are configured for translation.
156
+ */
160
157
  function writeLangJsFiles(context, dstDir, records) {
161
- const xlatMap = /* @__PURE__ */ new Map();
162
- const sortedRecords = getSortedRecords(records);
163
- const enStrings = /* @__PURE__ */ new Map();
164
- for (const record of sortedRecords) {
165
- const s = record.str;
166
- enStrings.set(record.key, utf8SubscriptToHtml(record.key, s));
167
- }
168
- xlatMap.set("en", enStrings);
169
- for (const lang of xlatMap.keys()) {
170
- const stringsForLang = xlatMap.get(lang);
171
- const stringsObj = Object.fromEntries(stringsForLang);
172
- const json = JSON.stringify(stringsObj, null, 2);
173
- context.writeStagedFile("strings", dstDir, `${lang}.js`, `export default ${json}`);
174
- }
158
+ const xlatMap = /* @__PURE__ */ new Map();
159
+ const sortedRecords = getSortedRecords(records);
160
+ const enStrings = /* @__PURE__ */ new Map();
161
+ for (const record of sortedRecords) {
162
+ const s = record.str;
163
+ enStrings.set(record.key, utf8SubscriptToHtml(record.key, s));
164
+ }
165
+ xlatMap.set("en", enStrings);
166
+ for (const lang of xlatMap.keys()) {
167
+ const stringsForLang = xlatMap.get(lang);
168
+ const stringsObj = Object.fromEntries(stringsForLang);
169
+ const json = JSON.stringify(stringsObj, null, 2);
170
+ context.writeStagedFile("strings", dstDir, `${lang}.js`, `export default ${json}`);
171
+ }
175
172
  }
176
-
177
- // src/context.ts
173
+ //#endregion
174
+ //#region src/context.ts
178
175
  var ConfigContext = class {
179
- constructor(buildContext, configDir, strings, colorMap, modelOptions) {
180
- this.buildContext = buildContext;
181
- this.configDir = configDir;
182
- this.strings = strings;
183
- this.colorMap = colorMap;
184
- this.modelOptions = modelOptions;
185
- this.inputSpecs = /* @__PURE__ */ new Map();
186
- this.outputVarNames = /* @__PURE__ */ new Map();
187
- this.staticVarNames = /* @__PURE__ */ new Map();
188
- }
189
- /**
190
- * Read a CSV file of the given name from the config directory.
191
- *
192
- * @param name The base name of the CSV file.
193
- */
194
- readConfigCsvFile(name) {
195
- return readConfigCsvFile(this.configDir, name);
196
- }
197
- /**
198
- * Log a message to the console and/or the in-browser overlay panel.
199
- *
200
- * @param level The log level (verbose, info, error).
201
- * @param msg The message.
202
- */
203
- log(level, msg) {
204
- this.buildContext.log(level, msg);
205
- }
206
- /**
207
- * Format a (subscripted or non-subscripted) model variable name into a canonical
208
- * identifier (with special characters converted to underscore, and subscript/dimension
209
- * parts separated by commas).
210
- *
211
- * @param name The name of the variable in the source model, e.g., `Variable name[DimA, B2]`.
212
- * @returns The canonical identifier for the given name, e.g., `_variable_name[_dima,_b2]`.
213
- */
214
- canonicalVarId(name) {
215
- return this.buildContext.canonicalVarId(name);
216
- }
217
- /**
218
- * Write a file to the staged directory.
219
- *
220
- * This file will be copied (along with other staged files) into the destination
221
- * directory only after the build process has completed. Copying all staged files
222
- * at once helps improve the local development experience by making it so that
223
- * live reloading tools only need to refresh once instead of every time a build
224
- * file is written.
225
- *
226
- * @param srcDir The directory underneath the configured `staged` directory where
227
- * the file will be written (this must be a relative path).
228
- * @param dstDir The absolute path to the destination directory where the staged
229
- * file will be copied when the build has completed.
230
- * @param filename The name of the file.
231
- * @param content The file content.
232
- */
233
- writeStagedFile(srcDir, dstDir, filename, content) {
234
- this.buildContext.writeStagedFile(srcDir, dstDir, filename, content);
235
- }
236
- addInputVariable(inputId, inputVarName, defaultValue, minValue, maxValue) {
237
- const varId = this.buildContext.canonicalVarId(inputVarName);
238
- if (this.inputSpecs.get(varId)) {
239
- console.error(`ERROR: Input variable ${inputVarName} was already added`);
240
- }
241
- this.inputSpecs.set(varId, {
242
- inputId,
243
- varName: inputVarName,
244
- defaultValue,
245
- minValue,
246
- maxValue
247
- });
248
- }
249
- addOutputVariable(outputVarName) {
250
- const varId = this.buildContext.canonicalVarId(outputVarName);
251
- this.outputVarNames.set(varId, outputVarName);
252
- }
253
- addStaticVariable(sourceName, varName) {
254
- const sourceVarNames = this.staticVarNames.get(sourceName);
255
- if (sourceVarNames) {
256
- sourceVarNames.add(varName);
257
- } else {
258
- const varNames = /* @__PURE__ */ new Set();
259
- varNames.add(varName);
260
- this.staticVarNames.set(sourceName, varNames);
261
- }
262
- }
263
- getHexColorForId(colorId) {
264
- return this.colorMap.get(colorId);
265
- }
266
- getOrderedInputs() {
267
- return Array.from(this.inputSpecs.values());
268
- }
269
- getOrderedOutputs() {
270
- const alphabetical = (a, b) => a > b ? 1 : b > a ? -1 : 0;
271
- const varNames = Array.from(this.outputVarNames.values()).sort(alphabetical);
272
- return varNames.map((varName) => {
273
- return {
274
- varName
275
- };
276
- });
277
- }
278
- writeStringsFiles(dstDir) {
279
- this.strings.writeJsFiles(
280
- this.buildContext,
281
- dstDir
282
- /*, xlatLangs*/
283
- );
284
- }
176
+ constructor(buildContext, configDir, strings, colorMap, modelOptions) {
177
+ this.buildContext = buildContext;
178
+ this.configDir = configDir;
179
+ this.strings = strings;
180
+ this.colorMap = colorMap;
181
+ this.modelOptions = modelOptions;
182
+ this.inputSpecs = /* @__PURE__ */ new Map();
183
+ this.outputVarNames = /* @__PURE__ */ new Map();
184
+ this.staticVarNames = /* @__PURE__ */ new Map();
185
+ }
186
+ /**
187
+ * Read a CSV file of the given name from the config directory.
188
+ *
189
+ * @param name The base name of the CSV file.
190
+ */
191
+ readConfigCsvFile(name) {
192
+ return readConfigCsvFile(this.configDir, name);
193
+ }
194
+ /**
195
+ * Log a message to the console and/or the in-browser overlay panel.
196
+ *
197
+ * @param level The log level (verbose, info, error).
198
+ * @param msg The message.
199
+ */
200
+ log(level, msg) {
201
+ this.buildContext.log(level, msg);
202
+ }
203
+ /**
204
+ * Format a (subscripted or non-subscripted) model variable name into a canonical
205
+ * identifier (with special characters converted to underscore, and subscript/dimension
206
+ * parts separated by commas).
207
+ *
208
+ * @param name The name of the variable in the source model, e.g., `Variable name[DimA, B2]`.
209
+ * @returns The canonical identifier for the given name, e.g., `_variable_name[_dima,_b2]`.
210
+ */
211
+ canonicalVarId(name) {
212
+ return this.buildContext.canonicalVarId(name);
213
+ }
214
+ /**
215
+ * Write a file to the staged directory.
216
+ *
217
+ * This file will be copied (along with other staged files) into the destination
218
+ * directory only after the build process has completed. Copying all staged files
219
+ * at once helps improve the local development experience by making it so that
220
+ * live reloading tools only need to refresh once instead of every time a build
221
+ * file is written.
222
+ *
223
+ * @param srcDir The directory underneath the configured `staged` directory where
224
+ * the file will be written (this must be a relative path).
225
+ * @param dstDir The absolute path to the destination directory where the staged
226
+ * file will be copied when the build has completed.
227
+ * @param filename The name of the file.
228
+ * @param content The file content.
229
+ */
230
+ writeStagedFile(srcDir, dstDir, filename, content) {
231
+ this.buildContext.writeStagedFile(srcDir, dstDir, filename, content);
232
+ }
233
+ addInputVariable(inputId, inputVarName, defaultValue, minValue, maxValue) {
234
+ const varId = this.buildContext.canonicalVarId(inputVarName);
235
+ if (this.inputSpecs.get(varId)) console.error(`ERROR: Input variable ${inputVarName} was already added`);
236
+ this.inputSpecs.set(varId, {
237
+ inputId,
238
+ varName: inputVarName,
239
+ defaultValue,
240
+ minValue,
241
+ maxValue
242
+ });
243
+ }
244
+ addOutputVariable(outputVarName) {
245
+ const varId = this.buildContext.canonicalVarId(outputVarName);
246
+ this.outputVarNames.set(varId, outputVarName);
247
+ }
248
+ addStaticVariable(sourceName, varName) {
249
+ const sourceVarNames = this.staticVarNames.get(sourceName);
250
+ if (sourceVarNames) sourceVarNames.add(varName);
251
+ else {
252
+ const varNames = /* @__PURE__ */ new Set();
253
+ varNames.add(varName);
254
+ this.staticVarNames.set(sourceName, varNames);
255
+ }
256
+ }
257
+ getHexColorForId(colorId) {
258
+ return this.colorMap.get(colorId);
259
+ }
260
+ getOrderedInputs() {
261
+ return Array.from(this.inputSpecs.values());
262
+ }
263
+ getOrderedOutputs() {
264
+ const alphabetical = (a, b) => a > b ? 1 : b > a ? -1 : 0;
265
+ return Array.from(this.outputVarNames.values()).sort(alphabetical).map((varName) => {
266
+ return { varName };
267
+ });
268
+ }
269
+ writeStringsFiles(dstDir) {
270
+ this.strings.writeJsFiles(this.buildContext, dstDir);
271
+ }
285
272
  };
286
273
  function createConfigContext(buildContext, configDir) {
287
- const modelCsv = readConfigCsvFile(configDir, "model")[0];
288
- const graphDefaultMinTime = Number(modelCsv["graph default min time"]);
289
- const graphDefaultMaxTime = Number(modelCsv["graph default max time"]);
290
- const datFilesString = modelCsv["model dat files"];
291
- const origDatFiles = datFilesString.length > 0 ? datFilesString.split(";") : [];
292
- const prepDir = buildContext.config.prepDir;
293
- const projDir = buildContext.config.rootDir;
294
- const datFiles = origDatFiles.map((f) => joinPath(relative(prepDir, projDir), f));
295
- const bundleListing = modelCsv["bundle listing"] === "true";
296
- const customConstants = modelCsv["custom constants"] === "true";
297
- const customLookups = modelCsv["custom lookups"] === "true";
298
- const customOutputs = modelCsv["custom outputs"] === "true";
299
- const modelOptions = {
300
- graphDefaultMinTime,
301
- graphDefaultMaxTime,
302
- datFiles,
303
- bundleListing,
304
- customConstants,
305
- customLookups,
306
- customOutputs
307
- };
308
- const strings = readStringsCsv(configDir);
309
- const colorsCsv = readConfigCsvFile(configDir, "colors");
310
- const colors = /* @__PURE__ */ new Map();
311
- for (const row of colorsCsv) {
312
- const colorId = row["id"];
313
- const hexColor = row["hex code"];
314
- colors.set(colorId, hexColor);
315
- }
316
- return new ConfigContext(buildContext, configDir, strings, colors, modelOptions);
274
+ const modelCsv = readConfigCsvFile(configDir, "model")[0];
275
+ const graphDefaultMinTime = Number(modelCsv["graph default min time"]);
276
+ const graphDefaultMaxTime = Number(modelCsv["graph default max time"]);
277
+ const datFilesString = modelCsv["model dat files"];
278
+ const origDatFiles = datFilesString.length > 0 ? datFilesString.split(";") : [];
279
+ const prepDir = buildContext.config.prepDir;
280
+ const projDir = buildContext.config.rootDir;
281
+ const modelOptions = {
282
+ graphDefaultMinTime,
283
+ graphDefaultMaxTime,
284
+ datFiles: origDatFiles.map((f) => join(relative(prepDir, projDir), f)),
285
+ bundleListing: modelCsv["bundle listing"] === "true",
286
+ customConstants: modelCsv["custom constants"] === "true",
287
+ customLookups: modelCsv["custom lookups"] === "true",
288
+ customOutputs: modelCsv["custom outputs"] === "true"
289
+ };
290
+ const strings = readStringsCsv(configDir);
291
+ const colorsCsv = readConfigCsvFile(configDir, "colors");
292
+ const colors = /* @__PURE__ */ new Map();
293
+ for (const row of colorsCsv) {
294
+ const colorId = row["id"];
295
+ const hexColor = row["hex code"];
296
+ colors.set(colorId, hexColor);
297
+ }
298
+ return new ConfigContext(buildContext, configDir, strings, colors, modelOptions);
317
299
  }
318
300
  function configFilePath(configDir, name, ext) {
319
- return joinPath(configDir, `${name}.${ext}`);
301
+ return join(configDir, `${name}.${ext}`);
320
302
  }
321
303
  function readCsvFile(path) {
322
- const data = readFileSync(path, "utf8");
323
- return parseCsv(data, {
324
- columns: true,
325
- trim: true,
326
- skip_empty_lines: true,
327
- skip_records_with_empty_values: true
328
- });
304
+ const data = readFileSync(path, "utf8");
305
+ return parse(data, {
306
+ columns: true,
307
+ trim: true,
308
+ skip_empty_lines: true,
309
+ skip_records_with_empty_values: true
310
+ });
329
311
  }
330
312
  function readConfigCsvFile(configDir, name) {
331
- return readCsvFile(configFilePath(configDir, name, "csv"));
313
+ return readCsvFile(configFilePath(configDir, name, "csv"));
332
314
  }
315
+ /**
316
+ * Initialize a `Strings` instance with the core strings from `strings.csv`.
317
+ */
333
318
  function readStringsCsv(configDir) {
334
- const strings = new Strings();
335
- const layout2 = "default";
336
- const context = "Core";
337
- const rows = readConfigCsvFile(configDir, "strings");
338
- for (const row of rows) {
339
- const key = row["id"];
340
- let str = row["string"];
341
- str = str ? str.trim() : "";
342
- if (str) {
343
- strings.add(key, str, layout2, context);
344
- }
345
- }
346
- return strings;
319
+ const strings = new Strings();
320
+ const layout = "default";
321
+ const context = "Core";
322
+ const rows = readConfigCsvFile(configDir, "strings");
323
+ for (const row of rows) {
324
+ const key = row["id"];
325
+ let str = row["string"];
326
+ str = str ? str.trim() : "";
327
+ if (str) strings.add(key, str, layout, context);
328
+ }
329
+ return strings;
347
330
  }
348
-
349
- // src/gen-model-spec.ts
331
+ //#endregion
332
+ //#region src/gen-model-spec.ts
333
+ /**
334
+ * Write the `model-spec.ts` file used by the core package to initialize the model.
335
+ */
350
336
  function writeModelSpec(context, dstDir) {
351
- const inputVarIds = context.getOrderedInputs().map((i) => context.canonicalVarId(i.varName));
352
- const outputVarIds = context.getOrderedOutputs().map((o) => context.canonicalVarId(o.varName));
353
- let tsContent = "";
354
- function emit(s) {
355
- tsContent += s + "\n";
356
- }
357
- emit("// This file is generated by `@sdeverywhere/plugin-config`; do not edit manually!");
358
- emit(`export const inputVarIds: string[] = ${JSON.stringify(inputVarIds, null, 2)}`);
359
- emit(`export const outputVarIds: string[] = ${JSON.stringify(outputVarIds, null, 2)}`);
360
- context.writeStagedFile("model", dstDir, "model-spec.ts", tsContent);
337
+ const inputVarIds = context.getOrderedInputs().map((i) => context.canonicalVarId(i.varName));
338
+ const outputVarIds = context.getOrderedOutputs().map((o) => context.canonicalVarId(o.varName));
339
+ let tsContent = "";
340
+ function emit(s) {
341
+ tsContent += s + "\n";
342
+ }
343
+ emit("// This file is generated by `@sdeverywhere/plugin-config`; do not edit manually!");
344
+ emit(`export const inputVarIds: string[] = ${JSON.stringify(inputVarIds, null, 2)}`);
345
+ emit(`export const outputVarIds: string[] = ${JSON.stringify(outputVarIds, null, 2)}`);
346
+ context.writeStagedFile("model", dstDir, "model-spec.ts", tsContent);
361
347
  }
362
-
363
- // src/gen-config-specs.ts
364
- import { readFileSync as readFileSync2 } from "fs";
365
- import { dirname, resolve as resolvePath } from "path";
366
- import { fileURLToPath } from "url";
367
-
368
- // src/read-config.ts
348
+ //#endregion
349
+ //#region src/read-config.ts
369
350
  function optionalString(stringValue) {
370
- if (stringValue !== void 0 && stringValue.length > 0) {
371
- return stringValue;
372
- } else {
373
- return void 0;
374
- }
351
+ if (stringValue !== void 0 && stringValue.length > 0) return stringValue;
352
+ else return;
375
353
  }
376
354
  function optionalNumber(stringValue) {
377
- if (stringValue !== void 0 && stringValue.length > 0) {
378
- return Number(stringValue);
379
- } else {
380
- return void 0;
381
- }
355
+ if (stringValue !== void 0 && stringValue.length > 0) return Number(stringValue);
356
+ else return;
382
357
  }
383
-
384
- // src/gen-graphs.ts
358
+ //#endregion
359
+ //#region src/gen-graphs.ts
360
+ /**
361
+ * Convert the `config/graphs.csv` file to config specs that can be used in
362
+ * the core package.
363
+ */
385
364
  function generateGraphSpecs(context) {
386
- const graphsCsv = context.readConfigCsvFile("graphs");
387
- const graphSpecs = /* @__PURE__ */ new Map();
388
- for (const row of graphsCsv) {
389
- const spec = graphSpecFromCsv(row, context);
390
- if (spec) {
391
- graphSpecs.set(spec.id, spec);
392
- }
393
- }
394
- return graphSpecs;
365
+ const graphsCsv = context.readConfigCsvFile("graphs");
366
+ const graphSpecs = /* @__PURE__ */ new Map();
367
+ for (const row of graphsCsv) {
368
+ const spec = graphSpecFromCsv(row, context);
369
+ if (spec) graphSpecs.set(spec.id, spec);
370
+ }
371
+ return graphSpecs;
395
372
  }
396
373
  function graphSpecFromCsv(g, context) {
397
- const strings = context.strings;
398
- const layout2 = "default";
399
- function requiredString(key2) {
400
- const value = g[key2];
401
- if (value === void 0 || typeof value !== "string" || value.trim().length === 0) {
402
- throw new Error(`Must specify '${key2}' for graph ${g.id}`);
403
- }
404
- return value;
405
- }
406
- const graphIdParts = requiredString("id").split(";");
407
- const graphId = graphIdParts[0];
408
- const graphIdBaseParts = graphId.split("-");
409
- const graphBaseId = graphIdBaseParts[0];
410
- const title = requiredString("graph title");
411
- const menuTitle = optionalString(g["menu title"]);
412
- const miniTitle = optionalString(g["mini title"]);
413
- const parentMenu = optionalString(g["parent menu"]);
414
- const description = optionalString(g["description"]);
415
- const kindString = optionalString(g["kind"]);
416
- if (!parentMenu) {
417
- context.log("info", `Skipping graph ${graphId} (${title})`);
418
- return void 0;
419
- }
420
- const key = (kind2) => `graph_${graphBaseId.padStart(3, "0")}_${kind2}`;
421
- const strCtxt = (kind2) => {
422
- const parent = htmlToUtf8(parentMenu).replace("&amp;", "&");
423
- const displayTitle = htmlToUtf8(menuTitle || title).replace("&amp;", "&");
424
- return `Graph ${kind2}: ${parent} > ${displayTitle}`;
425
- };
426
- const titleKey = strings.add(key("title"), title, layout2, strCtxt("Title"));
427
- let menuTitleKey;
428
- if (menuTitle) {
429
- menuTitleKey = strings.add(key("menu_title"), menuTitle, layout2, strCtxt("Menu Item"));
430
- }
431
- let miniTitleKey;
432
- if (miniTitle) {
433
- miniTitleKey = strings.add(key("mini_title"), miniTitle, layout2, strCtxt("Title (for Mini View)"));
434
- }
435
- let descriptionKey;
436
- if (description) {
437
- descriptionKey = strings.add(key("description"), description, layout2, strCtxt("Description"), "graph-descriptions");
438
- }
439
- const kind = kindString;
440
- const sideString = optionalString(g["side"]);
441
- const side = sideString;
442
- const unitsString = optionalString(g["units"]);
443
- const altIdString = optionalString(g["alternate"]);
444
- let unitSystem;
445
- let alternates;
446
- if (unitsString && altIdString) {
447
- if (unitsString === "metric") {
448
- unitSystem = "metric";
449
- alternates = [
450
- {
451
- id: altIdString,
452
- unitSystem: "us"
453
- }
454
- ];
455
- } else if (unitsString === "us") {
456
- unitSystem = "us";
457
- alternates = [
458
- {
459
- id: altIdString,
460
- unitSystem: "metric"
461
- }
462
- ];
463
- }
464
- }
465
- const modelOptions = context.modelOptions;
466
- const xMin = optionalNumber(g["x axis min"]) || modelOptions.graphDefaultMinTime;
467
- const xMax = optionalNumber(g["x axis max"]) || modelOptions.graphDefaultMaxTime;
468
- const xAxisLabel = optionalString(g["x axis label"]);
469
- let xAxisLabelKey;
470
- if (xAxisLabel) {
471
- xAxisLabelKey = strings.add(genStringKey("graph_xaxis_label", xAxisLabel), xAxisLabel, layout2, "Graph X-Axis Label");
472
- }
473
- const yMin = optionalNumber(g["y axis min"]) || 0;
474
- const yMax = optionalNumber(g["y axis max"]);
475
- const ySoftMax = optionalNumber(g["y axis soft max"]);
476
- const yFormat = optionalString(g["y axis format"]) || ".0f";
477
- const yAxisLabel = optionalString(g["y axis label"]);
478
- let yAxisLabelKey;
479
- if (yAxisLabel) {
480
- yAxisLabelKey = strings.add(genStringKey("graph_yaxis_label", yAxisLabel), yAxisLabel, layout2, "Graph Y-Axis Label");
481
- }
482
- const datasets = [];
483
- function addDataset(index, overrides) {
484
- const plotKey = (name) => `plot ${index} ${name}`;
485
- const varName = g[plotKey("variable")];
486
- if (!varName) {
487
- return;
488
- }
489
- const varId = context.canonicalVarId(varName);
490
- const externalSourceName = overrides?.sourceName || optionalString(g[plotKey("source")]);
491
- const datasetLabel = optionalString(g[plotKey("label")]);
492
- let labelKey;
493
- if (datasetLabel) {
494
- labelKey = strings.add(
495
- genStringKey("graph_dataset_label", datasetLabel),
496
- datasetLabel,
497
- layout2,
498
- "Graph Dataset Label"
499
- );
500
- }
501
- const colorId = overrides?.colorId || requiredString(plotKey("color"));
502
- const hexColor = context.getHexColorForId(colorId);
503
- if (!hexColor) {
504
- throw new Error(`Graph ${graphId} references an unknown color ${colorId}`);
505
- }
506
- const lineStyleAndModString = optionalString(g[plotKey("style")]) || "line";
507
- const lineStyleParts = lineStyleAndModString.split(";");
508
- const lineStyleString = lineStyleParts[0];
509
- const lineStyleModifierString = lineStyleParts.length > 1 ? lineStyleParts[1] : void 0;
510
- const lineStyle = lineStyleString;
511
- let lineStyleModifiers;
512
- if (lineStyleModifierString) {
513
- lineStyleModifiers = [lineStyleModifierString];
514
- }
515
- if (externalSourceName === void 0 || externalSourceName.startsWith("Scenario") || externalSourceName === "Ref") {
516
- context.addOutputVariable(varName);
517
- } else {
518
- context.addStaticVariable(externalSourceName, varName);
519
- }
520
- const datasetSpec = {
521
- varId,
522
- varName,
523
- externalSourceName,
524
- labelKey,
525
- color: hexColor,
526
- lineStyle,
527
- lineStyleModifiers
528
- };
529
- datasets.push(datasetSpec);
530
- }
531
- for (let i = 1; i <= 11; i++) {
532
- addDataset(i);
533
- }
534
- const legendItems = datasets.filter((dataset) => dataset.labelKey?.length > 0).map((dataset) => {
535
- return {
536
- color: dataset.color,
537
- labelKey: dataset.labelKey
538
- };
539
- });
540
- const graphSpec = {
541
- id: graphId,
542
- kind,
543
- titleKey,
544
- miniTitleKey,
545
- menuTitleKey,
546
- descriptionKey,
547
- side,
548
- unitSystem,
549
- alternates,
550
- xMin,
551
- xMax,
552
- xAxisLabelKey,
553
- yMin,
554
- yMax,
555
- ySoftMax,
556
- yAxisLabelKey,
557
- yFormat,
558
- datasets,
559
- legendItems
560
- };
561
- return graphSpec;
374
+ const strings = context.strings;
375
+ const layout = "default";
376
+ function requiredString(key) {
377
+ const value = g[key];
378
+ if (value === void 0 || typeof value !== "string" || value.trim().length === 0) throw new Error(`Must specify '${key}' for graph ${g.id}`);
379
+ return value;
380
+ }
381
+ const graphId = requiredString("id").split(";")[0];
382
+ const graphBaseId = graphId.split("-")[0];
383
+ const title = requiredString("graph title");
384
+ const menuTitle = optionalString(g["menu title"]);
385
+ const miniTitle = optionalString(g["mini title"]);
386
+ const parentMenu = optionalString(g["parent menu"]);
387
+ const description = optionalString(g["description"]);
388
+ const kindString = optionalString(g["kind"]);
389
+ if (!parentMenu) {
390
+ context.log("info", `Skipping graph ${graphId} (${title})`);
391
+ return;
392
+ }
393
+ const key = (kind) => `graph_${graphBaseId.padStart(3, "0")}_${kind}`;
394
+ const strCtxt = (kind) => {
395
+ return `Graph ${kind}: ${htmlToUtf8(parentMenu).replace("&amp;", "&")} > ${htmlToUtf8(menuTitle || title).replace("&amp;", "&")}`;
396
+ };
397
+ const titleKey = strings.add(key("title"), title, layout, strCtxt("Title"));
398
+ let menuTitleKey;
399
+ if (menuTitle) menuTitleKey = strings.add(key("menu_title"), menuTitle, layout, strCtxt("Menu Item"));
400
+ let miniTitleKey;
401
+ if (miniTitle) miniTitleKey = strings.add(key("mini_title"), miniTitle, layout, strCtxt("Title (for Mini View)"));
402
+ let descriptionKey;
403
+ if (description) descriptionKey = strings.add(key("description"), description, layout, strCtxt("Description"), "graph-descriptions");
404
+ const kind = kindString;
405
+ const side = optionalString(g["side"]);
406
+ const unitsString = optionalString(g["units"]);
407
+ const altIdString = optionalString(g["alternate"]);
408
+ let unitSystem;
409
+ let alternates;
410
+ if (unitsString && altIdString) {
411
+ if (unitsString === "metric") {
412
+ unitSystem = "metric";
413
+ alternates = [{
414
+ id: altIdString,
415
+ unitSystem: "us"
416
+ }];
417
+ } else if (unitsString === "us") {
418
+ unitSystem = "us";
419
+ alternates = [{
420
+ id: altIdString,
421
+ unitSystem: "metric"
422
+ }];
423
+ }
424
+ }
425
+ const modelOptions = context.modelOptions;
426
+ const xMin = optionalNumber(g["x axis min"]) || modelOptions.graphDefaultMinTime;
427
+ const xMax = optionalNumber(g["x axis max"]) || modelOptions.graphDefaultMaxTime;
428
+ const xAxisLabel = optionalString(g["x axis label"]);
429
+ let xAxisLabelKey;
430
+ if (xAxisLabel) xAxisLabelKey = strings.add(genStringKey("graph_xaxis_label", xAxisLabel), xAxisLabel, layout, "Graph X-Axis Label");
431
+ const yMin = optionalNumber(g["y axis min"]) || 0;
432
+ const yMax = optionalNumber(g["y axis max"]);
433
+ const ySoftMax = optionalNumber(g["y axis soft max"]);
434
+ const yFormat = optionalString(g["y axis format"]) || ".0f";
435
+ const yAxisLabel = optionalString(g["y axis label"]);
436
+ let yAxisLabelKey;
437
+ if (yAxisLabel) yAxisLabelKey = strings.add(genStringKey("graph_yaxis_label", yAxisLabel), yAxisLabel, layout, "Graph Y-Axis Label");
438
+ const datasets = [];
439
+ function addDataset(index, overrides) {
440
+ const plotKey = (name) => `plot ${index} ${name}`;
441
+ const varName = g[plotKey("variable")];
442
+ if (!varName) return;
443
+ const varId = context.canonicalVarId(varName);
444
+ const externalSourceName = overrides?.sourceName || optionalString(g[plotKey("source")]);
445
+ const datasetLabel = optionalString(g[plotKey("label")]);
446
+ let labelKey;
447
+ if (datasetLabel) labelKey = strings.add(genStringKey("graph_dataset_label", datasetLabel), datasetLabel, layout, "Graph Dataset Label");
448
+ const colorId = overrides?.colorId || requiredString(plotKey("color"));
449
+ const hexColor = context.getHexColorForId(colorId);
450
+ if (!hexColor) throw new Error(`Graph ${graphId} references an unknown color ${colorId}`);
451
+ const lineStyleParts = (optionalString(g[plotKey("style")]) || "line").split(";");
452
+ const lineStyleString = lineStyleParts[0];
453
+ const lineStyleModifierString = lineStyleParts.length > 1 ? lineStyleParts[1] : void 0;
454
+ const lineStyle = lineStyleString;
455
+ let lineStyleModifiers;
456
+ if (lineStyleModifierString) lineStyleModifiers = [lineStyleModifierString];
457
+ if (externalSourceName === void 0 || externalSourceName.startsWith("Scenario") || externalSourceName === "Ref") context.addOutputVariable(varName);
458
+ else context.addStaticVariable(externalSourceName, varName);
459
+ const datasetSpec = {
460
+ varId,
461
+ varName,
462
+ externalSourceName,
463
+ labelKey,
464
+ color: hexColor,
465
+ lineStyle,
466
+ lineStyleModifiers
467
+ };
468
+ datasets.push(datasetSpec);
469
+ }
470
+ for (let i = 1; i <= 11; i++) addDataset(i);
471
+ const legendItems = datasets.filter((dataset) => dataset.labelKey?.length > 0).map((dataset) => {
472
+ return {
473
+ color: dataset.color,
474
+ labelKey: dataset.labelKey
475
+ };
476
+ });
477
+ return {
478
+ id: graphId,
479
+ kind,
480
+ titleKey,
481
+ miniTitleKey,
482
+ menuTitleKey,
483
+ descriptionKey,
484
+ side,
485
+ unitSystem,
486
+ alternates,
487
+ xMin,
488
+ xMax,
489
+ xAxisLabelKey,
490
+ yMin,
491
+ yMax,
492
+ ySoftMax,
493
+ yAxisLabelKey,
494
+ yFormat,
495
+ datasets,
496
+ legendItems
497
+ };
562
498
  }
563
-
564
- // src/gen-inputs.ts
565
- var layout = "default";
499
+ //#endregion
500
+ //#region src/gen-inputs.ts
501
+ const layout = "default";
502
+ /**
503
+ * Convert the `config/inputs.csv` file to config specs that can be used in
504
+ * the core package.
505
+ */
566
506
  function generateInputsConfig(context) {
567
- const inputsCsv = context.readConfigCsvFile("inputs");
568
- const inputSpecs = /* @__PURE__ */ new Map();
569
- for (const row of inputsCsv) {
570
- const spec = inputSpecFromCsv(row, context);
571
- if (spec) {
572
- inputSpecs.set(spec.id, spec);
573
- }
574
- }
575
- return inputSpecs;
507
+ const inputsCsv = context.readConfigCsvFile("inputs");
508
+ const inputSpecs = /* @__PURE__ */ new Map();
509
+ for (const row of inputsCsv) {
510
+ const spec = inputSpecFromCsv(row, context);
511
+ if (spec) inputSpecs.set(spec.id, spec);
512
+ }
513
+ return inputSpecs;
576
514
  }
577
515
  function inputSpecFromCsv(r, context) {
578
- const strings = context.strings;
579
- function requiredString(key2) {
580
- const value = r[key2];
581
- if (value === void 0 || typeof value !== "string" || value.trim().length === 0) {
582
- throw new Error(`Must specify '${key2}' for input ${r.id}`);
583
- }
584
- return value;
585
- }
586
- function requiredNumber(key2) {
587
- const stringValue = requiredString(key2);
588
- const numValue = Number(stringValue);
589
- if (numValue === void 0) {
590
- throw new Error(`Must specify numeric '${key2}' for input ${r.id}`);
591
- }
592
- return numValue;
593
- }
594
- const inputIdParts = requiredString("id").split(";");
595
- const inputId = inputIdParts[0];
596
- const viewId = optionalString(r["viewid"]);
597
- const label = optionalString(r["label"]) || "";
598
- const inputType = requiredString("input type");
599
- if (!viewId) {
600
- context.log("info", `Skipping input ${inputId} (${label})`);
601
- return void 0;
602
- }
603
- const description = optionalString(r["description"]);
604
- const key = (kind) => `input_${inputId.padStart(3, "0")}_${kind}`;
605
- const groupTitle = optionalString(r["group name"]);
606
- if (!groupTitle) {
607
- throw new Error(`Must specify 'group name' for input ${inputId}`);
608
- }
609
- const groupTitleKey = genStringKey("input_group_title", groupTitle);
610
- strings.add(groupTitleKey, groupTitle, layout, "Input Group Title");
611
- let typeLabel;
612
- switch (inputType) {
613
- case "slider":
614
- typeLabel = "Slider";
615
- break;
616
- case "switch":
617
- typeLabel = "Switch";
618
- break;
619
- case "checkbox":
620
- typeLabel = "Checkbox";
621
- break;
622
- case "checkbox group":
623
- typeLabel = "Checkbox Group";
624
- break;
625
- default:
626
- throw new Error(`Unexpected input type ${inputType}`);
627
- }
628
- const strCtxt = (kind) => {
629
- const labelText = htmlToUtf8(label).replace("&amp;", "&");
630
- return `${typeLabel} ${kind}: ${groupTitle} > ${labelText}`;
631
- };
632
- const labelKey = strings.add(key("label"), label, layout, strCtxt("Label"));
633
- const listingLabel = optionalString(r["listing label"]);
634
- let listingLabelKey;
635
- if (listingLabel) {
636
- listingLabelKey = strings.add(key("action_label"), listingLabel, layout, strCtxt("Action Label"));
637
- }
638
- let descriptionKey;
639
- if (description) {
640
- descriptionKey = strings.add(key("description"), description, layout, strCtxt("Description"), "input-descriptions");
641
- }
642
- function sliderSpecFromCsv() {
643
- const varName = requiredString("varname");
644
- const varId = context.canonicalVarId(varName);
645
- const defaultValue = requiredNumber("slider/switch default");
646
- const minValue = requiredNumber("slider min");
647
- const maxValue = requiredNumber("slider max");
648
- const step = requiredNumber("slider step");
649
- const reversed = optionalString(r["reversed"]) === "yes";
650
- if (defaultValue < minValue || defaultValue > maxValue) {
651
- let e = `Default value for slider ${inputId} is out of range: `;
652
- e += `default=${defaultValue} min=${minValue} max=${maxValue}`;
653
- throw new Error(e);
654
- }
655
- context.addInputVariable(inputId, varName, defaultValue, minValue, maxValue);
656
- const format = optionalString(r["format"]) || ".0f";
657
- const units = optionalString(r["units"]);
658
- let unitsKey;
659
- if (units) {
660
- unitsKey = strings.add(genStringKey("input_units", units), units, layout, "Slider Units");
661
- }
662
- const rangeInfo = getSliderRangeInfo(r, maxValue, context);
663
- const rangeLabelKeys = rangeInfo.labelKeys;
664
- const rangeDividers = rangeInfo.dividers;
665
- return {
666
- kind: "slider",
667
- id: inputId,
668
- varId,
669
- varName,
670
- defaultValue,
671
- minValue,
672
- maxValue,
673
- step,
674
- reversed,
675
- labelKey,
676
- listingLabelKey,
677
- descriptionKey,
678
- unitsKey,
679
- rangeLabelKeys,
680
- rangeDividers,
681
- format
682
- };
683
- }
684
- function switchSpecFromCsv() {
685
- const varName = requiredString("varname");
686
- const varId = context.canonicalVarId(varName);
687
- const onValue = requiredNumber("enabled value");
688
- const offValue = requiredNumber("disabled value");
689
- const defaultValue = requiredNumber("slider/switch default");
690
- if (defaultValue !== onValue && defaultValue !== offValue) {
691
- throw new Error(
692
- `Invalid default value for switch ${inputId}: off=${offValue} on=${onValue} default=${defaultValue}`
693
- );
694
- }
695
- const minValue = offValue;
696
- const maxValue = onValue;
697
- context.addInputVariable(inputId, varName, defaultValue, minValue, maxValue);
698
- const controlledInputIds = requiredString("controlled input ids");
699
- const controlledParts = controlledInputIds.split("|");
700
- const rowsActiveWhenOff = controlledParts[0].split(";").filter((id) => id.trim().length > 0);
701
- const rowsActiveWhenOn = controlledParts[1].split(";").filter((id) => id.trim().length > 0);
702
- return {
703
- kind: "switch",
704
- id: inputId,
705
- varId,
706
- varName,
707
- labelKey,
708
- listingLabelKey,
709
- descriptionKey,
710
- defaultValue,
711
- offValue,
712
- onValue,
713
- slidersActiveWhenOff: rowsActiveWhenOff,
714
- slidersActiveWhenOn: rowsActiveWhenOn
715
- };
716
- }
717
- let inputSpec;
718
- switch (inputType) {
719
- case "slider": {
720
- inputSpec = sliderSpecFromCsv();
721
- break;
722
- }
723
- case "switch":
724
- case "checkbox": {
725
- inputSpec = switchSpecFromCsv();
726
- break;
727
- }
728
- case "checkbox group":
729
- break;
730
- default:
731
- throw new Error(`Unexpected input type ${inputType}`);
732
- }
733
- return inputSpec;
516
+ const strings = context.strings;
517
+ function requiredString(key) {
518
+ const value = r[key];
519
+ if (value === void 0 || typeof value !== "string" || value.trim().length === 0) throw new Error(`Must specify '${key}' for input ${r.id}`);
520
+ return value;
521
+ }
522
+ function requiredNumber(key) {
523
+ const stringValue = requiredString(key);
524
+ const numValue = Number(stringValue);
525
+ if (numValue === void 0) throw new Error(`Must specify numeric '${key}' for input ${r.id}`);
526
+ return numValue;
527
+ }
528
+ const inputId = requiredString("id").split(";")[0];
529
+ const viewId = optionalString(r["viewid"]);
530
+ const label = optionalString(r["label"]) || "";
531
+ const inputType = requiredString("input type");
532
+ if (!viewId) {
533
+ context.log("info", `Skipping input ${inputId} (${label})`);
534
+ return;
535
+ }
536
+ const description = optionalString(r["description"]);
537
+ const key = (kind) => `input_${inputId.padStart(3, "0")}_${kind}`;
538
+ const groupTitle = optionalString(r["group name"]);
539
+ if (!groupTitle) throw new Error(`Must specify 'group name' for input ${inputId}`);
540
+ const groupTitleKey = genStringKey("input_group_title", groupTitle);
541
+ strings.add(groupTitleKey, groupTitle, layout, "Input Group Title");
542
+ let typeLabel;
543
+ switch (inputType) {
544
+ case "slider":
545
+ typeLabel = "Slider";
546
+ break;
547
+ case "switch":
548
+ typeLabel = "Switch";
549
+ break;
550
+ case "checkbox":
551
+ typeLabel = "Checkbox";
552
+ break;
553
+ case "checkbox group":
554
+ typeLabel = "Checkbox Group";
555
+ break;
556
+ default: throw new Error(`Unexpected input type ${inputType}`);
557
+ }
558
+ const strCtxt = (kind) => {
559
+ const labelText = htmlToUtf8(label).replace("&amp;", "&");
560
+ return `${typeLabel} ${kind}: ${groupTitle} > ${labelText}`;
561
+ };
562
+ const labelKey = strings.add(key("label"), label, layout, strCtxt("Label"));
563
+ const listingLabel = optionalString(r["listing label"]);
564
+ let listingLabelKey;
565
+ if (listingLabel) listingLabelKey = strings.add(key("action_label"), listingLabel, layout, strCtxt("Action Label"));
566
+ let descriptionKey;
567
+ if (description) descriptionKey = strings.add(key("description"), description, layout, strCtxt("Description"), "input-descriptions");
568
+ function sliderSpecFromCsv() {
569
+ const varName = requiredString("varname");
570
+ const varId = context.canonicalVarId(varName);
571
+ const defaultValue = requiredNumber("slider/switch default");
572
+ const minValue = requiredNumber("slider min");
573
+ const maxValue = requiredNumber("slider max");
574
+ const step = requiredNumber("slider step");
575
+ const reversed = optionalString(r["reversed"]) === "yes";
576
+ if (defaultValue < minValue || defaultValue > maxValue) {
577
+ let e = `Default value for slider ${inputId} is out of range: `;
578
+ e += `default=${defaultValue} min=${minValue} max=${maxValue}`;
579
+ throw new Error(e);
580
+ }
581
+ context.addInputVariable(inputId, varName, defaultValue, minValue, maxValue);
582
+ const format = optionalString(r["format"]) || ".0f";
583
+ const units = optionalString(r["units"]);
584
+ let unitsKey;
585
+ if (units) unitsKey = strings.add(genStringKey("input_units", units), units, layout, "Slider Units");
586
+ const rangeInfo = getSliderRangeInfo(r, maxValue, context);
587
+ const rangeLabelKeys = rangeInfo.labelKeys;
588
+ const rangeDividers = rangeInfo.dividers;
589
+ return {
590
+ kind: "slider",
591
+ id: inputId,
592
+ varId,
593
+ varName,
594
+ defaultValue,
595
+ minValue,
596
+ maxValue,
597
+ step,
598
+ reversed,
599
+ labelKey,
600
+ listingLabelKey,
601
+ descriptionKey,
602
+ unitsKey,
603
+ rangeLabelKeys,
604
+ rangeDividers,
605
+ format
606
+ };
607
+ }
608
+ function switchSpecFromCsv() {
609
+ const varName = requiredString("varname");
610
+ const varId = context.canonicalVarId(varName);
611
+ const onValue = requiredNumber("enabled value");
612
+ const offValue = requiredNumber("disabled value");
613
+ const defaultValue = requiredNumber("slider/switch default");
614
+ if (defaultValue !== onValue && defaultValue !== offValue) throw new Error(`Invalid default value for switch ${inputId}: off=${offValue} on=${onValue} default=${defaultValue}`);
615
+ const minValue = offValue;
616
+ const maxValue = onValue;
617
+ context.addInputVariable(inputId, varName, defaultValue, minValue, maxValue);
618
+ const controlledParts = requiredString("controlled input ids").split("|");
619
+ const rowsActiveWhenOff = controlledParts[0].split(";").filter((id) => id.trim().length > 0);
620
+ const rowsActiveWhenOn = controlledParts[1].split(";").filter((id) => id.trim().length > 0);
621
+ return {
622
+ kind: "switch",
623
+ id: inputId,
624
+ varId,
625
+ varName,
626
+ labelKey,
627
+ listingLabelKey,
628
+ descriptionKey,
629
+ defaultValue,
630
+ offValue,
631
+ onValue,
632
+ slidersActiveWhenOff: rowsActiveWhenOff,
633
+ slidersActiveWhenOn: rowsActiveWhenOn
634
+ };
635
+ }
636
+ let inputSpec;
637
+ switch (inputType) {
638
+ case "slider":
639
+ inputSpec = sliderSpecFromCsv();
640
+ break;
641
+ case "switch":
642
+ case "checkbox":
643
+ inputSpec = switchSpecFromCsv();
644
+ break;
645
+ case "checkbox group": break;
646
+ default: throw new Error(`Unexpected input type ${inputType}`);
647
+ }
648
+ return inputSpec;
734
649
  }
735
650
  function getSliderRangeInfo(r, maxValue, context) {
736
- const strings = context.strings;
737
- const labelKeys = [];
738
- const dividers = [];
739
- let rangeNum = 1;
740
- while (rangeNum <= 5) {
741
- const label = optionalString(r[`range ${rangeNum} label`]);
742
- if (!label) {
743
- break;
744
- }
745
- const labelKey = strings.add(genStringKey("input_range", label), label, layout, "Slider Range Label");
746
- if (!labelKey) {
747
- break;
748
- }
749
- labelKeys.push(labelKey);
750
- rangeNum++;
751
- }
752
- const numRanges = rangeNum - 1;
753
- for (rangeNum = 2; rangeNum <= numRanges; rangeNum++) {
754
- let divider = optionalNumber(r[`range ${rangeNum} start`]);
755
- if (divider === void 0) {
756
- divider = maxValue;
757
- }
758
- dividers.push(divider);
759
- }
760
- return {
761
- labelKeys,
762
- dividers
763
- };
651
+ const strings = context.strings;
652
+ const labelKeys = [];
653
+ const dividers = [];
654
+ let rangeNum = 1;
655
+ while (rangeNum <= 5) {
656
+ const label = optionalString(r[`range ${rangeNum} label`]);
657
+ if (!label) break;
658
+ const labelKey = strings.add(genStringKey("input_range", label), label, layout, "Slider Range Label");
659
+ if (!labelKey) break;
660
+ labelKeys.push(labelKey);
661
+ rangeNum++;
662
+ }
663
+ const numRanges = rangeNum - 1;
664
+ for (rangeNum = 2; rangeNum <= numRanges; rangeNum++) {
665
+ let divider = optionalNumber(r[`range ${rangeNum} start`]);
666
+ if (divider === void 0) divider = maxValue;
667
+ dividers.push(divider);
668
+ }
669
+ return {
670
+ labelKeys,
671
+ dividers
672
+ };
764
673
  }
765
-
766
- // src/gen-config-specs.ts
767
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
674
+ //#endregion
675
+ //#region src/gen-config-specs.ts
676
+ const __dirname = dirname(fileURLToPath(import.meta.url));
677
+ /**
678
+ * Convert the CSV files in the `config` directory to config specs that can be
679
+ * used in the core package.
680
+ */
768
681
  function generateConfigSpecs(context) {
769
- context.log("verbose", " Reading graph specs");
770
- const graphSpecs = generateGraphSpecs(context);
771
- context.log("verbose", " Reading input specs");
772
- const inputSpecs = generateInputsConfig(context);
773
- context.log("verbose", " Reading extra output variables");
774
- const extraOutputsCsv = context.readConfigCsvFile("outputs");
775
- for (const row of extraOutputsCsv) {
776
- const varName = row["variable name"];
777
- if (varName) {
778
- context.addOutputVariable(varName);
779
- }
780
- }
781
- return {
782
- graphSpecs,
783
- inputSpecs
784
- };
682
+ context.log("verbose", " Reading graph specs");
683
+ const graphSpecs = generateGraphSpecs(context);
684
+ context.log("verbose", " Reading input specs");
685
+ const inputSpecs = generateInputsConfig(context);
686
+ context.log("verbose", " Reading extra output variables");
687
+ const extraOutputsCsv = context.readConfigCsvFile("outputs");
688
+ for (const row of extraOutputsCsv) {
689
+ const varName = row["variable name"];
690
+ if (varName) context.addOutputVariable(varName);
691
+ }
692
+ return {
693
+ graphSpecs,
694
+ inputSpecs
695
+ };
785
696
  }
697
+ /**
698
+ * Write the `config-specs.ts` file to the given destination directory.
699
+ */
786
700
  function writeConfigSpecs(context, config, dstDir) {
787
- let tsContent = "";
788
- function emit(s) {
789
- tsContent += s + "\n";
790
- }
791
- emit("// This file is generated by `@sdeverywhere/plugin-config`; do not edit manually!");
792
- emit("");
793
- emit(`import type { GraphSpec, InputSpec } from './spec-types'`);
794
- function emitArray(type, values) {
795
- const varName = type.charAt(0).toLowerCase() + type.slice(1) + "s";
796
- const array = Array.from(values);
797
- const json = JSON.stringify(array, null, 2);
798
- emit("");
799
- emit(`export const ${varName}: ${type}[] = ${json}`);
800
- }
801
- emitArray("GraphSpec", config.graphSpecs.values());
802
- emitArray("InputSpec", config.inputSpecs.values());
803
- context.writeStagedFile("config", dstDir, "config-specs.ts", tsContent);
701
+ let tsContent = "";
702
+ function emit(s) {
703
+ tsContent += s + "\n";
704
+ }
705
+ emit("// This file is generated by `@sdeverywhere/plugin-config`; do not edit manually!");
706
+ emit("");
707
+ emit(`import type { GraphSpec, InputSpec } from './spec-types'`);
708
+ function emitArray(type, values) {
709
+ const varName = type.charAt(0).toLowerCase() + type.slice(1) + "s";
710
+ const array = Array.from(values);
711
+ const json = JSON.stringify(array, null, 2);
712
+ emit("");
713
+ emit(`export const ${varName}: ${type}[] = ${json}`);
714
+ }
715
+ emitArray("GraphSpec", config.graphSpecs.values());
716
+ emitArray("InputSpec", config.inputSpecs.values());
717
+ context.writeStagedFile("config", dstDir, "config-specs.ts", tsContent);
804
718
  }
719
+ /**
720
+ * Write the `spec-types.ts` file to the given destination directory.
721
+ */
805
722
  function writeSpecTypes(context, dstDir) {
806
- const tsFile = "spec-types.ts";
807
- const tsPath = resolvePath(__dirname2, tsFile);
808
- const tsContent = readFileSync2(tsPath, "utf8");
809
- context.writeStagedFile("config", dstDir, tsFile, tsContent);
723
+ const tsFile = "spec-types.ts";
724
+ const tsPath = resolve(__dirname, tsFile);
725
+ const tsContent = readFileSync(tsPath, "utf8");
726
+ context.writeStagedFile("config", dstDir, tsFile, tsContent);
810
727
  }
811
-
812
- // src/processor.ts
728
+ //#endregion
729
+ //#region src/processor.ts
730
+ /**
731
+ * Returns a function that can be passed as the `modelSpec` function for the SDEverywhere
732
+ * `UserConfig`. The returned function:
733
+ * - reads CSV files from a `config` directory
734
+ * - writes JS files to the configured output directories
735
+ * - returns a `ModelSpec` that guides the rest of the `sde` build process
736
+ */
813
737
  function configProcessor(options) {
814
- return (buildContext) => {
815
- return processModelConfig(buildContext, options);
816
- };
738
+ return (buildContext) => {
739
+ return processModelConfig(buildContext, options);
740
+ };
817
741
  }
818
742
  async function processModelConfig(buildContext, options) {
819
- const t0 = performance.now();
820
- if (!existsSync(options.config)) {
821
- throw new Error(`The provided config dir '${options.config}' does not exist`);
822
- }
823
- let outModelSpecsDir;
824
- if (options.out) {
825
- if (typeof options.out === "string") {
826
- outModelSpecsDir = joinPath2(options.out, "src", "model", "generated");
827
- } else {
828
- outModelSpecsDir = options.out.modelSpecsDir;
829
- }
830
- }
831
- let outConfigSpecsDir;
832
- if (options.out) {
833
- if (typeof options.out === "string") {
834
- outConfigSpecsDir = joinPath2(options.out, "src", "config", "generated");
835
- } else {
836
- outConfigSpecsDir = options.out.configSpecsDir;
837
- }
838
- }
839
- let outStringsDir;
840
- if (options.out) {
841
- if (typeof options.out === "string") {
842
- outStringsDir = joinPath2(options.out, "strings");
843
- } else {
844
- outStringsDir = options.out.stringsDir;
845
- }
846
- }
847
- const context = createConfigContext(buildContext, options.config);
848
- const modelOptions = context.modelOptions;
849
- context.log("info", "Generating files...");
850
- const configSpecs = generateConfigSpecs(context);
851
- if (outConfigSpecsDir) {
852
- context.log("verbose", " Writing config specs");
853
- writeConfigSpecs(context, configSpecs, outConfigSpecsDir);
854
- writeSpecTypes(context, outConfigSpecsDir);
855
- }
856
- if (outModelSpecsDir) {
857
- context.log("verbose", " Writing model specs");
858
- writeModelSpec(context, outModelSpecsDir);
859
- }
860
- if (outStringsDir) {
861
- context.log("verbose", " Writing strings");
862
- context.writeStringsFiles(outStringsDir);
863
- }
864
- const t1 = performance.now();
865
- const elapsed = ((t1 - t0) / 1e3).toFixed(1);
866
- context.log("info", `Done generating files (${elapsed}s)`);
867
- return {
868
- // Include any additional properties that cannot be derived from the config files
869
- ...options.spec,
870
- inputs: context.getOrderedInputs(),
871
- outputs: context.getOrderedOutputs(),
872
- datFiles: modelOptions.datFiles,
873
- bundleListing: modelOptions.bundleListing,
874
- customConstants: modelOptions.customConstants,
875
- customLookups: modelOptions.customLookups,
876
- customOutputs: modelOptions.customOutputs
877
- };
743
+ const t0 = performance.now();
744
+ if (!existsSync(options.config)) throw new Error(`The provided config dir '${options.config}' does not exist`);
745
+ let outModelSpecsDir;
746
+ if (options.out) {
747
+ if (typeof options.out === "string") outModelSpecsDir = join(options.out, "src", "model", "generated");
748
+ else outModelSpecsDir = options.out.modelSpecsDir;
749
+ }
750
+ let outConfigSpecsDir;
751
+ if (options.out) {
752
+ if (typeof options.out === "string") outConfigSpecsDir = join(options.out, "src", "config", "generated");
753
+ else outConfigSpecsDir = options.out.configSpecsDir;
754
+ }
755
+ let outStringsDir;
756
+ if (options.out) {
757
+ if (typeof options.out === "string") outStringsDir = join(options.out, "strings");
758
+ else outStringsDir = options.out.stringsDir;
759
+ }
760
+ const context = createConfigContext(buildContext, options.config);
761
+ const modelOptions = context.modelOptions;
762
+ context.log("info", "Generating files...");
763
+ const configSpecs = generateConfigSpecs(context);
764
+ if (outConfigSpecsDir) {
765
+ context.log("verbose", " Writing config specs");
766
+ writeConfigSpecs(context, configSpecs, outConfigSpecsDir);
767
+ writeSpecTypes(context, outConfigSpecsDir);
768
+ }
769
+ if (outModelSpecsDir) {
770
+ context.log("verbose", " Writing model specs");
771
+ writeModelSpec(context, outModelSpecsDir);
772
+ }
773
+ if (outStringsDir) {
774
+ context.log("verbose", " Writing strings");
775
+ context.writeStringsFiles(outStringsDir);
776
+ }
777
+ const elapsed = ((performance.now() - t0) / 1e3).toFixed(1);
778
+ context.log("info", `Done generating files (${elapsed}s)`);
779
+ return {
780
+ ...options.spec,
781
+ inputs: context.getOrderedInputs(),
782
+ outputs: context.getOrderedOutputs(),
783
+ datFiles: modelOptions.datFiles,
784
+ bundleListing: modelOptions.bundleListing,
785
+ customConstants: modelOptions.customConstants,
786
+ customLookups: modelOptions.customLookups,
787
+ customOutputs: modelOptions.customOutputs
788
+ };
878
789
  }
879
- export {
880
- configProcessor
881
- };
790
+ //#endregion
791
+ export { configProcessor };
792
+
882
793
  //# sourceMappingURL=index.js.map