markascend 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/copying +18 -0
- data/doc/api.ma +65 -0
- data/doc/index.ma +23 -0
- data/doc/syntax.ma +264 -0
- data/lib/markascend.rb +172 -0
- data/lib/markascend/builtin_macros.rb +154 -0
- data/lib/markascend/env.rb +68 -0
- data/lib/markascend/line_unit.rb +226 -0
- data/lib/markascend/macro.rb +13 -0
- data/lib/markascend/parser.rb +220 -0
- data/lib/markascend/popular_company_macros.rb +87 -0
- data/rakefile +176 -0
- data/readme +4 -0
- data/test/builtin_macros_test.rb +73 -0
- data/test/line_unit_test.rb +40 -0
- data/test/markascend_test.rb +19 -0
- data/test/parser_test.rb +93 -0
- data/test/popular_company_macros_test.rb +42 -0
- data/test/test_helper.rb +21 -0
- metadata +64 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
module Markascend
|
2
|
+
class Parser
|
3
|
+
REC_START = /\A[\+\-\>]\ /
|
4
|
+
NON_PARA_START = /
|
5
|
+
^[\+\-\>]\ # rec block
|
6
|
+
|
|
7
|
+
^\|\ *(?!\d)\w*\ *$ # block code
|
8
|
+
|
|
9
|
+
^h[1-6](?:\#\w+(?:-\w+)*)?\ # header
|
10
|
+
/x
|
11
|
+
REC_BLOCK_STARTS = {
|
12
|
+
'+ ' => /\+\ /,
|
13
|
+
'- ' => /\-\ /,
|
14
|
+
'> ' => /\>\ /
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize env, src
|
18
|
+
@src = StringScanner.new src
|
19
|
+
@env = env
|
20
|
+
@top_level = @env.srcs.empty?
|
21
|
+
@env.srcs.push @src
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse
|
25
|
+
@out = []
|
26
|
+
while parse_new_line or parse_rec_block or parse_hx or parse_block_code or parse_paragraph
|
27
|
+
end
|
28
|
+
unless @src.eos?
|
29
|
+
@env.warn 'reached end of input'
|
30
|
+
end
|
31
|
+
@env.srcs.pop
|
32
|
+
|
33
|
+
@out.map! do |(node, content)|
|
34
|
+
case node
|
35
|
+
when :footnode_id_ref
|
36
|
+
if content < 1 or content > @env.footnotes.size
|
37
|
+
raise "footnote not defined: #{content}"
|
38
|
+
end
|
39
|
+
%Q|<a href="#footnote-#{content}">#{content}</a>|
|
40
|
+
when :footnode_acronym_ref
|
41
|
+
unless index = @env.footnotes.find_index{|k, _| k == content }
|
42
|
+
raise "footnote note defined: #{content}"
|
43
|
+
end
|
44
|
+
%Q|<a href="#footnote-#{index + 1}">#{content}</a>|
|
45
|
+
else
|
46
|
+
node
|
47
|
+
end
|
48
|
+
end
|
49
|
+
@out.join
|
50
|
+
end
|
51
|
+
|
52
|
+
def warnings
|
53
|
+
@env.warnings
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_new_line
|
57
|
+
if @src.scan(/\ *\n/)
|
58
|
+
@out << '<br>'
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_rec_block
|
64
|
+
return unless @src.match? REC_START
|
65
|
+
|
66
|
+
# first elem, scans the whole of following string:
|
67
|
+
# |
|
68
|
+
# + line 1 of the li. an embed \macro
|
69
|
+
# macro content
|
70
|
+
# line 2 of the li.
|
71
|
+
# line 3 of the li.
|
72
|
+
#
|
73
|
+
# NOTE that first line is always not indented
|
74
|
+
line, block = scan_line_and_block 2
|
75
|
+
return unless line
|
76
|
+
rec_start = line[REC_START]
|
77
|
+
wrapper_begin, elem_begin, elem_end, wrapper_end =
|
78
|
+
case rec_start
|
79
|
+
when '+ '; ['<ol>', '<li>', '</li>', '</ol>']
|
80
|
+
when '- '; ['<ul>', '<li>', '</li>', '</ul>']
|
81
|
+
when '> '; ['', '<quote>', '</quote>', '']
|
82
|
+
end
|
83
|
+
elems = ["#{line[2..-1]}#{block}"]
|
84
|
+
|
85
|
+
# followed up elems
|
86
|
+
block_start_re = REC_BLOCK_STARTS[rec_start]
|
87
|
+
while @src.match?(block_start_re)
|
88
|
+
line, block = scan_line_and_block 2
|
89
|
+
break unless line
|
90
|
+
elems << "#{line[2..-1]}#{block}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# generate
|
94
|
+
@out << wrapper_begin
|
95
|
+
elems.each do |elem|
|
96
|
+
@out << elem_begin
|
97
|
+
elem.rstrip!
|
98
|
+
@out << Parser.new(@env, elem).parse
|
99
|
+
@out << elem_end
|
100
|
+
end
|
101
|
+
@out << wrapper_end
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_hx
|
106
|
+
hx = @src.scan /h[1-6](\#\w+(-\w+)*)?\ /
|
107
|
+
return unless hx
|
108
|
+
hx.strip!
|
109
|
+
|
110
|
+
# fiddle id
|
111
|
+
if @env.sandbox
|
112
|
+
if @env.toc
|
113
|
+
id = "-#{@env.toc.size}"
|
114
|
+
end
|
115
|
+
else
|
116
|
+
if hx.size > 2
|
117
|
+
id = hx[3..-1]
|
118
|
+
elsif @env.toc
|
119
|
+
id = "-#{@env.toc.size}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
if id
|
123
|
+
id_attr = %Q{ id="#{id}"}
|
124
|
+
end
|
125
|
+
hx = hx[0...2]
|
126
|
+
|
127
|
+
@out << "<#{hx}#{id_attr}>"
|
128
|
+
line, block = scan_line_and_block
|
129
|
+
if line
|
130
|
+
out = []
|
131
|
+
LineUnit.new(@env, line, block).parse(out)
|
132
|
+
out.pop if out.last == :"<br>"
|
133
|
+
out.each do |token|
|
134
|
+
@out << token
|
135
|
+
end
|
136
|
+
if id and @env.toc
|
137
|
+
@env.toc[id] = [hx[1].to_i, out.join]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
@out << "</#{hx}>"
|
141
|
+
|
142
|
+
# consume one more empty line if possible
|
143
|
+
@src.scan /\ *\n/ if (!block or block.empty?)
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_block_code
|
148
|
+
if lang = @src.scan(/\|\ *(?!\d)\w*\ *\n/)
|
149
|
+
lang = lang[1..-1].strip
|
150
|
+
if lang.empty? and @env.hi
|
151
|
+
lang = @env.hi
|
152
|
+
end
|
153
|
+
block = @src.scan(/
|
154
|
+
(
|
155
|
+
\ *\n # empty line
|
156
|
+
|
|
157
|
+
\ {2,}.*\n # line indented equal to 2 or more than 2
|
158
|
+
)*
|
159
|
+
/x)
|
160
|
+
block.gsub!(/^ /, '')
|
161
|
+
block.rstrip!
|
162
|
+
@out << (::Markascend.hilite block, lang)
|
163
|
+
true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def parse_paragraph
|
168
|
+
@src.match?(/\ */)
|
169
|
+
indent = @src.matched_size
|
170
|
+
line, block = scan_line_and_block
|
171
|
+
if line
|
172
|
+
@out << :"<p>" if @top_level
|
173
|
+
LineUnit.new(@env, line, block).parse(@out)
|
174
|
+
|
175
|
+
# same indent and not matching rec/code blocks
|
176
|
+
while (@src.match?(/\ */); @src.matched_size) == indent and !@src.match?(NON_PARA_START)
|
177
|
+
line, block = scan_line_and_block
|
178
|
+
break unless line
|
179
|
+
LineUnit.new(@env, line, block).parse(@out)
|
180
|
+
end
|
181
|
+
# consume one more empty line if possible
|
182
|
+
@src.scan /\ *\n/
|
183
|
+
# delete back last <br>
|
184
|
+
@out.pop if @out.last == :"<br>"
|
185
|
+
@out << :"</p>" if @top_level
|
186
|
+
true
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def scan_line_and_block undent=:all
|
191
|
+
if line = @src.scan(/.+?(?:\n|\z)/)
|
192
|
+
block = @src.scan(/
|
193
|
+
(?:\ *\n)* # leading empty lines
|
194
|
+
(?<indent>\ {2,})
|
195
|
+
.*?(?:\n|\z) # first line content
|
196
|
+
(
|
197
|
+
\g<indent>\ *
|
198
|
+
.*?(?:\n|\z) # rest line content
|
199
|
+
|
|
200
|
+
(?:\ *\n)* # rest empty line
|
201
|
+
)*
|
202
|
+
/x)
|
203
|
+
block = nil if block =~ /\A\s*\z/
|
204
|
+
# undent block
|
205
|
+
if block
|
206
|
+
if undent == :all
|
207
|
+
/
|
208
|
+
(?:\ *\n)*
|
209
|
+
(?<indent>\ {2,})
|
210
|
+
/x =~ block
|
211
|
+
block.gsub! /^#{indent}/, ''
|
212
|
+
else
|
213
|
+
block.gsub! /^\ \ /, ''
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
[line, block]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Markascend
|
2
|
+
# video
|
3
|
+
|
4
|
+
class Macro
|
5
|
+
# accepts:
|
6
|
+
# |
|
7
|
+
# @somebody
|
8
|
+
def parse_twitter
|
9
|
+
if content.start_with?('@')
|
10
|
+
text = ::Markascend.escape_html content
|
11
|
+
link = "https://twitter.com/#{::Markascend.escape_attr content[1..-1]}"
|
12
|
+
else
|
13
|
+
# TODO embed tweet
|
14
|
+
raise 'not implemented yet'
|
15
|
+
end
|
16
|
+
%Q{<a href="#{link}">#{text}</a>}
|
17
|
+
end
|
18
|
+
|
19
|
+
# accepts:
|
20
|
+
# |
|
21
|
+
# @somebody
|
22
|
+
def parse_weibo
|
23
|
+
if content.start_with?('@')
|
24
|
+
text = ::Markascend.escape_html content
|
25
|
+
link = "https://weibo.com/#{::Markascend.escape_attr content[1..-1]}"
|
26
|
+
else
|
27
|
+
# TODO embed tweet
|
28
|
+
raise 'not implemented yet'
|
29
|
+
end
|
30
|
+
%Q{<a href="#{link}">#{text}</a>}
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_wiki
|
34
|
+
%Q|<a href="http://en.wikipedia.org/wiki/#{content}">#{content}</a>|
|
35
|
+
end
|
36
|
+
|
37
|
+
# embed gist, accepts:
|
38
|
+
# |
|
39
|
+
# luikore/737238
|
40
|
+
# gist.github.com/luikore/737238
|
41
|
+
# https://gist.github.com/luikore/737238
|
42
|
+
def parse_gist
|
43
|
+
src = content.strip
|
44
|
+
if src =~ /\A\w+(\-\w+)*\/\d+/
|
45
|
+
src = "https://gist.github.com/#{src}"
|
46
|
+
else
|
47
|
+
src.sub! /\A(?=gist\.github\.com)/, 'https://'
|
48
|
+
end
|
49
|
+
src.sub!(/((?<!\.js)|\.git)$/, '.js')
|
50
|
+
%Q|<script src="#{src}"></script>|
|
51
|
+
end
|
52
|
+
|
53
|
+
# embed video, calculates embed iframe by urls from various simple formats, but not accept iframe code
|
54
|
+
def parse_video
|
55
|
+
# standard
|
56
|
+
unless /\A\s*(?<width>\d+)x(?<height>\d+)\s+(?<url>.+)\z/ =~ content
|
57
|
+
env.warn 'can not parse \video content, should be "#{WIDTH}x#{HEIGHT} #{URL}"'
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
case url
|
62
|
+
when /youtu\.?be/
|
63
|
+
# NOTE merging them into one regexp fails (because longest match?)
|
64
|
+
unless id = url[/(?<=watch\?v=)\w+/] || url[/(?<=embed\/)\w+/] || url[/(?<=youtu\.be\/)\w+/]
|
65
|
+
env.warn 'can not parse youtube id'
|
66
|
+
return
|
67
|
+
end
|
68
|
+
%Q|<iframe width="#{width}" height="#{height}" src="https://www.youtube-nocookie.com/embed/#{id}?rel=0" frameborder="0" allowfullscreen></iframe>|
|
69
|
+
when /vimeo/
|
70
|
+
unless id = url[/(?<=vimeo\.com\/)\w+/]
|
71
|
+
env.warn 'can not parse vimeo id, should use link like this "http://vimeo.com/#{DIGITS}"'
|
72
|
+
return
|
73
|
+
end
|
74
|
+
%Q|<iframe width="#{width}" height="#{height}" src="https://player.vimeo.com/video/#{id}" frameborder="0" allowFullScreen></iframe>|
|
75
|
+
when /sm/
|
76
|
+
unless id = url[/\bsm\d+/]
|
77
|
+
env.warn 'can not find "sm#{DIGITS}" from link'
|
78
|
+
return
|
79
|
+
end
|
80
|
+
%Q|<script src="https://ext.nicovideo.jp/thumb_watch/#{id}?w=#{width}&h=#{height}"></script>"|
|
81
|
+
else
|
82
|
+
env.warn 'failed to parse video link, currently only youtube, vimeo and niconico are supported'
|
83
|
+
return
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/rakefile
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
def osascript src
|
2
|
+
IO.popen 'osascript', 'r+' do |f|
|
3
|
+
f.puts src
|
4
|
+
f.close_write
|
5
|
+
f.read
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_default_browser
|
10
|
+
# search handler for http
|
11
|
+
res = osascript <<-APPLESCRIPT
|
12
|
+
tell (system attribute "sysv") to set MacOS_version to it mod 4096 div 16
|
13
|
+
if MacOS_version is 5 then
|
14
|
+
set {a1, a2} to {1, 2}
|
15
|
+
else
|
16
|
+
set {a1, a2} to {2, 1}
|
17
|
+
end if
|
18
|
+
set pListpath to (path to preferences as Unicode text) & "com.apple.LaunchServices.plist"
|
19
|
+
tell application "System Events"
|
20
|
+
repeat with i in property list items of property list item 1 of contents of property list file pListpath
|
21
|
+
if value of property list item a2 of i is "http" then
|
22
|
+
return value of property list item a1 of i
|
23
|
+
end if
|
24
|
+
end repeat
|
25
|
+
return "com.apple.Safari"
|
26
|
+
end tell
|
27
|
+
APPLESCRIPT
|
28
|
+
case res
|
29
|
+
when /canary/
|
30
|
+
"Google Chrome Canary"
|
31
|
+
when /chrome/
|
32
|
+
"Google Chrome"
|
33
|
+
when /safari/
|
34
|
+
"Safari"
|
35
|
+
else
|
36
|
+
raise "browser not supported yet"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# [window, tab]
|
41
|
+
def search_tab_with_url browser, url
|
42
|
+
case browser
|
43
|
+
when /Chrome/
|
44
|
+
res = osascript <<-APPLESCRIPT
|
45
|
+
set urls to ""
|
46
|
+
tell application "#{browser}"
|
47
|
+
set i to 1
|
48
|
+
repeat with w in windows
|
49
|
+
set j to 1
|
50
|
+
repeat with t in (tabs of w)
|
51
|
+
set u to the URL of t
|
52
|
+
set u to (i as text) & "," & (j as text) & u
|
53
|
+
set urls to urls & u & return
|
54
|
+
set j to j + 1
|
55
|
+
end repeat
|
56
|
+
set i to i + 1
|
57
|
+
end repeat
|
58
|
+
end tell
|
59
|
+
return urls
|
60
|
+
APPLESCRIPT
|
61
|
+
when /Safari/
|
62
|
+
# URL is not set when loading, so need a stupid null testing
|
63
|
+
res = osascript <<-APPLESCRIPT
|
64
|
+
set urls to ""
|
65
|
+
tell application "Safari"
|
66
|
+
set i to 1
|
67
|
+
repeat with w in windows
|
68
|
+
set j to 1
|
69
|
+
repeat with t in (tabs of w)
|
70
|
+
set u to the URL of t
|
71
|
+
set isNull to false
|
72
|
+
try
|
73
|
+
get u
|
74
|
+
on error
|
75
|
+
set isNull to true
|
76
|
+
end try
|
77
|
+
if isNull then
|
78
|
+
else
|
79
|
+
set u to (i as text) & "," & (j as text) & u
|
80
|
+
set urls to urls & u & return
|
81
|
+
end if
|
82
|
+
set j to j + 1
|
83
|
+
end repeat
|
84
|
+
set i to i + 1
|
85
|
+
end repeat
|
86
|
+
end tell
|
87
|
+
return urls
|
88
|
+
APPLESCRIPT
|
89
|
+
end
|
90
|
+
res.strip.split(/[\r\n]+/).each do |u|
|
91
|
+
if u.index url
|
92
|
+
/^(?<window>\d+),(?<tab>\d+)/ =~ u
|
93
|
+
return [window, tab]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def activate_url url
|
100
|
+
browser = get_default_browser
|
101
|
+
window, tab = search_tab_with_url browser, url
|
102
|
+
url.gsub! '"', '\"'
|
103
|
+
if tab # activate existing doc
|
104
|
+
case browser
|
105
|
+
when /Chrome/
|
106
|
+
osascript <<-APPLESCRIPT
|
107
|
+
tell application "#{browser}"
|
108
|
+
activate
|
109
|
+
set active tab index of window #{window} to #{tab}
|
110
|
+
reload tab #{tab} of window #{window}
|
111
|
+
end tell
|
112
|
+
APPLESCRIPT
|
113
|
+
when /Safari/
|
114
|
+
osascript <<-APPLESCRIPT
|
115
|
+
tell application "#{browser}"
|
116
|
+
activate
|
117
|
+
tell window #{window} to set current tab to tab #{tab}
|
118
|
+
set the url of the front document to "#{url}"
|
119
|
+
end tell
|
120
|
+
APPLESCRIPT
|
121
|
+
end
|
122
|
+
else # open new doc
|
123
|
+
case browser
|
124
|
+
when /Chrome/
|
125
|
+
# NOTE some user sets handler for .html to editor than browser
|
126
|
+
# so we don't use `open` command here
|
127
|
+
osascript <<-APPLESCRIPT
|
128
|
+
tell application "#{browser}"
|
129
|
+
activate
|
130
|
+
make new tab at the end of tabs of window 1
|
131
|
+
set the URL of active tab of window 1 to "#{url}"
|
132
|
+
end tell
|
133
|
+
APPLESCRIPT
|
134
|
+
when /Safari/
|
135
|
+
osascript <<-APPLESCRIPT
|
136
|
+
tell application "#{browser}"
|
137
|
+
activate
|
138
|
+
make new document at the end of documents
|
139
|
+
set the url of the front document to "#{url}"
|
140
|
+
end tell
|
141
|
+
APPLESCRIPT
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
desc "run tests"
|
147
|
+
task :default do
|
148
|
+
dir = File.dirname __FILE__
|
149
|
+
Dir.glob "#{dir}/test/*_test.rb" do |f|
|
150
|
+
require f
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
desc "generate gh-pages"
|
155
|
+
task :site do
|
156
|
+
# layout
|
157
|
+
require "slim"
|
158
|
+
template = Slim::Template.new "doc/layout.slim"
|
159
|
+
|
160
|
+
# html
|
161
|
+
require_relative "lib/markascend"
|
162
|
+
%w[syntax api].each do |doc|
|
163
|
+
File.open "gh-pages/#{doc}.html", 'w' do |f|
|
164
|
+
f.puts template.render{ Markascend.compile File.read "doc/#{doc}.ma" }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# css
|
169
|
+
system 'sass --compass -C doc/style.sass gh-pages/style.css'
|
170
|
+
end
|
171
|
+
|
172
|
+
desc "preview gh-pages in default browser"
|
173
|
+
task :preview => :site do
|
174
|
+
url = File.expand_path "gh-pages/syntax.html"
|
175
|
+
activate_url "file://#{url}"
|
176
|
+
end
|