sablon 0.0.21 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -3
  3. data/Gemfile.lock +9 -9
  4. data/README.md +120 -11
  5. data/lib/sablon.rb +7 -1
  6. data/lib/sablon/configuration/configuration.rb +165 -0
  7. data/lib/sablon/configuration/html_tag.rb +99 -0
  8. data/lib/sablon/content.rb +12 -9
  9. data/lib/sablon/context.rb +27 -20
  10. data/lib/sablon/environment.rb +31 -0
  11. data/lib/sablon/html/ast.rb +290 -75
  12. data/lib/sablon/html/ast_builder.rb +90 -0
  13. data/lib/sablon/html/converter.rb +3 -123
  14. data/lib/sablon/numbering.rb +0 -5
  15. data/lib/sablon/operations.rb +11 -11
  16. data/lib/sablon/parser/mail_merge.rb +7 -6
  17. data/lib/sablon/processor/document.rb +9 -9
  18. data/lib/sablon/processor/numbering.rb +4 -4
  19. data/lib/sablon/template.rb +5 -4
  20. data/lib/sablon/version.rb +1 -1
  21. data/sablon.gemspec +3 -3
  22. data/test/configuration_test.rb +122 -0
  23. data/test/content_test.rb +7 -6
  24. data/test/context_test.rb +11 -11
  25. data/test/environment_test.rb +27 -0
  26. data/test/expression_test.rb +2 -2
  27. data/test/fixtures/html/html_test_content.html +174 -0
  28. data/test/fixtures/html_sample.docx +0 -0
  29. data/test/fixtures/xml/comment_block_and_comment_as_key.xml +31 -0
  30. data/test/html/ast_builder_test.rb +65 -0
  31. data/test/html/ast_test.rb +117 -0
  32. data/test/html/converter_test.rb +386 -87
  33. data/test/html/node_properties_test.rb +113 -0
  34. data/test/html_test.rb +10 -10
  35. data/test/mail_merge_parser_test.rb +3 -2
  36. data/test/processor/document_test.rb +20 -2
  37. data/test/section_properties_test.rb +1 -1
  38. data/test/support/html_snippets.rb +9 -0
  39. data/test/test_helper.rb +0 -1
  40. metadata +27 -7
@@ -1,3 +1,3 @@
1
1
  module Sablon
2
- VERSION = "0.0.21"
2
+ VERSION = "0.0.22"
3
3
  end
data/sablon.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Sablon::VERSION
9
9
  spec.authors = ["Yves Senn"]
10
10
  spec.email = ["yves.senn@gmail.com"]
11
- spec.summary = %q{docx tempalte processor}
11
+ spec.summary = %q{docx template processor}
12
12
  spec.description = %q{Sablon is a document template processor. At this time it works only with docx and MailMerge fields.}
13
13
  spec.homepage = "http://github.com/senny/sablon"
14
14
  spec.license = "MIT"
@@ -20,10 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_runtime_dependency 'nokogiri', ">= 1.6.0"
23
- spec.add_runtime_dependency 'rubyzip', ">= 1.1"
23
+ spec.add_runtime_dependency 'rubyzip', ">= 1.1.1"
24
24
 
25
25
  spec.add_development_dependency "bundler", ">= 1.6"
26
- spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rake", "~> 12.0"
27
27
  spec.add_development_dependency "minitest", "~> 5.4"
28
28
  spec.add_development_dependency "xml-simple"
29
29
  end
