fronde 0.3.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.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webrick'
4
+ require 'fronde/config'
5
+
6
+ module Fronde # rubocop:disable Style/Documentation
7
+ # A tiny preview server, which main goal is to replace references to
8
+ # the target domain by localhost.
9
+ class PreviewServlet < WEBrick::HTTPServlet::AbstractServlet
10
+ include WEBrick::HTTPUtils
11
+
12
+ def do_GET(request, response) # rubocop:disable Naming/MethodName
13
+ file = local_path(request.path)
14
+ response.body = parse_body(file, "http://#{request.host}:#{request.port}")
15
+ response.status = 200
16
+ response.content_type = mime_type(file, DefaultMimeTypes)
17
+ end
18
+
19
+ private
20
+
21
+ def local_path(requested_path)
22
+ routes = Fronde::Config.settings.dig('preview', 'routes') || {}
23
+ return routes[requested_path] if routes.has_key? requested_path
24
+ local_path = Fronde::Config.settings['public_folder'] + requested_path
25
+ if File.directory? local_path
26
+ local_path = format(
27
+ '%<path>s/index.html', path: local_path.delete_suffix('/')
28
+ )
29
+ end
30
+ return local_path if File.exist? local_path
31
+ raise WEBrick::HTTPStatus::NotFound, 'Not found.'
32
+ end
33
+
34
+ def parse_body(local_path, local_host)
35
+ body = IO.read local_path
36
+ return body unless local_path.match?(/\.(?:ht|x)ml\z/)
37
+ domain = Fronde::Config.settings['domain']
38
+ return body if domain == ''
39
+ body.gsub(/"file:\/\//, format('"%<host>s', host: local_host))
40
+ .gsub(/"#{domain}/, format('"%<host>s', host: local_host))
41
+ end
42
+ end
43
+
44
+ class << self
45
+ def start_preview
46
+ # Inspired by ruby un.rb library, which allows normally to start a
47
+ # webrick server in one line: ruby -run -e httpd public_html -p 5000
48
+ port = Fronde::Config.settings.dig('preview', 'server_port') || 5000
49
+ s = WEBrick::HTTPServer.new(Port: port)
50
+ s.mount '/', Fronde::PreviewServlet
51
+ ['TERM', 'QUIT', 'INT'].each { |sig| trap(sig, proc { s.shutdown }) }
52
+ s.start
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'digest/md5'
5
+ require 'fronde/org_file'
6
+
7
+ module Fronde
8
+ # Insert custom part inside generated HTML files.
9
+ class Templater
10
+ def initialize(source, dom, opts = {})
11
+ @dom = dom
12
+ @org_file = source
13
+ @position = opts['type'] || 'after'
14
+ @content = extract_content opts
15
+ @element = @dom.css(opts['selector'])
16
+ digest = Digest::MD5.hexdigest(@content)
17
+ @check_line = " Fronde Template: #{digest} "
18
+ end
19
+
20
+ def apply
21
+ flag_head
22
+ content = @org_file.format(@content)
23
+ @element.each do |e|
24
+ insert_new_node_at e, content
25
+ end
26
+ end
27
+
28
+ def in_head?
29
+ @dom.xpath('//head').children.to_a.filter(&:comment?).each do |c|
30
+ return true if c.text == @check_line
31
+ end
32
+ false
33
+ end
34
+
35
+ class << self
36
+ def customize_output(file_name, source = nil)
37
+ templates_to_apply = filter_templates(file_name)
38
+ return if templates_to_apply.empty?
39
+ if source.nil?
40
+ sourcepath = Fronde::OrgFile.source_for_target(file_name)
41
+ source = Fronde::OrgFile.new(sourcepath)
42
+ end
43
+ dom = open_dom(file_name)
44
+ templates_to_apply.each do |t|
45
+ tpl = Fronde::Templater.new(source, dom, t)
46
+ next if tpl.in_head?
47
+ tpl.apply
48
+ end
49
+ write_dom(file_name, dom)
50
+ end
51
+
52
+ private
53
+
54
+ def filter_templates(file_name)
55
+ templates = Fronde::Config.settings['templates']
56
+ return [] if templates.nil? || templates.empty?
57
+ templates.filter { |t| check_required_keys(t, file_name) }
58
+ end
59
+
60
+ def open_dom(file_name)
61
+ file = File.new file_name, 'r'
62
+ dom = Nokogiri::HTML file
63
+ file.close
64
+ dom
65
+ end
66
+
67
+ def write_dom(file_name, dom)
68
+ file = File.new file_name, 'w'
69
+ dom.write_to file
70
+ file.close
71
+ end
72
+
73
+ def check_path(file_name, pathes)
74
+ pub_folder = Fronde::Config.settings['public_folder']
75
+ if pathes.is_a?(Array)
76
+ pathes.each do |tp|
77
+ return true if File.fnmatch?("#{pub_folder}#{tp}",
78
+ file_name, File::FNM_DOTMATCH)
79
+ end
80
+ return false
81
+ end
82
+ File.fnmatch?("#{pub_folder}#{pathes}",
83
+ file_name, File::FNM_DOTMATCH)
84
+ end
85
+
86
+ def check_required_keys(opts, file_name)
87
+ return false unless opts.has_key?('selector')
88
+ return false unless opts.has_key?('content') || opts.has_key?('source')
89
+ return check_path(file_name, opts['path']) if opts.has_key?('path')
90
+ true
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def flag_head
97
+ @dom.xpath('//head').first.prepend_child("<!--#{@check_line}-->\n")
98
+ end
99
+
100
+ def insert_new_node_at(elem, content)
101
+ case @position
102
+ when 'before'
103
+ elem.add_previous_sibling content
104
+ when 'replace'
105
+ elem.replace content
106
+ else
107
+ elem.add_next_sibling content
108
+ end
109
+ end
110
+
111
+ def extract_content(opts)
112
+ return opts['content'] if opts['content']
113
+ # If we don't have a content option, then we must have a source
114
+ # one.
115
+ @dom.css(opts['source']).unlink.to_s
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'rainbow'
5
+ require 'net/http'
6
+ require 'r18n-core'
7
+ require 'fronde/config'
8
+
9
+ module Fronde
10
+ # Embeds usefull methods, mainly used in rake tasks.
11
+ module Utils
12
+ # @return [Hash] the possible throbber themes
13
+ THROBBER_FRAMES = {
14
+ 'basic' => '-\|/',
15
+ 'basicdots' => '⋯⋱⋮⋰',
16
+ 'moon' => '🌑🌒🌓🌔🌕🌖🌗🌘',
17
+ 'clock' => '🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚',
18
+ 'bricks' => '⣷⣯⣟⡿⢿⣻⣽⣾',
19
+ 'points' => '·⁘∷⁛∷⁘',
20
+ 'quadrant2' => '▙▛▜▟',
21
+ 'default' => ['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
22
+ '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂']
23
+ }.freeze
24
+
25
+ # @return [Hash] the possible ~fronde~ options and their
26
+ # configuration
27
+ FRONDE_OPTIONS = {
28
+ '-a' => { long: 'author' },
29
+ '-f' => { long: 'force', boolean: true },
30
+ '-h' => { long: 'help', boolean: true, meth: :on_tail },
31
+ '-l' => { long: 'lang', keyword: 'LOCALE' },
32
+ '-t' => { long: 'title' },
33
+ '-v' => { long: 'verbose', boolean: true, meth: :on_tail },
34
+ '-V' => { long: 'version', boolean: true, meth: :on_tail }
35
+ }.freeze
36
+
37
+ # @return [Hash] the possible ~fronde~ subcommands and their
38
+ # configuration
39
+ FRONDE_COMMANDS = {
40
+ 'init' => { opts: ['-a', '-h', '-l', '-t', '-v'] },
41
+ 'config' => { alias: 'init' },
42
+ 'preview' => { opts: ['-h'] },
43
+ 'open' => { opts: ['-a', '-h', '-l', '-t', '-v'] },
44
+ 'edit' => { alias: 'open' },
45
+ 'build' => { opts: ['-f', '-h'] },
46
+ 'publish' => { opts: ['-h'] },
47
+ 'help' => { opts: ['-h'] },
48
+ 'basic' => { opts: ['-h', '-V'], label: '<command>' }
49
+ }.freeze
50
+
51
+ class << self
52
+ # Animates strings in the user console to alert him that something
53
+ # is running in the background.
54
+ #
55
+ # The animation is chosen among a bunch of themes, with the
56
+ # configuration option ~throbber~ (retrieved via
57
+ # {Fronde::Config#settings}).
58
+ #
59
+ # @example
60
+ # long_stuff = Thread.new { very_long_operation }
61
+ # Fronde::Utils.throbber(long_stuff, 'Computing hard stuff:')
62
+ #
63
+ # @param thread [Thread] the long-running operation to decorate
64
+ # @param message [String] the message to display before the throbber
65
+ # @return [void]
66
+ def throbber(thread, message)
67
+ frames = select_throbber_frames
68
+ begin
69
+ run_and_decorate_thread thread, message, frames
70
+ rescue RuntimeError => e
71
+ throbber_error message
72
+ raise e
73
+ else
74
+ done = Rainbow('done'.ljust(frames[0].length)).green
75
+ puts "#{message} #{done}"
76
+ end
77
+ end
78
+
79
+ # Returns the short and long options specification for a given
80
+ # short option.
81
+ #
82
+ # This method use the {Fronde::Utils::FRONDE_OPTIONS} Hash to
83
+ # retrieve corresponding values.
84
+ #
85
+ # @example
86
+ # spec = Fronde::Utils.decorate_option('-a')
87
+ # => ['-a AUTHOR', '--author AUTHOR']
88
+ #
89
+ # @param short [String] the short option to decorate
90
+ # @return [Array] the short and long specification for an option
91
+ def decorate_option(short)
92
+ opt = Fronde::Utils::FRONDE_OPTIONS[short]
93
+ long = "--#{opt[:long]}"
94
+ return [short, long] if opt[:boolean]
95
+ key = opt[:keyword] || opt[:long].upcase
96
+ [short + key, format('%<long>s %<key>s', long: long, key: key)]
97
+ end
98
+
99
+ # Returns the ~fronde~ help summary for a given command.
100
+ #
101
+ # @param command [String] the command for which a summary
102
+ # should be given
103
+ # @return [String]
104
+ def summarize_command(command)
105
+ Fronde::Utils::FRONDE_COMMANDS[command][:opts].map do |k|
106
+ short, long = Fronde::Utils.decorate_option(k)
107
+ opt = Fronde::Utils::FRONDE_OPTIONS[k]
108
+ label = [short, long].join(', ')
109
+ line = [format(' %<opt>s', opt: label).ljust(30)]
110
+ if R18n.t.fronde.bin.options[opt[:long]].translated?
111
+ line << R18n.t.fronde.bin.options[opt[:long]]
112
+ end
113
+ line.join(' ')
114
+ end.join("\n")
115
+ end
116
+
117
+ # Returns a formatted list of available commands for ~fronde~.
118
+ #
119
+ # @return [String]
120
+ def list_commands
121
+ lines = []
122
+ Fronde::Utils::FRONDE_COMMANDS.each do |cmd, opt|
123
+ next if cmd == 'basic'
124
+ line = [' ', cmd.ljust(10)]
125
+ if opt.has_key? :alias
126
+ line << R18n.t.fronde.bin.commands.alias(opt[:alias])
127
+ else
128
+ line << R18n.t.fronde.bin.commands[cmd]
129
+ end
130
+ lines << line.join(' ')
131
+ end
132
+ lines.join("\n")
133
+ end
134
+
135
+ # Returns the real command name for a given command, which may be
136
+ # an alias.
137
+ #
138
+ # @param command [String] the command to resolve
139
+ # @return [String]
140
+ def resolve_possible_alias(command)
141
+ return 'basic' unless Fronde::Utils::FRONDE_COMMANDS.include?(command)
142
+ cmd_opt = Fronde::Utils::FRONDE_COMMANDS[command]
143
+ return cmd_opt[:alias] if cmd_opt.has_key?(:alias)
144
+ command
145
+ end
146
+
147
+ # Try to discover the current host operating system.
148
+ #
149
+ # @return [String] either apple, windows or linux (default)
150
+ # :nocov:
151
+ def current_os
152
+ if ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM.include?('cygwin')
153
+ return 'windows'
154
+ end
155
+ return 'apple' if RUBY_PLATFORM.include?('darwin')
156
+ 'linux'
157
+ end
158
+ # :nocov:
159
+
160
+ # Download latest org-mode tarball.
161
+ #
162
+ # @param destination [String] where to save the org-mode tarball
163
+ # @return [String] the downloaded org-mode version
164
+ def download_org(destination = 'var/tmp')
165
+ # :nocov:
166
+ return if Fronde::Config.org_last_version.nil?
167
+ # :nocov:
168
+ tarball = "org-#{Fronde::Config.org_last_version}.tar.gz"
169
+ # Remove version number in dest file to allow easy rake file
170
+ # task naming
171
+ dest_file = File.expand_path('org.tar.gz', destination)
172
+ return if File.exist?(dest_file)
173
+ uri = URI("https://orgmode.org/#{tarball}")
174
+ # Will crash on purpose if anything goes wrong
175
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
176
+ http.request(Net::HTTP::Get.new(uri)) do |response|
177
+ File.open(dest_file, 'w') do |io|
178
+ response.read_body { |chunk| io.write chunk }
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def throbber_error(message)
187
+ warn(
188
+ format(
189
+ "%<message>s %<label>s\n%<explanation>s",
190
+ message: message,
191
+ label: Rainbow(R18n.t.fronde.error.label).bold.red,
192
+ explanation: Rainbow(R18n.t.fronde.error.explanation).bold
193
+ )
194
+ )
195
+ end
196
+
197
+ def select_throbber_frames
198
+ model = Fronde::Config.settings['throbber'] || 'default'
199
+ model = 'default' unless Fronde::Utils::THROBBER_FRAMES.has_key?(model)
200
+ Fronde::Utils::THROBBER_FRAMES[model]
201
+ end
202
+
203
+ def run_and_decorate_thread(thread, message, frames)
204
+ thread.abort_on_exception = true
205
+ current = 0
206
+ while thread.alive?
207
+ sleep 0.1
208
+ print "#{message} #{frames[current % frames.length]}\r"
209
+ current += 1
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ # @return [String] the version number of the current Fronde release.
5
+ VERSION = '0.3.0'
6
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+
5
+ # Fronde::Config is required by Fronde::Utils
6
+ require 'fronde/utils'
7
+
8
+ require 'rake/clean'
9
+
10
+ CLOBBER.push(
11
+ 'var/tmp/org.tar.gz', 'var/tmp/last_org_version',
12
+ 'var/lib/org-config.el', '.dir-locals.el', 'lib/htmlize.el'
13
+ )
14
+
15
+ namespace :org do
16
+ directory 'var/tmp'
17
+
18
+ desc 'Download last version of Org'
19
+ file 'var/tmp/org.tar.gz' => 'var/tmp' do
20
+ verbose = Rake::FileUtilsExt.verbose_flag
21
+ download = Thread.new do
22
+ Thread.current[:org_version] = Fronde::Config.org_last_version
23
+ Fronde::Utils.download_org
24
+ end
25
+ if verbose
26
+ download.join
27
+ warn "Org version #{download[:org_version]} has been downloaded"
28
+ else
29
+ Fronde::Utils.throbber(download, 'Downloading Org:')
30
+ end
31
+ end
32
+
33
+ desc 'Compile Org'
34
+ task compile: 'var/tmp/org.tar.gz' do |task|
35
+ verbose = Rake::FileUtilsExt.verbose_flag
36
+ org_version = Fronde::Config.org_last_version
37
+ org_dir = "lib/org-#{org_version}"
38
+ next if Dir.exist?("#{org_dir}/lisp")
39
+ make = ['make', '-C', org_dir]
40
+ unless verbose
41
+ make << '-s'
42
+ make << 'EMACSQ="emacs -Q --eval \'(setq inhibit-message t)\'"'
43
+ end
44
+ build = Thread.new do
45
+ sh "tar -C lib -xzf #{task.prerequisites[0]}"
46
+ sh((make + ['compile']).join(' '))
47
+ sh((make + ['autoloads']).join(' '))
48
+ Dir.glob('lib/org-[0-9.]*').each do |ov|
49
+ next if ov == org_dir
50
+ rm_r ov
51
+ end
52
+ end
53
+ if verbose
54
+ build.join
55
+ warn "#{org_version} has been locally installed"
56
+ else
57
+ Fronde::Utils.throbber(build, 'Installing Org:')
58
+ end
59
+ end
60
+
61
+ directory 'lib'
62
+
63
+ file 'lib/htmlize.el' => 'lib' do
64
+ htmlize = URI(
65
+ 'https://raw.githubusercontent.com/hniksic/emacs-htmlize/master/htmlize.el'
66
+ ).open.read
67
+ IO.write 'lib/htmlize.el', htmlize
68
+ end
69
+
70
+ file 'var/lib/org-config.el' => 'lib/htmlize.el' do
71
+ Fronde::Config.write_org_lisp_config
72
+ end
73
+
74
+ file '.dir-locals.el' => 'var/lib/org-config.el' do
75
+ Fronde::Config.write_dir_locals
76
+ end
77
+
78
+ desc 'Install Org'
79
+ multitask install: ['org:compile', '.dir-locals.el'] do
80
+ mkdir_p "#{Fronde::Config.settings['public_folder']}/assets"
81
+ Fronde::Config.sources.each do |s|
82
+ mkdir_p s['path'] unless Dir.exist? s['path']
83
+ end
84
+ end
85
+
86
+ # The following task only run the clobber task (not provided by us)
87
+ # and the org:install one, which is already tested. Thus, we can
88
+ # safely remove it from coverage.
89
+ # :nocov:
90
+ desc 'Upgrade Org'
91
+ task :upgrade do
92
+ Rake::Task['clobber'].execute
93
+ Rake::Task['org:install'].invoke
94
+ end
95
+ # :nocov:
96
+ end