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.
- data/History.txt +81 -77
- data/README.rdoc +66 -66
- data/Rakefile +28 -28
- data/bin/org-ruby +40 -40
- data/lib/org-ruby.rb +50 -50
- data/lib/org-ruby/headline.rb +102 -120
- data/lib/org-ruby/html_output_buffer.rb +174 -156
- data/lib/org-ruby/line.rb +206 -260
- data/lib/org-ruby/output_buffer.rb +227 -191
- data/lib/org-ruby/parser.rb +320 -272
- data/lib/org-ruby/regexp_helper.rb +156 -156
- data/lib/org-ruby/textile_output_buffer.rb +67 -68
- data/spec/data/freeform-example.org +113 -113
- data/spec/data/freeform.org +111 -111
- data/spec/data/hyp-planning.org +335 -335
- data/spec/data/remember.org +53 -53
- data/spec/headline_spec.rb +55 -55
- data/spec/html_examples/advanced-code.html +36 -36
- data/spec/html_examples/advanced-code.org +53 -53
- data/spec/html_examples/advanced-lists.html +31 -31
- data/spec/html_examples/advanced-lists.org +31 -31
- data/spec/html_examples/block_code.html +28 -28
- data/spec/html_examples/block_code.org +35 -35
- data/spec/html_examples/blockquote.html +7 -7
- data/spec/html_examples/blockquote.org +13 -13
- data/spec/html_examples/code-comment.html +18 -18
- data/spec/html_examples/code-comment.org +22 -22
- data/spec/html_examples/custom-seq-todo.html +15 -15
- data/spec/html_examples/custom-seq-todo.org +24 -24
- data/spec/html_examples/custom-todo.html +15 -15
- data/spec/html_examples/custom-todo.org +24 -24
- data/spec/html_examples/custom-typ-todo.html +15 -15
- data/spec/html_examples/custom-typ-todo.org +24 -24
- data/spec/html_examples/entities.html +4 -4
- data/spec/html_examples/entities.org +11 -11
- data/spec/html_examples/escape-pre.html +6 -6
- data/spec/html_examples/escape-pre.org +6 -6
- data/spec/html_examples/export-exclude-only.html +13 -13
- data/spec/html_examples/export-exclude-only.org +81 -81
- data/spec/html_examples/export-keywords.html +4 -4
- data/spec/html_examples/export-keywords.org +18 -18
- data/spec/html_examples/export-tags.html +8 -8
- data/spec/html_examples/export-tags.org +82 -82
- data/spec/html_examples/export-title.html +2 -2
- data/spec/html_examples/export-title.org +4 -4
- data/spec/html_examples/html-literal.html +2 -2
- data/spec/html_examples/html-literal.org +6 -6
- data/spec/html_examples/inline-formatting.html +10 -10
- data/spec/html_examples/inline-formatting.org +17 -17
- data/spec/html_examples/link-features.html +8 -8
- data/spec/html_examples/link-features.org +19 -19
- data/spec/html_examples/lists.html +19 -19
- data/spec/html_examples/lists.org +36 -36
- data/spec/html_examples/metadata-comment.html +27 -27
- data/spec/html_examples/metadata-comment.org +30 -30
- data/spec/html_examples/only-list.html +5 -5
- data/spec/html_examples/only-list.org +3 -3
- data/spec/html_examples/only-table.html +6 -6
- data/spec/html_examples/only-table.org +5 -5
- data/spec/html_examples/skip-header.html +3 -3
- data/spec/html_examples/skip-header.org +28 -28
- data/spec/html_examples/skip-table.html +4 -4
- data/spec/html_examples/skip-table.org +19 -19
- data/spec/html_examples/tables.html +20 -20
- data/spec/html_examples/tables.org +26 -26
- data/spec/html_examples/text.html +2 -2
- data/spec/html_examples/text.org +16 -16
- data/spec/line_spec.rb +151 -151
- data/spec/output_buffer_spec.rb +19 -0
- data/spec/parser_spec.rb +152 -166
- data/spec/regexp_helper_spec.rb +57 -57
- data/spec/spec_helper.rb +21 -21
- data/spec/textile_examples/block_code.org +35 -35
- data/spec/textile_examples/block_code.textile +29 -29
- data/spec/textile_examples/blockquote.org +13 -13
- data/spec/textile_examples/blockquote.textile +11 -11
- data/spec/textile_examples/keywords.org +13 -13
- data/spec/textile_examples/keywords.textile +11 -11
- data/spec/textile_examples/links.org +11 -11
- data/spec/textile_examples/links.textile +10 -10
- data/spec/textile_examples/lists.org +36 -36
- data/spec/textile_examples/lists.textile +20 -20
- data/spec/textile_examples/single-space-plain-list.org +13 -13
- data/spec/textile_examples/single-space-plain-list.textile +10 -10
- data/spec/textile_examples/tables.org +26 -26
- data/spec/textile_examples/tables.textile +23 -23
- data/spec/textile_output_buffer_spec.rb +21 -21
- data/tasks/test_case.rake +49 -49
- 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
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
@
|
36
|
-
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
@mode_stack.
|
61
|
-
end
|
62
|
-
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
#
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
def
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
#
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
data/lib/org-ruby/parser.rb
CHANGED
@@ -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
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
@
|
88
|
-
@
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
@
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
end
|
245
|
-
|
246
|
-
#
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|