motion-markdown-it 4.4.0 → 8.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -16
  3. data/lib/motion-markdown-it.rb +7 -5
  4. data/lib/motion-markdown-it/common/html_blocks.rb +6 -2
  5. data/lib/motion-markdown-it/common/utils.rb +19 -4
  6. data/lib/motion-markdown-it/helpers/helper_wrapper.rb +9 -0
  7. data/lib/motion-markdown-it/helpers/parse_link_destination.rb +8 -7
  8. data/lib/motion-markdown-it/index.rb +60 -18
  9. data/lib/motion-markdown-it/parser_block.rb +7 -10
  10. data/lib/motion-markdown-it/parser_inline.rb +50 -14
  11. data/lib/motion-markdown-it/presets/commonmark.rb +7 -1
  12. data/lib/motion-markdown-it/presets/default.rb +4 -3
  13. data/lib/motion-markdown-it/presets/zero.rb +6 -1
  14. data/lib/motion-markdown-it/renderer.rb +46 -14
  15. data/lib/motion-markdown-it/rules_block/blockquote.rb +167 -31
  16. data/lib/motion-markdown-it/rules_block/code.rb +4 -3
  17. data/lib/motion-markdown-it/rules_block/fence.rb +9 -4
  18. data/lib/motion-markdown-it/rules_block/heading.rb +8 -3
  19. data/lib/motion-markdown-it/rules_block/hr.rb +10 -5
  20. data/lib/motion-markdown-it/rules_block/html_block.rb +6 -3
  21. data/lib/motion-markdown-it/rules_block/lheading.rb +64 -26
  22. data/lib/motion-markdown-it/rules_block/list.rb +91 -22
  23. data/lib/motion-markdown-it/rules_block/paragraph.rb +14 -9
  24. data/lib/motion-markdown-it/rules_block/reference.rb +24 -14
  25. data/lib/motion-markdown-it/rules_block/state_block.rb +79 -24
  26. data/lib/motion-markdown-it/rules_block/table.rb +52 -26
  27. data/lib/motion-markdown-it/rules_core/normalize.rb +1 -23
  28. data/lib/motion-markdown-it/rules_core/replacements.rb +22 -2
  29. data/lib/motion-markdown-it/rules_core/smartquotes.rb +41 -12
  30. data/lib/motion-markdown-it/rules_inline/autolink.rb +5 -4
  31. data/lib/motion-markdown-it/rules_inline/balance_pairs.rb +48 -0
  32. data/lib/motion-markdown-it/rules_inline/emphasis.rb +104 -149
  33. data/lib/motion-markdown-it/rules_inline/entity.rb +2 -2
  34. data/lib/motion-markdown-it/rules_inline/escape.rb +5 -3
  35. data/lib/motion-markdown-it/rules_inline/image.rb +12 -23
  36. data/lib/motion-markdown-it/rules_inline/link.rb +20 -25
  37. data/lib/motion-markdown-it/rules_inline/newline.rb +2 -1
  38. data/lib/motion-markdown-it/rules_inline/state_inline.rb +60 -1
  39. data/lib/motion-markdown-it/rules_inline/strikethrough.rb +81 -97
  40. data/lib/motion-markdown-it/rules_inline/text_collapse.rb +40 -0
  41. data/lib/motion-markdown-it/token.rb +46 -1
  42. data/lib/motion-markdown-it/version.rb +1 -1
  43. data/spec/motion-markdown-it/markdown_it_spec.rb +2 -2
  44. data/spec/motion-markdown-it/misc_spec.rb +90 -14
  45. data/spec/motion-markdown-it/testgen_helper.rb +1 -1
  46. data/spec/spec_helper.rb +2 -3
  47. metadata +13 -13
  48. data/lib/motion-markdown-it/common/url_schemas.rb +0 -173
  49. data/spec/motion-markdown-it/bench_mark_spec.rb +0 -44
@@ -10,15 +10,18 @@ module MarkdownIt
10
10
  terminatorRules = state.md.block.ruler.getRules('paragraph')
11
11
  endLine = state.lineMax
12
12
 
13
+ oldParentType = state.parentType
14
+ state.parentType = 'paragraph'
15
+
13
16
  # jump line-by-line until empty one or EOF
