workshop-rs 1.23.0

Canonical multi-locale Overwatch Workshop semantic core: catalog, parser, WIR, validation, emitter.
Documentation
//! Structural keyword spelling lookups shared by the parser and by source
//! layout scans that read a token stream without a parse cursor.

use super::lexer::{Token, TokenKind};
use super::parser::{canonical_keyword, structural_keyword_map};
use crate::catalog::{Catalog, Kind, Locale};
use crate::settings::table;

/// The spellings accepted for structural `id` under `locale`: its `locale`
/// spellings, the primary locale's when it differs, and the static
/// structural-keyword table whose English resolves to `id` — the same set
/// `ParseContext` caches per id.
pub(crate) fn structural_spellings(catalog: &Catalog, locale: &Locale, id: &str) -> Vec<String> {
    let mut spellings = Vec::new();
    if let Some(entry) = catalog.entry(Kind::Structural, id) {
        spellings.extend(entry.spellings(locale).iter().cloned());
        let primary = catalog.primary_locale();
        if locale != primary {
            spellings.extend(entry.spellings(primary).iter().cloned());
        }
    }
    for (spelling, english) in structural_keyword_map() {
        if catalog
            .resolve(Kind::Structural, catalog.primary_locale(), english)
            .is_some_and(|entry| entry.id == id)
        {
            spellings.push(spelling.clone());
        }
    }
    spellings
}

/// The canonical structural id `spelling` opens under `locale` — the same
/// resolution `ParseContext::canonical_keyword` applies (locale, then
/// primary locale, then the static keyword table), defaulting to the
/// spelling itself.
pub(crate) fn canonical_structural_id(
    catalog: &Catalog,
    locale: &Locale,
    spelling: &str,
) -> String {
    catalog
        .resolve(Kind::Structural, locale, spelling)
        .or_else(|| {
            (locale != catalog.primary_locale())
                .then(|| catalog.resolve(Kind::Structural, catalog.primary_locale(), spelling))?
        })
        .map(|entry| entry.id.clone())
        .unwrap_or_else(|| canonical_keyword(spelling).to_string())
}

/// The token count `spelling` covers at `index` — the same check
/// `ParseContext::match_spelling_tokens` performs at the cursor.
pub(crate) fn spelling_token_len(tokens: &[Token], index: usize, spelling: &str) -> Option<usize> {
    let mut offset = 0;
    for piece in spelling.split(' ') {
        let (word, dotted) = match piece.strip_suffix('.') {
            Some(word) => (word, true),
            None => (piece, false),
        };
        if !word.is_empty() {
            match tokens.get(index + offset).map(|token| &token.kind) {
                Some(TokenKind::Word(candidate)) if table::names_eq(candidate, word) => {}
                Some(TokenKind::Number { text, .. }) if text == word => {}
                _ => return None,
            }
            offset += 1;
        }
        if dotted {
            match tokens.get(index + offset).map(|token| &token.kind) {
                Some(TokenKind::Dot) => offset += 1,
                _ => return None,
            }
        }
    }
    (offset > 0).then_some(offset)
}

/// The longest structural keyword prefix among `ids` at `index`, without
/// consuming — the lookup half of `ParseContext::keyword_prefix` split from
/// its cursor advance.
pub(crate) fn structural_keyword_prefix(
    tokens: &[Token],
    index: usize,
    catalog: &Catalog,
    locale: &Locale,
    ids: &[&str],
) -> Option<(String, usize)> {
    let mut best: Option<(String, usize)> = None;
    for id in ids {
        for spelling in structural_spellings(catalog, locale, id) {
            if let Some(len) = spelling_token_len(tokens, index, &spelling)
                && best.as_ref().is_none_or(|(_, length)| len > *length)
            {
                best = Some(((*id).to_string(), len));
            }
        }
    }
    best
}