use std::collections::{BTreeMap, HashSet};
use regex::Regex;
use tramlines_core::grammar::Scope;
use super::{CoreSchema, Format, FormatRegistry, KeyDerivation, Only, RuleCommon, Shape, States};
use crate::{Spanned, markdown::BlockKind};
#[derive(Debug, Clone)]
pub struct Heading {
pub shape: Shape,
pub key: Option<String>,
pub unique: Scope,
pub key_derives_from: Option<KeyDerivation>,
pub example: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Section {
pub common: RuleCommon,
pub heading: Heading,
pub renamed_from: Vec<String>,
pub rule_id: Option<String>,
pub level: u8,
pub optional: bool,
pub repeat: bool,
pub headings_open: bool,
pub subheadings_open: bool,
pub for_each_key_of: Option<Vec<String>>,
pub parent: Option<String>,
pub number_from_parent: bool,
pub blocks: Option<Vec<Block>>,
pub blocks_ordered: bool,
pub guidance: Option<String>,
pub examples: Vec<String>,
}
impl Section {
pub fn label(&self) -> String {
self.heading.shape.label()
}
pub fn is_exact(&self) -> bool {
matches!(self.heading.shape, Shape::Text(_))
}
}
#[derive(Debug, Clone)]
pub struct Preamble {
pub common: RuleCommon,
pub blocks: Option<Vec<Block>>,
pub blocks_ordered: bool,
pub guidance: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Lang {
Any,
Tags(Vec<String>),
}
#[derive(Debug, Clone)]
pub struct Block {
pub common: RuleCommon,
pub kind: BlockKind,
pub optional: bool,
pub repeat: bool,
pub min: usize,
pub max: Option<usize>,
pub each: Option<Unit>,
pub sequence: Option<Vec<Unit>>,
pub rules: Option<Vec<Unit>>,
pub units_open: bool,
pub lang: Option<Lang>,
pub fence: usize,
pub columns: Option<Vec<String>>,
pub columns_open: bool,
pub source_pattern: Option<Regex>,
pub inline_html: bool,
pub blocks: Option<Vec<Block>>,
pub rule_id: Option<String>,
pub examples: Vec<String>,
pub example_list: bool,
pub guidance: Option<String>,
pub pos: usize,
}
#[derive(Debug, Clone)]
pub struct Unit {
pub common: RuleCommon,
pub shape: Option<Shape>,
pub format: Option<String>,
pub key: Option<String>,
pub key_optional: bool,
pub unique: Scope,
pub key_repeats: bool,
pub key_derives_from: Option<KeyDerivation>,
pub rule_id: Option<String>,
pub for_each_key_of: Option<Vec<String>>,
pub prohibited_patterns: Vec<tramlines_core::grammar::Prohibition>,
pub optional: bool,
pub repeat: bool,
pub blocks: Option<Vec<Block>>,
pub lines: Option<Vec<Unit>>,
pub lines_open: bool,
pub select: Option<Regex>,
pub min: usize,
pub max: Option<usize>,
pub examples: Vec<String>,
pub example_list: bool,
}
impl Unit {
pub(crate) fn format_in<'f>(&self, formats: &'f FormatRegistry) -> Option<&'f Format> {
formats.get(self.format.as_deref()?)
}
pub(crate) fn fits(
&self,
formats: &FormatRegistry,
text: &str,
value: impl Fn(&str) -> Option<String>,
) -> bool {
match &self.shape {
Some(shape) => shape.matches(text, value),
None => self
.format_in(formats)
.is_some_and(|f| f.shape_for(text).is_some()),
}
}
pub(crate) fn reach(&self, formats: &FormatRegistry, text: &str) -> usize {
match &self.shape {
Some(shape) => shape.reach(text),
None => self.format_in(formats).map_or(0, |f| f.reach(text)),
}
}
pub fn example(&self) -> Option<&str> {
self.examples.first().map(String::as_str)
}
pub fn has_shape(&self) -> bool {
self.shape.is_some() || self.format.is_some()
}
pub fn is_counter(&self) -> bool {
self.select.is_some()
&& !self.has_shape()
&& self.key.is_none()
&& self.prohibited_patterns.is_empty()
&& self.blocks.is_none()
&& self.lines.is_none()
&& self.for_each_key_of.is_none()
}
pub fn is_production(&self) -> bool {
self.select.is_none() && self.has_shape()
}
}
#[derive(Debug, Clone)]
pub struct ProhibitedHeading {
pub common: RuleCommon,
pub heading: String,
}
#[derive(Debug, Clone)]
pub struct Scaffold {
pub document: String,
pub lifecycle: Option<Only>,
pub line: usize,
}
#[derive(Debug, Clone)]
pub struct Schema {
pub core: CoreSchema,
pub preamble: Option<Preamble>,
pub sections: Vec<Section>,
pub sections_ordered: bool,
pub sections_prohibited: Vec<ProhibitedHeading>,
pub extensions: BTreeMap<String, Spanned>,
}
impl Schema {
pub fn demand_nothing() -> Schema {
Schema {
core: CoreSchema::demand_nothing(),
preamble: None,
sections: vec![],
sections_ordered: true,
sections_prohibited: vec![],
extensions: BTreeMap::new(),
}
}
pub fn states(&self, frontmatter: Option<&Spanned>) -> States {
self.core.states(frontmatter)
}
pub fn rule_ids(&self) -> Vec<String> {
self.rule_id_lines().into_iter().map(|(id, _)| id).collect()
}
pub fn rule_id_lines(&self) -> Vec<(String, usize)> {
let mut ids = Vec::new();
for s in &self.sections {
ids.extend(s.rule_id.clone().map(|id| (id, s.common.line)));
collect_block_ids(s.blocks.as_deref(), &mut ids);
}
if let Some(p) = &self.preamble {
collect_block_ids(p.blocks.as_deref(), &mut ids);
}
ids
}
pub fn unit_captures_key(&self, u: &Unit) -> bool {
u.key.is_some()
|| u.format
.as_ref()
.and_then(|name| self.core.formats.get(name))
.is_some_and(|f| f.key.is_some())
}
pub fn rule_captures_key(&self, id: &str) -> Result<(), RuleLookup> {
let mut owners = HashSet::new();
for s in &self.sections {
if s.heading.key.is_some()
&& let Some(r) = &s.rule_id
{
owners.insert(r.clone());
}
self.key_owners(s.blocks.as_deref(), None, s.rule_id.as_deref(), &mut owners);
}
if let Some(p) = &self.preamble {
self.key_owners(p.blocks.as_deref(), None, None, &mut owners);
}
if owners.contains(id) {
Ok(())
} else if self.rule_ids().iter().any(|r| r == id) {
Err(RuleLookup::NoKey)
} else {
Err(RuleLookup::NoRule)
}
}
fn key_owners(
&self,
blocks: Option<&[Block]>,
group: Option<&str>,
section: Option<&str>,
owners: &mut HashSet<String>,
) {
for b in blocks.unwrap_or(&[]) {
if b.kind == BlockKind::Group {
let group = b.rule_id.as_deref().or(group);
self.key_owners(b.blocks.as_deref(), group, section, owners);
continue;
}
for u in b.units() {
self.unit_key_owners(u, b, group, section, owners);
}
}
}
fn unit_key_owners(
&self,
u: &Unit,
block: &Block,
group: Option<&str>,
section: Option<&str>,
owners: &mut HashSet<String>,
) {
if self.unit_captures_key(u)
&& let Some(owner) = u
.rule_id
.as_deref()
.or(block.rule_id.as_deref())
.or(group)
.or(section)
{
owners.insert(owner.to_string());
}
self.key_owners(u.blocks.as_deref(), group, section, owners);
for line in u.lines.iter().flatten() {
self.unit_key_owners(line, block, group, section, owners);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleLookup {
NoRule,
NoKey,
}
impl RuleLookup {
pub fn message(self, what: &str, id: &str) -> String {
match self {
RuleLookup::NoRule => format!("`{what}` names no rule `{id}`"),
RuleLookup::NoKey => format!("`{what}` names rule `{id}`, which captures no key"),
}
}
}
fn collect_block_ids(blocks: Option<&[Block]>, ids: &mut Vec<(String, usize)>) {
for b in blocks.unwrap_or(&[]) {
ids.extend(b.rule_id.clone().map(|id| (id, b.common.line)));
collect_block_ids(b.blocks.as_deref(), ids);
for u in b.units() {
collect_unit_ids(u, ids);
}
}
}
fn collect_unit_ids(u: &Unit, ids: &mut Vec<(String, usize)>) {
ids.extend(u.rule_id.clone().map(|id| (id, u.common.line)));
collect_block_ids(u.blocks.as_deref(), ids);
for line in u.lines.iter().flatten() {
collect_unit_ids(line, ids);
}
}
impl Block {
pub fn example(&self) -> Option<&str> {
self.examples.first().map(String::as_str)
}
pub fn units(&self) -> impl Iterator<Item = &Unit> {
self.each
.iter()
.chain(self.sequence.iter().flatten())
.chain(self.rules.iter().flatten())
}
}