motion-markdown-it 0.4.0.3.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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +243 -0
  3. data/lib/motion-markdown-it.rb +71 -0
  4. data/lib/motion-markdown-it/common/entities.rb +1084 -0
  5. data/lib/motion-markdown-it/common/html_blocks.rb +60 -0
  6. data/lib/motion-markdown-it/common/html_re.rb +28 -0
  7. data/lib/motion-markdown-it/common/string.rb +14 -0
  8. data/lib/motion-markdown-it/common/url_schemas.rb +173 -0
  9. data/lib/motion-markdown-it/common/utils.rb +216 -0
  10. data/lib/motion-markdown-it/helpers/parse_link_destination.rb +75 -0
  11. data/lib/motion-markdown-it/helpers/parse_link_label.rb +51 -0
  12. data/lib/motion-markdown-it/helpers/parse_link_title.rb +48 -0
  13. data/lib/motion-markdown-it/index.rb +507 -0
  14. data/lib/motion-markdown-it/parser_block.rb +113 -0
  15. data/lib/motion-markdown-it/parser_core.rb +46 -0
  16. data/lib/motion-markdown-it/parser_inline.rb +121 -0
  17. data/lib/motion-markdown-it/presets/commonmark.rb +76 -0
  18. data/lib/motion-markdown-it/presets/default.rb +42 -0
  19. data/lib/motion-markdown-it/presets/zero.rb +59 -0
  20. data/lib/motion-markdown-it/renderer.rb +286 -0
  21. data/lib/motion-markdown-it/ruler.rb +327 -0
  22. data/lib/motion-markdown-it/rules_block/blockquote.rb +138 -0
  23. data/lib/motion-markdown-it/rules_block/code.rb +35 -0
  24. data/lib/motion-markdown-it/rules_block/fence.rb +94 -0
  25. data/lib/motion-markdown-it/rules_block/heading.rb +56 -0
  26. data/lib/motion-markdown-it/rules_block/hr.rb +45 -0
  27. data/lib/motion-markdown-it/rules_block/html_block.rb +73 -0
  28. data/lib/motion-markdown-it/rules_block/lheading.rb +54 -0
  29. data/lib/motion-markdown-it/rules_block/list.rb +242 -0
  30. data/lib/motion-markdown-it/rules_block/paragraph.rb +51 -0
  31. data/lib/motion-markdown-it/rules_block/reference.rb +161 -0
  32. data/lib/motion-markdown-it/rules_block/state_block.rb +184 -0
  33. data/lib/motion-markdown-it/rules_block/table.rb +161 -0
  34. data/lib/motion-markdown-it/rules_core/block.rb +20 -0
  35. data/lib/motion-markdown-it/rules_core/inline.rb +20 -0
  36. data/lib/motion-markdown-it/rules_core/linkify.rb +138 -0
  37. data/lib/motion-markdown-it/rules_core/normalize.rb +44 -0
  38. data/lib/motion-markdown-it/rules_core/replacements.rb +90 -0
  39. data/lib/motion-markdown-it/rules_core/smartquotes.rb +158 -0
  40. data/lib/motion-markdown-it/rules_core/state_core.rb +20 -0
  41. data/lib/motion-markdown-it/rules_inline/autolink.rb +74 -0
  42. data/lib/motion-markdown-it/rules_inline/backticks.rb +51 -0
  43. data/lib/motion-markdown-it/rules_inline/emphasis.rb +172 -0
  44. data/lib/motion-markdown-it/rules_inline/entity.rb +51 -0
  45. data/lib/motion-markdown-it/rules_inline/escape.rb +55 -0
  46. data/lib/motion-markdown-it/rules_inline/html_inline.rb +49 -0
  47. data/lib/motion-markdown-it/rules_inline/image.rb +158 -0
  48. data/lib/motion-markdown-it/rules_inline/link.rb +153 -0
  49. data/lib/motion-markdown-it/rules_inline/newline.rb +47 -0
  50. data/lib/motion-markdown-it/rules_inline/state_inline.rb +57 -0
  51. data/lib/motion-markdown-it/rules_inline/strikethrough.rb +130 -0
  52. data/lib/motion-markdown-it/rules_inline/text.rb +94 -0
  53. data/lib/motion-markdown-it/token.rb +134 -0
  54. data/lib/motion-markdown-it/version.rb +5 -0
  55. data/spec/motion-markdown-it/bench_mark_spec.rb +44 -0
  56. data/spec/motion-markdown-it/commonmark_spec.rb +16 -0
  57. data/spec/motion-markdown-it/markdown_it_spec.rb +18 -0
  58. data/spec/motion-markdown-it/misc_spec.rb +277 -0
  59. data/spec/motion-markdown-it/ruler_spec.rb +153 -0
  60. data/spec/motion-markdown-it/testgen_helper.rb +68 -0
  61. data/spec/motion-markdown-it/token_spec.rb +17 -0
  62. data/spec/motion-markdown-it/utils_spec.rb +82 -0
  63. data/spec/spec_helper.rb +6 -0
  64. metadata +158 -0
