diff --git a/scripts/verify-build-artifacts.js b/scripts/verify-build-artifacts.js index 4c32dabeb..b29354aac 100644 --- a/scripts/verify-build-artifacts.js +++ b/scripts/verify-build-artifacts.js @@ -91,7 +91,7 @@ async function verifyFilesExist(directoryPath, filePatterns) { if (!match) { die( `No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``, - `\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`, + `\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`, ); } } diff --git a/src/application/Common/Text/FilterEmptyStrings.ts b/src/application/Common/Text/FilterEmptyStrings.ts new file mode 100644 index 000000000..aca4e2a95 --- /dev/null +++ b/src/application/Common/Text/FilterEmptyStrings.ts @@ -0,0 +1,25 @@ +import { isArray } from '@/TypeHelpers'; + +export type OptionalString = string | undefined | null; + +export function filterEmptyStrings( + texts: readonly OptionalString[], + isArrayType: typeof isArray = isArray, +): string[] { + if (!isArrayType(texts)) { + throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`); + } + assertArrayItemsAreStringLike(texts); + return texts + .filter((title): title is string => Boolean(title)); +} + +function assertArrayItemsAreStringLike( + texts: readonly unknown[], +): asserts texts is readonly OptionalString[] { + const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null)); + if (invalidItems.length > 0) { + const invalidTypes = invalidItems.map((item) => typeof item).join(', '); + throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`); + } +} diff --git a/src/application/Common/Text/IndentText.ts b/src/application/Common/Text/IndentText.ts new file mode 100644 index 000000000..ed7d104a2 --- /dev/null +++ b/src/application/Common/Text/IndentText.ts @@ -0,0 +1,29 @@ +import { isString } from '@/TypeHelpers'; +import { splitTextIntoLines } from './SplitTextIntoLines'; + +export function indentText( + text: string, + indentLevel = 1, + utilities: TextIndentationUtilities = DefaultUtilities, +): string { + if (!utilities.isStringType(text)) { + throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`); + } + if (indentLevel <= 0) { + throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`); + } + const indentation = '\t'.repeat(indentLevel); + return utilities.splitIntoLines(text) + .map((line) => (line ? `${indentation}${line}` : line)) + .join('\n'); +} + +interface TextIndentationUtilities { + readonly splitIntoLines: typeof splitTextIntoLines; + readonly isStringType: typeof isString; +} + +const DefaultUtilities: TextIndentationUtilities = { + splitIntoLines: splitTextIntoLines, + isStringType: isString, +}; diff --git a/src/application/Common/Text/SplitTextIntoLines.ts b/src/application/Common/Text/SplitTextIntoLines.ts new file mode 100644 index 000000000..a57871c9b --- /dev/null +++ b/src/application/Common/Text/SplitTextIntoLines.ts @@ -0,0 +1,11 @@ +import { isString } from '@/TypeHelpers'; + +export function splitTextIntoLines( + text: string, + isStringType = isString, +): string[] { + if (!isStringType(text)) { + throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`); + } + return text.split(/\r\n|\r|\n/); +} diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts index aa99a78ed..9eb526a80 100644 --- a/src/application/Context/State/Code/Event/CodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -1,6 +1,7 @@ import type { Script } from '@/domain/Executables/Script/Script'; import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; import type { ICodeChangedEvent } from './ICodeChangedEvent'; export class CodeChangedEvent implements ICodeChangedEvent { @@ -52,12 +53,12 @@ export class CodeChangedEvent implements ICodeChangedEvent { } function ensureAllPositionsExist(script: string, positions: ReadonlyArray) { - const totalLines = script.split(/\r\n|\r|\n/).length; + const totalLines = splitTextIntoLines(script).length; // TODO: Test it preserves empty lines const missingPositions = positions.filter((position) => position.endLine > totalLines); if (missingPositions.length > 0) { throw new Error( `Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"` - + `(total code lines: ${totalLines}).`, + + ` (total code lines: ${totalLines}).`, ); } } diff --git a/src/application/Context/State/Code/Generation/CodeBuilder.ts b/src/application/Context/State/Code/Generation/CodeBuilder.ts index 55cc5be3a..1ab2a6722 100644 --- a/src/application/Context/State/Code/Generation/CodeBuilder.ts +++ b/src/application/Context/State/Code/Generation/CodeBuilder.ts @@ -1,3 +1,4 @@ +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; import type { ICodeBuilder } from './ICodeBuilder'; const TotalFunctionSeparatorChars = 58; @@ -15,7 +16,7 @@ export abstract class CodeBuilder implements ICodeBuilder { this.lines.push(''); return this; } - const lines = code.match(/[^\r\n]+/g); + const lines = splitTextIntoLines(code); if (lines) { this.lines.push(...lines); } diff --git a/src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts b/src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts index 797456772..f586d24de 100644 --- a/src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts +++ b/src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts @@ -1,3 +1,4 @@ +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; import type { IPipe } from '../IPipe'; export class InlinePowerShell implements IPipe { @@ -89,10 +90,6 @@ function inlineComments(code: string): string { */ } -function getLines(code: string): string[] { - return (code?.split(/\r\n|\r|\n/) || []); -} - /* Merges inline here-strings to a single lined string with Windows line terminator (\r\n) https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings @@ -102,7 +99,7 @@ function mergeHereStrings(code: string) { return code.replaceAll(regex, (_$, quotes, scope) => { const newString = getHereStringHandler(quotes); const escaped = scope.replaceAll(quotes, newString.escapedQuotes); - const lines = getLines(escaped); + const lines = splitTextIntoLines(escaped); const inlined = lines.join(newString.separator); const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`; return quoted; @@ -159,7 +156,7 @@ function mergeLinesWithBacktick(code: string) { } function mergeNewLines(code: string) { - return getLines(code) + return splitTextIntoLines(code) .map((line) => line.trim()) .filter((line) => line.length > 0) .join('; '); diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts index 74fa0b129..fd0b97e56 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts @@ -1,3 +1,4 @@ +import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings'; import type { CompiledCode } from '../CompiledCode'; import type { CodeSegmentMerger } from './CodeSegmentMerger'; @@ -8,11 +9,9 @@ export class NewlineCodeSegmentMerger implements CodeSegmentMerger { } return { code: joinCodeParts(codeSegments.map((f) => f.code)), - revertCode: joinCodeParts( - codeSegments - .map((f) => f.revertCode) - .filter((code): code is string => Boolean(code)), - ), + revertCode: joinCodeParts(filterEmptyStrings( + codeSegments.map((f) => f.revertCode), + )), }; } } diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts index 9f4cfe633..57b031618 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts @@ -3,6 +3,7 @@ import type { IExpressionsCompiler } from '@/application/Parser/Executable/Scrip import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { indentText } from '@/application/Common/Text/IndentText'; import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy { @@ -22,10 +23,12 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy { if (calledFunction.body.type !== FunctionBodyType.Code) { throw new Error([ 'Unexpected function body type.', - `\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`, - `\tActual: "${FunctionBodyType[calledFunction.body.type]}"`, + indentText([ + `Expected: "${FunctionBodyType[FunctionBodyType.Code]}"`, + `Actual: "${FunctionBodyType[calledFunction.body.type]}"`, + ].join('\n')), 'Function:', - `\t${JSON.stringify(callToFunction)}`, + indentText(JSON.stringify(callToFunction)), ].join('\n')); } const { code } = calledFunction.body; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts index 934b4cdfb..7b3c79fac 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts @@ -9,6 +9,7 @@ import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Valida import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings'; import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { SharedFunctionCollection } from './SharedFunctionCollection'; import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser'; @@ -82,8 +83,7 @@ function validateCode( syntax: ILanguageSyntax, validator: ICodeValidator, ): void { - [data.code, data.revertCode] - .filter((code): code is string => Boolean(code)) + filterEmptyStrings([data.code, data.revertCode]) .forEach( (code) => validator.throwIfInvalid( code, @@ -204,9 +204,9 @@ function ensureNoDuplicateCode(functions: readonly FunctionData[]) { if (duplicateCodes.length > 0) { throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`); } - const duplicateRevertCodes = getDuplicates(callFunctions - .map((func) => func.revertCode) - .filter((code): code is string => Boolean(code))); + const duplicateRevertCodes = getDuplicates(filterEmptyStrings( + callFunctions.map((func) => func.revertCode), + )); if (duplicateRevertCodes.length > 0) { throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`); } diff --git a/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts b/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts index 5ad8d6c9c..fc24b59ac 100644 --- a/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts +++ b/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts @@ -6,6 +6,7 @@ import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/ import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; +import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings'; import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler'; import { parseFunctionCalls } from './Function/Call/FunctionCallsParser'; import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser'; @@ -71,9 +72,7 @@ export class ScriptCompiler implements IScriptCompiler { } function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void { - [compiledCode.code, compiledCode.revertCode] - .filter((code): code is string => Boolean(code)) - .map((code) => code as string) + filterEmptyStrings([compiledCode.code, compiledCode.revertCode]) .forEach( (code) => validator.throwIfInvalid( code, diff --git a/src/application/Parser/Executable/Script/ScriptParser.ts b/src/application/Parser/Executable/Script/ScriptParser.ts index e248bde18..446227c56 100644 --- a/src/application/Parser/Executable/Script/ScriptParser.ts +++ b/src/application/Parser/Executable/Script/ScriptParser.ts @@ -10,6 +10,7 @@ import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptC import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; import type { Script } from '@/domain/Executables/Script/Script'; import { createEnumParser, type EnumParser } from '@/application/Common/Enum'; +import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings'; import { parseDocs, type DocsParser } from '../DocumentationParser'; import { ExecutableType } from '../Validation/ExecutableType'; import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator'; @@ -86,8 +87,7 @@ function validateHardcodedCodeWithoutCalls( validator: ICodeValidator, syntax: ILanguageSyntax, ) { - [scriptCode.execute, scriptCode.revert] - .filter((code): code is string => Boolean(code)) + filterEmptyStrings([scriptCode.execute, scriptCode.revert]) .forEach( (code) => validator.throwIfInvalid( code, diff --git a/src/application/Parser/Executable/Script/Validation/CodeValidator.ts b/src/application/Parser/Executable/Script/Validation/CodeValidator.ts index f3778ab3a..1cac826a5 100644 --- a/src/application/Parser/Executable/Script/Validation/CodeValidator.ts +++ b/src/application/Parser/Executable/Script/Validation/CodeValidator.ts @@ -1,3 +1,4 @@ +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; import type { ICodeLine } from './ICodeLine'; import type { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule'; import type { ICodeValidator } from './ICodeValidator'; @@ -24,12 +25,11 @@ export class CodeValidator implements ICodeValidator { } function extractLines(code: string): ICodeLine[] { - return code - .split(/\r\n|\r|\n/) - .map((lineText, lineIndex): ICodeLine => ({ - index: lineIndex + 1, - text: lineText, - })); + const lines = splitTextIntoLines(code); // TODO: Verify it preserves empty lines + return lines.map((lineText, lineIndex): ICodeLine => ({ + index: lineIndex + 1, + text: lineText, + })); } function printLines( diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue index 21a296888..854c1a7b4 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue @@ -8,6 +8,7 @@