// ---------------------------------------------------------------------------
// SEO Content Analysis Engine
// Pure TypeScript — no external dependencies
// ---------------------------------------------------------------------------
import { analyzeImageAlts } from './alt-text.js';
// ---- Helpers --------------------------------------------------------------
const TRANSITION_WORDS = [
    'however',
    'therefore',
    'furthermore',
    'additionally',
    'moreover',
    'consequently',
    'nevertheless',
    'in addition',
    'for example',
    'for instance',
    'in contrast',
    'on the other hand',
    'similarly',
    'likewise',
    'meanwhile',
    'specifically',
    'in particular',
    'as a result',
    'in conclusion',
    'to summarize',
    'in other words',
    'that is',
    'namely',
    'above all',
    'after all',
    'in fact',
    'of course',
    'indeed',
    'certainly',
    'besides',
    'also',
    'hence',
    'thus',
    'accordingly',
    'finally',
    'subsequently',
    'although',
    'though',
    'even though',
    'whereas',
    'while',
    'despite',
    'in spite of',
    'regardless',
    'first',
    'second',
    'third',
    'next',
    'then',
    'lastly',
];
const PASSIVE_AUXILIARIES = /\b(is|are|was|were|be|been|being|gets|got|gotten)\b/i;
const PAST_PARTICIPLE = /\b((?:[a-z]+ed)|(?:chosen|broken|spoken|written|driven|given|taken|made|seen|done|gone|known|shown|grown|drawn|thrown|blown|flown|worn|torn|born|sworn|frozen|stolen|forgotten|hidden|bitten|beaten|eaten|fallen|shaken|mistaken|awoken|risen|ridden))\b/i;
/** Remove all HTML tags, returning plain text. */
export function stripHtmlTags(html) {
    return html
        .replace(/<br\s*\/?>/gi, '\n')
        .replace(/<\/p>/gi, '\n\n')
        .replace(/<[^>]*>/g, '')
        .replace(/&nbsp;/gi, ' ')
        .replace(/&amp;/gi, '&')
        .replace(/&lt;/gi, '<')
        .replace(/&gt;/gi, '>')
        .replace(/&quot;/gi, '"')
        .replace(/&#39;/gi, "'")
        .replace(/\n{3,}/g, '\n\n')
        .trim();
}
/**
 * Estimate the syllable count of an English word.
 *
 * Heuristic: count vowel groups, subtract 1 for a trailing silent 'e',
 * and clamp to a minimum of 1.
 */
export function countSyllables(word) {
    const w = word.toLowerCase().replace(/[^a-z]/g, '');
    if (w.length <= 2)
        return 1;
    const vowelGroups = w.match(/[aeiouy]+/g);
    if (!vowelGroups)
        return 1;
    let count = vowelGroups.length;
    if (w.endsWith('e') && !w.endsWith('le') && count > 1) {
        count--;
    }
    if (w.endsWith('les') && w.length > 4) {
        // "examples" — the 'les' keeps its syllable
    }
    if (w.endsWith('ed') && !w.endsWith('ted') && !w.endsWith('ded') && count > 1) {
        count--;
    }
    return Math.max(1, count);
}
/** Detect passive voice in a single sentence. */
export function detectPassiveVoice(sentence) {
    const words = sentence.split(/\s+/);
    for (let i = 0; i < words.length - 1; i++) {
        if (words[i] &&
            words[i + 1] &&
            PASSIVE_AUXILIARIES.test(words[i]) &&
            PAST_PARTICIPLE.test(words[i + 1])) {
            return true;
        }
    }
    return false;
}
/** Split plain text into sentences. */
export function splitSentences(text) {
    const raw = text
        .replace(/\n+/g, ' ')
        .split(/(?<=[.!?])\s+/)
        .map((s) => s.trim())
        .filter((s) => s.length > 0);
    return raw;
}
/** Split plain text into paragraphs (double newlines or `<p>` boundaries). */
export function splitParagraphs(text) {
    return text
        .split(/\n{2,}/)
        .map((p) => p.trim())
        .filter((p) => p.length > 0);
}
// ---- Internal utilities ---------------------------------------------------
function getWords(text) {
    return text
        .split(/\s+/)
        .map((w) => w.replace(/[^a-zA-Z0-9'-]/g, ''))
        .filter((w) => w.length > 0);
}
function fleschLabel(score) {
    if (score >= 90)
        return 'Very Easy';
    if (score >= 80)
        return 'Easy';
    if (score >= 70)
        return 'Fairly Easy';
    if (score >= 60)
        return 'Standard';
    if (score >= 50)
        return 'Fairly Difficult';
    if (score >= 30)
        return 'Difficult';
    return 'Very Difficult';
}
function extractTag(html, tag) {
    const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'gi');
    const results = [];
    let m;
    while ((m = re.exec(html)) !== null) {
        results.push(stripHtmlTags(m[1] ?? ''));
    }
    return results;
}
/**
 * Extract headings of a given level from content that may be HTML **or**
 * Markdown. The content analysis runs against whatever editors store in the
 * body field; rich-text collections store HTML while markdown collections store
 * raw `#`/`##` ATX (and `===`/`---` Setext) headings. Matching only HTML tags
 * made every markdown post fail the "no H1 / no subheadings" checks. This
 * recognises both syntaxes so the score reflects the real document structure.
 */
export function extractHeadings(content, level) {
    const headings = extractTag(content, `h${level}`);
    // Markdown ATX headings: a line beginning with exactly `level` `#` characters
    // followed by whitespace. Skip lines inside fenced code blocks.
    const lines = content.split('\n');
    let inFence = false;
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? '';
        if (/^\s*(```|~~~)/.test(line)) {
            inFence = !inFence;
            continue;
        }
        if (inFence)
            continue;
        const atx = line.match(/^\s{0,3}(#{1,6})\s+(.+?)(?:\s+#+\s*)?$/);
        if (atx && atx[1].length === level) {
            headings.push(atx[2].trim());
            continue;
        }
        // Setext headings only exist for levels 1 (`===`) and 2 (`---`): a text
        // line immediately followed by a line of `=` or `-`.
        if (level <= 2) {
            const next = lines[i + 1] ?? '';
            const underline = level === 1 ? /^\s{0,3}=+\s*$/ : /^\s{0,3}-+\s*$/;
            if (line.trim().length > 0 && !/^\s*#/.test(line) && underline.test(next)) {
                headings.push(line.trim());
            }
        }
    }
    return headings;
}
function extractImgAlts(html) {
    // Decorative images (alt="" + role="presentation"/aria-hidden) are correct,
    // so they're excluded from the coverage denominator: `total` counts only
    // images that are expected to have alt text. `total - withAlt` is the number
    // missing. Shared with the audit engine / scanner so all surfaces agree.
    const info = analyzeImageAlts(html);
    return { total: info.withAlt + info.missing, withAlt: info.withAlt, altTexts: info.altTexts };
}
function extractLinks(html) {
    const anchors = html.match(/<a[^>]*href\s*=\s*["'][^"']*["'][^>]*>/gi) ?? [];
    let internal = 0;
    let external = 0;
    for (const a of anchors) {
        const href = a.match(/href\s*=\s*["']([^"']*)["']/i)?.[1] ?? '';
        if (/^https?:\/\//i.test(href)) {
            external++;
        }
        else if (href.startsWith('/') || href.startsWith('#') || href.startsWith('./')) {
            internal++;
        }
    }
    return { internal, external };
}
function keyphraseNormalize(kp) {
    return kp
        .toLowerCase()
        .replace(/[^a-z0-9\s]/g, '')
        .trim();
}
function containsKeyphrase(text, keyphrase) {
    const norm = keyphraseNormalize(keyphrase);
    const target = text.toLowerCase().replace(/[^a-z0-9\s]/g, '');
    return target.includes(norm);
}
function keyphraseWordDensity(text, keyphrase) {
    const words = getWords(text.toLowerCase());
    if (words.length === 0)
        return 0;
    const kpWords = getWords(keyphraseNormalize(keyphrase));
    if (kpWords.length === 0)
        return 0;
    let occurrences = 0;
    const joined = words.join(' ');
    const kpJoined = kpWords.join(' ');
    let idx = 0;
    while ((idx = joined.indexOf(kpJoined, idx)) !== -1) {
        occurrences++;
        idx += kpJoined.length;
    }
    return (occurrences * kpWords.length * 100) / words.length;
}
// ---- Main analysis --------------------------------------------------------
/**
 * Run a comprehensive SEO analysis on the given content and metadata.
 *
 * Returns a 0-100 score together with detailed per-check results across
 * keyphrase, SEO, readability, and social categories.
 */
export function analyzeContent(input) {
    const checks = [];
    const plainText = stripHtmlTags(input.content);
    const words = getWords(plainText);
    // -- Keyphrase checks ---------------------------------------------------
    if (input.focusKeyphrase && input.focusKeyphrase.trim().length > 0) {
        const kp = input.focusKeyphrase;
        const titleSource = input.metaTitle ?? input.title;
        checks.push({
            id: 'keyphrase-in-title',
            category: 'keyphrase',
            title: 'Keyphrase in title',
            status: containsKeyphrase(titleSource, kp) ? 'good' : 'problem',
            description: containsKeyphrase(titleSource, kp)
                ? 'Focus keyphrase appears in the page title.'
                : 'The focus keyphrase does not appear in the page title. Add it for better relevance.',
        });
        const metaDesc = input.metaDescription ?? '';
        checks.push({
            id: 'keyphrase-in-meta-description',
            category: 'keyphrase',
            title: 'Keyphrase in meta description',
            status: metaDesc.length > 0 && containsKeyphrase(metaDesc, kp) ? 'good' : 'problem',
            description: metaDesc.length > 0 && containsKeyphrase(metaDesc, kp)
                ? 'Focus keyphrase appears in the meta description.'
                : 'The focus keyphrase does not appear in the meta description.',
        });
        checks.push({
            id: 'keyphrase-in-slug',
            category: 'keyphrase',
            title: 'Keyphrase in slug',
            status: containsKeyphrase(input.slug.replace(/-/g, ' '), kp) ? 'good' : 'improvement',
            description: containsKeyphrase(input.slug.replace(/-/g, ' '), kp)
                ? 'Focus keyphrase appears in the URL slug.'
                : 'Consider adding the focus keyphrase to the URL slug.',
        });
        const first200 = plainText.slice(0, 200);
        checks.push({
            id: 'keyphrase-in-first-paragraph',
            category: 'keyphrase',
            title: 'Keyphrase in introduction',
            status: containsKeyphrase(first200, kp) ? 'good' : 'problem',
            description: containsKeyphrase(first200, kp)
                ? 'Focus keyphrase appears in the first paragraph.'
                : 'The focus keyphrase should appear in the opening paragraph of your content.',
        });
        const density = keyphraseWordDensity(plainText, kp);
        const densityStatus = density >= 0.5 && density <= 3
            ? 'good'
            : density > 0 && density < 0.5
                ? 'improvement'
                : 'problem';
        checks.push({
            id: 'keyphrase-density',
            category: 'keyphrase',
            title: 'Keyphrase density',
            status: densityStatus,
            description: densityStatus === 'good'
                ? `Keyphrase density is ${density.toFixed(1)}%, which is within the recommended 0.5–3% range.`
                : `Keyphrase density is ${density.toFixed(1)}%. Aim for 0.5–3% of your content words.`,
        });
        const h1s = extractHeadings(input.content, 1);
        const kpInH1 = h1s.some((h) => containsKeyphrase(h, kp));
        checks.push({
            id: 'keyphrase-in-h1',
            category: 'keyphrase',
            title: 'Keyphrase in H1',
            status: kpInH1 ? 'good' : 'problem',
            description: kpInH1
                ? 'Focus keyphrase appears in the H1 heading.'
                : 'The focus keyphrase does not appear in any H1 heading.',
        });
        const subheadings = [...extractHeadings(input.content, 2), ...extractHeadings(input.content, 3)];
        const kpInSub = subheadings.some((h) => containsKeyphrase(h, kp));
        checks.push({
            id: 'keyphrase-in-subheadings',
            category: 'keyphrase',
            title: 'Keyphrase in subheadings',
            status: kpInSub ? 'good' : subheadings.length === 0 ? 'problem' : 'improvement',
            description: kpInSub
                ? 'Focus keyphrase appears in at least one subheading.'
                : 'Consider using the focus keyphrase in an H2 or H3 subheading.',
        });
        const imgData = extractImgAlts(input.content);
        const kpInAlt = imgData.altTexts.some((a) => containsKeyphrase(a, kp));
        checks.push({
            id: 'keyphrase-in-image-alt',
            category: 'keyphrase',
            title: 'Keyphrase in image alt text',
            status: imgData.total === 0 ? 'improvement' : kpInAlt ? 'good' : 'improvement',
            description: imgData.total === 0
                ? 'No images found. Consider adding images with the focus keyphrase in the alt text.'
                : kpInAlt
                    ? 'Focus keyphrase appears in at least one image alt attribute.'
                    : 'None of the image alt attributes contain the focus keyphrase.',
        });
    }
    // -- SEO checks ----------------------------------------------------------
    const metaTitleLen = (input.metaTitle ?? '').length;
    checks.push({
        id: 'meta-title-length',
        category: 'seo',
        title: 'Meta title length',
        status: metaTitleLen >= 30 && metaTitleLen <= 60
            ? 'good'
            : metaTitleLen > 0 && metaTitleLen < 30
                ? 'improvement'
                : 'problem',
        description: metaTitleLen === 0
            ? 'No meta title set. Add a meta title between 30 and 60 characters.'
            : metaTitleLen > 60
                ? `Meta title is ${metaTitleLen} characters. Keep it under 60 to avoid truncation in search results.`
                : metaTitleLen < 30
                    ? `Meta title is only ${metaTitleLen} characters. Aim for at least 30 characters.`
                    : `Meta title is ${metaTitleLen} characters — within the ideal 30–60 range.`,
    });
    const metaDescLen = (input.metaDescription ?? '').length;
    checks.push({
        id: 'meta-description-length',
        category: 'seo',
        title: 'Meta description length',
        status: metaDescLen >= 120 && metaDescLen <= 160
            ? 'good'
            : metaDescLen > 0 && metaDescLen < 120
                ? 'improvement'
                : 'problem',
        description: metaDescLen === 0
            ? 'No meta description set. Add one between 120 and 160 characters.'
            : metaDescLen > 160
                ? `Meta description is ${metaDescLen} characters. Keep it under 160.`
                : metaDescLen < 120
                    ? `Meta description is ${metaDescLen} characters. Aim for 120–160 characters.`
                    : `Meta description is ${metaDescLen} characters — within the ideal range.`,
    });
    const minWords = input.isCornerstone ? 900 : 300;
    const improvementMin = input.isCornerstone ? 600 : 150;
    checks.push({
        id: 'content-length',
        category: 'seo',
        title: 'Content length',
        status: words.length >= minWords
            ? 'good'
            : words.length >= improvementMin
                ? 'improvement'
                : 'problem',
        description: words.length >= minWords
            ? `Content is ${words.length} words — meets the ${input.isCornerstone ? 'cornerstone' : 'recommended'} minimum of ${minWords}.`
            : `Content is only ${words.length} words. Aim for at least ${minWords} words${input.isCornerstone ? ' for cornerstone content' : ''}.`,
    });
    const links = extractLinks(input.content);
    checks.push({
        id: 'internal-links',
        category: 'seo',
        title: 'Internal links',
        status: links.internal >= 1 ? 'good' : 'improvement',
        description: links.internal >= 1
            ? `Found ${links.internal} internal link${links.internal > 1 ? 's' : ''}.`
            : 'No internal links found. Add links to related content on your site.',
    });
    checks.push({
        id: 'outbound-links',
        category: 'seo',
        title: 'Outbound links',
        status: links.external >= 1 ? 'good' : 'improvement',
        description: links.external >= 1
            ? `Found ${links.external} outbound link${links.external > 1 ? 's' : ''}.`
            : 'No outbound links found. Consider linking to authoritative external sources.',
    });
    const imgInfo = extractImgAlts(input.content);
    checks.push({
        id: 'image-alt-coverage',
        category: 'seo',
        title: 'Image alt text',
        status: imgInfo.total === 0 ? 'improvement' : imgInfo.withAlt === imgInfo.total ? 'good' : 'problem',
        description: imgInfo.total === 0
            ? 'No images found in the content.'
            : imgInfo.withAlt === imgInfo.total
                ? `All ${imgInfo.total} image${imgInfo.total > 1 ? 's have' : ' has'} alt text.`
                : `${imgInfo.total - imgInfo.withAlt} of ${imgInfo.total} image${imgInfo.total > 1 ? 's are' : ' is'} missing alt text.`,
    });
    const h1Tags = extractHeadings(input.content, 1);
    checks.push({
        id: 'h1-present',
        category: 'seo',
        title: 'H1 heading',
        status: h1Tags.length === 1 ? 'good' : 'problem',
        description: h1Tags.length === 0
            ? 'No H1 heading found. Add exactly one H1 to the content.'
            : h1Tags.length === 1
                ? 'Content has exactly one H1 heading.'
                : `Found ${h1Tags.length} H1 headings. Use exactly one H1 per page.`,
    });
    const subheadingAll = [
        ...extractHeadings(input.content, 2),
        ...extractHeadings(input.content, 3),
        ...extractHeadings(input.content, 4),
    ];
    const shouldHaveSubheadings = words.length > 300;
    const subheadingOk = !shouldHaveSubheadings || subheadingAll.length >= Math.floor(words.length / 300);
    checks.push({
        id: 'subheading-distribution',
        category: 'seo',
        title: 'Subheading distribution',
        status: !shouldHaveSubheadings ? 'good' : subheadingOk ? 'good' : 'improvement',
        description: subheadingOk
            ? 'Subheadings are well distributed throughout the content.'
            : `Content is ${words.length} words but has only ${subheadingAll.length} subheading${subheadingAll.length !== 1 ? 's' : ''}. Add subheadings roughly every 300 words.`,
    });
    checks.push({
        id: 'canonical-url',
        category: 'seo',
        title: 'Canonical URL',
        status: input.canonical && input.canonical.length > 0 ? 'good' : 'improvement',
        description: input.canonical && input.canonical.length > 0
            ? 'Canonical URL is set.'
            : 'No canonical URL set. Consider adding one to avoid duplicate content issues.',
    });
    // -- Readability checks --------------------------------------------------
    const readability = calculateReadability(plainText);
    checks.push({
        id: 'flesch-reading-ease',
        category: 'readability',
        title: 'Flesch reading ease',
        status: readability.fleschScore >= 60
            ? 'good'
            : readability.fleschScore >= 40
                ? 'improvement'
                : 'problem',
        description: `Flesch reading ease score is ${readability.fleschScore.toFixed(1)} (${readability.fleschLabel}). ${readability.fleschScore >= 60 ? 'Content is easy to read.' : 'Try using shorter sentences and simpler words.'}`,
    });
    checks.push({
        id: 'avg-sentence-length',
        category: 'readability',
        title: 'Sentence length',
        status: readability.avgSentenceLength <= 20
            ? 'good'
            : readability.avgSentenceLength <= 25
                ? 'improvement'
                : 'problem',
        description: readability.avgSentenceLength <= 20
            ? `Average sentence length is ${readability.avgSentenceLength.toFixed(1)} words — easy to follow.`
            : `Average sentence length is ${readability.avgSentenceLength.toFixed(1)} words. Aim for under 20 words per sentence.`,
    });
    checks.push({
        id: 'passive-voice',
        category: 'readability',
        title: 'Passive voice',
        status: readability.passiveVoicePercent < 10
            ? 'good'
            : readability.passiveVoicePercent <= 15
                ? 'improvement'
                : 'problem',
        description: readability.passiveVoicePercent < 10
            ? `${readability.passiveVoicePercent.toFixed(1)}% of sentences use passive voice — within the recommended limit.`
            : `${readability.passiveVoicePercent.toFixed(1)}% of sentences use passive voice. Aim for less than 10%.`,
    });
    const paragraphs = splitParagraphs(plainText);
    const longParagraphs = paragraphs.filter((p) => getWords(p).length > 150);
    checks.push({
        id: 'paragraph-length',
        category: 'readability',
        title: 'Paragraph length',
        status: longParagraphs.length === 0 ? 'good' : 'improvement',
        description: longParagraphs.length === 0
            ? 'All paragraphs are a reasonable length.'
            : `${longParagraphs.length} paragraph${longParagraphs.length > 1 ? 's exceed' : ' exceeds'} 150 words. Break long paragraphs into smaller ones.`,
    });
    const sentences = splitSentences(plainText);
    const sentencesWithTransition = sentences.filter((s) => {
        const lower = s.toLowerCase();
        return TRANSITION_WORDS.some((tw) => {
            const idx = lower.indexOf(tw);
            if (idx === -1)
                return false;
            if (idx === 0)
                return true;
            const before = lower[idx - 1];
            return before === ' ' || before === ',' || before === ';';
        });
    });
    const transitionPct = sentences.length > 0 ? (sentencesWithTransition.length / sentences.length) * 100 : 0;
    checks.push({
        id: 'transition-words',
        category: 'readability',
        title: 'Transition words',
        status: transitionPct >= 30 ? 'good' : transitionPct >= 20 ? 'improvement' : 'problem',
        description: transitionPct >= 30
            ? `${transitionPct.toFixed(1)}% of sentences contain transition words — good flow.`
            : `Only ${transitionPct.toFixed(1)}% of sentences contain transition words. Use words like "however", "therefore", and "for example" to improve flow.`,
    });
    let consecutiveCount = 0;
    for (let i = 1; i < sentences.length; i++) {
        const prevFirst = getWords(sentences[i - 1] ?? '')[0]?.toLowerCase();
        const currFirst = getWords(sentences[i] ?? '')[0]?.toLowerCase();
        if (prevFirst && currFirst && prevFirst === currFirst) {
            consecutiveCount++;
        }
    }
    checks.push({
        id: 'consecutive-sentences',
        category: 'readability',
        title: 'Consecutive sentences',
        status: consecutiveCount === 0 ? 'good' : consecutiveCount <= 2 ? 'improvement' : 'problem',
        description: consecutiveCount === 0
            ? 'No consecutive sentences start with the same word.'
            : `${consecutiveCount} pair${consecutiveCount > 1 ? 's' : ''} of consecutive sentences start with the same word. Vary your sentence openings.`,
    });
    // -- Social checks -------------------------------------------------------
    checks.push({
        id: 'og-title',
        category: 'social',
        title: 'Open Graph title',
        status: input.ogTitle && input.ogTitle.length > 0 ? 'good' : 'improvement',
        description: input.ogTitle && input.ogTitle.length > 0
            ? 'Open Graph title is set for social sharing.'
            : 'No Open Graph title set. Add one for better social media previews.',
    });
    checks.push({
        id: 'og-description',
        category: 'social',
        title: 'Open Graph description',
        status: input.ogDescription && input.ogDescription.length > 0 ? 'good' : 'improvement',
        description: input.ogDescription && input.ogDescription.length > 0
            ? 'Open Graph description is set for social sharing.'
            : 'No Open Graph description set. Add one for better social media previews.',
    });
    checks.push({
        id: 'og-image',
        category: 'social',
        title: 'Open Graph image',
        status: input.ogImage && input.ogImage.length > 0 ? 'good' : 'improvement',
        description: input.ogImage && input.ogImage.length > 0
            ? 'Open Graph image is set for social sharing.'
            : 'No Open Graph image set. Posts with images get significantly more engagement.',
    });
    // -- Score calculation ----------------------------------------------------
    const totalPoints = checks.reduce((sum, c) => {
        if (c.status === 'good')
            return sum + 1;
        if (c.status === 'improvement')
            return sum + 0.5;
        return sum;
    }, 0);
    const score = checks.length > 0 ? Math.round((totalPoints / checks.length) * 100) : 0;
    return { score, checks };
}
// ---- Readability ----------------------------------------------------------
/**
 * Calculate readability metrics for a block of plain text.
 *
 * Includes the Flesch reading ease score, average sentence/word lengths,
 * passive voice percentage, word/sentence/paragraph counts, and estimated
 * reading time.
 */
export function calculateReadability(text) {
    const sentences = splitSentences(text);
    const paragraphs = splitParagraphs(text);
    const words = getWords(text);
    const wordCount = words.length;
    const sentenceCount = Math.max(sentences.length, 1);
    const paragraphCount = paragraphs.length;
    const totalSyllables = words.reduce((sum, w) => sum + countSyllables(w), 0);
    const avgSentenceLength = wordCount / sentenceCount;
    const avgWordLength = wordCount > 0
        ? words.reduce((sum, w) => sum + w.replace(/[^a-zA-Z]/g, '').length, 0) / wordCount
        : 0;
    const fleschScore = wordCount > 0 ? 206.835 - 1.015 * avgSentenceLength - 84.6 * (totalSyllables / wordCount) : 0;
    const clampedFlesch = Math.max(0, Math.min(100, fleschScore));
    const passiveSentences = sentences.filter(detectPassiveVoice).length;
    const passiveVoicePercent = sentenceCount > 0 ? (passiveSentences / sentenceCount) * 100 : 0;
    const readingTimeMinutes = Math.max(1, Math.ceil(wordCount / 200));
    return {
        fleschScore: Math.round(clampedFlesch * 10) / 10,
        fleschLabel: fleschLabel(clampedFlesch),
        avgSentenceLength: Math.round(avgSentenceLength * 10) / 10,
        avgWordLength: Math.round(avgWordLength * 10) / 10,
        passiveVoicePercent: Math.round(passiveVoicePercent * 10) / 10,
        wordCount,
        sentenceCount,
        paragraphCount,
        readingTimeMinutes,
    };
}
//# sourceMappingURL=analysis.js.map