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
@@ -4,11 +4,9 @@ module MarkdownIt
4
4
  module RulesCore
5
5
  class Normalize
6
6
 
7
- TABS_SCAN_RE = /[\n\t]/
8
- NEWLINES_RE = /\r[\n\u0085]|[\u2424\u2028\u0085]/
7
+ NEWLINES_RE = /\r[\n\u0085]?|[\u2424\u2028\u0085]/
9
8
  NULL_RE = /\u0000/
10
9
 
11
-
12
10
  #------------------------------------------------------------------------------
13
11
  def self.inline(state)
14
12
  # Normalize newlines
@@ -17,26 +15,6 @@ module MarkdownIt
17
15
  # Replace NULL characters
18
16
  str = str.gsub(NULL_RE, '\uFFFD')
19
17
 
20
- # Replace tabs with proper number of spaces (1..4)
21
- if str.include?("\t")
22
- lineStart = 0
23
- lastTabPos = 0
24
-
25
- str = str.gsub(TABS_SCAN_RE) do
26
- md = Regexp.last_match
27
- match = md.to_s
28
- offset = md.begin(0)
29
- if str.charCodeAt(offset) == 0x0A
30
- lineStart = offset + 1
31
- lastTabPos = 0
32
- next match
33
- end
34
- result = ' '.slice_to_end((offset - lineStart - lastTabPos) % 4)
35
- lastTabPos = offset - lineStart + 1
36
- result
37
- end
38
- end
39
-
40
18
  state.src = str
41
19
  end
42
20
  end
@@ -34,19 +34,31 @@ module MarkdownIt
34
34
 
35
35
  #------------------------------------------------------------------------------
36
36
  def self.replace_scoped(inlineTokens)
37
+ inside_autolink = 0
38
+
37
39
  (inlineTokens.length - 1).downto(0) do |i|
38
40
  token = inlineTokens[i]
39
- if (token.type == 'text')
41
+ if token.type == 'text' && inside_autolink == 0
40
42
  token.content = token.content.gsub(SCOPED_ABBR_RE) {|match| self.replaceFn(match, $1)}
41
43
  end
44
+
45
+ if token.type == 'link_open' && token.info == 'auto'
46
+ inside_autolink -= 1
47
+ end
48
+
49
+ if token.type == 'link_close' && token.info == 'auto'
50
+ inside_autolink += 1
51
+ end
42
52
  end
43
53
  end
44
54
 
45
55
  #------------------------------------------------------------------------------
46
56
  def self.replace_rare(inlineTokens)
57
+ inside_autolink = 0
58
+
47
59
  (inlineTokens.length - 1).downto(0) do |i|
48
60
  token = inlineTokens[i]
49
- if (token.type == 'text')
61
+ if token.type == 'text' && inside_autolink == 0
50
62
  if (RARE_RE =~ token.content)
51
63
  token.content = token.content.
52
64
  gsub(/\+-/, '±').
@@ -61,6 +73,14 @@ module MarkdownIt
61
73
  gsub(/(^|[^-\s])--([^-\s]|$)/m, "\\1\u2013\\2")
62
74
  end
63
75
  end
76
+
77
+ if token.type == 'link_open' && token.info == 'auto'
78
+ inside_autolink -= 1
79
+ end
80
+
81
+ if token.type == 'link_close' && token.info == 'auto'
82
+ inside_autolink += 1
83
+ end
64
84
  end
65
85
  end
66
86
 
@@ -4,12 +4,11 @@ module MarkdownIt
4
4
  module RulesCore
5
5
  class Smartquotes
6
6
  extend Common::Utils
