hwpforge_core/style_lookup.rs
1//! Format-agnostic style querying trait.
2//!
3//! [`StyleLookup`] provides a uniform interface for retrieving character,
4//! paragraph, and style properties by index. Each format-specific style
5//! store (e.g. `HwpxStyleStore`) implements this trait so that downstream
6//! consumers (e.g. the Markdown encoder) can query styles without knowing
7//! the underlying format.
8//!
9//! All methods have default implementations returning `None`, so
10//! implementors only need to override the methods they can support.
11
12use hwpforge_foundation::{
13 Alignment, CharShapeIndex, Color, HwpUnit, ParaShapeIndex, StyleIndex, UnderlineType,
14};
15
16/// Trait for querying resolved style properties by index.
17///
18/// This is the bridge between format-specific style stores and
19/// format-independent consumers (like the Markdown encoder). Each method
20/// takes a branded index and returns `Option<T>`, where `None` means the
21/// property is unavailable or unsupported.
22///
23/// # Default Implementations
24///
25/// Every method defaults to `None`, so an empty implementation is valid:
26///
27/// ```
28/// use hwpforge_core::StyleLookup;
29/// use hwpforge_foundation::CharShapeIndex;
30///
31/// struct NoopStore;
32/// impl StyleLookup for NoopStore {}
33///
34/// let store = NoopStore;
35/// assert!(store.char_bold(CharShapeIndex::new(0)).is_none());
36/// ```
37pub trait StyleLookup {
38 /// Returns whether the character shape at `id` is bold.
39 fn char_bold(&self, _id: CharShapeIndex) -> Option<bool> {
40 None
41 }
42
43 /// Returns whether the character shape at `id` is italic.
44 fn char_italic(&self, _id: CharShapeIndex) -> Option<bool> {
45 None
46 }
47
48 /// Returns the underline type of the character shape at `id`.
49 fn char_underline(&self, _id: CharShapeIndex) -> Option<UnderlineType> {
50 None
51 }
52
53 /// Returns whether the character shape at `id` has strikeout.
54 fn char_strikeout(&self, _id: CharShapeIndex) -> Option<bool> {
55 None
56 }
57
58 /// Returns whether the character shape at `id` is superscript.
59 fn char_superscript(&self, _id: CharShapeIndex) -> Option<bool> {
60 None
61 }
62
63 /// Returns whether the character shape at `id` is subscript.
64 fn char_subscript(&self, _id: CharShapeIndex) -> Option<bool> {
65 None
66 }
67
68 /// Returns the font name of the character shape at `id`.
69 fn char_font_name(&self, _id: CharShapeIndex) -> Option<&str> {
70 None
71 }
72
73 /// Returns the font size (in [`HwpUnit`]) of the character shape at `id`.
74 fn char_font_size(&self, _id: CharShapeIndex) -> Option<HwpUnit> {
75 None
76 }
77
78 /// Returns the text color of the character shape at `id`.
79 fn char_text_color(&self, _id: CharShapeIndex) -> Option<Color> {
80 None
81 }
82
83 /// Returns the horizontal alignment of the paragraph shape at `id`.
84 fn para_alignment(&self, _id: ParaShapeIndex) -> Option<Alignment> {
85 None
86 }
87
88 /// Returns the left indent of the paragraph shape at `id`.
89 fn para_indent_left(&self, _id: ParaShapeIndex) -> Option<HwpUnit> {
90 None
91 }
92
93 /// Returns the first-line indent of the paragraph shape at `id`.
94 fn para_indent_first_line(&self, _id: ParaShapeIndex) -> Option<HwpUnit> {
95 None
96 }
97
98 /// Returns the list type for a paragraph shape: `"BULLET"`, `"NUMBER"`, or `None`.
99 ///
100 /// Returns `None` if the paragraph has no list heading or if the heading
101 /// type is `NONE` / `OUTLINE`.
102 fn para_list_type(&self, _id: ParaShapeIndex) -> Option<&str> {
103 None
104 }
105
106 /// Returns the zero-based list nesting level for a paragraph shape.
107 ///
108 /// This is only meaningful for numbered/bulleted list semantics. Outline
109 /// headings should use [`para_heading_level`](Self::para_heading_level)
110 /// instead.
111 fn para_list_level(&self, _id: ParaShapeIndex) -> Option<u8> {
112 None
113 }
114
115 /// Returns the checkbox state for a paragraph shape when it is a checkable bullet.
116 ///
117 /// `Some(true)` means a checked checkbox item, `Some(false)` means an
118 /// unchecked checkbox item, and `None` means the paragraph is not a
119 /// checkable bullet.
120 fn para_checked_state(&self, _id: ParaShapeIndex) -> Option<bool> {
121 None
122 }
123
124 /// Returns the preferred style name associated with the paragraph shape.
125 ///
126 /// This is useful for encoders that need to recover semantics carried by a
127 /// dedicated paragraph shape even when the paragraph itself has no explicit
128 /// `style_id`.
129 fn para_style_name(&self, _id: ParaShapeIndex) -> Option<&str> {
130 None
131 }
132
133 /// Returns the heading level (1–6) implied by the paragraph shape at `id`.
134 ///
135 /// This is the format-agnostic truth source for paragraph-level outline
136 /// semantics. Implementors that can inspect real paragraph-shape outline
137 /// metadata should override this method; downstream styled export paths use
138 /// it before style-name heuristics whenever both are available.
139 fn para_heading_level(&self, _id: ParaShapeIndex) -> Option<u8> {
140 None
141 }
142
143 /// Returns the Korean name of the style at `id`.
144 fn style_name(&self, _id: StyleIndex) -> Option<&str> {
145 None
146 }
147
148 /// Returns the heading level (1–6) of the style at `id`, if it is
149 /// a heading style. Returns `None` for non-heading styles.
150 fn style_heading_level(&self, _id: StyleIndex) -> Option<u8> {
151 None
152 }
153
154 /// Resolves a `binaryItemIDRef` (e.g. `"BinData/image1"`) to the actual
155 /// filename with extension (e.g. `"image1.png"`).
156 ///
157 /// Returns `None` if no matching image is found.
158 fn image_resolve_filename(&self, _key: &str) -> Option<&str> {
159 None
160 }
161
162 /// Returns the raw binary data for the image identified by `key`.
163 ///
164 /// `key` is typically a path like `"image1.jpg"`. Returns `None` if
165 /// the image is not available or if the implementor does not store
166 /// image data.
167 fn image_data(&self, _key: &str) -> Option<&[u8]> {
168 None
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use hwpforge_foundation::{ParaShapeIndex, StyleIndex};
176
177 struct NoopStore;
178 impl StyleLookup for NoopStore {}
179
180 #[test]
181 fn noop_store_returns_none_for_all_methods() {
182 let store = NoopStore;
183 let cs = CharShapeIndex::new(0);
184 let ps = ParaShapeIndex::new(0);
185 let si = StyleIndex::new(0);
186
187 assert!(store.char_bold(cs).is_none());
188 assert!(store.char_italic(cs).is_none());
189 assert!(store.char_underline(cs).is_none());
190 assert!(store.char_strikeout(cs).is_none());
191 assert!(store.char_superscript(cs).is_none());
192 assert!(store.char_subscript(cs).is_none());
193 assert!(store.char_font_name(cs).is_none());
194 assert!(store.char_font_size(cs).is_none());
195 assert!(store.char_text_color(cs).is_none());
196 assert!(store.para_alignment(ps).is_none());
197 assert!(store.para_indent_left(ps).is_none());
198 assert!(store.para_indent_first_line(ps).is_none());
199 assert!(store.para_list_type(ps).is_none());
200 assert!(store.para_list_level(ps).is_none());
201 assert!(store.para_checked_state(ps).is_none());
202 assert!(store.para_style_name(ps).is_none());
203 assert!(store.para_heading_level(ps).is_none());
204 assert!(store.style_name(si).is_none());
205 assert!(store.style_heading_level(si).is_none());
206 assert!(store.image_data("image1.jpg").is_none());
207 }
208
209 #[test]
210 fn partial_impl_returns_some_for_overridden_methods() {
211 struct BoldOnly;
212 impl StyleLookup for BoldOnly {
213 fn char_bold(&self, _id: CharShapeIndex) -> Option<bool> {
214 Some(true)
215 }
216 }
217
218 let store = BoldOnly;
219 assert_eq!(store.char_bold(CharShapeIndex::new(0)), Some(true));
220 // Non-overridden methods still return None
221 assert!(store.char_italic(CharShapeIndex::new(0)).is_none());
222 }
223
224 #[test]
225 fn trait_object_works() {
226 let store: &dyn StyleLookup = &NoopStore;
227 assert!(store.char_bold(CharShapeIndex::new(0)).is_none());
228 }
229}