use std::str::FromStr;
use svugy_model::style::AnyStyleKey;
use super::ast::{
D2Document, EdgeDeclaration, EdgeLink, Field, FieldAssignment, KeyPath, KeySegment, Keyword,
Scalar, ShapeDeclaration, Span, Statement, StyleProperty, SvugyProperty,
};
use super::diagnostic::{Construct, Diagnostics};
use super::parse::{
SyntaxAssignment, SyntaxDeclaration, SyntaxHead, SyntaxKey, SyntaxLink, SyntaxStatement,
SyntaxValue,
};
const SVUGY_ENTRY_ERROR: &str = "the `svugy` variable holds only `key: value` entries";
const D2_LEGEND_VARIABLE: &str = "d2-legend";
const SVUGY_LEGEND_ATTRIBUTE: &str = "legend";
const EDGE_VARS_ERROR: &str =
"d2 rejects a `vars` map in an edge map: set the edge's traveler in Markdown (`.md`)";
pub(crate) struct SubsetFilter<'sink> {
diagnostics: &'sink mut Diagnostics,
}
enum KeyRole {
Shape(KeyPath),
Field(FieldKey),
}
struct FieldKey {
target: KeyPath,
keyword: Keyword,
rest: Vec<KeySegment>,
}
impl<'sink> SubsetFilter<'sink> {
pub(crate) fn filter_document(
statements: Vec<SyntaxStatement>,
diagnostics: &'sink mut Diagnostics,
) -> D2Document {
let mut filter = Self { diagnostics };
let statements = filter.filter_statements(statements);
D2Document { statements }
}
fn filter_statements(&mut self, statements: Vec<SyntaxStatement>) -> Vec<Statement> {
statements
.into_iter()
.filter_map(|statement| self.filter_statement(statement))
.collect()
}
fn filter_statement(&mut self, statement: SyntaxStatement) -> Option<Statement> {
match statement {
SyntaxStatement::Declaration(declaration) => self.filter_declaration(declaration),
SyntaxStatement::EdgeReference(span) => {
self.diagnostics
.record_warning(Construct::EdgeReference, span);
None
}
SyntaxStatement::Spread { value, span } => {
let is_import = matches!(value, SyntaxValue::Import(_import_span));
let construct = if is_import {
Construct::Import
} else {
Construct::Variables
};
self.diagnostics.record_warning(construct, span);
None
}
}
}
fn filter_declaration(&mut self, declaration: SyntaxDeclaration) -> Option<Statement> {
let SyntaxDeclaration {
head,
assignment,
span,
} = declaration;
match head {
SyntaxHead::Key(key) => self.filter_key_declaration(key, assignment, span),
SyntaxHead::Edges { source, links } => {
self.filter_edge_declaration(source, links, assignment, span)
}
}
}
fn filter_key_declaration(
&mut self,
key: SyntaxKey,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Statement> {
let path = self.filter_plain_path(key)?;
let key_role = KeyRole::of(path);
match key_role {
KeyRole::Shape(path) => self.filter_shape_declaration(path, assignment, span),
KeyRole::Field(field_key) => self.filter_field_assignment(field_key, assignment, span),
}
}
fn filter_plain_path(&mut self, key: SyntaxKey) -> Option<KeyPath> {
let has_glob_segment = key.segments.iter().any(|segment| segment.is_glob);
let is_glob_or_filter = has_glob_segment || key.is_filtered;
if is_glob_or_filter {
self.diagnostics.record_warning(Construct::Glob, key.span);
return None;
}
let mut syntax_segments = key.segments;
let parent_references = syntax_segments
.iter()
.take_while(|segment| segment.is_parent_reference)
.count();
let named_segments = syntax_segments.split_off(parent_references);
let misplaced_parent_reference = named_segments
.iter()
.find(|segment| segment.is_parent_reference);
if let Some(misplaced_parent_reference) = misplaced_parent_reference {
let message = "`_` names the parent scope only at the start of a key, e.g. `_.x`";
let misplaced_span = misplaced_parent_reference.segment.span;
self.diagnostics.record_error(message, misplaced_span);
return None;
}
let is_only_parent_references = named_segments.is_empty();
if is_only_parent_references {
let message = "a key needs a name after its `_` parent references";
self.diagnostics.record_error(message, key.span);
return None;
}
let segments = named_segments
.into_iter()
.map(|segment| segment.segment)
.collect();
let path = KeyPath {
parent_references,
segments,
span: key.span,
};
Some(path)
}
fn filter_shape_declaration(
&mut self,
path: KeyPath,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Statement> {
let SyntaxAssignment { value, map } = assignment;
let label = self.filter_label(value, span)?;
let body = map
.map(|map| self.filter_statements(map))
.unwrap_or_default();
let shape = ShapeDeclaration {
path,
label,
body,
span,
};
let statement = Statement::Shape(shape);
Some(statement)
}
fn filter_label(&mut self, value: Option<SyntaxValue>, span: Span) -> Option<Option<Scalar>> {
if let Some(SyntaxValue::Null(_null_span)) = value {
self.diagnostics.record_warning(Construct::Null, span);
return None;
}
let label = value.and_then(|value| self.filter_scalar(value));
Some(label)
}
fn filter_field_assignment(
&mut self,
field_key: FieldKey,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Statement> {
let FieldKey {
target,
keyword,
rest,
} = field_key;
let field = match keyword {
Keyword::Shape => self
.filter_scalar_field(keyword, rest, assignment, span)
.map(Field::Shape)?,
Keyword::Label => self
.filter_scalar_field(keyword, rest, assignment, span)
.map(Field::Label)?,
Keyword::Direction => self
.filter_scalar_field(keyword, rest, assignment, span)
.map(Field::Direction)?,
Keyword::Link => self
.filter_scalar_field(keyword, rest, assignment, span)
.map(Field::Link)?,
Keyword::Tooltip => self
.filter_scalar_field(keyword, rest, assignment, span)
.map(Field::Tooltip)?,
Keyword::Style => self.filter_style_field(rest, assignment, span)?,
Keyword::Vars => self.filter_vars_field(rest, assignment, span)?,
Keyword::Class | Keyword::Classes => {
self.diagnostics.record_warning(Construct::Classes, span);
return None;
}
Keyword::Layers | Keyword::Scenarios | Keyword::Steps => {
let board_keyword = keyword.into();
let construct = Construct::Board(board_keyword);
self.diagnostics.record_warning(construct, span);
return None;
}
reserved_keyword => {
let reserved_name = reserved_keyword.into();
let construct = Construct::ReservedField(reserved_name);
self.diagnostics.record_warning(construct, span);
return None;
}
};
let assignment = FieldAssignment {
target,
field,
span,
};
let statement = Statement::Field(assignment);
Some(statement)
}
fn filter_scalar_field(
&mut self,
keyword: Keyword,
rest: Vec<KeySegment>,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Scalar> {
let has_sub_key = !rest.is_empty();
if has_sub_key {
self.warn_sub_keyed_form(keyword, &rest, span);
return None;
}
let value = self.take_value_warning_on_map(keyword, assignment, span)?;
self.filter_scalar(value)
}
fn warn_sub_keyed_form(&mut self, keyword: Keyword, rest: &[KeySegment], span: Span) {
let keyword_name: &str = keyword.into();
let rest_names = rest.iter().map(|segment| segment.name.as_str());
let names: Vec<&str> = std::iter::once(keyword_name).chain(rest_names).collect();
let dotted_name = names.join(".");
let construct = Construct::FieldForm(dotted_name);
self.diagnostics.record_warning(construct, span);
}
fn take_value_warning_on_map(
&mut self,
keyword: Keyword,
assignment: SyntaxAssignment,
span: Span,
) -> Option<SyntaxValue> {
let SyntaxAssignment { value, map } = assignment;
let Some(value) = value else {
self.warn_field_form(keyword, span);
return None;
};
let has_map_beside_value = map.is_some();
if has_map_beside_value {
self.warn_field_form(keyword, span);
}
Some(value)
}
fn take_map_warning_on_value(
&mut self,
keyword: Keyword,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Vec<SyntaxStatement>> {
let SyntaxAssignment { value, map } = assignment;
let Some(map) = map else {
self.warn_field_form(keyword, span);
return None;
};
let has_value_beside_map = value.is_some();
if has_value_beside_map {
self.warn_field_form(keyword, span);
}
Some(map)
}
fn warn_field_form(&mut self, keyword: Keyword, span: Span) {
let keyword_name: &str = keyword.into();
let owned_keyword_name = keyword_name.to_owned();
let field_form = Construct::FieldForm(owned_keyword_name);
self.diagnostics.record_warning(field_form, span);
}
fn filter_style_field(
&mut self,
mut rest: Vec<KeySegment>,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Field> {
let sub_key_property_name = rest.pop();
let is_deeper_than_one_property = !rest.is_empty();
if is_deeper_than_one_property {
self.warn_field_form(Keyword::Style, span);
return None;
}
let Some(name) = sub_key_property_name else {
return self.filter_style_map(assignment, span);
};
let value = self.take_value_warning_on_map(Keyword::Style, assignment, span)?;
let property = self.filter_style_entry(name, value, span)?;
let properties = vec![property];
let style_field = Field::Style(properties);
Some(style_field)
}
fn filter_style_map(&mut self, assignment: SyntaxAssignment, span: Span) -> Option<Field> {
let map = self.take_map_warning_on_value(Keyword::Style, assignment, span)?;
let properties = map
.into_iter()
.filter_map(|statement| self.filter_style_property(statement))
.collect();
let style_field = Field::Style(properties);
Some(style_field)
}
fn filter_style_property(&mut self, statement: SyntaxStatement) -> Option<StyleProperty> {
let SyntaxStatement::Declaration(declaration) = statement else {
let span = statement.span();
self.warn_field_form(Keyword::Style, span);
return None;
};
let SyntaxDeclaration {
head,
assignment,
span,
} = declaration;
let SyntaxHead::Key(key) = head else {
self.warn_field_form(Keyword::Style, span);
return None;
};
let path = self.filter_plain_path(key)?;
let has_parent_reference = path.parent_references > 0;
if has_parent_reference {
self.warn_field_form(Keyword::Style, span);
return None;
}
let Ok([name]) = <[KeySegment; 1]>::try_from(path.segments) else {
self.warn_field_form(Keyword::Style, span);
return None;
};
let value = self.take_value_warning_on_map(Keyword::Style, assignment, span)?;
self.filter_style_entry(name, value, span)
}
fn filter_style_entry(
&mut self,
name: KeySegment,
value: SyntaxValue,
span: Span,
) -> Option<StyleProperty> {
let Some(_style_key) = AnyStyleKey::find(&name.name) else {
let unknown_style_key = Construct::UnknownStyleKey(name.name);
self.diagnostics.record_warning(unknown_style_key, span);
return None;
};
let value = self.filter_scalar(value)?;
let property = StyleProperty { name, value, span };
Some(property)
}
fn filter_vars_field(
&mut self,
rest: Vec<KeySegment>,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Field> {
let has_sub_key = !rest.is_empty();
if has_sub_key {
self.warn_sub_keyed_form(Keyword::Vars, &rest, span);
return None;
}
let map = self.take_map_warning_on_value(Keyword::Vars, assignment, span)?;
let properties: Vec<SvugyProperty> = map
.into_iter()
.flat_map(|statement| self.collect_vars_entry(statement))
.collect();
let is_carrier_absent = properties.is_empty();
if is_carrier_absent {
return None;
}
let svugy_field = Field::Svugy(properties);
Some(svugy_field)
}
fn collect_vars_entry(&mut self, statement: SyntaxStatement) -> Vec<SvugyProperty> {
let span = statement.span();
let Some((key, assignment)) = statement.into_key_declaration() else {
self.diagnostics.record_warning(Construct::Variables, span);
return Vec::new();
};
let first_segment = key.segments.first();
let variable_name = first_segment.map(|first_segment| first_segment.segment.name.as_str());
let is_d2_legend = variable_name == Some(D2_LEGEND_VARIABLE);
if is_d2_legend {
self.reject_legend(D2_LEGEND_VARIABLE, span);
return Vec::new();
}
let is_user_variable = variable_name != Some(SvugyProperty::VARIABLE);
if is_user_variable {
self.diagnostics.record_warning(Construct::Variables, span);
return Vec::new();
}
let Some(path) = self.filter_plain_path(key) else {
return Vec::new();
};
let below_variable_segments = path.segments.into_iter().skip(1);
let attribute_path = below_variable_segments
.map(|segment| segment.name)
.collect();
self.collect_svugy_properties(attribute_path, assignment, span)
}
fn collect_svugy_properties(
&mut self,
attribute_path: Vec<String>,
assignment: SyntaxAssignment,
span: Span,
) -> Vec<SvugyProperty> {
let first_attribute = attribute_path.first();
let is_svugy_legend = first_attribute.is_some_and(|name| name == SVUGY_LEGEND_ATTRIBUTE);
if is_svugy_legend {
self.reject_legend("svugy.legend", span);
return Vec::new();
}
let mut properties = Vec::new();
let SyntaxAssignment { value, map } = assignment;
let carried_value = value.and_then(|value| self.filter_carried_value(value));
if let Some(value) = carried_value {
let property_attribute_path = attribute_path.clone();
let property = SvugyProperty {
attribute_path: property_attribute_path,
value,
span,
};
properties.push(property);
}
let Some(map) = map else {
return properties;
};
for statement in map {
let entry_span = statement.span();
let Some((key, entry_assignment)) = statement.into_key_declaration() else {
self.diagnostics.record_error(SVUGY_ENTRY_ERROR, entry_span);
continue;
};
let Some(path) = self.filter_plain_path(key) else {
continue;
};
let has_parent_reference = path.parent_references > 0;
if has_parent_reference {
self.diagnostics.record_error(SVUGY_ENTRY_ERROR, entry_span);
continue;
}
let entry_names = path.segments.into_iter().map(|segment| segment.name);
let mut entry_attribute_path = attribute_path.clone();
entry_attribute_path.extend(entry_names);
let entry_properties =
self.collect_svugy_properties(entry_attribute_path, entry_assignment, entry_span);
properties.extend(entry_properties);
}
properties
}
fn reject_legend(&mut self, legend_key: &str, span: Span) {
let message = format!(
"`{legend_key}` sets a legend, but svugy v4 has no legend: say what each entry meant in step prose or edge descriptions"
);
self.diagnostics.record_error(message, span);
}
fn filter_carried_value(&mut self, value: SyntaxValue) -> Option<Scalar> {
let SyntaxValue::Array(array_span) = value else {
return self.filter_scalar(value);
};
let message = "a svugy attribute takes one value, not a list";
self.diagnostics.record_error(message, array_span);
None
}
fn filter_edge_declaration(
&mut self,
source: SyntaxKey,
links: Vec<SyntaxLink>,
assignment: SyntaxAssignment,
span: Span,
) -> Option<Statement> {
let source = self.filter_plain_path(source)?;
let link_count = links.len();
let mut kept_links = Vec::with_capacity(link_count);
for link in links {
let target = self.filter_plain_path(link.target)?;
let kept_link = EdgeLink {
operator: link.operator,
operator_span: link.operator_span,
target,
};
kept_links.push(kept_link);
}
let SyntaxAssignment { value, map } = assignment;
let label = self.filter_label(value, span)?;
let fields = map
.map(|map| self.filter_edge_fields(map))
.unwrap_or_default();
let edge = EdgeDeclaration {
source,
links: kept_links,
label,
fields,
span,
};
let statement = Statement::Edge(edge);
Some(statement)
}
fn filter_edge_fields(&mut self, map: Vec<SyntaxStatement>) -> Vec<Field> {
let statement_count = map.len();
let mut fields = Vec::with_capacity(statement_count);
for syntax_statement in map {
let is_vars_map = syntax_statement.is_vars_map();
if is_vars_map {
let vars_span = syntax_statement.span();
self.diagnostics.record_error(EDGE_VARS_ERROR, vars_span);
continue;
}
let Some(statement) = self.filter_statement(syntax_statement) else {
continue;
};
let Statement::Field(assignment) = statement else {
let declaration_span = statement.span();
self.diagnostics
.record_warning(Construct::DeclarationInEdgeMap, declaration_span);
continue;
};
let has_target_segment = !assignment.target.segments.is_empty();
let has_parent_reference = assignment.target.parent_references > 0;
let has_target = has_target_segment || has_parent_reference;
if has_target {
self.diagnostics
.record_warning(Construct::DeclarationInEdgeMap, assignment.span);
continue;
}
fields.push(assignment.field);
}
fields
}
fn filter_scalar(&mut self, value: SyntaxValue) -> Option<Scalar> {
let (construct, span) = match value {
SyntaxValue::Scalar(scalar) => return Some(scalar),
SyntaxValue::Null(span) => (Construct::Null, span),
SyntaxValue::BlockString(span) => (Construct::BlockString, span),
SyntaxValue::Import(span) => (Construct::Import, span),
SyntaxValue::Substitution(span) => (Construct::Variables, span),
SyntaxValue::Array(span) => (Construct::Array, span),
};
self.diagnostics.record_warning(construct, span);
None
}
}
impl KeyRole {
fn of(path: KeyPath) -> Self {
let keyword_position = path
.segments
.iter()
.enumerate()
.find_map(|(index, segment)| {
let parsed_keyword = Keyword::from_str(&segment.name);
let keyword = parsed_keyword.ok()?;
Some((index, keyword))
});
let Some((keyword_index, keyword)) = keyword_position else {
return Self::Shape(path);
};
let mut target_segments = path.segments;
let mut rest = target_segments.split_off(keyword_index);
let keyword_segment = rest.remove(0);
let last_target_segment = target_segments.last();
let target_end = last_target_segment.map_or(keyword_segment.span.start, |last_segment| {
last_segment.span.end
});
let target_span = Span::new(path.span.start, target_end);
let target = KeyPath {
parent_references: path.parent_references,
segments: target_segments,
span: target_span,
};
let field_key = FieldKey {
target,
keyword,
rest,
};
Self::Field(field_key)
}
}
impl SyntaxStatement {
fn is_vars_map(&self) -> bool {
let Self::Declaration(declaration) = self else {
return false;
};
let SyntaxHead::Key(key) = &declaration.head else {
return false;
};
let [only_segment] = key.segments.as_slice() else {
return false;
};
let parsed_keyword = Keyword::from_str(&only_segment.segment.name);
let is_vars_key = parsed_keyword == Ok(Keyword::Vars);
let has_map = declaration.assignment.map.is_some();
is_vars_key && has_map
}
fn into_key_declaration(self) -> Option<(SyntaxKey, SyntaxAssignment)> {
let Self::Declaration(declaration) = self else {
return None;
};
let SyntaxHead::Key(key) = declaration.head else {
return None;
};
Some((key, declaration.assignment))
}
}