string-dedent
De-indents multi-line JavaScript template literals and strings while preserving relative indentation.
Repository Health
Technical Analysis
string-dedent removes the common leading whitespace shared by every line of a JavaScript template literal or string, so developers can indent multi-line text naturally in their source code without that indentation leaking into the runtime value. It works as a tagged template function, a plain function call, or a wrapper around another tag function, and correctly handles template expressions, escape sequences, and mixed indentation without disturbing the content itself.
Because it operates on the raw template strings array (not just the cooked output), it can also re-cook escape sequences manually, which lets it wrap tags like String.raw or lit-html’s html and still produce correct results even when only raw strings are available.
What You Get
- Tagged-template dedenting that correctly skips whitespace on lines containing interpolated expressions
- A plain function-call mode for dedenting an already-built string
- A tag-wrapping mode for composing dedenting behavior into other tagged-template libraries
- Manual escape-sequence cooking so wrapped raw-string tags (like
String.raw) still resolve correctly - WeakMap-based caching of processed template strings arrays so repeat calls to the same tagged template only dedent once
Common Use Cases
- Formatting indented multi-line strings (SQL queries, help text, error messages) directly in source code
- Wrapping lit-html-style tagged template functions to avoid stray whitespace text nodes in rendered output
- Building code-generation tools where the generator’s own source stays indented but produces clean, unindented output
- Any workflow needing template literals indented to match surrounding code without polluting the resulting string
Under The Hood
Architecture
The entire library lives in one module, src/dedent.ts, exporting a single default function that dispatches on argument type: a plain string, a TemplateStringsArray (tagged-template usage), or another tag function to wrap. All three paths funnel into process(), which splits each template quasi into lines, computes the common leading whitespace across every non-empty, non-expression-adjacent line via commonStart(), and slices that amount off each line when reconstructing the strings. Processed TemplateStringsArray results are memoized in a WeakMap keyed by the original array, so a tagged template called repeatedly in a loop is only dedented once. Because every call shape shares this same process() path, a change to the core algorithm affects all three usage modes simultaneously — there is no per-mode logic to diverge.
Tech Stack
Written in strict-mode TypeScript (ES2015 target/module) with zero runtime dependencies. Built with Rollup and @rollup/plugin-typescript into UMD and ESM bundles plus hand-fixed .d.ts/.d.mts declaration files (via a custom fix-npm-types script). Tested with Jest and ts-jest, linted with ESLint (@typescript-eslint) and Prettier, and orchestrated through npm-run-all. CI runs on both a legacy Travis config and a GitHub Actions workflow across multiple Node versions; Renovate manages dependency updates.
Code Quality
The test suite pairs a small hand-written unit-test file exercising all three call shapes (function, tagged template, wrapped tag) with an extensive fixture-based suite under test/fixtures/tag/ covering dozens of edge cases — escape sequences, unicode code points, expression placement, and whitespace-only lines — each as paired input.js/output.txt (or error.txt) files. Linting and formatting checks run as part of the test script itself, so a lint violation fails CI the same as a failing assertion. Invalid tagged-template usage throws explicit errors; the internal escape-cooking helpers return sentinel values (undefined/-1) for malformed escapes rather than throwing, with callers checking those sentinels explicitly rather than letting failures pass silently.
What Makes It Unique
The library’s distinguishing feature is its tag-wrapping mode: rather than only dedenting its own tagged templates, dedent(tag) returns a new tag function that dedents the strings passed to any other tag — including String.raw, whose consumer only ever sees raw (uncooked) strings. To make that work, the library reimplements JavaScript’s own escape-sequence cooking (hex, unicode, octal, and control-character escapes) so it can dedent and then correctly re-cook a raw string on the wrapped tag’s behalf, a level of manual template-literal semantics most whitespace-trimming utilities don’t attempt.