Class: Epuber::Server

Inherits:
Sinatra::Base
  • Object
show all
Defined in:
lib/epuber/server.rb,
lib/epuber/server/handlers.rb

Overview

API:

LATER

/file/<path-or-pattern> – displays pretty file (image, text file) (for example: /file/text/s01.xhtml or

/file/text/s01.bade)

Defined Under Namespace

Classes: ShowExceptions

Helpers collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeServer

Returns a new instance of Server.



145
146
147
148
149
150
# File 'lib/epuber/server.rb', line 145

def initialize
  super
  _log :ui, 'Init compile'

  self.class.compile_book
end

Class Method Details

._compile_bookObject



341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/epuber/server.rb', line 341

def self._compile_book
  compiler = Epuber::Compiler.new(book, target)
  compiler.compile(build_path)
  self.file_resolver = compiler.file_resolver

  true
rescue StandardError => e
  self.file_resolver = compiler.file_resolver

  Epuber::UI.error("Compile error: #{e}", location: e)

  false
end

._log(level, message) ⇒ Object

Returns nil.

Parameters:

  • level (Symbol)
  • message (String)

Returns:

  • nil



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/epuber/server.rb', line 187

def self._log(level, message)
  case level
  when :ui
    UI.info message
  when :info
    UI.info "INFO: #{message}" if verbose
  when :get
    UI.info " GET: #{message}" if verbose
  when :ws
    UI.info "  WS: #{message}" if verbose
  else
    raise "Unknown log level #{level}"
  end
end

.build_pathString

Returns base path.

Returns:

  • (String)

    base path



172
173
174
# File 'lib/epuber/server.rb', line 172

def self.build_path
  Epuber::Config.instance.build_path(target)
end

.changes_detected(modified, added, removed) ⇒ Object

Parameters:

  • modified (Array<String>)
  • added (Array<String>)
  • removed (Array<String>)


408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/epuber/server.rb', line 408

def self.changes_detected(modified, added, removed)
  all_changed = (modified + added + removed).uniq.map(&:unicode_normalize)

  reload_bookspec if all_changed.any? { |file| file == book.file_path }

  changed = filter_not_project_files(all_changed) || []
  return if changed.count.zero?

  notify_clients(:compile_start)


  _log :ui, "#{Time.now}  Compiling"
  compile_book do |success|
    unless success
      _log :ui, 'Skipping other steps'
      notify_clients(:compile_end)
      next
    end

    _log :ui, 'Notifying clients'

    # transform all paths to relatives to the server
    changed.map! do |file|
      relative = relative_path_to_book_file(file)
      File.join('', 'book', relative) unless relative.nil?
    end

    # remove nil paths (for example bookspec can't be found so the relative path is nil)
    changed.compact!

    changed_only_styles = changed.all? do |file|
      file.end_with?(*Epuber::Compiler::FileFinders::GROUP_EXTENSIONS[:style])
    end

    if changed.size.positive? && changed_only_styles
      notify_clients(:styles, changed)
    else
      notify_clients(:reload, changed)
    end
  end
end

.compile_book(&completion) ⇒ Object



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/epuber/server.rb', line 355

def self.compile_book(&completion)
  if !@compilation_thread.nil? && @compilation_thread.status != false
    @compilation_thread.kill
    @compilation_thread = nil
  end

  @compilation_thread = Thread.new do
    result = _compile_book
    completion&.call(result)
  end

  return unless completion.nil?

  @compilation_thread.join
end

.filter_not_project_files(files_paths) ⇒ Array<String>

Parameters:

  • files_paths (Array<String>)

Returns:

  • (Array<String>)


398
399
400
401
402
# File 'lib/epuber/server.rb', line 398

def self.filter_not_project_files(files_paths)
  return nil if file_resolver.nil?

  files_paths.select { |file| file_resolver.file_with_source_path(file) || book.file_path == file }
end

.instance_class_accessor(name) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/epuber/server.rb', line 53

def self.instance_class_accessor(name)
  instance_name = "@#{name}"

  define_singleton_method(name) do
    instance_variable_get(instance_name)
  end
  define_singleton_method("#{name}=") do |new_value|
    instance_variable_set(instance_name, new_value)
  end

  define_method(name) do
    self.class.send(name)
  end
  define_method("#{name}=") do |new_value|
    self.class.send("#{name}=", new_value)
  end
end

.notify_clients(type, data = nil) ⇒ Object

Parameters:

  • type (Symbol)


382
383
384
385
386
387
388
389
390
391
392
# File 'lib/epuber/server.rb', line 382

