Class: Sakusei::PreviewServer
- Inherits:
-
Object
- Object
- Sakusei::PreviewServer
- 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
-
#initialize(source_file, options = {}) ⇒ PreviewServer
constructor
A new instance of PreviewServer.
- #run ⇒ Object
Constructor Details
#initialize(source_file, options = {}) ⇒ PreviewServer
Returns a new instance of PreviewServer.
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, = {}) @source_file = File.(source_file) raise Error, "File not found: #{source_file}" unless File.exist?(@source_file) @options = @port = [:port] || DEFAULT_PORT @open_browser = .fetch(:open, true) @use_paged_js = .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
#run ⇒ Object
148 149 150 151 152 |
# File 'lib/sakusei/preview_server.rb', line 148 def run build_now(initial: true) start_listeners start_server end |