diff --git a/internal/build_message.ts b/internal/build_message.ts index 396405726..d7bf29bce 100644 --- a/internal/build_message.ts +++ b/internal/build_message.ts @@ -11,16 +11,26 @@ import type { DiffResult, DiffType } from "./types.ts"; * @param background If true, colors the background instead of the text. * * @returns A function that colors the input string. + * + * @example Usage + * ```ts + * import { createColor } from "@std/internal"; + * import { assertEquals } from "@std/assert/assert-equals"; + * import { bold, green, red, white } from "@std/fmt/colors"; + * + * assertEquals(createColor("added")("foo"), green(bold("foo"))); + * assertEquals(createColor("removed")("foo"), red(bold("foo"))); + * assertEquals(createColor("common")("foo"), white("foo")); + * ``` */ -function createColor( +export function createColor( diffType: DiffType, - background = false, -): (s: string) => string { /** * TODO(@littledivy): Remove this when we can detect true color terminals. See * https://github.com/denoland/deno_std/issues/2575. */ - background = false; + background = false, +): (s: string) => string { switch (diffType) { case "added": return (s) => background ? bgGreen(white(s)) : green(bold(s)); @@ -37,8 +47,18 @@ function createColor( * @param diffType Difference type, either added or removed * * @returns A string representing the sign. + * + * @example Usage + * ```ts + * import { createSign } from "@std/internal"; + * import { assertEquals } from "@std/assert/assert-equals"; + * + * assertEquals(createSign("added"), "+ "); + * assertEquals(createSign("removed"), "- "); + * assertEquals(createSign("common"), " "); + * ``` */ -function createSign(diffType: DiffType): string { +export function createSign(diffType: DiffType): string { switch (diffType) { case "added": return "+ "; diff --git a/internal/build_message_test.ts b/internal/build_message_test.ts new file mode 100644 index 000000000..2525894fd --- /dev/null +++ b/internal/build_message_test.ts @@ -0,0 +1,41 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { assertEquals } from "@std/assert"; +import { bgGreen, bgRed, bold, gray, green, red, white } from "@std/fmt/colors"; +import { buildMessage, createColor, createSign } from "./build_message.ts"; + +Deno.test("buildMessage()", () => { + const messages = [ + "", + "", + ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${ + green(bold("Expected")) + }`, + "", + "", + ]; + assertEquals(buildMessage([]), [...messages, ""]); + assertEquals( + buildMessage([{ type: "added", value: "foo" }, { + type: "removed", + value: "bar", + }]), + [...messages, green(bold("+ foo")), red(bold("- bar")), ""], + ); +}); + +Deno.test("createColor()", () => { + assertEquals(createColor("added")("foo"), green(bold("foo"))); + assertEquals(createColor("removed")("foo"), red(bold("foo"))); + assertEquals(createColor("common")("foo"), white("foo")); + assertEquals(createColor("added", true)("foo"), bgGreen(white("foo"))); + assertEquals(createColor("removed", true)("foo"), bgRed(white("foo"))); + assertEquals(createColor("common", true)("foo"), white("foo")); +}); + +Deno.test("createSign()", () => { + assertEquals(createSign("added"), "+ "); + assertEquals(createSign("removed"), "- "); + assertEquals(createSign("common"), " "); + // deno-lint-ignore no-explicit-any + assertEquals(createSign("unknown" as any), " "); +}); diff --git a/internal/diff.ts b/internal/diff.ts index aea90cb9f..b5835b718 100644 --- a/internal/diff.ts +++ b/internal/diff.ts @@ -3,8 +3,11 @@ import type { DiffResult, DiffType } from "./types.ts"; -interface FarthestPoint { +/** Represents the farthest point in the diff algorithm. */ +export interface FarthestPoint { + /** The y-coordinate of the point. */ y: number; + /** The id of the point. */ id: number; } @@ -21,8 +24,19 @@ const ADDED = 3; * @param B The second array. * * @returns An array containing the common elements between the two arrays. + * + * @example Usage + * ```ts + * import { createCommon } from "@std/internal/diff"; + * import { assertEquals } from "@std/assert/assert-equals"; + * + * const a = [1, 2, 3]; + * const b = [1, 2, 4]; + * + * assertEquals(createCommon(a, b), [1, 2]); + * ``` */ -function createCommon(A: T[], B: T[]): T[] { +export function createCommon(A: T[], B: T[]): T[] { const common: T[] = []; if (A.length === 0 || B.length === 0) return []; for (let i = 0; i < Math.min(A.length, B.length); i += 1) { @@ -37,13 +51,62 @@ function createCommon(A: T[], B: T[]): T[] { return common; } -function assertFp(value: unknown): asserts value is FarthestPoint { - if (value === undefined) { +/** + * Asserts that the value is a {@linkcode FarthestPoint}. + * If not, an error is thrown. + * + * @param value The value to check. + * + * @returns A void value that returns once the assertion completes. + * + * @example Usage + * ```ts + * import { assertFp } from "@std/internal/diff"; + * import { assertThrows } from "@std/assert/assert-throws"; + * + * assertFp({ y: 0, id: 0 }); + * assertThrows(() => assertFp({ id: 0 })); + * assertThrows(() => assertFp({ y: 0 })); + * assertThrows(() => assertFp(undefined)); + * ``` + */ +export function assertFp(value: unknown): asserts value is FarthestPoint { + if ( + value == null || + typeof value !== "object" || + typeof (value as FarthestPoint)?.y !== "number" || + typeof (value as FarthestPoint)?.id !== "number" + ) { throw new Error("Unexpected missing FarthestPoint"); } } -function backTrace( +/** + * Creates an array of backtraced differences. + * + * @typeParam T The type of elements in the arrays. + * + * @param A The first array. + * @param B The second array. + * @param current The current {@linkcode FarthestPoint}. + * @param swapped Boolean indicating if the arrays are swapped. + * @param routes The routes array. + * @param diffTypesPtrOffset The offset of the diff types in the routes array. + * + * @returns An array of backtraced differences. + * + * @example Usage + * ```ts + * import { backTrace } from "@std/internal/diff"; + * import { assertEquals } from "@std/assert/assert-equals"; + * + * assertEquals( + * backTrace([], [], { y: 0, id: 0 }, false, new Uint32Array(0), 0), + * [], + * ); + * ``` + */ +export function backTrace( A: T[], B: T[], current: FarthestPoint, @@ -87,7 +150,39 @@ function backTrace( return result; } -function createFp( +/** + * Creates a {@linkcode FarthestPoint}. + * + * @param k The current index. + * @param M The length of the first array. + * @param routes The routes array. + * @param diffTypesPtrOffset The offset of the diff types in the routes array. + * @param ptr The current pointer. + * @param slide The slide {@linkcode FarthestPoint}. + * @param down The down {@linkcode FarthestPoint}. + * + * @returns A {@linkcode FarthestPoint}. + * + * @example Usage + * ```ts + * import { createFp } from "@std/internal/diff"; + * import { assertEquals } from "@std/assert/assert-equals"; + * + * assertEquals( + * createFp( + * 0, + * 0, + * new Uint32Array(0), + * 0, + * 0, + * { y: -1, id: 0 }, + * { y: 0, id: 0 }, + * ), + * { y: -1, id: 1 }, + * ); + * ``` + */ +export function createFp( k: number, M: number, routes: Uint32Array, @@ -147,22 +242,17 @@ function createFp( */ export function diff(A: T[], B: T[]): DiffResult[] { const prefixCommon = createCommon(A, B); - const suffixCommon = createCommon( - A.slice(prefixCommon.length), - B.slice(prefixCommon.length), - ); - A = A.slice(prefixCommon.length, -suffixCommon.length || undefined); - B = B.slice(prefixCommon.length, -suffixCommon.length || undefined); + A = A.slice(prefixCommon.length); + B = B.slice(prefixCommon.length); const swapped = B.length > A.length; [A, B] = swapped ? [B, A] : [A, B]; const M = A.length; const N = B.length; - if (!M && !N && !suffixCommon.length && !prefixCommon.length) return []; + if (!M && !N && !prefixCommon.length) return []; if (!N) { return [ ...prefixCommon.map((value) => ({ type: "common", value })), ...A.map((value) => ({ type: swapped ? "added" : "removed", value })), - ...suffixCommon.map((value) => ({ type: "common", value })), ] as DiffResult[]; } const offset = N; @@ -187,7 +277,6 @@ export function diff(A: T[], B: T[]): DiffResult[] { ): FarthestPoint { const M = A.length; const N = B.length; - if (k < -N || M < k) return { y: -1, id: -1 }; const fp = createFp(k, M, routes, diffTypesPtrOffset, ptr, slide, down); ptr = fp.id; while (fp.y + k < M && fp.y < N && A[fp.y + k] === B[fp.y]) { @@ -222,6 +311,5 @@ export function diff(A: T[], B: T[]): DiffResult[] { return [ ...prefixCommon.map((value) => ({ type: "common", value })), ...backTrace(A, B, currentFp, swapped, routes, diffTypesPtrOffset), - ...suffixCommon.map((value) => ({ type: "common", value })), ] as DiffResult[]; } diff --git a/internal/diff_str.ts b/internal/diff_str.ts index e2797429a..b16037372 100644 --- a/internal/diff_str.ts +++ b/internal/diff_str.ts @@ -10,8 +10,16 @@ import { diff } from "./diff.ts"; * @param string String to unescape. * * @returns Unescaped string. + * + * @example Usage + * ```ts + * import { unescape } from "@std/internal/diff-str"; + * import { assertEquals } from "@std/assert/assert-equals"; + * + * assertEquals(unescape("Hello\nWorld"), "Hello\\n\nWorld"); + * ``` */ -function unescape(string: string): string { +export function unescape(string: string): string { return string .replaceAll("\b", "\\b") .replaceAll("\f", "\\f") @@ -25,8 +33,6 @@ function unescape(string: string): string { } const WHITESPACE_SYMBOLS = /([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/; -const EXT_LATIN_CHARS = - /^[a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}]+$/u; /** * Tokenizes a string into an array of tokens. @@ -35,26 +41,20 @@ const EXT_LATIN_CHARS = * @param wordDiff If true, performs word-based tokenization. Default is false. * * @returns An array of tokens. + * + * @example Usage + * ```ts + * import { tokenize } from "@std/internal/diff-str"; + * import { assertEquals } from "@std/assert/assert-equals"; + * + * assertEquals(tokenize("Hello\nWorld"), ["Hello\n", "World"]); + * ``` */ -function tokenize(string: string, wordDiff = false): string[] { +export function tokenize(string: string, wordDiff = false): string[] { if (wordDiff) { - const tokens = string.split(WHITESPACE_SYMBOLS).filter((token) => token); - for (let i = 0; i < tokens.length - 1; i++) { - const token = tokens[i]; - const tokenPlusTwo = tokens[i + 2]; - if ( - !tokens[i + 1] && - token && - tokenPlusTwo && - EXT_LATIN_CHARS.test(token) && - EXT_LATIN_CHARS.test(tokenPlusTwo) - ) { - tokens[i] += tokenPlusTwo; - tokens.splice(i + 1, 2); - i--; - } - } - return tokens; + return string + .split(WHITESPACE_SYMBOLS) + .filter((token) => token); } const tokens: string[] = []; const lines = string.split(/(\n|\r\n)/).filter((line) => line); @@ -77,11 +77,27 @@ function tokenize(string: string, wordDiff = false): string[] { * @param tokens Word-diff tokens * * @returns Array of diff results. + * + * @example Usage + * ```ts + * import { createDetails } from "@std/internal/diff-str"; + * import { assertEquals } from "@std/assert/assert-equals"; + * + * const tokens = [ + * { type: "added", value: "a" }, + * { type: "removed", value: "b" }, + * { type: "common", value: "c" }, + * ] as const; + * assertEquals( + * createDetails({ type: "added", value: "a" }, [...tokens]), + * [{ type: "added", value: "a" }, { type: "common", value: "c" }] + * ); + * ``` */ -function createDetails( +export function createDetails( line: DiffResult, - tokens: Array>, -) { + tokens: DiffResult[], +): DiffResult[] { return tokens.filter(({ type }) => type === line.type || type === "common") .map((result, i, t) => { const token = t[i - 1]; @@ -163,7 +179,7 @@ export function diffStr(A: string, B: string): DiffResult[] { b = bLines.shift(); const tokenized = [ tokenize(a.value, true), - tokenize(b?.value ?? "", true), + tokenize(b!.value, true), ] as [string[], string[]]; if (hasMoreRemovedLines) tokenized.reverse(); tokens = diff(tokenized[0], tokenized[1]); diff --git a/internal/diff_str_test.ts b/internal/diff_str_test.ts index b7bb4fcfc..8aa367e6a 100644 --- a/internal/diff_str_test.ts +++ b/internal/diff_str_test.ts @@ -1,8 +1,33 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { diffStr } from "./diff_str.ts"; +import { createDetails, diffStr, tokenize, unescape } from "./diff_str.ts"; import { assertEquals } from "@std/assert/assert-equals"; +Deno.test({ + name: 'diff() "a" vs "b" (diffstr)', + fn() { + const diffResult = diffStr("a", "b"); + assertEquals(diffResult, [ + { + details: [ + { type: "removed", value: "a" }, + { type: "common", value: "\n" }, + ], + type: "removed", + value: "a\n", + }, + { + details: [ + { type: "added", value: "b" }, + { type: "common", value: "\n" }, + ], + type: "added", + value: "b\n", + }, + ]); + }, +}); + Deno.test({ name: 'diff() "a b c d" vs "a b x d e" (diffStr)', fn() { @@ -236,3 +261,47 @@ Deno.test({ ]); }, }); + +Deno.test({ + name: "createDetails()", + fn() { + const tokens = [ + { type: "added", value: "a" }, + { type: "removed", value: "b" }, + { type: "common", value: "c" }, + ] as const; + for (const token of tokens) { + assertEquals( + createDetails(token, [...tokens]), + tokens.filter(({ type }) => type === token.type || type === "common"), + ); + } + }, +}); + +Deno.test({ + name: "tokenize()", + fn() { + assertEquals(tokenize("a\nb"), ["a\n", "b"]); + assertEquals(tokenize("a\r\nb"), ["a\r\n", "b"]); + assertEquals(tokenize("a\nb\n"), ["a\n", "b\n"]); + assertEquals(tokenize("a b"), ["a b"]); + assertEquals(tokenize("a b", true), ["a", " ", "b"]); + assertEquals(tokenize("abc bcd", true), ["abc", " ", "bcd"]); + assertEquals(tokenize("abc ", true), ["abc", " "]); + }, +}); + +Deno.test({ + name: "unescape()", + fn() { + assertEquals(unescape("Hello\nWorld"), "Hello\\n\nWorld"); + assertEquals(unescape("a\b"), "a\\b"); + assertEquals(unescape("a\f"), "a\\f"); + assertEquals(unescape("a\t"), "a\\t"); + assertEquals(unescape("a\v"), "a\\v"); + assertEquals(unescape("a\r"), "a\\r"); + assertEquals(unescape("a\n"), "a\\n\n"); + assertEquals(unescape("a\r\n"), "a\\r\\n\r\n"); + }, +}); diff --git a/internal/diff_test.ts b/internal/diff_test.ts index 43e473a8c..d2f014a84 100644 --- a/internal/diff_test.ts +++ b/internal/diff_test.ts @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { diff } from "./diff.ts"; -import { assertEquals } from "@std/assert/assert-equals"; +import { assertFp, backTrace, createCommon, createFp, diff } from "./diff.ts"; +import { assertEquals, assertThrows } from "@std/assert"; Deno.test({ name: "diff() with empty values", @@ -51,6 +51,18 @@ Deno.test({ }, }); +Deno.test({ + name: 'diff() "a, x, c" vs "a, b, c"', + fn() { + assertEquals(diff(["a", "x", "c"], ["a", "b", "c"]), [ + { type: "common", value: "a" }, + { type: "removed", value: "x" }, + { type: "added", value: "b" }, + { type: "common", value: "c" }, + ]); + }, +}); + Deno.test({ name: 'diff() "strength" vs "string"', fn() { @@ -110,3 +122,113 @@ Deno.test({ ]); }, }); + +Deno.test({ + name: "assertFp()", + fn() { + const fp = { y: 0, id: 0 }; + assertEquals(assertFp(fp), undefined); + }, +}); + +Deno.test({ + name: "assertFp() throws", + fn() { + const error = "Unexpected missing FarthestPoint"; + assertThrows(() => assertFp({ id: 0 }), Error, error); + assertThrows(() => assertFp({ y: 0 }), Error, error); + assertThrows(() => assertFp(undefined), Error, error); + assertThrows(() => assertFp(null), Error, error); + }, +}); + +Deno.test({ + name: "backTrace()", + fn() { + assertEquals( + backTrace([], [], { y: 0, id: 0 }, false, new Uint32Array(0), 0), + [], + ); + assertEquals( + backTrace(["a"], ["b"], { y: 1, id: 3 }, false, new Uint32Array(10), 5), + [], + ); + }, +}); + +Deno.test({ + name: "createCommon()", + fn() { + assertEquals(createCommon([], []), []); + assertEquals(createCommon([1], []), []); + assertEquals(createCommon([], [1]), []); + assertEquals(createCommon([1], [1]), [1]); + assertEquals(createCommon([1, 2], [1]), [1]); + assertEquals(createCommon([1], [1, 2]), [1]); + }, +}); + +Deno.test({ + name: "createFp()", + fn() { + assertEquals( + createFp( + 0, + 0, + new Uint32Array(0), + 0, + 0, + { y: -1, id: 0 }, + { y: -1, id: 0 }, + ), + { y: 0, id: 0 }, + ); + }, +}); + +Deno.test({ + name: 'createFp() "isAdding"', + fn() { + assertEquals( + createFp( + 0, + 0, + new Uint32Array(0), + 0, + 0, + { y: 0, id: 0 }, + { y: -1, id: 0 }, + ), + { y: 0, id: 1 }, + ); + }, +}); + +Deno.test({ + name: 'createFp() "!isAdding"', + fn() { + assertEquals( + createFp( + 0, + 0, + new Uint32Array(0), + 0, + 0, + { y: -1, id: 0 }, + { y: 0, id: 0 }, + ), + { y: -1, id: 1 }, + ); + }, +}); + +Deno.test({ + name: "createFp() throws", + fn() { + assertThrows( + () => createFp(0, 0, new Uint32Array(0), 0, 0), + Error, + "Unexpected missing FarthestPoint", + ); + }, +}); diff --git a/internal/format_test.ts b/internal/format_test.ts index 23e03d01a..88a1c476d 100644 --- a/internal/format_test.ts +++ b/internal/format_test.ts @@ -96,3 +96,16 @@ Deno.test("format() doesn't truncate long strings in object", () => { }`, ); }); + +Deno.test("format() has fallback to String if Deno.inspect is not available", () => { + // Simulates the environment where Deno.inspect is not available + const inspect = Deno.inspect; + // deno-lint-ignore no-explicit-any + delete (Deno as any).inspect; + try { + assertEquals(format([..."abcd"]), `"a,b,c,d"`); + assertEquals(format({ a: 1, b: 2 }), `"[object Object]"`); + } finally { + Deno.inspect = inspect; + } +}); diff --git a/internal/styles_test.ts b/internal/styles_test.ts index 7c19e9c18..3216be54b 100644 --- a/internal/styles_test.ts +++ b/internal/styles_test.ts @@ -51,3 +51,30 @@ Deno.test("stripAnsiCode()", function () { "foofoo", ); }); + +Deno.test("noColor", async function () { + const fixtures = [ + ["true", "foo bar\n"], + ["1", "foo bar\n"], + ["", "foo bar\n"], + ] as const; + + const code = ` + import * as c from "${import.meta.resolve("./styles.ts")}"; + console.log(c.red("foo bar")); + `; + + for await (const [fixture, expected] of fixtures) { + const command = new Deno.Command(Deno.execPath(), { + args: ["eval", "--no-lock", code], + clearEnv: true, + env: { + NO_COLOR: fixture, + }, + }); + const { stdout } = await command.output(); + const decoder = new TextDecoder(); + const output = decoder.decode(stdout); + assertEquals(output, expected); + } +});