7
-
7
+
8
8
  QUOTE_TEST_RE = /['"]/
9
9
  QUOTE_RE = /['"]/
10
10
  APOSTROPHE = "\u2019" # ’
11
11
 
12
-
13
12
  #------------------------------------------------------------------------------
14
13
  def self.replaceAt(str, index, ch)
15
14
  return str[0, index] + ch + str.slice_to_end(index + 1)
@@ -34,7 +33,7 @@ module MarkdownIt
34
33
  stack = (j < stack.length ? stack.slice(0, j + 1) : stack.fill(nil, stack.length...(j+1)))
35
34
 
36
35
  next if (token.type != 'text')
37
-
36
+
38
37
  text = token.content
39
38
  pos = 0
40
39
  max = text.length
@@ -50,12 +49,42 @@ module MarkdownIt
50
49
  pos = t.begin(0) + 1
51
50
  isSingle = (t[0] == "'")
52
51
 
53
- # treat begin/end of the line as a whitespace
54
- lastChar = t.begin(0) - 1 >= 0 ? text.charCodeAt(t.begin(0) - 1) : 0x20
55
- nextChar = pos < max ? text.charCodeAt(pos) : 0x20
52
+ # Find previous character,
53
+ # default to space if it's the beginning of the line
54
+ #
55
+ lastChar = 0x20
56
+
57
+ if t.begin(0) - 1 >= 0
58
+ lastChar = text.charCodeAt(t.begin(0) - 1)
59
+ else
60
+ (i - 1).downto(0) do |j|
61
+ break if tokens[j].type == 'softbreak' || tokens[j].type == 'hardbreak' # lastChar defaults to 0x20
62
+ next if tokens[j].type != 'text'
63
+
64
+ lastChar = tokens[j].content.charCodeAt(tokens[j].content.length - 1)
65
+ break
66
+ end
67
+ end
68
+
69
+ # Find next character,
70
+ # default to space if it's the end of the line
71
+ #
72
+ nextChar = 0x20
56
73
 
57
- isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(lastChar.chr(Encoding::UTF_8))
58
- isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(nextChar.chr(Encoding::UTF_8))
74
+ if pos < max
75
+ nextChar = text.charCodeAt(pos)
76
+ else
77
+ (i + 1).upto(tokens.length - 1) do |j|
78
+ break if tokens[j].type == 'softbreak' || tokens[j].type == 'hardbreak' # nextChar defaults to 0x20
79
+ next if tokens[j].type != 'text'
80
+
81
+ nextChar = tokens[j].content.charCodeAt(0)
82
+ break
83
+ end
84
+ end
85
+
86
+ isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(fromCodePoint(lastChar))
87
+ isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(fromCodePoint(nextChar))
59
88
 
60
89
  isLastWhiteSpace = isWhiteSpace(lastChar)
61
90
  isNextWhiteSpace = isWhiteSpace(nextChar)
@@ -112,7 +141,7 @@ module MarkdownIt
112
141
  openQuote = state.md.options[:quotes][0]
113
142
  closeQuote = state.md.options[:quotes][1]
114
143
  end
115
-
144
+
116
145
  # replace token.content *before* tokens[item.token].content,
117
146
  # because, if they are pointing at the same token, replaceAt
118
147
  # could mess up indices when quote length != 1
@@ -124,7 +153,7 @@ module MarkdownIt
124
153
 
125
154
  text = token.content
126
155
  max = text.length
127
-
156
+
128
157
  stack = (j < stack.length ? stack.slice(0, j) : stack.fill(nil, stack.length...(j))) # stack.length = j
129
158
  continue_outer_loop = true # continue OUTER;
130
159
  break
@@ -133,7 +162,7 @@ module MarkdownIt
133
162
  end
134
163
  end
135
164
  next if continue_outer_loop
136
-
165
+
137
166
  if (canOpen)
138
167
  stack.push({
139
168
  token: i,
@@ -164,7 +193,7 @@ module MarkdownIt
164
193
  blkIdx -= 1
165
194
  end
166
195
  end
167
-
196
+
168
197
  end
169
198
  end
170
199
  end
@@ -5,8 +5,7 @@ module MarkdownIt
5
5
  class Autolink
6
6
 
7
7
  EMAIL_RE = /^<([a-zA-Z0-9.!#$\%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/
8
- AUTOLINK_RE = /^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/
9
-
8
+ AUTOLINK_RE = /^<([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)>/
10
9
 
11
10
  #------------------------------------------------------------------------------
12
11
  def self.autolink(state, silent)
@@ -21,8 +20,6 @@ module MarkdownIt
21
20
  if (AUTOLINK_RE =~ tail)
22
21
  linkMatch = tail.match(AUTOLINK_RE)
23
22
 
24
- return false if !URL_SCHEMAS.include?(linkMatch[1].downcase)
25
-
26
23
  url = linkMatch[0].slice(1...-1)
27
24
  fullUrl = state.md.normalizeLink.call(url)
28
25
  return false if (!state.md.validateLink.call(fullUrl))
@@ -30,11 +27,15 @@ module MarkdownIt
30
27
  if (!silent)
31
28
  token = state.push('link_open', 'a', 1)
32
29
  token.attrs = [ [ 'href', fullUrl ] ]
30
+ token.markup = 'autolink'
31
+ token.info = 'auto'
33
32
 
34
33
  token = state.push('text', '', 0)
35
34
  token.content = state.md.normalizeLinkText.call(url)
36
35
 
37
36
  token = state.push('link_close', 'a', -1)
37
+ token.markup = 'autolink'
38
+ token.info = 'auto'
38
39
  end
39
40
 
40
41
  state.pos += linkMatch[0].length
@@ -0,0 +1,48 @@
1
+ # For each opening emphasis-like marker find a matching closing one
2
+ #------------------------------------------------------------------------------
3
+ module MarkdownIt
4
+ module RulesInline
5
+ class BalancePairs
6
+
7
+ #------------------------------------------------------------------------------
8
+ def self.link_pairs(state)
9
+ delimiters = state.delimiters
10
+ max = state.delimiters.length
11
+
12
+ 0.upto(max - 1) do |i|
13
+ lastDelim = delimiters[i]
14
+
15
+ next if !lastDelim[:close]
16
+
17
+ j = i - lastDelim[:jump] - 1
18
+
19
+ while j >= 0
20
+ currDelim = delimiters[j]
21
+
22
+ if currDelim[:open] &&
23
+ currDelim[:marker] == lastDelim[:marker] &&
24
+ currDelim[:end] < 0 &&
25
+ currDelim[:level] == lastDelim[:level]
26
+
27
+ # typeofs are for backward compatibility with plugins
28
+ # not needed: typeof currDelim.length !== 'undefined' &&
29
+ # typeof lastDelim.length !== 'undefined' &&
30
+ odd_match = (currDelim[:close] || lastDelim[:open]) &&
31
+ (currDelim[:length] + lastDelim[:length]) % 3 == 0
32
+
33
+ if !odd_match
34
+ lastDelim[:jump] = i - j
35
+ lastDelim[:open] = false
36
+ currDelim[:end] = i
37
+ currDelim[:jump] = 0
38
+ break
39
+ end
40
+ end
41
+
42
+ j -= currDelim[:jump] + 1
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,166 +4,121 @@ module MarkdownIt
4
4
  module RulesInline
5
5
  class Emphasis
6
6
  extend MarkdownIt::Common::Utils
7
-
8
- # parse sequence of emphasis markers,
9
- # "start" should point at a valid marker
10
- #------------------------------------------------------------------------------
11
- def self.scanDelims(state, start)
12
- pos = start
13
- left_flanking = true
14
- right_flanking = true
15
- max = state.posMax
16
- marker = state.src.charCodeAt(start)
17
-
18
- # treat beginning of the line as a whitespace
19
- lastChar = start > 0 ? state.src.charCodeAt(start - 1) : 0x20
20
-
21
- while (pos < max && state.src.charCodeAt(pos) == marker)
22
- pos += 1
23
- end
24
-
25
- count = pos - start
26
-
27
- # treat end of the line as a whitespace
28
- nextChar = pos < max ? state.src.charCodeAt(pos) : 0x20
29
-
30
- isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(lastChar.chr(Encoding::UTF_8))
31
- isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(nextChar.chr(Encoding::UTF_8))
32
7
 
33
- isLastWhiteSpace = isWhiteSpace(lastChar)
34
- isNextWhiteSpace = isWhiteSpace(nextChar)
35
-
36
- if (isNextWhiteSpace)
37
- left_flanking = false
38
- elsif (isNextPunctChar)
39
- if (!(isLastWhiteSpace || isLastPunctChar))
40
- left_flanking = false
41
- end
8
+ # Insert each marker as a separate text token, and add it to delimiter list
9
+ #
10
+ def self.tokenize(state, silent)
11
+ start = state.pos
12
+ marker = state.src.charCodeAt(start)
13
+
14
+ return false if silent
15
+
16
+ return false if (marker != 0x5F && marker != 0x2A) # _ and *
17
+
18
+ scanned = state.scanDelims(state.pos, marker == 0x2A)
19
+
20
+ 0.upto(scanned[:length] - 1) do |i|
21
+ token = state.push('text', '', 0)
22
+ token.content = fromCodePoint(marker)
23
+
24
+ state.delimiters.push({
25
+ # Char code of the starting marker (number).
26
+ #
27
+ marker: marker,
28
+
29
+ # Total length of these series of delimiters.
30
+ #
31
+ length: scanned[:length],
32
+
33
+ # An amount of characters before this one that's equivalent to
34
+ # current one. In plain English: if this delimiter does not open
35
+ # an emphasis, neither do previous `jump` characters.
36
+ #
37
+ # Used to skip sequences like "*****" in one step, for 1st asterisk
38
+ # value will be 0, for 2nd it's 1 and so on.
39
+ #
40
+ jump: i,
41
+
42
+ # A position of the token this delimiter corresponds to.
43
+ #
44
+ token: state.tokens.length - 1,
45
+
46
+ # Token level.
47
+ #
48
+ level: state.level,
49
+
50
+ # If this delimiter is matched as a valid opener, `end` will be
51
+ # equal to its position, otherwise it's `-1`.
52
+ #
53
+ end: -1,
54
+
55
+ # Boolean flags that determine if this delimiter could open or close
56
+ # an emphasis.
57
+ #
58
+ open: scanned[:can_open],
59
+ close: scanned[:can_close]
60
+ })
42
61
  end
43
62
 
44
- if (isLastWhiteSpace)
45
- right_flanking = false
46
- elsif (isLastPunctChar)
47
- if (!(isNextWhiteSpace || isNextPunctChar))
48
- right_flanking = false
49
- end
50
- end
51
-
52
- if (marker == 0x5F) # _
53
- # "_" inside a word can neither open nor close an emphasis
54
- can_open = left_flanking && (!right_flanking || isLastPunctChar)
55
- can_close = right_flanking && (!left_flanking || isNextPunctChar)
56
- else
57
- can_open = left_flanking
58
- can_close = right_flanking
59
- end
63
+ state.pos += scanned[:length]
60
64
 
61
- return { can_open: can_open, can_close: can_close, delims: count }
65
+ return true
62
66
  end
63
67
 
64
- #------------------------------------------------------------------------------
65
- def self.emphasis(state, silent)
66
- max = state.posMax
67
- start = state.pos
68
- marker = state.src.charCodeAt(start)
69
-
70
- return false if (marker != 0x5F && marker != 0x2A) # _ *
71
- return false if (silent) # don't run any pairs in validation mode
72
-
73
- res = scanDelims(state, start)
74
- startCount = res[:delims]
75
- if (!res[:can_open])
76
- state.pos += startCount
77
- # Earlier we checked !silent, but this implementation does not need it
78
- state.pending += state.src.slice(start...state.pos)
79
- return true
80
- end
81
68
 
82
- state.pos = start + startCount
83
- stack = [ startCount ]
84
-
85
- while (state.pos < max)
86
- if (state.src.charCodeAt(state.pos) == marker)
87
- res = scanDelims(state, state.pos)
88
- count = res[:delims]
89
- if (res[:can_close])
90
- oldCount = stack.pop()
91
- newCount = count
92
-
93
- while (oldCount != newCount)
94
- if (newCount < oldCount)
95
- stack.push(oldCount - newCount)
96
- break
97
- end
98
-
99
- # assert(newCount > oldCount)
100
- newCount -= oldCount
101
-
102
- break if (stack.length == 0)
103
- state.pos += oldCount
104
- oldCount = stack.pop()
105
- end
106
-
107
- if (stack.length == 0)
108
- startCount = oldCount
109
- found = true
110
- break
111
- end
112
- state.pos += count
113
- next
114
- end
115
-
116
- stack.push(count) if (res[:can_open])
117
- state.pos += count
118
- next
69
+ # Walk through delimiter list and replace text tokens with tags
70
+ #
71
+ def self.postProcess(state)
72
+ delimiters = state.delimiters
73
+ max = state.delimiters.length
74
+
75
+ i = max - 1
76
+ while i >= 0
77
+ startDelim = delimiters[i]
78
+
79
+ (i -= 1) and next if startDelim[:marker] != 0x5F && startDelim[:marker] != 0x2A # _ and *
80
+
81
+ # Process only opening markers
82
+ (i -= 1) and next if startDelim[:end] == -1
83
+
84
+ endDelim = delimiters[startDelim[:end]]
85
+
86
+ # If the previous delimiter has the same marker and is adjacent to this one,
87
+ # merge those into one strong delimiter.
88
+ #
89
+ # `<em><em>whatever</em></em>` -> `<strong>whatever</strong>`
90
+ #
91
+ isStrong = i > 0 &&
92
+ delimiters[i - 1][:end] == startDelim[:end] + 1 &&
93
+ delimiters[i - 1][:token] == startDelim[:token] - 1 &&
94
+ delimiters[startDelim[:end] + 1][:token] == endDelim[:token] + 1 &&
95
+ delimiters[i - 1][:marker] == startDelim[:marker]
96
+
97
+ ch = fromCodePoint(startDelim[:marker])
98
+
99
+ token = state.tokens[startDelim[:token]]
100
+ token.type = isStrong ? 'strong_open' : 'em_open'
101
+ token.tag = isStrong ? 'strong' : 'em'
102
+ token.nesting = 1
103
+ token.markup = isStrong ? ch + ch : ch
104
+ token.content = ''
105
+
106
+ token = state.tokens[endDelim[:token]]
107
+ token.type = isStrong ? 'strong_close' : 'em_close'
108
+ token.tag = isStrong ? 'strong' : 'em'
109
+ token.nesting = -1
110
+ token.markup = isStrong ? ch + ch : ch
111
+ token.content = ''
112
+
113
+ if isStrong
114
+ state.tokens[delimiters[i - 1][:token]].content = ''
115
+ state.tokens[delimiters[startDelim[:end] + 1][:token]].content = ''
116
+ i -= 1
119
117
  end
120
118
 
121
- state.md.inline.skipToken(state)
122
- end
123
-
124
- if (!found)
125
- # parser failed to find ending tag, so it's not valid emphasis
126
- state.pos = start
127
- return false
119
+ i -= 1
128
120
  end
129
-
130
- # found!
131
- state.posMax = state.pos
132
- state.pos = start + startCount
133
-
134
- # Earlier we checked !silent, but this implementation does not need it
135
-
136
- # we have `startCount` starting and ending markers,
137
- # now trying to serialize them into tokens
138
- count = startCount
139
- while count > 1
140
- token = state.push('strong_open', 'strong', 1)
141
- token.markup = marker.chr + marker.chr
142
- count -= 2
143
- end
144
- if (count % 2 == 1)
145
- token = state.push('em_open', 'em', 1)
146
- token.markup = marker.chr
147
- end
148
-
149
- state.md.inline.tokenize(state)
150
-
151
- if (count % 2 == 1)
152
- token = state.push('em_close', 'em', -1)
153
- token.markup = marker.chr
154
- end
155
- count = startCount
156
- while count > 1
157
- token = state.push('strong_close', 'strong', -1)
158
- token.markup = marker.chr + marker.chr
159
- count -= 2
160
- end
161
-
162
- state.pos = state.posMax + startCount
163
- state.posMax = max
164
- return true
165
121
  end
166
-
167
122
  end
168
123
  end
169
124
  end