relay-knowledge 1.1.9

Graph-database-based knowledge graph project.
Documentation
use super::super::{
    model::{ConfigFact, ConfigImport},
    source::{push_definition, push_import, source_lines},
};

pub(in crate::code::config_files) fn facts(content: &str, definitions: &mut Vec<ConfigFact>) {
    let mut state = ScanState::default();
    for line in source_lines(content) {
        for name in element_names(line.text, &mut state) {
            push_definition(definitions, name, "element", line.range());
        }
    }
}

pub(in crate::code::config_files) fn imports(content: &str, imports: &mut Vec<ConfigImport>) {
    let mut state = ScanState::default();
    for line in source_lines(content) {
        for module in import_modules(line.text, &mut state) {
            push_import(imports, module, line.range());
        }
    }
}

#[derive(Default)]
struct ScanState {
    in_comment: bool,
    in_cdata: bool,
    pending_start_tag: String,
}

fn element_names<'a>(line: &'a str, state: &mut ScanState) -> Vec<&'a str> {
    let mut names = Vec::new();
    let mut rest = line;
    loop {
        if state.in_comment {
            let Some(end) = rest.find("-->") else {
                return names;
            };
            state.in_comment = false;
            rest = &rest[end + "-->".len()..];
        }
        if state.in_cdata {
            let Some(end) = rest.find("]]>") else {
                return names;
            };
            state.in_cdata = false;
            rest = &rest[end + "]]>".len()..];
        }

        let Some(index) = rest.find('<') else {
            return names;
        };
        let after = &rest[index + 1..];
        if let Some(next) = after.strip_prefix('/') {
            rest = next;
            continue;
        }
        let skip_marker = if after.starts_with("![CDATA[") {
            Some("]]>")
        } else if after.starts_with("!--") {
            Some("-->")
        } else if after.starts_with('?') {
            Some("?>")
        } else if after.starts_with('!') {
            Some(">")
        } else {
            None
        };
        if after.starts_with("![CDATA[") {
            let Some(end) = after.find("]]>") else {
                state.in_cdata = true;
                return names;
            };
            rest = &after[end + "]]>".len()..];
            continue;
        }
        if after.starts_with("!--") {
            let Some(end) = after.find("-->") else {
                state.in_comment = true;
                return names;
            };
            rest = &after[end + "-->".len()..];
            continue;
        }
        if let Some(marker) = skip_marker {
            rest = after
                .find(marker)
                .map(|end| &after[end + marker.len()..])
                .unwrap_or("");
            continue;
        }
        if let Some(name) = name(after) {
            names.push(name);
        }
        rest = after.find('>').map(|end| &after[end + 1..]).unwrap_or("");
    }
}

fn strip_ignored(value: &str, state: &mut ScanState) -> String {
    let mut output = String::new();
    let mut rest = value;
    loop {
        if state.in_comment {
            let Some(end) = rest.find("-->") else {
                return output;
            };
            state.in_comment = false;
            rest = &rest[end + "-->".len()..];
        }
        if state.in_cdata {
            let Some(end) = rest.find("]]>") else {
                return output;
            };
            state.in_cdata = false;
            rest = &rest[end + "]]>".len()..];
        }
        let Some((start, marker)) = next_ignored_marker(rest) else {
            output.push_str(rest);
            return output;
        };
        output.push_str(&rest[..start]);
        match marker {
            IgnoredMarker::Comment => {
                rest = &rest[start + "<!--".len()..];
                state.in_comment = true;
            }
            IgnoredMarker::Cdata => {
                rest = &rest[start + "<![CDATA[".len()..];
                state.in_cdata = true;
            }
        }
    }
}

fn import_modules(line: &str, state: &mut ScanState) -> Vec<String> {
    let text = strip_ignored(line, state);
    let mut modules = Vec::new();

    for tag in start_tags(&text, state) {
        for value in attribute_values(&tag, "schemaLocation") {
            modules.extend(schema_location_paths(value).map(str::to_owned));
        }
        for value in attribute_values(&tag, "noNamespaceSchemaLocation") {
            modules.push(value.to_owned());
        }
        for attribute in ["href", "location"] {
            for value in attribute_values(&tag, attribute) {
                modules.push(value.to_owned());
            }
        }
    }

    modules
}

