forked from boazsegev/facil.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver
executable file
·342 lines (303 loc) · 11.9 KB
/
server
1
2
3
4
5
6
7
8
9
10
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
#!/usr/bin/env ruby
# About This Script:
# =================
#
# This is a quilted script torn and patched from a number of different projects I wrote here and there.
#
# I wrote this script specifically to handle the GitHub pages environment, so I could post static pages.
#
# I wouldn't recommend trying to read the code, it will only give you a headache...
#
# ...even I'm not sure what it does or how. I just threw it together to have something that works.
# Gems and stuff we use in this script
require 'redcarpet'
require 'erb'
require 'slim'
require 'sass'
require 'iodine'
require 'fileutils'
require 'yaml'
require 'set'
require 'json'
require 'rouge'
require 'rouge/plugins/redcarpet'
# The folder for source files
SOURCE_ROOT = File.dirname(__FILE__)
# The output folder for the static site
STATIC_ROOT = File.join(File.dirname(__FILE__), "..")
# File / folder names to be excluded from the script
EXCLUDE = [File.basename(__FILE__), "layout.html.slim", "Gemfile", "layouts"]
# Constants for Rack and HTTP headers for Iodine's X-Sendfile support
PATH_INFO = 'PATH_INFO'.freeze
X_SENDFILE = 'X-Sendfile'.freeze
# # We don't need this, but we might
# unless File.directory?(STATIC_ROOT)
# Dir.mkdir(STATIC_ROOT, 0777)
# end
# A README.md file that will be placed in the static site's folder
README = <<EOS
# Contributing to the Website / Documentation
Thank you for your interest in contributing to the facil.io website and documentation.
NOTICE: `_SOURCE` is the folder that contains the actual documentation files. Edits to the documentation should be placed in this folder.
Anything outside the `_SOURCE` folder (including this file) is created automatically by the `server` script and shouldn't be edited.
If you want to contribute to the documentation, please do so by opening a Pull Request (PR) with updates to the files in the `_SOURCE` folder.
## Running the website locally
It's possible to run a local version of the website using Ruby (make sure to have Ruby and Ruby gems available on your system).
Open the terminal window and go to the `_SOURCE` folder. Than run (currently runs on macOS and Linux):
$ bundle install
$ ./server
EOS
IO.binwrite(File.join(STATIC_ROOT, "README.md"), README)
# Schema Description for the layout template
SCHEMA_ABOUT = "facil.io - a light web application framework in C, with support for HTTP, WebSockets and Pub/Sub out of the box.".freeze
# Schema JSON for the layout template
SCHEMA_ORG = {
'@context' => 'http://schema.org',
'@type' => 'WebSite',
url: 'http://facil.io',
name: 'facil.io',
description: SCHEMA_ABOUT,
keywords: 'C, web, framework, websockets, websocket, realtime, real-time, easy',
image: 'http://facil.io/website/logo/facil-io.svg',
# potentialAction: {
# "@type" => "SearchAction",
# target: "http://example.com/search?&q={query}",
# "query-input" => "required",
# },
author: [
{
'@type' => 'Person',
name: 'Bo (Myst)',
url: 'http://stackoverflow.com/users/4025095/myst',
email: 'bo(at)facil.io'
}
],
sourceOrganization: {
'@context' => 'http://schema.org',
'@type' => 'Organization',
name: 'Plezi',
url: 'http://facil.io',
description: SCHEMA_ABOUT,
logo: 'http://facil.io/website/logo/facil-io.svg',
image: 'http://facil.io/website/logo/facil-io.svg',
email: 'bo(at)facil.io',
member: [
{
'@type' => 'Person',
name: 'Bo (Myst)',
url: 'http://stackoverflow.com/users/4025095/myst',
email: 'bo(at)facil.io'
}
]
}
}.to_json
# The Rack application - this is where things get messy.
#
# This module does it all - it "bakes" pages into static pages as well as allows Rack to serve the updated version.
#
# In production mode (which we don't need), the static pages will be served directly once they were baked (no live updates).
module APP
# for the sitemap data
@sitemap = {}.to_set
# This HashMap will map file extensions to a Proc that will render the file
@extensions = {}
# File extensions that might require a page to be rendered (unlike jpeg, which is passed through)
@bakers = %w{.css .html .js}.to_set
# Converts templates to static pages and saves the pages to the static location.
def self.bake_all
@sitemap.clear
# things that need to be rendered
@extensions.keys.each do |k|
Dir[File.join SOURCE_ROOT, '**', "*#{k}"].each do |pt|
next if EXCLUDE.include?( File.basename(pt)) || File.basename(pt).start_with?('_')
begin
env = {PATH_INFO => pt[SOURCE_ROOT.length..(-1-k.length)]}
APP.call(env)
puts "INFO: pre-baked: #{env[PATH_INFO]}"
@sitemap << env[PATH_INFO]
rescue => e
puts "WARN: couldn't pre-bake #{pt}: #{e.message}"
raise e
end
end
end
# things that need to be copied
Dir[File.join SOURCE_ROOT, '**', "*"].each do |pt|
next if EXCLUDE.include?( File.basename(pt)) || File.basename(pt).start_with?('_')
unless @extensions[File.extname(pt)] || File.directory?(pt) || (File.expand_path(pt) == File.expand_path(__FILE__))
begin
target = pt[SOURCE_ROOT.length..-1]
bake target, IO.binread(pt)
puts "INFO: copied #{pt} to #{target}"
# @sitemap << target
rescue => e
puts "WARN: bake copy failed at #{pt}: #{e.message}"
end
end
end
# output sitemap
out = "".dup
@sitemap.each {|url| out << "http://facil.io#{url[0...-5]}\r\n" if File.extname(url) == ".html" }
out << "\r\n"
IO.binwrite File.join(STATIC_ROOT, "sitemap.txt"), out
end
# define different Rack application methods, depending on the environment.
if ENV['RACK_ENV'] == 'production'
# No live updates mean that this shouldn't have been called (maybe except to result in 404 errors)
def self.call env
puts "WARN: render was requested for #{env[PATH_INFO]}" unless env.keys.length == 1
if (File.directory?( "#{STATIC_ROOT}#{env[PATH_INFO]}"))
if (env[PATH_INFO][-1] == '/')
env[PATH_INFO] << 'index.html'.freeze
else
env[PATH_INFO] << '/index.html'.freeze
end
end
env[PATH_INFO] << ".html" if(File.extname(env[PATH_INFO]) == "")
[200, {X_SENDFILE => "#{STATIC_ROOT}#{env[PATH_INFO]}"}, ["".freeze]]
end
else
# Live update and send with X-Sendfile
def self.call env
if (File.directory?( "#{STATIC_ROOT}#{env[PATH_INFO]}"))
if (env[PATH_INFO][-1] == '/')
env[PATH_INFO] << 'index.html'.freeze
else
env[PATH_INFO] << '/index.html'.freeze
end
end
env[PATH_INFO] << ".html" if(File.extname(env[PATH_INFO]) == "".freeze)
data = render(env[PATH_INFO]) if(@bakers.include?(File.extname(env[PATH_INFO])))
[200, {X_SENDFILE => "#{STATIC_ROOT}#{env[PATH_INFO]}"}, [data]]
end
end
# Render a template / resource
def self.render path
name = File.join(SOURCE_ROOT, path).to_s
base = name[0..(-1-(File.extname(path).length))]
data = nil
@extensions.keys.each { |k| data = try_name(name, k) || try_name(base, k); break if data }
bake path, data if data
end
# Attempt rendering for a specific extension
def self.try_name name, ext
name = "#{name}#{ext}"
return nil unless File.exist?(name)
@extensions[ext].call(name)
end
# Save a (rendered) result
def self.bake path, data
return unless data
path = "#{STATIC_ROOT}#{path}"
FileUtils.mkpath File.dirname(path)
IO.binwrite path, data
data
end
# "Simple" markdown rendering
MD_EXTENSIONS = { with_toc_data: true, strikethrough: true, autolink: true, fenced_code_blocks: true, no_intra_emphasis: true, tables: true, footnotes: true, underline: true, highlight: true }.freeze
class RedcarpetWithRouge < ::Redcarpet::Render::HTML
include Rouge::Plugins::Redcarpet
def block_code(code, language)
language ||= 'c'
super
end
end
# MD_RENDERER = ::Redcarpet::Markdown.new ::Redcarpet::Render::HTML.new(MD_EXTENSIONS.dup), MD_EXTENSIONS.dup
MD_RENDERER = ::Redcarpet::Markdown.new RedcarpetWithRouge.new(MD_EXTENSIONS.dup), MD_EXTENSIONS.dup
MD_RENDERER_TOC = Redcarpet::Markdown.new Redcarpet::Render::HTML_TOC.new(MD_EXTENSIONS.dup), MD_EXTENSIONS.dup
YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
SAFE_TYPES = [Symbol, Date, Time, Encoding, Struct, Regexp, Range, Set].freeze
@extensions['.md'] = proc do |name|
# read file
data = IO.binread(name)
# collect YAML front-matter
vars = {}
front = data.match YAML_FRONT_MATTER_REGEXP
if(front)
vars = YAML.safe_load(front[1], SAFE_TYPES) || {}
data = front.post_match
end
# some sane defaults, even if there's no fron matter
unless File.basename(name).start_with?('_')
vars['layout'] ||= "layouts/layout.html.slim"
vars['sidebar'] ||= "_versions.md"
vars['toc'] = true unless vars.has_key?('toc')
end
# try mustache template rendering before rendering the Markdown
begin
data = Iodine::Mustache.render(name, vars, data)
rescue Exception => e
puts "mustache error: #{name}"
p vars
raise e
end
# Render the markdown
if(vars['toc'])
data = "<div class='toc'>#{MD_RENDERER_TOC.render(data)}</div>#{MD_RENDERER.render(data)}"
else
data = MD_RENDERER.render(data)
end
# Attach any side-bar / layout required
layout = File.join(SOURCE_ROOT, vars['layout'].to_s).to_s
if(vars['layout'] && File.exist?(layout) && @extensions[File.extname(layout)])
# render sidebar to a String
sidebar = File.join(SOURCE_ROOT, vars['sidebar'].to_s).to_s
if(vars['sidebar'] && File.exist?(sidebar) && @extensions[File.extname(sidebar)])
vars['sidebar'] = @extensions[File.extname(sidebar)].call(sidebar)
elsif vars['sidebar']
puts "can't find #{vars['sidebar']} (extension #{File.extname(sidebar)})?"
end
# Return the layout with the data
block = proc { data }
@extensions[File.extname(layout)].call(layout, vars, block)
else
# Return the data as is (no layout)
data
end
end
# slim rendering (support variables and code blocks)
@extensions['.slim'] = proc do |name, vars, block|
engine = (Slim::Template.new { IO.binread(name).force_encoding(::Encoding::UTF_8) })
if(block)
engine.render((vars || {}), &block)
else
engine.render((vars || {}))
end
end
# Common SASS options
SASS_OPTIONS = { cache_store: Sass::CacheStores::Memory.new, style: (ENV['SASS_STYLE'] || ((ENV['ENV'] || ENV['RACK_ENV']) == 'production' ? :compressed : :nested)) }.dup
# SASS rendering
@extensions['.scss'] = @extensions['.sass'] = proc do |name|
eng = Sass::Engine.for_file(name, SASS_OPTIONS)
map_name = name.gsub /s[ac]ss$/, 'map'
map_name.gsub! /^#{SOURCE_ROOT}/, ''
css, map = eng.render_with_sourcemap(File.basename(map_name))
bake map_name, map.to_json(css_uri: File.basename(name))
css
end
# erb rendering (support variables and code blocks)
@extensions['.erb'] = proc do |name, vars, block|
vars ||= {}
engine = ::ERB.new(IO.binread(name).force_encoding(::Encoding::UTF_8))
if(block)
engine.result(vars.binding(&block), &block)
else
engine.result
end
end
end
# Copy the updated CHANGELOG.md file to the source folder
FileUtils.cp(File.join(STATIC_ROOT, '..', 'CHANGELOG.md'), File.join(SOURCE_ROOT, 'changelog.md')) rescue nil
# "Bake" the templates and copy the data
APP.bake_all
# Remove the changelog from the source folder
FileUtils.rm(File.join(SOURCE_ROOT, 'changelog.md')) rescue nil
# Setup iodine to serve static files and to run the Rack application `APP`
Iodine.listen service: :http, handler: APP, log: true, public: ((ENV['RACK_ENV'] == 'production') ? STATIC_ROOT : SOURCE_ROOT)
# If no threads / processes were setup, use half the cores for multi-threading and a single process
if(Iodine.threads == 0 && Iodine.workers == 0)
Iodine.threads =-2;
Iodine.workers =1;
end
# Start up the server
Iodine.start