document_graphics/
chess.rs1use cozy_chess::Board;
4pub use cozy_chess::{Color, Piece, Square};
5use std::{
6 collections::BTreeMap,
7 ops::Range,
8 sync::{
9 Arc,
10 atomic::{AtomicBool, Ordering},
11 },
12};
13mod parser;
14mod position;
15mod render;
16pub use position::Position;
17pub use render::{RenderOptions, rasterize, to_svg};
18
19pub const MAX_SOURCE: usize = 8 * 1024 * 1024;
20pub const MAX_NODES: usize = 100_000;
21pub const MAX_VALUE: usize = 64 * 1024;
22pub const MAX_DEPTH: usize = 128;
23pub const MAX_PLIES: u32 = 16_384;
24pub type NodeId = usize;
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Format {
27 Pgn,
28 Fen,
29}
30#[derive(Debug, Clone, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Error {
33 Invalid {
34 offset: usize,
35 message: &'static str,
36 },
37 Unsupported(&'static str),
38 Limit(&'static str),
39 Cancelled,
40 Render(String),
41}
42impl std::fmt::Display for Error {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Invalid { offset, message } => write!(f, "{message} at byte {}", offset + 1),
46 Self::Unsupported(s) | Self::Limit(s) => f.write_str(s),
47 Self::Cancelled => f.write_str("Chess preview cancelled"),
48 Self::Render(s) => f.write_str(s),
49 }
50 }
51}
52impl std::error::Error for Error {}
53#[derive(Debug)]
54pub struct Node {
55 pub parent: Option<NodeId>,
56 pub first_child: Option<NodeId>,
57 pub next_sibling: Option<NodeId>,
58 pub source: Range<usize>,
59 pub game: usize,
60 pub ply: u32,
62 san: Range<usize>,
63 comments: Vec<Range<usize>>,
64 annotations: Vec<Range<usize>>,
65}
66#[derive(Debug)]
67pub struct Game {
68 pub root: NodeId,
69 pub tags: BTreeMap<String, String>,
70 start: Board,
71}
72#[derive(Debug)]
74pub struct Collection {
75 source: Arc<[u8]>,
76 nodes: Vec<Node>,
77 games: Vec<Game>,
78}
79impl Collection {
80 pub fn parse(source: Arc<[u8]>, format: Format, cancel: &AtomicBool) -> Result<Self, Error> {
81 parser::parse(source, format, cancel)
82 }
83 pub fn nodes(&self) -> &[Node] {
84 &self.nodes
85 }
86 pub fn games(&self) -> &[Game] {
87 &self.games
88 }
89 pub fn source(&self) -> &[u8] {
90 &self.source
91 }
92 pub fn node_for_offset(&self, offset: usize) -> NodeId {
93 self.nodes
94 .partition_point(|n| n.source.start <= offset)
95 .saturating_sub(1)
96 }
97 pub fn san(&self, node: NodeId) -> Option<&str> {
98 self.nodes
99 .get(node)
100 .filter(|n| n.parent.is_some())
101 .and_then(|n| std::str::from_utf8(&self.source[n.san.clone()]).ok())
102 }
103 pub fn comment(&self, node: NodeId) -> String {
104 self.nodes.get(node).map_or_else(String::new, |n| {
105 n.comments
106 .iter()
107 .map(|r| decode(&self.source[r.clone()]))
108 .collect::<Vec<_>>()
109 .join("\n\n")
110 })
111 }
112 pub fn annotations(&self, node: NodeId) -> String {
113 self.nodes.get(node).map_or_else(String::new, |n| {
114 n.annotations
115 .iter()
116 .map(|r| decode(&self.source[r.clone()]))
117 .collect::<Vec<_>>()
118 .join(" ")
119 })
120 }
121 pub fn has_comment(&self, node: NodeId) -> bool {
122 self.nodes
123 .get(node)
124 .is_some_and(|n| !n.comments.is_empty() || !n.annotations.is_empty())
125 }
126 pub fn move_number(&self, node: NodeId) -> u32 {
127 let n = &self.nodes[node];
128 let start = &self.games[n.game].start;
129 u32::from(start.fullmove_number())
130 + (n.ply.saturating_sub(1) + u32::from(start.side_to_move() == Color::Black)) / 2
131 }
132 pub fn move_color(&self, node: NodeId) -> Option<Color> {
133 let n = self.nodes.get(node)?;
134 n.parent?;
135 let start = self.games[n.game].start.side_to_move();
136 Some(if n.ply % 2 == 1 { start } else { !start })
137 }
138 pub fn title(&self, game: usize) -> String {
139 let Some(g) = self.games.get(game) else {
140 return "Chess game".into();
141 };
142 match (g.tags.get("White"), g.tags.get("Black")) {
143 (Some(w), Some(b)) => format!("{w} ยท {b}"),
144 _ => g
145 .tags
146 .get("Event")
147 .cloned()
148 .unwrap_or_else(|| "Chess position".into()),
149 }
150 }
151 pub fn position(&self, node: NodeId, cancel: &AtomicBool) -> Result<Position, Error> {
152 position::at(self, node, cancel)
153 }
154}
155fn check(cancel: &AtomicBool) -> Result<(), Error> {
156 if cancel.load(Ordering::Relaxed) {
157 Err(Error::Cancelled)
158 } else {
159 Ok(())
160 }
161}
162fn invalid(offset: usize, message: &'static str) -> Error {
163 Error::Invalid { offset, message }
164}
165fn decode(bytes: &[u8]) -> String {
166 std::str::from_utf8(bytes)
167 .map(str::to_owned)
168 .unwrap_or_else(|_| bytes.iter().copied().map(char::from).collect())
169}
170#[cfg(test)]
171mod tests;