1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! block_text_newtype {
8 ($name:ident) => {
9 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10 pub struct $name(String);
11
12 impl $name {
13 pub fn new(input: &str) -> Result<Self, WordPressBlockError> {
14 let trimmed = input.trim();
15 if trimmed.is_empty() {
16 Err(WordPressBlockError::Empty)
17 } else {
18 Ok(Self(trimmed.to_string()))
19 }
20 }
21
22 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25 }
26
27 impl fmt::Display for $name {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 formatter.write_str(self.as_str())
30 }
31 }
32
33 impl FromStr for $name {
34 type Err = WordPressBlockError;
35
36 fn from_str(input: &str) -> Result<Self, Self::Err> {
37 Self::new(input)
38 }
39 }
40 };
41}
42
43block_text_newtype!(WordPressBlockCategory);
44block_text_newtype!(WordPressBlockAttributeName);
45block_text_newtype!(WordPressBlockAssetPath);
46
47#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub struct WordPressBlockName(String);
50
51impl WordPressBlockName {
52 pub fn new(input: &str) -> Result<Self, WordPressBlockError> {
53 let trimmed = input.trim();
54 let Some((namespace, block)) = trimmed.split_once('/') else {
55 return Err(WordPressBlockError::InvalidBlockName);
56 };
57 if namespace.is_empty() || block.is_empty() || block.contains('/') {
58 return Err(WordPressBlockError::InvalidBlockName);
59 }
60 Ok(Self(trimmed.to_string()))
61 }
62
63 pub fn as_str(&self) -> &str {
64 &self.0
65 }
66}
67
68impl fmt::Display for WordPressBlockName {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter.write_str(self.as_str())
71 }
72}
73
74impl FromStr for WordPressBlockName {
75 type Err = WordPressBlockError;
76
77 fn from_str(input: &str) -> Result<Self, Self::Err> {
78 Self::new(input)
79 }
80}
81
82#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub enum WordPressBlockAttributeType {
85 String,
86 Boolean,
87 Number,
88 Integer,
89 Object,
90 Array,
91}
92
93impl WordPressBlockAttributeType {
94 pub const fn as_str(self) -> &'static str {
95 match self {
96 Self::String => "string",
97 Self::Boolean => "boolean",
98 Self::Number => "number",
99 Self::Integer => "integer",
100 Self::Object => "object",
101 Self::Array => "array",
102 }
103 }
104}
105
106#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct WordPressBlockAttribute {
109 name: WordPressBlockAttributeName,
110 attribute_type: WordPressBlockAttributeType,
111}
112
113impl WordPressBlockAttribute {
114 pub const fn new(
115 name: WordPressBlockAttributeName,
116 attribute_type: WordPressBlockAttributeType,
117 ) -> Self {
118 Self {
119 name,
120 attribute_type,
121 }
122 }
123
124 pub const fn name(&self) -> &WordPressBlockAttributeName {
125 &self.name
126 }
127
128 pub const fn attribute_type(&self) -> WordPressBlockAttributeType {
129 self.attribute_type
130 }
131}
132
133#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
135pub enum WordPressBlockSupport {
136 Align,
137 Color,
138 Typography,
139 Spacing,
140 Html,
141 Multiple,
142}
143
144impl WordPressBlockSupport {
145 pub const fn as_str(self) -> &'static str {
146 match self {
147 Self::Align => "align",
148 Self::Color => "color",
149 Self::Typography => "typography",
150 Self::Spacing => "spacing",
151 Self::Html => "html",
152 Self::Multiple => "multiple",
153 }
154 }
155}
156
157pub type WordPressBlockSupports = Vec<WordPressBlockSupport>;
158
159#[derive(Clone, Debug, Eq, PartialEq)]
161pub struct WordPressBlockJson {
162 name: WordPressBlockName,
163 category: Option<WordPressBlockCategory>,
164 attributes: Vec<WordPressBlockAttribute>,
165 supports: WordPressBlockSupports,
166 editor_script: Option<WordPressBlockAssetPath>,
167 style: Option<WordPressBlockAssetPath>,
168 render: Option<WordPressBlockAssetPath>,
169}
170
171impl WordPressBlockJson {
172 pub fn new(name: WordPressBlockName) -> Self {
173 Self {
174 name,
175 category: None,
176 attributes: Vec::new(),
177 supports: Vec::new(),
178 editor_script: None,
179 style: None,
180 render: None,
181 }
182 }
183
184 pub fn with_category(mut self, category: WordPressBlockCategory) -> Self {
185 self.category = Some(category);
186 self
187 }
188
189 pub fn with_attribute(mut self, attribute: WordPressBlockAttribute) -> Self {
190 self.attributes.push(attribute);
191 self
192 }
193
194 pub fn with_support(mut self, support: WordPressBlockSupport) -> Self {
195 self.supports.push(support);
196 self
197 }
198
199 pub fn with_render(mut self, render: WordPressBlockAssetPath) -> Self {
200 self.render = Some(render);
201 self
202 }
203
204 pub const fn name(&self) -> &WordPressBlockName {
205 &self.name
206 }
207
208 pub const fn category(&self) -> Option<&WordPressBlockCategory> {
209 self.category.as_ref()
210 }
211
212 pub fn attributes(&self) -> &[WordPressBlockAttribute] {
213 &self.attributes
214 }
215
216 pub fn supports(&self) -> &[WordPressBlockSupport] {
217 &self.supports
218 }
219
220 pub const fn render(&self) -> Option<&WordPressBlockAssetPath> {
221 self.render.as_ref()
222 }
223}
224
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub enum WordPressBlockError {
228 Empty,
229 InvalidBlockName,
230}
231
232impl fmt::Display for WordPressBlockError {
233 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self {
235 Self::Empty => formatter.write_str("WordPress block metadata cannot be empty"),
236 Self::InvalidBlockName => {
237 formatter.write_str("WordPress block names must look like namespace/block")
238 },
239 }
240 }
241}
242
243impl Error for WordPressBlockError {}
244
245#[cfg(test)]
246mod tests {
247 use super::{
248 WordPressBlockAttribute, WordPressBlockAttributeName, WordPressBlockAttributeType,
249 WordPressBlockError, WordPressBlockJson, WordPressBlockName, WordPressBlockSupport,
250 };
251
252 #[test]
253 fn builds_block_json_metadata() -> Result<(), WordPressBlockError> {
254 let block = WordPressBlockJson::new(WordPressBlockName::new("acme/book-card")?)
255 .with_attribute(WordPressBlockAttribute::new(
256 WordPressBlockAttributeName::new("title")?,
257 WordPressBlockAttributeType::String,
258 ))
259 .with_support(WordPressBlockSupport::Color);
260
261 assert_eq!(block.name().as_str(), "acme/book-card");
262 assert_eq!(block.attributes()[0].name().as_str(), "title");
263 assert_eq!(block.supports(), &[WordPressBlockSupport::Color]);
264 Ok(())
265 }
266}