summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/textproc.h24
-rw-r--r--src/bots.cpp5
-rw-r--r--src/command.cpp10
-rw-r--r--src/misc.cpp180
-rw-r--r--src/users.cpp11
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);
}
}
}