reverse_markdown 0.4.7 → 0.5.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +2 -3
- data/LICENSE +13 -0
- data/README.md +69 -30
- data/bin/reverse_markdown +1 -1
- data/lib/reverse_markdown/cleaner.rb +66 -0
- data/lib/reverse_markdown/config.rb +20 -0
- data/lib/reverse_markdown/converters/a.rb +19 -0
- data/lib/reverse_markdown/converters/base.rb +24 -0
- data/lib/reverse_markdown/converters/blockquote.rb +13 -0
- data/lib/reverse_markdown/converters/br.rb +11 -0
- data/lib/reverse_markdown/converters/code.rb +11 -0
- data/lib/reverse_markdown/converters/div.rb +11 -0
- data/lib/reverse_markdown/converters/dump.rb +9 -0
- data/lib/reverse_markdown/converters/em.rb +21 -0
- data/lib/reverse_markdown/converters/h.rb +17 -0
- data/lib/reverse_markdown/converters/hr.rb +11 -0
- data/lib/reverse_markdown/converters/img.rb +14 -0
- data/lib/reverse_markdown/converters/li.rb +28 -0
- data/lib/reverse_markdown/converters/ol.rb +12 -0
- data/lib/reverse_markdown/converters/p.rb +11 -0
- data/lib/reverse_markdown/converters/pass_through.rb +14 -0
- data/lib/reverse_markdown/converters/pre.rb +17 -0
- data/lib/reverse_markdown/converters/strong.rb +21 -0
- data/lib/reverse_markdown/converters/table.rb +11 -0
- data/lib/reverse_markdown/converters/td.rb +13 -0
- data/lib/reverse_markdown/converters/text.rb +30 -0
- data/lib/reverse_markdown/converters/tr.rb +21 -0
- data/lib/reverse_markdown/converters.rb +22 -0
- data/lib/reverse_markdown/errors.rb +6 -2
- data/lib/reverse_markdown/version.rb +1 -1
- data/lib/reverse_markdown.rb +40 -8
- data/reverse_markdown.gemspec +3 -1
- data/spec/assets/lists.html +24 -0
- data/spec/assets/unknown_tags.html +9 -0
- data/spec/components/anchors_spec.rb +5 -5
- data/spec/components/basic_spec.rb +5 -5
- data/spec/components/code_spec.rb +5 -5
- data/spec/components/escapables_spec.rb +3 -3
- data/spec/components/from_the_wild_spec.rb +5 -5
- data/spec/components/html_fragment_spec.rb +2 -2
- data/spec/components/lists_spec.rb +18 -7
- data/spec/components/paragraphs_spec.rb +2 -2
- data/spec/components/quotation_spec.rb +5 -5
- data/spec/components/tables_spec.rb +2 -2
- data/spec/components/unknown_tags_spec.rb +21 -0
- data/spec/html_to_markdown_to_html_spec.rb +1 -2
- data/spec/lib/reverse_markdown/cleaner_spec.rb +76 -0
- data/spec/lib/reverse_markdown/converters/blockquote_spec.rb +18 -0
- data/spec/lib/reverse_markdown/converters/br_spec.rb +9 -0
- data/spec/lib/reverse_markdown_spec.rb +37 -0
- data/spec/spec_helper.rb +9 -1
- metadata +83 -24
- data/License-MIT +0 -7
- data/lib/reverse_markdown/mapper.rb +0 -241
- data/spec/mapper_spec.rb +0 -63
- data/spec/reverse_markdown_spec.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d29227386b5418e906da5a4cb42ce616a2d3872b
|
4
|
+
data.tar.gz: 0f433297212a99d783573daac3fd3723e7d9f6a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23ff8eb3e7a76e197b5526fcdf6c14d899e1ec600f1b40046d5584e2c40d62a9f22734656b5843f38ec5fa53dcf3a31f7b9baa7c2962e993eb07e5d4486f3afa
|
7
|
+
data.tar.gz: 2e46ce50817a0e735329dd2e73a9d47943e284a7cfb440362c8f8b6b1cbbb4d9945a6c492bafe8bd34843b7987fe1556410d29d4f6b34ce1d7936c570759f5dc
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
2
|
+
Version 2, December 2004
|
3
|
+
|
4
|
+
Copyright (C) 2014 Johannes Opper <xijo@gmx.de>
|
5
|
+
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
7
|
+
copies of this license document, and changing it is allowed as long
|
8
|
+
as the name is changed.
|
9
|
+
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
12
|
+
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
data/README.md
CHANGED
@@ -1,69 +1,108 @@
|
|
1
1
|
# Summary
|
2
2
|
|
3
|
-
Transform
|
3
|
+
Transform html into markdown. Useful for example if you want to import html into your markdown based application.
|
4
4
|
|
5
|
-
[](https://travis-ci.org/xijo/reverse_markdown) [](http://badge.fury.io/rb/reverse_markdown)
|
5
|
+
[](https://travis-ci.org/xijo/reverse_markdown) [](http://badge.fury.io/rb/reverse_markdown) [](https://codeclimate.com/github/xijo/reverse_markdown) [](https://codeclimate.com/github/xijo/reverse_markdown)
|
6
6
|
|
7
|
-
|
7
|
+
## Migrate to 0.5.0
|
8
8
|
|
9
|
-
|
9
|
+
There were some breaking changes, please make sure you don't miss them:
|
10
10
|
|
11
|
-
|
11
|
+
1. Only ruby versions 2.0.0 or a above are supported
|
12
|
+
2. There is no `Mapper` class any more. Just use `ReverseMarkdown.convert(input, options)`
|
13
|
+
3. Config option `github_style_code_blocks` changed its name to `github_flavored`
|
12
14
|
|
13
|
-
|
15
|
+
Please open an issue and let me know about it if you have any trouble with the new version.
|
14
16
|
|
15
|
-
|
17
|
+
## Requirements
|
16
18
|
|
17
|
-
|
19
|
+
1. [Nokogiri](http://nokogiri.org/)
|
20
|
+
2. Ruby 2.0.0 or higher
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Install the gem
|
25
|
+
|
26
|
+
```sh
|
27
|
+
[sudo] gem install reverse_markdown
|
28
|
+
```
|
29
|
+
|
30
|
+
or add it to your Gemfile
|
18
31
|
|
19
32
|
```ruby
|
20
33
|
gem 'reverse_markdown'
|
21
34
|
```
|
22
35
|
|
23
|
-
|
36
|
+
## Features
|
24
37
|
|
25
|
-
|
38
|
+
- Supports all the established html tags like `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `p`, `em`, `strong`, `i`, `b`, `blockquote`, `code`, `img`, `a`, `hr`, `li`, `ol`, `ul`, `table`, `tr`, `th`, `td`, `br`
|
39
|
+
- Module based - if you miss a tag, just add it
|
40
|
+
- Can deal with nested lists
|
41
|
+
- Inline and block code is supported
|
42
|
+
- Supports blockquote
|
26
43
|
|
27
|
-
```ruby
|
28
|
-
ReverseMarkdown.parse content
|
29
|
-
````
|
30
44
|
|
31
|
-
|
45
|
+
# Usage
|
46
|
+
|
47
|
+
## Ruby
|
48
|
+
|
49
|
+
You can convert html content as string or Nokogiri document:
|
32
50
|
|
33
51
|
```ruby
|
34
|
-
|
35
|
-
ReverseMarkdown.
|
52
|
+
input = '<strong>feelings</strong>'
|
53
|
+
result = ReverseMarkdown.convert input
|
54
|
+
result.inspect # " **feelings** "
|
36
55
|
````
|
37
56
|
|
38
|
-
|
57
|
+
## Commandline
|
58
|
+
|
59
|
+
It's also possible to convert html files to markdown using the binary:
|
39
60
|
|
40
61
|
```sh
|
41
|
-
$ reverse_markdown file.html > file.
|
42
|
-
$ cat file.html | reverse_markdown > file.
|
62
|
+
$ reverse_markdown file.html > file.md
|
63
|
+
$ cat file.html | reverse_markdown > file.md
|
43
64
|
````
|
44
65
|
|
45
|
-
|
66
|
+
## Configuration
|
67
|
+
|
68
|
+
The following options are available:
|
69
|
+
|
70
|
+
- `ignore_unknown_tags` (default `true`) - bypass unknown tags instead of raising an `ReverseMarkdown::UnknownTagError`
|
71
|
+
- `github_flavored` (default `false`) - use [github flavored markdown](https://help.github.com/articles/github-flavored-markdown) (yet only code blocks are supported)
|
72
|
+
|
73
|
+
### As options
|
74
|
+
|
75
|
+
Just pass your chosen configuration options in after the input
|
46
76
|
|
47
77
|
```ruby
|
48
|
-
ReverseMarkdown.
|
49
|
-
|
78
|
+
ReverseMarkdown.convert(input, ignore_unknown_tags: false, github_flavored: true)
|
79
|
+
```
|
80
|
+
|
81
|
+
### Preconfigure
|
50
82
|
|
51
|
-
|
83
|
+
Or configure it block style on a initializer level
|
52
84
|
|
53
|
-
|
85
|
+
```ruby
|
86
|
+
ReverseMarkdown.config do |config|
|
87
|
+
config.ignore_unknown_tags = false
|
88
|
+
config.github_flavored = true
|
89
|
+
end
|
90
|
+
```
|
54
91
|
|
55
|
-
- supported tags: `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `p`, `em`, `strong`, `i`, `b`, `blockquote`, `code`, `img`, `a`, `hr`, `li`, `ol`, `ul`, `table`, `tr`, `th`, `td`
|
56
|
-
- nested lists
|
57
|
-
- inline and block code
|
58
92
|
|
59
|
-
#
|
93
|
+
# Related stuff
|
60
94
|
|
61
|
-
- [
|
95
|
+
- [html_massage](https://github.com/harlantwood/html_massage) - A gem by Harlan T. Wood to convert regular sites into markdown using reverse_markdown
|
96
|
+
- [word-to-markdown](https://github.com/benbalter/word-to-markdown) - Convert word docs into markdown while using reverse_markdown, by Ben Balter
|
62
97
|
- [markdown syntax](http://daringfireball.net/projects/markdown) - The markdown syntax specification
|
63
|
-
- [
|
98
|
+
- [github flavored markdown](https://help.github.com/articles/github-flavored-markdown) - Githubs extension to markdown
|
99
|
+
- [wmd-editor](http://wmd-editor.com) - Markdown flavored text editor
|
100
|
+
|
64
101
|
|
65
102
|
# Thanks
|
66
103
|
|
67
104
|
..to Ben Woosley for his improvements to the first version.
|
68
105
|
|
69
106
|
..to Harlan T. Wood for his help with the newer versions.
|
107
|
+
|
108
|
+
..and all contributors
|
data/bin/reverse_markdown
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
class Cleaner
|
3
|
+
|
4
|
+
def tidy(string)
|
5
|
+
clean_tag_borders(remove_leading_newlines(remove_newlines(remove_inner_whitespaces(string))))
|
6
|
+
end
|
7
|
+
|
8
|
+
def remove_newlines(string)
|
9
|
+
string.gsub(/\n{3,}/, "\n\n")
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove_leading_newlines(string)
|
13
|
+
string.gsub /\A\n\n?/, ''
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove_inner_whitespaces(string)
|
17
|
+
string.each_line.inject("") do |memo, line|
|
18
|
+
memo + preserve_border_whitespaces(line) do
|
19
|
+
line.strip.gsub(/[ \t]{2,}/, ' ')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Find non-asterisk content that is enclosed by two or
|
25
|
+
# more asterisks. Ensure that only one whitespace occur
|
26
|
+
# in the border area.
|
27
|
+
# Same for underscores and brackets.
|
28
|
+
def clean_tag_borders(string)
|
29
|
+
result = string.gsub /\s?\*{2,}.*?\*{2,}\s?/ do |match|
|
30
|
+
preserve_border_whitespaces(match, default_border: ' ') do
|
31
|
+
match.strip.sub('** ', '**').sub(' **', '**')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
result = result.gsub /\s?\_{2,}.*?\_{2,}\s?/ do |match|
|
36
|
+
preserve_border_whitespaces(match, default_border: ' ') do
|
37
|
+
match.strip.sub('__ ', '__').sub(' __', '__')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
result.gsub /\s?\[.*?\]\s?/ do |match|
|
42
|
+
preserve_border_whitespaces(match) do
|
43
|
+
match.strip.sub('[ ', '[').sub(' ]', ']')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def preserve_border_whitespaces(string, default_border: '', &block)
|
51
|
+
string_start = present_or_default(string[/\A\s*/], default_border)
|
52
|
+
string_end = present_or_default(string[/\s*\Z/], default_border)
|
53
|
+
result = yield
|
54
|
+
string_start + result + string_end
|
55
|
+
end
|
56
|
+
|
57
|
+
def present_or_default(string, default)
|
58
|
+
if string.nil? || string.empty?
|
59
|
+
default
|
60
|
+
else
|
61
|
+
string
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
class Config
|
3
|
+
attr_accessor :ignore_unknown_tags, :github_flavored
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
reset
|
7
|
+
end
|
8
|
+
|
9
|
+
def apply(options = {})
|
10
|
+
options.each do |method, value|
|
11
|
+
__send__(:"#{method}=", value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset
|
16
|
+
@ignore_unknown_tags = true
|
17
|
+
@github_flavored = false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class A < Base
|
4
|
+
def convert(node)
|
5
|
+
name = treat_children(node)
|
6
|
+
href = node['href']
|
7
|
+
title = extract_title(node)
|
8
|
+
|
9
|
+
if href.to_s.start_with?('#') || href.to_s.empty? || name.empty?
|
10
|
+
name
|
11
|
+
else
|
12
|
+
" [#{name}](#{href}#{title})"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
register :a, A.new
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class Base
|
4
|
+
def treat_children(node)
|
5
|
+
node.children.inject('') do |memo, child|
|
6
|
+
memo << treat(child)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def treat(node)
|
11
|
+
ReverseMarkdown::Converters.lookup(node.name).convert(node)
|
12
|
+
end
|
13
|
+
|
14
|
+
def escape_keychars(string)
|
15
|
+
string.gsub(/[\*\_]/, '*' => '\*', '_' => '\_')
|
16
|
+
end
|
17
|
+
|
18
|
+
def extract_title(node)
|
19
|
+
title = escape_keychars(node['title'].to_s)
|
20
|
+
title.empty? ? '' : %[ "#{title}"]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class Blockquote < Base
|
4
|
+
def convert(node)
|
5
|
+
content = treat_children(node).strip
|
6
|
+
content = ReverseMarkdown.cleaner.remove_newlines(content)
|
7
|
+
'> ' << content.lines.join('> ')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
register :blockquote, Blockquote.new
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class Em < Base
|
4
|
+
def convert(node)
|
5
|
+
content = treat_children(node)
|
6
|
+
if content.strip.empty? || already_italic?(node)
|
7
|
+
content
|
8
|
+
else
|
9
|
+
"_#{content}_"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def already_italic?(node)
|
14
|
+
node.ancestors('italic').size > 0 || node.ancestors('em').size > 0
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
register :em, Em.new
|
19
|
+
register :i, Em.new
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class H < Base
|
4
|
+
def convert(node)
|
5
|
+
prefix = '#' * node.name[/\d/].to_i
|
6
|
+
["\n", prefix, ' ', treat_children(node), "\n"].join
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
register :h1, H.new
|
11
|
+
register :h2, H.new
|
12
|
+
register :h3, H.new
|
13
|
+
register :h4, H.new
|
14
|
+
register :h5, H.new
|
15
|
+
register :h6, H.new
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class Li < Base
|
4
|
+
def convert(node)
|
5
|
+
content = treat_children(node)
|
6
|
+
indentation = indentation_for(node)
|
7
|
+
prefix = prefix_for(node)
|
8
|
+
"#{indentation}#{prefix}#{content}\n"
|
9
|
+
end
|
10
|
+
|
11
|
+
def prefix_for(node)
|
12
|
+
if node.parent.name == 'ol'
|
13
|
+
index = node.parent.xpath('li').index(node)
|
14
|
+
"#{index.to_i + 1}. "
|
15
|
+
else
|
16
|
+
'- '
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def indentation_for(node)
|
21
|
+
length = node.ancestors('ol').size + node.ancestors('ul').size
|
22
|
+
' ' * (length - 1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
register :li, Li.new
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class PassThrough < Base
|
4
|
+
def convert(node)
|
5
|
+
treat_children(node)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
register :document, PassThrough.new
|
10
|
+
register :html, PassThrough.new
|
11
|
+
register :body, PassThrough.new
|
12
|
+
register :span, PassThrough.new
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module ReverseMarkdown
|
4
|
+
module Converters
|
5
|
+
class Pre < Base
|
6
|
+
def convert(node)
|
7
|
+
if ReverseMarkdown.config.github_flavored
|
8
|
+
"```\n" << node.text.strip << "\n```\n"
|
9
|
+
else
|
10
|
+
"\n\n " << node.text.strip.lines.join(" ") << "\n\n"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
register :pre, Pre.new
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class Strong < Base
|
4
|
+
def convert(node)
|
5
|
+
content = treat_children(node)
|
6
|
+
if content.strip.empty? || already_strong?(node)
|
7
|
+
content
|
8
|
+
else
|
9
|
+
"**#{content}**"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def already_strong?(node)
|
14
|
+
node.ancestors('strong').size > 0 || node.ancestors('b').size > 0
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
register :strong, Strong.new
|
19
|
+
register :b, Strong.new
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class Text < Base
|
4
|
+
def convert(node, options = {})
|
5
|
+
if node.text.strip.empty?
|
6
|
+
treat_empty(node)
|
7
|
+
else
|
8
|
+
treat_text(node)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def treat_empty(node)
|
13
|
+
parent = node.parent.name.to_sym
|
14
|
+
if [:ol, :ul].include?(parent) # Otherwise the identation is broken
|
15
|
+
''
|
16
|
+
elsif node.text == ' ' # Regular whitespace text node
|
17
|
+
' '
|
18
|
+
else
|
19
|
+
''
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def treat_text(node)
|
24
|
+
escape_keychars node.text.tr("\n\t", '').squeeze(' ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
register :text, Text.new
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ReverseMarkdown
|
2
|
+
module Converters
|
3
|
+
class Tr < Base
|
4
|
+
def convert(node)
|
5
|
+
content = treat_children(node).rstrip
|
6
|
+
result = "|#{content}\n"
|
7
|
+
table_header_row?(node) ? result + underline_for(node) : result
|
8
|
+
end
|
9
|
+
|
10
|
+
def table_header_row?(node)
|
11
|
+
node.element_children.all? {|child| child.name.to_sym == :th}
|
12
|
+
end
|
13
|
+
|
14
|
+
def underline_for(node)
|
15
|
+
"| " + (['---'] * node.element_children.size).join(' | ') + " |\n"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
register :tr, Tr.new
|
20
|
+
end
|
21
|
+
end
|