]> git.wincent.com - wikitext.git/commitdiff
Change 4 VALUE (String) members of the parser_t struct to str_t type
authorWincent Colaiuta <win@wincent.com>
Fri, 8 May 2009 15:44:01 +0000 (17:44 +0200)
committerWincent Colaiuta <win@wincent.com>
Sun, 10 May 2009 15:03:39 +0000 (17:03 +0200)
This is unfortuantely quite a large commit because the nature of
the change requires many parts to be modified at once; the
intermediate stages are not buildable and therefore not
bisectable.

Change the capture, output, link_target and link_text members of
the parser struct from VALUE (String) type to str_t. This should
improve performance because the str_t is faster and designed for
easy reuse so we can allocate a few instances at the beginning
of parsing and then use them repeatedly throughout the parse,
thus avoid many time-consuming allocations.

Remove the "capturing" member and instead use the "capture"
pointer as an indication of whether capturing is in progress.

Change the type of the "target" param in the
_Wikitext_pop_from_stack function (and the other "pop
from stack" functions) from VALUE (String) to pointer
to str_t.

Change the type of the "check_autolink" parameter to the
_Wikitext_append_hyperlink function from VALUE (boolean) to
bool.

Remove redundant passing in of parser->output to the
_Wikitext_pop_from_stack function.

Teach _Wikitext_blank to accept a pointer to a str_t struct
rather than a Ruby String (VALUE).

Add parser_new function to encapsulate the initial allocation
and initialization of the parser_t struct.

Rename str_append_rb_str function to str_append_string for
consistency with other functions in str.c.

Signed-off-by: Wincent Colaiuta <win@wincent.com>
ext/parser.c
ext/str.c
ext/str.h

index 4955500619855cd795d15e34b9a662ab10d844d7..061138b65a9cbcdc874dac4b8aa94ad3e9886fe4 100644 (file)
 // poor man's object orientation in C:
 // instead of parsing around multiple parameters between functions in the parser
 // we pack everything into a struct and pass around only a pointer to that
-// TODO: consider changing some of the VALUE members (eg link_target) to the more efficient str_t type
 typedef struct
 {
-    VALUE   output;                 // for accumulating output to be returned
-    VALUE   capture;                // for capturing substrings
-    VALUE   link_target;            // short term "memory" for parsing links
-    VALUE   link_text;              // short term "memory" for parsing links
-    VALUE   external_link_class;    // CSS class applied to external links
-    VALUE   mailto_class;           // CSS class applied to email (mailto) links
-    VALUE   img_prefix;             // path prepended when emitting img tags
+    str_t   *capture;               // capturing to link_target, link_text, or NULL (direct to output, not capturing)
+    str_t   *output;                // for accumulating output to be returned
+    str_t   *link_target;           // short term "memory" for parsing links
+    str_t   *link_text;             // short term "memory" for parsing links
     str_t   *line_ending;
     str_t   *tabulation;            // caching buffer for emitting indentation
     ary_t   *scope;                 // stack for tracking scope
     ary_t   *line;                  // stack for tracking scope as implied by current line
     ary_t   *line_buffer;           // stack for tracking raw tokens (not scope) on current line
+    VALUE   external_link_class;    // CSS class applied to external links
+    VALUE   mailto_class;           // CSS class applied to email (mailto) links
+    VALUE   img_prefix;             // path prepended when emitting img tags
     int     base_indent;            // controlled by the :indent option to Wikitext::Parser#parse
     int     current_indent;         // fluctuates according to currently nested structures
     int     base_heading_level;
-    bool    capturing;
     bool    pending_crlf;
     bool    autolink;
     bool    space_to_underscore;
@@ -138,13 +136,41 @@ const char img_alt[]                    = "\" alt=\"";
 // the C stack to prevent the structure from being prematurely collected.
 #define GC_WRAP_PARSER(ptr, name) volatile VALUE name __attribute__((unused)) = Data_Wrap_Struct(rb_cObject, 0, parser_free, ptr)
 
+parser_t *parser_new(void)
+{
+    parser_t *parser                = ALLOC_N(parser_t, 1);
+    parser->capture                 = NULL; // not a real instance, pointer to other member's instance
+    parser->output                  = str_new();
+    parser->link_target             = str_new();
+    parser->link_text               = str_new();
+    parser->line_ending             = NULL; // caller should set up
+    parser->tabulation              = str_new();
+    parser->scope                   = ary_new();
+    parser->line                    = ary_new();
+    parser->line_buffer             = ary_new();
+    parser->external_link_class     = Qnil; // caller should set up
+    parser->mailto_class            = Qnil; // caller should set up
+    parser->img_prefix              = Qnil; // caller should set up
+    parser->base_indent             = 0;
+    parser->current_indent          = 0;
+    parser->base_heading_level      = 0;
+    parser->pending_crlf            = false;
+    parser->autolink                = true;
+    parser->space_to_underscore     = true;
+    return parser;
+}
+
 void parser_free(parser_t *parser)
 {
+    // we don't free parser->capture; it's just a redundant pointer
+    if (parser->output)         str_free(parser->output);
+    if (parser->link_target)    str_free(parser->link_target);
+    if (parser->link_text)      str_free(parser->link_text);
+    if (parser->line_ending)    str_free(parser->line_ending);
+    if (parser->tabulation)     str_free(parser->tabulation);
     if (parser->scope)          ary_free(parser->scope);
     if (parser->line)           ary_free(parser->line);
     if (parser->line_buffer)    ary_free(parser->line_buffer);
-    if (parser->line_ending)    str_free(parser->line_ending);
-    if (parser->tabulation)     str_free(parser->tabulation);
     free(parser);
 }
 
@@ -235,33 +261,30 @@ VALUE Wikitext_parser_fulltext_tokenize(int argc, VALUE *argv, VALUE self)
     return tokens;
 }
 
-// we downcase "in place", overwriting the original contents of the buffer and returning the same string
-VALUE _Wikitext_downcase_bang(VALUE string)
+// we downcase "in place", overwriting the original contents of the buffer
+void _Wikitext_downcase_bang(char *ptr, long len)
 {
-    char *ptr   = RSTRING_PTR(string);
-    long len    = RSTRING_LEN(string);
     for (long i = 0; i < len; i++)
     {
         if (ptr[i] >= 'A' && ptr[i] <= 'Z')
             ptr[i] += 32;
     }
-    return string;
 }
 
 // prepare hyperlink and append it to parser->output
-// if check_autolink is Qtrue, checks parser->autolink to decide whether to emit a real hyperlink
+// if check_autolink is true, checks parser->autolink to decide whether to emit a real hyperlink
 // or merely the literal link target
 // if link_text is Qnil, the link_target is re-used for the link text
-void _Wikitext_append_hyperlink(parser_t *parser, VALUE link_prefix, VALUE link_target, VALUE link_text, VALUE link_class, VALUE check_autolink)
+void _Wikitext_append_hyperlink(parser_t *parser, VALUE link_prefix, VALUE link_target, VALUE link_text, VALUE link_class, bool check_autolink)
 {
-    if (check_autolink == Qtrue && !parser->autolink)
-        rb_str_append(parser->output, link_target);
+    if (check_autolink && !parser->autolink)
+        str_append_string(parser->output, link_target);
     else
     {
-        rb_str_cat(parser->output, a_start, sizeof(a_start) - 1);               // <a href="
+        str_append(parser->output, a_start, sizeof(a_start) - 1);               // <a href="
         if (!NIL_P(link_prefix))
-            rb_str_append(parser->output, link_prefix);
-        rb_str_append(parser->output, link_target);
+            str_append_string(parser->output, link_prefix);
+        str_append_string(parser->output, link_target);
 
         // special handling for mailto URIs
         const char *mailto = "mailto:";
@@ -271,27 +294,27 @@ void _Wikitext_append_hyperlink(parser_t *parser, VALUE link_prefix, VALUE link_
             link_class = parser->mailto_class; // use mailto_class from parser
         if (link_class != Qnil)
         {
-            rb_str_cat(parser->output, a_class, sizeof(a_class) - 1);           // " class="
-            rb_str_append(parser->output, link_class);
+            str_append(parser->output, a_class, sizeof(a_class) - 1);           // " class="
+            str_append_string(parser->output, link_class);
         }
-        rb_str_cat(parser->output, a_start_close, sizeof(a_start_close) - 1);   // ">
+        str_append(parser->output, a_start_close, sizeof(a_start_close) - 1);   // ">
         if (NIL_P(link_text)) // re-use link_target
-            rb_str_append(parser->output, link_target);
+            str_append_string(parser->output, link_target);
         else
-            rb_str_append(parser->output, link_text);
-        rb_str_cat(parser->output, a_end, sizeof(a_end) - 1);                   // </a>
+            str_append_string(parser->output, link_text);
+        str_append(parser->output, a_end, sizeof(a_end) - 1);                   // </a>
     }
 }
 
 void _Wikitext_append_img(parser_t *parser, char *token_ptr, int token_len)
 {
-    rb_str_cat(parser->output, img_start, sizeof(img_start) - 1);   // <img src="
+    str_append(parser->output, img_start, sizeof(img_start) - 1);   // <img src="
     if (!NIL_P(parser->img_prefix) && *token_ptr != '/')            // len always > 0
-        rb_str_append(parser->output, parser->img_prefix);
-    rb_str_cat(parser->output, token_ptr, token_len);
-    rb_str_cat(parser->output, img_alt, sizeof(img_alt) - 1);       // " alt="
-    rb_str_cat(parser->output, token_ptr, token_len);
-    rb_str_cat(parser->output, img_end, sizeof(img_end) - 1);       // " />
+        str_append_string(parser->output, parser->img_prefix);
+    str_append(parser->output, token_ptr, token_len);
+    str_append(parser->output, img_alt, sizeof(img_alt) - 1);       // " alt="
+    str_append(parser->output, token_ptr, token_len);
+    str_append(parser->output, img_end, sizeof(img_end) - 1);       // " />
 }
 
 // will emit indentation only if we are about to emit any of:
@@ -313,7 +336,7 @@ void _Wikitext_indent(parser_t *parser)
             *old_end++ = ' ';
         if (space_count > parser->tabulation->len)
             parser->tabulation->len = space_count;
-        rb_str_cat(parser->output, parser->tabulation->ptr, space_count);
+        str_append(parser->output, parser->tabulation->ptr, space_count);
     }
     parser->current_indent += 2;
 }
