markdownplus 0.1.0 → 0.2.0
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 +4 -4
- data/README.md +137 -3
- data/lib/markdownplus.rb +5 -0
- data/lib/markdownplus/directive_parser.rb +17 -0
- data/lib/markdownplus/directives.treetop +33 -0
- data/lib/markdownplus/github_renderer.rb +19 -0
- data/lib/markdownplus/handler.rb +120 -0
- data/lib/markdownplus/handler_registry.rb +17 -0
- data/lib/markdownplus/literals.rb +101 -0
- data/lib/markdownplus/parser.rb +75 -125
- data/lib/markdownplus/version.rb +1 -1
- data/markdownplus.gemspec +1 -0
- data/spec/directive_parser_spec.rb +275 -0
- data/spec/fixtures/bad_json.json +1 -0
- data/spec/fixtures/directives.html +110 -0
- data/spec/fixtures/directives.mdp +28 -0
- data/spec/fixtures/fake.csv +21 -0
- data/spec/fixtures/fake.json +1 -0
- data/spec/fixtures/simple.mdp +2 -6
- data/spec/fixtures/variables.html +29 -0
- data/spec/fixtures/variables.mdp +14 -0
- data/spec/parser_spec.rb +124 -47
- metadata +38 -4
- data/spec/fixtures/include.mdp +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b122d954d5f9a9b2fa74b04626054c7d4bb3fdd4
|
4
|
+
data.tar.gz: 812679f9e80a65de77281ec0d5f53b95038022cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6506836776c5df3516be8187fe47441a0f0170133596b498d72ccf8046f0106ee49d1722631a52c84ec0db6a7ac98b8b20b75b1d04116c0ce3ba00d292bf893f
|
7
|
+
data.tar.gz: 6977a66ef13badcee4bf28594346313c66b0d01c8f516daabed6b718d7a8382fcb587546d1366b6bf2fd3330f55bdedf1d22da0f73d1f3307dfbc01efb538d36
|
data/README.md
CHANGED
@@ -1,6 +1,141 @@
|
|
1
1
|
# Markdownplus
|
2
2
|
|
3
|
-
|
3
|
+
Markdownplus extends [Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/) by bringing programmatic features to the language portion of the fenced code blocks. A Markdownplus document can be transformed into a valid Markdown document, or taken all the way to html.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
### Syntax
|
8
|
+
|
9
|
+
Markdownplus files look like normal [Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/) files. However, the fenced code blocks have most functionality. You can still use
|
10
|
+
|
11
|
+
```markdown
|
12
|
+
```ruby
|
13
|
+
```
|
14
|
+
|
15
|
+
or
|
16
|
+
|
17
|
+
```markdown
|
18
|
+
```json
|
19
|
+
```
|
20
|
+
|
21
|
+
to add syntax hightlight, but you can also use a pipeline of functions to bring in other files, or perform more drastic formatting.
|
22
|
+
|
23
|
+
For instance, you could download a json file, format it, then highlight it as usual using:
|
24
|
+
|
25
|
+
```
|
26
|
+
```include('https://gist.githubusercontent.com/cpetersen/c6571117df132443ac81/raw/e5ac97e8e0665a0e4014ebc85ecef214763a7729/fake.json'),pretty_json()
|
27
|
+
```
|
28
|
+
|
29
|
+
or, you could download a csv file and turn it into an html table:
|
30
|
+
|
31
|
+
```
|
32
|
+
```include('https://gist.githubusercontent.com/cpetersen/b5a473ddf0b796cd9502/raw/e140bdc32ff2f6a600e357c2575220c0312a88ee/fake.csv'),csv2html()
|
33
|
+
```
|
34
|
+
|
35
|
+
### Execution
|
36
|
+
|
37
|
+
Given a markdown plus file, you can get the resulting Markdown using the following:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
require 'markdownplus'
|
41
|
+
|
42
|
+
parser = Markdownplus::Parser.parse(File.read("kitchensink.mdp")); nil
|
43
|
+
parser.execute; nil
|
44
|
+
puts parser.output_markdown; nil
|
45
|
+
```
|
46
|
+
|
47
|
+
If the resulting HTML is what you're interested in, you can use ```html```:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
require 'markdownplus'
|
51
|
+
|
52
|
+
parser = Markdownplus::Parser.parse(File.read("kitchensink.mdp")); nil
|
53
|
+
parser.execute; nil
|
54
|
+
puts parser.html; nil
|
55
|
+
```
|
56
|
+
|
57
|
+
## Function Pipeline
|
58
|
+
|
59
|
+
The function pipeline is the heart of Markdownplus.
|
60
|
+
|
61
|
+
### Functions
|
62
|
+
|
63
|
+
A function looks like:
|
64
|
+
|
65
|
+
```
|
66
|
+
function_name()
|
67
|
+
function_name(symbol_parameter)
|
68
|
+
function_name("string parameter", 'other string parameter')
|
69
|
+
function_name(mix, 'and', match, "parameters")
|
70
|
+
function_name(you, "may pass", nested_methods("also"))
|
71
|
+
```
|
72
|
+
|
73
|
+
The first function in the pipeline gets the contents of the fenced code block as input. For instance:
|
74
|
+
|
75
|
+
```
|
76
|
+
```pretty_json()
|
77
|
+
{"a":1,"b":2,"c":3}
|
78
|
+
```
|
79
|
+
|
80
|
+
would get `{"a":1,"b":2,"c":3}` as the input variable and produce the following:
|
81
|
+
|
82
|
+
```json
|
83
|
+
{
|
84
|
+
"a": 1,
|
85
|
+
"b": 2,
|
86
|
+
"c": 3
|
87
|
+
}
|
88
|
+
```
|
89
|
+
|
90
|
+
### Pipeline
|
91
|
+
|
92
|
+
You create pipeline, you string multiple functions together with a comma:
|
93
|
+
|
94
|
+
```
|
95
|
+
```include("some url"), csv2html()
|
96
|
+
```
|
97
|
+
|
98
|
+
In this case, the first function (include) gets the contents of the fenced code block as input. The second function (csv2html) gets the output of the first function as input.
|
99
|
+
|
100
|
+
The output of the last function in the pipeline is used as the content when generating the `output_markdown` and ultimately the resulting `html`
|
101
|
+
|
102
|
+
## Built in functions:
|
103
|
+
|
104
|
+
### include()
|
105
|
+
|
106
|
+
`include` takes a single parameter, a url. It downloads this url and outputs the result.
|
107
|
+
|
108
|
+
### csv2html()
|
109
|
+
|
110
|
+
`csv2html` takes no parameters, but expects valid CSV as input. It creates an HTML table from the given CSV.
|
111
|
+
|
112
|
+
### pretty_json()
|
113
|
+
|
114
|
+
`pretty_json` takes no parameters, but expects valid JSON as input. It formats the JSON nicely using Ruby's `JSON.pretty_generate` and outputs a fenced code block with the language specified as `json`.
|
115
|
+
|
116
|
+
### set('variable_name')
|
117
|
+
|
118
|
+
Stores the input in a `variable_name` for use later in the page.
|
119
|
+
|
120
|
+
### get('variable_name')
|
121
|
+
|
122
|
+
Outputs the content stored at a the `variable_name`.
|
123
|
+
|
124
|
+
### empty()
|
125
|
+
|
126
|
+
Hides the output. Often used with `set()`
|
127
|
+
|
128
|
+
### raw()
|
129
|
+
|
130
|
+
Output the content as raw html and skip any markdown formatting that may apply.
|
131
|
+
|
132
|
+
### strip_whitespace()
|
133
|
+
|
134
|
+
Strips any leading or trailing whitespace from the `input` and outputs the result.
|
135
|
+
|
136
|
+
## Extendable
|
137
|
+
|
138
|
+
|
4
139
|
|
5
140
|
## Installation
|
6
141
|
|
@@ -23,7 +158,6 @@ Or install it yourself as:
|
|
23
158
|
Use a fenced code block:
|
24
159
|
|
25
160
|
```markdown
|
26
|
-
```include
|
27
|
-
https://gist.githubusercontent.com/cpetersen/b5a473ddf0b796cd9502/raw/e140bdc32ff2f6a600e357c2575220c0312a88ee/fake.csv
|
161
|
+
```include('https://gist.githubusercontent.com/cpetersen/b5a473ddf0b796cd9502/raw/e140bdc32ff2f6a600e357c2575220c0312a88ee/fake.csv'),csv()
|
28
162
|
```
|
29
163
|
```
|
data/lib/markdownplus.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
require "markdownplus/version"
|
2
|
+
require "markdownplus/github_renderer"
|
2
3
|
require "markdownplus/parser"
|
4
|
+
require "markdownplus/literals"
|
5
|
+
require "markdownplus/directive_parser"
|
6
|
+
require "markdownplus/handler_registry"
|
7
|
+
require "markdownplus/handler"
|
3
8
|
|
4
9
|
module Markdownplus
|
5
10
|
# Your code goes here...
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
|
3
|
+
module Markdownplus
|
4
|
+
class DirectiveParser
|
5
|
+
def self.parse(data)
|
6
|
+
Treetop.load(File.expand_path("../directives", __FILE__))
|
7
|
+
@@parser ||= TransformationParser.new
|
8
|
+
tree = @@parser.parse(data)
|
9
|
+
# If the AST is nil then there was an error during parsing
|
10
|
+
# we need to report a simple error message to help the user
|
11
|
+
raise "Parse error at offset: #{@@parser.index}" if(tree.nil?)
|
12
|
+
|
13
|
+
return tree
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
grammar Transformation
|
2
|
+
rule transformations
|
3
|
+
( function / "" ) (',' function)* <Markdownplus::Literals::TransformationLiteral>
|
4
|
+
end
|
5
|
+
|
6
|
+
rule expression
|
7
|
+
( function / single_quote_string / double_quote_string / symbol / "" ) (',' expression)* <Markdownplus::Literals::ExpressionLiteral>
|
8
|
+
end
|
9
|
+
|
10
|
+
rule function
|
11
|
+
symbol parameters <Markdownplus::Literals::FunctionLiteral>
|
12
|
+
end
|
13
|
+
|
14
|
+
rule parameters
|
15
|
+
"(" expression ")" <Markdownplus::Literals::ParensLiteral>
|
16
|
+
end
|
17
|
+
|
18
|
+
rule single_quote_string
|
19
|
+
space? "'" [a-zA-Z0-9\-_@ \t:\/\.]+ "'" space? <Markdownplus::Literals::StringLiteral>
|
20
|
+
end
|
21
|
+
|
22
|
+
rule double_quote_string
|
23
|
+
space? '"' [a-zA-Z0-9\-_@ \t:\/\.]+ '"' space? <Markdownplus::Literals::StringLiteral>
|
24
|
+
end
|
25
|
+
|
26
|
+
rule symbol
|
27
|
+
space? [a-zA-Z0-9\-_]+ space? <Markdownplus::Literals::SymbolLiteral>
|
28
|
+
end
|
29
|
+
|
30
|
+
rule space
|
31
|
+
[\s]+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'pygments'
|
2
|
+
require 'redcarpet'
|
3
|
+
|
4
|
+
module Markdownplus
|
5
|
+
class GithubRenderer < Redcarpet::Render::HTML
|
6
|
+
# alias_method :existing_block_code, :block_code
|
7
|
+
def block_code(code, language)
|
8
|
+
if language == "raw"
|
9
|
+
code
|
10
|
+
else
|
11
|
+
begin
|
12
|
+
Pygments.highlight(code, lexer: language)
|
13
|
+
rescue
|
14
|
+
"<pre><code>#{code}</code></pre>"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Markdownplus
|
2
|
+
class Handler
|
3
|
+
def execute(input, parameters)
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
class IncludeHandler < Handler
|
8
|
+
def execute(input, parameters, variables, warnings, errors)
|
9
|
+
output = nil
|
10
|
+
warnings << "Include handler ignores input" if(input!=nil && !input.strip.empty?)
|
11
|
+
if parameters==nil
|
12
|
+
errors << "No url given"
|
13
|
+
elsif parameters.count == 0
|
14
|
+
errors << "No url given"
|
15
|
+
else
|
16
|
+
begin
|
17
|
+
output = IncludeHandler.cached(parameters.first.to_s)
|
18
|
+
rescue => e
|
19
|
+
errors << "Error opening [#{parameters.first}] [#{e.message}]"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
output
|
23
|
+
end
|
24
|
+
|
25
|
+
@@cache = {}
|
26
|
+
def self.cached(url)
|
27
|
+
return @@cache[url] if @@cache[url]
|
28
|
+
@@cache[url] = open(url).read
|
29
|
+
@@cache[url]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
HandlerRegistry.register("include", IncludeHandler)
|
33
|
+
|
34
|
+
class Csv2HtmlHandler < Handler
|
35
|
+
def execute(input, parameters, variables, warnings, errors)
|
36
|
+
output = "<table class='table table-striped'>"
|
37
|
+
row_num = 0
|
38
|
+
CSV.parse(input) do |row|
|
39
|
+
if row_num == 0
|
40
|
+
output += "<thead><tr>#{row.collect { |c| "<th>#{c}</th>"}.join}</tr></thead>\n<tbody>\n"
|
41
|
+
else
|
42
|
+
output += "<tr>#{row.collect { |c| "<td>#{c}</td>"}.join}</tr>\n"
|
43
|
+
end
|
44
|
+
row_num += 1
|
45
|
+
end
|
46
|
+
output += "</tbody></table>"
|
47
|
+
output
|
48
|
+
end
|
49
|
+
end
|
50
|
+
HandlerRegistry.register("csv2html", Csv2HtmlHandler)
|
51
|
+
|
52
|
+
class PrettyJsonHandler < Handler
|
53
|
+
def execute(input, parameters, variables, warnings, errors)
|
54
|
+
begin
|
55
|
+
obj = JSON.parse(input)
|
56
|
+
output = JSON.pretty_generate(obj)
|
57
|
+
rescue => e
|
58
|
+
output = input
|
59
|
+
errors << "Invalid json"
|
60
|
+
end
|
61
|
+
"```json\n#{output}\n```"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
HandlerRegistry.register("pretty_json", PrettyJsonHandler)
|
65
|
+
|
66
|
+
class StripWhitespaceHandler < Handler
|
67
|
+
def execute(input, parameters, variables, warnings, errors)
|
68
|
+
input.gsub(/\s*\n\s*/,"\n")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
HandlerRegistry.register("strip_whitespace", StripWhitespaceHandler)
|
72
|
+
|
73
|
+
class RawHandler < Handler
|
74
|
+
def execute(input, parameters, variables, warnings, errors)
|
75
|
+
"```raw\n#{input}\n```"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
HandlerRegistry.register("raw", RawHandler)
|
79
|
+
|
80
|
+
class EmptyHandler < Handler
|
81
|
+
def execute(input, parameters, variables, warnings, errors)
|
82
|
+
""
|
83
|
+
end
|
84
|
+
end
|
85
|
+
HandlerRegistry.register("empty", EmptyHandler)
|
86
|
+
|
87
|
+
### START VARIABLES ###
|
88
|
+
class SetHandler < Handler
|
89
|
+
def execute(input, parameters, variables, warnings, errors)
|
90
|
+
if parameters==nil
|
91
|
+
errors << "No variable name given"
|
92
|
+
elsif parameters.count == 0
|
93
|
+
errors << "No variable name given"
|
94
|
+
else
|
95
|
+
warnings << "More than one variable name given [#{parameters.inspect}]" if parameters.count > 1
|
96
|
+
variables[parameters.first.to_s] = input
|
97
|
+
end
|
98
|
+
input
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class GetHandler < Handler
|
103
|
+
def execute(input, parameters, variables, warnings, errors)
|
104
|
+
output = input
|
105
|
+
if parameters==nil
|
106
|
+
errors << "No variable name given"
|
107
|
+
elsif parameters.count == 0
|
108
|
+
errors << "No variable name given"
|
109
|
+
else
|
110
|
+
warnings << "More than one variable name given [#{parameters.inspect}]" if parameters.count > 1
|
111
|
+
output = variables[parameters.first.to_s]
|
112
|
+
end
|
113
|
+
output
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
HandlerRegistry.register("set", SetHandler)
|
118
|
+
HandlerRegistry.register("get", GetHandler)
|
119
|
+
#### END VARIABLES ####
|
120
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Markdownplus
|
2
|
+
class HandlerRegistry
|
3
|
+
@@registry = {}
|
4
|
+
|
5
|
+
def self.handler_instance(name)
|
6
|
+
handler_class(name).new if handler_class(name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.handler_class(name)
|
10
|
+
@@registry[name]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.register(name, handler)
|
14
|
+
@@registry[name] = handler
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
|
3
|
+
module Markdownplus
|
4
|
+
module Literals
|
5
|
+
class ExpressionLiteral < Treetop::Runtime::SyntaxNode
|
6
|
+
def functions
|
7
|
+
_functions(self.elements).flatten.compact
|
8
|
+
end
|
9
|
+
|
10
|
+
def _functions(elements)
|
11
|
+
return unless elements
|
12
|
+
results = elements.select { |e| e.class==Markdownplus::Literals::FunctionLiteral }
|
13
|
+
elements.each do |element|
|
14
|
+
if [Treetop::Runtime::SyntaxNode, Markdownplus::Literals::ExpressionLiteral, Markdownplus::Literals::TransformationLiteral].include?(element.class)
|
15
|
+
results << _functions(element.elements)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
results
|
19
|
+
end
|
20
|
+
def symbols
|
21
|
+
self.elements.select { |e| e.class==SymbolLiteral }
|
22
|
+
end
|
23
|
+
def parens
|
24
|
+
self.elements.select { |e| e.class==ParensLiteral }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class TransformationLiteral < ExpressionLiteral
|
29
|
+
#Specific subclass, the root should only match this
|
30
|
+
end
|
31
|
+
|
32
|
+
class FunctionLiteral < ExpressionLiteral
|
33
|
+
def function_name
|
34
|
+
self.symbols[0].text_value.strip
|
35
|
+
end
|
36
|
+
|
37
|
+
def function_parameters
|
38
|
+
self.parens.first.function_parameters
|
39
|
+
end
|
40
|
+
|
41
|
+
def function_parameter_values(input, variables, warnings, errors)
|
42
|
+
self.parens.first.function_parameters.collect { |fp| fp.value(input, variables, warnings, errors) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute(input, variables, warnings, errors)
|
46
|
+
handler = HandlerRegistry.handler_instance(self.function_name)
|
47
|
+
if handler
|
48
|
+
output = handler.execute(input, self.function_parameter_values(nil, variables, warnings, errors), variables, warnings, errors)
|
49
|
+
else
|
50
|
+
errors << "No handler defined for [#{self.function_name}]"
|
51
|
+
end
|
52
|
+
output
|
53
|
+
end
|
54
|
+
|
55
|
+
def value(input, variables, warnings, errors)
|
56
|
+
execute(input, variables, warnings, errors)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
class ParensLiteral < ExpressionLiteral
|
62
|
+
def function_parameters
|
63
|
+
self.find_parameters(self.elements)
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_parameters(elements, params=[])
|
67
|
+
return params unless elements
|
68
|
+
elements.each do |element|
|
69
|
+
if [StringLiteral, SymbolLiteral, FunctionLiteral].include?(element.class)
|
70
|
+
params << element
|
71
|
+
else
|
72
|
+
find_parameters(element.elements, params)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
return params
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class StringLiteral < ExpressionLiteral
|
80
|
+
def to_s
|
81
|
+
v = self.text_value.strip
|
82
|
+
v[1..v.length-2]
|
83
|
+
end
|
84
|
+
|
85
|
+
def value(input, variables, warnings, errors)
|
86
|
+
to_s
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class SymbolLiteral < ExpressionLiteral
|
91
|
+
def to_s
|
92
|
+
self.text_value.strip
|
93
|
+
end
|
94
|
+
|
95
|
+
def value(input, variables, warnings, errors)
|
96
|
+
to_s
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|