markascend 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,154 @@
1
+ module Markascend
2
+ class Macro
3
+ %w[del underline sub sup].each do |m|
4
+ eval <<-RUBY
5
+ def parse_#{m}
6
+ "<#{m}>\#{::Markascend.escape_html content}</#{m}>"
7
+ end
8
+ RUBY
9
+ end
10
+
11
+ def parse_img
12
+ s = ::StringScanner.new content
13
+ unless src = s.scan(/(\\\ |\S)+/)
14
+ env.warn "require src for \\img"
15
+ return
16
+ end
17
+ alt = Macro.scan_attr s, 'alt'
18
+ s.skip /\s+/
19
+ href = Macro.scan_attr s, 'href'
20
+ s.skip /\s+/
21
+ if alt2 = Macro.scan_attr(s, 'alt')
22
+ alt = alt2
23
+ end
24
+ unless s.eos?
25
+ env.warn "parse error for content of \\img"
26
+ return
27
+ end
28
+
29
+ alt = ::Markascend.escape_attr alt
30
+ if env.inline_img
31
+ begin
32
+ if env.pwd
33
+ Dir.chdir env.pwd do
34
+ data = open src, 'rb', &:read
35
+ end
36
+ else
37
+ data = open src, 'rb', &:read
38
+ end
39
+ mime = ::Markascend.mime data
40
+ src = "data:#{mime};base64,#{::Base64.strict_encode64 data}"
41
+ rescue
42
+ env.warn $!.message
43
+ end
44
+ end
45
+
46
+ img = %Q|<img src="#{src}" alt="#{alt}"/>|
47
+ if href
48
+ href = ::Markascend.escape_attr href
49
+ %Q|<a href="#{href}">#{img}</a>|
50
+ else
51
+ img
52
+ end
53
+ end
54
+
55
+ def parse_html
56
+ # TODO sanitize in strict mode
57
+ content
58
+ end
59
+
60
+ def parse_slim
61
+ ::Slim::Template.new(){content}.render env.scope
62
+ end
63
+
64
+ def parse_csv
65
+ Macro.generate_csv content, false
66
+ end
67
+
68
+ def parse_headless_csv
69
+ Macro.generate_csv content, true
70
+ end
71
+
72
+ def parse_latex
73
+ %Q|<code class="latex">#{content}</code>|
74
+ end
75
+
76
+ def parse_options
77
+ yaml = ::YAML.load(content) rescue nil
78
+ if yaml.is_a?(Hash)
79
+ env.options.merge! yaml
80
+ else
81
+ env.warn '\options content should be a yaml hash'
82
+ end
83
+ ''
84
+ end
85
+
86
+ def parse_hi
87
+ # TODO validate syntax name
88
+ env.hi = content == 'none' ? nil : content
89
+ ''
90
+ end
91
+
92
+ def parse_dot
93
+ err, out, code = nil
94
+ ::Open3.popen3 'dot', '-Tpng' do |i, o, e, t|
95
+ i.puts content
96
+ i.close
97
+ err = e.read
98
+ out = o.read
99
+ code = t.value.to_i
100
+ e.close
101
+ o.close
102
+ end
103
+ if code != 0
104
+ env.warn err
105
+ return
106
+ end
107
+
108
+ data = ::Base64.strict_encode64 out
109
+ %Q|<img src="data:image/png;base64,#{data}" alt="#{Markascend.escape_attr content}"/>|
110
+ end
111
+
112
+ def Macro.generate_csv content, headless
113
+ rows = ::CSV.parse(content)
114
+ return if rows.empty?
115
+
116
+ table = "<table>"
117
+ unless headless
118
+ table << "<thead>"
119
+ head = rows.shift
120
+ head.map!{|e| "<th>#{::Markascend.escape_html e}</th>" }
121
+ table << "<tr>#{head.join}</tr>"
122
+ table << "</thead>"
123
+ end
124
+
125
+ table << "<tbody>"
126
+ rows.each do |row|
127
+ row.map!{|e| "<td>#{::Markascend.escape_html e}</td>" }
128
+ table << "<tr>#{row.join}</tr>"
129
+ end
130
+ table << "</tbody></table>"
131
+ end
132
+
133
+ def Macro.scan_attr strscan, attr_name
134
+ pos = strscan.pos
135
+ strscan.skip /\s+/
136
+ unless strscan.peek(attr_name.size) == attr_name
137
+ strscan.pos = pos
138
+ return
139
+ end
140
+ strscan.pos += attr_name.size
141
+
142
+ if strscan.scan(/\s*=\s*/)
143
+ # http://www.w3.org/TR/html5/syntax.html#attributes-0
144
+ if v = strscan.scan(/(["']).*?\1/)
145
+ v[1...-1]
146
+ else
147
+ strscan.scan(/\w+/)
148
+ end
149
+ else
150
+ attr_name
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,68 @@
1
+ module Markascend
2
+ class Env
3
+ attr_reader :autolink, :inline_img, :sandbox, :toc, :pwd
4
+ attr_reader :macros, :line_units, :scope, :options, :footnotes, :srcs, :warnings
5
+ attr_accessor :hi
6
+
7
+ def initialize(autolink: %w[http https ftp mailto], inline_img: false, sandbox: false, toc: false, **opts)
8
+ @autolink = autolink
9
+ @inline_img = inline_img
10
+ @sandbox = sandbox
11
+ @toc = toc ? {} : false # {id => [x, header_content]}
12
+
13
+ if opts[:path]
14
+ pwd = File.dirname opts[:path]
15
+ if File.directory?(pwd)
16
+ @pwd = pwd
17
+ end
18
+ end
19
+
20
+ if opts[:macros]
21
+ @macros = {}
22
+ opts[:macros].each do |m|
23
+ meth = "parse_#{m}"
24
+ if Macro.respond_to?(meth)
25
+ @macros[m] = meth
26
+ else
27
+ raise ArgumentError, "macro processor #{meth} not defined"
28
+ end
29
+ end
30
+ elsif @sandbox
31
+ @macros = SANDBOX_MACROS
32
+ else
33
+ @macros = DEFAULT_MACROS
34
+ end
35
+
36
+ if opts[:line_units]
37
+ @line_units = opts[:line_units].map do |m|
38
+ meth = "parse_#{m}"
39
+ if LineUnit.respond_to?(meth)
40
+ meth
41
+ else
42
+ raise ArgumentError, "line-unit parser #{meth} not defined"
43
+ end
44
+ end
45
+ else
46
+ @line_units = DEFAULT_LINE_UNITS
47
+ end
48
+
49
+ @scope = opts[:scope] || Object.new.send(:binding)
50
+ @options = {} # for \options macro
51
+ @footnotes = {} # {abbrev => details}. for [.] and [:] elements
52
+ @srcs = [] # recursive parser stack, everyone has the contiguous right one scanned
53
+ @warnings = {} # {line => message}
54
+ @hi = nil # current syntax hiliter
55
+ end
56
+
57
+ def warn msg
58
+ if @srcs.size
59
+ current_src = @srcs.last
60
+ line = @srcs.first.string.count("\n") - current_src.string[(current_src.pos)..-1].count("\n")
61
+ @warnings[line] = msg
62
+ else
63
+ # warnings without source is set to line 0
64
+ @warnings[0] = msg
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,226 @@
1
+ module Markascend
2
+ LineUnit = ::Struct.new :env, :line, :block
3
+ # process a line with (maybe) a followed up indented block
4
+ class LineUnit
5
+ def parse out=nil
6
+ @out = []
7
+ @src = ::StringScanner.new line
8
+ parsers = env.line_units
9
+ while parsers.any?{|p| send p}
10
+ end
11
+
12
+ if out
13
+ @out.each do |token|
14
+ out << token
15
+ end
16
+ else
17
+ @out.join
18
+ end
19
+ end
20
+
21
+ # the same as markdown
22
+ def parse_inline_code
23
+ if s = @src.scan(/
24
+ (`{1,})(\ ?)
25
+ .*?
26
+ \2\1
27
+ /x)
28
+ s =~ /^
29
+ (`{1,})(\ ?)
30
+ (.*?)
31
+ \2\1
32
+ $/x
33
+ @out << (::Markascend.hilite $3, env.hi, true)
34
+ true
35
+ end
36
+ end
37
+
38
+ # no escape chars, but \\ and \$ are treated as atomic parsing units
39
+ def parse_math
40
+ if (s = @src.scan /\$(?:\\[\\\$]|[^\$])*\$/)
41
+ @out << '<code class="math">'
42
+ @out << (::Markascend.escape_html s[1...-1])
43
+ @out << '</code>'
44
+ true
45
+ end
46
+ end
47
+
48
+ def parse_auto_link
49
+ # TODO make protocol configurable
50
+ if (s = @src.scan /(https|http|ftp|mailto)\:\/\/\S+/)
51
+ s.gsub! /"/, '\\"'
52
+ @out << '<a href="#{s}">'
53
+ @out << (::Markascend.escape_html s)
54
+ @out << '</a>'
55
+ true
56
+ end
57
+ end
58
+
59
+ def parse_macro
60
+ start = @src.pos
61
+ return unless macro = @src.scan(/\\(?!\d)\w+/)
62
+ macro = macro[1..-1]
63
+ if block = scan_lexical_parens
64
+ inline_content = @src.string[start..(@src.pos)]
65
+ elsif block = scan_recursive_braces
66
+ inline_content = @src.string[start..(@src.pos)]
67
+ else
68
+ block = self.block
69
+ end
70
+ @out << ::Markascend::Macro.new(env, block, inline_content).parse(macro)
71
+ true
72
+ end
73
+
74
+ def parse_link
75
+ pos = @src.pos
76
+ return unless @src.scan(/\[/)
77
+
78
+ footnote = @src.scan(/[\.\:]/)
79
+ content = ''
80
+ loop do
81
+ if res = @src.scan(/\]/)
82
+ break
83
+ elsif res = parse_char(false)
84
+ content << res
85
+ else
86
+ # failed
87
+ @src.pos = pos
88
+ return
89
+ end
90
+ end
91
+
92
+ if footnote and env.sandbox
93
+ scan_lexical_parens || scan_recursive_braces
94
+ @out << ::Markascend.escape_html(@src.string[pos..(@src.pos)])
95
+ return true
96
+ end
97
+
98
+ case footnote
99
+ when '.'
100
+ unless explain = scan_lexical_parens || scan_recursive_braces
101
+ @src.pos = pos
102
+ return
103
+ end
104
+
105
+ footnotes = env.footnotes
106
+ if content =~ /\A\s*\z/
107
+ content = footnotes.size + 1
108
+ end
109
+ raise "Already defined footnote: #{content}" if footnotes.has_key?(content)
110
+ footnotes[content] = explain
111
+ # TODO id prefix configurable
112
+ @out << %Q|<a href="#footnote-#{footnotes.size}">#{content}</a>|
113
+ true
114
+ when ':'
115
+ footnotes = env.footnotes
116
+ if content =~ /\A\s*(\d+)\s*\z/
117
+ @out << [:footnote_id_ref, $1.to_i]
118
+ else
119
+ content.strip!
120
+ @out << [:footnote_acronym_ref, content]
121
+ end
122
+ true
123
+ else
124
+ if addr = scan_lexical_parens || scan_recursive_braces
125
+ # TODO smarter addr to recognize things like a.b.com
126
+ addr = ::Markascend.escape_attr addr
127
+ @out << %Q|<a href="#{addr}">#{content}</a>|
128
+ true
129
+ else
130
+ @src.pos = pos
131
+ nil
132
+ end
133
+ end
134
+ end
135
+
136
+ def parse_bold_italic emit=true
137
+ pos = @src.pos
138
+ if s = @src.scan(/\*\*/)
139
+ term = /\*\*/
140
+ out = ['<b>']
141
+ out_end = '</b>'
142
+ elsif s = @src.scan(/\*/)
143
+ term = /\*/
144
+ out = ['<i>']
145
+ out_end = '</i>'
146
+ else
147
+ return
148
+ end
149
+
150
+ loop do
151
+ if res = @src.scan(/\\[\\\*]/)
152
+ out << res[1]
153
+ elsif @src.scan(term)
154
+ out << out_end
155
+ break
156
+ elsif res = parse_bold_italic(false)
157
+ out << res
158
+ elsif res = parse_char(false)
159
+ out << res
160
+ else
161
+ # failed
162
+ @src.pos = pos
163
+ return
164
+ end
165
+ end
166
+
167
+ if emit
168
+ @out << out.join
169
+ true
170
+ else
171
+ out.join
172
+ end
173
+ end
174
+
175
+ def parse_char emit=true
176
+ # entity list generated by:
177
+ # ruby -rregexp_optimized_union -e'puts Regexp.optimized_union($stdin.read.scan(/(?<=ENTITY )\w+/)).source' < tools/html4_entities.dtd
178
+ # dtd from http://www.w3.org/TR/html4/sgml/entities.html
179
+ if c = @src.scan(/
180
+ &(?:
181
+ n(?:bsp|ot|tilde)|i(?:excl|quest|grave|acute|circ|uml)|c(?:e(?:nt|dil)|urren|opy|cedil)|p(?:ound|lusmn|ara)|y(?:en|acute|uml)|brvbar|s(?:ect|hy|up[123]|zlig)|u(?:ml|grave|acute|circ|uml)|o(?:rd[fm]|grave|acute|circ|tilde|uml|slash)|laquo|r(?:eg|aquo)|m(?:acr|i(?:cro|ddot))|d(?:eg|ivide)|a(?:c(?:ute|irc)|grave|acute|tilde|uml|ring|elig)|frac(?:1[24]|34)|A(?:grave|acute|circ|tilde|uml|ring|Elig)|Ccedil|E(?:grave|acute|circ|uml|TH)|I(?:grave|acute|circ|uml)|Ntilde|O(?:grave|acute|circ|tilde|uml|slash)|t(?:imes|horn)|U(?:grave|acute|circ|uml)|Yacute|THORN|e(?:grave|acute|circ|uml|th)
182
+ |\#\d{4}
183
+ |\#x\h{4}
184
+ );
185
+ /x)
186
+ elsif c = @src.scan(/\\\W/)
187
+ c = ::Markascend.escape_html c[1]
188
+ elsif c = @src.scan(/\n/)
189
+ c = :'<br>'
190
+ elsif c = @src.scan(/./)
191
+ c = ::Markascend.escape_html c
192
+ else
193
+ return
194
+ end
195
+
196
+ if emit
197
+ @out << c
198
+ true
199
+ else
200
+ c
201
+ end
202
+ end
203
+
204
+ # {...}
205
+ def scan_recursive_braces
206
+ if s = @src.scan(/
207
+ (?<braces> \{
208
+ ([^\{]+ | \g<braces>)* # recursive rule
209
+ \})
210
+ /x)
211
+ s[1...-1]
212
+ end
213
+ end
214
+
215
+ # (...)
216
+ def scan_lexical_parens
217
+ if s = @src.scan(/
218
+ \(
219
+ (?: \\ [\\\)] | [^\)] )+ # lexical rule
220
+ \)
221
+ /x)
222
+ s[1...-1].gsub(/\\[\)\\]/){|x| x[1]}
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,13 @@
1
+ module Markascend
2
+ # inline contains the src for fallback
3
+ Macro = ::Struct.new :env, :content, :inline
4
+ class Macro
5
+ def parse name
6
+ self.content ||= ''
7
+ if meth = env.macros[name]
8
+ res = send meth
9
+ end
10
+ res or (inline ? ::Markascend.escape_html(inline) : "\\#{name}")
11
+ end
12
+ end
13
+ end