1use std::collections::{BTreeMap, BTreeSet};
2use std::error::Error;
3use std::fmt;
4
5use crate::{LinkId, LinkMetadata, LinkNetwork, LinkType, ParseConfiguration, TranslationRuleSet};
6
7const PROFILE_TERM: &str = "language-profile";
8const PROFILE_LINK_TYPE_TERM: &str = "language-profile:link-type";
9const PROFILE_CONCEPT_TERM: &str = "language-profile:concept";
10const PROFILE_TRANSLATION_RULE_TERM: &str = "language-profile:translation-rule";
11const PROFILE_DIAGNOSTIC_TERM: &str = "language-profile:unsupported-feature";
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct LanguageProfile {
16 name: String,
17 language: String,
18 link_types: BTreeSet<LinkType>,
19 concepts: BTreeSet<String>,
20 translation_rules: BTreeSet<String>,
21 fallbacks: BTreeMap<String, String>,
24}
25
26impl LanguageProfile {
27 #[must_use]
29 pub fn new(name: impl Into<String>, language: impl Into<String>) -> Self {
30 Self {
31 name: name.into(),
32 language: language.into(),
33 link_types: BTreeSet::new(),
34 concepts: BTreeSet::new(),
35 translation_rules: BTreeSet::new(),
36 fallbacks: BTreeMap::new(),
37 }
38 }
39
40 #[must_use]
42 pub fn javascript() -> Self {
43 let mut profile = Self::new("JavaScript", "JavaScript");
44 for link_type in [
45 LinkType::Link,
46 LinkType::Reference,
47 LinkType::Relation,
48 LinkType::Language,
49 LinkType::Grammar,
50 LinkType::Type,
51 LinkType::Concept,
52 LinkType::Syntax,
53 LinkType::Field,
54 LinkType::Trivia,
55 LinkType::Token,
56 LinkType::Document,
57 LinkType::Semantic,
58 LinkType::Region,
59 LinkType::Object,
60 ] {
61 profile = profile.with_link_type(link_type);
62 }
63 profile
64 }
65
66 #[must_use]
68 pub fn builtin(name: &str) -> Option<Self> {
69 match name.to_ascii_lowercase().as_str() {
70 "javascript" | "js" => Some(Self::javascript()),
71 _ => None,
72 }
73 }
74
75 #[must_use]
81 pub fn from_rule_set(
82 name: impl Into<String>,
83 language: impl Into<String>,
84 rule_set: &TranslationRuleSet,
85 ) -> Self {
86 let mut profile = Self::new(name, language);
87 for rule in rule_set.rules() {
88 profile = profile.with_translation_rule(rule.name());
89 if let Some(link_type) = rule.query().link_type_filter() {
90 profile = profile.with_link_type(link_type);
91 }
92 if let Some(term) = rule.query().term_filter() {
93 profile = profile.with_concept(term);
94 }
95 }
96 profile
97 }
98
99 #[must_use]
101 pub fn name(&self) -> &str {
102 &self.name
103 }
104
105 #[must_use]
107 pub fn language(&self) -> &str {
108 &self.language
109 }
110
111 #[must_use]
113 pub const fn link_types(&self) -> &BTreeSet<LinkType> {
114 &self.link_types
115 }
116
117 #[must_use]
119 pub const fn concepts(&self) -> &BTreeSet<String> {
120 &self.concepts
121 }
122
123 #[must_use]
125 pub const fn translation_rules(&self) -> &BTreeSet<String> {
126 &self.translation_rules
127 }
128
129 #[must_use]
136 pub const fn fallbacks(&self) -> &BTreeMap<String, String> {
137 &self.fallbacks
138 }
139
140 #[must_use]
142 pub fn with_link_type(mut self, link_type: LinkType) -> Self {
143 self.link_types.insert(link_type);
144 self
145 }
146
147 #[must_use]
149 pub fn with_concept(mut self, concept: impl Into<String>) -> Self {
150 self.concepts.insert(concept.into());
151 self
152 }
153
154 #[must_use]
156 pub fn with_translation_rule(mut self, rule: impl Into<String>) -> Self {
157 self.translation_rules.insert(rule.into());
158 self
159 }
160
161 #[must_use]
167 pub fn with_concept_fallback(
168 mut self,
169 concept: impl Into<String>,
170 fallback: impl Into<String>,
171 ) -> Self {
172 self.fallbacks.insert(concept.into(), fallback.into());
173 self
174 }
175
176 #[must_use]
179 pub fn concept_fallback(&self, concept: &str) -> Option<&str> {
180 self.fallbacks.get(concept).map(String::as_str)
181 }
182
183 #[must_use]
185 pub fn supports_link_type(&self, link_type: LinkType) -> bool {
186 self.link_types.contains(&link_type)
187 }
188
189 #[must_use]
191 pub fn supports_concept(&self, concept: &str) -> bool {
192 self.concepts.contains(concept)
193 }
194
195 #[must_use]
197 pub fn supports_translation_rule(&self, rule: &str) -> bool {
198 self.translation_rules.contains(rule)
199 }
200
201 pub fn declare_in(&self, network: &mut LinkNetwork) -> LanguageProfileLinks {
203 let profile = self.profile_link(network).unwrap_or_else(|| {
204 network.insert_link(
205 [],
206 LinkMetadata::new()
207 .with_link_type(LinkType::Semantic)
208 .with_named(true)
209 .with_term(PROFILE_TERM)
210 .with_language(&self.language)
211 .with_definition(&self.name),
212 )
213 });
214 let mut capabilities = Vec::new();
215
216 for link_type in &self.link_types {
217 capabilities.push(self.ensure_capability_link(
218 network,
219 profile,
220 PROFILE_LINK_TYPE_TERM,
221 &link_type.to_string(),
222 ));
223 }
224 for concept in &self.concepts {
225 capabilities.push(self.ensure_capability_link(
226 network,
227 profile,
228 PROFILE_CONCEPT_TERM,
229 concept,
230 ));
231 }
232 for rule in &self.translation_rules {
233 capabilities.push(self.ensure_capability_link(
234 network,
235 profile,
236 PROFILE_TRANSLATION_RULE_TERM,
237 rule,
238 ));
239 }
240
241 LanguageProfileLinks {
242 profile,
243 capabilities,
244 }
245 }
246
247 pub fn validate_network(&self, network: &LinkNetwork) -> Result<(), LanguageProfileViolation> {
254 for link in network.links() {
255 if let Some(link_type) = link.metadata().link_type() {
256 if !self.supports_link_type(link_type) {
257 return Err(LanguageProfileViolation::new(
258 format!("link type `{link_type}`"),
259 format!(
260 "Profile `{}` for `{}` does not support link type `{link_type}`.",
261 self.name, self.language
262 ),
263 ));
264 }
265 }
266
267 if self.concepts.is_empty()
268 || !matches!(
269 link.metadata().link_type(),
270 Some(LinkType::Concept | LinkType::Semantic)
271 )
272 {
273 continue;
274 }
275 let Some(term) = link.metadata().term() else {
276 continue;
277 };
278 if is_profile_control_term(term) || self.supports_concept(term) {
279 continue;
280 }
281 return Err(LanguageProfileViolation::new(
282 format!("concept `{term}`"),
283 format!(
284 "Profile `{}` for `{}` does not support concept `{term}`.",
285 self.name, self.language
286 ),
287 ));
288 }
289 Ok(())
290 }
291
292 pub(crate) fn validate_transform_result(
293 &self,
294 network: &LinkNetwork,
295 ) -> Result<(), LanguageProfileViolation> {
296 self.validate_network(network)?;
297
298 let source = network.reconstruct_text();
299 if source.is_empty() {
300 return Ok(());
301 }
302
303 let parsed = LinkNetwork::parse(&source, &self.language, ParseConfiguration::default());
304 let report = parsed.verify_full_match(None);
305 if report.issues().is_empty() {
306 Ok(())
307 } else {
308 Err(LanguageProfileViolation::new(
309 format!("{} syntax", self.language),
310 format!(
311 "Profile `{}` for `{}` rejects source text that is not valid {}.",
312 self.name, self.language, self.language
313 ),
314 ))
315 }
316 }
317
318 pub(crate) fn insert_diagnostic(
319 &self,
320 network: &mut LinkNetwork,
321 violation: &LanguageProfileViolation,
322 subject: Option<LinkId>,
323 ) -> LinkId {
324 let profile = self.declare_in(network).profile();
325 let metadata = LinkMetadata::new()
326 .with_link_type(LinkType::Semantic)
327 .with_named(true)
328 .with_term(PROFILE_DIAGNOSTIC_TERM)
329 .with_language(&self.language)
330 .with_definition(violation.to_string());
331
332 match subject {
333 Some(subject) => network.insert_link([profile, subject], metadata),
334 None => network.insert_link([profile], metadata),
335 }
336 }
337
338 fn profile_link(&self, network: &LinkNetwork) -> Option<LinkId> {
339 network
340 .links()
341 .find(|link| {
342 link.metadata().link_type() == Some(LinkType::Semantic)
343 && link.metadata().term() == Some(PROFILE_TERM)
344 && link.metadata().language() == Some(self.language())
345 && link.metadata().definition() == Some(self.name())
346 })
347 .map(crate::Link::id)
348 }
349
350 fn ensure_capability_link(
351 &self,
352 network: &mut LinkNetwork,
353 profile: LinkId,
354 term: &str,
355 definition: &str,
356 ) -> LinkId {
357 if let Some(existing) = network
358 .links()
359 .find(|link| {
360 link.references() == [profile]
361 && link.metadata().link_type() == Some(LinkType::Semantic)
362 && link.metadata().term() == Some(term)
363 && link.metadata().language() == Some(self.language())
364 && link.metadata().definition() == Some(definition)
365 })
366 .map(crate::Link::id)
367 {
368 return existing;
369 }
370
371 network.insert_link(
372 [profile],
373 LinkMetadata::new()
374 .with_link_type(LinkType::Semantic)
375 .with_named(true)
376 .with_term(term)
377 .with_language(&self.language)
378 .with_definition(definition),
379 )
380 }
381}
382
383#[derive(Clone, Debug, PartialEq, Eq)]
385pub struct LanguageProfileLinks {
386 profile: LinkId,
387 capabilities: Vec<LinkId>,
388}
389
390impl LanguageProfileLinks {
391 #[must_use]
393 pub const fn profile(&self) -> LinkId {
394 self.profile
395 }
396
397 #[must_use]
399 pub fn capabilities(&self) -> &[LinkId] {
400 &self.capabilities
401 }
402}
403
404#[derive(Clone, Debug, PartialEq, Eq)]
406pub struct LanguageProfileViolation {
407 feature: String,
408 message: String,
409}
410
411impl LanguageProfileViolation {
412 fn new(feature: impl Into<String>, message: impl Into<String>) -> Self {
413 Self {
414 feature: feature.into(),
415 message: message.into(),
416 }
417 }
418
419 #[must_use]
421 pub fn feature(&self) -> &str {
422 &self.feature
423 }
424}
425
426impl fmt::Display for LanguageProfileViolation {
427 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
428 write!(
429 formatter,
430 "{} Unsupported feature: {}.",
431 self.message, self.feature
432 )
433 }
434}
435
436impl Error for LanguageProfileViolation {}
437
438fn is_profile_control_term(term: &str) -> bool {
439 term.starts_with("language-profile")
440 || term.starts_with("translation-rule:")
441 || term == "translation-rule"
442 || term == "translation-rule-set"
443}