1use std::fmt;
4
5#[must_use]
7pub fn wrap_comment(text: &str, width: usize) -> Vec<String> {
8 let effective = if width > 2 { width - 2 } else { width };
9 let mut lines = Vec::new();
10 for paragraph in text.split('\n') {
11 let paragraph = paragraph.trim();
12 if paragraph.is_empty() {
13 lines.push("# ".to_string());
14 continue;
15 }
16 let words: Vec<&str> = paragraph.split_whitespace().collect();
17 let mut current_line = String::new();
18 for word in words {
19 if current_line.is_empty() {
20 current_line.push_str(word);
21 } else if current_line.len() + 1 + word.len() <= effective {
22 current_line.push(' ');
23 current_line.push_str(word);
24 } else {
25 lines.push(format!("# {current_line}"));
26 current_line = word.to_string();
27 }
28 }
29 if !current_line.is_empty() {
30 lines.push(format!("# {current_line}"));
31 }
32 }
33 lines
34}
35
36#[derive(Debug, Clone)]
38pub struct RncSchema {
39 pub header_comments: Vec<String>,
40 pub namespaces: Vec<RncNamespace>,
41 pub layers: Vec<RncLayer>,
42}
43
44#[derive(Debug, Clone)]
46pub struct RncNamespace {
47 pub prefix: String,
48 pub uri: String,
49}
50
51#[derive(Debug, Clone)]
53pub struct RncLayer {
54 pub name: String,
55 pub prefix: String,
56 pub description: Option<String>,
57 pub patterns: Vec<RncPattern>,
58 pub elements: Vec<RncGlobalElement>,
59 pub enum_summaries: Vec<RncEnumSummary>,
60}
61
62#[derive(Debug, Clone)]
64pub struct RncPattern {
65 pub name: String,
66 pub body: Vec<RncBodyItem>,
67 pub description: Option<String>,
68}
69
70#[derive(Debug, Clone)]
72pub struct RncGlobalElement {
73 pub prefix: String,
74 pub name: String,
75 pub body: Vec<RncBodyItem>,
76 pub description: Option<String>,
77}
78
79#[derive(Debug, Clone)]
81pub struct RncEnumSummary {
82 pub type_name: String,
83 pub values: Vec<String>,
84}
85
86#[derive(Debug, Clone)]
88pub enum RncBodyItem {
89 Attribute(RncAttribute),
90 Element(RncElement),
91 PatternRef(String),
92 Choice {
93 options: Vec<RncBodyItem>,
94 quantifier: RncQuantifier,
95 },
96 Mixed(Vec<RncBodyItem>),
97 Type(String),
98 Empty,
99 AnyElement(RncQuantifier),
100 PatternedText(String),
101 InlineEnum(Vec<String>),
102}
103
104#[derive(Debug, Clone)]
106pub struct RncAttribute {
107 pub name: String,
108 pub type_str: String,
109 pub quantifier: RncQuantifier,
110 pub default: Option<String>,
111}
112
113#[derive(Debug, Clone)]
115pub struct RncElement {
116 pub prefix: String,
117 pub name: String,
118 pub body: Vec<RncBodyItem>,
119 pub quantifier: RncQuantifier,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum RncQuantifier {
125 One,
126 Optional,
127 ZeroOrMore,
128 OneOrMore,
129}
130
131impl fmt::Display for RncQuantifier {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::One => Ok(()),
137 Self::Optional => f.write_str("?"),
138 Self::ZeroOrMore => f.write_str("*"),
139 Self::OneOrMore => f.write_str("+"),
140 }
141 }
142}
143
144impl fmt::Display for RncAttribute {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 write!(
147 f,
148 "attribute {} {{ {} }}{}",
149 self.name, self.type_str, self.quantifier
150 )?;
151 if let Some(ref d) = self.default {
152 write!(f, " # default: {d}")?;
153 }
154 Ok(())
155 }
156}
157
158impl fmt::Display for RncElement {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 let ns_name = format!("{}:{}", self.prefix, self.name);
161 let inner = format_body_items(&self.body);
162 write!(f, "element {ns_name} {{ {inner} }}{}", self.quantifier)
163 }
164}
165
166impl fmt::Display for RncBodyItem {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 match self {
169 Self::Attribute(a) => write!(f, "{a}"),
170 Self::Element(e) => write!(f, "{e}"),
171 Self::PatternRef(name) => write!(f, "{name}"),
172 Self::Choice {
173 options,
174 quantifier,
175 } => {
176 let opts: Vec<String> = options.iter().map(ToString::to_string).collect();
177 write!(f, "({}){quantifier}", opts.join(" | "))
178 }
179 Self::Mixed(items) => {
180 let inner = format_body_items(items);
181 write!(f, "mixed {{ {inner} }}")
182 }
183 Self::Type(t) => write!(f, "{t}"),
184 Self::Empty => write!(f, "empty"),
185 Self::AnyElement(q) => write!(f, "anyElement{q}"),
186 Self::PatternedText(pat) => write!(f, "text # pattern: {pat}"),
187 Self::InlineEnum(vals) => {
188 let parts: Vec<String> = vals.iter().map(|v| format!("\"{v}\"")).collect();
189 write!(f, "{}", parts.join(" | "))
190 }
191 }
192 }
193}
194
195fn format_body_items(items: &[RncBodyItem]) -> String {
196 items
197 .iter()
198 .map(ToString::to_string)
199 .collect::<Vec<_>>()
200 .join(", ")
201}
202
203impl fmt::Display for RncEnumSummary {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(f, "# {}: {}", self.type_name, self.values.join(" | "))
206 }
207}
208
209impl fmt::Display for RncNamespace {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 write!(f, "namespace {} = \"{}\"", self.prefix, self.uri)
212 }
213}
214
215impl fmt::Display for RncPattern {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 if let Some(ref desc) = self.description {
218 for line in wrap_comment(desc, 78) {
219 writeln!(f, "{line}")?;
220 }
221 }
222 let body_str = format_body_items(&self.body);
223 write!(f, "{} = {body_str}", self.name)
224 }
225}
226
227impl fmt::Display for RncGlobalElement {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 if let Some(ref desc) = self.description {
230 for line in wrap_comment(desc, 78) {
231 writeln!(f, "{line}")?;
232 }
233 }
234 let ns_name = format!("{}:{}", self.prefix, self.name);
235 let flat = format_body_items(&self.body);
236 if flat.len() < 70 {
237 write!(f, "{ns_name} = element {ns_name} {{ {flat} }}")
238 } else {
239 writeln!(f, "{ns_name} = element {ns_name} {{")?;
240 for item in &self.body {
241 writeln!(f, " {item}")?;
242 }
243 write!(f, "}}")
244 }
245 }
246}
247
248impl fmt::Display for RncLayer {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 writeln!(f, "# {}", "=".repeat(50))?;
251 writeln!(f, "# {} LAYER ({}:)", self.name.to_uppercase(), self.prefix)?;
252 if let Some(ref desc) = self.description {
253 for line in wrap_comment(desc, 78) {
254 writeln!(f, "{line}")?;
255 }
256 }
257 writeln!(f)?;
258
259 for pat in &self.patterns {
260 writeln!(f, "{pat}")?;
261 writeln!(f)?;
262 }
263
264 for elem in &self.elements {
265 writeln!(f, "{elem}")?;
266 writeln!(f)?;
267 }
268
269 if !self.enum_summaries.is_empty() {
270 for es in &self.enum_summaries {
271 writeln!(f, "{es}")?;
272 }
273 writeln!(f)?;
274 }
275 Ok(())
276 }
277}
278
279impl fmt::Display for RncSchema {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 for comment in &self.header_comments {
282 writeln!(f, "# {comment}")?;
283 }
284 writeln!(f)?;
285
286 for ns in &self.namespaces {
287 writeln!(f, "{ns}")?;
288 }
289 writeln!(f)?;
290
291 for layer in &self.layers {
292 write!(f, "{layer}")?;
293 }
294 Ok(())
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn quantifier_display() {
304 assert_eq!(RncQuantifier::One.to_string(), "");
305 assert_eq!(RncQuantifier::Optional.to_string(), "?");
306 assert_eq!(RncQuantifier::ZeroOrMore.to_string(), "*");
307 assert_eq!(RncQuantifier::OneOrMore.to_string(), "+");
308 }
309
310 #[test]
311 fn attribute_display_required() {
312 let attr = RncAttribute {
313 name: "id".to_string(),
314 type_str: "xsd:ID".to_string(),
315 quantifier: RncQuantifier::One,
316 default: None,
317 };
318 assert_eq!(attr.to_string(), "attribute id { xsd:ID }");
319 }
320
321 #[test]
322 fn attribute_display_optional_with_default() {
323 let attr = RncAttribute {
324 name: "type".to_string(),
325 type_str: "text".to_string(),
326 quantifier: RncQuantifier::Optional,
327 default: Some("info".to_string()),
328 };
329 assert_eq!(
330 attr.to_string(),
331 "attribute type { text }? # default: info"
332 );
333 }
334
335 #[test]
336 fn element_display() {
337 let elem = RncElement {
338 prefix: "pr".to_string(),
339 name: "title".to_string(),
340 body: vec![RncBodyItem::Type("text".to_string())],
341 quantifier: RncQuantifier::One,
342 };
343 assert_eq!(elem.to_string(), "element pr:title { text }");
344 }
345
346 #[test]
347 fn element_display_optional() {
348 let elem = RncElement {
349 prefix: "pr".to_string(),
350 name: "shortdesc".to_string(),
351 body: vec![RncBodyItem::Type("text".to_string())],
352 quantifier: RncQuantifier::Optional,
353 };
354 assert_eq!(elem.to_string(), "element pr:shortdesc { text }?");
355 }
356
357 #[test]
358 fn body_item_choice() {
359 let choice = RncBodyItem::Choice {
360 options: vec![
361 RncBodyItem::Element(RncElement {
362 prefix: "pr".to_string(),
363 name: "p".to_string(),
364 body: vec![RncBodyItem::Type("text".to_string())],
365 quantifier: RncQuantifier::One,
366 }),
367 RncBodyItem::Element(RncElement {
368 prefix: "pr".to_string(),
369 name: "ul".to_string(),
370 body: vec![RncBodyItem::Empty],
371 quantifier: RncQuantifier::One,
372 }),
373 ],
374 quantifier: RncQuantifier::ZeroOrMore,
375 };
376 assert_eq!(
377 choice.to_string(),
378 "(element pr:p { text } | element pr:ul { empty })*"
379 );
380 }
381
382 #[test]
383 fn body_item_mixed() {
384 let mixed = RncBodyItem::Mixed(vec![RncBodyItem::Type("text".to_string())]);
385 assert_eq!(mixed.to_string(), "mixed { text }");
386 }
387
388 #[test]
389 fn body_item_any_element() {
390 assert_eq!(
391 RncBodyItem::AnyElement(RncQuantifier::ZeroOrMore).to_string(),
392 "anyElement*"
393 );
394 }
395
396 #[test]
397 fn body_item_patterned_text() {
398 let pt = RncBodyItem::PatternedText("[a-z]+".to_string());
399 assert_eq!(pt.to_string(), "text # pattern: [a-z]+");
400 }
401
402 #[test]
403 fn body_item_inline_enum() {
404 let ie = RncBodyItem::InlineEnum(vec!["info".to_string(), "warning".to_string()]);
405 assert_eq!(ie.to_string(), "\"info\" | \"warning\"");
406 }
407
408 #[test]
409 fn namespace_display() {
410 let ns = RncNamespace {
411 prefix: "pr".to_string(),
412 uri: "urn:clayers:prose".to_string(),
413 };
414 assert_eq!(ns.to_string(), "namespace pr = \"urn:clayers:prose\"");
415 }
416
417 #[test]
418 fn global_element_single_line() {
419 let elem = RncGlobalElement {
420 prefix: "org".to_string(),
421 name: "concept".to_string(),
422 body: vec![
423 RncBodyItem::Attribute(RncAttribute {
424 name: "ref".to_string(),
425 type_str: "text".to_string(),
426 quantifier: RncQuantifier::One,
427 default: None,
428 }),
429 RncBodyItem::Type("text".to_string()),
430 ],
431 description: None,
432 };
433 assert_eq!(
434 elem.to_string(),
435 "org:concept = element org:concept { attribute ref { text }, text }"
436 );
437 }
438
439 #[test]
440 fn global_element_multi_line() {
441 let elem = RncGlobalElement {
442 prefix: "pr".to_string(),
443 name: "section".to_string(),
444 body: vec![
445 RncBodyItem::Attribute(RncAttribute {
446 name: "id".to_string(),
447 type_str: "xsd:ID".to_string(),
448 quantifier: RncQuantifier::One,
449 default: None,
450 }),
451 RncBodyItem::Element(RncElement {
452 prefix: "pr".to_string(),
453 name: "title".to_string(),
454 body: vec![RncBodyItem::Type("text".to_string())],
455 quantifier: RncQuantifier::One,
456 }),
457 RncBodyItem::Choice {
458 options: vec![
459 RncBodyItem::Element(RncElement {
460 prefix: "pr".to_string(),
461 name: "p".to_string(),
462 body: vec![RncBodyItem::Type("text".to_string())],
463 quantifier: RncQuantifier::One,
464 }),
465 RncBodyItem::Element(RncElement {
466 prefix: "pr".to_string(),
467 name: "section".to_string(),
468 body: vec![RncBodyItem::PatternRef("SectionType".to_string())],
469 quantifier: RncQuantifier::One,
470 }),
471 ],
472 quantifier: RncQuantifier::ZeroOrMore,
473 },
474 ],
475 description: None,
476 };
477 let s = elem.to_string();
478 assert!(s.contains("pr:section = element pr:section {"));
479 assert!(s.contains('}'));
480 }
481
482 #[test]
483 fn pattern_with_description() {
484 let pat = RncPattern {
485 name: "SectionType".to_string(),
486 body: vec![RncBodyItem::Type("text".to_string())],
487 description: Some("A structural section.".to_string()),
488 };
489 let s = pat.to_string();
490 assert!(s.contains("# A structural section."));
491 assert!(s.contains("SectionType = text"));
492 }
493
494 #[test]
495 fn enum_summary_display() {
496 let es = RncEnumSummary {
497 type_name: "NoteKind".to_string(),
498 values: vec![
499 "info".to_string(),
500 "important".to_string(),
501 "warning".to_string(),
502 ],
503 };
504 assert_eq!(es.to_string(), "# NoteKind: info | important | warning");
505 }
506
507 #[test]
508 fn wrap_comment_short() {
509 let lines = wrap_comment("Short text.", 78);
510 assert_eq!(lines, vec!["# Short text."]);
511 }
512
513 #[test]
514 fn wrap_comment_long() {
515 let text = "This is a very long description that should be wrapped across multiple lines because it exceeds the maximum width.";
516 let lines = wrap_comment(text, 40);
517 assert!(lines.len() > 1);
518 for line in &lines {
519 assert!(line.starts_with("# "));
520 }
521 }
522
523 #[test]
524 fn schema_display() {
525 let schema = RncSchema {
526 header_comments: vec!["Test schema".to_string()],
527 namespaces: vec![RncNamespace {
528 prefix: "pr".to_string(),
529 uri: "urn:test:prose".to_string(),
530 }],
531 layers: vec![],
532 };
533 let s = schema.to_string();
534 assert!(s.contains("# Test schema"));
535 assert!(s.contains("namespace pr = \"urn:test:prose\""));
536 }
537}