core/validators/page.validator.ts

core/validators/page.validator.ts is a file in Bane's Lab Social Share. 88 lines of code and 18 definitions.

import {
    ADDRESS_OVERFLOWS,
    LIST_SEPARATOR,
    OFF_BRAND,
    OFF_TONE,
    PAGE_SHARED,
    PAGE_WITHOUT_CARD,
    SUBJECT_SEPARATOR,
} from "#configuration/strings/card.strings";
import type { CardContext, CardFinding, CardPage, LayerKind, RegisteredCard } from "#types/card.types";
import { ADDRESS_ADVANCE } from "#configuration/constants/card.constants";
import { pixelValue } from "#core/evaluators/card.fragment.evaluator";
import { resolveCard } from "#core/evaluators/card.evaluator";

const FONT_SIZE = "fontSize";

const coverageOf = function coverageOf(page: CardPage, cards: readonly RegisteredCard[]): readonly CardFinding[] {
    const serving = cards.filter((card) => card.spec.page === page.id);
    if (serving.length === 0) {
        return [{ card: page.id, message: PAGE_WITHOUT_CARD }];
    }
    if (serving.length > 1) {
        const ids = serving.map((card) => card.id).join(LIST_SEPARATOR);
        return [{ card: page.id, message: PAGE_SHARED + SUBJECT_SEPARATOR + ids }];
    }
    return [];
};

const SHOWN_KINDS: ReadonlySet<LayerKind> = new Set<LayerKind>(["animation", "image", "text"]);

const shownContent = function shownContent(card: RegisteredCard, context: CardContext): ReadonlySet<string> {
    const shown = new Set<string>();
    for (const profile of context.profiles.filter((candidate) => card.spec.profiles.includes(candidate.id))) {
        for (const layer of resolveCard(card.spec, profile, card.spec.timeline.keyFrame).layers) {
            if (SHOWN_KINDS.has(layer.kind)) {
                shown.add(layer.content);
            }
        }
    }
    return shown;
};

const overflowsOf = function overflowsOf(
    card: RegisteredCard,
    page: CardPage,
    context: CardContext,
): readonly string[] {
    return context.profiles
        .filter((profile) => card.spec.profiles.includes(profile.id))
        .filter((profile) =>
            resolveCard(card.spec, profile, card.spec.timeline.keyFrame).layers.some((layer) => {
                const size = pixelValue(layer.style[FONT_SIZE] ?? "");
                const room = layer.placement.width * profile.width;
                return (
                    layer.kind === "text" &&
                    layer.content === page.address &&
                    page.address.length * ADDRESS_ADVANCE * size > room
                );
            }),
        )
        .map((profile) => profile.id);
};

const brandOf = function brandOf(card: RegisteredCard, page: CardPage, context: CardContext): readonly CardFinding[] {
    const shown = shownContent(card, context);
    const parts = [page.mark, page.headline, page.tagline, page.address, ...context.brand];
    const missing = parts.filter((mark) => !shown.has(mark));
    const overflows = overflowsOf(card, page, context);
    return [
        ...(card.spec.tone === page.accent ? [] : [{ card: card.id, message: OFF_TONE }]),
        ...(overflows.length === 0
            ? []
            : [{ card: card.id, message: ADDRESS_OVERFLOWS + SUBJECT_SEPARATOR + overflows.join(LIST_SEPARATOR) }]),
        ...(missing.length === 0
            ? []
            : [{ card: card.id, message: OFF_BRAND + SUBJECT_SEPARATOR + missing.join(LIST_SEPARATOR) }]),
    ];
};

export const coverageFindings = function coverageFindings(
    cards: readonly RegisteredCard[],
    pages: readonly CardPage[],
): readonly CardFinding[] {
    return pages.flatMap((page) => coverageOf(page, cards));
};

export const brandFindings = function brandFindings(
    cards: readonly RegisteredCard[],
    context: CardContext,
): readonly CardFinding[] {
    const pages = new Map(context.pages.map((page) => [page.id, page]));
    return cards.flatMap((card) => {
        const page = pages.get(card.spec.page);
        return page === undefined ? [] : brandOf(card, page, context);
    });
};