pasta 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 +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +57 -0
- data/Rakefile +9 -0
- data/bin/pasta +40 -0
- data/lib/pasta.rb +18 -0
- data/lib/pasta/cookbook.rb +13 -0
- data/lib/pasta/dish.rb +62 -0
- data/lib/pasta/error.rb +3 -0
- data/lib/pasta/ingredients.rb +49 -0
- data/lib/pasta/ingredients/anything.rb +12 -0
- data/lib/pasta/ingredients/atx_headings.rb +21 -0
- data/lib/pasta/ingredients/automatic_links.rb +14 -0
- data/lib/pasta/ingredients/blockquotes.rb +23 -0
- data/lib/pasta/ingredients/code.rb +15 -0
- data/lib/pasta/ingredients/codeblocks.rb +19 -0
- data/lib/pasta/ingredients/emphasis.rb +14 -0
- data/lib/pasta/ingredients/empty_lines.rb +13 -0
- data/lib/pasta/ingredients/html_entities.rb +17 -0
- data/lib/pasta/ingredients/links.rb +58 -0
- data/lib/pasta/ingredients/lists.rb +31 -0
- data/lib/pasta/ingredients/page_breaks.rb +14 -0
- data/lib/pasta/ingredients/paragraphs.rb +27 -0
- data/lib/pasta/ingredients/setext_headings.rb +20 -0
- data/lib/pasta/recipes/base.rb +40 -0
- data/lib/pasta/recipes/gruber.rb +27 -0
- data/lib/pasta/recipes/markdown.rb +11 -0
- data/lib/pasta/version.rb +3 -0
- data/pasta.gemspec +24 -0
- data/specs/base_spec.rb +8 -0
- data/specs/dish_spec.rb +30 -0
- data/specs/dsl_spec.rb +12 -0
- data/specs/recipes/gruber/automatic_links.yml +12 -0
- data/specs/recipes/gruber/blockquotes.yml +112 -0
- data/specs/recipes/gruber/code.yml +48 -0
- data/specs/recipes/gruber/codeblocks.yml +47 -0
- data/specs/recipes/gruber/emphasis.yml +57 -0
- data/specs/recipes/gruber/gruber_spec.rb +22 -0
- data/specs/recipes/gruber/headers.yml +88 -0
- data/specs/recipes/gruber/images.yml +49 -0
- data/specs/recipes/gruber/links.yml +184 -0
- data/specs/recipes/gruber/lists.yml +171 -0
- data/specs/recipes/gruber/page_breaks.yml +31 -0
- data/specs/recipes/gruber/paragraphs.yml +44 -0
- data/specs/spec_helper.rb +3 -0
- metadata +149 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
HTML_ENTITIES = {
|
5
|
+
'&' => '&'
|
6
|
+
}
|
7
|
+
|
8
|
+
def html_entities(text, html)
|
9
|
+
HTML_ENTITIES.each do |entity, transformation|
|
10
|
+
html = text.gsub(/[#{entity}]/) do |match|
|
11
|
+
match = transformation
|
12
|
+
end
|
13
|
+
end
|
14
|
+
[text, html]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
# Parses both links and images
|
5
|
+
# $1 => "!", $2 => link text, $4 => inline url, $5 => reference id
|
6
|
+
LINKS = /([!]*)\[(.+?)\](\((.+?)\)|\s?\[(.*?)\])/
|
7
|
+
|
8
|
+
LINK_DEFINITION = /^ {0,3}\[([\w\s]+?)\]: (.+?)(^\s*[\n\r]|\z)/m
|
9
|
+
|
10
|
+
def links(text, html)
|
11
|
+
find_link_definitions(text)
|
12
|
+
|
13
|
+
text.gsub!(LINKS) do |match|
|
14
|
+
link_text, target = $2, ''
|
15
|
+
if $4
|
16
|
+
# It's an inline link
|
17
|
+
target = $4
|
18
|
+
else
|
19
|
+
# It's a reference link
|
20
|
+
id = $5 == '' ? $2.downcase : $5.downcase
|
21
|
+
break unless @link_definitions && @link_definitions.key?(id)
|
22
|
+
target = @link_definitions[id][:target]
|
23
|
+
end
|
24
|
+
# format image or link
|
25
|
+
url, title = parse_link target
|
26
|
+
if $1 == '!'
|
27
|
+
match = "<img src=\"#{url}\" alt=\"#{link_text}\"#{title} />"
|
28
|
+
else
|
29
|
+
match = "<a href=\"#{url}\"#{title}>#{link_text}</a>"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
text = clean_link_definitions(text)
|
34
|
+
[text, html]
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_link_definitions(text)
|
38
|
+
@link_definitions = {}
|
39
|
+
text.gsub(LINK_DEFINITION) do |match|
|
40
|
+
@link_definitions[$1.downcase] = {target: $2, used: false}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def clean_link_definitions(text)
|
45
|
+
@link_definitions.each do |key,val|
|
46
|
+
text.gsub!(/^ {0,3}\[#{key}\]: (.+?)(^\s*[\n\r]|\z)/mi, '')
|
47
|
+
end
|
48
|
+
text
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_link(text)
|
52
|
+
target = text.split(' ')
|
53
|
+
url = target[0].gsub(/[<>]/, '')
|
54
|
+
title = " title=#{target[1..-1].join(' ').strip}" if target.length > 1
|
55
|
+
[url, title]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
LIST = /\A\s{0,3}(([-*+]|([0-9]+)\.) .+?)((^\s*[\n\r])|\z)/m
|
5
|
+
|
6
|
+
def lists(text, html)
|
7
|
+
text.sub!(LIST) do |match|
|
8
|
+
inner_text, inner_html, list_type = $1, '', 'ul'
|
9
|
+
|
10
|
+
# determine the list_type
|
11
|
+
list_type = 'ol' unless %w{- * +}.include? $1.strip[0]
|
12
|
+
|
13
|
+
# extract list items
|
14
|
+
inner_html = list_items(inner_text)
|
15
|
+
|
16
|
+
html << "<#{list_type}>\n#{inner_html}</#{list_type}>\n\n"
|
17
|
+
match = ''
|
18
|
+
end
|
19
|
+
[text, html]
|
20
|
+
end
|
21
|
+
|
22
|
+
def list_items(text)
|
23
|
+
html = ''
|
24
|
+
text.gsub(/^([-*+]|\d+\.) (.+?)$(?=([\n\r]([*+-]|(\d+\.))|\z))/m) do |match|
|
25
|
+
html << "<li>#{$2.gsub(/\n\s+/, ' ').strip}</li>\n"
|
26
|
+
match = ''
|
27
|
+
end
|
28
|
+
html
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
PARAGRAPH = /\A(?![>])(.+?)((^\s*[\n\r])|\z)/m
|
5
|
+
|
6
|
+
def paragraphs(text, html)
|
7
|
+
text.sub!(PARAGRAPH) do |match|
|
8
|
+
|
9
|
+
inner_text, inner_html = $1, ''
|
10
|
+
inner_text.gsub!(/[ ]{2,}[\n\r]/, "<br />\n")
|
11
|
+
inner_text.strip!
|
12
|
+
|
13
|
+
if grammars.key? :paragraphs
|
14
|
+
grammars[:paragraphs].each do |rule|
|
15
|
+
inner_text, inner_html = send(rule, inner_text, '')
|
16
|
+
end
|
17
|
+
inner_text = inner_html unless inner_html == ''
|
18
|
+
end
|
19
|
+
|
20
|
+
html << "<p>#{inner_text}</p>\n\n" unless inner_text == ''
|
21
|
+
match = ''
|
22
|
+
end
|
23
|
+
[text, html]
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
SETEXT_HEADING = /\A(.+?)[\n\r]([=|-]+)([\n\r]|\z)/
|
5
|
+
|
6
|
+
def setext_headings(text, html)
|
7
|
+
text.sub!(SETEXT_HEADING) do |match|
|
8
|
+
if grammars.key? :heading
|
9
|
+
grammars[:heading].each do |rule|
|
10
|
+
inner = send(rule, $1, '')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
level = ($2[0] == '=') ? 1 : 2
|
14
|
+
html << "<h#{level}>#{$1}</h#{level}>\n" unless $1.nil?
|
15
|
+
match = ''
|
16
|
+
end
|
17
|
+
[text, html]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'pasta/ingredients'
|
2
|
+
|
3
|
+
module Pasta
|
4
|
+
module Recipes
|
5
|
+
class Base
|
6
|
+
|
7
|
+
include Pasta::Ingredients
|
8
|
+
|
9
|
+
# Public. Generic to_x parser that converts a string to the specified output format.
|
10
|
+
# Uses a depth first strategy to parse a text according to incredient rules as defined in a grammar
|
11
|
+
# Raises a Pasta::Error if a grammar has not been defined
|
12
|
+
#
|
13
|
+
# text - a String to be parsed
|
14
|
+
#
|
15
|
+
# Example
|
16
|
+
# to_html('some yummy text')
|
17
|
+
# # => '<p>some yummy text</p>'
|
18
|
+
#
|
19
|
+
# Returns a String
|
20
|
+
def method_missing(method, *args, &block)
|
21
|
+
if method.to_s =~ /^to_(.+)$/
|
22
|
+
# ensure a grammar has been defined
|
23
|
+
raise Pasta::Error.new "A grammar for #{$1} has not been defined in recipe #{self.class.name}" if grammars($1.to_sym).nil?
|
24
|
+
|
25
|
+
# Depth first parsing - each ingredient should accept and return an input and output
|
26
|
+
input, output = args[0], ''
|
27
|
+
while input != "" do
|
28
|
+
grammars($1.to_sym).each do |rule|
|
29
|
+
input, output = send(rule, input, output)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
output.strip # remove any trailing whitespace
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Recipes
|
3
|
+
class Gruber < Base
|
4
|
+
|
5
|
+
# Define the grammars
|
6
|
+
grammar(:html) {
|
7
|
+
[:automatic_links, :links, :empty_lines, :page_breaks, :codeblocks, :blockquotes, :lists, :atx_headings, :setext_headings, :paragraphs]
|
8
|
+
}
|
9
|
+
grammar(:blockquotes) {
|
10
|
+
[:empty_lines, :page_breaks, :codeblocks, :blockquotes, :lists, :atx_headings, :setext_headings, :paragraphs]
|
11
|
+
}
|
12
|
+
grammar(:lists) {
|
13
|
+
[:empty_lines, :codeblocks, :blockquotes, :lists, :atx_headings, :setext_headings, :paragraphs]
|
14
|
+
}
|
15
|
+
grammar(:atx_headings) {
|
16
|
+
[:emphasis]
|
17
|
+
}
|
18
|
+
grammar(:setext_headings) {
|
19
|
+
[:emphasis]
|
20
|
+
}
|
21
|
+
grammar(:paragraphs) {
|
22
|
+
[:code, :emphasis]
|
23
|
+
}
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/pasta.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pasta/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pasta"
|
8
|
+
spec.version = Pasta::VERSION
|
9
|
+
spec.authors = ["Dave Kinkead"]
|
10
|
+
spec.email = ["dave@kinkead.com.au"]
|
11
|
+
spec.description = %q{À la carte markdown parsing}
|
12
|
+
spec.summary = %q{Pasta is an extensible markdown text parser with a delicious menu of options}
|
13
|
+
spec.homepage = "https://github.com/davekinkead"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|specs|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "markdown-testsuite"
|
24
|
+
end
|
data/specs/base_spec.rb
ADDED
data/specs/dish_spec.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pasta::Dish do
|
4
|
+
|
5
|
+
specify { ->{ Pasta::Dish.new }.must_raise ArgumentError }
|
6
|
+
|
7
|
+
let(:spaghetti) { Pasta::Dish.new 'Long, thin, cylindrical strings of durum' }
|
8
|
+
|
9
|
+
specify { spaghetti.must_be_kind_of Pasta::Dish }
|
10
|
+
specify { spaghetti.must_respond_to :with }
|
11
|
+
specify { spaghetti.must_respond_to :make }
|
12
|
+
|
13
|
+
let(:fettuccine) { Pasta::Dish.new 'Flat, thick, strings of egg and durum'}
|
14
|
+
|
15
|
+
describe "#with" do
|
16
|
+
it "raises an error if a recipe can't be found" do
|
17
|
+
->{ fettuccine.with(:unknown) }.must_raise Pasta::Error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#make" do
|
22
|
+
it "raises an error if a recipe can't be found" do
|
23
|
+
->{ fettuccine.make(:unknown) }.must_raise Pasta::Error
|
24
|
+
end
|
25
|
+
|
26
|
+
it "converts the sauce text into a string" do
|
27
|
+
fettuccine.make(:html).must_be_kind_of String
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/specs/dsl_spec.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pasta do
|
4
|
+
specify { Pasta.must_respond_to :prepare }
|
5
|
+
specify { ->{ Pasta.prepare }.must_raise ArgumentError }
|
6
|
+
specify { Pasta.prepare('some yummy text').must_be_kind_of Pasta::Dish }
|
7
|
+
specify { Pasta.prepare('a handful of text').with(:gruber).make(:html).must_equal '<p>a handful of text</p>' }
|
8
|
+
|
9
|
+
it "uses :markdown as the default recipe" do
|
10
|
+
Pasta.prepare('thick, soft balls of flour and egg').make(:html).must_equal '<p>thick, soft balls of flour and egg</p>'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
overview: |
|
2
|
+
Markdown supports a shortcut style for creating “automatic” links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link.
|
3
|
+
|
4
|
+
http://daringfireball.net/projects/markdown/syntax
|
5
|
+
tests:
|
6
|
+
- name: Automatic Links
|
7
|
+
desc: URL inside html tags are automatically parsed
|
8
|
+
text: |
|
9
|
+
<http://example.com/>
|
10
|
+
html: |
|
11
|
+
<p><a href="http://example.com/">http://example.com/</a></p>
|
12
|
+
|
@@ -0,0 +1,112 @@
|
|
1
|
+
overview: |
|
2
|
+
Markdown uses email-style > characters for blockquoting. If you’re familiar with quoting passages of text in an email message, then you know how to create a blockquote in Markdown. It looks best if you hard wrap the text and put a > before every line:
|
3
|
+
|
4
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
5
|
+
> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
6
|
+
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
7
|
+
>
|
8
|
+
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
9
|
+
> id sem consectetuer libero luctus adipiscing.
|
10
|
+
|
11
|
+
Markdown allows you to be lazy and only put the > before the first line of a hard-wrapped paragraph:
|
12
|
+
|
13
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
14
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
15
|
+
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
16
|
+
|
17
|
+
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
18
|
+
id sem consectetuer libero luctus adipiscing.
|
19
|
+
|
20
|
+
Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by adding additional levels of >:
|
21
|
+
|
22
|
+
> This is the first level of quoting.
|
23
|
+
>
|
24
|
+
> > This is nested blockquote.
|
25
|
+
>
|
26
|
+
> Back to the first level.
|
27
|
+
|
28
|
+
Blockquotes can contain other Markdown elements, including headers, lists, and code blocks:
|
29
|
+
|
30
|
+
> ## This is a header.
|
31
|
+
>
|
32
|
+
> 1. This is the first list item.
|
33
|
+
> 2. This is the second list item.
|
34
|
+
>
|
35
|
+
> Here's some example code:
|
36
|
+
>
|
37
|
+
> return shell_exec("echo $input | $markdown_script");
|
38
|
+
|
39
|
+
Any decent text editor should make email-style quoting easy. For example, with BBEdit, you can make a selection and choose Increase Quote Level from the Text menu.
|
40
|
+
|
41
|
+
http://daringfireball.net/projects/markdown/syntax
|
42
|
+
tests:
|
43
|
+
- name: Single line block quote
|
44
|
+
desc: Quoting a single line of text
|
45
|
+
text: |
|
46
|
+
> This is a snazzy quote
|
47
|
+
html: |
|
48
|
+
<blockquote>
|
49
|
+
<p>This is a snazzy quote</p>
|
50
|
+
</blockquote>
|
51
|
+
|
52
|
+
- name: Double line block quote
|
53
|
+
desc: Quoting a two paragraphs of text
|
54
|
+
text: |
|
55
|
+
> This is a snazzy quote
|
56
|
+
>
|
57
|
+
> It's longer than you think!
|
58
|
+
html: |
|
59
|
+
<blockquote>
|
60
|
+
<p>This is a snazzy quote</p>
|
61
|
+
|
62
|
+
<p>It's longer than you think!</p>
|
63
|
+
</blockquote>
|
64
|
+
|
65
|
+
- name: Lazy double quotes
|
66
|
+
desc: Quoting two lines of text with only lead >
|
67
|
+
text: |
|
68
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
69
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
70
|
+
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
71
|
+
html: |
|
72
|
+
<blockquote>
|
73
|
+
<p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
74
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
75
|
+
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
|
76
|
+
</blockquote>
|
77
|
+
|
78
|
+
- name: Multiple blockquotes
|
79
|
+
desc: Using multiple blockquotes in succession
|
80
|
+
text: |
|
81
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
82
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
83
|
+
|
84
|
+
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
85
|
+
html: |
|
86
|
+
<blockquote>
|
87
|
+
<p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
88
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.</p>
|
89
|
+
</blockquote>
|
90
|
+
|
91
|
+
<blockquote>
|
92
|
+
<p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
|
93
|
+
</blockquote>
|
94
|
+
|
95
|
+
- name: Nested blockquotes
|
96
|
+
desc: Blockquotes can contain blockquotes
|
97
|
+
text: |
|
98
|
+
> This is the first level of quoting.
|
99
|
+
>
|
100
|
+
> > This is nested blockquote.
|
101
|
+
>
|
102
|
+
> Back to the first level.
|
103
|
+
html: |
|
104
|
+
<blockquote>
|
105
|
+
<p>This is the first level of quoting.</p>
|
106
|
+
|
107
|
+
<blockquote>
|
108
|
+
<p>This is nested blockquote.</p>
|
109
|
+
</blockquote>
|
110
|
+
|
111
|
+
<p>Back to the first level.</p>
|
112
|
+
</blockquote>
|