Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions text/unstable_dedent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,22 +90,52 @@ export function dedent(
const commonPrefix = longestCommonPrefix(linesToCheck);
const indent = commonPrefix.match(INDENT_REGEXP)?.[0];

const inputString = typeof input === "string"
? input
: String.raw({ raw: input }, ...values);
const trimmedInput = inputString.replace(/^\n/, "").replace(/\n[\t ]*$/, "");
const whitespaceOnlyLineRegex = new RegExp(
WHITE_SPACE_ONLY_LINE_REGEXP,
WHITE_SPACE_ONLY_LINE_REGEXP.flags + "g",
);

// No lines to indent
if (!indent) return trimmedInput;
if (typeof input === "string") {
const trimmedInput = input.replace(/^\n/, "").replace(/\n[\t ]*$/, "");
if (!indent) return trimmedInput;
const minIndentRegex = new RegExp(String.raw`^${indent}`, "gmu");
return trimmedInput
.replaceAll(minIndentRegex, "")
.replaceAll(whitespaceOnlyLineRegex, "");
}

// #6830: previously the indent regex was applied to the fully substituted
// input string, which also stripped leading whitespace from substitution
// values whenever that whitespace happened to match the outer template's
// computed indent. Apply indent stripping only to the literal template
// parts so a multi-line substituted value keeps its own indentation
// verbatim.
const stripIndentAfterNewlineRegex = indent
? new RegExp(String.raw`\n${indent}`, "gu")
: null;
const stripIndentAtStartRegex = indent
? new RegExp(String.raw`^${indent}`, "u")
: null;

const minIndentRegex = new RegExp(String.raw`^${indent}`, "gmu");
return trimmedInput
.replaceAll(minIndentRegex, "")
.replaceAll(
new RegExp(
WHITE_SPACE_ONLY_LINE_REGEXP,
WHITE_SPACE_ONLY_LINE_REGEXP.flags + "g",
),
"",
);
let assembled = "";
for (let i = 0; i < input.raw.length; i++) {
let part = input.raw[i] ?? "";
if (stripIndentAfterNewlineRegex) {
part = part.replaceAll(stripIndentAfterNewlineRegex, "\n");
}
// The very first literal char is at the start of the template (no
// preceding newline), so leading-indent there is not caught by the
// `\n${indent}` rule above.
if (i === 0 && stripIndentAtStartRegex) {
part = part.replace(stripIndentAtStartRegex, "");
}
assembled += part;
if (i < values.length) {
assembled += String(values[i]);
}
}

const trimmedInput = assembled.replace(/^\n/, "").replace(/\n[\t ]*$/, "");
if (!indent) return trimmedInput;
return trimmedInput.replaceAll(whitespaceOnlyLineRegex, "");
}
45 changes: 45 additions & 0 deletions text/unstable_dedent_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,51 @@ import { dedent } from "./unstable_dedent.ts";
import { assertEquals } from "@std/assert";
import { stub } from "@std/testing/mock";

Deno.test("dedent() preserves indentation inside multi-line substitution values (#6830)", () => {
// The substituted `inner` has lines with 4 and 6 spaces of leading
// whitespace. The outer template's computed indent is 6 spaces.
// Previously the regex stripped 6 spaces from every line including the
// substituted content's " ...", turning it into "...". The fix
// applies indent stripping only to literal template parts so the
// substitution survives unchanged.
const inner = dedent`
[
...
...
]
`;
assertEquals(inner, " [\n ...\n...\n ]");

const outerV1 = dedent`
a
b
${inner}
`;
assertEquals(outerV1, `a\n b\n${inner}`);

// V2 has a wider outer indent (8 spaces vs 6) but the same substitution.
// Should produce the identical output — V1 and V2 were diverging before
// the fix.
const outerV2 = dedent`
a
b
${inner}
`;
assertEquals(outerV2, `a\n b\n${inner}`);
});

Deno.test("dedent() does not strip indent that originated inside a substituted value", () => {
const sub = " leading-four-spaces";
const result = dedent`
prefix
${sub}
`;
// The outer indent is 6 spaces. The substituted line starts with 4
// spaces. Those 4 spaces are part of `sub`, not part of the literal
// template's indent, so they must survive.
assertEquals(result, `prefix\n${sub}`);
});

Deno.test("dedent() handles example 1", () => {
assertEquals(
dedent(`
Expand Down
Loading