1use crate::error::ServerError;
7use citum_engine::{
8 Bibliography, Citation, DocumentOptions, Processor, StyleInput,
9 render::{djot::Djot, html::Html, latex::Latex, plain::PlainText, typst::Typst},
10};
11use citum_schema::Style;
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14use std::io::{self, BufRead, Write};
15
16#[derive(Debug, Deserialize)]
18#[cfg_attr(
19 any(feature = "schema", feature = "schema-types"),
20 derive(schemars::JsonSchema)
21)]
22pub struct RpcRequest {
23 pub id: Value,
25 pub method: String,
27 pub params: Value,
29}
30
31#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
33#[serde(rename_all = "lowercase")]
34#[cfg_attr(
35 any(feature = "schema", feature = "schema-types"),
36 derive(schemars::JsonSchema)
37)]
38pub enum OutputFormat {
39 #[default]
41 Plain,
42 Html,
44 Djot,
46 Latex,
48 Typst,
50}
51
52#[derive(Debug, Deserialize)]
54#[cfg_attr(
55 any(feature = "schema", feature = "schema-types"),
56 derive(schemars::JsonSchema)
57)]
58pub struct RenderCitationParams {
59 pub style_path: String,
61 pub refs: serde_json::Value,
63 pub citation: serde_json::Value,
65 pub output_format: Option<OutputFormat>,
67 pub inject_ast_indices: Option<bool>,
69}
70
71#[derive(Debug, Deserialize)]
73#[cfg_attr(
74 any(feature = "schema", feature = "schema-types"),
75 derive(schemars::JsonSchema)
76)]
77pub struct RenderBibliographyParams {
78 pub style_path: String,
80 pub refs: serde_json::Value,
82 pub output_format: Option<OutputFormat>,
84 pub inject_ast_indices: Option<bool>,
86}
87
88#[derive(Debug, Deserialize)]
90#[cfg_attr(
91 any(feature = "schema", feature = "schema-types"),
92 derive(schemars::JsonSchema)
93)]
94pub struct ValidateStyleParams {
95 pub style_path: String,
97}
98
99#[derive(Debug, Deserialize)]
101#[cfg_attr(
102 any(feature = "schema", feature = "schema-types"),
103 derive(schemars::JsonSchema)
104)]
105pub struct FormatDocumentParams {
106 pub style: StyleInput,
108 pub locale: Option<String>,
110 pub output_format: Option<OutputFormat>,
112 pub refs: serde_json::Value,
114 pub citations: serde_json::Value,
116 pub document_options: Option<DocumentOptions>,
118}
119
120#[derive(Debug, Serialize)]
121struct BibliographyResult {
122 format: OutputFormat,
123 content: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 entries: Option<Vec<String>>,
126}
127
128fn require_field(params: &Value, field: &'static str) -> Result<(), ServerError> {
130 if params.get(field).is_none() {
131 return Err(ServerError::MissingField(field.into()));
132 }
133 Ok(())
134}
135
136fn validate_output_format(params: &Value) -> Result<(), ServerError> {
138 if let Some(v) = params.get("output_format") {
139 serde_json::from_value::<OutputFormat>(v.clone()).map_err(|_| {
140 let raw = v.as_str().unwrap_or("unknown").to_string();
141 ServerError::UnsupportedOutputFormat(raw.into())
142 })?;
143 }
144 Ok(())
145}
146
147pub fn dispatch(req: RpcRequest) -> Result<Value, (Option<Value>, String)> {
158 let id = req.id.clone();
159
160 match req.method.as_str() {
161 "render_citation" => {
162 render_citation(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
163 }
164 "render_bibliography" => {
165 render_bibliography(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
166 }
167 "validate_style" => {
168 validate_style(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
169 }
170 "format_document" => {
171 format_document(&req.params, id).map_err(|e| (Some(req.id), e.to_string()))
172 }
173 _ => Err((Some(req.id), format!("unknown method: {}", req.method))),
174 }
175}
176
177fn render_citation(params: &Value, id: Value) -> Result<Value, ServerError> {
179 require_field(params, "style_path")?;
180 require_field(params, "refs")?;
181 require_field(params, "citation")?;
182 validate_output_format(params)?;
183 let params: RenderCitationParams = serde_json::from_value(params.clone())
184 .map_err(|e| ServerError::CitationError(e.to_string()))?;
185
186 let style = load_style(¶ms.style_path)?;
188
189 let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
191 .map_err(|e| ServerError::BibliographyError(e.to_string()))?;
192
193 let citation: Citation = serde_json::from_value(params.citation.clone())
194 .map_err(|e| ServerError::CitationError(e.to_string()))?;
195
196 let mut processor = Processor::new(style, bibliography);
198 let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
199 processor.set_inject_ast_indices(inject_ast_indices);
200
201 let output_format = params.output_format.unwrap_or_default();
202 let result = render_citation_with_format(&processor, &citation, output_format)
203 .map_err(|e| ServerError::CitationError(e.to_string()))?;
204
205 Ok(json!({
206 "id": id,
207 "result": result
208 }))
209}
210
211fn render_bibliography(params: &Value, id: Value) -> Result<Value, ServerError> {
213 require_field(params, "style_path")?;
214 require_field(params, "refs")?;
215 validate_output_format(params)?;
216 let params: RenderBibliographyParams = serde_json::from_value(params.clone())
217 .map_err(|e| ServerError::CitationError(e.to_string()))?;
218
219 let style = load_style(¶ms.style_path)?;
221
222 let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
224 .map_err(|e| ServerError::BibliographyError(e.to_string()))?;
225
226 let mut processor = Processor::new(style, bibliography);
228 let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
229 processor.set_inject_ast_indices(inject_ast_indices);
230
231 let output_format = params.output_format.unwrap_or_default();
232 let content = render_bibliography_with_format(&processor, output_format)?;
233 let entries = matches!(output_format, OutputFormat::Plain).then(|| {
234 content
235 .lines()
236 .filter(|line| !line.is_empty())
237 .map(std::string::ToString::to_string)
238 .collect()
239 });
240 let result = BibliographyResult {
241 format: output_format,
242 content,
243 entries,
244 };
245
246 Ok(json!({
247 "id": id,
248 "result": result
249 }))
250}
251
252fn render_citation_with_format(
253 processor: &Processor,
254 citation: &Citation,
255 format: OutputFormat,
256) -> Result<String, ServerError> {
257 match format {
258 OutputFormat::Plain => Ok(processor.process_citation_with_format::<PlainText>(citation)?),
259 OutputFormat::Html => Ok(processor.process_citation_with_format::<Html>(citation)?),
260 OutputFormat::Djot => Ok(processor.process_citation_with_format::<Djot>(citation)?),
261 OutputFormat::Latex => Ok(processor.process_citation_with_format::<Latex>(citation)?),
262 OutputFormat::Typst => Ok(processor.process_citation_with_format::<Typst>(citation)?),
263 }
264}
265
266fn render_bibliography_with_format(
267 processor: &Processor,
268 format: OutputFormat,
269) -> Result<String, ServerError> {
270 match format {
271 OutputFormat::Plain => Ok(processor.render_bibliography_with_format::<PlainText>()),
272 OutputFormat::Html => Ok(processor.render_bibliography_with_format::<Html>()),
273 OutputFormat::Djot => Ok(processor.render_bibliography_with_format::<Djot>()),
274 OutputFormat::Latex => Ok(processor.render_bibliography_with_format::<Latex>()),
275 OutputFormat::Typst => Ok(processor.render_bibliography_with_format::<Typst>()),
276 }
277}
278
279fn validate_style(params: &Value, id: Value) -> Result<Value, ServerError> {
281 require_field(params, "style_path")?;
282 let params: ValidateStyleParams = serde_json::from_value(params.clone())
283 .map_err(|e| ServerError::CitationError(e.to_string()))?;
284
285 match load_style(¶ms.style_path) {
286 Ok(_) => Ok(json!({
287 "id": id,
288 "result": {
289 "valid": true,
290 "warnings": []
291 }
292 })),
293 Err(e) => Ok(json!({
294 "id": id,
295 "result": {
296 "valid": false,
297 "warnings": [e.to_string()]
298 }
299 })),
300 }
301}
302
303fn format_document(params: &Value, id: Value) -> Result<Value, ServerError> {
305 let request: citum_engine::FormatDocumentRequest = serde_json::from_value(params.clone())
306 .map_err(|e| ServerError::CitationError(format!("Invalid request JSON: {}", e)))?;
307
308 let result = match &request.style {
309 citum_engine::StyleInput::Yaml(_) => citum_engine::format_document(request)
310 .map_err(|e| ServerError::CitationError(e.to_string()))?,
311 citum_engine::StyleInput::Id(s)
312 | citum_engine::StyleInput::Uri(s)
313 | citum_engine::StyleInput::Path(s) => {
314 let style = load_style(s)?;
315 citum_engine::format_document_with_style(style, request)
316 .map_err(|e| ServerError::CitationError(e.to_string()))?
317 }
318 };
319
320 let result_json =
321 serde_json::to_value(&result).map_err(|e| ServerError::CitationError(e.to_string()))?;
322
323 Ok(json!({
324 "id": id,
325 "result": result_json
326 }))
327}
328
329fn load_style(style_input: &str) -> Result<Style, ServerError> {
335 use citum_store::resolver::{ResolverError, StyleResolver};
336
337 let chain = citum_store::build_standard_chain()
338 .map_err(|e| ServerError::ResolverError(e.to_string()))?;
339
340 match chain.resolve_style(style_input) {
341 Ok(style) => {
342 let mut resolved = style
343 .try_into_resolved_with(Some(&chain))
344 .map_err(|e| ServerError::StyleResolution(e.to_string()))?;
345 resolved.extends = None;
346 Ok(resolved)
347 }
348 Err(ResolverError::StyleNotFound(_)) => {
349 Err(ServerError::StyleNotFound(style_input.to_string()))
350 }
351 Err(e) => Err(ServerError::ResolverError(e.to_string())),
352 }
353}
354
355pub fn run_stdio() -> io::Result<()> {
363 let stdin = io::stdin();
364 let mut stdout = io::stdout();
365
366 let reader = stdin.lock();
367 for line in reader.lines() {
368 let line = line?;
369
370 if line.is_empty() {
372 continue;
373 }
374
375 let response = match serde_json::from_str::<RpcRequest>(&line) {
377 Ok(req) => match dispatch(req.clone()) {
378 Ok(result) => result,
379 Err((id, error)) => json!({
380 "id": id,
381 "error": error
382 }),
383 },
384 Err(e) => {
385 json!({
387 "id": Value::Null,
388 "error": format!("invalid JSON: {}", e)
389 })
390 }
391 };
392
393 writeln!(stdout, "{response}")?;
395 stdout.flush()?;
396 }
397
398 Ok(())
399}
400
401impl Clone for RpcRequest {
403 fn clone(&self) -> Self {
404 RpcRequest {
405 id: self.id.clone(),
406 method: self.method.clone(),
407 params: self.params.clone(),
408 }
409 }
410}