diff --git a/src/utils/auto-generated.ts b/src/utils/auto-generated.ts new file mode 100644 index 0000000..ecceb1d --- /dev/null +++ b/src/utils/auto-generated.ts @@ -0,0 +1,163 @@ +import { pad, splitLines, StringPadOptions } from "@/utils/string-utils"; +import { $i } from "@/utils/collections/iterable"; +import { DEFAULT_NEWLINE } from "@/utils/environment"; + +/** + * Options for generating an auto-generated warning frame. + */ +export interface AutoGeneratedWarningFrameOptions { + /** + * An optional string representing the name of the source file. + * + * If provided, this will be displayed in the generated warning message. + */ + sourceFileName?: string; + + /** + * An optional string representing the warning message to display in the generated warning frame. + * + * If not provided, the default warning message will be used. + */ + message?: string; + + /** + * An optional string or array of strings representing how to align the contents of each line in the generated frame. + * + * If multiple values are provided, they will be used in order for each successive line. + */ + align?: StringPadOptions["align"] | StringPadOptions["align"][]; + + /** + * Options for customizing the style of a frame. + */ + style?: FrameStyle; + + /** + * An optional number representing the maximum length of each line in the generated frame. + * + * If not provided, the length of each line will not be limited. + */ + lineWidth?: number; + + /** + * An optional string representing the character(s) to use for the newline sequence. + * + * If not provided, the value of {@link DEFAULT_NEWLINE} (`\r\n` on Windows, `\n` on Unix) will be used. + */ + newline?: string; +} + +/** + * Options for generating an auto-generated warning frame, with an additional property for toggling whether or not to generate the warning message at all. + */ +export interface OptionalAutoGeneratedWarningFrameOptions extends AutoGeneratedWarningFrameOptions { + /** + * A boolean indicating whether or not to generate an auto-generated warning message in the formatted output. + * + * Defaults to `true`. + */ + generateAutoGeneratedWarningMessage?: boolean; +} + +/** + * Options for customizing the style of a frame. + */ +export interface FrameStyle { + /** + * An optional string to prepend to each line of the generated frame. + * + * If not provided, the value of {@link filler} will be used. + */ + lineStart?: string; + + /** + * An optional string representing the character to use for the frame border. + * + * If not provided, `"#"` will be used. + */ + filler?: string; + + /** + * An optional string to append to each line of the generated frame. + * + * If not provided, the value of {@link filler} will be used. + */ + lineEnd?: string; +} + + +/** + * A predefined frame style for generating YAML-style frames with `#` characters. + */ +export const YAML_FRAME_STYLE: FrameStyle = { filler: "#" }; + +/** + * A predefined frame style for generating JavaScript-style multiline comment frames with `/*...*‎/` syntax. + */ +export const JS_MULTILINE_FRAME_STYLE: FrameStyle = { lineStart: "/* ", filler: "*", lineEnd: " */" }; + +/** + * A predefined frame-style for generating JavaScript-style single-line comment frames with `//` syntax. + */ +export const JS_SINGLELINE_FRAME_STYLE: FrameStyle = { filler: "//" }; + +/** + * The default frame style to use if no style is specified. + * + * Uses the `YAML_FRAME_STYLE` style with `#` characters. + */ +export const DEFAULT_FRAME_STYLE: FrameStyle = YAML_FRAME_STYLE; + +/** + * The default alignment settings to use for the contents of each line in the generated frame. + */ +export const DEFAULT_FRAME_ALIGN = ["center"] as const; + + +/** + * Generates a warning message that indicates the file is auto-generated and should not be edited. + * + * @param sourceFileName - An optional string that represents the name of the source file. If provided, the warning message will include instructions for modifying the source file instead of the auto-generated file. + * + * @returns A warning message that indicates the file is auto-generated and should not be edited. + */ +export function generateAutoGeneratedWarningText(sourceFileName?: string): string { + const baseWarning = "WARNING: AUTO-GENERATED FILE - DO NOT EDIT!\n\nPlease be advised that this is an auto-generated file and should NOT be modified. Any changes made to this file WILL BE OVERWRITTEN."; + if (!sourceFileName) { + return baseWarning; + } + + return `${baseWarning}\n\nTo make changes to the contents of this file, please modify the ${sourceFileName} file instead. This will ensure that your changes are properly reflected in the auto-generated file.`; +} + +/** + * Generates a warning frame containing an auto-generated warning message. + * + * @param options - Options for generating the warning frame. + * + * @returns A string representing the generated warning frame. + */ +export function generateAutoGeneratedWarningFrame(options?: AutoGeneratedWarningFrameOptions): string { + const message = options?.message ?? generateAutoGeneratedWarningText(options?.sourceFileName); + const align = Array.isArray(options?.align) ? options.align : typeof options?.align === "string" ? [options.align] : DEFAULT_FRAME_ALIGN; + const filler = options?.style?.filler ?? DEFAULT_FRAME_STYLE.filler; + const lineStart = options?.style?.lineStart ?? `${filler} `; + const lineEnd = options?.style?.lineEnd ?? ` ${filler}`; + const newline = options?.newline ?? DEFAULT_NEWLINE + + const minLineLength = lineStart.length + lineEnd.length; + const maxLineLength = Math.max((options?.lineWidth || 0) - minLineLength, 0); + const lines = splitLines(message, { maxLength: maxLineLength }); + + const frameSize = $i(lines).map(x => x.length).max() || 0; + const fillerCount = Math.ceil(frameSize / filler.length); + const frameLine = `${lineStart}${filler.repeat(fillerCount)}${lineEnd}`; + const builtFrame = $i(lines) + .map((x, i) => pad(x, frameSize, { align: align[Math.min(i, align.length - 1)] })) + .map(x => `${lineStart}${x}${lineEnd}`) + .append(frameLine) + .prepend(frameLine) + .join(newline); + + return builtFrame; +} diff --git a/tests/unit/utils/auto-generated.spec.ts b/tests/unit/utils/auto-generated.spec.ts new file mode 100644 index 0000000..3e69497 --- /dev/null +++ b/tests/unit/utils/auto-generated.spec.ts @@ -0,0 +1,95 @@ +import { generateAutoGeneratedWarningText, generateAutoGeneratedWarningFrame, YAML_FRAME_STYLE } from "@/utils/auto-generated"; + +describe("generateAutoGeneratedWarningText", () => { + test("generates the correct warning text without a source file name", () => { + const warning = generateAutoGeneratedWarningText(); + + expect(warning).toMatch("WARNING: AUTO-GENERATED FILE - DO NOT EDIT!"); + expect(warning).not.toMatch("To make changes to the contents of this file, please modify"); + }); + + test("generates the correct warning text with a source file name", () => { + const warning = generateAutoGeneratedWarningText("test.ts"); + + expect(warning).toMatch("WARNING: AUTO-GENERATED FILE - DO NOT EDIT!"); + expect(warning).toMatch("To make changes to the contents of this file, please modify the test.ts file instead."); + }); +}); + +describe("generateAutoGeneratedWarningFrame", () => { + test("generates a warning frame with default options", () => { + const frame = generateAutoGeneratedWarningFrame(); + + expect(frame).toMatch(/^#\s+WARNING: AUTO-GENERATED FILE - DO NOT EDIT!\s+#$/m); + expect(frame).toMatch(/^#\s+Please be advised that this is an auto-generated file and should NOT be modified. Any changes made to this file WILL BE OVERWRITTEN.\s+#$/m); + }); + + test("generates a warning frame with a custom message", () => { + const frame = generateAutoGeneratedWarningFrame({ message: "Custom message" }); + + expect(frame).toMatch(/^#\sCustom message\s#$/m); + }); + + test("generates a warning frame with custom frame style", () => { + const frame = generateAutoGeneratedWarningFrame({ + style: { + lineStart: "/** ", + filler: "=", + lineEnd: " **/", + }, + }); + expect(frame).toMatch(/^\/\*\*\s=+\s\*\*\/$/m); + expect(frame).toMatch(/^\/\*\*\s+WARNING: AUTO-GENERATED FILE - DO NOT EDIT!\s+\*\*\/$/m); + expect(frame).toMatch(/^\/\*\*\s+Please be advised that this is an auto-generated file and should NOT be modified. Any changes made to this file WILL BE OVERWRITTEN.\s+\*\*\/$/m); + }); + + test("generates a warning frame respecting the lineWidth option", () => { + const frame = generateAutoGeneratedWarningFrame({ lineWidth: 40 }); + const lines = frame.split(/\r?\n/); + const lineWidths = new Set(lines.map(x => x.length)); + const lineWidth = [...lineWidths.values()][0]; + + expect(lineWidths.size).toBe(1); + expect(Math.abs(40 - lineWidth)).toBeLessThanOrEqual(1); + }); + + test("aligns message to the left when align option is 'left'", () => { + const frame = generateAutoGeneratedWarningFrame({ align: "left" }); + + expect(frame).toMatch(/^#\sWARNING: AUTO-GENERATED FILE - DO NOT EDIT!\s{2,}#$/m); + }); + + test("aligns message to the center when align option is 'center'", () => { + const frame = generateAutoGeneratedWarningFrame({ align: "center" }); + + expect(frame).toMatch(/^#\s{2,}WARNING: AUTO-GENERATED FILE - DO NOT EDIT!\s{2,}#$/m); + }); + + test("aligns message to the right when align option is 'right'", () => { + const frame = generateAutoGeneratedWarningFrame({ align: "right" }); + + expect(frame).toMatch(/^#\s{2,}WARNING: AUTO-GENERATED FILE - DO NOT EDIT!\s#$/m); + }); + + test("uses \\n as newline sequence when newline option is '\\n'", () => { + const frame = generateAutoGeneratedWarningFrame({ newline: "\n" }); + + expect(frame).toMatch(/\n/); + expect(frame).not.toMatch(/\r\n/); + }); + + test("uses \\r\\n as newline sequence when newline option is '\\r\\n'", () => { + const frame = generateAutoGeneratedWarningFrame({ newline: "\r\n" }); + + expect(frame).toMatch(/\r\n/); + expect(frame).not.toMatch(/[^\r]\n/); + }); + + test("uses custom string as newline sequence when newline option is a custom string", () => { + const frame = generateAutoGeneratedWarningFrame({ newline: "NEWLINE" }); + + expect(frame).toMatch(/NEWLINE/); + expect(frame).not.toMatch(/\n/); + expect(frame).not.toMatch(/\r\n/); + }); +});