changeset 424:cd3f7c712d9e

Irccd: add new Irccd.Util.cut function, closes #635
author David Demelier <markand@malikania.fr>
date Thu, 09 Feb 2017 18:05:41 +0100
parents 186864e9f131
children 70b0c9e40131
files doc/html/CMakeLists.txt doc/html/api/module/Irccd.Util/Irccd.Util.cut.md doc/html/api/module/Irccd.Util/index.md libirccd-js/irccd/mod-util.cpp tests/js-util/main.cpp
diffstat 5 files changed, 435 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/doc/html/CMakeLists.txt	Thu Feb 02 13:55:32 2017 +0100
+++ b/doc/html/CMakeLists.txt	Thu Feb 09 18:05:41 2017 +0100
@@ -108,8 +108,9 @@
     ${html_SOURCE_DIR}/api/module/Irccd.Unicode/Irccd.Unicode.isDigit.md
     ${html_SOURCE_DIR}/api/module/Irccd.Unicode/Irccd.Unicode.isLower.md
     ${html_SOURCE_DIR}/api/module/Irccd.Util/index.md
+    ${html_SOURCE_DIR}/api/module/Irccd.Util/Irccd.Util.cut.md
+    ${html_SOURCE_DIR}/api/module/Irccd.Util/Irccd.Util.format.md
     ${html_SOURCE_DIR}/api/module/Irccd.Util/Irccd.Util.splithost.md
-    ${html_SOURCE_DIR}/api/module/Irccd.Util/Irccd.Util.format.md
     ${html_SOURCE_DIR}/api/module/Irccd.Util/Irccd.Util.splituser.md
     ${html_SOURCE_DIR}/api/index.md
     ${html_SOURCE_DIR}/api/event/onWhois.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/html/api/module/Irccd.Util/Irccd.Util.cut.md	Thu Feb 09 18:05:41 2017 +0100
@@ -0,0 +1,30 @@
+---
+function: format
+js: true
+summary: "Cut a piece of data into several lines."
+synopsis: "lines = Irccd.Util.cut(data, maxc, maxl)"
+arguments:
+  - "**data**: a string or an array of strings,"
+  - "**maxc**: max number of colums (Optional, default: 72),"
+  - "**maxl**: max number of lines (Optional, default: undefined)."
+returns: "A list of strings ready to be sent or undefined if the data is too big."
+throws:
+  - "**RangeError** if maxl or maxc are negative numbers,"
+  - "**RangeError** if one word length was bigger than maxc,"
+  - "**TypeError** if data is not a string or a list of strings."
+---
+
+The argument data is a string or a list of strings. In any case, all strings
+are first splitted by spaces and trimmed. This ensure that useless
+whitespaces are discarded.
+
+The argument maxc controls the maximum of characters allowed per line, it can
+be a positive integer. If undefined is given, a default of 72 is used.
+
+The argument maxl controls the maximum of lines allowed. It can be a positive
+integer or undefined for an infinite list.
+
+If maxl is used as a limit and the data can not fit within the bounds,
+undefined is returned.
+
+An empty list may be returned if empty strings were found.
--- a/doc/html/api/module/Irccd.Util/index.md	Thu Feb 02 13:55:32 2017 +0100
+++ b/doc/html/api/module/Irccd.Util/index.md	Thu Feb 09 18:05:41 2017 +0100
@@ -9,6 +9,7 @@
 
 ## Functions
 
+  - [cut](Irccd.Util.cut.html)
   - [format](Irccd.Util.format.html)
   - [splituser](Irccd.Util.splituser.html)
   - [splithost](Irccd.Util.splithost.html)
--- a/libirccd-js/irccd/mod-util.cpp	Thu Feb 02 13:55:32 2017 +0100
+++ b/libirccd-js/irccd/mod-util.cpp	Thu Feb 09 18:05:41 2017 +0100
@@ -16,6 +16,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <climits>
+
 #include <libircclient.h>
 
 #include "mod-util.hpp"
