use crate::ast::Node;
use crate::error::{WriteError, WriteResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NewlineStrategy {
None,
Conditional,
Always,
Inherit,
Smart,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RenderingMode {
Block,
InlineWithBlocks,
PureInline,
TableCell,
ListItem,
Custom,
}
#[derive(Debug, Clone)]
pub struct NewlineContext {
pub mode: RenderingMode,
pub strategy: NewlineStrategy,
pub allows_blocks: bool,
pub is_container_end: bool,
pub parent: Option<Box<NewlineContext>>,
pub custom_data: Option<String>,
}
impl NewlineContext {
pub fn block() -> Self {
Self {
mode: RenderingMode::Block,
strategy: NewlineStrategy::Always,
allows_blocks: true,
is_container_end: false,
parent: None,
custom_data: None,
}
}
pub fn inline_with_blocks() -> Self {
Self {
mode: RenderingMode::InlineWithBlocks,
strategy: NewlineStrategy::Smart,
allows_blocks: true,
is_container_end: false,
parent: None,
custom_data: None,
}
}
pub fn pure_inline() -> Self {
Self {
mode: RenderingMode::PureInline,
strategy: NewlineStrategy::None,
allows_blocks: false,
is_container_end: false,
parent: None,
custom_data: None,
}
}
pub fn table_cell() -> Self {
Self {
mode: RenderingMode::TableCell,
strategy: NewlineStrategy::Smart,
allows_blocks: false, is_container_end: false,
parent: None,
custom_data: None,
}
}
pub fn list_item() -> Self {
Self {
mode: RenderingMode::ListItem,
strategy: NewlineStrategy::Conditional,
allows_blocks: true,
is_container_end: false,
parent: None,
custom_data: None,
}
}
pub fn custom(strategy: NewlineStrategy, allows_blocks: bool) -> Self {
Self {
mode: RenderingMode::Custom,
strategy,
allows_blocks,
is_container_end: false,
parent: None,
custom_data: None,
}
}
pub fn with_strategy(mut self, strategy: NewlineStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_blocks_allowed(mut self, allows_blocks: bool) -> Self {
self.allows_blocks = allows_blocks;
self
}
pub fn with_container_end(mut self, is_container_end: bool) -> Self {
self.is_container_end = is_container_end;
self
}
pub fn with_parent(mut self, parent: NewlineContext) -> Self {
self.parent = Some(Box::new(parent));
self
}
pub fn with_custom_data(mut self, data: String) -> Self {
self.custom_data = Some(data);
self
}
pub fn should_add_trailing_newline(&self, content: &str, node: Option<&Node>) -> bool {
match self.strategy {
NewlineStrategy::None => false,
NewlineStrategy::Always => true,
NewlineStrategy::Conditional => !content.ends_with('\n'),
NewlineStrategy::Inherit => {
if let Some(parent) = &self.parent {
parent.should_add_trailing_newline(content, node)
} else {
match self.mode {
RenderingMode::Block => true,
RenderingMode::InlineWithBlocks => !content.ends_with('\n'),
_ => false,
}
}
}
NewlineStrategy::Smart => self.smart_newline_decision(content, node),
}
}
fn smart_newline_decision(&self, content: &str, node: Option<&Node>) -> bool {
if content.ends_with('\n') && !self.is_container_end {
return false;
}
match self.mode {
RenderingMode::Block => true,
RenderingMode::InlineWithBlocks => {
if let Some(node) = node {
if node.is_block() {
return true;
}
}
self.is_container_end && !content.ends_with('\n')
}
RenderingMode::PureInline => false,
RenderingMode::TableCell => {
self.is_container_end && !content.ends_with('\n')
}
RenderingMode::ListItem => {
!content.ends_with('\n')
}
RenderingMode::Custom => {
!content.ends_with('\n')
}
}
}
pub fn allows_block_elements(&self) -> bool {
self.allows_blocks
}
pub fn effective_strategy(&self) -> NewlineStrategy {
if self.strategy == NewlineStrategy::Inherit {
if let Some(parent) = &self.parent {
parent.effective_strategy()
} else {
NewlineStrategy::Conditional
}
} else {
self.strategy
}
}
pub fn validate_node(&self, node: &Node) -> WriteResult<()> {
if !self.allows_blocks && node.is_block() {
return Err(WriteError::InvalidStructure(
format!(
"Block-level node {:?} not allowed in {:?} context",
node.type_name(),
self.mode
)
.into(),
));
}
Ok(())
}
}
impl Default for NewlineContext {
fn default() -> Self {
Self::block()
}
}
pub struct NewlineContextBuilder {
context: NewlineContext,
}
impl NewlineContextBuilder {
pub fn new() -> Self {
Self {
context: NewlineContext::default(),
}
}
pub fn mode(mut self, mode: RenderingMode) -> Self {
self.context.mode = mode;
self
}
pub fn strategy(mut self, strategy: NewlineStrategy) -> Self {
self.context.strategy = strategy;
self
}
pub fn allow_blocks(mut self, allow: bool) -> Self {
self.context.allows_blocks = allow;
self
}
pub fn container_end(mut self, is_end: bool) -> Self {
self.context.is_container_end = is_end;
self
}
pub fn parent(mut self, parent: NewlineContext) -> Self {
self.context.parent = Some(Box::new(parent));
self
}
pub fn custom_data(mut self, data: String) -> Self {
self.context.custom_data = Some(data);
self
}
pub fn build(self) -> NewlineContext {
self.context
}
}
impl Default for NewlineContextBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_block_context() {
let ctx = NewlineContext::block();
assert_eq!(ctx.mode, RenderingMode::Block);
assert_eq!(ctx.strategy, NewlineStrategy::Always);
assert!(ctx.allows_blocks);
}
#[test]
fn test_inline_with_blocks_context() {
let ctx = NewlineContext::inline_with_blocks();
assert_eq!(ctx.mode, RenderingMode::InlineWithBlocks);
assert_eq!(ctx.strategy, NewlineStrategy::Smart);
assert!(ctx.allows_blocks);
}
#[test]
fn test_smart_newline_decision() {
let ctx = NewlineContext::inline_with_blocks();
assert!(!ctx.should_add_trailing_newline("content", None));
let ctx_at_end = ctx.with_container_end(true);
assert!(ctx_at_end.should_add_trailing_newline("content", None));
assert!(!ctx_at_end.should_add_trailing_newline("content\n", None));
}
#[test]
fn test_builder_pattern() {
let ctx = NewlineContextBuilder::new()
.mode(RenderingMode::Custom)
.strategy(NewlineStrategy::Smart)
.allow_blocks(false)
.container_end(true)
.build();
assert_eq!(ctx.mode, RenderingMode::Custom);
assert_eq!(ctx.strategy, NewlineStrategy::Smart);
assert!(!ctx.allows_blocks);
assert!(ctx.is_container_end);
}
}