gulp-mu-gulp-api 0.3.6 → 0.3.9
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 +54 -2
- package/package.json +1 -1
- package/src/i18x-engine.mjs +66 -5
- package/src/i18x-hyphen.mjs +166 -0
- package/src/i18x.mjs +91 -9
- package/src/index.mjs +186 -1
package/README.md
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
</a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
+
<p align="center"><strong>Rock your Gulp!</strong><br/>
|
|
16
|
+
µGulp™, the interactive near-zero-latency task orchestrator with rich visuals, webview controls, and audio feedback.</p>
|
|
17
|
+
|
|
15
18
|
Public task API of the **µGulp™** orchestrator — progress reporting, i18x console output and interactive dashboard inputs from within gulp tasks.
|
|
16
19
|
|
|
17
20
|
[](https://www.npmjs.com/package/gulp-mu-gulp-api) · [µGulp™ on GitHub](https://github.com/mamekudz/microGulp) · [source tree](https://github.com/mamekudz/microGulp/tree/main/gulp-mu-gulp-api)
|
|
@@ -22,6 +25,8 @@ Public task API of the **µGulp™** orchestrator — progress reporting, i18x c
|
|
|
22
25
|
|
|
23
26
|
# English
|
|
24
27
|
|
|
28
|
+
**Rock your Gulp!** — µGulp™, the interactive near-zero-latency task orchestrator with rich visuals, webview controls, and audio feedback.
|
|
29
|
+
|
|
25
30
|
**gulp-mu-gulp-api** gives gulp tasks (and any npm package inside the task stream) access to the extended µGulp™ dashboard features: progress bars, validated text inputs, color and font pickers, single and multi selections, sliders and complete forms.
|
|
26
31
|
|
|
27
32
|
The module is deliberately **dependency-free** and knows nothing about µGulp™ internals: when the task runs under µGulp™, it talks directly to the engine over the worker process IPC channel. When the gulpfile runs through the classic gulp CLI, every function degrades gracefully — progress renders on the terminal, inputs are asked via readline (TTY) or answered with the declared defaults (CI/non-TTY). Tasks stay fully runnable without µGulp™.
|
|
@@ -32,6 +37,25 @@ The module is deliberately **dependency-free** and knows nothing about µGulp™
|
|
|
32
37
|
npm install gulp-mu-gulp-api
|
|
33
38
|
```
|
|
34
39
|
|
|
40
|
+
### 0.3.8
|
|
41
|
+
|
|
42
|
+
- **Added** structured log elements rendered directly in the run log (JSON payloads, Content-Security-Policy safe, no plugin/iframe needed): `LogCallout()` (info/success/warning/error box), `LogKeyValue()` (metrics/summary list), `LogBadges()` (status chips), `LogCode()` (monospace code block) and `LogChart()` (inline-SVG bar chart).
|
|
43
|
+
- Each new helper degrades to a readable stdout fallback when running outside µGulp (bracketed callout, aligned key/value lines, bracketed badges, verbatim code, ASCII bar chart).
|
|
44
|
+
|
|
45
|
+
### 0.3.7
|
|
46
|
+
|
|
47
|
+
- **Added** engine date/time placeholders: `weekofyear`, `dayofyear`, `utcweekofyear`, `utcdayofyear`, `timezoneoffset`, `timezoneshort`, `timezoneplace`.
|
|
48
|
+
- **Added** `TimeZoneOffset()` — JavaScript-compatible offset in minutes (same sign as `Date.getTimezoneOffset()`).
|
|
49
|
+
- **Added** hyphenation API: `WordHyphenation()`, `Hyphenation()`, `LoadHyphenData()`, `InstallHyphenPrototypes()` with µLib (`WordHyphenation`/`Hyphenation`) and i18xe-sync (`wordHyphenation`/`hyphenation`) aliases; without `<lid>.hyphen.json` data the methods no-op.
|
|
50
|
+
- **Added** i18xe-sync lowercase format aliases: `Number.prototype.format()` / `.formatTimestamp()` / `.formatTicks()`, `Date.prototype.format()`.
|
|
51
|
+
- **Changed** `Translate(text, values?, timeZone?)` and `.i18xTrans(values?, timeZone?)` accept an optional IANA timezone override.
|
|
52
|
+
|
|
53
|
+
### 0.3.6
|
|
54
|
+
|
|
55
|
+
- **Added** full eval-free i18x engine (`i18x-engine.mjs`): `FormatValue()`/`Translate()` now honor the complete i18x notation — `if`/`ifnot`/`ifin` conditions, nested formats, `expression="…"`, enumerations, digit fill/padding, date/time parts and roman numerals.
|
|
56
|
+
- **Added** µLib-compatible `Number.prototype.Format()`, `Number.prototype.FormatTimestamp()`, `Number.prototype.FormatTicks()`, `Date.prototype.Format()` and `String.prototype.Format()` (installed automatically on import; see `InstallFormatPrototypes()`).
|
|
57
|
+
- **Changed** `FormatValue(value, format, lid?, timeZone?)` gained the optional `timeZone` argument; date/time formats interpret the value as Unix milliseconds.
|
|
58
|
+
|
|
35
59
|
### 0.3.5
|
|
36
60
|
|
|
37
61
|
- **Added** `PlaySound(sample, { loop? })` and `StopSound(sample)` — play named samples from the dashboard µAU sound atlas; `loop: true` repeats until `StopSound`.
|
|
@@ -113,9 +137,12 @@ let values = await RequestForm({
|
|
|
113
137
|
| Function | Purpose |
|
|
114
138
|
| :--- | :--- |
|
|
115
139
|
| `IsMicroGulp()` / `IsµGulp()` | `true` when the task runs under µGulp™ with an attached dashboard |
|
|
116
|
-
| `InstallStringExtensions()` | Installs µLib-compatible `String.prototype.i18xTrans()`/`i18xRegister()` (auto-run on import) |
|
|
140
|
+
| `InstallStringExtensions()` | Installs µLib-compatible `String.prototype.i18xTrans()`/`i18xRegister()` and the Format prototypes (auto-run on import) |
|
|
141
|
+
| `InstallFormatPrototypes()` | Installs `Number`/`Date`/`String.prototype.Format()` (auto-run on import) |
|
|
117
142
|
| `Log(text, values?)` / `Warn` / `LogError` | Localized `console.*` via i18x (`<context="task log"/>` etc.) — preferred for task log output |
|
|
118
143
|
| `Translate(text, values?)` | Low-level: returns the translated string without logging (rarely needed directly) |
|
|
144
|
+
| `FormatValue(value, format, lid?, timeZone?)` | Formats a number/date with a named i18x format (`int`, `floatFix2`, `byteSize`, `stdDateTime`, …) |
|
|
145
|
+
| `(1536).Format('byteSize')` | Prototype sugar for `FormatValue`; `.FormatTimestamp()`/`.FormatTicks()` on numbers, `.Format()` on dates |
|
|
119
146
|
| `"phrase".i18xTrans(values?)` | Prototype sugar for `Translate` — same result, ergonomic in expressions |
|
|
120
147
|
| `"phrase".i18xRegister()` | Registers metadata text (returns the source phrase unchanged as the i18x key) |
|
|
121
148
|
| `GetLid()` / `SetLid(lid?)` | Active language id; `SetLid` clears to automatic when omitted |
|
|
@@ -227,6 +254,9 @@ $env:NPM_OTP="123456"; npm run publish:api
|
|
|
227
254
|
|
|
228
255
|
# Deutsch
|
|
229
256
|
|
|
257
|
+
<p align="center"><strong>Rock your Gulp!</strong><br/>
|
|
258
|
+
µGulp™ — der interaktive Task-Orchestrator mit nahezu null Latenz, stylischer Dashboard-Ausgabe, Webview-Steuerung und akustischem Feedback.</p>
|
|
259
|
+
|
|
230
260
|
**gulp-mu-gulp-api** ist die öffentliche Task-API des **µGulp™**-Orchestrators. Gulp-Tasks (und beliebige npm-Pakete innerhalb des Task-Streams) erhalten damit Zugriff auf die erweiterten µGulp™-Dashboard-Funktionen: Fortschrittsanzeige, i18x-Konsolenausgabe, Texteingaben mit Validierung, Farb- und Schriftart-Auswahl, Einzel- und Mehrfachauswahl, Slider und komplette Formulare.
|
|
231
261
|
|
|
232
262
|
[](https://www.npmjs.com/package/gulp-mu-gulp-api) · [µGulp™ auf GitHub](https://github.com/mamekudz/microGulp)
|
|
@@ -239,6 +269,25 @@ Das Modul ist bewusst **abhängigkeitsfrei** und kennt keine µGulp™-Interna:
|
|
|
239
269
|
npm install gulp-mu-gulp-api
|
|
240
270
|
```
|
|
241
271
|
|
|
272
|
+
### 0.3.8
|
|
273
|
+
|
|
274
|
+
- **Neu** strukturierte Log-Elemente, die direkt im Run-Log gerendert werden (JSON-Payloads, Content-Security-Policy-sicher, ohne Plugin/iframe): `LogCallout()` (Info/Success/Warning/Error-Box), `LogKeyValue()` (Metriken/Zusammenfassung), `LogBadges()` (Status-Chips), `LogCode()` (Monospace-Codeblock) und `LogChart()` (Inline-SVG-Balkendiagramm).
|
|
275
|
+
- Jeder neue Helfer fällt außerhalb von µGulp auf eine lesbare stdout-Ausgabe zurück (Callout in Klammern, ausgerichtete Key/Value-Zeilen, Badges in Klammern, Code wortgetreu, ASCII-Balkendiagramm).
|
|
276
|
+
|
|
277
|
+
### 0.3.7
|
|
278
|
+
|
|
279
|
+
- **Neu** Engine-Datum/Zeit-Platzhalter: `weekofyear`, `dayofyear`, `utcweekofyear`, `utcdayofyear`, `timezoneoffset`, `timezoneshort`, `timezoneplace`.
|
|
280
|
+
- **Neu** `TimeZoneOffset()` — JavaScript-kompatibler Offset in Minuten (gleiches Vorzeichen wie `Date.getTimezoneOffset()`).
|
|
281
|
+
- **Neu** Silbentrennungs-API: `WordHyphenation()`, `Hyphenation()`, `LoadHyphenData()`, `InstallHyphenPrototypes()` mit µLib- (`WordHyphenation`/`Hyphenation`) und i18xe-sync-Aliasen (`wordHyphenation`/`hyphenation`); ohne `<lid>.hyphen.json`-Daten No-op.
|
|
282
|
+
- **Neu** i18xe-sync-Lowercase-Format-Aliase: `Number.prototype.format()` / `.formatTimestamp()` / `.formatTicks()`, `Date.prototype.format()`.
|
|
283
|
+
- **Geändert** `Translate(text, values?, timeZone?)` und `.i18xTrans(values?, timeZone?)` akzeptieren optional eine IANA-Zeitzone.
|
|
284
|
+
|
|
285
|
+
### 0.3.6
|
|
286
|
+
|
|
287
|
+
- **Neu** vollständige eval-freie i18x-Engine (`i18x-engine.mjs`): `FormatValue()`/`Translate()` beherrschen jetzt die komplette i18x-Notation — `if`/`ifnot`/`ifin`-Bedingungen, verschachtelte Formate, `expression="…"`, Enumerationen, Ziffern-Fill/Padding, Datum/Zeit-Teile und römische Zahlen.
|
|
288
|
+
- **Neu** µLib-kompatible `Number.prototype.Format()`, `Number.prototype.FormatTimestamp()`, `Number.prototype.FormatTicks()`, `Date.prototype.Format()` und `String.prototype.Format()` (werden beim Import automatisch installiert; siehe `InstallFormatPrototypes()`).
|
|
289
|
+
- **Geändert** `FormatValue(value, format, lid?, timeZone?)` um das optionale `timeZone`-Argument erweitert; Datum/Zeit-Formate interpretieren den Wert als Unix-Millisekunden.
|
|
290
|
+
|
|
242
291
|
### 0.3.5
|
|
243
292
|
|
|
244
293
|
- **Neu** `PlaySound(sample, { loop? })` und `StopSound(sample)` — benannte Samples aus dem Dashboard-µAU-Sound-Atlas; `loop: true` wiederholt bis `StopSound`.
|
|
@@ -320,7 +369,10 @@ let values = await RequestForm({
|
|
|
320
369
|
| Funktion | Zweck |
|
|
321
370
|
| :--- | :--- |
|
|
322
371
|
| `IsMicroGulp()` / `IsµGulp()` | `true`, wenn der Task unter µGulp™ mit angebundenem Dashboard läuft |
|
|
323
|
-
| `InstallStringExtensions()` | Installiert µLib-kompatible `String.prototype.i18xTrans()`/`i18xRegister()` (läuft beim Import automatisch) |
|
|
372
|
+
| `InstallStringExtensions()` | Installiert µLib-kompatible `String.prototype.i18xTrans()`/`i18xRegister()` und die Format-Prototypes (läuft beim Import automatisch) |
|
|
373
|
+
| `InstallFormatPrototypes()` | Installiert `Number`/`Date`/`String.prototype.Format()` (läuft beim Import automatisch) |
|
|
374
|
+
| `FormatValue(value, format, lid?, timeZone?)` | Formatiert Zahl/Datum mit einem benannten i18x-Format (`int`, `floatFix2`, `byteSize`, `stdDateTime`, …) |
|
|
375
|
+
| `(1536).Format('byteSize')` | Prototype-Kurzform für `FormatValue`; `.FormatTimestamp()`/`.FormatTicks()` an Zahlen, `.Format()` an Dates |
|
|
324
376
|
| `Log(text, values?)` / `Warn` / `LogError` | Lokalisierte `console.*`-Ausgabe über i18x — bevorzugt für Task-Logs |
|
|
325
377
|
| `Translate(text, values?)` | Low-Level: übersetzter String ohne Log (selten direkt nötig) |
|
|
326
378
|
| `"phrase".i18xTrans(values?)` | Prototype-Kurzform für `Translate` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gulp-mu-gulp-api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Public task API for the µGulp orchestrator: progress reporting, sound signals, speech output, localized console output (i18x) and interactive UI inputs (text, password, number, textarea, color, font, select, radio, multi-select checkbox, range slider, date/time, file) from within gulp tasks — with graceful CLI fallbacks when running without µGulp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
package/src/i18x-engine.mjs
CHANGED
|
@@ -243,7 +243,7 @@ export function ParseFormatDef(_def) {
|
|
|
243
243
|
attrs,
|
|
244
244
|
xml,
|
|
245
245
|
maxes: _ScanDigitMaxes(xml),
|
|
246
|
-
hasDateTags: /<\/?(?:utc)?(?:day|month|year|hour|minute|second|millisecond|weekday|weekofyear|dayofyear|timezone)[\s/>]/.test(xml),
|
|
246
|
+
hasDateTags: /<\/?(?:utc)?(?:day|month|year|hour|minute|second|millisecond|weekday|weekofyear|dayofyear|timezone(?:offset|short|place)?|summertimeoffset)[\s/>]/.test(xml),
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
|
|
@@ -401,19 +401,80 @@ function _WallClockParts(_timestampMs, _timeZone) {
|
|
|
401
401
|
};
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
/** ISO-8601 week number (1..53) for a calendar date (month is 0-based). */
|
|
405
|
+
function _IsoWeekOfYear(_year, _month, _day) {
|
|
406
|
+
let d = new Date(Date.UTC(_year, _month, _day));
|
|
407
|
+
let dayOfWeek = d.getUTCDay();
|
|
408
|
+
if (dayOfWeek === 0) dayOfWeek = 7;
|
|
409
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayOfWeek);
|
|
410
|
+
let yearStart = Date.UTC(d.getUTCFullYear(), 0, 1);
|
|
411
|
+
return Math.ceil(((d.getTime() - yearStart) / 86400000 + 1) / 7);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Day of year (1..366) for a calendar date (month is 0-based). */
|
|
415
|
+
function _DayOfYear(_year, _month, _day) {
|
|
416
|
+
let start = Date.UTC(_year, 0, 1);
|
|
417
|
+
let current = Date.UTC(_year, _month, _day);
|
|
418
|
+
return Math.floor((current - start) / 86400000) + 1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** JavaScript-compatible getTimezoneOffset (minutes to add to local time to get UTC). */
|
|
422
|
+
function _TimeZoneOffsetJsMinutes(_ms, _timeZone) {
|
|
423
|
+
let ms = Number(_ms) || 0;
|
|
424
|
+
let local = _WallClockParts(ms, _timeZone);
|
|
425
|
+
let utcDate = new Date(ms);
|
|
426
|
+
let wallAsUTC = Date.UTC(
|
|
427
|
+
local.year, local.month, local.day,
|
|
428
|
+
local.hour, local.minute, local.second, utcDate.getUTCMilliseconds(),
|
|
429
|
+
);
|
|
430
|
+
return Math.round((ms - wallAsUTC) / 60000);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const TIME_ZONE_SHORT_FORMATTERS = new Map();
|
|
434
|
+
|
|
435
|
+
/** Best-effort timezone abbreviation via Intl (no TZNames database). */
|
|
436
|
+
function _TimeZoneShort(_ms, _timeZone, _lid) {
|
|
437
|
+
let key = (_lid || 'en-US') + '\0' + (_timeZone || 'UTC');
|
|
438
|
+
let formatter = TIME_ZONE_SHORT_FORMATTERS.get(key);
|
|
439
|
+
if (!formatter) {
|
|
440
|
+
try {
|
|
441
|
+
formatter = new Intl.DateTimeFormat(_lid || 'en-US', { timeZone: _timeZone || 'UTC', timeZoneName: 'short' });
|
|
442
|
+
} catch {
|
|
443
|
+
formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', timeZoneName: 'short' });
|
|
444
|
+
}
|
|
445
|
+
TIME_ZONE_SHORT_FORMATTERS.set(key, formatter);
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
for (let part of formatter.formatToParts(new Date(Number(_ms) || 0))) {
|
|
449
|
+
if (part.type === 'timeZoneName') return part.value;
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
// Invalid timezone — leave empty.
|
|
453
|
+
}
|
|
454
|
+
return '';
|
|
455
|
+
}
|
|
456
|
+
|
|
404
457
|
/** Decomposes a timestamp into date/time placeholders (local tz + utc variants). */
|
|
405
|
-
function _DatePlaceholders(_value, _timeZone) {
|
|
458
|
+
function _DatePlaceholders(_value, _timeZone, _lid) {
|
|
406
459
|
let ms = Number(_value) || 0;
|
|
407
|
-
let
|
|
460
|
+
let tz = _timeZone || 'UTC';
|
|
461
|
+
let local = _WallClockParts(ms, tz);
|
|
408
462
|
let utcDate = new Date(ms);
|
|
409
463
|
return {
|
|
410
464
|
day: local.day, month: local.month, year: local.year,
|
|
411
465
|
hour: local.hour, minute: local.minute, second: local.second,
|
|
412
466
|
weekday: local.weekday, millisecond: utcDate.getUTCMilliseconds(),
|
|
413
|
-
|
|
467
|
+
weekofyear: _IsoWeekOfYear(local.year, local.month, local.day),
|
|
468
|
+
dayofyear: _DayOfYear(local.year, local.month, local.day),
|
|
469
|
+
timezone: tz,
|
|
470
|
+
timezoneoffset: _TimeZoneOffsetJsMinutes(ms, tz),
|
|
471
|
+
timezoneshort: _TimeZoneShort(ms, tz, _lid),
|
|
472
|
+
timezoneplace: tz,
|
|
414
473
|
utcday: utcDate.getUTCDate(), utcmonth: utcDate.getUTCMonth(), utcyear: utcDate.getUTCFullYear(),
|
|
415
474
|
utchour: utcDate.getUTCHours(), utcminute: utcDate.getUTCMinutes(), utcsecond: utcDate.getUTCSeconds(),
|
|
416
475
|
utcweekday: utcDate.getUTCDay(), utcmillisecond: utcDate.getUTCMilliseconds(),
|
|
476
|
+
utcweekofyear: _IsoWeekOfYear(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate()),
|
|
477
|
+
utcdayofyear: _DayOfYear(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate()),
|
|
417
478
|
};
|
|
418
479
|
}
|
|
419
480
|
|
|
@@ -432,7 +493,7 @@ export function FormatValue(_value, _formatName, _ctx) {
|
|
|
432
493
|
let def = _ResolveFormat(_formatName, _ctx);
|
|
433
494
|
if (!def) return String(_value);
|
|
434
495
|
let placeholders = _NumberPlaceholders(_value, def);
|
|
435
|
-
if (def.hasDateTags) Object.assign(placeholders, _DatePlaceholders(_value, _ctx.timeZone), { x: Number(_value) || 0 });
|
|
496
|
+
if (def.hasDateTags) Object.assign(placeholders, _DatePlaceholders(_value, _ctx.timeZone, _ctx.lid), { x: Number(_value) || 0 });
|
|
436
497
|
return Trans(def.xml, placeholders, _ctx);
|
|
437
498
|
}
|
|
438
499
|
|
|
@@ -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
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
33
33
|
import { join } from 'node:path';
|
|
34
34
|
import { Trans as EngineTrans, FormatValue as EngineFormatValue, RegisterFormat } from './i18x-engine.mjs';
|
|
35
|
+
import { InstallHyphenPrototypes as _InstallHyphenPrototypes } from './i18x-hyphen.mjs';
|
|
35
36
|
|
|
36
37
|
const DEFAULT_LID = 'en-US';
|
|
37
38
|
|
|
@@ -92,6 +93,22 @@ export function DateInTimeZone(_date, _timeZone) {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
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
|
+
|
|
95
112
|
/** Normalizes casing: language lower, 2-letter region upper, 4-letter script Title. */
|
|
96
113
|
function _NormalizeLid(_lid) {
|
|
97
114
|
if (!_lid) return '';
|
|
@@ -229,9 +246,9 @@ function _FormatRegistry(_lid) {
|
|
|
229
246
|
}
|
|
230
247
|
|
|
231
248
|
/** Assembles the engine render context (language, timezone, format registry). */
|
|
232
|
-
function _RenderContext(_lid) {
|
|
249
|
+
function _RenderContext(_lid, _timeZone) {
|
|
233
250
|
let lid = _lid ?? GetLid();
|
|
234
|
-
return { lid, timeZone: GetTimeZone(), formats: _FormatRegistry(lid) };
|
|
251
|
+
return { lid, timeZone: _timeZone ?? GetTimeZone(), formats: _FormatRegistry(lid) };
|
|
235
252
|
}
|
|
236
253
|
|
|
237
254
|
/**
|
|
@@ -239,13 +256,15 @@ function _RenderContext(_lid) {
|
|
|
239
256
|
* project's i18x/gulp dictionaries (synced from i18xe prod). Delegates to the
|
|
240
257
|
* eval-free i18x engine, so the full i18x notation (fill/padding, `if`
|
|
241
258
|
* conditions, nested formats, expressions, enumerations, date parts) is honored.
|
|
259
|
+
* Date/time formats interpret the value as milliseconds since the Unix epoch.
|
|
242
260
|
* @param {number|string} _value
|
|
243
261
|
* @param {string} _formatName e.g. 'int', 'floatFix2', 'byteSize', 'stdDateTime'
|
|
244
262
|
* @param {string} [_lid]
|
|
263
|
+
* @param {string} [_timeZone] IANA timezone id (defaults to GetTimeZone())
|
|
245
264
|
* @returns {string}
|
|
246
265
|
*/
|
|
247
|
-
export function FormatValue(_value, _formatName, _lid) {
|
|
248
|
-
let context = _RenderContext(_lid);
|
|
266
|
+
export function FormatValue(_value, _formatName, _lid, _timeZone) {
|
|
267
|
+
let context = _RenderContext(_lid, _timeZone);
|
|
249
268
|
if (!Object.prototype.hasOwnProperty.call(context.formats, _formatName)) return String(_value);
|
|
250
269
|
return EngineFormatValue(_value, _formatName, context);
|
|
251
270
|
}
|
|
@@ -256,9 +275,10 @@ export function FormatValue(_value, _formatName, _lid) {
|
|
|
256
275
|
* `if` conditions, …). Unknown phrases fall back to the en-US source text.
|
|
257
276
|
* @param {string} _text source phrase, e.g. 'Build ready<context="task log"/>'
|
|
258
277
|
* @param {object} [_values] placeholder values for `<name/>` tags
|
|
278
|
+
* @param {string} [_timeZone] IANA timezone id (defaults to GetTimeZone())
|
|
259
279
|
* @returns {string}
|
|
260
280
|
*/
|
|
261
|
-
export function Translate(_text, _values) {
|
|
281
|
+
export function Translate(_text, _values, _timeZone) {
|
|
262
282
|
if (_text == null) return '';
|
|
263
283
|
let source = String(_text);
|
|
264
284
|
let lid = GetLid();
|
|
@@ -266,7 +286,7 @@ export function Translate(_text, _values) {
|
|
|
266
286
|
let translated = (dictionary && Object.prototype.hasOwnProperty.call(dictionary, source)) ? dictionary[source] : source;
|
|
267
287
|
// The context tag is part of the dictionary key only; strip it before rendering.
|
|
268
288
|
translated = translated.replace(/<context=[^>]*>/g, '');
|
|
269
|
-
return EngineTrans(translated, _values ?? {}, _RenderContext(lid));
|
|
289
|
+
return EngineTrans(translated, _values ?? {}, _RenderContext(lid, _timeZone));
|
|
270
290
|
}
|
|
271
291
|
|
|
272
292
|
/**
|
|
@@ -308,7 +328,7 @@ export function LogError(_text, _values) {
|
|
|
308
328
|
export function InstallStringExtensions() {
|
|
309
329
|
if (typeof String.prototype.I18xTrans !== 'function') {
|
|
310
330
|
Object.defineProperty(String.prototype, 'I18xTrans', {
|
|
311
|
-
value: function (_values) { return Translate(String(this), _values); },
|
|
331
|
+
value: function (_values, _timeZone) { return Translate(String(this), _values, _timeZone); },
|
|
312
332
|
writable: true, configurable: true,
|
|
313
333
|
});
|
|
314
334
|
}
|
|
@@ -330,11 +350,73 @@ export function InstallStringExtensions() {
|
|
|
330
350
|
writable: true, configurable: true,
|
|
331
351
|
});
|
|
332
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
|
+
});
|
|
333
414
|
}
|
|
334
415
|
|
|
335
416
|
InstallStringExtensions();
|
|
336
417
|
|
|
337
418
|
export default {
|
|
338
|
-
GetLid, SetLid, GetTimeZone, DateInTimeZone,
|
|
339
|
-
Translate, FormatValue, Log, Warn, LogError,
|
|
419
|
+
GetLid, SetLid, GetTimeZone, DateInTimeZone, TimeZoneOffset,
|
|
420
|
+
Translate, FormatValue, Log, Warn, LogError,
|
|
421
|
+
InstallStringExtensions, InstallFormatPrototypes, InstallHyphenPrototypes,
|
|
340
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,9 +625,114 @@ 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
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Asks µGulp to re-evaluate dynamic task metadata (µEnabled predicates,
|
|
722
|
+
* disabled tooltips) and refresh the dashboard task list. Call after
|
|
723
|
+
* changing module-level state that drives enable/disable rules.
|
|
724
|
+
* No-op outside a µGulp worker.
|
|
725
|
+
*/
|
|
726
|
+
export function NotifyTasksChanged() {
|
|
727
|
+
if (typeof process.send === 'function') {
|
|
728
|
+
process.send({ type: 'refresh-tasks' });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
556
732
|
export default {
|
|
557
733
|
IsMicroGulp,
|
|
558
734
|
IsµGulp,
|
|
735
|
+
NotifyTasksChanged,
|
|
559
736
|
ReportProgress,
|
|
560
737
|
CreateProgress,
|
|
561
738
|
PlaySignal,
|
|
@@ -576,10 +753,18 @@ export default {
|
|
|
576
753
|
SetLid: I18x.SetLid,
|
|
577
754
|
GetTimeZone: I18x.GetTimeZone,
|
|
578
755
|
DateInTimeZone: I18x.DateInTimeZone,
|
|
756
|
+
TimeZoneOffset: I18x.TimeZoneOffset,
|
|
579
757
|
InstallStringExtensions: I18x.InstallStringExtensions,
|
|
758
|
+
InstallFormatPrototypes: I18x.InstallFormatPrototypes,
|
|
759
|
+
InstallHyphenPrototypes: I18x.InstallHyphenPrototypes,
|
|
580
760
|
LogTable,
|
|
581
761
|
LogTree,
|
|
582
762
|
LogGallery,
|
|
583
763
|
LogAssetPreviews,
|
|
584
764
|
LogBuildDebugReport,
|
|
765
|
+
LogCallout,
|
|
766
|
+
LogKeyValue,
|
|
767
|
+
LogBadges,
|
|
768
|
+
LogCode,
|
|
769
|
+
LogChart,
|
|
585
770
|
};
|