qiita-markdown 0.44.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -2
  3. data/.rubocop.yml +0 -4
  4. data/.rubocop_todo.yml +15 -239
  5. data/CHANGELOG.md +15 -0
  6. data/README.md +5 -3
  7. data/lib/qiita/markdown/filters/checkbox.rb +5 -1
  8. data/lib/qiita/markdown/filters/code_block.rb +13 -13
  9. data/lib/qiita/markdown/filters/custom_block.rb +8 -6
  10. data/lib/qiita/markdown/filters/external_link.rb +2 -0
  11. data/lib/qiita/markdown/filters/final_sanitizer.rb +126 -120
  12. data/lib/qiita/markdown/filters/footnote.rb +2 -0
  13. data/lib/qiita/markdown/filters/group_mention.rb +2 -2
  14. data/lib/qiita/markdown/filters/heading_anchor.rb +44 -0
  15. data/lib/qiita/markdown/filters/html_toc.rb +67 -0
  16. data/lib/qiita/markdown/filters/image_link.rb +6 -6
  17. data/lib/qiita/markdown/filters/inline_code_color.rb +8 -8
  18. data/lib/qiita/markdown/filters/mention.rb +11 -9
  19. data/lib/qiita/markdown/filters/qiita_marker.rb +55 -0
  20. data/lib/qiita/markdown/filters/simplify.rb +1 -0
  21. data/lib/qiita/markdown/filters/syntax_highlight.rb +4 -4
  22. data/lib/qiita/markdown/filters/truncate.rb +1 -3
  23. data/lib/qiita/markdown/filters/user_input_sanitizer.rb +34 -29
  24. data/lib/qiita/markdown/processor.rb +2 -1
  25. data/lib/qiita/markdown/summary_processor.rb +1 -1
  26. data/lib/qiita/markdown/transformers/filter_attributes.rb +1 -0
  27. data/lib/qiita/markdown/transformers/filter_iframe.rb +1 -2
  28. data/lib/qiita/markdown/transformers/filter_script.rb +1 -1
  29. data/lib/qiita/markdown/transformers/strip_invalid_node.rb +1 -3
  30. data/lib/qiita/markdown/version.rb +1 -1
  31. data/lib/qiita/markdown.rb +4 -5
  32. data/qiita-markdown.gemspec +7 -8
  33. data/spec/qiita/markdown/filters/checkbox_spec.rb +28 -0
  34. data/spec/qiita/markdown/filters/heading_anchor_spec.rb +73 -0
  35. data/spec/qiita/markdown/filters/html_toc_spec.rb +223 -0
  36. data/spec/qiita/markdown/filters/qiita_marker_spec.rb +60 -0
  37. data/spec/qiita/markdown/processor_spec.rb +64 -70
  38. data/spec/qiita/markdown/summary_processor_spec.rb +4 -4
  39. metadata +80 -102
  40. data/benchmark/heading_anchor_rendering.rb +0 -248
  41. data/benchmark/sample.md +0 -317
  42. data/lib/qiita/markdown/filters/greenmat.rb +0 -38
  43. data/lib/qiita/markdown/greenmat/heading_rendering.rb +0 -61
  44. data/lib/qiita/markdown/greenmat/html_renderer.rb +0 -60
  45. data/lib/qiita/markdown/greenmat/html_toc_renderer.rb +0 -78
  46. data/spec/qiita/markdown/filters/greenmat_spec.rb +0 -15
  47. data/spec/qiita/markdown/greenmat/html_toc_renderer_spec.rb +0 -156
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Qiita
2
4
  module Markdown
3
5
  module Filters
@@ -9,44 +11,45 @@ module Qiita
9
11
  # generated by other filters.
10
12
  #
11
13
  # @see Qiita::Markdown::Filters::UserInputSanitizerr
