use crate::arrows::*;
use chess::{Board, ChessMove, Color, File, Piece, Rank, Square};
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::errors::Error as MdBookError;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Parser, Tag};
use pulldown_cmark_to_cmark::cmark;
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use tracing::{debug, error, info};
const X_OFFSET: f32 = 0.6;
const Y_OFFSET: f32 = 0.3;
pub const PREPROCESSOR_NAME: &'static str = "mdbook-chess";
const fn true_value() -> bool {
true
}
#[derive(Debug, Clone, Deserialize)]
pub struct BoardBlock {
load: Option<String>,
save: Option<ManyOrOne>,
board: Option<String>,
#[serde(default)]
moves: Vec<String>,
#[serde(default = "true_value")]
overwrite: bool,
#[serde(default)]
highlights: Vec<String>,
#[serde(default)]
lines: Vec<Line>,
}
pub struct ChessPreprocessor;
impl Preprocessor for ChessPreprocessor {
fn name(&self) -> &str {
PREPROCESSOR_NAME
}
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, MdBookError> {
book.for_each_mut(|item| {
if let BookItem::Chapter(chapter) = item {
let _ = process_code_blocks(chapter).map(|s| {
chapter.content = s;
});
}
});
Ok(book)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ManyOrOne {
One(String),
Many(Vec<String>),
}
impl ManyOrOne {
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &'a String> + 'a> {
match self {
Self::One(s) => Box::new(std::iter::once(s)),
Self::Many(s) => Box::new(s.iter()),
}
}
}
impl BoardBlock {
fn get_highlights(&self) -> Vec<Square> {
self.highlights
.iter()
.filter_map(|x| {
Square::from_str(x)
.map_err(|e| {
error!("Invalid square {}: {}", x, e);
})
.ok()
})
.collect()
}
}
const fn get_board() -> &'static str {
include_str!("../res/board.svg")
}
const fn get_piece(p: Piece, c: Color) -> &'static str {
match (p, c) {
(Piece::Pawn, Color::White) => include_str!("../res/P.svg"),
(Piece::Knight, Color::White) => include_str!("../res/N.svg"),
(Piece::Bishop, Color::White) => include_str!("../res/B.svg"),
(Piece::Rook, Color::White) => include_str!("../res/R.svg"),
(Piece::Queen, Color::White) => include_str!("../res/Q.svg"),
(Piece::King, Color::White) => include_str!("../res/K.svg"),
(Piece::Pawn, Color::Black) => include_str!("../res/p.svg"),
(Piece::Knight, Color::Black) => include_str!("../res/n.svg"),
(Piece::Bishop, Color::Black) => include_str!("../res/b.svg"),
(Piece::Rook, Color::Black) => include_str!("../res/r.svg"),
(Piece::Queen, Color::Black) => include_str!("../res/q.svg"),
(Piece::King, Color::Black) => include_str!("../res/k.svg"),
}
}
const fn square_highlight() -> &'static str {
include_str!("../res/highlight.svg")
}
pub fn coordinate_from_square(square: &Square) -> (f32, f32) {
coordinate(square.get_file(), square.get_rank())
}
#[inline(always)]
pub fn coordinate(file: File, rank: Rank) -> (f32, f32) {
let rank = 70 - (rank.to_index() * 10);
(
(file.to_index() * 10) as f32 + X_OFFSET,
rank as f32 + Y_OFFSET,
)
}
pub fn generate_board(board: &Board, highlights: Option<Vec<Square>>, lines: &[Line]) -> String {
let mut pieces = String::new();
for i in 0..64 {
let square = unsafe { Square::new(i) };
if let Some((piece, color)) = board.piece_on(square).zip(board.color_on(square)) {
let (x, y) = coordinate_from_square(&square);
let piece_svg = get_piece(piece, color)
.replace("$X_POSITION", &x.to_string())
.replace("$Y_POSITION", &y.to_string());
pieces.push_str(&piece_svg);
}
}
if let Some(highlights) = highlights {
for square in highlights.iter() {
let (x, y) = coordinate_from_square(square);
let square = square_highlight()
.replace("$X_POSITION", &(x - 0.6).to_string())
.replace("$Y_POSITION", &(y - 0.27).to_string());
pieces.push_str(&square);
}
}
for line in lines {
pieces.push_str(&line.svg_string());
}
get_board().replace("<!-- PIECES -->", &pieces)
}
fn process_code_blocks(chapter: &mut Chapter) -> Result<String, fmt::Error> {
use CodeBlockKind::*;
use CowStr::*;
use Event::*;
use Tag::{CodeBlock, Paragraph};
if chapter.content.contains("```chess") {
let mut boards = HashMap::new();
let mut logged_found = false;
let mut output = String::with_capacity(chapter.content.len());
let mut inside_block = false;
let events = Parser::new(&chapter.content).map(|e| match (&e, inside_block) {
(Start(CodeBlock(Fenced(Borrowed("chess")))), false) => {
inside_block = true;
if !logged_found {
info!("Found chess block(s) in {}", chapter.name);
logged_found = true;
}
Start(Paragraph)
}
(Text(Borrowed(text)), true) => {
inside_block = false;
Html(process_chess_block(text, &mut boards).into())
}
(End(CodeBlock(Fenced(Borrowed("chess")))), false) => End(Paragraph),
(Text(text), _) => Html(text.clone()),
_ => {
debug!("Ignoring event: {:?}", e);
e
}
});
cmark(events, &mut output).map(|_| output)
} else {
Ok(chapter.content.clone())
}
}
fn process_chess_block(input: &str, boards: &mut HashMap<String, Board>) -> String {
match serde_yaml::from_str::<BoardBlock>(input) {
Ok(block) => {
let name = block.load.clone();
let mut board = match block.board.as_deref() {
Some("start") => Board::default(),
Some(s) => match Board::from_str(s) {
Ok(b) => b,
Err(e) => {
error!("Invalid FEN String: {}", e);
return get_board().to_string();
}
},
None => {
let res = name.as_ref().and_then(|name| boards.get(name)).cloned();
match res {
Some(b) => b,
None => Board::default(),
}
}
};
for m in &block.moves {
match ChessMove::from_san(&board, m.as_str()) {
Ok(chess_move) => {
let new_board = board.make_move_new(chess_move);
board = new_board;
}
Err(_) => {
error!("{} is an invalid SAN move", m);
return get_board().to_string();
}
}
}
if let Some(s) = block.save.as_ref() {
for save in s.iter() {
boards.insert(save.clone(), board.clone());
}
}
if block.overwrite && !matches!(name.as_deref(), Some("start")) {
if let Some(name) = name.clone() {
boards.insert(name, board.clone());
}
}
generate_board(&board, Some(block.get_highlights()), &block.lines)
}
Err(e) => {
error!("Creating default board invalid YAML: {}", e);
generate_board(&Board::default(), None, &[])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn ensure_svg_in_output() {
let mut chapter = Chapter::new(
"test",
"```chess\nBoard: default\n```".to_string(),
".",
vec![],
);
let s = process_code_blocks(&mut chapter).unwrap();
assert!(s.contains(&generate_board(&Board::default(), None, &[])));
}
#[test]
fn dont_remove_tables() {
let mut chapter = Chapter::new(
"test",
"|foo|bar|\n|---|---|\n|a|b|".to_string(),
".",
vec![],
);
let s = process_code_blocks(&mut chapter).unwrap();
assert!(!s.contains(r#"|foo|bar|\n|---|---|\n|a|b|"#), "{}", s);
}
#[test]
fn dont_break_emphasis_in_tables() {
let content = fs::read_to_string("demo-book/src/no_chess.md").unwrap();
let mut chapter = Chapter::new("test", content.clone(), ".", vec![]);
let s = process_code_blocks(&mut chapter).unwrap();
assert_eq!(content, s);
}
}