use std::error::Error;
use std::fmt;
use std::ops::Range;
use crate::Span;
use crate::layout::{self, is_separator};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LineBreak {
Author,
Typeset,
}
impl LineBreak {
pub fn name(self) -> &'static str {
match self {
LineBreak::Author => "author",
LineBreak::Typeset => "typeset",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Line {
pub span: Span,
pub break_before: Option<LineBreak>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Sentence {
pub span: Span,
pub tokens: Range<usize>,
pub words: Range<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Paragraph {
pub span: Span,
pub sentences: Range<usize>,
pub tokens: Range<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Column {
Lemma,
Upos,
Xpos,
Feats,
Deprel,
Deps,
Misc,
}
impl Column {
pub const ALL: [Column; 7] = [
Column::Lemma,
Column::Upos,
Column::Xpos,
Column::Feats,
Column::Deprel,
Column::Deps,
Column::Misc,
];
pub fn name(self) -> &'static str {
match self {
Column::Lemma => "lemma",
Column::Upos => "upos",
Column::Xpos => "xpos",
Column::Feats => "feats",
Column::Deprel => "deprel",
Column::Deps => "deps",
Column::Misc => "misc",
}
}
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|column| column.name() == name)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DocumentError(pub(crate) String);
impl fmt::Display for DocumentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Error for DocumentError {}
fn fitted<T: Copy>(vector: Vec<T>) -> Vec<T> {
if vector.capacity() > vector.len() {
vector.as_slice().to_vec()
} else {
vector
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Verbatim {
pub(crate) comments: Vec<Vec<String>>,
pub(crate) multiword: Vec<(usize, String)>,
pub(crate) empty: Vec<Vec<(usize, String)>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Document {
pub(crate) text: String,
pub(crate) tokens: Vec<Span>,
pub(crate) token_words: Vec<usize>,
pub(crate) word_tokens: Vec<usize>,
pub(crate) forms: Vec<Option<String>>,
pub(crate) columns: [Vec<Option<String>>; 7],
pub(crate) heads: Vec<Option<u32>>,
pub(crate) sentence_tokens: Vec<usize>,
pub(crate) paragraph_sentences: Vec<usize>,
pub(crate) lines: Vec<Line>,
pub(crate) verbatim: Option<Box<Verbatim>>,
}
impl Document {
pub fn from_tokens(
text: String,
tokens: Vec<Span>,
sentence_starts: Option<&[usize]>,
) -> Result<Self, DocumentError> {
let mut previous = 0;
for (index, span) in tokens.iter().enumerate() {
let error = |message: &str| Err(DocumentError(format!("token {index}: {message}")));
if span.start >= span.end {
return error("is empty");
}
if span.end > text.len() {
return error("ends after the text");
}
if !text.is_char_boundary(span.start) || !text.is_char_boundary(span.end) {
return error("is not on character boundaries");
}
if span.start < previous {
return error("overlaps or precedes the token before it");
}
let piece = span.slice(&text);
if piece.starts_with(is_separator) || piece.ends_with(is_separator) {
return error("starts or ends with whitespace");
}
if !text[previous..span.start].chars().all(is_separator) {
return error("the text before it holds characters that no token has");
}
previous = span.end;
}
if !text[previous..].chars().all(is_separator) {
return Err(DocumentError(
"the text after the last token holds characters that no token has".to_owned(),
));
}
if let Some(starts) = sentence_starts {
for (position, &start) in starts.iter().enumerate() {
if start >= tokens.len() || (position > 0 && start <= starts[position - 1]) {
return Err(DocumentError(
"sentence starts must be increasing token indices".to_owned(),
));
}
}
}
Self::assemble(text, tokens, sentence_starts)
}
pub(crate) fn segmented(
text: String,
tokens: Vec<Span>,
mut sentence_tokens: Vec<usize>,
mut paragraph_sentences: Vec<usize>,
lines: Vec<Line>,
) -> Self {
let count = tokens.len();
paragraph_sentences.push(sentence_tokens.len());
sentence_tokens.push(count);
Document {
text,
tokens: fitted(tokens),
token_words: (0..=count).collect(),
word_tokens: (0..count).collect(),
forms: Vec::new(),
columns: Default::default(),
heads: Vec::new(),
sentence_tokens: fitted(sentence_tokens),
paragraph_sentences: fitted(paragraph_sentences),
lines: fitted(lines),
verbatim: None,
}
}
pub(crate) fn assemble(
text: String,
tokens: Vec<Span>,
sentence_starts: Option<&[usize]>,
) -> Result<Self, DocumentError> {
let mut starts = Vec::new();
let mut paragraph_starts = Vec::new();
let mut lines = Vec::new();
let mut token = 0;
for paragraph in layout::paragraphs(&text) {
let end = paragraph.last().map_or(0, |line| line.end);
let first = token;
while token < tokens.len() && tokens[token].start < end {
if tokens[token].end > end {
return Err(DocumentError(format!(
"token {token}: runs over a paragraph boundary"
)));
}
token += 1;
}
if token == first {
continue;
}
paragraph_starts.push(first);
starts.push(first);
lines.extend(paragraph.iter().enumerate().map(|(index, &span)| Line {
span,
break_before: (index > 0).then_some(LineBreak::Author),
}));
}
if let Some(given) = sentence_starts {
starts.extend_from_slice(given);
starts.sort_unstable();
starts.dedup();
}
let paragraph_sentences = paragraph_starts
.iter()
.map(|first| starts.partition_point(|start| start < first))
.chain([starts.len()])
.collect();
let count = tokens.len();
starts.push(count);
Ok(Document {
text,
tokens,
token_words: (0..=count).collect(),
word_tokens: (0..count).collect(),
forms: Vec::new(),
columns: Default::default(),
heads: Vec::new(),
sentence_tokens: starts,
paragraph_sentences,
lines,
verbatim: None,
})
}
pub fn text(&self) -> &str {
&self.text
}
pub fn tokens(&self) -> &[Span] {
&self.tokens
}
pub fn token_words(&self, token: usize) -> Range<usize> {
self.token_words[token]..self.token_words[token + 1]
}
pub fn word_count(&self) -> usize {
self.word_tokens.len()
}
pub fn word_token(&self, word: usize) -> usize {
self.word_tokens[word]
}
pub fn form(&self, word: usize) -> &str {
match self.forms.get(word) {
Some(Some(form)) => form,
_ => self.tokens[self.word_tokens[word]].slice(&self.text),
}
}
pub fn column(&self, column: Column, word: usize) -> Option<&str> {
self.check_word(word);
self.columns[column as usize]
.get(word)
.and_then(Option::as_deref)
}
pub fn head(&self, word: usize) -> Option<u32> {
self.check_word(word);
self.heads.get(word).copied().flatten()
}
fn check_word(&self, word: usize) {
let count = self.word_count();
assert!(
word < count,
"word {word} is out of range for {count} words"
);
}
pub fn sentence_count(&self) -> usize {
self.sentence_tokens.len() - 1
}
pub fn sentence(&self, index: usize) -> Sentence {
let tokens = self.sentence_tokens[index]..self.sentence_tokens[index + 1];
Sentence {
span: self.cover(&tokens),
words: self.token_words[tokens.start]..self.token_words[tokens.end],
tokens,
}
}
pub fn token_sentence(&self, token: usize) -> usize {
let count = self.tokens.len();
assert!(
token < count,
"token {token} is out of range for {count} tokens"
);
self.sentence_tokens
.partition_point(|&start| start <= token)
- 1
}
pub fn word_sentence(&self, word: usize) -> usize {
self.token_sentence(self.word_tokens[word])
}
pub fn word_id(&self, word: usize) -> usize {
let first = self.sentence_tokens[self.word_sentence(word)];
word - self.token_words[first] + 1
}
pub fn paragraph_count(&self) -> usize {
self.paragraph_sentences.len() - 1
}
pub fn paragraph(&self, index: usize) -> Paragraph {
let sentences = self.paragraph_sentences[index]..self.paragraph_sentences[index + 1];
let tokens = self.sentence_tokens[sentences.start]..self.sentence_tokens[sentences.end];
Paragraph {
span: self.cover(&tokens),
sentences,
tokens,
}
}
pub fn sentence_paragraph(&self, sentence: usize) -> usize {
let count = self.sentence_count();
assert!(
sentence < count,
"sentence {sentence} is out of range for {count} sentences"
);
self.paragraph_sentences
.partition_point(|&start| start <= sentence)
- 1
}
pub fn lines(&self) -> &[Line] {
&self.lines
}
fn cover(&self, tokens: &Range<usize>) -> Span {
Span {
start: self.tokens[tokens.start].start,
end: self.tokens[tokens.end - 1].end,
}
}
pub fn set_columns(
&mut self,
columns: Vec<(Column, Vec<Option<String>>)>,
heads: Option<Vec<Option<u32>>>,
) -> Result<(), DocumentError> {
let count = self.word_count();
let length = |name: &str, length: usize| {
if length == count {
Ok(())
} else {
Err(DocumentError(format!(
"{name} has {length} values for {count} words"
)))
}
};
for (column, values) in &columns {
length(column.name(), values.len())?;
let (wrong, what): (fn(char) -> bool, &str) = match column {
Column::Lemma | Column::Misc => {
(|c| matches!(c, '\t' | '\n' | '\r'), "a tab or a line break")
}
_ => (char::is_whitespace, "whitespace"),
};
for (word, value) in values.iter().enumerate() {
if let Some(value) = value
&& (value.is_empty() || value.contains(wrong))
{
return Err(DocumentError(format!(
"{} of word {word} is empty or holds {what}",
column.name()
)));
}
}
}
if let Some(heads) = &heads {
length("head", heads.len())?;
for (word, head) in heads.iter().enumerate() {
if let Some(head) = *head {
let words = self.sentence(self.word_sentence(word)).words.len();
if head as usize > words {
return Err(DocumentError(format!(
"head of word {word} is {head}, but its sentence has {words} words"
)));
}
}
}
}
for (column, values) in columns {
self.columns[column as usize] = if values.iter().all(Option::is_none) {
Vec::new()
} else {
values
};
}
if let Some(heads) = heads {
self.heads = if heads.iter().all(Option::is_none) {
Vec::new()
} else {
heads
};
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn document(text: &str, starts: Option<&[usize]>) -> Document {
let mut tokens = Vec::new();
layout::runs(
text,
Span {
start: 0,
end: text.len(),
},
&mut tokens,
);
Document::from_tokens(text.to_owned(), tokens, starts).unwrap()
}
#[test]
fn paragraphs_start_sentences() {
let doc = document("Ёж идёт.\nКот спит.\n\n Утро.", Some(&[1, 3]));
assert_eq!(doc.sentence_tokens, [0, 1, 3, 4, 5]);
assert_eq!(doc.paragraph_sentences, [0, 3, 4]);
assert_eq!(doc.paragraph(1).span.slice(doc.text()), "Утро.");
assert_eq!(doc.sentence(1).span.slice(doc.text()), "идёт.\nКот");
assert_eq!(doc.sentence_paragraph(2), 0);
assert_eq!(doc.token_sentence(4), 3);
let lines: Vec<(&str, Option<LineBreak>)> = doc
.lines()
.iter()
.map(|line| (line.span.slice(doc.text()), line.break_before))
.collect();
assert_eq!(
lines,
[
("Ёж идёт.", None),
("Кот спит.", Some(LineBreak::Author)),
("Утро.", None)
]
);
}
#[test]
fn words_are_tokens() {
let doc = document("Ёж идёт", None);
assert_eq!(doc.word_count(), 2);
assert_eq!(doc.form(1), "идёт");
assert_eq!(doc.word_token(1), 1);
assert_eq!(doc.token_words(1), 1..2);
assert_eq!(doc.word_id(1), 2);
assert_eq!(doc.column(Column::Lemma, 0), None);
assert_eq!(doc.head(0), None);
}
#[test]
fn indices_out_of_range() {
let doc = document("Ёж идёт", None);
for call in [
|doc: &Document| {
let _ = doc.column(Column::Lemma, 2);
},
|doc: &Document| {
let _ = doc.head(2);
},
|doc: &Document| {
let _ = doc.form(2);
},
|doc: &Document| {
let _ = doc.token_sentence(2);
},
|doc: &Document| {
let _ = doc.sentence_paragraph(1);
},
|doc: &Document| {
let _ = doc.sentence(1);
},
] {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| call(&doc)));
assert!(result.is_err());
}
}
#[test]
fn fitted_vectors_are_new_blocks() {
let mut vector = Vec::with_capacity(200_000);
vector.resize(150_000, Span { start: 0, end: 1 });
let block = vector.as_ptr();
let vector = fitted(vector);
assert_eq!(vector.capacity(), vector.len());
assert_ne!(vector.as_ptr(), block);
}
#[test]
fn blank_text() {
let doc = document(" \n\n ", None);
assert_eq!(
(
doc.sentence_count(),
doc.paragraph_count(),
doc.lines().len()
),
(0, 0, 0)
);
}
#[test]
fn wrong_tokens() {
let text = "Ёж идёт\n\nдомой".to_owned();
let span = |start, end| Span { start, end };
for (tokens, message) in [
(vec![span(0, 0)], "token 0: is empty"),
(vec![span(0, 99)], "token 0: ends after the text"),
(vec![span(0, 1)], "token 0: is not on character boundaries"),
(
vec![span(0, 4), span(5, 13), span(0, 4)],
"token 2: overlaps or precedes the token before it",
),
(vec![span(4, 13)], "token 0: starts or ends with whitespace"),
(
vec![span(5, 13), span(15, 25)],
"token 0: the text before it holds characters that no token has",
),
(
vec![span(0, 4), span(5, 13)],
"the text after the last token holds characters that no token has",
),
(
vec![span(0, 4), span(5, 25)],
"token 1: runs over a paragraph boundary",
),
] {
assert_eq!(
Document::from_tokens(text.clone(), tokens, None).unwrap_err(),
DocumentError(message.to_owned())
);
}
let tokens = vec![span(0, 4), span(5, 13), span(15, 25)];
for starts in [&[0, 0][..], &[1, 0], &[3]] {
assert_eq!(
Document::from_tokens(text.clone(), tokens.clone(), Some(starts)).unwrap_err(),
DocumentError("sentence starts must be increasing token indices".to_owned())
);
}
}
#[test]
fn set_columns() {
let mut doc = document("Ёж идёт. Кот", Some(&[2]));
doc.set_columns(
vec![(Column::Upos, vec![Some("NOUN".to_owned()), None, None])],
Some(vec![Some(2), Some(0), Some(0)]),
)
.unwrap();
assert_eq!(doc.column(Column::Upos, 0), Some("NOUN"));
assert_eq!(doc.head(0), Some(2));
for (columns, heads, message) in [
(
vec![(Column::Lemma, vec![None])],
None,
"lemma has 1 values for 3 words",
),
(
vec![(Column::Feats, vec![None, Some("a b".to_owned()), None])],
None,
"feats of word 1 is empty or holds whitespace",
),
(
vec![(Column::Lemma, vec![None, Some("a\nb".to_owned()), None])],
None,
"lemma of word 1 is empty or holds a tab or a line break",
),
(
vec![],
Some(vec![None, None, Some(2)]),
"head of word 2 is 2, but its sentence has 1 words",
),
] {
assert_eq!(
doc.set_columns(columns, heads).unwrap_err(),
DocumentError(message.to_owned())
);
}
assert_eq!(doc.column(Column::Upos, 0), Some("NOUN"));
doc.set_columns(vec![(Column::Upos, vec![None, None, None])], None)
.unwrap();
assert!(doc.columns[Column::Upos as usize].is_empty());
}
}