14
17
  # for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
15
18
  while nextLine < endLine && !state.isEmpty(nextLine)
16
19
  # this would be a code block normally, but after paragraph
17
20
  # it's considered a lazy continuation regardless of what's there
18
- (nextLine += 1) && next if (state.tShift[nextLine] - state.blkIndent > 3)
21
+ (nextLine += 1) and next if (state.sCount[nextLine] - state.blkIndent > 3)
19
22
 
20
23
  # quirk for blockquotes, this line should already be checked by that rule
21
- (nextLine += 1) && next if state.tShift[nextLine] < 0
24
+ (nextLine += 1) and next if state.sCount[nextLine] < 0
22
25
 
23
26
  # Some tags can terminate paragraph without empty line.
24
27
  terminate = false
@@ -36,15 +39,17 @@ module MarkdownIt
36
39
 
37
40
  state.line = nextLine
38
41
 
39
- token = state.push('paragraph_open', 'p', 1)
40
- token.map = [ startLine, state.line ]
42
+ token = state.push('paragraph_open', 'p', 1)
43
+ token.map = [ startLine, state.line ]
44
+
45
+ token = state.push('inline', '', 0)
46
+ token.content = content
47
+ token.map = [ startLine, state.line ]
48
+ token.children = []
41
49
 
42
- token = state.push('inline', '', 0)
43
- token.content = content
44
- token.map = [ startLine, state.line ]
45
- token.children = []
50
+ token = state.push('paragraph_close', 'p', -1)
46
51
 
47
- token = state.push('paragraph_close', 'p', -1)
52
+ state.parentType = oldParentType
48
53
 
49
54
  return true
50
55
  end
@@ -1,10 +1,8 @@
1
1
  module MarkdownIt
2
2
  module RulesBlock
3
3
  class Reference
4
- extend Helpers::ParseLinkDestination
5
- extend Helpers::ParseLinkTitle
6
4
  extend Common::Utils
7
-
5
+
8
6
  #------------------------------------------------------------------------------
9
7
  def self.reference(state, startLine, _endLine, silent)
10
8
  lines = 0
@@ -12,7 +10,10 @@ module MarkdownIt
12
10
  max = state.eMarks[startLine]
13
11
  nextLine = startLine + 1
14
12
 
15
- return false if (state.src.charCodeAt(pos) != 0x5B) # [
13
+ # if it's indented more than 3 spaces, it should be a code block
14
+ return false if state.sCount[startLine] - state.blkIndent >= 4
15
+
16
+ return false if state.src.charCodeAt(pos) != 0x5B # [
16
17
 
17
18
  # Simple check to quickly interrupt scan on [link](url) at the start of line.
18
19
  # Can be useful on practice: https://github.com/markdown-it/markdown-it/issues/54
@@ -30,15 +31,18 @@ module MarkdownIt
30
31
  endLine = state.lineMax
31
32
 
32
33
  # jump line-by-line until empty one or EOF
33
- terminatorRules = state.md.block.ruler.getRules('reference')
34
+ terminatorRules = state.md.block.ruler.getRules('reference')
35
+
36
+ oldParentType = state.parentType
37
+ state.parentType = 'reference'
34
38
 
35
39
  while nextLine < endLine && !state.isEmpty(nextLine)
36
40
  # this would be a code block normally, but after paragraph
37
41
  # it's considered a lazy continuation regardless of what's there
38
- (nextLine += 1) && next if (state.tShift[nextLine] - state.blkIndent > 3)
42
+ (nextLine += 1) and next if (state.sCount[nextLine] - state.blkIndent > 3)
39
43
 
40
44
  # quirk for blockquotes, this line should already be checked by that rule
41
- (nextLine += 1) && next if state.tShift[nextLine] < 0
45
+ (nextLine += 1) and next if state.sCount[nextLine] < 0
42
46
 
43
47
  # Some tags can terminate paragraph without empty line.
44
48
  terminate = false
@@ -59,7 +63,7 @@ module MarkdownIt
59
63
  pos = 1
60
64
  while pos < max
61
65
  ch = str.charCodeAt(pos)
62
- if (ch == 0x5B ) # [
66
+ if (ch == 0x5B ) # [
63
67
  return false
