org-ruby 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. data/History.txt +18 -0
  2. data/lib/org-ruby.rb +1 -1
  3. data/lib/org-ruby/headline.rb +49 -9
  4. data/lib/org-ruby/html_output_buffer.rb +37 -7
  5. data/lib/org-ruby/line.rb +34 -3
  6. data/lib/org-ruby/output_buffer.rb +2 -2
  7. data/lib/org-ruby/parser.rb +180 -10
  8. data/spec/data/freeform-example.org +113 -0
  9. data/spec/html_examples/custom-seq-todo.html +15 -0
  10. data/spec/html_examples/custom-seq-todo.org +24 -0
  11. data/spec/html_examples/custom-todo.html +15 -0
  12. data/spec/html_examples/custom-todo.org +24 -0
  13. data/spec/html_examples/custom-typ-todo.html +15 -0
  14. data/spec/html_examples/custom-typ-todo.org +24 -0
  15. data/spec/html_examples/entities.html +1 -0
  16. data/spec/html_examples/entities.org +3 -0
  17. data/spec/html_examples/export-exclude-only.html +13 -0
  18. data/spec/html_examples/export-exclude-only.org +81 -0
  19. data/spec/html_examples/export-keywords.html +4 -0
  20. data/spec/html_examples/export-keywords.org +18 -0
  21. data/spec/html_examples/export-tags.html +8 -0
  22. data/spec/html_examples/export-tags.org +82 -0
  23. data/spec/html_examples/export-title.html +2 -0
  24. data/spec/html_examples/export-title.org +4 -0
  25. data/spec/html_examples/link-features.html +8 -0
  26. data/spec/html_examples/link-features.org +19 -0
  27. data/spec/html_examples/only-table.html +1 -1
  28. data/spec/html_examples/skip-header.html +3 -0
  29. data/spec/html_examples/skip-header.org +28 -0
  30. data/spec/html_examples/skip-table.html +4 -0
  31. data/spec/html_examples/skip-table.org +19 -0
  32. data/spec/html_examples/tables.html +1 -1
  33. data/spec/line_spec.rb +35 -0
  34. data/spec/parser_spec.rb +94 -14
  35. data/spec/spec_helper.rb +1 -0
  36. metadata +23 -2
@@ -1,3 +1,21 @@
1
+ == 0.5.0 / 2009-12-29
2
+
3
+ * Parse (but not necessarily *use*) in-buffer settings. The following
4
+ in-buffer settings *are* used:
5
+ * Understand the #+TITLE: directive.
6
+ * Exporting todo keywords (option todo:t)
7
+ * Numbering headlines (option num:t)
8
+ * Skipping text before the first headline (option skip:t)
9
+ * Skipping tables (option |:nil)
10
+ * Custom todo keywords
11
+ * EXPORT_SELECT_TAGS and EXPORT_EXLUDE_TAGS for controlling parts of
12
+ the tree to export
13
+ * Rewrite "file:(blah).org" links to "http:(blah).html" links. This
14
+ makes the inter-links to other org-mode files work.
15
+ * Uses <th> tags inside table rows that precede table separators.
16
+ * Bugfixes:
17
+ * Headings now have HTML escaped.
18
+
1
19
  == 0.4.2 / 2009-12-29
2
20
 
3
21
  * Got rid of the extraneous newline at the start of code blocks.
@@ -3,7 +3,7 @@ unless defined? ::OrgRuby
3
3
  module OrgRuby
4
4
 
5
5
  # :stopdoc:
6
- VERSION = '0.4.2'
6
+ VERSION = '0.5.0'
7
7
  LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
8
8
  PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
9
9
  # :startdoc:
@@ -21,6 +21,15 @@ module Orgmode
21
21
  # Optional keyword found at the beginning of the headline.
22
22
  attr_reader :keyword
23
23
 
24
+ # Valid states for partial export.
25
+ # exclude:: The entire subtree from this heading should be excluded.
26
+ # headline_only:: The headline should be exported, but not the body.
27
+ # all:: Everything should be exported, headline/body/children.
28
+ ValidExportStates = [:exclude, :headline_only, :all]
29
+
30
+ # The export state of this headline. See +ValidExportStates+.
31
+ attr_accessor :export_state
32
+
24
33
  # This is the regex that matches a line
25
34
  LineRegexp = /^\*+\s+/
26
35
 
@@ -30,12 +39,13 @@ module Orgmode
30
39
  # Special keywords allowed at the start of a line.
31
40
  Keywords = %w[TODO DONE]
32
41
 
