# core/validators/panel.validator.ts

> 81 lines of code and 14 definitions.

Tree: Bane's Lab Content
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/content#file-content-core-validators-panel-validator-ts
Source text: https://banes-lab.com/source/content/core/validators/panel.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/content/core/validators.md), after [core/validators/link.validator.ts](https://banes-lab.com/source/content/core/validators/link.validator.ts.md) and before [core/validators/writing.validator.ts](https://banes-lab.com/source/content/core/validators/writing.validator.ts.md).

## Definitions

- `tagFindings` (lexical_declaration, line 24)
- `unknownCites` (lexical_declaration, line 71)
- `wordCount` (lexical_declaration, line 20)
- `duplicateTags` (lexical_declaration, line 41)
- `citesIn` (lexical_declaration, line 55)
- `validatePanels` (lexical_declaration, line 76, exported)
- `messages` (lexical_declaration, line 28)
- `seen` (lexical_declaration, line 42)
- `twice` (lexical_declaration, line 43)
- `cites` (lexical_declaration, line 56)
- `open` (lexical_declaration, line 57)
- `close` (lexical_declaration, line 59)
- `tags` (lexical_declaration, line 72)
- `panelCount` (lexical_declaration, line 86, exported)

## Contained in

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

## Linked from

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

## Source

```typescript
import {
    CITE_CLOSE,
    CITE_OPEN,
    LINK_OPEN,
    TAG_SEPARATOR,
    TAG_WORD_LIMIT,
} from "#configuration/constants/panel.constants";
import {
    PANEL_CITE_UNKNOWN,
    PANEL_TAG_SEPARATED,
    PANEL_TAG_TOO_LONG,
    PANEL_TAG_TWICE,
    PANEL_UNCITED,
    PANEL_UNTAGGED,
} from "#configuration/strings/panel.strings";
import type { PanelSection, PayloadPanel } from "#types/panel.types";
import type { GraphFinding } from "#types/coverage.types";
import { WORD_SEPARATOR } from "#configuration/constants/leak.constants";

const wordCount = function wordCount(tag: string): number {
    return tag.split(WORD_SEPARATOR).filter((word) => word.length > 0).length;
};

const tagFindings = function tagFindings(section: PanelSection, panel: PayloadPanel): string[] {
    if (panel.tag === null) {
        return [PANEL_UNTAGGED];
    }
    const messages: string[] = [];
    if (wordCount(panel.tag) > TAG_WORD_LIMIT) {
        messages.push(PANEL_TAG_TOO_LONG + panel.tag);
    }
    if (panel.tag.includes(TAG_SEPARATOR)) {
        messages.push(PANEL_TAG_SEPARATED + panel.tag);
    }
    if (!section.prose.includes(CITE_OPEN + panel.tag + CITE_CLOSE)) {
        messages.push(PANEL_UNCITED + panel.tag);
    }
    return messages;
};

const duplicateTags = function duplicateTags(section: PanelSection): string[] {
    const seen = new Set<string>();
    const twice: string[] = [];
    for (const panel of section.panels) {
        if (panel.tag !== null && seen.has(panel.tag)) {
            twice.push(panel.tag);
        }
        if (panel.tag !== null) {
            seen.add(panel.tag);
        }
    }
    return twice;
};

const citesIn = function citesIn(prose: string): readonly string[] {
    const cites: string[] = [];
    let open = prose.indexOf(CITE_OPEN);
    while (open !== -1) {
        const close = prose.indexOf(CITE_CLOSE, open + CITE_OPEN.length);
        if (close === -1) {
            break;
        }
        if (prose.charAt(close + CITE_CLOSE.length) !== LINK_OPEN) {
            cites.push(prose.slice(open + CITE_OPEN.length, close));
        }
        open = prose.indexOf(CITE_OPEN, close + CITE_CLOSE.length);
    }
    return cites;
};

const unknownCites = function unknownCites(section: PanelSection): readonly string[] {
    const tags = new Set(section.panels.flatMap((panel) => (panel.tag === null ? [] : [panel.tag])));
    return [...new Set(citesIn(section.prose))].filter((cite) => !tags.has(cite));
};

export const validatePanels = function validatePanels(page: string, sections: readonly PanelSection[]): GraphFinding[] {
    return sections.flatMap((section) => [
        ...section.panels.flatMap((panel) =>
            tagFindings(section, panel).map((message) => ({ message, page, section: section.id })),
        ),
        ...duplicateTags(section).map((tag) => ({ message: PANEL_TAG_TWICE + tag, page, section: section.id })),
        ...unknownCites(section).map((cite) => ({ message: PANEL_CITE_UNKNOWN + cite, page, section: section.id })),
    ]);
};

export const panelCount = function panelCount(sections: readonly PanelSection[]): number {
    return sections.reduce((sum, section) => sum + section.panels.length, 0);
};
```
