]> git.wincent.com - wikitext.git/commitdiff
Add "base_heading_level" option
authorWincent Colaiuta <win@wincent.com>
Mon, 23 Feb 2009 11:28:52 +0000 (12:28 +0100)
committerWincent Colaiuta <win@wincent.com>
Mon, 23 Feb 2009 11:28:52 +0000 (12:28 +0100)
An integer between 0 and 6 denoting the current "heading level".
This can be used to inform the parser of the "context" in which
it is translating markup.

For example, the parser might be translating blog post excerpts
on a page where there is an "h1" title element for the page itself
and an "h2" title element for each excerpt. In this context it is
useful to set base_heading_level to 2, so that any "top level"
headings in the markup (that is "h1" elements) can be automatically
transformed into "h3" elements so that they appear to be
appropriately "nested" inside the containing page elements.

In this way, markup authors can be freed from thinking about
which header size they should use and just always start from "h1"
for their most general content and work their way down.

An additional benefit is that markup can be used in different
contexts at different levels of nesting and the headings will be
adjusted to suit automatically with no intervention from the
markup author.

Finally, it's worth noting that in contexts where the user input
is not necessarily trusted, this setting can be used to prevent
users from inappropriately employing "h1" tags in deeply-nested
contexts where they would otherwise disturb the visual harmony of
the page.

Signed-off-by: Wincent Colaiuta <win@wincent.com>
doc/RELEASE-NOTES
doc/rdoc.rb
ext/parser.c
ext/token.c
ext/token.h
ext/wikitext.c
spec/base_heading_level_spec.rb [new file with mode: 0644]

index 4d91f554c3c0555905368f73ff661a0696c70b98..4bb6007c81c9304f94dab64ffbbf435691513632 100644 (file)
@@ -1,3 +1,7 @@
+= Changes in 1.5.0
+
+* added +base_heading_level+ parser option
+
 = Changes in 1.4.1
 
 * include necessary file that was missing from 1.4.0 gem
index 5c06082b3954de9a56c08f62c1941ca6a7660f9b..f11211ff8d3cc657b6fe897b1212c6192a511040 100644 (file)
@@ -34,7 +34,7 @@ module Wikitext
   # == +internal_link_prefix+ (String)
   #
   # The prefix to be prepended to internal links (defaults to "/wiki/").
-  # For example, given an internal_link_prefix of "/wiki/", the internal
+  # For example, given an +internal_link_prefix+ of "/wiki/", the internal
   # link:
   #     [[Apple]]
   # would be transformed into:
@@ -43,7 +43,7 @@ module Wikitext
   # == +external_link_class+ (String)
   #
   # The CSS class to be applied to external links (defaults to "external").
-  # For example, given an external_link_class of "external", the external
+  # For example, given an +external_link_class+ of "external", the external
   # link:
   #     [http://www.google.com/ the best search engine]
   # would be transformed into:
@@ -99,6 +99,35 @@ module Wikitext
   # "foo bar"; it is therefore recommended that you explicitly disallow
   # underscores in titles at the application level so as to avoid this kind of
   # confusion.
+  #
+  # == +base_heading_level+ (integer)
+  #
+  # An integer between 0 and 6 denoting the current "heading level".
+  # This can be used to inform the parser of the "context" in which
+  # it is translating markup.
+  #
+  # For example, the parser might be translating blog post excerpts
+  # on a page where there is an "h1" title element for the page itself
+  # and an "h2" title element for each excerpt. In this context it is
+  # useful to set +base_heading_level+ to 2, so that any "top level"
+  # headings in the markup (that is "h1" elements) can be automatically
+  # transformed into "h3" elements so that they appear to be
+  # appropriately "nested" inside the containing page elements.
+  #
+  # In this way, markup authors can be freed from thinking about
+  # which header size they should use and just always start from "h1"
+  # for their most general content and work their way down.
+  #
+  # An additional benefit is that markup can be used in different
+  # contexts at different levels of nesting and the headings will be
+  # adjusted to suit automatically with no intervention from the
+  # markup author.
+  #
+  # Finally, it's worth noting that in contexts where the user input
+  # is not necessarily trusted, this setting can be used to prevent
+  # users from inappropriately employing "h1" tags in deeply-nested
+  # contexts where they would otherwise disturb the visual harmony of
+  # the page.
   class Parser
 
     # Sanitizes an internal link target for inclusion within the HTML