@@ -56,6 +58,156 @@
 }
 
 /*
+ * split (for Irccd.Util.cut as cut)
+ * ------------------------------------------------------------------
+ *
+ * Extract individual tokens in array or a whole string as a std:::vector.
+ */
+std::vector<std::string> split(duk_context *ctx)
+{
+    duk_require_type_mask(ctx, 0, DUK_TYPE_MASK_OBJECT | DUK_TYPE_MASK_STRING);
+
+    std::vector<std::string> result;
+    std::string pattern = " \t\n";
+
+    if (duk_is_string(ctx, 0)) {
+        result = util::split(dukx_get_std_string(ctx, 0), pattern);
+    } else if (duk_is_array(ctx, 0)) {
+        duk_enum(ctx, 0, DUK_ENUM_ARRAY_INDICES_ONLY);
+
+        while (duk_next(ctx, -1, 1)) {
+            // Split individual tokens as array if spaces are found.
+            auto tmp = util::split(duk_to_string(ctx, -1), pattern);
+
+            result.insert(result.end(), tmp.begin(), tmp.end());
+            duk_pop_2(ctx);
+        }
+    }
+
+    return result;
+}
+
+/*
+ * limit (for Irccd.Util.cut as cut)
+ * ------------------------------------------------------------------
+ *
+ * Get the maxl/maxc argument.
+ *
+ * The argument value is the default and also used as the result returned.
+ */
+int limit(duk_context *ctx, int index, const char *name, int value)
+{
+    if (duk_get_top(ctx) < index || !duk_is_number(ctx, index)) {
+        return value;
+    }
+
+    value = duk_to_int(ctx, index);
+    
+    if (value <= 0) {
+        duk_error(ctx, DUK_ERR_RANGE_ERROR, "argument %d (%s) must be positive", index, name);
+    }
+
+    return value;
+}
+
+/*
+ * lines (for Irccd.Util.cut as cut)
+ * ------------------------------------------------------------------
+ *
+ * Build a list of lines.
+ *
+ * Several cases possible:
+ *
+ *   - s is the current line
+ *   - abc is the token to add
+ *
+ * s   = ""                 (new line)
+ * s  -> "abc"
+ *
+ * s   = "hello world"      (enough room)
+ * s  -> "hello world abc"
+ *
+ * s   = "hello world"      (not enough room: maxc is smaller)
+ * s+1 = "abc"
+ */
+std::vector<std::string> lines(duk_context *ctx, const std::vector<std::string>& tokens, int maxc)
+{
+    std::vector<std::string> result{""};
+
+    for (const auto &s : tokens) {
+        if (s.length() > static_cast<std::size_t>(maxc)) {
+            duk_error(ctx, DUK_ERR_RANGE_ERROR, "word '%s' could not fit in maxc limit (%d)", s.c_str(), maxc);
+        }
+
+        // Compute the length required (prepend a space if needed)
+        auto required = s.length() + (result.back().empty() ? 0 : 1);
+
+        if (result.back().length() + required > static_cast<std::size_t>(maxc)) {
+            result.push_back(s);
+        } else {
+            if (!result.back().empty()) {
+                result.back() += ' ';
+            }
+            result.back() += s;
+        }
+    }
+
+    return result;
+}
+
+/*
+ * Function: Irccd.Util.cut(data, maxc, maxl)
+ * --------------------------------------------------------
+ *
+ * Cut a piece of data into several lines.
+ *
+ * The argument data is a string or a list of strings. In any case, all strings
+ * are first splitted by spaces and trimmed. This ensure that useless
+ * whitespaces are discarded.
+ *
+ * The argument maxc controls the maximum of characters allowed per line, it can
+ * be a positive integer. If undefined is given, a default of 72 is used.
+ *
+ * The argument maxl controls the maximum of lines allowed. It can be a positive
+ * integer or undefined for an infinite list.
+ *
+ * If maxl is used as a limit and the data can not fit within the bounds,
+ * undefined is returned.
+ *
+ * An empty list may be returned if empty strings were found.
+ *
+ * Arguments:
+ *   - data, a string or an array of strings,
+ *   - maxc, max number of colums (Optional, default: 72),
+ *   - maxl, max number of lines (Optional, default: undefined).
+ * Returns:
+ *   A list of strings ready to be sent or undefined if the data is too big.
+ * Throws:
+ *   - RangeError if maxl or maxc are negative numbers,
+ *   - RangeError if one word length was bigger than maxc,
+ *   - TypeError if data is not a string or a list of strings.
+ */
+duk_ret_t cut(duk_context *ctx)
+{
+    auto list = lines(ctx, split(ctx), limit(ctx, 1, "maxc", 72));
+    auto maxl = limit(ctx, 2, "maxl", INT_MAX);
+
+    if (list.size() > static_cast<std::size_t>(maxl)) {
+        return 0;
+    }
+
+    // Empty list but lines() returns at least one.
+    if (list.size() == 1 && list[0].empty()) {
+        duk_push_array(ctx);
+        return 1;
+    }
+
+    dukx_push_array(ctx, list, dukx_push_std_string);
+
+    return 1;
+}
+
+/*
  * Function: Irccd.Util.format(text, parameters)
  * --------------------------------------------------------
  *
@@ -123,6 +275,7 @@
 }
 
 const duk_function_list_entry functions[] = {
+    { "cut",        cut,        DUK_VARARGS },
     { "format",     format,     DUK_VARARGS },
     { "splituser",  splituser,  1           },
     { "splithost",  splithost,  1           },
--- a/tests/js-util/main.cpp	Thu Feb 02 13:55:32 2017 +0100
+++ b/tests/js-util/main.cpp	Thu Feb 09 18:05:41 2017 +0100
@@ -83,6 +83,255 @@
     }
 }
 
+/*
+ * Irccd.Util.cut
+ * ------------------------------------------------------------------
+ */
+
+TEST_F(TestJsUtil, cut_string_simple)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "lines = Irccd.Util.cut('hello world');\n"
+            "line0 = lines[0];\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line0"));
+        ASSERT_STREQ("hello world", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_string_double)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "lines = Irccd.Util.cut('hello world', 5);\n"
+            "line0 = lines[0];\n"
+            "line1 = lines[1];\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line0"));
+        ASSERT_STREQ("hello", duk_get_string(m_plugin->context(), -1));
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line1"));
+        ASSERT_STREQ("world", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_string_dirty)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "lines = Irccd.Util.cut('     hello    world     ', 5);\n"
+            "line0 = lines[0];\n"
+            "line1 = lines[1];\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line0"));
+        ASSERT_STREQ("hello", duk_get_string(m_plugin->context(), -1));
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line1"));
+        ASSERT_STREQ("world", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_string_too_much_lines)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "lines = Irccd.Util.cut('abc def ghi jkl', 3, 3);\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "lines"));
+        ASSERT_TRUE(duk_is_undefined(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_string_token_too_big)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "try {\n"
+            "  lines = Irccd.Util.cut('hello world', 3);\n"
+            "} catch (e) {\n"
+            "  name = e.name;\n"
+            "  message = e.message;\n"
+            "}\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "name"));
+        ASSERT_STREQ("RangeError", duk_get_string(m_plugin->context(), -1));
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "message"));
+        ASSERT_STREQ("word 'hello' could not fit in maxc limit (3)", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_string_negative_maxc)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "try {\n"
+            "  lines = Irccd.Util.cut('hello world', -3);\n"
+            "} catch (e) {\n"
+            "  name = e.name;\n"
+            "  message = e.message;\n"
+            "}\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "name"));
+        ASSERT_STREQ("RangeError", duk_get_string(m_plugin->context(), -1));
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "message"));
+        ASSERT_STREQ("argument 1 (maxc) must be positive", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_string_negative_maxl)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "try {\n"
+            "  lines = Irccd.Util.cut('hello world', undefined, -1);\n"
+            "} catch (e) {\n"
+            "  name = e.name;\n"
+            "  message = e.message;\n"
+            "}\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "name"));
+        ASSERT_STREQ("RangeError", duk_get_string(m_plugin->context(), -1));
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "message"));
+        ASSERT_STREQ("argument 2 (maxl) must be positive", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_array_simple)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "lines = Irccd.Util.cut([ 'hello', 'world' ]);\n"
+            "line0 = lines[0];\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line0"));
+        ASSERT_STREQ("hello world", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_array_double)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "lines = Irccd.Util.cut([ 'hello', 'world' ], 5);\n"
+            "line0 = lines[0];\n"
+            "line1 = lines[1];\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line0"));
+        ASSERT_STREQ("hello", duk_get_string(m_plugin->context(), -1));
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line1"));
+        ASSERT_STREQ("world", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_array_dirty)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "lines = Irccd.Util.cut([ '   ', ' hello  ', '  world ', '    '], 5);\n"
+            "line0 = lines[0];\n"
+            "line1 = lines[1];\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line0"));
+        ASSERT_STREQ("hello", duk_get_string(m_plugin->context(), -1));
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "line1"));
+        ASSERT_STREQ("world", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(TestJsUtil, cut_invalid_data)
+{
+    try {
+        auto ret = duk_peval_string(m_plugin->context(),
+            "try {\n"
+            "  lines = Irccd.Util.cut(123);\n"
+            "print(':(');"
+            "} catch (e) {\n"
+            "print(':)');"
+            "  name = e.name;\n"
+            "  message = e.message;\n"
+            "}\n"
+        );
+
+        if (ret != 0) {
+            throw dukx_exception(m_plugin->context(), -1);
+        }
+
+        ASSERT_TRUE(duk_get_global_string(m_plugin->context(), "name"));
+        ASSERT_STREQ("TypeError", duk_get_string(m_plugin->context(), -1));
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
 int main(int argc, char **argv)
 {
     testing::InitGoogleTest(&argc, argv);