def self.notify_clients(type, data = nil)
  _log :info, "Notifying clients with type #{type.inspect}"
  raise "Not known type `#{type}`" unless %i[styles reload compile_start compile_end].include?(type)

  message = {
    name: type,
  }
  message[:data] = data unless data.nil?

  send_to_clients(message.to_json)
end

.relative_path_to_book_file(path) ⇒ String

Parameters:

  • path (String)

Returns:

  • (String)


233
234
235
236
237
238
# File 'lib/epuber/server.rb', line 233

def self.relative_path_to_book_file(path)
  file = file_resolver.file_with_source_path(path)
  return if file.nil?

  file.pkg_destination_path
end

.reload_bookspecObject



328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/epuber/server.rb', line 328

def self.reload_bookspec
  last_target = target
  Config.instance.bookspec = nil
  self.book = Config.instance.bookspec

  if (new_target = book.target_named(last_target.name))
    self.target = new_target
  else
    self.target = book.targets.first
    _log :ui, "[!] Not found previous target after reloading bookspec file, jumping to first #{target.name}"
  end
end

.render_bade(file_path) ⇒ String

Parameters:

  • file_path (String)

    path to bade file to render

Returns:

  • (String)


61
62
63
64
65
66
# File 'lib/epuber/server/handlers.rb', line 61

def self.render_bade(file_path)
  renderer = Bade::Renderer.from_file(file_path)
                           .with_locals(book: book, target: target, file_resolver: file_resolver)

  renderer.render(new_line: '', indent: '')
end

.run!(book, target, verbose: false) ⇒ Object

Returns nil.

Returns:

  • nil



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/epuber/server.rb', line 105

def self.run!(book, target, verbose: false)
  Encoding.default_internal = Encoding::UTF_8 if Encoding.default_internal.nil?

  self.book = book
  self.target = target
  self.verbose = verbose

  start_listening_if_needed

  old_stderr = $stderr
  $stderr = StringIO.new unless verbose

  super() do |server|
    $stderr = old_stderr
    UI.info "Started development server on #{server.host}:#{server.port}"

    host = if server.host == '0.0.0.0'
             'localhost'
           else
             server.host
           end

    yield URI("http://#{host}:#{server.port}") if block_given?
  end
end

.send_to_clients(message) ⇒ Object

Parameters:

  • message (String)


373
374
375
376
377
378
379
# File 'lib/epuber/server.rb', line 373

def self.send_to_clients(message)
  _log :info, "sending message to clients #{message.inspect}"

  sockets.each do |ws|
    ws.send(message)
  end
end

.socketsObject



95
96
97
# File 'lib/epuber/server.rb', line 95

def self.sockets
  @sockets ||= []
end

.start_listening_if_neededObject



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/epuber/server.rb', line 152

