markascend 0.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.
@@ -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