ephemeris_documents/yaml/source/
text.rs1use std::{fmt, iter, mem};
4
5#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub(crate) struct Text(String);
8
9impl Text {
10 pub(crate) const fn is_empty(&self) -> bool {
11 self.0.is_empty()
12 }
13
14 pub(crate) const fn as_str(&self) -> &str {
15 self.0.as_str()
16 }
17
18 pub(crate) fn append(&mut self, other: &Self) {
19 self.0.push_str(&other.0);
20 }
21
22 pub(crate) fn terminate(&mut self) {
24 if !self.0.ends_with('\n') {
25 self.0.push('\n');
26 }
27 }
28
29 pub(crate) fn comment_start(line: &str) -> Option<usize> {
32 let hash = line.char_indices().find_map(|(index, c)| {
33 let before = line.get(..index)?;
34 (c == '#' && (before.is_empty() || before.ends_with([' ', '\t']))).then_some(index)
35 })?;
36 Some(line.get(..hash)?.trim_end_matches([' ', '\t']).len())
37 }
38
39 pub(crate) fn comment_in(line: &str) -> Option<&str> {
41 let start = Self::comment_start(line)?;
42 line.get(start..).map(str::trim_end)
43 }
44
45 pub(crate) fn take_before_comment_block(&mut self) -> Self {
49 let mut start = self.0.len();
50 for line in self.0.split_inclusive('\n').rev() {
51 if !line.trim_start().starts_with('#') {
52 break;
53 }
54 start = start.saturating_sub(line.len());
55 }
56 let rest = self.0.split_off(start);
57 Self(mem::replace(&mut self.0, rest))
58 }
59
60 pub(crate) fn reindent(&mut self, shift: Reindent, origin: Origin) {
63 if shift.is_noop() {
64 return;
65 }
66 let mut shifted = String::with_capacity(self.0.len());
67 for (index, line) in self.0.split_inclusive('\n').enumerate() {
68 if index == 0 && origin == Origin::MidLine {
69 shifted.push_str(line);
70 } else {
71 shift.line(line, &mut shifted);
72 }
73 }
74 self.0 = shifted;
75 }
76}
77
78impl From<&str> for Text {
79 fn from(text: &str) -> Self {
80 Self(text.to_owned())
81 }
82}
83
84impl From<String> for Text {
85 fn from(text: String) -> Self {
86 Self(text)
87 }
88}
89
90impl fmt::Display for Text {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.write_str(&self.0)
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub(crate) enum Origin {
99 LineStart,
100 MidLine,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub(crate) struct Reindent {
106 from: usize,
107 to: usize,
108}
109
110impl Reindent {
111 pub(crate) const fn new(from: usize, to: usize) -> Self {
112 Self { from, to }
113 }
114
115 pub(crate) const fn is_noop(self) -> bool {
116 self.from == self.to
117 }
118
119 fn line(self, line: &str, out: &mut String) {
122 if line.is_empty() || line == "\n" {
123 out.push_str(line);
124 return;
125 }
126 if let Some(added) = self.to.checked_sub(self.from) {
127 out.extend(iter::repeat_n(' ', added));
128 out.push_str(line);
129 return;
130 }
131 let removed = self.from.saturating_sub(self.to);
132 let spaces = line.bytes().take_while(|byte| *byte == b' ').count();
133 out.push_str(line.get(spaces.min(removed)..).unwrap_or_default());
134 }
135}