64
68
  elsif (ch == 0x5D) # ]
65
69
  labelEnd = pos
@@ -84,7 +88,7 @@ module MarkdownIt
84
88
  ch = str.charCodeAt(pos)
85
89
  if (ch == 0x0A)
86
90
  lines += 1
87
- elsif (ch == 0x20)
91
+ elsif isSpace(ch)
88
92
  else
89
93
  break
90
94
  end
@@ -93,7 +97,7 @@ module MarkdownIt
93
97
 
94
98
  # [label]: destination 'title'
95
99
  # ^^^^^^^^^^^ parse this
96
- res = parseLinkDestination(str, pos, max)
100
+ res = state.md.helpers.parseLinkDestination(str, pos, max)
97
101
  return false if (!res[:ok])
98
102
 
99
103
  href = state.md.normalizeLink.call(res[:str])
@@ -113,7 +117,7 @@ module MarkdownIt
113
117
  ch = str.charCodeAt(pos)
114
118
  if (ch == 0x0A)
115
119
  lines += 1
116
- elsif (ch == 0x20)
120
+ elsif isSpace(ch)
117
121
  else
118
122
  break
119
123
  end
@@ -122,7 +126,7 @@ module MarkdownIt
122
126
 
123
127
  # [label]: destination 'title'
124
128
  # ^^^^^^^ parse this
125
- res = parseLinkTitle(str, pos, max)
129
+ res = state.md.helpers.parseLinkTitle(str, pos, max)
126
130
  if (pos < max && start != pos && res[:ok])
127
131
  title = res[:str]
128
132
  pos = res[:pos]
@@ -134,7 +138,9 @@ module MarkdownIt
134
138
  end
135
139
 
136
140
  # skip trailing spaces until the rest of the line
137
- while (pos < max && str.charCodeAt(pos) == 0x20) # space
141
+ while pos < max
142
+ ch = str.charCodeAt(pos)
143
+ break if !isSpace(ch)
138
144
  pos += 1
139
145
  end
140
146
 
@@ -145,7 +151,9 @@ module MarkdownIt
145
151
  title = ''
146
152
  pos = destEndPos
147
153
  lines = destEndLineNo
148
- while (pos < max && str.charCodeAt(pos) == 0x20) # space
154
+ while pos < max
155
+ ch = str.charCodeAt(pos)
156
+ break if !isSpace(ch)
149
157
  pos += 1
150
158
  end
151
159
  end
@@ -173,6 +181,8 @@ module MarkdownIt
173
181
  state.env[:references][label] = { title: title, href: href }
174
182
  end
175
183
 
184
+ state.parentType = oldParentType
185
+
176
186
  state.line = startLine + lines + 1
177
187
  return true
178
188
  end
@@ -3,11 +3,12 @@
3
3
  module MarkdownIt
4
4
  module RulesBlock
5
5
  class StateBlock
6
+ include MarkdownIt::Common::Utils
6
7
 
7
- attr_accessor :src, :md, :env, :tokens, :bMarks, :eMarks, :tShift
8
+ attr_accessor :src, :md, :env, :tokens, :bMarks, :eMarks, :tShift, :sCount, :bsCount
8
9
  attr_accessor :blkIndent, :line, :lineMax, :tight, :parentType, :ddIndent
9
10
  attr_accessor :level, :result
10
-
11
+
11
12
  #------------------------------------------------------------------------------
12
13
  def initialize(src, md, env, tokens)
13
14
  @src = src
@@ -22,7 +23,20 @@ module MarkdownIt
22
23
 
23
24
  @bMarks = [] # line begin offsets for fast jumps
24
25
  @eMarks = [] # line end offsets for fast jumps
25
- @tShift = [] # indent for each line
26
+ @tShift = [] # offsets of the first non-space characters (tabs not expanded)
27
+ @sCount = [] # indents for each line (tabs expanded)
28
+
29
+ # An amount of virtual spaces (tabs expanded) between beginning
30
+ # of each line (bMarks) and real beginning of that line.
31
+ #
32
+ # It exists only as a hack because blockquotes override bMarks
33
+ # losing information in the process.
34
+ #
35
+ # It's used only when expanding tabs, you can think about it as
36
+ # an initial tab length, e.g. bsCount=21 applied to string `\t123`
37
+ # means first tab should be expanded to 4-21%4 === 3 spaces.
38
+ #
39
+ @bsCount = []
26
40
 
