Index: raggle
===================================================================
RCS file: /var/lib/cvs/raggle/raggle,v
retrieving revision 1.139
diff -a -u -r1.139 raggle
--- raggle	10 Jul 2003 12:48:13 -0000	1.139
+++ raggle	23 Jul 2003 11:57:43 -0000
@@ -40,8 +40,12 @@
 $VERSION = '0.2.0'
 
 # As early as possible, ^C and ^\ are common, and dumping a trace is ugly
-trap('INT') { puts "Interrupted"; exit -1 }
-trap('QUIT') { puts "Quit"; exit -1 }
+# On the other hand, dumping trace is very useful when running tests, therefore
+# disable these unless this file is executed.
+if __FILE__ == $0
+  trap('INT') { puts "Interrupted"; exit -1 }
+  trap('QUIT') { puts "Quit"; exit -1 }
+end
 
 # load required modules
 require 'getoptlong'
@@ -279,6 +283,282 @@
   end
 end
 
+module HTML
+  # Tag set defines all tags that the renderer
+  # can handle
+  #
+  # Each tag can modify the context by
+  # defining +:context+ key (see pre).
+  #
+  # Tag's can also have actions which
+  # will be executed sequentially when
+  # the tag occurs in the token stream.
+  # Actions must be defined to occurences
+  # of start tags and end tags separately.
+  class TagSet
+    class Tag
+      def initialize
+	@context = nil
+	@start_actions = []
+	@end_actions = []
+      end
+
+      attr_accessor :context
+      attr_reader :start_actions
+      attr_reader :end_actions
+
+      def start_actions=(*actions)
+	@start_actions = actions.flatten
+      end
+
+      def end_actions=(*actions)
+	@end_actions = actions.flatten
+      end
+    end
+    
+    def initialize
+      @tags = Hash.new
+      yield self
+    end
+
+    def define_tag(*names)
+      yield tag_spec = Tag.new
+      names.each do |name|
+	yield @tags[name] = tag_spec
+      end
+    end
+
+    def defined?(name)
+      @tags.has_key?(name)
+    end
+    
+    def [](name)
+      @tags[name]
+    end
+  end
+
+  TAG_SET = TagSet.new do |tag_set|
+    tag_set.define_tag 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'  do |tag|
+      tag.start_actions = :maybe_new_paragraph
+      tag.end_actions = :new_paragraph
+    end
+
+    tag_set.define_tag 'pre' do |tag|
+      tag.context = :in_pre
+      tag.start_actions = :maybe_new_paragraph
+      tag.end_actions = :new_paragraph
+    end
+
+    tag_set.define_tag 'br', 'br/' do |tag|
+      tag.start_actions = :force_line_break
+    end
+
+    tag_set.define_tag 'a' do |tag|
+      tag.start_actions = :save_href
+      tag.end_actions = :insert_link_ref
+    end
+  end
+
+  def self.render_html(source, width=72)
+    r = Renderer.new(source, width)
+    r.rendered_text
+  end
+  
+  class Renderer
+    def initialize(source, width)
+      @source, @width = source, width
+      @links = []
+      @rendered_text = render
+    end
+
+    attr_reader :rendered_text
+    
+    # Execute actions defined for +tag+ in this
+    # +phase+.
+    #
+    # actions:: actions set
+    # tag::     tag from tag set
+    # phase::   either +:start+ or +:end+
+    # params::  params passed to the actions
+    def call_actions(tag, phase, *params)
+      actions = phase == :start ? tag.start_actions : tag.end_actions
+      actions.each do |action|
+	self.send("action_#{action}", *params)
+      end
+    end
+    
+    # Enter to context defined by tag (if any)
+    #
+    # context:: current context stack
+    # tag::     tag from tag set
+    def context_enter(context, tag)
+      if tag.context
+	context << tag.context
+      end
+    end
+    
+    # Exit from context defined by tag (if any)
+    #
+    # context:: current context stack
+    # tag::     tag from tag set
+    def context_exit(context, tag)
+      if tag.context
+	context.pop
+      end
+    end
+    
+    # Reflow +text+, and append the result to
+    # +lines+
+    #
+    # lines:: line array representing lines on screen
+    # text::  text to be reflown
+    # width:: maximum width of each line on screen
+    def reflow_text(text)
+      cur_line = @lines.pop || ''
+      text.split(/\s+/).each do |word|
+	if cur_line.length + word.length > @width
+	  @lines << cur_line
+	  cur_line = ""
+	end
+	cur_line << " " unless cur_line.empty? || cur_line[-1] == ?\ # fix emacs
+	cur_line << word.chomp
+      end
+      @lines << cur_line
+    end
+    
+    
+    # Renders HTML in +src+ to a screen
+    # with maximum width +width+. Returns
+    # +String+ containing rendered text.
+    #
+    # src::   HTML source
+    # width:: Screen width
+    def render
+      @lines = []
+      @context = []
+      
+      Parser::each_token(@source) do |token, data, attributes|
+	#puts "C: #{context[-1]} T: #{token} D: <#{data}> L: #{lines.inspect}"
+	case token
+	when :TEXT
+	  if @context[-1] == :in_pre
+	    @lines.pop if @lines[-1] == ""
+	    @lines += data.split("\n", -1)
+	  else
+	    reflow_text(data)
+	  end
+	when :START_TAG
+	  @current_attributes = attributes
+	  tag = TAG_SET[data]
+	  next unless tag
+	  context_enter(@context, tag)
+	  call_actions(tag, :start)
+	when :END_TAG
+	  tag = TAG_SET[data]
+	  next unless tag
+	  context_exit(@context, tag)
+	  call_actions(tag, :end)
+	end
+      end
+
+      # trim trailine new lines
+      until @lines[-1] != ''
+	@lines.delete_at(@lines.size - 1)
+      end
+
+      # If there are links, insert the them here
+      unless @links.empty?
+	@lines << ''
+	@lines << 'LINKS:'
+	padding = @links.size / 10 + 1
+	@links.each_with_index do |link, index|
+	  @lines << "%#{padding}d. %s" % [index + 1, link]
+	end
+      end
+      
+      rendered_text = @lines.join("\n") + "\n"
+      $config['unescape_html'] ? rendered_text.unescape_html : rendered_text
+    end
+
+    # Actions used in the tag set
+
+    # Append paragraph break
+    def action_new_paragraph
+      @lines.push(*['', ''])
+    end
+
+    # Append paragraph break, unless there
+    # is one already.
+    def action_maybe_new_paragraph
+      unless @lines[-1] == '' || @lines.empty?
+	@lines.push(*['', ''])
+      end
+    end
+
+    # Force line break.
+    def action_force_line_break
+      @lines << ''
+    end
+
+    # Save href of the current tag (if it exists)
+    def action_save_href
+      @links << @current_attributes['href']
+    end
+
+    # insert reference to the latest link into the
+    # rendered text
+    def action_insert_link_ref
+      @lines[-1] << '[%d]' % @links.size
+    end
+  end
+
+  module Parser
+    NO_ATTRIBUTES = {}.freeze
+    ATTRIBUTE_LIST_RE = /\s*([^>=\s]+)\s*(?:=\s*(?:(?:['"]([^'">]*)['"])|([^'"\s>]+)))?/ #'
+    PARSER_RE = %r!<(/?\w+[^>]*?/?)>|([^<]*)!m
+    
+    # Parses tag's attributes and returns them
+    # in a hash.
+    def self.parse_attributes(source)
+      #puts "SOURCE: ", source
+      attributes = {}
+      source.scan(ATTRIBUTE_LIST_RE) do |name, value, unquoted_value|
+	attributes[name] = value || unquoted_value || ''
+      end
+      attributes
+    end
+    
+    # Parses HTML in +source+ and invokes
+    # block with each token as a paramater.
+    #
+    # Parameters to the block:
+    #  token id   | data        | attributes
+    #  :TEXT      | text        | NO_ATTRIBUTES
+    #  :START_TAG | tag's name  | attributes of current tag
+    #  :END_TAG   | tag's name  | NO_ATTRIBUTES
+    #
+    # source:: HTML source
+    def self.each_token(source)
+      source.scan(PARSER_RE) do |tag, text|
+	#p tag, text
+	if tag
+	  if tag[0] == ?/
+	    yield :END_TAG, tag[1..-1], NO_ATTRIBUTES
+	  else
+	    if tag =~ /\A(\w+)\s*(.*)\z/m
+	      attributes = NO_ATTRIBUTES
+	      attributes = parse_attributes($2) if $2
+	      yield :START_TAG, $1, attributes
+	    end
+	  end
+	else
+	  yield :TEXT, text, NO_ATTRIBUTES unless text == ""
+	end
+      end
+    end
+  end
+end
+
 # Very simple OPML importer/exporter
 module OPML
   # Import OPML file
@@ -969,7 +1249,7 @@
 class TextWindow < Window
   def reflow_string(str)
     w = dimensions[0]
-    str.reflow(w - 4, $config['force_text_wrap'])
+    HTML.render_html(str, w - 4)
   end
        
   def draw_items
@@ -1005,7 +1285,7 @@
       y += 2
       str = item['title'] || ''
       str = str.strip_tags if $config['strip_html_tags']
-      str = str.unescape_html if $config['unescape_html']
+     
       # Offset + 1, so we scroll on the *first* press
       draw(str, 1, y, 'text', true, true, @offset + 1)
       y += (reflow_string(str).lines - @offset)
@@ -2224,7 +2504,7 @@
   'grab_log_mode'         => 'w',
 
   # strip html from item contents?
-  'strip_html_tags'       => true,
+  'strip_html_tags'       => false,
 
   # decode html escape sequences?
   'unescape_html'         => true,
@@ -2675,15 +2955,17 @@
   ],
 }
 
-if $config['diag']
-  begin
+if __FILE__ == $0
+  if $config['diag']
+    begin
+      main
+    rescue => err
+      puts "#{err.message} (#{err.class})"
+      #err.backtrace.collect! { |name| name = "[#{name}]" }
+      #puts err.backtrace.reverse.join(" -> ")
+      err.backtrace.each { |frame| puts frame }
+    end
+  else
     main
-  rescue => err
-    puts "#{err.message} (#{err.class})"
-    #err.backtrace.collect! { |name| name = "[#{name}]" }
-    #puts err.backtrace.reverse.join(" -> ")
-    err.backtrace.each { |frame| puts frame }
   end
-else
-  main
 end
Index: test_html_renderer.rb
===================================================================
RCS file: test_html_renderer.rb
diff -N test_html_renderer.rb
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ test_html_renderer.rb	23 Jul 2003 11:57:45 -0000
@@ -0,0 +1,367 @@
+load "raggle" # require './raggle' doesn't work :(
+require 'test/unit'
+
+class TestEachElement < Test::Unit::TestCase
+  def check_stream(source, tokens)
+    HTML::Parser::each_token(source) do |type, data, attributes|
+      assert !tokens.empty?, "too many tokens found. type: #{type} data: \"#{data}\""
+      token = tokens.shift
+      assert_equal token[0], type, "type"
+      assert_equal token[1], data, "data"
+      assert_equal token[2] || {}, attributes, "attributes"
+    end
+    assert tokens.empty?, "all tokens found: #{tokens.inspect}"
+  end
+  
+  def test_just_text
+    check_stream "foo", [[:TEXT, "foo"]]
+  end
+
+  def test_multiple_lines_of_text_is_just_one_text_token
+    check_stream "foo\nbar\nbaz\n", [[:TEXT, "foo\nbar\nbaz\n"]]
+  end
+  
+  def test_one_start_tag
+    check_stream "<foo>", [[:START_TAG, "foo"]]
+  end
+  
+  def test_two_start_tags
+    check_stream "<foo><bar>", [[:START_TAG, "foo"], [:START_TAG, "bar"]]
+  end
+  
+  def test_start_tags_and_text
+    check_stream "<foo>bar<baz>",
+      [[:START_TAG, "foo"],
+      [:TEXT, "bar"],
+      [:START_TAG, "baz"]]
+  end
+  
+  def test_one_end_tag
+    check_stream "</foo>", [[:END_TAG, "foo"]]
+  end
+  
+  def test_two_end_tags
+    check_stream "</foo></bar>", [[:END_TAG, "foo"], [:END_TAG, "bar"]]
+  end
+  
+  def test_start_and_end_tags
+    check_stream "<foo><bar></bar></foo>",
+      [[:START_TAG, "foo"],
+      [:START_TAG, "bar"],
+      [:END_TAG, "bar"],
+      [:END_TAG, "foo"]]
+    
+    check_stream "<foo></foo><bar></bar>",
+      [[:START_TAG, "foo"],
+      [:END_TAG, "foo"],
+      [:START_TAG, "bar"],
+      [:END_TAG, "bar"]]
+  end
+  
+  def test_text_and_tags
+    check_stream "<foo>bar</foo>",
+      [[:START_TAG, "foo"],
+      [:TEXT, "bar"],
+      [:END_TAG, "foo"]]
+    
+    check_stream "<foo>a<bar>b</bar>c</foo>",
+      [[:START_TAG, "foo"],
+      [:TEXT, "a"],
+      [:START_TAG, "bar"],
+      [:TEXT, "b"],
+      [:END_TAG, "bar"],
+      [:TEXT, "c"],
+      [:END_TAG, "foo"]]
+  end
+  
+  def test_tag_with_attributes
+    check_stream "<foo bar='baz'>", [[:START_TAG, "foo", {'bar' => 'baz'}]]
+    check_stream "<foo bar='baz'>abc", [[:START_TAG, "foo", {'bar' => 'baz'}], [:TEXT, "abc"]]
+  end
+
+  def test_attributes_dont_need_values
+    check_stream '<foo bar>', [[:START_TAG, 'foo', {'bar' => ''}]]
+  end
+
+  def test_attributes_can_use_single_and_double_quotes
+    check_stream '<foo bar="baz">', [[:START_TAG, 'foo', {'bar' => 'baz'}]]
+    check_stream "<foo bar='baz'>", [[:START_TAG, 'foo', {'bar' => 'baz'}]]
+    check_stream "<foo bar=\"baz'>", [[:START_TAG, 'foo', {'bar' => 'baz'}]] # XXX
+  end
+
+  def test_attribites_value_doesnt_need_to_be_quoted
+    check_stream '<foo bar=baz>', [[:START_TAG, 'foo', {'bar' => 'baz'}]]
+  end
+
+  def test_different_kinds_of_attributes_can_be_combined
+    check_stream "<foo bar='baz' quux xyzzy=\"foo\" kala=kukko>",
+      [[:START_TAG, 'foo', {'bar' => 'baz', 'quux' => '', 'xyzzy' => 'foo', 'kala' => 'kukko'}]]
+  end
+
+  def test_tag_can_span_multiple_lines
+    check_stream "<foo \nbar=baz>", [[:START_TAG, 'foo', {'bar' => 'baz'}]]
+    check_stream "<kala\nbar\n=\nbaz\n>", [[:START_TAG, 'kala', {'bar' => 'baz'}]]
+  end
+
+  def test_incomplete_tags_are_not_skipped
+    # incomplete tags are skipped
+    block_was_run = false
+    HTML::Parser::each_token "<fooba" do |type, data|
+      block_was_run = true
+    end
+    assert block_was_run, "incomplete tags should be skipped"
+    
+    block_was_run = false
+    HTML::Parser::each_token "<foobar baz='quux'" do |type, data|
+      block_was_run = true
+    end
+    assert block_was_run, "incomplete tags aren't skipped if the tag has attributes"
+  end
+end
+
+class TestRenderer < Test::Unit::TestCase
+  def test_simple_text
+    assert_equal "foo\n", render("foo")
+    assert_equal "foo bar\n", render("foo\nbar"), "on default, newlines are ignored"
+  end
+  
+  def test_too_long_lines_are_wrapped
+    lines = render("a"*70 + "\nabcdef\n").split("\n")
+    assert_equal "a"*70, lines[0], "first line"
+    assert_equal "abcdef", lines[1], "second line"
+  end
+  
+  def test_split_at_word_boundary
+    lines = render("a"*70 + " abcdef").split("\n")
+    assert_equal "a"*70, lines[0], "first line"
+    assert_equal "abcdef", lines[1], "second line"
+  end
+  
+  def test_split_at_word_and_line_boundary
+    lines = render("a"*70 + "\n" + "a" * 70 + " abcdef").split("\n")
+    assert_equal 3, lines.size, "three lines"
+    assert_equal "a"*70, lines[0], "first line"
+    assert_equal "a"*70, lines[1], "second line"
+    assert_equal "abcdef", lines[2], "third line"
+  end
+  
+  def test_unknown_tags_insert_one_space
+    assert_equal "abc def\n", render("<foo>abc</foo><bar>def</bar>"), "unknown tags are skipped"
+  end
+  
+  def test_unknown_tags_shouldn_insert_two_consecutive_spaces
+    assert_equal "abc def\n", render("abc<foo><bar>def"), "unknown tags are skipped"
+  end
+  
+  def test_unknown_tags_dont_interfere_with_reflow
+    lines = render("a" * 70 + "<footag>" + "b" * 70).split("\n")
+    assert_equal 2, lines.size, "two lines"
+    assert_equal "a" * 70, lines[0], "first line"
+    assert_equal "b" * 70, lines[1], "second line"
+  end
+  
+  def test_p_tag
+    assert_equal "foo\n\nbar\n\n", render("<p>foo</p><p>bar</p>"), "</p> adds \\n"
+  end
+  
+  def test_br_tag
+    assert_equal "a\nb\nc\n", render("a<br/>b<br/>c<br/>"), "<br/> forces linebreak"
+    assert_equal "a\nb\nc\n", render("a<br>b<br>c<br>"), "<br> is recognized too"
+  end
+  
+  def test_pre_tag
+    assert_equal "a\nb \n\n", render("<pre>a\nb \n</pre>"), "<pre> just dumps the text"
+    assert_equal "a\nb \n\nc\n", render("<pre>a\nb </pre>c"), "newline is added unless it is already there"
+  end
+  
+  def test_width_affects_line_wrapping
+    lines = render(("a" * 30 + " ") * 3, 40).split("\n")
+    assert_equal 3, lines.size, "3 lines"
+  end
+end
+
+class TestRenderer < Test::Unit::TestCase
+  def render(text, width=72)
+    HTML::render_html(text, width)
+  end
+  
+  def test_untagged_text_is_rendered_like_text_inside_p
+    assert_equal "foo\n", render("foo")
+    assert_equal "foo bar\n", render("foo\nbar"), "on default, newlines are ignored"
+  end
+  
+  def test_too_long_lines_are_wrapped
+    lines = render("a"*70 + "\nabcdef\n").split("\n")
+    assert_equal "a"*70, lines[0], "first line"
+    assert_equal "abcdef", lines[1], "second line"
+  end
+  
+  def test_split_at_word_boundary
+    lines = render("a"*70 + " abcdef").split("\n")
+    assert_equal "a"*70, lines[0], "first line"
+    assert_equal "abcdef", lines[1], "second line"
+  end
+  
+  def test_split_at_word_and_line_boundary
+    lines = render("a"*70 + "\n" + "a" * 70 + " abcdef").split("\n")
+    assert_equal 3, lines.size, "three lines"
+    assert_equal "a"*70, lines[0], "first line"
+    assert_equal "a"*70, lines[1], "second line"
+    assert_equal "abcdef", lines[2], "third line"
+  end
+  
+  def test_unknown_tags_insert_one_space
+    assert_equal "abc def\n", render("<foo>abc</foo><bar>def</bar>"), "unknown tags are skipped"
+  end
+  
+  def test_unknown_tags_shouldn_insert_two_consecutive_spaces
+    assert_equal "abc def\n", render("abc<foo><bar>def"), "unknown tags are skipped"
+  end
+  
+  def test_unknown_tags_dont_interfere_with_reflow
+    lines = render("a" * 70 + "<footag>" + "b" * 70).split("\n")
+    assert_equal 2, lines.size, "two lines"
+    assert_equal "a" * 70, lines[0], "first line"
+    assert_equal "b" * 70, lines[1], "second line"
+  end
+  
+  def test_tags_are_case_insentive
+    assert_equal render("<P>foo bar<BR></P>"), render("<p>foo bar<br></p>"), "tags are case insensitive"
+  end
+  
+  def test_p_tag
+    assert_equal "foo\n\nbar\n", render("<p>foo</p><p>bar</p>"), "</p> adds \\n. But not at the end of text"
+  end
+  
+  def test_two_consecutive_p_tags_newline_in_the_middle
+    assert_equal "foo\n\nbar\n", render("<p>foo</p>\n<p>bar</p>"), "<p>\n<p>"
+  end
+  
+  def test_p_untagged_text_p_equals_three_paragraphs
+    assert_equal "foo\n\nbar\n\nbaz\n", render("<p>foo</p>bar<p>baz</p>")
+  end
+  
+  def test_p_after_normal_text_adds_empty_line
+    assert_equal "foo bar\n\nfoo\n", render("foo bar<p>foo</p>"), "<p> after normal text"
+  end
+  
+  def test_br_tag
+    assert_equal "a\nb\nc\n", render("a<br/>b<br/>c<br/>"), "<br/> forces linebreak"
+    assert_equal "a\nb\nc\n", render("a<br>b<br>c<br>"), "<br> is recognized too"
+  end
+  
+  def test_pre_tag
+    assert_equal "a\nb \n", render("<pre>a\nb \n</pre>"), "<pre> just dumps the text"
+    assert_equal "a\nb \n\nc\n", render("<pre>a\nb </pre>c"), "newline is added unless it is already there"
+  end
+  
+  def test_pre_after_normal_text_adds_empty_line
+    assert_equal "foo bar\n\nfoo\n", render("foo bar<pre>foo</pre>"), "<pre> after normal text"
+  end
+
+  def test_links_are_rendered_offline
+    assert_equal "foo bar[1] baz\n\nLINKS:\n1. http://example.com\n", render("<p>foo <a href=\"http://example.com\">bar</a> baz</p>")
+  end
+
+  def test_link_references_are_aligned_automagically
+    text = ''
+    10.times { |i| text << "<a href='http://example.com/#{i}'>Link #{i}" }
+    lines = render(text)
+    # if there are more than 9 links, then link refs < 10 are padded with one space
+    assert_match %r{^ 1. http://example.com/0$}, lines, 'references are padded'
+    assert_match %r{^10. http://example.com/9$}, lines, 'no padding needed'
+  end
+  
+  def test_h1_behaves_like_p
+    assert_equal "foo bar\n\nb\n", render("<h1>foo bar</h1>b"), "H1"
+  end
+
+  def test_h2_behaves_like_
+    assert_equal "foo bar\n\nb\n", render("<h2>foo bar</h2>b"), "H2"
+  end
+
+  def test_h3_behaves_like_p
+    assert_equal "foo bar\n\nb\n", render("<h3>foo bar</h3>b"), "H3"
+  end
+
+  def test_h4_behaves_like_p
+    assert_equal "foo bar\n\nb\n", render("<h4>foo bar</h4>b"), "H4"
+  end
+
+  def test_h5_behaves_like_p
+    assert_equal "foo bar\n\nb\n", render("<h5>foo bar</h5>b"), "H5"
+  end
+
+  def test_h6_behaves_like_p
+    assert_equal "foo bar\n\nb\n", render("<h6>foo bar</h6>b"), "H6"
+  end
+
+  def test_width_affects_line_wrapping
+    lines = render(("a" * 30 + " ") * 3, 40).split("\n")
+    assert_equal 3, lines.size, "3 lines"
+  end
+end
+
+class TestTagSet < Test::Unit::TestCase
+  def test_define_tag
+    tags = HTML::TagSet.new do |tag_set|
+      tag_set.define_tag 'foo' do |tag|
+      end
+    end
+
+    assert tags['foo'] != nil, 'tag foo defined'
+  end
+
+  def test_define_multiple_identical_tags
+    tags = HTML::TagSet.new do |tag_set|
+      tag_set.define_tag 'foo', 'bar' do |tag|
+      end
+    end
+
+    assert tags['foo'] != nil, 'tag foo defined'
+    assert tags['bar'] != nil, 'tag bar defined'
+    assert_same tags['foo'], tags['bar'], 'tag foo and bar are identical (same)'
+  end
+
+  def test_define_context
+    tags = HTML::TagSet.new do |tag_set|
+      tag_set.define_tag 'foo' do |tag|
+	tag.context = :in_foo
+      end
+
+      tag_set.define_tag 'bar' do |tag|
+      end
+    end
+
+    assert_equal :in_foo,  tags['foo'].context, 'context defined'
+    assert_equal nil, tags['bar'].context, 'on default context is nil'
+  end
+
+  def test_defining_actions
+    tags = HTML::TagSet.new do |tag_set|
+      tag_set.define_tag 'actions' do |tag|
+	tag.start_actions = :a, :b
+	tag.end_actions = :c, :d
+      end
+
+      tag_set.define_tag 'no_actions' do |tag|
+      end
+    end
+
+    assert_equal [:a, :b], tags['actions'].start_actions, 'start actions'
+    assert_equal [:c, :d], tags['actions'].end_actions, 'end actions'
+    assert_equal [], tags['no_actions'].start_actions, 'on default action list is empty'
+    assert_equal [], tags['no_actions'].end_actions, 'on default actions list is empty'
+  end
+
+  def test_defined?
+    tags = HTML::TagSet.new do |tag_set|
+      tag_set.define_tag 'foo' do |tag|
+      end
+    end
+
+    assert tags.defined?('foo'), 'foo tag defined'
+    assert !tags.defined?('bar'), 'bar tag isnt defined'
+  end
+end
+

