org-ruby 0.5.1 → 0.5.2

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 (89) hide show
  1. data/History.txt +81 -77
  2. data/README.rdoc +66 -66
  3. data/Rakefile +28 -28
  4. data/bin/org-ruby +40 -40
  5. data/lib/org-ruby.rb +50 -50
  6. data/lib/org-ruby/headline.rb +102 -120
  7. data/lib/org-ruby/html_output_buffer.rb +174 -156
  8. data/lib/org-ruby/line.rb +206 -260
  9. data/lib/org-ruby/output_buffer.rb +227 -191
  10. data/lib/org-ruby/parser.rb +320 -272
  11. data/lib/org-ruby/regexp_helper.rb +156 -156
  12. data/lib/org-ruby/textile_output_buffer.rb +67 -68
  13. data/spec/data/freeform-example.org +113 -113
  14. data/spec/data/freeform.org +111 -111
  15. data/spec/data/hyp-planning.org +335 -335
  16. data/spec/data/remember.org +53 -53
  17. data/spec/headline_spec.rb +55 -55
  18. data/spec/html_examples/advanced-code.html +36 -36
  19. data/spec/html_examples/advanced-code.org +53 -53
  20. data/spec/html_examples/advanced-lists.html +31 -31
  21. data/spec/html_examples/advanced-lists.org +31 -31
  22. data/spec/html_examples/block_code.html +28 -28
  23. data/spec/html_examples/block_code.org +35 -35
  24. data/spec/html_examples/blockquote.html +7 -7
  25. data/spec/html_examples/blockquote.org +13 -13
  26. data/spec/html_examples/code-comment.html +18 -18
  27. data/spec/html_examples/code-comment.org +22 -22
  28. data/spec/html_examples/custom-seq-todo.html +15 -15
  29. data/spec/html_examples/custom-seq-todo.org +24 -24
  30. data/spec/html_examples/custom-todo.html +15 -15
  31. data/spec/html_examples/custom-todo.org +24 -24
  32. data/spec/html_examples/custom-typ-todo.html +15 -15
  33. data/spec/html_examples/custom-typ-todo.org +24 -24
  34. data/spec/html_examples/entities.html +4 -4
  35. data/spec/html_examples/entities.org +11 -11
  36. data/spec/html_examples/escape-pre.html +6 -6
  37. data/spec/html_examples/escape-pre.org +6 -6
  38. data/spec/html_examples/export-exclude-only.html +13 -13
  39. data/spec/html_examples/export-exclude-only.org +81 -81
  40. data/spec/html_examples/export-keywords.html +4 -4
  41. data/spec/html_examples/export-keywords.org +18 -18
  42. data/spec/html_examples/export-tags.html +8 -8
  43. data/spec/html_examples/export-tags.org +82 -82
  44. data/spec/html_examples/export-title.html +2 -2
  45. data/spec/html_examples/export-title.org +4 -4
  46. data/spec/html_examples/html-literal.html +2 -2
  47. data/spec/html_examples/html-literal.org +6 -6
  48. data/spec/html_examples/inline-formatting.html +10 -10
  49. data/spec/html_examples/inline-formatting.org +17 -17
  50. data/spec/html_examples/link-features.html +8 -8
  51. data/spec/html_examples/link-features.org +19 -19
  52. data/spec/html_examples/lists.html +19 -19
  53. data/spec/html_examples/lists.org +36 -36
  54. data/spec/html_examples/metadata-comment.html +27 -27
  55. data/spec/html_examples/metadata-comment.org +30 -30
  56. data/spec/html_examples/only-list.html +5 -5
  57. data/spec/html_examples/only-list.org +3 -3
  58. data/spec/html_examples/only-table.html +6 -6
  59. data/spec/html_examples/only-table.org +5 -5
  60. data/spec/html_examples/skip-header.html +3 -3
  61. data/spec/html_examples/skip-header.org +28 -28
  62. data/spec/html_examples/skip-table.html +4 -4
  63. data/spec/html_examples/skip-table.org +19 -19
  64. data/spec/html_examples/tables.html +20 -20
  65. data/spec/html_examples/tables.org +26 -26
  66. data/spec/html_examples/text.html +2 -2
  67. data/spec/html_examples/text.org +16 -16
  68. data/spec/line_spec.rb +151 -151
  69. data/spec/output_buffer_spec.rb +19 -0
  70. data/spec/parser_spec.rb +152 -166
  71. data/spec/regexp_helper_spec.rb +57 -57
  72. data/spec/spec_helper.rb +21 -21
  73. data/spec/textile_examples/block_code.org +35 -35
  74. data/spec/textile_examples/block_code.textile +29 -29
  75. data/spec/textile_examples/blockquote.org +13 -13
  76. data/spec/textile_examples/blockquote.textile +11 -11
  77. data/spec/textile_examples/keywords.org +13 -13
  78. data/spec/textile_examples/keywords.textile +11 -11
  79. data/spec/textile_examples/links.org +11 -11
  80. data/spec/textile_examples/links.textile +10 -10
  81. data/spec/textile_examples/lists.org +36 -36
  82. data/spec/textile_examples/lists.textile +20 -20
  83. data/spec/textile_examples/single-space-plain-list.org +13 -13
  84. data/spec/textile_examples/single-space-plain-list.textile +10 -10
  85. data/spec/textile_examples/tables.org +26 -26
  86. data/spec/textile_examples/tables.textile +23 -23
  87. data/spec/textile_output_buffer_spec.rb +21 -21
  88. data/tasks/test_case.rake +49 -49
  89. metadata +3 -2
