sitediff 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/sitediff +1 -1
- data/lib/sitediff.rb +79 -63
- data/lib/sitediff/cache.rb +61 -0
- data/lib/sitediff/cli.rb +144 -23
- data/lib/sitediff/config.rb +46 -9
- data/lib/sitediff/config/creator.rb +122 -0
- data/lib/sitediff/crawler.rb +95 -0
- data/lib/sitediff/diff.rb +2 -1
- data/lib/sitediff/exception.rb +3 -0
- data/lib/sitediff/fetch.rb +55 -0
- data/lib/sitediff/files/html_report.html.erb +20 -4
- data/lib/sitediff/files/rules/drupal.yaml +33 -0
- data/lib/sitediff/files/sidebyside.html.erb +13 -0
- data/lib/sitediff/files/sitediff.css +11 -0
- data/lib/sitediff/result.rb +12 -9
- data/lib/sitediff/rules.rb +65 -0
- data/lib/sitediff/sanitize.rb +163 -168
- data/lib/sitediff/sanitize/dom_transform.rb +92 -0
- data/lib/sitediff/sanitize/regexp.rb +56 -0
- data/lib/sitediff/uriwrapper.rb +19 -7
- data/lib/sitediff/webserver.rb +82 -0
- data/lib/sitediff/webserver/resultserver.rb +98 -0
- metadata +70 -25
- checksums.yaml +0 -7
- data/lib/sitediff/util/cache.rb +0 -32
- data/lib/sitediff/util/webserver.rb +0 -77
@@ -0,0 +1,33 @@
|
|
1
|
+
sanitization:
|
2
|
+
- title: Strip Drupal.settings
|
3
|
+
selector: script
|
4
|
+
pattern: '^(<script>)?jQuery.extend\(Drupal.settings.*$'
|
5
|
+
- title: Strip form build ID
|
6
|
+
selector: input
|
7
|
+
pattern: 'name="form_build_id" value="form-[-\w]{43}"'
|
8
|
+
substitution: 'name="form_build_id" value="form-DRUPAL_FORM_BUILD_ID"'
|
9
|
+
- title: Strip view DOM ID
|
10
|
+
pattern: '(class="view .*) view-dom-id-[a-f0-9]{32}"'
|
11
|
+
substitution: '\1 view-dom-id-DRUPAL_VIEW_DOM_ID"'
|
12
|
+
- title: Strip CSS aggregation filenames
|
13
|
+
selector: link[rel=stylesheet]
|
14
|
+
pattern: '(href="[^"]*/files/css/css_)[-\w]{43}\.css"'
|
15
|
+
substitution: '\1DRUPAL_AGGREGATED_CSS.css"'
|
16
|
+
- title: Strip JS aggregation filenames
|
17
|
+
selector: script
|
18
|
+
pattern: '(src="[^"]*/files/js/js_)[-\w]{43}\.js"'
|
19
|
+
substitution: '\1DRUPAL_AGGREGATED_JS.js"'
|
20
|
+
- title: Strip CSS/JS cache IDs
|
21
|
+
selector: style, script
|
22
|
+
pattern: '("[^"]*\.(js|css))\?[a-z0-9]{6}"'
|
23
|
+
substitution: '\1'
|
24
|
+
- title: Strip IE CSS/JS cache IDs
|
25
|
+
pattern: '("[^"]*ie\d?\.(js|css))\?[a-z0-9]{6}"'
|
26
|
+
substitution: '\1'
|
27
|
+
- title: Strip Drupal JS version tags
|
28
|
+
selector: script
|
29
|
+
pattern: '(src="[^"]*/misc/\w+\.js)?v=\d+\.\d+"'
|
30
|
+
substitution: '\1'
|
31
|
+
- title: Strip domain names from absolute URLs
|
32
|
+
pattern: 'http:\/\/[a-zA-Z0-9.:-]+'
|
33
|
+
substitute: '__domain__'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>Comparison for <%= path %></title>
|
4
|
+
<style>
|
5
|
+
<%= SiteDiff::Diff.css %>
|
6
|
+
</style>
|
7
|
+
<meta charset="utf-8" />
|
8
|
+
</head>
|
9
|
+
<body id="sidebyside">
|
10
|
+
<iframe src="<%= before %>"></iframe>
|
11
|
+
<iframe src="<%= after %>"></iframe>
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -33,6 +33,7 @@
|
|
33
33
|
background-color: salmon;
|
34
34
|
}
|
35
35
|
.sitediff .before-col,
|
36
|
+
.sitediff .both-col,
|
36
37
|
.sitediff .after-col,
|
37
38
|
.sitediff .diff-stat-col {
|
38
39
|
width: 10%;
|
@@ -40,3 +41,13 @@
|
|
40
41
|
.sitediff .path-col {
|
41
42
|
width: 55%;
|
42
43
|
}
|
44
|
+
|
45
|
+
#sidebyside {
|
46
|
+
margin: 0;
|
47
|
+
}
|
48
|
+
#sidebyside iframe {
|
49
|
+
float: left;
|
50
|
+
height: 100%;
|
51
|
+
width: 50%;
|
52
|
+
border: 0;
|
53
|
+
}
|
data/lib/sitediff/result.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
require '
|
1
|
+
require 'sitediff'
|
2
|
+
require 'sitediff/diff'
|
2
3
|
require 'digest/sha1'
|
4
|
+
require 'fileutils'
|
3
5
|
|
4
6
|
class SiteDiff
|
5
|
-
class Result < Struct.new(:path, :before, :after, :error)
|
7
|
+
class Result < Struct.new(:path, :before, :after, :error, :verbose)
|
6
8
|
STATUS_SUCCESS = 0 # Identical before and after
|
7
9
|
STATUS_FAILURE = 1 # Different before and after
|
8
10
|
STATUS_ERROR = 2 # Couldn't fetch page
|
@@ -30,8 +32,9 @@ class SiteDiff
|
|
30
32
|
end
|
31
33
|
|
32
34
|
# Printable URL
|
33
|
-
def url(prefix)
|
34
|
-
|
35
|
+
def url(tag, prefix, cache)
|
36
|
+
base = cache.read_tags.include?(tag) ? "/cache/#{tag}" : prefix
|
37
|
+
base.to_s + path
|
35
38
|
end
|
36
39
|
|
37
40
|
# Filename to store diff
|
@@ -49,15 +52,15 @@ class SiteDiff
|
|
49
52
|
end
|
50
53
|
|
51
54
|
# Log the result to the terminal
|
52
|
-
def log
|
55
|
+
def log(verbose=true)
|
53
56
|
case status
|
54
57
|
when STATUS_SUCCESS then
|
55
|
-
SiteDiff::log path, :
|
58
|
+
SiteDiff::log path, :diff_success, 'SUCCESS'
|
56
59
|
when STATUS_ERROR then
|
57
|
-
SiteDiff::log path, :
|
60
|
+
SiteDiff::log path, :warn, "ERROR (#{error})"
|
58
61
|
when STATUS_FAILURE then
|
59
|
-
SiteDiff::log path, :
|
60
|
-
puts Diff::terminal_diffy(before, after)
|
62
|
+
SiteDiff::log path, :diff_failure, "FAILURE"
|
63
|
+
puts Diff::terminal_diffy(before, after) if verbose
|
61
64
|
end
|
62
65
|
end
|
63
66
|
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'sitediff/sanitize/regexp'
|
2
|
+
require 'pathname'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
class SiteDiff
|
6
|
+
# Find appropriate rules for a given site
|
7
|
+
class Rules
|
8
|
+
def initialize(config, disabled = false)
|
9
|
+
@disabled = disabled
|
10
|
+
@config = config
|
11
|
+
find_sanitization_candidates
|
12
|
+
@rules = Hash.new { |h, k| h[k] = Set.new }
|
13
|
+
end
|
14
|
+
|
15
|
+
def find_sanitization_candidates
|
16
|
+
@candidates = Set.new
|
17
|
+
|
18
|
+
rules_dir = Pathname.new(__FILE__).dirname + 'files' + 'rules'
|
19
|
+
rules_dir.children.each do |f|
|
20
|
+
next unless f.file? && f.extname == '.yaml'
|
21
|
+
conf = YAML.load_file(f)
|
22
|
+
@candidates.merge(conf['sanitization'])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_page(tag, html, doc)
|
27
|
+
found = find_rules(html, doc)
|
28
|
+
@rules[tag].merge(found)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Yield a set of rules that seem reasonable for this HTML
|
32
|
+
# assumption: the YAML file is a list of regexp rules only
|
33
|
+
def find_rules(html, doc)
|
34
|
+
rules = []
|
35
|
+
|
36
|
+
return @candidates.select do |rule|
|
37
|
+
re = SiteDiff::Sanitizer::Regexp.create(rule)
|
38
|
+
re.applies?(html, doc)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Find all rules from all rulesets that apply for all pages
|
43
|
+
def add_config
|
44
|
+
have_both = @rules.include?(:before)
|
45
|
+
|
46
|
+
r1, r2 = *@rules.values_at(:before, :after)
|
47
|
+
if have_both
|
48
|
+
add_section('before', r1 - r2)
|
49
|
+
add_section('after', r2 - r1)
|
50
|
+
add_section(nil, r1 & r2)
|
51
|
+
else
|
52
|
+
add_section(nil, r2)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_section(name, rules)
|
57
|
+
return if rules.empty?
|
58
|
+
conf = name ? @config[name] : @config
|
59
|
+
if @disabled
|
60
|
+
rules.each { |r| r['disabled'] = true }
|
61
|
+
end
|
62
|
+
conf['sanitization'] = rules.to_a.sort_by { |r| r['title'] }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/sitediff/sanitize.rb
CHANGED
@@ -1,193 +1,188 @@
|
|
1
|
+
require 'sitediff'
|
2
|
+
require 'sitediff/exception'
|
3
|
+
require 'sitediff/sanitize/dom_transform'
|
4
|
+
require 'sitediff/sanitize/regexp'
|
1
5
|
require 'nokogiri'
|
2
6
|
require 'set'
|
3
7
|
|
4
8
|
class SiteDiff
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
# * { :type => "unwrap_root" }
|
21
|
-
# * { :type => "unwrap", :selector => "div.field-item" }
|
22
|
-
# * { :type => "remove", :selector => "div.extra-stuff" }
|
23
|
-
#
|
24
|
-
# @arg node - Nokogiri document or Node
|
25
|
-
# @arg rules - array of dom_transform rules
|
26
|
-
# @return - transformed Nokogiri document node
|
27
|
-
def perform_dom_transforms(node, rules)
|
28
|
-
rules.each do |rule|
|
29
|
-
type = rule['type'] or
|
30
|
-
raise InvalidSanitization, "DOM transform needs a type"
|
31
|
-
DOM_TRANSFORMS.include?(type) or
|
32
|
-
raise InvalidSanitization, "No DOM transform named #{type}"
|
33
|
-
|
34
|
-
meth = 'transform_' + type
|
35
|
-
|
36
|
-
if sels = rule['selector']
|
37
|
-
sels = [sels].flatten # Either array or scalar is fine
|
38
|
-
# Call method for each node the selectors find
|
39
|
-
sels.each do |sel|
|
40
|
-
node.css(sel).each { |e| send(meth, rule, e) }
|
41
|
-
end
|
42
|
-
else
|
43
|
-
send(meth, rule, node)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
9
|
+
class Sanitizer
|
10
|
+
class InvalidSanitization < SiteDiffException; end
|
11
|
+
|
12
|
+
TOOLS = {
|
13
|
+
:array => %w[dom_transform sanitization],
|
14
|
+
:scalar => %w[selector remove_spacing],
|
15
|
+
}
|
16
|
+
DOM_TRANSFORMS = Set.new(%w[remove unwrap_root unwrap remove_class])
|
17
|
+
|
18
|
+
def initialize(html, config, opts = {})
|
19
|
+
@html = html
|
20
|
+
@config = config
|
21
|
+
@opts = opts
|
22
|
+
end
|
47
23
|
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
def transform_unwrap(rule, el)
|
52
|
-
el.add_next_sibling(el.children)
|
53
|
-
el.remove
|
54
|
-
end
|
55
|
-
def transform_remove_class(rule, el)
|
56
|
-
# Must call remove_class on a NodeSet!
|
57
|
-
ns = Nokogiri::XML::NodeSet.new(el.document, [el])
|
58
|
-
[rule['class']].flatten.each do |class_name|
|
59
|
-
ns.remove_class(class_name)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
def transform_unwrap_root(rule, node)
|
63
|
-
node.children.size == 1 or
|
64
|
-
raise InvalidSanitization, "Multiple root elements in unwrap_root"
|
65
|
-
node.children = node.children[0].children
|
66
|
-
end
|
24
|
+
def sanitize
|
25
|
+
return '' if @html == '' # Quick return on empty input
|
67
26
|
|
68
|
-
|
69
|
-
if force_doc || /<!DOCTYPE/.match(str[0, 512])
|
70
|
-
doc = Nokogiri::HTML(str)
|
71
|
-
doc
|
72
|
-
else
|
73
|
-
doc = Nokogiri::HTML.fragment(str)
|
74
|
-
end
|
75
|
-
if log_errors
|
76
|
-
doc.errors.each do |e|
|
77
|
-
SiteDiff::log "Error in parsing HTML document: #{e}", :error
|
78
|
-
end
|
79
|
-
end
|
80
|
-
doc
|
81
|
-
end
|
27
|
+
@node, @html = Sanitizer.domify(@html), nil
|
82
28
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
elsif Nokogiri::XML::Node === obj # or fragment
|
88
|
-
return parse(obj.to_s, true)
|
89
|
-
|
90
|
-
# This ought to work, and would be faster,
|
91
|
-
# but seems to segfault Nokogiri
|
92
|
-
# doc = Nokogiri::HTML('<html><body>')
|
93
|
-
# doc.at('body').children = obj.children
|
94
|
-
# return doc
|
95
|
-
else
|
96
|
-
return to_document(parse(obj))
|
97
|
-
end
|
98
|
-
end
|
29
|
+
remove_spacing
|
30
|
+
selector
|
31
|
+
dom_transforms
|
32
|
+
regexps
|
99
33
|
|
100
|
-
|
101
|
-
|
102
|
-
@stylesheet ||= begin
|
103
|
-
stylesheet_path = File.join(SiteDiff::FILES_DIR, 'pretty_print.xsl')
|
104
|
-
Nokogiri::XSLT(File.read(stylesheet_path))
|
105
|
-
end
|
34
|
+
return @html || Sanitizer.prettify(@node)
|
35
|
+
end
|
106
36
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
37
|
+
# Return whether or not we want to keep a rule
|
38
|
+
def want_rule(rule)
|
39
|
+
return false unless rule
|
40
|
+
return false if rule['disabled']
|
41
|
+
|
42
|
+
# Filter out if path regexp doesn't match
|
43
|
+
if (pathre = rule['path']) and (path = @opts[:path])
|
44
|
+
return ::Regexp.new(pathre).match(path)
|
45
|
+
end
|
111
46
|
|
112
|
-
|
113
|
-
|
114
|
-
str.sub!(/\A^<html>$\n/, '')
|
115
|
-
str.sub!(%r[</html>\n\Z], '')
|
47
|
+
return true
|
48
|
+
end
|
116
49
|
|
117
|
-
|
118
|
-
|
119
|
-
|
50
|
+
# Canonicalize a simple rule, eg: 'remove_spacing' or 'selector'.
|
51
|
+
# It may be a simple value, or a hash, or an array of hashes.
|
52
|
+
# Turn it into an array of hashes.
|
53
|
+
def canonicalize_rule(name)
|
54
|
+
rules = @config[name] or return nil
|
55
|
+
|
56
|
+
if rules[0] && rules[0].respond_to?(:[]) && rules[0]['value']
|
57
|
+
# Already an array
|
58
|
+
elsif rules['value']
|
59
|
+
# Hash, put it in an array
|
60
|
+
rules = [rules]
|
61
|
+
else
|
62
|
+
# Scalar, put it in a hash
|
63
|
+
rules = [{ 'value' => rules }]
|
64
|
+
end
|
120
65
|
|
121
|
-
|
122
|
-
|
66
|
+
want = rules.select { |r| want_rule(r) }
|
67
|
+
return nil if want.empty?
|
68
|
+
raise "Too many matching rules of type #{name}" if want.size > 1
|
69
|
+
return want.first
|
70
|
+
end
|
123
71
|
|
124
|
-
|
125
|
-
|
72
|
+
# Perform 'remove_spacing' action
|
73
|
+
def remove_spacing
|
74
|
+
rule = canonicalize_rule('remove_spacing') or return
|
75
|
+
Sanitizer.remove_node_spacing(@node) if rule['value']
|
76
|
+
end
|
126
77
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
end
|
78
|
+
# Perform 'selector' action, to choose a new root
|
79
|
+
def selector
|
80
|
+
rule = canonicalize_rule('selector') or return
|
81
|
+
@node = Sanitizer.select_fragments(@node, rule['value'])
|
82
|
+
end
|
133
83
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
str
|
139
|
-
end
|
84
|
+
# Applies regexps. Also
|
85
|
+
def regexps
|
86
|
+
rules = @config['sanitization'] or return
|
87
|
+
rules = rules.select { |r| want_rule(r) }
|
140
88
|
|
141
|
-
|
142
|
-
|
143
|
-
rules ||= []
|
144
|
-
|
145
|
-
# First do rules with a selector
|
146
|
-
rules.each do |rule|
|
147
|
-
if sel = rule['selector']
|
148
|
-
node.css(sel).each do |e|
|
149
|
-
e.replace(substitute(e.to_html, rule))
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
# If needed, do rules without a selector. We'd rather not convert to
|
155
|
-
# a string unless necessary.
|
156
|
-
global_rules = rules.reject { |r| r['selector'] }
|
157
|
-
return node if global_rules.empty?
|
158
|
-
|
159
|
-
str = node.to_html # Convert to string
|
160
|
-
global_rules.each { |r| substitute(str, r) }
|
161
|
-
return str
|
162
|
-
end
|
89
|
+
rules.map! { |r| Regexp.create(r) }
|
90
|
+
selector, global = rules.partition { |r| r.selector? }
|
163
91
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
# and lose any DOCTYPE and such.
|
169
|
-
ns = node.css(sel)
|
170
|
-
unless node.fragment?
|
171
|
-
node = Nokogiri::HTML.fragment('')
|
172
|
-
end
|
173
|
-
node.children = ns
|
174
|
-
return node
|
175
|
-
end
|
92
|
+
selector.each { |r| r.apply(@node) }
|
93
|
+
@html, @node = Sanitizer.prettify(@node), nil
|
94
|
+
global.each { |r| r.apply(@html) }
|
95
|
+
end
|
176
96
|
|
177
|
-
|
178
|
-
|
97
|
+
# Perform DOM transforms
|
98
|
+
def dom_transforms
|
99
|
+
rules = @config['dom_transform'] or return
|
100
|
+
rules = rules.select { |r| want_rule(r) }
|
179
101
|
|
180
|
-
|
102
|
+
rules.each do |rule|
|
103
|
+
transform = DomTransform.create(rule)
|
104
|
+
transform.apply(@node)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##### Implementations of actions #####
|
109
|
+
|
110
|
+
# Remove double-spacing inside text nodes
|
111
|
+
def self.remove_node_spacing(node)
|
112
|
+
# remove double spacing, but only inside text nodes (eg not attributes)
|
113
|
+
node.xpath('//text()').each do |el|
|
114
|
+
el.content = el.content.gsub(/ +/, ' ')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get a fragment consisting of the elements matching the selector(s)
|
119
|
+
def self.select_fragments(node, sel)
|
120
|
+
# When we choose a new root, we always become a DocumentFragment,
|
121
|
+
# and lose any DOCTYPE and such.
|
122
|
+
ns = node.css(sel)
|
123
|
+
unless node.fragment?
|
124
|
+
node = Nokogiri::HTML.fragment('')
|
125
|
+
end
|
126
|
+
node.children = ns
|
127
|
+
return node
|
128
|
+
end
|
129
|
+
|
130
|
+
# Pretty-print some HTML
|
131
|
+
def self.prettify(obj)
|
132
|
+
@stylesheet ||= begin
|
133
|
+
stylesheet_path = File.join(SiteDiff::FILES_DIR, 'pretty_print.xsl')
|
134
|
+
Nokogiri::XSLT(File.read(stylesheet_path))
|
135
|
+
end
|
181
136
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
end
|
137
|
+
# Pull out the html element's children
|
138
|
+
# The obvious way to do this is to iterate over pretty.css('html'),
|
139
|
+
# but that tends to segfault Nokogiri
|
140
|
+
str = @stylesheet.apply_to(to_document(obj))
|
187
141
|
|
188
|
-
|
142
|
+
# There's a lot of cruft left over,that we don't want
|
189
143
|
|
190
|
-
|
144
|
+
# Remove xml declaration and <html> tags
|
145
|
+
str.sub!(/\A<\?xml.*$\n/, '')
|
146
|
+
str.sub!(/\A^<html>$\n/, '')
|
147
|
+
str.sub!(%r[</html>\n\Z], '')
|
148
|
+
|
149
|
+
# Remove top-level indentation
|
150
|
+
indent = /\A(\s*)/.match(str)[1].size
|
151
|
+
str.gsub!(/^\s{,#{indent}}/, '')
|
152
|
+
|
153
|
+
# Remove blank lines
|
154
|
+
str.gsub!(/^\s*$\n/, '')
|
155
|
+
|
156
|
+
return str
|
157
|
+
end
|
158
|
+
|
159
|
+
# Parse HTML into a node
|
160
|
+
def self.domify(str, force_doc = false)
|
161
|
+
if force_doc || /<!DOCTYPE/.match(str[0, 512])
|
162
|
+
return Nokogiri::HTML(str)
|
163
|
+
else
|
164
|
+
return Nokogiri::HTML.fragment(str)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Force this object to be a document, so we can apply a stylesheet
|
169
|
+
def self.to_document(obj)
|
170
|
+
if Nokogiri::XML::Document === obj
|
171
|
+
return obj
|
172
|
+
elsif Nokogiri::XML::Node === obj # node or fragment
|
173
|
+
return domify(obj.to_s, true)
|
174
|
+
|
175
|
+
# This ought to work, and would be faster,
|
176
|
+
# but seems to segfault Nokogiri
|
177
|
+
if false
|
178
|
+
doc = Nokogiri::HTML('<html><body>')
|
179
|
+
doc.at('body').children = obj.children
|
180
|
+
return doc
|
191
181
|
end
|
182
|
+
else
|
183
|
+
return to_document(domify(obj))
|
192
184
|
end
|
193
185
|
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|