shared/analyzers/sentence.analyzer.ts

shared/analyzers/sentence.analyzer.ts is a file in GovLab Extension Host. 94 lines of code and 17 definitions.

import {
    AGENT_MARKER,
    COORDINATORS,
    IRREGULAR_PARTICIPLES,
    NOT_PARTICIPLES,
    PARTICIPLE_MIN_LENGTH,
    PARTICIPLE_SUFFIX,
    PASSIVE_ADVERBS,
    PASSIVE_ADVERB_SUFFIX,
    PASSIVE_AUXILIARIES,
    SENTENCE_TERMINALS,
    SENTENCE_WORD_STOPS,
} from "../manifests/sentence.manifest.ts";
import type { SentenceShape } from "../../types/writing.types.ts";

export const sentencesOf = function sentencesOf(text: string): string[] {
    const sentences: string[] = [];
    let current = "";
    for (const char of text) {
        current += char;
        if (SENTENCE_TERMINALS.has(char)) {
            if (current.trim().length > 0) {
                sentences.push(current.trim());
            }
            current = "";
        }
    }
    if (current.trim().length > 0) {
        sentences.push(current.trim());
    }
    return sentences;
};

export const sentenceWordsOf = function sentenceWordsOf(sentence: string): string[] {
    const words: string[] = [];
    let current = "";
    for (const char of sentence) {
        if (SENTENCE_WORD_STOPS.has(char)) {
            if (current.length > 0) {
                words.push(current.toLowerCase());
            }
            current = "";
        } else {
            current += char;
        }
    }
    if (current.length > 0) {
        words.push(current.toLowerCase());
    }
    return words;
};

export const isParticiple = function isParticiple(word: string): boolean {
    if (IRREGULAR_PARTICIPLES.has(word)) {
        return true;
    }
    if (NOT_PARTICIPLES.has(word) || word.length < PARTICIPLE_MIN_LENGTH) {
        return false;
    }
    return word.endsWith(PARTICIPLE_SUFFIX) && !word.endsWith(`e${PARTICIPLE_SUFFIX}`);
};

const isAdverb = function isAdverb(word: string): boolean {
    return PASSIVE_ADVERBS.has(word) || word.endsWith(PASSIVE_ADVERB_SUFFIX);
};

const participleAfter = function participleAfter(words: readonly string[], auxiliary: number): number {
    const next = words[auxiliary + 1] ?? "";
    if (isParticiple(next)) {
        return auxiliary + 1;
    }
    const after = words[auxiliary + 2] ?? "";
    return isAdverb(next) && isParticiple(after) ? auxiliary + 2 : -1;
};

export const isAgentlessPassive = function isAgentlessPassive(words: readonly string[]): boolean {
    for (let index = 0; index < words.length; index += 1) {
        if (!PASSIVE_AUXILIARIES.has(words[index] ?? "")) {
            continue;
        }
        const participle = participleAfter(words, index);
        if (participle !== -1) {
            return !words.slice(participle + 1).includes(AGENT_MARKER);
        }
    }
    return false;
};

export const joinsOf = function joinsOf(words: readonly string[]): number {
    return words.filter((word) => COORDINATORS.has(word)).length;
};

export const shapeOf = function shapeOf(sentence: string): SentenceShape {
    const words = sentenceWordsOf(sentence);
    return { agentlessPassive: isAgentlessPassive(words), joins: joinsOf(words), words: words.length };
};

export const shapesOf = function shapesOf(text: string): SentenceShape[] {
    return sentencesOf(text).map(shapeOf);
};

export const longestOf = function longestOf(shapes: readonly SentenceShape[]): number {
    return shapes.reduce((longest, shape) => Math.max(longest, shape.words), 0);
};