oxedyne/fe2o3/fe2o3_austenite/src/bin/pearl_demo_gen.rs
5.1 KiB, 1 run
created by r1870400018:38584, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! `pearl_demo_gen` -- THROWAWAY generator for the web reader's link + annotation demo sample. |
| 2 | //! |
| 3 | //! No author UI exists yet, so this exercises the create -> store -> read -> render path by hand: it |
| 4 | //! builds a small two-page document that carries an external `#link`, an internal `@ref` (resolving on |
| 5 | //! page two through the shipped ledger), then attaches one `highlight` and one `note` annotation to the |
| 6 | //! page block hashes with the public [`PearlDoc::add_annotation`] API, and writes the result to |
| 7 | //! `web/pearl-reader/samples/demo.prl`. It is a demo fixture builder, not part of the engine; delete |
| 8 | //! once a real annotation authoring path lands. |
| 9 | //! |
| 10 | //! Usage: `pearl_demo_gen <OUTPUT.prl>`. |
| 11 | |
| 12 | use std::sync::Arc; |
| 13 | |
| 14 | use oxedyne_fe2o3_austenite::emit::pearl::{ |
| 15 | Annotation, |
| 16 | AnnotationKind, |
| 17 | PearlBuilder, |
| 18 | PearlDoc, |
| 19 | }; |
| 20 | use oxedyne_fe2o3_austenite::font::ShapedText; |
| 21 | use oxedyne_fe2o3_austenite::fonts; |
| 22 | use oxedyne_fe2o3_austenite::ir::{ |
| 23 | Dims, |
| 24 | DrawOp, |
| 25 | Graphic, |
| 26 | LinkTarget, |
| 27 | Sp, |
| 28 | }; |
| 29 | use oxedyne_fe2o3_austenite::ledger::{ |
| 30 | Anchor, |
| 31 | AnchorId, |
| 32 | AnchorKind, |
| 33 | Ledger, |
| 34 | Position, |
| 35 | }; |
| 36 | use oxedyne_fe2o3_austenite::page::{ |
| 37 | Frame, |
| 38 | Page, |
| 39 | PageGeometry, |
| 40 | Placed, |
| 41 | PlacedKind, |
| 42 | }; |
| 43 | |
| 44 | use oxedyne_fe2o3_core::prelude::*; |
| 45 | use oxedyne_fe2o3_font::{ |
| 46 | face::Role, |
| 47 | shape::Dir, |
| 48 | }; |
| 49 | use oxedyne_fe2o3_graphics::{ |
| 50 | colour::Rgba, |
| 51 | path::{ |
| 52 | Bounds, |
| 53 | Path, |
| 54 | }, |
| 55 | }; |
| 56 | |
| 57 | fn main() -> Outcome<()> { |
| 58 | let out = match std::env::args().nth(1) { |
| 59 | Some(s) => s, |
| 60 | None => return Err(err!( |
| 61 | "Usage: pearl_demo_gen <OUTPUT.prl>"; Input, Invalid, Missing)), |
| 62 | }; |
| 63 | |
| 64 | let fonts = Arc::new(res!(fonts::libertinus())); |
| 65 | let geom = PageGeometry::a4(); |
| 66 | let anchor = AnchorId::new(AnchorKind::Label, "sec:page-two"); |
| 67 | |
| 68 | // A helper placing a shaped line of body text at (x, y) points from the page's top-left, returning the |
| 69 | // dimensions so the caller can line up an annotation rectangle over it. |
| 70 | let text = |frame: &mut Frame, x: f64, y: f64, size: f64, s: &str| -> Outcome<Dims> { |
| 71 | let shaped = res!(ShapedText::new(fonts.clone(), Role::Body, Dir::Ltr, Sp::from_pt(size), s)); |
| 72 | let dims = shaped.dims(); |
| 73 | frame.push(Placed::new(Sp::from_pt(x), Sp::from_pt(y), dims, PlacedKind::Text(shaped))); |
| 74 | Ok(dims) |
| 75 | }; |
| 76 | |
| 77 | // A small filled box acting as a linked figure: the `link` leaf covers its placement box, so the reader |
| 78 | // lays a clickable hotspot there. |
| 79 | let link_box = |target: LinkTarget, w: f64, h: f64| -> Outcome<Graphic> { |
| 80 | let fill = res!(Path::rect(Bounds::new(0.0, 0.0, w as f32, h as f32))); |
| 81 | let mut g = Graphic::new( |
| 82 | vec![DrawOp::Fill { path: fill, colour: Rgba::new(219, 234, 254, 255) }], |
| 83 | Dims::new(Sp::from_pt(w), Sp::from_pt(h), Sp::ZERO)); |
| 84 | g.link = Some(target); |
| 85 | Ok(g) |
| 86 | }; |
| 87 | |
| 88 | // Page one: a title, a highlighted body line, and two linked boxes -- one external, one internal. |
| 89 | let mut f1 = Frame::new(); |
| 90 | res!(text(&mut f1, 72.0, 90.0, 20.0, "The Pearl Format")); |
| 91 | let hl = res!(text(&mut f1, 72.0, 140.0, 12.0, |
| 92 | "This line of body text carries a highlight annotation.")); |
| 93 | res!(text(&mut f1, 72.0, 190.0, 12.0, "External link (opens in a new tab):")); |
| 94 | let ext = res!(link_box(LinkTarget::Uri("https://oxedyne.com".to_string()), 150.0, 16.0)); |
| 95 | f1.push(Placed::new(Sp::from_pt(300.0), Sp::from_pt(180.0), ext.dims, PlacedKind::Graphic(Arc::new(ext)))); |
| 96 | res!(text(&mut f1, 72.0, 230.0, 12.0, "Internal link (jumps to page two):")); |
| 97 | let int = res!(link_box(LinkTarget::Anchor(anchor.clone()), 150.0, 16.0)); |
| 98 | f1.push(Placed::new(Sp::from_pt(300.0), Sp::from_pt(220.0), int.dims, PlacedKind::Graphic(Arc::new(int)))); |
| 99 | let page1 = Page::new(1, geom, f1); |
| 100 | |
| 101 | // Page two: the anchor target, and a line that carries the note annotation. |
| 102 | let mut f2 = Frame::new(); |
| 103 | res!(text(&mut f2, 72.0, 90.0, 20.0, "Page Two")); |
| 104 | res!(text(&mut f2, 72.0, 140.0, 12.0, "The internal link scrolled the reader here.")); |
| 105 | res!(text(&mut f2, 72.0, 170.0, 12.0, "This block carries a margin note annotation.")); |
| 106 | let page2 = Page::new(2, geom, f2); |
| 107 | |
| 108 | // The ledger fixes the internal anchor on page two, as a composition pass would. |
| 109 | let mut ledger = Ledger::new(); |
| 110 | ledger.record(Anchor::new(anchor.clone(), Position::new(2, Sp::ZERO, Sp::ZERO))); |
| 111 | |
| 112 | let mut builder = res!(PearlBuilder::new(&ledger, geom)); |
| 113 | res!(builder.add_page(&page1)); |
| 114 | res!(builder.add_page(&page2)); |
| 115 | let mut doc = res!(PearlDoc::from_string(res!(builder.to_string()))); |
| 116 | |
| 117 | // Attach the two annotations by block hash: a highlight over the body line on page one, a whole-block |
| 118 | // note on page two. |
| 119 | let hashes = res!(doc.block_hashes()); |
| 120 | let hl_h = hl.height.to_pt(); |
| 121 | res!(doc.add_annotation(Annotation::new( |
| 122 | hashes[0].as_str(), AnnotationKind::Highlight, |
| 123 | "The core claim of the document.", "jason", "2026-09-17T10:00:00Z") |
| 124 | .with_rect( |
| 125 | Sp::from_pt(70.0), |
| 126 | Sp::from_pt(140.0 - 2.0), |
| 127 | Sp::from_pt(330.0), |
| 128 | Sp::from_pt(hl_h + 5.0)))); |
| 129 | res!(doc.add_annotation(Annotation::new( |
| 130 | hashes[1].as_str(), AnnotationKind::Note, |
| 131 | "This is an anchored note. It reveals its text on click, and follows the block across repagination.", |
| 132 | "jason", "2026-09-17T11:00:00Z"))); |
| 133 | |
| 134 | res!(doc.write_file(&out)); |
| 135 | println!("wrote {} ({} pages, {} annotations, links on page 1)", |
| 136 | out, res!(doc.page_count()), res!(doc.annotations()).len()); |
| 137 | Ok(()) |
| 138 | } |