@@ -0,0 +1,184 @@
1
+ # Parser state class
2
+ #------------------------------------------------------------------------------
3
+ module MarkdownIt
4
+ module RulesBlock
5
+ class StateBlock
6
+
7
+ attr_accessor :src, :md, :env, :tokens, :bMarks, :eMarks, :tShift
8
+ attr_accessor :blkIndent, :line, :lineMax, :tight, :parentType, :ddIndent
9
+ attr_accessor :level, :result
10
+
11
+ #------------------------------------------------------------------------------
12
+ def initialize(src, md, env, tokens)
13
+ @src = src
14
+
15
+ # link to parser instance
16
+ @md = md
17
+ @env = env
18
+
19
+ #--- Internal state variables
20
+
21
+ @tokens = tokens
22
+
23
+ @bMarks = [] # line begin offsets for fast jumps
24
+ @eMarks = [] # line end offsets for fast jumps
25
+ @tShift = [] # indent for each line
26
+
27
+ # block parser variables
28
+ @blkIndent = 0 # required block content indent (for example, if we are in list)
29
+ @line = 0 # line index in src
30
+ @lineMax = 0 # lines count
31
+ @tight = false # loose/tight mode for lists
32
+ @parentType = 'root' # if `list`, block parser stops on two newlines
33
+ @ddIndent = -1 # indent of the current dd block (-1 if there isn't any)
34
+
35
+ @level = 0
36
+
37
+ # renderer
38
+ @result = ''
39
+
40
+ # Create caches
41
+ # Generate markers.
42
+ s = @src
43
+ indent = 0
44
+ indent_found = false
45
+
46
+ start = pos = indent = 0
47
+ len = s.length
48
+ start.upto(len - 1) do |pos|
49
+ # !!!!!!
50
+ # for (start = pos = indent = 0, len = s.length pos < len pos++) {
51
+ ch = s.charCodeAt(pos)
52
+
53
+ if !indent_found
54
+ if ch == 0x20 # space
55
+ indent += 1
56
+ next
57
+ else
58
+ indent_found = true
59
+ end
60
+ end
61
+
62
+ if ch == 0x0A || pos == (len - 1)
63
+ pos += 1 if ch != 0x0A
64
+ @bMarks.push(start)
65
+ @eMarks.push(pos)
66
+ @tShift.push(indent)
67
+
68
+ indent_found = false
69
+ indent = 0
70
+ start = pos + 1
71
+ end
72
+ end
73
+
74
+ # Push fake entry to simplify cache bounds checks
75
+ @bMarks.push(s.length)
76
+ @eMarks.push(s.length)
77
+ @tShift.push(0)
78
+
79
+ @lineMax = @bMarks.length - 1 # don't count last fake line
80
+ end
81
+
82
+ # Push new token to "stream".
83
+ #------------------------------------------------------------------------------
84
+ def push(type, tag, nesting)
85
+ token = Token.new(type, tag, nesting)
86
+ token.block = true
87
+
88
+ @level -= 1 if nesting < 0
89
+ token.level = @level
90
+ @level += 1 if nesting > 0
91
+
92
+ @tokens.push(token)
93
+ return token
94
+ end
95
+
96
+ #------------------------------------------------------------------------------
97
+ def isEmpty(line)
98
+ return @bMarks[line] + @tShift[line] >= @eMarks[line]
99
+ end
100
+
101
+ #------------------------------------------------------------------------------
102
+ def skipEmptyLines(from)
103
+ while from < @lineMax
104
+ break if (@bMarks[from] + @tShift[from] < @eMarks[from])
105
+ from += 1
106
+ end
107
+ return from
108
+ end
109
+
110
+ # Skip spaces from given position.
111
+ #------------------------------------------------------------------------------
112
+ def skipSpaces(pos)
113
+ max = @src.length
114
+ while pos < max
115
+ break if @src.charCodeAt(pos) != 0x20 # space
116
+ pos += 1
117
+ end
118
+ return pos
119
+ end
120
+
121
+ # Skip char codes from given position
122
+ #------------------------------------------------------------------------------
123
+ def skipChars(pos, code)
124
+ max = @src.length
125
+ while pos < max
126
+ break if (@src.charCodeAt(pos) != code)
127
+ pos += 1
128
+ end
129
+ return pos
130
+ end
131
+
132
+ # Skip char codes reverse from given position - 1
133
+ #------------------------------------------------------------------------------
134
+ def skipCharsBack(pos, code, min)
135
+ return pos if pos <= min
136
+
137
+ while (pos > min)
138
+ return (pos + 1) if code != @src.charCodeAt(pos -= 1)
139
+ end
140
+ return pos
141
+ end
142
+
143
+ # cut lines range from source.
144
+ #------------------------------------------------------------------------------
145
+ def getLines(line_begin, line_end, indent, keepLastLF)
146
+ line = line_begin
147
+
148
+ return '' if line_begin >= line_end
149
+
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 = keepLastLF ? @bMarks[line_end] : @eMarks[line_end - 1]
154
+ return @src.slice(first...last)
155
+ end
156
+
157
+ queue = Array.new(line_end - line_begin)
158
+
159
+ i = 0
160
+ 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
166
+
167
+ if line + 1 < line_end || keepLastLF
168
+ # No need for bounds check because we have fake entry on tail.
169
+ last = @eMarks[line] + 1
170
+ else
171
+ last = @eMarks[line]
172
+ end
173
+
174
+ queue[i] = @src.slice(first...last)
175
+ line += 1
176
+ i += 1
177
+ end
178
+
179
+ return queue.join('')
180
+ end
181
+
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,161 @@
1
+ # GFM table, non-standard
2
+ #------------------------------------------------------------------------------
3
+ module MarkdownIt
4
+ module RulesBlock
5
+ class Table
6
+
7
+ #------------------------------------------------------------------------------
8
+ def self.getLine(state, line)
9
+ pos = state.bMarks[line] + state.blkIndent
10
+ max = state.eMarks[line]
11
+
12
+ return state.src[pos, max - pos]
13
+ end
14
+
15
+ #------------------------------------------------------------------------------
16
+ def self.escapedSplit(str)
17
+ result = []
18
+ pos = 0
19
+ max = str.length
20
+ escapes = 0
21
+ lastPos = 0
22
+ ch = str.charCodeAt(pos)
23
+
24
+ while (pos < max)
25
+ if (ch == 0x7c && (escapes % 2 == 0)) # '|'
26
+ result.push(str[lastPos...pos])
27
+ lastPos = pos + 1
28
+ elsif (ch == 0x5c) # '\'
29
+ escapes += 1
30
+ else
31
+ escapes = 0
32
+ end
33
+
34
+ pos += 1
35
+ ch = str.charCodeAt(pos)
36
+ end
37
+
38
+ result.push(str.slice_to_end(lastPos))
39
+
40
+ return result
41
+ end
42
+
43
+
44
+ #------------------------------------------------------------------------------
45
+ def self.table(state, startLine, endLine, silent)
46
+ # should have at least three lines
47
+ return false if (startLine + 2 > endLine)
48
+
49
+ nextLine = startLine + 1
50
+
51
+ return false if (state.tShift[nextLine] < state.blkIndent)
52
+
53
+ # first character of the second line should be '|' or '-'
54
+ pos = state.bMarks[nextLine] + state.tShift[nextLine]
55
+ return false if (pos >= state.eMarks[nextLine])
56
+
57
+ ch = state.src.charCodeAt(pos)
58
+ return false if (ch != 0x7C && ch != 0x2D && ch != 0x3A) # != '|' && '-' && ':'
59
+
60
+ lineText = getLine(state, startLine + 1)
61
+ return false if (/^[-:| ]+$/ =~ lineText).nil?
62
+
63
+ rows = lineText.split('|')
64
+ return false if (rows.length < 2)
65
+ aligns = []
66
+ (0...rows.length).each do |i|
67
+ t = rows[i].strip
68
+ if t.empty?
69
+ # allow empty columns before and after table, but not in between columns
70
+ # e.g. allow ` |---| `, disallow ` ---||--- `
71
+ if (i == 0 || i == rows.length - 1)
72
+ next
73
+ else
74
+ return false
75
+ end
76
+ end
77
+
78
+ return false if (/^:?-+:?$/ =~ t).nil?
79
+ if (t.charCodeAt(t.length - 1) == 0x3A) # ':'
80
+ aligns.push(t.charCodeAt(0) == 0x3A ? 'center' : 'right')
81
+ elsif (t.charCodeAt(0) == 0x3A)
82
+ aligns.push('left')
83
+ else
84
+ aligns.push('')
85
+ end
86
+ end
87
+
88
+ lineText = getLine(state, startLine).strip
89
+ return false if !lineText.include?('|')
90
+ rows = self.escapedSplit(lineText.gsub(/^\||\|$/, ''))
91
+ return false if (aligns.length != rows.length)
92
+ return true if silent
93
+
94
+ token = state.push('table_open', 'table', 1)
95
+ token.map = tableLines = [ startLine, 0 ]
96
+
97
+ token = state.push('thead_open', 'thead', 1)
98
+ token.map = [ startLine, startLine + 1 ]
99
+
100
+ token = state.push('tr_open', 'tr', 1)
101
+ token.map = [ startLine, startLine + 1 ]
102
+
103
+ (0...rows.length).each do |i|
104
+ token = state.push('th_open', 'th', 1)
105
+ token.map = [ startLine, startLine + 1 ]
106
+ unless aligns[i].empty?
107
+ token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ]
108
+ end
109
+
110
+ token = state.push('inline', '', 0)
111
+ token.content = rows[i].strip
112
+ token.map = [ startLine, startLine + 1 ]
113
+ token.children = []
114
+
115
+ token = state.push('th_close', 'th', -1)
116
+ end
117
+
118
+ token = state.push('tr_close', 'tr', -1)
119
+ token = state.push('thead_close', 'thead', -1)
120
+
121
+ token = state.push('tbody_open', 'tbody', 1)
122
+ token.map = tbodyLines = [ startLine + 2, 0 ]
123
+
124
+ nextLine = startLine + 2
125
+ while nextLine < endLine
126
+ break if (state.tShift[nextLine] < state.blkIndent)
127
+
128
+ lineText = getLine(state, nextLine).strip
129
+ break if !lineText.include?('|')
130
+ rows = self.escapedSplit(lineText.gsub(/^\||\|$/, ''))
131
+
132
+ # set number of columns to number of columns in header row
133
+ rows_length = aligns.length
134
+
135
+ token = state.push('tr_open', 'tr', 1)
136
+ (0...rows_length).each do |i|
137
+ token = state.push('td_open', 'td', 1)
138
+ unless aligns[i].empty?
139
+ token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ]
140
+ end
141
+
142
+ token = state.push('inline', '', 0)
143
+ token.content = rows[i] ? rows[i].strip : ''
144
+ token.children = []
145
+
146
+ token = state.push('td_close', 'td', -1)
147
+ end
148
+ token = state.push('tr_close', 'tr', -1)
149
+ nextLine += 1
150
+ end
151
+ token = state.push('tbody_close', 'tbody', -1)
152
+ token = state.push('table_close', 'table', -1)
153
+
154
+ tableLines[1] = tbodyLines[1] = nextLine
155
+ state.line = nextLine
156
+ return true
157
+ end
158
+
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,20 @@
1
+ module MarkdownIt
2
+ module RulesCore
3
+ class Block
4
+
5
+ #------------------------------------------------------------------------------
6
+ def self.block(state)
7
+ if state.inlineMode
8
+ token = Token.new('inline', '', 0)
9
+ token.content = state.src
10
+ token.map = [ 0, 1 ]
11
+ token.children = []
12
+ state.tokens.push(token)
13
+ else
14
+ state.md.block.parse(state.src, state.md, state.env, state.tokens)
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module MarkdownIt
2
+ module RulesCore
3
+ class Inline
4
+
5
+ #------------------------------------------------------------------------------
6
+ def self.inline(state)
7
+ tokens = state.tokens
8
+
9
+ # Parse inlines
10
+ 0.upto(tokens.length - 1) do |i|
11
+ tok = tokens[i]
12
+ if tok.type == 'inline'
13
+ state.md.inline.parse(tok.content, state.md, state.env, tok.children)
14
+ end
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,138 @@
1
+ # Replace link-like texts with link nodes.
2
+ #
3
+ # Currently restricted by `md.validateLink()` to http/https/ftp
4
+ #------------------------------------------------------------------------------
5
+ module MarkdownIt
6
+ module RulesCore
7
+ class Linkify
8
+
9
+ #------------------------------------------------------------------------------
10
+ def self.isLinkOpen(str)
11
+ return !(/^<a[>\s]/i =~ str).nil?
12
+ end
13
+ def self.isLinkClose(str)
14
+ return !(/^<\/a\s*>/i =~ str).nil?
15
+ end
16
+
17
+ #------------------------------------------------------------------------------
18
+ def self.linkify(state)
19
+ blockTokens = state.tokens
20
+
21
+ return if (!state.md.options[:linkify])
22
+
23
+ (0...blockTokens.length).each do |j|
24
+ if (blockTokens[j].type != 'inline' || !state.md.linkify.pretest(blockTokens[j].content))
25
+ next
26
+ end
27
+
28
+ tokens = blockTokens[j].children
29
+
30
+ htmlLinkLevel = 0
31
+
32
+ # We scan from the end, to keep position when new tags added.
33
+ # Use reversed logic in links start/end match
34
+ i = tokens.length - 1
35
+ while i >= 0
36
+ currentToken = tokens[i]
37
+
38
+ # Skip content of markdown links
39
+ if (currentToken.type == 'link_close')
40
+ i -= 1
41
+ while (tokens[i].level != currentToken.level && tokens[i].type != 'link_open')
42
+ i -= 1
43
+ end
44
+ next
45
+ end
46
+
47
+ # Skip content of html tag links
48
+ if (currentToken.type == 'html_inline')
49
+ if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0)
50
+ htmlLinkLevel -= 1
51
+ end
52
+ if (isLinkClose(currentToken.content))
53
+ htmlLinkLevel -= 1
54
+ end
55
+ end
56
+ next if (htmlLinkLevel > 0)
57
+
58
+ if (currentToken.type == 'text' && state.md.linkify =~ currentToken.content)
59
+
60
+ text = currentToken.content
61
+ links = state.md.linkify.match(text)
62
+
63
+ # Now split string to nodes
64
+ nodes = []
65
+ level = currentToken.level
66
+ lastPos = 0
67
+
68
+ (0...links.length).each do |ln|
69
+ url = links[ln].url
70
+ fullUrl = state.md.normalizeLink.call(url)
71
+ next if (!state.md.validateLink.call(fullUrl))
72
+
73
+ urlText = links[ln].text
74
+
75
+ # Linkifier might send raw hostnames like "example.com", where url
76
+ # starts with domain name. So we prepend http:// in those cases,
77
+ # and remove it afterwards.
78
+ #
79
+
80
+ # TODO work on this when clearer
81
+ puts "Linkify requires work"
82
+ # if (!links[ln].schema)
83
+ # urlText = state.md.normalizeLinkText.call('http://' + urlText).replace(/^http:\/\//, '')
84
+ # elsif (links[ln].schema == 'mailto:' && !Regexp.new('^mailto:/i') =~ urlText)
85
+ # urlText = state.md.normalizeLinkText.call('mailto:' + urlText).replace(/^mailto:/, '');
86
+ # } else {
87
+ # urlText = state.md.normalizeLinkText.call(urlText);
88
+ # }
89
+
90
+ pos = links[ln].index
91
+
92
+ if (pos > lastPos)
93
+ token = Token.new('text', '', 0)
94
+ token.content = text.slice(lastPos...pos)
95
+ token.level = level
96
+ nodes.push(token)
97
+ end
98
+
99
+ token = Token.new('link_open', 'a', 1)
100
+ token.attrs = [ [ 'href', fullUrl ] ]
101
+ token.level = level
102
+ level += 1
103
+ token.markup = 'linkify'
104
+ token.info = 'auto'
105
+ nodes.push(token)
106
+
107
+ token = Token.new('text', '', 0)
108
+ token.content = urlText
109
+ token.level = level
110
+ nodes.push(token)
111
+
112
+ token = Token.new('link_close', 'a', -1)
113
+ level -= 1
114
+ token.level = level
115
+ token.markup = 'linkify'
116
+ token.info = 'auto'
117
+ nodes.push(token)
118
+
119
+ lastPos = links[ln].lastIndex
120
+ end
121
+ if (lastPos < text.length)
122
+ token = Token.new('text', '', 0)
123
+ token.content = text.slice_to_end(lastPos)
124
+ token.level = level
125
+ nodes.push(token)
126
+ end
127
+
128
+ # replace current node
129
+ blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes)
130
+ end
131
+ i -= 1
132
+ end
133
+ end
134
+ end
135
+
136
+ end
137
+ end
138
+ end