@@ -327,18 +350,18 @@ void _Wikitext_dedent(parser_t *parser, VALUE emit)
         return;
     int space_count = parser->current_indent + parser->base_indent;
     if (space_count > 0)
-        rb_str_cat(parser->output, parser->tabulation->ptr, space_count);
+        str_append(parser->output, parser->tabulation->ptr, space_count);
 }
 
 // Pops a single item off the parser's scope stack.
 // A corresponding closing tag is written to the target string.
 // The target string may be the main output buffer, or a substring capturing buffer if a link is being scanned.
-void _Wikitext_pop_from_stack(parser_t *parser, VALUE target)
+void _Wikitext_pop_from_stack(parser_t *parser, str_t *target)
 {
     int top = ary_entry(parser->scope, -1);
     if (NO_ITEM(top))
         return;
-    if (NIL_P(target))
+    if (!target)
         target = parser->output;
 
     // for headings, take base_heading_level into account
@@ -354,16 +377,16 @@ void _Wikitext_pop_from_stack(parser_t *parser, VALUE target)
     {
         case PRE:
         case PRE_START:
-            rb_str_cat(target, pre_end, sizeof(pre_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, pre_end, sizeof(pre_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
         case BLOCKQUOTE:
         case BLOCKQUOTE_START:
             _Wikitext_dedent(parser, Qtrue);
-            rb_str_cat(target, blockquote_end, sizeof(blockquote_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, blockquote_end, sizeof(blockquote_end) - 1);
+            str_append_str(target, parser->line_ending);
             break;
 
         case NO_WIKI_START:
@@ -372,29 +395,29 @@ void _Wikitext_pop_from_stack(parser_t *parser, VALUE target)
 
         case STRONG:
         case STRONG_START:
-            rb_str_cat(target, strong_end, sizeof(strong_end) - 1);
+            str_append(target, strong_end, sizeof(strong_end) - 1);
             break;
 
         case EM:
         case EM_START:
-            rb_str_cat(target, em_end, sizeof(em_end) - 1);
+            str_append(target, em_end, sizeof(em_end) - 1);
             break;
 
         case TT:
         case TT_START:
-            rb_str_cat(target, tt_end, sizeof(tt_end) - 1);
+            str_append(target, tt_end, sizeof(tt_end) - 1);
             break;
 
         case OL:
             _Wikitext_dedent(parser, Qtrue);
-            rb_str_cat(target, ol_end, sizeof(ol_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, ol_end, sizeof(ol_end) - 1);
+            str_append_str(target, parser->line_ending);
             break;
 
         case UL:
             _Wikitext_dedent(parser, Qtrue);
-            rb_str_cat(target, ul_end, sizeof(ul_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, ul_end, sizeof(ul_end) - 1);
+            str_append_str(target, parser->line_ending);
             break;
 
         case NESTED_LIST:
@@ -408,44 +431,44 @@ void _Wikitext_pop_from_stack(parser_t *parser, VALUE target)
             break;
 
         case LI:
-            rb_str_cat(target, li_end, sizeof(li_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, li_end, sizeof(li_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
         case H6_START:
-            rb_str_cat(target, h6_end, sizeof(h6_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, h6_end, sizeof(h6_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
         case H5_START:
-            rb_str_cat(target, h5_end, sizeof(h5_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, h5_end, sizeof(h5_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
         case H4_START:
-            rb_str_cat(target, h4_end, sizeof(h4_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, h4_end, sizeof(h4_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
         case H3_START:
-            rb_str_cat(target, h3_end, sizeof(h3_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, h3_end, sizeof(h3_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
         case H2_START:
-            rb_str_cat(target, h2_end, sizeof(h2_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, h2_end, sizeof(h2_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
         case H1_START:
-            rb_str_cat(target, h1_end, sizeof(h1_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, h1_end, sizeof(h1_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
@@ -470,8 +493,8 @@ void _Wikitext_pop_from_stack(parser_t *parser, VALUE target)
             break;
 
         case P:
-            rb_str_cat(target, p_end, sizeof(p_end) - 1);
-            rb_str_cat(target, parser->line_ending->ptr, parser->line_ending->len);
+            str_append(target, p_end, sizeof(p_end) - 1);
+            str_append_str(target, parser->line_ending);
             _Wikitext_dedent(parser, Qfalse);
             break;
 
@@ -487,9 +510,9 @@ void _Wikitext_pop_from_stack(parser_t *parser, VALUE target)
 }
 
 // Pops items off the top of parser's scope stack, accumulating closing tags for them into the target string, until item is reached.
-// If including is Qtrue then the item itself is also popped.
+// If including is true then the item itself is also popped.
 // The target string may be the main output buffer, or a substring capturing buffer when scanning links.
-void _Wikitext_pop_from_stack_up_to(parser_t *parser, VALUE target, int item, VALUE including)
+void _Wikitext_pop_from_stack_up_to(parser_t *parser, str_t *target, int item, bool including)
 {
     int continue_looping = 1;
     do
@@ -499,7 +522,7 @@ void _Wikitext_pop_from_stack_up_to(parser_t *parser, VALUE target, int item, VA
             return;
         if (top == item)
         {
-            if (including != Qtrue)
+            if (!including)
                 return;
             continue_looping = 0;
         }
@@ -510,12 +533,12 @@ void _Wikitext_pop_from_stack_up_to(parser_t *parser, VALUE target, int item, VA
 void _Wikitext_pop_all_from_stack(parser_t *parser)
 {
     for (int i = 0, max = parser->scope->count; i < max; i++)
-        _Wikitext_pop_from_stack(parser, Qnil);
+        _Wikitext_pop_from_stack(parser, NULL);
 }
 
 void _Wikitext_start_para_if_necessary(parser_t *parser)
 {
-    if (parser->capturing)
+    if (parser->capture)
         return;
 
     // if no block open yet, or top of stack is BLOCKQUOTE/BLOCKQUOTE_START (with nothing in it yet)
@@ -524,7 +547,7 @@ void _Wikitext_start_para_if_necessary(parser_t *parser)
         ary_entry(parser->scope, -1) == BLOCKQUOTE_START)
     {
         _Wikitext_indent(parser);
-        rb_str_cat(parser->output, p_start, sizeof(p_start) - 1);
+        str_append(parser->output, p_start, sizeof(p_start) - 1);
         ary_push(parser->scope, P);
         ary_push(parser->line, P);
     }
@@ -532,10 +555,10 @@ void _Wikitext_start_para_if_necessary(parser_t *parser)
     {
         if (IN(P))
             // already in a paragraph block; convert pending CRLF into a space
-            rb_str_cat(parser->output, space, sizeof(space) - 1);
+            str_append(parser->output, space, sizeof(space) - 1);
         else if (IN(PRE))
             // PRE blocks can have pending CRLF too (helps us avoid emitting the trailing newline)
-            rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+            str_append_str(parser->output, parser->line_ending);
     }
     parser->pending_crlf = false;
 }
@@ -544,7 +567,7 @@ void _Wikitext_emit_pending_crlf_if_necessary(parser_t *parser)
 {
     if (parser->pending_crlf)
     {
-        rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+        str_append_str(parser->output, parser->line_ending);
         parser->pending_crlf = false;
     }
 }
@@ -573,7 +596,7 @@ void _Wikitext_emit_pending_crlf_if_necessary(parser_t *parser)
 // entering the for loop.
 void _Wikitext_pop_excess_elements(parser_t *parser)
 {
-    if (parser->capturing)
+    if (parser->capture)
         return;
     for (int i = parser->scope->count - ary_count(parser->scope, BLOCKQUOTE_START), j = parser->line->count; i > j; i--)
     {
@@ -588,7 +611,7 @@ void _Wikitext_pop_excess_elements(parser_t *parser)
                 continue;
             }
         }
-        _Wikitext_pop_from_stack(parser, parser->output);
+        _Wikitext_pop_from_stack(parser, NULL);
     }
 }
 
@@ -659,14 +682,14 @@ VALUE _Wikitext_utf32_char_to_entity(uint32_t character)
     return rb_str_new((const char *)hex_string, sizeof(hex_string));
 }
 
-VALUE _Wikitext_parser_trim_link_target(VALUE string)
+// trim parser->link_text in place
+void _Wikitext_parser_trim_link_text(parser_t *parser)
 {
-    string              = StringValue(string);
-    char    *src        = RSTRING_PTR(string);
+    char    *src        = parser->link_text->ptr;
     char    *start      = src;                  // remember this so we can check if we're at the start
     char    *left       = src;
     char    *non_space  = src;                  // remember last non-space character output
-    long    len         = RSTRING_LEN(string);
+    long    len         = parser->link_text->len;
     char    *end        = src + len;
     while (src < end)
     {
@@ -679,23 +702,25 @@ VALUE _Wikitext_parser_trim_link_target(VALUE string)
             non_space = src;
         src++;
     }
-    if (left == start && non_space + 1 == end)
-        return string;
-    else
-        return rb_str_new(left, (non_space + 1) - left);
+    if (left != start || non_space + 1 != end)
+    {
+        // TODO: could potentially avoid this memmove by extending the str_t struct with an "offset" or "free" member
+        parser->link_text->len = (non_space + 1) - left;
+        memmove(parser->link_text->ptr, left, parser->link_text->len);
+    }
 }
 
 // - non-printable (non-ASCII) characters converted to numeric entities
 // - QUOT and AMP characters converted to named entities
-// - if rollback is Qtrue, there is no special treatment of spaces
-// - if rollback is Qfalse, leading and trailing whitespace trimmed
-VALUE _Wikitext_parser_sanitize_link_target(parser_t *parser, VALUE rollback)
+// - if rollback is true, there is no special treatment of spaces
+// - if rollback is false, leading and trailing whitespace trimmed
+VALUE _Wikitext_parser_sanitize_link_target(parser_t *parser, bool rollback)
 {
-    VALUE string        = StringValue(parser->link_target); // raises if string is nil or doesn't quack like a string
-    char    *src        = RSTRING_PTR(string);
-    char    *start      = src;                  // remember this so we can check if we're at the start
-    long    len         = RSTRING_LEN(string);
-    char    *end        = src + len;
+    VALUE   string  = string_from_str(parser->link_target);
+    char    *src    = RSTRING_PTR(string);
+    char    *start  = src;                  // remember this so we can check if we're at the start
+    long    len     = RSTRING_LEN(string);
+    char    *end    = src + len;
 
     // start with a destination buffer twice the size of the source, will realloc if necessary
     // slop = (len / 8) * 8 (ie. one in every 8 characters can be converted into an entity, each entity requires 8 bytes)
@@ -745,7 +770,7 @@ VALUE _Wikitext_parser_sanitize_link_target(parser_t *parser, VALUE rollback)
             free(dest_ptr);
             rb_raise(rb_eRangeError, "invalid link text (\">\" may not appear in link text)");
         }
-        else if (*src == ' ' && src == start && rollback == Qfalse)
+        else if (*src == ' ' && src == start && !rollback)
             start++;                // we eat leading space
         else if (*src >= 0x20 && *src <= 0x7e)    // printable ASCII
         {
@@ -770,7 +795,7 @@ VALUE _Wikitext_parser_sanitize_link_target(parser_t *parser, VALUE rollback)
     }
 
     // trim trailing space if necessary
-    if (rollback == Qfalse && non_space > dest_ptr && dest != non_space)
+    if (!rollback && non_space > dest_ptr && dest != non_space)
         len = non_space - dest_ptr;
     else
         len = dest - dest_ptr;
@@ -782,8 +807,9 @@ VALUE _Wikitext_parser_sanitize_link_target(parser_t *parser, VALUE rollback)
 VALUE Wikitext_parser_sanitize_link_target(VALUE self, VALUE string)
 {
     parser_t parser;
-    parser.link_target          = string;
-    return _Wikitext_parser_sanitize_link_target(&parser, Qfalse);
+    parser.link_target = str_new_from_string(string);
+    GC_WRAP_STR(parser.link_target, link_target_gc);
+    return _Wikitext_parser_sanitize_link_target(&parser, false);
 }
 
 // encodes the input string according to RFCs 2396 and 2718
@@ -796,7 +822,7 @@ VALUE Wikitext_parser_sanitize_link_target(VALUE self, VALUE string)
 //         thing. [[Foo]] was...
 static void _Wikitext_parser_encode_link_target(parser_t *parser)
 {
-    VALUE in                = StringValue(parser->link_target);
+    VALUE in                = string_from_str(parser->link_target);
     char        *input      = RSTRING_PTR(in);
     char        *start      = input;            // remember this so we can check if we're at the start
     long        len         = RSTRING_LEN(in);
@@ -864,37 +890,42 @@ static void _Wikitext_parser_encode_link_target(parser_t *parser)
         dest_len = non_space - dest_ptr;
     else
         dest_len = dest - dest_ptr;
-    parser->link_target = rb_str_new(dest_ptr, dest_len);
+
+    // TOOD: try to avoid this copy by working directly inside link_target buffer
+    str_clear(parser->link_target);
+    str_append(parser->link_target, dest_ptr, dest_len);
     free(dest_ptr);
 }
 
 VALUE Wikitext_parser_encode_link_target(VALUE self, VALUE in)
 {
     parser_t parser;
-    parser.link_target              = in;
     parser.space_to_underscore      = false;
+    parser.link_target              = str_new_from_string(in);
+    GC_WRAP_STR(parser.link_target, link_target_gc);
     _Wikitext_parser_encode_link_target(&parser);
-    return parser.link_target;
+    return string_from_str(parser.link_target);
 }
 
 // this method exposed for testing only
 VALUE Wikitext_parser_encode_special_link_target(VALUE self, VALUE in)
 {
     parser_t parser;
-    parser.link_target              = in;
     parser.space_to_underscore      = false;
+    parser.link_target              = str_new_from_string(in);
+    GC_WRAP_STR(parser.link_target, link_target_gc);
     _Wikitext_parser_encode_link_target(&parser);
-    return parser.link_target;
+    return string_from_str(parser.link_target);
 }
 
 // returns 1 (true) if supplied string is blank (nil, empty, or all whitespace)
 // returns 0 (false) otherwise
-int _Wikitext_blank(VALUE str)
+bool _Wikitext_blank(str_t *str)
 {
-    if (NIL_P(str) || RSTRING_LEN(str) == 0)
+    if (str->len == 0)
         return true;
-    for (char *ptr = RSTRING_PTR(str),
-        *end = RSTRING_PTR(str) + RSTRING_LEN(str);
+    for (char *ptr = str->ptr,
+        *end = str->ptr + str->len;
         ptr < end; ptr++)
     {
         if (*ptr != ' ')
@@ -908,22 +939,23 @@ void _Wikitext_rollback_failed_internal_link(parser_t *parser)
     if (!IN(LINK_START))
         return; // nothing to do!
     int scope_includes_separator = IN(SEPARATOR);
-    _Wikitext_pop_from_stack_up_to(parser, Qnil, LINK_START, Qtrue);
-    rb_str_cat(parser->output, link_start, sizeof(link_start) - 1);
-    if (!NIL_P(parser->link_target))
+    _Wikitext_pop_from_stack_up_to(parser, NULL, LINK_START, true);
+    str_append(parser->output, link_start, sizeof(link_start) - 1);
+    if (parser->link_target->len > 0)
     {
-        VALUE sanitized = _Wikitext_parser_sanitize_link_target(parser, Qtrue);
-        rb_str_append(parser->output, sanitized);
+        // TODO: make _Wikitext_parser_sanitize_link_target return something else? or append directly to buffer would be much better
+        VALUE sanitized = _Wikitext_parser_sanitize_link_target(parser, true);
+        str_append_string(parser->output, sanitized);
         if (scope_includes_separator)
         {
-            rb_str_cat(parser->output, separator, sizeof(separator) - 1);
-            if (!NIL_P(parser->link_text))
-                rb_str_append(parser->output, parser->link_text);
+            str_append(parser->output, separator, sizeof(separator) - 1);
+            if (parser->link_text->len > 0)
+                str_append_str(parser->output, parser->link_text);
         }
     }
-    parser->capturing   = false;
-    parser->link_target = Qnil;
-    parser->link_text   = Qnil;
+    parser->capture = NULL;
+    str_clear(parser->link_target);
+    str_clear(parser->link_text);
 }
 
 void _Wikitext_rollback_failed_external_link(parser_t *parser)
@@ -934,22 +966,22 @@ void _Wikitext_rollback_failed_external_link(parser_t *parser)
     // store a couple of values before popping
     int scope_includes_space = IN(SPACE);
     VALUE link_class = IN(PATH) ? Qnil : parser->external_link_class;
-    _Wikitext_pop_from_stack_up_to(parser, Qnil, EXT_LINK_START, Qtrue);
+    _Wikitext_pop_from_stack_up_to(parser, NULL, EXT_LINK_START, true);
 
-    rb_str_cat(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
-    if (!NIL_P(parser->link_target))
+    str_append(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
+    if (parser->link_target->len > 0)
     {
-        _Wikitext_append_hyperlink(parser, Qnil, parser->link_target, Qnil, link_class, Qtrue);
+        _Wikitext_append_hyperlink(parser, Qnil, string_from_str(parser->link_target), Qnil, link_class, true);
         if (scope_includes_space)
         {
-            rb_str_cat(parser->output, space, sizeof(space) - 1);
-            if (!NIL_P(parser->link_text))
-                rb_str_append(parser->output, parser->link_text);
+            str_append(parser->output, space, sizeof(space) - 1);
+            if (parser->link_text->len > 0)
+                str_append_str(parser->output, parser->link_text);
         }
     }
-    parser->capturing   = false;
-    parser->link_target = Qnil;
-    parser->link_text   = Qnil;
+    parser->capture = NULL;
+    str_clear(parser->link_target);
+    str_clear(parser->link_text);
 }
 
 void _Wikitext_rollback_failed_link(parser_t *parser)
@@ -1067,28 +1099,15 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
     VALUE prefix        = rb_iv_get(self, "@internal_link_prefix");
 
     // set up parser struct to make passing parameters a little easier
-    // eventually this will encapsulate most or all of the variables above
-    parser_t *parser                = ALLOC_N(parser_t, 1);
+    parser_t *parser                = parser_new();
     GC_WRAP_PARSER(parser, parser_gc);
-    memset(parser, 0, sizeof(parser_t));
-    parser->output                  = rb_str_new2("");
-    parser->capture                 = Qnil;
-    parser->capturing               = false;
-    parser->link_target             = Qnil;
-    parser->link_text               = Qnil;
     parser->external_link_class     = link_class;
     parser->mailto_class            = mailto_class;
     parser->img_prefix              = rb_iv_get(self, "@img_prefix");
-    parser->scope                   = ary_new();
-    parser->line                    = ary_new();
-    parser->line_buffer             = ary_new();
-    parser->pending_crlf            = false;
     parser->autolink                = rb_iv_get(self, "@autolink") == Qtrue ? true : false;
     parser->space_to_underscore     = rb_iv_get(self, "@space_to_underscore") == Qtrue ? true : false;
     parser->line_ending             = str_new_from_string(line_ending);
     parser->base_indent             = base_indent;
-    parser->current_indent          = 0;
-    parser->tabulation              = str_new();
     parser->base_heading_level      = base_heading_level;
 
     // this simple looping design leads to a single enormous function,
@@ -1126,10 +1145,11 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
         long remove_strong          = -1;
         long remove_em              = -1;
 
-        // general purpose counters and flags
+        // general purpose counters, flags and pointers
         long i                      = 0;
         long j                      = 0;
         long k                      = 0;
+        str_t *output              = NULL;
 
         // The following giant switch statement contains cases for all the possible token types.
         // In the most basic sense we are emitting the HTML that corresponds to each token,
@@ -1151,7 +1171,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
             case PRE:
                 if (IN(NO_WIKI_START) || IN(PRE_START))
                 {
-                    rb_str_cat(parser->output, space, sizeof(space) - 1);
+                    str_append(parser->output, space, sizeof(space) - 1);
                     break;
                 }
                 else if (IN(BLOCKQUOTE_START))
@@ -1159,8 +1179,8 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     // this kind of nesting not allowed (to avoid user confusion)
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    i = parser->capturing ? parser->capture : parser->output;
-                    rb_str_cat(i, space, sizeof(space) - 1);
+                    output = parser->capture ? parser->capture : parser->output;
+                    str_append(output, space, sizeof(space) - 1);
                     break;
                 }
 
@@ -1172,15 +1192,15 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 {
                     // must pop (reduce nesting level)
                     for (i = j - i; i > 0; i--)
-                        _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE, true);
                 }
 
                 if (!IN(PRE))
                 {
                     parser->pending_crlf = false;
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE, Qfalse);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE, false);
                     _Wikitext_indent(parser);
-                    rb_str_cat(parser->output, pre_start, sizeof(pre_start) - 1);
+                    str_append(parser->output, pre_start, sizeof(pre_start) - 1);
                     ary_push(parser->scope, PRE);
                 }
                 break;
@@ -1189,14 +1209,14 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_pre_start, sizeof(escaped_pre_start) - 1);
+                    str_append(parser->output, escaped_pre_start, sizeof(escaped_pre_start) - 1);
                 }
                 else if (IN(BLOCKQUOTE_START))
                 {
                     _Wikitext_rollback_failed_link(parser); // if any
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE_START, Qfalse);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE_START, false);
                     _Wikitext_indent(parser);
-                    rb_str_cat(parser->output, pre_start, sizeof(pre_start) - 1);
+                    str_append(parser->output, pre_start, sizeof(pre_start) - 1);
                     ary_push(parser->scope, PRE_START);
                     ary_push(parser->line, PRE_START);
                 }
@@ -1207,24 +1227,24 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         _Wikitext_rollback_failed_link(parser); // if any
                         _Wikitext_pop_all_from_stack(parser);
                         _Wikitext_indent(parser);
-                        rb_str_cat(parser->output, pre_start, sizeof(pre_start) - 1);
+                        str_append(parser->output, pre_start, sizeof(pre_start) - 1);
                         ary_push(parser->scope, PRE_START);
                         ary_push(parser->line, PRE_START);
                     }
                     else // PRE_START illegal here
                     {
-                        i = parser->capturing ? parser->capture : parser->output;
+                        output = parser->capture ? parser->capture : parser->output;
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, escaped_pre_start, sizeof(escaped_pre_start) - 1);
+                        str_append(output, escaped_pre_start, sizeof(escaped_pre_start) - 1);
                     }
                 }
                 else
                 {
                     _Wikitext_rollback_failed_link(parser); // if any
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, P, Qtrue);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, P, true);
                     _Wikitext_indent(parser);
-                    rb_str_cat(parser->output, pre_start, sizeof(pre_start) - 1);
+                    str_append(parser->output, pre_start, sizeof(pre_start) - 1);
                     ary_push(parser->scope, PRE_START);
                     ary_push(parser->line, PRE_START);
                 }
@@ -1234,18 +1254,18 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_pre_end, sizeof(escaped_pre_end) - 1);
+                    str_append(parser->output, escaped_pre_end, sizeof(escaped_pre_end) - 1);
                 }
                 else
                 {
                     if (IN(PRE_START))
-                        _Wikitext_pop_from_stack_up_to(parser, parser->output, PRE_START, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, parser->output, PRE_START, true);
                     else
                     {
-                        i = parser->capturing ? parser->capture : parser->output;
+                        output = parser->capture ? parser->capture : parser->output;
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, escaped_pre_end, sizeof(escaped_pre_end) - 1);
+                        str_append(output, escaped_pre_end, sizeof(escaped_pre_end) - 1);
                     }
                 }
                 break;
@@ -1253,14 +1273,14 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
             case BLOCKQUOTE:
                 if (IN(NO_WIKI_START) || IN(PRE_START))
                     // no need to check for <pre>; can never appear inside it
-                    rb_str_cat(parser->output, escaped_blockquote, TOKEN_LEN(token) + 3); // will either emit "&gt;" or "&gt; "
+                    str_append(parser->output, escaped_blockquote, TOKEN_LEN(token) + 3); // will either emit "&gt;" or "&gt; "
                 else if (IN(BLOCKQUOTE_START))
                 {
                     // this kind of nesting not allowed (to avoid user confusion)
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    i = parser->capturing ? parser->capture : parser->output;
-                    rb_str_cat(i, escaped_blockquote, TOKEN_LEN(token) + 3); // will either emit "&gt;" or "&gt; "
+                    output = parser->capture ? parser->capture : parser->output;
+                    str_append(output, escaped_blockquote, TOKEN_LEN(token) + 3); // will either emit "&gt;" or "&gt; "
                     break;
                 }
                 else
@@ -1282,12 +1302,12 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     if (i > j)
                     {
                         // must push (increase nesting level)
-                        _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE, Qfalse);
+                        _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE, false);
                         for (i = i - j; i > 0; i--)
                         {
                             _Wikitext_indent(parser);
-                            rb_str_cat(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
-                            rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                            str_append(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
+                            str_append_str(parser->output, parser->line_ending);
                             ary_push(parser->scope, BLOCKQUOTE);
                         }
                     }
@@ -1295,7 +1315,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     {
                         // must pop (reduce nesting level)
                         for (i = j - i; i > 0; i--)
-                            _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE, Qtrue);
+                            _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE, true);
                     }
 
                     // jump to top of the loop to process token we scanned during lookahead
@@ -1307,16 +1327,16 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_blockquote_start, sizeof(escaped_blockquote_start) - 1);
+                    str_append(parser->output, escaped_blockquote_start, sizeof(escaped_blockquote_start) - 1);
                 }
                 else if (IN(BLOCKQUOTE_START))
                 {
                     // nesting is fine here
                     _Wikitext_rollback_failed_link(parser); // if any
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE_START, Qfalse);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE_START, false);
                     _Wikitext_indent(parser);
-                    rb_str_cat(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
-                    rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                    str_append(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
+                    str_append_str(parser->output, parser->line_ending);
                     ary_push(parser->scope, BLOCKQUOTE_START);
                     ary_push(parser->line, BLOCKQUOTE_START);
                 }
@@ -1327,27 +1347,27 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         _Wikitext_rollback_failed_link(parser); // if any
                         _Wikitext_pop_all_from_stack(parser);
                         _Wikitext_indent(parser);
-                        rb_str_cat(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
-                        rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                        str_append(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
+                        str_append_str(parser->output, parser->line_ending);
                         ary_push(parser->scope, BLOCKQUOTE_START);
                         ary_push(parser->line, BLOCKQUOTE_START);
                     }
                     else // BLOCKQUOTE_START illegal here
                     {
-                        i = parser->capturing ? parser->capture : parser->output;
+                        output = parser->capture ? parser->capture : parser->output;
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, escaped_blockquote_start, sizeof(escaped_blockquote_start) - 1);
+                        str_append(output, escaped_blockquote_start, sizeof(escaped_blockquote_start) - 1);
                     }
                 }
                 else
                 {
                     // would be nice to eliminate the repetition here but it's probably the clearest way
                     _Wikitext_rollback_failed_link(parser); // if any
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, P, Qtrue);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, P, true);
                     _Wikitext_indent(parser);
-                    rb_str_cat(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
-                    rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                    str_append(parser->output, blockquote_start, sizeof(blockquote_start) - 1);
+                    str_append_str(parser->output, parser->line_ending);
                     ary_push(parser->scope, BLOCKQUOTE_START);
                     ary_push(parser->line, BLOCKQUOTE_START);
                 }
@@ -1357,18 +1377,18 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_blockquote_end, sizeof(escaped_blockquote_end) - 1);
+                    str_append(parser->output, escaped_blockquote_end, sizeof(escaped_blockquote_end) - 1);
                 }
                 else
                 {
                     if (IN(BLOCKQUOTE_START))
-                        _Wikitext_pop_from_stack_up_to(parser, parser->output, BLOCKQUOTE_START, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, parser->output, BLOCKQUOTE_START, true);
                     else
                     {
-                        i = parser->capturing ? parser->capture : parser->output;
+                        output = parser->capture ? parser->capture : parser->output;
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, escaped_blockquote_end, sizeof(escaped_blockquote_end) - 1);
+                        str_append(output, escaped_blockquote_end, sizeof(escaped_blockquote_end) - 1);
                     }
                 }
                 break;
@@ -1377,7 +1397,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_no_wiki_start, sizeof(escaped_no_wiki_start) - 1);
+                    str_append(parser->output, escaped_no_wiki_start, sizeof(escaped_no_wiki_start) - 1);
                 }
                 else
                 {
@@ -1391,12 +1411,12 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
             case NO_WIKI_END:
                 if (IN(NO_WIKI_START))
                     // <nowiki> should always only ever be the last item in the stack, but use the helper routine just in case
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, NO_WIKI_START, Qtrue);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, NO_WIKI_START, true);
                 else
                 {
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_no_wiki_end, sizeof(escaped_no_wiki_end) - 1);
+                    str_append(parser->output, escaped_no_wiki_end, sizeof(escaped_no_wiki_end) - 1);
                 }
                 break;
 
@@ -1404,11 +1424,11 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_strong_em, sizeof(literal_strong_em) - 1);
+                    str_append(parser->output, literal_strong_em, sizeof(literal_strong_em) - 1);
                     break;
                 }
 
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
 
                 // if you've seen STRONG/STRONG_START or EM/EM_START, must close them in the reverse order that you saw them!
