ephemeris_documents/
header.rs1use std::borrow::Cow;
4
5use crate::ownership::Ownership;
6use crate::text::CommentSyntax;
7
8pub(crate) const GENERATED: &str = "Generated by Ephemeris. Do not edit by hand.";
10
11#[derive(Debug, PartialEq, Eq)]
13pub(crate) struct Applied<'a> {
14 pub(crate) block: String,
15 pub(crate) rest: Cow<'a, str>,
16}
17
18impl Applied<'_> {
19 pub(crate) fn into_owned(self) -> Applied<'static> {
21 Applied {
22 block: self.block,
23 rest: Cow::Owned(self.rest.into_owned()),
24 }
25 }
26
27 pub(crate) fn into_text(self) -> String {
28 [self.block.as_str(), &self.rest].concat()
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub(crate) enum Existing {
35 Replace,
37 Keep,
39}
40
41impl Existing {
42 pub(crate) const fn under(ownership: &Ownership) -> Self {
44 if ownership.default_claim().is_owned() {
45 Self::Replace
46 } else {
47 Self::Keep
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy)]
58pub(crate) struct Header<'a>(&'a str);
59
60impl<'a> Header<'a> {
61 pub(crate) fn new(text: &'a str) -> Self {
63 Self(text.trim_end_matches(['\n', '\r', ' ', '\t']))
64 }
65
66 pub(crate) fn block(self) -> String {
69 if self.0.is_empty() {
70 String::new()
71 } else {
72 format!("{}\n\n", self.0)
73 }
74 }
75
76 pub(crate) fn applied_to(self, current: &str, existing: Existing) -> Option<Applied<'_>> {
81 let start = self.changed_from(current, existing)?;
82 Some(Applied {
83 block: self.block(),
84 rest: Cow::Borrowed(current.get(start..).unwrap_or_default()),
85 })
86 }
87
88 pub(crate) fn applied_to_parts<'c>(
91 self,
92 root: &str,
93 first: &'c str,
94 existing: Existing,
95 ) -> Option<Applied<'c>> {
96 if root.is_empty() {
97 return self.applied_to(first, existing);
98 }
99 let joined = [root, first].concat();
100 let start = self.changed_from(&joined, existing)?;
101 Some(Applied {
102 block: self.block(),
103 rest: Cow::Owned(joined.get(start..).unwrap_or_default().to_owned()),
104 })
105 }
106
107 fn changed_from(self, current: &str, existing: Existing) -> Option<usize> {
112 if self.0.is_empty() {
113 return None;
114 }
115 let len = Self::block_len(current);
116 let unchanged = current.trim_end() == self.0
117 || current
118 .get(..len)
119 .and_then(|old| old.strip_prefix(self.0))
120 .is_some_and(|tail| tail == "\n\n");
121 match existing {
122 _ if unchanged => None,
123 Existing::Replace => Some(len),
124 Existing::Keep => Some(0),
125 }
126 }
127
128 fn block_len(text: &str) -> usize {
132 let mut lines = text.split_inclusive('\n').peekable();
133 let mut len = 0usize;
134 while let Some(line) = lines.next_if(|line| !Self::is_blank(line)) {
135 len = len.saturating_add(line.len());
136 }
137 if len == 0 || lines.peek().is_none() {
138 return 0;
139 }
140 while let Some(line) = lines.next_if(|line| Self::is_blank(line)) {
141 len = len.saturating_add(line.len());
142 }
143 len
144 }
145
146 fn is_blank(line: &str) -> bool {
147 line.trim().is_empty()
148 }
149}
150
151#[must_use]
154pub fn is_generated(content: &[u8]) -> bool {
155 let first = content
156 .split(|&byte| byte == b'\n')
157 .next()
158 .unwrap_or_default();
159 let Ok(line) = str::from_utf8(first) else {
160 return false;
161 };
162 let line = line.trim();
163 CommentSyntax::ALL
164 .iter()
165 .filter_map(|syntax| syntax.uncomment(line))
166 .any(|text| text.trim() == GENERATED)
167}
168
169#[cfg(test)]
170mod tests;