Skip to main content

meta_language/
parser_registry.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::language_parser::{BuiltInLanguageParser, LanguageParser};
6use crate::{LinkNetwork, ParseConfiguration};
7
8/// A pluggable dispatch table mapping language keys to [`LanguageParser`]s.
9///
10/// The registry starts with the [`BuiltInLanguageParser`] as a fallback, so an
11/// unmodified registry behaves exactly like [`LinkNetwork::parse`]: `lino` is
12/// handled by the links-notation parser and every other key is routed through
13/// the tree-sitter adapter with a lossless text fallback.
14///
15/// Users register parsers for new language keys or override an existing one.
16/// Following TXL's grammar-override model and SWC's plugin lesson, a user
17/// registration *shadows* the built-in dispatch for the same key rather than
18/// forking the pipeline: any key without an explicit registration still falls
19/// through to the built-in set.
20///
21/// Language keys are matched case-insensitively, mirroring the built-in
22/// dispatch (`LINO`, `Lino`, and `lino` resolve to the same parser).
23///
24/// # Examples
25///
26/// ```
27/// use std::sync::Arc;
28/// use meta_language::{
29///     LanguageParser, LinkNetwork, ParseConfiguration, ParserRegistry,
30/// };
31///
32/// #[derive(Debug)]
33/// struct ShoutParser;
34///
35/// impl LanguageParser for ShoutParser {
36///     fn parse_source(
37///         &self,
38///         text: &str,
39///         language: &str,
40///         configuration: ParseConfiguration,
41///     ) -> LinkNetwork {
42///         LinkNetwork::parse_lossless_text(&text.to_uppercase(), language, configuration)
43///     }
44/// }
45///
46/// let registry = ParserRegistry::new().with_parser("shout", Arc::new(ShoutParser));
47/// let network = registry.parse("hi", "shout", ParseConfiguration::default());
48/// assert_eq!(network.reconstruct_text(), "HI");
49/// ```
50#[derive(Clone)]
51pub struct ParserRegistry {
52    parsers: HashMap<String, Arc<dyn LanguageParser>>,
53    fallback: Arc<dyn LanguageParser>,
54}
55
56impl ParserRegistry {
57    /// Creates a registry backed by the [`BuiltInLanguageParser`] fallback and
58    /// no user registrations.
59    #[must_use]
60    pub fn new() -> Self {
61        Self {
62            parsers: HashMap::new(),
63            fallback: Arc::new(BuiltInLanguageParser),
64        }
65    }
66
67    /// Registers `parser` for `language`, shadowing any prior registration or
68    /// built-in dispatch for the same (case-insensitive) key.
69    ///
70    /// Returns `&mut Self` so registrations can be chained.
71    pub fn register(
72        &mut self,
73        language: impl Into<String>,
74        parser: Arc<dyn LanguageParser>,
75    ) -> &mut Self {
76        let key: String = language.into();
77        self.parsers.insert(normalize(&key), parser);
78        self
79    }
80
81    /// Builder-style variant of [`register`](Self::register) that consumes and
82    /// returns the registry.
83    #[must_use]
84    pub fn with_parser(
85        mut self,
86        language: impl Into<String>,
87        parser: Arc<dyn LanguageParser>,
88    ) -> Self {
89        self.register(language, parser);
90        self
91    }
92
93    /// Returns the parser explicitly registered for `language`, if any.
94    ///
95    /// Keys served by the built-in fallback report `None`; use
96    /// [`parse`](Self::parse) to dispatch including the fallback.
97    #[must_use]
98    pub fn parser_for(&self, language: &str) -> Option<&Arc<dyn LanguageParser>> {
99        self.parsers.get(&normalize(language))
100    }
101
102    /// Whether `language` has an explicit (non-fallback) registration.
103    #[must_use]
104    pub fn is_registered(&self, language: &str) -> bool {
105        self.parsers.contains_key(&normalize(language))
106    }
107
108    /// The number of explicit registrations, excluding the built-in fallback.
109    #[must_use]
110    pub fn len(&self) -> usize {
111        self.parsers.len()
112    }
113
114    /// Whether the registry holds no explicit registrations.
115    ///
116    /// An empty registry still parses every key through the built-in fallback.
117    #[must_use]
118    pub fn is_empty(&self) -> bool {
119        self.parsers.is_empty()
120    }
121
122    /// Parses `text` for `language`, dispatching to a registered parser when
123    /// one shadows the key and otherwise to the built-in fallback.
124    #[must_use]
125    pub fn parse(
126        &self,
127        text: &str,
128        language: &str,
129        configuration: ParseConfiguration,
130    ) -> LinkNetwork {
131        self.parsers.get(&normalize(language)).map_or_else(
132            || self.fallback.parse_source(text, language, configuration),
133            |parser| parser.parse_source(text, language, configuration),
134        )
135    }
136}
137
138impl Default for ParserRegistry {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144impl LinkNetwork {
145    /// Parses source text through a pluggable [`ParserRegistry`].
146    ///
147    /// Dispatch honors user registrations, which shadow the built-in set for
148    /// the same language key; keys without an explicit registration fall
149    /// through to the same built-in dispatch [`LinkNetwork::parse`] uses.
150    #[must_use]
151    pub fn parse_with_registry(
152        registry: &ParserRegistry,
153        text: &str,
154        language: &str,
155        configuration: ParseConfiguration,
156    ) -> Self {
157        registry.parse(text, language, configuration)
158    }
159}
160
161impl fmt::Debug for ParserRegistry {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        let mut keys: Vec<&str> = self.parsers.keys().map(String::as_str).collect();
164        keys.sort_unstable();
165        f.debug_struct("ParserRegistry")
166            .field("registered", &keys)
167            .finish_non_exhaustive()
168    }
169}
170
171/// Normalizes a language key for case-insensitive lookup, matching the
172/// built-in dispatch's `eq_ignore_ascii_case` handling.
173fn normalize(language: &str) -> String {
174    language.to_ascii_lowercase()
175}