/* ---------------------------------------------------------------------------- Copyright (c) 2021, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT License. A copy of the license can be found in the "LICENSE" file at the root of this distribution. -----------------------------------------------------------------------------*/ #include #include #include #include #include "common.h" #include "attr.h" #include "term.h" #include "bbcode.h" //------------------------------------------------------------- // HTML color table //------------------------------------------------------------- #include "bbcode_colors.c" //------------------------------------------------------------- // Types //------------------------------------------------------------- typedef struct style_s { const char* name; // name of the style attr_t attr; // attribute to apply } style_t; typedef enum align_e { IC_ALIGN_LEFT, IC_ALIGN_CENTER, IC_ALIGN_RIGHT } align_t; typedef struct width_s { ssize_t w; // > 0 align_t align; bool dots; // "..." (e.g. "sentence...") char fill; // " " (e.g. "hello ") } width_t; typedef struct tag_s { const char* name; // tag open name attr_t attr; // the saved attribute before applying the style width_t width; // start sequence of at most "width" columns ssize_t pos; // start position in the output (used for width restriction) } tag_t; static void tag_init(tag_t* tag) { memset(tag,0,sizeof(*tag)); } struct bbcode_s { tag_t* tags; // stack of tags; one entry for each open tag ssize_t tags_capacity; ssize_t tags_nesting; style_t* styles; // list of used defined styles ssize_t styles_capacity; ssize_t styles_count; term_t* term; // terminal alloc_t* mem; // allocator // caches stringbuf_t* out; // print buffer attrbuf_t* out_attrs; stringbuf_t* vout; // vprintf buffer }; //------------------------------------------------------------- // Create, helpers //------------------------------------------------------------- ic_private bbcode_t* bbcode_new( alloc_t* mem, term_t* term ) { bbcode_t* bb = mem_zalloc_tp(mem,bbcode_t); if (bb==NULL) return NULL; bb->mem = mem; bb->term = term; bb->out = sbuf_new(mem); bb->out_attrs = attrbuf_new(mem); bb->vout = sbuf_new(mem); return bb; } ic_private void bbcode_free( bbcode_t* bb ) { for(ssize_t i = 0; i < bb->styles_count; i++) { mem_free(bb->mem, bb->styles[i].name); } mem_free(bb->mem, bb->tags); mem_free(bb->mem, bb->styles); sbuf_free(bb->vout); sbuf_free(bb->out); attrbuf_free(bb->out_attrs); mem_free(bb->mem, bb); } ic_private void bbcode_style_add( bbcode_t* bb, const char* style_name, attr_t attr ) { if (bb->styles_count >= bb->styles_capacity) { ssize_t newlen = bb->styles_capacity + 32; style_t* p = mem_realloc_tp( bb->mem, style_t, bb->styles, newlen ); if (p == NULL) return; bb->styles = p; bb->styles_capacity = newlen; } assert(bb->styles_count < bb->styles_capacity); bb->styles[bb->styles_count].name = mem_strdup( bb->mem, style_name ); bb->styles[bb->styles_count].attr = attr; bb->styles_count++; } static ssize_t bbcode_tag_push( bbcode_t* bb, const tag_t* tag ) { if (bb->tags_nesting >= bb->tags_capacity) { ssize_t newcap = bb->tags_capacity + 32; tag_t* p = mem_realloc_tp( bb->mem, tag_t, bb->tags, newcap ); if (p == NULL) return -1; bb->tags = p; bb->tags_capacity = newcap; } assert(bb->tags_nesting < bb->tags_capacity); bb->tags[bb->tags_nesting] = *tag; bb->tags_nesting++; return (bb->tags_nesting-1); } static void bbcode_tag_pop( bbcode_t* bb, tag_t* tag ) { if (bb->tags_nesting <= 0) { if (tag != NULL) { tag_init(tag); } } else { bb->tags_nesting--; if (tag != NULL) { *tag = bb->tags[bb->tags_nesting]; } } } //------------------------------------------------------------- // Invalid parse/values/balance //------------------------------------------------------------- static void bbcode_invalid(const char* fmt, ... ) { if (getenv("ISOCLINE_BBCODE_DEBUG") != NULL) { va_list args; va_start(args,fmt); vfprintf(stderr,fmt,args); va_end(args); } } //------------------------------------------------------------- // Set attributes //------------------------------------------------------------- static attr_t bbcode_open( bbcode_t* bb, ssize_t out_pos, const tag_t* tag, attr_t current ) { // save current and set tag_t cur; tag_init(&cur); cur.name = tag->name; cur.attr = current; cur.width = tag->width; cur.pos = out_pos; bbcode_tag_push(bb,&cur); return attr_update_with( current, tag->attr ); } static bool bbcode_close( bbcode_t* bb, ssize_t base, const char* name, tag_t* pprev ) { // pop until match while (bb->tags_nesting > base) { tag_t prev; bbcode_tag_pop(bb,&prev); if (name==NULL || prev.name==NULL || ic_stricmp(prev.name,name) == 0) { // matched if (pprev != NULL) { *pprev = prev; } return true; } else { // unbalanced: we either continue popping or restore the tags depending if there is a matching open tag in our tags. bool has_open_tag = false; if (name != NULL) { for( ssize_t i = bb->tags_nesting - 1; i > base; i--) { if (bb->tags[i].name != NULL && ic_stricmp(bb->tags[i].name, name) == 0) { has_open_tag = true; break; } } } bbcode_invalid("bbcode: unbalanced tags: open [%s], close [/%s]\n", prev.name, name); if (!has_open_tag) { bbcode_tag_push( bb, &prev ); // restore the tags and ignore this close tag break; } else { // continue until we hit our open tag } } } if (pprev != NULL) { memset(pprev,0,sizeof(*pprev)); } return false; } //------------------------------------------------------------- // Update attributes //------------------------------------------------------------- static const char* attr_update_bool( const char* fname, signed int* field, const char* value ) { if (value == NULL || value[0] == 0 || strcmp(value,"on") || strcmp(value,"true") || strcmp(value,"1")) { *field = IC_ON; } else if (strcmp(value,"off") || strcmp(value,"false") || strcmp(value,"0")) { *field = IC_OFF; } else { bbcode_invalid("bbcode: invalid %s value: %s\n", fname, value ); } return fname; } static const char* attr_update_color( const char* fname, ic_color_t* field, const char* value ) { if (value == NULL || value[0] == 0 || strcmp(value,"none") == 0) { *field = IC_COLOR_NONE; return fname; } // hex value if (value[0] == '#') { uint32_t rgb = 0; if (sscanf(value,"#%x",&rgb) == 1) { *field = ic_rgb(rgb); } else { bbcode_invalid("bbcode: invalid color code: %s\n", value); } return fname; } // search color names ssize_t lo = 0; ssize_t hi = IC_HTML_COLOR_COUNT-1; while( lo <= hi ) { ssize_t mid = (lo + hi) / 2; style_color_t* info = &html_colors[mid]; int cmp = strcmp(info->name,value); if (cmp < 0) { lo = mid+1; } else if (cmp > 0) { hi = mid-1; } else { *field = info->color; return fname; } } bbcode_invalid("bbcode: unknown %s: %s\n", fname, value); *field = IC_COLOR_NONE; return fname; } static const char* attr_update_sgr( const char* fname, attr_t* attr, const char* value ) { *attr = attr_update_with(*attr, attr_from_sgr(value, ic_strlen(value))); return fname; } static void attr_update_width( width_t* pwidth, char default_fill, const char* value ) { // parse width value: ;;; width_t width; memset(&width, 0, sizeof(width)); width.fill = default_fill; // use 0 for no-fill (as for max-width) if (ic_atoz(value, &width.w)) { ssize_t i = 0; while( value[i] != ';' && value[i] != 0 ) { i++; } if (value[i] == ';') { i++; ssize_t len = 0; while( value[i+len] != ';' && value[i+len] != 0 ) { len++; } if (len == 4 && ic_istarts_with(value+i,"left")) { width.align = IC_ALIGN_LEFT; } else if (len == 5 && ic_istarts_with(value+i,"right")) { width.align = IC_ALIGN_RIGHT; } else if (len == 6 && ic_istarts_with(value+i,"center")) { width.align = IC_ALIGN_CENTER; } i += len; if (value[i] == ';') { i++; len = 0; while( value[i+len] != ';' && value[i+len] != 0 ) { len++; } if (len == 1) { width.fill = value[i]; } i+= len; if (value[i] == ';') { i++; len = 0; while( value[i+len] != ';' && value[i+len] != 0 ) { len++; } if ((len == 2 && ic_istarts_with(value+i,"on")) || (len == 1 && value[i] == '1')) { width.dots = true; } i += len; } } } } else { bbcode_invalid("bbcode: illegal width: %s\n", value); } *pwidth = width; } static const char* attr_update_ansi_color( const char* fname, ic_color_t* color, const char* value ) { ssize_t num = 0; if (ic_atoz(value, &num) && num >= 0 && num <= 256) { *color = color_from_ansi256(num); } return fname; } static const char* attr_update_property( tag_t* tag, const char* attr_name, const char* value ) { const char* fname = NULL; fname = "bold"; if (strcmp(attr_name,fname) == 0) { signed int b = IC_NONE; attr_update_bool(fname,&b, value); if (b != IC_NONE) { tag->attr.x.bold = b; } return fname; } fname = "italic"; if (strcmp(attr_name,fname) == 0) { signed int b = IC_NONE; attr_update_bool(fname,&b, value); if (b != IC_NONE) { tag->attr.x.italic = b; } return fname; } fname = "underline"; if (strcmp(attr_name,fname) == 0) { signed int b = IC_NONE; attr_update_bool(fname,&b, value); if (b != IC_NONE) { tag->attr.x.underline = b; } return fname; } fname = "reverse"; if (strcmp(attr_name,fname) == 0) { signed int b = IC_NONE; attr_update_bool(fname,&b, value); if (b != IC_NONE) { tag->attr.x.reverse = b; } return fname; } fname = "color"; if (strcmp(attr_name,fname) == 0) { unsigned int color = IC_COLOR_NONE; attr_update_color(fname, &color, value); if (color != IC_COLOR_NONE) { tag->attr.x.color = color; } return fname; } fname = "bgcolor"; if (strcmp(attr_name,fname) == 0) { unsigned int color = IC_COLOR_NONE; attr_update_color(fname, &color, value); if (color != IC_COLOR_NONE) { tag->attr.x.bgcolor = color; } return fname; } fname = "ansi-sgr"; if (strcmp(attr_name,fname) == 0) { attr_update_sgr(fname, &tag->attr, value); return fname; } fname = "ansi-color"; if (strcmp(attr_name,fname) == 0) { ic_color_t color = IC_COLOR_NONE;; attr_update_ansi_color(fname, &color, value); if (color != IC_COLOR_NONE) { tag->attr.x.color = color; } return fname; } fname = "ansi-bgcolor"; if (strcmp(attr_name,fname) == 0) { ic_color_t color = IC_COLOR_NONE;; attr_update_ansi_color(fname, &color, value); if (color != IC_COLOR_NONE) { tag->attr.x.bgcolor = color; } return fname; } fname = "width"; if (strcmp(attr_name,fname) == 0) { attr_update_width(&tag->width, ' ', value); return fname; } fname = "max-width"; if (strcmp(attr_name,fname) == 0) { attr_update_width(&tag->width, 0, value); return "width"; } else { return NULL; } } static const style_t builtin_styles[] = { { "b", { { IC_COLOR_NONE, IC_ON , IC_NONE, IC_COLOR_NONE, IC_NONE, IC_NONE } } }, { "r", { { IC_COLOR_NONE, IC_NONE, IC_ON , IC_COLOR_NONE, IC_NONE, IC_NONE } } }, { "u", { { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_ON , IC_NONE } } }, { "i", { { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_NONE, IC_ON } } }, { "em", { { IC_COLOR_NONE, IC_ON , IC_NONE, IC_COLOR_NONE, IC_NONE, IC_NONE } } }, // bold { "url",{ { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_ON, IC_NONE } } }, // underline { NULL, { { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_NONE, IC_NONE } } } }; static void attr_update_with_styles( tag_t* tag, const char* attr_name, const char* value, bool usebgcolor, const style_t* styles, ssize_t count ) { // direct hex color? if (attr_name[0] == '#' && (value == NULL || value[0]==0)) { value = attr_name; attr_name = (usebgcolor ? "bgcolor" : "color"); } // first try if it is a builtin property const char* name; if ((name = attr_update_property(tag,attr_name,value)) != NULL) { if (tag->name != NULL) tag->name = name; return; } // then check all styles while( count-- > 0 ) { const style_t* style = styles + count; if (strcmp(style->name,attr_name) == 0) { tag->attr = attr_update_with(tag->attr,style->attr); if (tag->name != NULL) tag->name = style->name; return; } } // check builtin styles; todo: binary search? for( const style_t* style = builtin_styles; style->name != NULL; style++) { if (strcmp(style->name,attr_name) == 0) { tag->attr = attr_update_with(tag->attr,style->attr); if (tag->name != NULL) tag->name = style->name; return; } } // check colors as a style ssize_t lo = 0; ssize_t hi = IC_HTML_COLOR_COUNT-1; while( lo <= hi ) { ssize_t mid = (lo + hi) / 2; style_color_t* info = &html_colors[mid]; int cmp = strcmp(info->name,attr_name); if (cmp < 0) { lo = mid+1; } else if (cmp > 0) { hi = mid-1; } else { attr_t cattr = attr_none(); if (usebgcolor) { cattr.x.bgcolor = info->color; } else { cattr.x.color = info->color; } tag->attr = attr_update_with(tag->attr,cattr); if (tag->name != NULL) tag->name = info->name; return; } } // not found bbcode_invalid("bbcode: unknown style: %s\n", attr_name); } ic_private attr_t bbcode_style( bbcode_t* bb, const char* style_name ) { tag_t tag; tag_init(&tag); attr_update_with_styles( &tag, style_name, NULL, false, bb->styles, bb->styles_count ); return tag.attr; } //------------------------------------------------------------- // Parse tags //------------------------------------------------------------- ic_private const char* parse_skip_white(const char* s) { while( *s != 0 && *s != ']') { if (!(*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r')) break; s++; } return s; } ic_private const char* parse_skip_to_white(const char* s) { while( *s != 0 && *s != ']') { if (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') break; s++; } return parse_skip_white(s); } ic_private const char* parse_skip_to_end(const char* s) { while( *s != 0 && *s != ']' ) { s++; } return s; } ic_private const char* parse_attr_name(const char* s) { if (*s == '#') { s++; // hex rgb color as id while( *s != 0 && *s != ']') { if (!((*s >= 'a' && *s <= 'f') || (*s >= 'A' && *s <= 'Z') || (*s >= '0' && *s <= '9'))) break; s++; } } else { while( *s != 0 && *s != ']') { if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || (*s >= '0' && *s <= '9') || *s == '_' || *s == '-')) break; s++; } } return s; } ic_private const char* parse_value(const char* s, const char** start, const char** end) { if (*s == '"') { s++; *start = s; while( *s != 0 ) { if (*s == '"') break; s++; } *end = s; if (*s == '"') { s++; } } else if (*s == '#') { *start = s; s++; while( *s != 0 ) { if (!((*s >= 'a' && *s <= 'f') || (*s >= 'A' && *s <= 'Z') || (*s >= '0' && *s <= '9'))) break; s++; } *end = s; } else { *start = s; while( *s != 0 ) { if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'F') || (*s >= '0' && *s <= '9') || *s == '-' || *s == '_')) break; s++; } *end = s; } return s; } ic_private const char* parse_tag_value( tag_t* tag, char* idbuf, const char* s, const style_t* styles, ssize_t scount ) { // parse: \s*[\w-]+\s*(=\s*) bool usebgcolor = false; const char* id = s; const char* idend = parse_attr_name(id); const char* val = NULL; const char* valend = NULL; if (id == idend) { bbcode_invalid("bbcode: empty identifier? %.10s...\n", id ); return parse_skip_to_white(id); } // "on" bgcolor? s = parse_skip_white(idend); if (idend - id == 2 && ic_strnicmp(id,"on",2) == 0 && *s != '=') { usebgcolor = true; id = s; idend = parse_attr_name(id); if (id == idend) { bbcode_invalid("bbcode: empty identifier follows 'on'? %.10s...\n", id ); return parse_skip_to_white(id); } s = parse_skip_white(idend); } // value if (*s == '=') { s++; s = parse_skip_white(s); s = parse_value(s, &val, &valend); s = parse_skip_white(s); } // limit name and attr to 128 bytes char valbuf[128]; ic_strncpy( idbuf, 128, id, idend - id); ic_strncpy( valbuf, 128, val, valend - val); ic_str_tolower(idbuf); ic_str_tolower(valbuf); attr_update_with_styles( tag, idbuf, valbuf, usebgcolor, styles, scount ); return s; } static const char* parse_tag_values( tag_t* tag, char* idbuf, const char* s, const style_t* styles, ssize_t scount ) { s = parse_skip_white(s); idbuf[0] = 0; ssize_t count = 0; while( *s != 0 && *s != ']') { char idbuf_next[128]; s = parse_tag_value(tag, (count==0 ? idbuf : idbuf_next), s, styles, scount); count++; } if (*s == ']') { s++; } return s; } static const char* parse_tag( tag_t* tag, char* idbuf, bool* open, bool* pre, const char* s, const style_t* styles, ssize_t scount ) { *open = true; *pre = false; if (*s != '[') return s; s = parse_skip_white(s+1); if (*s == '!') { // pre *pre = true; s = parse_skip_white(s+1); } else if (*s == '/') { *open = false; s = parse_skip_white(s+1); }; s = parse_tag_values( tag, idbuf, s, styles, scount); return s; } //--------------------------------------------------------- // Styles //--------------------------------------------------------- static void bbcode_parse_tag_content( bbcode_t* bb, const char* s, tag_t* tag ) { tag_init(tag); if (s != NULL) { char idbuf[128]; parse_tag_values(tag, idbuf, s, bb->styles, bb->styles_count); } } ic_private void bbcode_style_def( bbcode_t* bb, const char* style_name, const char* s ) { tag_t tag; bbcode_parse_tag_content( bb, s, &tag); bbcode_style_add(bb, style_name, tag.attr); } ic_private void bbcode_style_open( bbcode_t* bb, const char* fmt ) { tag_t tag; bbcode_parse_tag_content(bb, fmt, &tag); term_set_attr( bb->term, bbcode_open(bb, 0, &tag, term_get_attr(bb->term)) ); } ic_private void bbcode_style_close( bbcode_t* bb, const char* fmt ) { const ssize_t base = bb->tags_nesting - 1; // as we end a style tag_t tag; bbcode_parse_tag_content(bb, fmt, &tag); tag_t prev; if (bbcode_close(bb, base, tag.name, &prev)) { term_set_attr( bb->term, prev.attr ); } } //--------------------------------------------------------- // Restrict to width //--------------------------------------------------------- static void bbcode_restrict_width( ssize_t start, width_t width, stringbuf_t* out, attrbuf_t* attr_out ) { if (width.w <= 0) return; assert(start <= sbuf_len(out)); assert(attr_out == NULL || sbuf_len(out) == attrbuf_len(attr_out)); const char* s = sbuf_string(out) + start; const ssize_t len = sbuf_len(out) - start; const ssize_t w = str_column_width(s); if (w == width.w) return; // fits exactly if (w > width.w) { // too large ssize_t innerw = (width.dots && width.w > 3 ? width.w-3 : width.w); if (width.align == IC_ALIGN_RIGHT) { // right align const ssize_t ndel = str_skip_until_fit( s, innerw ); sbuf_delete_at( out, start, ndel ); attrbuf_delete_at( attr_out, start, ndel ); if (innerw < width.w) { // add dots sbuf_insert_at( out, "...", start ); attr_t attr = attrbuf_attr_at(attr_out, start); attrbuf_insert_at( attr_out, start, 3, attr); } } else { // left or center align ssize_t count = str_take_while_fit( s, innerw ); sbuf_delete_at( out, start + count, len - count ); attrbuf_delete_at( attr_out, start + count, len - count ); if (innerw < width.w) { // add dots attr_t attr = attrbuf_attr_at(attr_out,start); attrbuf_append_n( out, attr_out, "...", 3, attr ); } } } else { // too short, pad to width const ssize_t diff = (width.w - w); const ssize_t pad_left = (width.align == IC_ALIGN_RIGHT ? diff : (width.align == IC_ALIGN_LEFT ? 0 : diff / 2)); const ssize_t pad_right = (width.align == IC_ALIGN_LEFT ? diff : (width.align == IC_ALIGN_RIGHT ? 0 : diff - pad_left)); if (width.fill != 0 && pad_left > 0) { const attr_t attr = attrbuf_attr_at(attr_out,start); for( ssize_t i = 0; i < pad_left; i++) { // todo: optimize sbuf_insert_char_at(out, width.fill, start); } attrbuf_insert_at( attr_out, start, pad_left, attr ); } if (width.fill != 0 && pad_right > 0) { const attr_t attr = attrbuf_attr_at(attr_out,sbuf_len(out) - 1); char buf[2]; buf[0] = width.fill; buf[1] = 0; for( ssize_t i = 0; i < pad_right; i++) { // todo: optimize attrbuf_append_n( out, attr_out, buf, 1, attr ); } } } } //--------------------------------------------------------- // Print //--------------------------------------------------------- ic_private ssize_t bbcode_process_tag( bbcode_t* bb, const char* s, const ssize_t nesting_base, stringbuf_t* out, attrbuf_t* attr_out, attr_t* cur_attr ) { assert(*s == '['); tag_t tag; tag_init(&tag); bool open = true; bool ispre = false; char idbuf[128]; const char* end = parse_tag( &tag, idbuf, &open, &ispre, s, bb->styles, bb->styles_count ); // todo: styles assert(end > s); if (open) { if (!ispre) { // open tag *cur_attr = bbcode_open( bb, sbuf_len(out), &tag, *cur_attr ); } else { // scan pre to end tag attr_t attr = attr_update_with(*cur_attr, tag.attr); char pre[132]; if (snprintf(pre, 132, "[/%s]", idbuf) < ssizeof(pre)) { const char* etag = strstr(end,pre); if (etag == NULL) { const ssize_t len = ic_strlen(end); attrbuf_append_n(out, attr_out, end, len, attr); end += len; } else { attrbuf_append_n(out, attr_out, end, (etag - end), attr); end = etag + ic_strlen(pre); } } } } else { // pop the tag tag_t prev; if (bbcode_close( bb, nesting_base, tag.name, &prev)) { *cur_attr = prev.attr; if (prev.width.w > 0) { // closed a width tag; restrict the output to width bbcode_restrict_width( prev.pos, prev.width, out, attr_out); } } } return (end - s); } ic_private void bbcode_append( bbcode_t* bb, const char* s, stringbuf_t* out, attrbuf_t* attr_out ) { attr_t attr = attr_none(); const ssize_t base = bb->tags_nesting; // base; will not be popped ssize_t i = 0; while( s[i] != 0 ) { // handle no tags in bulk ssize_t nobb = 0; char c; while( (c = s[i+nobb]) != 0) { if (c == '[' || c == '\\') { break; } if (c == '\x1B' && s[i+nobb+1] == '[') { nobb++; // don't count 'ESC[' as a tag opener } nobb++; } if (nobb > 0) { attrbuf_append_n(out, attr_out, s+i, nobb, attr); } i += nobb; // tag if (s[i] == '[') { i += bbcode_process_tag(bb, s+i, base, out, attr_out, &attr); } else if (s[i] == '\\') { if (s[i+1] == '\\' || s[i+1] == '[') { attrbuf_append_n(out, attr_out, s+i+1, 1, attr); // escape '\[' and '\\' i += 2; } else { attrbuf_append_n(out, attr_out, s+i, 1, attr); // pass '\\' as is i++; } } } // pop unclosed openings assert(bb->tags_nesting >= base); while( bb->tags_nesting > base ) { bbcode_tag_pop(bb,NULL); }; } ic_private void bbcode_print( bbcode_t* bb, const char* s ) { if (bb->out == NULL || bb->out_attrs == NULL) return; assert(sbuf_len(bb->out) == 0 && attrbuf_len(bb->out_attrs) == 0); bbcode_append( bb, s, bb->out, bb->out_attrs ); term_write_formatted( bb->term, sbuf_string(bb->out), attrbuf_attrs(bb->out_attrs,sbuf_len(bb->out)) ); attrbuf_clear(bb->out_attrs); sbuf_clear(bb->out); } ic_private void bbcode_println( bbcode_t* bb, const char* s ) { bbcode_print(bb,s); term_writeln(bb->term, ""); } ic_private void bbcode_vprintf( bbcode_t* bb, const char* fmt, va_list args ) { if (bb->vout == NULL) return; assert(sbuf_len(bb->vout) == 0); sbuf_append_vprintf(bb->vout,fmt,args); bbcode_print(bb, sbuf_string(bb->vout)); sbuf_clear(bb->vout); } ic_private void bbcode_printf( bbcode_t* bb, const char* fmt, ... ) { va_list args; va_start(args,fmt); bbcode_vprintf(bb,fmt,args); va_end(args); } ic_private ssize_t bbcode_column_width( bbcode_t* bb, const char* s ) { if (s==0 || s[0] == 0) return 0; if (bb->vout == NULL) { return str_column_width(s); } assert(sbuf_len(bb->vout) == 0); bbcode_append( bb, s, bb->vout, NULL); const ssize_t w = str_column_width(sbuf_string(bb->vout)); sbuf_clear(bb->vout); return w; }