stickyflag 0.2.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.
- data/.gitignore +7 -0
- data/.rspec +4 -0
- data/.simplecov +9 -0
- data/.travis.yml +13 -0
- data/Gemfile +2 -0
- data/LICENSE.md +7 -0
- data/README.md +49 -0
- data/Rakefile +19 -0
- data/TODO.md +3 -0
- data/bin/stickyflag +6 -0
- data/features/clear.feature +14 -0
- data/features/clear_quietly.feature +23 -0
- data/features/configuration.feature +14 -0
- data/features/get.feature +14 -0
- data/features/get_quietly.feature +23 -0
- data/features/set.feature +14 -0
- data/features/set_quietly.feature +22 -0
- data/features/step_definitions/configuration_steps.rb +31 -0
- data/features/step_definitions/database_steps.rb +41 -0
- data/features/step_definitions/pending_steps.rb +5 -0
- data/features/step_definitions/tag_steps.rb +62 -0
- data/features/support/cukegem.rb +82 -0
- data/features/support/env.rb +37 -0
- data/features/tags.feature +18 -0
- data/features/unset.feature +14 -0
- data/features/unset_quietly.feature +23 -0
- data/lib/stickyflag/configuration.rb +66 -0
- data/lib/stickyflag/database.rb +162 -0
- data/lib/stickyflag/external_cmds.rb +64 -0
- data/lib/stickyflag/patches/tempfile_encoding.rb +22 -0
- data/lib/stickyflag/patches/tmpnam.rb +38 -0
- data/lib/stickyflag/paths.rb +47 -0
- data/lib/stickyflag/tag_factory.rb +108 -0
- data/lib/stickyflag/tags/c.rb +25 -0
- data/lib/stickyflag/tags/mmd.rb +168 -0
- data/lib/stickyflag/tags/pdf.rb +119 -0
- data/lib/stickyflag/tags/png.rb +61 -0
- data/lib/stickyflag/tags/source_code.rb +99 -0
- data/lib/stickyflag/tags/tex.rb +25 -0
- data/lib/stickyflag/version.rb +12 -0
- data/lib/stickyflag.rb +253 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/stickyflag/configuration_spec.rb +132 -0
- data/spec/stickyflag/database_spec.rb +331 -0
- data/spec/stickyflag/external_cmds_spec.rb +175 -0
- data/spec/stickyflag/patches/tempfile_encoding_spec.rb +26 -0
- data/spec/stickyflag/patches/tmpnam_spec.rb +35 -0
- data/spec/stickyflag/paths_spec.rb +29 -0
- data/spec/stickyflag/tag_factory_spec.rb +185 -0
- data/spec/stickyflag/tags/c_spec.rb +14 -0
- data/spec/stickyflag/tags/mmd_spec.rb +40 -0
- data/spec/stickyflag/tags/pdf_spec.rb +39 -0
- data/spec/stickyflag/tags/png_spec.rb +6 -0
- data/spec/stickyflag/tags/tex_spec.rb +6 -0
- data/spec/stickyflag_spec.rb +482 -0
- data/spec/support/examples/c_all_comments.c +3 -0
- data/spec/support/examples/c_no_tags.c +5 -0
- data/spec/support/examples/c_with_tag.c +6 -0
- data/spec/support/examples/mmd_all_meta.mmd +6 -0
- data/spec/support/examples/mmd_crazy_keys.mmd +8 -0
- data/spec/support/examples/mmd_crazy_tags.mmd +9 -0
- data/spec/support/examples/mmd_no_tags.mmd +1 -0
- data/spec/support/examples/mmd_with_tag.mmd +3 -0
- data/spec/support/examples/pdf_no_tags.pdf +0 -0
- data/spec/support/examples/pdf_with_tag.pdf +0 -0
- data/spec/support/examples/png_no_tags.png +0 -0
- data/spec/support/examples/png_with_tag.png +0 -0
- data/spec/support/examples/tex_no_tags.tex +10 -0
- data/spec/support/examples/tex_with_tag.tex +11 -0
- data/spec/support/examples/untaggable.txt +0 -0
- data/spec/support/examples.rb +32 -0
- data/spec/support/run_with_args.rb +36 -0
- data/spec/support/silence_stream.rb +12 -0
- data/spec/support/tag_handler_behavior.rb +125 -0
- data/stickyflag.gemspec +48 -0
- metadata +399 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'thread'
|
3
|
+
require 'sequel'
|
4
|
+
|
5
|
+
module StickyFlag
|
6
|
+
module Database
|
7
|
+
def load_database
|
8
|
+
@database.disconnect if @database
|
9
|
+
|
10
|
+
if RUBY_PLATFORM == 'java'
|
11
|
+
@database = Sequel.connect("jdbc:sqlite:#{database_path}")
|
12
|
+
else
|
13
|
+
@database = Sequel.sqlite(:database => database_path)
|
14
|
+
end
|
15
|
+
raise Thor::Error.new("ERROR: Could not create database at '#{database_path}'") if @database.nil?
|
16
|
+
|
17
|
+
create_tables
|
18
|
+
|
19
|
+
# Do not do automatic cleanup from the RSpec test suite; this registers
|
20
|
+
# dozens of at_exit hooks and crashes Ruby
|
21
|
+
unless ENV['RSPEC_TESTING']
|
22
|
+
at_exit { @database.disconnect }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_tables
|
27
|
+
@database.create_table?(:tag_list) do
|
28
|
+
primary_key :id, :type => Bignum
|
29
|
+
String :tag_name, :text => true
|
30
|
+
end
|
31
|
+
@database.create_table?(:file_list) do
|
32
|
+
primary_key :id, :type => Bignum
|
33
|
+
String :file_name, :text => true
|
34
|
+
end
|
35
|
+
@database.create_table?(:tagged_files) do
|
36
|
+
primary_key :id
|
37
|
+
Bignum :file
|
38
|
+
Bignum :tag
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def drop_tables
|
43
|
+
@database.drop_table(:tag_list, :file_list, :tagged_files)
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_tag_id(tag)
|
47
|
+
tag_id = @database[:tag_list].where(:tag_name => tag).get(:id)
|
48
|
+
unless tag_id
|
49
|
+
tag_id = @database[:tag_list].insert(:tag_name => tag)
|
50
|
+
end
|
51
|
+
|
52
|
+
tag_id
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_file_id(file_name)
|
56
|
+
file_id = @database[:file_list].where(:file_name => file_name).get(:id)
|
57
|
+
unless file_id
|
58
|
+
file_id = @database[:file_list].insert(:file_name => file_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
file_id
|
62
|
+
end
|
63
|
+
|
64
|
+
def update_database_from_files(directory = '.')
|
65
|
+
drop_tables
|
66
|
+
create_tables
|
67
|
+
|
68
|
+
Dir.glob(File.join(directory, '**', "*{#{available_tagging_extensions.join(',')}}")).each do |file|
|
69
|
+
begin
|
70
|
+
tags = get_tags_for file
|
71
|
+
rescue Thor::Error
|
72
|
+
# Just skip this file, then, don't error out entirely
|
73
|
+
say_status :warning, "Could not read tags from '#{file}', despite a valid extension", :yellow
|
74
|
+
next
|
75
|
+
end
|
76
|
+
|
77
|
+
# Don't record files in the DB that have no tags
|
78
|
+
next if tags.empty?
|
79
|
+
|
80
|
+
file_id = get_file_id file
|
81
|
+
tags.each do |tag|
|
82
|
+
tag_id = get_tag_id tag
|
83
|
+
@database[:tagged_files].insert(:file => file_id, :tag => tag_id)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def set_database_tag(file_name, tag)
|
89
|
+
# Don't put in multiple entries for the same tag
|
90
|
+
file_id = get_file_id file_name
|
91
|
+
tag_id = get_tag_id tag
|
92
|
+
|
93
|
+
return unless @database[:tagged_files].where(:file => file_id).and(:tag => tag_id).empty?
|
94
|
+
@database[:tagged_files].insert(:file => file_id, :tag => tag_id)
|
95
|
+
end
|
96
|
+
|
97
|
+
def unset_database_tag(file_name, tag)
|
98
|
+
file_id = get_file_id file_name
|
99
|
+
tag_id = get_tag_id tag
|
100
|
+
@database[:tagged_files].where(:file => file_id).and(:tag => tag_id).delete
|
101
|
+
|
102
|
+
# See if that was the last file with this tag, and delete it if so
|
103
|
+
if @database[:tagged_files].where(:tag => tag_id).empty?
|
104
|
+
@database[:tag_list].where(:id => tag_id).delete
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def clear_database_tags(file_name)
|
109
|
+
file_id = get_file_id file_name
|
110
|
+
@database[:tagged_files].where(:file => file_id).delete
|
111
|
+
|
112
|
+
# That operation might have removed the last instance of a tag, clean up
|
113
|
+
# the tag list
|
114
|
+
@database[:tag_list].each do |row|
|
115
|
+
raise Thor::Error.new("INTERNAL ERROR: Database row error in tag_list") unless row.include? :id
|
116
|
+
tag_id = row[:id]
|
117
|
+
|
118
|
+
if @database[:tagged_files].where(:tag => tag_id).empty?
|
119
|
+
@database[:tag_list].where(:id => tag_id).delete
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def files_for_tags(tags)
|
125
|
+
# Map tags to tag IDs, but don't add any missing tags
|
126
|
+
bad_tag = false
|
127
|
+
tag_ids = []
|
128
|
+
tags.each do |tag|
|
129
|
+
tag_id = @database[:tag_list].where(:tag_name => tag).get(:id)
|
130
|
+
unless tag_id
|
131
|
+
say_status :warning, "Tag '#{tag}' is not present in the database (try `stickyflag update`)", :yellow unless options.quiet?
|
132
|
+
bad_tag = true
|
133
|
+
next
|
134
|
+
end
|
135
|
+
|
136
|
+
tag_ids << tag_id
|
137
|
+
end
|
138
|
+
return [] if bad_tag
|
139
|
+
|
140
|
+
file_rows = @database[:tagged_files].where(:tag => tag_ids).group_by(:file).having{[[count(:*){}, tag_ids.count]]}
|
141
|
+
if file_rows.empty?
|
142
|
+
say_status :warning, "Requested combination of tags not found", :yellow unless options.quiet?
|
143
|
+
return []
|
144
|
+
end
|
145
|
+
|
146
|
+
files = []
|
147
|
+
file_rows.each do |row|
|
148
|
+
file_id = row[:file]
|
149
|
+
file = @database[:file_list].where(:id => file_id).get(:file_name)
|
150
|
+
raise Thor::Error.new("ERROR: Could not get file_name for id saved in database (re-run `stickyflag update`)") unless file
|
151
|
+
|
152
|
+
files << file
|
153
|
+
end
|
154
|
+
|
155
|
+
files
|
156
|
+
end
|
157
|
+
|
158
|
+
def tag_list
|
159
|
+
@database[:tag_list].map { |r| r[:tag_name] }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module StickyFlag
|
4
|
+
module ExternalCmds
|
5
|
+
|
6
|
+
EXTERNAL_CMDS = {
|
7
|
+
'pdftk' => 'read and write PDF tags'
|
8
|
+
}
|
9
|
+
|
10
|
+
def find_external_cmds
|
11
|
+
EXTERNAL_CMDS.each do |cmd, desc|
|
12
|
+
path = get_config "#{cmd}_path"
|
13
|
+
|
14
|
+
# First, make sure that the listed file actually exists and is executable,
|
15
|
+
# so that if it isn't, we'll be able to fix it by checking $PATH down
|
16
|
+
# below.
|
17
|
+
unless path.nil? || path.empty?
|
18
|
+
unless File.executable? path
|
19
|
+
say_status :error, "Path set for #{cmd} is invalid", :red unless options.quiet?
|
20
|
+
|
21
|
+
path = ''
|
22
|
+
set_config "#{cmd}_path", ''
|
23
|
+
set_config "have_#{cmd}", false
|
24
|
+
else
|
25
|
+
# We do have this, make sure it's set that way
|
26
|
+
set_config "have_#{cmd}", true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if path.nil? || path.empty?
|
31
|
+
# We don't have a path for this command, see if we can find it
|
32
|
+
found = which(cmd)
|
33
|
+
if found.nil?
|
34
|
+
say_status :warning, "Cannot find #{cmd} in path, will not be able to #{desc}", :yellow unless options.quiet?
|
35
|
+
set_config "have_#{cmd}", false
|
36
|
+
next
|
37
|
+
end
|
38
|
+
|
39
|
+
# Okay, found it, set the configuration parameter
|
40
|
+
set_config "#{cmd}_path", found
|
41
|
+
set_config "have_#{cmd}", true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Save our results out to the configuration file
|
46
|
+
save_config!
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Thanks to mislav on Stack Overflow for this
|
52
|
+
def which(cmd)
|
53
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
54
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
55
|
+
exts.each { |ext|
|
56
|
+
exe = "#{path}#{File::SEPARATOR}#{cmd}#{ext}"
|
57
|
+
return exe if File.executable? exe
|
58
|
+
}
|
59
|
+
end
|
60
|
+
return nil
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
class Tempfile
|
5
|
+
|
6
|
+
unless Tempfile.respond_to?(:new_with_encoding)
|
7
|
+
def self.new_with_encoding(params)
|
8
|
+
# JRuby's Tempfile constructor got the third hash parameter in JRuby
|
9
|
+
# 1.7.0-preview1.
|
10
|
+
if RUBY_VERSION >= "1.9.0" && (RUBY_PLATFORM != 'java' || JRUBY_VERSION >= '1.7.0')
|
11
|
+
return Tempfile.new(params, Dir.tmpdir, :encoding => "UTF-8")
|
12
|
+
else
|
13
|
+
# No coverage on Ruby 1.8, ignore
|
14
|
+
#:nocov:
|
15
|
+
return Tempfile.new(params)
|
16
|
+
#:nocov:
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
class File
|
5
|
+
|
6
|
+
unless File.respond_to?(:tmpnam)
|
7
|
+
def self.tmpnam(ext = '')
|
8
|
+
if ext != ''
|
9
|
+
if ext[0] != '.'
|
10
|
+
ext = ".#{ext}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
pid = Process.pid
|
15
|
+
time = Time.now
|
16
|
+
sec = time.to_i
|
17
|
+
usec = time.usec
|
18
|
+
|
19
|
+
counter = 0
|
20
|
+
path = File.join(Dir.tmpdir, "#{pid}_#{sec}_#{usec}_#{rand(1000)}#{ext}")
|
21
|
+
|
22
|
+
# This is a corner case that should effectively never be possible, so
|
23
|
+
# don't try to cover it.
|
24
|
+
#:nocov:
|
25
|
+
while File.exist? path
|
26
|
+
sec = Time.now.to_i
|
27
|
+
usec = Time.now.usec
|
28
|
+
path = File.join(Dir.tmpdir, "#{pid}_#{sec}_#{usec}_#{rand(1000)}#{ext}")
|
29
|
+
|
30
|
+
counter += 1
|
31
|
+
raise 'ERROR: Cannot get unique temporary name' if counter >= 100
|
32
|
+
end
|
33
|
+
#:nocov:
|
34
|
+
|
35
|
+
path
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'rbconfig'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module StickyFlag
|
6
|
+
module Paths
|
7
|
+
def config_path
|
8
|
+
# No code coverage: only ever runs one operating system branch
|
9
|
+
#:nocov:
|
10
|
+
case RbConfig::CONFIG['target_os']
|
11
|
+
when /darwin/i
|
12
|
+
root_dir = File.expand_path("~/Library/Application Support/StickyFlag")
|
13
|
+
when /linux/i
|
14
|
+
require 'xdg'
|
15
|
+
root_dir = File.join(XDG['CONFIG_HOME'].to_s, 'stickyflag')
|
16
|
+
when /mswin|mingw/i
|
17
|
+
root_dir = File.join(ENV['APPDATA'], 'StickyFlag')
|
18
|
+
else
|
19
|
+
root_dir = File.expand_path('~/.stickyflag')
|
20
|
+
end
|
21
|
+
#:nocov:
|
22
|
+
|
23
|
+
FileUtils.mkdir_p root_dir
|
24
|
+
File.join(root_dir, 'config.yml')
|
25
|
+
end
|
26
|
+
|
27
|
+
def database_path
|
28
|
+
# No code coverage: only ever runs one operating system branch
|
29
|
+
#:nocov:
|
30
|
+
case RbConfig::CONFIG['target_os']
|
31
|
+
when /darwin/i
|
32
|
+
root_dir = File.expand_path("~/Library/Application Support/StickyFlag")
|
33
|
+
when /linux/i
|
34
|
+
require 'xdg'
|
35
|
+
root_dir = File.join(XDG['DATA_HOME'].to_s, 'stickyflag')
|
36
|
+
when /mswin|mingw/i
|
37
|
+
root_dir = File.join(ENV['APPDATA'], 'StickyFlag')
|
38
|
+
else
|
39
|
+
root_dir = File.expand_path('~/.stickyflag')
|
40
|
+
end
|
41
|
+
#:nocov:
|
42
|
+
|
43
|
+
FileUtils.mkdir_p root_dir
|
44
|
+
File.join(root_dir, 'db.sqlite')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'thor'
|
3
|
+
require 'pathname'
|
4
|
+
require 'backports'
|
5
|
+
require 'stickyflag/tags/pdf'
|
6
|
+
require 'stickyflag/tags/pdf'
|
7
|
+
require 'stickyflag/tags/png'
|
8
|
+
require 'stickyflag/tags/tex'
|
9
|
+
require 'stickyflag/tags/c'
|
10
|
+
require 'stickyflag/tags/mmd'
|
11
|
+
|
12
|
+
module StickyFlag
|
13
|
+
module TagFactory
|
14
|
+
def available_tagging_extensions
|
15
|
+
['.pdf', '.png', '.tex', '.c', '.cpp', '.cxx', '.c++',
|
16
|
+
'.h', '.hpp', '.hxx', '.mmd']
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_tags_for(file_name)
|
20
|
+
extension = File.extname file_name
|
21
|
+
case extension
|
22
|
+
when '.pdf' then return StickyFlag::Tags::PDF::get(file_name, get_config(:pdftk_path))
|
23
|
+
when '.png' then return StickyFlag::Tags::PNG::get(file_name)
|
24
|
+
when '.tex' then return StickyFlag::Tags::TeX::get(file_name)
|
25
|
+
when '.c' then return StickyFlag::Tags::C::get(file_name)
|
26
|
+
when '.cpp' then return StickyFlag::Tags::C::get(file_name)
|
27
|
+
when '.cxx' then return StickyFlag::Tags::C::get(file_name)
|
28
|
+
when '.c++' then return StickyFlag::Tags::C::get(file_name)
|
29
|
+
when '.h' then return StickyFlag::Tags::C::get(file_name)
|
30
|
+
when '.hpp' then return StickyFlag::Tags::C::get(file_name)
|
31
|
+
when '.hxx' then return StickyFlag::Tags::C::get(file_name)
|
32
|
+
when '.mmd' then return StickyFlag::Tags::MMD::get(file_name)
|
33
|
+
else raise Thor::Error.new("ERROR: Don't know how to get tags for a file of extension '#{extension}'")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_tag_for(file_name, tag)
|
38
|
+
tags = get_tags_for file_name
|
39
|
+
if tags.include? tag
|
40
|
+
# Already set
|
41
|
+
return
|
42
|
+
end
|
43
|
+
|
44
|
+
set_database_tag file_name, tag
|
45
|
+
|
46
|
+
extension = File.extname file_name
|
47
|
+
case extension
|
48
|
+
when '.pdf' then return StickyFlag::Tags::PDF::set(file_name, tag, get_config(:pdftk_path))
|
49
|
+
when '.png' then return StickyFlag::Tags::PNG::set(file_name, tag)
|
50
|
+
when '.tex' then return StickyFlag::Tags::TeX::set(file_name, tag)
|
51
|
+
when '.c' then return StickyFlag::Tags::C::set(file_name, tag)
|
52
|
+
when '.cpp' then return StickyFlag::Tags::C::set(file_name, tag)
|
53
|
+
when '.cxx' then return StickyFlag::Tags::C::set(file_name, tag)
|
54
|
+
when '.c++' then return StickyFlag::Tags::C::set(file_name, tag)
|
55
|
+
when '.h' then return StickyFlag::Tags::C::set(file_name, tag)
|
56
|
+
when '.hpp' then return StickyFlag::Tags::C::set(file_name, tag)
|
57
|
+
when '.hxx' then return StickyFlag::Tags::C::set(file_name, tag)
|
58
|
+
when '.mmd' then return StickyFlag::Tags::MMD::set(file_name, tag)
|
59
|
+
else raise Thor::Error.new("ERROR: Don't know how to set tags for a file of extension '#{extension}'")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def unset_tag_for(file_name, tag)
|
64
|
+
tags = get_tags_for file_name
|
65
|
+
unless tags.include? tag
|
66
|
+
raise Thor::Error.new("ERROR: Cannot unset tag #{tag} from file, not set")
|
67
|
+
end
|
68
|
+
|
69
|
+
unset_database_tag file_name, tag
|
70
|
+
|
71
|
+
extension = File.extname file_name
|
72
|
+
case extension
|
73
|
+
when '.pdf' then return StickyFlag::Tags::PDF::unset(file_name, tag, get_config(:pdftk_path))
|
74
|
+
when '.png' then return StickyFlag::Tags::PNG::unset(file_name, tag)
|
75
|
+
when '.tex' then return StickyFlag::Tags::TeX::unset(file_name, tag)
|
76
|
+
when '.c' then return StickyFlag::Tags::C::unset(file_name, tag)
|
77
|
+
when '.cpp' then return StickyFlag::Tags::C::unset(file_name, tag)
|
78
|
+
when '.cxx' then return StickyFlag::Tags::C::unset(file_name, tag)
|
79
|
+
when '.c++' then return StickyFlag::Tags::C::unset(file_name, tag)
|
80
|
+
when '.h' then return StickyFlag::Tags::C::unset(file_name, tag)
|
81
|
+
when '.hpp' then return StickyFlag::Tags::C::unset(file_name, tag)
|
82
|
+
when '.hxx' then return StickyFlag::Tags::C::unset(file_name, tag)
|
83
|
+
when '.mmd' then return StickyFlag::Tags::MMD::unset(file_name, tag)
|
84
|
+
else raise Thor::Error.new("ERROR: Don't know how to unset tags for a file of extension '#{extension}'")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def clear_tags_for(file_name)
|
89
|
+
clear_database_tags file_name
|
90
|
+
|
91
|
+
extension = File.extname file_name
|
92
|
+
case extension
|
93
|
+
when '.pdf' then return StickyFlag::Tags::PDF::clear(file_name, get_config(:pdftk_path))
|
94
|
+
when '.png' then return StickyFlag::Tags::PNG::clear(file_name)
|
95
|
+
when '.tex' then return StickyFlag::Tags::TeX::clear(file_name)
|
96
|
+
when '.c' then return StickyFlag::Tags::C::clear(file_name)
|
97
|
+
when '.cpp' then return StickyFlag::Tags::C::clear(file_name)
|
98
|
+
when '.cxx' then return StickyFlag::Tags::C::clear(file_name)
|
99
|
+
when '.c++' then return StickyFlag::Tags::C::clear(file_name)
|
100
|
+
when '.h' then return StickyFlag::Tags::C::clear(file_name)
|
101
|
+
when '.hpp' then return StickyFlag::Tags::C::clear(file_name)
|
102
|
+
when '.hxx' then return StickyFlag::Tags::C::clear(file_name)
|
103
|
+
when '.mmd' then return StickyFlag::Tags::MMD::clear(file_name)
|
104
|
+
else raise Thor::Error.new("ERROR: Don't know how to clear all tags for a file of extension '#{extension}'")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'stickyflag/tags/source_code'
|
3
|
+
|
4
|
+
module StickyFlag
|
5
|
+
module Tags
|
6
|
+
module C
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def comment_line_regex
|
10
|
+
/\A\/\/ .*/
|
11
|
+
end
|
12
|
+
|
13
|
+
def tag_line_regex
|
14
|
+
/\A\/\/ SF_TAGS = (.*)/
|
15
|
+
end
|
16
|
+
|
17
|
+
def tag_line_for(str)
|
18
|
+
"// SF_TAGS = #{str}"
|
19
|
+
end
|
20
|
+
|
21
|
+
include StickyFlag::Tags::SourceCode
|
22
|
+
module_function :get, :set, :unset, :clear
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'stickyflag/patches/tmpnam.rb'
|
3
|
+
|
4
|
+
module StickyFlag
|
5
|
+
module Tags
|
6
|
+
module MMD
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# We duplicate the source-code module here because we want to have
|
10
|
+
# cute support for the indentation of tag contents in MMD, and because
|
11
|
+
# MMD metadata blocks have slightly weird syntax rules.
|
12
|
+
|
13
|
+
def metadata_key_regex
|
14
|
+
/([A-Za-z0-9][A-Za-z0-9\-_ ]*):\s*(.*)/
|
15
|
+
end
|
16
|
+
def tag_line_regex
|
17
|
+
/[Tt]\s*[Aa]\s*[Gg]\s*[Ss]\s*:.*/
|
18
|
+
end
|
19
|
+
|
20
|
+
def tag_line_for(tags, last_line = nil)
|
21
|
+
padding = ''
|
22
|
+
|
23
|
+
if last_line
|
24
|
+
match = last_line.match(/([A-Za-z0-9][A-Za-z0-9\-_ ]*:\s*|\s*).*/)
|
25
|
+
if match
|
26
|
+
num = match[1].length - 'Tags: '.length
|
27
|
+
if num > 0
|
28
|
+
padding = ' ' * num
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
"Tags: #{padding}#{tags.join(', ')} "
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_tags(file_name, tags)
|
37
|
+
set_tags = false
|
38
|
+
eating_tags = false
|
39
|
+
last_line = ''
|
40
|
+
counter = 0
|
41
|
+
|
42
|
+
outpath = File.tmpnam
|
43
|
+
File.open(outpath, 'w:UTF-8') do |outfile|
|
44
|
+
File.open(file_name, 'r:UTF-8').each_line do |line|
|
45
|
+
if counter == 0
|
46
|
+
if line !~ metadata_key_regex
|
47
|
+
# If we're on the first line and there's no metadata at all,
|
48
|
+
# then we have to add a metadata block
|
49
|
+
outfile.puts tag_line_for(tags) if tags
|
50
|
+
outfile.puts ''
|
51
|
+
set_tags = true
|
52
|
+
end
|
53
|
+
else
|
54
|
+
# Not on the first line
|
55
|
+
if set_tags == true
|
56
|
+
# If we've already set tags, this is easy
|
57
|
+
outfile.puts line
|
58
|
+
else
|
59
|
+
# We're somewhere in the metadta, and we've yet to set any
|
60
|
+
# tags.
|
61
|
+
if line =~ tag_line_regex
|
62
|
+
# We want to eat the tag block, which could theoretically
|
63
|
+
# be extended onto multiple lines
|
64
|
+
eating_tags = true
|
65
|
+
elsif eating_tags
|
66
|
+
# We're currently eating tags; if this line is blank or a
|
67
|
+
# key/value pair, we're done eating tags, time to print
|
68
|
+
if line =~ metadata_key_regex || line.strip.empty?
|
69
|
+
eating_tags = false
|
70
|
+
outfile.puts tag_line_for(tags, last_line) if tags
|
71
|
+
set_tags = true
|
72
|
+
end
|
73
|
+
else
|
74
|
+
# Not eating the tags key, just keep going, checking to see
|
75
|
+
# if the metadata block is over
|
76
|
+
if line.strip.empty?
|
77
|
+
outfile.puts tag_line_for(tags, last_line) if tags
|
78
|
+
set_tags = true
|
79
|
+
end
|
80
|
+
|
81
|
+
outfile.puts line
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
counter += 1
|
87
|
+
last_line = line
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
FileUtils.mv(outpath, file_name)
|
92
|
+
end
|
93
|
+
|
94
|
+
def get(file_name)
|
95
|
+
counter = 0
|
96
|
+
key = ''
|
97
|
+
value = ''
|
98
|
+
|
99
|
+
File.open(file_name, 'r:UTF-8').each_line do |line|
|
100
|
+
if counter == 0
|
101
|
+
# Check to see if there's any metadata at all
|
102
|
+
m = line.match(metadata_key_regex)
|
103
|
+
return [] unless m
|
104
|
+
|
105
|
+
# Start capturing values
|
106
|
+
key = m[1]
|
107
|
+
value = m[2]
|
108
|
+
else
|
109
|
+
# Check to see if the metadata is over, or if there's another
|
110
|
+
# key/value pair starting on this line
|
111
|
+
m = line.match(metadata_key_regex)
|
112
|
+
|
113
|
+
if m || line.strip.empty?
|
114
|
+
# We're done capturing this value, check if it's 'tags' (after
|
115
|
+
# removing whitespace and lowercasing, as per the spec)
|
116
|
+
key.gsub!(/\s/, '')
|
117
|
+
key.downcase!
|
118
|
+
|
119
|
+
if key == 'tags'
|
120
|
+
return [] if value.nil? || value.empty?
|
121
|
+
return value.split(',').map { |t| t.empty? ? nil : t.strip }.compact
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Quit if there's no more metadata
|
126
|
+
return if line.strip.empty?
|
127
|
+
|
128
|
+
# Start grabbing a new key/value if there is one, or else just
|
129
|
+
# add to the current value
|
130
|
+
if m
|
131
|
+
key = m[1]
|
132
|
+
value = m[2]
|
133
|
+
else
|
134
|
+
# This doesn't strip the indentation off of the values, but
|
135
|
+
# that's okay, since we always strip whitespace off
|
136
|
+
# individual tags
|
137
|
+
value << line
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
counter += 1
|
142
|
+
end
|
143
|
+
|
144
|
+
[]
|
145
|
+
end
|
146
|
+
|
147
|
+
def set(file_name, tag)
|
148
|
+
tags = get(file_name)
|
149
|
+
return if tags.include? tag
|
150
|
+
|
151
|
+
tags << tag
|
152
|
+
write_tags(file_name, tags)
|
153
|
+
end
|
154
|
+
|
155
|
+
def unset(file_name, tag)
|
156
|
+
tags = get(file_name)
|
157
|
+
return unless tags.include? tag
|
158
|
+
|
159
|
+
tags.delete(tag)
|
160
|
+
write_tags(file_name, tags)
|
161
|
+
end
|
162
|
+
|
163
|
+
def clear(file_name)
|
164
|
+
write_tags(file_name, nil)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|