1use std::path::Path;
8
9use crate::containment::PathGuard;
10use crate::ops;
11use crate::plan::Operation;
12
13use super::{ApplyMode, EditResult};
14
15#[cfg(any(feature = "cli", feature = "files"))]
17fn md_write(
18 op: Operation,
19 path: &Path,
20 mode: ApplyMode,
21 guard: Option<&PathGuard>,
22 action: &'static str,
23) -> anyhow::Result<EditResult> {
24 let abs = super::library_abs_path(path, guard)?;
25 let mut op = op;
26 rewrite_md_op_path(&mut op, &super::library_op_path(path, &abs, guard));
27 let display = path.to_string_lossy();
28 super::execute_as_edit_result_with_path(
29 op,
30 mode,
31 super::library_project_root(&abs, guard),
32 guard,
33 action,
34 None,
35 Some(display.as_ref()),
36 )
37}
38
39#[cfg(any(feature = "cli", feature = "files"))]
40fn rewrite_md_op_path(op: &mut Operation, dest: &str) {
41 match op {
42 Operation::MdReplaceSection { path, .. }
43 | Operation::MdInsertAfterHeading { path, .. }
44 | Operation::MdInsertAfterSection { path, .. }
45 | Operation::MdInsertBeforeHeading { path, .. }
46 | Operation::MdUpsertBullet { path, .. }
47 | Operation::MdTableAppend { path, .. }
48 | Operation::MdMoveSection { path, .. }
49 | Operation::MdDedupeHeadings { path, .. }
50 | Operation::MdLintAgents { path, .. } => {
51 *path = dest.into();
52 }
53 _ => {}
54 }
55}
56
57#[cfg(not(any(feature = "cli", feature = "files")))]
58fn md_write(
59 _op: Operation,
60 path: &Path,
61 mode: ApplyMode,
62 guard: Option<&PathGuard>,
63 action: &'static str,
64) -> anyhow::Result<EditResult> {
65 use crate::write::WritePolicy;
68
69 let display = path.to_string_lossy();
72 let path_owned = super::library_abs_path(path, guard)?;
73 let path = path_owned.as_path();
74 let path_str = display;
75 let original = crate::files::load_text_strict(path, &path_str)?;
76
77 let new_content = match _op {
78 Operation::MdReplaceSection {
79 heading, content, ..
80 } => ops::md::replace_section_in(&original, &heading, &content)
81 .map_err(|e| e.into_anyhow(&heading))?,
82 Operation::MdInsertAfterHeading {
83 heading, content, ..
84 } => ops::md::insert_after_heading_in(&original, &heading, &content)
85 .map_err(|e| e.into_anyhow(&heading))?,
86 Operation::MdInsertAfterSection {
87 heading, content, ..
88 } => ops::md::insert_after_section_in(&original, &heading, &content)
89 .map_err(|e| e.into_anyhow(&heading))?,
90 Operation::MdInsertBeforeHeading {
91 heading, content, ..
92 } => ops::md::insert_before_heading_in(&original, &heading, &content)
93 .map_err(|e| e.into_anyhow(&heading))?,
94 Operation::MdUpsertBullet {
95 heading, bullet, ..
96 } => ops::md::upsert_bullet_in(&original, &heading, &bullet)
97 .map_err(|e| e.into_anyhow(&heading))?,
98 Operation::MdTableAppend { heading, row, .. } => {
99 let (body_start, body_end) =
100 ops::md::find_section(&original, &heading).map_err(|e| e.into_anyhow(&heading))?;
101 return match ops::md::table_append_in(&original, body_start, body_end, &row) {
102 Ok(new_content) => {
103 let policy = WritePolicy::default();
104 let (applied, backup_session) =
105 super::write_if_apply(path, &new_content, mode, &policy, guard)?;
106 let mut edit = super::build_edit_result(
107 &path_str,
108 original,
109 new_content,
110 applied,
111 action,
112 None,
113 );
114 edit.backup_session = backup_session;
115 Ok(edit)
116 }
117 Err(e) => Err(anyhow::Error::new(crate::exit::InvalidInputError {
118 msg: format!("{e} under heading {heading:?} in {path_str}"),
119 })),
120 };
121 }
122 _ => {
123 return Err(anyhow::Error::new(crate::exit::InvalidInputError {
124 msg: format!("unsupported md operation in no-cli path for {path_str}"),
125 }));
126 }
127 };
128
129 let policy = WritePolicy::default();
130 let (applied, backup_session) =
131 super::write_if_apply(path, &new_content, mode, &policy, guard)?;
132 let mut edit =
133 super::build_edit_result(&path_str, original, new_content, applied, action, None);
134 edit.backup_session = backup_session;
135 Ok(edit)
136}
137
138pub fn md_replace_section(
140 path: &Path,
141 heading: &str,
142 content: &str,
143 mode: ApplyMode,
144 guard: Option<&PathGuard>,
145) -> anyhow::Result<EditResult> {
146 let op = Operation::MdReplaceSection {
147 path: path.to_string_lossy().into(),
148 heading: heading.into(),
149 content: content.into(),
150 };
151 md_write(op, path, mode, guard, "md.replace_section")
152}
153
154pub fn md_upsert_bullet(
158 path: &Path,
159 heading: &str,
160 bullet: &str,
161 mode: ApplyMode,
162 guard: Option<&PathGuard>,
163) -> anyhow::Result<EditResult> {
164 let op = Operation::MdUpsertBullet {
165 path: path.to_string_lossy().into(),
166 heading: heading.into(),
167 bullet: bullet.into(),
168 };
169 md_write(op, path, mode, guard, "md.upsert_bullet")
170}
171
172pub fn md_table_append(
174 path: &Path,
175 heading: &str,
176 row: &str,
177 mode: ApplyMode,
178 guard: Option<&PathGuard>,
179) -> anyhow::Result<EditResult> {
180 let op = Operation::MdTableAppend {
181 path: path.to_string_lossy().into(),
182 heading: heading.into(),
183 row: row.into(),
184 };
185 md_write(op, path, mode, guard, "md.table_append")
186}
187
188pub fn md_insert_after_heading(
190 path: &Path,
191 heading: &str,
192 insertion: &str,
193 mode: ApplyMode,
194 guard: Option<&PathGuard>,
195) -> anyhow::Result<EditResult> {
196 let op = Operation::MdInsertAfterHeading {
197 path: path.to_string_lossy().into(),
198 heading: heading.into(),
199 content: insertion.into(),
200 };
201 md_write(op, path, mode, guard, "md.insert_after_heading")
202}
203
204pub fn md_insert_after_section(
206 path: &Path,
207 heading: &str,
208 insertion: &str,
209 mode: ApplyMode,
210 guard: Option<&PathGuard>,
211) -> anyhow::Result<EditResult> {
212 let op = Operation::MdInsertAfterSection {
213 path: path.to_string_lossy().into(),
214 heading: heading.into(),
215 content: insertion.into(),
216 };
217 md_write(op, path, mode, guard, "md.insert_after_section")
218}
219
220pub fn md_move_section(
229 path: &Path,
230 heading: &str,
231 position: (&str, &str),
232 to: Option<&Path>,
233 mode: ApplyMode,
234 guard: Option<&PathGuard>,
235) -> anyhow::Result<EditResult> {
236 let display = path.to_string_lossy().into_owned();
240 let path_owned = super::library_abs_path(path, guard)?;
241 let dest_owned = match to {
242 Some(dest_path) => Some(super::library_abs_path(dest_path, guard)?),
243 None => None,
244 };
245 let path = path_owned.as_path();
246 let to = dest_owned.as_deref();
247 let path_str = display;
248 let original = crate::files::load_text_strict(path, &path_str)?;
249
250 let dest_content = match to {
251 Some(dest_path) => {
252 let d = dest_path.to_string_lossy();
253 crate::files::load_text_strict(dest_path, &d)?
254 }
255 None => original.clone(),
256 };
257
258 let (new_source, new_dest) =
259 ops::md::move_section_in(&original, heading, &dest_content, position, to.is_none())
260 .map_err(|e| e.into_anyhow(heading, position.1))?;
261
262 let policy = crate::write::WritePolicy::default();
263 let dest = to.map(|p| p.to_string_lossy().to_string());
264
265 let (applied, backup_session) = if let Some(dest_path) = to {
268 let backup_root = super::library_project_root(path, guard);
269 let files: [(&Path, &str); 2] =
270 [(dest_path, new_dest.as_str()), (path, new_source.as_str())];
271 super::write_if_apply_many(&files, mode, &policy, guard, backup_root)?
272 } else {
273 super::write_if_apply(path, &new_source, mode, &policy, guard)?
274 };
275
276 let mut edit =
277 super::build_edit_result(&path_str, original, new_source, applied, "md.move", dest);
278 edit.backup_session = backup_session;
279 Ok(edit)
280}
281
282pub fn md_dedupe_headings(
288 path: &Path,
289 mode: ApplyMode,
290 guard: Option<&PathGuard>,
291) -> anyhow::Result<(EditResult, Vec<String>)> {
292 let display = path.to_string_lossy();
293 let path_owned = super::library_abs_path(path, guard)?;
294 let path = path_owned.as_path();
295 let path_str = display;
296 let original = crate::files::load_text_strict(path, &path_str)?;
297
298 let (new_content, removed) = ops::md::dedupe_headings_in(&original);
299
300 let policy = crate::write::WritePolicy::default();
301 let (applied, backup_session) =
302 super::write_if_apply(path, &new_content, mode, &policy, guard)?;
303 let mut edit =
304 super::build_edit_result(&path_str, original, new_content, applied, "md.dedupe", None);
305 edit.backup_session = backup_session;
306 Ok((edit, removed))
307}
308
309pub use crate::ops::md::LintIssue;
314
315pub fn md_lint_agents(path: &Path) -> anyhow::Result<Vec<LintIssue>> {
320 let display = path.to_string_lossy();
321 let content = crate::files::load_text_strict(path, &display)?;
322 Ok(crate::ops::md::lint_agents_content(&content))
323}
324
325pub fn md_insert_before_heading(
327 path: &Path,
328 heading: &str,
329 insertion: &str,
330 mode: ApplyMode,
331 guard: Option<&PathGuard>,
332) -> anyhow::Result<EditResult> {
333 let op = Operation::MdInsertBeforeHeading {
334 path: path.to_string_lossy().into(),
335 heading: heading.into(),
336 content: insertion.into(),
337 };
338 md_write(op, path, mode, guard, "md.insert_before_heading")
339}