def self.start_listening_if_needed
  return unless listener.nil?

  self.listener = Listen.to(Config.instance.project_path, debug: true) do |modified, added, removed|
    changes_detected(modified, added, removed)
  rescue StandardError => e
    # print error, do not send error further, listener will die otherwise
    warn e
    warn e.backtrace
  end

  listener.ignore(/\.idea/)
  listener.ignore(/#{Config.instance.working_path}/)
  listener.ignore(%r{#{Config::WORKING_PATH}/})

  listener.start
end

.verbose=(verbose) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/epuber/server.rb', line 131

def self.verbose=(verbose)
  @verbose = verbose
  @default_thin_logger ||= Thin::Logging.logger

  if verbose
    Thin::Logging.logger = @default_thin_logger
  else
    Thin::Logging.logger = ::Logger.new(nil)
    Thin::Logging.logger.level = :fatal
  end

  set :logging, verbose
end

Instance Method Details

#_log(level, message) ⇒ Object



202
203
204
# File 'lib/epuber/server.rb', line 202

def _log(level, message)
  self.class._log(level, message)
end

#add_auto_refresh_script(html_doc) ⇒ Object

Parameters:

  • html_doc (Nokogiri::HTML::Document)


277
278
279
280
281
282
283
# File 'lib/epuber/server.rb', line 277

def add_auto_refresh_script(html_doc)
  add_file_to_head(:js, html_doc, 'auto_refresh/reloader.coffee')
  add_file_to_head(:js, html_doc, 'auto_refresh/connector.coffee')
  add_file_to_head(:js, html_doc, 'auto_refresh/protocol.coffee')
  add_file_to_head(:js, html_doc, 'auto_refresh/auto_refresh.coffee')
  add_script_to_head(html_doc, 'var auto_refresh = new AutoRefresh(window, console);')
end

#add_file_to_head(type, html_doc, file_path) ⇒ Object

Parameters:

  • html_doc (Nokogiri::HTML::Document)


295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/epuber/server.rb', line 295

def add_file_to_head(type, html_doc, file_path)
  head = html_doc.at_css('head')
  node = case type
         when :style
           html_doc.create_element('link', href: "/server/raw/#{file_path}", rel: 'stylesheet', type: 'text/css')
         when :js
           html_doc.create_element('script', src: "/server/raw/#{file_path}", type: 'text/javascript')
         else
           raise "Unknown file type `#{type}`"
         end

  return if head.css('script, link').any? do |n|
              (!n['href'].nil? && n['href'] == node['href']) || (!n['src'].nil? && n['src'] == node['src'])
            end

  head.add_child(node)
end

#add_keyboard_control_script(html_doc, previous_path, next_path) ⇒ Object

Parameters:

  • html_doc (Nokogiri::HTML::Document)


287
288
289
290
291
# File 'lib/epuber/server.rb', line 287

def add_keyboard_control_script(html_doc, previous_path, next_path)
  add_script_file_to_head(html_doc, 'keyboard_control.coffee',
                          '$previous_path' => previous_path,
                          '$next_path' => next_path)
end

#add_meta_to_head(name, content, html_doc, force: false) ⇒ Object

Parameters:

  • html_doc (Nokogiri::HTML::Document)
  • key (String)
  • value (String)


317
318
319
320
321
322
323
324
325
326
# File 'lib/epuber/server.rb', line 317

def add_meta_to_head(name, content, html_doc, force: false)
  head = html_doc.at_css('head')
  meta = head.at_css("meta[name=\"#{name}\"]")
  return if force == false && !meta.nil?

  meta ||= html_doc.create_element('meta')
  meta['name'] = name
  meta['content'] = content
  head.add_child(meta)
end

#add_script_file_to_head(html_doc, file_name, *args) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/epuber/server.rb', line 240

def add_script_file_to_head(html_doc, file_name, *args)
  source = File.read(File.expand_path("server/#{file_name}", File.dirname(__FILE__)))

  source = CoffeeScript.compile(source) if File.extname(file_name) == '.coffee'

  args.each do |hash|
    hash.each do |key, value|
      opt_value = if value
                    "'#{value}'"
                  else
                    'null'
                  end
      source.gsub!(key, opt_value)
    end
  end

  source = yield source if block_given?
  add_script_to_head(html_doc, source)
end

#add_script_to_head(html_doc, script_text) ⇒ Object

Parameters:

  • html_doc (Nokogiri::HTML::Document)
  • script_text (String)


263
264
265
266
267
268
269
270
271
272
273
# File 'lib/epuber/server.rb', line 263

def add_script_to_head(html_doc, script_text)
  script_node = html_doc.create_element('script', script_text, type: 'text/javascript')

  head = html_doc.at_css('head')

  if head.nil?
    head = html_doc.create_element('head')
    html_doc.at_css('html').add_child(head)
  end
  head.add_child(script_node)
end

#bookEpuber::Book::Book

Returns:

  • (Epuber::Book::Book)


73
# File 'lib/epuber/server.rb', line 73

instance_class_accessor :book

#build_pathString

Returns base path.

Returns:

  • (String)

    base path



178
179
180
# File 'lib/epuber/server.rb', line 178

def build_path
  self.class.build_path
end

#file_resolverEpuber::Compiler::FileResolver



85
# File 'lib/epuber/server.rb', line 85

instance_class_accessor :file_resolver

#find_file(pattern = params[:splat].first, source_path: build_path) ⇒ String

Returns path to file.

Parameters:

  • pattern (String) (defaults to: params[:splat].first)

Returns:

  • (String)

    path to file



215
216
217
218
# File 'lib/epuber/server.rb', line 215

def find_file(pattern = params[:splat].first, source_path: build_path)
  finder = Compiler::FileFinders::Normal.new(source_path)
  finder.find_files(pattern).first
end

#handle_bade(file_path) ⇒ (Fixnum, String)

Parameters:

  • file_path (String)

    path to bade file to render

Returns:

  • ((Fixnum, String))


50
51
52
53
54
55
# File 'lib/epuber/server/handlers.rb', line 50

def handle_bade(file_path)
  [200, self.class.render_bade(file_path)]
rescue StandardError => e
  env['sinatra.error'] = e
  ShowExceptions.new(self).call(env)
end

#handle_file(file_path) ⇒ Object

Parameters:

  • file_path (String)


486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/epuber/server.rb', line 486

def handle_file(file_path)
  return not_found unless File.exist?(file_path)

  mtime = File.mtime(file_path)
  last_modified(mtime)
  etag(mtime.to_s)
  cache_control :public, :must_revalidate

  case File.extname(file_path)
  when '.styl'
    content_type('text/css')
    body(Stylus.compile(::File.new(file_path)))
  when '.coffee'
    content_type('text/javascript')
    body(CoffeeScript.compile(::File.read(file_path)))
  else
    extname = File.extname(file_path)
    type    = unless Compiler::FileFinders::BINARY_EXTENSIONS.include?(extname)
                mime_type = MIME::Types.of(file_path).first
                if mime_type.nil?
                  'text/plain'
                else
                  content_type
                end
              end

    send_file(file_path, type: type, last_modified: mtime)
  end
end

#handle_server_bade(file_name) ⇒ (Fixnum, String)

Parameters:

  • file_name (String)

    name of the file located in ./pages/

Returns:

  • ((Fixnum, String))


42
43
44
# File 'lib/epuber/server/handlers.rb', line 42

def handle_server_bade(file_name)
  handle_bade(File.expand_path("pages/#{file_name}", File.dirname(__FILE__)))
end

#handle_websocket(path) ⇒ Object

Parameters:

  • path (String)


454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/epuber/server.rb', line 454

def handle_websocket(path)
  _log :ws, "#{path}: start"
  request.websocket do |ws|
    thread = nil

    ws.onopen do
      sockets << ws

      ws.send({ name: :hello }.to_json)

      thread = Thread.new do
        loop do
          sleep(10)
          ws.send({ name: :heartbeat }.to_json)
        end
      end
    end

    ws.onmessage do |msg|
      _log :ws, "#{path}: received message: #{msg.inspect}"
    end

    ws.onclose do
      _log :ws, "#{path}: socket closed"
      sockets.delete(ws)
      thread.kill
    end
  end
end

#handle_xhtml_file(file_path) ⇒ (Fixnum, String)

Parameters:

  • file_path (String)

    absolute path to xhtml file

Returns:

  • ((Fixnum, String))


11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/epuber/server/handlers.rb', line 11

def handle_xhtml_file(file_path)
  html_doc = Nokogiri::XML(File.open(file_path))

  add_file_to_head(:js, html_doc, 'vendor/bower/jquery/jquery.min.js')
  add_file_to_head(:js, html_doc, 'vendor/bower/spin/spin.js')
  add_file_to_head(:js, html_doc, 'vendor/bower/cookies/cookies.min.js')
  add_file_to_head(:js, html_doc, 'vendor/bower/uri/URI.min.js')
  add_file_to_head(:js, html_doc, 'vendor/bower/keymaster/keymaster.js')
  add_file_to_head(:style, html_doc, 'book_content.styl')

  add_file_to_head(:js, html_doc, 'support.coffee')
  add_meta_to_head(:viewport, 'width=device-width, initial-scale=1.0', html_doc)
  add_auto_refresh_script(html_doc)

  unless file_resolver.nil?
    current_index = file_resolver.spine_files.index { |file| file.final_destination_path == file_path }

    unless current_index.nil?
      previous_path = spine_file_at(current_index - 1).try(:pkg_destination_path)
      next_path     = spine_file_at(current_index + 1).try(:pkg_destination_path)
      add_keyboard_control_script(html_doc, previous_path, next_path)
    end
  end

  [200, html_doc.to_xhtml]
end

#listenerListener

Returns:

  • (Listener)


101
# File 'lib/epuber/server.rb', line 101

instance_class_accessor :listener

#socketsArray<SinatraWebsocket::Connection>

Returns:

  • (Array<SinatraWebsocket::Connection>)


93
# File 'lib/epuber/server.rb', line 93

instance_class_accessor :sockets

#spineArray<Epuber::Compiler::File>

Returns:

  • (Array<Epuber::Compiler::File>)


89
# File 'lib/epuber/server.rb', line 89

instance_class_accessor :spine

#spine_file_at(index) ⇒ Epuber::Book::File?

Parameters:

  • index (Fixnum)

Returns:

  • (Epuber::Book::File, nil)


223
224
225
226
227
# File 'lib/epuber/server.rb', line 223

def spine_file_at(index)
  return unless !file_resolver.nil? && index >= 0 && index < file_resolver.spine_files.count

  file_resolver.spine_files[index]
end

#targetEpuber::Book::Target



77
# File 'lib/epuber/server.rb', line 77

instance_class_accessor :target

#verboseFalseClass, TrueClass

Returns:

  • (FalseClass, TrueClass)


81
# File 'lib/epuber/server.rb', line 81

instance_class_accessor :verbose