@@ -1421,12 +1441,12 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     int val = ary_entry(parser->scope, j);
                     if (val == STRONG || val == STRONG_START)
                     {
-                        rb_str_cat(i, strong_end, sizeof(strong_end) - 1);
+                        str_append(output, strong_end, sizeof(strong_end) - 1);
                         remove_strong = j;
                     }
                     else if (val == EM || val == EM_START)
                     {
-                        rb_str_cat(i, em_end, sizeof(em_end) - 1);
+                        str_append(output, em_end, sizeof(em_end) - 1);
                         remove_em = j;
                     }
                 }
@@ -1438,7 +1458,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         ary_pop(parser->scope);
                     else    // there was no em to remove!, so consider this an opening em tag
                     {
-                        rb_str_cat(i, em_start, sizeof(em_start) - 1);
+                        str_append(output, em_start, sizeof(em_start) - 1);
                         ary_push(parser->scope, EM);
                         ary_push(parser->line, EM);
                     }
@@ -1450,7 +1470,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         ary_pop(parser->scope);
                     else    // there was no strong to remove!, so consider this an opening strong tag
                     {
-                        rb_str_cat(i, strong_start, sizeof(strong_start) - 1);
+                        str_append(output, strong_start, sizeof(strong_start) - 1);
                         ary_push(parser->scope, STRONG);
                         ary_push(parser->line, STRONG);
                     }
