@logtape/logtape 1.2.0-dev.354 → 1.2.0-dev.359

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "1.2.0-dev.354+e11ec186",
3
+ "version": "1.2.0-dev.359+da6face5",
4
4
  "license": "MIT",
5
5
  "exports": "./src/mod.ts",
6
6
  "imports": {
package/dist/logger.cjs CHANGED
@@ -330,10 +330,227 @@ var LoggerCtx = class LoggerCtx {
330
330
  */
331
331
  const metaLogger = LoggerImpl.getLogger(["logtape", "meta"]);
332
332
  /**
333
+ * Check if a property access key contains nested access patterns.
334
+ * @param key The property key to check.
335
+ * @returns True if the key contains nested access patterns.
336
+ */
337
+ function isNestedAccess(key) {
338
+ return key.includes(".") || key.includes("[") || key.includes("?.");
339
+ }
340
+ /**
341
+ * Safely access an own property from an object, blocking prototype pollution.
342
+ *
343
+ * @param obj The object to access the property from.
344
+ * @param key The property key to access.
345
+ * @returns The property value or undefined if not accessible.
346
+ */
347
+ function getOwnProperty(obj, key) {
348
+ if (key === "__proto__" || key === "prototype" || key === "constructor") return void 0;
349
+ if ((typeof obj === "object" || typeof obj === "function") && obj !== null) return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : void 0;
350
+ return void 0;
351
+ }
352
+ /**
353
+ * Parse the next segment from a property path string.
354
+ *
355
+ * @param path The full property path string.
356
+ * @param fromIndex The index to start parsing from.
357
+ * @returns The parsed segment and next index, or null if parsing fails.
358
+ */
359
+ function parseNextSegment(path, fromIndex) {
360
+ const len = path.length;
361
+ let i = fromIndex;
362
+ if (i >= len) return null;
363
+ let segment;
364
+ if (path[i] === "[") {
365
+ i++;
366
+ if (i >= len) return null;
367
+ if (path[i] === "\"" || path[i] === "'") {
368
+ const quote = path[i];
369
+ i++;
370
+ let segmentStr = "";
371
+ while (i < len && path[i] !== quote) if (path[i] === "\\") {
372
+ i++;
373
+ if (i < len) {
374
+ const escapeChar = path[i];
375
+ switch (escapeChar) {
376
+ case "n":
377
+ segmentStr += "\n";
378
+ break;
379
+ case "t":
380
+ segmentStr += " ";
381
+ break;
382
+ case "r":
383
+ segmentStr += "\r";
384
+ break;
385
+ case "b":
386
+ segmentStr += "\b";
387
+ break;
388
+ case "f":
389
+ segmentStr += "\f";
390
+ break;
391
+ case "v":
392
+ segmentStr += "\v";
393
+ break;
394
+ case "0":
395
+ segmentStr += "\0";
396
+ break;
397
+ case "\\":
398
+ segmentStr += "\\";
399
+ break;
400
+ case "\"":
401
+ segmentStr += "\"";
402
+ break;
403
+ case "'":
404
+ segmentStr += "'";
405
+ break;
406
+ case "u":
407
+ if (i + 4 < len) {
408
+ const hex = path.slice(i + 1, i + 5);
409
+ const codePoint = Number.parseInt(hex, 16);
410
+ if (!Number.isNaN(codePoint)) {
411
+ segmentStr += String.fromCharCode(codePoint);
412
+ i += 4;
413
+ } else segmentStr += escapeChar;
414
+ } else segmentStr += escapeChar;
415
+ break;
416
+ default: segmentStr += escapeChar;
417
+ }
418
+ i++;
419
+ }
420
+ } else {
421
+ segmentStr += path[i];
422
+ i++;
423
+ }
424
+ if (i >= len) return null;
425
+ segment = segmentStr;
426
+ i++;
427
+ } else {
428
+ const startIndex = i;
429
+ while (i < len && path[i] !== "]" && path[i] !== "'" && path[i] !== "\"") i++;
430
+ if (i >= len) return null;
431
+ const indexStr = path.slice(startIndex, i);
432
+ if (indexStr.length === 0) return null;
433
+ const indexNum = Number(indexStr);
434
+ segment = Number.isNaN(indexNum) ? indexStr : indexNum;
435
+ }
436
+ while (i < len && path[i] !== "]") i++;
437
+ if (i < len) i++;
438
+ } else {
439
+ const startIndex = i;
440
+ while (i < len && path[i] !== "." && path[i] !== "[" && path[i] !== "?" && path[i] !== "]") i++;
441
+ segment = path.slice(startIndex, i);
442
+ if (segment.length === 0) return null;
443
+ }
444
+ if (i < len && path[i] === ".") i++;
445
+ return {
446
+ segment,
447
+ nextIndex: i
448
+ };
449
+ }
450
+ /**
451
+ * Access a property or index on an object or array.
452
+ *
453
+ * @param obj The object or array to access.
454
+ * @param segment The property key or array index.
455
+ * @returns The accessed value or undefined if not accessible.
456
+ */
457
+ function accessProperty(obj, segment) {
458
+ if (typeof segment === "string") return getOwnProperty(obj, segment);
459
+ if (Array.isArray(obj) && segment >= 0 && segment < obj.length) return obj[segment];
460
+ return void 0;
461
+ }
462
+ /**
463
+ * Resolve a nested property path from an object.
464
+ *
465
+ * There are two types of property access patterns:
466
+ * 1. Array/index access: [0] or ["prop"]
467
+ * 2. Property access: prop or prop?.next
468
+ *
469
+ * @param obj The object to traverse.
470
+ * @param path The property path (e.g., "user.name", "users[0].email", "user['full-name']").
471
+ * @returns The resolved value or undefined if path doesn't exist.
472
+ */
473
+ function resolvePropertyPath(obj, path) {
474
+ if (obj == null) return void 0;
475
+ if (path.length === 0 || path.endsWith(".")) return void 0;
476
+ let current = obj;
477
+ let i = 0;
478
+ const len = path.length;
479
+ while (i < len) {
480
+ const isOptional = path.slice(i, i + 2) === "?.";
481
+ if (isOptional) {
482
+ i += 2;
483
+ if (current == null) return void 0;
484
+ } else if (current == null) return void 0;
485
+ const result = parseNextSegment(path, i);
486
+ if (result === null) return void 0;
487
+ const { segment, nextIndex } = result;
488
+ i = nextIndex;
489
+ current = accessProperty(current, segment);
490
+ if (current === void 0) return void 0;
491
+ }
492
+ return current;
493
+ }
494
+ /**
333
495
  * Parse a message template into a message template array and a values array.
334
- * @param template The message template.
496
+ *
497
+ * Placeholders to be replaced with `values` are indicated by keys in curly braces
498
+ * (e.g., `{value}`). The system supports both simple property access and nested
499
+ * property access patterns:
500
+ *
501
+ * **Simple property access:**
502
+ * ```ts
503
+ * parseMessageTemplate("Hello, {user}!", { user: "foo" })
504
+ * // Returns: ["Hello, ", "foo", "!"]
505
+ * ```
506
+ *
507
+ * **Nested property access (dot notation):**
508
+ * ```ts
509
+ * parseMessageTemplate("Hello, {user.name}!", {
510
+ * user: { name: "foo", email: "foo@example.com" }
511
+ * })
512
+ * // Returns: ["Hello, ", "foo", "!"]
513
+ * ```
514
+ *
515
+ * **Array indexing:**
516
+ * ```ts
517
+ * parseMessageTemplate("First: {users[0]}", {
518
+ * users: ["foo", "bar", "baz"]
519
+ * })
520
+ * // Returns: ["First: ", "foo", ""]
521
+ * ```
522
+ *
523
+ * **Bracket notation for special property names:**
524
+ * ```ts
525
+ * parseMessageTemplate("Name: {user[\"full-name\"]}", {
526
+ * user: { "full-name": "foo bar" }
527
+ * })
528
+ * // Returns: ["Name: ", "foo bar", ""]
529
+ * ```
530
+ *
531
+ * **Optional chaining for safe navigation:**
532
+ * ```ts
533
+ * parseMessageTemplate("Email: {user?.profile?.email}", {
534
+ * user: { name: "foo" }
535
+ * })
536
+ * // Returns: ["Email: ", undefined, ""]
537
+ * ```
538
+ *
539
+ * **Wildcard patterns:**
540
+ * - `{*}` - Replaced with the entire properties object
541
+ * - `{ key-with-whitespace }` - Whitespace is trimmed when looking up keys
542
+ *
543
+ * **Escaping:**
544
+ * - `{{` and `}}` are escaped literal braces
545
+ *
546
+ * **Error handling:**
547
+ * - Non-existent paths return `undefined`
548
+ * - Malformed expressions resolve to `undefined` without throwing errors
549
+ * - Out of bounds array access returns `undefined`
550
+ *
551
+ * @param template The message template string containing placeholders.
335
552
  * @param properties The values to replace placeholders with.
336
- * @returns The message template array and the values array.
553
+ * @returns The message template array with values interleaved between text segments.
337
554
  */
338
555
  function parseMessageTemplate(template, properties) {
339
556
  const length = template.length;
@@ -357,8 +574,11 @@ function parseMessageTemplate(template, properties) {
357
574
  let prop;
358
575
  const trimmedKey = key.trim();
359
576
  if (trimmedKey === "*") prop = key in properties ? properties[key] : "*" in properties ? properties["*"] : properties;
360
- else if (key !== trimmedKey) prop = key in properties ? properties[key] : properties[trimmedKey];
361
- else prop = properties[key];
577
+ else {
578
+ if (key !== trimmedKey) prop = key in properties ? properties[key] : properties[trimmedKey];
579
+ else prop = properties[key];
580
+ if (prop === void 0 && isNestedAccess(trimmedKey)) prop = resolvePropertyPath(properties, trimmedKey);
581
+ }
362
582
  message.push(prop);
363
583
  i = closeIndex;
364
584
  startIndex = i + 1;
package/dist/logger.js CHANGED
@@ -330,10 +330,227 @@ var LoggerCtx = class LoggerCtx {
330
330
  */
331
331
  const metaLogger = LoggerImpl.getLogger(["logtape", "meta"]);
332
332
  /**
333
+ * Check if a property access key contains nested access patterns.
334
+ * @param key The property key to check.
335
+ * @returns True if the key contains nested access patterns.
336
+ */
337
+ function isNestedAccess(key) {
338
+ return key.includes(".") || key.includes("[") || key.includes("?.");
339
+ }
340
+ /**
341
+ * Safely access an own property from an object, blocking prototype pollution.
342
+ *
343
+ * @param obj The object to access the property from.
344
+ * @param key The property key to access.
345
+ * @returns The property value or undefined if not accessible.
346
+ */
347
+ function getOwnProperty(obj, key) {
348
+ if (key === "__proto__" || key === "prototype" || key === "constructor") return void 0;
349
+ if ((typeof obj === "object" || typeof obj === "function") && obj !== null) return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : void 0;
350
+ return void 0;
351
+ }
352
+ /**
353
+ * Parse the next segment from a property path string.
354
+ *
355
+ * @param path The full property path string.
356
+ * @param fromIndex The index to start parsing from.
357
+ * @returns The parsed segment and next index, or null if parsing fails.
358
+ */
359
+ function parseNextSegment(path, fromIndex) {
360
+ const len = path.length;
361
+ let i = fromIndex;
362
+ if (i >= len) return null;
363
+ let segment;
364
+ if (path[i] === "[") {
365
+ i++;
366
+ if (i >= len) return null;
367
+ if (path[i] === "\"" || path[i] === "'") {
368
+ const quote = path[i];
369
+ i++;
370
+ let segmentStr = "";
371
+ while (i < len && path[i] !== quote) if (path[i] === "\\") {
372
+ i++;
373
+ if (i < len) {
374
+ const escapeChar = path[i];
375
+ switch (escapeChar) {
376
+ case "n":
377
+ segmentStr += "\n";
378
+ break;
379
+ case "t":
380
+ segmentStr += " ";
381
+ break;
382
+ case "r":
383
+ segmentStr += "\r";
384
+ break;
385
+ case "b":
386
+ segmentStr += "\b";
387
+ break;
388
+ case "f":
389
+ segmentStr += "\f";
390
+ break;
391
+ case "v":
392
+ segmentStr += "\v";
393
+ break;
394
+ case "0":
395
+ segmentStr += "\0";
396
+ break;
397
+ case "\\":
398
+ segmentStr += "\\";
399
+ break;
400
+ case "\"":
401
+ segmentStr += "\"";
402
+ break;
403
+ case "'":
404
+ segmentStr += "'";
405
+ break;
406
+ case "u":
407
+ if (i + 4 < len) {
408
+ const hex = path.slice(i + 1, i + 5);
409
+ const codePoint = Number.parseInt(hex, 16);
410
+ if (!Number.isNaN(codePoint)) {
411
+ segmentStr += String.fromCharCode(codePoint);
412
+ i += 4;
413
+ } else segmentStr += escapeChar;
414
+ } else segmentStr += escapeChar;
415
+ break;
416
+ default: segmentStr += escapeChar;
417
+ }
418
+ i++;
419
+ }
420
+ } else {
421
+ segmentStr += path[i];
422
+ i++;
423
+ }
424
+ if (i >= len) return null;
425
+ segment = segmentStr;
426
+ i++;
427
+ } else {
428
+ const startIndex = i;
429
+ while (i < len && path[i] !== "]" && path[i] !== "'" && path[i] !== "\"") i++;
430
+ if (i >= len) return null;
431
+ const indexStr = path.slice(startIndex, i);
432
+ if (indexStr.length === 0) return null;
433
+ const indexNum = Number(indexStr);
434
+ segment = Number.isNaN(indexNum) ? indexStr : indexNum;
435
+ }
436
+ while (i < len && path[i] !== "]") i++;
437
+ if (i < len) i++;
438
+ } else {
439
+ const startIndex = i;
440
+ while (i < len && path[i] !== "." && path[i] !== "[" && path[i] !== "?" && path[i] !== "]") i++;
441
+ segment = path.slice(startIndex, i);
442
+ if (segment.length === 0) return null;
443
+ }
444
+ if (i < len && path[i] === ".") i++;
445
+ return {
446
+ segment,
447
+ nextIndex: i
448
+ };
449
+ }
450
+ /**
451
+ * Access a property or index on an object or array.
452
+ *
453
+ * @param obj The object or array to access.
454
+ * @param segment The property key or array index.
455
+ * @returns The accessed value or undefined if not accessible.
456
+ */
457
+ function accessProperty(obj, segment) {
458
+ if (typeof segment === "string") return getOwnProperty(obj, segment);
459
+ if (Array.isArray(obj) && segment >= 0 && segment < obj.length) return obj[segment];
460
+ return void 0;
461
+ }
462
+ /**
463
+ * Resolve a nested property path from an object.
464
+ *
465
+ * There are two types of property access patterns:
466
+ * 1. Array/index access: [0] or ["prop"]
467
+ * 2. Property access: prop or prop?.next
468
+ *
469
+ * @param obj The object to traverse.
470
+ * @param path The property path (e.g., "user.name", "users[0].email", "user['full-name']").
471
+ * @returns The resolved value or undefined if path doesn't exist.
472
+ */
473
+ function resolvePropertyPath(obj, path) {
474
+ if (obj == null) return void 0;
475
+ if (path.length === 0 || path.endsWith(".")) return void 0;
476
+ let current = obj;
477
+ let i = 0;
478
+ const len = path.length;
479
+ while (i < len) {
480
+ const isOptional = path.slice(i, i + 2) === "?.";
481
+ if (isOptional) {
482
+ i += 2;
483
+ if (current == null) return void 0;
484
+ } else if (current == null) return void 0;
485
+ const result = parseNextSegment(path, i);
486
+ if (result === null) return void 0;
487
+ const { segment, nextIndex } = result;
488
+ i = nextIndex;
489
+ current = accessProperty(current, segment);
490
+ if (current === void 0) return void 0;
491
+ }
492
+ return current;
493
+ }
494
+ /**
333
495
  * Parse a message template into a message template array and a values array.
334
- * @param template The message template.
496
+ *
497
+ * Placeholders to be replaced with `values` are indicated by keys in curly braces
498
+ * (e.g., `{value}`). The system supports both simple property access and nested
499
+ * property access patterns:
500
+ *
501
+ * **Simple property access:**
502
+ * ```ts
503
+ * parseMessageTemplate("Hello, {user}!", { user: "foo" })
504
+ * // Returns: ["Hello, ", "foo", "!"]
505
+ * ```
506
+ *
507
+ * **Nested property access (dot notation):**
508
+ * ```ts
509
+ * parseMessageTemplate("Hello, {user.name}!", {
510
+ * user: { name: "foo", email: "foo@example.com" }
511
+ * })
512
+ * // Returns: ["Hello, ", "foo", "!"]
513
+ * ```
514
+ *
515
+ * **Array indexing:**
516
+ * ```ts
517
+ * parseMessageTemplate("First: {users[0]}", {
518
+ * users: ["foo", "bar", "baz"]
519
+ * })
520
+ * // Returns: ["First: ", "foo", ""]
521
+ * ```
522
+ *
523
+ * **Bracket notation for special property names:**
524
+ * ```ts
525
+ * parseMessageTemplate("Name: {user[\"full-name\"]}", {
526
+ * user: { "full-name": "foo bar" }
527
+ * })
528
+ * // Returns: ["Name: ", "foo bar", ""]
529
+ * ```
530
+ *
531
+ * **Optional chaining for safe navigation:**
532
+ * ```ts
533
+ * parseMessageTemplate("Email: {user?.profile?.email}", {
534
+ * user: { name: "foo" }
535
+ * })
536
+ * // Returns: ["Email: ", undefined, ""]
537
+ * ```
538
+ *
539
+ * **Wildcard patterns:**
540
+ * - `{*}` - Replaced with the entire properties object
541
+ * - `{ key-with-whitespace }` - Whitespace is trimmed when looking up keys
542
+ *
543
+ * **Escaping:**
544
+ * - `{{` and `}}` are escaped literal braces
545
+ *
546
+ * **Error handling:**
547
+ * - Non-existent paths return `undefined`
548
+ * - Malformed expressions resolve to `undefined` without throwing errors
549
+ * - Out of bounds array access returns `undefined`
550
+ *
551
+ * @param template The message template string containing placeholders.
335
552
  * @param properties The values to replace placeholders with.
336
- * @returns The message template array and the values array.
553
+ * @returns The message template array with values interleaved between text segments.
337
554
  */
338
555
  function parseMessageTemplate(template, properties) {
339
556
  const length = template.length;
@@ -357,8 +574,11 @@ function parseMessageTemplate(template, properties) {
357
574
  let prop;
358
575
  const trimmedKey = key.trim();
359
576
  if (trimmedKey === "*") prop = key in properties ? properties[key] : "*" in properties ? properties["*"] : properties;
360
- else if (key !== trimmedKey) prop = key in properties ? properties[key] : properties[trimmedKey];
361
- else prop = properties[key];
577
+ else {
578
+ if (key !== trimmedKey) prop = key in properties ? properties[key] : properties[trimmedKey];
579
+ else prop = properties[key];
580
+ if (prop === void 0 && isNestedAccess(trimmedKey)) prop = resolvePropertyPath(properties, trimmedKey);
581
+ }
362
582
  message.push(prop);
363
583
  i = closeIndex;
364
584
  startIndex = i + 1;