#include "note_text.h" #include namespace roro::notes { namespace { bool continuation(char c) { return (static_cast(c) & 0xC0) == 0x80; } } // namespace NoteText::NoteText(int cols, int rows) : cols_(std::max(1, cols)), rows_(std::max(1, rows)) { text_.reserve(kMaxBytes); } NoteText::NoteText(int cols, int rows, std::string&& text) : cols_(std::max(1, cols)), rows_(std::max(1, rows)), text_(std::move(text)) { dropCarriageReturns(); if (text_.size() > kMaxBytes) text_.resize(kMaxBytes); // the caller checks sizes: not reached text_.reserve(kMaxBytes); } void NoteText::dropCarriageReturns() { size_t kept = 0; for (size_t i = 0; i < text_.size(); i++) if (!(text_[i] == '\r' && i + 1 < text_.size() && text_[i + 1] == '\n')) text_[kept++] = text_[i]; text_.resize(kept); } bool NoteText::setText(const std::string& text) { size_t kept = text.size(); for (size_t i = 0; i + 1 < text.size(); i++) if (text[i] == '\r' && text[i + 1] == '\n') kept--; if (kept > kMaxBytes) return false; text_.assign(text); dropCarriageReturns(); cursor_ = top_ = 0; goal_ = -1; revision_++; return true; } size_t NoteText::nextLine(size_t start) const { size_t size = text_.size(); int count = 0; size_t lastSpace = 0; bool space = false; for (size_t i = start; i < size; i++) { char c = text_[i]; if (c == '\n') return i + 1; if (continuation(c)) continue; if (count == cols_) { // one character too many: wrap if (c == ' ') return i + 1; // the space stays at the end of this line if (space) return lastSpace + 1; // after the last space that fits return i; // a word longer than the screen is cut } count++; if (c == ' ') { lastSpace = i; space = true; } } return size; } size_t NoteText::lineOf(size_t pos) const { size_t size = text_.size(); pos = std::min(pos, size); size_t a = pos; // the start of the paragraph: after the newline before `pos` while (a > 0 && text_[a - 1] != '\n') a--; while (a < size) { size_t n = nextLine(a); if (n > pos) return a; // The end of the text is on the last line, unless that line ended with a newline: then // it's on an empty line of its own. if (n == size && pos == size && text_[size - 1] != '\n') return a; a = n; } return a; } bool NoteText::hasLineAfter(size_t start) const { size_t n = nextLine(start), size = text_.size(); if (n < size) return true; return start < size && lineOf(size) != start; // the empty line after a final newline } size_t NoteText::lastSpot(size_t start) const { size_t n = nextLine(start), size = text_.size(); if (n == start) return start; // the empty line at the end if (n == size && lineOf(size) == start) return size; size_t p = n - 1; // before the newline, the space or the last character the line ends with while (p > start && continuation(text_[p])) p--; return p; } int NoteText::columnOf(size_t start, size_t pos) const { int col = 0; for (size_t i = start; i < pos && i < text_.size(); i++) if (!continuation(text_[i])) col++; return col; } size_t NoteText::atColumn(size_t start, int col) const { size_t last = lastSpot(start), p = start; while (p < last && col > 0) { p++; while (p < last && continuation(text_[p])) p++; col--; } return p; } void NoteText::moved(bool keepGoal) { if (!keepGoal) goal_ = -1; } bool NoteText::insertText(const std::string& s) { if (text_.size() + s.size() > kMaxBytes) return false; text_.insert(cursor_, s); cursor_ += s.size(); revision_++; moved(); return true; } bool NoteText::insert(uint32_t cp) { std::string s; if (cp < 0x80) s += static_cast(cp); else if (cp < 0x800) { s += static_cast(0xC0 | (cp >> 6)); s += static_cast(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { s += static_cast(0xE0 | (cp >> 12)); s += static_cast(0x80 | ((cp >> 6) & 0x3F)); s += static_cast(0x80 | (cp & 0x3F)); } else { s += static_cast(0xF0 | (cp >> 18)); s += static_cast(0x80 | ((cp >> 12) & 0x3F)); s += static_cast(0x80 | ((cp >> 6) & 0x3F)); s += static_cast(0x80 | (cp & 0x3F)); } return insertText(s); } void NoteText::backspace() { if (cursor_ == 0) return; size_t from = cursor_ - 1; while (from > 0 && continuation(text_[from])) from--; text_.erase(from, cursor_ - from); cursor_ = from; revision_++; moved(); } void NoteText::left() { if (cursor_ == 0) return; cursor_--; while (cursor_ > 0 && continuation(text_[cursor_])) cursor_--; moved(); } void NoteText::right() { if (cursor_ >= text_.size()) return; cursor_++; while (cursor_ < text_.size() && continuation(text_[cursor_])) cursor_++; moved(); } void NoteText::up() { size_t line = lineOf(cursor_); if (goal_ < 0) goal_ = columnOf(line, cursor_); if (line == 0) return; cursor_ = atColumn(lineOf(line - 1), goal_); moved(true); } void NoteText::down() { size_t line = lineOf(cursor_); if (goal_ < 0) goal_ = columnOf(line, cursor_); if (!hasLineAfter(line)) return; size_t next = nextLine(line); cursor_ = next >= text_.size() ? text_.size() : atColumn(next, goal_); moved(true); } void NoteText::pageUp() { for (int i = 1; i < rows_; i++) up(); } void NoteText::pageDown() { for (int i = 1; i < rows_; i++) down(); } void NoteText::lineStart() { cursor_ = lineOf(cursor_); moved(); } void NoteText::lineEnd() { cursor_ = lastSpot(lineOf(cursor_)); moved(); } void NoteText::toStart() { cursor_ = 0; moved(); } void NoteText::toEnd() { cursor_ = text_.size(); moved(); } void NoteText::follow() { size_t line = lineOf(cursor_); top_ = lineOf(std::min(top_, text_.size())); // an edit above may have moved where lines start if (line < top_) { top_ = line; return; } size_t a = top_; for (int i = 0; i < rows_; i++) { if (a == line) return; // on screen if (!hasLineAfter(a)) return; a = nextLine(a); } // Below the screen: the cursor's line becomes the last row. top_ = line; for (int i = 1; i < rows_ && top_ > 0; i++) top_ = lineOf(top_ - 1); } std::vector NoteText::rows() { follow(); std::vector out; size_t a = top_, size = text_.size(); for (int i = 0; i < rows_; i++) { size_t n = nextLine(a); std::string row = text_.substr(a, n - a); if (!row.empty() && row.back() == '\n') row.pop_back(); for (char& c : row) if (c == '\t') c = ' '; out.push_back(std::move(row)); if (!hasLineAfter(a)) break; a = n >= size ? size : n; } return out; } int NoteText::cursorRow() { follow(); size_t line = lineOf(cursor_), a = top_; for (int i = 0; i < rows_; i++) { if (a == line) return i; a = nextLine(a); } return rows_ - 1; } int NoteText::cursorCol() { return columnOf(lineOf(cursor_), cursor_); } // At most a screen's worth of it: a note that is one line of 16 KB must not be copied whole. std::string NoteText::firstLine() const { return text_.substr(0, std::min(text_.find('\n'), 160)); } namespace { // U+00C0 to U+00FF as the plain letters a file name gets; 0: left out. const char kPlain[64] = { 'a', 'a', 'a', 'a', 'a', 'a', 0, 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', // À..Ï 0, 'n', 'o', 'o', 'o', 'o', 'o', 0, 'o', 'u', 'u', 'u', 'u', 'y', 0, 's', // Ð..ß 'a', 'a', 'a', 'a', 'a', 'a', 0, 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', // à..ï 0, 'n', 'o', 'o', 'o', 'o', 'o', 0, 'o', 'u', 'u', 'u', 'u', 'y', 0, 'y'}; // ð..ÿ // Drops a character the end of the string cuts in two. void dropPartial(std::string& s) { size_t k = s.size(); while (k > 0 && continuation(s[k - 1]) && s.size() - k < 3) k--; if (k == 0) return; uint8_t lead = static_cast(s[k - 1]); size_t want = lead >= 0xF0 ? 4 : lead >= 0xE0 ? 3 : lead >= 0xC0 ? 2 : 1; if (s.size() - (k - 1) < want) s.resize(k - 1); } } // namespace std::string nameFromFirstLine(const std::string& firstLine, const std::string& stamp) { std::string out; bool dash = false; for (size_t i = 0; i < firstLine.size() && out.size() < 32; i++) { uint8_t c = static_cast(firstLine[i]); char letter = 0; if (c < 0x80) { if (c >= 'A' && c <= 'Z') letter = static_cast(c + 32); else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) letter = static_cast(c); } else if (c == 0xC3 && i + 1 < firstLine.size()) { // U+00C0..U+00FF letter = kPlain[static_cast(firstLine[++i]) & 0x3F]; } else { while (i + 1 < firstLine.size() && continuation(firstLine[i + 1])) i++; // anything else is left out } if (letter) { if (dash && !out.empty()) out += '-'; dash = false; out += letter; } else { dash = true; } } return out.empty() ? "note-" + stamp : out; } std::string titleFrom(const std::string& head, size_t maxChars) { for (size_t at = 0; at < head.size();) { size_t end = head.find('\n', at); bool cut = end == std::string::npos; // the line goes on past what was read if (cut) end = head.size(); std::string line = head.substr(at, end - at); if (cut) dropPartial(line); while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) line.pop_back(); size_t first = line.find_first_not_of(" \t"); if (first != std::string::npos) { line.erase(0, first); size_t bytes = 0; for (size_t chars = 0; bytes < line.size() && chars < maxChars; chars++) { bytes++; while (bytes < line.size() && continuation(line[bytes])) bytes++; } line.resize(bytes); return line; } at = end + 1; } return ""; } } // namespace roro::notes