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:
iamdoubz
2026-07-02 11:02:46 -05:00
parent ec7ae5c01f
commit 0425f64cfe
+60
View File
@@ -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
}