@@ -0,0 +1,122 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "test_helper"
3
+
4
+ class ConfigurationTest < Sablon::TestCase
5
+ def setup
6
+ super
7
+ @config = Sablon::Configuration.send(:new)
8
+ end
9
+
10
+ def test_register_tag
11
+ options = {
12
+ ast_class: :paragraph,
13
+ attributes: { dummy: 'value' },
14
+ properties: { pstyle: 'ListBullet' },
15
+ allowed_children: %i[_inline ol ul li]
16
+ }
17
+ # test initialization without type
18
+ tag = @config.register_html_tag(:test_tag, **options)
19
+ assert_equal @config.permitted_html_tags[:test_tag], tag
20
+ assert_equal tag.name, :test_tag
21
+ assert_equal tag.type, :inline
22
+ assert_equal tag.ast_class, Sablon::HTMLConverter::Paragraph
23
+ assert_equal tag.attributes, dummy: 'value'
24
+ assert_equal tag.properties, pstyle: 'ListBullet'
25
+ assert_equal tag.allowed_children, %i[_inline ol ul li]
26
+
27
+ # test initialization with type
28
+ tag = @config.register_html_tag('test_tag2', :block, **options)
29
+ assert_equal @config.permitted_html_tags[:test_tag2], tag
30
+ assert_equal tag.name, :test_tag2
31
+ assert_equal tag.type, :block
32
+ end
33
+
34
+ def test_remove_tag
35
+ tag = @config.register_html_tag(:test)
36
+ assert_equal @config.remove_html_tag(:test), tag
37
+ assert_nil @config.permitted_html_tags[:test]
38
+ end
39
+
40
+ def test_register_style_converter_on_existing_ast_class
41
+ converter = ->(v) { return "test-attr-#{v}" }
42
+ @config.register_style_converter(:run, 'my-test-attr', converter)
43
+ #
44
+ assert @config.defined_style_conversions[:run]['my-test-attr'], 'converter should be stored in hash'
45
+ assert_equal 'test-attr-123', @config.defined_style_conversions[:run]['my-test-attr'].call(123)
46
+ end
47
+
48
+ def test_register_style_converter_on_newast_class
49
+ converter = ->(v) { return "test-attr-#{v}" }
50
+ @config.register_style_converter(:unset_ast_class, 'my-test-attr', converter)
51
+ #
52
+ assert @config.defined_style_conversions[:unset_ast_class]['my-test-attr'], 'converter should be stored in hash'
53
+ end
54
+
55
+ def test_remove_style_converter
56
+ converter = ->(v) { return "test-attr-#{v}" }
57
+ converter = @config.register_style_converter(:run, 'my-test-attr', converter)
58
+ #
59
+ assert_equal converter, @config.remove_style_converter(:run, 'my-test-attr')
60
+ assert_nil @config.defined_style_conversions[:run]['my-test-attr']
61
+ end
62
+ end
63
+
64
+ class ConfigurationHTMLTagTest < Sablon::TestCase
65
+ # test basic instantiation of an HTMLTag
66
+ def test_html_tag_defaults
67
+ tag = Sablon::Configuration::HTMLTag.new(:a, :inline)
68
+ assert_equal tag.name, :a
69
+ assert_equal tag.type, :inline
70
+ assert_nil tag.ast_class
71
+ assert_equal tag.attributes, {}
72
+ assert_equal tag.properties, {}
73
+ assert_equal tag.allowed_children, %i[_inline ol ul]
74
+ end
75
+
76
+ # Exercising more of the logic used to conform args into valid
77
+ def test_html_tag_full_init
78
+ args = ['a', 'inline', ast_class: Sablon::HTMLConverter::Run]
79
+ tag = Sablon::Configuration::HTMLTag.new(*args)
80
+ assert_equal tag.name, :a
81
+ assert_equal tag.type, :inline
82
+ assert_equal tag.ast_class, Sablon::HTMLConverter::Run
83
+ #
84
+ options = {
85
+ ast_class: :run,
86
+ attributes: { dummy: 'value1' },
87
+ properties: { dummy2: 'value2' },
88
+ allowed_children: 'text'
89
+ }
90
+ tag = Sablon::Configuration::HTMLTag.new('a', 'inline', **options)
91
+ #
92
+ assert_equal tag.name, :a
93
+ assert_equal tag.type, :inline
94
+ assert_equal tag.ast_class, Sablon::HTMLConverter::Run
95
+ assert_equal tag.attributes, dummy: 'value1'
96
+ assert_equal tag.properties, dummy2: 'value2'
97
+ assert_equal tag.allowed_children, [:text]
98
+ end
99
+
100
+ def test_html_tag_init_block_without_class
101
+ e = assert_raises ArgumentError do
102
+ Sablon::Configuration::HTMLTag.new(:form, :block)
103
+ end
104
+ assert_equal "Block level tag form must have an AST class.", e.message
105
+ end
106
+
107
+ def test_html_tag_allowed_children
108
+ # define different tags for testing
109
+ text = Sablon::Configuration::HTMLTag.new(:text, :inline)
110
+ div = Sablon::Configuration::HTMLTag.new(:div, :block, ast_class: :paragraph)
111
+ olist = Sablon::Configuration::HTMLTag.new(:ol, :block, ast_class: :paragraph, allowed_children: %i[_block])
112
+
113
+ # test default allowances
114
+ assert div.allowed_child?(text) # all inline elements allowed
115
+ assert div.allowed_child?(olist) # tag name is included even though it is bock leve
116
+ assert_equal div.allowed_child?(div), false # other block elms are not allowed
117
+
118
+ # test olist with allowances for all blocks but no inline
119
+ assert olist.allowed_child?(div) # all block elements allowed
120
+ assert_equal olist.allowed_child?(text), false # no inline elements
121
+ end
122
+ end
data/test/content_test.rb CHANGED
@@ -76,6 +76,7 @@ module ContentTestSetup
76
76
  @document = Nokogiri::XML.fragment(@template_text)
