docdown 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -0
- data/README.md +126 -0
- data/Rakefile +16 -0
- data/bin/docdown +47 -0
- data/docdown.gemspec +28 -0
- data/lib/docdown.rb +37 -0
- data/lib/docdown/code_command.rb +37 -0
- data/lib/docdown/code_commands/bash.rb +23 -0
- data/lib/docdown/code_commands/no_such_command.rb +6 -0
- data/lib/docdown/code_commands/repl.rb +38 -0
- data/lib/docdown/code_commands/write.rb +27 -0
- data/lib/docdown/parser.rb +138 -0
- data/lib/docdown/version.rb +3 -0
- data/test/docdown/parser_test.rb +104 -0
- data/test/docdown/regex_test.rb +200 -0
- data/test/docdown/test_parse_java.rb +8 -0
- data/test/fixtures/README.md +905 -0
- data/test/fixtures/docdown.rb +8 -0
- data/test/fixtures/java_websockets.md +225 -0
- data/test/test_helper.rb +15 -0
- data/tmp.file +0 -0
- metadata +131 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
module Docdown
|
2
|
+
class Parser
|
3
|
+
class ParseError < StandardError
|
4
|
+
def initialize(options = {})
|
5
|
+
keyword = options[:keyword]
|
6
|
+
command = options[:command]
|
7
|
+
line_number = options[:line_number]
|
8
|
+
block = options[:block].lines.map do |line|
|
9
|
+
if line == command
|
10
|
+
" > #{line}"
|
11
|
+
else
|
12
|
+
" #{line}"
|
13
|
+
end
|
14
|
+
end.join("")
|
15
|
+
|
16
|
+
msg = "Error parsing (line:#{line_number}):\n"
|
17
|
+
msg << "> '#{command.strip}'\n"
|
18
|
+
msg << "No such registered command: '#{keyword}'\n"
|
19
|
+
msg << "registered commands: #{Docdown.known_commands.inspect}\n\n"
|
20
|
+
msg << block
|
21
|
+
msg << "\n"
|
22
|
+
super msg
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
DEFAULT_KEYWORD = ":::"
|
28
|
+
INDENT_BLOCK = '(?<before_indent>(^\s*$\n|\A)(^(?:[ ]{4}|\t))(?<indent_contents>.*)(?<after_indent>[^\s].*$\n?(?:(?:^\s*$\n?)*^(?:[ ]{4}|\t).*[^\s].*$\n?)*))'
|
29
|
+
GITHUB_BLOCK = '^(?<fence>(?<fence_char>~|`){3,})\s*?(?<lang>\w+)?\s*?\n(?<contents>.*?)^\g<fence>\g<fence_char>*\s*?\n'
|
30
|
+
CODEBLOCK_REGEX = /(#{GITHUB_BLOCK})/m
|
31
|
+
COMMAND_REGEX = ->(keyword) {
|
32
|
+
/^#{keyword}(?<tag>(\s|=|-)?)\s*(?<command>(\S)+)\s+(?<statement>.*)$/
|
33
|
+
}
|
34
|
+
|
35
|
+
attr_reader :contents, :keyword, :stack
|
36
|
+
|
37
|
+
def initialize(contents, options = {})
|
38
|
+
@contents = contents
|
39
|
+
@original = contents.dup
|
40
|
+
@keyword = options[:keyword] || DEFAULT_KEYWORD
|
41
|
+
@stack = []
|
42
|
+
partition
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def to_md
|
47
|
+
@stack.map do |s|
|
48
|
+
if s.respond_to?(:render)
|
49
|
+
s.render
|
50
|
+
else
|
51
|
+
s
|
52
|
+
end
|
53
|
+
end.join("")
|
54
|
+
end
|
55
|
+
|
56
|
+
# split into [before_code, code, after_code], process code, and re-run until tail is empty
|
57
|
+
def partition
|
58
|
+
until contents.empty?
|
59
|
+
head, code, tail = contents.partition(CODEBLOCK_REGEX)
|
60
|
+
@stack << head unless head.empty?
|
61
|
+
add_fenced_code(code) unless code.empty?
|
62
|
+
@contents = tail
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_fenced_code(fenced_code_str)
|
67
|
+
fenced_code_str.match(CODEBLOCK_REGEX) do |m|
|
68
|
+
fence = m[:fence]
|
69
|
+
lang = m[:lang]
|
70
|
+
code = m[:contents]
|
71
|
+
@stack << "#{fence}#{lang}\n"
|
72
|
+
add_code_commands(code)
|
73
|
+
@stack << "#{fence}\n"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_match_to_code_commands(match, commands)
|
78
|
+
command = match[:command]
|
79
|
+
tag = match[:tag]
|
80
|
+
statement = match[:statement]
|
81
|
+
|
82
|
+
code_command = Docdown.code_command_from_keyword(command, statement)
|
83
|
+
|
84
|
+
case tag
|
85
|
+
when /\-/
|
86
|
+
code_command.hidden = true
|
87
|
+
when /\=/
|
88
|
+
code_command.render_result = true
|
89
|
+
when /\s/
|
90
|
+
# default do nothing
|
91
|
+
end
|
92
|
+
|
93
|
+
@stack << "\n" if commands.last.is_a?(Docdown::CodeCommand)
|
94
|
+
@stack << code_command
|
95
|
+
commands << code_command
|
96
|
+
code_command
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_parse_error(command, code_block)
|
100
|
+
return unless code_command = @stack.last
|
101
|
+
return unless code_command.is_a?(Docdown::CodeCommands::NoSuchCommand)
|
102
|
+
@original.lines.each_with_index do |line, index|
|
103
|
+
next unless line == command
|
104
|
+
raise ParseError.new(keyword: code_command.keyword,
|
105
|
+
block: code_block,
|
106
|
+
command: command,
|
107
|
+
line_number: index.next)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def add_code_commands(code)
|
112
|
+
commands = []
|
113
|
+
code.lines.each do |line|
|
114
|
+
if match = line.match(command_regex)
|
115
|
+
add_match_to_code_commands(match, commands)
|
116
|
+
check_parse_error(line, code)
|
117
|
+
else
|
118
|
+
unless commands.empty?
|
119
|
+
commands.last << line
|
120
|
+
next
|
121
|
+
end
|
122
|
+
@stack << line
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def contents_to_array
|
128
|
+
partition
|
129
|
+
end
|
130
|
+
|
131
|
+
def command_regex
|
132
|
+
COMMAND_REGEX.call(keyword)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
# convert string of markdown to array of strings and code_commands
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ParserTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_parse_bash
|
9
|
+
contents = <<-RUBY
|
10
|
+
sup
|
11
|
+
|
12
|
+
```
|
13
|
+
::: $ mkdir foo
|
14
|
+
:::= $ ls
|
15
|
+
```
|
16
|
+
|
17
|
+
yo
|
18
|
+
RUBY
|
19
|
+
|
20
|
+
|
21
|
+
Dir.mktmpdir do |dir|
|
22
|
+
Dir.chdir(dir) do
|
23
|
+
expected = "sup\n\n```\n$ mkdir foo \n$ ls \nfoo\n```\n\nyo\n"
|
24
|
+
parsed = Docdown::Parser.new(contents)
|
25
|
+
actual = parsed.to_md
|
26
|
+
assert_equal expected, actual
|
27
|
+
|
28
|
+
parsed = Docdown::Parser.new("\n```\n:::= $ ls\n```\n")
|
29
|
+
actual = parsed.to_md
|
30
|
+
expected = "\n```\n$ ls \nfoo\n```\n"
|
31
|
+
assert_equal expected, actual
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def test_parse_write_commands
|
38
|
+
contents = <<-RUBY
|
39
|
+
sup
|
40
|
+
|
41
|
+
```
|
42
|
+
::: write foo/code.rb
|
43
|
+
a = 1 + 1
|
44
|
+
b = a * 2
|
45
|
+
```
|
46
|
+
yo
|
47
|
+
RUBY
|
48
|
+
|
49
|
+
Dir.mktmpdir do |dir|
|
50
|
+
Dir.chdir(dir) do
|
51
|
+
|
52
|
+
expected = "sup\n\n```\nIn file `foo/code.rb` add:\na = 1 + 1\nb = a * 2\n```\nyo\n"
|
53
|
+
parsed = Docdown::Parser.new(contents)
|
54
|
+
actual = parsed.to_md
|
55
|
+
assert_equal expected, actual
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
contents = <<-RUBY
|
61
|
+
|
62
|
+
```
|
63
|
+
::: write foo/newb.rb
|
64
|
+
puts 'hello world'
|
65
|
+
:::= $ cat foo/newb.rb
|
66
|
+
```
|
67
|
+
RUBY
|
68
|
+
|
69
|
+
Dir.mktmpdir do |dir|
|
70
|
+
Dir.chdir(dir) do
|
71
|
+
|
72
|
+
expected = "\n```\nIn file `foo/newb.rb` add:\nputs 'hello world'\n\n$ cat foo/newb.rb \nputs 'hello world'\n```\n"
|
73
|
+
parsed = Docdown::Parser.new(contents)
|
74
|
+
actual = parsed.to_md
|
75
|
+
assert_equal expected, actual
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_irb
|
81
|
+
|
82
|
+
contents = <<-RUBY
|
83
|
+
```
|
84
|
+
:::= irb --simple-prompt
|
85
|
+
a = 3
|
86
|
+
b = "foo" * a
|
87
|
+
puts b
|
88
|
+
```
|
89
|
+
RUBY
|
90
|
+
|
91
|
+
|
92
|
+
Dir.mktmpdir do |dir|
|
93
|
+
Dir.chdir(dir) do
|
94
|
+
|
95
|
+
parsed = Docdown::Parser.new(contents)
|
96
|
+
actual = parsed.to_md
|
97
|
+
expected = "```\n$ irb --simple-prompt\na = 3\n=> 3\r\nb = \"foo\" * a\n=> \"foofoofoo\"\r\nputs b\nfoofoofoo\r\n=> nil\r```\n"
|
98
|
+
assert_equal expected, actual
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class RegexTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_indent_regex
|
9
|
+
|
10
|
+
contents = <<-RUBY
|
11
|
+
foo
|
12
|
+
|
13
|
+
$ cd
|
14
|
+
yo
|
15
|
+
sup
|
16
|
+
|
17
|
+
bar
|
18
|
+
RUBY
|
19
|
+
|
20
|
+
regex = Docdown::Parser::INDENT_BLOCK
|
21
|
+
parsed = contents.match(/#{regex}/).to_s
|
22
|
+
assert_equal "\n $ cd\n yo\n sup\n", parsed
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_github_regex
|
26
|
+
|
27
|
+
contents = <<-RUBY
|
28
|
+
foo
|
29
|
+
|
30
|
+
```
|
31
|
+
$ cd
|
32
|
+
yo
|
33
|
+
sup
|
34
|
+
```
|
35
|
+
|
36
|
+
bar
|
37
|
+
RUBY
|
38
|
+
|
39
|
+
regex = Docdown::Parser::GITHUB_BLOCK
|
40
|
+
parsed = contents.match(/#{regex}/m).to_s
|
41
|
+
assert_equal "```\n$ cd\nyo\nsup\n```\n", parsed
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_github_tagged_regex
|
45
|
+
contents = <<-RUBY
|
46
|
+
foo
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
$ cd
|
50
|
+
yo
|
51
|
+
sup
|
52
|
+
```
|
53
|
+
|
54
|
+
bar
|
55
|
+
RUBY
|
56
|
+
|
57
|
+
regex = Docdown::Parser::GITHUB_BLOCK
|
58
|
+
parsed = contents.match(/#{regex}/m).to_s
|
59
|
+
assert_equal "```ruby\n$ cd\nyo\nsup\n```\n", parsed
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_command_regex
|
63
|
+
regex = Docdown::Parser::COMMAND_REGEX.call(":::")
|
64
|
+
|
65
|
+
contents = ":::$ mkdir schneems"
|
66
|
+
match = contents.match(regex)
|
67
|
+
assert_equal "", match[:tag]
|
68
|
+
assert_equal "$", match[:command]
|
69
|
+
assert_equal "mkdir schneems", match[:statement]
|
70
|
+
|
71
|
+
contents = ":::=$ mkdir schneems"
|
72
|
+
match = contents.match(regex)
|
73
|
+
assert_equal "=", match[:tag]
|
74
|
+
assert_equal "$", match[:command]
|
75
|
+
assert_equal "mkdir schneems", match[:statement]
|
76
|
+
|
77
|
+
contents = ":::= $ mkdir schneems"
|
78
|
+
match = contents.match(regex)
|
79
|
+
assert_equal "=", match[:tag]
|
80
|
+
assert_equal "$", match[:command]
|
81
|
+
assert_equal "mkdir schneems", match[:statement]
|
82
|
+
|
83
|
+
contents = ":::-$ mkdir schneems"
|
84
|
+
match = contents.match(regex)
|
85
|
+
assert_equal "-", match[:tag]
|
86
|
+
assert_equal "$", match[:command]
|
87
|
+
assert_equal "mkdir schneems", match[:statement]
|
88
|
+
|
89
|
+
contents = ":::- $ mkdir schneems"
|
90
|
+
match = contents.match(regex)
|
91
|
+
assert_equal "-", match[:tag]
|
92
|
+
assert_equal "$", match[:command]
|
93
|
+
assert_equal "mkdir schneems", match[:statement]
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def test_codeblock_regex
|
98
|
+
|
99
|
+
contents = <<-RUBY
|
100
|
+
foo
|
101
|
+
|
102
|
+
```
|
103
|
+
:::$ mkdir
|
104
|
+
```
|
105
|
+
|
106
|
+
zoo
|
107
|
+
|
108
|
+
```
|
109
|
+
:::$ cd ..
|
110
|
+
something
|
111
|
+
```
|
112
|
+
|
113
|
+
bar
|
114
|
+
RUBY
|
115
|
+
|
116
|
+
regex = Docdown::Parser::CODEBLOCK_REGEX
|
117
|
+
|
118
|
+
actual = contents.partition(regex)
|
119
|
+
expected = ["foo\n\n",
|
120
|
+
"```\n:::$ mkdir\n```\n",
|
121
|
+
"\nzoo\n\n```\n:::$ cd ..\nsomething\n```\n\nbar\n"]
|
122
|
+
|
123
|
+
assert_equal expected, actual
|
124
|
+
|
125
|
+
str = "```\n:::$ mkdir\n```\n"
|
126
|
+
match = str.match(regex)
|
127
|
+
assert_equal ":::$ mkdir\n", match[:contents]
|
128
|
+
|
129
|
+
str = "\n\n```\n:::$ cd ..\nsomething\n```\n\nbar\n"
|
130
|
+
match = str.match(regex)
|
131
|
+
assert_equal ":::$ cd ..\nsomething\n", match[:contents]
|
132
|
+
|
133
|
+
# partition, shift, codebloc,
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_complex_regex
|
137
|
+
|
138
|
+
contents = <<-RUBY
|
139
|
+
```java
|
140
|
+
:::= write app/controllers/Application.java
|
141
|
+
package controllers;
|
142
|
+
|
143
|
+
import static java.util.concurrent.TimeUnit.SECONDS;
|
144
|
+
import models.Pinger;
|
145
|
+
import play.libs.Akka;
|
146
|
+
import play.libs.F.Callback0;
|
147
|
+
import play.mvc.Controller;
|
148
|
+
import play.mvc.Result;
|
149
|
+
import play.mvc.WebSocket;
|
150
|
+
import scala.concurrent.duration.Duration;
|
151
|
+
import views.html.index;
|
152
|
+
import akka.actor.ActorRef;
|
153
|
+
import akka.actor.Cancellable;
|
154
|
+
import akka.actor.Props;
|
155
|
+
|
156
|
+
public class Application extends Controller {
|
157
|
+
public static WebSocket<String> pingWs() {
|
158
|
+
return new WebSocket<String>() {
|
159
|
+
public void onReady(WebSocket.In<String> in, WebSocket.Out<String> out) {
|
160
|
+
final ActorRef pingActor = Akka.system().actorOf(Props.create(Pinger.class, in, out));
|
161
|
+
final Cancellable cancellable = Akka.system().scheduler().schedule(Duration.create(1, SECONDS),
|
162
|
+
Duration.create(1, SECONDS),
|
163
|
+
pingActor,
|
164
|
+
"Tick",
|
165
|
+
Akka.system().dispatcher(),
|
166
|
+
null
|
167
|
+
);
|
168
|
+
|
169
|
+
in.onClose(new Callback0() {
|
170
|
+
@Override
|
171
|
+
public void invoke() throws Throwable {
|
172
|
+
cancellable.cancel();
|
173
|
+
}
|
174
|
+
});
|
175
|
+
}
|
176
|
+
|
177
|
+
};
|
178
|
+
}
|
179
|
+
|
180
|
+
public static Result pingJs() {
|
181
|
+
return ok(views.js.ping.render());
|
182
|
+
}
|
183
|
+
|
184
|
+
public static Result index() {
|
185
|
+
return ok(index.render());
|
186
|
+
}
|
187
|
+
}
|
188
|
+
```
|
189
|
+
RUBY
|
190
|
+
|
191
|
+
regex = Docdown::Parser::CODEBLOCK_REGEX
|
192
|
+
match = contents.match(regex)
|
193
|
+
assert_equal 'java', match[:lang]
|
194
|
+
assert_equal '```', match[:fence]
|
195
|
+
assert_equal '`', match[:fence_char]
|
196
|
+
|
197
|
+
assert_equal contents.strip, match.to_s.strip
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|