1pub mod content;
12
13pub use content::{
14 Content, ContentFormat, ContentMeta, DiscoverResult, PostInfo, PostPlatformConfig,
15};
16
17use serde::{Deserialize, Deserializer, Serialize};
18use std::fmt;
19use std::ops::Deref;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum MathRendering {
29 #[default]
31 Svg,
32 Latex,
34 Png,
37}
38
39impl MathRendering {
40 pub fn code_expr(&self) -> &'static str {
42 match self {
43 Self::Svg => "MathRendering::Svg",
44 Self::Latex => "MathRendering::Latex",
45 Self::Png => "MathRendering::Png",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum MathDelimiters {
58 #[default]
60 Dollar,
61 Brackets,
63 #[serde(rename = "brackets_inline_dollar_block")]
66 BracketsInlineDollarBlock,
67}
68
69impl MathDelimiters {
70 pub fn code_expr(&self) -> &'static str {
72 match self {
73 Self::Dollar => "MathDelimiters::Dollar",
74 Self::Brackets => "MathDelimiters::Brackets",
75 Self::BracketsInlineDollarBlock => "MathDelimiters::BracketsInlineDollarBlock",
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum AssetStrategy {
88 Copy,
90 Embed,
92 Upload,
94 External,
97}
98
99impl AssetStrategy {
100 pub fn parse(s: &str) -> Option<Self> {
102 match s.to_lowercase().as_str() {
103 "copy" => Some(Self::Copy),
104 "embed" => Some(Self::Embed),
105 "upload" => Some(Self::Upload),
106 "external" => Some(Self::External),
107 _ => None,
108 }
109 }
110
111 pub fn requires_deferred_upload(&self) -> bool {
115 matches!(self, Self::Upload | Self::External)
116 }
117
118 pub fn code_expr(&self) -> &'static str {
120 match self {
121 Self::Copy => "AssetStrategy::Copy",
122 Self::Embed => "AssetStrategy::Embed",
123 Self::Upload => "AssetStrategy::Upload",
124 Self::External => "AssetStrategy::External",
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135pub enum CapabilityGapBehavior {
136 WarnAndDegrade,
137 HardError,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum NodePolicyAction {
144 Pass,
145 Sanitize,
146 Drop,
147 Error,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum CapabilitySupport {
153 Supported,
154 Unsupported(CapabilityGapBehavior),
155}
156
157impl CapabilitySupport {
158 pub fn gap_behavior(&self) -> Option<CapabilityGapBehavior> {
160 match self {
161 Self::Supported => None,
162 Self::Unsupported(behavior) => Some(*behavior),
163 }
164 }
165
166 pub fn code_expr(&self) -> &'static str {
168 match self {
169 Self::Supported => "CapabilitySupport::Supported",
170 Self::Unsupported(CapabilityGapBehavior::WarnAndDegrade) => {
171 "CapabilitySupport::Unsupported(UnsupportedBehavior::WarnAndDegrade)"
172 }
173 Self::Unsupported(CapabilityGapBehavior::HardError) => {
174 "CapabilitySupport::Unsupported(UnsupportedBehavior::HardError)"
175 }
176 }
177 }
178}
179
180impl<'de> Deserialize<'de> for CapabilitySupport {
182 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
183 where
184 D: Deserializer<'de>,
185 {
186 let s = String::deserialize(deserializer)?;
187 match s.as_str() {
188 "supported" => Ok(Self::Supported),
189 "unsupported_warn" => Ok(Self::Unsupported(CapabilityGapBehavior::WarnAndDegrade)),
190 "unsupported_error" => Ok(Self::Unsupported(CapabilityGapBehavior::HardError)),
191 other => Err(serde::de::Error::unknown_variant(
192 other,
193 &["supported", "unsupported_warn", "unsupported_error"],
194 )),
195 }
196 }
197}
198
199impl Serialize for CapabilitySupport {
200 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
201 where
202 S: serde::Serializer,
203 {
204 let s = match self {
205 Self::Supported => "supported",
206 Self::Unsupported(CapabilityGapBehavior::WarnAndDegrade) => "unsupported_warn",
207 Self::Unsupported(CapabilityGapBehavior::HardError) => "unsupported_error",
208 };
209 serializer.serialize_str(s)
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum DraftSupport {
220 None,
222 StatusField { reversible: bool },
225 SeparateObjects,
227}
228
229impl DraftSupport {
230 pub fn code_expr(&self) -> &'static str {
232 match self {
233 Self::None => "DraftSupport::None",
234 Self::StatusField { reversible: true } => {
235 "DraftSupport::StatusField { reversible: true }"
236 }
237 Self::StatusField { reversible: false } => {
238 "DraftSupport::StatusField { reversible: false }"
239 }
240 Self::SeparateObjects => "DraftSupport::SeparateObjects",
241 }
242 }
243}
244
245impl<'de> Deserialize<'de> for DraftSupport {
247 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
248 where
249 D: Deserializer<'de>,
250 {
251 let s = String::deserialize(deserializer)?;
252 match s.as_str() {
253 "none" => Ok(Self::None),
254 "status_field_reversible" => Ok(Self::StatusField { reversible: true }),
255 "status_field_irreversible" => Ok(Self::StatusField { reversible: false }),
256 "separate_objects" => Ok(Self::SeparateObjects),
257 other => Err(serde::de::Error::unknown_variant(
258 other,
259 &[
260 "none",
261 "status_field_reversible",
262 "status_field_irreversible",
263 "separate_objects",
264 ],
265 )),
266 }
267 }
268}
269
270impl Serialize for DraftSupport {
271 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
272 where
273 S: serde::Serializer,
274 {
275 let s = match self {
276 Self::None => "none",
277 Self::StatusField { reversible: true } => "status_field_reversible",
278 Self::StatusField { reversible: false } => "status_field_irreversible",
279 Self::SeparateObjects => "separate_objects",
280 };
281 serializer.serialize_str(s)
282 }
283}
284
285#[derive(Debug, Clone, Copy)]
292pub struct TaxonomyCapability {
293 pub tags: CapabilitySupport,
294 pub categories: CapabilitySupport,
295 pub internal_links: CapabilitySupport,
296 pub draft: DraftSupport,
297}
298
299impl TaxonomyCapability {
300 pub const fn new(
302 tags: CapabilitySupport,
303 categories: CapabilitySupport,
304 internal_links: CapabilitySupport,
305 draft: DraftSupport,
306 ) -> Self {
307 Self {
308 tags,
309 categories,
310 internal_links,
311 draft,
312 }
313 }
314
315 pub const fn full() -> Self {
317 Self {
318 tags: CapabilitySupport::Supported,
319 categories: CapabilitySupport::Supported,
320 internal_links: CapabilitySupport::Supported,
321 draft: DraftSupport::StatusField { reversible: true },
322 }
323 }
324
325 pub const fn minimal() -> Self {
327 Self {
328 tags: CapabilitySupport::Unsupported(CapabilityGapBehavior::WarnAndDegrade),
329 categories: CapabilitySupport::Unsupported(CapabilityGapBehavior::WarnAndDegrade),
330 internal_links: CapabilitySupport::Supported,
331 draft: DraftSupport::None,
332 }
333 }
334
335 pub fn tags_gap_behavior(&self) -> Option<CapabilityGapBehavior> {
336 self.tags.gap_behavior()
337 }
338
339 pub fn categories_gap_behavior(&self) -> Option<CapabilityGapBehavior> {
340 self.categories.gap_behavior()
341 }
342
343 pub fn internal_links_gap_behavior(&self) -> Option<CapabilityGapBehavior> {
344 self.internal_links.gap_behavior()
345 }
346
347 pub fn draft_support(&self) -> DraftSupport {
348 self.draft
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
361#[serde(transparent)]
362pub struct ThemeId(String);
363
364impl ThemeId {
365 pub fn new(s: impl Into<String>) -> Self {
367 Self(s.into())
368 }
369
370 pub fn as_str(&self) -> &str {
372 &self.0
373 }
374
375 pub fn into_inner(self) -> String {
377 self.0
378 }
379}
380
381impl Deref for ThemeId {
382 type Target = str;
383
384 fn deref(&self) -> &str {
385 &self.0
386 }
387}
388
389impl fmt::Display for ThemeId {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 f.write_str(&self.0)
392 }
393}
394
395impl From<String> for ThemeId {
396 fn from(s: String) -> Self {
397 Self(s)
398 }
399}
400
401impl From<&str> for ThemeId {
402 fn from(s: &str) -> Self {
403 Self(s.to_string())
404 }
405}
406
407impl AsRef<str> for ThemeId {
408 fn as_ref(&self) -> &str {
409 &self.0
410 }
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum LinkResolution {
420 NonInternal,
422 InternalResolved { slug: String, url: String },
424 InternalUnresolved { slug: String },
426}
427
428#[cfg(test)]
433mod tests {
434 #![allow(clippy::expect_used)]
435 use super::*;
436
437 #[test]
438 fn test_math_rendering_serde_roundtrip() {
439 let json = serde_json::to_string(&MathRendering::Svg).expect("serialize");
440 assert_eq!(json, r#""svg""#);
441 let parsed: MathRendering = serde_json::from_str(&json).expect("deserialize");
442 assert_eq!(parsed, MathRendering::Svg);
443 }
444
445 #[test]
446 fn test_math_rendering_code_expr() {
447 assert_eq!(MathRendering::Svg.code_expr(), "MathRendering::Svg");
448 assert_eq!(MathRendering::Latex.code_expr(), "MathRendering::Latex");
449 }
450
451 #[test]
452 fn test_math_delimiters_serde_roundtrip() {
453 let json = serde_json::to_string(&MathDelimiters::Brackets).expect("serialize");
454 assert_eq!(json, r#""brackets""#);
455 let parsed: MathDelimiters = serde_json::from_str(&json).expect("deserialize");
456 assert_eq!(parsed, MathDelimiters::Brackets);
457 }
458
459 #[test]
460 fn test_asset_strategy_serde() {
461 let json = serde_json::to_string(&AssetStrategy::External).expect("serialize");
462 assert_eq!(json, r#""external""#);
463 let parsed: AssetStrategy = serde_json::from_str(&json).expect("deserialize");
464 assert_eq!(parsed, AssetStrategy::External);
465 }
466
467 #[test]
468 fn test_asset_strategy_parse_aliases() {
469 assert_eq!(
470 AssetStrategy::parse("external"),
471 Some(AssetStrategy::External)
472 );
473 assert_eq!(AssetStrategy::parse("upload"), Some(AssetStrategy::Upload));
474 assert_eq!(AssetStrategy::parse("COPY"), Some(AssetStrategy::Copy));
475 assert_eq!(AssetStrategy::parse("unknown"), None);
476 }
477
478 #[test]
479 fn test_capability_support_serde() {
480 let json =
481 serde_json::to_string(&CapabilitySupport::Supported).expect("serialize supported");
482 assert_eq!(json, r#""supported""#);
483
484 let warn = CapabilitySupport::Unsupported(CapabilityGapBehavior::WarnAndDegrade);
485 let json = serde_json::to_string(&warn).expect("serialize warn");
486 assert_eq!(json, r#""unsupported_warn""#);
487
488 let parsed: CapabilitySupport =
489 serde_json::from_str(r#""unsupported_error""#).expect("deserialize error");
490 assert_eq!(
491 parsed,
492 CapabilitySupport::Unsupported(CapabilityGapBehavior::HardError)
493 );
494 }
495
496 #[test]
497 fn test_capability_support_invalid() {
498 let result = serde_json::from_str::<CapabilitySupport>(r#""garbage""#);
499 assert!(result.is_err());
500 }
501
502 #[test]
503 fn test_draft_support_serde() {
504 let json = serde_json::to_string(&DraftSupport::None).expect("serialize none");
505 assert_eq!(json, r#""none""#);
506
507 let reversible = DraftSupport::StatusField { reversible: true };
508 let json = serde_json::to_string(&reversible).expect("serialize reversible");
509 assert_eq!(json, r#""status_field_reversible""#);
510
511 let parsed: DraftSupport =
512 serde_json::from_str(r#""separate_objects""#).expect("deserialize");
513 assert_eq!(parsed, DraftSupport::SeparateObjects);
514 }
515
516 #[test]
517 fn test_theme_id_deref_and_display() {
518 let id = ThemeId::new("wechat-green");
519 assert_eq!(&*id, "wechat-green");
520 assert_eq!(id.as_str(), "wechat-green");
521 assert_eq!(format!("{}", id), "wechat-green");
522 }
523
524 #[test]
525 fn test_theme_id_serde() {
526 let id = ThemeId::new("elegant");
527 let json = serde_json::to_string(&id).expect("serialize");
528 assert_eq!(json, r#""elegant""#);
529 let parsed: ThemeId = serde_json::from_str(&json).expect("deserialize");
530 assert_eq!(parsed, id);
531 }
532
533 #[test]
534 fn test_theme_id_from() {
535 let id: ThemeId = "dark".into();
536 assert_eq!(id.as_str(), "dark");
537
538 let id: ThemeId = String::from("github").into();
539 assert_eq!(id.as_str(), "github");
540 }
541}