33
- KeywordsRegexp = Regexp.new("\\s*(#{Keywords.join('|')})\\s*")
42
+ KeywordsRegexp = Regexp.new("^(#{Keywords.join('|')})\$")
34
43
 
35
- def initialize(line)
36
- super(line)
44
+ def initialize(line, parser = nil)
45
+ super(line, parser)
37
46
  @body_lines = []
38
47
  @tags = []
48
+ @export_state = :exclude
39
49
  if (@line =~ LineRegexp) then
40
50
  @level = $&.strip.length
41
51
  @headline_text = $'.strip
@@ -44,10 +54,8 @@ module Orgmode
44
54
  @tags.delete_at(0) # the first item will be empty; discard
45
55
  @headline_text.gsub!(TagsRegexp, "") # Removes the tags from the headline
46
56
  end
47
- if (@headline_text =~ KeywordsRegexp) then
48
- @headline_text = $'
49
- @keyword = $1
50
- end
57
+ @keyword = nil
58
+ parse_keywords
51
59
  else
52
60
  raise "'#{line}' is not a valid headline"
53
61
  end
@@ -67,14 +75,46 @@ module Orgmode
67
75
  end
68
76
 
69
77
  def to_html(opts = {})
78
+ return "" if @export_state == :exclude
70
79
  if opts[:decorate_title]
71
80
  decoration = " class=\"title\""
81
+ opts.delete(:decorate_title)
72
82
  else
73
83
  decoration = ""
74
84
  end
75
- output = "<h#{@level}#{decoration}>#{@headline_text}</h#{@level}>\n"
76
- output << Line.to_html(@body_lines)
85
+ output = "<h#{@level}#{decoration}>"
86
+ if @parser and @parser.export_heading_number? then
87
+ heading_number = @parser.get_next_headline_number(@level)
88
+ output << "<span class=\"heading-number heading-number-#{@level}\">#{heading_number} </span>"
89
+ end
90
+ if @parser and @parser.export_todo? and @keyword then
91
+ output << "<span class=\"todo-keyword #{@keyword}\">#{@keyword} </span>"
92
+ end
93
+ output << "#{escape(@headline_text)}</h#{@level}>\n"
94
+ return output if @export_state == :headline_only
95
+ output << Line.to_html(@body_lines, opts)
77
96
  output
78
97
  end
98
+
99
+ ######################################################################
100
+ private
101
+
102
+ # TODO 2009-12-29 This duplicates escape_buffer! in html_output_buffer. DRY.
103
+ def escape(str)
104
+ str = str.gsub(/&/, "&amp;")
105
+ str = str.gsub(/</, "&lt;")
106
+ str = str.gsub(/>/, "&gt;")
107
+ str
108
+ end
109
+
110
+ def parse_keywords
111
+ re = @parser.custom_keyword_regexp if @parser
112
+ re ||= KeywordsRegexp
113
+ words = @headline_text.split
114
+ if words.length > 0 && words[0] =~ re then
115
+ @keyword = words[0]
116
+ @headline_text.sub!(Regexp.new("^#{@keyword}\s*"), "")
117
+ end
118
+ end
79
119
  end # class Headline
80
120
  end # class Orgmode
@@ -8,7 +8,8 @@ module Orgmode
8
8
  :paragraph => "p",
9
9
  :ordered_list => "li",
10
10
  :unordered_list => "li",
11
- :table_row => "tr"
11
+ :table_row => "tr",
12
+ :table_header => "tr"
12
13
  }
13
14
 
14
15
  ModeTag = {
@@ -19,6 +20,8 @@ module Orgmode
19
20
  :code => "pre"
20
21
  }
21
22
 
23
+ attr_reader :options
24
+
22
25
  def initialize(output, opts = {})
23
26
  super(output)
24
27
  if opts[:decorate_title] then
@@ -26,13 +29,15 @@ module Orgmode
26
29
  else
27
30
  @title_decoration = ""
28
31
  end
32
+ @options = opts
33
+ @logger.debug "HTML export options: #{@options.inspect}"
29
34
  end
30
35
 
31
36
  def push_mode(mode)
32
37
  if ModeTag[mode] then
33
38
  output_indentation
34
39
  @logger.debug "<#{ModeTag[mode]}>\n"
35
- @output << "<#{ModeTag[mode]}>\n"
40
+ @output << "<#{ModeTag[mode]}>\n" unless mode == :table and skip_tables?
36
41
  # Entering a new mode obliterates the title decoration
37
42
  @title_decoration = ""
38
43
  end
@@ -44,7 +49,7 @@ module Orgmode
44
49
  if ModeTag[m] then
45
50
  output_indentation
