use super::{Inline, SourceSpan, is_zero_u16};
use crate::EntryFacts;
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct LayoutHint {
#[serde(default, skip_serializing_if = "is_zero_i32")]
pub indent_columns: i32,
#[serde(default, skip_serializing_if = "is_zero_i32")]
pub continuation_indent_columns: i32,
#[serde(default, skip_serializing_if = "is_zero_u16")]
pub spacing_before_lines: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(
tag = "type",
rename_all = "kebab-case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum Block {
Paragraph {
children: Vec<Inline>,
#[serde(default, skip_serializing_if = "LayoutHint::is_empty")]
layout: LayoutHint,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
Preformatted {
children: Vec<Inline>,
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<String>,
#[serde(default, skip_serializing_if = "LayoutHint::is_empty")]
layout: LayoutHint,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
List {
kind: ListKind,
#[serde(default, skip_serializing_if = "is_false")]
compact: bool,
items: Vec<ListItem>,
#[serde(default, skip_serializing_if = "LayoutHint::is_empty")]
layout: LayoutHint,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
DefinitionList {
items: Vec<DefinitionItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
declaration_groups: Vec<crate::DeclarationGroup>,
#[serde(default, skip_serializing_if = "is_false")]
compact: bool,
#[serde(default, skip_serializing_if = "LayoutHint::is_empty")]
layout: LayoutHint,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
Table {
rows: Vec<TableRow>,
#[serde(default, skip_serializing_if = "LayoutHint::is_empty")]
layout: LayoutHint,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
Equation {
value: String,
#[serde(default, skip_serializing_if = "is_false")]
display: bool,
#[serde(default, skip_serializing_if = "LayoutHint::is_empty")]
layout: LayoutHint,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
VerticalSpace {
lines: u16,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
ThematicBreak {
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
Unsupported {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
text: String,
#[serde(default, skip_serializing_if = "LayoutHint::is_empty")]
layout: LayoutHint,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceSpan>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ListKind {
Bullet,
Ordered {
#[serde(default, skip_serializing_if = "Option::is_none")]
start: Option<u64>,
},
Plain,
}
#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
enum ClosedListKind {
Bullet {},
Ordered {
#[serde(default)]
start: Option<u64>,
},
Plain {},
}
impl<'de> Deserialize<'de> for ListKind {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(match ClosedListKind::deserialize(deserializer)? {
ClosedListKind::Bullet {} => Self::Bullet,
ClosedListKind::Ordered { start } => Self::Ordered { start },
ClosedListKind::Plain {} => Self::Plain,
})
}
}
impl ListKind {
#[must_use]
pub fn ordinal(self, index: usize) -> Option<u64> {
match self {
Self::Ordered { start } => Some(
start
.unwrap_or(1)
.saturating_add(u64::try_from(index).unwrap_or(u64::MAX)),
),
Self::Bullet | Self::Plain => None,
}
}
#[must_use]
pub fn for_excerpt(self, index: usize) -> Self {
self.ordinal(index)
.map_or(self, |start| Self::Ordered { start: Some(start) })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListItem {
#[serde(default, skip_serializing_if = "ListItemLayout::is_empty")]
pub layout: ListItemLayout,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<SourceSpan>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry: Option<EntryFacts>,
pub blocks: Vec<Block>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListItemLayout {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spacing_before_lines: Option<u16>,
}
impl ListItemLayout {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.spacing_before_lines.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DefinitionItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<SourceSpan>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entry: Option<EntryFacts>,
pub terms: Vec<Vec<Inline>>,
pub description: Vec<Block>,
#[serde(default, skip_serializing_if = "DefinitionLayout::is_empty")]
pub layout: DefinitionLayout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DefinitionLayout {
#[serde(default, skip_serializing_if = "is_false")]
pub inline_term: bool,
#[serde(
default = "default_definition_indent",
skip_serializing_if = "is_default_definition_indent"
)]
pub body_indent_columns: i32,
#[serde(
default = "default_term_gap",
skip_serializing_if = "is_default_term_gap"
)]
pub min_term_gap_columns: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub spacing_before_lines: Option<u16>,
}
impl DefinitionLayout {
#[must_use]
pub const fn is_empty(&self) -> bool {
!self.inline_term
&& self.spacing_before_lines.is_none()
&& self.body_indent_columns == default_definition_indent()
&& self.min_term_gap_columns == default_term_gap()
}
}
impl Default for DefinitionLayout {
fn default() -> Self {
Self {
inline_term: false,
body_indent_columns: default_definition_indent(),
min_term_gap_columns: default_term_gap(),
spacing_before_lines: None,
}
}
}
const fn default_definition_indent() -> i32 {
4
}
const fn default_term_gap() -> u16 {
1
}
#[allow(clippy::trivially_copy_pass_by_ref)] const fn is_default_definition_indent(value: &i32) -> bool {
*value == default_definition_indent()
}
#[allow(clippy::trivially_copy_pass_by_ref)] const fn is_default_term_gap(value: &u16) -> bool {
*value == default_term_gap()
}
impl DefinitionItem {
pub const DESCRIPTION_INDENT_COLUMNS: u16 = 4;
#[must_use]
pub fn inline_description(&self) -> Option<(&[Inline], &LayoutHint)> {
if !self.layout.inline_term {
return None;
}
match self.description.first()? {
Block::Paragraph {
children, layout, ..
} if layout.spacing_before_lines == 0 => Some((children, layout)),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TableRow {
pub cells: Vec<TableCell>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TableCell {
pub blocks: Vec<Block>,
#[serde(default = "one_u16", skip_serializing_if = "is_one_u16")]
pub column_span: u16,
#[serde(default = "one_u16", skip_serializing_if = "is_one_u16")]
pub row_span: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub alignment: Option<TableAlignment>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TableAlignment {
Left,
Center,
Right,
}
impl LayoutHint {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.indent_columns == 0
&& self.continuation_indent_columns == 0
&& self.spacing_before_lines == 0
}
}
#[allow(clippy::trivially_copy_pass_by_ref)] const fn is_zero_i32(value: &i32) -> bool {
*value == 0
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_one_u16(value: &u16) -> bool {
*value == 1
}
const fn one_u16() -> u16 {
1
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_false(value: &bool) -> bool {
!*value
}