# core/validators/payload.validator.ts

> 101 lines of code and 19 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-validators-payload-validator-ts
Source text: https://banes-lab.com/source/build-scripts/core/validators/payload.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/build-scripts/core/validators.md), after [core/validators/metadata.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/metadata.validator.ts.md) and before [core/validators/schema.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/schema.validator.ts.md).

## Definitions

- `checkPayload` (lexical_declaration, line 72, exported)
- `checkPayloadContent` (lexical_declaration, line 64)
- `checkMarkdown` (lexical_declaration, line 92, exported)
- `INLINE_TAG_OPEN` (lexical_declaration, line 14)
- `INLINE_TAG_STOPS` (lexical_declaration, line 15)
- `INLINE_TAGS` (lexical_declaration, line 16)
- `MIN_MARKDOWN_LINES` (lexical_declaration, line 17)
- `Payload` (interface_declaration, line 19)
- `tagAt` (lexical_declaration, line 25)
- `end` (lexical_declaration, line 26)
- `inlineMarkupIn` (lexical_declaration, line 33, exported)
- `open` (lexical_declaration, line 35, exported)
- `tag` (lexical_declaration, line 37, exported)
- `found` (lexical_declaration, line 47, exported)
- `parsePayload` (lexical_declaration, line 56)
- `parsed` (lexical_declaration, line 57)
- `markup` (lexical_declaration, line 68)
- `payload` (lexical_declaration, line 78, exported)
- `findings` (lexical_declaration, line 98, exported)

## Contained in

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

## Uses

- [configuration/strings/payload.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/payload.strings.ts.md)

## Used by

- [core/coordinators/validation.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/validation.coordinator.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-configuration-strings.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/validators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-validators.md)

## Source

```typescript
import {
    BODILESS_ALTERNATE,
    EMPTY_PAYLOAD,
    MALFORMED_PAYLOAD,
    inlineTag,
    unanchoredAlternate,
    untitledAlternate,
    wrongPayloadId,
    wrongPayloadTab,
} from "#configuration/strings/payload.strings";
import type { Finding } from "#types/validation.types";
import { isRecord } from "#core/selectors/base.selector";

const INLINE_TAG_OPEN = "<";
const INLINE_TAG_STOPS = new Set([">", " ", "/"]);
const INLINE_TAGS = new Set(["strong", "b", "em", "i", "code", "a", "span", "br"]);
const MIN_MARKDOWN_LINES = 5;

interface Payload {
    readonly content: unknown;
    readonly id: unknown;
    readonly tab?: unknown;
}

const tagAt = function tagAt(text: string, open: number): string {
    let end = open + 1;
    while (end < text.length && !INLINE_TAG_STOPS.has(text.charAt(end))) {
        end += 1;
    }
    return text.slice(open + 1, end).toLowerCase();
};

export const inlineMarkupIn = function inlineMarkupIn(value: unknown): string | null {
    if (typeof value === "string") {
        let open = value.indexOf(INLINE_TAG_OPEN);
        while (open !== -1) {
            const tag = tagAt(value, open);
            if (INLINE_TAGS.has(tag)) {
                return tag;
            }
            open = value.indexOf(INLINE_TAG_OPEN, open + 1);
        }
        return null;
    }
    if (typeof value === "object" && value !== null) {
        for (const entry of Object.values(value)) {
            const found = inlineMarkupIn(entry);
            if (found !== null) {
                return found;
            }
        }
    }
    return null;
};

const parsePayload = function parsePayload(raw: string): Payload | null {
    const parsed: unknown = JSON.parse(raw);
    if (!isRecord(parsed) || !("content" in parsed) || !("id" in parsed)) {
        return null;
    }
    return { content: parsed["content"], id: parsed["id"], tab: parsed["tab"] };
};

const checkPayloadContent = function checkPayloadContent(file: string, content: unknown): Finding[] {
    if (content === undefined || content === null) {
        return [{ file, message: EMPTY_PAYLOAD }];
    }
    const markup = inlineMarkupIn(content);
    return markup === null ? [] : [{ file, message: inlineTag(markup) }];
};

export const checkPayload = function checkPayload(
    file: string,
    id: string,
    tab: string | null,
    raw: string,
): Finding[] {
    const payload = parsePayload(raw);
    if (payload === null) {
        return [{ file, message: MALFORMED_PAYLOAD }];
    }
    const findings: Finding[] = [];
    if (payload.id !== id) {
        findings.push({ file, message: wrongPayloadId(String(payload.id), id) });
    }
    if (payload.tab !== tab) {
        findings.push({ file, message: wrongPayloadTab(String(tab)) });
    }
    return [...findings, ...checkPayloadContent(file, payload.content)];
};

export const checkMarkdown = function checkMarkdown(
    file: string,
    text: string,
    title: string,
    address: string,
): Finding[] {
    const findings: Finding[] = [];
    if (!text.startsWith(`# ${title}`)) {
        findings.push({ file, message: untitledAlternate(title) });
    }
    if (!text.includes(address)) {
        findings.push({ file, message: unanchoredAlternate(address) });
    }
    if (text.trim().split("\n").length < MIN_MARKDOWN_LINES) {
        findings.push({ file, message: BODILESS_ALTERNATE });
    }
    return findings;
};
```