27
41
  # block parser variables
28
42
  @blkIndent = 0 # required block content indent (for example, if we are in list)
@@ -32,6 +46,10 @@ module MarkdownIt
32
46
  @parentType = 'root' # if `list`, block parser stops on two newlines
33
47
  @ddIndent = -1 # indent of the current dd block (-1 if there isn't any)
34
48
 
49
+ # can be 'blockquote', 'list', 'root', 'paragraph' or 'reference'
50
+ # used in lists to determine if they interrupt a paragraph
51
+ @parentType = 'root'
52
+
35
53
  @level = 0
36
54
 
37
55
  # renderer
@@ -40,20 +58,23 @@ module MarkdownIt
40
58
  # Create caches
41
59
  # Generate markers.
42
60
  s = @src
43
- indent = 0
44
61
  indent_found = false
45
62
 
46
- start = pos = indent = 0
63
+ start = pos = indent = offset = 0
47
64
  len = s.length
48
- start.upto(len - 1) do |pos|
49
- # !!!!!!
50
- # for (start = pos = indent = 0, len = s.length pos < len pos++) {
65
+ while pos < len
51
66
  ch = s.charCodeAt(pos)
52
67
 
53
68
  if !indent_found
54
- if ch == 0x20 # space
69
+ if isSpace(ch)
55
70
  indent += 1
56
- next
71
+
72
+ if ch == 0x09
73
+ offset += 4 - offset % 4
74
+ else
75
+ offset += 1
76
+ end
77
+ (pos += 1) and next
57
78
  else
58
79
  indent_found = true
59
80
  end
@@ -64,17 +85,24 @@ module MarkdownIt
64
85
  @bMarks.push(start)
65
86
  @eMarks.push(pos)
66
87
  @tShift.push(indent)
88
+ @sCount.push(offset)
89
+ @bsCount.push(0)
67
90
 
68
91
  indent_found = false
69
92
  indent = 0
93
+ offset = 0
70
94
  start = pos + 1
71
95
  end
96
+
97
+ pos += 1
72
98
  end
73
99
 
74
100
  # Push fake entry to simplify cache bounds checks
75
101
  @bMarks.push(s.length)
76
102
  @eMarks.push(s.length)
77
103
  @tShift.push(0)
104
+ @sCount.push(0)
105
+ @bsCount.push(0)
78
106
 
79
107
  @lineMax = @bMarks.length - 1 # don't count last fake line
80
108
  end
@@ -112,12 +140,24 @@ module MarkdownIt
112
140
  def skipSpaces(pos)
113
141
  max = @src.length
114
142
  while pos < max
115
- break if @src.charCodeAt(pos) != 0x20 # space
143
+ ch = @src.charCodeAt(pos)
144
+ break if !isSpace(ch)
116
145
  pos += 1
117
146
  end
118
147
  return pos
119
148
  end
120
149
 
150
+ # Skip spaces from given position in reverse.
151
+ #------------------------------------------------------------------------------
152
+ def skipSpacesBack(pos, min)
153
+ return pos if pos <= min
154
+
155
+ while (pos > min)
156
+ return pos + 1 if !isSpace(@src.charCodeAt(pos -= 1))
157
+ end
158
+ return pos
159
+ end
160
+
121
161
  # Skip char codes from given position
122
162
  #------------------------------------------------------------------------------
123
163
  def skipChars(pos, code)
@@ -147,22 +187,12 @@ module MarkdownIt
147
187
 
148
188
  return '' if line_begin >= line_end
149
189
 
150
- # Opt: don't use push queue for single line
151
- if (line + 1) == line_end
152
- first = @bMarks[line] + [@tShift[line], indent].min
153
- last = @eMarks[line_end - 1] + (keepLastLF ? 1 : 0)
154
- return @src.slice(first...last)
155
- end
156
-
157
190
  queue = Array.new(line_end - line_begin)
158
191
 
159
192
  i = 0
160
193
  while line < line_end