12
- class FinalSanitizer < HTML::Pipeline::Filter
14
+ class FinalSanitizer < ::HTML::Pipeline::Filter
13
15
  RULE = {
14
16
  attributes: {
15
- "a" => [
16
- "data-hovercard-target-name",
17
- "data-hovercard-target-type",
18
- "href",
19
- "rel",
17
+ "a" => %w[
18
+ data-hovercard-target-name
19
+ data-hovercard-target-type
20
+ href
21
+ rel
20
22
  ],
21
23
  "blockquote" => Embed::Tweet::ATTRIBUTES,
22
- "iframe" => [
23
- "allowfullscreen",
24
- "frameborder",
25
- "height",
26
- "marginheight",
27
- "marginwidth",
28
- "scrolling",
29
- "src",
30
- "style",
31
- "width",
24
+ "iframe" => %w[
25
+ allowfullscreen
26
+ frameborder
27
+ height
28
+ loading
29
+ marginheight
30
+ marginwidth
31
+ scrolling
32
+ src
33
+ style
34
+ width
32
35
  ],
33
36
  "img" => [
34
37
  "src",
35
38
  ],
36
- "input" => [
37
- "checked",
38
- "disabled",
39
- "type",
39
+ "input" => %w[
40
+ checked
41
+ disabled
42
+ type
40
43
  ],
41
- "div" => [
42
- "itemscope",
43
- "itemtype",
44
+ "div" => %w[
45
+ itemscope
46
+ itemtype
44
47
  ],
45
48
  "p" => Embed::CodePen::ATTRIBUTES,
46
- "script" => [
47
- "async",
48
- "src",
49
- "type",
49
+ "script" => %w[
50
+ async
51
+ src
52
+ type
50
53
  ].concat(
51
54
  Embed::SpeekerDeck::ATTRIBUTES,
52
55
  Embed::Docswell::ATTRIBUTES,
@@ -60,104 +63,107 @@ module Qiita
60
63
  "th" => [
61
64
  "style",
62
65
  ],
63
- "video" => [
64
- "src",
65
- "autoplay",
66
- "controls",
67
- "loop",
68
- "muted",
69
- "poster",
66
+ "video" => %w[
67
+ src
68
+ autoplay
69
+ controls
70
+ loop
71
+ muted
72
+ poster
70
73
  ],
71
- all: [
72
- "abbr",
73
- "align",
74
- "alt",
75
- "border",
76
- "cellpadding",
77
- "cellspacing",
78
- "cite",
79
- "class",
80
- "color",
81
- "cols",
82
- "colspan",
83
- "data-lang",
84
- "datetime",
85
- "height",
86
- "hreflang",
87
- "id",
88
- "itemprop",
89
- "lang",
90
- "name",
91
- "rowspan",
92
- "tabindex",
93
- "target",
94
- "title",
95
- "width",
74
+ all: %w[
75
+ abbr
76
+ align
77
+ alt
78
+ border
79
+ cellpadding
80
+ cellspacing
81
+ cite
82
+ class
83
+ color
84
+ cols
85
+ colspan
86
+ data-lang
87
+ data-sourcepos
88
+ datetime
89
+ height
90
+ hreflang
91
+ id
92
+ itemprop
93
+ lang
94
+ name
95
+ rowspan
96
+ tabindex
97
+ target
98
+ title
99
+ width
96
100
  ],
97
101
  },
98
102
  css: {
99
- properties: [
100
- "background-color",
101
- "border",
102
- "text-align",
103
+ properties: %w[
104
+ background-color
105
+ border
106
+ text-align
103
107
  ],
104
108
  },
105
- elements: [
106
- "a",
107
- "b",
108
- "blockquote",
109
- "br",
110
- "code",
111
- "dd",
112
- "del",
113
- "details",
114
- "div",
115
- "dl",
116
- "dt",
117
- "em",
118
- "font",
119
- "h1",
120
- "h2",
121
- "h3",
122
- "h4",
123
- "h5",
124
- "h6",
125
- "h7",
126
- "h8",
127
- "hr",
128
- "i",
129
- "img",
130
- "input",
131
- "ins",
132
- "kbd",
133
- "li",
134
- "ol",
135
- "p",
136
- "pre",
137
- "q",
138
- "rp",
139
- "rt",
140
- "ruby",
141
- "s",
142
- "samp",
143
- "script",
144
- "iframe",
145
- "span",
146
- "strike",
147
- "strong",
148
- "sub",
149
- "summary",
150
- "sup",
151
- "table",
152
- "tbody",
153
- "td",
154
- "tfoot",
155
- "th",
156
- "thead",
157
- "tr",
158
- "tt",
159
- "ul",
160
- "var",
109
+ elements: %w[
110
+ a
111
+ b
112
+ blockquote
113
+ br
114
+ caption
115
+ code
116
+ dd
117
+ del
118
+ details
119
+ div
120
+ dl
121
+ dt
122
+ em
123
+ font
124
+ h1
125
+ h2
126
+ h3
127
+ h4
128
+ h5
129
+ h6
130
+ h7
131
+ h8
132
+ hr
133
+ i
134
+ img
135
+ input
136
+ ins
137
+ kbd
138
+ li
139
+ ol
140
+ p
141
+ pre
142
+ q
143
+ rp
144
+ rt
145
+ ruby
146
+ s
147
+ samp
148
+ script
149
+ iframe
150
+ section
151
+ span
152
+ strike
153
+ strong
154
+ sub
155
+ summary
156
+ sup
157
+ table
158
+ tbody
159
+ td
160
+ tfoot
161
+ th
162
+ thead
163
+ tr
164
+ tt
165
+ ul
166
+ var
161
167
  ],
162
168
  protocols: {
163
169
  "a" => {
@@ -200,7 +206,7 @@ module Qiita
200
206
  rule[:attributes][:all] = rule[:attributes][:all] + [:data]
201
207
  rule[:elements] = RULE[:elements] + ["video"]
202
208
  rule[:transformers] = rule[:transformers] - [Transformers::FilterScript, Transformers::FilterIframe]
203
- end
209
+ end.freeze
204
210
 
205
211
  def call
206
212
  ::Sanitize.clean_node!(doc, rule)
@@ -6,6 +6,7 @@ module Qiita
6
6
  doc.search("sup > a").each do |a|
7
7
  footnote = find_footnote(a)
8
8
  next unless footnote
9
+
9
10
  a[:title] = footnote.text.gsub(/\A\n/, "").gsub(/ ↩\n\z/, "")
10
11
  end
11
12
  doc
@@ -16,6 +17,7 @@ module Qiita
16
17
  def find_footnote(a)
17
18
  href = a["href"]
18
19
  return nil if !href || href.match(/\A#fn\d+\z/).nil?
20
+
19
21
  doc.search(href).first
20
22
  end
21
23
  end
@@ -21,9 +21,9 @@ module Qiita
21
21
  GROUP_IDENTIFIER_PATTERN = %r{
22
22
  (?:^|\W)
23
23
  @((?>[a-z\d][a-z\d-]{2,31}))
24
- \/
24
+ /
25
25
  ([A-Za-z\d][A-Za-z\d-]{0,62}[A-Za-z\d])
26
- (?!\/)
26
+ (?!/)
27
27
  (?=
28
28
  \.+[ \t\W]|
29
29
  \.+$|
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qiita
4
+ module Markdown
5
+ module Filters
6
+ class HeadingAnchor < ::HTML::Pipeline::Filter
7
+ def call
8
+ doc.search("h1, h2, h3, h4, h5, h6").each do |heading|
9
+ heading["id"] = suffixed_id(heading)
10
+ end
11
+
12
+ doc
13
+ end
14
+
15
+ private
16
+
17
+ def counter
18
+ @counter ||= ::Hash.new(0)
19
+ end
20
+
21
+ def get_count(id)
22
+ counter[id]
23
+ end
24
+
25
+ def increment_count(id)
26
+ counter[id] += 1
27
+ end
28
+
29
+ def heading_id(node)
30
+ node.text.downcase.gsub(/[^\p{Word}\- ]/u, "").tr(" ", "-")
31
+ end
32
+
33
+ def suffixed_id(node)
34
+ id = heading_id(node)
35
+ count = get_count(id)
36
+ suffix = count.positive? ? "-#{count}" : ""
37
+ increment_count(id)
38
+
39
+ "#{id}#{suffix}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qiita
4
+ module Markdown
5
+ module Filters
6
+ class HtmlToc < ::HTML::Pipeline::Filter
7
+ # @return [Nokogiri::HTML::DocumentFragment]
8
+ def call
9
+ headings = doc.search("h1, h2, h3, h4, h5, h6")
10
+ return "" if headings.empty?
11
+
12
+ toc = %W[<ul>\n]
13
+ top_level = nil
14
+ last_level = nil
15
+ depth = 1
16
+
17
+ headings.each do |node|
18
+ heading_rank = node.name.match(/h(\d)/)[1].to_i
19
+
20
+ # The first heading is displayed as the top level.
21
+ # The following headings, of higher rank than the first, are placed as top level.
22
+ top_level ||= heading_rank
23
+ current_level = [heading_rank, top_level].max
24
+
25
+ link = toc_with_link(node.text, node.attributes["id"]&.value)
26
+ toc << (nest_string(last_level, current_level) + link)
27
+
28
+ depth += current_level - last_level if last_level
29
+
30
+ last_level = current_level
31
+ end
32
+
33
+ toc << ("</li>\n</ul>\n" * depth)
34
+ toc.join
35
+ end
36
+
37
+ private
38
+
39
+ # @param text [String]
40
+ # @param id [String]
41
+ # @return [String]
42
+ def toc_with_link(text, id)
43
+ %(<a href="##{id}">#{CGI.escapeHTML(text)}</a>\n)
44
+ end
45
+
46
+ # @param last_level [Integer, nil]
47
+ # @param current_level [Integer]
48
+ # @return [String]
49
+ def nest_string(last_level, current_level)
50
+ if last_level.nil?
51
+ return "<li>\n"
52
+ elsif current_level == last_level
53
+ return "</li>\n<li>\n"
54
+ elsif current_level > last_level
55
+ level_difference = current_level - last_level
56
+ return "<ul>\n<li>\n" * level_difference
57
+ elsif current_level < last_level
58
+ level_difference = last_level - current_level
59
+ return %(#{"</li>\n</ul>\n" * level_difference}</li>\n<li>\n)
60
+ end
61
+
62
+ ""
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -4,12 +4,12 @@ module Qiita
4
4
  class ImageLink < HTML::Pipeline::Filter
5
5
  def call
6
6
  doc.search("img").each do |img|
7
- unless img.ancestors.any? { |ancestor| ancestor.name == "a" }
8
- outer = Nokogiri::HTML.fragment(%(<a href="#{img['src']}" target="_blank"></a>))
9
- inner = img.clone
10
- outer.at("a").add_child(inner)
11
- img.replace(outer)
12
- end
7
+ next if img.ancestors.any? { |ancestor| ancestor.name == "a" }
8
+
9
+ outer = Nokogiri::HTML.fragment(%(<a href="#{img['src']}" target="_blank"></a>))
10
+ inner = img.clone
11
+ outer.at("a").add_child(inner)
12
+ img.replace(outer)
13
13
  end
14
14
  doc
15
15
  end
@@ -6,15 +6,15 @@ module Qiita
6
6
 
7
7
  REGEXPS = Regexp.union(
8
8
  /\#(?:\h{3}|\h{6})/,
9
- /rgba?\(\s*(?:\d+(?:\,|\s)\s*){2}\d+\s*\)/,
10
- /rgba?\(\s*(?:\d+%(?:\,|\s)\s*){2}\d+%\s*\)/,
11
- /rgba?\(\s*(?:\d+\,\s*){3}\d*\.?\d+%?\s*\)/,
12
- /rgba?\(\s*(?:\d+\s*){2}\d+\s*\/\s*\d?\.?\d+%?\s*\)/,
13
- /rgba?\(\s*(?:\d+%\s*){2}\d+%\s*\/\s*\d?\.?\d+%?\s*\)/,
14
- /hsla?\(\s*\d+(?:deg|rad|grad|turn)?\,\s*\d+%\,\s*\d+%\s*\)/,
9
+ /rgba?\(\s*(?:\d+(?:,|\s)\s*){2}\d+\s*\)/,
10
+ /rgba?\(\s*(?:\d+%(?:,|\s)\s*){2}\d+%\s*\)/,
11
+ /rgba?\(\s*(?:\d+,\s*){3}\d*\.?\d+%?\s*\)/,
12
+ %r{rgba?\(\s*(?:\d+\s*){2}\d+\s*/\s*\d?\.?\d+%?\s*\)},
13
+ %r{rgba?\(\s*(?:\d+%\s*){2}\d+%\s*/\s*\d?\.?\d+%?\s*\)},
14
+ /hsla?\(\s*\d+(?:deg|rad|grad|turn)?,\s*\d+%,\s*\d+%\s*\)/,
15
15
  /hsla?\(\s*\d+(?:deg|rad|grad|turn)?\s+\d+%\s+\d+%\s*\)/,
16
- /hsla?\(\s*\d+(?:deg|rad|grad|turn)?\,\s*(?:\d+%\,\s*){2}\d?\.?\d+%?\s*\)/,
17
- /hsla?\(\s*\d+(?:deg|rad|grad|turn)?\s+\d+%\s+\d+%\s*\/\s*\d?\.?\d+%?\s*\)/,
16
+ /hsla?\(\s*\d+(?:deg|rad|grad|turn)?,\s*(?:\d+%,\s*){2}\d?\.?\d+%?\s*\)/,
17
+ %r{hsla?\(\s*\d+(?:deg|rad|grad|turn)?\s+\d+%\s+\d+%\s*/\s*\d?\.?\d+%?\s*\)},
18
18
  )
19
19
 
20
20
  COLOR_CODE_PATTERN = /\A\s*(#{REGEXPS})\s*\z/
@@ -8,17 +8,17 @@ module Qiita
8
8
  class Mention < HTML::Pipeline::MentionFilter
9
9
  IGNORE_PARENTS = ::HTML::Pipeline::MentionFilter::IGNORE_PARENTS + Set["blockquote"]
10
10
 
11
- MentionPattern = /
11
+ MentionPattern = %r{
12
12
  (?:^|\W)
13
- @((?>[\w][\w-]{0,30}\w(?:@github)?))
14
- (?!\/)
13
+ @((?>\w[\w-]{0,30}\w(?:@github)?))
14
+ (?!/)
15
15
  (?=
16
16
  \.+[ \t\W]|
17
17
  \.+$|
18
18
  [^0-9a-zA-Z_.]|
19
19
  $
20
20
  )
21
- /ix
21
+ }ix
22
22
 
23
23
  # @note Override to use another IGNORE_PARENTS
24
24
  def call
@@ -28,8 +28,10 @@ module Qiita
28
28
  content = node.to_html
29
29
  next unless content.include?("@")
30
30
  next if has_ancestor?(node, IGNORE_PARENTS)
31
+
31
32
  html = mention_link_filter(content, base_url, info_url, username_pattern)
32
33
  next if html == content
34
+
33
35
  node.replace(html)
34
36
  end
35
37
  doc
@@ -38,22 +40,22 @@ module Qiita
38
40
  # @note Override to use customized MentionPattern and allowed_usernames logic.
39
41
  def mention_link_filter(text, _, _, _)
40
42
  text.gsub(MentionPattern) do |match|
41
- name = $1
43
+ name = ::Regexp.last_match(1)
42
44
  case
43
45
  when allowed_usernames && name == "all"
44
46
  result[:mentioned_usernames] |= allowed_usernames
45
47
  match.sub(
46
48
  "@#{name}",
47
- %[<a href="/" class="user-mention" title="#{name}">@#{name}</a>]
49
+ %(<a href="/" class="user-mention" title="#{name}">@#{name}</a>),
48
50
  )
49
- when allowed_usernames && !allowed_usernames.include?(name) || name == "all"
51
+ when (allowed_usernames && !allowed_usernames.include?(name)) || name == "all"
50
52
  match
51
53
  else
52
54
  result[:mentioned_usernames] |= [name]
53
55
  url = File.join(base_url, name)
54
56
  match.sub(
55
57
  "@#{name}",
56
- %[<a href="#{url}" class="user-mention js-hovercard" title="#{name}" data-hovercard-target-type="user" data-hovercard-target-name="#{name}">@#{name}</a>]
58
+ %(<a href="#{url}" class="user-mention js-hovercard" title="#{name}" data-hovercard-target-type="user" data-hovercard-target-name="#{name}">@#{name}</a>),
57
59
  )
58
60
  end
59
61
  end
@@ -66,7 +68,7 @@ module Qiita
66
68
  end
67
69
 
68
70
  def has_ancestor?(node, tags)
69
- super || node.parent.parent && node.parent.parent["class"] == "code-lang"
71
+ super || (node.parent.parent && node.parent.parent["class"] == "code-lang")
70
72
  end
71
73
  end
72
74
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qiita
4
+ module Markdown
5
+ module Filters
6
+ class QiitaMarker < ::HTML::Pipeline::TextFilter
7
+ DEFAULT_OPTIONS = {
8
+ footnotes: true,
9
+ sourcepos: false,
10
+ }.freeze
11
+
12
+ # @return [Nokogiri::HTML::DocumentFragment]
13
+ def call
14
+ ::Nokogiri::HTML.fragment(render(@text))
15
+ end
16
+
17
+ private
18
+
19
+ # @param text [String]
20
+ # @return [String]
21
+ def render(text)
22
+ ::QiitaMarker.render_html(text, qiita_marker_options, qiita_marker_extensions)
23
+ end
24
+
25
+ def qiita_marker_options
26
+ options_to_append = (options[:footnotes] ? [:FOOTNOTES] : [])
27
+ .concat(options[:sourcepos] ? [:SOURCEPOS] : [])
28
+ @qiita_marker_options ||= %i[
29
+ HARDBREAKS
30
+ UNSAFE
31
+ LIBERAL_HTML_TAG
32
+ STRIKETHROUGH_DOUBLE_TILDE
33
+ TABLE_PREFER_STYLE_ATTRIBUTES
34
+ CODE_DATA_METADATA
35
+ MENTION_NO_EMPHASIS
36
+ AUTOLINK_CLASS_NAME
37
+ ].concat(options_to_append)
38
+ end
39
+
40
+ def qiita_marker_extensions
41
+ @qiita_marker_extensions ||= %i[
42
+ table
43
+ strikethrough
44
+ autolink
45
+ custom_block
46
+ ]
47
+ end
48
+
49
+ def options
50
+ @options ||= DEFAULT_OPTIONS.merge(context[:markdown] || {})
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -38,6 +38,7 @@ module Qiita
38
38
  doc.traverse do |node|
39
39
  next unless node.element?
40
40
  next if SIMPLE_ELEMENTS.include?(node.name)
41
+
41
42
  node.replace(node.children)
42
43
  end
43
44
  end
@@ -54,7 +54,7 @@ module Qiita
54
54
  end
55
55
 
56
56
  def call
57
- outer = Nokogiri::HTML.fragment(%Q[<div class="code-frame" data-lang="#{language}">])
57
+ outer = Nokogiri::HTML.fragment(%(<div class="code-frame" data-lang="#{language}">))
58
58
  frame = outer.at("div")
59
59
  frame.add_child(filename_node) if filename
60
60
  frame.add_child(highlighted_node)
@@ -72,7 +72,7 @@ module Qiita
72
72
  end
73
73
 
74
74
  def filename_node
75
- %Q[<div class="code-lang"><span class="bold">#{filename}</span></div>]
75
+ %(<div class="code-lang"><span class="bold">#{filename}</span></div>)
76
76
  end
77
77
 
78
78
  def has_inline_php?
@@ -87,7 +87,7 @@ module Qiita
87
87
  if specific_language && Rouge::Lexer.find(specific_language)
88
88
  begin
89
89
  highlight(specific_language).presence or raise
90
- rescue
90
+ rescue StandardError
91
91
  highlight(@default_language)
92
92
  end
93
93
  else
@@ -100,7 +100,7 @@ module Qiita
100
100
  end
101
101
 
102
102
  def language_node
103
- Nokogiri::HTML.fragment(%Q[<div class="code-frame" data-lang="#{language}"></div>])
103
+ Nokogiri::HTML.fragment(%(<div class="code-frame" data-lang="#{language}"></div>))
104
104
  end
105
105
 
106
106
  def specific_language
@@ -51,9 +51,7 @@ module Qiita
51
51
  node.content.each_char.with_index do |char, index|
52
52
  current_char_is_blank = char.strip.empty?
53
53
 
54
- if !@previous_char_was_blank || !current_char_is_blank
55
- @current_length += 1
56
- end
54
+ @current_length += 1 if !@previous_char_was_blank || !current_char_is_blank
57
55
 
58
56
  @previous_char_was_blank = current_char_is_blank
59
57