gulp-mu-gulp-api 0.3.5 → 0.3.8
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/README.md +64 -9
- package/package.json +1 -1
- package/src/i18x-engine.mjs +697 -0
- package/src/i18x-hyphen.mjs +166 -0
- package/src/i18x.mjs +117 -78
- package/src/index.mjs +173 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// ===========================================
|
|
2
|
+
// i18x-hyphen.mjs — TeX-style hyphenation (µLib / i18xe-compatible)
|
|
3
|
+
// © 2026 Meinolf Amekudzi
|
|
4
|
+
// ===========================================
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const HYPHEN_CHAR = '\xAD'; // U+00AD soft hyphen
|
|
10
|
+
const hyphenData = {};
|
|
11
|
+
const hyphenCache = {};
|
|
12
|
+
|
|
13
|
+
/** Directory holding hyphen rule files: env override or `<cwd>/i18x/prod`. */
|
|
14
|
+
function _HyphenDir() {
|
|
15
|
+
return process.env.MICROGULP_I18X_HYPHEN_DIR || join(process.cwd(), 'i18x', 'prod');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads hyphen rules for a localization id from `<dir>/<lid>.hyphen.json`.
|
|
20
|
+
* Missing or broken files yield a no-op catalog (`{ exist: false }`).
|
|
21
|
+
* @param {string} _lid
|
|
22
|
+
* @returns {object}
|
|
23
|
+
*/
|
|
24
|
+
export function LoadHyphenData(_lid) {
|
|
25
|
+
if (hyphenData[_lid]) return hyphenData[_lid];
|
|
26
|
+
let data = { exist: false };
|
|
27
|
+
try {
|
|
28
|
+
let filePath = join(_HyphenDir(), _lid + '.hyphen.json');
|
|
29
|
+
if (existsSync(filePath)) data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
// Malformed hyphen data must not break the build — fall back to no-op.
|
|
32
|
+
}
|
|
33
|
+
if (!data || typeof data !== 'object') data = { exist: false };
|
|
34
|
+
hyphenData[_lid] = data;
|
|
35
|
+
hyphenCache[_lid] = {};
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hyphenates a single word; returns the original when no rules exist.
|
|
41
|
+
* @param {string} _word
|
|
42
|
+
* @param {string} _lid
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function WordHyphenation(_word, _lid) {
|
|
46
|
+
let word = String(_word);
|
|
47
|
+
let data = LoadHyphenData(_lid);
|
|
48
|
+
if (!data.exist) return word;
|
|
49
|
+
if (!hyphenCache[_lid]) hyphenCache[_lid] = {};
|
|
50
|
+
if (hyphenCache[_lid][word]) return hyphenCache[_lid][word];
|
|
51
|
+
|
|
52
|
+
let lower = word.toLowerCase();
|
|
53
|
+
let ret = word;
|
|
54
|
+
|
|
55
|
+
if (!data.exceptions?.[lower]) {
|
|
56
|
+
let sylbs = data.sylbs;
|
|
57
|
+
let minSylbs = data.minSylbs;
|
|
58
|
+
let maxSylbs = data.maxSylbs;
|
|
59
|
+
let l = word.length + 1;
|
|
60
|
+
let myword = '.' + lower;
|
|
61
|
+
let vals = new Array(l + 1).fill(0);
|
|
62
|
+
for (let i = 0; i < l; i++) {
|
|
63
|
+
let sylb = '';
|
|
64
|
+
for (let j = 0; (i + j) < l; j++) {
|
|
65
|
+
sylb += myword.charAt(i + j);
|
|
66
|
+
if (j >= minSylbs && j <= maxSylbs) {
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(sylbs, sylb)) {
|
|
68
|
+
let sylbVals = sylbs[sylb];
|
|
69
|
+
let sl = sylb.length;
|
|
70
|
+
for (let k = -1; k <= sl; k++) vals[i + k] = Math.max(sylbVals[k + 1], vals[i + k]);
|
|
71
|
+
}
|
|
72
|
+
} else if (j > maxSylbs) break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
ret = '';
|
|
76
|
+
let k = l - 3;
|
|
77
|
+
let m = 2;
|
|
78
|
+
l--;
|
|
79
|
+
for (let i = 0; i < l; i++) {
|
|
80
|
+
if ((vals[i] % 2) === 1 && i > m && i < k) ret += HYPHEN_CHAR;
|
|
81
|
+
ret += word.charAt(i);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
let w = data.exceptions[lower];
|
|
85
|
+
let j = 0;
|
|
86
|
+
for (let i = 0; i < w.length; i++) {
|
|
87
|
+
if (w.charAt(i) === HYPHEN_CHAR) {
|
|
88
|
+
ret += word.charAt(j++);
|
|
89
|
+
} else {
|
|
90
|
+
ret += w.charAt(i);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
hyphenCache[_lid][word] = ret;
|
|
96
|
+
return ret;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Hyphenates every word in a text; preserves HTML/XML tags.
|
|
101
|
+
* @param {string} _text
|
|
102
|
+
* @param {string} _lid
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
export function Hyphenation(_text, _lid) {
|
|
106
|
+
let txts = String(_text);
|
|
107
|
+
let ret = [];
|
|
108
|
+
|
|
109
|
+
if (txts.indexOf('<') >= 0 && txts.indexOf('>') >= 0) {
|
|
110
|
+
let tags = {};
|
|
111
|
+
let matches = txts.match(/(<[^>]+>)/g);
|
|
112
|
+
if (matches != null) {
|
|
113
|
+
for (let i = 0; i < matches.length; i++) {
|
|
114
|
+
tags[matches[i]] = '\x7F' + i + '\x7F';
|
|
115
|
+
txts = txts.replace(matches[i], tags[matches[i]]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let words = txts.split(' ');
|
|
119
|
+
for (let i = 0; i < words.length; i++) {
|
|
120
|
+
let t = words[i].trim();
|
|
121
|
+
ret.push(t ? WordHyphenation(t, _lid) : t);
|
|
122
|
+
}
|
|
123
|
+
txts = ret.join(' ');
|
|
124
|
+
if (matches != null) {
|
|
125
|
+
for (let i = 0; i < matches.length; i++) txts = txts.replace(tags[matches[i]], matches[i]);
|
|
126
|
+
}
|
|
127
|
+
return txts;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let words = txts.split(' ');
|
|
131
|
+
for (let i = 0; i < words.length; i++) {
|
|
132
|
+
let t = words[i].trim();
|
|
133
|
+
ret.push(t ? WordHyphenation(t, _lid) : t);
|
|
134
|
+
}
|
|
135
|
+
return ret.join(' ');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @param {string} _target @param {string} _name @param {Function} _value */
|
|
139
|
+
function _DefinePrototype(_target, _name, _value) {
|
|
140
|
+
if (typeof _target[_name] !== 'function') {
|
|
141
|
+
Object.defineProperty(_target, _name, { value: _value, writable: true, configurable: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Installs µLib- and i18xe-compatible String hyphenation prototypes.
|
|
147
|
+
* @param {function(): string} [_resolveLid] defaults to empty string when omitted
|
|
148
|
+
*/
|
|
149
|
+
export function InstallHyphenPrototypes(_resolveLid) {
|
|
150
|
+
function _ResolveLid(_lid) {
|
|
151
|
+
if (_lid) return _lid;
|
|
152
|
+
try {
|
|
153
|
+
return _resolveLid?.() ?? '';
|
|
154
|
+
} catch {
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
let wordFn = function (_lid = '') { return WordHyphenation(String(this), _ResolveLid(_lid)); };
|
|
159
|
+
let textFn = function (_lid = '') { return Hyphenation(String(this), _ResolveLid(_lid)); };
|
|
160
|
+
_DefinePrototype(String.prototype, 'WordHyphenation', wordFn);
|
|
161
|
+
_DefinePrototype(String.prototype, 'Hyphenation', textFn);
|
|
162
|
+
_DefinePrototype(String.prototype, 'wordHyphenation', wordFn);
|
|
163
|
+
_DefinePrototype(String.prototype, 'hyphenation', textFn);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default { HYPHEN_CHAR, LoadHyphenData, WordHyphenation, Hyphenation, InstallHyphenPrototypes };
|
package/src/i18x.mjs
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
|
|
32
32
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
33
33
|
import { join } from 'node:path';
|
|
34
|
+
import { Trans as EngineTrans, FormatValue as EngineFormatValue, RegisterFormat } from './i18x-engine.mjs';
|
|
35
|
+
import { InstallHyphenPrototypes as _InstallHyphenPrototypes } from './i18x-hyphen.mjs';
|
|
34
36
|
|
|
35
37
|
const DEFAULT_LID = 'en-US';
|
|
36
38
|
|
|
@@ -51,6 +53,7 @@ const LIDS_MAIN_CULTURES = {
|
|
|
51
53
|
|
|
52
54
|
let dictionaryCache = new Map();
|
|
53
55
|
let formatDefinitionCache = new Map();
|
|
56
|
+
let formatRegistryCache = new Map();
|
|
54
57
|
let availableLids = null;
|
|
55
58
|
let activeLid = null;
|
|
56
59
|
const timeZoneFormatters = {};
|
|
@@ -90,6 +93,22 @@ export function DateInTimeZone(_date, _timeZone) {
|
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Offset in minutes (UTC minus wall-clock, same sign as Date.getTimezoneOffset).
|
|
98
|
+
* @param {Date|number} _date
|
|
99
|
+
* @param {string} [_timeZone]
|
|
100
|
+
* @returns {number}
|
|
101
|
+
*/
|
|
102
|
+
export function TimeZoneOffset(_date, _timeZone) {
|
|
103
|
+
let date = _date instanceof Date ? _date : new Date(Number(_date) || 0);
|
|
104
|
+
let shifted = DateInTimeZone(date, _timeZone);
|
|
105
|
+
let wallAsUTC = Date.UTC(
|
|
106
|
+
shifted.getFullYear(), shifted.getMonth(), shifted.getDate(),
|
|
107
|
+
shifted.getHours(), shifted.getMinutes(), shifted.getSeconds(), shifted.getMilliseconds(),
|
|
108
|
+
);
|
|
109
|
+
return Math.round((date.getTime() - wallAsUTC) / 60000);
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
/** Normalizes casing: language lower, 2-letter region upper, 4-letter script Title. */
|
|
94
113
|
function _NormalizeLid(_lid) {
|
|
95
114
|
if (!_lid) return '';
|
|
@@ -177,6 +196,7 @@ export function GetLid() {
|
|
|
177
196
|
export function SetLid(_lid) {
|
|
178
197
|
activeLid = _lid ? _BestAvailableLid(_lid, _AvailableLids()) : null;
|
|
179
198
|
formatDefinitionCache.clear();
|
|
199
|
+
formatRegistryCache.clear();
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
/** Loads and caches `<dir>/<lid>.json`; a missing/broken file yields source text. */
|
|
@@ -215,101 +235,58 @@ function _LoadFormatDefinitions(_lid) {
|
|
|
215
235
|
return formats;
|
|
216
236
|
}
|
|
217
237
|
|
|
218
|
-
/**
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
let
|
|
222
|
-
let
|
|
223
|
-
let
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
/** Builds (and caches) the name→parsed-definition format registry for a language. */
|
|
239
|
+
function _FormatRegistry(_lid) {
|
|
240
|
+
if (formatRegistryCache.has(_lid)) return formatRegistryCache.get(_lid);
|
|
241
|
+
let registry = Object.create(null);
|
|
242
|
+
let definitions = _LoadFormatDefinitions(_lid);
|
|
243
|
+
for (let xml of Object.values(definitions)) RegisterFormat(registry, xml, false);
|
|
244
|
+
formatRegistryCache.set(_lid, registry);
|
|
245
|
+
return registry;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Assembles the engine render context (language, timezone, format registry). */
|
|
249
|
+
function _RenderContext(_lid, _timeZone) {
|
|
250
|
+
let lid = _lid ?? GetLid();
|
|
251
|
+
return { lid, timeZone: _timeZone ?? GetTimeZone(), formats: _FormatRegistry(lid) };
|
|
227
252
|
}
|
|
228
253
|
|
|
229
254
|
/**
|
|
230
|
-
* Formats a numeric value with a named i18xe format definition from the
|
|
231
|
-
* project's i18x/gulp dictionaries (synced from i18xe prod).
|
|
255
|
+
* Formats a numeric/date value with a named i18xe format definition from the
|
|
256
|
+
* project's i18x/gulp dictionaries (synced from i18xe prod). Delegates to the
|
|
257
|
+
* eval-free i18x engine, so the full i18x notation (fill/padding, `if`
|
|
258
|
+
* conditions, nested formats, expressions, enumerations, date parts) is honored.
|
|
259
|
+
* Date/time formats interpret the value as milliseconds since the Unix epoch.
|
|
232
260
|
* @param {number|string} _value
|
|
233
|
-
* @param {string} _formatName e.g. 'int', 'floatFix2', 'byteSize'
|
|
261
|
+
* @param {string} _formatName e.g. 'int', 'floatFix2', 'byteSize', 'stdDateTime'
|
|
234
262
|
* @param {string} [_lid]
|
|
263
|
+
* @param {string} [_timeZone] IANA timezone id (defaults to GetTimeZone())
|
|
235
264
|
* @returns {string}
|
|
236
265
|
*/
|
|
237
|
-
export function FormatValue(_value, _formatName, _lid) {
|
|
238
|
-
let
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (_formatName === 'stdTime') {
|
|
242
|
-
let date = new Date(Number(_value) || 0);
|
|
243
|
-
try {
|
|
244
|
-
return new Intl.DateTimeFormat(lid, { hour: '2-digit', minute: '2-digit', timeZone: tz }).format(date);
|
|
245
|
-
} catch {
|
|
246
|
-
return date.toISOString();
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (_formatName === 'stdDateTime' || _formatName === 'fullDateTime') {
|
|
250
|
-
let date = new Date(Number(_value) || 0);
|
|
251
|
-
try {
|
|
252
|
-
return new Intl.DateTimeFormat(lid, {
|
|
253
|
-
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
254
|
-
hour: '2-digit', minute: '2-digit', timeZone: tz,
|
|
255
|
-
}).format(date);
|
|
256
|
-
} catch {
|
|
257
|
-
return date.toISOString();
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (_formatName === 'byteSize') {
|
|
261
|
-
let bytes = Number(_value);
|
|
262
|
-
if (!Number.isFinite(bytes)) return String(_value);
|
|
263
|
-
if (bytes >= 1073741824) return FormatValue(bytes / 1073741824, 'floatFix2', lid) + ' GB';
|
|
264
|
-
if (bytes >= 1048576) return FormatValue(bytes / 1048576, 'floatFix2', lid) + ' MB';
|
|
265
|
-
if (bytes >= 1024) return FormatValue(bytes / 1024, 'floatFix2', lid) + ' KB';
|
|
266
|
-
return FormatValue(bytes, 'int', lid) + ' Bytes';
|
|
267
|
-
}
|
|
268
|
-
let number = Number(_value);
|
|
269
|
-
if (!Number.isFinite(number)) return String(_value);
|
|
270
|
-
let definition = _LoadFormatDefinitions(lid)[_formatName];
|
|
271
|
-
if (!definition) return String(_value);
|
|
272
|
-
let groupSeparator = definition.match(/<xa if="1000\|1000\+">([^<]*)<\/xa>/)?.[1] ?? ',';
|
|
273
|
-
let decimalSeparator = definition.match(/if="0~">([^<]*)<m0\/>/)?.[1]
|
|
274
|
-
?? definition.match(/<\/xa>([^<]*)<f0\/>/)?.[1] ?? '.';
|
|
275
|
-
let fractionDigits = _formatName === 'floatFix1' ? 1
|
|
276
|
-
: (_formatName.startsWith('floatFix') ? 2 : 0);
|
|
277
|
-
if (_formatName === 'int') number = Math.round(number);
|
|
278
|
-
let negative = number < 0;
|
|
279
|
-
let absolute = Math.abs(number);
|
|
280
|
-
let integerPart = String(Math.trunc(fractionDigits ? absolute : Math.round(absolute)));
|
|
281
|
-
let grouped = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator);
|
|
282
|
-
let fraction = fractionDigits ? decimalSeparator + absolute.toFixed(fractionDigits).split('.')[1] : '';
|
|
283
|
-
return (negative ? '-' : '') + grouped + fraction;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Substitutes `<name/>` placeholders; honors `format="…"` via FormatValue. */
|
|
287
|
-
function _ApplyPlaceholders(_text, _values) {
|
|
288
|
-
let text = String(_text);
|
|
289
|
-
let lid = GetLid();
|
|
290
|
-
for (let key of Object.keys(_values ?? {})) {
|
|
291
|
-
let pattern = new RegExp('<' + key + '(?:\\s+format="([^"]*)")?\\s*/>', 'g');
|
|
292
|
-
text = text.replace(pattern, (_match, _formatName) =>
|
|
293
|
-
_formatName ? FormatValue(_values[key], _formatName, lid) : String(_values[key]));
|
|
294
|
-
}
|
|
295
|
-
return text.replace(/<[^>]*>/g, '');
|
|
266
|
+
export function FormatValue(_value, _formatName, _lid, _timeZone) {
|
|
267
|
+
let context = _RenderContext(_lid, _timeZone);
|
|
268
|
+
if (!Object.prototype.hasOwnProperty.call(context.formats, _formatName)) return String(_value);
|
|
269
|
+
return EngineFormatValue(_value, _formatName, context);
|
|
296
270
|
}
|
|
297
271
|
|
|
298
272
|
/**
|
|
299
273
|
* Translates a source phrase (including its context tag) into the active
|
|
300
|
-
* language and
|
|
301
|
-
*
|
|
274
|
+
* language and renders it through the i18x engine (placeholders, `format="…"`,
|
|
275
|
+
* `if` conditions, …). Unknown phrases fall back to the en-US source text.
|
|
302
276
|
* @param {string} _text source phrase, e.g. 'Build ready<context="task log"/>'
|
|
303
277
|
* @param {object} [_values] placeholder values for `<name/>` tags
|
|
278
|
+
* @param {string} [_timeZone] IANA timezone id (defaults to GetTimeZone())
|
|
304
279
|
* @returns {string}
|
|
305
280
|
*/
|
|
306
|
-
export function Translate(_text, _values) {
|
|
281
|
+
export function Translate(_text, _values, _timeZone) {
|
|
307
282
|
if (_text == null) return '';
|
|
308
283
|
let source = String(_text);
|
|
309
284
|
let lid = GetLid();
|
|
310
285
|
let dictionary = lid === DEFAULT_LID ? null : _LoadDictionary(lid);
|
|
311
286
|
let translated = (dictionary && Object.prototype.hasOwnProperty.call(dictionary, source)) ? dictionary[source] : source;
|
|
312
|
-
|
|
287
|
+
// The context tag is part of the dictionary key only; strip it before rendering.
|
|
288
|
+
translated = translated.replace(/<context=[^>]*>/g, '');
|
|
289
|
+
return EngineTrans(translated, _values ?? {}, _RenderContext(lid, _timeZone));
|
|
313
290
|
}
|
|
314
291
|
|
|
315
292
|
/**
|
|
@@ -351,7 +328,7 @@ export function LogError(_text, _values) {
|
|
|
351
328
|
export function InstallStringExtensions() {
|
|
352
329
|
if (typeof String.prototype.I18xTrans !== 'function') {
|
|
353
330
|
Object.defineProperty(String.prototype, 'I18xTrans', {
|
|
354
|
-
value: function (_values) { return Translate(String(this), _values); },
|
|
331
|
+
value: function (_values, _timeZone) { return Translate(String(this), _values, _timeZone); },
|
|
355
332
|
writable: true, configurable: true,
|
|
356
333
|
});
|
|
357
334
|
}
|
|
@@ -373,11 +350,73 @@ export function InstallStringExtensions() {
|
|
|
373
350
|
writable: true, configurable: true,
|
|
374
351
|
});
|
|
375
352
|
}
|
|
353
|
+
InstallFormatPrototypes();
|
|
354
|
+
InstallHyphenPrototypes();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Installs String hyphenation prototypes (µLib + i18xe aliases). Called from
|
|
359
|
+
* InstallStringExtensions (auto-run on import); also exported for explicit control.
|
|
360
|
+
*/
|
|
361
|
+
export function InstallHyphenPrototypes() {
|
|
362
|
+
_InstallHyphenPrototypes(GetLid);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// .NET ticks are 100-nanosecond intervals since 0001-01-01; this offset is the
|
|
366
|
+
// tick count at the Unix epoch (1970-01-01), so ms = (ticks - offset) / 10000.
|
|
367
|
+
const TICKS_EPOCH_OFFSET = 621355968000000000;
|
|
368
|
+
const TICKS_PER_MILLISECOND = 10000;
|
|
369
|
+
|
|
370
|
+
/** @param {string} _name @param {Function} _value install a prototype method only if absent (µLib wins) */
|
|
371
|
+
function _DefinePrototype(_target, _name, _value) {
|
|
372
|
+
if (typeof _target[_name] !== 'function') {
|
|
373
|
+
Object.defineProperty(_target, _name, { value: _value, writable: true, configurable: true });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* µLib-compatible Number/Date/String `Format` prototypes for gulpfiles that
|
|
379
|
+
* prefer `(1536).Format('byteSize')` or `new Date().Format('stdDateTime')`
|
|
380
|
+
* over the FormatValue() helper. Date/time formats interpret their value as
|
|
381
|
+
* milliseconds since the Unix epoch (µGulp convention), except FormatTicks
|
|
382
|
+
* which takes .NET ticks. Never overwrites an implementation already provided
|
|
383
|
+
* by µLib (µLib wins when both are loaded). Called from InstallStringExtensions
|
|
384
|
+
* (auto-run on import); also exported for explicit control.
|
|
385
|
+
*/
|
|
386
|
+
export function InstallFormatPrototypes() {
|
|
387
|
+
// Number.prototype.Format — number formats use the value directly, date
|
|
388
|
+
// formats interpret it as milliseconds since the Unix epoch.
|
|
389
|
+
_DefinePrototype(Number.prototype, 'Format', function (_format, _lid, _timeZone) {
|
|
390
|
+
return FormatValue(this.valueOf(), _format, _lid, _timeZone);
|
|
391
|
+
});
|
|
392
|
+
// Number.prototype.FormatTimestamp — an explicit Unix millisecond timestamp.
|
|
393
|
+
_DefinePrototype(Number.prototype, 'FormatTimestamp', function (_format, _lid, _timeZone) {
|
|
394
|
+
return FormatValue(this.valueOf(), _format, _lid, _timeZone);
|
|
395
|
+
});
|
|
396
|
+
// Number.prototype.FormatTicks — a .NET tick count, converted to ms first.
|
|
397
|
+
_DefinePrototype(Number.prototype, 'FormatTicks', function (_format, _lid, _timeZone) {
|
|
398
|
+
let ms = (this.valueOf() - TICKS_EPOCH_OFFSET) / TICKS_PER_MILLISECOND;
|
|
399
|
+
return FormatValue(ms, _format, _lid, _timeZone);
|
|
400
|
+
});
|
|
401
|
+
// i18xe-sync lowercase aliases.
|
|
402
|
+
_DefinePrototype(Number.prototype, 'format', Number.prototype.Format);
|
|
403
|
+
_DefinePrototype(Number.prototype, 'formatTimestamp', Number.prototype.FormatTimestamp);
|
|
404
|
+
_DefinePrototype(Number.prototype, 'formatTicks', Number.prototype.FormatTicks);
|
|
405
|
+
// Date.prototype.Format — formats via the date's millisecond timestamp.
|
|
406
|
+
_DefinePrototype(Date.prototype, 'Format', function (_format, _lid, _timeZone) {
|
|
407
|
+
return FormatValue(this.getTime(), _format, _lid, _timeZone);
|
|
408
|
+
});
|
|
409
|
+
_DefinePrototype(Date.prototype, 'format', Date.prototype.Format);
|
|
410
|
+
// String.prototype.Format — µLib parity: a string has no numeric format.
|
|
411
|
+
_DefinePrototype(String.prototype, 'Format', function () {
|
|
412
|
+
return this.valueOf();
|
|
413
|
+
});
|
|
376
414
|
}
|
|
377
415
|
|
|
378
416
|
InstallStringExtensions();
|
|
379
417
|
|
|
380
418
|
export default {
|
|
381
|
-
GetLid, SetLid, GetTimeZone, DateInTimeZone,
|
|
382
|
-
Translate, FormatValue, Log, Warn, LogError,
|
|
419
|
+
GetLid, SetLid, GetTimeZone, DateInTimeZone, TimeZoneOffset,
|
|
420
|
+
Translate, FormatValue, Log, Warn, LogError,
|
|
421
|
+
InstallStringExtensions, InstallFormatPrototypes, InstallHyphenPrototypes,
|
|
383
422
|
};
|
package/src/index.mjs
CHANGED
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
|
|
31
31
|
// Localized console output (i18x). Re-exported so tasks import everything
|
|
32
32
|
// from one place: `import { Log, Warn, ReportProgress } from 'gulp-mu-gulp-api'`.
|
|
33
|
-
export { Log, Warn, LogError, Translate, FormatValue, GetLid, SetLid, GetTimeZone, DateInTimeZone, InstallStringExtensions } from './i18x.mjs';
|
|
33
|
+
export { Log, Warn, LogError, Translate, FormatValue, GetLid, SetLid, GetTimeZone, DateInTimeZone, TimeZoneOffset, InstallStringExtensions, InstallFormatPrototypes, InstallHyphenPrototypes } from './i18x.mjs';
|
|
34
|
+
export { WordHyphenation, Hyphenation, LoadHyphenData } from './i18x-hyphen.mjs';
|
|
34
35
|
import * as I18x from './i18x.mjs';
|
|
35
36
|
|
|
36
37
|
let uiRequestCounter = 0;
|
|
@@ -207,6 +208,77 @@ export function LogBuildDebugReport(_report, _options = {}) {
|
|
|
207
208
|
});
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Sends a highlighted callout box (info/success/warning/error) to the dashboard
|
|
213
|
+
* log pane. The payload is JSON: `{ variant, title?, message }`.
|
|
214
|
+
* CLI fallback: a bracketed line on stdout.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} _spec `{ variant?: 'info'|'success'|'warning'|'error', title?, message }`
|
|
217
|
+
*/
|
|
218
|
+
export function LogCallout(_spec) {
|
|
219
|
+
let payload = _NormalizeCalloutSpec(_spec);
|
|
220
|
+
if (_SendStructuredLog('callout', payload)) return;
|
|
221
|
+
let head = payload.variant.toUpperCase() + (payload.title ? ': ' + payload.title : '');
|
|
222
|
+
console.log('[' + head + '] ' + payload.message);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Sends a key/value list (metrics, build summary, environment) to the dashboard
|
|
227
|
+
* log pane. The payload is JSON: `{ title?, items: {key, value}[] }`.
|
|
228
|
+
* CLI fallback: aligned `key: value` lines on stdout.
|
|
229
|
+
*
|
|
230
|
+
* @param {object} _spec `{ title?, items: Array<{key, value}> | Record<string, any> }`
|
|
231
|
+
*/
|
|
232
|
+
export function LogKeyValue(_spec) {
|
|
233
|
+
let payload = _NormalizeKeyValueSpec(_spec);
|
|
234
|
+
if (_SendStructuredLog('key-value', payload)) return;
|
|
235
|
+
if (payload.title) console.log(payload.title);
|
|
236
|
+
let width = payload.items.reduce((_max, _item) => Math.max(_max, _item.key.length), 0);
|
|
237
|
+
for (let item of payload.items) console.log(' ' + item.key.padEnd(width) + ' ' + item.value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Sends a row of status badges/chips to the dashboard log pane. The payload is
|
|
242
|
+
* JSON: `{ title?, items: {label, variant?}[] }`.
|
|
243
|
+
* CLI fallback: bracketed labels on one stdout line.
|
|
244
|
+
*
|
|
245
|
+
* @param {object} _spec `{ title?, items: Array<string|{label, variant?}> }`
|
|
246
|
+
*/
|
|
247
|
+
export function LogBadges(_spec) {
|
|
248
|
+
let payload = _NormalizeBadgesSpec(_spec);
|
|
249
|
+
if (_SendStructuredLog('badges', payload)) return;
|
|
250
|
+
let chips = payload.items.map((_item) => '[' + _item.label + ']').join(' ');
|
|
251
|
+
console.log((payload.title ? payload.title + ' ' : '') + chips);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Sends a code block (monospace, preserved whitespace) to the dashboard log
|
|
256
|
+
* pane. The payload is JSON: `{ title?, language?, code }`.
|
|
257
|
+
* CLI fallback: the code printed verbatim on stdout.
|
|
258
|
+
*
|
|
259
|
+
* @param {object} _spec `{ title?, language?, code }`
|
|
260
|
+
*/
|
|
261
|
+
export function LogCode(_spec) {
|
|
262
|
+
let payload = _NormalizeCodeSpec(_spec);
|
|
263
|
+
if (_SendStructuredLog('code', payload)) return;
|
|
264
|
+
if (payload.title) console.log(payload.title + (payload.language ? ' (' + payload.language + ')' : ''));
|
|
265
|
+
console.log(payload.code);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Sends a bar chart to the dashboard log pane, rendered as inline SVG (no
|
|
270
|
+
* scripts — Content-Security-Policy safe). The payload is JSON:
|
|
271
|
+
* `{ title?, type?: 'bar', series: {label, value, color?}[], max?, unit? }`.
|
|
272
|
+
* CLI fallback: an ASCII bar chart on stdout.
|
|
273
|
+
*
|
|
274
|
+
* @param {object} _spec `{ title?, type?, series: Array<{label, value, color?}>, max?, unit? }`
|
|
275
|
+
*/
|
|
276
|
+
export function LogChart(_spec) {
|
|
277
|
+
let payload = _NormalizeChartSpec(_spec);
|
|
278
|
+
if (_SendStructuredLog('chart', payload)) return;
|
|
279
|
+
for (let line of _AsciiChart(payload)) console.log(line);
|
|
280
|
+
}
|
|
281
|
+
|
|
210
282
|
// -------------------------------------------------
|
|
211
283
|
// sound & speech
|
|
212
284
|
// -------------------------------------------------
|
|
@@ -553,6 +625,98 @@ function _AsciiGallery(_payload) {
|
|
|
553
625
|
return lines.join('\n');
|
|
554
626
|
}
|
|
555
627
|
|
|
628
|
+
const CALLOUT_VARIANTS = new Set(['info', 'success', 'warning', 'error']);
|
|
629
|
+
|
|
630
|
+
function _NormalizeCalloutSpec(_spec) {
|
|
631
|
+
let variant = String(_spec?.variant ?? 'info').toLowerCase();
|
|
632
|
+
if (!CALLOUT_VARIANTS.has(variant)) variant = 'info';
|
|
633
|
+
return {
|
|
634
|
+
variant,
|
|
635
|
+
title: _spec?.title != null ? String(_spec.title) : '',
|
|
636
|
+
message: String(_spec?.message ?? _spec?.text ?? ''),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function _NormalizeKeyValueSpec(_spec) {
|
|
641
|
+
let rawItems = _spec?.items ?? _spec ?? [];
|
|
642
|
+
let items = [];
|
|
643
|
+
if (Array.isArray(rawItems)) {
|
|
644
|
+
for (let item of rawItems) {
|
|
645
|
+
if (item == null) continue;
|
|
646
|
+
if (typeof item === 'object' && !Array.isArray(item)) {
|
|
647
|
+
items.push({ key: String(item.key ?? ''), value: String(item.value ?? '') });
|
|
648
|
+
} else if (Array.isArray(item)) {
|
|
649
|
+
items.push({ key: String(item[0] ?? ''), value: String(item[1] ?? '') });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} else if (rawItems && typeof rawItems === 'object') {
|
|
653
|
+
for (let [key, value] of Object.entries(rawItems)) {
|
|
654
|
+
if (key === 'title') continue;
|
|
655
|
+
items.push({ key: String(key), value: String(value) });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return { title: _spec?.title != null ? String(_spec.title) : '', items };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const BADGE_VARIANTS = new Set(['neutral', 'info', 'success', 'warning', 'error']);
|
|
662
|
+
|
|
663
|
+
function _NormalizeBadgesSpec(_spec) {
|
|
664
|
+
let items = (_spec?.items ?? []).map((_item) => {
|
|
665
|
+
if (_item == null) return null;
|
|
666
|
+
if (typeof _item === 'object') {
|
|
667
|
+
let variant = String(_item.variant ?? 'neutral').toLowerCase();
|
|
668
|
+
return { label: String(_item.label ?? _item.value ?? ''), variant: BADGE_VARIANTS.has(variant) ? variant : 'neutral' };
|
|
669
|
+
}
|
|
670
|
+
return { label: String(_item), variant: 'neutral' };
|
|
671
|
+
}).filter((_item) => _item && _item.label);
|
|
672
|
+
return { title: _spec?.title != null ? String(_spec.title) : '', items };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function _NormalizeCodeSpec(_spec) {
|
|
676
|
+
return {
|
|
677
|
+
title: _spec?.title != null ? String(_spec.title) : '',
|
|
678
|
+
language: _spec?.language != null ? String(_spec.language) : '',
|
|
679
|
+
code: String(_spec?.code ?? _spec?.text ?? ''),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function _NormalizeChartSpec(_spec) {
|
|
684
|
+
let series = (_spec?.series ?? _spec?.data ?? []).map((_entry) => {
|
|
685
|
+
if (_entry == null) return null;
|
|
686
|
+
if (typeof _entry === 'object' && !Array.isArray(_entry)) {
|
|
687
|
+
return {
|
|
688
|
+
label: String(_entry.label ?? ''),
|
|
689
|
+
value: Number(_entry.value) || 0,
|
|
690
|
+
color: _entry.color != null ? String(_entry.color) : '',
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (Array.isArray(_entry)) return { label: String(_entry[0] ?? ''), value: Number(_entry[1]) || 0, color: '' };
|
|
694
|
+
return { label: '', value: Number(_entry) || 0, color: '' };
|
|
695
|
+
}).filter(Boolean);
|
|
696
|
+
let explicitMax = Number(_spec?.max);
|
|
697
|
+
let dataMax = series.reduce((_max, _entry) => Math.max(_max, _entry.value), 0);
|
|
698
|
+
return {
|
|
699
|
+
title: _spec?.title != null ? String(_spec.title) : '',
|
|
700
|
+
type: 'bar',
|
|
701
|
+
unit: _spec?.unit != null ? String(_spec.unit) : '',
|
|
702
|
+
max: Number.isFinite(explicitMax) && explicitMax > 0 ? explicitMax : (dataMax || 1),
|
|
703
|
+
series,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function _AsciiChart(_payload) {
|
|
708
|
+
let lines = [];
|
|
709
|
+
if (_payload.title) lines.push(_payload.title);
|
|
710
|
+
let labelWidth = _payload.series.reduce((_max, _entry) => Math.max(_max, _entry.label.length), 0);
|
|
711
|
+
const BAR_WIDTH = 24;
|
|
712
|
+
for (let entry of _payload.series) {
|
|
713
|
+
let filled = _payload.max > 0 ? Math.round((entry.value / _payload.max) * BAR_WIDTH) : 0;
|
|
714
|
+
let bar = '\u2588'.repeat(filled) + '\u00b7'.repeat(Math.max(0, BAR_WIDTH - filled));
|
|
715
|
+
lines.push(' ' + entry.label.padEnd(labelWidth) + ' ' + bar + ' ' + entry.value + (_payload.unit ? ' ' + _payload.unit : ''));
|
|
716
|
+
}
|
|
717
|
+
return lines;
|
|
718
|
+
}
|
|
719
|
+
|
|
556
720
|
export default {
|
|
557
721
|
IsMicroGulp,
|
|
558
722
|
IsµGulp,
|
|
@@ -576,10 +740,18 @@ export default {
|
|
|
576
740
|
SetLid: I18x.SetLid,
|
|
577
741
|
GetTimeZone: I18x.GetTimeZone,
|
|
578
742
|
DateInTimeZone: I18x.DateInTimeZone,
|
|
743
|
+
TimeZoneOffset: I18x.TimeZoneOffset,
|
|
579
744
|
InstallStringExtensions: I18x.InstallStringExtensions,
|
|
745
|
+
InstallFormatPrototypes: I18x.InstallFormatPrototypes,
|
|
746
|
+
InstallHyphenPrototypes: I18x.InstallHyphenPrototypes,
|
|
580
747
|
LogTable,
|
|
581
748
|
LogTree,
|
|
582
749
|
LogGallery,
|
|
583
750
|
LogAssetPreviews,
|
|
584
751
|
LogBuildDebugReport,
|
|
752
|
+
LogCallout,
|
|
753
|
+
LogKeyValue,
|
|
754
|
+
LogBadges,
|
|
755
|
+
LogCode,
|
|
756
|
+
LogChart,
|
|
585
757
|
};
|