46
51
  @logger.debug "</#{ModeTag[m]}>\n"
47
- @output << "</#{ModeTag[m]}>\n"
52
+ @output << "</#{ModeTag[m]}>\n" unless mode == :table and skip_tables?
48
53
  end
49
54
  end
50
55
 
@@ -60,12 +65,16 @@ module Orgmode
60
65
  @output << @buffer << "\n"
61
66
  else
62
67
  if (@buffer.length > 0) then
63
- @logger.debug "FLUSH ==========> #{@output_type}"
64
- output_indentation
65
- @output << "<#{HtmlBlockTag[@output_type]}#{@title_decoration}>" \
68
+ unless buffer_mode_is_table? and skip_tables?
69
+ @logger.debug "FLUSH ==========> #{@buffer_mode}"
70
+ output_indentation
71
+ @output << "<#{HtmlBlockTag[@output_type]}#{@title_decoration}>" \
66
72
  << inline_formatting(@buffer) \
67
73
  << "</#{HtmlBlockTag[@output_type]}>\n"
68
- @title_decoration = ""
74
+ @title_decoration = ""
75
+ else
76
+ @logger.debug "SKIP ==========> #{@buffer_mode}"
77
+ end
69
78
  end
70
79
  end
71
80
  @buffer = ""
@@ -75,6 +84,14 @@ module Orgmode
75
84
  ######################################################################
76
85
  private
77
86
 
87
+ def skip_tables?
88
+ @options[:skip_tables]
89
+ end
90
+
91
+ def buffer_mode_is_table?
92
+ @buffer_mode == :table
93
+ end
94
+
78
95
  # Escapes any HTML content in the output accumulation buffer @buffer.
79
96
  def escape_buffer!
80
97
  @buffer.gsub!(/&/, "&amp;")
@@ -105,6 +122,14 @@ module Orgmode
105
122
  end
106
123
  str = @re_help.rewrite_links(str) do |link, text|
107
124
  text ||= link
125
+ link = link.sub(/^file:(.*)::(.*?)$/) do
126
+
127
+ # We don't support search links right now. Get rid of it.
128
+
129
+ "file:#{$1}"
130
+ end
131
+ link = link.sub(/^file:/i, "") # will default to HTTP
132
+ link = link.sub(/\.org$/i, ".html")
108
133
  "<a href=\"#{link}\">#{text}</a>"
109
134
  end
110
135
  if (@output_type == :table_row) then
@@ -112,6 +137,11 @@ module Orgmode
112
137
  str.gsub!(/\s*\|$/, "</td>")
113
138
  str.gsub!(/\s*\|\s*/, "</td><td>")
114
139
  end
140
+ if (@output_type == :table_header) then
141
+ str.gsub!(/^\|\s*/, "<th>")
142
+ str.gsub!(/\s*\|$/, "</th>")
143
+ str.gsub!(/\s*\|\s*/, "</th><th>")
144
+ end
115
145
  str
116
146
  end
117
147
 
@@ -11,6 +11,9 @@ module Orgmode
11
11
  # TODO 2009-12-20 bdewey: Handle tabs
12
12
  attr_reader :indent
13
13
 
14
+ # Backpointer to the parser that owns this line.
15
+ attr_reader :parser
16
+
14
17
  # A line can have its type assigned instead of inferred from its
15
18
  # content. For example, something that parses as a "table" on its
16
19
  # own ("| one | two|\n") may just be a paragraph if it's inside
@@ -18,7 +21,8 @@ module Orgmode
18
21
  # type. This will then affect the value of +paragraph_type+.
19
22
  attr_accessor :assigned_paragraph_type
20
23
 
21
- def initialize(line)
24
+ def initialize(line, parser = nil)
25
+ @parser = parser
22
26
  @line = line
23
27
  @indent = 0
24
28
  @line =~ /\s*/
@@ -90,8 +94,13 @@ module Orgmode
90
94
  check_assignment_or_regexp(:table_separator, /^\s*\|[-\|\+]*\s*$/)
91
95
  end
92
96
 
97
+ # Checks if this line is a table header.
98
+ def table_header?
99
+ @assigned_paragraph_type == :table_header
100
+ end
101
+
93
102
  def table?
94
- table_row? or table_separator?
103
+ table_row? or table_separator? or table_header?
95
104
  end
96
105
 
97
106
  BlockRegexp = /^\s*#\+(BEGIN|END)_(\w*)/
@@ -108,6 +117,27 @@ module Orgmode
108
117
  $2 if @line =~ BlockRegexp
109
118
  end
110
119
 
