docdown 0.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.
- 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
|