# rules/eslint/strings-punctuation.eslint.rule.ts

> 116 lines of code and 25 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-strings-punctuation-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/strings-punctuation.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/strings-link-construction.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/strings-link-construction.eslint.rule.ts.md) and before [rules/eslint/strings-sentence-shape.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/strings-sentence-shape.eslint.rule.ts.md).

## Definitions

- `report` (lexical_declaration, line 78, exported)
- `quasiTexts` (lexical_declaration, line 66)
- `isNumeric` (lexical_declaration, line 45)
- `isDigit` (lexical_declaration, line 14)
- `trimTerminal` (lexical_declaration, line 37)
- `templateLiteral` (method_definition, line 103, exported)
- `isDigitMetric` (lexical_declaration, line 55)
- `digitMetricIn` (lexical_declaration, line 61)
- `create` (method_definition, line 73, exported)
- `literal` (method_definition, line 94, exported)
- `program` (method_definition, line 100, exported)
- `STRINGS_SUFFIX` (lexical_declaration, line 11)
- `WORD_STOPS` (lexical_declaration, line 12)
- `wordsOf` (lexical_declaration, line 18)
- `words` (lexical_declaration, line 19)
- `current` (lexical_declaration, line 20)
- `end` (lexical_declaration, line 38)
- `index` (lexical_declaration, line 46)
- `char` (lexical_declaration, line 47)
- `lower` (lexical_declaration, line 56)
- `unit` (lexical_declaration, line 57)
- `found` (lexical_declaration, line 62)
- `code` (lexical_declaration, line 77, exported)
- `metric` (lexical_declaration, line 88, exported)
- `text` (lexical_declaration, line 95, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/selectors/literal.selector.ts](https://banes-lab.com/source/governance/shared/selectors/literal.selector.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Standardization](https://banes-lab.com/records/architecture/standardization.md)

## Source

```typescript
import { LONG_DASH, METRIC_UNITS, PUNCTUATION_POLICY, SEMICOLON } from "../../shared/manifests/punctuation.manifest.ts";
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { literalString, locOf, nodesAt, recordAt, stringIn } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { basenameOf } from "../../shared/resolvers/anchor.resolver.ts";
import { codeLiteralsOf } from "../../shared/selectors/literal.selector.ts";
import { concernSuffix } from "../../shared/manifests/taxonomy.manifest.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";

const STRINGS_SUFFIX = concernSuffix("strings");
const WORD_STOPS = new Set([" ", "\n", "\t", ",", ";", ":", "!", "?", "(", ")", '"', "'", "*", "`"]);

const isDigit = function isDigit(char: string): boolean {
    return char >= "0" && char <= "9";
};

const wordsOf = function wordsOf(text: string): string[] {
    const words: string[] = [];
    let current = "";
    for (const char of text) {
        if (WORD_STOPS.has(char)) {
            if (current.length > 0) {
                words.push(current);
            }
            current = "";
        } else {
            current += char;
        }
    }
    if (current.length > 0) {
        words.push(current);
    }
    return words;
};

const trimTerminal = function trimTerminal(word: string): string {
    let end = word.length;
    while (end > 0 && word.charAt(end - 1) === ".") {
        end -= 1;
    }
    return word.slice(0, end);
};

const isNumeric = function isNumeric(stem: string): boolean {
    for (let index = 0; index < stem.length; index += 1) {
        const char = stem.charAt(index);
        if (!isDigit(char) && char !== ".") {
            return false;
        }
    }
    return stem.length > 0;
};

const isDigitMetric = function isDigitMetric(word: string): boolean {
    const lower = trimTerminal(word.toLowerCase());
    const unit = METRIC_UNITS.find((held) => lower.endsWith(held) && lower.length > held.length);
    return unit === undefined ? false : isNumeric(lower.slice(0, -unit.length));
};

const digitMetricIn = function digitMetricIn(text: string): string | null {
    const found = wordsOf(text).find(isDigitMetric);
    return found === undefined ? null : trimTerminal(found);
};

const quasiTexts = function quasiTexts(node: AstNode): string {
    return nodesAt(node, "quasis")
        .map((part) => stringIn(recordAt(part, "value"), "cooked"))
        .join(" ");
};

export default {
    create(context: RuleContext): RuleListener {
        if (!basenameOf(context.filename).endsWith(STRINGS_SUFFIX)) {
            return {};
        }
        let code: ReadonlySet<AstNode> = new Set();
        const report = function report(node: AstNode, text: string): void {
            if (code.has(node)) {
                return;
            }
            if (PUNCTUATION_POLICY.longDash && text.includes(LONG_DASH)) {
                context.report({ loc: locOf(node), messageId: "longDash" });
            }
            if (PUNCTUATION_POLICY.semicolon && text.includes(SEMICOLON)) {
                context.report({ loc: locOf(node), messageId: "semicolon" });
            }
            const metric = PUNCTUATION_POLICY.digitMetric ? digitMetricIn(text) : null;
            if (metric !== null) {
                context.report({ data: { metric }, loc: locOf(node), messageId: "digitMetric" });
            }
        };
        return listener({
            literal(view) {
                const text = literalString(view);
                if (text !== null) {
                    report(view, text);
                }
            },
            program(view) {
                code = codeLiteralsOf(view);
            },
            templateLiteral(view) {
                report(view, quasiTexts(view));
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:standardization"] }),
            description:
                "A user-visible string honors the punctuation policy the registry declares: no long dash where the policy refuses it, no semicolon where it refuses it, and no digit-form metric standing beside a unit or a percent sign, because a number in copy is a claim that needs a measurement behind it. Each half of the policy is a boolean in the registry, so the operator turns a half on by editing data and the rule never changes. A literal consumed as a code sample is code, not prose, and is not held to the policy.",
        },
        messages: {
            digitMetric:
                "The literal states the digit-form metric '{{metric}}'. A number in user-visible copy is a measured claim; state the shape in words, or carry the measurement into a generated surface where the number is derived.",
            longDash:
                "The literal carries a long dash and the punctuation policy refuses it. Split the sentence or use the separator the policy allows.",
            semicolon:
                "The literal carries a semicolon and the punctuation policy refuses it. Split the sentence in two.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