77
77
  @paragraph = @document.children.first
78
78
  @node = @document.css("span").first
79
+ @env = Sablon::Environment.new(nil)
79
80
  end
80
81
 
81
82
  private
@@ -88,7 +89,7 @@ class ContentStringTest < Sablon::TestCase
88
89
  include ContentTestSetup
89
90
 
90
91
  def test_single_line_string
91
- Sablon.content(:string, "a normal string").append_to @paragraph, @node
92
+ Sablon.content(:string, "a normal string").append_to @paragraph, @node, @env
92
93
 
93
94
  output = <<-XML.strip
94
95
  <w:p><span>template</span><span>a normal string</span></w:p><w:p>AFTER</w:p>
@@ -97,7 +98,7 @@ class ContentStringTest < Sablon::TestCase
97
98
  end
98
99
 
99
100
  def test_numeric_string
100
- Sablon.content(:string, 42).append_to @paragraph, @node
101
+ Sablon.content(:string, 42).append_to @paragraph, @node, @env
101
102
 
102
103
  output = <<-XML.strip
103
104
  <w:p><span>template</span><span>42</span></w:p><w:p>AFTER</w:p>
@@ -106,7 +107,7 @@ class ContentStringTest < Sablon::TestCase
106
107
  end
107
108
 
108
109
  def test_string_with_newlines
109
- Sablon.content(:string, "a\nmultiline\n\nstring").append_to @paragraph, @node
110
+ Sablon.content(:string, "a\nmultiline\n\nstring").append_to @paragraph, @node, @env
110
111
 
111
112
  output = <<-XML.strip.gsub("\n", "")
112
113
  <w:p>
@@ -125,7 +126,7 @@ class ContentStringTest < Sablon::TestCase
125
126
  end
126
127
 
127
128
  def test_blank_string
128
- Sablon.content(:string, "").append_to @paragraph, @node
129
+ Sablon.content(:string, "").append_to @paragraph, @node, @env
129
130
 
130
131
  assert_xml_equal @template_text, @document
131
132
  end
@@ -135,14 +136,14 @@ class ContentWordMLTest < Sablon::TestCase
135
136
  include ContentTestSetup
136
137
 
137
138
  def test_blank_word_ml
138
- Sablon.content(:word_ml, "").append_to @paragraph, @node
139
+ Sablon.content(:word_ml, "").append_to @paragraph, @node, @env
139
140
 
140
141
  assert_xml_equal "<w:p>AFTER</w:p>", @document
141
142
  end
142
143
 
143
144
  def test_inserts_word_ml_into_the_document
144
145
  @word_ml = '<w:p><w:r><w:t xml:space="preserve">a </w:t></w:r></w:p>'
145
- Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node
146
+ Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
146
147
 
147
148
  output = <<-XML.strip.gsub("\n", "")
148
149
  <w:p>
data/test/context_test.rb CHANGED
@@ -1,28 +1,28 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require "test_helper"
3
3
 
4
- class ContextTest < Sablon::TestCase
4
+ class EnvironmentTest < Sablon::TestCase
5
5
  def test_converts_symbol_keys_to_string_keys
