archival 0.0.0 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/bin/ruby-rewrite ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-rewrite' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("parser", "ruby-rewrite")
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/exe/archival ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'archival'
5
+
6
+ VALID_COMMANDS = %w[
7
+ build
8
+ run
9
+ ].freeze
10
+
11
+ command = ARGV[0]
12
+
13
+ unless !command || VALID_COMMANDS.include?(command)
14
+ raise StandardError,
15
+ "Invalid command #{command}"
16
+ end
17
+
18
+ build_dir = Dir.pwd
19
+
20
+ case command
21
+ when 'build'
22
+ Archival::Logger.benchmark('built') do
23
+ config = Archival::Config.new('root' => build_dir)
24
+ builder = Archival::Builder.new(config)
25
+ builder.write_all
26
+ end
27
+ when 'run'
28
+ Archival.listen('root' => build_dir)
29
+ else
30
+ # print help
31
+ puts 'archival [command]'
32
+ puts ''
33
+ puts 'Commands:'
34
+ puts ' build Builds the current directory as an archival website.'
35
+ puts ' run Runs the current directory in development mode, '
36
+ + 'recompiling when files change.'
37
+ end
@@ -0,0 +1,91 @@
1
+ /**
2
+ * When running archival in development mode (archival run), this file is
3
+ * injected into all pages, and is responsible for reloading the page when the
4
+ * source has changed.
5
+ */
6
+
7
+ (function () {
8
+ const remotePort = $PORT;
9
+ const CONNECTING_COLOR = "#bd270d";
10
+ const CONNECTED_COLOR = "#19bd0d";
11
+ const CHECK_INTERVAL = 500;
12
+ const DISCONNECTED_INTERVAL = 1000;
13
+ const connectionDot = document.createElement("div");
14
+ connectionDot.style = `position: absolute; z-index: 9999; bottom: 10px; right: 10px; background-color: ${CONNECTING_COLOR}; width: 15px; height: 15px; border-radius: 50%; opacity: 0.8;`;
15
+ connectionDot.setAttribute("title", "Archival Dev Server: Connecting");
16
+ connectionDot.addEventListener(
17
+ "mouseenter",
18
+ () => (connectionDot.style.opacity = 0.2)
19
+ );
20
+ connectionDot.addEventListener(
21
+ "mouseleave",
22
+ () => (connectionDot.style.opacity = 0.8)
23
+ );
24
+
25
+ let lastContact = -1;
26
+ let isConnecting = false;
27
+ let connection;
28
+
29
+ function connectionLoop() {
30
+ connection.send(`page:${window.location.pathname}`);
31
+ if (Date.now() - lastContact > DISCONNECTED_INTERVAL) {
32
+ setConnected(false);
33
+ connectSocket();
34
+ }
35
+ setTimeout(connectionLoop, CHECK_INTERVAL);
36
+ }
37
+
38
+ function setConnected(connected) {
39
+ connectionDot.style.backgroundColor = connected
40
+ ? CONNECTED_COLOR
41
+ : CONNECTING_COLOR;
42
+ connectionDot.setAttribute(
43
+ "title",
44
+ `Archival Dev Server: ${connected ? "Connected" : "Disconnected"}`
45
+ );
46
+ }
47
+
48
+ window.onload = () => {
49
+ connectSocket(true);
50
+ };
51
+
52
+ function connectSocket(init) {
53
+ if (isConnecting) {
54
+ return;
55
+ }
56
+ isConnecting = true;
57
+ console.log(
58
+ `${init ? "connecting" : "reconnecting"} to archival dev server...`
59
+ );
60
+ document.body.appendChild(connectionDot);
61
+ connection = new WebSocket(`ws://localhost:${remotePort}`);
62
+ connection.onerror = () => {
63
+ isConnecting = false;
64
+ };
65
+
66
+ connection.onopen = () => {
67
+ isConnecting = false;
68
+ connection.send("connected");
69
+ if (init) {
70
+ connectionLoop();
71
+ }
72
+ };
73
+ connection.onmessage = (event) => {
74
+ lastContact = Date.now();
75
+ switch (event.data) {
76
+ case "ready":
77
+ console.log("connected to archival dev server.");
78
+ break;
79
+ case "ok":
80
+ setConnected(true);
81
+ break;
82
+ case "refresh":
83
+ window.location.reload();
84
+ break;
85
+ default:
86
+ console.log(`receieved unexpected message ${event.data}`);
87
+ break;
88
+ }
89
+ };
90
+ }
91
+ })();
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'liquid'
4
+ require 'tomlrb'
5
+ require 'tags/layout'
6
+ require 'redcarpet'
7
+
8
+ Liquid::Template.error_mode = :strict
9
+ Liquid::Template.register_tag('layout', Layout)
10
+
11
+ module Archival
12
+ class Builder
13
+ attr_reader :page_templates
14
+
15
+ def initialize(config, *_args)
16
+ @config = config
17
+ @markdown = Redcarpet::Markdown.new(
18
+ Redcarpet::Render::HTML.new(prettify: true,
19
+ hard_wrap: true), no_intra_emphasis: true,
20
+ fenced_code_blocks: true,
21
+ autolink: true,
22
+ strikethrough: true,
23
+ underline: true
24
+ )
25
+ refresh_config
26
+ end
27
+
28
+ def refresh_config
29
+ @file_system = Liquid::LocalFileSystem.new(
30
+ File.join(@config.root, @config.pages_dir), '%s.liquid'
31
+ )
32
+ @variables = {}
33
+ @object_types = {}
34
+ @page_templates = {}
35
+
36
+ Liquid::Template.file_system = Liquid::LocalFileSystem.new(
37
+ File.join(@config.root, @config.pages_dir), '_%s.liquid'
38
+ )
39
+
40
+ objects_definition_file = File.join(@config.root,
41
+ 'objects.toml')
42
+ if File.file? objects_definition_file
43
+ @object_types = Tomlrb.load_file(objects_definition_file)
44
+ end
45
+
46
+ update_pages
47
+ update_objects
48
+ end
49
+
50
+ def full_rebuild
51
+ Layout.reset_cache
52
+ refresh_config
53
+ end
54
+
55
+ def update_pages
56
+ do_update_pages(File.join(@config.root, @config.pages_dir))
57
+ end
58
+
59
+ def do_update_pages(dir, prefix = nil)
60
+ add_prefix = lambda { |entry|
61
+ prefix ? File.join(prefix, entry) : entry
62
+ }
63
+ Dir.foreach(dir) do |entry|
64
+ if File.directory? entry
65
+ unless [
66
+ '.', '..'
67
+ ].include?(entry)
68
+ update_pages(File.join(dir, entry),
69
+ add_prefix(entry))
70
+ end
71
+ elsif File.file? File.join(dir, entry)
72
+ if entry.end_with?('.liquid') && !(entry.start_with? '_')
73
+ page_name = File.basename(entry,
74
+ '.liquid')
75
+ template_file = add_prefix.call(page_name)
76
+ content = @file_system.read_template_file(template_file)
77
+ content += dev_mode_content if @config.dev_mode
78
+ @page_templates[add_prefix.call(page_name)] =
79
+ Liquid::Template.parse(content)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def update_objects
86
+ do_update_objects(File.join(@config.root,
87
+ @config.objects_dir))
88
+ end
89
+
90
+ def do_update_objects(dir)
91
+ objects = {}
92
+ @object_types.each do |name, definition|
93
+ objects[name] = {}
94
+ obj_dir = File.join(dir, name)
95
+ if File.directory? obj_dir
96
+ Dir.foreach(obj_dir) do |file|
97
+ if file.end_with? '.toml'
98
+ object = Tomlrb.load_file(File.join(
99
+ obj_dir, file
100
+ ))
101
+ object[:name] =
102
+ File.basename(file, '.toml')
103
+ objects[name][object[:name]] = parse_object(object, definition)
104
+ end
105
+ end
106
+ end
107
+ objects[name] = sort_objects(objects[name])
108
+ end
109
+ @variables['objects'] = objects
110
+ end
111
+
112
+ def sort_objects(objects)
113
+ # Since objects are hashes but we'd like them to be iterable based on
114
+ # arbitrary "order" keys, and in ruby hashes enumerate in insert order,
115
+ # we just need to re-insert in the correct order.
116
+ sorted_keys = objects.sort_by do |name, obj|
117
+ obj['order'].to_s || name
118
+ end
119
+ sorted_objects = {}
120
+ sorted_keys.each do |d|
121
+ sorted_objects[d[0]] = d[1]
122
+ end
123
+ sorted_objects
124
+ end
125
+
126
+ def parse_object(object, definition)
127
+ definition.each do |name, type|
128
+ case type
129
+ when 'markdown'
130
+ object[name] = @markdown.render(object[name]) if object[name]
131
+ end
132
+ end
133
+ object
134
+ end
135
+
136
+ def set_var(name, value)
137
+ @variables[name] = value
138
+ end
139
+
140
+ def render(page)
141
+ template = @page_templates[page]
142
+ template.render(@variables)
143
+ end
144
+
145
+ def write_all
146
+ Dir.mkdir(@config.build_dir) unless File.exist? @config.build_dir
147
+ @page_templates.each_key do |template|
148
+ out_dir = File.join(@config.build_dir,
149
+ File.dirname(template))
150
+ Dir.mkdir(out_dir) unless File.exist? out_dir
151
+ out_path = File.join(@config.build_dir,
152
+ "#{template}.html")
153
+ File.open(out_path, 'w+') do |file|
154
+ file.write(render(template))
155
+ end
156
+ end
157
+ return if @config.dev_mode
158
+
159
+ # in production, also copy all assets to the dist folder.
160
+ @config.assets_dirs.each do |asset_dir|
161
+ FileUtils.copy_entry File.join(@config.root, asset_dir),
162
+ File.join(@config.build_dir, asset_dir)
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ def dev_mode_content
169
+ "<script src=\"http://localhost:#{@config.helper_port}/js/archival-helper.js\" type=\"application/javascript\"></script>" # rubocop:disable Layout/LineLength
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tomlrb'
4
+
5
+ module Archival
6
+ class Config
7
+ attr_reader :pages_dir, :objects_dir, :assets_dirs, :root, :build_dir,
8
+ :helper_port, :dev_mode
9
+
10
+ def initialize(config = {})
11
+ @root = config['root'] || Dir.pwd
12
+ manifest = load_manifest
13
+ @pages_dir = config['pages'] || manifest['pages'] || 'pages'
14
+ @objects_dir = config['objects'] || manifest['objects'] || 'objects'
15
+ @build_dir = config['build_dir'] || manifest['build_dir'] || File.join(
16
+ @root, 'dist'
17
+ )
18
+ @helper_port = config['helper_port'] || manifest['helper_port'] || 2701
19
+ @assets_dirs = config['assets_dirs'] || manifest['assets'] || []
20
+ @dev_mode = config[:dev_mode] || false
21
+ end
22
+
23
+ def load_manifest
24
+ manifest_file = File.join(@root, 'manifest.toml')
25
+ return Tomlrb.load_file(manifest_file) if File.file? manifest_file
26
+
27
+ {}
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'open-uri'
5
+
6
+ module Archival
7
+ class HelperServer
8
+ attr_reader :page
9
+
10
+ def initialize(port, build_dir)
11
+ @port = port
12
+ @build_dir = build_dir
13
+ @helper_dir = File.expand_path(File.join(File.dirname(__FILE__),
14
+ '../../helper'))
15
+ end
16
+
17
+ def start
18
+ server = TCPServer.new @port
19
+ loop do
20
+ Thread.start(server.accept) do |client|
21
+ req = ''
22
+ method = nil
23
+ path = nil
24
+ while (line = client.gets) && (line != "\r\n")
25
+ unless method
26
+ req_info = line.split
27
+ method = req_info[0]
28
+ path = req_info[1]
29
+ end
30
+ req += line
31
+ end
32
+ client.close unless req
33
+ handle_request(client, req, method, path)
34
+ end
35
+ end
36
+ end
37
+
38
+ def refresh_client
39
+ ws_sendmessage('refresh')
40
+ end
41
+
42
+ private
43
+
44
+ MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
45
+
46
+ def handle_request(client, req, method, path)
47
+ if method == 'GET' && path.start_with?('/js/')
48
+ # For static paths, just serve the files they refer to.
49
+ http_response(client, type: 'application/javascript') do
50
+ serve_static(client, path)
51
+ end
52
+ client.close
53
+ elsif (matches = req.match(/^Sec-WebSocket-Key: (\S+)/))
54
+ websocket_key = matches[1]
55
+ # puts "Websocket handshake detected with key: #{websocket_key}"
56
+ connect_socket(client, websocket_key)
57
+ else
58
+ client.close
59
+ end
60
+ end
61
+
62
+ def connect_socket(client, websocket_key)
63
+ @socket = client
64
+ response_key = Digest::SHA1.base64digest([websocket_key,
65
+ MAGIC_GUID].join)
66
+ # puts "Responding to handshake with key: #{response_key}"
67
+
68
+ @socket.write "HTTP/1.1 101 Switching Protocols\r\n"
69
+ @socket.write "Upgrade: websocket\r\n"
70
+ @socket.write "Connection: Upgrade\r\n"
71
+ @socket.write "Sec-WebSocket-Accept: #{response_key}\r\n"
72
+ @socket.write "\r\n"
73
+
74
+ # puts 'Handshake completed.'
75
+ ws_loop
76
+ end
77
+
78
+ def ws_loop
79
+ loop do
80
+ msg = ws_getmessage
81
+ next unless msg
82
+
83
+ if msg == 'connected'
84
+ ws_sendmessage('ready')
85
+ elsif msg.start_with?('page:')
86
+ page_path = Pathname.new(msg.sub(/^page:/, ''))
87
+ @page = page_path.relative_path_from(@build_dir)
88
+ ws_sendmessage('ok')
89
+ end
90
+ end
91
+ end
92
+
93
+ def validate_ws_message
94
+ first_byte = @socket.getbyte
95
+ return unless first_byte
96
+
97
+ fin = first_byte & 0b10000000
98
+ opcode = first_byte & 0b00001111
99
+
100
+ # Our server only supports single-frame, text messages.
101
+ # Raise an exception if the client tries to send anything else.
102
+ raise "We don't support continuations" unless fin
103
+ raise 'We only support opcode 1' unless opcode == 1
104
+
105
+ second_byte = @socket.getbyte
106
+ is_masked = second_byte & 0b10000000
107
+ payload_size = second_byte & 0b01111111
108
+
109
+ raise 'frame masked incorrectly' unless is_masked
110
+ raise 'payload must be < 126 bytes in length' unless payload_size < 126
111
+
112
+ payload_size
113
+ end
114
+
115
+ def ws_getmessage
116
+ payload_size = validate_ws_message
117
+ return unless payload_size
118
+
119
+ # warn "Payload size: #{payload_size} bytes"
120
+
121
+ mask = 4.times.map { @socket.getbyte }
122
+ # warn "Got mask: #{mask.inspect}"
123
+
124
+ data = payload_size.times.map { @socket.getbyte }
125
+ # warn "Got masked data: #{data.inspect}"
126
+
127
+ unmasked_data = data.each_with_index.map do |byte, i|
128
+ byte ^ mask[i % 4]
129
+ end
130
+ # warn "Unmasked the data: #{unmasked_data.inspect}"
131
+
132
+ unmasked_data.pack('C*').force_encoding('utf-8')
133
+ end
134
+
135
+ def ws_sendmessage(message)
136
+ return unless @socket
137
+
138
+ output = [0b10000001, message.size, message]
139
+ @socket.write output.pack("CCA#{message.size}")
140
+ end
141
+
142
+ def serve_static(client, path)
143
+ buffer = File.open(File.join(@helper_dir, path)).read
144
+ buffer.sub! '$PORT', @port.to_s
145
+ client.print buffer
146
+ end
147
+
148
+ def http_response(client, config)
149
+ status = config[:status] ||= 200
150
+ type = config[:type] ||= 'text/html'
151
+ client.print "HTTP/1.1 #{status}\r\n"
152
+ client.print "Content-Type: #{type}\r\n"
153
+ client.print "\r\n"
154
+ yield
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'listen'
4
+ require 'pathname'
5
+
6
+ module Archival
7
+ def listen(config = {})
8
+ @config = Config.new(config.merge(dev_mode: true))
9
+ builder = Builder.new(@config)
10
+ Logger.benchmark('built') do
11
+ builder.write_all
12
+ end
13
+ ignore = %r{/dist/}
14
+ listener = Listen.to(@config.root,
15
+ ignore: ignore) do |modified, added, removed|
16
+ updated_pages = []
17
+ updated_objects = []
18
+ updated_assets = []
19
+ (modified + added + removed).each do |file|
20
+ case change_type(file)
21
+ when :pages
22
+ updated_pages << file
23
+ when :objects
24
+ updated_objects << file
25
+ when :assets
26
+ updated_assets << file
27
+ end
28
+ end
29
+ @server.refresh_client if rebuild?(builder, updated_objects,
30
+ updated_pages, updated_assets)
31
+ end
32
+ listener.start
33
+ serve_helpers
34
+ end
35
+
36
+ module_function :listen
37
+
38
+ class << self
39
+ private
40
+
41
+ def child?(parent, child)
42
+ path = Pathname.new(child)
43
+ return true if path.fnmatch?(File.join(parent, '**'))
44
+
45
+ false
46
+ end
47
+
48
+ def change_type(file)
49
+ # a page was modified, rebuild the pages.
50
+ return :pages if child?(File.join(@config.root, @config.pages_dir),
51
+ file)
52
+ # an object was modified, rebuild the objects.
53
+ return :objects if child?(File.join(@config.root, @config.objects_dir),
54
+ file)
55
+
56
+ # layout and other assets. For now, this is everything.
57
+ @config.assets_dirs.each do |dir|
58
+ return :assets if child?(File.join(@config.root, dir), file)
59
+ end
60
+ return :assets if child?(File.join(@config.root, 'layout'), file)
61
+ return :assets if ['manifest.toml',
62
+ 'objects.toml'].include? File.basename(file)
63
+
64
+ :none
65
+ end
66
+
67
+ def rebuild?(builder, updated_objects, updated_pages, updated_assets)
68
+ if updated_pages.empty? && updated_objects.empty? && updated_assets.empty?
69
+ return false
70
+ end
71
+
72
+ Logger.benchmark('rebuilt') do
73
+ builder.update_objects if updated_objects.length
74
+ builder.update_pages if updated_pages.length
75
+ builder.full_rebuild if updated_assets.length
76
+ builder.write_all
77
+ end
78
+ true
79
+ end
80
+
81
+ def serve_helpers
82
+ @server = HelperServer.new(@config.helper_port, @config.build_dir)
83
+ @server.start
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ module Archival
6
+ class Logger
7
+ def self.benchmark(message, &block)
8
+ Benchmark.bm do |bm|
9
+ bm.report(message, &block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+ require 'rake/tasklib'
5
+ require 'archival'
6
+
7
+ class RakeTasks
8
+ # Common tasks for archival.
9
+ #
10
+ # To include, just add
11
+ #
12
+ # require 'archival/rake_tasks'
13
+ #
14
+ # to your Rakefile.
15
+ include Rake::DSL if defined? Rake::DSL
16
+
17
+ class << self
18
+ # set when install'd.
19
+ attr_accessor :instance
20
+
21
+ def install_tasks
22
+ new.install
23
+ end
24
+ end
25
+
26
+ def install
27
+ build_dir = Dir.pwd
28
+
29
+ task 'build' do
30
+ Archival::Logger.benchmark('built') do
31
+ config = Archival::Config.new('root' => build_dir)
32
+ builder = Archival::Builder.new(config)
33
+ builder.write_all
34
+ end
35
+ end
36
+
37
+ task 'run' do
38
+ Archival.listen('root' => build_dir)
39
+ end
40
+
41
+ RakeTasks.instance = self
42
+ end
43
+ end
44
+
45
+ RakeTasks.install_tasks
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archival
4
+ VERSION = '0.0.5'
5
+ end
data/lib/archival.rb CHANGED
@@ -1,3 +1,12 @@
1
+ # frozen_string_literal: true
1
2
 
2
- class Archival
3
- end
3
+ module Archival
4
+ # Main Archival module. See https://archival.dev for docs.
5
+ end
6
+
7
+ require 'archival/version'
8
+ require 'archival/logger'
9
+ require 'archival/config'
10
+ require 'archival/helper_server'
11
+ require 'archival/builder'
12
+ require 'archival/listen'