whiteout/parser/apply.rs
1use super::types::Decoration;
2
3/// Apply decorations to content
4pub fn apply_decorations(
5 content: &str,
6 decorations: &[Decoration],
7 use_local: bool,
8) -> String {
9 if decorations.is_empty() {
10 return content.to_string();
11 }
12
13 let lines: Vec<&str> = content.lines().collect();
14 let mut result = Vec::new();
15 let mut skip_until = 0;
16
17 for (idx, line) in lines.iter().enumerate() {
18 let line_num = idx + 1;
19
20 if line_num <= skip_until {
21 continue;
22 }
23
24 let mut line_processed = false;
25
26 // Check for block decorations
27 for decoration in decorations {
28 if let Decoration::Block { start_line, end_line, local_content, committed_content } = decoration {
29 if line_num == *start_line {
30 if use_local {
31 // Smudge: Check if this is a simple @whiteout or block with markers
32 let is_simple_pattern = line.contains("@whiteout") &&
33 !line.contains("@whiteout-start") &&
34 !line.contains("@whiteout:");
35
36 if is_simple_pattern {
37 // Simple @whiteout: Keep marker and show local content
38 result.push(line.to_string()); // Keep @whiteout marker
39 for content_line in local_content.lines() {
40 result.push(content_line.to_string());
41 }
42 // Skip to end of the block
43 skip_until = *end_line;
44 } else {
45 // Block with markers: Keep markers and show local content
46 result.push(line.to_string()); // Keep @whiteout-start
47 for content_line in local_content.lines() {
48 result.push(content_line.to_string());
49 }
50 // Find and add the end marker
51 if *end_line <= lines.len() {
52 result.push(lines[*end_line - 1].to_string()); // Keep @whiteout-end
53 }
54 // Skip the committed content that follows
55 skip_until = *end_line;
56
57 // Count lines of committed content to skip
58 let committed_lines = committed_content.lines().count();
59 if committed_lines > 0 {
60 skip_until += committed_lines;
61 }
62 }
63 } else {
64 // Clean: Check if this is a simple @whiteout or block with markers
65 let is_simple_pattern = line.contains("@whiteout") &&
66 !line.contains("@whiteout-start") &&
67 !line.contains("@whiteout:");
68
69 if is_simple_pattern {
70 // Simple @whiteout: Remove the marker and skip the local content
71 // Don't push the @whiteout marker line
72 // Skip all the local content lines
73 skip_until = *end_line;
74 } else {
75 // Block with @whiteout-start/end: Keep markers with empty content
76 result.push(line.to_string()); // Keep @whiteout-start
77 // No local content in between (it's been cleaned)
78
79 // Add the end marker
80 if *end_line <= lines.len() {
81 result.push(lines[*end_line - 1].to_string()); // Keep @whiteout-end
82 }
83
84 // Add the committed content that follows the block
85 if !committed_content.is_empty() {
86 for content_line in committed_content.lines() {
87 result.push(content_line.to_string());
88 }
89 }
90
91 // Skip to end of original block plus any following committed content
92 skip_until = *end_line + committed_content.lines().count();
93 }
94 }
95 line_processed = true;
96 break;
97 }
98 }
99 }
100
101 if line_processed {
102 continue;
103 }
104
105 // Check for inline decorations
106 let mut found_inline = false;
107 for decoration in decorations {
108 if let Decoration::Inline { line: dec_line, local_value, committed_value } = decoration {
109 if line_num == *dec_line {
110 if use_local {
111 // Smudge: Show local value with decoration
112 result.push(format!("{} // @whiteout: {}", local_value, committed_value));
113 } else {
114 // Clean: Show committed value WITH decoration marker for smudge to work
115 result.push(format!("{} // @whiteout: {}", committed_value, committed_value));
116 }
117 found_inline = true;
118 line_processed = true;
119 break;
120 }
121 }
122 }
123
124 if found_inline {
125 continue;
126 }
127
128 // Check for partial replacements
129 for decoration in decorations {
130 if let Decoration::Partial { line: dec_line, replacements } = decoration {
131 if line_num == *dec_line {
132 let mut processed_line = line.to_string();
133
134 for replacement in replacements.iter().rev() {
135 let new_value = if use_local {
136 // Smudge: Use local value in the pattern
137 format!("[[{}||{}]]",
138 replacement.local_value,
139 replacement.committed_value)
140 } else {
141 // Clean: Preserve pattern structure with committed value for smudge to work
142 format!("[[{}||{}]]",
143 replacement.committed_value.clone(),
144 replacement.committed_value)
145 };
146
147 if replacement.start < processed_line.len() {
148 processed_line.replace_range(
149 replacement.start..replacement.end.min(processed_line.len()),
150 &new_value
151 );
152 }
153 }
154
155 result.push(processed_line);
156 line_processed = true;
157 break;
158 }
159 }
160 }
161
162 if !line_processed {
163 result.push(line.to_string());
164 }
165 }
166
167 let mut output = result.join("\n");
168 // Preserve trailing newline if original had one
169 if content.ends_with('\n') && !output.ends_with('\n') {
170 output.push('\n');
171 }
172 output
173}