@@ -1458,7 +1478,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 else    // no strong or em to remove, so this must be a new opening of both
                 {
                     _Wikitext_start_para_if_necessary(parser);
-                    rb_str_cat(i, strong_em_start, sizeof(strong_em_start) - 1);
+                    str_append(output, strong_em_start, sizeof(strong_em_start) - 1);
                     ary_push(parser->scope, STRONG);
                     ary_push(parser->line, STRONG);
                     ary_push(parser->scope, EM);
@@ -1470,23 +1490,23 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_strong, sizeof(literal_strong) - 1);
+                    str_append(parser->output, literal_strong, sizeof(literal_strong) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(STRONG_START))
                         // already in span started with <strong>, no choice but to emit this literally
-                        rb_str_cat(parser->output, literal_strong, sizeof(literal_strong) - 1);
+                        str_append(output, literal_strong, sizeof(literal_strong) - 1);
                     else if (IN(STRONG))
                         // STRONG already seen, this is a closing tag
-                        _Wikitext_pop_from_stack_up_to(parser, i, STRONG, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, output, STRONG, true);
                     else
                     {
                         // this is a new opening
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, strong_start, sizeof(strong_start) - 1);
+                        str_append(output, strong_start, sizeof(strong_start) - 1);
                         ary_push(parser->scope, STRONG);
                         ary_push(parser->line, STRONG);
                     }
