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 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
  [![npm version](https://img.shields.io/npm/v/gulp-mu-gulp-api.svg)](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
  [![npm version](https://img.shields.io/npm/v/gulp-mu-gulp-api.svg)](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.6",
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",
@@ -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 local = _WallClockParts(ms, _timeZone);
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
- timezone: _timeZone || 'UTC',
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, InstallStringExtensions,
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
  };