120
+ InBufferSettingRegexp = /^#\+(\w+):\s*(.*)$/
121
+
122
+ # call-seq:
123
+ # line.in_buffer_setting? => boolean
124
+ # line.in_buffer_setting? { |key, value| ... }
125
+ #
126
+ # Called without a block, this method determines if the line
127
+ # contains an in-buffer setting. Called with a block, the block
128
+ # will get called if the line contains an in-buffer setting with
129
+ # the key and value for the setting.
130
+ def in_buffer_setting?
131
+ return false if @assigned_paragraph_type && @assigned_paragraph_type != :comment
132
+ if block_given? then
133
+ if @line =~ InBufferSettingRegexp
134
+ yield $1, $2
135
+ end
136
+ else
137
+ @line =~ InBufferSettingRegexp
138
+ end
139
+ end
140
+
111
141
  # Determines the paragraph type of the current line.
112
142
  def paragraph_type
113
143
  return :blank if blank?
@@ -117,6 +147,7 @@ module Orgmode
117
147
  return :comment if comment?
118
148
  return :table_separator if table_separator?
119
149
  return :table_row if table_row?
150
+ return :table_header if table_header?
120
151
  return :paragraph
121
152
  end
122
153
 
@@ -157,7 +188,7 @@ module Orgmode
157
188
  output_buffer << line.line if output_buffer.preserve_whitespace?
158
189
  end
159
190
 
160
- when :table_row
191
+ when :table_row, :table_header
161
192
 
162
193
  output_buffer << line.line.lstrip
163
194
 
@@ -81,13 +81,13 @@ module Orgmode
81
81
 
82
82
  # Tests if we are entering a table mode.
83
83
  def enter_table?
84
- ((@output_type == :table_row) || (@output_type == :table_separator)) &&
84
+ ((@output_type == :table_row) || (@output_type == :table_header) || (@output_type == :table_separator)) &&
85
85
  (current_mode != :table)
86
86
  end
87
87
 
88
88
  # Tests if we are existing a table mode.
89
89
  def exit_table?
90
- ((@output_type != :table_row) && (@output_type != :table_separator)) &&
90
+ ((@output_type != :table_row) && (@output_type != :table_header) && (@output_type != :table_separator)) &&
91
91
  (current_mode == :table)
92
92
  end
93
93
 
@@ -17,7 +17,77 @@ module Orgmode
17
17
 
18
18
  # These are any lines before the first headline
19
19
  attr_reader :header_lines
20
-
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
+
21
91
  # I can construct a parser object either with an array of lines
22
92
  # or with a single string that I will split along \n boundaries.
23
93
  def initialize(lines)
@@ -28,20 +98,32 @@ module Orgmode
28
98
  else
29
99
  raise "Unsupported type for +lines+: #{lines.class}"
30
100
  end
31
-
101
+
102
+ @custom_keywords = []
32
103
  @headlines = Array.new
33
104
  @current_headline = nil
34
105
  @header_lines = []
106
+ @in_buffer_settings = { }
107
+ @headline_number_stack = []
108
+ @options = { }
35
109
  mode = :normal
110
+ previous_line = nil
36
111
  @lines.each do |line|
37
112
  case mode
38
113
  when :normal
39
114
 
40
115
  if (Headline.headline? line) then
41
- @current_headline = Headline.new line
116
+ @current_headline = Headline.new line, self
42
117
  @headlines << @current_headline
43
118
  else
44
- line = Line.new line
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
45
127
  mode = :code if line.begin_block? and line.block_type == "EXAMPLE"
46
128
  if (@current_headline) then
47
129
  @current_headline.body_lines << line
@@ -66,7 +148,8 @@ module Orgmode
66
148
  @header_lines << line
67
149
  end
68
150
  end # case
69
- end
151
+ previous_line = line
152
+ end # @lines.each
70
153
  end # initialize
71
154
 
72
155
  # Creates a new parser from the data in a given file
@@ -87,16 +170,103 @@ module Orgmode
87
170
 
88
171
  # Converts the loaded org-mode file to HTML.
89
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?
90
177
  output = ""
91
- decorate = true
92
- output << Line.to_html(@header_lines, :decorate_title => decorate)
93
- decorate = (output.length == 0)
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)
94
187
  @headlines.each do |headline|
95
- output << headline.to_html(:decorate_title => decorate)
96
- decorate = (output.length == 0)
188
+ output << headline.to_html(export_options)
189
+ export_options.delete(:decorate_title) if (output.length > 0)
97
190
  end
98
191
  rp = RubyPants.new(output)
99
192
  rp.to_html
100
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
101
271
  end # class Parser
102
272
  end # module Orgmode