org-ruby 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
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