1use std::{
7 ops::Range,
8 sync::{
9 Arc,
10 atomic::{AtomicBool, Ordering},
11 },
12};
13
14mod parser;
15mod position;
16mod render;
17pub use position::{Color, Line, Mark, MarkKind, Played, Point, Position};
18pub use render::{RenderOptions, rasterize, to_svg};
19
20pub const MAX_SOURCE: usize = 8 * 1024 * 1024;
21pub const MAX_NODES: usize = 100_000;
22pub const MAX_VALUE: usize = 128 * 1024;
23pub type NodeId = usize;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum Error {
28 Invalid {
29 offset: usize,
30 message: &'static str,
31 },
32 Unsupported(&'static str),
33 Limit(&'static str),
34 Cancelled,
35 Render(String),
36}
37impl std::fmt::Display for Error {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 Self::Invalid { offset, message } => write!(f, "{message} at byte {}", offset + 1),
41 Self::Unsupported(s) | Self::Limit(s) => f.write_str(s),
42 Self::Cancelled => f.write_str("SGF preview cancelled"),
43 Self::Render(s) => f.write_str(s),
44 }
45 }
46}
47impl std::error::Error for Error {}
48
49#[derive(Debug)]
50pub struct Node {
51 pub parent: Option<NodeId>,
52 pub first_child: Option<NodeId>,
53 pub next_sibling: Option<NodeId>,
54 pub source: Range<usize>,
55 pub game: usize,
56 properties: Range<usize>,
57}
58#[derive(Debug)]
59pub struct Game {
60 pub root: NodeId,
61 pub width: u8,
62 pub height: u8,
63 encoding: Encoding,
64}
65#[derive(Debug, Clone, Copy)]
66enum Encoding {
67 Utf8,
68 Latin1,
69}
70#[derive(Debug)]
71struct Property {
72 name: Range<usize>,
73 values: Range<usize>,
74}
75
76#[derive(Debug)]
79pub struct Collection {
80 source: Arc<[u8]>,
81 nodes: Vec<Node>,
82 properties: Vec<Property>,
83 values: Vec<Range<usize>>,
84 games: Vec<Game>,
85}
86impl Collection {
87 pub fn parse(source: Arc<[u8]>, cancel: &AtomicBool) -> Result<Self, Error> {
88 parser::parse(source, cancel)
89 }
90 pub fn nodes(&self) -> &[Node] {
91 &self.nodes
92 }
93 pub fn games(&self) -> &[Game] {
94 &self.games
95 }
96 pub fn source(&self) -> &[u8] {
97 &self.source
98 }
99 pub fn node_for_offset(&self, offset: usize) -> NodeId {
100 self.nodes
101 .partition_point(|n| n.source.start <= offset)
102 .saturating_sub(1)
103 }
104 pub fn text(&self, node: NodeId, property: &str) -> Option<String> {
105 self.values(node, property).next()
106 }
107 pub fn children(&self, node: NodeId) -> impl Iterator<Item = NodeId> + '_ {
108 std::iter::successors(self.nodes.get(node).and_then(|n| n.first_child), |&id| {
109 self.nodes[id].next_sibling
110 })
111 }
112 pub fn title(&self, game: usize) -> String {
113 let Some(game) = self.games.get(game) else {
114 return "Go game".into();
115 };
116 if let Some(title) = self.text(game.root, "GN").filter(|s| !s.is_empty()) {
117 return title;
118 }
119 let player = |name| self.text(game.root, name).filter(|s| !s.is_empty());
120 match (player("PB"), player("PW")) {
121 (Some(b), Some(w)) => format!("{b} ยท {w}"),
122 _ => "Go game".into(),
123 }
124 }
125 pub fn position(&self, node: NodeId, cancel: &AtomicBool) -> Result<Position, Error> {
126 position::at(self, node, cancel)
127 }
128 fn ranges(&self, node: NodeId, name: &str) -> &[Range<usize>] {
129 self.nodes
130 .get(node)
131 .and_then(|n| {
132 self.properties[n.properties.clone()]
133 .iter()
134 .find(|p| &self.source[p.name.clone()] == name.as_bytes())
135 })
136 .map_or(&[], |p| &self.values[p.values.clone()])
137 }
138 fn values<'a>(&'a self, node: NodeId, name: &str) -> impl Iterator<Item = String> + 'a {
139 let text = matches!(name, "C" | "GC");
140 let encoding = self
141 .nodes
142 .get(node)
143 .map_or(Encoding::Utf8, |n| self.games[n.game].encoding);
144 self.ranges(node, name)
145 .iter()
146 .map(move |range| decode(&self.source[range.clone()], encoding, text))
147 }
148}
149
150fn check(cancel: &AtomicBool) -> Result<(), Error> {
151 if cancel.load(Ordering::Relaxed) {
152 Err(Error::Cancelled)
153 } else {
154 Ok(())
155 }
156}
157fn invalid(offset: usize, message: &'static str) -> Error {
158 Error::Invalid { offset, message }
159}
160
161fn decode(bytes: &[u8], encoding: Encoding, text: bool) -> String {
162 let mut out = Vec::with_capacity(bytes.len());
163 let mut i = 0;
164 while i < bytes.len() {
165 let escaped = bytes[i] == b'\\';
166 if escaped {
167 i += 1;
168 if i == bytes.len() {
169 break;
170 }
171 }
172 let c = bytes[i];
173 i += 1;
174 if matches!(c, b'\r' | b'\n') {
175 if i < bytes.len() && matches!(bytes[i], b'\r' | b'\n') && bytes[i] != c {
176 i += 1;
177 }
178 if !escaped {
179 out.push(if text { b'\n' } else { b' ' });
180 }
181 } else if c.is_ascii_whitespace() {
182 out.push(b' ');
183 } else {
184 out.push(c);
185 }
186 }
187 match encoding {
188 Encoding::Utf8 => String::from_utf8_lossy(&out).into_owned(),
189 Encoding::Latin1 => out.into_iter().map(char::from).collect(),
190 }
191}
192
193#[cfg(test)]
194mod tests;