@@ -1497,18 +1517,18 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_strong_start, sizeof(escaped_strong_start) - 1);
+                    str_append(parser->output, escaped_strong_start, sizeof(escaped_strong_start) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(STRONG_START) || IN(STRONG))
-                        rb_str_cat(parser->output, escaped_strong_start, sizeof(escaped_strong_start) - 1);
+                        str_append(output, escaped_strong_start, sizeof(escaped_strong_start) - 1);
                     else
                     {
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, strong_start, sizeof(strong_start) - 1);
+                        str_append(output, strong_start, sizeof(strong_start) - 1);
                         ary_push(parser->scope, STRONG_START);
                         ary_push(parser->line, STRONG_START);
                     }
@@ -1519,19 +1539,19 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_strong_end, sizeof(escaped_strong_end) - 1);
+                    str_append(parser->output, escaped_strong_end, sizeof(escaped_strong_end) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(STRONG_START))
-                        _Wikitext_pop_from_stack_up_to(parser, i, STRONG_START, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, output, STRONG_START, true);
                     else
                     {
                         // no STRONG_START in scope, so must interpret the STRONG_END without any special meaning
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, escaped_strong_end, sizeof(escaped_strong_end) - 1);
+                        str_append(output, escaped_strong_end, sizeof(escaped_strong_end) - 1);
                     }
                 }
                 break;
@@ -1540,23 +1560,23 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_em, sizeof(literal_em) - 1);
+                    str_append(parser->output, literal_em, sizeof(literal_em) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(EM_START))
                         // already in span started with <em>, no choice but to emit this literally
-                        rb_str_cat(parser->output, literal_em, sizeof(literal_em) - 1);
+                        str_append(output, literal_em, sizeof(literal_em) - 1);
                     else if (IN(EM))
                         // EM already seen, this is a closing tag
-                        _Wikitext_pop_from_stack_up_to(parser, i, EM, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, output, EM, true);
                     else
                     {
                         // this is a new opening
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, em_start, sizeof(em_start) - 1);
+                        str_append(output, em_start, sizeof(em_start) - 1);
                         ary_push(parser->scope, EM);
                         ary_push(parser->line, EM);
                     }
@@ -1567,18 +1587,18 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_em_start, sizeof(escaped_em_start) - 1);
+                    str_append(parser->output, escaped_em_start, sizeof(escaped_em_start) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(EM_START) || IN(EM))
-                        rb_str_cat(i, escaped_em_start, sizeof(escaped_em_start) - 1);
+                        str_append(output, escaped_em_start, sizeof(escaped_em_start) - 1);
                     else
                     {
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, em_start, sizeof(em_start) - 1);
+                        str_append(output, em_start, sizeof(em_start) - 1);
                         ary_push(parser->scope, EM_START);
                         ary_push(parser->line, EM_START);
                     }
@@ -1589,19 +1609,19 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_em_end, sizeof(escaped_em_end) - 1);
+                    str_append(parser->output, escaped_em_end, sizeof(escaped_em_end) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(EM_START))
-                        _Wikitext_pop_from_stack_up_to(parser, i, EM_START, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, output, EM_START, true);
                     else
                     {
                         // no EM_START in scope, so must interpret the TT_END without any special meaning
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, escaped_em_end, sizeof(escaped_em_end) - 1);
+                        str_append(output, escaped_em_end, sizeof(escaped_em_end) - 1);
                     }
                 }
                 break;
@@ -1610,23 +1630,23 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, backtick, sizeof(backtick) - 1);
+                    str_append(parser->output, backtick, sizeof(backtick) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(TT_START))
                         // already in span started with <tt>, no choice but to emit this literally
-                        rb_str_cat(parser->output, backtick, sizeof(backtick) - 1);
+                        str_append(output, backtick, sizeof(backtick) - 1);
                     else if (IN(TT))
                         // TT (`) already seen, this is a closing tag
-                        _Wikitext_pop_from_stack_up_to(parser, i, TT, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, output, TT, true);
                     else
                     {
                         // this is a new opening
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, tt_start, sizeof(tt_start) - 1);
+                        str_append(output, tt_start, sizeof(tt_start) - 1);
                         ary_push(parser->scope, TT);
                         ary_push(parser->line, TT);
                     }
@@ -1637,18 +1657,18 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_tt_start, sizeof(escaped_tt_start) - 1);
+                    str_append(parser->output, escaped_tt_start, sizeof(escaped_tt_start) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(TT_START) || IN(TT))
-                        rb_str_cat(i, escaped_tt_start, sizeof(escaped_tt_start) - 1);
+                        str_append(output, escaped_tt_start, sizeof(escaped_tt_start) - 1);
                     else
                     {
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, tt_start, sizeof(tt_start) - 1);
+                        str_append(output, tt_start, sizeof(tt_start) - 1);
                         ary_push(parser->scope, TT_START);
                         ary_push(parser->line, TT_START);
                     }
@@ -1659,19 +1679,19 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, escaped_tt_end, sizeof(escaped_tt_end) - 1);
+                    str_append(parser->output, escaped_tt_end, sizeof(escaped_tt_end) - 1);
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     if (IN(TT_START))
-                        _Wikitext_pop_from_stack_up_to(parser, i, TT_START, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, output, TT_START, true);
                     else
                     {
                         // no TT_START in scope, so must interpret the TT_END without any special meaning
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, escaped_tt_end, sizeof(escaped_tt_end) - 1);
+                        str_append(output, escaped_tt_end, sizeof(escaped_tt_end) - 1);
                     }
                 }
                 break;
@@ -1681,7 +1701,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE_START))
                 {
                     // no need to check for PRE; can never appear inside it
-                    rb_str_cat(parser->output, token->start, TOKEN_LEN(token));
+                    str_append(parser->output, token->start, TOKEN_LEN(token));
                     break;
                 }
 
@@ -1711,7 +1731,8 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         // want to compare line with scope but can only do so if scope has enough items on it
                         if (j >= i)
                         {
-                            if (ary_entry(parser->scope, i + bq_count - 2) == type && ary_entry(parser->scope, i + bq_count - 1) == LI)
+                            if (ary_entry(parser->scope, i + bq_count - 2) == type &&
+                                ary_entry(parser->scope, i + bq_count - 1) == LI)
                             {
                                 // line and scope match at this point: do nothing yet
                             }
@@ -1720,7 +1741,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                                 // item just pushed onto line does not match corresponding slot of scope!
                                 for (; j >= i - 2; j--)
                                     // must pop back before emitting
-                                    _Wikitext_pop_from_stack(parser, Qnil);
+                                    _Wikitext_pop_from_stack(parser, NULL);
 
                                 // will emit UL or OL, then LI
                                 break;
@@ -1734,13 +1755,13 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         // not a OL or UL token!
                         if (j == i)
                             // must close existing LI and re-open new one
-                            _Wikitext_pop_from_stack(parser, Qnil);
+                            _Wikitext_pop_from_stack(parser, NULL);
                         else if (j > i)
                         {
                             // item just pushed onto line does not match corresponding slot of scope!
                             for (; j >= i; j--)
                                 // must pop back before emitting
-                                _Wikitext_pop_from_stack(parser, Qnil);
+                                _Wikitext_pop_from_stack(parser, NULL);
                         }
                         break;
                     }
@@ -1754,33 +1775,33 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     if (j > 0 && ary_entry(parser->scope, -1) == LI)
                     {
                         // so we should precede it with a CRLF, and indicate that it's a nested list
-                        rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                        str_append(parser->output, parser->line_ending->ptr, parser->line_ending->len);
                         ary_push(parser->scope, NESTED_LIST);
                     }
                     else
                     {
                         // this is a new list
                         if (IN(BLOCKQUOTE_START))
-                            _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE_START, Qfalse);
+                            _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE_START, false);
                         else
-                            _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE, Qfalse);
+                            _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE, false);
                     }
 
                     // emit
                     _Wikitext_indent(parser);
                     if (type == OL)
-                        rb_str_cat(parser->output, ol_start, sizeof(ol_start) - 1);
+                        str_append(parser->output, ol_start, sizeof(ol_start) - 1);
                     else if (type == UL)
-                        rb_str_cat(parser->output, ul_start, sizeof(ul_start) - 1);
+                        str_append(parser->output, ul_start, sizeof(ul_start) - 1);
                     ary_push(parser->scope, type);
-                    rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                    str_append(parser->output, parser->line_ending->ptr, parser->line_ending->len);
                 }
                 else if (type == SPACE)
                     // silently throw away the optional SPACE token after final list marker
                     token = NULL;
 
                 _Wikitext_indent(parser);
-                rb_str_cat(parser->output, li_start, sizeof(li_start) - 1);
+                str_append(parser->output, li_start, sizeof(li_start) - 1);
                 ary_push(parser->scope, LI);
 
                 // any subsequent UL or OL tokens on this line are syntax errors and must be emitted literally