index fd586184ad49c5ab11d19159a9ec064866a6df38..290635ae3e02100a8f3c710f49139a5f231a799b 100644 (file)
@@ -42,6 +42,7 @@ typedef struct
     int     base_indent;            // controlled by the :indent option to Wikitext::Parser#parse
     int     current_indent;         // fluctuates according to currently nested structures
     str_t   *tabulation;            // caching buffer for emitting indentation
+    int     base_heading_level;
 } parser_t;
 
 const char escaped_no_wiki_start[]      = "&lt;nowiki&gt;";
@@ -289,6 +290,16 @@ void _Wikitext_pop_from_stack(parser_t *parser, VALUE target)
         return;
     if (NIL_P(target))
         target = parser->output;
+
+    // for headings, take base_heading_level into account
+    if (top >= H1_START && top <= H6_START)
+    {
+        top += parser->base_heading_level;
+        // no need to check for underflow (base_heading_level is never negative)
+        if (top > H6_START)
+            top = H6_START;
+    }
+
     switch (top)
     {
         case PRE:
@@ -883,6 +894,7 @@ VALUE Wikitext_parser_initialize(int argc, VALUE *argv, VALUE self)
     VALUE img_prefix                    = rb_str_new2("/images/");
     VALUE space_to_underscore           = Qtrue;
     VALUE minimum_fulltext_token_length = INT2NUM(3);
+    VALUE base_heading_level            = INT2NUM(0);
 
     // process options hash (override defaults)
     if (!NIL_P(options) && TYPE(options) == T_HASH)
@@ -897,6 +909,7 @@ VALUE Wikitext_parser_initialize(int argc, VALUE *argv, VALUE self)
         img_prefix                      = OVERRIDE_IF_SET(img_prefix);
         space_to_underscore             = OVERRIDE_IF_SET(space_to_underscore);
         minimum_fulltext_token_length   = OVERRIDE_IF_SET(minimum_fulltext_token_length);
+        base_heading_level              = OVERRIDE_IF_SET(base_heading_level);
     }
 
     // no need to call super here; rb_call_super()
@@ -908,6 +921,7 @@ VALUE Wikitext_parser_initialize(int argc, VALUE *argv, VALUE self)
     rb_iv_set(self, "@img_prefix",                      img_prefix);
     rb_iv_set(self, "@space_to_underscore",             space_to_underscore);
     rb_iv_set(self, "@minimum_fulltext_token_length",   minimum_fulltext_token_length);
+    rb_iv_set(self, "@base_heading_level",              base_heading_level);
     return self;
 }
 
@@ -930,18 +944,28 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
 
     // process options hash
     int base_indent = 0;
-    VALUE indent = Qnil;
+    int base_heading_level = NUM2INT(rb_iv_get(self, "@base_heading_level"));
     if (!NIL_P(options) && TYPE(options) == T_HASH)
     {
+        // :indent => 0 (or more)
         if (rb_funcall(options, rb_intern("has_key?"), 1, ID2SYM(rb_intern("indent"))) == Qtrue)
         {
-            indent = rb_hash_aref(options, ID2SYM(rb_intern("indent")));
-            base_indent = NUM2INT(indent);
+            base_indent = NUM2INT(rb_hash_aref(options, ID2SYM(rb_intern("indent"))));
             if (base_indent < 0)
                 base_indent = 0;
         }
+
+        // :base_heading_level => 0/1/2/3/4/5/6
+        if (rb_funcall(options, rb_intern("has_key?"), 1, ID2SYM(rb_intern("base_heading_level"))) == Qtrue)
+            base_heading_level = NUM2INT(rb_hash_aref(options, ID2SYM(rb_intern("base_heading_level"))));
     }
 
