--- lbbs/src/lml.c 2025/10/30 03:18:56 1.35 +++ lbbs/src/lml.c 2025/11/17 12:47:41 1.48 @@ -1,28 +1,32 @@ -/*************************************************************************** - lml.h - description - ------------------- - Copyright : (C) 2004-2025 by Leaflet - Email : leaflet@leafok.com - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 3 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * lml + * - LML render + * + * Copyright (C) 2004-2025 Leaflet + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif #include "common.h" #include "lml.h" #include "log.h" +#include "str_process.h" +#include #include #include #include -#define LML_TAG_PARAM_BUF_LEN 256 -#define LML_TAG_OUTPUT_BUF_LEN 1024 +enum _lml_constant_t +{ + LML_TAG_PARAM_BUF_LEN = 256, + LML_TAG_OUTPUT_BUF_LEN = 1024, + LML_TAG_QUOTE_MAX_LEVEL = 10, +}; + +clock_t lml_total_exec_duration = 0; typedef int (*lml_tag_filter_cb)(const char *tag_name, const char *tag_param_buf, char *tag_output_buf, size_t tag_output_buf_len, int quote_mode); @@ -62,8 +66,6 @@ static int lml_tag_color_filter(const ch return 0; } -#define LML_TAG_QUOTE_MAX_LEVEL 10 - static const char *lml_tag_quote_color[] = { "\033[33m", // yellow "\033[32m", // green @@ -158,9 +160,8 @@ const LML_TAG_DEF lml_tag_def[] = { {"bwf", "\033[1;31m****\033[m", "", "****", NULL}, }; -#define LML_TAG_COUNT (sizeof(lml_tag_def) / sizeof(LML_TAG_DEF)) - -static int lml_tag_name_len[LML_TAG_COUNT]; +static const int lml_tag_count = sizeof(lml_tag_def) / sizeof(LML_TAG_DEF); +static int lml_tag_name_len[sizeof(lml_tag_def) / sizeof(LML_TAG_DEF)]; static int lml_ready = 0; inline static void lml_init(void) @@ -169,7 +170,7 @@ inline static void lml_init(void) if (!lml_ready) { - for (i = 0; i < LML_TAG_COUNT; i++) + for (i = 0; i < lml_tag_count; i++) { lml_tag_name_len[i] = (int)strlen(lml_tag_def[i].tag_name); } @@ -178,7 +179,7 @@ inline static void lml_init(void) } } -#define CHECK_AND_APPEND_OUTPUT(out_buf, out_buf_len, out_buf_offset, tag_out, tag_out_len) \ +#define CHECK_AND_APPEND_OUTPUT(out_buf, out_buf_len, out_buf_offset, tag_out, tag_out_len, line_width) \ { \ if ((out_buf_offset) + (tag_out_len) >= (out_buf_len)) \ { \ @@ -187,11 +188,16 @@ inline static void lml_init(void) return (out_buf_offset); \ } \ memcpy((out_buf) + (out_buf_offset), (tag_out), (size_t)(tag_out_len)); \ + *((out_buf) + (out_buf_offset) + (size_t)(tag_out_len)) = '\0'; \ + (line_width) += str_length((out_buf) + (out_buf_offset), 1); \ (out_buf_offset) += (tag_out_len); \ } -int lml_render(const char *str_in, char *str_out, int buf_len, int quote_mode) +int lml_render(const char *str_in, char *str_out, int buf_len, int width, int quote_mode) { + clock_t clock_begin; + clock_t clock_end; + char c; char tag_param_buf[LML_TAG_PARAM_BUF_LEN]; char tag_output_buf[LML_TAG_OUTPUT_BUF_LEN]; @@ -206,83 +212,162 @@ int lml_render(const char *str_in, char int new_line = 1; int fb_quote_level = 0; int tag_name_found; + int line_width = 0; + char tab_spaces[TAB_SIZE + 1]; + int tab_width = 0; + + clock_begin = clock(); lml_init(); lml_tag_disabled = 0; lml_tag_quote_level = 0; + if (width <= 0) + { + width = INT_MAX; + } + for (i = 0; str_in[i] != '\0'; i++) { - if (!quote_mode && !lml_tag_disabled && new_line) + if (!lml_tag_disabled && new_line) { - fb_quote_level = 0; - - while (str_in[i + fb_quote_level * 2] == ':' && str_in[i + fb_quote_level * 2 + 1] == ' ') // FB2000 quote leading str + while (str_in[i] == ':' && str_in[i + 1] == ' ') // FB2000 quote leading str { fb_quote_level++; + lml_tag_quote_level++; + i += 2; } - lml_tag_quote_level += fb_quote_level; - - if (lml_tag_quote_level > 0) + if (!quote_mode && lml_tag_quote_level > 0) { tag_output_len = snprintf(tag_output_buf, LML_TAG_OUTPUT_BUF_LEN, "%s", lml_tag_quote_color[lml_tag_quote_level % LML_TAG_QUOTE_LEVEL_LOOP]); - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len); + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len, line_width); + } + + for (k = 0; k < fb_quote_level; k++) + { + tag_output_buf[k * 2] = ':'; + tag_output_buf[k * 2 + 1] = ' '; } + tag_output_buf[fb_quote_level * 2] = '\0'; + tag_output_len = fb_quote_level * 2; + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len, line_width); new_line = 0; } - if (str_in[i] == '\033' && str_in[i + 1] == '[') // Escape sequence -- copy directly + if (lml_tag_disabled && new_line) { - for (k = i + 2; str_in[k] != '\0' && str_in[k] != 'm' && str_in[k] != '\033'; k++) + new_line = 0; + } + + if (!quote_mode && !lml_tag_disabled && str_in[i] == '\033' && str_in[i + 1] == '[') // Escape sequence + { + for (k = i + 2; isdigit((int)str_in[k]) || str_in[k] == ';' || str_in[k] == '?'; k++) ; - if (str_in[k] != 'm') // invalid + if (str_in[k] == 'm') // valid -- copy directly + { + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + i, k - i + 1, line_width); + } + else if (isalpha((int)str_in[k])) + { + // unsupported ANSI CSI command + } + else { k--; } - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + i, k - i + 1); i = k; continue; } if (str_in[i] == '\n') // jump out of tag at end of line { - if (tag_start_pos != -1) // tag is not closed + if (!lml_tag_disabled && tag_start_pos != -1) // tag is not closed { - tag_end_pos = i - 1; - tag_output_len = tag_end_pos - tag_start_pos + 1; - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + tag_start_pos, tag_output_len); + if (line_width + 1 > width) + { + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "\n", 1, line_width); + new_line = 1; + line_width = 0; + i--; // redo at current i + continue; + } + + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "[", 1, line_width); + i = tag_start_pos; // restart from tag_start_pos + 1 + tag_start_pos = -1; + tag_name_pos = -1; + continue; } - if (fb_quote_level > 0) + if (!lml_tag_disabled && fb_quote_level > 0) { lml_tag_quote_level -= fb_quote_level; - tag_output_len = snprintf(tag_output_buf, LML_TAG_OUTPUT_BUF_LEN, "\033[m"); - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len); + tag_output_len = snprintf(tag_output_buf, LML_TAG_OUTPUT_BUF_LEN, (quote_mode ? "" : "\033[m")); + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len, line_width); + + fb_quote_level = 0; + } + + if (new_line) + { + continue; } tag_start_pos = -1; tag_name_pos = -1; new_line = 1; + line_width = -1; } - else if (str_in[i] == '\r') + else if (str_in[i] == '\r' || str_in[i] == '\7') { - continue; // ignore '\r' + continue; // Skip special characters + } + else if (str_in[i] == '\t') + { + tab_width = TAB_SIZE - (line_width % TAB_SIZE); + if (line_width + tab_width > width) + { + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "\n", 1, line_width); + new_line = 1; + line_width = 0; + // skip current Tab + continue; + } + + for (k = 0; k < tab_width; k++) + { + tab_spaces[k] = ' '; + } + tab_spaces[tab_width] = '\0'; + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tab_spaces, tab_width, line_width); + continue; } if (!lml_tag_disabled && str_in[i] == '[') { if (tag_start_pos != -1) // tag is not closed { - tag_end_pos = i - 1; - tag_output_len = tag_end_pos - tag_start_pos + 1; - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + tag_start_pos, tag_output_len); + if (line_width + 1 > width) + { + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "\n", 1, line_width); + new_line = 1; + line_width = 0; + i--; // redo at current i + continue; + } + + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "[", 1, line_width); + i = tag_start_pos; // restart from tag_start_pos + 1 + tag_start_pos = -1; + tag_name_pos = -1; + continue; } tag_start_pos = i; @@ -298,7 +383,7 @@ int lml_render(const char *str_in, char tag_name_pos++; } - for (tag_name_found = 0, k = 0; k < LML_TAG_COUNT; k++) + for (tag_name_found = 0, k = 0; k < lml_tag_count; k++) { if (strncasecmp(lml_tag_def[k].tag_name, str_in + tag_name_pos, (size_t)lml_tag_name_len[k]) == 0) { @@ -346,7 +431,7 @@ int lml_render(const char *str_in, char lml_tag_def[k].tag_name, tag_param_buf, tag_output_buf, LML_TAG_OUTPUT_BUF_LEN, 1); } } - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len); + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len, line_width); break; default: // tag_name not match continue; @@ -357,8 +442,20 @@ int lml_render(const char *str_in, char if (!tag_name_found) { - tag_output_len = tag_end_pos - tag_start_pos + 1; - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + tag_start_pos, tag_output_len); + if (line_width + 1 > width) + { + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "\n", 1, line_width); + new_line = 1; + line_width = 0; + i--; // redo at current i + continue; + } + + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "[", 1, line_width); + i = tag_start_pos; // restart from tag_start_pos + 1 + tag_start_pos = -1; + tag_name_pos = -1; + continue; } tag_start_pos = -1; @@ -366,35 +463,32 @@ int lml_render(const char *str_in, char } else if (lml_tag_disabled || tag_name_pos == -1) // not in LML tag { - if (str_in[i] & 0x80) // head of multi-byte character + if (line_width + (str_in[i] & 0x80 ? 2 : 1) > width) { - if (j + 4 >= buf_len) // Assuming UTF-8 CJK characters use 4 bytes, though most of them actually use 3 bytes - { - log_error("Buffer is not longer enough for output string %ld >= %d\n", j + 4, buf_len); - str_out[j] = '\0'; - return j; - } + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, "\n", 1, line_width); + new_line = 1; + line_width = 0; + i--; // redo at current i + continue; + } + tag_output_len = 1; + if (str_in[i] & 0x80) // head of multi-byte character + { c = (str_in[i] & 0x70) << 1; while (c & 0x80) { - str_out[j++] = str_in[i++]; - if (str_in[i] == '\0') + if (str_in[i + tag_output_len] == '\0') { - str_out[j] = '\0'; - return j; + break; } + tag_output_len++; c = (c & 0x7f) << 1; } } - if (j + 1 >= buf_len) - { - log_error("Buffer is not longer enough for output string %ld >= %d\n", j + 1, buf_len); - str_out[j] = '\0'; - return j; - } - str_out[j++] = str_in[i]; + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + i, tag_output_len, line_width); + i += (tag_output_len - 1); } else // in LML tag { @@ -402,20 +496,23 @@ int lml_render(const char *str_in, char } } - if (tag_start_pos != -1) // tag is not closed + if (!lml_tag_disabled && tag_start_pos != -1) // tag is not closed { tag_end_pos = i - 1; tag_output_len = tag_end_pos - tag_start_pos + 1; - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + tag_start_pos, tag_output_len); + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, str_in + tag_start_pos, tag_output_len, line_width); } if (!quote_mode && !lml_tag_disabled && lml_tag_quote_level > 0) { tag_output_len = snprintf(tag_output_buf, LML_TAG_OUTPUT_BUF_LEN, "\033[m"); - CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len); + CHECK_AND_APPEND_OUTPUT(str_out, buf_len, j, tag_output_buf, tag_output_len, line_width); } str_out[j] = '\0'; + clock_end = clock(); + lml_total_exec_duration += (clock_end - clock_begin); + return j; }