core/converters/surface.converter.ts

core/converters/surface.converter.ts is a file in Bane's Lab Build Scripts. 88 lines of code and 21 definitions.

import {
    CODE_FENCE,
    RECORD_CLOSE,
    RECORD_OPEN,
    ROSTER_LINES,
    SECTION_BANNER,
} from "#configuration/constants/surface.constants";
import type { FigureFrame, Region, ScenarioTool } from "#types/surface.types";
import type { SurfaceFigureName, SurfaceLine, SurfaceRecording } from "@banes-lab/web/types/surface.types.ts";

const QUOTE = '"';

const SPACE = " ";

export const changedRegion = function changedRegion(
    before: readonly string[],
    after: readonly string[],
): Region | null {
    const shorter = Math.min(before.length, after.length);
    let prefix = 0;
    while (prefix < shorter && before[prefix] === after[prefix]) {
        prefix += 1;
    }
    let suffix = 0;
    while (suffix < shorter - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) {
        suffix += 1;
    }
    const to = after.length - suffix;
    return prefix === after.length && before.length === after.length
        ? null
        : { from: prefix, to: Math.max(prefix, to) };
};

interface ViewPlace {
    readonly fenced: boolean;
    readonly inRecord: boolean;
    readonly inTail: boolean;
    readonly afterKept: boolean;
}

const isViewLine = function isViewLine(line: string, place: ViewPlace): boolean {
    if (place.inTail || place.inRecord) {
        return true;
    }
    if (line === "") {
        return place.afterKept;
    }
    return !place.fenced && ROSTER_LINES.some((roster) => line.startsWith(roster));
};

export const venueView = function venueView(lines: readonly string[]): string[] {
    const tail = lines.findLastIndex((line) => line.startsWith(SECTION_BANNER));
    const kept: string[] = [];
    let fenced = false;
    let inRecord = false;
    let afterKept = false;
    for (const [index, line] of lines.entries()) {
        fenced = fenced !== line.startsWith(CODE_FENCE);
        inRecord ||= !fenced && line.startsWith(RECORD_OPEN);
        afterKept = isViewLine(line, { afterKept, fenced, inRecord, inTail: tail !== -1 && index >= tail });
        if (afterKept) {
            kept.push(line);
        }
        inRecord &&= !line.startsWith(RECORD_CLOSE);
    }
    return kept;
};

export const markedLines = function markedLines(lines: readonly string[], highlight: Region | null): SurfaceLine[] {
    return lines.map((text, at) => ({
        changed: highlight !== null && at >= highlight.from && at < highlight.to,
        text,
    }));
};

const quoted = function quoted(argument: string): string {
    return argument.includes(SPACE) ? QUOTE + argument + QUOTE : argument;
};

export const commandLine = function commandLine(tool: ScenarioTool, args: readonly string[]): string {
    return `npm run ${tool} -- ${args.map(quoted).join(SPACE)}`;
};

export const appendLine = function appendLine(text: string, file: string): string {
    return `echo ${QUOTE}${text}${QUOTE} >> ${file}`;
};

export const recordingsOf = function recordingsOf(
    frames: readonly FigureFrame[],
    inputs: string,
): Map<SurfaceFigureName, SurfaceRecording> {
    const grouped = Map.groupBy(frames, (entry) => entry.figure);
    return new Map(
        [...grouped].map(([figure, entries]) => [
            figure,
            { figure, frames: entries.map((entry) => entry.frame), inputs, opening: entries.at(0)?.before ?? [] },
        ]),
    );
};