+    // normalize, regardless of whether this came from instance variable or override
+    if (base_heading_level < 0)
+        base_heading_level = 0;
+    if (base_heading_level > 6)
+        base_heading_level = 6;
+
     // set up scanner
     char *p = RSTRING_PTR(string);
     long len = RSTRING_LEN(string);
@@ -981,6 +1005,7 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
     parser->current_indent          = 0;
     parser->tabulation              = str_new();
     GC_WRAP_STR(parser->tabulation, tabulation_gc);
+    parser->base_heading_level      = base_heading_level;
 
     // this simple looping design leads to a single enormous function,
     // but it's faster than doing actual recursive descent and also secure in the face of
@@ -1716,6 +1741,11 @@ VALUE Wikitext_parser_parse(int argc, VALUE *argv, VALUE self)
                 ary_push(parser->scope, type);
                 _Wikitext_indent(parser);
 
+                // take base_heading_level into account
+                type += base_heading_level;
+                if (type > H6_START) // no need to check for underflow (base_heading_level never negative)
+                    type = H6_START;
+
                 // 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);
index d6986f6dcd80b371601e1c7f23dbf456c4e5a09f..907b186fcc9be193ea0d2f0a7917a8b4b0051f5b 100644 (file)
@@ -49,18 +49,18 @@ VALUE Wikitext_parser_token_types(VALUE self)
     SET_TOKEN_TYPE(TT);
     SET_TOKEN_TYPE(OL);
     SET_TOKEN_TYPE(UL);
-    SET_TOKEN_TYPE(H6_START);
-    SET_TOKEN_TYPE(H5_START);
-    SET_TOKEN_TYPE(H4_START);
-    SET_TOKEN_TYPE(H3_START);
-    SET_TOKEN_TYPE(H2_START);
     SET_TOKEN_TYPE(H1_START);
-    SET_TOKEN_TYPE(H6_END);
-    SET_TOKEN_TYPE(H5_END);
-    SET_TOKEN_TYPE(H4_END);
-    SET_TOKEN_TYPE(H3_END);
-    SET_TOKEN_TYPE(H2_END);
+    SET_TOKEN_TYPE(H2_START);
+    SET_TOKEN_TYPE(H3_START);
+    SET_TOKEN_TYPE(H4_START);
+    SET_TOKEN_TYPE(H5_START);
+    SET_TOKEN_TYPE(H6_START);
     SET_TOKEN_TYPE(H1_END);
+    SET_TOKEN_TYPE(H2_END);
+    SET_TOKEN_TYPE(H3_END);
+    SET_TOKEN_TYPE(H4_END);
+    SET_TOKEN_TYPE(H5_END);
+    SET_TOKEN_TYPE(H6_END);
     SET_TOKEN_TYPE(URI);
     SET_TOKEN_TYPE(MAIL);
     SET_TOKEN_TYPE(PATH);
