1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
diff --git a/lib/haml/engine.rb b/lib/haml/engine.rb
index dd4c7a2..d491d77 100644
--- a/lib/haml/engine.rb
+++ b/lib/haml/engine.rb
@@ -37,7 +37,7 @@ module Haml
       @options = {
         :suppress_eval => false,
         :attr_wrapper => "'",
-        :autoclose => ['meta', 'img', 'link', 'br', 'hr', 'input', 'area'],
+        :autoclose => %w(meta img link br hr input area param col base),
         :filters => {
           'sass' => Haml::Filters::Sass,
           'plain' => Haml::Filters::Plain,
@@ -47,7 +47,8 @@ module Haml
           'textile' => Haml::Filters::Textile,
           'markdown' => Haml::Filters::Markdown },
         :filename => '(haml)',
-        :ugly => false
+        :ugly => false,
+        :html4 => false
       }
       @options.rec_merge! options
 
diff --git a/lib/haml/precompiler.rb b/lib/haml/precompiler.rb
index e79438e..922e434 100644
--- a/lib/haml/precompiler.rb
+++ b/lib/haml/precompiler.rb
@@ -473,8 +473,9 @@ END
       result.compact.sort.join
     end
 
-    def prerender_tag(name, atomic, attributes)
-      "<#{name}#{Precompiler.build_attributes(@options[:attr_wrapper], attributes)}#{atomic ? ' />' : '>'}"
+    def prerender_tag(name, self_close, attributes)
+      attributes_string = Precompiler.build_attributes(@options[:attr_wrapper], attributes)
+      "<#{name}#{attributes_string}#{self_close && !@options[:html4] ? ' /' : ''}>"
     end
     
     # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
@@ -502,7 +503,7 @@ END
       raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
 
       case action
-      when '/'; atomic = true
+      when '/'; atomic = !@options[:html4]
       when '~'; parse = flattened = true
       when '='
         parse = true
@@ -521,12 +522,12 @@ END
       attributes = parse_class_and_id(attributes)
       Buffer.merge_attrs(attributes, static_attributes) if static_attributes
 
-      raise SyntaxError.new("Illegal Nesting: Nesting within an atomic tag is illegal.") if @block_opened && atomic
-      raise SyntaxError.new("Illegal Nesting: Content can't be both given on the same line as %#{tag_name} and nested within it.") if @block_opened && !value.empty?
-      raise SyntaxError.new("Tag has no content.") if parse && value.empty?
-      raise SyntaxError.new("Atomic tags can't have content.") if atomic && !value.empty?
+      raise SyntaxError, "Illegal Nesting: Nesting within an atomic tag is illegal." if @block_opened && atomic
+      raise SyntaxError, "Illegal Nesting: Content can't be both given on the same line as %#{tag_name} and nested within it." if @block_opened && !value.empty?
+      raise SyntaxError, "Tag has no content." if parse && value.empty?
+      raise SyntaxError, "Atomic tags can't have content." if atomic && !value.empty?
 
-      atomic = true if !@block_opened && value.empty? && @options[:autoclose].include?(tag_name)
+      atomic ||= !!( !@block_opened && value.empty? && @options[:autoclose].include?(tag_name) )
       
       if object_ref == "nil" && attributes_hash.nil? && !flattened && (parse || Buffer.one_liner?(value))
         # This means that we can render the tag directly to text and not process it in the buffer
@@ -600,22 +601,32 @@ END
     def text_for_doctype(text)
       text = text[3..-1].lstrip.downcase
       if text[0...3] == "xml"
+        return nil if @options[:html4]
         wrapper = @options[:attr_wrapper]
         return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
       end
 
       version, type = text.scan(DOCTYPE_REGEX)[0]
-      if version == "1.1"
-        return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
-      end
+      
+      unless @options[:html4]
+        if version == "1.1"
+          return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
+        end
 
-      case type
-      when "strict";   return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
-      when "frameset"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
-      else             return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
+        case type
+        when "strict";   return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
+        when "frameset"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
+        else             return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
+        end
+      else
+        case type
+        when "strict";   return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
+        when "frameset"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
+        else             return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
+        end
       end
     end
-
+    
     # Starts a filtered block.
     def start_filtered(name)
       raise SyntaxError.new('Filters must have nested text.') unless @block_opened
diff --git a/test/haml/engine_test.rb b/test/haml/engine_test.rb
index 7bc9842..5e5644c 100644
--- a/test/haml/engine_test.rb
+++ b/test/haml/engine_test.rb
@@ -394,4 +394,28 @@ class EngineTest < Test::Unit::TestCase
     assert_equal("<p>#{'s' * 75}</p>\n",
                  render("%p= 's' * 75", :ugly => true))
   end
+
+  # HTML 4.0
+
+  def test_html_has_no_self_closing_tags
+    assert_equal "<p>\n  <br>\n</p>\n", render("%p\n  %br", :html4 => :true)
+    assert_equal "<br>\n", render("%br/", :html4 => :true)
+  end
+
+  def test_html_renders_empty_node_with_closing_tag
+    assert_equal %{<div class='foo'>\n</div>\n}, render(".foo", :html4 => :true)
+  end
+
+  def test_html_ignores_explicit_self_closing_declaration
+    assert_equal "<a>\n</a>\n", render("%a/", :html4 => :true)
+  end
+
+  def test_html_ignores_xml_prolog_declaration
+    assert_equal "\n", render('!!! XML', :html4 => :true)
+  end
+
+  def test_html_has_different_doctype
+    assert_equal %{<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n},
+      render('!!!', :html4 => :true)
+  end
 end