org-ruby 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.
Files changed (51) hide show
  1. data/.bnsignore +18 -0
  2. data/History.txt +24 -0
  3. data/README.txt +66 -0
  4. data/Rakefile +22 -0
  5. data/TAGS +128 -0
  6. data/bin/org-ruby +40 -0
  7. data/lib/org-ruby.rb +48 -0
  8. data/lib/org-ruby/headline.rb +75 -0
  9. data/lib/org-ruby/html_output_buffer.rb +80 -0
  10. data/lib/org-ruby/line.rb +172 -0
  11. data/lib/org-ruby/output_buffer.rb +154 -0
  12. data/lib/org-ruby/parser.rb +72 -0
  13. data/lib/org-ruby/regexp_helper.rb +156 -0
  14. data/lib/org-ruby/textile_output_buffer.rb +67 -0
  15. data/spec/data/freeform.org +111 -0
  16. data/spec/data/hyp-planning.org +335 -0
  17. data/spec/data/remember.org +53 -0
  18. data/spec/headline_spec.rb +55 -0
  19. data/spec/html_examples/block_code.html +29 -0
  20. data/spec/html_examples/block_code.org +35 -0
  21. data/spec/html_examples/blockquote.html +7 -0
  22. data/spec/html_examples/blockquote.org +13 -0
  23. data/spec/html_examples/inline-formatting.html +10 -0
  24. data/spec/html_examples/inline-formatting.org +17 -0
  25. data/spec/html_examples/lists.html +19 -0
  26. data/spec/html_examples/lists.org +36 -0
  27. data/spec/html_examples/tables.html +20 -0
  28. data/spec/html_examples/tables.org +26 -0
  29. data/spec/html_examples/text.html +2 -0
  30. data/spec/html_examples/text.org +16 -0
  31. data/spec/line_spec.rb +89 -0
  32. data/spec/parser_spec.rb +86 -0
  33. data/spec/regexp_helper_spec.rb +57 -0
  34. data/spec/spec_helper.rb +20 -0
  35. data/spec/textile_examples/block_code.org +35 -0
  36. data/spec/textile_examples/block_code.textile +29 -0
  37. data/spec/textile_examples/blockquote.org +13 -0
  38. data/spec/textile_examples/blockquote.textile +11 -0
  39. data/spec/textile_examples/keywords.org +13 -0
  40. data/spec/textile_examples/keywords.textile +11 -0
  41. data/spec/textile_examples/links.org +11 -0
  42. data/spec/textile_examples/links.textile +10 -0
  43. data/spec/textile_examples/lists.org +36 -0
  44. data/spec/textile_examples/lists.textile +20 -0
  45. data/spec/textile_examples/single-space-plain-list.org +13 -0
  46. data/spec/textile_examples/single-space-plain-list.textile +10 -0
  47. data/spec/textile_examples/tables.org +26 -0
  48. data/spec/textile_examples/tables.textile +23 -0
  49. data/spec/textile_output_buffer_spec.rb +21 -0
  50. data/test/test_orgmode_parser.rb +0 -0
  51. metadata +120 -0
