diff options
-rw-r--r-- | include/textproc.h | 24 | ||||
-rw-r--r-- | src/bots.cpp | 5 | ||||
-rw-r--r-- | src/command.cpp | 10 | ||||
-rw-r--r-- | src/misc.cpp | 180 | ||||
-rw-r--r-- | src/users.cpp | 11 |
5 files changed, 216 insertions, 14 deletions
diff --git a/include/textproc.h b/include/textproc.h new file mode 100644 index 000000000..62c88bcb4 --- /dev/null +++ b/include/textproc.h @@ -0,0 +1,24 @@ +/* + * + * (C) 2003-2025 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#pragma once + +class TextSplitter final +{ +private: + Anope::string text; + std::vector<Anope::string> formatting; + +public: + TextSplitter(const Anope::string &t); + bool GetLine(Anope::string &out); +}; + diff --git a/src/bots.cpp b/src/bots.cpp index d2cbd0cb4..33afc7595 100644 --- a/src/bots.cpp +++ b/src/bots.cpp @@ -299,7 +299,10 @@ Anope::string BotInfo::GetQueryCommand(const Anope::string &command, const Anope if (!extra.empty()) buf.append(" ").append(extra); - return buf; + // We use a substitute (ASCII 0x1A) instead of a space (ASCII 0x20) so it + // doesn't get line wrapped when put into a message. The line wrapper will + // convert this to a space before it is sent to clients. + return buf.replace_all_cs("\x20", "\x1A"); } BotInfo *BotInfo::Find(const Anope::string &nick, bool nick_only) diff --git a/src/command.cpp b/src/command.cpp index 94f88aba5..f307e8aa4 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -113,7 +113,7 @@ void CommandSource::Reply(const char *message, ...) va_start(args, message); vsnprintf(buf, sizeof(buf), translated_message, args); - this->Reply(Anope::string(buf)); + this->reply->SendMessage(*this, buf); va_end(args); } @@ -128,7 +128,7 @@ void CommandSource::Reply(int count, const char *single, const char *plural, ... va_start(args, plural); vsnprintf(buf, sizeof(buf), translated_message, args); - this->Reply(Anope::string(buf)); + this->reply->SendMessage(*this, buf); va_end(args); } @@ -136,11 +136,7 @@ void CommandSource::Reply(int count, const char *single, const char *plural, ... void CommandSource::Reply(const Anope::string &message) { const char *translated_message = Language::Translate(this->nc, message.c_str()); - - sepstream sep(translated_message, '\n', true); - Anope::string tok; - while (sep.GetToken(tok)) - this->reply->SendMessage(*this, tok); + this->reply->SendMessage(*this, translated_message); } Command::Command(Module *o, const Anope::string &sname, size_t minparams, size_t maxparams) : Service(o, "Command", sname), max_params(maxparams), min_params(minparams), module(o) diff --git a/src/misc.cpp b/src/misc.cpp index 73a6fd9bd..0a6a38488 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -18,6 +18,7 @@ #include "language.h" #include "regexpr.h" #include "sockets.h" +#include "textproc.h" #include <cerrno> #include <climits> @@ -263,6 +264,185 @@ void InfoFormatter::AddOption(const Anope::string &opt) *optstr += Language::Translate(nc, opt.c_str()); } +TextSplitter::TextSplitter(const Anope::string &t) + : text(t) +{ +} + +bool TextSplitter::GetLine(Anope::string &out) +{ + out.clear(); + if (text.empty()) + return false; + + // Start by copying all of the formatting from the previous line + // onto this one. + for (const auto &fmt : formatting) + out.append(fmt); + + // The maximum length of a line. + const auto max_length = Config->GetBlock("options").Get<size_t>("linelength", "120"); + + // The current printable length of the output. + size_t current_length = 0; + + // Whether a newline was encountered or we hit the max line length. + bool forced_linebreak = false; + + // The index of the last space we can split on. + size_t last_space = 0; + + // Formatting which has been seen since the last space. + std::vector<Anope::string> uncertain_formatting; + + size_t idx = 0; + auto toggle_formatting = [this, &idx, &uncertain_formatting](const Anope::string &fmt) + { + auto it = std::find_if(formatting.begin(), formatting.end(), [&fmt](const auto& f) { + return f[0] == fmt[0]; + }); + if (it == formatting.end()) + { + formatting.push_back(fmt); + uncertain_formatting.push_back(fmt); + } + else + { + formatting.erase(it); + uncertain_formatting.push_back(*it); + } + }; + + for (idx = 0; idx < text.length(); ++idx) + { + if (current_length >= max_length) + { + for (const auto &uf : uncertain_formatting) + toggle_formatting(uf); + + forced_linebreak = true; + break; // Max length reached. + } + + auto chr = text[idx]; + switch (chr) + { + case '\x02': // IRC bold + case '\x1D': // IRC italic + case '\x11': // IRC monospace + case '\x16': // IRC reverse + case '\x1E': // IRC strikethrough + case '\x1F': // IRC underline + { + // These formatting characters are a simple toggle. + toggle_formatting(chr); + out.push_back(chr); + break; + } + + case '\x03': // Color + { + const auto start = idx; + while (++idx < text.length() && idx - start < 6) + { + chr = text[idx]; + if (chr != ',' && (chr < '0' || chr > '9')) + { + idx--; + break; + } + } + + auto color = text.substr(start, start - idx); + toggle_formatting(color); + out.append(color); + break; + } + case '\x04': // Hex color + { + const auto start = idx; + while (++idx < text.length() && idx - start < 14) + { + chr = text[idx]; + if (chr != ',' && (chr < '0' || chr > '9') && (chr < 'A' || chr > 'F') && (chr < 'a' || chr > 'f')) + { + idx--; + break; + } + + auto color = text.substr(start, start - idx); + toggle_formatting(color); + out.append(color); + } + + break; + } + + case '\x0A': // Forced newline (line feed) + case '\x0D': // Forced newline (carriage return) + { + formatting.clear(); + last_space = idx; + forced_linebreak = true; + break; + } + + case '\x0F': // IRC reset. + { + formatting.clear(); + out.push_back(chr); + break; + } + + case '\x1A': // Non-breaking space + { + // There aren't any single byte non-breaking spaces so we use + // a substitute for that purpose. + current_length++; + out.push_back(' '); + break; + } + + case '\x20': // Breaking space. + { + + // Unlike above we can split on this. + last_space = idx; + uncertain_formatting.clear(); + current_length++; + out.push_back(' '); + break; + } + + default: // Non-formatting character. + current_length++; + out.push_back(chr); + break; + } + + if (forced_linebreak) + break; + } + + if (forced_linebreak) + { + if (!last_space) + last_space = idx; + + text.erase(0, last_space + 1); + out.erase(last_space); + } + else + { + // We either reached to the end of the text without needing to line wrap or + // we encountered a word so big that it couldn't be linewrapped. + text.clear(); + } + + return true; +} + + bool Anope::IsFile(const Anope::string &filename) { struct stat fileinfo; diff --git a/src/users.cpp b/src/users.cpp index 8f8e9570d..33e840160 100644 --- a/src/users.cpp +++ b/src/users.cpp @@ -21,6 +21,7 @@ #include "opertype.h" #include "language.h" #include "sockets.h" +#include "textproc.h" #include "uplink.h" user_map UserListByNick, UserListByUID; @@ -347,15 +348,13 @@ namespace { void SendMessageInternal(BotInfo *source, User *target, const Anope::string &msg, const Anope::map<Anope::string> &tags) { - const char *translated_message = Language::Translate(target, msg.c_str()); - - sepstream sep(translated_message, '\n', true); - for (Anope::string tok; sep.GetToken(tok);) + TextSplitter ts(Language::Translate(target, msg.c_str())); + for (Anope::string line; ts.GetLine(line); ) { if (target->ShouldPrivmsg()) - IRCD->SendPrivmsg(source, target->GetUID(), tok, tags); + IRCD->SendPrivmsg(source, target->GetUID(), line, tags); else - IRCD->SendNotice(source, target->GetUID(), tok, tags); + IRCD->SendNotice(source, target->GetUID(), line, tags); } } } |