161
- shift = @tShift[line]
162
- shift = indent if shift > indent
163
- shift = 0 if shift < 0
164
-
165
- first = @bMarks[line] + shift
194
+ lineIndent = 0
195
+ lineStart = first = @bMarks[line]
166
196
 
167
197
  if line + 1 < line_end || keepLastLF
168
198
  # No need for bounds check because we have fake entry on tail.
@@ -171,7 +201,32 @@ module MarkdownIt
171
201
  last = @eMarks[line]
172
202
  end
173
203
 
174
- queue[i] = @src.slice(first...last)
204
+ while first < last && lineIndent < indent
205
+ ch = @src.charCodeAt(first)
206
+
207
+ if isSpace(ch)
208
+ if ch === 0x09
209
+ lineIndent += 4 - (lineIndent + @bsCount[line]) % 4
210
+ else
211
+ lineIndent += 1
212
+ end
213
+ elsif first - lineStart < @tShift[line]
214
+ # patched tShift masked characters to look like spaces (blockquotes, list markers)
215
+ lineIndent += 1
216
+ else
217
+ break
218
+ end
219
+
220
+ first += 1
221
+ end
222
+
223
+ if lineIndent > indent
224
+ # partially expanding tabs in code blocks, e.g '\t\tfoobar'
225
+ # with indent=2 becomes ' \tfoobar'
226
+ queue[i] = (' ' * (lineIndent - indent)) + @src.slice(first...last)
227
+ else
228
+ queue[i] = @src.slice(first...last)
229
+ end
175
230
  line += 1
176
231
  i += 1
177
232
  end
@@ -3,6 +3,7 @@
3
3
  module MarkdownIt
4
4
  module RulesBlock
5
5
  class Table
6
+ extend Common::Utils
6
7
 
7
8
  #------------------------------------------------------------------------------
8
9
  def self.getLine(state, line)
@@ -21,17 +22,26 @@ module MarkdownIt
21
22
  lastPos = 0
22
23
  backTicked = false
23
24
  lastBackTick = 0
24
-
25
+
25
26
  ch = str.charCodeAt(pos)
26
27
 
27
28
  while (pos < max)
28
- if (ch == 0x60 && (escapes % 2 == 0)) # `
29
- backTicked = !backTicked
30
- lastBackTick = pos
29
+ if ch == 0x60 # `
30
+ if (backTicked)
31
+ # make \` close code sequence, but not open it;
32
+ # the reason is: `\` is correct code block
33
+ backTicked = false
34
+ lastBackTick = pos
35
+ elsif escapes % 2 == 0
36
+ backTicked = !backTicked
37
+ lastBackTick = pos
38
+ end
31
39
  elsif (ch == 0x7c && (escapes % 2 == 0) && !backTicked) # '|'
32
40
  result.push(str[lastPos...pos])
33
41
  lastPos = pos + 1
34
- elsif (ch == 0x5c) # '\'
42
+ end
43
+
44
+ if ch == 0x5c # '\'
35
45
  escapes += 1
36
46
  else
37
47
  escapes = 0
@@ -55,32 +65,44 @@ module MarkdownIt
55
65
 
56
66
  #------------------------------------------------------------------------------
57
67
  def self.table(state, startLine, endLine, silent)
58
- # should have at least three lines
68
+ # should have at least two lines
59
69
  return false if (startLine + 2 > endLine)
60
70
 
61
71
  nextLine = startLine + 1
62
72
 
63
- return false if (state.tShift[nextLine] < state.blkIndent)
73
+ return false if (state.sCount[nextLine] < state.blkIndent)
74
+
75
+ # if it's indented more than 3 spaces, it should be a code block
76
+ return false if state.sCount[nextLine] - state.blkIndent >= 4
77
+
78
+ # first character of the second line should be '|', '-', ':',
79
+ # and no other characters are allowed but spaces;
80
+ # basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
64
81
 
65
- # first character of the second line should be '|' or '-'
66
82
  pos = state.bMarks[nextLine] + state.tShift[nextLine]
67
83
  return false if (pos >= state.eMarks[nextLine])
68
84
 
69
85
  ch = state.src.charCodeAt(pos)
