org-ruby 0.4.2 → 0.5.0

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