# core/validators/card.validator.ts

> 148 lines of code and 28 definitions.

Tree: Bane's Lab Social Share
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/social-share#file-social-share-core-validators-card-validator-ts
Source text: https://banes-lab.com/source/social-share/core/validators/card.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/social-share/core/validators.md), before [core/validators/loop.validator.ts](https://banes-lab.com/source/social-share/core/validators/loop.validator.ts.md).

## Definitions

- `within` (lexical_declaration, line 53)
- `finding` (lexical_declaration, line 43)
- `resolvedFindings` (lexical_declaration, line 74)
- `frameFindings` (lexical_declaration, line 82)
- `seamOf` (lexical_declaration, line 121)
- `targetFindings` (lexical_declaration, line 94)
- `layerSetFindings` (lexical_declaration, line 113)
- `specFindings` (lexical_declaration, line 129)
- `validateCards` (lexical_declaration, line 140, exported)
- `layerFindings` (lexical_declaration, line 57)
- `timelineFindings` (lexical_declaration, line 104)
- `SLASH` (lexical_declaration, line 39)
- `BACKSLASH` (lexical_declaration, line 40)
- `PLUGIN_MARK` (lexical_declaration, line 41)
- `folderOf` (lexical_declaration, line 47, exported)
- `path` (lexical_declaration, line 48, exported)
- `end` (lexical_declaration, line 49, exported)
- `{ placement }` (lexical_declaration, line 58)
- `numbers` (lexical_declaration, line 59)
- `messages` (lexical_declaration, line 83)
- `frame` (lexical_declaration, line 85)
- `known` (lexical_declaration, line 95)
- `{ fps, frames, keyFrame }` (lexical_declaration, line 105)
- `layerIds` (lexical_declaration, line 114)
- `timeline` (lexical_declaration, line 130)
- `origins` (lexical_declaration, line 145, exported)
- `own` (lexical_declaration, line 146, exported)
- `sound` (lexical_declaration, line 150, exported)

## Contained in

