1use std::sync::LazyLock;
2
3use regex::Regex;
4
5use crate::{
6 HasSpan, Parser, Span,
7 content::{Content, SubstitutionGroup},
8};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct RevisionLine<'src> {
16 revnumber: Option<String>,
17 revdate: String,
18 revremark: Option<String>,
19 source: Span<'src>,
20}
21
22impl<'src> RevisionLine<'src> {
23 pub(crate) fn parse(source: Span<'src>, parser: &mut Parser) -> Self {
24 let (left_of_colon, revremark) = if let Some((loc, remark)) = source.split_once(':') {
25 (loc.to_owned(), Some(remark.trim().to_owned()))
26 } else {
27 (source.data().to_owned(), None)
28 };
29
30 let (revnumber, revdate) = if let Some((rev, date)) = left_of_colon.split_once(',') {
31 let rev_trimmed = rev.trim();
33 let cleaned_rev = strip_non_numeric_prefix(rev_trimmed);
34 (Some(cleaned_rev), date.trim().to_owned())
35 } else {
36 let trimmed = left_of_colon.trim();
38 if is_valid_standalone_revision(trimmed) {
39 let cleaned_rev = strip_non_numeric_prefix(trimmed);
41 (Some(cleaned_rev), String::new())
42 } else {
43 (None, trimmed.to_owned())
45 }
46 };
47
48 let revnumber = revnumber.map(|s| {
54 let resolved = apply_header_subs(&s, parser);
55 parser.set_attribute_by_value_from_header("revnumber", &resolved);
56 resolved
57 });
58
59 let revdate = {
60 let resolved = apply_header_subs(&revdate, parser);
61 parser.set_attribute_by_value_from_header("revdate", &resolved);
62 resolved
63 };
64
65 let revremark = revremark.map(|s| {
66 let resolved = apply_header_subs(&s, parser);
67 parser.set_attribute_by_value_from_header("revremark", &resolved);
68 resolved
69 });
70
71 Self {
72 revnumber,
73 revdate,
74 revremark,
75 source,
76 }
77 }
78
79 pub fn revnumber(&self) -> Option<&str> {
90 self.revnumber.as_deref()
91 }
92
93 pub fn revdate(&self) -> &str {
101 &self.revdate
102 }
103
104 pub fn revremark(&self) -> Option<&str> {
110 self.revremark.as_deref()
111 }
112}
113
114impl<'src> HasSpan<'src> for RevisionLine<'src> {
115 fn span(&self) -> Span<'src> {
116 self.source
117 }
118}
119
120fn apply_header_subs(source: &str, parser: &Parser) -> String {
121 let span = Span::new(source);
122
123 let mut content = Content::from(span);
124 SubstitutionGroup::Header.apply(&mut content, parser, None);
125
126 content.rendered().to_string()
127}
128
129fn is_valid_standalone_revision(s: &str) -> bool {
130 STANDALONE_REVISION.is_match(s)
131}
132
133fn strip_non_numeric_prefix(s: &str) -> String {
134 NON_NUMERIC_PREFIX
135 .captures(s)
136 .and_then(|captures| captures.get(1))
137 .map_or_else(|| s.to_owned(), |m| m.as_str().to_owned())
138}
139
140static STANDALONE_REVISION: LazyLock<Regex> = LazyLock::new(|| {
141 #[allow(clippy::unwrap_used)]
142 Regex::new(r"^v\d").unwrap()
143});
144
145static NON_NUMERIC_PREFIX: LazyLock<Regex> = LazyLock::new(|| {
146 #[allow(clippy::unwrap_used)]
152 Regex::new(r"^[^0-9{]*(.*)$").unwrap()
153});
154
155#[cfg(test)]
156mod tests {
157 #![allow(clippy::unwrap_used)]
158
159 use crate::{Span, tests::prelude::*};
160
161 #[test]
162 fn v_prefix_standalone() {
163 let mut parser = Parser::default();
164 let result = crate::document::RevisionLine::parse(Span::new("v1.2.3"), &mut parser);
165
166 assert_eq!(result.revnumber(), Some("1.2.3"));
167 assert_eq!(result.revdate(), "");
168 assert_eq!(result.revremark(), None);
169 }
170
171 #[test]
172 fn standalone_number_without_v_prefix() {
173 let mut parser = Parser::default();
174 let result = crate::document::RevisionLine::parse(Span::new("1.2.3"), &mut parser);
175
176 assert_eq!(result.revnumber(), None);
179 assert_eq!(result.revdate(), "1.2.3");
180 assert_eq!(result.revremark(), None);
181 }
182
183 #[test]
184 fn other_prefix_standalone() {
185 let mut parser = Parser::default();
186 let result = crate::document::RevisionLine::parse(Span::new("LPR1.2.3"), &mut parser);
187
188 assert_eq!(result.revnumber(), None);
190 assert_eq!(result.revdate(), "LPR1.2.3");
191 assert_eq!(result.revremark(), None);
192 }
193
194 #[test]
195 fn v_prefix_with_comma_and_date() {
196 let mut parser = Parser::default();
197 let result =
198 crate::document::RevisionLine::parse(Span::new("v1.2.3, 2023-01-15"), &mut parser);
199
200 assert_eq!(result.revnumber(), Some("1.2.3"));
201 assert_eq!(result.revdate(), "2023-01-15");
202 assert_eq!(result.revremark(), None);
203 }
204
205 #[test]
206 fn other_prefix_with_comma_and_date() {
207 let mut parser = Parser::default();
208 let result =
209 crate::document::RevisionLine::parse(Span::new("LPR1.2.3, 2023-01-15"), &mut parser);
210
211 assert_eq!(result.revnumber(), Some("1.2.3"));
213 assert_eq!(result.revdate(), "2023-01-15");
214 assert_eq!(result.revremark(), None);
215 }
216
217 #[test]
218 fn revision_with_colon_and_remark() {
219 let mut parser = Parser::default();
220 let result =
221 crate::document::RevisionLine::parse(Span::new("v1.2.3: A great release"), &mut parser);
222
223 assert_eq!(result.revnumber(), Some("1.2.3"));
224 assert_eq!(result.revdate(), "");
225 assert_eq!(result.revremark(), Some("A great release"));
226 }
227
228 #[test]
229 fn full_revision_line() {
230 let mut parser = Parser::default();
231 let result = crate::document::RevisionLine::parse(
232 Span::new("v2.1.0, 2023-12-25: Christmas release"),
233 &mut parser,
234 );
235
236 assert_eq!(result.revnumber(), Some("2.1.0"));
237 assert_eq!(result.revdate(), "2023-12-25");
238 assert_eq!(result.revremark(), Some("Christmas release"));
239 }
240
241 #[test]
242 fn only_date() {
243 let mut parser = Parser::default();
244 let result = crate::document::RevisionLine::parse(Span::new("2023-01-15"), &mut parser);
245
246 assert_eq!(result.revnumber(), None);
248 assert_eq!(result.revdate(), "2023-01-15");
249 assert_eq!(result.revremark(), None);
250 }
251
252 #[test]
253 fn date_with_remark() {
254 let mut parser = Parser::default();
255 let result = crate::document::RevisionLine::parse(
256 Span::new("2023-01-15: New year update"),
257 &mut parser,
258 );
259
260 assert_eq!(result.revnumber(), None);
261 assert_eq!(result.revdate(), "2023-01-15");
262 assert_eq!(result.revremark(), Some("New year update"));
263 }
264
265 #[test]
266 fn whitespace_handling() {
267 let mut parser = Parser::default();
268 let result = crate::document::RevisionLine::parse(
269 Span::new(" v1.0.0 , Jan 1, 2023 : Initial release "),
270 &mut parser,
271 );
272
273 assert_eq!(result.revnumber(), Some("1.0.0"));
274 assert_eq!(result.revdate(), "Jan 1, 2023");
275 assert_eq!(result.revremark(), Some("Initial release"));
276 }
277
278 #[test]
279 fn v_only_no_digits() {
280 let mut parser = Parser::default();
281 let result = crate::document::RevisionLine::parse(Span::new("v"), &mut parser);
282
283 assert_eq!(result.revnumber(), None);
285 assert_eq!(result.revdate(), "v");
286 assert_eq!(result.revremark(), None);
287 }
288
289 #[test]
290 fn complex_version_with_v() {
291 let mut parser = Parser::default();
292 let result = crate::document::RevisionLine::parse(Span::new("v1.2.3-beta.1"), &mut parser);
293
294 assert_eq!(result.revnumber(), Some("1.2.3-beta.1"));
295 assert_eq!(result.revdate(), "");
296 assert_eq!(result.revremark(), None);
297 }
298
299 #[test]
300 fn numeric_prefix_stripped() {
301 let mut parser = Parser::default();
302 let result =
303 crate::document::RevisionLine::parse(Span::new("abc123def, 2023-01-01"), &mut parser);
304
305 assert_eq!(result.revnumber(), Some("123def"));
307 assert_eq!(result.revdate(), "2023-01-01");
308 assert_eq!(result.revremark(), None);
309 }
310
311 #[test]
312 fn no_numeric_content() {
313 let mut parser = Parser::default();
314 let result =
315 crate::document::RevisionLine::parse(Span::new("nodigits, 2023-01-01"), &mut parser);
316
317 assert_eq!(result.revnumber(), Some(""));
319 assert_eq!(result.revdate(), "2023-01-01");
320 assert_eq!(result.revremark(), None);
321 }
322
323 #[test]
324 fn comma_form_prefix_stops_at_attribute_reference() {
325 let mut parser = Parser::default();
331 let result =
332 crate::document::RevisionLine::parse(Span::new("v{draft}, 2024-01-01"), &mut parser);
333
334 assert_eq!(result.revnumber(), Some("{draft}"));
335 assert_eq!(result.revdate(), "2024-01-01");
336 assert_eq!(result.revremark(), None);
337 }
338
339 #[test]
340 fn comma_form_prefix_absorbs_escape_before_reference() {
341 let mut parser = Parser::default().with_intrinsic_attribute(
346 "draft",
347 "beta",
348 ModificationContext::Anywhere,
349 );
350 let result =
351 crate::document::RevisionLine::parse(Span::new("v\\{draft}1, 2024-01-01"), &mut parser);
352
353 assert_eq!(result.revnumber(), Some("beta1"));
354 assert_eq!(result.revdate(), "2024-01-01");
355 assert_eq!(result.revremark(), None);
356 }
357
358 #[test]
359 fn sets_document_attributes_with_all_components() {
360 let mut parser = Parser::default();
361 let _result = crate::document::RevisionLine::parse(
362 Span::new("v2.1.0, 2023-12-25: Christmas release"),
363 &mut parser,
364 );
365
366 assert_eq!(
367 parser.attribute_value("revnumber").as_maybe_str(),
368 Some("2.1.0")
369 );
370
371 assert_eq!(
372 parser.attribute_value("revdate").as_maybe_str(),
373 Some("2023-12-25")
374 );
375
376 assert_eq!(
377 parser.attribute_value("revremark").as_maybe_str(),
378 Some("Christmas release")
379 );
380 }
381
382 #[test]
383 fn sets_document_attributes_revision_number_only() {
384 let mut parser = Parser::default();
385 let _result = crate::document::RevisionLine::parse(Span::new("v1.2.3"), &mut parser);
386
387 assert_eq!(
388 parser.attribute_value("revnumber").as_maybe_str(),
389 Some("1.2.3")
390 );
391
392 assert_eq!(parser.attribute_value("revdate").as_maybe_str(), Some(""));
393 assert_eq!(parser.attribute_value("revremark").as_maybe_str(), None);
394 }
395
396 #[test]
397 fn sets_document_attributes_date_only() {
398 let mut parser = Parser::default();
399 let _result = crate::document::RevisionLine::parse(Span::new("2023-01-15"), &mut parser);
400
401 assert_eq!(parser.attribute_value("revnumber").as_maybe_str(), None);
402
403 assert_eq!(
404 parser.attribute_value("revdate").as_maybe_str(),
405 Some("2023-01-15")
406 );
407
408 assert_eq!(parser.attribute_value("revremark").as_maybe_str(), None);
409 }
410
411 #[test]
412 fn sets_document_attributes_date_with_remark() {
413 let mut parser = Parser::default();
414 let _result = crate::document::RevisionLine::parse(
415 Span::new("2023-01-15: New year update"),
416 &mut parser,
417 );
418
419 assert_eq!(parser.attribute_value("revnumber").as_maybe_str(), None);
420
421 assert_eq!(
422 parser.attribute_value("revdate").as_maybe_str(),
423 Some("2023-01-15")
424 );
425
426 assert_eq!(
427 parser.attribute_value("revremark").as_maybe_str(),
428 Some("New year update")
429 );
430 }
431
432 #[test]
433 fn sets_document_attributes_revision_with_date() {
434 let mut parser = Parser::default();
435 let _result =
436 crate::document::RevisionLine::parse(Span::new("v1.2.3, 2023-01-15"), &mut parser);
437
438 assert_eq!(
439 parser.attribute_value("revnumber").as_maybe_str(),
440 Some("1.2.3")
441 );
442
443 assert_eq!(
444 parser.attribute_value("revdate").as_maybe_str(),
445 Some("2023-01-15")
446 );
447
448 assert_eq!(parser.attribute_value("revremark").as_maybe_str(), None);
449 }
450
451 #[test]
452 fn sets_document_attributes_revision_with_remark_only() {
453 let mut parser = Parser::default();
454 let _result =
455 crate::document::RevisionLine::parse(Span::new("v1.2.3: A great release"), &mut parser);
456
457 assert_eq!(
458 parser.attribute_value("revnumber").as_maybe_str(),
459 Some("1.2.3")
460 );
461
462 assert_eq!(parser.attribute_value("revdate").as_maybe_str(), Some(""));
463
464 assert_eq!(
465 parser.attribute_value("revremark").as_maybe_str(),
466 Some("A great release")
467 );
468 }
469
470 #[test]
471 fn sets_document_attributes_with_whitespace_handling() {
472 let mut parser = Parser::default();
473 let _result = crate::document::RevisionLine::parse(
474 Span::new(" v1.0.0 , Jan 1, 2023 : Initial release "),
475 &mut parser,
476 );
477
478 assert_eq!(
479 parser.attribute_value("revnumber").as_maybe_str(),
480 Some("1.0.0")
481 );
482
483 assert_eq!(
484 parser.attribute_value("revdate").as_maybe_str(),
485 Some("Jan 1, 2023")
486 );
487
488 assert_eq!(
489 parser.attribute_value("revremark").as_maybe_str(),
490 Some("Initial release")
491 );
492 }
493
494 #[test]
495 fn sets_document_attributes_with_prefix_stripping() {
496 let mut parser = Parser::default();
497 let _result =
498 crate::document::RevisionLine::parse(Span::new("abc123def, 2023-01-01"), &mut parser);
499
500 assert_eq!(
501 parser.attribute_value("revnumber").as_maybe_str(),
502 Some("123def")
503 );
504
505 assert_eq!(
506 parser.attribute_value("revdate").as_maybe_str(),
507 Some("2023-01-01")
508 );
509
510 assert_eq!(parser.attribute_value("revremark").as_maybe_str(), None);
511 }
512
513 #[test]
514 fn sets_document_attributes_complex_version() {
515 let mut parser = Parser::default();
516 let _result = crate::document::RevisionLine::parse(Span::new("v1.2.3-beta.1"), &mut parser);
517
518 assert_eq!(
519 parser.attribute_value("revnumber").as_maybe_str(),
520 Some("1.2.3-beta.1")
521 );
522
523 assert_eq!(parser.attribute_value("revdate").as_maybe_str(), Some(""));
524 assert_eq!(parser.attribute_value("revremark").as_maybe_str(), None);
525 }
526}