pasta 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +57 -0
  6. data/Rakefile +9 -0
  7. data/bin/pasta +40 -0
  8. data/lib/pasta.rb +18 -0
  9. data/lib/pasta/cookbook.rb +13 -0
  10. data/lib/pasta/dish.rb +62 -0
  11. data/lib/pasta/error.rb +3 -0
  12. data/lib/pasta/ingredients.rb +49 -0
  13. data/lib/pasta/ingredients/anything.rb +12 -0
  14. data/lib/pasta/ingredients/atx_headings.rb +21 -0
  15. data/lib/pasta/ingredients/automatic_links.rb +14 -0
  16. data/lib/pasta/ingredients/blockquotes.rb +23 -0
  17. data/lib/pasta/ingredients/code.rb +15 -0
  18. data/lib/pasta/ingredients/codeblocks.rb +19 -0
  19. data/lib/pasta/ingredients/emphasis.rb +14 -0
  20. data/lib/pasta/ingredients/empty_lines.rb +13 -0
  21. data/lib/pasta/ingredients/html_entities.rb +17 -0
  22. data/lib/pasta/ingredients/links.rb +58 -0
  23. data/lib/pasta/ingredients/lists.rb +31 -0
  24. data/lib/pasta/ingredients/page_breaks.rb +14 -0
  25. data/lib/pasta/ingredients/paragraphs.rb +27 -0
  26. data/lib/pasta/ingredients/setext_headings.rb +20 -0
  27. data/lib/pasta/recipes/base.rb +40 -0
  28. data/lib/pasta/recipes/gruber.rb +27 -0
  29. data/lib/pasta/recipes/markdown.rb +11 -0
  30. data/lib/pasta/version.rb +3 -0
  31. data/pasta.gemspec +24 -0
  32. data/specs/base_spec.rb +8 -0
  33. data/specs/dish_spec.rb +30 -0
  34. data/specs/dsl_spec.rb +12 -0
  35. data/specs/recipes/gruber/automatic_links.yml +12 -0
  36. data/specs/recipes/gruber/blockquotes.yml +112 -0
  37. data/specs/recipes/gruber/code.yml +48 -0
  38. data/specs/recipes/gruber/codeblocks.yml +47 -0
  39. data/specs/recipes/gruber/emphasis.yml +57 -0
  40. data/specs/recipes/gruber/gruber_spec.rb +22 -0
  41. data/specs/recipes/gruber/headers.yml +88 -0
  42. data/specs/recipes/gruber/images.yml +49 -0
  43. data/specs/recipes/gruber/links.yml +184 -0
  44. data/specs/recipes/gruber/lists.yml +171 -0
  45. data/specs/recipes/gruber/page_breaks.yml +31 -0
  46. data/specs/recipes/gruber/paragraphs.yml +44 -0
  47. data/specs/spec_helper.rb +3 -0
  48. metadata +149 -0
@@ -0,0 +1,13 @@
1
+ module Pasta
2
+ module Ingredients
3
+
4
+ EMPTY_LINE = /\A\s*[\n\r]+/
5
+
6
+ def empty_lines(text, html)
7
+ text.sub!(EMPTY_LINE) do |match|
8
+ match = ''
9
+ end
10
+ [text, html]
11
+ end
12
+ end
13
+ end
@@ -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,14 @@
1
+ module Pasta
2
+ module Ingredients
3
+
4
+ PAGE_BREAK = /\A([*-_]\s?){3,}$/
5
+
6
+ def page_breaks(text, html)
7
+ text.sub!(PAGE_BREAK) do |match|
8
+ html << "<hr />"
9
+ match = ''
10
+ end
11
+ [text, html]
12
+ end
13
+ end
14
+ 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
@@ -0,0 +1,11 @@
1
+ module Pasta
2
+ module Recipes
3
+ class Markdown < Gruber
4
+
5
+ # This is currently identical to the Gruber recipe.
6
+ # TODO: Extract the specs from http://www.w3.org/community/markdown/ and rewrite the grammar
7
+
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Pasta
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pasta::Recipes::Base do
4
+ let(:base) { Pasta::Recipes::Base.new }
5
+
6
+ specify { ->{ base.to_nothing }.must_raise Pasta::Error }
7
+ specify { ->{ base.to_html 'some yummy text' }.must_raise Pasta::Error }
8
+ end
@@ -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
@@ -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>