Class: Sakusei::PreviewServer

Inherits:
Object
  • Object
show all
Defined in:
lib/sakusei/preview_server.rb

Overview

Live-reload preview server. Watches a markdown source and its dependencies (style pack, Vue components, @include partials, images) and re-renders to styled HTML on change. Browser long-polls /__events to know when to update; updates apply incrementally via paged.js’s Previewer API (no full reload).

Constant Summary collapse

DEFAULT_PORT =
4567
DEBOUNCE_SECONDS =
0.05
POLL_TIMEOUT_SECONDS =
30
PAGED_JS_CDN =
'https://unpkg.com/pagedjs/dist/paged.js'
PREVIEW_CSS =
<<~CSS
  html, body {
    background: #2a2a2a;
    margin: 0;
    padding: 0;
  }
  .pagedjs_pages {
    padding: 24px 0;
  }
  .pagedjs_page {
    background: white;
    margin: 24px auto !important;
    box-shadow: 0 8px 32px rgba(0,0,0,0.55);
  }
  #__sakusei_render {
    min-height: 100vh;
  }
  #__sakusei_render.no-paged {
    max-width: 850px;
    margin: 24px auto;
    padding: 48px 64px;
    background: white;
    box-shadow: 0 8px 32px rgba(0,0,0,0.55);
  }
CSS
PREVIEW_JS =
<<~'JS'
  (function() {
    var version = window.__SAKUSEI_VERSION__ || 0;
    var renderTarget = null;
    var ready = false;

    function fullReload() { window.location.reload(); }

    function render(html) {
      renderTarget.innerHTML = '';
      renderTarget.classList.remove('no-paged');
      if (typeof Paged === 'undefined' || !Paged.Previewer) {
        renderTarget.classList.add('no-paged');
        renderTarget.innerHTML = html;
        return Promise.resolve();
      }
      var previewer = new Paged.Previewer();
      return previewer.preview(html, undefined, renderTarget).catch(function(e) {
        console.error('[sakusei-preview] paged.js failed:', e);
        renderTarget.classList.add('no-paged');
        renderTarget.innerHTML = html;
      });
    }

    function init() {
      var sourceHTML = document.body.innerHTML;
      document.body.innerHTML = '';
      renderTarget = document.createElement('div');
      renderTarget.id = '__sakusei_render';
      document.body.appendChild(renderTarget);
      render(sourceHTML).then(function() {
        ready = true;
        poll();
      });
    }

    function refresh(newVersion) {
      fetch('/__content').then(function(r) {
        if (!r.ok) throw new Error('content fetch failed');
        return r.text();
      }).then(function(html) {
        var scrollY = window.scrollY;
        version = newVersion;
        return render(html).then(function() {
          window.scrollTo(0, scrollY);
          poll();
        });
      }).catch(function(e) {
        console.warn('[sakusei-preview] partial update failed, reloading:', e);
        fullReload();
      });
    }

    function poll() {
      if (!ready) return;
      fetch('/__events?since=' + version).then(function(r) {
        if (r.status === 200) {
          return r.text().then(function(t) {
            var v = parseInt(t, 10);
            if (v > version) {
              refresh(v);
            } else {
              poll();
            }
          });
        }
        setTimeout(poll, 50);
      }).catch(function() {
        setTimeout(poll, 1000);
      });
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', init);
    } else {
      init();
    }
  })();
JS

Instance Method Summary collapse

Constructor Details

#initialize(source_file, options = {}) ⇒ PreviewServer

Returns a new instance of PreviewServer.

Raises:



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/sakusei/preview_server.rb', line 125

def initialize(source_file, options = {})
  @source_file = File.expand_path(source_file)
  raise Error, "File not found: #{source_file}" unless File.exist?(@source_file)

  @options = options
  @port = options[:port] || DEFAULT_PORT
  @open_browser = options.fetch(:open, true)
  @use_paged_js = options.fetch(:paged, true)

  @source_dir = File.dirname(@source_file)
  @lock = Mutex.new
  @cv = ConditionVariable.new
  @version = 0
  @cached_html = nil
  @cached_body = nil
  @cached_error = nil
  @style_pack_path = nil

  @rebuild_lock = Mutex.new
  @debounce_timer = nil
  @shutting_down = false
end

Instance Method Details

#runObject



148
149
150
151
152
# File 'lib/sakusei/preview_server.rb', line 148

def run
  build_now(initial: true)
  start_listeners
  start_server
end