use std::sync::OnceLock;
use regex::Regex;
use crate::builtin_grammar::{GrammarNode, propagate_errors};
use crate::line_index::LineIndex;
use crate::lino_grammar::parse_lino_cst;
use crate::natural_language::{annotate_natural_language, canonical_natural_language};
use crate::pdf_grammar::parse_pdf_cst;
use crate::tree_sitter_adapter::SpanOffset;
use crate::{
ByteRange, LinkFlags, LinkId, LinkMetadata, LinkNetwork, LinkType, ParseConfiguration,
SourceSpan,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TextGrammar {
Plain,
Lino,
Pdf,
Natural,
}
impl TextGrammar {
fn for_language(language: &str) -> Option<Self> {
match language.to_ascii_lowercase().as_str() {
"txt" | "text" | "plain text" => Some(Self::Plain),
"lino" => Some(Self::Lino),
"pdf" => Some(Self::Pdf),
_ if canonical_natural_language(language).is_some() => Some(Self::Natural),
_ => None,
}
}
}
pub fn parse_plain(text: &str, language: &str, configuration: ParseConfiguration) -> LinkNetwork {
parse_document(text, language, configuration, TextGrammar::Plain)
}
pub fn parse_lino(text: &str, language: &str, configuration: ParseConfiguration) -> LinkNetwork {
parse_document(text, language, configuration, TextGrammar::Lino)
}
pub fn parse_pdf(text: &str, language: &str, configuration: ParseConfiguration) -> LinkNetwork {
parse_document(text, language, configuration, TextGrammar::Pdf)
}
pub fn parse_natural(text: &str, language: &str, configuration: ParseConfiguration) -> LinkNetwork {
let (mut network, document) = LinkNetwork::new_parse_document(text, language);
insert_grammar(
&mut network,
document,
text,
language,
configuration,
TextGrammar::Natural,
SpanOffset::zero(),
);
network.attach_embedded_regions(document, text, language, configuration);
annotate_natural_language(&mut network, document, text, language, configuration);
network
}
pub fn parse_embedded_region_into(
network: &mut LinkNetwork,
region: LinkId,
text: &str,
language: &str,
span: SourceSpan,
configuration: ParseConfiguration,
) -> Option<LinkId> {
let grammar = TextGrammar::for_language(language)?;
let offset = SpanOffset::new(span.byte_range().start(), span.start_point());
Some(insert_grammar(
network,
region,
text,
language,
configuration,
grammar,
offset,
))
}
fn parse_document(
text: &str,
language: &str,
configuration: ParseConfiguration,
grammar: TextGrammar,
) -> LinkNetwork {
let (mut network, document) = LinkNetwork::new_parse_document(text, language);
insert_grammar(
&mut network,
document,
text,
language,
configuration,
grammar,
SpanOffset::zero(),
);
network.attach_embedded_regions(document, text, language, configuration);
network
}
fn insert_grammar(
network: &mut LinkNetwork,
parent: LinkId,
text: &str,
language: &str,
configuration: ParseConfiguration,
grammar: TextGrammar,
offset: SpanOffset,
) -> LinkId {
let source = Source {
text,
language,
lines: LineIndex::new(text),
offset,
configuration,
};
let tree = match grammar {
TextGrammar::Plain => parse_plain_text_cst(text),
TextGrammar::Lino => parse_lino_cst(text),
TextGrammar::Pdf => parse_pdf_cst(text),
TextGrammar::Natural => parse_natural_language_cst(text),
};
insert_grammar_node(network, parent, &source, &tree)
}
struct Source<'a> {
text: &'a str,
language: &'a str,
lines: LineIndex,
offset: SpanOffset,
configuration: ParseConfiguration,
}
impl Source<'_> {
fn span(&self, start: usize, end: usize) -> SourceSpan {
SourceSpan::new(
ByteRange::new(self.offset.byte(start), self.offset.byte(end)),
self.offset.point(self.lines.byte_point(start)),
self.offset.point(self.lines.byte_point(end)),
)
}
}
fn insert_grammar_node(
network: &mut LinkNetwork,
parent: LinkId,
source: &Source<'_>,
node: &GrammarNode,
) -> LinkId {
let flags = if node.is_error {
LinkFlags::error()
} else if node.is_missing {
LinkFlags::missing()
} else if node.has_error {
LinkFlags::containing_error()
} else if node.extra {
LinkFlags::extra()
} else {
LinkFlags::clean()
};
let span = source.span(node.start, node.end);
let syntax = network.insert_link(
[parent],
LinkMetadata::new()
.with_link_type(LinkType::Syntax)
.with_named(node.named)
.with_term(node.term)
.with_language(source.language)
.with_span(span)
.with_flags(flags),
);
if node.is_token() {
let token = network.insert_link(
[syntax],
LinkMetadata::new()
.with_link_type(LinkType::Token)
.with_named(node.named)
.with_term(&source.text[node.start..node.end])
.with_language(source.language)
.with_span(span)
.with_flags(flags),
);
if node.extra {
network.attach_trivia(
syntax,
token,
span,
source.configuration.trivia_attachment_policy(),
);
}
}
for child in &node.children {
let child_id = insert_grammar_node(network, syntax, source, child);
if let Some(field) = child.field {
network.insert_field(syntax, field, child_id);
}
}
syntax
}
pub fn parse_plain_text_cst(text: &str) -> GrammarNode {
let mut lines = Vec::new();
let mut start = 0;
for line in text.split_inclusive('\n') {
let end = start + line.len();
lines.push(GrammarNode::node(
"line",
start,
end,
lexical_nodes(text, start, end),
));
start = end;
}
if start < text.len() || text.is_empty() {
lines.push(GrammarNode::node(
"line",
start,
text.len(),
lexical_nodes(text, start, text.len()),
));
}
let mut root = GrammarNode::node("text_document", 0, text.len(), lines);
root.is_error = !balanced_parentheses(text);
propagate_errors(&mut root);
root
}
pub fn parse_natural_language_cst(text: &str) -> GrammarNode {
let sentences = sentence_ranges(text)
.into_iter()
.map(|(start, end)| {
GrammarNode::node("sentence", start, end, lexical_nodes(text, start, end))
})
.collect();
let mut root = GrammarNode::node("natural_language_document", 0, text.len(), sentences);
propagate_errors(&mut root);
root
}
fn lexical_nodes(text: &str, start: usize, end: usize) -> Vec<GrammarNode> {
lexical_pattern()
.find_iter(&text[start..end])
.map(|segment_match| {
let segment = segment_match.as_str();
let segment_start = start + segment_match.start();
let segment_end = start + segment_match.end();
if segment.chars().all(char::is_whitespace) {
GrammarNode::extra("whitespace", false, segment_start, segment_end)
} else if segment.chars().all(is_control_character) {
GrammarNode::error(segment_start, segment_end, Vec::new())
} else if word_pattern().is_match(segment) {
GrammarNode::node("word", segment_start, segment_end, Vec::new())
} else {
GrammarNode::node("punctuation", segment_start, segment_end, Vec::new())
}
})
.collect()
}
const WORD_CHARACTERS: &str = r"\p{L}\p{N}\p{M}_'\-";
const CONTROL_CHARACTERS: &str = r"\x00-\x08\x0E-\x1F\x7F-\x{84}\x{86}-\x{9F}";
const fn is_control_character(character: char) -> bool {
matches!(
character,
'\u{0}'..='\u{8}' | '\u{E}'..='\u{1F}' | '\u{7F}'..='\u{84}' | '\u{86}'..='\u{9F}'
)
}
fn lexical_pattern() -> &'static Regex {
static PATTERN: OnceLock<Regex> = OnceLock::new();
PATTERN.get_or_init(|| {
Regex::new(&format!(
r"\p{{White_Space}}+|[{WORD_CHARACTERS}]+|[{CONTROL_CHARACTERS}]+|[^\p{{White_Space}}{WORD_CHARACTERS}{CONTROL_CHARACTERS}]+"
))
.expect("the built-in lexical grammar is a valid pattern")
})
}
fn word_pattern() -> &'static Regex {
static PATTERN: OnceLock<Regex> = OnceLock::new();
PATTERN.get_or_init(|| {
Regex::new(&format!(r"^[{WORD_CHARACTERS}]+$")).expect("the built-in word pattern is valid")
})
}
fn sentence_pattern() -> &'static Regex {
static PATTERN: OnceLock<Regex> = OnceLock::new();
PATTERN.get_or_init(|| {
Regex::new(
r"(?s).*?[.!?\x{0964}\x{3002}\x{061F}\x{06D4}\x{FF01}\x{FF1F}]+[\p{Pe}\p{Pf}\x{0022}\x{0027}]*\p{White_Space}*",
)
.expect("the built-in sentence grammar is a valid pattern")
})
}
fn sentence_ranges(text: &str) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut start = 0;
for sentence in sentence_pattern().find_iter(text) {
ranges.push((sentence.start(), sentence.end()));
start = sentence.end();
}
if start < text.len() {
ranges.push((start, text.len()));
}
ranges
}
fn balanced_parentheses(text: &str) -> bool {
let mut depth = 0_usize;
for character in text.chars() {
match character {
'(' => depth += 1,
')' if depth == 0 => return false,
')' => depth -= 1,
_ => {}
}
}
depth == 0
}