6
- transformed = Sablon::Context.transform({a: 1, b: {c: 2, "d" => 3}})
7
- assert_equal({"a"=>1, "b"=>{"c" =>2, "d"=>3}}, transformed)
6
+ context = Sablon::Context.transform_hash(a: 1, b: { c: 2, "d" => 3 })
7
+ assert_equal({ "a" => 1, "b" => { "c" => 2, "d" => 3 } }, context)
8
8
  end
9
9
 
10
10
  def test_recognizes_wordml_keys
11
- transformed = Sablon::Context.transform({"word_ml:mykey" => "<w:p><w:p>", "otherkey" => "<nope>"})
12
- assert_equal({ "mykey"=>Sablon.content(:word_ml, "<w:p><w:p>"),
13
- "otherkey"=>"<nope>"}, transformed)
11
+ context = Sablon::Context.transform_hash("word_ml:mykey" => "<w:p><w:p>", "otherkey" => "<nope>")
12
+ assert_equal({ "mykey" => Sablon.content(:word_ml, "<w:p><w:p>"),
13
+ "otherkey" => "<nope>"}, context)
14
14
  end
15
15
 
16
16
  def test_recognizes_html_keys
17
- transformed = Sablon::Context.transform({"html:mykey" => "**yay**", "otherkey" => "<nope>"})
18
- assert_equal({ "mykey"=>Sablon.content(:html, "**yay**"),
19
- "otherkey"=>"<nope>"}, transformed)
17
+ context = Sablon::Context.transform_hash("html:mykey" => "**yay**", "otherkey" => "<nope>")
18
+ assert_equal({ "mykey" => Sablon.content(:html, "**yay**"),
19
+ "otherkey" => "<nope>"}, context)
20
20
  end
21
21
 
22
22
  def test_does_not_wrap_html_and_wordml_with_nil_value
23
- transformed = Sablon::Context.transform({"html:mykey" => nil, "word_ml:otherkey" => nil, "normalkey" => nil})
23
+ context = Sablon::Context.transform_hash("html:mykey" => nil, "word_ml:otherkey" => nil, "normalkey" => nil)
24
24
  assert_equal({ "mykey" => nil,
25
25
  "otherkey" => nil,
26
- "normalkey" => nil}, transformed)
26
+ "normalkey" => nil}, context)
27
27
  end
28
28
  end
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "test_helper"
3
+
4
+ class EnvironmentTest < Sablon::TestCase
5
+ def test_transforms_internal_hash
6
+ context = Sablon::Context.transform_hash(a: 1, b: { c: 2, "d" => 3 })
7
+ env = Sablon::Environment.new(nil, a: 1, b: { c: 2, "d" => 3 })
8
+ #
9
+ assert_nil env.template
10
+ assert_equal context, env.context
11
+ end
12
+
13
+ def test_alter_context
14
+ # set initial context
15
+ env = Sablon::Environment.new(nil, a: 1, b: { c: 2, "d" => 3 })
16
+ # alter context to change a single key and set a new one
17
+ env2 = env.alter_context(a: "a", e: "new-key")
18
+ assert_equal({ "a" => "a", "b" => { "c" => 2, "d" => 3 }, "e" => "new-key" }, env2.context)
19
+ # check that the old context was not modified
20
+ assert_equal({"a" => 1, "b" => { "c" => 2, "d" => 3 }}, env.context)
21
+ # check that numbering and template are the same references
22
+ assert env.template.equal?(env2.template), "#{env.template} != #{env2.template}"
23
+ assert env.numbering.equal?(env2.numbering), "#{env.numbering} != #{env2.numbering}"
24
+ # check that a new context reference was created
25
+ assert !env.context.equal?(env2.context), "#{env.context} == #{env2.context}"
26
+ end
27
+ end
@@ -55,7 +55,7 @@ class LookupOrMethodCallTest < Sablon::TestCase
55
55
  def test_missing_receiver
56
56
  user = OpenStruct.new(first_name: "Jack")
57
57
  expr = Sablon::Expression.parse("user.address.line_1")
58
- assert_equal nil, expr.evaluate("user" => user)
59
- assert_equal nil, expr.evaluate({})
58
+ assert_nil expr.evaluate("user" => user)
59
+ assert_nil expr.evaluate({})
60
60
  end
61
61
  end
