Mercurial > irccd
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);