sablon 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>