@@ -0,0 +1,174 @@
1
+ <h1>Sablon HTML insertion</h1>
2
+
3
+ <h2>Text</h2>
4
+
5
+ <div>
6
+ Lorem&nbsp;<strong>ipsum</strong>&nbsp;<em>dolor</em>&nbsp;<strong>sit</strong>
7
+ &nbsp;<em>amet</em>,&nbsp;<strong>consectetur adipiscing elit</strong>.
8
+ &nbsp;<em>Suspendisse a tempus turpis</em>. <span>Duis urna <b>justo</b>,
9
+ <i>vehicula</i> vitae ultricies vel, congue at sem.</span> Fusce turpis
10
+ turpis, aliquet id pulvinar aliquam, iaculis non elit. Nulla feugiat
11
+ lectus nulla, in dictum ipsum cursus ac. Quisque at odio neque.
12
+ Sed ac tortor iaculis, bibendum leo ut, malesuada velit. Donec iaculis
13
+ sed urna eget pharetra. <u>Praesent ornare fermentum turpis</u>, placerat
14
+ iaculis urna bibendum vitae. Nunc in quam consequat, tristique tellus in,
15
+ commodo turpis. Curabitur ullamcorper odio purus, lobortis egestas magna
16
+ laoreet vitae. Nunc fringilla velit ante, eu aliquam nisi cursus vitae.
17
+ Suspendisse sit amet dui <s><sup>egestas</sup>, <sub>volutpat</sub></s>
18
+ nisi vel, mattis justo. Nullam pellentesque, ipsum eget blandit pharetra,
19
+ augue elit <sup>aliquam <sub>mauris</sub></sup>, vel mollis nisl augue ut
20
+ <s>ipsum</s>.
21
+ </div>
22
+
23
+ <h2>HTML Entities</h2>
24
+ <div>
25
+ All HTML entities should get passed through to the final doc <br/>
26
+ Less Than: &lt; <br/>
27
+ Ampersand: &amp; <br/>
28
+ Percent: &#37; <br/>
29
+ One Quarter: &frac14; <br/>
30
+ </div>
31
+
32
+
33
+ <h2>Lists</h2>
34
+
35
+ <ol>
36
+ <li>
37
+ Vestibulum&nbsp;
38
+ <ol>
39
+ <li>ante ipsum primis&nbsp;</li>
40
+ </ol>
41
+ </li>
42
+ <li>
43
+ in faucibus orci luctus&nbsp;
44
+ <ol>
45
+ <li>et ultrices posuere cubilia Curae;&nbsp;
46
+ <ol>
47
+ <li>Aliquam vel dolor&nbsp;</li>
48
+ <li>sed sem maximus&nbsp;</li>
49
+ </ol>
50
+ </li>
51
+ <li>
52
+ fermentum in non odio.&nbsp;
53
+ <ol>
54
+ <li>Fusce hendrerit ornare mollis.&nbsp;</li>
55
+ </ol>
56
+ </li>
57
+ <li>Nunc scelerisque nibh nec turpis tempor pulvinar.&nbsp;</li>
58
+ </ol>
59
+ </li>
60
+ <li>Donec eros turpis,&nbsp;</li>
61
+ <li>
62
+ aliquet vel volutpat sit amet,&nbsp;
63
+ <ol>
64
+ <li>semper eu purus.&nbsp;</li>
65
+ <li>
66
+ Proin ac erat nec urna efficitur vulputate.&nbsp;
67
+ <ol>
68
+ <li>Quisque varius convallis ultricies.&nbsp;</li>
69
+ <li>Nullam vel fermentum eros.&nbsp;</li>
70
+ </ol>
71
+ </li>
72
+ </ol>
73
+ </li>
74
+ </ol>
75
+
76
+ <div>
77
+ Pellentesque nulla leo, auctor ornare erat sed, rhoncus congue diam.
78
+ Duis non porttitor nulla, ut eleifend enim. Pellentesque non tempor sem.
79
+ </div>
80
+
81
+ <div>Mauris auctor egestas arcu,&nbsp;</div>
82
+
83
+ <ol>
84
+ <li>id venenatis nibh dignissim id.&nbsp;</li>
85
+ <li>In non placerat metus.&nbsp;</li>
86
+ </ol>
87
+
88
+ <ul>
89
+ <li>Nunc sed consequat metus.&nbsp;</li>
90
+ <li>Nulla consectetur lorem consequat,&nbsp;</li>
91
+ <li>malesuada dui at, lacinia lectus.&nbsp;</li>
92
+ </ul>
93
+
94
+ <ol>
95
+ <li>Aliquam efficitur&nbsp;</li>
96
+ <li>lorem a mauris feugiat,&nbsp;</li>
97
+ <li>at semper eros pellentesque.&nbsp;</li>
98
+ </ol>
99
+
100
+ <div>
101
+ Nunc lacus diam, consectetur ut odio sit amet, placerat pharetra erat.
102
+ Sed commodo ut sem id congue. Sed eget neque elit. Curabitur at erat tortor.
103
+ Maecenas eget sapien vitae est sagittis accumsan et nec orci. Integer
104
+ luctus at nisl eget venenatis. Nunc nunc eros, consectetur at tortor et,
105
+ tristique ultrices elit. Nulla in turpis nibh.
106
+ </div>
107
+
108
+ <ul>
109
+ <li>
110
+ Nam consectetur&nbsp;
111
+ <ul>
112
+ <li>venenatis tempor.&nbsp;</li>
113
+ </ul>
114
+ </li>
115
+ <li>
116
+ Aenean&nbsp;
117
+ <ul>
118
+ <li>blandit
119
+ <ul>
120
+ <li>porttitor massa,&nbsp;
121
+ <ul>
122
+ <li>non efficitur&nbsp;
123
+ <ul>
124
+ <li>metus.&nbsp;</li>
125
+ </ul>
126
+ </li>
127
+ </ul>
128
+ </li>
129
+ </ul>
130
+ </li>
131
+ </ul>
132
+ </li>
133
+ <li>Duis faucibus nunc nec venenatis faucibus.&nbsp;</li>
134
+ <li>Aliquam erat volutpat.&nbsp;</li>
135
+ </ul>
136
+ <div style="border: 5px double #FF00FF">
137
+ <strong>Quisque non neque ut lacus eleifend volutpat quis sed lacus.
138
+ <br />Praesent ultrices purus eu quam elementum, sit amet faucibus elit
139
+ interdum. In lectus orci,<br /> elementum quis dictum ac, porta ac ante.
140
+ Fusce tempus ac mauris id cursus. Phasellus a erat nulla. <em>Mauris dolor orci</em>,
141
+ malesuada auctor dignissim non, <u>posuere nec odio</u>. Etiam hendrerit
142
+ justo nec diam ullamcorper, nec blandit elit sodales.</strong>
143
+ </div>
144
+
145
+
146
+ <div style="text-align: both; background-color: #EAFEDA; vertical-align: top">
147
+ <span style="font-size: 24;">U</span>t eget auctor enim.
148
+ <span style="text-decoration: underline wavyDouble #123456">Quisque id
149
+ neque eu nibh feugiat <b style="text-decoration: line-through">imperdiet</b>
150
+ id ut dui.</span> Ut auctor libero eget <em style="text-decoration: emboss; color: #F3AADE">
151
+ massa tristique pharetra</em>. Cras tincidunt finibus sapien, ut maximus
152
+ tortor tempor at. <span style="background-color: #00FF00">Proin pulvinar
153
+ pretium</span> justo vitae malesuada. Suspendisse porta purus eget tortor
154
+ tincidunt vestibulum. Maecenas id egestas purus, quis vulputate
155
+ lacus. Quisque <span style="vertical-align: superscript">non</span>
156
+ <span style="vertical-align: subscript">eleifend</span> est.
157
+ </div>
158
+
159
+ <ul style="background-color: #F19F42">
160
+ <li>Item 1</li>
161
+ <li>Item 2</li>
162
+ <ul>
163
+ <li>Nested 1</li>
164
+ <li style="background-color: #FFAAAA">
165
+ Nested 2
166
+ <ul>
167
+ <li>Nested 2.1</li>
168
+ <li><span style="font-style: italic; font-weight: bold">Nested</span> 2.2</li>
169
+ <li>Nested 2.3</li>
170
+ </ul>
171
+ </li>
172
+ </ul>
173
+ <li>Item 3</li>
174
+ </ul>