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 **distinct** font face names referenced across the
74 /// per-language axes (hangul/latin/hanja/…) of the character shape.
75 ///
76 /// Formats with per-language font references (HWPX `fontRef`) override
77 /// this to surface axis mismatches — a result longer than 1 means the
78 /// character shape renders with different fonts per script, which a
79 /// single-font consumer cannot reproduce faithfully. The first element
80 /// matches [`char_font_name`](Self::char_font_name) when both resolve.
81 ///
82 /// The default implementation returns the single
83 /// [`char_font_name`](Self::char_font_name) (no axis information).
84 fn char_font_axis_names(&self, id: CharShapeIndex) -> Vec<&str> {
85 self.char_font_name(id).into_iter().collect()
86 }
87
88 /// Returns the font size (in [`HwpUnit`]) of the character shape at `id`.
89 fn char_font_size(&self, _id: CharShapeIndex) -> Option<HwpUnit> {
90 None
91 }
92
93 /// Returns the text color of the character shape at `id`.
94 fn char_text_color(&self, _id: CharShapeIndex) -> Option<Color> {
95 None
96 }
97
98 /// Returns the horizontal alignment of the paragraph shape at `id`.
99 fn para_alignment(&self, _id: ParaShapeIndex) -> Option<Alignment> {
100 None
101 }
102
103 /// Returns the left indent of the paragraph shape at `id`.
104 fn para_indent_left(&self, _id: ParaShapeIndex) -> Option<HwpUnit> {
105 None
106 }
107
108 /// Returns the first-line indent of the paragraph shape at `id`.
109 fn para_indent_first_line(&self, _id: ParaShapeIndex) -> Option<HwpUnit> {
110 None
111 }
112
113 /// Returns the list type for a paragraph shape: `"BULLET"`, `"NUMBER"`, or `None`.
114 ///
115 /// Returns `None` if the paragraph has no list heading or if the heading
116 /// type is `NONE` / `OUTLINE`.
117 fn para_list_type(&self, _id: ParaShapeIndex) -> Option<&str> {
118 None
119 }
120
121 /// Returns the zero-based list nesting level for a paragraph shape.
122 ///
123 /// This is only meaningful for numbered/bulleted list semantics. Outline
124 /// headings should use [`para_heading_level`](Self::para_heading_level)
125 /// instead.
126 fn para_list_level(&self, _id: ParaShapeIndex) -> Option<u8> {
127 None
128 }
129
130 /// Returns the checkbox state for a paragraph shape when it is a checkable bullet.
131 ///
132 /// `Some(true)` means a checked checkbox item, `Some(false)` means an
133 /// unchecked checkbox item, and `None` means the paragraph is not a
134 /// checkable bullet.
135 fn para_checked_state(&self, _id: ParaShapeIndex) -> Option<bool> {
136 None
137 }
138
139 /// Returns the preferred style name associated with the paragraph shape.
140 ///
141 /// This is useful for encoders that need to recover semantics carried by a
142 /// dedicated paragraph shape even when the paragraph itself has no explicit
143 /// `style_id`.
144 fn para_style_name(&self, _id: ParaShapeIndex) -> Option<&str> {
145 None
146 }
147
148 /// Returns the heading level (1–6) implied by the paragraph shape at `id`.
149 ///
150 /// This is the format-agnostic truth source for paragraph-level outline
151 /// semantics. Implementors that can inspect real paragraph-shape outline
152 /// metadata should override this method; downstream styled export paths use
153 /// it before style-name heuristics whenever both are available.
154 fn para_heading_level(&self, _id: ParaShapeIndex) -> Option<u8> {
155 None
156 }
157
158 /// Returns the Korean name of the style at `id`.
159 fn style_name(&self, _id: StyleIndex) -> Option<&str> {
160 None
161 }
162
163 /// Returns the heading level (1–6) of the style at `id`, if it is
164 /// a heading style. Returns `None` for non-heading styles.
165 fn style_heading_level(&self, _id: StyleIndex) -> Option<u8> {
166 None
167 }
168
169 /// Resolves a `binaryItemIDRef` (e.g. `"BinData/image1"`) to the actual
170 /// filename with extension (e.g. `"image1.png"`).
171 ///
172 /// Returns `None` if no matching image is found.
173 fn image_resolve_filename(&self, _key: &str) -> Option<&str> {
174 None
175 }
176
177 /// Returns the raw binary data for the image identified by `key`.
178 ///
179 /// `key` is typically a path like `"image1.jpg"`. Returns `None` if
180 /// the image is not available or if the implementor does not store
181 /// image data.
182 fn image_data(&self, _key: &str) -> Option<&[u8]> {
183 None
184 }
185
186 /// Returns the four rendered border edges of the `borderFill` at `id`
187 /// (1-based wire reference, e.g. [`crate::table::TableCell::border_fill_id`]).
188 ///
189 /// `None` means the id is not registered in this store.
190 fn border_fill_lines(&self, _id: u32) -> Option<BorderFillLines> {
191 None
192 }
193
194 /// Returns the face-fill verdict of the `borderFill` at `id` (1-based
195 /// wire reference).
196 ///
197 /// `None` means the id is not registered — distinguish this from
198 /// [`FillKind::None`] (registered, but transparent) and
199 /// [`FillKind::Unsupported`] (registered, but not renderable — warn).
200 fn border_fill_face(&self, _id: u32) -> Option<FillKind> {
201 None
202 }
203}
204
205/// Render kind of one border edge.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207#[non_exhaustive]
208pub enum BorderLineKind {
209 /// No line on this edge.
210 None,
211 /// Solid stroke.
212 Solid,
213 /// Any other style (dashed, double, …) or an unparsable width/color —
214 /// consumers must warn and skip instead of guessing (no fake support).
215 Other,
216}
217
218/// One rendered border edge of a `borderFill`.
219#[derive(Debug, Clone, Copy, PartialEq)]
220#[non_exhaustive]
221pub struct BorderLine {
222 /// Render kind of this edge.
223 pub kind: BorderLineKind,
224 /// Stroke width ([`HwpUnit::ZERO`] when [`BorderLineKind::None`]/`Other`).
225 pub width: HwpUnit,
226 /// Stroke color.
227 pub color: Color,
228}
229
230impl BorderLine {
231 /// Creates a border line.
232 #[must_use]
233 pub fn new(kind: BorderLineKind, width: HwpUnit, color: Color) -> Self {
234 Self { kind, width, color }
235 }
236}
237
238/// The four rendered edges of a `borderFill`.
239#[derive(Debug, Clone, Copy, PartialEq)]
240#[non_exhaustive]
241pub struct BorderFillLines {
242 /// Left edge.
243 pub left: BorderLine,
244 /// Right edge.
245 pub right: BorderLine,
246 /// Top edge.
247 pub top: BorderLine,
248 /// Bottom edge.
249 pub bottom: BorderLine,
250}
251
252impl BorderFillLines {
253 /// Creates the four edges.
254 #[must_use]
255 pub fn new(left: BorderLine, right: BorderLine, top: BorderLine, bottom: BorderLine) -> Self {
256 Self { left, right, top, bottom }
257 }
258}
259
260/// Face-fill verdict of a `borderFill` — distinguishes "no fill" from
261/// "unsupported fill": only the latter warrants a consumer warning.
262#[derive(Debug, Clone, Copy, PartialEq)]
263#[non_exhaustive]
264pub enum FillKind {
265 /// No fill (transparent) — normal, no warning.
266 None,
267 /// Solid color fill.
268 Solid(Color),
269 /// Gradient/image/hatch fill or an unparsable color — consumers must
270 /// warn and skip instead of guessing (no fake support).
271 Unsupported,
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use hwpforge_foundation::{ParaShapeIndex, StyleIndex};
278
279 struct NoopStore;
280 impl StyleLookup for NoopStore {}
281
282 #[test]
283 fn noop_store_returns_none_for_all_methods() {
284 let store = NoopStore;
285 let cs = CharShapeIndex::new(0);
286 let ps = ParaShapeIndex::new(0);
287 let si = StyleIndex::new(0);
288
289 assert!(store.char_bold(cs).is_none());
290 assert!(store.char_italic(cs).is_none());
291 assert!(store.char_underline(cs).is_none());
292 assert!(store.char_strikeout(cs).is_none());
293 assert!(store.char_superscript(cs).is_none());
294 assert!(store.char_subscript(cs).is_none());
295 assert!(store.char_font_name(cs).is_none());
296 assert!(store.char_font_size(cs).is_none());
297 assert!(store.char_text_color(cs).is_none());
298 assert!(store.para_alignment(ps).is_none());
299 assert!(store.para_indent_left(ps).is_none());
300 assert!(store.para_indent_first_line(ps).is_none());
301 assert!(store.para_list_type(ps).is_none());
302 assert!(store.para_list_level(ps).is_none());
303 assert!(store.para_checked_state(ps).is_none());
304 assert!(store.para_style_name(ps).is_none());
305 assert!(store.para_heading_level(ps).is_none());
306 assert!(store.style_name(si).is_none());
307 assert!(store.style_heading_level(si).is_none());
308 assert!(store.image_data("image1.jpg").is_none());
309 assert!(store.border_fill_lines(1).is_none());
310 assert!(store.border_fill_face(1).is_none());
311 }
312
313 #[test]
314 fn partial_impl_returns_some_for_overridden_methods() {
315 struct BoldOnly;
316 impl StyleLookup for BoldOnly {
317 fn char_bold(&self, _id: CharShapeIndex) -> Option<bool> {
318 Some(true)
319 }
320 }
321
322 let store = BoldOnly;
323 assert_eq!(store.char_bold(CharShapeIndex::new(0)), Some(true));
324 // Non-overridden methods still return None
325 assert!(store.char_italic(CharShapeIndex::new(0)).is_none());
326 }
327
328 #[test]
329 fn trait_object_works() {
330 let store: &dyn StyleLookup = &NoopStore;
331 assert!(store.char_bold(CharShapeIndex::new(0)).is_none());
332 }
333
334 #[test]
335 fn default_axis_names_mirror_single_font_name() {
336 // 기본 구현 = char_font_name 단일 원소 (축 정보 없는 포맷).
337 let cs = CharShapeIndex::new(0);
338 assert!(NoopStore.char_font_axis_names(cs).is_empty());
339
340 struct OneFont;
341 impl StyleLookup for OneFont {
342 fn char_font_name(&self, _id: CharShapeIndex) -> Option<&str> {
343 Some("함초롬바탕")
344 }
345 }
346 assert_eq!(OneFont.char_font_axis_names(cs), vec!["함초롬바탕"]);
347 }
348}