@@ -1790,7 +1811,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     while (k++, NEXT_TOKEN(), (type = token->type))
                     {
                         if (type == OL || type == UL)
-                            rb_str_cat(parser->output, token->start, TOKEN_LEN(token));
+                            str_append(parser->output, token->start, TOKEN_LEN(token));
                         else if (type == SPACE && k == 1)
                         {
                             // silently throw away the optional SPACE token after final list marker
@@ -1814,15 +1835,15 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE_START))
                 {
                     // no need to check for PRE; can never appear inside it
-                    rb_str_cat(parser->output, token->start, TOKEN_LEN(token));
+                    str_append(parser->output, token->start, TOKEN_LEN(token));
                     break;
                 }
 
                 // pop up to but not including the last BLOCKQUOTE on the scope stack
                 if (IN(BLOCKQUOTE_START))
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE_START, Qfalse);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE_START, false);
                 else
-                    _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE, Qfalse);
+                    _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE, false);
 
                 // count number of BLOCKQUOTE tokens in line buffer and in scope stack
                 ary_push(parser->line, type);
@@ -1834,7 +1855,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 {
                     // must pop (reduce nesting level)
                     for (i = j - i; i > 0; i--)
-                        _Wikitext_pop_from_stack_up_to(parser, Qnil, BLOCKQUOTE, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, NULL, BLOCKQUOTE, true);
                 }
 
                 // discard any whitespace here (so that "== foo ==" will be translated to "<h2>foo</h2>" rather than "<h2> foo </h2")
@@ -1851,17 +1872,17 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
 
                 // rather than repeat all that code for each kind of heading, share it and use a conditional here
                 if (type == H6_START)
-                    rb_str_cat(parser->output, h6_start, sizeof(h6_start) - 1);
+                    str_append(parser->output, h6_start, sizeof(h6_start) - 1);
                 else if (type == H5_START)
-                    rb_str_cat(parser->output, h5_start, sizeof(h5_start) - 1);
+                    str_append(parser->output, h5_start, sizeof(h5_start) - 1);
                 else if (type == H4_START)
-                    rb_str_cat(parser->output, h4_start, sizeof(h4_start) - 1);
+                    str_append(parser->output, h4_start, sizeof(h4_start) - 1);
                 else if (type == H3_START)
-                    rb_str_cat(parser->output, h3_start, sizeof(h3_start) - 1);
+                    str_append(parser->output, h3_start, sizeof(h3_start) - 1);
                 else if (type == H2_START)
-                    rb_str_cat(parser->output, h2_start, sizeof(h2_start) - 1);
+                    str_append(parser->output, h2_start, sizeof(h2_start) - 1);
                 else if (type == H1_START)
-                    rb_str_cat(parser->output, h1_start, sizeof(h1_start) - 1);
+                    str_append(parser->output, h1_start, sizeof(h1_start) - 1);
 
                 // jump to top of the loop to process token we scanned during lookahead
                 continue;
@@ -1870,7 +1891,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_h6, sizeof(literal_h6) - 1);
+                    str_append(parser->output, literal_h6, sizeof(literal_h6) - 1);
                 }
                 else
                 {
@@ -1879,7 +1900,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     {
                         // literal output only if not in h6 scope (we stay silent in that case)
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(parser->output, literal_h6, sizeof(literal_h6) - 1);
+                        str_append(parser->output, literal_h6, sizeof(literal_h6) - 1);
                     }
                 }
                 break;
@@ -1888,7 +1909,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_h5, sizeof(literal_h5) - 1);
+                    str_append(parser->output, literal_h5, sizeof(literal_h5) - 1);
                 }
                 else
                 {
@@ -1897,7 +1918,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     {
                         // literal output only if not in h5 scope (we stay silent in that case)
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(parser->output, literal_h5, sizeof(literal_h5) - 1);
+                        str_append(parser->output, literal_h5, sizeof(literal_h5) - 1);
                     }
                 }
                 break;
@@ -1906,7 +1927,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_h4, sizeof(literal_h4) - 1);
+                    str_append(parser->output, literal_h4, sizeof(literal_h4) - 1);
                 }
                 else
                 {
@@ -1915,7 +1936,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     {
                         // literal output only if not in h4 scope (we stay silent in that case)
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(parser->output, literal_h4, sizeof(literal_h4) - 1);
+                        str_append(parser->output, literal_h4, sizeof(literal_h4) - 1);
                     }
                 }
                 break;
@@ -1924,7 +1945,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_h3, sizeof(literal_h3) - 1);
+                    str_append(parser->output, literal_h3, sizeof(literal_h3) - 1);
                 }
                 else
                 {
@@ -1933,7 +1954,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     {
                         // literal output only if not in h3 scope (we stay silent in that case)
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(parser->output, literal_h3, sizeof(literal_h3) - 1);
+                        str_append(parser->output, literal_h3, sizeof(literal_h3) - 1);
                     }
                 }
                 break;
@@ -1942,7 +1963,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_h2, sizeof(literal_h2) - 1);
+                    str_append(parser->output, literal_h2, sizeof(literal_h2) - 1);
                 }
                 else
                 {
@@ -1951,7 +1972,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     {
                         // literal output only if not in h2 scope (we stay silent in that case)
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(parser->output, literal_h2, sizeof(literal_h2) - 1);
+                        str_append(parser->output, literal_h2, sizeof(literal_h2) - 1);
                     }
                 }
                 break;
@@ -1960,7 +1981,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, literal_h1, sizeof(literal_h1) - 1);
+                    str_append(parser->output, literal_h1, sizeof(literal_h1) - 1);
                 }
                 else
                 {
@@ -1969,7 +1990,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     {
                         // literal output only if not in h1 scope (we stay silent in that case)
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(parser->output, literal_h1, sizeof(literal_h1) - 1);
+                        str_append(parser->output, literal_h1, sizeof(literal_h1) - 1);
                     }
                 }
                 break;
@@ -1978,13 +1999,13 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, token->start, TOKEN_LEN(token));
+                    str_append(parser->output, token->start, TOKEN_LEN(token));
                 }
                 else
                 {
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    _Wikitext_append_hyperlink(parser, rb_str_new2("mailto:"), TOKEN_TEXT(token), Qnil, mailto_class, Qtrue);
+                    _Wikitext_append_hyperlink(parser, rb_str_new2("mailto:"), TOKEN_TEXT(token), Qnil, mailto_class, true);
                 }
                 break;
 
@@ -1992,16 +2013,16 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START))
                     // user can temporarily suppress autolinking by using <nowiki></nowiki>
                     // note that unlike MediaWiki, we do allow autolinking inside PRE blocks
