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.
- 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
|