Skip to content

Improve dedent implementation with performance and robustness enhancements #21

Open

Description

@travisennis

Improve dedent implementation with performance and robustness enhancements

Summary

This issue proposes several core improvements to the dedent template tag in source/dedent.ts. The changes focus on performance optimization, more predictable whitespace handling, and safer interpolation of values - inspired by best practices from similar libraries like lmstudio-js's text() function.

Motivation

The current dedent implementation works well but has opportunities for improvement:

  1. Performance: No caching means the same template literals are reprocessed on every call
  2. Whitespace handling: Global trim() can remove intentional leading/trailing content
  3. Value interpolation: No special handling for objects/Errors, leading to inconsistent stringification
  4. Indentation: Mixed tab/space handling can cause unpredictable results

These improvements will make dedent faster, more predictable, and safer for production use.

Proposed Changes

1. Add WeakMap compilation cache

What: Cache compiled template segments using WeakMap<TemplateStringsArray, string[]>

Why: Template literals reuse the same TemplateStringsArray object across calls. Caching a preprocessed representation avoids redoing parsing and dedent logic for repeated calls, significantly improving performance in hot paths (logging, prompt building, etc.).

Implementation:

const compiledTemplatesCache = new WeakMap<TemplateStringsArray, string[]>();

The cache should store a 2N + 1 length array where:

  • Even indices (0, 2, 4...) contain the static string segments
  • Odd indices (1, 3, 5...) contain empty placeholders for interpolated values

2. Replace global trim() with targeted trimming

What: Replace result.trim() with:

  • Remove only leading newlines at the very start
  • Remove only trailing newlines and spaces at the very end

Why: Global trim() removes all leading/trailing whitespace, which can unintentionally strip meaningful content. Targeted trimming preserves intentional formatting while cleaning up template literal artifacts.

Implementation:

// Instead of:
result = result.trim();

// Use:
result = result.replace(/^\n+/, '').replace(/[\n ]+$/, '');

3. Improve value interpolation safety

What: Add robust handling for interpolated values:

  • Use String() for primitives
  • Use JSON.stringify() for objects with try/catch fallback
  • Special handling for Error-like objects (use .stack if available)

Why: Prevents inconsistent [object Object] output and provides better diagnostics for error objects. Makes interpolation behavior predictable and safe.

Implementation:

function safeStringify(value: unknown): string {
  if (typeof value === 'object' && value !== null) {
    if (typeof (value as any).stack === 'string') {
      return (value as any).stack;
    }
    try {
      return JSON.stringify(value);
    } catch {
      return '[Object failed to stringify]';
    }
  }
  return String(value);
}

4. Use spaces-only for indentation detection

What: Modify the indentation detection regex to only consider spaces, not tabs

Why: Creates predictable dedent behavior across different environments. Tabs can have different visual widths, leading to inconsistent results. Space-only indentation is more reliable and matches common linting rules.

Implementation:

// Instead of:
const spaces = /^(\s+)\S+/;

// Use:
const spaces = /^( +)\S+/;

5. Restructure for compile-time optimization

What: Split the function into:

  • compile(): Processes static template strings, handles indentation, creates cached segments
  • Main function: Updates cached segments with interpolated values and joins

Why: Enables caching and ensures stable formatting regardless of runtime values. Reduces per-call computational overhead.

Implementation structure:

const compiledTemplatesCache = new WeakMap<TemplateStringsArray, string[]>();

function compile(strings: TemplateStringsArray): string[] {
  // Static processing: trimming, indentation, line handling
  // Returns 2N+1 element array with value placeholders
}

export const dedent: Dedent = (
  strings: TemplateStringsArray,
  ...values: unknown[]
): string => {
  let compiled = compiledTemplatesCache.get(strings);
  if (!compiled) {
    compiled = compile(strings);
    compiledTemplatesCache.set(strings, compiled);
  }

  // Insert values into odd indices
  for (let i = 0; i < values.length; i++) {
    compiled[i * 2 + 1] = safeStringify(values[i]);
  }

  return compiled.join('');
};

Implementation Details

New Helper Functions

  1. safeStringify(value: unknown): string

    • Handles all value types safely
    • Special Error.stack handling
    • JSON.stringify with fallback
  2. compile(strings: TemplateStringsArray): string[]

    • Static preprocessing
    • Indentation calculation and removal
    • Targeted trimming
    • Returns segment array for caching

Changes to Existing Function

  • Remove escape sequence handling (\\n, \`, \$, \{) - template literals handle these natively
  • Remove post-interpolation result.replace(/\\n/g, "\n") - unnecessary with proper template handling
  • Restructure to use cached compilation model
  • Update indentation regex to spaces-only

Performance Considerations

  • WeakMap cache has minimal memory overhead and automatically cleans up when template arrays are garbage collected
  • Compilation happens once per unique template literal
  • Per-call overhead reduced to value insertion and array joining

Backward Compatibility

  • API surface unchanged: still dedenttemplate...``
  • Output format very similar, except:
    • More consistent indentation handling (tabs ignored)
    • Safer object stringification
    • No escape sequence processing (rarely needed)
    • More precise trimming behavior

Expected Benefits

  1. Performance: 2-10x faster for repeated template calls due to caching
  2. Robustness: Predictable output for all input types, especially objects and errors
  3. Maintainability: Cleaner separation of concerns between compilation and interpolation
  4. Reliability: Consistent indentation behavior across environments

Testing Considerations

The implementation should be tested with:

  • Repeated calls to the same template (cache verification)
  • Mixed tab/space indentation
  • Various value types (strings, numbers, objects, Errors, null, undefined)
  • Edge cases (empty templates, single-line templates, templates with only whitespace)
  • Leading/trailing whitespace scenarios

Alternative Approaches Considered

  • Paragraph merging: Rejected for this issue as it would change the API contract significantly. Could be added as a separate function or opt-in flag in the future.
  • Tab support: Rejected in favor of spaces-only for consistency. Could be added as a configuration option if needed.

Out of Scope

  • Paragraph merging (single newline → space, blank line → paragraph break)
  • Tab indentation support
  • Additional escape sequence processing
  • Configuration options for behavior variants

These could be addressed in future issues if there's demand.


This issue provides sufficient detail for a coding agent to implement the improvements while maintaining backward compatibility and following the project's coding standards.

Activity

  1. changed the title [-]Improve implementation with performance and robustness enhancements[/-] [+]Improve dedent implementation with performance and robustness enhancements[/+] on Aug 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions