#![doc(
html_logo_url = "https://mosaic.kjanat.dev/assets/A4.svg",
html_favicon_url = "https://mosaic.kjanat.dev/assets/A4.svg"
)]
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct NodeId(pub u64);
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ContentHash(pub u128);
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct StyleId(pub u32);
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum NodeKind {
Document,
Section,
Paragraph,
Text,
Emphasis,
Strong,
BoldItalic,
Math,
Equation,
Figure,
Image,
Table,
Citation,
Reference,
Theorem,
Footnote,
Bibliography,
Raw,
List,
ListItem,
}
#[derive(Clone, Debug)]
pub struct Node {
pub id: NodeId,
pub kind: NodeKind,
pub span: SourceSpan,
pub content_hash: ContentHash,
pub style_id: StyleId,
pub children: Vec<NodeId>,
pub attributes: AttrMap,
}
pub type AttrMap = BTreeMap<String, AttrValue>;
#[derive(Clone, Debug, PartialEq)]
pub enum AttrValue {
Bool(bool),
Int(i64),
Float(f64),
Str(String),
List(Vec<Self>),
Length(f64),
Bytes(Arc<[u8]>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceSpan {
pub file: PathBuf,
pub start: usize,
pub end: usize,
}
impl SourceSpan {
#[must_use]
pub fn new(file: PathBuf, start: usize, end: usize) -> Self {
Self { file, start, end }
}
#[must_use]
pub fn placeholder(file: PathBuf) -> Self {
Self {
file,
start: 0,
end: 0,
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Severity {
Error,
Warning,
Note,
Help,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct DiagnosticCode(pub &'static str);
#[derive(Clone, Debug)]
pub struct DiagnosticNote {
pub message: String,
pub span: Option<SourceSpan>,
}
#[derive(Clone, Debug)]
pub struct Suggestion {
pub message: String,
pub replacement: Option<String>,
pub span: Option<SourceSpan>,
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
pub severity: Severity,
pub code: DiagnosticCode,
pub message: String,
pub span: Option<SourceSpan>,
pub notes: Vec<DiagnosticNote>,
pub suggestions: Vec<Suggestion>,
}
impl Diagnostic {
pub fn error(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self {
severity: Severity::Error,
code,
message: message.into(),
span: None,
notes: Vec::new(),
suggestions: Vec::new(),
}
}
#[must_use]
pub fn with_span(mut self, span: SourceSpan) -> Self {
self.span = Some(span);
self
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code.0, self.message)
}
}
#[must_use]
pub fn linecol(src: &str, byte_offset: usize) -> (usize, usize) {
let mut clamped = byte_offset.min(src.len());
while clamped > 0 && !src.is_char_boundary(clamped) {
clamped -= 1;
}
let mut line = 1_usize;
let mut line_start = 0_usize;
for (i, b) in src.as_bytes().iter().enumerate().take(clamped) {
if *b == b'\n' {
line += 1;
line_start = i + 1;
}
}
let column = src[line_start..clamped].chars().count() + 1;
(line, column)
}
impl std::error::Error for Diagnostic {}
#[derive(thiserror::Error, Debug)]
pub enum CoreError {
#[error("not yet implemented: {0}")]
Unimplemented(&'static str),
#[error(transparent)]
Diagnostic(Box<Diagnostic>),
}
pub type Result<T> = std::result::Result<T, CoreError>;
#[derive(Debug)]
pub struct Document {
pub root: NodeId,
pub file: PathBuf,
nodes: BTreeMap<NodeId, Node>,
next_id: u64,
}
impl Document {
#[must_use]
pub fn new(file: PathBuf) -> Self {
let root_id = NodeId(0);
let root_node = Node {
id: root_id,
kind: NodeKind::Document,
span: SourceSpan::placeholder(file.clone()),
content_hash: ContentHash::default(),
style_id: StyleId::default(),
children: Vec::new(),
attributes: AttrMap::new(),
};
let mut nodes = BTreeMap::new();
nodes.insert(root_id, root_node);
Self {
root: root_id,
file,
nodes,
next_id: 1,
}
}
pub fn alloc(&mut self, mut node: Node) -> NodeId {
let id = NodeId(self.next_id);
self.next_id += 1;
node.id = id;
self.nodes.insert(id, node);
id
}
pub fn alloc_child(&mut self, parent: NodeId, node: Node) -> NodeId {
assert!(
self.nodes.contains_key(&parent),
"Document::alloc_child: unknown parent {parent:?}"
);
let child_id = self.alloc(node);
if let Some(parent_node) = self.nodes.get_mut(&parent) {
parent_node.children.push(child_id);
}
child_id
}
#[must_use]
pub fn get(&self, id: NodeId) -> Option<&Node> {
self.nodes.get(&id)
}
#[must_use]
pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Node> {
self.nodes.get_mut(&id)
}
pub fn nodes(&self) -> impl Iterator<Item = &Node> {
self.nodes.values()
}
#[must_use]
pub fn len(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() <= 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linecol_handles_ascii_offsets() {
let src = "ab\ncd\nef";
assert_eq!(linecol(src, 0), (1, 1));
assert_eq!(linecol(src, 1), (1, 2));
assert_eq!(linecol(src, 2), (1, 3));
assert_eq!(linecol(src, 3), (2, 1));
assert_eq!(linecol(src, 6), (3, 1));
assert_eq!(linecol(src, 7), (3, 2));
assert_eq!(linecol(src, 9999), (3, 3));
}
#[test]
fn linecol_counts_chars_not_bytes() {
let src = "µx\n字y\n";
assert_eq!(linecol(src, 0), (1, 1));
assert_eq!(linecol(src, 2), (1, 2)); assert_eq!(linecol(src, 3), (1, 3)); assert_eq!(linecol(src, 4), (2, 1)); assert_eq!(linecol(src, 7), (2, 2)); }
#[test]
fn linecol_offsets_inside_codepoints_round_down() {
let src = "µ";
assert_eq!(linecol(src, 1), (1, 1));
}
#[test]
#[should_panic(expected = "unknown parent")]
fn alloc_child_panics_on_unknown_parent() {
let mut doc = Document::new(PathBuf::from("test.mos"));
doc.alloc_child(
NodeId(9999),
Node {
id: NodeId::default(),
kind: NodeKind::Text,
span: SourceSpan::placeholder(PathBuf::from("test.mos")),
content_hash: ContentHash::default(),
style_id: StyleId::default(),
children: Vec::new(),
attributes: AttrMap::new(),
},
);
}
#[test]
fn document_alloc_and_traverse() {
let mut doc = Document::new(PathBuf::from("test.mos"));
let para = doc.alloc_child(
doc.root,
Node {
id: NodeId::default(),
kind: NodeKind::Paragraph,
span: SourceSpan::placeholder(PathBuf::from("test.mos")),
content_hash: ContentHash::default(),
style_id: StyleId::default(),
children: Vec::new(),
attributes: AttrMap::new(),
},
);
doc.alloc_child(
para,
Node {
id: NodeId::default(),
kind: NodeKind::Text,
span: SourceSpan::placeholder(PathBuf::from("test.mos")),
content_hash: ContentHash::default(),
style_id: StyleId::default(),
children: Vec::new(),
attributes: AttrMap::new(),
},
);
assert_eq!(doc.len(), 3);
assert_eq!(doc.get(doc.root).unwrap().children.len(), 1);
assert_eq!(doc.get(para).unwrap().children.len(), 1);
}
}