use aozora_encoding::gaiji::Resolved;
use crate::borrowed::{self, Arena, Interner};
use crate::{
AlignEnd, AnnotationKind, AozoraHeadingKind, BoutenKind, BoutenPosition, Container, Indent,
Keigakomi, SectionKind,
};
#[derive(Debug)]
pub struct BorrowedAllocator<'a> {
arena: &'a Arena,
interner: Interner<'a>,
}
#[allow(
clippy::unused_self,
reason = "API consistency: every BorrowedAllocator builder method takes &mut self even when the variant is a pure wrapper, so call sites have a uniform shape (alloc.method(...) for every variant). Switching trivial wrappers to free fns would split the API in half."
)]
impl<'a> BorrowedAllocator<'a> {
#[must_use]
pub fn with_capacity(arena: &'a Arena, interner_capacity: usize) -> Self {
Self {
arena,
interner: Interner::with_capacity_in(interner_capacity, arena),
}
}
#[must_use]
pub fn new(arena: &'a Arena) -> Self {
Self::with_capacity(arena, 64)
}
#[must_use]
pub fn arena(&self) -> &'a Arena {
self.arena
}
#[must_use]
pub fn into_interner(self) -> Interner<'a> {
self.interner
}
pub fn content_plain(&mut self, s: &str) -> borrowed::Content<'a> {
if s.is_empty() {
borrowed::Content::EMPTY
} else {
borrowed::Content::Plain(self.interner.intern(s))
}
}
pub fn content_segments(&mut self, segs: &[borrowed::Segment<'a>]) -> borrowed::Content<'a> {
if segs.is_empty() {
return borrowed::Content::EMPTY;
}
if segs.iter().all(|s| matches!(s, borrowed::Segment::Text(_))) {
let total: usize = segs
.iter()
.map(|s| match s {
borrowed::Segment::Text(t) => t.len(),
_ => 0,
})
.sum();
let mut buf = String::with_capacity(total);
for s in segs {
if let borrowed::Segment::Text(t) = s {
buf.push_str(t);
}
}
return borrowed::Content::Plain(self.interner.intern(&buf));
}
borrowed::Content::Segments(self.arena.alloc_slice_copy(segs))
}
pub fn seg_text(&mut self, s: &str) -> borrowed::Segment<'a> {
borrowed::Segment::Text(self.interner.intern(s))
}
#[must_use]
pub fn seg_gaiji(&self, g: &'a borrowed::Gaiji<'a>) -> borrowed::Segment<'a> {
borrowed::Segment::Gaiji(g)
}
#[must_use]
pub fn seg_annotation(&self, a: &'a borrowed::Annotation<'a>) -> borrowed::Segment<'a> {
borrowed::Segment::Annotation(a)
}
pub fn make_gaiji(
&mut self,
description: &str,
ucs: Option<Resolved>,
mencode: Option<&str>,
) -> &'a borrowed::Gaiji<'a> {
let g = borrowed::Gaiji {
description: self.interner.intern(description),
ucs,
mencode: mencode.map(|s| self.interner.intern(s)),
};
self.arena.alloc(g)
}
pub fn make_annotation(
&mut self,
raw: &str,
kind: AnnotationKind,
) -> &'a borrowed::Annotation<'a> {
let raw = borrowed::NonEmptyStr::new(self.interner.intern(raw))
.expect("Phase 3 must emit Annotation with non-empty raw bytes");
let a = borrowed::Annotation { raw, kind };
self.arena.alloc(a)
}
#[must_use]
pub fn ruby(
&self,
base: borrowed::Content<'a>,
reading: borrowed::Content<'a>,
delim_explicit: bool,
) -> borrowed::AozoraNode<'a> {
let base =
borrowed::NonEmpty::new(base).expect("Phase 3 must emit Ruby with non-empty base");
let reading = borrowed::NonEmpty::new(reading)
.expect("Phase 3 must emit Ruby with non-empty reading");
borrowed::AozoraNode::Ruby(self.arena.alloc(borrowed::Ruby {
base,
reading,
delim_explicit,
}))
}
#[must_use]
#[allow(
clippy::too_many_arguments,
reason = "every parameter is part of the public bouten contract — kind / target / position / consumed_predecessor each carry independent semantics and grouping them into a builder would add a layer without saving the caller anything"
)]
pub fn bouten(
&self,
kind: BoutenKind,
target: borrowed::Content<'a>,
position: BoutenPosition,
consumed_predecessor: bool,
) -> borrowed::AozoraNode<'a> {
let target = borrowed::NonEmpty::new(target)
.expect("Phase 3 must emit Bouten with a resolved non-empty target");
borrowed::AozoraNode::Bouten(self.arena.alloc(borrowed::Bouten {
kind,
target,
position,
consumed_predecessor,
}))
}
#[must_use]
pub fn tate_chu_yoko(
&self,
text: borrowed::Content<'a>,
consumed_predecessor: bool,
) -> borrowed::AozoraNode<'a> {
let text = borrowed::NonEmpty::new(text)
.expect("Phase 3 must emit TateChuYoko with non-empty text");
borrowed::AozoraNode::TateChuYoko(self.arena.alloc(borrowed::TateChuYoko {
text,
consumed_predecessor,
}))
}
#[must_use]
pub fn gaiji(&self, g: &'a borrowed::Gaiji<'a>) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::Gaiji(g)
}
#[must_use]
pub fn indent(&self, i: Indent) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::Indent(i)
}
#[must_use]
pub fn align_end(&self, a: AlignEnd) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::AlignEnd(a)
}
#[must_use]
pub fn warichu(
&self,
upper: borrowed::Content<'a>,
lower: borrowed::Content<'a>,
) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::Warichu(self.arena.alloc(borrowed::Warichu { upper, lower }))
}
#[must_use]
pub fn keigakomi(&self, k: Keigakomi) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::Keigakomi(k)
}
#[must_use]
pub fn page_break(&self) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::PageBreak
}
#[must_use]
pub fn section_break(&self, k: SectionKind) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::SectionBreak(k)
}
#[must_use]
pub fn aozora_heading(
&self,
kind: AozoraHeadingKind,
text: borrowed::Content<'a>,
) -> borrowed::AozoraNode<'a> {
let text = borrowed::NonEmpty::new(text)
.expect("Phase 3 must emit AozoraHeading with non-empty text");
borrowed::AozoraNode::AozoraHeading(
self.arena.alloc(borrowed::AozoraHeading { kind, text }),
)
}
pub fn heading_hint(&mut self, level: u8, target: &str) -> borrowed::AozoraNode<'a> {
let target = borrowed::NonEmptyStr::new(self.interner.intern(target))
.expect("Phase 3 must emit HeadingHint with non-empty target");
borrowed::AozoraNode::HeadingHint(self.arena.alloc(borrowed::HeadingHint { level, target }))
}
pub fn sashie(
&mut self,
file: &str,
caption: Option<borrowed::Content<'a>>,
) -> borrowed::AozoraNode<'a> {
let file = borrowed::NonEmptyStr::new(self.interner.intern(file))
.expect("Phase 3 must emit Sashie with non-empty file path");
borrowed::AozoraNode::Sashie(self.arena.alloc(borrowed::Sashie { file, caption }))
}
pub fn kaeriten(&mut self, mark: &str) -> borrowed::AozoraNode<'a> {
let mark = borrowed::NonEmptyStr::new(self.interner.intern(mark))
.expect("Phase 3 must emit Kaeriten with non-empty mark");
borrowed::AozoraNode::Kaeriten(self.arena.alloc(borrowed::Kaeriten { mark }))
}
#[must_use]
pub fn annotation(&self, a: &'a borrowed::Annotation<'a>) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::Annotation(a)
}
#[must_use]
pub fn double_ruby(&self, content: borrowed::Content<'a>) -> borrowed::AozoraNode<'a> {
let content = borrowed::NonEmpty::new(content)
.expect("Phase 3 pre-filters empty DoubleRuby into plain");
borrowed::AozoraNode::DoubleRuby(self.arena.alloc(borrowed::DoubleRuby { content }))
}
#[must_use]
pub fn container(&self, c: Container) -> borrowed::AozoraNode<'a> {
borrowed::AozoraNode::Container(c)
}
}
#[cfg(test)]
mod tests {
use core::ptr;
use super::*;
use crate::borrowed;
use crate::{
AlignEnd, AnnotationKind, AozoraHeadingKind, BoutenKind, BoutenPosition, Container,
ContainerKind, Indent, Keigakomi, SectionKind,
};
fn fresh_alloc(arena: &Arena) -> BorrowedAllocator<'_> {
BorrowedAllocator::new(arena)
}
#[test]
fn ruby_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let base = a.content_plain("青梅");
let reading = a.content_plain("おうめ");
let n = a.ruby(base, reading, true);
match n {
borrowed::AozoraNode::Ruby(r) => {
assert_eq!(r.base.as_plain(), Some("青梅"));
assert_eq!(r.reading.as_plain(), Some("おうめ"));
assert!(r.delim_explicit);
}
other => panic!("expected Ruby, got {other:?}"),
}
}
#[test]
fn bouten_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let target = a.content_plain("青空");
let n = a.bouten(BoutenKind::Goma, target, BoutenPosition::Right, false);
match n {
borrowed::AozoraNode::Bouten(b) => {
assert_eq!(b.kind, BoutenKind::Goma);
assert_eq!(b.target.as_plain(), Some("青空"));
assert_eq!(b.position, BoutenPosition::Right);
assert!(!b.consumed_predecessor);
}
other => panic!("expected Bouten, got {other:?}"),
}
}
#[test]
fn tate_chu_yoko_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let text = a.content_plain("12");
let n = a.tate_chu_yoko(text, false);
match n {
borrowed::AozoraNode::TateChuYoko(t) => {
assert_eq!(t.text.as_plain(), Some("12"));
}
other => panic!("expected TateChuYoko, got {other:?}"),
}
}
#[test]
fn gaiji_full_metadata() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let g = a.make_gaiji(
"木+吶のつくり",
Some(Resolved::Char('𠀋')),
Some("第3水準1-85-54"),
);
let n = a.gaiji(g);
match n {
borrowed::AozoraNode::Gaiji(gn) => {
assert_eq!(gn.description, "木+吶のつくり");
assert_eq!(gn.ucs, Some(Resolved::Char('𠀋')));
assert_eq!(gn.mencode, Some("第3水準1-85-54"));
}
other => panic!("expected Gaiji, got {other:?}"),
}
}
#[test]
fn gaiji_no_mencode() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let g = a.make_gaiji("desc", None, None);
let n = a.gaiji(g);
match n {
borrowed::AozoraNode::Gaiji(gn) => {
assert_eq!(gn.description, "desc");
assert!(gn.ucs.is_none());
assert!(gn.mencode.is_none());
}
other => panic!("expected Gaiji, got {other:?}"),
}
}
#[test]
fn indent_round_trip() {
let arena = Arena::new();
let a = fresh_alloc(&arena);
let n = a.indent(Indent { amount: 3 });
assert!(matches!(
n,
borrowed::AozoraNode::Indent(Indent { amount: 3 })
));
}
#[test]
fn align_end_round_trip() {
let arena = Arena::new();
let a = fresh_alloc(&arena);
let n = a.align_end(AlignEnd { offset: 2 });
assert!(matches!(
n,
borrowed::AozoraNode::AlignEnd(AlignEnd { offset: 2 })
));
}
#[test]
fn warichu_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let upper = a.content_plain("上");
let lower = a.content_plain("下");
let n = a.warichu(upper, lower);
match n {
borrowed::AozoraNode::Warichu(w) => {
assert_eq!(w.upper.as_plain(), Some("上"));
assert_eq!(w.lower.as_plain(), Some("下"));
}
other => panic!("expected Warichu, got {other:?}"),
}
}
#[test]
fn keigakomi_round_trip() {
let arena = Arena::new();
let a = fresh_alloc(&arena);
let n = a.keigakomi(Keigakomi);
assert!(matches!(n, borrowed::AozoraNode::Keigakomi(Keigakomi)));
}
#[test]
fn page_break_round_trip() {
let arena = Arena::new();
let a = fresh_alloc(&arena);
let n = a.page_break();
assert!(matches!(n, borrowed::AozoraNode::PageBreak));
}
#[test]
fn section_break_round_trip() {
let arena = Arena::new();
let a = fresh_alloc(&arena);
let n = a.section_break(SectionKind::Choho);
assert!(matches!(
n,
borrowed::AozoraNode::SectionBreak(SectionKind::Choho)
));
}
#[test]
fn aozora_heading_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let text = a.content_plain("見出し");
let n = a.aozora_heading(AozoraHeadingKind::Window, text);
match n {
borrowed::AozoraNode::AozoraHeading(h) => {
assert_eq!(h.kind, AozoraHeadingKind::Window);
assert_eq!(h.text.as_plain(), Some("見出し"));
}
other => panic!("expected AozoraHeading, got {other:?}"),
}
}
#[test]
fn heading_hint_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let n = a.heading_hint(2, "対象");
match n {
borrowed::AozoraNode::HeadingHint(h) => {
assert_eq!(h.level, 2);
assert_eq!(h.target.as_str(), "対象");
}
other => panic!("expected HeadingHint, got {other:?}"),
}
}
#[test]
fn sashie_with_caption() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let caption = a.content_plain("挿絵キャプション");
let n = a.sashie("fig01.png", Some(caption));
match n {
borrowed::AozoraNode::Sashie(s) => {
assert_eq!(s.file.as_str(), "fig01.png");
assert_eq!(
s.caption.and_then(borrowed::Content::as_plain),
Some("挿絵キャプション")
);
}
other => panic!("expected Sashie, got {other:?}"),
}
}
#[test]
fn sashie_without_caption() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let n = a.sashie("fig02.png", None);
match n {
borrowed::AozoraNode::Sashie(s) => {
assert_eq!(s.file.as_str(), "fig02.png");
assert!(s.caption.is_none());
}
other => panic!("expected Sashie, got {other:?}"),
}
}
#[test]
fn kaeriten_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let n = a.kaeriten("一");
match n {
borrowed::AozoraNode::Kaeriten(k) => assert_eq!(k.mark.as_str(), "一"),
other => panic!("expected Kaeriten, got {other:?}"),
}
}
#[test]
fn annotation_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let payload = a.make_annotation("[#X]", AnnotationKind::Unknown);
let n = a.annotation(payload);
match n {
borrowed::AozoraNode::Annotation(an) => {
assert_eq!(an.raw.as_str(), "[#X]");
assert_eq!(an.kind, AnnotationKind::Unknown);
}
other => panic!("expected Annotation, got {other:?}"),
}
}
#[test]
fn double_ruby_round_trip() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let content = a.content_plain("重要");
let n = a.double_ruby(content);
match n {
borrowed::AozoraNode::DoubleRuby(d) => {
assert_eq!(d.content.as_plain(), Some("重要"));
}
other => panic!("expected DoubleRuby, got {other:?}"),
}
}
#[test]
fn container_round_trip() {
let arena = Arena::new();
let a = fresh_alloc(&arena);
let c = Container {
kind: ContainerKind::Indent { amount: 1 },
};
let n = a.container(c);
assert!(matches!(n, borrowed::AozoraNode::Container(cc) if cc == c));
}
#[test]
fn content_plain_empty_collapses_to_empty_segments() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let c = a.content_plain("");
assert!(matches!(c, borrowed::Content::Segments(s) if s.is_empty()));
}
#[test]
fn content_plain_nonempty_returns_plain_variant() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let c = a.content_plain("hello");
assert_eq!(c.as_plain(), Some("hello"));
}
#[test]
fn content_segments_preserves_order_and_kind() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let g = a.make_gaiji("X", None, None);
let seg_g = a.seg_gaiji(g);
let seg_t1 = a.seg_text("before ");
let seg_t2 = a.seg_text(" after");
let ann = a.make_annotation("[#X]", AnnotationKind::Unknown);
let seg_a = a.seg_annotation(ann);
let c = a.content_segments(&[seg_t1, seg_g, seg_t2, seg_a]);
let borrowed::Content::Segments(segs) = c else {
panic!("expected Segments variant for mixed-kind input");
};
assert_eq!(segs.len(), 4);
assert!(matches!(&segs[0], borrowed::Segment::Text(t) if *t == "before "));
assert!(matches!(&segs[1], borrowed::Segment::Gaiji(_)));
assert!(matches!(&segs[2], borrowed::Segment::Text(t) if *t == " after"));
assert!(matches!(&segs[3], borrowed::Segment::Annotation(_)));
}
#[test]
fn content_segments_all_text_collapses_to_plain() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let s1 = a.seg_text("hi ");
let s2 = a.seg_text("there");
let c = a.content_segments(&[s1, s2]);
assert_eq!(c.as_plain(), Some("hi there"));
}
#[test]
fn content_segments_empty_collapses_to_empty_segments() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let c = a.content_segments(&[]);
assert!(matches!(c, borrowed::Content::Segments(s) if s.is_empty()));
}
#[test]
fn interner_dedups_repeated_readings() {
let arena = Arena::new();
let mut a = fresh_alloc(&arena);
let base1 = a.content_plain("青梅");
let reading1 = a.content_plain("おうめ");
let n1 = a.ruby(base1, reading1, false);
let base2 = a.content_plain("青梅");
let reading2 = a.content_plain("おうめ");
let n2 = a.ruby(base2, reading2, false);
let borrowed::AozoraNode::Ruby(r1) = n1 else {
unreachable!();
};
let borrowed::AozoraNode::Ruby(r2) = n2 else {
unreachable!();
};
let s1 = r1.reading.as_plain().expect("plain");
let s2 = r2.reading.as_plain().expect("plain");
assert_eq!(
s1.as_ptr(),
s2.as_ptr(),
"interner must dedup repeated readings"
);
}
#[test]
fn arena_accessor_returns_construction_arena() {
let arena = Arena::new();
let a = fresh_alloc(&arena);
assert!(ptr::eq(a.arena(), &raw const arena));
}
}