@@ -0,0 +1,172 @@
1
+ module Orgmode
2
+
3
+ # Represents a single line of an orgmode file.
4
+ class Line
5
+
6
+ # This is the line itself.
7
+ attr_reader :line
8
+
9
+ # The indent level of this line. this is important to properly translate
10
+ # nested lists from orgmode to textile.
11
+ # TODO 2009-12-20 bdewey: Handle tabs
12
+ attr_reader :indent
13
+
14
+ def initialize(line)
15
+ @line = line
16
+ @indent = 0
17
+ @line =~ /\s*/
18
+ @indent = $&.length unless blank?
19
+ end
20
+
21
+ def to_s
22
+ return @line
23
+ end
24
+
25
+ # Tests if a line is a comment.
26
+ def comment?
27
+ @line =~ /^\s*#/
28
+ end
29
+
30
+ # Tests if a line contains metadata instead of actual content.
31
+ def metadata?
32
+ @line =~ /^\s*(CLOCK|DEADLINE|START|CLOSED|SCHEDULED):/
33
+ end
34
+
35
+ def nonprinting?
36
+ comment? || metadata?
37
+ end
38
+
39
+ def blank?
40
+ @line =~ /^\s*$/
41
+ end
42
+
43
+ def plain_list?
44
+ ordered_list? or unordered_list?
45
+ end
46
+
47
+ UnorderedListRegexp = /^\s*(-|\+)\s*/
48
+
49
+ def unordered_list?
50
+ @line =~ UnorderedListRegexp
51
+ end
52
+
53
+ def strip_unordered_list_tag
54
+ @line.sub(UnorderedListRegexp, "")
55
+ end
56
+
57
+ OrderedListRegexp = /^\s*\d+(\.|\))\s*/
58
+
59
+ def ordered_list?
60
+ @line =~ OrderedListRegexp
61
+ end
62
+
63
+ def strip_ordered_list_tag
64
+ @line.sub(OrderedListRegexp, "")
65
+ end
66
+
67
+ def plain_text?
68
+ not metadata? and not blank? and not plain_list?
69
+ end
70
+
71
+ def table_row?
72
+ # for an org-mode table, the first non-whitespace character is a
73
+ # | (pipe).
74
+ @line =~ /^\s*\|/
75
+ end
76
+
77
+ def table_separator?
78
+ # an org-mode table separator has the first non-whitespace
79
+ # character as a | (pipe), then consists of nothing else other
80
+ # than pipes, hyphens, and pluses.
81
+
82
+ @line =~ /^\s*\|[-\|\+]*\s*$/
83
+ end
84
+
85
+ def table?
86
+ table_row? or table_separator?
87
+ end
88
+
89
+ BlockRegexp = /^\s*#\+(BEGIN|END)_(\w*)/
90
+
91
+ def begin_block?
92
+ @line =~ BlockRegexp && $1 == "BEGIN"
93
+ end
94
+
95
+ def end_block?
96
+ @line =~ BlockRegexp && $1 == "END"
97
+ end
98
+
99
+ def block_type
100
+ $2 if @line =~ BlockRegexp
101
+ end
102
+
103
+ # Determines the paragraph type of the current line.
104
+ def paragraph_type
105
+ return :blank if blank?
106
+ return :ordered_list if ordered_list?
107
+ return :unordered_list if unordered_list?
108
+ return :metadata if metadata?
109
+ return :comment if comment?
110
+ return :table_separator if table_separator?
111
+ return :table_row if table_row?
112
+ return :paragraph
113
+ end
114
+
115
+ def self.to_textile(lines)
116
+ output = ""
117
+ output_buffer = TextileOutputBuffer.new(output)
118
+ translate(lines, output_buffer)
119
+ end
120
+
121
+ def self.to_html(lines)
122
+ output = ""
123
+ output_buffer = HtmlOutputBuffer.new(output)
124
+ translate(lines, output_buffer)
125
+ end
126
+
127
+ # Converts an array of lines to textile format.
128
+ def self.translate(lines, output_buffer)
129
+ lines.each do |line|
130
+
131
+ # See if we're carrying paragraph payload, and output
132
+ # it if we're about to switch to some other output type.
133
+ output_buffer.prepare(line)
134
+
135
+ case line.paragraph_type
136
+ when :metadata, :table_separator, :blank
137
+
138
+ # IGNORE
139
+
140
+ when :comment
141
+
142
+ output_buffer.push_mode(:blockquote) if line.begin_block? and line.block_type == "QUOTE"
143
+ output_buffer.push_mode(:code) if line.begin_block? and line.block_type == "EXAMPLE"
144
+ output_buffer.pop_mode(:blockquote) if line.end_block? and line.block_type == "QUOTE"
145
+ output_buffer.pop_mode(:code) if line.end_block? and line.block_type == "EXAMPLE"
146
+
147
+ when :table_row
148
+
149
+ output_buffer << line.line.lstrip
150
+
151
+ when :ordered_list
152
+
153
+ output_buffer << line.strip_ordered_list_tag << " "
154
+
155
+ when :unordered_list
156
+
157
+ output_buffer << line.strip_unordered_list_tag << " "
158
+
159
+ when :paragraph
160
+
161
+ if output_buffer.preserve_whitespace? then
162
+ output_buffer << line.line
163
+ else
164
+ output_buffer << line.line.strip << " "
165
+ end
166
+ end
167
+ end
168
+ output_buffer.flush!
169
+ output_buffer.output
170
+ end
171
+ end # class Line
172
+ end # module Orgmode
@@ -0,0 +1,154 @@
1
+ require 'logger'
2
+
3
+ module Orgmode
4
+
5
+ # The OutputBuffer is used to accumulate multiple lines of orgmode
6
+ # text, and then emit them to the output all in one go. The class
7
+ # will do the final textile substitution for inline formatting and
8
+ # add a newline character prior emitting the output.
9
+ class OutputBuffer
10
+
11
+ # This is the temporary buffer that we accumulate into.
12
+ attr_reader :buffer
13
+
14
+ # This is the overall output buffer
15
+ attr_reader :output
16
+
17
+ # This is the current type of output being accumulated.
18
+ attr_accessor :output_type
19
+
20
+ # Creates a new OutputBuffer object that is bound to an output object.
21
+ # The output will get flushed to =output=.
22
+ def initialize(output)
23
+ @output = output
24
+ @buffer = ""
25
+ @output_type = :start
26
+ @list_indent_stack = []
27
+ @paragraph_modifier = nil
28
+ @cancel_modifier = false
29
+ @mode_stack = []
30
+ push_mode(:normal)
31
+
32
+ @logger = Logger.new(STDERR)
33
+ @logger.level = Logger::WARN
34
+
35
+ @re_help = RegexpHelper.new
36
+ end
37
+
38
+ Modes = [:normal, :ordered_list, :unordered_list, :blockquote, :code, :table]
39
+
40
+ def current_mode
41
+ @mode_stack.last
42
+ end
43
+
44
+ def current_mode_list?
45
+ (current_mode == :ordered_list) or (current_mode == :unordered_list)
46
+ end
47
+
48
+ def push_mode(mode)
49
+ raise "Not a recognized mode: #{mode}" unless Modes.include?(mode)
50
+ @mode_stack.push(mode)
51
+ end
52
+
53
+ def pop_mode(mode = nil)
54
+ m = @mode_stack.pop
55
+ @logger.warn "Modes don't match. Expected to pop #{mode}, but popped #{m}" if mode && mode != m
56
+ m
57
+ end
58
+
59
+ # Prepares the output buffer to receive content from a line.
60
+ # As a side effect, this may flush the current accumulated text.
61
+ def prepare(line)
62
+ @logger.debug "Looking at #{line.paragraph_type}: #{line.to_s}"
63
+ if not should_accumulate_output?(line) then
64
+ flush!
65
+ maintain_list_indent_stack(line)
66
+ @output_type = line.paragraph_type
67
+ end
68
+ push_mode(:table) if enter_table?
69
+ pop_mode(:table) if exit_table?
70
+ end
71
+
72
+ # Tests if we are entering a table mode.
73
+ def enter_table?
74
+ ((@output_type == :table_row) || (@output_type == :table_separator)) &&
75
+ (current_mode != :table)
76
+ end
77
+
78
+ # Tests if we are existing a table mode.
79
+ def exit_table?
80
+ ((@output_type != :table_row) && (@output_type != :table_separator)) &&
81
+ (current_mode == :table)
82
+ end
83
+
84
+ # Accumulate the string @str@.
85
+ def << (str)
86
+ @buffer << str
87
+ end
88
+
89
+ # Gets the current list indent level.
90
+ def list_indent_level
91
+ @list_indent_stack.length
92
+ end
93
+
94
+ # Test if we're in an output mode in which whitespace is significant.
95
+ def preserve_whitespace?
96
+ return current_mode == :code
97
+ end
98
+
99
+ ######################################################################
100
+ private
101
+
102
+ def maintain_list_indent_stack(line)
103
+ if (line.plain_list?) then
104
+ while (not @list_indent_stack.empty? \
105
+ and (@list_indent_stack.last > line.indent))
106
+ @list_indent_stack.pop
107
+ pop_mode
108
+ end
109
+ if (@list_indent_stack.empty? \
110
+ or @list_indent_stack.last < line.indent)
111
+ @list_indent_stack.push(line.indent)
112
+ push_mode line.paragraph_type
113
+ end
114
+ else
115
+ @list_indent_stack = []
116
+ while ((current_mode == :ordered_list) or
117
+ (current_mode == :unordered_list))
118
+ pop_mode
119
+ end
120
+ end
121
+ end
122
+
123
+ # Tests if the current line should be accumulated in the current
124
+ # output buffer. (Extraneous line breaks in the orgmode buffer
125
+ # are removed by accumulating lines in the output buffer without
126
+ # line breaks.)
127
+ def should_accumulate_output?(line)
128
+
129
+ # Special case: Preserve line breaks in block code mode.
130
+ return false if preserve_whitespace?
131
+
132
+ # Special case: Multiple blank lines get accumulated.
133
+ return true if line.paragraph_type == :blank and @output_type == :blank
134
+
135
+ # Currently only "paragraphs" get accumulated with previous output.
136
+ return false unless line.paragraph_type == :paragraph
137
+ if ((@output_type == :ordered_list) or
138
+ (@output_type == :unordered_list)) then
139
+
140
+ # If the previous output type was a list item, then we only put a paragraph in it
141
+ # if its indent level is greater than the list indent level.
142
+
143
+ return false unless line.indent > @list_indent_stack.last
144
+ end
145
+
146
+ # Only accumulate paragraphs with lists & paragraphs.
147
+ return false unless
148
+ ((@output_type == :paragraph) or
149
+ (@output_type == :ordered_list) or
150
+ (@output_type == :unordered_list))
151
+ true
152
+ end
153
+ end # class OutputBuffer
154
+ end # module Orgmode
@@ -0,0 +1,72 @@
1
+ ##
2
+ ## Simple routines for loading / saving an ORG file.
3
+ ##
4
+
5
+ module Orgmode
6
+
7
+ class Parser
8
+
9
+ # All of the lines of the orgmode file
10
+ attr_reader :lines
11
+
12
+ # All of the headlines in the org file
13
+ attr_reader :headlines
14
+
15
+ # These are any lines before the first headline
16
+ attr_reader :header_lines
17
+
18
+ # I can construct a parser object either with an array of lines
19
+ # or with a single string that I will split along \n boundaries.
20
+ def initialize(lines)
21
+ if lines.is_a? Array then
22
+ @lines = lines
23
+ elsif lines.is_a? String then
24
+ @lines = lines.split("\n")
25
+ else
26
+ raise "Unsupported type for +lines+: #{lines.class}"
27
+ end
28
+
29
+ @headlines = Array.new
30
+ @current_headline = nil
31
+ @header_lines = []
32
+ @lines.each do |line|
33
+ if (Headline.headline? line) then
34
+ @current_headline = Headline.new line
35
+ @headlines << @current_headline
36
+ else
37
+ line = Line.new line
38
+ if (@current_headline) then
39
+ @current_headline.body_lines << line
40
+ else
41
+ @header_lines << line
42
+ end
43
+ end
44
+ end
45
+ end # initialize
46
+
47
+ # Creates a new parser from the data in a given file
48
+ def self.load(fname)
49
+ lines = IO.readlines(fname)
50
+ return self.new(lines)
51
+ end
52
+
53
+ # Saves the loaded orgmode file as a textile file.
54
+ def to_textile
55
+ output = ""
56
+ output << Line.to_textile(@header_lines)
57
+ @headlines.each do |headline|
58
+ output << headline.to_textile
59
+ end
60
+ output
61
+ end
62
+
63
+ def to_html
64
+ output = ""
65
+ output << Line.to_html(@header_lines)
66
+ @headlines.each do |headline|
67
+ output << headline.to_html
68
+ end
69
+ output
70
+ end
71
+ end # class Parser
72
+ end # module Orgmode
@@ -0,0 +1,156 @@
1
+ require 'logger'
2
+
3
+ module Orgmode
4
+
5
+ # = Summary
6
+ #
7
+ # This class contains helper routines to deal with the Regexp "black
8
+ # magic" you need to properly parse org-mode files.
9
+ #
10
+ # = Key methods
11
+ #
12
+ # * Use +rewrite_emphasis+ to replace org-mode emphasis strings (e.g.,
13
+ # \/italic/) with the suitable markup for the output.
14
+ #
15
+ # * Use +rewrite_links+ to get a chance to rewrite all org-mode
16
+ # links with suitable markup for the output.
17
+ class RegexpHelper
18
+
19
+ ######################################################################
20
+ # EMPHASIS
21
+ #
22
+ # I figure it's best to stick as closely to the elisp implementation
23
+ # as possible for emphasis. org.el defines the regular expression that
24
+ # is used to apply "emphasis" (in my terminology, inline formatting
25
+ # instead of block formatting). Here's the documentation from org.el.
26
+ #
27
+ # Terminology: In an emphasis string like " *strong word* ", we
28
+ # call the initial space PREMATCH, the final space POSTMATCH, the
29
+ # stars MARKERS, "s" and "d" are BORDER characters and "trong wor"
30
+ # is the body. The different components in this variable specify
31
+ # what is allowed/forbidden in each part:
32
+ #
33
+ # pre Chars allowed as prematch. Line beginning allowed, too.
34
+ # post Chars allowed as postmatch. Line end will be allowed too.
35
+ # border The chars *forbidden* as border characters.
36
+ # body-regexp A regexp like \".\" to match a body character. Don't use
37
+ # non-shy groups here, and don't allow newline here.
38
+ # newline The maximum number of newlines allowed in an emphasis exp.
39
+ #
40
+ # I currently don't use +newline+ because I've thrown this information
41
+ # away by this point in the code. TODO -- revisit?
42
+ attr_reader :pre_emphasis
43
+ attr_reader :post_emphasis
44
+ attr_reader :border_forbidden
45
+ attr_reader :body_regexp
46
+ attr_reader :markers
47
+
48
+ attr_reader :org_emphasis_regexp
49
+
50
+ def initialize
51
+ # Set up the emphasis regular expression.
52
+ @pre_emphasis = " \t\\('\""
53
+ @post_emphasis = "- \t.,:!?;'\"\\)"
54
+ @border_forbidden = " \t\r\n,\"'"
55
+ @body_regexp = ".*?"
56
+ @markers = "*/_=~+"
57
+ @logger = Logger.new(STDERR)
58
+ @logger.level = Logger::WARN
59
+ build_org_emphasis_regexp
60
+ build_org_link_regexp
61
+ end
62
+
63
+ # Finds all emphasis matches in a string.
64
+ # Supply a block that will get the marker and body as parameters.
65
+ def match_all(str)
66
+ str.scan(@org_emphasis_regexp) do |match|
67
+ yield $2, $3
68
+ end
69
+ end
70
+
71
+ # Compute replacements for all matching emphasized phrases.
72
+ # Supply a block that will get the marker and body as parameters;
73
+ # return the replacement string from your block.
74
+ #
75
+ # = Example
76
+ #
77
+ # re = RegexpHelper.new
78
+ # result = re.rewrite_emphasis("*bold*, /italic/, =code=") do |marker, body|
79
+ # "<#{map[marker]}>#{body}</#{map[marker]}>"
80
+ # end
81
+ #
82
+ # In this example, the block body will get called three times:
83
+ #
84
+ # 1. Marker: "*", body: "bold"
85
+ # 2. Marker: "/", body: "italic"
86
+ # 3. Marker: "=", body: "code"
87
+ #
88
+ # The return from this block is a string that will be used to
89
+ # replace "*bold*", "/italic/", and "=code=",
90
+ # respectively. (Clearly this sample string will use HTML-like
91
+ # syntax, assuming +map+ is defined appropriately.)
92
+ def rewrite_emphasis(str)
93
+ str.gsub(@org_emphasis_regexp) do |match|
94
+ inner = yield $2, $3
95
+ "#{$1}#{inner}#{$4}"
96
+ end
97
+ end
98
+
99
+ # = Summary
100
+ #
101
+ # Rewrite org-mode links in a string to markup suitable to the
102
+ # output format.
103
+ #
104
+ # = Usage
105
+ #
106
+ # Give this a block that expect the link and optional friendly
107
+ # text. Return how that link should get formatted.
108
+ #
109
+ # = Example
110
+ #
111
+ # re = RegexpHelper.new
112
+ # result = re.rewrite_links("[[http://www.bing.com]] and [[http://www.hotmail.com][Hotmail]]") do |link, text}
113
+ # text ||= link
114
+ # "<a href=\"#{link}\">#{text}</a>"
115
+ # end
116
+ #
117
+ # In this example, the block body will get called two times. In the
118
+ # first instance, +text+ will be nil (the org-mode markup gives no
119
+ # friendly text for the link +http://www.bing.com+. In the second
120
+ # instance, the block will get text of *Hotmail* and the link
121
+ # +http://www.hotmail.com+. In both cases, the block returns an
122
+ # HTML-style link, and that is how things will get recorded in
123
+ # +result+.
124
+ def rewrite_links(str)
125
+ i = str.gsub(@org_link_regexp) do |match|
126
+ yield $1, nil
127
+ end
128
+ i.gsub(@org_link_text_regexp) do |match|
129
+ yield $1, $2
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def build_org_emphasis_regexp
136
+ @org_emphasis_regexp = Regexp.new("([#{@pre_emphasis}]|^)\n" +
137
+ "( [#{@markers}] )\n" +
138
+ "( [^#{@border_forbidden}] | " +
139
+ " [^#{@border_forbidden}]#{@body_regexp}[^#{@border_forbidden}] )\n" +
140
+ "\\2\n" +
141
+ "([#{@post_emphasis}]|$)\n", Regexp::EXTENDED)
142
+ @logger.debug "Just created regexp: #{@org_emphasis_regexp}"
143
+ end
144
+
145
+ def build_org_link_regexp
146
+ @org_link_regexp = /\[\[
147
+ ([^\]]*) # This is the URL
148
+ \]\]/x
149
+ @org_link_text_regexp = /\[\[
150
+ ([^\]]*) # This is the URL
151
+ \]\[
152
+ ([^\]]*) # This is the friendly text
153
+ \]\]/x
154
+ end
155
+ end # class Emphasis
156
+ end # module Orgmode