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