#[derive(Clone, Copy)]
enum IgnoredMarker {
    Comment,
    Cdata,
}

fn next_ignored_marker(value: &str) -> Option<(usize, IgnoredMarker)> {
    match (value.find("<!--"), value.find("<![CDATA[")) {
        (Some(comment), Some(cdata)) if comment <= cdata => Some((comment, IgnoredMarker::Comment)),
        (Some(_), Some(cdata)) => Some((cdata, IgnoredMarker::Cdata)),
        (Some(comment), None) => Some((comment, IgnoredMarker::Comment)),
        (None, Some(cdata)) => Some((cdata, IgnoredMarker::Cdata)),
        (None, None) => None,
    }
}

fn schema_location_paths(value: &str) -> impl Iterator<Item = &str> {
    value
        .split_whitespace()
        .enumerate()
        .filter_map(|(index, part)| (index % 2 == 1).then_some(part))
}

fn start_tags(line: &str, state: &mut ScanState) -> Vec<String> {
    let mut tags = Vec::new();
    let mut rest = line;
    if !state.pending_start_tag.is_empty() {
        let Some(end) = rest.find('>') else {
            append_pending_start_tag(&mut state.pending_start_tag, rest);
            return tags;
        };
        append_pending_start_tag(&mut state.pending_start_tag, &rest[..end]);
        tags.push(std::mem::take(&mut state.pending_start_tag));
        rest = &rest[end + '>'.len_utf8()..];
    }

    while let Some(index) = rest.find('<') {
        let after = &rest[index + '<'.len_utf8()..];
        if after
            .chars()
            .next()
            .is_some_and(|character| matches!(character, '/' | '?' | '!'))
        {
            rest = after.find('>').map(|end| &after[end + 1..]).unwrap_or("");
            continue;
        }
        let Some(end) = after.find('>') else {
            append_pending_start_tag(&mut state.pending_start_tag, after);
            break;
        };
        tags.push(after[..end].to_owned());
        rest = &after[end + 1..];
    }

    tags
}

fn append_pending_start_tag(tag: &mut String, text: &str) {
    let text = text.trim();
    if text.is_empty() {
        return;
    }
    if !tag.is_empty() {
        tag.push(' ');
    }
    tag.push_str(text);
}

fn attribute_values<'a>(line: &'a str, name: &str) -> Vec<&'a str> {
    let mut values = Vec::new();
    let mut search_start = 0;
    while let Some(relative_index) = line[search_start..].find(name) {
        let index = search_start + relative_index;
        search_start = index + name.len();
        if !attribute_name_starts_at(line, index) {
            continue;
        }
        let after_name = line[index + name.len()..].trim_start();
        if !after_name.starts_with('=') {
            continue;
        }
        let after_equals = after_name['='.len_utf8()..].trim_start();
        if let Some((value, after)) = first_quoted_with_rest(after_equals) {
            values.push(value);
            search_start = line.len() - after.len();
        } else {
            break;
        }
    }

    values
}

fn attribute_name_starts_at(line: &str, index: usize) -> bool {
    line[..index]
        .chars()
        .next_back()
        .is_none_or(|character| character.is_whitespace() || matches!(character, '<' | '/' | ':'))
}

fn first_quoted_with_rest(value: &str) -> Option<(&str, &str)> {
    let quote = value
        .chars()
        .find(|character| matches!(character, '"' | '\''))?;
    let start = value.find(quote)? + quote.len_utf8();
    let rest = &value[start..];
    let end = rest.find(quote)?;

    Some((&rest[..end], &rest[end + quote.len_utf8()..]))
}

fn name(value: &str) -> Option<&str> {
    let end = value
        .find(|character: char| character.is_whitespace() || matches!(character, '>' | '/'))
        .unwrap_or(value.len());
    let name = &value[..end];
    (!name.is_empty()).then_some(name)
}