use crate::error::ServerError;
use citum_engine::{
Bibliography, Citation, DocumentOptions, Processor, StyleInput,
render::{djot::Djot, html::Html, latex::Latex, plain::PlainText, typst::Typst},
};
use citum_schema::Style;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::io::{self, BufRead, Write};
#[derive(Debug, Deserialize)]
#[cfg_attr(
any(feature = "schema", feature = "schema-types"),
derive(schemars::JsonSchema)
)]
pub struct RpcRequest {
pub id: Value,
pub method: String,
pub params: Value,
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(
any(feature = "schema", feature = "schema-types"),
derive(schemars::JsonSchema)
)]
pub enum OutputFormat {
#[default]
Plain,
Html,
Djot,
Latex,
Typst,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(
any(feature = "schema", feature = "schema-types"),
derive(schemars::JsonSchema)
)]
pub struct RenderCitationParams {
pub style_path: String,
pub refs: serde_json::Value,
pub citation: serde_json::Value,
pub output_format: Option<OutputFormat>,
pub inject_ast_indices: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(
any(feature = "schema", feature = "schema-types"),
derive(schemars::JsonSchema)
)]
pub struct RenderBibliographyParams {
pub style_path: String,
pub refs: serde_json::Value,
pub output_format: Option<OutputFormat>,
pub inject_ast_indices: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(
any(feature = "schema", feature = "schema-types"),
derive(schemars::JsonSchema)
)]
pub struct ValidateStyleParams {
pub style_path: String,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(
any(feature = "schema", feature = "schema-types"),
derive(schemars::JsonSchema)
)]
pub struct FormatDocumentParams {
pub style: StyleInput,
pub locale: Option<String>,
pub output_format: Option<OutputFormat>,
pub refs: serde_json::Value,
pub citations: serde_json::Value,
pub document_options: Option<DocumentOptions>,
}
#[derive(Debug, Serialize)]
struct BibliographyResult {
format: OutputFormat,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
entries: Option<Vec<String>>,
}
fn require_field(params: &Value, field: &'static str) -> Result<(), ServerError> {
if params.get(field).is_none() {
return Err(ServerError::MissingField(field.into()));
}
Ok(())
}
fn validate_output_format(params: &Value) -> Result<(), ServerError> {
if let Some(v) = params.get("output_format") {
serde_json::from_value::<OutputFormat>(v.clone()).map_err(|_| {
let raw = v.as_str().unwrap_or("unknown").to_string();
ServerError::UnsupportedOutputFormat(raw.into())
})?;
}
Ok(())
}
pub fn dispatch(req: RpcRequest) -> Result<Value, (Option<Value>, String)> {
let id = req.id.clone();
match req.method.as_str() {
"render_citation" => {
render_citation(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
}
"render_bibliography" => {
render_bibliography(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
}
"validate_style" => {
validate_style(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
}
"format_document" => {
format_document(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
}
_ => Err((Some(req.id), format!("unknown method: {}", req.method))),
}
}
fn render_citation(params: &Value, id: Value) -> Result<Value, ServerError> {
require_field(params, "style_path")?;
require_field(params, "refs")?;
require_field(params, "citation")?;
validate_output_format(params)?;
let params: RenderCitationParams = serde_json::from_value(params.clone())
.map_err(|e| ServerError::CitationError(e.to_string()))?;
let style = load_style(¶ms.style_path)?;
let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
.map_err(|e| ServerError::BibliographyError(e.to_string()))?;
let citation: Citation = serde_json::from_value(params.citation.clone())
.map_err(|e| ServerError::CitationError(e.to_string()))?;
let mut processor = Processor::new(style, bibliography);
let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
processor.set_inject_ast_indices(inject_ast_indices);
let output_format = params.output_format.unwrap_or_default();
let result = render_citation_with_format(&processor, &citation, output_format)
.map_err(|e| ServerError::CitationError(e.to_string()))?;
Ok(json!({
"id": id,
"result": result
}))
}
fn render_bibliography(params: &Value, id: Value) -> Result<Value, ServerError> {
require_field(params, "style_path")?;
require_field(params, "refs")?;
validate_output_format(params)?;
let params: RenderBibliographyParams = serde_json::from_value(params.clone())
.map_err(|e| ServerError::CitationError(e.to_string()))?;
let style = load_style(¶ms.style_path)?;
let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
.map_err(|e| ServerError::BibliographyError(e.to_string()))?;
let mut processor = Processor::new(style, bibliography);
let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
processor.set_inject_ast_indices(inject_ast_indices);
let output_format = params.output_format.unwrap_or_default();
let content = render_bibliography_with_format(&processor, output_format)?;
let entries = matches!(output_format, OutputFormat::Plain).then(|| {
content
.lines()
.filter(|line| !line.is_empty())
.map(std::string::ToString::to_string)
.collect()
});
let result = BibliographyResult {
format: output_format,
content,
entries,
};
Ok(json!({
"id": id,
"result": result
}))
}
fn render_citation_with_format(
processor: &Processor,
citation: &Citation,
format: OutputFormat,
) -> Result<String, ServerError> {
match format {
OutputFormat::Plain => Ok(processor.process_citation_with_format::<PlainText>(citation)?),
OutputFormat::Html => Ok(processor.process_citation_with_format::<Html>(citation)?),
OutputFormat::Djot => Ok(processor.process_citation_with_format::<Djot>(citation)?),
OutputFormat::Latex => Ok(processor.process_citation_with_format::<Latex>(citation)?),
OutputFormat::Typst => Ok(processor.process_citation_with_format::<Typst>(citation)?),
}
}
fn render_bibliography_with_format(
processor: &Processor,
format: OutputFormat,
) -> Result<String, ServerError> {
match format {
OutputFormat::Plain => Ok(processor.render_bibliography_with_format::<PlainText>()),
OutputFormat::Html => Ok(processor.render_bibliography_with_format::<Html>()),
OutputFormat::Djot => Ok(processor.render_bibliography_with_format::<Djot>()),
OutputFormat::Latex => Ok(processor.render_bibliography_with_format::<Latex>()),
OutputFormat::Typst => Ok(processor.render_bibliography_with_format::<Typst>()),
}
}
fn validate_style(params: &Value, id: Value) -> Result<Value, ServerError> {
require_field(params, "style_path")?;
let params: ValidateStyleParams = serde_json::from_value(params.clone())
.map_err(|e| ServerError::CitationError(e.to_string()))?;
match load_style(¶ms.style_path) {
Ok(_) => Ok(json!({
"id": id,
"result": {
"valid": true,
"warnings": []
}
})),
Err(e) => Ok(json!({
"id": id,
"result": {
"valid": false,
"warnings": [e.to_string()]
}
})),
}
}
fn format_document(params: &Value, id: Value) -> Result<Value, ServerError> {
let request: citum_engine::FormatDocumentRequest = serde_json::from_value(params.clone())
.map_err(|e| ServerError::CitationError(format!("Invalid request JSON: {}", e)))?;
let result = match &request.style {
citum_engine::StyleInput::Yaml(_) => citum_engine::format_document(request)
.map_err(|e| ServerError::CitationError(e.to_string()))?,
citum_engine::StyleInput::Id(s)
| citum_engine::StyleInput::Uri(s)
| citum_engine::StyleInput::Path(s) => {
let style = load_style(s)?;
citum_engine::format_document_with_style(style, request)
.map_err(|e| ServerError::CitationError(e.to_string()))?
}
};
let result_json =
serde_json::to_value(&result).map_err(|e| ServerError::CitationError(e.to_string()))?;
Ok(json!({
"id": id,
"result": result_json
}))
}
fn load_style(style_input: &str) -> Result<Style, ServerError> {
use citum_store::resolver::{ResolverError, StyleResolver};
let chain = citum_store::build_standard_chain()
.map_err(|e| ServerError::ResolverError(e.to_string()))?;
match chain.resolve_style(style_input) {
Ok(style) => {
let mut resolved = style
.try_into_resolved_with(Some(&chain))
.map_err(|e| ServerError::StyleResolution(e.to_string()))?;
resolved.extends = None;
Ok(resolved)
}
Err(ResolverError::StyleNotFound(_)) => {
Err(ServerError::StyleNotFound(style_input.to_string()))
}
Err(e) => Err(ServerError::ResolverError(e.to_string())),
}
}
pub fn run_stdio() -> io::Result<()> {
let stdin = io::stdin();
let mut stdout = io::stdout();
let reader = stdin.lock();
for line in reader.lines() {
let line = line?;
if line.is_empty() {
continue;
}
let response = match serde_json::from_str::<RpcRequest>(&line) {
Ok(req) => match dispatch(req.clone()) {
Ok(result) => result,
Err((id, error)) => json!({
"id": id,
"error": error
}),
},
Err(e) => {
json!({
"id": Value::Null,
"error": format!("invalid JSON: {}", e)
})
}
};
writeln!(stdout, "{response}")?;
stdout.flush()?;
}
Ok(())
}
impl Clone for RpcRequest {
fn clone(&self) -> Self {
RpcRequest {
id: self.id.clone(),
method: self.method.clone(),
params: self.params.clone(),
}
}
}