- [core/validators](https://banes-lab.com/anatomy/social-share/folder-social-share-core-validators.md)

## Uses

- [core/evaluators/card.evaluator.ts](https://banes-lab.com/source/social-share/core/evaluators/card.evaluator.ts.md)
- [core/validators/loop.validator.ts](https://banes-lab.com/source/social-share/core/validators/loop.validator.ts.md)
- [core/validators/page.validator.ts](https://banes-lab.com/source/social-share/core/validators/page.validator.ts.md)

## Linked from

- [core/evaluators](https://banes-lab.com/anatomy/social-share/folder-social-share-core-evaluators.md)
- [core/validators](https://banes-lab.com/anatomy/social-share/folder-social-share-core-validators.md)

## Source

```typescript
import { ARTICLE_PROFILES, SHARE_PROFILE } from "#configuration/configs/card.config";
import type { CardContext, CardFinding, CardSpec, Profile, RegisteredCard, ResolvedLayer } from "#types/card.types";
import {
    DUPLICATE_CARD,
    DUPLICATE_LAYER,
    EMPTY_LAYERS,
    EXPRESSION_FAILED,
    FOLDER_MISMATCH,
    FPS_OUT_OF_RANGE,
    FRAMES_OUT_OF_RANGE,
    KEY_FRAME_OUT_OF_RANGE,
    MISSING_ARTICLE_PROFILE,
    MISSING_SHARE_PROFILE,
    NOT_FINITE,
    OPACITY_OUT_OF_RANGE,
    OUTSIDE_CANVAS,
    SHADER_WITHOUT_ENTRY,
    STYLE_NOT_ALLOWED,
    SUBJECT_SEPARATOR,
    TOO_MANY_UNIFORMS,
    UNKNOWN_PAGE,
    UNKNOWN_PROFILE,
    UNREGISTERED_PLUGIN,
} from "#configuration/strings/card.strings";
import {
    MAX_FPS,
    MAX_FRAMES,
    MIN_FPS,
    MIN_FRAMES,
    PLUGIN_FOLDER,
    SHADER_ENTRY,
    UNIFORM_SLOTS,
} from "#configuration/constants/card.constants";
import { OPACITY_MAX, OPACITY_MIN, PLACEMENT_MAX, PLACEMENT_MIN, STYLE_PROPERTIES } from "#core/schemas/card.schema";
import { brandFindings, coverageFindings } from "#core/validators/page.validator";
import { loopFindings } from "#core/validators/loop.validator";
import { resolveCard } from "#core/evaluators/card.evaluator";

const SLASH = "/";
const BACKSLASH = "\\";
const PLUGIN_MARK = SLASH + PLUGIN_FOLDER + SLASH;

const finding = function finding(card: string, message: string): CardFinding {
    return { card, message };
};

export const folderOf = function folderOf(location: string): string {
    const path = location.split(BACKSLASH).join(SLASH);
    const end = path.lastIndexOf(PLUGIN_MARK);
    return end === -1 ? "" : path.slice(path.lastIndexOf(SLASH, end - 1) + 1, end);
};

const within = function within(value: number, low: number, high: number): boolean {
    return Number.isFinite(value) && value >= low && value <= high;
};

const layerFindings = function layerFindings(layer: ResolvedLayer): readonly string[] {
    const { placement } = layer;
    const numbers = [placement.x, placement.y, placement.width, placement.height, placement.rotate, placement.scale];
    return [
        ...(numbers.every((value) => Number.isFinite(value)) && layer.uniforms.every((value) => Number.isFinite(value))
            ? []
            : [NOT_FINITE]),
        ...(within(placement.x, PLACEMENT_MIN, PLACEMENT_MAX) && within(placement.y, PLACEMENT_MIN, PLACEMENT_MAX)
            ? []
            : [OUTSIDE_CANVAS]),
        ...(within(layer.opacity, OPACITY_MIN, OPACITY_MAX) ? [] : [OPACITY_OUT_OF_RANGE]),
        ...(layer.uniforms.length > UNIFORM_SLOTS ? [TOO_MANY_UNIFORMS] : []),
        ...(layer.kind === "shader" && !layer.content.includes(SHADER_ENTRY) ? [SHADER_WITHOUT_ENTRY] : []),
        ...(Object.keys(layer.style).every((property) => STYLE_PROPERTIES.has(property)) ? [] : [STYLE_NOT_ALLOWED]),
    ];
};

const resolvedFindings = function resolvedFindings(spec: CardSpec, profile: Profile, frame: number): readonly string[] {
    try {
        return resolveCard(spec, profile, frame).layers.flatMap(layerFindings);
    } catch (error) {
        return [EXPRESSION_FAILED + SUBJECT_SEPARATOR + (error instanceof Error ? error.message : String(error))];
    }
};

const frameFindings = function frameFindings(spec: CardSpec, profiles: readonly Profile[]): readonly string[] {
    const messages = new Set<string>();
    for (const profile of profiles.filter((candidate) => spec.profiles.includes(candidate.id))) {
        for (let frame = 0; frame < spec.timeline.frames; frame += 1) {
            for (const message of resolvedFindings(spec, profile, frame)) {
                messages.add(message);
            }
        }
    }
    return [...messages];
};

const targetFindings = function targetFindings(spec: CardSpec, context: CardContext): readonly string[] {
    const known = new Set(context.profiles.map((profile) => profile.id));
    return [
        ...(spec.profiles.every((profile) => known.has(profile)) ? [] : [UNKNOWN_PROFILE]),
        ...(context.pages.some((page) => page.id === spec.page) ? [] : [UNKNOWN_PAGE]),
        ...(spec.profiles.includes(SHARE_PROFILE) ? [] : [MISSING_SHARE_PROFILE]),
        ...(ARTICLE_PROFILES.every((profile) => spec.profiles.includes(profile)) ? [] : [MISSING_ARTICLE_PROFILE]),
    ];
};

const timelineFindings = function timelineFindings(spec: CardSpec): readonly string[] {
    const { fps, frames, keyFrame } = spec.timeline;
    return [
        ...(within(frames, MIN_FRAMES, MAX_FRAMES) ? [] : [FRAMES_OUT_OF_RANGE]),
        ...(within(fps, MIN_FPS, MAX_FPS) ? [] : [FPS_OUT_OF_RANGE]),
        ...(within(keyFrame, 0, frames - 1) ? [] : [KEY_FRAME_OUT_OF_RANGE]),
    ];
};

const layerSetFindings = function layerSetFindings(spec: CardSpec): readonly string[] {
    const layerIds = spec.layers.map((layer) => layer.id);
    return [
        ...(spec.layers.length === 0 ? [EMPTY_LAYERS] : []),
        ...(new Set(layerIds).size === layerIds.length ? [] : [DUPLICATE_LAYER]),
    ];
};

const seamOf = function seamOf(spec: CardSpec, profiles: readonly Profile[]): readonly string[] {
    try {
        return loopFindings(spec, profiles);
    } catch (error) {
        return [EXPRESSION_FAILED + SUBJECT_SEPARATOR + (error instanceof Error ? error.message : String(error))];
    }
};

const specFindings = function specFindings(spec: CardSpec, context: CardContext): readonly string[] {
    const timeline = timelineFindings(spec);
    return [
        ...targetFindings(spec, context),
        ...timeline,
        ...layerSetFindings(spec),
        ...(timeline.length === 0 ? frameFindings(spec, context.profiles) : []),
        ...(timeline.length === 0 ? seamOf(spec, context.profiles) : []),
    ];
};

export const validateCards = function validateCards(
    cards: readonly RegisteredCard[],
    plugins: readonly string[],
    context: CardContext,
): readonly CardFinding[] {
    const origins = new Set(cards.map((card) => folderOf(card.origin)));
    const own = cards.flatMap((card) => [
        ...(folderOf(card.origin) === card.id ? [] : [finding(card.id, FOLDER_MISMATCH)]),
        ...specFindings(card.spec, context).map((message) => finding(card.id, message)),
    ]);
    const sound = cards.filter((card) => !own.some((entry) => entry.card === card.id));
    return [
        ...context.repeated.map((card) => finding(card, DUPLICATE_CARD)),
        ...plugins
            .map(folderOf)
            .filter((folder) => !origins.has(folder))
            .map((folder) => finding(folder, UNREGISTERED_PLUGIN)),
        ...own,
        ...coverageFindings(cards, context.pages),
        ...brandFindings(sound, context),
    ];
};
```
