@sdeverywhere/plugin-config 0.2.13 → 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.d.ts +39 -38
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +737 -826
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/dist/index.cjs +0 -922
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -57
package/dist/index.js
CHANGED
|
@@ -1,882 +1,793 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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> ");
|
|
78
|
+
return s;
|
|
112
79
|
}
|
|
113
80
|
function htmlSubscriptAndSuperscriptToUtf8(s) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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, " ");
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/context.ts
|
|
178
175
|
var ConfigContext = class {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
301
|
+
return join(configDir, `${name}.${ext}`);
|
|
320
302
|
}
|
|
321
303
|
function readCsvFile(path) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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("&", "&")} > ${htmlToUtf8(menuTitle || title).replace("&", "&")}`;
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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("&", "&");
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
767
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
738
|
+
return (buildContext) => {
|
|
739
|
+
return processModelConfig(buildContext, options);
|
|
740
|
+
};
|
|
817
741
|
}
|
|
818
742
|
async function processModelConfig(buildContext, options) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
790
|
+
//#endregion
|
|
791
|
+
export { configProcessor };
|
|
792
|
+
|
|
882
793
|
//# sourceMappingURL=index.js.map
|