org_mode 0.0.1

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