pirka 0.1.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/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