-                    rb_str_cat(parser->output, token->start, TOKEN_LEN(token));
+                    str_append(parser->output, token->start, TOKEN_LEN(token));
                 else if (IN(LINK_START))
                 {
                     // if the URI were allowed it would have been handled already in LINK_START
                     _Wikitext_rollback_failed_internal_link(parser);
-                    _Wikitext_append_hyperlink(parser, Qnil, TOKEN_TEXT(token), Qnil, parser->external_link_class, Qtrue);
+                    _Wikitext_append_hyperlink(parser, Qnil, TOKEN_TEXT(token), Qnil, parser->external_link_class, true);
                 }
                 else if (IN(EXT_LINK_START))
                 {
-                    if (NIL_P(parser->link_target))
+                    if (parser->link_target->len == 0)
                     {
                         // this must be our link target: look ahead to make sure we see the space we're expecting to see
                         i = TOKEN_TEXT(token);
@@ -2009,46 +2030,38 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         if (token->type == SPACE)
                         {
                             ary_push(parser->scope, SPACE);
-                            parser->link_target = i;
-                            parser->link_text   = rb_str_new2("");
+                            str_append_string(parser->link_target, i);
+                            str_clear(parser->link_text);
                             parser->capture     = parser->link_text;
-                            parser->capturing   = true;
                             token               = NULL; // silently consume space
                         }
                         else
                         {
                             // didn't see the space! this must be an error
-                            _Wikitext_pop_from_stack(parser, Qnil);
+                            _Wikitext_pop_from_stack(parser, NULL);
                             _Wikitext_pop_excess_elements(parser);
                             _Wikitext_start_para_if_necessary(parser);
-                            rb_str_cat(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
-                            _Wikitext_append_hyperlink(parser, Qnil, i, Qnil, parser->external_link_class, Qtrue);
+                            str_append(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
+                            _Wikitext_append_hyperlink(parser, Qnil, i, Qnil, parser->external_link_class, true);
                         }
                     }
                     else
-                    {
-                        if (NIL_P(parser->link_text))
-                            // this must be the first part of our link text
-                            parser->link_text = TOKEN_TEXT(token);
-                        else
-                            // add to existing link text
-                            rb_str_cat(parser->link_text, token->start, TOKEN_LEN(token));
-                    }
+                        str_append(parser->link_text, token->start, TOKEN_LEN(token));
                 }
                 else
                 {
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    _Wikitext_append_hyperlink(parser, Qnil, TOKEN_TEXT(token), Qnil, parser->external_link_class, Qtrue);
+                    _Wikitext_append_hyperlink(parser, Qnil, TOKEN_TEXT(token), Qnil, parser->external_link_class, true);
                 }
                 break;
 
             case PATH:
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
-                    rb_str_cat(parser->output, token->start, TOKEN_LEN(token));
+                    str_append(parser->output, token->start, TOKEN_LEN(token));
                 else if (IN(EXT_LINK_START))
                 {
-                    if (NIL_P(parser->link_target))
+                    if (parser->link_target->len == 0)
                     {
                         // this must be our link target: look ahead to make sure we see the space we're expecting to see
                         i = TOKEN_TEXT(token);
@@ -2057,38 +2070,30 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         {
                             ary_push(parser->scope, PATH);
                             ary_push(parser->scope, SPACE);
-                            parser->link_target = i;
-                            parser->link_text   = rb_str_new2("");
+                            str_append_string(parser->link_target, i);
+                            str_clear(parser->link_text);
                             parser->capture     = parser->link_text;
-                            parser->capturing   = true;
                             token               = NULL; // silently consume space
                         }
                         else
                         {
                             // didn't see the space! this must be an error
-                            _Wikitext_pop_from_stack(parser, Qnil);
+                            _Wikitext_pop_from_stack(parser, NULL);
                             _Wikitext_pop_excess_elements(parser);
                             _Wikitext_start_para_if_necessary(parser);
-                            rb_str_cat(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
-                            rb_str_append(parser->output, i);
+                            str_append(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
+                            str_append_string(parser->output, i);
                         }
                     }
                     else
-                    {
-                        if (NIL_P(parser->link_text))
-                            // this must be the first part of our link text
-                            parser->link_text = TOKEN_TEXT(token);
-                        else
-                            // add to existing link text
-                            rb_str_cat(parser->link_text, token->start, TOKEN_LEN(token));
-                    }
+                        str_append(parser->link_text, token->start, TOKEN_LEN(token));
                 }
                 else
                 {
-                    i = parser->capturing ? parser->capture : parser->output;
+                    output = parser->capture ? parser->capture : parser->output;
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    rb_str_cat(i, token->start, TOKEN_LEN(token));
+                    str_append(output, token->start, TOKEN_LEN(token));
                 }
                 break;
 
@@ -2115,20 +2120,20 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
             //      SPACE, SPECIAL_URI_CHARS, PRINTABLE, PATH, ALNUM, DEFAULT, QUOT and AMP
             // everything else will be rejected
             case LINK_START:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(i, link_start, sizeof(link_start) - 1);
+                    str_append(output, link_start, sizeof(link_start) - 1);
                 }
                 else if (IN(EXT_LINK_START))
                     // already in external link scope! (and in fact, must be capturing link_text right now)
-                    rb_str_cat(i, link_start, sizeof(link_start) - 1);
+                    str_append(output, link_start, sizeof(link_start) - 1);
                 else if (IN(LINK_START))
                 {
                     // already in internal link scope! this is a syntax error
                     _Wikitext_rollback_failed_internal_link(parser);
-                    rb_str_cat(parser->output, link_start, sizeof(link_start) - 1);
+                    str_append(parser->output, link_start, sizeof(link_start) - 1);
                 }
                 else if (IN(SEPARATOR))
                 {
@@ -2160,37 +2165,35 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                             type == RIGHT_CURLY)
                         {
                             // accumulate these tokens into link_target
-                            if (NIL_P(parser->link_target))
+                            if (parser->link_target->len == 0)
                             {
-                                parser->link_target = rb_str_new2("");
-                                parser->capture     = parser->link_target;
-                                parser->capturing   = true;
+                                str_clear(parser->link_target);
+                                parser->capture = parser->link_target;
                             }
                             if (type == QUOT_ENTITY)
                                 // don't insert the entity, insert the literal quote
-                                rb_str_cat(parser->link_target, quote, sizeof(quote) - 1);
+                                str_append(parser->link_target, quote, sizeof(quote) - 1);
                             else if (type == AMP_ENTITY)
                                 // don't insert the entity, insert the literal ampersand
-                                rb_str_cat(parser->link_target, ampersand, sizeof(ampersand) - 1);
+                                str_append(parser->link_target, ampersand, sizeof(ampersand) - 1);
                             else
-                                rb_str_cat(parser->link_target, token->start, TOKEN_LEN(token));
+                                str_append(parser->link_target, token->start, TOKEN_LEN(token));
                         }
                         else if (type == LINK_END)
                         {
-                            if (NIL_P(parser->link_target)) // bail for inputs like "[[]]"
+                            if (parser->link_target->len == 0) // bail for inputs like "[[]]"
                                 _Wikitext_rollback_failed_internal_link(parser);
                             break; // jump back to top of loop (will handle this in LINK_END case below)
                         }
                         else if (type == SEPARATOR)
                         {
-                            if (NIL_P(parser->link_target)) // bail for inputs like "[[|"
+                            if (parser->link_target->len == 0) // bail for inputs like "[[|"
                                 _Wikitext_rollback_failed_internal_link(parser);
                             else
                             {
                                 ary_push(parser->scope, SEPARATOR);
-                                parser->link_text   = rb_str_new2("");
+                                str_clear(parser->link_text);
                                 parser->capture     = parser->link_text;
-                                parser->capturing   = true;
                                 token               = NULL;
                             }
                             break;
@@ -2208,44 +2211,45 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 break;
 
             case LINK_END:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(i, link_end, sizeof(link_end) - 1);
+                    str_append(output, link_end, sizeof(link_end) - 1);
                 }
                 else if (IN(EXT_LINK_START))
                     // already in external link scope! (and in fact, must be capturing link_text right now)
-                    rb_str_cat(i, link_end, sizeof(link_end) - 1);
+                    str_append(output, link_end, sizeof(link_end) - 1);
                 else if (IN(LINK_START)) // in internal link scope!
                 {
                     if (_Wikitext_blank(parser->link_target))
                     {
                         // special case for inputs like "[[    ]]"
                         _Wikitext_rollback_failed_internal_link(parser);
-                        rb_str_cat(parser->output, link_end, sizeof(link_end) - 1);
+                        str_append(parser->output, link_end, sizeof(link_end) - 1);
                         break;
                     }
-                    if (NIL_P(parser->link_text) ||
-                        RSTRING_LEN(parser->link_text) == 0 ||
+                    if (parser->link_text->len == 0 ||
                         _Wikitext_blank(parser->link_text))
+                    {
                         // use link target as link text
-                        parser->link_text = _Wikitext_parser_sanitize_link_target(parser, Qfalse);
+                        str_clear(parser->link_text);
+                        str_append_string(parser->link_text, _Wikitext_parser_sanitize_link_target(parser, false));
+                    }
                     else
-                        parser->link_text = _Wikitext_parser_trim_link_target(parser->link_text);
+                        _Wikitext_parser_trim_link_text(parser);
                     _Wikitext_parser_encode_link_target(parser);
-                    _Wikitext_pop_from_stack_up_to(parser, i, LINK_START, Qtrue);
-                    parser->capture     = Qnil;
-                    parser->capturing   = false;
-                    _Wikitext_append_hyperlink(parser, prefix, parser->link_target, parser->link_text, Qnil, Qfalse);
-                    parser->link_target = Qnil;
-                    parser->link_text   = Qnil;
+                    _Wikitext_pop_from_stack_up_to(parser, output, LINK_START, true);
+                    parser->capture = NULL;
+                    _Wikitext_append_hyperlink(parser, prefix, string_from_str(parser->link_target), string_from_str(parser->link_text), Qnil, false);
+                    str_clear(parser->link_target);
+                    str_clear(parser->link_text);
                 }
                 else // wasn't in internal link scope
                 {
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    rb_str_cat(i, link_end, sizeof(link_end) - 1);
+                    str_append(output, link_end, sizeof(link_end) - 1);
                 }
                 break;
 
@@ -2255,35 +2259,28 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
             // strings in square brackets which don't match this syntax get passed through literally; eg:
             //      he was very angery [sic] about the turn of events
             case EXT_LINK_START:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(i, ext_link_start, sizeof(ext_link_start) - 1);
+                    str_append(output, ext_link_start, sizeof(ext_link_start) - 1);
                 }
                 else if (IN(EXT_LINK_START))
                     // already in external link scope! (and in fact, must be capturing link_text right now)
-                    rb_str_cat(i, ext_link_start, sizeof(ext_link_start) - 1);
+                    str_append(output, ext_link_start, sizeof(ext_link_start) - 1);
                 else if (IN(LINK_START))
                 {
                     // already in internal link scope!
-                    i = rb_str_new(ext_link_start, sizeof(ext_link_start) - 1);
-                    if (NIL_P(parser->link_target))
+                    if (parser->link_target->len == 0)
                         // this must be the first character of our link target
-                        parser->link_target = i;
+                        str_append(parser->link_target, ext_link_start, sizeof(ext_link_start) - 1);
                     else if (IN(SPACE))
-                    {
                         // link target has already been scanned
-                        if (NIL_P(parser->link_text))
-                            // this must be the first character of our link text
-                            parser->link_text = i;
-                        else
-                            // add to existing link text
-                            rb_str_append(parser->link_text, i);
-                    }
+                        str_append(parser->link_text, ext_link_start, sizeof(ext_link_start) - 1);
                     else
+                        // TODO: possibly roll this condition into the above
                         // add to existing link target
-                        rb_str_append(parser->link_target, i);
+                        str_append(parser->link_target, ext_link_start, sizeof(ext_link_start) - 1);
                 }
                 else // not in external link scope yet
                 {
@@ -2297,56 +2294,55 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         ary_push(parser->scope, EXT_LINK_START);    // so far so good, jump back to the top of the loop
                     else
                         // only get here if there was a syntax error (missing URI)
-                        rb_str_cat(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
+                        str_append(parser->output, ext_link_start, sizeof(ext_link_start) - 1);
                     continue; // jump back to top of loop to handle token (either URI or whatever it is)
                 }
                 break;
 
             case EXT_LINK_END:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(i, ext_link_end, sizeof(ext_link_end) - 1);
+                    str_append(output, ext_link_end, sizeof(ext_link_end) - 1);
                 }
                 else if (IN(EXT_LINK_START))
                 {
-                    if (NIL_P(parser->link_text))
+                    if (parser->link_text->len == 0)
                         // syntax error: external link with no link text
                         _Wikitext_rollback_failed_external_link(parser);
                     else
                     {
                         // success!
                         j = IN(PATH) ? Qnil : parser->external_link_class;
-                        _Wikitext_pop_from_stack_up_to(parser, i, EXT_LINK_START, Qtrue);
-                        parser->capture     = Qnil;
-                        parser->capturing   = false;
-                        _Wikitext_append_hyperlink(parser, Qnil, parser->link_target, parser->link_text, j, Qfalse);
+                        _Wikitext_pop_from_stack_up_to(parser, output, EXT_LINK_START, true);
+                        parser->capture = NULL;
+                        _Wikitext_append_hyperlink(parser, Qnil, string_from_str(parser->link_target), string_from_str(parser->link_text), j, false);
                     }
-                    parser->link_target = Qnil;
-                    parser->link_text   = Qnil;
+                    str_clear(parser->link_target);
+                    str_clear(parser->link_text);
                 }
                 else
                 {
                     _Wikitext_pop_excess_elements(parser);
                     _Wikitext_start_para_if_necessary(parser);
-                    rb_str_cat(parser->output, ext_link_end, sizeof(ext_link_end) - 1);
+                    str_append(parser->output, ext_link_end, sizeof(ext_link_end) - 1);
                 }
                 break;
 
             case SEPARATOR:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_cat(i, separator, sizeof(separator) - 1);
+                str_append(output, separator, sizeof(separator) - 1);
                 break;
 
             case SPACE:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(i, token->start, TOKEN_LEN(token));
+                    str_append(output, token->start, TOKEN_LEN(token));
                 }
                 else
                 {
@@ -2369,7 +2365,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         // emit the space
                         _Wikitext_pop_excess_elements(parser);
                         _Wikitext_start_para_if_necessary(parser);
-                        rb_str_cat(i, token_ptr, token_len);
+                        str_append(output, token_ptr, token_len);
                     }
 
                     // jump to top of the loop to process token we scanned during lookahead
@@ -2382,56 +2378,57 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
             case NAMED_ENTITY:
             case DECIMAL_ENTITY:
                 // pass these through unaltered as they are case sensitive
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_cat(i, token->start, TOKEN_LEN(token));
+                str_append(output, token->start, TOKEN_LEN(token));
                 break;
 
             case HEX_ENTITY:
                 // normalize hex entities (downcase them)
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_append(i, _Wikitext_downcase_bang(TOKEN_TEXT(token)));
+                str_append(output, token->start, TOKEN_LEN(token));
+                _Wikitext_downcase_bang(output->ptr + output->len - TOKEN_LEN(token), TOKEN_LEN(token));
                 break;
 
             case QUOT:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_cat(i, quot_entity, sizeof(quot_entity) - 1);
+                str_append(output, quot_entity, sizeof(quot_entity) - 1);
                 break;
 
             case AMP:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_cat(i, amp_entity, sizeof(amp_entity) - 1);
+                str_append(output, amp_entity, sizeof(amp_entity) - 1);
                 break;
 
             case LESS:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_cat(i, lt_entity, sizeof(lt_entity) - 1);
+                str_append(output, lt_entity, sizeof(lt_entity) - 1);
                 break;
 
             case GREATER:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_cat(i, gt_entity, sizeof(gt_entity) - 1);
+                str_append(output, gt_entity, sizeof(gt_entity) - 1);
                 break;
 
             case IMG_START:
                 if (IN(NO_WIKI_START) || IN(PRE) || IN(PRE_START))
                 {
                     _Wikitext_emit_pending_crlf_if_necessary(parser);
-                    rb_str_cat(parser->output, token->start, TOKEN_LEN(token));
+                    str_append(parser->output, token->start, TOKEN_LEN(token));
                 }
-                else if (parser->capturing)
-                    rb_str_cat(parser->capture, token->start, TOKEN_LEN(token));
+                else if (parser->capture)
+                    str_append(parser->capture, token->start, TOKEN_LEN(token));
                 else
                 {
                     // not currently capturing: will be emitting something on success or failure, so get ready
@@ -2440,30 +2437,29 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
 
                     // scan ahead consuming PATH, PRINTABLE, ALNUM and SPECIAL_URI_CHARS tokens
                     // will cheat here and abuse the link_target capture buffer to accumulate text
-                    if (NIL_P(parser->link_target))
-                        parser->link_target = rb_str_new2("");
                     while (NEXT_TOKEN(), (type = token->type))
                     {
                         if (type == PATH || type == PRINTABLE || type == ALNUM || type == SPECIAL_URI_CHARS)
-                            rb_str_cat(parser->link_target, token->start, TOKEN_LEN(token));
-                        else if (type == IMG_END && RSTRING_LEN(parser->link_target) > 0)
+                            str_append(parser->link_target, token->start, TOKEN_LEN(token));
+                        else if (type == IMG_END && parser->link_target->len > 0)
                         {
                             // success
-                            _Wikitext_append_img(parser, RSTRING_PTR(parser->link_target), RSTRING_LEN(parser->link_target));
+                            _Wikitext_append_img(parser, parser->link_target->ptr, parser->link_target->len);
                             token = NULL;
                             break;
                         }
                         else // unexpected token or zero-length target (syntax error)
                         {
                             // rollback
-                            rb_str_cat(parser->output, literal_img_start, sizeof(literal_img_start) - 1);
-                            rb_str_cat(parser->output, RSTRING_PTR(parser->link_target), RSTRING_LEN(parser->link_target));
+                            str_append(parser->output, literal_img_start, sizeof(literal_img_start) - 1);
+                            if (parser->link_target->len > 0)
+                                str_append(parser->output, parser->link_target->ptr, parser->link_target->len);
                             break;
                         }
                     }
 
                     // jump to top of the loop to process token we scanned during lookahead
-                    parser->link_target = Qnil;
+                    str_clear(parser->link_target);
                     continue;
                 }
                 break;
@@ -2475,7 +2471,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(NO_WIKI_START) || IN(PRE_START))
                 {
                     ary_clear(parser->line_buffer);
-                    rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                    str_append_str(parser->output, parser->line_ending);
                     break;
                 }
                 else if (IN(PRE))
@@ -2483,14 +2479,14 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                     // beware when BLOCKQUOTE on line buffer (not line stack!) prior to CRLF, that must be end of PRE block
                     if (ary_entry(parser->line_buffer, -2) == BLOCKQUOTE)
                         // don't emit in this case
-                        _Wikitext_pop_from_stack_up_to(parser, parser->output, PRE, Qtrue);
+                        _Wikitext_pop_from_stack_up_to(parser, parser->output, PRE, true);
                     else
                     {
                         if (ary_entry(parser->line_buffer, -2) == PRE)
                         {
                              // only thing on line is the PRE: emit pending line ending (if we had one)
                              if (i)
-                                 rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                                 str_append_str(parser->output, parser->line_ending);
                         }
 
                         // clear these _before_ calling NEXT_TOKEN (NEXT_TOKEN adds to the line_buffer)
@@ -2502,7 +2498,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                         type = token->type;
                         if (type != BLOCKQUOTE && type != PRE)
                             // this is definitely the end of the block, so don't emit
-                            _Wikitext_pop_from_stack_up_to(parser, parser->output, PRE, Qtrue);
+                            _Wikitext_pop_from_stack_up_to(parser, parser->output, PRE, true);
                         else
                             // potentially will emit
                             parser->pending_crlf = true;
@@ -2540,7 +2536,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                                 // not a paragraph break!
                                 continue;
                         }
-                        _Wikitext_pop_from_stack(parser, Qnil);
+                        _Wikitext_pop_from_stack(parser, NULL);
                     }
                 }
 
