motion-markdown-it 0.4.0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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