pirka 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'epub-parser', path: '../epub-parser'
4
+ gem 'epub-maker', path: '../epub-maker'
5
+
6
+ gemspec
7
+
8
+ group :development do
9
+ gem 'kramdown'
10
+ end
11
+
12
+ if RUBY_PLATFORM.match /darwin/
13
+ gem 'terminal-notifier'
14
+ end
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ Pirka
2
+ =====
3
+
4
+ * [Homepage](https://gitlab.com/KitaitiMakoto/pirka)
5
+ * [Documentation](http://www.rubydoc.info/gems/pirka)
6
+ * [Email](mailto:KitaitiMakoto at gmail.com)
7
+
8
+ Description
9
+ -----------
10
+
11
+ Pirka highlights source code syntax in EPUB books
12
+
13
+ Features
14
+ --------
15
+
16
+ * Highlights synatax in EPUB files
17
+ * Extracts `<code>` elements from EPUB files and asks you which programing language they are, and then uses them to highlight
18
+ * Downloads shared code and language information from Git repository
19
+
20
+ Examples
21
+ --------
22
+
23
+ ### Highlighting source code syntax in EPUB books ###
24
+
25
+ $ pirka path/to/book.epub
26
+
27
+ It's a short cut to:
28
+
29
+ $ pirka highlight path/to/book.epub
30
+
31
+ ### Detecting source code from EPUB books ###
32
+
33
+ $ pirka detect path/to/book.epub
34
+ Detecting code from "Book Title"
35
+ Library file was saved to:
36
+ path/to/library.yaml
37
+
38
+ `library.yaml` here includes:
39
+
40
+ * location of `<code>` element(expressed by [EPUB CFI][] but you don't need to undarstand it.)
41
+ * `language` field with blank value
42
+ * file path in EPUB file(zip archive)
43
+ * source code
44
+
45
+ Example:
46
+
47
+ epubcfi(/6/64!/4/2/30/2): # location
48
+ language: # language name. blank at first
49
+ item: OEBPS/text/p-003-003.xhtml # file path in zip archive
50
+ code: | # source code
51
+ f1 = open("|cat", "w")
52
+ f2 = open("|sed 's/a/b/'", "w")
53
+ f1.print "Hello\n"
54
+ f2.print "abc\n"
55
+ f1.close
56
+ f2.close
57
+
58
+ In Pirka context, the file is called *library*.
59
+
60
+ `pirka highlight` command determines programming languages of source code according to this file.
61
+ Read source code, determine languages, write it at `language` field, remove `code` field, and then
62
+ you can highlight the EPUB file by `pirka highlight`.
63
+
64
+ You also determine languages interactively. Set `-i`(`--interactive`) option:
65
+
66
+ $ pirka detect -i path/to/book.epub
67
+
68
+ [EPUB CFI]: http://www.idpf.org/epub/linking/cfi/
69
+
70
+ ### Updating libraries ###
71
+
72
+ $ pirka update
73
+
74
+ Pirka provides official library files for some EPUB books as Git repository((https://gitlab.com/KitaitiMakoto/pirka-library)[https://gitlab.com/KitaitiMakoto/pirka-library]). `pirka update` command fethes the files from the repository and you benefit from it.
75
+
76
+ Additionally, you can host library files by your own and make Pirka recognizes it by configuration file. See later section for that.
77
+
78
+ ### Listing supported books ###
79
+
80
+ $ pirka lib
81
+
82
+ `pirka lib` command lists books Pirka can highlight with:
83
+
84
+ * Release Identifier(special identifier for EPUB books)
85
+ * location of library file
86
+ * title
87
+ * some other metadata
88
+
89
+ ### Configuration ###
90
+
91
+ Pirka can be configured by environment variables, config file and command-line options.
92
+
93
+ #### Environment variables ####
94
+
95
+ XDG_DATA_HOME
96
+ : Affects directory to save library files.
97
+ : Library files are saved to `$XDG_DATA_HOME/pirka/local`
98
+ : The directory is used to search library, too.
99
+ : Default: `$HOME/.local/share`
100
+
101
+ XDG_DATA_DIRS
102
+ : Affects directory to save library files.
103
+ : You can specify multiple directory by seperating with a colon like `XDG_DATA_DIRS=/dir1:/dir2`.
104
+ : `/dir1/pirka/local` and `/dir2/pirka/local` are used to search library, for example.
105
+ : Default: `/usr/local/share:/usr/share`
106
+
107
+ XDG_CONFIG_HOME
108
+ : Affects directory to search and save config file.
109
+ : `$XDG_CONFIG_DIRS/pirka.yaml` is recognized as config file.
110
+ : Default: `$HOME/.config`
111
+
112
+ XDG_CONFIG_DIRS
113
+ : Affects directory to search config file.
114
+ : You can specify multiple directory by seperating with a colon like `XDG_CONFIG_DIRS=/dir1:/dir2`.
115
+ : `/dir1/pirka.yaml` and `/dir2/pirka.yaml` are searched as config file.
116
+ : Default: `/etc/xdg`
117
+
118
+ #### Config file ####
119
+
120
+ Config file is a YAML file. Properties below are recognized:
121
+
122
+ data_home
123
+ : Directory to save and search library files.
124
+ : Default: `$XDG_CONFIG_HOME/pirka/local`
125
+
126
+ additional_directories
127
+ : Directories to search library files.
128
+ : Expressed by sequence(array).
129
+ : Default: `[]`
130
+
131
+ library_repositories
132
+ : Git repository URIs used by `pirka lib` command.
133
+ : Expressed by sequence(array).
134
+ : Default: `[]`
135
+
136
+ #### Command-line options ####
137
+
138
+ You can configure Pirka by `pirka` command's global options:
139
+
140
+ `-c`, `--config=FILE`
141
+ : Path to config file.
142
+ : Default: /Users/ikeda/.config/pirka.yaml
143
+
144
+ `-s`, `--data-home=DIRECTORY`
145
+ : Same to config file's `data_home` property.
146
+
147
+ `-d`, `--directory=DIRECTORY`
148
+ : Same to config file's `additional_directories` property.
149
+ : Able to multilpe times.
150
+
151
+ You can also see help by
152
+
153
+ $ pirka --help
154
+ Pirka highlights source code syntax in EPUB files
155
+
156
+ Usage: pirka [global options] [<command>] [options]
157
+
158
+ Global options:
159
+ -c, --config=FILE Config file. Defaults to /Users/ikeda/.config/pirka.yaml
160
+ -s, --data-home=DIRECTORY Directory to *SAVE* library data
161
+ -d, --directory=DIRECTORY Directory to *SEARCH* library data.
162
+ Specify multiple times to add multiple directories.
163
+
164
+ Commands:
165
+ highlight Highlights source code in EPUB file
166
+ detect Detects source code from EPUB file and generate library file
167
+ update Update library files by remote files
168
+ lib Show library infomation
169
+ If command is ommitted, highlight is used with no option
170
+
171
+ Requirements
172
+ ------------
173
+
174
+ * Ruby 2.2 or later
175
+ * C compiler to compile [Nokogiri][] gem
176
+
177
+ [Nokogiri]: http://www.nokogiri.org/
178
+
179
+ Install
180
+ -------
181
+
182
+ $ gem install pirka
183
+
184
+ ### Make faster ###
185
+
186
+ By default, Pirka uses [archive-zip][] gem, a pure Ruby implementation, for zip archive but you can make command execution faster by using [Zip/Ruby][] gem, a C implementation. Just install Zip/Ruby:
187
+
188
+ $ gem install zipruby
189
+
190
+ Pirka, actually internally-used [EPUB Parser][], tries to load Zip/Ruby and use it if available.
191
+
192
+ [archive-zip]: https://github.com/javanthropus/archive-zip
193
+ [Zip/Ruby]: https://bitbucket.org/winebarrel/zip-ruby/wiki/Home
194
+ [EPUB Parser]: http://www.rubydoc.info/gems/epub-parser/file/docs/Home.markdown
195
+
196
+ Copyright
197
+ ---------
198
+
199
+ Copyright (c) 2017 KITAITI Makoto
200
+
201
+ See {file:COPYING.txt} for details.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler/setup'
7
+ rescue LoadError => e
8
+ abort e.message
9
+ end
10
+
11
+ require 'rake'
12
+
13
+ task :default => :test
14
+
15
+ require 'rubygems/tasks'
16
+ Gem::Tasks.new
17
+
18
+ require 'rake/testtask'
19
+ Rake::TestTask.new do |test|
20
+ test.libs << 'test'
21
+ test.pattern = 'test/**/test_*.rb'
22
+ test.verbose = true
23
+ end
24
+
25
+ require 'yard'
26
+ YARD::Rake::YardocTask.new
27
+ task :doc => :yard
data/app/detect.rb ADDED
@@ -0,0 +1,166 @@
1
+ require "optparse"
2
+ require "optparse/pathname"
3
+ require "epub/parser"
4
+ require "epub/cfi"
5
+ require "epub/searcher"
6
+ require "rouge"
7
+ require "rouge/lexers/fluentd"
8
+ require "colored"
9
+ require "pirka/library"
10
+ require_relative "subcommand"
11
+
12
+ module Pirka
13
+ class App
14
+ class Detect
15
+ PROGRAM_NAME = "detect"
16
+ DESCRIPTION = "Detects source code from EPUB file and generate library file"
17
+ ARGS = %w[EPUB_FILE]
18
+
19
+ include Subcommand
20
+
21
+ def initialize(config)
22
+ super
23
+
24
+ @library_path = nil
25
+ @interactive = false
26
+
27
+ @available_lexers = Rouge::Lexer.all.sort_by(&:tag).each_with_object({}).with_index {|(lexer, lexers), index|
28
+ lexers[(index + 1).to_s] = lexer
29
+ }
30
+ initial = nil
31
+ @lexers_display = @available_lexers.collect {|(index, lexer)|
32
+ init = lexer.title[0].upcase
33
+ if initial == init
34
+ option = ""
35
+ else
36
+ option = "\n"
37
+ initial = init
38
+ end
39
+ option << "#{index})".bold << " " << lexer.title
40
+ option << "(#{lexer.aliases.join(', ')})" unless lexer.aliases.empty?
41
+ option
42
+ }.join(" ")
43
+ @commands = ["s) skip", "q) quit", "c) show code", "o) show options"].join(" ")
44
+ @commands = {
45
+ "s" => "skip",
46
+ "q" => "quit",
47
+ "c" => "show code",
48
+ "o" => "show options"
49
+ }.collect {|(key, command)|
50
+ "#{key})".bold << " " << command
51
+ }.join(" ")
52
+ end
53
+
54
+ def run(argv)
55
+ parse_options! argv
56
+
57
+ epub_path = argv.shift
58
+ raise ArgumentError, 'Specify EPUB file' unless epub_path
59
+
60
+ begin
61
+ # @todo Make this optional
62
+ require 'epub/maker/ocf/physical_container/zipruby'
63
+ EPUB::OCF::PhysicalContainer.adapter = :Zipruby
64
+ rescue LoadError
65
+ end
66
+ epub = EPUB::Parser.parse(epub_path)
67
+ $stderr.puts "Detecting code from \"#{epub.title}\""
68
+
69
+ codelist = {}
70
+ library = Library.new
71
+ library.metadata["Release Identifier"] = epub.release_identifier
72
+ library.metadata["title"] = epub.title
73
+ catch do |quit|
74
+ EPUB::Searcher.search_element(epub, css: 'code').each do |result|
75
+ item = result[:itemref].item
76
+ if @interactive
77
+ catch do |skip|
78
+ show_item item
79
+ show_code result[:element]
80
+ show_options
81
+ show_commands
82
+ i = ask
83
+
84
+ while true
85
+ case i
86
+ when "s"
87
+ throw skip
88
+ when "q"
89
+ throw quit
90
+ when "c"
91
+ show_item item
92
+ show_code(result[:element])
93
+ show_options
94
+ show_commands
95
+ i = ask
96
+ when "o"
97
+ show_options
98
+ show_commands
99
+ i = ask
100
+ else
101
+ lexer = @available_lexers[i]
102
+ unless lexer
103
+ i = ask
104
+ next
105
+ end
106
+ library.codelist[result[:location]] = {"language" => lexer.tag}
107
+ break
108
+ end
109
+ end
110
+ end
111
+ else
112
+ library.codelist[result[:location]] = ({
113
+ "language" => nil,
114
+ "item" => result[:itemref].item.entry_name,
115
+ "code" => result[:element].content
116
+ })
117
+ end
118
+ end
119
+
120
+ path = library.save(@library_path)
121
+ $stderr.puts "Library file was saved to:"
122
+ $stdout.puts path
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def parse_options!(argv)
129
+ super do |opt|
130
+ opt.separator ""
131
+ opt.on "-i", "--interactive" do
132
+ @interactive = true
133
+ end
134
+ opt.on "-o", "--output=FILE", "File to save library data", Pathname do |path|
135
+ @library_path = path
136
+ end
137
+ end
138
+ end
139
+
140
+ def show_item(item)
141
+ $stderr.puts
142
+ $stderr.puts item.entry_name
143
+ end
144
+
145
+ def show_code(code)
146
+ $stderr.puts
147
+ $stderr.puts code.content
148
+ $stderr.puts
149
+ end
150
+
151
+ def show_options
152
+ $stderr.puts @lexers_display
153
+ end
154
+
155
+ def show_commands
156
+ $stderr.puts
157
+ $stderr.puts @commands
158
+ end
159
+
160
+ def ask
161
+ $stderr.print "Which language? "
162
+ $stdin.gets.chomp
163
+ end
164
+ end
165
+ end
166
+ end
data/app/highlight.rb ADDED
@@ -0,0 +1,161 @@
1
+ require "pathname"
2
+ require "optparse"
3
+ require "optparse/pathname"
4
+ require "epub/parser"
5
+ require "epub/maker"
6
+ require "rouge"
7
+ require "rouge/lexers/fluentd"
8
+ require "pirka/library"
9
+ require_relative "subcommand"
10
+
11
+ module Pirka
12
+ class App
13
+ class Highlight
14
+ PROGRAM_NAME = "highlight"
15
+ DESCRIPTION = "Highlights source code in EPUB file"
16
+ ARGS = %w[EPUB_FILE]
17
+
18
+ include Subcommand
19
+
20
+ DUMMY_ORIGIN = Addressable::URI.parse("file:///")
21
+ CSS_PATH = "pirka/style.css" # @todo Avoid conflict with existing item by other than Pirka
22
+ CSS_CLASS_NAME = "pirka"
23
+ SCOPE = "code.#{CSS_CLASS_NAME}"
24
+ THEME = "github"
25
+
26
+ def initialize(config)
27
+ super
28
+ @library_path = nil
29
+ end
30
+
31
+ # @todo Handle multiple renditions
32
+ def run(argv)
33
+ parse_options! argv
34
+
35
+ epub_path = argv.shift
36
+ raise ArgumentError, 'Specify EPUB file' unless epub_path
37
+
38
+ begin
39
+ # @todo Make this optional
40
+ require 'epub/maker/ocf/physical_container/zipruby'
41
+ EPUB::OCF::PhysicalContainer.adapter = :Zipruby
42
+ rescue LoadError
43
+ end
44
+ epub = EPUB::Parser.parse(epub_path)
45
+ library = find_library(epub.unique_identifier, epub.modified)
46
+ raise RuntimeError, "Cannot find code list #{Library.filename(epub.release_identifier)} for #{epub.release_identifier}(#{epub_path}) in any directory of #{Library.directories.join(", ")}" unless library
47
+
48
+ css_item = add_css_file(epub)
49
+ need_save = highlight_contents(epub, css_item, library)
50
+ need_save << css_item
51
+ need_save.uniq!
52
+ update_modified_date(epub, Time.now)
53
+
54
+ save_file epub, need_save
55
+ end
56
+
57
+ # @param [EPUB::Book, EPUB::Book::Features] epub
58
+ # @return [EPUB::Publication::Package::Manifest::Item] item indicating added CSS file
59
+ def add_css_file(epub)
60
+ rootfile_path = DUMMY_ORIGIN + epub.ocf.container.rootfile.full_path
61
+ style = Rouge::Theme.find(THEME).new(scope: SCOPE).render
62
+
63
+ epub.package.manifest.make_item {|item|
64
+ item.href = (DUMMY_ORIGIN + CSS_PATH).route_from(rootfile_path)
65
+ # IMPROVEMENT: Want to call item.entry_name = css_path
66
+ item.media_type = 'text/css'
67
+ item.id = CSS_PATH.gsub('/', '-') # @todo Avoid conflict with existing items
68
+ item.content = style
69
+ }
70
+ end
71
+
72
+ # @todo Do the best when file for release identifier is not find but for unique identifier found
73
+ def find_library(unique_identifier, modified)
74
+ @library_path ? Library.load_file(@library_path) :
75
+ Library.find_by_release_identifier("#{unique_identifier}@#{modified}")
76
+ end
77
+
78
+ # @todo Consider descendant elements of code
79
+ def highlight_contents(epub, css_item, library)
80
+ need_save = []
81
+
82
+ formatter = Rouge::Formatters::HTML.new
83
+
84
+ library.each.reverse_each do |(cfi, data)|
85
+ lang = data["language"]
86
+ unless lang
87
+ warn "Language for #{cfi} is not detected"
88
+ next
89
+ end
90
+ itemref, elem, _ = EPUB::Searcher.search_by_cfi(epub, cfi)
91
+ item = itemref.item
92
+ doc = elem.document
93
+ lexer = Rouge::Lexer.find(lang) || Rouge::Lexer.guess(source: elem.content)
94
+ unless lexer
95
+ warn "Cannot find lexer for #{lang}"
96
+ next
97
+ end
98
+ elem.inner_html = formatter.format(lexer.lex(elem.content)) # @todo Consider the case `elem` has descendants
99
+
100
+ classes = (elem["class"] || "").split(/\s+/)
101
+ unless classes.include? CSS_CLASS_NAME
102
+ classes << CSS_CLASS_NAME
103
+ elem["class"] = classes.join(" ")
104
+ end
105
+
106
+ link = doc.at('#pirka') # @todo Avoid conflict with existing link
107
+ unless link
108
+ item_entry_name = DUMMY_ORIGIN + item.entry_name
109
+ entry_name = DUMMY_ORIGIN + css_item.entry_name
110
+ href = entry_name.route_from(item_entry_name)
111
+ link = Nokogiri::XML::Node.new('link', doc)
112
+ link['href'] = href
113
+ link['type'] = 'text/css'
114
+ link['rel'] = 'stylesheet'
115
+ link['id'] = 'pirka'
116
+ head = (doc/'head').first
117
+ head << link
118
+ end
119
+ item.content = doc.to_xml
120
+ need_save << item
121
+ end
122
+
123
+ need_save
124
+ end
125
+
126
+ def update_modified_date(epub, time = Time.now)
127
+ modified = epub.modified
128
+ unless modified
129
+ modified = EPUB::Publication::Package::Metadata::Meta.new
130
+ modified.property = 'dcterms:modified'
131
+ epub.package.metadata.metas << modified
132
+ end
133
+ modified.content = time.utc.iso8601
134
+
135
+ modified
136
+ end
137
+
138
+ def save_file(epub, need_save)
139
+ need_save.each do |item|
140
+ item.save
141
+ epub.package.manifest << item
142
+ end
143
+ epub.package.edit
144
+ end
145
+
146
+ private
147
+
148
+ # @todo theme
149
+ # @todo CSS file path
150
+ # @todo scope
151
+ def parse_options!(argv)
152
+ super do |opt|
153
+ opt.separator ""
154
+ opt.on "-l", "--library=FILE", "library file", Pathname do |path|
155
+ @library_path = path
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
data/app/lib.rb ADDED
@@ -0,0 +1,52 @@
1
+ require "pirka/library"
2
+
3
+ module Pirka
4
+ class App
5
+ class Lib
6
+ PROGRAM_NAME = "lib"
7
+ DESCRIPTION = "Show library infomation"
8
+
9
+ include Subcommand
10
+
11
+ def run(argv)
12
+ # show all
13
+ # show remote repos
14
+ # show data dirs
15
+ # show data home
16
+ # show books
17
+ # show book metadata
18
+
19
+ no_dir_file_length = Library::SUBDIR_LENGTH + Library::EXT.length
20
+ Library.directories.each do |dir|
21
+ next unless dir.directory?
22
+
23
+ dir.each_child do |child|
24
+ if child.to_path.length < no_dir_file_length && child.extname != Library::EXT
25
+ show_info child
26
+ next
27
+ end
28
+ next unless child.directory?
29
+ next unless child.basename.to_path.length == Library::SUBDIR_LENGTH
30
+ child.each_child do |lib|
31
+ next unless lib.extname == Library::EXT
32
+ next unless lib.file?
33
+ show_info lib
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def show_info(path)
40
+ $stdout.puts Library.load_file(path).metadata.to_yaml
41
+ $stdout.puts "library: #{path}"
42
+ end
43
+
44
+ private
45
+
46
+ def parse_options!(argv)
47
+ super do |opt|
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
data/app/subcommand.rb ADDED
@@ -0,0 +1,35 @@
1
+ module Pirka
2
+ class App
3
+ module Subcommand
4
+ class << self
5
+ def included(base)
6
+ APPS[base::PROGRAM_NAME] = base
7
+ end
8
+ end
9
+
10
+ # @param [Config]
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ private
16
+
17
+ # @todo Consider the case the subcommand has no option
18
+ def parse_options!(argv)
19
+ parser = OptionParser.new {|opt|
20
+ usage = "Usage: #{opt.program_name} [options] #{self.class::PROGRAM_NAME}"
21
+ usage << " " << self.class::ARGS.join(" ") if self::class.const_defined?(:ARGS)
22
+
23
+ opt.program_name = "#{opt.program_name} [global options] #{self.class::PROGRAM_NAME}"
24
+ opt.banner = <<EOB
25
+ #{self::class::DESCRIPTION}
26
+
27
+ #{usage}
28
+ EOB
29
+ yield opt if block_given?
30
+ }
31
+ parser.order! argv
32
+ end
33
+ end
34
+ end
35
+ end