novel_cli/cmd/
transform.rs1use std::fmt::Write;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use clap::Args;
6use color_eyre::eyre::{self, Result};
7use fluent_templates::Loader;
8use pulldown_cmark::{Event, MetadataBlockKind, Options, Parser, Tag, TagEnd, TextMergeWithOffset};
9use walkdir::WalkDir;
10
11use crate::cmd::Convert;
12use crate::utils::{self, Metadata};
13use crate::{LANG_ID, LOCALES};
14
15#[must_use]
16#[derive(Args)]
17#[command(arg_required_else_help = true,
18 about = LOCALES.lookup(&LANG_ID, "transform_command"))]
19pub struct Transform {
20 #[arg(help = LOCALES.lookup(&LANG_ID, "file_path"))]
21 pub file_path: PathBuf,
22
23 #[arg(short, long, value_enum, value_delimiter = ',',
24 help = LOCALES.lookup(&LANG_ID, "converts"))]
25 pub converts: Vec<Convert>,
26
27 #[arg(short, long, default_value_t = false,
28 help = LOCALES.lookup(&LANG_ID, "delete"))]
29 pub delete: bool,
30}
31
32pub fn execute(config: Transform) -> Result<()> {
33 let input_file_path;
34 let input_file_parent_path;
35
36 if utils::is_markdown_or_txt_file(&config.file_path)? {
37 input_file_path = dunce::canonicalize(&config.file_path)?;
38 input_file_parent_path = input_file_path.parent().unwrap().to_path_buf();
39 } else if let Ok(Some(path)) =
40 utils::try_get_markdown_or_txt_file_name_in_dir(&config.file_path)
41 {
42 input_file_path = path;
43 input_file_parent_path = dunce::canonicalize(&config.file_path)?;
44 } else {
45 eyre::bail!("Invalid input path: `{}`", config.file_path.display());
46 }
47 tracing::info!("Input file path: `{}`", input_file_path.display());
48
49 let input_file_stem = input_file_path.file_stem().unwrap().to_str().unwrap();
50 let input_file_ext = input_file_path.extension().unwrap().to_str().unwrap();
51
52 let bytes = fs::read(&input_file_path)?;
53 let markdown = simdutf8::basic::from_utf8(&bytes)?;
54 let mut parser = TextMergeWithOffset::new(
55 Parser::new_ext(markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS).into_offset_iter(),
56 );
57
58 let mut metadata = utils::get_metadata(&mut parser)?;
59 convert_metadata(&mut metadata, &config.converts, &input_file_parent_path)?;
60
61 let mut image_index = 1;
62 let mut in_heading = false;
63 let parser = parser.map(|(event, range)| match event {
64 Event::Start(Tag::CodeBlock(_)) | Event::End(TagEnd::CodeBlock) => {
65 panic!("Cannot contain CodeBlock: {}", &markdown[range]);
66 }
67 Event::Start(Tag::Heading {
68 level,
69 id,
70 classes,
71 attrs,
72 }) => {
73 in_heading = true;
74 Event::Start(Tag::Heading {
75 level,
76 id,
77 classes,
78 attrs,
79 })
80 }
81 Event::End(TagEnd::Heading(level)) => {
82 in_heading = false;
83 Event::End(TagEnd::Heading(level))
84 }
85 Event::Text(text) => Event::Text(
86 utils::convert_str(text, &config.converts, in_heading)
87 .unwrap()
88 .into(),
89 ),
90 Event::Start(Tag::Image {
91 link_type,
92 dest_url,
93 title,
94 id,
95 }) => {
96 let new_image_path =
97 utils::convert_image_ext(input_file_parent_path.join(dest_url.as_ref())).unwrap();
98
99 let new_image_path =
100 utils::convert_image_file_stem(new_image_path, utils::num_to_str(image_index))
101 .unwrap();
102 image_index += 1;
103
104 Event::Start(Tag::Image {
105 link_type,
106 dest_url: new_image_path
107 .file_name()
108 .unwrap()
109 .to_str()
110 .unwrap()
111 .to_string()
112 .into(),
113 title,
114 id,
115 })
116 }
117 _ => event,
118 });
119
120 let metadata_block = [
121 Event::Start(Tag::MetadataBlock(MetadataBlockKind::YamlStyle)),
122 Event::Text(serde_yaml::to_string(&metadata)?.into()),
123 Event::End(TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle)),
124 ];
125
126 let mut buf = String::with_capacity(markdown.len());
127 pulldown_cmark_to_cmark::cmark(metadata_block.iter(), &mut buf)?;
128 buf.write_char('\n')?;
129 pulldown_cmark_to_cmark::cmark(parser, &mut buf)?;
130 buf.write_char('\n')?;
131
132 if config.delete {
133 utils::remove_file_or_dir(&input_file_path)?;
134 } else {
135 let backup_file_path =
136 input_file_parent_path.join(format!("{input_file_stem}.old.{input_file_ext}"));
137 tracing::info!("Backup file path: `{}`", backup_file_path.display());
138
139 fs::rename(&input_file_path, backup_file_path)?;
140 }
141
142 let new_file_name = utils::to_novel_dir_name(utils::convert_str(
143 &metadata.title,
144 &config.converts,
145 false,
146 )?)
147 .with_extension(input_file_ext);
148 let output_file_path = input_file_parent_path.join(new_file_name);
149 tracing::info!("Output file path: `{}`", output_file_path.display());
150
151 if cfg!(windows) {
152 buf = buf.replace('\n', "\r\n");
153 }
154 fs::write(&output_file_path, buf)?;
155
156 if config.delete {
157 let image_paths = utils::read_markdown_to_images(&output_file_path)?;
158
159 let mut to_remove = Vec::new();
160 for entry in WalkDir::new(&input_file_parent_path).max_depth(1) {
161 let path = entry?.path().to_path_buf();
162
163 if path != output_file_path && path != input_file_parent_path {
164 let file_name = path.file_name().unwrap().to_str().unwrap();
165 if !image_paths.contains(&PathBuf::from(file_name)) {
166 to_remove.push(path);
167 }
168 }
169 }
170
171 utils::remove_file_or_dir_all(&to_remove)?;
172 }
173
174 Ok(())
175}
176
177fn convert_metadata(metadata: &mut Metadata, converts: &[Convert], input_dir: &Path) -> Result<()> {
178 metadata.title = utils::convert_str(&metadata.title, converts, false)?;
179 metadata.author = utils::convert_str(&metadata.author, converts, false)?;
180 metadata.lang = utils::lang(converts);
181
182 if metadata.description.is_some() {
183 let mut description = Vec::with_capacity(4);
184
185 for line in metadata.description.as_ref().unwrap().split('\n') {
186 description.push(utils::convert_str(line, converts, false).unwrap());
187 }
188
189 metadata.description = Some(description.join("\n"));
190 }
191
192 if metadata.cover_image.is_some() {
193 let new_image_path =
194 utils::convert_image_ext(input_dir.join(metadata.cover_image.as_ref().unwrap()))
195 .unwrap();
196
197 let new_image_path = utils::convert_image_file_stem(new_image_path, "cover").unwrap();
198
199 metadata.cover_image = Some(PathBuf::from(
200 new_image_path.file_name().unwrap().to_str().unwrap(),
201 ));
202 }
203
204 Ok(())
205}