1use crate::diagnostic::{Code, Diagnostic, Severity};
10use crate::schema::{AnnotationKind, AnnotationSchema, Registry};
11use crate::source::{RelPath, SourceSpan};
12use crate::value::{Value, game_path, identifier, parse_value, scalar};
13use foch::game::eu4::script::parser::{
14 AstStatement, AstValue, ScalarValue, Span, SpanRange, parse_clausewitz_content,
15};
16use serde::Serialize;
17
18#[derive(Clone, Debug, Serialize)]
19pub struct Argument {
20 pub name: String,
21 pub span: SourceSpan,
22 pub value: Value,
23}
24
25#[derive(Clone, Debug, Serialize)]
26pub struct Annotation {
27 pub schema: &'static AnnotationSchema,
28 pub span: SourceSpan,
29 pub arguments: Vec<Argument>,
30 pub positional: Vec<(String, SourceSpan)>,
32}
33
34impl Annotation {
35 pub fn name(&self) -> &'static str {
36 self.schema.name
37 }
38
39 pub fn get(&self, name: &str) -> Option<&Value> {
40 self.arguments
41 .iter()
42 .find(|argument| argument.name == name)
43 .map(|argument| &argument.value)
44 }
45}
46
47#[derive(Clone, Debug, Serialize)]
49pub struct Applied {
50 pub annotation: Annotation,
51 pub modifiers: Vec<Annotation>,
52}
53
54#[derive(Clone, Debug, Serialize)]
55pub struct Target {
56 pub key: String,
57 pub id: Option<String>,
59 pub span: SourceSpan,
60}
61
62#[derive(Clone, Debug, Serialize)]
63pub struct Attachment {
64 pub target: Target,
65 pub annotations: Vec<Applied>,
66}
67
68#[derive(Clone, Debug, Default, Serialize)]
69pub struct Extraction {
70 pub attachments: Vec<Attachment>,
71 pub diagnostics: Vec<Diagnostic>,
72}
73
74pub fn extract(path: &RelPath, text: &str, registry: &Registry) -> Extraction {
75 let parsed = parse_clausewitz_content(game_path(path.as_str()), text);
76 if !parsed.diagnostics.is_empty() {
77 return Extraction {
78 attachments: Vec::new(),
79 diagnostics: parsed
80 .diagnostics
81 .into_iter()
82 .map(|diagnostic| {
83 Diagnostic::error(
84 Code::Parse,
85 SourceSpan::from_parser(path, &diagnostic.span),
86 diagnostic.message,
87 )
88 })
89 .collect(),
90 };
91 }
92 extract_parsed(path, text, &parsed.ast.statements, registry)
93}
94
95pub fn extract_parsed(
97 path: &RelPath,
98 text: &str,
99 statements: &[AstStatement],
100 registry: &Registry,
101) -> Extraction {
102 let mut out = Extraction::default();
103 let mut modifiers: Vec<Annotation> = Vec::new();
104 let mut primaries: Vec<Applied> = Vec::new();
105 let error = |code, span: &SpanRange, message: String| {
106 Diagnostic::error(code, SourceSpan::from_parser(path, span), message)
107 };
108 let mut index = 0;
109 while index < statements.len() {
110 match &statements[index] {
111 AstStatement::Comment {
112 text: comment,
113 span,
114 } => {
115 let Some(schema) = annotation_schema(comment, registry) else {
116 if let Some(name) = unknown_annotation(comment) {
117 out.diagnostics.push(Diagnostic {
118 severity: Severity::Warning,
119 ..error(
120 Code::UnknownAnnotation,
121 span,
122 format!("#{name}(...) is not a known annotation"),
123 )
124 });
125 }
126 index += 1;
127 continue;
128 };
129 let start = span.clone();
130 let annotation = read_annotation(statements, &mut index, text, schema.name)
131 .map_err(|message| vec![error(Code::Malformed, &start, message)])
132 .and_then(|raw| parse_arguments(path, schema, raw));
133 match annotation {
134 Err(diagnostics) => out.diagnostics.extend(diagnostics),
135 Ok(annotation) => match schema.kind {
136 AnnotationKind::Modifier { .. } => modifiers.push(annotation),
137 AnnotationKind::Primary => {
138 let mut applied = Applied {
139 annotation,
140 modifiers: Vec::new(),
141 };
142 for modifier in modifiers.drain(..) {
143 attach_modifier(&mut applied, modifier, &mut out.diagnostics);
144 }
145 primaries.push(applied);
146 }
147 },
148 }
149 }
150 statement => {
151 for modifier in modifiers.drain(..) {
152 let of = match modifier.schema.kind {
153 AnnotationKind::Modifier { of } => of,
154 AnnotationKind::Primary => unreachable!("only modifiers wait"),
155 };
156 out.diagnostics.push(Diagnostic::error(
157 Code::Orphan,
158 modifier.span.clone(),
159 format!("#{} must precede a #{of}", modifier.name()),
160 ));
161 }
162 if !primaries.is_empty() {
163 let target = target(path, statement);
164 let mut annotations = Vec::new();
165 for applied in primaries.drain(..) {
166 match check_target(statement, applied.annotation.schema) {
167 Ok(()) => annotations.push(applied),
168 Err(message) => out.diagnostics.push(Diagnostic::error(
169 Code::UnsupportedTarget,
170 applied.annotation.span.clone(),
171 message,
172 )),
173 }
174 }
175 if !annotations.is_empty() {
176 out.attachments.push(Attachment {
177 target,
178 annotations,
179 });
180 }
181 }
182 nested(path, statement, registry, &mut out.diagnostics);
183 index += 1;
184 }
185 }
186 }
187 for annotation in modifiers
188 .into_iter()
189 .chain(primaries.into_iter().map(|applied| applied.annotation))
190 {
191 out.diagnostics.push(Diagnostic::error(
192 Code::Orphan,
193 annotation.span.clone(),
194 format!("#{} has no following definition", annotation.name()),
195 ));
196 }
197 out
198}
199
200fn attach_modifier(applied: &mut Applied, modifier: Annotation, diagnostics: &mut Vec<Diagnostic>) {
201 let primary = applied.annotation.name();
202 if modifier.schema.kind != (AnnotationKind::Modifier { of: primary }) {
203 diagnostics.push(Diagnostic::error(
204 Code::Orphan,
205 modifier.span.clone(),
206 format!("#{} does not apply to #{primary}", modifier.name()),
207 ));
208 } else if applied
209 .modifiers
210 .iter()
211 .any(|existing| existing.name() == modifier.name())
212 {
213 diagnostics.push(Diagnostic::error(
214 Code::DuplicateModifier,
215 modifier.span.clone(),
216 format!(
217 "#{} is given twice for the same #{primary}",
218 modifier.name()
219 ),
220 ));
221 } else {
222 applied.modifiers.push(modifier);
223 }
224}
225
226pub fn annotation_schema(comment: &str, registry: &Registry) -> Option<&'static AnnotationSchema> {
230 let text = comment.trim_start();
231 registry.schemas.iter().find(|schema| {
232 text.strip_prefix(schema.name).is_some_and(|rest| {
233 rest.trim_start().starts_with('(')
234 || (schema.kind == AnnotationKind::Primary && rest.trim().is_empty())
235 })
236 })
237}
238
239fn unknown_annotation(comment: &str) -> Option<&str> {
241 let text = comment.trim_start();
242 let name = &text[..text.find('(')?];
243 (!name.is_empty()
244 && name
245 .bytes()
246 .all(|byte| byte.is_ascii_lowercase() || byte == b'_'))
247 .then_some(name)
248}
249
250fn nested(
251 path: &RelPath,
252 statement: &AstStatement,
253 registry: &Registry,
254 out: &mut Vec<Diagnostic>,
255) {
256 let value = match statement {
257 AstStatement::Assignment { value, .. } | AstStatement::Item { value, .. } => value,
258 AstStatement::Comment { .. } => return,
259 };
260 if let AstValue::Block { items, .. } = value {
261 for item in items {
262 match item {
263 AstStatement::Comment { text, span } => {
264 if let Some(schema) = annotation_schema(text, registry) {
265 out.push(Diagnostic::error(
266 Code::Nested,
267 SourceSpan::from_parser(path, span),
268 format!("#{} must precede a top-level definition", schema.name),
269 ));
270 }
271 }
272 _ => nested(path, item, registry, out),
273 }
274 }
275 }
276}
277
278fn statement_span(statement: &AstStatement) -> &SpanRange {
279 match statement {
280 AstStatement::Assignment { span, .. }
281 | AstStatement::Item { span, .. }
282 | AstStatement::Comment { span, .. } => span,
283 }
284}
285
286fn target(path: &RelPath, statement: &AstStatement) -> Target {
287 let (key, id) = match statement {
288 AstStatement::Assignment {
289 key,
290 value: AstValue::Block { items, .. },
291 ..
292 } => (key.clone(), single_scalar(items, "id")),
293 AstStatement::Assignment { key, .. } => (key.clone(), None),
294 _ => (String::new(), None),
295 };
296 Target {
297 key,
298 id,
299 span: SourceSpan::from_parser(path, statement_span(statement)),
300 }
301}
302
303fn single_scalar(items: &[AstStatement], name: &str) -> Option<String> {
304 let mut values = items.iter().filter_map(|item| match item {
305 AstStatement::Assignment { key, value, .. } if key == name => Some(value),
306 _ => None,
307 });
308 let value = values.next()?;
309 values
310 .next()
311 .is_none()
312 .then(|| scalar(value).ok())
313 .flatten()
314}
315
316fn check_target(statement: &AstStatement, schema: &AnnotationSchema) -> Result<(), String> {
317 let Some(rule) = schema.target else {
318 return Ok(());
319 };
320 let AstStatement::Assignment {
321 key,
322 value: AstValue::Block { items, .. },
323 ..
324 } = statement
325 else {
326 return Err(format!(
327 "#{} must be followed by a {} definition",
328 schema.name, rule.key
329 ));
330 };
331 if key != rule.key {
332 return Err(format!("#{} supports {}, not {key}", schema.name, rule.key));
333 }
334 for field in rule.required_yes {
335 let set = items
336 .iter()
337 .filter(|item| matches!(item, AstStatement::Assignment { key, .. } if key == field));
338 let values: Vec<_> = set.collect();
339 let yes = matches!(
340 values.as_slice(),
341 [AstStatement::Assignment {
342 value: AstValue::Scalar {
343 value: ScalarValue::Bool(true),
344 ..
345 },
346 ..
347 }]
348 );
349 if !yes {
350 return Err(format!(
351 "#{} requires the {} to declare {field} = yes exactly once",
352 schema.name, rule.key
353 ));
354 }
355 }
356 Ok(())
357}
358
359struct RawAnnotation {
360 span: SpanRange,
361 statements: Vec<AstStatement>,
362 source: String,
364}
365
366fn parse_arguments(
367 path: &RelPath,
368 schema: &'static AnnotationSchema,
369 raw: RawAnnotation,
370) -> Result<Annotation, Vec<Diagnostic>> {
371 let span = SourceSpan::from_parser(path, &raw.span);
372 let mut diagnostics = Vec::new();
373 let mut annotation = Annotation {
374 schema,
375 span: span.clone(),
376 arguments: Vec::new(),
377 positional: Vec::new(),
378 };
379 let mut given = Vec::new();
380 for statement in &raw.statements {
381 match statement {
382 AstStatement::Comment { .. } => {}
383 AstStatement::Assignment {
384 key,
385 key_span,
386 value,
387 ..
388 } => {
389 let key_span = SourceSpan::from_parser(path, key_span);
390 given.push(key.as_str());
391 let Some(param) = schema.param(key) else {
392 diagnostics.push(Diagnostic::error(
393 Code::UnknownParameter,
394 key_span,
395 format!("unknown #{} parameter {key}", schema.name),
396 ));
397 continue;
398 };
399 if given[..given.len() - 1].contains(&key.as_str()) {
400 diagnostics.push(Diagnostic::error(
401 Code::DuplicateParameter,
402 key_span,
403 format!("duplicate #{} parameter {key}", schema.name),
404 ));
405 continue;
406 }
407 match parse_value(path, &raw.source, value, param.value_type) {
408 Ok(parsed) => annotation.arguments.push(Argument {
409 name: key.clone(),
410 span: key_span,
411 value: parsed,
412 }),
413 Err(message) => diagnostics.push(Diagnostic::error(
414 Code::InvalidValue,
415 SourceSpan::from_parser(path, value.span()),
416 format!("{key}: {message}"),
417 )),
418 }
419 }
420 AstStatement::Item { value, span: item } => {
421 let item_span = SourceSpan::from_parser(path, item);
422 let label = scalar(value).ok().filter(|label| identifier(label));
423 match (schema.positional, label) {
424 (Some(_), Some(label)) => annotation.positional.push((label, item_span)),
425 (Some(_), None) => diagnostics.push(Diagnostic::error(
426 Code::InvalidValue,
427 item_span,
428 format!("#{} expects identifiers", schema.name),
429 )),
430 (None, _) => diagnostics.push(Diagnostic::error(
431 Code::Malformed,
432 item_span,
433 format!("#{} expects named key=value arguments", schema.name),
434 )),
435 }
436 }
437 }
438 }
439 for param in schema.params.iter().filter(|param| param.required) {
440 if !given.contains(¶m.name) {
441 diagnostics.push(Diagnostic::error(
442 Code::MissingParameter,
443 span.clone(),
444 format!("missing required #{} parameter {}", schema.name, param.name),
445 ));
446 }
447 }
448 if diagnostics.is_empty() {
449 Ok(annotation)
450 } else {
451 Err(diagnostics)
452 }
453}
454
455fn read_annotation(
458 statements: &[AstStatement],
459 index: &mut usize,
460 source: &str,
461 name: &str,
462) -> Result<RawAnnotation, String> {
463 let AstStatement::Comment { span: start, .. } = &statements[*index] else {
464 unreachable!("annotations start at a comment")
465 };
466 let first = *index;
467 let start = start.clone();
468 let mut virtual_source: Vec<u8> = source
469 .bytes()
470 .map(|byte| if byte == b'\n' { b'\n' } else { b' ' })
471 .collect();
472 let mut quote = false;
473 let mut escape = false;
474 let mut braces = 0usize;
475 let mut end_offset = None;
476 while *index < statements.len() {
477 let AstStatement::Comment { span, .. } = &statements[*index] else {
478 break;
479 };
480 let line_start = source[..span.start.offset]
481 .rfind('\n')
482 .map_or(0, |offset| offset + 1);
483 if !source[line_start..span.start.offset].trim().is_empty() {
484 *index += 1;
485 return Err(format!(
486 "#{name} and its continuation must occupy full comment lines"
487 ));
488 }
489 let mut content_start = span.start.offset + 1;
490 if *index == first {
491 let content = &source[content_start..span.end.offset];
492 let rest = content
493 .trim_start()
494 .strip_prefix(name)
495 .unwrap_or_default()
496 .trim_start();
497 if !rest.starts_with('(') {
498 *index += 1;
499 return Err(format!("expected #{name}(...)"));
500 }
501 content_start = span.end.offset - rest.len() + 1;
502 }
503 *index += 1;
504 if span.end.offset - start.start.offset > 65_536 {
505 return Err("annotation exceeds 64 KiB".into());
506 }
507 for offset in content_start..span.end.offset {
508 let byte = source.as_bytes()[offset];
509 if quote {
510 virtual_source[offset] = byte;
511 if escape {
512 escape = false;
513 } else if byte == b'\\' {
514 escape = true;
515 } else if byte == b'"' {
516 quote = false;
517 }
518 continue;
519 }
520 match byte {
521 b'#' => break,
522 b'"' => {
523 quote = true;
524 virtual_source[offset] = byte;
525 }
526 b'{' => {
527 braces += 1;
528 virtual_source[offset] = byte;
529 }
530 b'}' => {
531 braces = braces.checked_sub(1).ok_or("unmatched } in annotation")?;
532 virtual_source[offset] = byte;
533 }
534 b')' if braces == 0 => {
535 if !source[offset + 1..span.end.offset].trim().is_empty() {
536 return Err(format!("unexpected text after #{name}(...)"));
537 }
538 end_offset = Some(offset + 1);
539 break;
540 }
541 b'(' | b')' if braces == 0 => {
542 return Err("unexpected parenthesis in annotation arguments".into());
543 }
544 b',' if braces == 0 => {}
545 _ => virtual_source[offset] = byte,
546 }
547 }
548 if end_offset.is_some() {
549 break;
550 }
551 }
552 let end_offset = end_offset.ok_or_else(|| {
553 format!("unterminated #{name}(...); every continuation must be a comment")
554 })?;
555 virtual_source.truncate(end_offset);
556 let virtual_source =
557 String::from_utf8(virtual_source).map_err(|_| "invalid annotation encoding")?;
558 let parsed = parse_clausewitz_content(game_path("annotation.txt"), &virtual_source);
559 if let Some(error) = parsed.diagnostics.first() {
560 return Err(format!(
561 "invalid #{name} arguments at line {}: {}",
562 error.span.start.line, error.message
563 ));
564 }
565 let end = Span {
566 line: source[..end_offset]
567 .bytes()
568 .filter(|&byte| byte == b'\n')
569 .count() + 1,
570 column: end_offset
571 - source[..end_offset]
572 .rfind('\n')
573 .map_or(0, |offset| offset + 1)
574 + 1,
575 offset: end_offset,
576 };
577 Ok(RawAnnotation {
578 span: SpanRange {
579 start: start.start,
580 end,
581 },
582 statements: parsed.ast.statements,
583 source: virtual_source,
584 })
585}