/** * # @okikio/undent * * Strip source-code indentation from template literals and strings. * * When you write multi-line template literals inside functions, classes, * or other indented blocks, the indentation from your source code bleeds * into the output string. `undent` removes that structural indent while * keeping any relative indentation you actually want. * * ```ts * // Deno * import { undent } from "jsr:@okikio/undent"; * * // Node / Bun (npm) * // npm install @okikio/undent * import { undent } from "@okikio/undent"; * ``` * * ```ts * // Without undent — output has 4 unwanted leading spaces per line: * const bad = ` * Hello, world! * Welcome aboard. * `; * * // With undent — clean output, readable source: * const good = undent` * Hello, world! * Welcome aboard. * `; * // "Hello, world!\nWelcome aboard." * ``` * * Two processing paths handle different input shapes: * * - **Tagged templates** split the literal into static segments and * interpolated values. Only the segments are processed — values pass * through untouched. Results are cached per call site. * * - **Plain strings** (via `.string()` or {@link dedentString}) scan * every line for the minimum indent, strip it, and trim wrapper blank * lines. Original newline sequences (`\n`, `\r\n`, `\r`) are * preserved byte-for-byte. * * Both paths share the same guarantees: non-whitespace content is never * removed, newlines in interpolated values are never normalized, and * multi-line values can be aligned at their insertion column with * {@link align} or {@link embed}. By default that insertion column is measured * with {@link columnOffset}, and callers can override that policy with the * `columnOffset` option when they need Unicode-aware visual alignment. * * Indent detection treats leading tabs and spaces as raw whitespace * characters, not visual columns. A line that starts with `"\t "` and a * line that starts with `" \t"` both have three leading indentation * characters, so `undent` strips three characters from each. If you need an * explicit baseline in mixed-indentation templates, normalize the source * indentation first or use {@link indent}. If you need visual-column * alignment for interpolated values, pass a Unicode-aware `columnOffset` * function from the `@okikio/undent/unicode` entry point. * * @module */ /** * Controls how leading and trailing blank lines are trimmed. * * - `"all"` — remove every blank line at the edge (default) * - `"one"` — remove at most one blank line from each end * - `"none"` — keep everything, including wrapper lines */ export type TrimMode = "all" | "one" | "none"; /** * Per-side trim control. Use this when you want different behavior * on the leading vs. trailing edge: * * ```ts * const u = undent.with({ * trim: { leading: "none", trailing: "all" }, * }); * ``` */ export interface TrimSides { /** How to trim blank lines at the start of the output. */ leading?: TrimMode; /** How to trim blank lines at the end of the output. */ trailing?: TrimMode; } /** * Measure the current insertion column for alignment. * * `undent` calls this with the output accumulated so far and expects the * number of spaces to prepend before later lines of an aligned value. * * The default implementation is {@link columnOffset}, which counts UTF-16 * code units after the last newline. Override it when you need alignment to * follow a different visual-width policy. */ export type ColumnOffsetFunction = (text: string) => number; /** * Options for configuring an `undent` instance. * * Every option has a sensible default. You only need to set the ones * you want to change: * * ```ts * const u = undent.with({ strategy: "first", trim: "one" }); * ``` */ export interface UndentOptions { /** * How to decide which whitespace is "structural" indent. * * - `"common"` — scan every content line and strip the smallest * shared indent. Safest default. * - `"first"` — use the first content line's indent as the * reference. Matches the `outdent` npm package. * * @default "common" */ strategy?: "common" | "first"; /** * How to handle the blank lines at the start and end of the output * (the newline after the opening backtick and the whitespace-only * line before the closing one). * * Pass a string for symmetric trimming, or an object to control * each side independently: * * ```ts * undent.with({ trim: { leading: "none", trailing: "all" } }); * ``` * * @default "all" */ trim?: TrimMode | TrimSides; /** * Replace newlines in template segments with this string. * * Set to `"\n"` to normalize all line endings to LF, or leave as * `null` to preserve the original `\n` / `\r\n` / `\r` sequences. * Newlines inside interpolated `${values}` are never touched. * * @default null */ newline?: string | null; /** * Automatically align every multi-line interpolated value at its * insertion column. * * When `false` (default), only values wrapped with {@link align} * or {@link embed} are aligned. Set to `true` to align all of them * without wrapping each one individually. * * @default false */ alignValues?: boolean; /** * Measure the insertion column used by {@link align}, {@link embed}, and * {@link alignValues}. * * The default is {@link columnOffset}, which counts UTF-16 code units after * the last newline. Override this when you need alignment to follow a custom * display-width policy such as Unicode terminal columns. * * @default columnOffset */ columnOffset?: ColumnOffsetFunction; } /** * A callable template tag with configuration and helper methods. * * Use it directly as a tagged template, or call `.with()` to create * a customized instance, or `.string()` to strip indent from a plain * string. * * ```ts * // As a template tag: * undent` * Hello, world! * `; * // "Hello, world!" * * // As a string processor: * undent.string(" indented text"); * // "indented text" * * // With custom options: * const u = undent.with({ trim: "none" }); * ``` */ export interface Undent { /** Strip structural indent from a tagged template literal. */ (strings: TemplateStringsArray, ...values: unknown[]): string; /** * Create a new instance with different options. The current instance * is never mutated — settings are inherited and overridden: * * ```ts * const base = undent.with({ newline: "\n" }); * const strict = base.with({ trim: "none" }); // inherits newline * ``` */ with(options: UndentOptions): Undent; /** * Strip indent from an arbitrary string (not a template literal). * * Uses the same trim and newline settings as the instance. Scans * every line for the minimum indent and strips it: * * ```ts * const sql = readFileSync("query.sql", "utf8"); * const clean = undent.string(sql); * ``` */ string(input: string): string; /** * Indent anchor symbol. Place as the first interpolation on its own * line to set an explicit left margin for the output. * * The anchor's column position becomes the indent baseline. Content * at the anchor's column becomes column 0 in the output; content * deeper than the anchor keeps its relative spacing. * * This gives you explicit control over stripping instead of relying * on automatic detection. It's especially useful in code generation * where templates live deep inside nested classes or functions. * * @example Content at anchor column becomes column 0 * ```ts * import { undent } from "@okikio/undent"; * * class Generator { * emit(name: string) { * return undent` * ${undent.indent} * export function ${name}() { * // implementation * } * `; * // anchor and content at same column → output at column 0: * // "export function hello() {\n // implementation\n}" * } * } * ``` * * @example Content deeper than anchor preserves relative spacing * ```ts * import { undent } from "@okikio/undent"; * * function indentedOutput() { * return undent` * ${undent.indent} * if (ready) { * run(); * } * `; * // Content is 2 deeper than anchor → 2-space indent preserved: * // " if (ready) {\n run();\n }" * } * ``` */ readonly indent: typeof indent; } /** * Fully resolved configuration where every field is required. * * `undent` works with this shape internally after merging defaults with user * overrides. Exported for consumers building custom configuration pipelines * via {@link resolveOptions}. */ export interface ResolvedOptions { /** Indent detection strategy: `"common"` scans all lines, `"first"` uses the first content line. */ strategy: "common" | "first"; /** How to trim blank lines at the start of the output. */ trimLeading: TrimMode; /** How to trim blank lines at the end of the output. */ trimTrailing: TrimMode; /** When set to a string, replaces newline sequences in template segments. `null` preserves originals. */ newline: string | null; /** When `true`, every multi-line interpolated value is automatically aligned at its insertion column. */ alignValues: boolean; /** How to measure the insertion column used for alignment padding. */ columnOffset: ColumnOffsetFunction; } /** * Indent anchor symbol. * * Place `${undent.indent}` (or import this symbol directly) as the * first interpolation on its own line to set the indent baseline. * The anchor's column becomes column 0 for content at the same * depth, and deeper content keeps its relative spacing. * * @example Using the indent symbol directly or via undent.indent * ```ts * import { undent, indent } from "@okikio/undent"; * * // These are equivalent: * undent` * ${undent.indent} * export class Foo { * bar = 1; * } * `; * undent` * ${indent} * export class Foo { * bar = 1; * } * `; * // Both produce: "export class Foo {\n bar = 1;\n}" * ``` */ export declare const indent: unique symbol; /** * Brand symbol for values wrapped by {@link align} or {@link embed}. * * You rarely need this directly — use {@link isAligned} to check * whether a value is wrapped, and {@link align}/{@link embed} to * create wrapped values. Exported so the {@link AlignedValue} * interface can reference it in public type signatures. */ export declare const ALIGNED: unique symbol; /** * A branded wrapper that tells `undent` to pad subsequent lines of * this value to the insertion column. Created by {@link align} and * {@link embed}. * * You don't need to construct this directly — use the helper * functions instead. */ export interface AlignedValue { /** Brand marker. Always `true` for values created by {@link align} or {@link embed}. */ readonly [ALIGNED]: true; /** The stringified content, ready for insertion into the template output. */ readonly value: string; } /** * Mark an interpolated value for column alignment. * * When a multi-line value is interpolated, its second and subsequent * lines normally start at column 0 — breaking the visual structure. * Wrapping it with `align()` pads those lines to match the insertion * column: * * ``` * Without align(): With align(): * * list: list: * - alpha - alpha * - beta ← col 0 - beta ← stays at col 2 * - gamma - gamma * end end * ``` * * @param value - Any value. It is stringified with `String(value)`. * @returns A branded {@link AlignedValue} wrapper. * * @example Aligning a multi-line list at its insertion column * ```ts * import { undent, align } from "@okikio/undent"; * * const items = "- alpha\n- beta\n- gamma"; * * undent` * list: * ${align(items)} * end * `; * // "list:\n - alpha\n - beta\n - gamma\nend" * ``` */ export declare function align(value: unknown): AlignedValue; /** * Strip a value's own indentation, then mark it for alignment. * * Use this when the value carries baked-in indent from its source * location (a SQL query written as an indented constant, a code block * loaded from a file, etc.). `embed()` runs {@link dedentString} on * the value first, then wraps the result with {@link align}: * * ``` * Input value (4-space indent): After embed(): * * SELECT id, name SELECT id, name * FROM users FROM users * WHERE active = true WHERE active = true * ``` * * Repeated calls to `embed()` reuse bounded internal caches for two different * steps: the dedented snippet itself and, for small multi-line snippets, * the aligned text used when the same snippet is inserted at the same column. * Those caches are keyed by exact string content, capped to bound retention, * and skipped for very large inputs. * * These caches improve hot-path rendering, but they are not a security * boundary. Distinct snippets, very large snippets, or many insertion columns * can still miss or evict cache entries. The observable behavior stays the * same because `embed()` always recomputes the exact aligned string when a * cache entry is absent. * * @param value - A string with baked-in indentation to strip. * @returns A branded {@link AlignedValue} wrapper. * * @example Embedding an indented SQL query into a template * ```ts * import { undent, embed } from "@okikio/undent"; * * const sql = ` * SELECT id, name * FROM users * WHERE active = true * `; * * undent` * query: * ${embed(sql)} * `; * // "query:\n SELECT id, name\n FROM users\n WHERE active = true" * ``` * * @example Embedding the same snippet at different columns without changing output * ```ts * import { undent, embed } from "@okikio/undent"; * * const block = " alpha\n beta"; * * const short = undent` * list: * ${embed(block)} * `; * * const wide = undent` * padding: * ${embed(block)} * `; * * // short === "list:\n alpha\n beta" * // wide === "padding:\n alpha\n beta" * ``` */ export declare function embed(value: string): AlignedValue; /** * Type guard: returns `true` if `value` was created by * {@link align} or {@link embed}. * * @example Checking whether a value is wrapped * ```ts * import { align, isAligned } from "@okikio/undent"; * * isAligned(align("hello")); // true * isAligned("hello"); // false * ``` */ export declare function isAligned(value: unknown): value is AlignedValue; /** * The default resolved options. Exported so you can inspect or extend * the defaults when building custom configuration pipelines. * * ```ts * import { DEFAULTS } from "@okikio/undent"; * * console.log(DEFAULTS.strategy); // "common" * console.log(DEFAULTS.trimLeading); // "all" * ``` */ export declare const DEFAULTS: ResolvedOptions; /** * Create a new `undent` instance with custom options. * * Starts from the default settings and applies your overrides. Use * this when you want a standalone instance that doesn't inherit from * an existing one (unlike `.with()`). * * @param options - Configuration overrides. Omitted fields use defaults. * @returns A new {@link Undent} instance. * * @example Creating an outdent-compatible instance * ```ts * import { createUndent } from "@okikio/undent"; * * // Matches the outdent npm package's behavior: * const myTag = createUndent({ strategy: "first", trim: "one" }); * * myTag` * first line sets the indent * deeper line stays deeper * `; * // "first line sets the indent\n deeper line stays deeper" * ``` */ export declare function createUndent(options?: UndentOptions): Undent; /** * Default instance: strips the common indent across all lines and * trims all leading/trailing blank lines. * * Also exported as the module's default export. * * @example Stripping structural indent from a template * ```ts * import { undent } from "@okikio/undent"; * * undent` * Hello, world! * `; * // "Hello, world!" * ``` */ export declare const undent: Undent; /** * Convenience alias for {@link undent}. * * Some codebases use the name "dedent" by convention. This export * lets you import whichever name feels natural: * * ```ts * import { dedent } from "@okikio/undent"; * ``` */ export declare const dedent: Undent; /** * Pre-configured instance that matches classic `outdent` npm behavior: * first-line indent detection and trim-one. * * @example First-line strategy with trim-one * ```ts * import { outdent } from "@okikio/undent"; * * outdent` * first line sets the indent * deeper line stays deeper * `; * // "first line sets the indent\n deeper line stays deeper" * ``` */ export declare const outdent: Undent; export default undent; /** * Merge user options onto a resolved base, producing a new * {@link ResolvedOptions}. * * This powers both {@link createUndent} (base = {@link DEFAULTS}) * and `.with()` (base = parent's options). Exported for consumers * who want to build custom configuration pipelines. * * @param base - The fully resolved starting options. * @param options - User overrides to apply on top of `base`. * @returns A new {@link ResolvedOptions} with overrides merged in. * * @example Merging custom options with defaults * ```ts * import { resolveOptions, DEFAULTS } from "@okikio/undent"; * * const opts = resolveOptions(DEFAULTS, { strategy: "first" }); * console.log(opts.strategy); // "first" * console.log(opts.trimLeading); // "all" (inherited from DEFAULTS) * ``` */ export declare function resolveOptions(base: ResolvedOptions, options: UndentOptions): ResolvedOptions; /** * Strip common leading indentation from a plain string. * * Plain strings go through the same core idea as `undent` template literals: * find the smallest shared indent across non-blank lines, then remove that * much leading whitespace. * * The key safety rule is simple: only leading spaces and tabs are removed. * The text itself is left alone. * * Spaces and tabs count as individual indentation characters here. The scan * does not expand tabs to visual tab stops, so mixed prefixes are compared by * raw leading character count rather than rendered column width. * * Two-pass approach: * * 1. **Scan** — walk each line, count leading spaces/tabs on non-blank * lines, track the minimum. * 2. **Strip** — remove up to `minIndent` characters from each line. * The first line is sliced directly; remaining lines use a cached * regex so newline bytes are preserved. * 3. **Trim** — apply leading/trailing blank-line trimming. * * Original newline sequences (`\n`, `\r\n`, `\r`) pass through * unchanged. Lines with less indent than the minimum lose only what * they have. * * @param input - The string to strip indent from. * @param trimLeading - How to handle leading blank lines. * @param trimTrailing - How to handle trailing blank lines. * @returns The dedented string. * * @example Stripping indent from a SQL string * ```ts * import { dedentString } from "@okikio/undent"; * * const clean = dedentString(` * SELECT * * FROM users * `); * // "SELECT *\nFROM users" * ``` * * @example Edge case — mixed indent depths: * ```ts * import { dedentString } from "@okikio/undent"; * * dedentString(" deep\n shallow"); * // " deep\nshallow" * // 2 spaces stripped (the minimum); "deep" keeps its extra 2. * ``` * * @example Mixed tabs and spaces are counted by raw characters * ```ts * import { dedentString } from "@okikio/undent"; * * dedentString("\t alpha\n \tbeta"); * // "alpha\nbeta" * // Each line starts with 3 indentation characters, so all 3 are stripped. * ``` */ export declare function dedentString(input: string, trimLeading?: TrimMode, trimTrailing?: TrimMode): string; /** * Pad subsequent lines of a multi-line string with a prefix. * * The first line is left unchanged (it's already at the insertion * point). Blank or whitespace-only lines are skipped to avoid * producing trailing whitespace. Original newline sequences are * preserved. * * Uses a character-level scanner instead of regex/split to minimize * allocations on large inputs. * * @param text - The multi-line string to align. * @param pad - A string (usually spaces) to prepend to lines 2+. * @returns The aligned string. * * @example Padding subsequent lines with a two-space prefix * ```ts * import { alignText } from "@okikio/undent"; * * alignText("a\nb\nc", " "); * // "a\n b\n c" * * alignText("a\r\nb\rc", " "); * // "a\r\n b\r c" — newline sequences preserved byte-for-byte * ``` * * @example Blank and whitespace-only lines are never padded * ```ts * import { alignText } from "@okikio/undent"; * * alignText("a\n\nc", " "); * // "a\n\n c" — blank line in the middle is left unchanged * * alignText("a\n \nc", " "); * // "a\n \n c" — whitespace-only line is also left unchanged * ``` */ export declare function alignText(text: string, pad: string): string; /** * Split a string into lines and their separators, preserving the exact * newline sequences (`\n`, `\r\n`, `\r`). * * Returns two arrays: `lines` (the content between newlines) and * `seps` (the newline sequences). They satisfy * `lines.length === seps.length + 1`, and the original string can be * reconstructed with {@link rejoinLines}. * * Pre-counts newlines to allocate arrays exactly, avoiding repeated * resizing. On 1K-line inputs, the approach is ~2x faster than regex split. * * @param text - The string to split. * @returns An object with `lines` and `seps` arrays. * * @example Splitting a string while preserving newline sequences * ```ts * import { splitLines } from "@okikio/undent"; * * const { lines, seps } = splitLines("hello\r\nworld\nfoo"); * // lines: ["hello", "world", "foo"] * // seps: ["\r\n", "\n"] * ``` */ export declare function splitLines(text: string): { lines: string[]; seps: string[]; }; /** * Reconstruct a string from the output of {@link splitLines}. * * Interleaves lines and separators with a single `join("")` call, * which V8 optimizes by pre-computing total length and copying once. * * @param lines - The content lines. * @param seps - The newline separators between lines. * @returns The reconstructed string. * * @example Round-tripping through split and rejoin * ```ts * import { splitLines, rejoinLines } from "@okikio/undent"; * * const { lines, seps } = splitLines("a\nb\nc"); * rejoinLines(lines, seps); // "a\nb\nc" * ``` */ export declare function rejoinLines(lines: ReadonlyArray, seps: ReadonlyArray): string; /** * Count how far the output has advanced since the last newline. * * Alignment uses this insertion offset to decide how many spaces to add before * later lines of a wrapped value. * * > This is a UTF-16 code-unit offset, not display width. That keeps the * > helper fast and deterministic for string processing, but editors may show * > a different visual column for tabs, emoji, combining marks, or full-width * > characters. * * Uses `lastIndexOf` (implemented in C++ by V8) instead of a charcode * loop for ~100x speedup on long strings. * * @param text - The string to measure. * @returns The number of UTF-16 code units after the final newline, * or the full string length if there are no newlines. * * @example Measuring the insertion column * ```ts * import { columnOffset } from "@okikio/undent"; * * columnOffset("abc\n "); // 2 * columnOffset("abc\r\n "); // 4 * columnOffset("no newline"); // 10 * ``` */ export declare function columnOffset(text: string): number; /** * Return the byte length of a newline sequence at position `i`. * * - `\n` → 1 * - `\r\n` → 2 * - `\r` → 1 * - anything else → 0 * * @param text - The string to inspect. * @param i - The character index to check. * @returns `0`, `1`, or `2`. * * @example Detecting different newline sequence lengths * ```ts * import { newlineLengthAt } from "@okikio/undent"; * * newlineLengthAt("a\nb", 1); // 1 — plain LF * newlineLengthAt("a\r\nb", 1); // 2 — CRLF pair counted as one sequence * newlineLengthAt("a\rb", 1); // 1 — bare CR * newlineLengthAt("abc", 1); // 0 — not a newline character * ``` */ export declare function newlineLengthAt(text: string, i: number): 0 | 1 | 2;