Skip to main content

qubit_codec_text/encode/
charset_encoder.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8use core::fmt;
9
10use qubit_codec::{
11    BufferedEncodeEngine,
12    BufferedEncoder,
13    BufferedTranscoder,
14    CapacityError,
15    FinishError,
16    TranscodeProgress,
17};
18
19use crate::{
20    CharsetEncodeError,
21    UnmappableAction,
22};
23
24use super::{
25    charset_encode_hooks::{
26        CharsetEncodeHooks,
27        replacement_len,
28    },
29    charset_encode_policy::CharsetEncodePolicy,
30    charset_encode_probe::CharsetEncodeProbe,
31};
32
33/// Converts Unicode scalar values into units of one charset.
34///
35/// `CharsetEncoder` wraps a low-level [`crate::CharsetCodec`] and applies the
36/// configured [`UnmappableAction`] whenever the codec reports that an input
37/// character cannot be represented by the target charset.
38///
39/// # Type Parameters
40///
41/// - `C`: Low-level charset codec used to encode one character into target
42///   storage units.
43#[derive(Clone)]
44pub struct CharsetEncoder<C>
45where
46    C: CharsetEncodeProbe,
47{
48    /// Common buffered encode engine.
49    engine: BufferedEncodeEngine<C, CharsetEncodeHooks<C::Unit>>,
50    /// Public unmappable-input policy metadata.
51    policy: CharsetEncodePolicy,
52    /// Number of units used by replacement policy.
53    replacement_units_len: usize,
54}
55
56impl<C> CharsetEncoder<C>
57where
58    C: CharsetEncodeProbe,
59{
60    /// Creates an encoder with default replacement policy.
61    ///
62    /// # Parameters
63    ///
64    /// - `codec`: Low-level charset codec used to encode output units.
65    ///
66    /// # Returns
67    ///
68    /// Returns an encoder whose unmappable action is
69    /// [`UnmappableAction::Replace`] and whose replacement character is
70    /// [`CharsetEncodePolicy::DEFAULT_REPLACEMENT`]. If the default cannot be
71    /// encoded by the codec,
72    /// [`CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT`] is used.
73    ///
74    /// # Panics
75    ///
76    /// Panics when neither [`CharsetEncodePolicy::DEFAULT_REPLACEMENT`] nor
77    /// [`CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT`] can be encoded by
78    /// `codec`.
79    /// Built-in codecs can always encode the fallback `?`; failure here means
80    /// the supplied codec cannot encode a minimal ASCII replacement. For custom
81    /// [`crate::CharsetCodec`] implementations, this indicates a broken codec
82    /// invariant rather than recoverable input data.
83    #[must_use]
84    pub fn new(codec: C) -> Self {
85        let policy = CharsetEncodePolicy::default();
86        match Self::create_hooks(&codec, policy) {
87            Ok((hooks, replacement_units_len)) => Self {
88                engine: BufferedEncodeEngine::new(codec, hooks),
89                policy,
90                replacement_units_len,
91            },
92            Err(default_error) => {
93                let fallback_policy = CharsetEncodePolicy::replace(
94                    CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT,
95                );
96                match Self::create_hooks(&codec, fallback_policy) {
97                    Ok((hooks, replacement_units_len)) => Self {
98                        engine: BufferedEncodeEngine::new(codec, hooks),
99                        policy: fallback_policy,
100                        replacement_units_len,
101                    },
102                    Err(_) => panic!(
103                        "cannot initialize CharsetEncoder for {:?}: neither {:?} nor {:?} is encodable ({default_error})",
104                        codec.charset(),
105                        CharsetEncodePolicy::DEFAULT_REPLACEMENT,
106                        CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT,
107                    ),
108                }
109            }
110        }
111    }
112
113    /// Creates an encoder with an explicit unmappable-input policy.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error when `policy` uses replacement and the replacement
118    /// character cannot be encoded by `codec`.
119    pub fn with_policy(
120        codec: C,
121        policy: CharsetEncodePolicy,
122    ) -> Result<Self, CharsetEncodeError> {
123        let (hooks, replacement_units_len) =
124            Self::create_hooks(&codec, policy)?;
125        Ok(Self {
126            engine: BufferedEncodeEngine::new(codec, hooks),
127            policy,
128            replacement_units_len,
129        })
130    }
131
132    /// Returns the configured unmappable-character action.
133    ///
134    /// # Returns
135    ///
136    /// Returns the action used when target encoding cannot represent a
137    /// character.
138    #[must_use]
139    #[inline(always)]
140    pub const fn unmappable_action(&self) -> UnmappableAction {
141        self.policy.unmappable_action()
142    }
143
144    /// Returns the configured replacement character.
145    ///
146    /// # Returns
147    ///
148    /// Returns the character encoded when [`UnmappableAction::Replace`] is
149    /// used.
150    #[must_use]
151    #[inline(always)]
152    pub const fn replacement(&self) -> char {
153        self.policy.replacement()
154    }
155
156    /// Creates encode hooks for `policy`.
157    pub(crate) fn create_hooks(
158        codec: &C,
159        policy: CharsetEncodePolicy,
160    ) -> Result<(CharsetEncodeHooks<C::Unit>, usize), CharsetEncodeError> {
161        let mut hooks = CharsetEncodeHooks::new(
162            policy.unmappable_action(),
163            policy.replacement(),
164        );
165        if policy.unmappable_action() != UnmappableAction::Replace {
166            return Ok((hooks, 0));
167        }
168        let replacement_units_len =
169            replacement_len(codec, policy.replacement())?;
170        hooks.set_replacement_units_len(replacement_units_len);
171        Ok((hooks, replacement_units_len))
172    }
173}
174
175impl<C> BufferedTranscoder<char, C::Unit> for CharsetEncoder<C>
176where
177    C: CharsetEncodeProbe,
178{
179    type Error = CharsetEncodeError;
180
181    /// Returns the maximum number of target units needed for `input_len`
182    /// characters.
183    #[inline(always)]
184    fn max_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
185        self.engine.max_output_len(input_len)
186    }
187
188    /// Returns the maximum target units emitted by finishing internal state.
189    #[inline(always)]
190    fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
191        Ok(self.engine.max_finish_output_len())
192    }
193
194    /// Clears hook-owned state while keeping encoder policy.
195    #[inline(always)]
196    fn reset(&mut self) {
197        self.engine.reset();
198    }
199
200    /// Encodes characters into the target charset while applying unmappable
201    /// policy.
202    #[inline(always)]
203    fn transcode(
204        &mut self,
205        input: &[char],
206        input_index: usize,
207        output: &mut [C::Unit],
208        output_index: usize,
209    ) -> Result<TranscodeProgress, Self::Error> {
210        self.engine
211            .transcode(input, input_index, output, output_index)
212    }
213
214    /// Finishes encoder-owned final output after EOF.
215    #[inline(always)]
216    fn finish(
217        &mut self,
218        output: &mut [C::Unit],
219        output_index: usize,
220    ) -> Result<usize, FinishError<Self::Error>> {
221        self.engine.finish(output, output_index)
222    }
223}
224
225impl<C> BufferedEncoder<char, C::Unit> for CharsetEncoder<C> where
226    C: CharsetEncodeProbe
227{
228}
229
230impl<C> Eq for CharsetEncoder<C> where C: CharsetEncodeProbe + Eq {}
231
232impl<C> PartialEq for CharsetEncoder<C>
233where
234    C: CharsetEncodeProbe + PartialEq,
235{
236    /// Compares encoder configuration without leaking unit trait bounds.
237    #[inline(always)]
238    fn eq(&self, other: &Self) -> bool {
239        self.engine == other.engine && self.policy == other.policy
240    }
241}
242
243impl<C> fmt::Debug for CharsetEncoder<C>
244where
245    C: CharsetEncodeProbe + fmt::Debug,
246{
247    /// Formats the encoder without exposing additional bounds for unit values.
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        f.debug_struct("CharsetEncoder")
250            .field("engine", &self.engine)
251            .field("unmappable_action", &self.unmappable_action())
252            .field("replacement", &self.replacement())
253            .field("replacement_units_len", &self.replacement_units_len)
254            .finish()
255    }
256}