org_mode 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .*.sw?
6
+ .rvmrc
7
+ tags
data/Gemfile ADDED
@@ -0,0 +1,26 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in org_mode.gemspec
4
+ gemspec
5
+
6
+ gem "commander"
7
+ gem "facets"
8
+ gem "mustache"
9
+
10
+ group :development do
11
+ if RUBY_VERSION =~ /^1\.9/
12
+ gem "ruby-debug19"
13
+ else
14
+ gem "ruby-debug"
15
+ end
16
+ end
17
+
18
+ group :test do
19
+ gem "rspec", "~> 2.8.0"
20
+ gem "cucumber"
21
+ gem "timecop"
22
+ gem "guard-cucumber"
23
+ gem "popen4"
24
+ end
25
+
26
+
data/README.mkd ADDED
@@ -0,0 +1,30 @@
1
+ # Org-Mode file parser and writer
2
+
3
+ Usin Vim for a long time, and having used e-macs for a while I got hooked on
4
+ org-mode. The flexible plain-tekst planning mode.
5
+
6
+ Using org-mode was an eye-opener, brilliant idea but it turns out I am more of a VIM
7
+ person.
8
+
9
+ So this ruby-gem is result of this personal org-mode dissonance.
10
+
11
+ ## Goal
12
+
13
+ Making lightweight/fast ruby org-mode executable to parse/reformat/analyse and
14
+ report on my org-files or the one in my buffer. And following the Unix philosopy
15
+ making a seperate process of it.
16
+
17
+ ## What I want
18
+
19
+ * flexible parsers
20
+ * flexible writers
21
+ * database storage
22
+ * org-mode website upload
23
+ * well tested module
24
+ * integration with vim
25
+ * flexible reporting
26
+ * remember mode
27
+
28
+ ## Code
29
+
30
+ See: `lib/**/*.rb` for source and `spec/**/*.rb` for specs. Binaries not available yet.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/org-mode ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby -Ilib
2
+
3
+ require 'rubygems'
4
+ require 'commander/import'
5
+ require 'org_mode/version'
6
+
7
+ require 'org_mode/commands'
8
+
9
+ program :version, OrgMode::VERSION
10
+ program :description, 'Formats and extracts information out of org-mode files'
11
+ program :help_formatter, :compact
12
+
13
+ command :agenda do |c|
14
+ c.syntax = 'org-mode agenda [options]'
15
+ c.summary = ''
16
+ c.description = ''
17
+ c.example 'description', 'command example'
18
+ c.option '--some-switch', 'Some switch that does something'
19
+ c.when_called OrgMode::Commands::Agenda, :execute
20
+ end
21
+
22
+ command :update do |c|
23
+ c.syntax = 'Orgy update [options]'
24
+ c.summary = ''
25
+ c.description = ''
26
+ c.example 'description', 'command example'
27
+ c.option '--some-switch', 'Some switch that does something'
28
+ c.action do |args, options|
29
+ # Do something or c.when_called Orgy::Commands::Update
30
+ end
31
+ end
32
+
@@ -0,0 +1,34 @@
1
+ Feature: agenda
2
+ org-mode tools should extract meaningfull information
3
+ out of the parsed org-mode files and present
4
+ it in a nicely correct format.
5
+
6
+ Scenario: should present meaningfull agenda information
7
+ Given date is "1-1-2012 15:00"
8
+ And we have an org-file with the following content
9
+ """
10
+ * TODO Scheduled task <1-1-2012 Wed 15:15>
11
+ """
12
+ When the script is executed on the org-file
13
+ When the script is called with "agenda"
14
+ Then the output should be
15
+ """
16
+ Agenda ()
17
+ 2012-01-01
18
+ TODO Scheduled task
19
+ """
20
+
21
+ # @focus
22
+ # Scenario: should limit restults in a day view
23
+ # Given date is "1-1-2012 15:00"
24
+ # And we have an org-file with the following content
25
+ # """
26
+ # * TODO Todays task <1-1-2012 Wed 15:15>
27
+ # * TODO Tommorrows task <2-1-2012 Wed 15:15>
28
+ # """
29
+ # When the script is called with "agenda today"
30
+ # Then the output should be
31
+ # """
32
+ # Todays activities:
33
+ # TODO Todays task
34
+ # """
@@ -0,0 +1,10 @@
1
+ Feature: Handles errors gracefully
2
+ Incomplete information or missing non-existing
3
+ files should be reported.
4
+
5
+ Scenario: non-existing file
6
+ When the script is called with "agenda /tmp/non-existent.org"
7
+ Then the output should be
8
+ """
9
+ Encountered a little problem: No such file or directory - /tmp/non-existent.org
10
+ """
@@ -0,0 +1,11 @@
1
+ Feature: noparams or reformat
2
+ Whenever the org-mode script is run without params
3
+ it should just display help
4
+
5
+ Scenario: script with no params should display help
6
+ When the script is called with "" should fail
7
+ Then the error should be
8
+ """
9
+ invalid command. Use --help for more information
10
+ """
11
+
@@ -0,0 +1,5 @@
1
+ Feature: todos
2
+ The org-mode tools should lists relevant
3
+ todos in a clear consice fomat.
4
+ It is also easy to extract todo's based on criteria
5
+ such as tags.
data/features/steps.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'timecop'
2
+
3
+ Given /^we have an org\-file with the following content$/ do |string|
4
+ @org_file = Tempfile.new('org_file')
5
+ @org_file.write(string)
6
+ @org_file.flush
7
+ end
8
+
9
+ When /^the script is called with "([^"]*)"( should fail)?$/ do |argv, should_fail|
10
+ org_mode_args = []
11
+ org_mode_args << argv if argv
12
+ org_mode_args << @org_file.path if @org_file
13
+
14
+ begin
15
+ @stdout, = org_mode_script(*org_mode_args)
16
+ rescue OrgModeScriptError => script_error
17
+ raise unless should_fail
18
+ @script_error = script_error
19
+ end
20
+ end
21
+
22
+ Given /^date is "([^"]*)"$/ do |date_string|
23
+ Timecop.freeze(Date.parse(date_string))
24
+ end
25
+
26
+ When /^the script is executed on the org-file$/ do
27
+ @stdout, = org_mode_script(:agenda, @org_file.path)
28
+ end
29
+
30
+ Then /^the output should be$/ do |string|
31
+ @stdout.should == string
32
+ end
33
+ Then /^the error should be$/ do |string|
34
+ @script_error.stderr.chomp.should == string
35
+ end
File without changes
@@ -0,0 +1,53 @@
1
+ require 'tempfile'
2
+ require 'ruby-debug'
3
+ require 'popen4'
4
+
5
+ class OrgModeScriptError < StandardError;
6
+ attr_accessor :stdout, :stderr, :status, :cmd
7
+
8
+ def initialize(stdout, stderr, status, cmd)
9
+ @stdout = stdout
10
+ @stderr = stderr
11
+ @status = status
12
+ @cmd = cmd
13
+ super("Failed command: #{cmd}")
14
+ end
15
+
16
+ def message
17
+ <<-eom.gsub(/^\s{4}/, '')
18
+ Failed command: #{cmd}
19
+ Stderr:
20
+ #{@stderr.empty? ? 'None' : @stderr}
21
+ Stdout:
22
+ #{@stdout.empty? ? 'None' : @stdout}
23
+ eom
24
+ end
25
+
26
+ end
27
+
28
+ # Private: runs org-mode binary and captures output
29
+ #
30
+ # builds command out of parameters and calls capturing
31
+ # stdout/stderr and the exit status
32
+ #
33
+ # Raises an OrgModeScriptError when status is not success
34
+ # OrgModeScriptError contains all the information on the command
35
+ #
36
+ # Returns stdout as String, stderr as String, status as Process::Status
37
+ def org_mode_script(cmd, *params)
38
+
39
+ # build command
40
+ cmd = %[bin/org-mode #{cmd} #{params * ' '}]
41
+
42
+ stdout, stderr = [ nil,nil ]
43
+ status = POpen4.popen4(cmd) do |pout,perr|
44
+ stdout = pout.read
45
+ stderr = perr.read
46
+ end
47
+
48
+ unless status.success?
49
+ raise OrgModeScriptError.new(stdout, stderr, status, cmd)
50
+ end
51
+
52
+ return [stdout.chomp, stderr, status]
53
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def strip_indent(count)
3
+ self.gsub(/^\s{#{count}}/,'')
4
+ end
5
+ end
@@ -0,0 +1,39 @@
1
+ require 'org_mode/parser'
2
+ require 'org_mode/loader'
3
+ require 'org_mode/reporters/agenda'
4
+
5
+ require 'core_ext/string'
6
+ require 'mustache'
7
+
8
+ module OrgMode::Commands
9
+ class Agenda
10
+ # Private: executes the agenda command
11
+ # parses all the files
12
+ #
13
+ # args - list of filenames
14
+ # options - switches set by the app
15
+ #
16
+ # Returns the restult to stdout
17
+ def execute(args, options)
18
+
19
+ file_collection = OrgMode::Loader.load_and_parse_files(*args)
20
+ agenda_reporter = OrgMode::Reporters::Agenda.new(file_collection)
21
+
22
+ tmpl_vars = {}
23
+ tmpl_vars[:noi_per_date] = agenda_reporter.open_nodes_grouped_by_day
24
+
25
+ puts Mustache.render <<-eos.strip_indent(8), tmpl_vars
26
+ Agenda ()
27
+ {{#noi_per_date}}
28
+ {{date}}
29
+ {{#nodes}}
30
+ {{todo_state}}{{title}}
31
+ {{/nodes}}
32
+ {{/noi_per_date}}
33
+ eos
34
+
35
+ rescue SystemCallError => e
36
+ puts "Encountered a little problem: #{e}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,4 @@
1
+ module OrgMode::Commands
2
+ class Update
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module OrgMode
2
+ module Commands
3
+ end
4
+ end
5
+
6
+ require 'org_mode/commands/agenda'
7
+ require 'org_mode/commands/update'
@@ -0,0 +1,17 @@
1
+ class OrgMode::Loader
2
+ # Public: loads and parses multiple files
3
+ #
4
+ # Returns OrgMode::FileCollection
5
+ def self.load_and_parse_files *paths
6
+ org_mode_files = paths.map {|f| load_and_parse_file(f) }
7
+ OrgMode::FileCollection.new(org_mode_files)
8
+ end
9
+
10
+ # Public: loads and parse a file
11
+ #
12
+ # Returns OrgMode::File
13
+ def self.load_and_parse_file path
14
+ f = File.open(path)
15
+ OrgMode::FileParser.parse(f.read)
16
+ end
17
+ end
@@ -0,0 +1,137 @@
1
+ # Public: Org File format parser
2
+ #
3
+ # A simple regexp based parser for orgfiles. Works by simply dividng
4
+ # the file in beginning, nodes, and ending. After this it will
5
+ # parse the individual nodes extracting remaining detailed information.
6
+ #
7
+ # Parser is decoupled from object model to make it easy to write updated
8
+ # parsers or use a database to serialize an org-mode file out of.
9
+ require 'org_mode'
10
+ require 'date'
11
+
12
+ module OrgMode
13
+ class FileParser
14
+ RxNodeTitle = %r{
15
+ ^ # beginning of line
16
+ (
17
+ \*+ # multiple stars
18
+ \s+ # one or more whitespace
19
+ .* # anything
20
+ )
21
+ $ # untill _end of line
22
+ }xs
23
+
24
+ class << self
25
+
26
+ # Public: parses buffer into nodes and
27
+ # collects there in a OrgMode::File object
28
+ #
29
+ # Returns OrgMode::File object containing all
30
+ # information of the file.
31
+ def parse(buffer)
32
+ b, nodes, e = parse_buffer(buffer)
33
+
34
+ parent_stack = []
35
+ nodes.map! do |title, content|
36
+ node = NodeParser.parse(title,content)
37
+ node.parent = parent_stack[node.stars - 1]
38
+ if node.parent
39
+ node.parent.children << node
40
+ end
41
+ parent_stack[node.stars] = node
42
+ end
43
+ return File.new(b,nodes,e)
44
+ end
45
+
46
+ def parse_buffer(buffer)
47
+ beginning_of_file, nodes, ending_of_file =
48
+ parse_into_tokens(buffer)
49
+ end
50
+
51
+
52
+ # Private: splits buffer into different parts
53
+ # First part is beginning of file
54
+ # Second part are the nodetitles in combination
55
+ # with the content
56
+ # Third part is the ending of the file
57
+ #
58
+ # buffer - org mode data
59
+ #
60
+ # Returns beginning_of_file, nodes, and ending
61
+ # if beginning is not present and empy string is
62
+ # returned. This function will never return nil
63
+ #
64
+ def parse_into_tokens buffer
65
+ tokens = buffer.split(RxNodeTitle).map(&:rstrip)
66
+ beginning_of_file = tokens.shift || ''
67
+
68
+ nodes = []
69
+ while !tokens.empty?
70
+ nodes << Array.new(2) { tokens.shift || '' }
71
+ end
72
+
73
+ nodes.map! { |t,c| [t,c[1..-1] || ''] }
74
+
75
+ [ beginning_of_file, nodes, "" ]
76
+ end
77
+ end
78
+ end
79
+
80
+ class NodeParser
81
+
82
+ class << self
83
+
84
+ # Public: Parses a node in the org-mode file-format
85
+ #
86
+ # title - a org-mode title, can contain date, todo statusses, tags
87
+ # everything specified in the org-mod file format
88
+ # content - the content block, which can also contain org-mode format
89
+ #
90
+ # Return a OrgMode::Node containing all parsable information
91
+ def parse(title,content)
92
+ node = OrgMode::Node.new
93
+ parse_title(node, title)
94
+ parse_extract_dates(node)
95
+ parse_content(node, content)
96
+ node
97
+ end
98
+
99
+ private
100
+
101
+ def parse_title(node,title)
102
+ matches = title.match( /^(\*+)\s+(TODO|DONE)?(.*)$/ )
103
+ node.stars = matches[1].length
104
+ node.todo_state = matches[2]
105
+ node.title = matches[3]
106
+ #node.indent = node.stars + 1
107
+ end
108
+
109
+ RxDateRegexp = /<(\d+-\d+-\d+)(?:\s(?:\w{3})?(?:\s(\d+:\d+))?)(?:-(\d+:\d+))?>/
110
+ def parse_extract_dates(node)
111
+ _, extracted_date, start_time, end_time = node.title.match(RxDateRegexp).to_a
112
+ node.title = node.title.gsub(RxDateRegexp, '')
113
+
114
+ if extracted_date
115
+ node.date_start_time = DateTime.parse("#{extracted_date} #{start_time}")
116
+ node.date_end_time = DateTime.parse("#{extracted_date} #{end_time}")
117
+ end
118
+ end
119
+
120
+ RxEmptyLine = /^\s*$/
121
+ def parse_content(node,content)
122
+ return unless content
123
+
124
+ minimum_indent = ( content.lines.map {|l| l =~ /\S/ }.reject(&:nil?) + [node.indent] ).min
125
+ content.gsub!(/^\s{#{minimum_indent}}/m, '')
126
+
127
+ # remove empty lines at beginning and ending
128
+ node.content = content.lines.
129
+ drop_while {|e| e =~ RxEmptyLine}.
130
+ reverse.
131
+ drop_while {|e| e =~ RxEmptyLine}.
132
+ reverse.
133
+ join
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,44 @@
1
+ require 'facets/to_hash'
2
+
3
+ module OrgMode
4
+ module Reporters
5
+ class Agenda
6
+ attr_accessor :file_collection
7
+ def initialize(file_collection)
8
+ @file_collection = file_collection
9
+ end
10
+
11
+ # Public: returns open nodes grouped by day
12
+ # ordered by date
13
+ #
14
+ # Returns an Array of Hash-es like
15
+ # [{:date => '%Y-%m-%d', :nodes => [{ .. }]}]
16
+ # ready to be used by fe Mustache
17
+ def open_nodes_grouped_by_day
18
+ # Get all nodes from all files
19
+ # extract scheduled items which are not done
20
+ # discard all DONE items
21
+ nodes_of_interest = file_collection.scheduled_nodes.select(&:open?)
22
+
23
+ # group them by date
24
+ noi_per_day = nodes_of_interest.group_by { |noi| noi.date.strftime('%Y-%m-%d') }
25
+
26
+ # build a nice orderd struct
27
+ noi_per_day.keys.sort.map do |date|
28
+ { :date => date, :nodes => noi_per_day[date].map { |n| node_to_hash(n) } }
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def node_to_hash(node)
35
+ rv = [:title, :content, :todo_state, :date, :stars].
36
+ map { |k| [ k, node.send(k) ] }.to_h
37
+
38
+ rv[:date] = rv[:date].strftime('%Y-%m-%d %H:%M') if rv[:date]
39
+ rv
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module OrgMode
2
+ VERSION = "0.0.1"
3
+ end
data/lib/org_mode.rb ADDED
@@ -0,0 +1,87 @@
1
+ # Public: Domain objects
2
+ # describing the elements of an org-mod file
3
+ #
4
+ # OrgMode::File encapsulates the org file, with all its settings
5
+ # customizations, code blocks, TODO statusses.
6
+ #
7
+ # This domain model will be build using one of the parsers which
8
+ # you can find somewhere in lib/org_mode/parser*
9
+ #
10
+ # Writing this domain-model to a file can be done using
11
+ # one of the Writes in lib/org_mode/writers/*
12
+ #
13
+ require "org_mode/version"
14
+
15
+ module OrgMode
16
+
17
+
18
+ class Node
19
+ attr_accessor :title, :content, :stars, :date_start_time, :date_end_time, :todo_state
20
+ attr_accessor :parent, :children
21
+
22
+ def initialize
23
+ @parent = nil
24
+ @children = []
25
+ end
26
+
27
+ alias :date :date_start_time
28
+ alias :date= :date_start_time=
29
+
30
+ def indent
31
+ stars + 1
32
+ end
33
+
34
+ def root_node?
35
+ stars == 1
36
+ end
37
+
38
+ def scheduled?
39
+ !date.nil?
40
+ end
41
+
42
+ def open?
43
+ not done?
44
+ end
45
+
46
+ def done?
47
+ todo_state == 'DONE'
48
+ end
49
+
50
+ end
51
+
52
+ module FileInterface
53
+ def root_nodes
54
+ nodes.select(&:root_node?)
55
+ end
56
+
57
+ def scheduled_nodes
58
+ nodes.select(&:scheduled?)
59
+ end
60
+ end
61
+
62
+ class File
63
+ attr_accessor :header, :nodes, :footer
64
+
65
+ include FileInterface
66
+
67
+ def initialize(header, nodes, footer)
68
+ @header = header
69
+ @footer = footer
70
+ @nodes = nodes
71
+ end
72
+ end
73
+
74
+ class FileCollection
75
+ attr_accessor :files
76
+
77
+ include FileInterface
78
+
79
+ def initialize(files)
80
+ @files = files
81
+ end
82
+
83
+ def nodes
84
+ files.map(&:nodes).flatten
85
+ end
86
+ end
87
+ end
data/org_mode.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "org_mode/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "org_mode"
7
+ s.version = OrgMode::VERSION
8
+ s.authors = ["Boy Maas"]
9
+ s.email = ["boy.maas@gmail.com"]
10
+ s.homepage = "https://github.com/boymaas/orgy"
11
+ s.summary = %q{Parses and does all kinds of tricks with org-mode files}
12
+ s.description = %q{Org-mode parser, presenters, and reformatters}
13
+
14
+ s.rubyforge_project = "org_mode"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ s.add_development_dependency "rake"
24
+ # s.add_runtime_dependency "rest-client"
25
+ s.add_runtime_dependency "commander"
26
+ s.add_runtime_dependency "facets"
27
+ s.add_runtime_dependency "mustache"
28
+ end
@@ -0,0 +1,12 @@
1
+ * Main
2
+ ** FirstChildMain
3
+ ** SecondChildMain
4
+ *** FirstChildSecondChildMain
5
+ * SecondMain
6
+ ** FirstChildMain
7
+ *** SecondChildMain
8
+ ** FirstChildSecondChildMain
9
+ * ThirdMain
10
+ ** FirstChildMain
11
+ *** FirstChildFirstChildMain
12
+ **** FirstChildFirstChildFirstChildMain