diff options
author | Lennart Poettering <lennart@poettering.net> | 2024-04-26 17:40:32 +0200 |
---|---|---|
committer | Lennart Poettering <lennart@poettering.net> | 2024-06-17 09:20:21 +0200 |
commit | aca093018c5d2cd8a63129cab67941fe1b8fd850 (patch) | |
tree | 7fd7cbd054dc18727f2f48909fcdba19bf3d61e8 /src | |
parent | utf8: export utf8_char_console_width() (diff) | |
download | systemd-aca093018c5d2cd8a63129cab67941fe1b8fd850.tar.xz systemd-aca093018c5d2cd8a63129cab67941fe1b8fd850.zip |
strv: add new helper strv_rebreak_lines() with a simple line breaking algorithm
Diffstat (limited to 'src')
-rw-r--r-- | src/basic/strv.c | 90 | ||||
-rw-r--r-- | src/basic/strv.h | 2 | ||||
-rw-r--r-- | src/test/test-strv.c | 58 |
3 files changed, 150 insertions, 0 deletions
diff --git a/src/basic/strv.c b/src/basic/strv.c index d081821a86..4709dfaf24 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -11,11 +11,13 @@ #include "escape.h" #include "extract-word.h" #include "fileio.h" +#include "gunicode.h" #include "memory-util.h" #include "nulstr-util.h" #include "sort-util.h" #include "string-util.h" #include "strv.h" +#include "utf8.h" char* strv_find(char * const *l, const char *name) { assert(name); @@ -967,3 +969,91 @@ int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const } DEFINE_HASH_OPS_FULL(string_strv_hash_ops, char, string_hash_func, string_compare_func, free, char*, strv_free); + +int strv_rebreak_lines(char **l, size_t width, char ***ret) { + _cleanup_strv_free_ char **broken = NULL; + int r; + + assert(ret); + + /* Implements a simple UTF-8 line breaking algorithm + * + * Goes through all entries in *l, and line-breaks each line that is longer than the specified + * character width. Breaks at the end of words/beginning of whitespace. Lines that do not contain whitespace are not + * broken. Retains whitespace at beginning of lines, removes it at end of lines. */ + + if (width == SIZE_MAX) { /* NOP? */ + broken = strv_copy(l); + if (!broken) + return -ENOMEM; + + *ret = TAKE_PTR(broken); + return 0; + } + + STRV_FOREACH(i, l) { + const char *start = *i, *whitespace_begin = NULL, *whitespace_end = NULL; + bool in_prefix = true; /* still in the whitespace in the beginning of the line? */ + size_t w = 0; + + for (const char *p = start; *p != 0; p = utf8_next_char(p)) { + if (strchr(NEWLINE, *p)) { + in_prefix = true; + whitespace_begin = whitespace_end = NULL; + w = 0; + } else if (strchr(WHITESPACE, *p)) { + if (!in_prefix && (!whitespace_begin || whitespace_end)) { + whitespace_begin = p; + whitespace_end = NULL; + } + } else { + if (whitespace_begin && !whitespace_end) + whitespace_end = p; + + in_prefix = false; + } + + int cw = utf8_char_console_width(p); + if (cw < 0) { + log_debug_errno(cw, "Comment to line break contains invalid UTF-8, ignoring."); + cw = 1; + } + + w += cw; + + if (w > width && whitespace_begin && whitespace_end) { + _cleanup_free_ char *truncated = NULL; + + truncated = strndup(start, whitespace_begin - start); + if (!truncated) + return -ENOMEM; + + r = strv_consume(&broken, TAKE_PTR(truncated)); + if (r < 0) + return r; + + p = start = whitespace_end; + whitespace_begin = whitespace_end = NULL; + w = cw; + } + } + + if (start) { /* Process rest of the line */ + if (in_prefix) /* Never seen anything non-whitespace? Generate empty line! */ + r = strv_extend(&broken, ""); + else if (whitespace_begin && !whitespace_end) { /* Ends in whitespace? Chop it off! */ + _cleanup_free_ char *truncated = strndup(start, whitespace_begin - start); + if (!truncated) + return -ENOMEM; + + r = strv_consume(&broken, TAKE_PTR(truncated)); + } else /* Otherwise use line as is */ + r = strv_extend(&broken, start); + if (r < 0) + return r; + } + } + + *ret = TAKE_PTR(broken); + return 0; +} diff --git a/src/basic/strv.h b/src/basic/strv.h index 169737d1d8..c828bd612f 100644 --- a/src/basic/strv.h +++ b/src/basic/strv.h @@ -257,3 +257,5 @@ int _string_strv_hashmap_put(Hashmap **h, const char *key, const char *value HA int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const char *value HASHMAP_DEBUG_PARAMS); #define string_strv_hashmap_put(h, k, v) _string_strv_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS) #define string_strv_ordered_hashmap_put(h, k, v) _string_strv_ordered_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS) + +int strv_rebreak_lines(char **l, size_t width, char ***ret); diff --git a/src/test/test-strv.c b/src/test/test-strv.c index 28b8b2270c..65afefed3b 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -1055,4 +1055,62 @@ TEST(strv_extend_many) { assert_se(strv_equal(l, STRV_MAKE("foo", "bar", "waldo", "quux", "1", "2", "3", "4", "yes", "no"))); } +TEST(strv_rebreak_lines) { + _cleanup_strv_free_ char **l = NULL; + + assert_se(strv_rebreak_lines(NULL, SIZE_MAX, &l) >= 0); + assert_se(strv_equal(l, NULL)); + l = strv_free(l); + + assert_se(strv_rebreak_lines(STRV_MAKE(""), SIZE_MAX, &l) >= 0); + assert_se(strv_equal(l, STRV_MAKE(""))); + l = strv_free(l); + + assert_se(strv_rebreak_lines(STRV_MAKE("", ""), SIZE_MAX, &l) >= 0); + assert_se(strv_equal(l, STRV_MAKE("", ""))); + l = strv_free(l); + + assert_se(strv_rebreak_lines(STRV_MAKE("foo"), SIZE_MAX, &l) >= 0); + assert_se(strv_equal(l, STRV_MAKE("foo"))); + l = strv_free(l); + + assert_se(strv_rebreak_lines(STRV_MAKE("foo", "bar"), SIZE_MAX, &l) >= 0); + assert_se(strv_equal(l, STRV_MAKE("foo", "bar"))); + l = strv_free(l); + + assert_se(strv_rebreak_lines(STRV_MAKE("Foo fOo foO FOo", "bar Bar bAr baR BAr"), 10, &l) >= 0); + assert_se(strv_equal(l, STRV_MAKE("Foo fOo", "foO FOo", "bar Bar", "bAr baR", "BAr"))); + l = strv_free(l); + + assert_se(strv_rebreak_lines(STRV_MAKE(" foo ", + " foo bar waldo quux "), + 10, &l) >= 0); + assert_se(strv_equal(l, STRV_MAKE(" foo", + " foo", + "bar", + "waldo quux"))); + l = strv_free(l); + + assert_se(strv_rebreak_lines(STRV_MAKE(" ", + "\tfoo bar\t", + "FOO\tBAR"), + 10, &l) >= 0); + assert_se(strv_equal(l, STRV_MAKE("", + "\tfoo", + "bar", + "FOO", + "BAR"))); + l = strv_free(l); + + /* Now make sure that breaking the lines a 2nd time does not modify the output anymore */ + for (size_t i = 1; i < 100; i++) { + _cleanup_strv_free_ char **a = NULL, **b = NULL; + + assert_se(strv_rebreak_lines(STRV_MAKE("foobar waldo waldo quux piep\tschnurz pimm"), i, &a) >= 0); + assert_se(strv_rebreak_lines(a, i, &b) >= 0); + + assert_se(strv_equal(a, b)); + } +} + DEFINE_TEST_MAIN(LOG_INFO); |