70
- return false if (ch != 0x7C && ch != 0x2D && ch != 0x3A) # != '|' && '-' && ':'
86
+ pos += 1
87
+ return false if (ch != 0x7C && ch != 0x2D && ch != 0x3A) # | or - or :
88
+
89
+ while pos < state.eMarks[nextLine]
90
+ ch = state.src.charCodeAt(pos)
91
+ return false if (ch != 0x7C && ch != 0x2D && ch != 0x3A && !isSpace(ch)) # | or - or :
92
+
93
+ pos += 1
94
+ end
71
95
 
72
96
  lineText = getLine(state, startLine + 1)
73
- return false if (/^[-:| ]+$/ =~ lineText).nil?
74
97
 
75
- rows = lineText.split('|')
76
- return false if (rows.length < 2)
98
+ columns = lineText.split('|')
77
99
  aligns = []
78
- (0...rows.length).each do |i|
79
- t = rows[i].strip
100
+ (0...columns.length).each do |i|
101
+ t = columns[i].strip
80
102
  if t.empty?
81
103
  # allow empty columns before and after table, but not in between columns
82
104
  # e.g. allow ` |---| `, disallow ` ---||--- `
83
- if (i == 0 || i == rows.length - 1)
105
+ if (i == 0 || i == columns.length - 1)
84
106
  next
85
107
  else
86
108
  return false
@@ -99,8 +121,14 @@ module MarkdownIt
99
121
 
100
122
  lineText = getLine(state, startLine).strip
101
123
  return false if !lineText.include?('|')
102
- rows = self.escapedSplit(lineText.gsub(/^\||\|$/, ''))
103
- return false if (aligns.length != rows.length)
124
+ return false if state.sCount[startLine] - state.blkIndent >= 4
125
+ columns = self.escapedSplit(lineText.gsub(/^\||\|$/, ''))
126
+
127
+ # header row will define an amount of columns in the entire table,
128
+ # and align row shouldn't be smaller than that (the rest of the rows can)
129
+ columnCount = columns.length
130
+ return false if columnCount > aligns.length
131
+
104
132
  return true if silent
105
133
 
106
134
  token = state.push('table_open', 'table', 1)
@@ -112,7 +140,7 @@ module MarkdownIt
112
140
  token = state.push('tr_open', 'tr', 1)
113
141
  token.map = [ startLine, startLine + 1 ]
114
142
 
115
- (0...rows.length).each do |i|
143
+ (0...columns.length).each do |i|
116
144
  token = state.push('th_open', 'th', 1)
117
145
  token.map = [ startLine, startLine + 1 ]
118
146
  unless aligns[i].empty?
@@ -120,7 +148,7 @@ module MarkdownIt
120
148
  end
121
149
 
122
150
  token = state.push('inline', '', 0)
123
- token.content = rows[i].strip
151
+ token.content = columns[i].strip
124
152
  token.map = [ startLine, startLine + 1 ]
125
153
  token.children = []
126
154
 
@@ -135,24 +163,22 @@ module MarkdownIt
135
163
 
136
164
  nextLine = startLine + 2
137
165
  while nextLine < endLine
138
- break if (state.tShift[nextLine] < state.blkIndent)
166
+ break if (state.sCount[nextLine] < state.blkIndent)
139
167
 
140
168
  lineText = getLine(state, nextLine).strip
141
169
  break if !lineText.include?('|')
142
- rows = self.escapedSplit(lineText.gsub(/^\||\|$/, ''))
143
-
144
- # set number of columns to number of columns in header row
145
- rows_length = aligns.length
170
+ break if state.sCount[nextLine] - state.blkIndent >= 4
171
+ columns = self.escapedSplit(lineText.gsub(/^\||\|$/, ''))
146
172
 
147
173
  token = state.push('tr_open', 'tr', 1)
148
- (0...rows_length).each do |i|
174
+ (0...columnCount).each do |i|
149
175
  token = state.push('td_open', 'td', 1)
150
176
  unless aligns[i].empty?
151
177
  token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ]
152
178
  end
153
179
 
154
180
  token = state.push('inline', '', 0)
155
- token.content = rows[i] ? rows[i].strip : ''
181
+ token.content = columns[i] ? columns[i].strip : ''
156
182
  token.children = []
157
183
 
158
184
  token = state.push('td_close', 'td', -1)