@@ -1,191 +1,227 @@
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 accumulation buffer. It's a holding pen so
12
- # consecutive lines of the right type can get stuck together
13
- # without intervening newlines.
14
- attr_reader :buffer
15
-
16
- # This is the output mode of the accumulation buffer.
17
- attr_reader :buffer_mode
18
-
19
- # This is the overall output buffer
20
- attr_reader :output
21
-
22
- # This is the current type of output being accumulated.
23
- attr_accessor :output_type
24
-
25
- # Creates a new OutputBuffer object that is bound to an output object.
26
- # The output will get flushed to =output=.
27
- def initialize(output)
28
- @output = output
29
- @buffer = ""
30
- @buffer_mode = nil
31
- @output_type = :start
32
- @list_indent_stack = []
33
- @paragraph_modifier = nil
34
- @cancel_modifier = false
35
- @mode_stack = []
36
-
37
- @logger = Logger.new(STDERR)
38
- if ENV['DEBUG']
39
- @logger.level = Logger::DEBUG
40
- else
41
- @logger.level = Logger::WARN
42
- end
43
-
44
- @re_help = RegexpHelper.new
45
- push_mode(:normal)
46
- end
47
-
48
- Modes = [:normal, :ordered_list, :unordered_list, :blockquote, :src, :example, :table, :inline_example]
49
-
50
- def current_mode
51
- @mode_stack.last
52
- end
53
-
54
- def current_mode_list?
55
- (current_mode == :ordered_list) or (current_mode == :unordered_list)
56
- end
57
-
58
- def push_mode(mode)
59
- raise "Not a recognized mode: #{mode}" unless Modes.include?(mode)
60
- @mode_stack.push(mode)
61
- end
62
-
63
- def pop_mode(mode = nil)
64
- m = @mode_stack.pop
65
- @logger.warn "Modes don't match. Expected to pop #{mode}, but popped #{m}" if mode && mode != m
66
- m
67
- end
68
-
69
- # Prepares the output buffer to receive content from a line.
70
- # As a side effect, this may flush the current accumulated text.
71
- def prepare(line)
72
- @logger.debug "Looking at #{line.paragraph_type}: #{line.to_s}"
73
- if not should_accumulate_output?(line) then
74
- flush!
75
- maintain_list_indent_stack(line)
76
- @output_type = line.paragraph_type
77
- end
78
- push_mode(:inline_example) if line.inline_example? and current_mode != :inline_example
79
- pop_mode(:inline_example) if current_mode == :inline_example && !line.inline_example?
80
- push_mode(:table) if enter_table?
81
- pop_mode(:table) if exit_table?
82
- end
83
-
84
- # Tests if we are entering a table mode.
85
- def enter_table?
86
- ((@output_type == :table_row) || (@output_type == :table_header) || (@output_type == :table_separator)) &&
87
- (current_mode != :table)
88
- end
89
-
90
- # Tests if we are existing a table mode.
91
- def exit_table?
92
- ((@output_type != :table_row) && (@output_type != :table_header) && (@output_type != :table_separator)) &&
93
- (current_mode == :table)
94
- end
95
-
96
- # Accumulate the string @str@.
97
- def << (str)
98
- if @buffer_mode && @buffer_mode != current_mode then
99
- raise "Accumulation buffer is mixing modes: @buffer_mode == #{@buffer_mode}, current_mode == #{current_mode}"
100
- else
101
- @buffer_mode = current_mode
102
- end
103
- @buffer << str
104
- end
105
-
106
- # Gets the current list indent level.
107
- def list_indent_level
108
- @list_indent_stack.length
109
- end
110
-
111
- # Test if we're in an output mode in which whitespace is significant.
112
- def preserve_whitespace?
113
- mode_is_code current_mode
114
- end
115
-
116
- ######################################################################
117
- private
118
-
119
- def mode_is_code(mode)
120
- case mode
121
- when :src, :inline_example, :example
122
- true
123
- else
124
- false
125
- end
126
- end
127
-
128
- def maintain_list_indent_stack(line)
129
- if (line.plain_list?) then
130
- while (not @list_indent_stack.empty? \
131
- and (@list_indent_stack.last > line.indent))
132
- @list_indent_stack.pop
133
- pop_mode
134
- end
135
- if (@list_indent_stack.empty? \
136
- or @list_indent_stack.last < line.indent)
137
- @list_indent_stack.push(line.indent)
138
- push_mode line.paragraph_type
139
- end
140
- elsif line.blank? then
141
-
142
- # Nothing
143
-
144
- elsif ((line.paragraph_type == :paragraph) and
145
- (not @list_indent_stack.empty? and
146
- line.indent > @list_indent_stack.last))
147
-
148
- # Nothing -- output this paragraph inside
149
- # the list block (ul/ol)
150
-
151
- else
152
- @list_indent_stack = []
153
- while ((current_mode == :ordered_list) or
154
- (current_mode == :unordered_list))
155
- pop_mode
156
- end
157
- end
158
- end
159
-
160
- # Tests if the current line should be accumulated in the current
161
- # output buffer. (Extraneous line breaks in the orgmode buffer
162
- # are removed by accumulating lines in the output buffer without
163
- # line breaks.)
164
- def should_accumulate_output?(line)
165
-
166
- # Special case: Preserve line breaks in block code mode.
167
- return false if preserve_whitespace?
168
-
169
- # Special case: Multiple blank lines get accumulated.
170
- return true if line.paragraph_type == :blank and @output_type == :blank
171
-
172
- # Currently only "paragraphs" get accumulated with previous output.
173
- return false unless line.paragraph_type == :paragraph
174
- if ((@output_type == :ordered_list) or
175
- (@output_type == :unordered_list)) then
176
-
177
- # If the previous output type was a list item, then we only put a paragraph in it
178
- # if its indent level is greater than the list indent level.
179
-
180
- return false unless line.indent > @list_indent_stack.last
181
- end
182
-
183
- # Only accumulate paragraphs with lists & paragraphs.
184
- return false unless
185
- ((@output_type == :paragraph) or
186
- (@output_type == :ordered_list) or
187
- (@output_type == :unordered_list))
188
- true
189
- end
190
- end # class OutputBuffer
191
- end # module Orgmode
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 accumulation buffer. It's a holding pen so
12
+ # consecutive lines of the right type can get stuck together
13
+ # without intervening newlines.
14
+ attr_reader :buffer
15
+
16
+ # These are the Line objects that are currently in the accumulation
17
+ # buffer.
18
+ attr_reader :buffered_lines
19
+
20
+ # This is the output mode of the accumulation buffer.
21
+ attr_reader :buffer_mode
22
+
23
+ # This is the overall output buffer
24
+ attr_reader :output
25
+
26
+ # This is the current type of output being accumulated.
27
+ attr_accessor :output_type
28
+
29
+ # This stack is used to do proper outline numbering of headlines.
30
+ attr_accessor :headline_number_stack
31
+
32
+ # Creates a new OutputBuffer object that is bound to an output object.
33
+ # The output will get flushed to =output=.
34
+ def initialize(output)
35
+ @output = output
36
+ @buffer = ""
37
+ @buffered_lines = []
38
+ @buffer_mode = nil
39
+ @output_type = :start
40
+ @list_indent_stack = []
41
+ @paragraph_modifier = nil
42
+ @cancel_modifier = false
43
+ @mode_stack = []
44
+ @headline_number_stack = []
45
+
46
+ @logger = Logger.new(STDERR)
47
+ if ENV['DEBUG']
48
+ @logger.level = Logger::DEBUG
49
+ else
50
+ @logger.level = Logger::WARN
51
+ end
52
+
53
+ @re_help = RegexpHelper.new
54
+ push_mode(:normal)
55
+ end
56
+
57
+ Modes = [:normal, :ordered_list, :unordered_list, :blockquote, :src, :example, :table, :inline_example]
58
+
59
+ def current_mode
60
+ @mode_stack.last
61
+ end
62
+
63
+ def current_mode_list?
64
+ (current_mode == :ordered_list) or (current_mode == :unordered_list)
65
+ end
66
+
67
+ def push_mode(mode)
68
+ raise "Not a recognized mode: #{mode}" unless Modes.include?(mode)
69
+ @mode_stack.push(mode)
70
+ end
71
+
72
+ def pop_mode(mode = nil)
73
+ m = @mode_stack.pop
74
+ @logger.warn "Modes don't match. Expected to pop #{mode}, but popped #{m}" if mode && mode != m
75
+ m
76
+ end
77
+
78
+ # Prepares the output buffer to receive content from a line.
79
+ # As a side effect, this may flush the current accumulated text.
80
+ def prepare(line)
81
+ @logger.debug "Looking at #{line.paragraph_type}: #{line.to_s}"
82
+ if not should_accumulate_output?(line) then
83
+ flush!
84
+ maintain_list_indent_stack(line)
85
+ @output_type = line.paragraph_type
86
+ end
87
+ push_mode(:inline_example) if line.inline_example? and current_mode != :inline_example
88
+ pop_mode(:inline_example) if current_mode == :inline_example && !line.inline_example?
89
+ push_mode(:table) if enter_table?
90
+ pop_mode(:table) if exit_table?
91
+ @buffered_lines.push(line)
92
+ end
93
+
94
+ # Flushes everything currently in the accumulation buffer into the
95
+ # output buffer. Derived classes must override this to actually move
96
+ # content into the output buffer with the appropriate markup. This
97
+ # method just does common bookkeeping cleanup.
98
+ def clear_accumulation_buffer!
99
+ @buffer = ""
100
+ @buffer_mode = nil
101
+ @buffered_lines = []
102
+ end
103
+
104
+ # Gets the next headline number for a given level. The intent is
105
+ # this function is called sequentially for each headline that
106
+ # needs to get numbered. It does standard outline numbering.
107
+ def get_next_headline_number(level)
108
+ raise "Headline level not valid: #{level}" if level <= 0
109
+ while level > @headline_number_stack.length do
110
+ @headline_number_stack.push 0
111
+ end
112
+ while level < @headline_number_stack.length do
113
+ @headline_number_stack.pop
114
+ end
115
+ raise "Oops, shouldn't happen" unless level == @headline_number_stack.length
116
+ @headline_number_stack[@headline_number_stack.length - 1] += 1
117
+ @headline_number_stack.join(".")
118
+ end
119
+
120
+ # Tests if we are entering a table mode.
121
+ def enter_table?
122
+ ((@output_type == :table_row) || (@output_type == :table_header) || (@output_type == :table_separator)) &&
123
+ (current_mode != :table)
124
+ end
125
+
126
+ # Tests if we are existing a table mode.
127
+ def exit_table?
128
+ ((@output_type != :table_row) && (@output_type != :table_header) && (@output_type != :table_separator)) &&
129
+ (current_mode == :table)
130
+ end
131
+
132
+ # Accumulate the string @str@.
133
+ def << (str)
134
+ if @buffer_mode && @buffer_mode != current_mode then
135
+ raise "Accumulation buffer is mixing modes: @buffer_mode == #{@buffer_mode}, current_mode == #{current_mode}"
136
+ else
137
+ @buffer_mode = current_mode
138
+ end
139
+ @buffer << str
140
+ end
141
+
142
+ # Gets the current list indent level.
143
+ def list_indent_level
144
+ @list_indent_stack.length
145
+ end
146
+
147
+ # Test if we're in an output mode in which whitespace is significant.
148
+ def preserve_whitespace?
149
+ mode_is_code current_mode
150
+ end
151
+
152
+ ######################################################################
153
+ private
154
+
155
+ def mode_is_code(mode)
156
+ case mode
157
+ when :src, :inline_example, :example
158
+ true
159
+ else
160
+ false
161
+ end
162
+ end
163
+
164
+ def maintain_list_indent_stack(line)
165
+ if (line.plain_list?) then
166
+ while (not @list_indent_stack.empty? \
167
+ and (@list_indent_stack.last > line.indent))
168
+ @list_indent_stack.pop
169
+ pop_mode
170
+ end
171
+ if (@list_indent_stack.empty? \
172
+ or @list_indent_stack.last < line.indent)
173
+ @list_indent_stack.push(line.indent)
174
+ push_mode line.paragraph_type
175
+ end
176
+ elsif line.blank? then
177
+
178
+ # Nothing
179
+
180
+ elsif ((line.paragraph_type == :paragraph) and
181
+ (not @list_indent_stack.empty? and
182
+ line.indent > @list_indent_stack.last))
183
+
184
+ # Nothing -- output this paragraph inside
185
+ # the list block (ul/ol)
186
+
187
+ else
188
+ @list_indent_stack = []
189
+ while ((current_mode == :ordered_list) or
190
+ (current_mode == :unordered_list))
191
+ pop_mode
192
+ end
193
+ end
194
+ end
195
+
196
+ # Tests if the current line should be accumulated in the current
197
+ # output buffer. (Extraneous line breaks in the orgmode buffer
198
+ # are removed by accumulating lines in the output buffer without
199
+ # line breaks.)
200
+ def should_accumulate_output?(line)
201
+
202
+ # Special case: Preserve line breaks in block code mode.
203
+ return false if preserve_whitespace?
204
+
205
+ # Special case: Multiple blank lines get accumulated.
206
+ return true if line.paragraph_type == :blank and @output_type == :blank
207
+
208
+ # Currently only "paragraphs" get accumulated with previous output.
209
+ return false unless line.paragraph_type == :paragraph
210
+ if ((@output_type == :ordered_list) or
211
+ (@output_type == :unordered_list)) then
212
+
213
+ # If the previous output type was a list item, then we only put a paragraph in it
214
+ # if its indent level is greater than the list indent level.
215
+
216
+ return false unless line.indent > @list_indent_stack.last
217
+ end
218
+
219
+ # Only accumulate paragraphs with lists & paragraphs.
220
+ return false unless
221
+ ((@output_type == :paragraph) or
222
+ (@output_type == :ordered_list) or
223
+ (@output_type == :unordered_list))
224
+ true
225
+ end
226
+ end # class OutputBuffer
227
+ end # module Orgmode
@@ -1,272 +1,320 @@
1
- require 'rubygems'
2
- require 'rubypants'
3
-
4
- module Orgmode
5
-
6
- ##
7
- ## Simple routines for loading / saving an ORG file.
8
- ##
9
-
10
- class Parser
11
-
12
- # All of the lines of the orgmode file
13
- attr_reader :lines
14
-
15
- # All of the headlines in the org file
16
- attr_reader :headlines
17
-
18
- # These are any lines before the first headline
19
- attr_reader :header_lines
20
-
21
- # This contains any in-buffer settings from the org-mode file.
22
- # See http://orgmode.org/manual/In_002dbuffer-settings.html#In_002dbuffer-settings
23
- attr_reader :in_buffer_settings
24
-
25
- # This contains in-buffer options; a special case of in-buffer settings.
26
- attr_reader :options
27
-
28
- # Array of custom keywords.
29
- attr_reader :custom_keywords
30
-
31
- # Regexp that recognizes words in custom_keywords.
32
- def custom_keyword_regexp
33
- return nil if @custom_keywords.empty?
34
- Regexp.new("^(#{@custom_keywords.join('|')})\$")
35
- end
36
-
37
- # A set of tags that, if present on any headlines in the org-file, means
38
- # only those headings will get exported.
39
- def export_select_tags
40
- return Array.new unless @in_buffer_settings["EXPORT_SELECT_TAGS"]
41
- @in_buffer_settings["EXPORT_SELECT_TAGS"].split
42
- end
43
-
44
- # A set of tags that, if present on any headlines in the org-file, means
45
- # that subtree will not get exported.
46
- def export_exclude_tags
47
- return Array.new unless @in_buffer_settings["EXPORT_EXCLUDE_TAGS"]
48
- @in_buffer_settings["EXPORT_EXCLUDE_TAGS"].split
49
- end
50
-
51
- # Returns true if we are to export todo keywords on headings.
52
- def export_todo?
53
- "t" == @options["todo"]
54
- end
55
-
56
- # This stack is used to do proper outline numbering of headlines.
57
- attr_accessor :headline_number_stack
58
-
59
- # Returns true if we are to export heading numbers.
60
- def export_heading_number?
61
- "t" == @options["num"]
62
- end
63
-
64
- # Should we skip exporting text before the first heading?
65
- def skip_header_lines?
66
- "t" == @options["skip"]
67
- end
68
-
69
- # Should we export tables? Defaults to true, must be overridden
70
- # with an explicit "nil"
71
- def export_tables?
72
- "nil" != @options["|"]
73
- end
74
-
75
- # Gets the next headline number for a given level. The intent is
76
- # this function is called sequentially for each headline that
77
- # needs to get numbered. It does standard outline numbering.
78
- def get_next_headline_number(level)
79
- raise "Headline level not valid: #{level}" if level <= 0
80
- while level > @headline_number_stack.length do
81
- @headline_number_stack.push 0
82
- end
83
- while level < @headline_number_stack.length do
84
- @headline_number_stack.pop
85
- end
86
- raise "Oops, shouldn't happen" unless level == @headline_number_stack.length
87
- @headline_number_stack[@headline_number_stack.length - 1] += 1
88
- @headline_number_stack.join(".")
89
- end
90
-
91
- # I can construct a parser object either with an array of lines
92
- # or with a single string that I will split along \n boundaries.
93
- def initialize(lines)
94
- if lines.is_a? Array then
95
- @lines = lines
96
- elsif lines.is_a? String then
97
- @lines = lines.split("\n")
98
- else
99
- raise "Unsupported type for +lines+: #{lines.class}"
100
- end
101
-
102
- @custom_keywords = []
103
- @headlines = Array.new
104
- @current_headline = nil
105
- @header_lines = []
106
- @in_buffer_settings = { }
107
- @headline_number_stack = []
108
- @options = { }
109
- mode = :normal
110
- previous_line = nil
111
- @lines.each do |line|
112
- case mode
113
- when :normal
114
-
115
- if (Headline.headline? line) then
116
- @current_headline = Headline.new line, self
117
- @headlines << @current_headline
118
- else
119
- line = Line.new line, self
120
- # If there is a setting on this line, remember it.
121
- line.in_buffer_setting? do |key, value|
122
- store_in_buffer_setting key, value
123
- end
124
- if line.table_separator? then
125
- previous_line.assigned_paragraph_type = :table_header if previous_line and previous_line.paragraph_type == :table_row
126
- end
127
- mode = :code if line.begin_block? and line.block_type == "EXAMPLE"
128
- if (@current_headline) then
129
- @current_headline.body_lines << line
130
- else
131
- @header_lines << line
132
- end
133
- end
134
-
135
- when :code
136
-
137
- # As long as we stay in code mode, force lines to be either blank or paragraphs.
138
- # Don't try to interpret structural items, like headings and tables.
139
- line = Line.new line
140
- if line.end_block? and line.block_type == "EXAMPLE"
141
- mode = :normal
142
- else
143
- line.assigned_paragraph_type = :paragraph unless line.blank?
144
- end
145
- if (@current_headline) then
146
- @current_headline.body_lines << line
147
- else
148
- @header_lines << line
149
- end
150
- end # case
151
- previous_line = line
152
- end # @lines.each
153
- end # initialize
154
-
155
- # Creates a new parser from the data in a given file
156
- def self.load(fname)
157
- lines = IO.readlines(fname)
158
- return self.new(lines)
159
- end
160
-
161
- # Saves the loaded orgmode file as a textile file.
162
- def to_textile
163
- output = ""
164
- output << Line.to_textile(@header_lines)
165
- @headlines.each do |headline|
166
- output << headline.to_textile
167
- end
168
- output
169
- end
170
-
171
- # Converts the loaded org-mode file to HTML.
172
- def to_html
173
- mark_trees_for_export
174
- @headline_number_stack = []
175
- export_options = { }
176
- export_options[:skip_tables] = true if not export_tables?
177
- output = ""
178
- if @in_buffer_settings["TITLE"] then
179
- output << "<p class=\"title\">#{@in_buffer_settings["TITLE"]}</p>\n"
180
- else
181
- export_options[:decorate_title] = true
182
- end
183
- output << Line.to_html(@header_lines, export_options) unless skip_header_lines?
184
-
185
- # If we've output anything at all, remove the :decorate_title option.
186
- export_options.delete(:decorate_title) if (output.length > 0)
187
- @headlines.each do |headline|
188
- output << headline.to_html(export_options)
189
- export_options.delete(:decorate_title) if (output.length > 0)
190
- end
191
- rp = RubyPants.new(output)
192
- rp.to_html
193
- end
194
-
195
- ######################################################################
196
- private
197
-
198
- # Uses export_select_tags and export_exclude_tags to determine
199
- # which parts of the org-file to export.
200
- def mark_trees_for_export
201
- marked_any = false
202
- # cache the tags
203
- select = export_select_tags
204
- exclude = export_exclude_tags
205
- inherit_export_level = nil
206
- ancestor_stack = []
207
-
208
- # First pass: See if any headlines are explicitly selected
209
- @headlines.each do |headline|
210
- ancestor_stack.pop while not ancestor_stack.empty? and headline.level <= ancestor_stack.last.level
211
- if inherit_export_level and headline.level > inherit_export_level
212
- headline.export_state = :all
213
- else
214
- inherit_export_level = nil
215
- headline.tags.each do |tag|
216
- if (select.include? tag) then
217
- marked_any = true
218
- headline.export_state = :all
219
- ancestor_stack.each { |a| a.export_state = :headline_only unless a.export_state == :all }
220
- inherit_export_level = headline.level
221
- end
222
- end
223
- end
224
- ancestor_stack.push headline
225
- end
226
-
227
- # If nothing was selected, then EVERYTHING is selected.
228
- @headlines.each { |h| h.export_state = :all } unless marked_any
229
-
230
- # Second pass. Look for things that should be excluded, and get rid of them.
231
- @headlines.each do |headline|
232
- if inherit_export_level and headline.level > inherit_export_level
233
- headline.export_state = :exclude
234
- else
235
- inherit_export_level = nil
236
- headline.tags.each do |tag|
237
- if (exclude.include? tag) then
238
- headline.export_state = :exclude
239
- inherit_export_level = headline.level
240
- end
241
- end
242
- end
243
- end
244
- end
245
-
246
- # Stores an in-buffer setting.
247
- def store_in_buffer_setting(key, value)
248
- if key == "OPTIONS" then
249
-
250
- # Options are stored in a hash. Special-case.
251
-
252
- value.split.each do |opt|
253
- if opt =~ /^(.*):(.*?)$/ then
254
- @options[$1] = $2
255
- else
256
- raise "Unexpected option: #{opt}"
257
- end
258
- end
259
- elsif key =~ /^(TODO|SEQ_TODO|TYP_TODO)$/ then
260
- # Handle todo keywords specially.
261
- value.split.each do |keyword|
262
- keyword.gsub!(/\(.*\)/, "") # Get rid of any parenthetical notes
263
- keyword = Regexp.escape(keyword)
264
- next if keyword == "\\|" # Special character in the todo format, not really a keyword
265
- @custom_keywords << keyword
266
- end
267
- else
268
- @in_buffer_settings[key] = value
269
- end
270
- end
271
- end # class Parser
272
- end # module Orgmode
1
+ require 'rubygems'
2
+ require 'rubypants'
3
+
4
+ module Orgmode
5
+
6
+ ##
7
+ ## Simple routines for loading / saving an ORG file.
8
+ ##
9
+
10
+ class Parser
11
+
12
+ # All of the lines of the orgmode file
13
+ attr_reader :lines
14
+
15
+ # All of the headlines in the org file
16
+ attr_reader :headlines
17
+
18
+ # These are any lines before the first headline
19
+ attr_reader :header_lines
20
+
21
+ # This contains any in-buffer settings from the org-mode file.
22
+ # See http://orgmode.org/manual/In_002dbuffer-settings.html#In_002dbuffer-settings
23
+ attr_reader :in_buffer_settings
24
+
25
+ # This contains in-buffer options; a special case of in-buffer settings.
26
+ attr_reader :options
27
+
28
+ # Array of custom keywords.
29
+ attr_reader :custom_keywords
30
+
31
+ # Regexp that recognizes words in custom_keywords.
32
+ def custom_keyword_regexp
33
+ return nil if @custom_keywords.empty?
34
+ Regexp.new("^(#{@custom_keywords.join('|')})\$")
35
+ end
36
+
37
+ # A set of tags that, if present on any headlines in the org-file, means
38
+ # only those headings will get exported.
39
+ def export_select_tags
40
+ return Array.new unless @in_buffer_settings["EXPORT_SELECT_TAGS"]
41
+ @in_buffer_settings["EXPORT_SELECT_TAGS"].split
42
+ end
43
+
44
+ # A set of tags that, if present on any headlines in the org-file, means
45
+ # that subtree will not get exported.
46
+ def export_exclude_tags
47
+ return Array.new unless @in_buffer_settings["EXPORT_EXCLUDE_TAGS"]
48
+ @in_buffer_settings["EXPORT_EXCLUDE_TAGS"].split
49
+ end
50
+
51
+ # Returns true if we are to export todo keywords on headings.
52
+ def export_todo?
53
+ "t" == @options["todo"]
54
+ end
55
+
56
+ # Returns true if we are to export heading numbers.
57
+ def export_heading_number?
58
+ "t" == @options["num"]
59
+ end
60
+
61
+ # Should we skip exporting text before the first heading?
62
+ def skip_header_lines?
63
+ "t" == @options["skip"]
64
+ end
65
+
66
+ # Should we export tables? Defaults to true, must be overridden
67
+ # with an explicit "nil"
68
+ def export_tables?
69
+ "nil" != @options["|"]
70
+ end
71
+
72
+ # I can construct a parser object either with an array of lines
73
+ # or with a single string that I will split along \n boundaries.
74
+ def initialize(lines)
75
+ if lines.is_a? Array then
76
+ @lines = lines
77
+ elsif lines.is_a? String then
78
+ @lines = lines.split("\n")
79
+ else
80
+ raise "Unsupported type for +lines+: #{lines.class}"
81
+ end
82
+
83
+ @custom_keywords = []
84
+ @headlines = Array.new
85
+ @current_headline = nil
86
+ @header_lines = []
87
+ @in_buffer_settings = { }
88
+ @options = { }
89
+ mode = :normal
90
+ previous_line = nil
91
+ @lines.each do |line|
92
+ case mode
93
+ when :normal
94
+
95
+ if (Headline.headline? line) then
96
+ @current_headline = Headline.new line, self
97
+ @headlines << @current_headline
98
+ else
99
+ line = Line.new line, self
100
+ # If there is a setting on this line, remember it.
101
+ line.in_buffer_setting? do |key, value|
102
+ store_in_buffer_setting key, value
103
+ end
104
+ if line.table_separator? then
105
+ previous_line.assigned_paragraph_type = :table_header if previous_line and previous_line.paragraph_type == :table_row
106
+ end
107
+ mode = :code if line.begin_block? and line.block_type == "EXAMPLE"
108
+ if (@current_headline) then
109
+ @current_headline.body_lines << line
110
+ else
111
+ @header_lines << line
112
+ end
113
+ end
114
+
115
+ when :code
116
+
117
+ # As long as we stay in code mode, force lines to be either blank or paragraphs.
118
+ # Don't try to interpret structural items, like headings and tables.
119
+ line = Line.new line, self
120
+ if line.end_block? and line.block_type == "EXAMPLE"
121
+ mode = :normal
122
+ else
123
+ line.assigned_paragraph_type = :paragraph unless line.blank?
124
+ end
125
+ if (@current_headline) then
126
+ @current_headline.body_lines << line
127
+ else
128
+ @header_lines << line
129
+ end
130
+ end # case
131
+ previous_line = line
132
+ end # @lines.each
133
+ end # initialize
134
+
135
+ # Creates a new parser from the data in a given file
136
+ def self.load(fname)
137
+ lines = IO.readlines(fname)
138
+ return self.new(lines)
139
+ end
140
+
141
+ # Saves the loaded orgmode file as a textile file.
142
+ def to_textile
143
+ output = ""
144
+ output << Line.to_textile(@header_lines)
145
+ @headlines.each do |headline|
146
+ output << headline.to_textile
147
+ end
148
+ output
149
+ end
150
+
151
+ # Converts the loaded org-mode file to HTML.
152
+ def to_html
153
+ mark_trees_for_export
154
+ export_options = {
155
+ :decorate_title => true,
156
+ :export_heading_number => export_heading_number?,
157
+ :export_todo => export_todo?
158
+ }
159
+ export_options[:skip_tables] = true if not export_tables?
160
+ output = ""
161
+ output_buffer = HtmlOutputBuffer.new(output, export_options)
162
+
163
+ if @in_buffer_settings["TITLE"] then
164
+
165
+ # If we're given a new title, then just create a new line
166
+ # for that title.
167
+ title = Line.new(@in_buffer_settings["TITLE"], self)
168
+ Parser.translate([title], output_buffer)
169
+ end
170
+ Parser.translate(@header_lines, output_buffer) unless skip_header_lines?
171
+
172
+ # If we've output anything at all, remove the :decorate_title option.
173
+ export_options.delete(:decorate_title) if (output.length > 0)
174
+ @headlines.each do |headline|
175
+ next if headline.export_state == :exclude
176
+ case headline.export_state
177
+ when :exclude
178
+ # NOTHING
179
+ when :headline_only
180
+ Parser.translate(headline.body_lines[0, 1], output_buffer)
181
+ when :all
182
+ Parser.translate(headline.body_lines, output_buffer)
183
+ end
184
+ end
185
+ rp = RubyPants.new(output)
186
+ rp.to_html
187
+ end
188
+
189
+ ######################################################################
190
+ private
191
+
192
+ # Converts an array of lines to the appropriate format.
193
+ # Writes the output to +output_buffer+.
194
+ def self.translate(lines, output_buffer)
195
+ lines.each do |line|
196
+
197
+ # See if we're carrying paragraph payload, and output
198
+ # it if we're about to switch to some other output type.
199
+ output_buffer.prepare(line)
200
+
201
+ case line.paragraph_type
202
+ when :metadata, :table_separator, :blank
203
+
204
+ output_buffer << line.line if output_buffer.preserve_whitespace?
205
+
206
+ when :comment
207
+
208
+ if line.begin_block?
209
+ output_buffer.push_mode(:blockquote) if line.block_type == "QUOTE"
210
+ output_buffer.push_mode(:src) if line.block_type == "SRC"
211
+ output_buffer.push_mode(:example) if line.block_type == "EXAMPLE"
212
+ elsif line.end_block?
213
+ output_buffer.pop_mode(:blockquote) if line.block_type == "QUOTE"
214
+ output_buffer.pop_mode(:src) if line.block_type == "SRC"
215
+ output_buffer.pop_mode(:example) if line.block_type == "EXAMPLE"
216
+ else
217
+ output_buffer << line.line if output_buffer.preserve_whitespace?
218
+ end
219
+
220
+ when :table_row, :table_header
221
+
222
+ output_buffer << line.line.lstrip
223
+
224
+ when :unordered_list, :ordered_list
225
+
226
+ output_buffer << line.output_text << " "
227
+
228
+ when :inline_example
229
+
230
+ output_buffer << line.output_text
231
+
232
+ else
233
+
234
+ if output_buffer.preserve_whitespace? then
235
+ output_buffer << line.output_text
236
+ else
237
+ output_buffer << line.output_text.strip << " "
238
+ end
239
+ end
240
+ end
241
+ output_buffer.flush!
242
+ output_buffer.pop_mode until output_buffer.current_mode == :normal
243
+ output_buffer.output
244
+ end
245
+
246
+ # Uses export_select_tags and export_exclude_tags to determine
247
+ # which parts of the org-file to export.
248
+ def mark_trees_for_export
249
+ marked_any = false
250
+ # cache the tags
251
+ select = export_select_tags
252
+ exclude = export_exclude_tags
253
+ inherit_export_level = nil
254
+ ancestor_stack = []
255
+
256
+ # First pass: See if any headlines are explicitly selected
257
+ @headlines.each do |headline|
258
+ ancestor_stack.pop while not ancestor_stack.empty? and headline.level <= ancestor_stack.last.level
259
+ if inherit_export_level and headline.level > inherit_export_level
260
+ headline.export_state = :all
261
+ else
262
+ inherit_export_level = nil
263
+ headline.tags.each do |tag|
264
+ if (select.include? tag) then
265
+ marked_any = true
266
+ headline.export_state = :all
267
+ ancestor_stack.each { |a| a.export_state = :headline_only unless a.export_state == :all }
268
+ inherit_export_level = headline.level
269
+ end
270
+ end
271
+ end
272
+ ancestor_stack.push headline
273
+ end
274
+
275
+ # If nothing was selected, then EVERYTHING is selected.
276
+ @headlines.each { |h| h.export_state = :all } unless marked_any
277
+
278
+ # Second pass. Look for things that should be excluded, and get rid of them.
279
+ @headlines.each do |headline|
280
+ if inherit_export_level and headline.level > inherit_export_level
281
+ headline.export_state = :exclude
282
+ else
283
+ inherit_export_level = nil
284
+ headline.tags.each do |tag|
285
+ if (exclude.include? tag) then
286
+ headline.export_state = :exclude
287
+ inherit_export_level = headline.level
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+ # Stores an in-buffer setting.
295
+ def store_in_buffer_setting(key, value)
296
+ if key == "OPTIONS" then
297
+
298
+ # Options are stored in a hash. Special-case.
299
+
300
+ value.split.each do |opt|
301
+ if opt =~ /^(.*):(.*?)$/ then
302
+ @options[$1] = $2
303
+ else
304
+ raise "Unexpected option: #{opt}"
305
+ end
306
+ end
307
+ elsif key =~ /^(TODO|SEQ_TODO|TYP_TODO)$/ then
308
+ # Handle todo keywords specially.
309
+ value.split.each do |keyword|
310
+ keyword.gsub!(/\(.*\)/, "") # Get rid of any parenthetical notes
311
+ keyword = Regexp.escape(keyword)
312
+ next if keyword == "\\|" # Special character in the todo format, not really a keyword
313
+ @custom_keywords << keyword
314
+ end
315
+ else
316
+ @in_buffer_settings[key] = value
317
+ end
318
+ end
319
+ end # class Parser
320
+ end # module Orgmode