index 8cc277ef35eef038d34807e54f76fd0455204a15..aba28aedbe404612cc11a01b0b9e584aa231c397 100644 (file)
@@ -55,18 +55,24 @@ enum token_types {
     TT,
     OL,
     UL,
-    H6_START,
-    H5_START,
-    H4_START,
-    H3_START,
-    H2_START,
+
+    // keep these consecutive, and in ascending order
+    // (the arithmetic for the base_heading_level feature assumes this)
     H1_START,
-    H6_END,
-    H5_END,
-    H4_END,
-    H3_END,
-    H2_END,
+    H2_START,
+    H3_START,
+    H4_START,
+    H5_START,
+    H6_START,
+
+    // likewise for the H*_END tokens
     H1_END,
+    H2_END,
+    H3_END,
+    H4_END,
+    H5_END,
+    H6_END,
+
     URI,
     MAIL,
     PATH,
index 7a26942a3a867e6fb420e318a02f98e6ad474b28..28ad6992c93ba1167ab8f4bb5d3ffa41b5a80c7f 100644 (file)
@@ -44,6 +44,7 @@ void Init_wikitext()
     rb_define_attr(cWikitextParser, "autolink", Qtrue, Qtrue);
     rb_define_attr(cWikitextParser, "space_to_underscore", Qtrue, Qtrue);
     rb_define_attr(cWikitextParser, "minimum_fulltext_token_length", Qtrue, Qtrue);
+    rb_define_attr(cWikitextParser, "base_heading_level", Qtrue, Qtrue);
 
     // Wikitext::Parser::Error
     eWikitextParserError = rb_define_class_under(cWikitextParser, "Error", rb_eException);
diff --git a/spec/base_heading_level_spec.rb b/spec/base_heading_level_spec.rb
new file mode 100644 (file)
index 0000000..a28abd7
--- /dev/null
@@ -0,0 +1,334 @@
+#!/usr/bin/env ruby
+# Copyright 2009 Wincent Colaiuta
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+require File.join(File.dirname(__FILE__), 'spec_helper.rb')
+require 'wikitext/parser'
+
+describe Wikitext::Parser, 'base_heading_level' do
+  before do
+    @parser = Wikitext::Parser.new
+  end
+
+  it 'should default to 0' do
+    @parser.base_heading_level.should == 0
+  end
+
+  it 'should be settable after initialization' do
+    @parser.base_heading_level = 4
+    @parser.base_heading_level.should == 4
+  end
+
+  it 'should be overrideable at initialization time' do
+    parser = Wikitext::Parser.new :base_heading_level => 3
+    parser.base_heading_level.should == 3
+  end
+
+  it 'should be overrideable at parse time' do
+    lambda { @parser.parse 'foo', :base_heading_level => 2 }.should_not raise_error
+  end
+
+  it 'should complain if overridden with non-integer value' do
+    lambda { @parser.parse 'foo', :base_heading_level => 'foo' }.should raise_error(TypeError)
+    lambda {
+      @parser.base_heading_level = 'bar'
+      @parser.parse 'foo' # error actually gets raised only at parse time
+    }.should raise_error(TypeError)
+    lambda {
+      parser = Wikitext::Parser.new :base_heading_level => 'baz'
+      parser.parse 'foo'  # error actually gets raised only at parse time
+    }.should raise_error(TypeError)
+  end
+
+  # these tested additionally below
+  it 'should handle negative levels' do
+    @parser.base_heading_level = -1
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    @parser.base_heading_level = -2
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    @parser.base_heading_level = -3
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    @parser.base_heading_level = -4
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    @parser.base_heading_level = -5
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    @parser.base_heading_level = -6
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+  end
+
+  # these tested additionally below
+  it 'should handle oversized levels (greater than six)' do
+    @parser.base_heading_level = 7
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.base_heading_level = 8
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.base_heading_level = 9
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.base_heading_level = 10
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.base_heading_level = 11
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.base_heading_level = 12
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+  end
+
+  # here we use a negative value
+  it 'should treat the three different override methods as equivalent (level: -1)' do
+    @parser.base_heading_level = -1
+    @parser.parse('= hello =').should == "<h1>hello</h1>\n"
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    @parser.parse('=== hello ===').should == "<h3>hello</h3>\n"
+    @parser.parse('==== hello ====').should == "<h4>hello</h4>\n"
+    @parser.parse('===== hello =====').should == "<h5>hello</h5>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => -1).should == "<h1>hello</h1>\n"
+    @parser.parse('== hello ==', :base_heading_level => -1).should == "<h2>hello</h2>\n"
+    @parser.parse('=== hello ===', :base_heading_level => -1).should == "<h3>hello</h3>\n"
+    @parser.parse('==== hello ====', :base_heading_level => -1).should == "<h4>hello</h4>\n"
+    @parser.parse('===== hello =====', :base_heading_level => -1).should == "<h5>hello</h5>\n"
+    @parser.parse('====== hello ======', :base_heading_level => -1).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => -1
+    parser.parse('= hello =').should == "<h1>hello</h1>\n"
+    parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    parser.parse('=== hello ===').should == "<h3>hello</h3>\n"
+    parser.parse('==== hello ====').should == "<h4>hello</h4>\n"
+    parser.parse('===== hello =====').should == "<h5>hello</h5>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  it 'should treat the three different override methods as equivalent (level: 0)' do
+    @parser.base_heading_level = 0
+    @parser.parse('= hello =').should == "<h1>hello</h1>\n"
+    @parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    @parser.parse('=== hello ===').should == "<h3>hello</h3>\n"
+    @parser.parse('==== hello ====').should == "<h4>hello</h4>\n"
+    @parser.parse('===== hello =====').should == "<h5>hello</h5>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 0).should == "<h1>hello</h1>\n"
+    @parser.parse('== hello ==', :base_heading_level => 0).should == "<h2>hello</h2>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 0).should == "<h3>hello</h3>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 0).should == "<h4>hello</h4>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 0).should == "<h5>hello</h5>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 0).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 0
+    parser.parse('= hello =').should == "<h1>hello</h1>\n"
+    parser.parse('== hello ==').should == "<h2>hello</h2>\n"
+    parser.parse('=== hello ===').should == "<h3>hello</h3>\n"
+    parser.parse('==== hello ====').should == "<h4>hello</h4>\n"
+    parser.parse('===== hello =====').should == "<h5>hello</h5>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  it 'should treat the three different override methods as equivalent (level: 1)' do
+    @parser.base_heading_level = 1
+    @parser.parse('= hello =').should == "<h2>hello</h2>\n"
+    @parser.parse('== hello ==').should == "<h3>hello</h3>\n"
+    @parser.parse('=== hello ===').should == "<h4>hello</h4>\n"
+    @parser.parse('==== hello ====').should == "<h5>hello</h5>\n"
+    @parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 1).should == "<h2>hello</h2>\n"
+    @parser.parse('== hello ==', :base_heading_level => 1).should == "<h3>hello</h3>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 1).should == "<h4>hello</h4>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 1).should == "<h5>hello</h5>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 1).should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 1).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 1
+    parser.parse('= hello =').should == "<h2>hello</h2>\n"
+    parser.parse('== hello ==').should == "<h3>hello</h3>\n"
+    parser.parse('=== hello ===').should == "<h4>hello</h4>\n"
+    parser.parse('==== hello ====').should == "<h5>hello</h5>\n"
+    parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  it 'should treat the three different override methods as equivalent (level: 2)' do
+    @parser.base_heading_level = 2
+    @parser.parse('= hello =').should == "<h3>hello</h3>\n"
+    @parser.parse('== hello ==').should == "<h4>hello</h4>\n"
+    @parser.parse('=== hello ===').should == "<h5>hello</h5>\n"
+    @parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 2).should == "<h3>hello</h3>\n"
+    @parser.parse('== hello ==', :base_heading_level => 2).should == "<h4>hello</h4>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 2).should == "<h5>hello</h5>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 2).should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 2).should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 2).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 2
+    parser.parse('= hello =').should == "<h3>hello</h3>\n"
+    parser.parse('== hello ==').should == "<h4>hello</h4>\n"
+    parser.parse('=== hello ===').should == "<h5>hello</h5>\n"
+    parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  it 'should treat the three different override methods as equivalent (level: 3)' do
+    @parser.base_heading_level = 3
+    @parser.parse('= hello =').should == "<h4>hello</h4>\n"
+    @parser.parse('== hello ==').should == "<h5>hello</h5>\n"
+    @parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 3).should == "<h4>hello</h4>\n"
+    @parser.parse('== hello ==', :base_heading_level => 3).should == "<h5>hello</h5>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 3).should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 3).should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 3).should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 3).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 3
+    parser.parse('= hello =').should == "<h4>hello</h4>\n"
+    parser.parse('== hello ==').should == "<h5>hello</h5>\n"
+    parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  it 'should treat the three different override methods as equivalent (level: 4)' do
+    @parser.base_heading_level = 4
+    @parser.parse('= hello =').should == "<h5>hello</h5>\n"
+    @parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 4).should == "<h5>hello</h5>\n"
+    @parser.parse('== hello ==', :base_heading_level => 4).should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 4).should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 4).should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 4).should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 4).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 4
+    parser.parse('= hello =').should == "<h5>hello</h5>\n"
+    parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  it 'should treat the three different override methods as equivalent (level: 5)' do
+    @parser.base_heading_level = 5
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 5).should == "<h6>hello</h6>\n"
+    @parser.parse('== hello ==', :base_heading_level => 5).should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 5).should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 5).should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 5).should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 5).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 5
+    parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  it 'should treat the three different override methods as equivalent (level: 6)' do
+    @parser.base_heading_level = 6
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 6).should == "<h6>hello</h6>\n"
+    @parser.parse('== hello ==', :base_heading_level => 6).should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 6).should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 6).should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 6).should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 6).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 6
+    parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  # here we exceed the limit
+  it 'should treat the three different override methods as equivalent (level: 7)' do
+    @parser.base_heading_level = 7
+    @parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    @parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+
+    @parser.parse('= hello =', :base_heading_level => 7).should == "<h6>hello</h6>\n"
+    @parser.parse('== hello ==', :base_heading_level => 7).should == "<h6>hello</h6>\n"
+    @parser.parse('=== hello ===', :base_heading_level => 7).should == "<h6>hello</h6>\n"
+    @parser.parse('==== hello ====', :base_heading_level => 7).should == "<h6>hello</h6>\n"
+    @parser.parse('===== hello =====', :base_heading_level => 7).should == "<h6>hello</h6>\n"
+    @parser.parse('====== hello ======', :base_heading_level => 7).should == "<h6>hello</h6>\n"
+
+    parser = Wikitext::Parser.new :base_heading_level => 7
+    parser.parse('= hello =').should == "<h6>hello</h6>\n"
+    parser.parse('== hello ==').should == "<h6>hello</h6>\n"
+    parser.parse('=== hello ===').should == "<h6>hello</h6>\n"
+    parser.parse('==== hello ====').should == "<h6>hello</h6>\n"
+    parser.parse('===== hello =====').should == "<h6>hello</h6>\n"
+    parser.parse('====== hello ======').should == "<h6>hello</h6>\n"
+  end
+
+  # for bad markup, we adjust any headings that we output (h2 -> h4),
+  # but we show the bad markup (===) as it was entered (not-adjusted)
+  it 'should show unbalanced markup exactly as it was entered' do
+    @parser.base_heading_level = 2
+
+    # missing trailing =
+    @parser.parse('== hello =').should == "<h4>hello =</h4>\n"
+
+    # excess trailing =
+    @parser.parse('== hello ===').should == "<h4>hello ===</h4>\n"
+  end
+
+  it 'should show markup in <nowiki> spans exactly as it was entered' do
+    @parser.base_heading_level = 3
+    @parser.parse("<nowiki>\n== foo ==\n</nowiki>").should == "<p>\n== foo ==\n</p>\n"
+  end
+
+  it 'should show markup in <pre> blocks exactly as it was entered' do
+    @parser.base_heading_level = 1
+    @parser.parse("<pre>\n== bar ==\n</pre>").should == "<pre>\n== bar ==\n</pre>\n"
+  end
+end