├── .gitignore
├── .rspec
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── lib
├── mobiledoc
│ ├── atoms
│ │ └── unknown.rb
│ ├── cards
│ │ ├── image.rb
│ │ └── unknown.rb
│ ├── error.rb
│ ├── renderers
│ │ ├── 0.2.rb
│ │ └── 0.3.rb
│ └── utils
│ │ ├── marker_types.rb
│ │ ├── section_types.rb
│ │ └── tag_names.rb
├── mobiledoc_html_renderer.rb
└── mobiledoc_html_renderer
│ └── version.rb
├── mobiledoc-html-renderer.gemspec
└── spec
├── 0.2_spec.rb
├── 0.3_spec.rb
├── mobiledoc_html_renderer_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.1.5
4 | before_install: gem install bundler -v 1.10.6
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6 |
7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8 |
9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10 |
11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12 |
13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
14 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in mobiledoc-html-renderer.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Justin Giancola
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mobiledoc HTML Renderer for Ruby
2 |
3 | This is an HTML renderer for the [Mobiledoc format](https://github.com/bustlelabs/mobiledoc-kit/blob/master/MOBILEDOC.md) used by [Mobiledoc-Kit](https://github.com/bustlelabs/mobiledoc-kit).
4 |
5 | To learn more about Mobiledoc cards and renderers, see the **[Mobiledoc Cards docs](https://github.com/bustlelabs/mobiledoc-kit/blob/master/CARDS.md)**
6 |
7 | The implementation is based closely on https://github.com/bustlelabs/mobiledoc-html-renderer (kinda sorta a port to Ruby).
8 |
9 | ## Installation
10 |
11 | Add this line to your application's Gemfile:
12 |
13 | ```ruby
14 | gem 'mobiledoc_html_renderer'
15 | ```
16 |
17 | And then execute:
18 |
19 | $ bundle
20 |
21 | Or install it yourself as:
22 |
23 | $ gem install mobiledoc_html_renderer
24 |
25 | ## Usage
26 |
27 | ```ruby
28 | require 'mobiledoc_html_renderer'
29 |
30 | mobiledoc = {
31 | "version" => "0.2.0",
32 | "sections" => [
33 | [ # markers
34 | ['B']
35 | ],
36 | [ # sections
37 | [1, 'P', [ # array of markups
38 | # markup
39 | [
40 | [0], # open markers (by index)
41 | 0, # close count
42 | 'hello world'
43 | ]
44 | ]
45 | ]
46 | ]
47 | }
48 |
49 | renderer = Mobiledoc::HTMLRenderer.new(cards: [])
50 | renderer.render(mobiledoc) # "
"
51 | ```
52 |
53 | ## Development
54 |
55 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
56 |
57 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
58 |
59 | ## Contributing
60 |
61 | Bug reports and pull requests are welcome on GitHub at https://github.com/elucid/mobiledoc-html-renderer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
62 |
63 |
64 | ## License
65 |
66 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
67 |
68 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task :default => :spec
7 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "mobiledoc/html/renderer"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start
15 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 |
5 | bundle install
6 |
7 | # Do any other automated setup that you need to do here
8 |
--------------------------------------------------------------------------------
/lib/mobiledoc/atoms/unknown.rb:
--------------------------------------------------------------------------------
1 | require "mobiledoc/error"
2 |
3 | module Mobiledoc
4 | module UnknownAtom
5 | module_function
6 |
7 | def type
8 | 'html'
9 | end
10 |
11 | def render(env, value, payload, options)
12 | name = env[:name]
13 |
14 | raise Mobiledoc::Error.new(%Q[Atom "#{name}" not found])
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/mobiledoc/cards/image.rb:
--------------------------------------------------------------------------------
1 | module Mobiledoc
2 | module ImageCard
3 | module_function
4 |
5 | def name
6 | 'image-card'
7 | end
8 |
9 | def type
10 | 'html'
11 | end
12 |
13 | def render(env, payload, options)
14 | if payload['src']
15 | %Q[
]
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/mobiledoc/cards/unknown.rb:
--------------------------------------------------------------------------------
1 | require "mobiledoc/error"
2 |
3 | module Mobiledoc
4 | module UnknownCard
5 | module_function
6 |
7 | def type
8 | 'html'
9 | end
10 |
11 | def render(env, payload, options)
12 | name = env[:name]
13 |
14 | raise Mobiledoc::Error.new(%Q[Card "#{name}" not found])
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/mobiledoc/error.rb:
--------------------------------------------------------------------------------
1 | module Mobiledoc
2 | class Error < StandardError
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/mobiledoc/renderers/0.2.rb:
--------------------------------------------------------------------------------
1 | require "nokogiri"
2 | require "mobiledoc/utils/section_types"
3 | require "mobiledoc/utils/tag_names"
4 | require "mobiledoc/cards/image"
5 | require "mobiledoc/error"
6 |
7 | module Mobiledoc
8 | class Renderer_0_2
9 | MOBILEDOC_VERSIONS = ['0.2.0']
10 |
11 | include Mobiledoc::Utils::SectionTypes
12 | include Mobiledoc::Utils::TagNames
13 |
14 | attr_accessor :root, :marker_types, :sections, :doc, :cards, :card_options, :unknown_card_handler
15 |
16 | def initialize(mobiledoc, state)
17 | version, section_data = *mobiledoc.values_at('version', 'sections')
18 | validate_version(version)
19 |
20 | self.marker_types, self.sections = *section_data
21 |
22 | self.doc = Nokogiri::HTML.fragment('')
23 | self.root = create_document_fragment
24 | self.cards = state[:cards]
25 | self.card_options = state[:card_options]
26 | self.unknown_card_handler = state[:unknown_card_handler]
27 | end
28 |
29 | def validate_version(version)
30 | unless self.class::MOBILEDOC_VERSIONS.to_a.include? version
31 | raise Mobiledoc::Error.new(%Q[Unexpected Mobiledoc version "#{version}"])
32 | end
33 | end
34 |
35 | def render
36 | root = create_document_fragment
37 |
38 | sections.each do |section|
39 | rendered = render_section(section)
40 |
41 | if rendered
42 | append_child(root, rendered)
43 | end
44 | end
45 |
46 | root.to_html(save_with: 0).gsub(' ', ' ')
47 | end
48 |
49 | def create_document_fragment
50 | create_element('div')
51 | end
52 |
53 | def create_element(tag_name)
54 | tag_name = normalize_tag_name(tag_name)
55 | Nokogiri::XML::Node.new(tag_name, doc)
56 | end
57 |
58 | def set_attribute(element, prop_name, prop_value)
59 | element.set_attribute(prop_name, prop_value)
60 | end
61 |
62 | def create_text_node(text)
63 | Nokogiri::XML::Text.new(text, doc)
64 | end
65 |
66 | def create_element_from_marker_type(tag_name='', attributes=[])
67 | element = create_element(tag_name)
68 |
69 | attributes.each_slice(2) do |prop_name, prop_value|
70 | set_attribute(element, prop_name, prop_value)
71 | end
72 |
73 | element
74 | end
75 |
76 | def append_child(target, child)
77 | target.add_child(child)
78 | end
79 |
80 | def render_section(section)
81 | type = section.first
82 | case type
83 | when MARKUP_SECTION_TYPE
84 | render_markup_section(*section)
85 | when IMAGE_SECTION_TYPE
86 | render_image_section(*section)
87 | when LIST_SECTION_TYPE
88 | render_list_section(*section)
89 | when CARD_SECTION_TYPE
90 | render_card_section(*section)
91 | end
92 | end
93 |
94 | def render_markup_section(type, tag_name, markers)
95 | return unless valid_section_tag_name?(tag_name, MARKUP_SECTION_TYPE)
96 |
97 | element = create_element(tag_name)
98 | _render_markers_on_element(element, markers)
99 | element
100 | end
101 |
102 | def render_image_section(type, url)
103 | element = create_element('img')
104 | set_attribute(element, 'src', url)
105 | element
106 | end
107 |
108 | def render_list_section(type, tag_name, items)
109 | return unless valid_section_tag_name?(tag_name, LIST_SECTION_TYPE)
110 |
111 | element = create_element(tag_name)
112 | items.each do |item|
113 | append_child(element, render_list_item(item))
114 | end
115 |
116 | element
117 | end
118 |
119 | def render_list_item(markers)
120 | element = create_element('li')
121 | _render_markers_on_element(element, markers)
122 | element
123 | end
124 |
125 | def render_card_section(type, name, payload={})
126 | card = find_card(name)
127 |
128 | _render_card_section(card, name, payload)
129 | end
130 |
131 | def _render_card_section(card, name, payload)
132 | card_wrapper = _create_card_element
133 | card_arg = _create_card_argument(name, payload)
134 | rendered = card.render(*card_arg)
135 |
136 | _validate_card_render(rendered, card.name)
137 |
138 | if rendered
139 | append_child(card_wrapper, rendered)
140 | end
141 |
142 | card_wrapper
143 | end
144 |
145 | def find_card(name)
146 | card = cards.find { |c| c.name == name }
147 |
148 | case
149 | when card
150 | card
151 | when ImageCard.name == name
152 | ImageCard
153 | else
154 | unknown_card_handler
155 | end
156 | end
157 |
158 | def _create_card_element
159 | create_element('div')
160 | end
161 |
162 | def _create_card_argument(card_name, payload={})
163 | env = {
164 | name: card_name
165 | }
166 |
167 | [ env, payload, card_options ]
168 | end
169 |
170 | def _validate_card_render(rendered, card_name)
171 | return unless rendered
172 |
173 | unless rendered.is_a?(String)
174 | raise Mobiledoc::Error.new(%Q[Card "#{card_name}" must render html, but result was #{rendered.class}"]);
175 | end
176 | end
177 |
178 | def _render_markers_on_element(element, markers)
179 | elements = [element]
180 | current_element = element
181 |
182 | markers.each do |marker|
183 | open_types, close_count, text = *marker
184 |
185 | open_types.each do |open_type|
186 | marker_type = marker_types[open_type]
187 | tag_name = marker_type.first
188 |
189 | if valid_marker_type?(tag_name)
190 | opened_element = create_element_from_marker_type(*marker_type)
191 | append_child(current_element, opened_element)
192 | elements.push(opened_element)
193 | current_element = opened_element
194 | else
195 | close_count -= 1
196 | end
197 | end
198 |
199 | append_child(current_element, create_text_node(text))
200 |
201 | close_count.times do
202 | elements.pop
203 | current_element = elements.last
204 | end
205 | end
206 | end
207 |
208 | def valid_section_tag_name?(tag_name, section_type)
209 | tag_name = normalize_tag_name(tag_name)
210 |
211 | case section_type
212 | when MARKUP_SECTION_TYPE
213 | MARKUP_SECTION_TAG_NAMES.include?(tag_name)
214 | when LIST_SECTION_TYPE
215 | LIST_SECTION_TAG_NAMES.include?(tag_name)
216 | else
217 | raise Mobiledoc::Error.new(%Q[Cannot validate tag_name for unknown section type "#{section_type}"])
218 | end
219 | end
220 |
221 | def valid_marker_type?(type)
222 | type = normalize_tag_name(type)
223 |
224 | MARKUP_TYPES.include?(type)
225 | end
226 | end
227 | end
228 |
--------------------------------------------------------------------------------
/lib/mobiledoc/renderers/0.3.rb:
--------------------------------------------------------------------------------
1 | require "mobiledoc/renderers/0.2"
2 | require 'mobiledoc/utils/marker_types'
3 | require "mobiledoc/error"
4 |
5 | module Mobiledoc
6 | class Renderer_0_3 < Renderer_0_2
7 | MOBILEDOC_VERSIONS = ['0.3.0', '0.3.1', '0.3.2']
8 |
9 | include Mobiledoc::Utils::MarkerTypes
10 |
11 | attr_accessor :atom_types, :card_types, :atoms, :unknown_atom_handler
12 |
13 | def initialize(mobiledoc, state)
14 | version, sections, atom_types, card_types, marker_types = *mobiledoc.values_at('version', 'sections', 'atoms', 'cards', 'markups')
15 | validate_version(version)
16 |
17 | self.doc = Nokogiri::HTML.fragment('')
18 | self.root = create_document_fragment
19 | self.sections = sections
20 | self.atom_types = atom_types
21 | self.card_types = card_types
22 | self.marker_types = marker_types
23 | self.cards = state[:cards]
24 | self.atoms = state[:atoms]
25 | self.card_options = state[:card_options]
26 | self.unknown_card_handler = state[:unknown_card_handler]
27 | self.unknown_atom_handler = state[:unknown_atom_handler]
28 | end
29 |
30 | def render_markup_section(type, tag_name, markers, attributes = [])
31 | return unless valid_section_tag_name?(tag_name, MARKUP_SECTION_TYPE)
32 |
33 | element = create_element(tag_name)
34 | attributes.each_slice(2) do |prop_name, prop_value|
35 | set_attribute(element, prop_name, prop_value)
36 | end
37 | _render_markers_on_element(element, markers)
38 | element
39 | end
40 |
41 | def render_card_section(type, index)
42 | card, name, payload = _find_card_by_index(index)
43 |
44 | _render_card_section(card, name, payload)
45 | end
46 |
47 | def _find_card_by_index(index)
48 | card_type = card_types[index]
49 |
50 | unless card_type
51 | raise Mobiledoc::Error.new("No card definition found at index #{index}")
52 | end
53 |
54 | name, payload = *card_type
55 | card = find_card(name)
56 |
57 | [ card, name, payload ]
58 | end
59 |
60 | def _render_markers_on_element(element, markers)
61 | elements = [element]
62 | current_element = element
63 |
64 | markers.each do |marker|
65 | type, open_types, close_count, value = *marker
66 |
67 | open_types.each do |open_type|
68 | marker_type = marker_types[open_type]
69 | tag_name = marker_type.first
70 |
71 | if valid_marker_type?(tag_name)
72 | opened_element = create_element_from_marker_type(*marker_type)
73 | append_child(current_element, opened_element)
74 | elements.push(opened_element)
75 | current_element = opened_element
76 | else
77 | close_count -= 1
78 | end
79 | end
80 |
81 | case type
82 | when MARKUP_MARKER_TYPE
83 | append_child(current_element, create_text_node(value))
84 | when ATOM_MARKER_TYPE
85 | append_child(current_element, _render_atom(value))
86 | else
87 | raise Mobiledoc::Error.new("Unknown markup type (#{type})");
88 | end
89 |
90 | close_count.times do
91 | elements.pop
92 | current_element = elements.last
93 | end
94 | end
95 | end
96 |
97 | def find_atom(name)
98 | atom = atoms.find { |a| a.name == name }
99 |
100 | atom || unknown_atom_handler
101 | end
102 |
103 | def _render_atom(index)
104 | atom, name, value, payload = _find_atom_by_index(index)
105 | atom_arg = _create_atom_argument(atom, name, value, payload)
106 | rendered = atom.render(*atom_arg)
107 |
108 | _validate_atom_render(rendered, atom.name)
109 |
110 | rendered || create_text_node('')
111 | end
112 |
113 | def _find_atom_by_index(index)
114 | atom_type = atom_types[index]
115 |
116 | unless atom_type
117 | raise Mobiledoc::Error.new("No atom definition found at index #{index}")
118 | end
119 |
120 | name, value, payload = *atom_type
121 | atom = find_atom(name)
122 |
123 | [ atom, name, value, payload ]
124 | end
125 |
126 | def _create_atom_argument(atom, atom_name, value, payload={})
127 | env = {
128 | name: atom_name
129 | }
130 |
131 | [ env, value, payload, card_options ]
132 | end
133 |
134 | def _validate_atom_render(rendered, atom_name)
135 | return unless rendered
136 |
137 | unless rendered.is_a?(String)
138 | raise Mobiledoc::Error.new(%Q[Atom "#{atom_name}" must render html, but result was #{rendered.class}"]);
139 | end
140 | end
141 |
142 | end
143 | end
144 |
--------------------------------------------------------------------------------
/lib/mobiledoc/utils/marker_types.rb:
--------------------------------------------------------------------------------
1 | module Mobiledoc
2 | module Utils
3 | module MarkerTypes
4 | MARKUP_MARKER_TYPE = 0
5 | ATOM_MARKER_TYPE = 1
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/mobiledoc/utils/section_types.rb:
--------------------------------------------------------------------------------
1 | module Mobiledoc
2 | module Utils
3 | module SectionTypes
4 | MARKUP_SECTION_TYPE = 1
5 | IMAGE_SECTION_TYPE = 2
6 | LIST_SECTION_TYPE = 3
7 | CARD_SECTION_TYPE = 10
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/mobiledoc/utils/tag_names.rb:
--------------------------------------------------------------------------------
1 | module Mobiledoc
2 | module Utils
3 | module TagNames
4 | MARKUP_SECTION_TAG_NAMES = [
5 | 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pull-quote', 'aside'
6 | ]
7 |
8 | LIST_SECTION_TAG_NAMES = [
9 | 'ul', 'ol'
10 | ]
11 |
12 | MARKUP_TYPES = [
13 | 'b', 'i', 'strong', 'em', 'a', 'u', 'sub', 'sup', 's', 'code'
14 | ]
15 |
16 | def normalize_tag_name(name)
17 | name.downcase
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/mobiledoc_html_renderer.rb:
--------------------------------------------------------------------------------
1 | require "mobiledoc_html_renderer/version"
2 | require "mobiledoc/renderers/0.2"
3 | require "mobiledoc/renderers/0.3"
4 | require "mobiledoc/cards/unknown"
5 | require "mobiledoc/atoms/unknown"
6 | require "mobiledoc/error"
7 |
8 | module Mobiledoc
9 | class HTMLRenderer
10 | attr_accessor :state
11 |
12 | def initialize(options={})
13 | cards = options[:cards] || []
14 | validate_cards(cards)
15 |
16 | atoms = options[:atoms] || []
17 | validate_atoms(atoms)
18 |
19 | card_options = options[:card_options] || {}
20 |
21 | unknown_card_handler = options[:unknown_card_handler] || UnknownCard
22 | unknown_atom_handler = options[:unknown_atom_handler] || UnknownAtom
23 |
24 | self.state = {
25 | cards: cards,
26 | atoms: atoms,
27 | card_options: card_options,
28 | unknown_card_handler: unknown_card_handler,
29 | unknown_atom_handler: unknown_atom_handler
30 | }
31 | end
32 |
33 | def validate_cards(cards)
34 | unless cards.is_a?(Array)
35 | raise Mobiledoc::Error.new("`cards` must be passed as an array")
36 | end
37 |
38 | cards.each do |card|
39 | unless card.type == 'html'
40 | raise Mobiledoc::Error.new(%Q[Card "#{card.name}" must be of type "html", was "#{card.type}"])
41 | end
42 |
43 | unless card.respond_to?(:render)
44 | raise Mobiledoc::Error.new(%Q[Card "#{card.name}" must define \`render\`])
45 | end
46 | end
47 | end
48 |
49 | def validate_atoms(atoms)
50 | unless atoms.is_a?(Array)
51 | raise Mobiledoc::Error.new("`atoms` must be passed as an array")
52 | end
53 |
54 | atoms.each do |atom|
55 | unless atom.type == 'html'
56 | raise Mobiledoc::Error.new(%Q[Atom "#{atom.name}" must be of type "html", was "#{atom.type}"])
57 | end
58 |
59 | unless atom.respond_to?(:render)
60 | raise Mobiledoc::Error.new(%Q[Atom "#{atom.name}" must define \`render\`])
61 | end
62 | end
63 | end
64 |
65 | def render(mobiledoc)
66 | version = mobiledoc['version']
67 |
68 | case version
69 | when '0.2.0'
70 | Renderer_0_2.new(mobiledoc, state).render
71 | when '0.3.0', '0.3.1', '0.3.2', nil
72 | Renderer_0_3.new(mobiledoc, state).render
73 | else
74 | raise Mobiledoc::Error.new(%Q[Unexpected Mobiledoc version "#{version}"])
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/mobiledoc_html_renderer/version.rb:
--------------------------------------------------------------------------------
1 | module Mobiledoc
2 | class HTMLRenderer
3 | VERSION = "1.0.0"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/mobiledoc-html-renderer.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'mobiledoc_html_renderer/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "mobiledoc-html-renderer"
8 | spec.version = Mobiledoc::HTMLRenderer::VERSION
9 | spec.authors = ["Justin Giancola"]
10 | spec.email = ["justin.giancola@gmail.com"]
11 |
12 | spec.summary = %q{MobileDoc HTML Renderer for Ruby}
13 | spec.homepage = "https://github.com/elucid/mobiledoc-html-renderer"
14 | spec.license = "MIT"
15 |
16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17 | spec.bindir = "exe"
18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19 | spec.require_paths = ["lib"]
20 |
21 | spec.add_development_dependency "bundler", "> 1.10"
22 | spec.add_development_dependency "rake", "~> 10.0"
23 | spec.add_development_dependency "rspec"
24 | spec.add_development_dependency "pry"
25 |
26 | spec.add_dependency "nokogiri", "~> 1.6"
27 | end
28 |
--------------------------------------------------------------------------------
/spec/0.2_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'mobiledoc/utils/section_types'
3 | require 'mobiledoc/cards/image'
4 |
5 | module ZeroTwoZero
6 | include Mobiledoc::Utils::SectionTypes
7 |
8 | MOBILEDOC_VERSION = '0.2.0'
9 |
10 | describe Mobiledoc::HTMLRenderer, "(#{MOBILEDOC_VERSION})" do
11 |
12 | def render(mobiledoc)
13 | described_class.new.render(mobiledoc)
14 | end
15 |
16 | let(:data_uri) { "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" }
17 |
18 | it 'renders an empty mobiledoc' do
19 | mobiledoc = {
20 | 'version' => MOBILEDOC_VERSION,
21 | 'sections' => [
22 | [], # markers
23 | [] # sections
24 | ]
25 | }
26 |
27 | rendered = render(mobiledoc)
28 |
29 | expect(rendered).to eq('')
30 | end
31 |
32 | it 'renders a mobiledoc without markups' do
33 | mobiledoc = {
34 | 'version' => MOBILEDOC_VERSION,
35 | 'sections' => [
36 | [], # markers
37 | [
38 | [MARKUP_SECTION_TYPE, 'P', [
39 | [[], 0, 'hello world']]
40 | ]
41 | ] # sections
42 | ]
43 | }
44 |
45 | rendered = render(mobiledoc)
46 |
47 | expect(rendered).to eq('')
48 | end
49 |
50 | it 'renders a mobiledoc with simple (no attributes) markup' do
51 | mobiledoc = {
52 | 'version' => MOBILEDOC_VERSION,
53 | 'sections' => [
54 | [ # markers
55 | ['B'],
56 | ],
57 | [ # sections
58 | [MARKUP_SECTION_TYPE, 'P', [
59 | [[0], 1, 'hello world']]
60 | ]
61 | ]
62 | ]
63 | }
64 |
65 | rendered = render(mobiledoc)
66 |
67 | expect(rendered).to eq('')
68 | end
69 |
70 | it 'renders a mobiledoc with complex (has attributes) markup' do
71 | mobiledoc = {
72 | 'version' => MOBILEDOC_VERSION,
73 | 'sections' => [
74 | [ # markers
75 | ['A', ['href', 'http://google.com']],
76 | ],
77 | [ # sections
78 | [MARKUP_SECTION_TYPE, 'P', [
79 | [[0], 1, 'hello world']
80 | ]]
81 | ]
82 | ]
83 | }
84 |
85 | rendered = render(mobiledoc)
86 |
87 | expect(rendered).to eq('')
88 | end
89 |
90 | it 'renders a mobiledoc with multiple markups in a section' do
91 | mobiledoc = {
92 | 'version' => MOBILEDOC_VERSION,
93 | 'sections' => [
94 | [ # markers
95 | ['B'],
96 | ['I']
97 | ],
98 | [ # sections
99 | [MARKUP_SECTION_TYPE, 'P', [
100 | [[0], 0, 'hello '], # b
101 | [[1], 0, 'brave '], # b + i
102 | [[], 1, 'new '], # close i
103 | [[], 1, 'world'] # close b
104 | ]]
105 | ]
106 | ]
107 | }
108 |
109 | rendered = render(mobiledoc)
110 |
111 | expect(rendered).to eq('')
112 | end
113 |
114 | it 'renders a mobiledoc with image section' do
115 | mobiledoc = {
116 | 'version' => MOBILEDOC_VERSION,
117 | 'sections' => [
118 | [], # markers
119 | [ # sections
120 | [IMAGE_SECTION_TYPE, data_uri]
121 | ]
122 | ]
123 | }
124 |
125 | rendered = render(mobiledoc)
126 |
127 | expect(rendered).to eq(%Q[])
128 | end
129 |
130 | it 'renders a mobiledoc with built-in image card' do
131 | card_name = Mobiledoc::ImageCard.name
132 |
133 | payload = { 'src' => data_uri }
134 |
135 | mobiledoc = {
136 | 'version' => MOBILEDOC_VERSION,
137 | 'sections' => [
138 | [], # markers
139 | [ # sections
140 | [CARD_SECTION_TYPE, card_name, payload]
141 | ]
142 | ]
143 | }
144 |
145 | rendered = render(mobiledoc)
146 |
147 | expect(rendered).to eq(%Q[])
148 | end
149 |
150 | it 'render mobiledoc with list section and list items' do
151 | mobiledoc = {
152 | 'version' => MOBILEDOC_VERSION,
153 | 'sections' => [
154 | [], # markers
155 | [ # sections
156 | [LIST_SECTION_TYPE, 'ul', [
157 | [[[], 0, 'first item']],
158 | [[[], 0, 'second item']]
159 | ]]
160 | ]
161 | ]
162 | }
163 |
164 | rendered = render(mobiledoc)
165 |
166 | expect(rendered).to eq('')
167 | end
168 |
169 | it 'renders a mobiledoc with card section' do
170 | card_name = 'title-card'
171 | expected_payload = {}
172 | expected_options = {}
173 |
174 | title_card = Module.new do
175 | module_function
176 |
177 | def name
178 | 'title-card'
179 | end
180 |
181 | def type
182 | 'html'
183 | end
184 |
185 | def render(env, payload, options)
186 | end
187 | end
188 |
189 | mobiledoc = {
190 | 'version' => MOBILEDOC_VERSION,
191 | 'sections' => [
192 | [], # markers
193 | [ # sections
194 | [CARD_SECTION_TYPE, card_name, expected_payload]
195 | ]
196 | ]
197 | }
198 |
199 | expect(title_card).to receive(:render).with({name: card_name}, expected_payload, expected_options).and_return("Howdy friend")
200 |
201 | renderer = Mobiledoc::HTMLRenderer.new(cards: [title_card], card_options: expected_options)
202 | rendered = renderer.render(mobiledoc)
203 |
204 | expect(rendered).to eq('')
205 | end
206 |
207 | it 'throws when given invalid card type' do
208 | bad_card = Module.new do
209 | module_function
210 |
211 | def name
212 | 'bad'
213 | end
214 |
215 | def type
216 | 'other'
217 | end
218 |
219 | def render(env, payload, options)
220 | end
221 | end
222 |
223 | expect{ Mobiledoc::HTMLRenderer.new(cards: [bad_card]) }.to raise_error(%Q[Card "bad" must be of type "html", was "other"])
224 | end
225 |
226 | it 'throws when given card without `render`' do
227 | bad_card = Module.new do
228 | module_function
229 |
230 | def name
231 | 'bad'
232 | end
233 |
234 | def type
235 | 'html'
236 | end
237 | end
238 |
239 | expect{ Mobiledoc::HTMLRenderer.new(cards: [bad_card]) }.to raise_error(%Q[Card "bad" must define `render`])
240 | end
241 |
242 | it 'throws if card render returns invalid result' do
243 | bad_card = Module.new do
244 | module_function
245 |
246 | def name
247 | 'bad'
248 | end
249 |
250 | def type
251 | 'html'
252 | end
253 |
254 | def render(env, payload, options)
255 | Object.new
256 | end
257 | end
258 |
259 | mobiledoc = {
260 | 'version' => MOBILEDOC_VERSION,
261 | 'sections' => [
262 | [], # markers
263 | [ # sections
264 | [CARD_SECTION_TYPE, 'bad']
265 | ]
266 | ]
267 | }
268 |
269 | renderer = Mobiledoc::HTMLRenderer.new(cards: [bad_card])
270 |
271 | expect{ renderer.render(mobiledoc) }.to raise_error(/Card "bad" must render html/)
272 | end
273 |
274 | it 'card may render nothing' do
275 | card = Module.new do
276 | module_function
277 |
278 | def name
279 | 'ok'
280 | end
281 |
282 | def type
283 | 'html'
284 | end
285 |
286 | def render(env, payload, options)
287 | end
288 | end
289 |
290 | mobiledoc = {
291 | 'version' => MOBILEDOC_VERSION,
292 | 'sections' => [
293 | [], # markers
294 | [ # sections
295 | [CARD_SECTION_TYPE, 'ok']
296 | ]
297 | ]
298 | }
299 |
300 | renderer = Mobiledoc::HTMLRenderer.new(cards: [card])
301 |
302 | expect{ renderer.render(mobiledoc) }.to_not raise_error
303 | end
304 |
305 | it 'rendering nested mobiledocs in cards' do
306 | card = Module.new do
307 | module_function
308 |
309 | def name
310 | 'nested-card'
311 | end
312 |
313 | def type
314 | 'html'
315 | end
316 |
317 | def render(env, payload, options)
318 | options[:renderer].render(payload['mobiledoc'])
319 | end
320 | end
321 |
322 | inner_mobiledoc = {
323 | 'version' => MOBILEDOC_VERSION,
324 | 'sections' => [
325 | [], # markers
326 | [ # sections
327 | [MARKUP_SECTION_TYPE, 'P', [
328 | [[], 0, 'hello world']]
329 | ]
330 | ]
331 | ]
332 | }
333 |
334 | mobiledoc = {
335 | 'version' => MOBILEDOC_VERSION,
336 | 'sections' => [
337 | [], # markers
338 | [ # sections
339 | [CARD_SECTION_TYPE, 'nested-card', { 'mobiledoc' => inner_mobiledoc }]
340 | ]
341 | ]
342 | }
343 |
344 | renderer = Mobiledoc::HTMLRenderer.new(cards: [card], card_options: { renderer: self })
345 |
346 | rendered = renderer.render(mobiledoc)
347 |
348 | expect(rendered).to eq('')
349 | end
350 |
351 | it 'rendering unknown card without unknown_card_handler throws' do
352 | card_name = 'missing-card'
353 |
354 | mobiledoc = {
355 | 'version' => MOBILEDOC_VERSION,
356 | 'sections' => [
357 | [], # markers
358 | [ # sections
359 | [CARD_SECTION_TYPE, card_name]
360 | ]
361 | ]
362 | }
363 |
364 | renderer = Mobiledoc::HTMLRenderer.new(cards: [])
365 |
366 | expect{ renderer.render(mobiledoc) }.to raise_error(%Q[Card "missing-card" not found])
367 | end
368 |
369 | it 'rendering unknown card uses unknown_card_handler' do
370 | card_name = 'missing-card'
371 | expected_payload = {}
372 | expected_options = {}
373 |
374 | unknown_card_handler = Module.new do
375 | module_function
376 |
377 | def type
378 | 'html'
379 | end
380 |
381 | def render(env, payload, options)
382 | end
383 | end
384 |
385 | mobiledoc = {
386 | 'version' => MOBILEDOC_VERSION,
387 | 'sections' => [
388 | [], # markers
389 | [ # sections
390 | [CARD_SECTION_TYPE, card_name, expected_payload]
391 | ]
392 | ]
393 | }
394 |
395 | expect(unknown_card_handler).to receive(:render).with({name: card_name}, expected_payload, expected_options)
396 |
397 | renderer = Mobiledoc::HTMLRenderer.new(cards: [], card_options: expected_options, unknown_card_handler: unknown_card_handler)
398 | rendered = renderer.render(mobiledoc)
399 | end
400 |
401 | it 'throws if given an object of cards' do
402 | expect{ Mobiledoc::HTMLRenderer.new(cards: {}) }.to raise_exception('`cards` must be passed as an array')
403 | end
404 |
405 | it 'XSS: tag contents are entity escaped' do
406 | xss = ""
407 |
408 | mobiledoc = {
409 | 'version' => MOBILEDOC_VERSION,
410 | 'sections' => [
411 | [], # markers
412 | [ # sections
413 | [MARKUP_SECTION_TYPE, 'P', [
414 | [[], 0, xss]]
415 | ]
416 | ]
417 | ]
418 | }
419 |
420 | rendered = render(mobiledoc)
421 |
422 | expect(rendered).to eq("<script>alert('xx')</script>
")
423 | end
424 |
425 | it 'multiple spaces should preserve whitespace with nbsps' do
426 | space = ' '
427 | text = [ space * 4, 'some', space * 5, 'text', space * 6].join
428 |
429 | mobiledoc = {
430 | 'version' => MOBILEDOC_VERSION,
431 | 'sections' => [
432 | [], # markers
433 | [ # sections
434 | [MARKUP_SECTION_TYPE, 'P', [
435 | [[], 0, text]]
436 | ]
437 | ]
438 | ]
439 | }
440 |
441 | rendered = render(mobiledoc)
442 |
443 | sn = ' '
444 | expected_text = [ sn * 2, 'some', sn * 2, space, 'text', sn * 3 ].join
445 |
446 | expect(rendered).to eq("")
447 | end
448 |
449 | it 'throws when given an unexpected mobiledoc version' do
450 | mobiledoc = {
451 | 'version' => '0.1.0',
452 | 'sections' => [
453 | [], []
454 | ]
455 | }
456 |
457 | expect{ render(mobiledoc) }.to raise_error('Unexpected Mobiledoc version "0.1.0"')
458 |
459 | mobiledoc['version'] = '0.2.1'
460 |
461 | expect{ render(mobiledoc) }.to raise_error('Unexpected Mobiledoc version "0.2.1"')
462 | end
463 |
464 | it 'XSS: unexpected markup and list section tag names are not renderered' do
465 | mobiledoc = {
466 | 'version' => MOBILEDOC_VERSION,
467 | 'sections' => [
468 | [],
469 | [
470 | [MARKUP_SECTION_TYPE, 'script', [
471 | [[], 0, 'alert("markup section XSS")']
472 | ]],
473 | [LIST_SECTION_TYPE, 'script', [
474 | [[[], 0, 'alert("list section XSS")']]
475 | ]]
476 | ]
477 | ]
478 | }
479 |
480 | rendered = render(mobiledoc)
481 |
482 | expect(rendered).to_not match(/script/)
483 | end
484 |
485 | it 'XSS: unexpected markup types are not rendered' do
486 | mobiledoc = {
487 | 'version' => MOBILEDOC_VERSION,
488 | 'sections' => [
489 | [
490 | ['b'], # valid
491 | ['em'], # valid
492 | ['script'] # invalid
493 | ],
494 | [
495 | [MARKUP_SECTION_TYPE, 'p', [
496 | [[0], 0, 'bold text'],
497 | [[1,2], 3, 'alert("markup XSS")'],
498 | [[], 0, 'plain text']
499 | ]]
500 | ]
501 | ]
502 | }
503 |
504 | rendered = render(mobiledoc)
505 |
506 | expect(rendered).to_not match(/script/)
507 | end
508 | end
509 | end
510 |
--------------------------------------------------------------------------------
/spec/0.3_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'mobiledoc/utils/section_types'
3 | require 'mobiledoc/utils/marker_types'
4 | require 'mobiledoc/cards/image'
5 |
6 | module ZeroThreeZero
7 | include Mobiledoc::Utils::SectionTypes
8 | include Mobiledoc::Utils::MarkerTypes
9 |
10 | MOBILEDOC_VERSION = '0.3.0'
11 |
12 | describe Mobiledoc::HTMLRenderer, "(#{MOBILEDOC_VERSION})" do
13 |
14 | def render(mobiledoc)
15 | described_class.new.render(mobiledoc)
16 | end
17 |
18 | let(:data_uri) { "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" }
19 |
20 | it 'renders an empty mobiledoc' do
21 | mobiledoc = {
22 | 'version' => MOBILEDOC_VERSION,
23 | 'atoms' => [],
24 | 'cards' => [],
25 | 'markups' => [],
26 | 'sections' => []
27 | }
28 |
29 | rendered = render(mobiledoc)
30 |
31 | expect(rendered).to eq('')
32 | end
33 |
34 | it 'renders a mobiledoc without markups' do
35 | mobiledoc = {
36 | 'version' => MOBILEDOC_VERSION,
37 | 'atoms' => [],
38 | 'cards' => [],
39 | 'markups' => [],
40 | 'sections' => [
41 | [MARKUP_SECTION_TYPE, 'P', [
42 | [MARKUP_MARKER_TYPE, [], 0, 'hello world']]
43 | ]
44 | ]
45 | }
46 |
47 | rendered = render(mobiledoc)
48 |
49 | expect(rendered).to eq('')
50 | end
51 |
52 | it 'renders a mobiledoc with simple (no attributes) markup' do
53 | mobiledoc = {
54 | 'version' => MOBILEDOC_VERSION,
55 | 'atoms' => [],
56 | 'cards' => [],
57 | 'markups' => [
58 | ['B']
59 | ],
60 | 'sections' => [
61 | [MARKUP_SECTION_TYPE, 'P', [
62 | [MARKUP_MARKER_TYPE, [0], 1, 'hello world']]
63 | ]
64 | ]
65 | }
66 |
67 | rendered = render(mobiledoc)
68 |
69 | expect(rendered).to eq('')
70 | end
71 |
72 | it 'renders a mobiledoc with complex (has attributes) markup' do
73 | mobiledoc = {
74 | 'version' => MOBILEDOC_VERSION,
75 | 'atoms' => [],
76 | 'cards' => [],
77 | 'markups' => [
78 | ['A', ['href', 'http://google.com']]
79 | ],
80 | 'sections' => [
81 | [MARKUP_SECTION_TYPE, 'P', [
82 | [MARKUP_MARKER_TYPE, [0], 1, 'hello world']
83 | ]]
84 | ]
85 | }
86 |
87 | rendered = render(mobiledoc)
88 |
89 | expect(rendered).to eq('')
90 | end
91 |
92 | it 'renders a mobiledoc with multiple markups in a section' do
93 | mobiledoc = {
94 | 'version' => MOBILEDOC_VERSION,
95 | 'atoms' => [],
96 | 'cards' => [],
97 | 'markups' => [
98 | ['B'],
99 | ['I']
100 | ],
101 | 'sections' => [
102 | [MARKUP_SECTION_TYPE, 'P', [
103 | [MARKUP_MARKER_TYPE, [0], 0, 'hello '], # b
104 | [MARKUP_MARKER_TYPE, [1], 0, 'brave '], # b + i
105 | [MARKUP_MARKER_TYPE, [], 1, 'new '], # close i
106 | [MARKUP_MARKER_TYPE, [], 1, 'world'] # close b
107 | ]]
108 | ]
109 | }
110 |
111 | rendered = render(mobiledoc)
112 |
113 | expect(rendered).to eq('')
114 | end
115 |
116 | it 'renders a mobiledoc with image section' do
117 | mobiledoc = {
118 | 'version' => MOBILEDOC_VERSION,
119 | 'atoms' => [],
120 | 'cards' => [],
121 | 'markups' => [],
122 | 'sections' => [
123 | [IMAGE_SECTION_TYPE, data_uri]
124 | ]
125 | }
126 |
127 | rendered = render(mobiledoc)
128 |
129 | expect(rendered).to eq(%Q[])
130 | end
131 |
132 | it 'renders a mobiledoc with built-in image card' do
133 | card_name = Mobiledoc::ImageCard.name
134 |
135 | payload = { 'src' => data_uri }
136 |
137 | mobiledoc = {
138 | 'version' => MOBILEDOC_VERSION,
139 | 'atoms' => [],
140 | 'cards' => [
141 | [card_name, payload]
142 | ],
143 | 'markups' => [],
144 | 'sections' => [
145 | [CARD_SECTION_TYPE, 0]
146 | ]
147 | }
148 |
149 | rendered = render(mobiledoc)
150 |
151 | expect(rendered).to eq(%Q[])
152 | end
153 |
154 | it 'render mobiledoc with list section and list items' do
155 | mobiledoc = {
156 | 'version' => MOBILEDOC_VERSION,
157 | 'atoms' => [],
158 | 'cards' => [],
159 | 'markups' => [],
160 | 'sections' => [
161 | [LIST_SECTION_TYPE, 'ul', [
162 | [[MARKUP_MARKER_TYPE, [], 0, 'first item']],
163 | [[MARKUP_MARKER_TYPE, [], 0, 'second item']]
164 | ]]
165 | ]
166 | }
167 |
168 | rendered = render(mobiledoc)
169 |
170 | expect(rendered).to eq('')
171 | end
172 |
173 | it 'renders a mobiledoc with card section' do
174 | card_name = 'title-card'
175 | expected_payload = {}
176 | expected_options = {}
177 |
178 | title_card = Module.new do
179 | module_function
180 |
181 | def name
182 | 'title-card'
183 | end
184 |
185 | def type
186 | 'html'
187 | end
188 |
189 | def render(env, payload, options)
190 | end
191 | end
192 |
193 | mobiledoc = {
194 | 'version' => MOBILEDOC_VERSION,
195 | 'atoms' => [],
196 | 'cards' => [
197 | [card_name, expected_payload]
198 | ],
199 | 'markups' => [],
200 | 'sections' => [
201 | [CARD_SECTION_TYPE, 0]
202 | ]
203 | }
204 |
205 | expect(title_card).to receive(:render).with({name: card_name}, expected_payload, expected_options).and_return("Howdy friend")
206 |
207 | renderer = Mobiledoc::HTMLRenderer.new(cards: [title_card], card_options: expected_options)
208 | rendered = renderer.render(mobiledoc)
209 |
210 | expect(rendered).to eq('')
211 | end
212 |
213 | it 'throws when given invalid card type' do
214 | bad_card = Module.new do
215 | module_function
216 |
217 | def name
218 | 'bad'
219 | end
220 |
221 | def type
222 | 'other'
223 | end
224 |
225 | def render(env, payload, options)
226 | end
227 | end
228 |
229 | expect{ Mobiledoc::HTMLRenderer.new(cards: [bad_card]) }.to raise_error(%Q[Card "bad" must be of type "html", was "other"])
230 | end
231 |
232 | it 'throws when given card without `render`' do
233 | bad_card = Module.new do
234 | module_function
235 |
236 | def name
237 | 'bad'
238 | end
239 |
240 | def type
241 | 'html'
242 | end
243 | end
244 |
245 | expect{ Mobiledoc::HTMLRenderer.new(cards: [bad_card]) }.to raise_error(%Q[Card "bad" must define `render`])
246 | end
247 |
248 | it 'throws if card render returns invalid result' do
249 | bad_card = Module.new do
250 | module_function
251 |
252 | def name
253 | 'bad'
254 | end
255 |
256 | def type
257 | 'html'
258 | end
259 |
260 | def render(env, payload, options)
261 | Object.new
262 | end
263 | end
264 |
265 | mobiledoc = {
266 | 'version' => MOBILEDOC_VERSION,
267 | 'atoms' => [],
268 | 'cards' => [
269 | [bad_card.name]
270 | ],
271 | 'markups' => [],
272 | 'sections' => [
273 | [CARD_SECTION_TYPE, 0]
274 | ]
275 | }
276 |
277 | renderer = Mobiledoc::HTMLRenderer.new(cards: [bad_card])
278 |
279 | expect{ renderer.render(mobiledoc) }.to raise_error(/Card "bad" must render html/)
280 | end
281 |
282 | it 'card may render nothing' do
283 | card = Module.new do
284 | module_function
285 |
286 | def name
287 | 'ok'
288 | end
289 |
290 | def type
291 | 'html'
292 | end
293 |
294 | def render(env, payload, options)
295 | end
296 | end
297 |
298 | mobiledoc = {
299 | 'version' => MOBILEDOC_VERSION,
300 | 'atoms' => [],
301 | 'cards' => [
302 | [card.name]
303 | ],
304 | 'markups' => [],
305 | 'sections' => [
306 | [CARD_SECTION_TYPE, 0]
307 | ]
308 | }
309 |
310 | renderer = Mobiledoc::HTMLRenderer.new(cards: [card])
311 |
312 | expect{ renderer.render(mobiledoc) }.to_not raise_error
313 | end
314 |
315 | it 'rendering nested mobiledocs in cards' do
316 | card = Module.new do
317 | module_function
318 |
319 | def name
320 | 'nested-card'
321 | end
322 |
323 | def type
324 | 'html'
325 | end
326 |
327 | def render(env, payload, options)
328 | options[:renderer].render(payload['mobiledoc'])
329 | end
330 | end
331 |
332 | inner_mobiledoc = {
333 | 'version' => MOBILEDOC_VERSION,
334 | 'sections' => [
335 | [MARKUP_SECTION_TYPE, 'P', [
336 | [MARKUP_MARKER_TYPE, [], 0, 'hello world']]
337 | ]
338 | ]
339 | }
340 |
341 | mobiledoc = {
342 | 'version' => MOBILEDOC_VERSION,
343 | 'atoms' => [],
344 | 'cards' => [
345 | [card.name, { 'mobiledoc' => inner_mobiledoc }]
346 | ],
347 | 'markups' => [],
348 | 'sections' => [
349 | [CARD_SECTION_TYPE, 0]
350 | ]
351 | }
352 |
353 | renderer = Mobiledoc::HTMLRenderer.new(cards: [card], card_options: { renderer: self })
354 |
355 | rendered = renderer.render(mobiledoc)
356 |
357 | expect(rendered).to eq('')
358 | end
359 |
360 | it 'rendering unknown card without unknown_card_handler throws' do
361 | card_name = 'missing-card'
362 |
363 | mobiledoc = {
364 | 'version' => MOBILEDOC_VERSION,
365 | 'atoms' => [],
366 | 'cards' => [
367 | [card_name]
368 | ],
369 | 'markups' => [],
370 | 'sections' => [
371 | [CARD_SECTION_TYPE, 0]
372 | ]
373 | }
374 |
375 | renderer = Mobiledoc::HTMLRenderer.new(cards: [])
376 |
377 | expect{ renderer.render(mobiledoc) }.to raise_error(%Q[Card "missing-card" not found])
378 | end
379 |
380 | it 'rendering unknown card uses unknown_card_handler' do
381 | card_name = 'missing-card'
382 | expected_payload = {}
383 | expected_options = {}
384 |
385 | unknown_card_handler = Module.new do
386 | module_function
387 |
388 | def type
389 | 'html'
390 | end
391 |
392 | def render(env, payload, options)
393 | end
394 | end
395 |
396 | mobiledoc = {
397 | 'version' => MOBILEDOC_VERSION,
398 | 'atoms' => [],
399 | 'cards' => [
400 | [card_name, expected_payload]
401 | ],
402 | 'markups' => [],
403 | 'sections' => [
404 | [CARD_SECTION_TYPE, 0]
405 | ]
406 | }
407 |
408 | expect(unknown_card_handler).to receive(:render).with({name: card_name}, expected_payload, expected_options)
409 |
410 | renderer = Mobiledoc::HTMLRenderer.new(cards: [], card_options: expected_options, unknown_card_handler: unknown_card_handler)
411 | rendered = renderer.render(mobiledoc)
412 | end
413 |
414 | it 'throws if given an object of cards' do
415 | expect{ Mobiledoc::HTMLRenderer.new(cards: {}) }.to raise_exception('`cards` must be passed as an array')
416 | end
417 |
418 | it 'XSS: tag contents are entity escaped' do
419 | xss = ""
420 |
421 | mobiledoc = {
422 | 'version' => MOBILEDOC_VERSION,
423 | 'atoms' => [],
424 | 'cards' => [],
425 | 'markups' => [],
426 | 'sections' => [
427 | [MARKUP_SECTION_TYPE, 'P', [
428 | [MARKUP_MARKER_TYPE, [], 0, xss]]
429 | ]
430 | ]
431 | }
432 |
433 | rendered = render(mobiledoc)
434 |
435 | expect(rendered).to eq("<script>alert('xx')</script>
")
436 | end
437 |
438 | it 'multiple spaces should preserve whitespace with nbsps' do
439 | space = ' '
440 | text = [ space * 4, 'some', space * 5, 'text', space * 6].join
441 |
442 | mobiledoc = {
443 | 'version' => MOBILEDOC_VERSION,
444 | 'atoms' => [],
445 | 'cards' => [],
446 | 'markups' => [],
447 | 'sections' => [
448 | [MARKUP_SECTION_TYPE, 'P', [
449 | [MARKUP_MARKER_TYPE, [], 0, text]]
450 | ]
451 | ]
452 | }
453 |
454 | rendered = render(mobiledoc)
455 |
456 | sn = ' '
457 | expected_text = [ sn * 2, 'some', sn * 2, space, 'text', sn * 3 ].join
458 |
459 | expect(rendered).to eq("")
460 | end
461 |
462 | it 'throws when given an unexpected mobiledoc version' do
463 | mobiledoc = {
464 | 'version' => '0.1.0',
465 | 'atoms' => [],
466 | 'cards' => [],
467 | 'markups' => [],
468 | 'sections' => []
469 | }
470 |
471 | expect{ render(mobiledoc) }.to raise_error('Unexpected Mobiledoc version "0.1.0"')
472 |
473 | mobiledoc['version'] = '0.2.1'
474 |
475 | expect{ render(mobiledoc) }.to raise_error('Unexpected Mobiledoc version "0.2.1"')
476 | end
477 |
478 | it 'XSS: unexpected markup and list section tag names are not renderered' do
479 | mobiledoc = {
480 | 'version' => MOBILEDOC_VERSION,
481 | 'atoms' => [],
482 | 'cards' => [],
483 | 'markups' => [],
484 | 'sections' => [
485 | [MARKUP_SECTION_TYPE, 'script', [
486 | [MARKUP_MARKER_TYPE, [], 0, 'alert("markup section XSS")']
487 | ]],
488 | [LIST_SECTION_TYPE, 'script', [
489 | [[MARKUP_MARKER_TYPE, [], 0, 'alert("list section XSS")']]
490 | ]]
491 | ]
492 | }
493 |
494 | rendered = render(mobiledoc)
495 |
496 | expect(rendered).to_not match(/script/)
497 | end
498 |
499 | it 'XSS: unexpected markup types are not rendered' do
500 | mobiledoc = {
501 | 'version' => MOBILEDOC_VERSION,
502 | 'atoms' => [],
503 | 'cards' => [],
504 | 'markups' => [
505 | ['b'], # valid
506 | ['em'], # valid
507 | ['script'] # invalid
508 | ],
509 | 'sections' => [
510 | [MARKUP_SECTION_TYPE, 'p', [
511 | [MARKUP_MARKER_TYPE, [0], 0, 'bold text'],
512 | [MARKUP_MARKER_TYPE, [1,2], 3, 'alert("markup XSS")'],
513 | [MARKUP_MARKER_TYPE, [], 0, 'plain text']
514 | ]]
515 | ]
516 | }
517 |
518 | rendered = render(mobiledoc)
519 |
520 | expect(rendered).to_not match(/script/)
521 | end
522 |
523 | it 'renders a mobiledoc with atom' do
524 | atom_name = 'hello-atom'
525 |
526 | expected_options = { some: :options }
527 | expected_payload = { some: :payload }
528 | expected_value = 'Bob'
529 |
530 | atom = Module.new do
531 | module_function
532 |
533 | def name
534 | 'hello-atom'
535 | end
536 |
537 | def type
538 | 'html'
539 | end
540 |
541 | def render(env, value, payload, options)
542 | end
543 | end
544 |
545 | mobiledoc = {
546 | 'version' => MOBILEDOC_VERSION,
547 | 'atoms' => [
548 | [atom_name, expected_value, expected_payload]
549 | ],
550 | 'cards' => [],
551 | 'markups' => [],
552 | 'sections' => [
553 | [MARKUP_SECTION_TYPE, 'P', [
554 | [ATOM_MARKER_TYPE, [], 0, 0]]
555 | ]
556 | ]
557 | }
558 |
559 | expect(atom).to receive(:render).with({name: atom_name}, expected_value, expected_payload, expected_options).and_return("Hello Bob")
560 |
561 | renderer = Mobiledoc::HTMLRenderer.new(atoms: [atom], card_options: expected_options)
562 | rendered = renderer.render(mobiledoc)
563 |
564 | expect(rendered).to eq('')
565 | end
566 |
567 | it 'throws when given atom with invalid type' do
568 | bad_atom = Module.new do
569 | module_function
570 |
571 | def name
572 | 'bad'
573 | end
574 |
575 | def type
576 | 'other'
577 | end
578 |
579 | def render(env, value, payload, options)
580 | end
581 | end
582 |
583 | expect{ Mobiledoc::HTMLRenderer.new(atoms: [bad_atom]) }.to raise_error(%Q[Atom "bad" must be of type "html", was "other"])
584 | end
585 |
586 | it 'throws when given atom without `render`' do
587 | bad_atom = Module.new do
588 | module_function
589 |
590 | def name
591 | 'bad'
592 | end
593 |
594 | def type
595 | 'html'
596 | end
597 | end
598 |
599 | expect{ Mobiledoc::HTMLRenderer.new(atoms: [bad_atom]) }.to raise_error(%Q[Atom "bad" must define `render`])
600 | end
601 |
602 | it 'throws if atom render returns invalid result' do
603 | bad_atom = Module.new do
604 | module_function
605 |
606 | def name
607 | 'bad'
608 | end
609 |
610 | def type
611 | 'html'
612 | end
613 |
614 | def render(env, value, payload, options)
615 | Object.new
616 | end
617 | end
618 |
619 | mobiledoc = {
620 | 'version' => MOBILEDOC_VERSION,
621 | 'atoms' => [
622 | [bad_atom.name, 'Bob', { id: 42 }]
623 | ],
624 | 'cards' => [],
625 | 'markups' => [],
626 | 'sections' => [
627 | [MARKUP_SECTION_TYPE, 'P', [
628 | [ATOM_MARKER_TYPE, [], 0, 0]]
629 | ]
630 | ]
631 | }
632 |
633 | renderer = Mobiledoc::HTMLRenderer.new(atoms: [bad_atom])
634 |
635 | expect{ renderer.render(mobiledoc) }.to raise_error(/Atom "bad" must render html/)
636 | end
637 |
638 | it 'atom may render nothing' do
639 | atom = Module.new do
640 | module_function
641 |
642 | def name
643 | 'ok'
644 | end
645 |
646 | def type
647 | 'html'
648 | end
649 |
650 | def render(env, value, payload, options)
651 | end
652 | end
653 |
654 | mobiledoc = {
655 | 'version' => MOBILEDOC_VERSION,
656 | 'atoms' => [
657 | [atom.name, 'Bob', { id: 42 }]
658 | ],
659 | 'cards' => [],
660 | 'markups' => [],
661 | 'sections' => [
662 | [MARKUP_SECTION_TYPE, 'P', [
663 | [ATOM_MARKER_TYPE, [], 0, 0]]
664 | ]
665 | ]
666 | }
667 |
668 | renderer = Mobiledoc::HTMLRenderer.new(atoms: [atom])
669 |
670 | expect{ renderer.render(mobiledoc) }.to_not raise_error
671 | end
672 |
673 | it 'throws when rendering unknown atom without unknown_atom_handler' do
674 | atom_name = 'missing-atom'
675 |
676 | mobiledoc = {
677 | 'version' => MOBILEDOC_VERSION,
678 | 'atoms' => [
679 | [atom_name, 'Bob', { id: 42 }]
680 | ],
681 | 'cards' => [],
682 | 'markups' => [],
683 | 'sections' => [
684 | [MARKUP_SECTION_TYPE, 'P', [
685 | [ATOM_MARKER_TYPE, [], 0, 0]]
686 | ]
687 | ]
688 | }
689 |
690 | renderer = Mobiledoc::HTMLRenderer.new(atoms: [])
691 |
692 | expect{ renderer.render(mobiledoc) }.to raise_error(%Q[Atom "missing-atom" not found])
693 | end
694 |
695 | it 'rendering unknown atom uses unknown_atom_handler' do
696 | atom_name = 'missing-atom'
697 |
698 | expected_value = 'Bob'
699 | expected_payload = { some: :payload }
700 | expected_options = { some: :options }
701 |
702 | unknown_atom_handler = Module.new do
703 | module_function
704 |
705 | def type
706 | 'html'
707 | end
708 |
709 | def render(env, value, payload, options)
710 | end
711 | end
712 |
713 | mobiledoc = {
714 | 'version' => MOBILEDOC_VERSION,
715 | 'atoms' => [
716 | [atom_name, expected_value, expected_payload]
717 | ],
718 | 'cards' => [],
719 | 'markups' => [],
720 | 'sections' => [
721 | [MARKUP_SECTION_TYPE, 'P', [
722 | [ATOM_MARKER_TYPE, [], 0, 0]]
723 | ]
724 | ]
725 | }
726 |
727 | expect(unknown_atom_handler).to receive(:render).with({name: atom_name}, expected_value, expected_payload, expected_options)
728 |
729 | renderer = Mobiledoc::HTMLRenderer.new(atoms: [], card_options: expected_options, unknown_atom_handler: unknown_atom_handler)
730 | rendered = renderer.render(mobiledoc)
731 | end
732 |
733 | context '0.3.1' do
734 | it 'renders 0.3.1-specific sections and markups' do
735 | mobiledoc = {
736 | 'version' => '0.3.1',
737 | 'atoms' => [],
738 | 'cards' => [],
739 | 'markups' => [
740 | ['CODE']
741 | ],
742 | 'sections' => [
743 | [MARKUP_SECTION_TYPE, 'ASIDE', [
744 | [MARKUP_MARKER_TYPE, [0], 1, 'hello world']]
745 | ]
746 | ]
747 | }
748 |
749 | rendered = render(mobiledoc)
750 |
751 | expect(rendered).to eq('')
752 | end
753 | end
754 |
755 | context '0.3.2' do
756 | it 'renders 0.3.2-specific sections with attributes' do
757 | mobiledoc = {
758 | 'version' => '0.3.2',
759 | 'atoms' => [],
760 | 'cards' => [],
761 | 'markups' => [
762 | ['CODE']
763 | ],
764 | 'sections' => [
765 | [
766 | MARKUP_SECTION_TYPE,
767 | 'ASIDE',
768 | [
769 | [MARKUP_MARKER_TYPE, [0], 1, 'hello world']
770 | ],
771 | ["data-md-text-align", "center"]
772 | ]
773 | ]
774 | }
775 |
776 | rendered = render(mobiledoc)
777 |
778 | expect(rendered).to eq('')
779 | end
780 | end
781 | end
782 | end
783 |
--------------------------------------------------------------------------------
/spec/mobiledoc_html_renderer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Mobiledoc::HTMLRenderer do
4 | it 'has a version number' do
5 | expect(Mobiledoc::HTMLRenderer::VERSION).not_to be nil
6 | end
7 |
8 | it 'can be instantiated' do
9 | expect { described_class.new }.to_not raise_exception
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2 | require 'mobiledoc_html_renderer'
3 |
--------------------------------------------------------------------------------