NoTeX 1.0.0
A modern noteworthy LaTeX template
Loading...
Searching...
No Matches
manager.cpp
1
9
10#include "notex/manager.hpp"
11
12#include <algorithm>
13#include <array>
14#include <charconv>
15#include <fstream>
16#include <nlohmann/json.hpp>
17#include <string_view>
18#include <system_error>
19
20#include "notex/assets.hpp"
21#include "notex/document.hpp"
22#include "notex/environment.hpp"
23#include "notex/errors.hpp"
24#include "notex/logging.hpp"
25#include "notex/output.hpp"
26
27namespace notex {
28
29namespace {
30
31std::filesystem::path config_file_for(const std::filesystem::path& root_dir) {
32 return root_dir / ".notex" / "notex.json";
33}
34
35void write_text_file(const std::filesystem::path& path,
36 const std::string& content) {
37 std::filesystem::create_directories(path.parent_path());
38 std::ofstream stream(path);
39 if (!stream) {
40 throw FilesystemError("could not write '" + path.string() + "'");
41 }
42 stream << content;
43}
44
45// Decides the \documentclass argument a freshly scaffolded main.tex
46// should use. The local check is done directly against the filesystem,
47// rather than through Environment, because Environment's constructor
48// always tries to resolve TEXMFHOME (even just to check a local
49// installation) and throws if it can't; a machine with no TeX
50// installation at all should still be able to scaffold a project that
51// will later use a local install.
52std::string resolve_class_path(const std::filesystem::path& target_dir) {
53 if (std::filesystem::exists(target_dir / "settings" / "notex.cls")) {
54 return "settings/notex";
55 }
56
57 try {
58 const Environment environment(target_dir);
59 if (environment.installation_type() == InstallationType::NONE) {
60 PLOG_WARNING << "No NoTeX installation found for '"
61 << target_dir.string()
62 << "'; scaffolding will reference "
63 "\\documentclass{notex}.";
65 "No NoTeX installation found; the generated project will "
66 "reference \\documentclass{notex}. Run 'notex install' "
67 "before compiling.");
68 }
69 } catch (const EnvironmentError& e) {
70 PLOG_WARNING << "Could not resolve the TeX environment for '"
71 << target_dir.string() << "': " << e.what();
73 "Could not resolve the TeX environment; the generated project "
74 "will reference \\documentclass{notex}. Run 'notex install' "
75 "before compiling.");
76 }
77 return "notex";
78}
79
80// Filename suffixes of known LaTeX build artefacts. Matched with
81// std::string::ends_with rather than std::filesystem::path::extension(),
82// because several of these, such as ".synctex.gz" and "-blx.bib", are not
83// true extensions and extension() would miss them entirely.
84constexpr std::array<std::string_view, 20> kCleanFileSuffixes = {
85 ".aux",
86 ".log",
87 ".out",
88 ".toc",
89 ".lof",
90 ".lot",
91 ".fls",
92 ".blg",
93 ".bbl",
94 ".bcf",
95 ".fdb_latexmk",
96 ".synctex.gz",
97 ".listing",
98 "-blx.bib",
99 ".run.xml",
100 ".nav",
101 ".snm",
102 ".vrb",
103 ".synctex.gz(busy)",
104 ".minted",
105};
106
107// Name prefixes of known LaTeX build artefact directories. The `minted`
108// package always names its cache directories `_minted-<jobname>`,
109// regardless of what the project's own files are called.
110constexpr std::array<std::string_view, 1> kCleanDirectoryPrefixes = {
111 "_minted-",
112};
113
114bool is_clean_target_file(std::string_view filename) {
115 for (const std::string_view suffix : kCleanFileSuffixes) {
116 if (filename.ends_with(suffix)) return true;
117 }
118 return false;
119}
120
121bool is_clean_target_directory(std::string_view name) {
122 for (const std::string_view prefix : kCleanDirectoryPrefixes) {
123 if (name.starts_with(prefix)) return true;
124 }
125 return false;
126}
127
128std::string_view trim(std::string_view text) {
129 const std::size_t begin = text.find_first_not_of(" \t");
130 if (begin == std::string_view::npos) return {};
131 const std::size_t end = text.find_last_not_of(" \t");
132 return text.substr(begin, end - begin + 1);
133}
134
135bool is_documentclass_line(const std::string& line) {
136 return trim(line).starts_with("\\documentclass");
137}
138
139bool is_end_document_line(const std::string& line) {
140 return trim(line) == "\\end{document}";
141}
142
143std::string subfile_line_for(std::string_view stem) {
144 return "\\subfile{sections/" + std::string(stem) + "}";
145}
146
147bool is_subfile_line_for(const std::string& line, std::string_view stem) {
148 return trim(line) == subfile_line_for(stem);
149}
150
151bool is_any_subfile_line(const std::string& line) {
152 return trim(line).starts_with("\\subfile{sections/");
153}
154
155bool is_biblatex_usepackage_line(const std::string& line) {
156 const std::string_view trimmed = trim(line);
157 return trimmed.starts_with("\\usepackage") &&
158 trimmed.find("{biblatex}") != std::string_view::npos;
159}
160
161bool is_addbibresource_line(const std::string& line) {
162 return trim(line).starts_with("\\addbibresource");
163}
164
165bool is_printbibliography_line(const std::string& line) {
166 return trim(line).starts_with("\\printbibliography");
167}
168
169bool is_bibliographystyle_line(const std::string& line) {
170 return trim(line).starts_with("\\bibliographystyle");
171}
172
173bool is_bibliography_command_line(const std::string& line) {
174 return trim(line).starts_with("\\bibliography{");
175}
176
177// Inserts @p text right after the main file's unique \documentclass line
178// (for preamble commands) or right before its unique \end{document} line
179// (for body commands), unless a line already matching @p already_present
180// exists, in which case nothing changes: the bibliography commands must
181// stay idempotent to re-running `add bib`.
182void ensure_line(Document& doc, const std::string& text,
183 const Document::LinePredicate& already_present,
184 bool after_documentclass) {
185 if (!doc.find_all(already_present).empty()) return;
186
187 if (after_documentclass) {
188 const std::size_t anchor =
189 doc.find_unique(is_documentclass_line, "a unique \\documentclass line");
190 doc.insert_line(anchor + 1, text);
191 } else {
192 const std::size_t anchor =
193 doc.find_unique(is_end_document_line, "a unique \\end{document} line");
194 doc.insert_line(anchor, text);
195 }
196}
197
198// Removes every line matching @p predicate, back to front so that
199// earlier indices stay valid as later ones are erased.
200void remove_matching_lines(Document& doc,
201 const Document::LinePredicate& predicate) {
202 const std::vector<std::size_t> matches = doc.find_all(predicate);
203 for (auto it = matches.rbegin(); it != matches.rend(); ++it) {
204 doc.remove_line(*it);
205 }
206}
207
208// Scans <sections_dir> for files named "<N>_....tex" and returns their
209// (number, stem) pairs, sorted by number. Deliberately reads the
210// filesystem directly rather than any tracked state, so that sections
211// added or removed by hand are always respected (see ProjectConfig's
212// docs); every other section helper below is built on this one.
213std::vector<std::pair<int, std::string>> scan_sections(
214 const std::filesystem::path& sections_dir) {
215 std::vector<std::pair<int, std::string>> numbered;
216 std::error_code ec;
217 if (!std::filesystem::is_directory(sections_dir, ec)) return numbered;
218
219 for (const auto& entry :
220 std::filesystem::directory_iterator(sections_dir, ec)) {
221 if (ec || !entry.is_regular_file()) continue;
222 const std::string stem = entry.path().stem().string();
223 const std::size_t underscore = stem.find('_');
224 if (underscore == std::string::npos) continue;
225
226 int number = 0;
227 const auto result = std::from_chars(
228 stem.data(), stem.data() + underscore, number);
229 if (result.ec != std::errc()) continue;
230 numbered.emplace_back(number, stem);
231 }
232 std::sort(numbered.begin(), numbered.end());
233 return numbered;
234}
235
236int highest_section_number(const std::filesystem::path& sections_dir) {
237 const auto numbered = scan_sections(sections_dir);
238 return numbered.empty() ? 0 : numbered.back().first;
239}
240
241std::vector<std::string> section_stems_sorted(
242 const std::filesystem::path& sections_dir) {
243 std::vector<std::string> stems;
244 for (auto& [number, stem] : scan_sections(sections_dir)) {
245 stems.push_back(std::move(stem));
246 }
247 return stems;
248}
249
250// Extracts "sections/NAME" from a "\subfile{sections/NAME}" line's NAME
251// part; returns std::nullopt if @p line isn't such a line.
252std::optional<std::string> subfile_target(const std::string& line) {
253 const std::string_view trimmed = trim(line);
254 constexpr std::string_view kPrefix = "\\subfile{sections/";
255 if (!trimmed.starts_with(kPrefix)) return std::nullopt;
256 const std::size_t end = trimmed.find('}', kPrefix.size());
257 if (end == std::string_view::npos) return std::nullopt;
258 return std::string(trimmed.substr(kPrefix.size(), end - kPrefix.size()));
259}
260
261std::optional<std::filesystem::path> find_section_file(
262 const std::filesystem::path& sections_dir, int number) {
263 const std::string prefix = std::to_string(number) + "_";
264 std::error_code ec;
265 if (!std::filesystem::is_directory(sections_dir, ec)) return std::nullopt;
266
267 for (const auto& entry :
268 std::filesystem::directory_iterator(sections_dir, ec)) {
269 if (ec || !entry.is_regular_file()) continue;
270 if (entry.path().filename().string().starts_with(prefix)) {
271 return entry.path();
272 }
273 }
274 return std::nullopt;
275}
276
277} // namespace
278
279std::optional<std::filesystem::path> Manager::find_project_root(
280 const std::filesystem::path& start_dir) {
281 std::filesystem::path current =
282 std::filesystem::absolute(start_dir).lexically_normal();
283
284 PLOG_DEBUG << "Scanning upward for .notex/ starting at '"
285 << current.string() << "'.";
286 for (;;) {
287 if (std::filesystem::is_directory(current / ".notex")) {
288 PLOG_DEBUG << "Found project root at '" << current.string()
289 << "'.";
290 return current;
291 }
292 const std::filesystem::path parent = current.parent_path();
293 if (parent == current) {
294 PLOG_DEBUG << "No .notex/ directory found above '"
295 << start_dir.string() << "'.";
296 return std::nullopt;
297 }
298 current = parent;
299 }
300}
301
302ProjectConfig Manager::load_config(const std::filesystem::path& config_file) {
303 std::ifstream stream(config_file);
304 if (!stream) {
305 throw ConfigError("could not open '" + config_file.string() + "'");
306 }
307
308 nlohmann::json parsed;
309 try {
310 stream >> parsed;
311 } catch (const nlohmann::json::exception& e) {
312 throw ConfigError("malformed '" + config_file.string() +
313 "': " + e.what());
314 }
315
316 ProjectConfig config;
317 try {
318 config.schema_version = parsed.value("schema_version", 1);
319 config.notex_version = parsed.value("notex_version", std::string());
320 config.project_type = parsed.value("project_type", std::string());
321 config.main_file = parsed.value("main_file", std::string("main.tex"));
322 config.installation_type =
323 parsed.value("installation_type", std::string());
324 config.theme = parsed.value("theme", std::string());
325 config.bibliography_file =
326 parsed.value("bibliography_file", std::string());
327 } catch (const nlohmann::json::exception& e) {
328 throw ConfigError("malformed '" + config_file.string() +
329 "': " + e.what());
330 }
331
332 PLOG_DEBUG << "Loaded project config from '" << config_file.string()
333 << "'.";
334 return config;
335}
336
337Manager::Manager(std::filesystem::path start_dir) {
338 const auto root = find_project_root(start_dir);
339 if (!root.has_value()) {
341 "no NoTeX project found in '" + start_dir.string() +
342 "' or any parent directory (missing .notex/); run 'notex "
343 "init' first.");
344 }
345
346 root_dir_ = *root;
347 config_ = load_config(config_file_for(root_dir_));
348}
349
350void Manager::save() const { write_config(root_dir_, config_); }
351
352void Manager::write_config(const std::filesystem::path& root_dir,
353 const ProjectConfig& config) {
354 nlohmann::json json;
355 json["schema_version"] = config.schema_version;
356 json["notex_version"] = config.notex_version;
357 json["project_type"] = config.project_type;
358 json["main_file"] = config.main_file;
359 json["installation_type"] = config.installation_type;
360 json["theme"] = config.theme;
361 json["bibliography_file"] = config.bibliography_file;
362
363 const std::filesystem::path config_file = config_file_for(root_dir);
364 std::filesystem::create_directories(config_file.parent_path());
365 std::ofstream stream(config_file);
366 if (!stream) {
367 throw ConfigError("could not write '" + config_file.string() + "'");
368 }
369 stream << json.dump(2) << '\n';
370 PLOG_DEBUG << "Wrote project config to '" << config_file.string()
371 << "'.";
372}
373
374Manager Manager::init(const std::filesystem::path& target_dir,
375 templates::ProjectType project_type, bool force) {
376 const std::filesystem::path main_file = target_dir / "main.tex";
377 const bool already_project =
378 std::filesystem::is_directory(target_dir / ".notex") ||
379 std::filesystem::exists(main_file);
380
381 if (already_project && !force) {
382 throw FilesystemError(
383 "'" + target_dir.string() +
384 "' already looks like a NoTeX project (main.tex or .notex/ "
385 "already exists); pass --force to overwrite it.");
386 }
387
388 const std::string class_path = resolve_class_path(target_dir);
389
390 if (project_type == templates::ProjectType::MONO) {
391 write_text_file(main_file, templates::mono_main(class_path));
392 } else {
393 write_text_file(main_file, templates::multi_main(class_path));
394 const std::string stem = templates::section_stem(1, "Introduction");
395 write_text_file(target_dir / "sections" / (stem + ".tex"),
396 templates::section(1, "Introduction"));
397 }
398
400 config.notex_version = PROJECT_VERSION;
401 config.project_type = std::string(templates::to_string(project_type));
402 config.main_file = "main.tex";
403 write_config(target_dir, config);
404
405 PLOG_DEBUG << "Scaffolded a " << templates::to_string(project_type)
406 << "-file project at '" << target_dir.string() << "'.";
407 return Manager(target_dir);
408}
409
410CleanReport Manager::clean(const std::filesystem::path& start_dir,
411 bool dry_run) {
412 CleanReport report;
413
414 std::error_code ec;
415 if (!std::filesystem::is_directory(start_dir, ec)) { return report; }
416
417 PLOG_DEBUG << "Scanning '" << start_dir.string()
418 << "' for build artefacts" << (dry_run ? " (dry-run)" : "")
419 << ".";
420
421 const auto options =
422 std::filesystem::directory_options::skip_permission_denied;
423 auto it =
424 std::filesystem::recursive_directory_iterator(start_dir, options, ec);
425 const auto end = std::filesystem::recursive_directory_iterator();
426
427 while (!ec && it != end) {
428 const std::filesystem::path path = it->path();
429 const std::string filename = path.filename().string();
430
431 if (it->is_directory(ec) && is_clean_target_directory(filename)) {
432 PLOG_DEBUG << "Matched artefact directory '" << path.string()
433 << "'.";
434 report.removed_directories.push_back(path);
435 if (!dry_run) { std::filesystem::remove_all(path, ec); }
436 it.disable_recursion_pending();
437 } else if (it->is_regular_file(ec) && is_clean_target_file(filename)) {
438 PLOG_DEBUG << "Matched artefact file '" << path.string() << "'.";
439 report.removed_files.push_back(path);
440 if (!dry_run) { std::filesystem::remove(path, ec); }
441 }
442
443 it.increment(ec);
444 }
445
446 PLOG_DEBUG << "Clean scan of '" << start_dir.string() << "' found "
447 << report.total_removed() << " artefact(s).";
448 return report;
449}
450
451std::vector<std::string> Manager::available_themes() {
452 constexpr std::string_view kPrefix = "notex-theme-";
453 constexpr std::string_view kSuffix = ".tex";
454
455 std::vector<std::string> themes;
456 for (const auto& file : assets::latex_files()) {
457 if (file.name.starts_with(kPrefix) && file.name.ends_with(kSuffix)) {
458 themes.emplace_back(file.name.substr(
459 kPrefix.size(), file.name.size() - kPrefix.size() - kSuffix.size()));
460 }
461 }
462 std::sort(themes.begin(), themes.end());
463 return themes;
464}
465
466void Manager::set_theme(std::string_view theme) {
467 const std::vector<std::string> themes = available_themes();
468 if (std::find(themes.begin(), themes.end(), std::string(theme)) ==
469 themes.end()) {
470 std::string available;
471 for (std::size_t i = 0; i < themes.size(); ++i) {
472 if (i != 0) available += ", ";
473 available += themes[i];
474 }
475 throw UsageError("'" + std::string(theme) +
476 "' is not a valid theme; available themes: " +
477 available + ".");
478 }
479
480 Document doc = Document::load(root_dir_ / config_.main_file);
481 doc.set_documentclass_option(theme, themes);
482 doc.save();
483
484 config_.theme = std::string(theme);
485 save();
486}
487
488void Manager::add_section(std::string_view title) {
489 Document doc = Document::load(root_dir_ / config_.main_file);
490
491 if (config_.project_type == "mono") {
492 const std::size_t anchor = doc.find_unique(
493 is_end_document_line, "a unique \\end{document} line");
494 doc.insert_lines(
495 anchor, {"\\section{" + std::string(title) + "}", "", ""});
496 doc.save();
497 return;
498 }
499
500 const std::filesystem::path sections_dir = root_dir_ / "sections";
501 const int number = highest_section_number(sections_dir) + 1;
502 const std::string stem = templates::section_stem(number, title);
503 write_text_file(sections_dir / (stem + ".tex"),
504 templates::section(number, title));
505
506 const std::optional<std::size_t> last_subfile =
507 doc.find_last(is_any_subfile_line);
508 if (!last_subfile.has_value()) {
509 throw DocumentError(
510 "could not find an existing \\subfile{sections/...} line in '" +
511 (root_dir_ / config_.main_file).string() +
512 "' to insert the new section after; the document may have "
513 "been restructured by hand.");
514 }
515 doc.insert_line(*last_subfile + 1, subfile_line_for(stem));
516 doc.save();
517}
518
519bool Manager::remove_section(int number) {
520 if (config_.project_type != "multi") {
521 throw UsageError(
522 "removing a section is only supported in multi-file projects.");
523 }
524
525 const std::filesystem::path sections_dir = root_dir_ / "sections";
526 const std::optional<std::filesystem::path> section_file =
527 find_section_file(sections_dir, number);
528 if (!section_file.has_value()) {
529 throw UsageError("no section numbered " + std::to_string(number) +
530 " was found in '" + sections_dir.string() + "'.");
531 }
532
533 const bool confirmed = ui::confirm(
534 "Remove section '" + section_file->filename().string() +
535 "' and its \\subfile line? This cannot be undone.",
536 false);
537 if (!confirmed) {
538 PLOG_WARNING << "Section removal cancelled by the user for '"
539 << section_file->string() << "'.";
540 ui::warning("Section removal cancelled.");
541 return false;
542 }
543
544 const std::string stem = section_file->stem().string();
545 Document doc = Document::load(root_dir_ / config_.main_file);
546 const std::vector<std::size_t> matches = doc.find_all(
547 [&](const std::string& line) { return is_subfile_line_for(line, stem); });
548 if (matches.size() > 1) {
549 throw DocumentError(
550 "found " + std::to_string(matches.size()) +
551 " \\subfile lines referencing '" + stem + "' in '" +
552 (root_dir_ / config_.main_file).string() +
553 "', expected at most one; please resolve the ambiguity by "
554 "hand.");
555 }
556 if (matches.empty()) {
557 PLOG_WARNING << "No \\subfile line referencing '" << stem
558 << "' was found; only the section file will be "
559 "removed.";
560 ui::warning("No \\subfile line referencing '" + stem +
561 "' was found; only the section file will be removed.");
562 } else {
563 doc.remove_line(matches.front());
564 doc.save();
565 }
566
567 std::filesystem::remove(*section_file);
568 return true;
569}
570
572 const std::string bib_filename = config_.bibliography_file.empty()
573 ? "bibliography.bib"
574 : config_.bibliography_file;
575 const std::filesystem::path bib_path = root_dir_ / bib_filename;
576 if (!std::filesystem::exists(bib_path)) {
577 write_text_file(bib_path, templates::bibliography_starter());
578 }
579
580 Document doc = Document::load(root_dir_ / config_.main_file);
581 const bool uses_biblatex = !doc.find_all(is_biblatex_usepackage_line).empty();
582 const std::string bib_stem =
583 std::filesystem::path(bib_filename).stem().string();
584
585 if (uses_biblatex) {
586 ensure_line(doc, "\\addbibresource{" + bib_filename + "}",
587 is_addbibresource_line, /*after_documentclass=*/true);
588 ensure_line(doc, "\\printbibliography", is_printbibliography_line,
589 /*after_documentclass=*/false);
590 } else {
591 ensure_line(doc, "\\bibliographystyle{unsrturl}",
592 is_bibliographystyle_line, /*after_documentclass=*/false);
593 ensure_line(doc, "\\bibliography{" + bib_stem + "}",
594 is_bibliography_command_line, /*after_documentclass=*/false);
595 }
596 doc.save();
597
598 config_.bibliography_file = bib_filename;
599 save();
600}
601
603 Document doc = Document::load(root_dir_ / config_.main_file);
604 remove_matching_lines(doc, is_addbibresource_line);
605 remove_matching_lines(doc, is_printbibliography_line);
606 remove_matching_lines(doc, is_bibliographystyle_line);
607 remove_matching_lines(doc, is_bibliography_command_line);
608 doc.save();
609
610 if (!config_.bibliography_file.empty()) {
611 const std::filesystem::path bib_path =
612 root_dir_ / config_.bibliography_file;
613 if (std::filesystem::exists(bib_path) &&
614 ui::confirm("Also delete '" + bib_path.string() + "'?", false)) {
615 std::filesystem::remove(bib_path);
616 }
617 }
618
619 config_.bibliography_file.clear();
620 save();
621}
622
623std::vector<std::filesystem::path> Manager::project_files() const {
624 std::vector<std::filesystem::path> files;
625
626 const std::filesystem::path main_path = root_dir_ / config_.main_file;
627 if (std::filesystem::exists(main_path)) { files.push_back(main_path); }
628
629 if (config_.project_type == "multi") {
630 std::error_code ec;
631 for (const auto& entry : std::filesystem::directory_iterator(
632 root_dir_ / "sections", ec)) {
633 std::error_code file_ec;
634 if (entry.is_regular_file(file_ec) && !file_ec) {
635 files.push_back(entry.path());
636 }
637 }
638 }
639
640 if (!config_.bibliography_file.empty()) {
641 const std::filesystem::path bib_path =
642 root_dir_ / config_.bibliography_file;
643 if (std::filesystem::exists(bib_path)) { files.push_back(bib_path); }
644 }
645
646 return files;
647}
648
649std::vector<std::string> Manager::orphan_sections() const {
650 if (config_.project_type != "multi") return {};
651
652 const std::filesystem::path main_path = root_dir_ / config_.main_file;
653 if (!std::filesystem::exists(main_path)) return {};
654
655 const Document doc = Document::load(main_path);
656 std::vector<std::string> referenced;
657 for (const std::string& line : doc.lines()) {
658 if (const auto target = subfile_target(line); target.has_value()) {
659 referenced.push_back(*target);
660 }
661 }
662
663 std::vector<std::string> orphans;
664 for (const std::string& stem :
665 section_stems_sorted(root_dir_ / "sections")) {
666 if (std::find(referenced.begin(), referenced.end(), stem) ==
667 referenced.end()) {
668 orphans.push_back(stem);
669 }
670 }
671 return orphans;
672}
673
674bool Manager::reset(bool force) {
675 if (!force &&
676 !ui::confirm("Regenerate '" + config_.main_file +
677 "' from the template? The current version will "
678 "be backed up to '" +
679 config_.main_file + ".bak'.",
680 false)) {
681 PLOG_WARNING << "Reset cancelled by the user for '"
682 << (root_dir_ / config_.main_file).string() << "'.";
683 ui::warning("Reset cancelled.");
684 return false;
685 }
686
687 const std::filesystem::path main_path = root_dir_ / config_.main_file;
688 if (std::filesystem::exists(main_path)) {
689 std::filesystem::copy_file(
690 main_path, root_dir_ / (config_.main_file + ".bak"),
691 std::filesystem::copy_options::overwrite_existing);
692 }
693
694 const std::string class_path = resolve_class_path(root_dir_);
695 if (config_.project_type == "mono") {
696 write_text_file(main_path, templates::mono_main(class_path));
697 } else {
698 std::vector<std::string> stems =
699 section_stems_sorted(root_dir_ / "sections");
700 if (stems.empty()) { stems.push_back("1_introduction"); }
701 write_text_file(main_path, templates::multi_main(class_path, stems));
702 }
703
704 return true;
705}
706
707bool Manager::delete_scaffolding(bool remove_all, bool force) {
708 const std::string prompt =
709 remove_all
710 ? "Delete the entire project at '" + root_dir_.string() +
711 "', including every file? This cannot be undone."
712 : "Remove NoTeX's scaffolding (.notex/, settings/, fonts/, "
713 "build artefacts) from '" +
714 root_dir_.string() +
715 "'? User files (the main file, sections/, the "
716 "bibliography) are kept.";
717 if (!force && !ui::confirm(prompt, false)) {
718 PLOG_WARNING << "Deletion cancelled by the user for '"
719 << root_dir_.string() << "'.";
720 ui::warning("Deletion cancelled.");
721 return false;
722 }
723
724 if (remove_all) {
725 std::filesystem::remove_all(root_dir_);
726 return true;
727 }
728
729 clean(root_dir_);
730 std::error_code ec;
731 std::filesystem::remove_all(root_dir_ / ".notex", ec);
732 std::filesystem::remove_all(root_dir_ / "settings", ec);
733 std::filesystem::remove_all(root_dir_ / "fonts", ec);
734 return true;
735}
736
737void Manager::set_config_value(std::string_view key, std::string_view value) {
738 if (key == "main_file") {
739 config_.main_file = std::string(value);
740 } else if (key == "theme") {
741 config_.theme = std::string(value);
742 } else if (key == "bibliography_file") {
743 config_.bibliography_file = std::string(value);
744 } else {
745 throw UsageError("'" + std::string(key) +
746 "' is not a settable key; expected one of "
747 "'main_file', 'theme', 'bibliography_file'.");
748 }
749 save();
750}
751
752} // namespace notex
Accessor over the LaTeX template and font files embedded into the binary.
Raised when notex.json is missing, malformed, or fails to write.
Definition errors.hpp:89
Loads a .tex file as a sequence of lines and offers editing primitives anchored on recognisable landm...
Definition document.hpp:31
void set_documentclass_option(std::string_view option, const std::vector< std::string > &mutually_exclusive_group)
Rewrites the document's unique \documentclass line so that its bracketed options contain option inste...
Definition document.cpp:103
static Document load(const std::filesystem::path &path)
Loads path as a sequence of lines.
Definition document.cpp:35
std::optional< std::size_t > find_last(const LinePredicate &predicate) const
Definition document.cpp:80
std::function< bool(const std::string &)> LinePredicate
Definition document.hpp:35
std::size_t find_unique(const LinePredicate &predicate, std::string_view description) const
Locates the single line matching predicate.
Definition document.cpp:57
std::vector< std::size_t > find_all(const LinePredicate &predicate) const
Definition document.cpp:48
void insert_lines(std::size_t index, std::vector< std::string > new_lines)
Inserts new_lines, in order, before the current line index.
Definition document.cpp:92
void insert_line(std::size_t index, std::string text)
Inserts text as a new line before the current line index.
Definition document.cpp:87
void remove_line(std::size_t index)
Removes the line at index.
Definition document.cpp:99
const std::vector< std::string > & lines() const noexcept
Definition document.hpp:45
void save() const
Writes the current lines back to disk atomically.
Definition document.cpp:169
Raised when the surrounding TeX environment cannot be resolved.
Definition errors.hpp:68
Represents the surrounding system as NoTeX perceives it: where TeX expects user files to live,...
Raised when a filesystem operation (copy, write, remove) fails.
Definition errors.hpp:82
bool delete_scaffolding(bool remove_all=false, bool force=false)
Removes NoTeX's own scaffolding from the project.
Definition manager.cpp:707
const std::filesystem::path & root_dir() const noexcept
Definition manager.hpp:100
void add_bibliography()
Adds a bibliography.
Definition manager.cpp:571
std::vector< std::string > orphan_sections() const
Definition manager.cpp:649
static CleanReport clean(const std::filesystem::path &start_dir, bool dry_run=false)
Recursively removes known LaTeX build artefacts from start_dir.
Definition manager.cpp:410
std::vector< std::filesystem::path > project_files() const
Definition manager.cpp:623
void set_config_value(std::string_view key, std::string_view value)
Sets a single whitelisted metadata key directly.
Definition manager.cpp:737
static std::optional< std::filesystem::path > find_project_root(const std::filesystem::path &start_dir)
Searches upward from start_dir for a .notex/ directory, without requiring one to exist.
Definition manager.cpp:279
void save() const
Persists the current metadata back to .notex/notex.json.
Definition manager.cpp:350
void add_section(std::string_view title)
Adds a new section titled title.
Definition manager.cpp:488
void remove_bibliography()
Removes the bibliography commands added by add_bibliography() from the main file.
Definition manager.cpp:602
void set_theme(std::string_view theme)
Switches the project's theme.
Definition manager.cpp:466
Manager(std::filesystem::path start_dir=std::filesystem::current_path())
Locates and loads the project that contains start_dir.
Definition manager.cpp:337
static void write_config(const std::filesystem::path &root_dir, const ProjectConfig &config)
Writes config as <root_dir>/.notex/notex.json, creating <root_dir>/.notex/ first if it does not alrea...
Definition manager.cpp:352
bool reset(bool force=false)
Regenerates the main file from the template, after backing up the previous one to <main_file>....
Definition manager.cpp:674
static std::vector< std::string > available_themes()
Definition manager.cpp:451
static Manager init(const std::filesystem::path &target_dir, templates::ProjectType project_type=templates::ProjectType::MULTI, bool force=false)
Creates a new NoTeX project inside target_dir.
Definition manager.cpp:374
const ProjectConfig & config() const noexcept
Definition manager.hpp:105
bool remove_section(int number)
Removes the section numbered number.
Definition manager.cpp:519
Raised when a command that requires a project is run outside one.
Definition errors.hpp:75
Raised when the command line supplied by the user is invalid.
Definition errors.hpp:61
Document: a small, safe line-oriented editor for a single .tex file.
Environment: discovers where TeX lives and whether NoTeX is installed.
Exception hierarchy and process exit codes used throughout NoTeX.
Logging utility with plog initialization.
Manager: represents and operates on a single NoTeX project.
const std::vector< EmbeddedFile > & latex_files()
Definition assets.cpp:56
std::string section(int number, std::string_view title)
Generates a numbered section subfile.
std::string mono_main(std::string_view class_path)
Generates the entry file for a single-file project.
std::string multi_main(std::string_view class_path, const std::vector< std::string > &section_stems={"1_introduction"})
Generates the entry file for a multi-file project, referencing section_stems in order.
std::string bibliography_starter()
std::string section_stem(int number, std::string_view title)
Computes the filename stem (no directory, no extension) for a numbered section.
@ MONO
A single main.tex file.
Definition templates.hpp:23
std::string_view to_string(ProjectType type)
void warning(std::string_view message)
Definition output.cpp:54
bool confirm(std::string_view prompt, bool default_answer=false)
Asks the user to confirm an action.
Definition output.cpp:68
@ NONE
Neither a local nor a global installation was found.
Styled terminal output and interactive confirmation prompts.
Outcome of a Manager::clean() call.
Definition manager.hpp:55
std::size_t total_removed() const noexcept
Definition manager.hpp:61
Persisted metadata for a single NoTeX project, stored as JSON in .notex/notex.json.
Definition manager.hpp:36