Skip to main content

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}