fronde 0.3.0

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