feat(export): DOCX renderer via docx-rs (T8.4, FR-NOTE-4)
Bullet items get a literal "• " prefix rather than a real Word numbered-list definition — this is read-only generated content, not something the user continues typing into, so numbering-continuity machinery isn't worth the extra API surface.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
//! Word (.docx) export (Phase 8, FR-NOTE-4) via docx-rs — no external
|
||||
//! binary/cloud conversion, consistent with the fully-local invariant.
|
||||
|
||||
use super::{Block, NotesError, Span};
|
||||
use docx_rs::{Docx, Paragraph, Run};
|
||||
use std::path::Path;
|
||||
|
||||
pub(super) fn render(blocks: &[Block], dest: &Path) -> Result<(), NotesError> {
|
||||
let mut docx = Docx::new();
|
||||
for block in blocks {
|
||||
docx = docx.add_paragraph(to_paragraph(block));
|
||||
}
|
||||
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(NotesError::Io)?;
|
||||
}
|
||||
let file = std::fs::File::create(dest).map_err(NotesError::Io)?;
|
||||
docx.build()
|
||||
.pack(file)
|
||||
.map_err(|e| NotesError::Export(format!("docx save: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_paragraph(block: &Block) -> Paragraph {
|
||||
match block {
|
||||
Block::Heading(level, spans) => {
|
||||
let size = match level {
|
||||
1 => 32,
|
||||
2 => 28,
|
||||
_ => 24,
|
||||
};
|
||||
let mut p = Paragraph::new();
|
||||
for s in spans {
|
||||
p = p.add_run(Run::new().add_text(s.text.as_str()).bold().size(size));
|
||||
}
|
||||
p
|
||||
}
|
||||
Block::Paragraph(spans) => spans_to_paragraph(spans, ""),
|
||||
// ponytail: a literal "• " prefix, not a real Word numbered-list
|
||||
// definition (AbstractNum/Num) — this is read-only generated
|
||||
// content, not something the user continues typing into, so
|
||||
// auto-numbering continuity isn't worth the extra API surface.
|
||||
Block::BulletItem(spans) => spans_to_paragraph(spans, "\u{2022} "),
|
||||
}
|
||||
}
|
||||
|
||||
fn spans_to_paragraph(spans: &[Span], prefix: &str) -> Paragraph {
|
||||
let mut p = Paragraph::new();
|
||||
if !prefix.is_empty() {
|
||||
p = p.add_run(Run::new().add_text(prefix));
|
||||
}
|
||||
for s in spans {
|
||||
let mut run = Run::new().add_text(s.text.as_str());
|
||||
if s.bold {
|
||||
run = run.bold();
|
||||
}
|
||||
p = p.add_run(run);
|
||||
}
|
||||
p
|
||||
}
|
||||
Reference in New Issue
Block a user