@@ -2555,17 +2551,17 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
             case IMG_END:
             case LEFT_CURLY:
             case RIGHT_CURLY:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_cat(i, token->start, TOKEN_LEN(token));
+                str_append(output, token->start, TOKEN_LEN(token));
                 break;
 
             case DEFAULT:
-                i = parser->capturing ? parser->capture : parser->output;
+                output = parser->capture ? parser->capture : parser->output;
                 _Wikitext_pop_excess_elements(parser);
                 _Wikitext_start_para_if_necessary(parser);
-                rb_str_append(i, _Wikitext_utf32_char_to_entity(token->code_point));    // convert to entity
+                str_append_string(output, _Wikitext_utf32_char_to_entity(token->code_point));    // convert to entity
                 break;
 
             case END_OF_FILE:
@@ -2573,7 +2569,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 if (IN(PRE) &&
                     ary_entry(parser->line_buffer, -2) == PRE &&
                     parser->pending_crlf)
-                    rb_str_cat(parser->output, parser->line_ending->ptr, parser->line_ending->len);
+                    str_append(parser->output, parser->line_ending->ptr, parser->line_ending->len);
 
                 // close any open scopes on hitting EOF
                 _Wikitext_rollback_failed_link(parser); // if any
@@ -2588,5 +2584,6 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
         token = NULL;
     } while (1);
 return_output:
-    return parser->output;
+    // TODO: make this fast
+    return string_from_str(parser->output);
 }
index a8f7d31b0c50bf941eb670ffb42f82e8faebce90..ca6395e0a1d35ace8e98717699f8b6971fcf5043 100644 (file)
--- a/ext/str.c
+++ b/ext/str.c
@@ -41,7 +41,7 @@ str_t *str_new_size(long len)
     return str;
 }
 
-str_t *str_new_copy(char *src, long len)
+str_t *str_new_copy(const char *src, long len)
 {
     str_t *str      = ALLOC_N(str_t, 1);
     str->ptr        = ALLOC_N(char, len);
@@ -83,7 +83,11 @@ void str_grow(str_t *str, long len)
     }
 }
 
-void str_append(str_t *str, char *src, long len)
+// try overallocating once we've switched more things over to str_t
+// ie. allocate 32 bytes too much, 64 bytes etc
+// and run benchmarks to see if it runs faster
+// and other tests to count the number of allocations
+void str_append(str_t *str, const char *src, long len)
 {
     long new_len = str->len + len;
     if (str->capacity < new_len)
@@ -103,7 +107,7 @@ void str_append_str(str_t *str, str_t *other)
     str_append(str, other->ptr, other->len);
 }
 
-void str_append_rb_str(str_t *str, VALUE other)
+void str_append_string(str_t *str, VALUE other)
 {
     str_append(str, RSTRING_PTR(other), RSTRING_LEN(other));
 }
index 5ae7d782f24f2ff8fee11145e4c7b3236506aad5..1f9d899d6c43f98bac8e066a46ef9b1f2991d83d 100644 (file)
--- a/ext/str.h
+++ b/ext/str.h
@@ -41,7 +41,7 @@ str_t *str_new(void);
 str_t *str_new_size(long len);
 
 // create a new string struct and initialize it with a copy of the buffer of length len pointed to by src
-str_t *str_new_copy(char *src, long len);
+str_t *str_new_copy(const char *src, long len);
 
 // create a new string struct and initialize it with the buffer of length len pointed to by src
 // no copy is made; the struct takes ownership of the buffer and will free it when the struct is disposed of
@@ -56,13 +56,13 @@ VALUE string_from_str(str_t *str);
 // grows a string's capacity to the specified length
 void str_grow(str_t *str, long len);
 
-void str_append(str_t *str, char *src, long len);
+void str_append(str_t *str, const char *src, long len);
 
 // appends the "other" string struct onto str
 void str_append_str(str_t *str, str_t *other);
 
 // appends the "other" string (a Ruby String) onto str
-void str_append_rb_str(str_t *str, VALUE other)
+void str_append_string(str_t *str, VALUE other);
 
 // this is a temporary convenience measure
 // later on if I develop in-place variants of some functions this won't be needed