Skip to main content

text_document_io/
document_io_controller.rs

1// Generated by Qleany v1.5.1 from feature_controller.tera
2
3use crate::ExportDjotDto;
4use crate::ExportDocxDto;
5use crate::ExportDocxResultDto;
6use crate::ExportEpubDto;
7use crate::ExportEpubResultDto;
8use crate::ExportHtmlDto;
9use crate::ExportLatexDto;
10use crate::ExportLatexResultDto;
11use crate::ExportMarkdownDto;
12use crate::ExportPdfDto;
13use crate::ExportPdfResultDto;
14use crate::ExportPlainTextDto;
15use crate::ImportDjotDto;
16use crate::ImportDjotResultDto;
17use crate::ImportHtmlDto;
18use crate::ImportHtmlResultDto;
19use crate::ImportMarkdownDto;
20use crate::ImportMarkdownResultDto;
21use crate::ImportPlainTextDto;
22use crate::units_of_work::export_djot_uow::ExportDjotUnitOfWorkFactory;
23use crate::units_of_work::export_docx_uow::ExportDocxUnitOfWorkFactory;
24use crate::units_of_work::export_epub_uow::ExportEpubUnitOfWorkFactory;
25use crate::units_of_work::export_html_uow::ExportHtmlUnitOfWorkFactory;
26use crate::units_of_work::export_latex_uow::ExportLatexUnitOfWorkFactory;
27use crate::units_of_work::export_markdown_uow::ExportMarkdownUnitOfWorkFactory;
28#[cfg(feature = "pdf")]
29use crate::units_of_work::export_pdf_uow::ExportPdfUnitOfWorkFactory;
30use crate::units_of_work::export_plain_text_uow::ExportPlainTextUnitOfWorkFactory;
31use crate::units_of_work::import_djot_uow::ImportDjotUnitOfWorkFactory;
32use crate::units_of_work::import_html_uow::ImportHtmlUnitOfWorkFactory;
33use crate::units_of_work::import_markdown_uow::ImportMarkdownUnitOfWorkFactory;
34use crate::units_of_work::import_plain_text_uow::ImportPlainTextUnitOfWorkFactory;
35use crate::use_cases::export_djot_uc::ExportDjotUseCase;
36use crate::use_cases::export_docx_uc::ExportDocxUseCase;
37use crate::use_cases::export_epub_uc::ExportEpubUseCase;
38use crate::use_cases::export_html_uc::ExportHtmlUseCase;
39use crate::use_cases::export_latex_uc::ExportLatexUseCase;
40use crate::use_cases::export_markdown_uc::ExportMarkdownUseCase;
41#[cfg(feature = "pdf")]
42use crate::use_cases::export_pdf_uc::ExportPdfUseCase;
43use crate::use_cases::export_plain_text_uc::ExportPlainTextUseCase;
44use crate::use_cases::import_djot_uc::ImportDjotUseCase;
45use crate::use_cases::import_html_uc::ImportHtmlUseCase;
46use crate::use_cases::import_markdown_uc::ImportMarkdownUseCase;
47use crate::use_cases::import_plain_text_uc::ImportPlainTextUseCase;
48use anyhow::Result;
49use common::event::{Event, Origin};
50use common::parser_tools::DjotExportOptions;
51
52use common::event::DocumentIoEvent::ExportDjot;
53use common::event::DocumentIoEvent::ExportHtml;
54use common::event::DocumentIoEvent::ExportLatex;
55use common::event::DocumentIoEvent::ExportMarkdown;
56use common::event::DocumentIoEvent::ExportPlainText;
57use common::event::DocumentIoEvent::ImportDjot;
58use common::event::DocumentIoEvent::ImportPlainText;
59
60use common::long_operation::{LongOperation, LongOperationManager, OperationProgress};
61use common::{database::db_context::DbContext, event::EventHub};
62use std::sync::Arc;
63
64pub fn import_plain_text(
65    db_context: &DbContext,
66    event_hub: &Arc<EventHub>,
67    dto: &ImportPlainTextDto,
68) -> Result<()> {
69    let uow_context = ImportPlainTextUnitOfWorkFactory::new(db_context, event_hub);
70    let mut uc = ImportPlainTextUseCase::new(Box::new(uow_context));
71    uc.execute(dto)?;
72    // Notify that the handling manifest has been loaded
73    event_hub.send_event(Event {
74        origin: Origin::DocumentIo(ImportPlainText),
75        ids: vec![],
76        data: None,
77    });
78    Ok(())
79}
80
81pub fn export_plain_text(
82    db_context: &DbContext,
83    event_hub: &Arc<EventHub>,
84) -> Result<ExportPlainTextDto> {
85    export_plain_text_with(
86        db_context,
87        event_hub,
88        common::parser_tools::PlainTextExportOptions::addressable(),
89    )
90}
91
92/// [`export_plain_text`], with the presentation options a written-out `.txt` file wants.
93///
94/// Separate entry point rather than flags on the default one, because the default one's
95/// output is load-bearing: it is pinned to the document's addressable text, so anything
96/// that indents or inserts would move every search offset after it.
97pub fn export_plain_text_with(
98    db_context: &DbContext,
99    event_hub: &Arc<EventHub>,
100    options: common::parser_tools::PlainTextExportOptions,
101) -> Result<ExportPlainTextDto> {
102    let uow_context = ExportPlainTextUnitOfWorkFactory::new(db_context);
103    let mut uc = ExportPlainTextUseCase::new(Box::new(uow_context));
104    let return_dto = uc.execute(options)?;
105    // Notify that the handling manifest has been loaded
106    event_hub.send_event(Event {
107        origin: Origin::DocumentIo(ExportPlainText),
108        ids: vec![],
109        data: None,
110    });
111    Ok(return_dto)
112}
113
114pub fn import_markdown(
115    db_context: &DbContext,
116    event_hub: &Arc<EventHub>,
117    long_operation_manager: &mut LongOperationManager,
118    dto: &ImportMarkdownDto,
119) -> Result<String> {
120    let uow_context = ImportMarkdownUnitOfWorkFactory::new(db_context, event_hub);
121    let uc = ImportMarkdownUseCase::new(Box::new(uow_context), dto);
122    let operation_id = long_operation_manager.start_operation(uc);
123    Ok(operation_id)
124}
125
126pub fn get_import_markdown_progress(
127    long_operation_manager: &LongOperationManager,
128    operation_id: &str,
129) -> Option<OperationProgress> {
130    long_operation_manager.get_operation_progress(operation_id)
131}
132
133pub fn get_import_markdown_result(
134    long_operation_manager: &LongOperationManager,
135    operation_id: &str,
136) -> Result<Option<ImportMarkdownResultDto>> {
137    // Get the operation result as a JSON string
138    let result_json = long_operation_manager.get_operation_result(operation_id);
139
140    // If there's no result, return None
141    if result_json.is_none() {
142        return Ok(None);
143    }
144    // Parse the JSON string into a ImportMarkdownResultDto
145    let result_dto: ImportMarkdownResultDto = serde_json::from_str(&result_json.unwrap())?;
146
147    Ok(Some(result_dto))
148}
149
150pub fn export_markdown(
151    db_context: &DbContext,
152    event_hub: &Arc<EventHub>,
153) -> Result<ExportMarkdownDto> {
154    export_markdown_with(
155        db_context,
156        event_hub,
157        common::parser_tools::MarkdownExportOptions::default(),
158    )
159}
160
161/// [`export_markdown`] with the presentation opt-ins. Separate for the same reason as
162/// its plain-text sibling: the default output stays plain Markdown, with no raw HTML in
163/// it that nobody asked for.
164pub fn export_markdown_with(
165    db_context: &DbContext,
166    event_hub: &Arc<EventHub>,
167    options: common::parser_tools::MarkdownExportOptions,
168) -> Result<ExportMarkdownDto> {
169    let uow_context = ExportMarkdownUnitOfWorkFactory::new(db_context);
170    let mut uc = ExportMarkdownUseCase::new(Box::new(uow_context), options);
171    let return_dto = uc.execute()?;
172    // Notify that the handling manifest has been loaded
173    event_hub.send_event(Event {
174        origin: Origin::DocumentIo(ExportMarkdown),
175        ids: vec![],
176        data: None,
177    });
178    Ok(return_dto)
179}
180
181pub fn import_djot(
182    db_context: &DbContext,
183    event_hub: &Arc<EventHub>,
184    long_operation_manager: &mut LongOperationManager,
185    dto: &ImportDjotDto,
186) -> Result<String> {
187    let uow_context = ImportDjotUnitOfWorkFactory::new(db_context, event_hub);
188    let uc = ImportDjotUseCase::new(Box::new(uow_context), dto);
189    let operation_id = long_operation_manager.start_operation(uc);
190    Ok(operation_id)
191}
192
193/// Import Djot **synchronously**, on the calling thread.
194///
195/// [`import_djot`] hands the use case to the `LongOperationManager`, which spawns a
196/// thread for it. That is right for a UI import (it must not block the frame loop),
197/// and wrong for a *batch* caller — a backend that has to parse and rewrite hundreds
198/// of documents in one operation would spawn hundreds of threads to do work it wants
199/// to do inline anyway.
200///
201/// The use case itself is thread-agnostic: it only needs a progress sink and a cancel
202/// flag. So this hands it a no-op sink and a never-set flag and calls it directly. No
203/// thread, no operation id, no polling — the result is simply returned.
204///
205/// (Cancellation is the caller's business here: a synchronous caller can just stop
206/// calling. The flag exists to satisfy the `LongOperation` signature, not to be used.)
207pub fn import_djot_sync(
208    db_context: &DbContext,
209    event_hub: &Arc<EventHub>,
210    dto: &ImportDjotDto,
211) -> Result<ImportDjotResultDto> {
212    let uow_context = ImportDjotUnitOfWorkFactory::new(db_context, event_hub);
213    let uc = ImportDjotUseCase::new(Box::new(uow_context), dto);
214    let result = uc.execute(
215        Box::new(|_progress| {}),
216        Arc::new(std::sync::atomic::AtomicBool::new(false)),
217    )?;
218    event_hub.send_event(Event {
219        origin: Origin::DocumentIo(ImportDjot),
220        ids: vec![],
221        data: None,
222    });
223    Ok(result)
224}
225
226pub fn get_import_djot_progress(
227    long_operation_manager: &LongOperationManager,
228    operation_id: &str,
229) -> Option<OperationProgress> {
230    long_operation_manager.get_operation_progress(operation_id)
231}
232
233pub fn get_import_djot_result(
234    long_operation_manager: &LongOperationManager,
235    operation_id: &str,
236) -> Result<Option<ImportDjotResultDto>> {
237    let result_json = long_operation_manager.get_operation_result(operation_id);
238    if result_json.is_none() {
239        return Ok(None);
240    }
241    let result_dto: ImportDjotResultDto = serde_json::from_str(&result_json.unwrap())?;
242    Ok(Some(result_dto))
243}
244
245pub fn export_djot(
246    db_context: &DbContext,
247    event_hub: &Arc<EventHub>,
248    options: &DjotExportOptions,
249) -> Result<ExportDjotDto> {
250    let uow_context = ExportDjotUnitOfWorkFactory::new(db_context);
251    let mut uc = ExportDjotUseCase::new(Box::new(uow_context));
252    let return_dto = uc.execute(options)?;
253    event_hub.send_event(Event {
254        origin: Origin::DocumentIo(ExportDjot),
255        ids: vec![],
256        data: None,
257    });
258    Ok(return_dto)
259}
260
261pub fn import_html(
262    db_context: &DbContext,
263    event_hub: &Arc<EventHub>,
264    long_operation_manager: &mut LongOperationManager,
265    dto: &ImportHtmlDto,
266) -> Result<String> {
267    let uow_context = ImportHtmlUnitOfWorkFactory::new(db_context, event_hub);
268    let uc = ImportHtmlUseCase::new(Box::new(uow_context), dto);
269    let operation_id = long_operation_manager.start_operation(uc);
270    Ok(operation_id)
271}
272
273pub fn get_import_html_progress(
274    long_operation_manager: &LongOperationManager,
275    operation_id: &str,
276) -> Option<OperationProgress> {
277    long_operation_manager.get_operation_progress(operation_id)
278}
279
280pub fn get_import_html_result(
281    long_operation_manager: &LongOperationManager,
282    operation_id: &str,
283) -> Result<Option<ImportHtmlResultDto>> {
284    // Get the operation result as a JSON string
285    let result_json = long_operation_manager.get_operation_result(operation_id);
286
287    // If there's no result, return None
288    if result_json.is_none() {
289        return Ok(None);
290    }
291    // Parse the JSON string into a ImportHtmlResultDto
292    let result_dto: ImportHtmlResultDto = serde_json::from_str(&result_json.unwrap())?;
293
294    Ok(Some(result_dto))
295}
296
297pub fn export_html(db_context: &DbContext, event_hub: &Arc<EventHub>) -> Result<ExportHtmlDto> {
298    export_html_with_options(db_context, event_hub, Default::default())
299}
300
301/// HTML export with an explicit image policy (reference / data URI / omit).
302pub fn export_html_with_options(
303    db_context: &DbContext,
304    event_hub: &Arc<EventHub>,
305    options: common::parser_tools::HtmlExportOptions,
306) -> Result<ExportHtmlDto> {
307    let uow_context = ExportHtmlUnitOfWorkFactory::new(db_context);
308    let mut uc = ExportHtmlUseCase::new(Box::new(uow_context), options);
309    let return_dto = uc.execute()?;
310    // Notify that the handling manifest has been loaded
311    event_hub.send_event(Event {
312        origin: Origin::DocumentIo(ExportHtml),
313        ids: vec![],
314        data: None,
315    });
316    Ok(return_dto)
317}
318
319pub fn export_latex(
320    db_context: &DbContext,
321    event_hub: &Arc<EventHub>,
322    dto: &ExportLatexDto,
323) -> Result<ExportLatexResultDto> {
324    let uow_context = ExportLatexUnitOfWorkFactory::new(db_context);
325    let mut uc = ExportLatexUseCase::new(Box::new(uow_context));
326    let return_dto = uc.execute(dto)?;
327    // Notify that the handling manifest has been loaded
328    event_hub.send_event(Event {
329        origin: Origin::DocumentIo(ExportLatex),
330        ids: vec![],
331        data: None,
332    });
333    Ok(return_dto)
334}
335
336pub fn export_docx(
337    db_context: &DbContext,
338    _event_hub: &Arc<EventHub>,
339    long_operation_manager: &mut LongOperationManager,
340    dto: &ExportDocxDto,
341) -> Result<String> {
342    let uow_context = ExportDocxUnitOfWorkFactory::new(db_context);
343    let uc = ExportDocxUseCase::new(Box::new(uow_context), dto);
344    let operation_id = long_operation_manager.start_operation(uc);
345    Ok(operation_id)
346}
347
348/// Build the in-memory DOCX document for `db_context` without writing a file.
349///
350/// This runs the exact same builder used by [`export_docx`] but returns the
351/// assembled [`docx_rs::Docx`] instead of packing it to disk, so callers can
352/// inspect the produced structure. Intended for tests; the type is re-exported
353/// as [`crate::docx_rs`] so callers can name it.
354#[doc(hidden)]
355pub fn build_docx_document(db_context: &DbContext, dto: &ExportDocxDto) -> Result<docx_rs::Docx> {
356    let uow_context = ExportDocxUnitOfWorkFactory::new(db_context);
357    let uc = ExportDocxUseCase::new(Box::new(uow_context), dto);
358    let (docx, _paragraph_count) = uc.build_document()?;
359    Ok(docx)
360}
361
362pub fn get_export_docx_progress(
363    long_operation_manager: &LongOperationManager,
364    operation_id: &str,
365) -> Option<OperationProgress> {
366    long_operation_manager.get_operation_progress(operation_id)
367}
368
369pub fn get_export_docx_result(
370    long_operation_manager: &LongOperationManager,
371    operation_id: &str,
372) -> Result<Option<ExportDocxResultDto>> {
373    // Get the operation result as a JSON string
374    let result_json = long_operation_manager.get_operation_result(operation_id);
375
376    // If there's no result, return None
377    if result_json.is_none() {
378        return Ok(None);
379    }
380    // Parse the JSON string into a ExportDocxResultDto
381    let result_dto: ExportDocxResultDto = serde_json::from_str(&result_json.unwrap())?;
382
383    Ok(Some(result_dto))
384}
385
386pub fn export_epub(
387    db_context: &DbContext,
388    _event_hub: &Arc<EventHub>,
389    long_operation_manager: &mut LongOperationManager,
390    dto: &ExportEpubDto,
391) -> Result<String> {
392    let uow_context = ExportEpubUnitOfWorkFactory::new(db_context);
393    let uc = ExportEpubUseCase::new(Box::new(uow_context), dto);
394    let operation_id = long_operation_manager.start_operation(uc);
395    Ok(operation_id)
396}
397
398/// Build the packaged EPUB bytes for `db_context` without writing a file.
399///
400/// This runs the exact same builder used by [`export_epub`] but returns the assembled `.epub`
401/// zip archive as bytes instead of writing it to disk, so callers (tests, notably) can inspect
402/// the produced package directly — e.g. with the `zip` crate.
403#[doc(hidden)]
404pub fn build_epub_document(db_context: &DbContext, dto: &ExportEpubDto) -> Result<Vec<u8>> {
405    let uow_context = ExportEpubUnitOfWorkFactory::new(db_context);
406    let uc = ExportEpubUseCase::new(Box::new(uow_context), dto);
407    let (epub_bytes, _chapter_count) = uc.build_document()?;
408    Ok(epub_bytes)
409}
410
411pub fn get_export_epub_progress(
412    long_operation_manager: &LongOperationManager,
413    operation_id: &str,
414) -> Option<OperationProgress> {
415    long_operation_manager.get_operation_progress(operation_id)
416}
417
418pub fn get_export_epub_result(
419    long_operation_manager: &LongOperationManager,
420    operation_id: &str,
421) -> Result<Option<ExportEpubResultDto>> {
422    // Get the operation result as a JSON string
423    let result_json = long_operation_manager.get_operation_result(operation_id);
424
425    // If there's no result, return None
426    if result_json.is_none() {
427        return Ok(None);
428    }
429    // Parse the JSON string into a ExportEpubResultDto
430    let result_dto: ExportEpubResultDto = serde_json::from_str(&result_json.unwrap())?;
431
432    Ok(Some(result_dto))
433}
434
435// ─── PDF export (via embedded Typst) — dual-cfg ────────────────────────────
436//
437// `export_pdf`/`get_export_pdf_progress`/`get_export_pdf_result` exist with the SAME signature
438// regardless of whether the `pdf` cargo feature is enabled on `text-document-io`. This is what
439// lets `frontend`'s command wrappers and `public_api`'s `to_pdf`/`to_pdf_with_options` call these
440// symbols unconditionally (no `#[cfg]` in that consumer code) — the feature only decides which
441// body is compiled: a real `LongOperation`-backed export, or an immediate "unsupported" error.
442// Cargo feature unification means any crate in the build graph enabling `pdf` turns it on
443// globally for that build, so a stable always-present API surface that fails at *runtime* is
444// strictly better here than two divergent source trees.
445
446#[cfg(feature = "pdf")]
447pub fn export_pdf(
448    db_context: &DbContext,
449    _event_hub: &Arc<EventHub>,
450    long_operation_manager: &mut LongOperationManager,
451    dto: &ExportPdfDto,
452) -> Result<String> {
453    let uow_context = ExportPdfUnitOfWorkFactory::new(db_context);
454    let uc = ExportPdfUseCase::new(Box::new(uow_context), dto);
455    let operation_id = long_operation_manager.start_operation(uc);
456    Ok(operation_id)
457}
458
459#[cfg(not(feature = "pdf"))]
460pub fn export_pdf(
461    _db_context: &DbContext,
462    _event_hub: &Arc<EventHub>,
463    _long_operation_manager: &mut LongOperationManager,
464    _dto: &ExportPdfDto,
465) -> Result<String> {
466    Err(anyhow::anyhow!(
467        "PDF export requires the `pdf` cargo feature on the `text-document-io` crate"
468    ))
469}
470
471/// Build the PDF bytes for `db_context` without writing a file.
472///
473/// This runs the exact same builder used by [`export_pdf`] but returns the compiled PDF bytes
474/// instead of writing them to disk, so callers (tests, notably) can inspect the produced document
475/// directly.
476#[cfg(feature = "pdf")]
477#[doc(hidden)]
478pub fn build_pdf_document(db_context: &DbContext, dto: &ExportPdfDto) -> Result<Vec<u8>> {
479    let uow_context = ExportPdfUnitOfWorkFactory::new(db_context);
480    let uc = ExportPdfUseCase::new(Box::new(uow_context), dto);
481    let (pdf_bytes, _page_count) = uc.build_document()?;
482    Ok(pdf_bytes)
483}
484
485#[cfg(feature = "pdf")]
486pub fn get_export_pdf_progress(
487    long_operation_manager: &LongOperationManager,
488    operation_id: &str,
489) -> Option<OperationProgress> {
490    long_operation_manager.get_operation_progress(operation_id)
491}
492
493#[cfg(not(feature = "pdf"))]
494pub fn get_export_pdf_progress(
495    _long_operation_manager: &LongOperationManager,
496    _operation_id: &str,
497) -> Option<OperationProgress> {
498    None
499}
500
501#[cfg(feature = "pdf")]
502pub fn get_export_pdf_result(
503    long_operation_manager: &LongOperationManager,
504    operation_id: &str,
505) -> Result<Option<ExportPdfResultDto>> {
506    // Get the operation result as a JSON string
507    let result_json = long_operation_manager.get_operation_result(operation_id);
508
509    // If there's no result, return None
510    if result_json.is_none() {
511        return Ok(None);
512    }
513    // Parse the JSON string into a ExportPdfResultDto
514    let result_dto: ExportPdfResultDto = serde_json::from_str(&result_json.unwrap())?;
515
516    Ok(Some(result_dto))
517}
518
519#[cfg(not(feature = "pdf"))]
520pub fn get_export_pdf_result(
521    _long_operation_manager: &LongOperationManager,
522    _operation_id: &str,
523) -> Result<Option<ExportPdfResultDto>> {
524    Ok(None)
525}