NoTeX 1.0.0
A modern noteworthy LaTeX template
Loading...
Searching...
No Matches
document.cpp
1
9
10#include "notex/document.hpp"
11
12#include <algorithm>
13#include <fstream>
14#include <system_error>
15
16#include "notex/errors.hpp"
17#include "notex/logging.hpp"
18
19namespace notex {
20
21namespace {
22
23std::string_view trim(std::string_view text) {
24 const std::size_t begin = text.find_first_not_of(" \t");
25 if (begin == std::string_view::npos) return {};
26 const std::size_t end = text.find_last_not_of(" \t");
27 return text.substr(begin, end - begin + 1);
28}
29
30} // namespace
31
32Document::Document(std::filesystem::path path, std::vector<std::string> lines)
33 : path_(std::move(path)), lines_(std::move(lines)) {}
34
35Document Document::load(const std::filesystem::path& path) {
36 std::ifstream stream(path);
37 if (!stream) {
38 throw FilesystemError("could not open '" + path.string() + "'");
39 }
40
41 std::vector<std::string> lines;
42 std::string line;
43 while (std::getline(stream, line)) { lines.push_back(line); }
44
45 return Document(path, std::move(lines));
46}
47
48std::vector<std::size_t> Document::find_all(
49 const LinePredicate& predicate) const {
50 std::vector<std::size_t> matches;
51 for (std::size_t i = 0; i < lines_.size(); ++i) {
52 if (predicate(lines_[i])) matches.push_back(i);
53 }
54 return matches;
55}
56
57std::size_t Document::find_unique(const LinePredicate& predicate,
58 std::string_view description) const {
59 const std::vector<std::size_t> matches = find_all(predicate);
60 PLOG_DEBUG << "find_unique(" << description << ") in '" << path_.string()
61 << "': " << matches.size() << " match(es).";
62
63 if (matches.empty()) {
64 throw DocumentError("could not find " + std::string(description) +
65 " in '" + path_.string() +
66 "'; the document may have been restructured by "
67 "hand.");
68 }
69 if (matches.size() > 1) {
70 throw DocumentError(
71 "found " + std::to_string(matches.size()) + " lines matching " +
72 std::string(description) + " in '" + path_.string() +
73 "', expected exactly one; please resolve the ambiguity by "
74 "hand.");
75 }
76
77 return matches.front();
78}
79
80std::optional<std::size_t> Document::find_last(
81 const LinePredicate& predicate) const {
82 const std::vector<std::size_t> matches = find_all(predicate);
83 if (matches.empty()) return std::nullopt;
84 return matches.back();
85}
86
87void Document::insert_line(std::size_t index, std::string text) {
88 lines_.insert(lines_.begin() + static_cast<std::ptrdiff_t>(index),
89 std::move(text));
90}
91
92void Document::insert_lines(std::size_t index,
93 std::vector<std::string> new_lines) {
94 lines_.insert(lines_.begin() + static_cast<std::ptrdiff_t>(index),
95 std::make_move_iterator(new_lines.begin()),
96 std::make_move_iterator(new_lines.end()));
97}
98
99void Document::remove_line(std::size_t index) {
100 lines_.erase(lines_.begin() + static_cast<std::ptrdiff_t>(index));
101}
102
104 std::string_view option,
105 const std::vector<std::string>& mutually_exclusive_group) {
106 const std::size_t index = find_unique(
107 [](const std::string& line) {
108 return trim(line).starts_with("\\documentclass");
109 },
110 "a unique \\documentclass line");
111
112 const std::string& line = lines_[index];
113 const std::size_t brace_pos = line.find('{');
114 if (brace_pos == std::string::npos) {
115 throw DocumentError("malformed \\documentclass line in '" +
116 path_.string() + "': missing '{'.");
117 }
118
119 const std::size_t bracket_start = line.find('[');
120 std::string prefix;
121 std::string suffix;
122 std::vector<std::string> options;
123
124 if (bracket_start != std::string::npos && bracket_start < brace_pos) {
125 const std::size_t bracket_end = line.find(']', bracket_start);
126 if (bracket_end == std::string::npos || bracket_end > brace_pos) {
127 throw DocumentError("malformed \\documentclass line in '" +
128 path_.string() + "': unmatched '['.");
129 }
130 prefix = line.substr(0, bracket_start);
131 suffix = line.substr(bracket_end + 1);
132
133 std::string_view remaining =
134 std::string_view(line).substr(bracket_start + 1,
135 bracket_end - bracket_start - 1);
136 while (!remaining.empty()) {
137 const std::size_t comma = remaining.find(',');
138 const std::string_view token =
139 trim(remaining.substr(0, comma));
140 if (!token.empty()) options.emplace_back(token);
141 if (comma == std::string_view::npos) break;
142 remaining = remaining.substr(comma + 1);
143 }
144 } else {
145 prefix = line.substr(0, brace_pos);
146 suffix = line.substr(brace_pos);
147 }
148
149 std::vector<std::string> rewritten;
150 rewritten.emplace_back(option);
151 for (const std::string& existing : options) {
152 const bool excluded =
153 existing == option ||
154 std::find(mutually_exclusive_group.begin(),
155 mutually_exclusive_group.end(),
156 existing) != mutually_exclusive_group.end();
157 if (!excluded) rewritten.push_back(existing);
158 }
159
160 std::string joined;
161 for (std::size_t i = 0; i < rewritten.size(); ++i) {
162 if (i != 0) joined += ", ";
163 joined += rewritten[i];
164 }
165
166 lines_[index] = prefix + "[" + joined + "]" + suffix;
167}
168
169void Document::save() const {
170 const std::filesystem::path temp_path =
171 path_.string() + ".notex.tmp";
172
173 PLOG_DEBUG << "Atomically writing '" << path_.string() << "' via '"
174 << temp_path.string() << "'.";
175 {
176 std::ofstream stream(temp_path, std::ios::binary);
177 if (!stream) {
178 throw FilesystemError("could not write '" + temp_path.string() +
179 "'");
180 }
181 for (const std::string& line : lines_) { stream << line << '\n'; }
182 }
183
184 std::error_code ec;
185 std::filesystem::rename(temp_path, path_, ec);
186 if (ec) {
187 std::error_code ignored;
188 std::filesystem::remove(temp_path, ignored);
189 throw FilesystemError("could not save '" + path_.string() +
190 "': " + ec.message());
191 }
192 PLOG_DEBUG << "Saved '" << path_.string() << "'.";
193}
194
195} // namespace notex
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 a filesystem operation (copy, write, remove) fails.
Definition errors.hpp:82
Document: a small, safe line-oriented editor for a single .tex file.
Exception hierarchy and process exit codes used throughout NoTeX.
Logging utility with plog initialization.