Exorcise harp and node.js, server, and add Swift plan

This commit is contained in:
Sami Samhuri 2019-12-01 10:59:05 -08:00
parent 309c7dddc0
commit 1e6348dbde
26 changed files with 81 additions and 10016 deletions

5
.gitignore vendored
View file

@ -1,6 +1 @@
.bundle
node_modules
www
server/auth.json
server/test-blog*
origin-updated

43
.snyk
View file

@ -1,43 +0,0 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.7.1
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:ejs:20161128':
- harp > terraform > ejs:
patched: '2017-04-21T04:58:35.183Z'
'npm:marked:20170112':
- harp > terraform > marked:
patched: '2017-04-21T04:58:35.183Z'
'npm:negotiator:20160616':
- harp > connect > compression > accepts > negotiator:
patched: '2017-04-21T04:58:35.183Z'
- harp > connect > serve-index > accepts > negotiator:
patched: '2017-04-21T04:58:35.183Z'
'npm:tar:20151103':
- harp > download-github-repo > download > decompress > tar:
patched: '2017-04-21T04:58:35.183Z'
'npm:uglify-js:20151024':
- harp > terraform > jade > transformers > uglify-js:
patched: '2017-04-21T04:58:35.183Z'
'npm:debug:20170905':
- harp > connect > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > express-session > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > finalhandler > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > morgan > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > serve-index > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > send > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > body-parser > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > compression > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > connect-timeout > debug:
patched: '2017-09-29T03:22:08.982Z'
- harp > connect > serve-static > send > debug:
patched: '2017-09-29T03:22:08.982Z'

View file

@ -1,2 +1,2 @@
exclude = "{$exclude,www,node_modules,tweets,wayback,test-blog*}"
exclude = "{$exclude,www,tweets,wayback}"
include = "{$include,.gitignore}"

View file

@ -6,7 +6,3 @@ gem 'htmlentities', '~> 4.3'
gem 'mustache', '~> 1.0'
gem 'nokogiri', '~> 1.10'
gem 'rdiscount', '~> 2.1'
gem 'sinatra', '~> 1.4'
gem 'rspec', '~> 3.3'
gem 'guard-rspec', '~> 4.6'

View file

@ -4,73 +4,15 @@ GEM
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
builder (3.2.3)
coderay (1.1.2)
css_parser (1.6.0)
addressable
diff-lcs (1.3)
ffi (1.11.2)
formatador (0.2.5)
guard (2.14.2)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.9.12)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-compat (1.2.1)
guard-rspec (4.7.3)
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
htmlentities (4.3.4)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
lumberjack (1.0.12)
method_source (0.9.0)
mini_portile2 (2.4.0)
mustache (1.0.5)
nenv (0.3.0)
nokogiri (1.10.4)
mini_portile2 (~> 2.4.0)
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
public_suffix (3.0.1)
rack (1.6.11)
rack-protection (1.5.5)
rack
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rdiscount (2.2.0.1)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-core (3.7.1)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-support (3.7.0)
ruby_dep (1.5.0)
shellany (0.0.1)
sinatra (1.4.8)
rack (~> 1.5)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
thor (0.20.0)
tilt (2.0.8)
PLATFORMS
ruby
@ -78,13 +20,10 @@ PLATFORMS
DEPENDENCIES
builder (~> 3.2)
css_parser (~> 1.3)
guard-rspec (~> 4.6)
htmlentities (~> 4.3)
mustache (~> 1.0)
nokogiri (~> 1.10)
rdiscount (~> 2.1)
rspec (~> 3.3)
sinatra (~> 1.4)
BUNDLED WITH
1.16.1
1.17.1

View file

@ -1,9 +1,5 @@
all: compile
rss:
@echo
ruby -w ./bin/rss.rb public
compile:
@echo
./bin/compile.sh .
@ -16,13 +12,4 @@ publish_beta: compile
@echo
./bin/publish.sh --beta --delete
test_blog:
./bin/create-test-blog.sh server/test-blog
clean:
rm -rf server/test-blog server/test-blog-origin.git
spec:
cd server && rspec -f documentation
.PHONY: rss compile publish publish_beta test_blog clean spec
.PHONY: compile publish publish_beta

53
Readme.md Normal file
View file

@ -0,0 +1,53 @@
# samhuri.net
The source code for [samhuri.net](https://samhuri.net).
# New version using Swift
The idea is to create a bespoke set of tools, not a general thing like Jekyll. If something can be factored out later that's a bonus, not a goal.
This is a plan for migrating from a mix of node.js (harp) and Ruby to Swift. Use Ink, John Sundell's Markdown parser, to render posts, and some other library for generating HTML. Will probably try generating HTML from code because I've never tried it and it seems fun. The pointfree.com guys have one already and Sundell is releasing one Real Soon(tm).
This version will go back to its roots and use headers at the top of markdown files. It was so much easier than indexing everything. Check out [9af9d75][] to see how nice it was back in the day. Perhaps it would even make sense to port that old version to Swift rather than working with the current version. Migrate the last few years' worth of posts over by hand since there isn't a lot there.
[9af9d75]: https://github.com/samsonjs/samhuri.net/tree/9af9d75565133104beb54f1bfdd3d4efe3e16982
Execution, trying TDD for the first time:
- [ ] Replace harp with custom Swift code
- [ ] Write a test harness that renders a site and then checks the output with `diff -r`
- [ ] Copy source files to their destination without transformations
- [ ] Transform files as needed:
- [ ] LESS -> CSS
- [ ] ? -> HTML
- [ ] \(decide if ? is code or templates (maybe Stencils?))
- [ ] Port .ejs templates to the new thing
- [ ] Add a link to the code for samhuri.net somewhere ... so meta (about page?)
- [ ] Migrate posts to markdown with headers somehow
- [ ] Decide whether to migrate from [9af9d75][] or the current harp format (probably easier to migrate the old format)
- [ ] Migrate posts
- [ ] Generate RSS feed (ditch mustache templates)
- [ ] Generate JSON feed
- [ ] Munge HTML files to make them available without an extension (index.html hack)
- [ ] Inline CSS?
- [ ] Minify JS
- [ ] Add a server for local use and simple production setups
- [ ] Figure out an iPad workflow with minimal code. Maybe a small app with some extensions and shortcuts?

View file

@ -3,14 +3,14 @@
# bail on errors
set -e
export PATH="$HOME/.rbenv/shims:$PATH"
# export PATH="$HOME/.rbenv/shims:$PATH"
echo "*** bootstrap samhuri.net"
echo "* bundle install"
bundle install
echo "* npm install"
npm install
# echo "* npm install"
# npm install
echo "*** done"

View file

@ -3,50 +3,33 @@
# bail on errors
set -e
export PATH="$HOME/.rbenv/shims:$PATH"
# export PATH="$HOME/.rbenv/shims:$PATH"
DIR=$(dirname "$0")
HARP="node_modules/harp/bin/harp"
# HARP="node_modules/harp/bin/harp"
BLOG_DIR="${1:-${DIR}/..}"
TARGET="${BLOG_DIR%/}/${2:-www}"
LOCK_FILE="$BLOG_DIR/compile.lock"
if [[ -e "$LOCK_FILE" ]]; then
echo "Bailing, another compilation is running"
exit 1
fi
function lock {
echo $$ >| "$LOCK_FILE"
}
function delete_lock_file {
rm -f "$LOCK_FILE"
}
trap delete_lock_file SIGHUP SIGINT SIGTERM SIGEXIT
lock
function main() {
echo "* compile rss feed"
compile_feeds
# compile_feeds
echo "* harp compile $BLOG_DIR $TARGET"
rm -rf "$TARGET/*" "$TARGET/.*"
"$HARP" compile "$BLOG_DIR" "$TARGET"
# echo "* harp compile $BLOG_DIR $TARGET"
# rm -rf "$TARGET/*" "$TARGET/.*"
# "$HARP" compile "$BLOG_DIR" "$TARGET"
# clean up temporary feeds
rm $BLOG_DIR/public/feed.xml
rm $BLOG_DIR/public/feed.json
# rm $BLOG_DIR/public/feed.xml
# rm $BLOG_DIR/public/feed.json
echo "* munge html files to make them available without an extension"
munge_html
# munge_html
echo "* inline CSS"
ruby -w $DIR/inline-css.rb "$TARGET"
# ruby -w $DIR/inline-css.rb "$TARGET"
echo "* minify js"
minify_js
delete_lock_file
# minify_js
}
function compile_feeds() {
@ -72,10 +55,10 @@ function munge_html() {
done
}
function minify_js() {
for FILE in "$TARGET"/js/*.js; do
$DIR/minify-js.sh "$FILE" > /tmp/minified.js && mv /tmp/minified.js "$FILE" || echo "* failed to minify $FILE"
done
}
# function minify_js() {
# for FILE in "$TARGET"/js/*.js; do
# $DIR/minify-js.sh "$FILE" > /tmp/minified.js && mv /tmp/minified.js "$FILE" || echo "* failed to minify $FILE"
# done
# }
main

View file

@ -1,18 +0,0 @@
#!/bin/zsh
set -e # bail on errors
export PATH="$HOME/.rbenv/shims:$PATH"
BLOG_PATH="$1"
ORIGIN_BLOG_PATH="${BLOG_PATH}-origin.git"
if [[ -e "$BLOG_PATH" ]]; then
echo ">>> Refusing to clobber $BLOG_PATH"
else
if [[ ! -e "$ORIGIN_BLOG_PATH" ]]; then
echo ">>> Mirroring local origin..."
git clone --mirror . "$ORIGIN_BLOG_PATH"
fi
echo ">>> Cloning test blog from local origin..."
git clone "$ORIGIN_BLOG_PATH" "$BLOG_PATH"
fi

View file

@ -1,16 +0,0 @@
#!/bin/bash
DIR=$(dirname "$0")
UGLIFY="node_modules/uglify-js/bin/uglifyjs"
function minify() {
INPUT="$1"
"$UGLIFY" "$INPUT"
}
if [[ "$1" != "" ]]; then
minify "$1"
else
echo "usage: $0 [input file]"
exit 1
fi

View file

@ -1,8 +0,0 @@
{
"globals": {
"site": "samhuri.net",
"author": "Sami J. Samhuri",
"email": "sami@samhuri.net",
"url": "https://samhuri.net"
}
}

View file

@ -1,39 +0,0 @@
server {
listen 443;
#listen [::]:443 default_server ipv6only=on; ## listen for ipv6
server_name ocean.samhuri.net;
ssl on;
ssl_certificate cert.pem;
ssl_certificate_key cert.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:6706;
}
}
server {
listen 5000;
#listen [::]:443 default_server ipv6only=on; ## listen for ipv6
server_name ocean.samhuri.net;
ssl on;
ssl_certificate cert.pem;
ssl_certificate_key cert.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /home/sjs/samhuri.net/www;
index index.html;
location / {
try_files $uri $uri/ $uri/index.html;
}
}

8073
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +0,0 @@
{
"name": "samhuri.net",
"description": "samhuri.net",
"version": "0.1.0",
"dependencies": {
"harp": "^0.31.0",
"thepusher": "^0.1.4",
"uglify-js": "^3.4.9",
"snyk": "^1.41.1"
},
"scripts": {
"snyk-protect": "snyk protect",
"prepare": "npm run snyk-protect"
},
"snyk": true
}

View file

@ -1,18 +0,0 @@
# A sample Guardfile
# More info at https://github.com/guard/guard#readme
# Note: The cmd option is now required due to the increasing number of ways
# rspec may be run, below are examples of the most common uses.
# * bundler: 'bundle exec rspec'
# * bundler binstubs: 'bin/rspec'
# * spring: 'bin/rsspec' (This will use spring if running and you have
# installed the spring binstubs per the docs)
# * zeus: 'zeus rspec' (requires the server to be started separetly)
# * 'just' rspec: 'rspec'
guard :rspec, cmd: 'bundle exec rspec' do
watch('auth.json') { 'spec/server_spec.rb' }
watch(%r{^(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/helpers.rb') { 'spec' }
watch(%r{^spec/.+_spec\.rb$})
end

View file

@ -1,11 +0,0 @@
require 'json'
class Auth
def initialize(filename)
@credentials = JSON.parse(File.read(filename))
end
def authenticated?(username, password)
@credentials['username'] == username && @credentials['password'] == password
end
end

View file

@ -1,467 +0,0 @@
require 'fileutils'
require 'json'
require './harp_blog/post'
require './web_title_finder'
require './web_version_finder'
class HarpBlog
class HarpBlogError < RuntimeError ; end
class InvalidDataError < HarpBlogError ; end
class PostExistsError < HarpBlogError ; end
class PostAlreadyPublishedError < HarpBlogError ; end
class PostNotPublishedError < HarpBlogError ; end
class PostSaveError < HarpBlogError
attr_reader :original_error
def initialize(message, original_error)
super(message)
@original_error = original_error
end
end
def initialize(path, dry_run = true, title_finder = nil, version_finder = nil)
@path = path
@dry_run = dry_run
@title_finder = title_finder || WebTitleFinder.new
@version_finder = version_finder || WebVersionFinder.new
@mutated = false
end
def local_version
git_sha
end
def remote_version
@version_finder.find_version
end
def dirty?
local_version != remote_version
end
def status
local = local_version
remote = remote_version
{
'local-version' => local,
'remote-version' => remote,
'dirty' => local != remote,
}
end
def years
update_if_needed
Dir[post_path('20*')].map { |x| File.basename(x) }.sort
end
def months
update_if_needed
years.map do |year|
# hack: month dirs (and only month dirs) are always 2 characters in length
Dir[post_path("#{year}/??")].map { |x| [year, File.basename(x)] }
end.flatten(1).sort
end
def posts_for_year(year)
posts = []
1.upto(12) do |n|
month = n < 10 ? "0#{n}" : "#{n}"
posts += posts_for_month(year, month)
end
posts
end
def posts_for_month(year, month)
read_posts(File.join(year, month))
end
def drafts
read_posts('drafts', draft: true)
end
def get_post(year, month, id)
read_post(File.join(year, month), id)
end
def get_draft(id)
read_post('drafts', id, draft: true)
end
def create_post(title, body, url, extra_fields = nil, options = nil)
if !title || title.strip.length == 0
title = url && find_title(url) or 'Untitled'
end
extra_fields ||= {}
options ||= {}
options[:commit] = true unless options.has_key?(:commit)
fields = extra_fields.merge({
title: title,
link: url,
body: body,
})
post = Post.new(fields)
begin
existing_post = read_post(post.dir, post.id, extra_fields)
rescue InvalidDataError => e
$stderr.puts "[HarpBlog#create_post] deleting post with invalid data: #{e.message}"
delete_post_from_dir(post.dir, post.id)
existing_post = nil
end
if existing_post
raise PostExistsError.new("post exists: #{post.dir}/#{post.id}")
else
save_post(post)
if options[:commit]
git_commit("create post '#{post.title}'", [year_index_path("#{post.time.year}"), post_path(post.dir)])
end
post
end
end
def update_post(post, title, body, link, timestamp = nil)
post.title = title
post.body = body
post.link = link
post.timestamp = timestamp if timestamp
save_post(post)
git_commit("update post '#{post.title}'", [post_path(post.dir)])
post
end
def delete_post(year, month, id)
dir = File.join(year, month)
delete_post_from_dir(dir, id)
git_commit("delete post #{year}/#{month}/#{id}", [post_path(dir)])
end
def delete_draft(id, options = nil)
options ||= {}
options[:commit] = true unless options.has_key?(:commit)
delete_post_from_dir('drafts', id)
if options[:commit]
git_commit("delete draft #{id}", [post_path('drafts')])
end
end
def publish_post(post)
if post.draft?
new_post = create_post(post.title, post.body, post.link, {}, {commit: false})
delete_post_from_dir('drafts', post.id)
build_rss
git_commit("publish '#{quote(post.title)}'", [post_path('drafts'), post_path(new_post.dir), root_data_path])
new_post
else
raise PostAlreadyPublishedError.new("post is already published: #{post.dir}/#{post.id}")
end
end
def unpublish_post(post)
if post.draft?
raise PostNotPublishedError.new("post is not published: #{post.dir}/#{post.id}")
else
new_post = create_post(post.title, post.body, post.link, {draft: true}, {commit: false})
delete_post_from_dir(post.dir, post.id)
build_rss
git_commit("unpublish '#{quote(post.title)}'", [post_path(post.dir), post_path('drafts'), root_data_path])
new_post
end
end
def publish(env)
target = env.to_s == 'production' ? 'publish' : 'publish_beta'
success, output = run("make #{target}")
commit_root_data
[success, output]
end
def sync
update_if_needed
git_push
end
def compile
success, output = run('make compile')
if success
@mutated = false
else
puts output
end
success
end
def compile_if_mutated
compile if @mutated
end
############################################################################################
private ##################################################################################
############################################################################################
def find_title(url)
@title_finder.find_title(url)
end
def path_for(*components)
File.join(@path, *components)
end
def root_data_path
path_for('public/_data.json')
end
def year_index_path(year)
path_for('public/posts', year, 'index.ejs')
end
def post_path(dir, id = nil)
args = ['public/posts', dir]
args << "#{id}.md" if id
path_for(*args)
end
def read_posts(post_dir, extra_fields = nil)
update_if_needed
extra_fields ||= {}
post_data = read_post_data(post_path(post_dir))
post_data.sort_by do |k, v|
(v['timestamp'] || Time.now).to_i
end.map do |id, fields|
fields[:id] = id
unless extra_fields[:draft]
fields[:slug] = id
end
post_filename = post_path(post_dir, id)
fields[:body] = File.read(post_filename)
Post.new(fields.merge(extra_fields))
end
end
def read_post(post_dir, id, extra_fields = nil)
update_if_needed
post_filename = post_path(post_dir, id)
post_data = read_post_data(post_path(post_dir))
if File.exist?(post_filename) && fields = post_data[id]
fields[:body] = File.read(post_filename)
if extra_fields
fields.merge!(extra_fields)
end
fields[:id] = id
unless fields[:draft]
fields[:slug] = id
end
Post.new(fields)
elsif fields
message = "missing post body for #{post_dir}/#{id}: #{post_filename}"
$stderr.puts "[HarpBlog#read_post] #{message}"
raise InvalidDataError.new(message)
elsif File.exist?(post_filename)
message = "missing metadata for #{post_dir}/#{id}: #{post_dir}/_data.json"
$stderr.puts "[HarpBlog#read_post] #{message}"
raise InvalidDataError.new(message)
end
end
def save_post(post)
update_if_needed
begin
write_post(post)
post
rescue => e
$stderr.puts "#{e.class}: #{e.message}"
$stderr.puts e.backtrace
git_reset_hard
raise PostSaveError.new('failed to save post', e)
end
end
def delete_post_from_dir(post_dir, id)
update_if_needed
post_dir = post_path(post_dir)
delete_post_body(post_dir, id)
delete_post_index(post_dir, id)
end
def write_post(post)
post_dir = post_path(post.dir)
unless post.draft?
ensure_post_dir_exists(post_dir)
end
write_post_body(post_dir, post.id, post.body)
begin
write_post_index(post_dir, post.id, post.persistent_fields)
rescue => e
$stderr.puts "#{e.class}: #{e.message}"
$stderr.puts e.backtrace
delete_post_body(post_dir, post.id)
raise e
end
end
def write_post_body(dir, id, body)
post_filename = File.join(dir, "#{id}.md")
write_file(post_filename, body)
end
def delete_post_body(dir, id)
post_filename = File.join(dir, "#{id}.md")
delete_file(post_filename)
end
def write_post_index(dir, id, fields)
post_data = read_post_data(dir)
post_data[id] = fields
write_post_data(dir, post_data)
end
def delete_post_index(dir, id)
post_data = read_post_data(dir)
if post_data[id]
post_data.delete(id)
write_post_data(dir, post_data)
end
end
def ensure_post_dir_exists(dir)
monthly_index_filename = File.join(dir, 'index.ejs')
unless File.exist?(monthly_index_filename)
source = File.join(dir, '../../2006/02/index.ejs')
cp(source, monthly_index_filename)
end
yearly_index_filename = File.join(dir, '../index.ejs')
unless File.exist?(yearly_index_filename)
source = File.join(dir, '../../2006/index.ejs')
cp(source, yearly_index_filename)
end
end
def commit_root_data
git_commit('commit root _data.json', [root_data_path])
end
def read_post_data(dir)
post_data_filename = File.join(dir, '_data.json')
if File.exist?(post_data_filename)
JSON.parse(File.read(post_data_filename))
else
{}
end
end
def write_post_data(dir, data)
post_data_filename = File.join(dir, '_data.json')
json = JSON.pretty_generate(data)
write_file(post_data_filename, json)
end
def ensure_dir_exists(dir)
unless File.directory?(dir)
if @dry_run
puts ">>> mkdir -p '#{dir}'"
else
FileUtils.mkdir_p(dir)
end
end
end
def cp(source, destination, clobber = false)
ensure_dir_exists(File.dirname(destination))
if !File.exist?(destination) || clobber
if @dry_run
puts ">>> cp '#{source}' '#{destination}'"
else
FileUtils.cp(source, destination)
end
end
end
def write_file(filename, data)
ensure_dir_exists(File.dirname(filename))
if @dry_run
puts ">>> write file '#{filename}', contents:"
puts data
else
File.write(filename, data)
end
end
def delete_file(filename)
if File.exist?(filename)
if @dry_run
puts ">>> unlink '#{filename}'"
else
File.unlink(filename)
end
end
end
def quote(s)
s.gsub('"', '\\"')
end
def run(cmd, safety = :destructive)
if safety == :destructive && @dry_run
puts ">>> cd '#{@path}' && #{cmd}"
[true, '']
else
output = `cd '#{@path}' && #{cmd} 2>&1`
[$?.success?, output]
end
end
def git_sha
update_if_needed
_, output = run 'git log -n1 | head -n1 | cut -d" " -f2', :nondestructive
output.strip
end
def git_commit(message, files)
quoted_files = files.map { |f| "\"#{quote(f)}\"" }
puts ">>> git add -A #{quoted_files.join(' ')} && git commit -m \"#{message}\""
success, output = run("git add -A #{quoted_files.join(' ')} && git commit -m \"#{message}\"")
if success
@mutated = true
else
puts output
end
[success, output]
end
def git_reset_hard(ref = nil)
args = ref ? "'#{ref}'" : ''
run("git reset --hard #{args}")
end
def git_push force = false
args = force ? '-f' : nil
run "git push #{args}"
end
def origin_updated_path
File.join @path, 'origin-updated'
end
def git_update(remote = 'origin')
if run "git update #{remote}", :nondestructive
File.unlink origin_updated_path if origin_updated?
end
end
def origin_updated?
File.exist? origin_updated_path
end
def update_if_needed
git_update if origin_updated?
end
def build_rss
run 'make rss'
end
end

View file

@ -1,137 +0,0 @@
require 'securerandom'
class HarpBlog
class Post
PERSISTENT_FIELDS = %w[id author title date timestamp link url tags].map(&:to_sym)
TRANSIENT_FIELDS = %w[time slug body draft].map(&:to_sym)
FIELDS = PERSISTENT_FIELDS + TRANSIENT_FIELDS
FIELDS.each { |f| attr_accessor f }
def initialize(fields = nil)
@timestamp = nil
if fields
FIELDS.each do |k|
if v = fields[k.to_s] || fields[k.to_sym]
instance_variable_set("@#{k}", v)
end
end
end
end
def persistent_fields
PERSISTENT_FIELDS.inject({}) do |h, k|
h[k] = send(k)
h
end
end
def fields
FIELDS.inject({}) do |h, k|
h[k] = send(k)
h
end
end
def link?
!!link
end
def draft
@draft ||= false
end
alias_method :draft?, :draft
def author
@author ||= 'Sami J. Samhuri'
end
def time
@time ||= @timestamp ? Time.at(@timestamp) : Time.now
end
def time=(time)
@timestamp = nil
@date = nil
@url = nil
@time = time
end
def timestamp
@timestamp ||= time.to_i
end
def timestamp=(timestamp)
@time = nil
@date = nil
@url = nil
@timestamp = timestamp
end
def id
@id ||=
if draft?
SecureRandom.uuid
else
slug
end
end
def url
if draft?
"/posts/drafts/#{id}"
else
"/posts/#{time.year}/#{padded_month}/#{slug}"
end
end
def slug
# TODO: be intelligent about unicode ... \p{Word} might help. negated char class with it?
if !draft? && title
@slug ||= title.downcase.
gsub(/'/, '').
gsub(/[^[:alpha:]\d_]/, '-').
gsub(/^-+|-+$/, '').
gsub(/-+/, '-')
end
end
def date
@date ||= time.strftime("#{ordinalize(time.day)} %B, %Y")
end
def ordinalize(n)
case
when n % 10 == 1 && n != 11
"#{n}st"
when n % 10 == 2 && n != 12
"#{n}nd"
when n % 10 == 3 && n != 13
"#{n}rd"
else
"#{n}th"
end
end
def tags
@tags ||= []
end
def padded_month
pad(time.month)
end
def dir
if draft?
'drafts'
else
File.join(time.year.to_s, padded_month)
end
end
def pad(n)
n.to_i < 10 ? "0#{n}" : "#{n}"
end
end
end

View file

@ -1,409 +0,0 @@
#!/usr/bin/env ruby -w
# An HTTP interface for my Harp blog.
require 'json'
require 'sinatra'
require './auth'
require './harp_blog'
CONFIG_DEFAULTS = {
auth: false,
dry_run: false,
path: File.expand_path('../test-blog', __FILE__),
host: '127.0.0.1',
hostname: `hostname --fqdn`.strip,
port: 6706,
external_port: 6706,
external_ssl: false,
preview_port: 9000,
preview_ssl: false,
}
def env_value(name)
env_name = "BLOG_#{name.to_s.upcase}"
raw_value = ENV[env_name]
case name
when :auth, :dry_run, :preview_ssl, :external_ssl
raw_value ? raw_value.to_i != 0 : false
when :port, :external_port, :preview_port
raw_value ? raw_value.to_i : nil
else
raw_value
end
end
$config = CONFIG_DEFAULTS.dup
$config.each_key do |name|
value = env_value(name)
unless value.nil?
$config[name] = value
end
end
if $config[:external_ssl] && $config[:external_port] == 80
$config[:external_port] = 443
end
if !$config[:external_ssl] && $config[:external_port] == 443
$config[:external_ssl] = true
end
proto = $config[:preview_ssl] ? 'https' : 'http'
$config[:preview_url] = "#{proto}://#{$config[:hostname]}:#{$config[:preview_port]}"
unless File.directory?($config[:path])
raise RuntimeError.new("file not found: #{$config[:path]}")
end
if $config[:host] == '0.0.0.0' && !$config[:auth]
raise RuntimeError.new("cowardly refusing to bind to 0.0.0.0 without authentication")
end
$auth = Auth.new(File.expand_path('../auth.json', __FILE__))
def authenticated?(auth)
if $config[:auth]
username, password = auth.to_s.split('|')
$auth.authenticated?(username, password)
else
true
end
end
host = $config[:hostname] || $config[:host]
proto = $config[:external_ssl] ? 'https' : 'http'
$url_root = "#{proto}://#{host}:#{$config[:external_port]}/"
puts "URL root: #{$url_root}"
def url_for(*components)
File.join($url_root, *components)
end
# Server
set :host, $config[:host]
set :port, $config[:port]
blog = HarpBlog.new($config[:path], $config[:dry_run])
before do
if request.body.size > 0
@fields =
case
when request.accept?('application/json')
request.body.rewind
json = request.body.read
JSON.parse(json)
else
params
end
else
@fields = {}
end
end
# favicon
get '/favicon.ico' do
status 302
headers 'Location' => "#{$config[:preview_url]}/favicon.ico"
end
# status
get '/status' do
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(status: blog.status)
end
# publish the site
post '/publish' do
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
blog.publish(@fields['env'])
status 204
end
# sync with github
post '/sync' do
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
blog.sync
status 204
nil
end
# list years
get '/years' do
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(years: blog.years)
end
# list months
get '/months' do
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(months: blog.months)
end
##############
### Drafts ###
##############
# list drafts
get '/posts/drafts' do
posts = blog.drafts
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(posts: posts.map(&:fields))
end
# get a draft
get '/posts/drafts/:id' do |id|
begin
post = blog.get_draft(id)
rescue HarpBlog::InvalidDataError => e
status 500
return "Failed to get draft, invalid data on disk: #{e.message}"
end
if post
if request.accept?('text/html')
status 302
headers 'Location' => "#{$config[:preview_url]}/posts/drafts/#{id}"
nil
elsif request.accept?('application/json')
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(post: post.fields)
else
status 400
"content not available in an acceptable format: #{request.accept.join(', ')}"
end
else
status 404
'not found'
end
end
# make a draft, and optionally publish it immediately
post '/posts/drafts' do
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
id, title, body, link = @fields.values_at('id', 'title', 'body', 'link')
begin
if post = blog.create_post(title, body, link, id: id, draft: true)
if env = @fields['env']
post = blog.publish_post(post)
Thread.new do
blog.publish(env)
end
end
url = url_for(post.url)
status 201
headers 'Location' => url, 'Content-Type' => 'application/json'
JSON.generate(post: post.fields)
else
status 500
'failed to create post'
end
rescue HarpBlog::PostExistsError => e
post = HarpBlog::Post.new({
title: title,
body: body,
link: link,
})
status 409
"refusing to clobber existing draft, update it instead: #{post.url}"
rescue HarpBlog::PostSaveError => e
status 500
if orig_err = e.original_error
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
else
"Failed to create draft: #{e.message}"
end
end
end
# update a draft
put '/posts/drafts/:id' do |id|
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
title, body, link = @fields.values_at('title', 'body', 'link')
begin
if post = blog.get_draft(id)
blog.update_post(post, title, body, link, Time.now.to_i)
status 204
else
status 404
'not found'
end
rescue HarpBlog::InvalidDataError => e
status 500
"Failed to update draft, invalid data on disk: #{e.message}"
rescue HarpBlog::PostSaveError => e
status 500
if orig_err = e.original_error
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
else
"Failed to update draft: #{e.message}"
end
end
end
# delete a draft
delete '/posts/drafts/:id' do |id|
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
blog.delete_draft(id)
status 204
end
# publish a post
post '/posts/drafts/:id/publish' do |id|
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
if post = blog.get_draft(id)
new_post = blog.publish_post(post)
status 201
headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
JSON.generate(post: new_post.fields)
else
status 404
'not found'
end
end
#######################
### Published Posts ###
#######################
# list published posts
get '/posts/:year/?:month?' do |year, month|
posts =
if month
blog.posts_for_month(year, month)
else
blog.posts_for_year(year)
end
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(posts: posts.map(&:fields))
end
# list all published posts
get '/posts' do
posts = blog.months.map do |year, month|
blog.posts_for_month(year, month)
end.flatten.reverse
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(posts: posts.map(&:fields))
end
# get a post
get '/posts/:year/:month/:id' do |year, month, id|
begin
post = blog.get_post(year, month, id)
rescue HarpBlog::InvalidDataError => e
status 500
return "Failed to get post, invalid data on disk: #{e.message}"
end
if post
if request.accept?('text/html')
status 302
headers 'Location'=> "#{$config[:preview_url]}/posts/#{year}/#{month}/#{id}"
nil
elsif request.accept?('application/json')
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(post: post.fields)
else
status 400
"content not available in an acceptable format: #{request.accept.join(', ')}"
end
else
status 404
'not found'
end
end
# update a post
put '/posts/:year/:month/:id' do |year, month, id|
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
title, body, link = @fields.values_at('title', 'body', 'link')
begin
if post = blog.get_post(year, month, id)
blog.update_post(post, title, body, link)
status 204
else
status 404
'not found'
end
rescue HarpBlog::InvalidDataError => e
status 500
"Failed to update post, invalid data on disk: #{e.message}"
rescue HarpBlog::PostSaveError => e
status 500
if orig_err = e.original_error
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
else
"Failed to update post: #{e.message}"
end
end
end
# delete a post
delete '/posts/:year/:month/:id' do |year, month, id|
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
blog.delete_post(year, month, id)
status 204
end
# unpublish a post
post '/posts/:year/:month/:id/unpublish' do |year, month, id|
unless authenticated?(request.env['HTTP_AUTH'])
status 403
return 'forbidden'
end
if post = blog.get_post(year, month, id)
new_post = blog.unpublish_post(post)
status 200
headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
JSON.generate(post: new_post.fields)
else
status 404
'not found'
end
end

View file

@ -1,182 +0,0 @@
require_relative '../harp_blog/post'
RSpec.describe HarpBlog::Post do
# Persistent fields: id, author, title, date, timestamp, link, url, tags
# Transient fields: time, slug, body
before :all do
@post_fields = {
title: 'samhuri.net',
link: 'https://samhuri.net',
body: 'this site is sick',
}
@post_slug = 'samhuri-net'
@draft_fields = {
title: 'reddit.com',
link: 'http://reddit.com',
body: 'hi reddit',
draft: true,
id: 'dummy-draft-id',
}
end
describe '#new' do
it "takes a Hash of fields" do
fields = @post_fields
post = HarpBlog::Post.new(fields)
expect(post.title).to eq(fields[:title])
expect(post.link).to eq(fields[:link])
expect(post.body).to eq(fields[:body])
end
it "accepts no parameters" do
post = HarpBlog::Post.new
expect(post).to be_truthy
end
it "ignores unknown fields" do
post = HarpBlog::Post.new(what: 'is this')
expect(post).to be_truthy
end
end
describe '#persistent_fields' do
it "contains all expected fields" do
all_keys = HarpBlog::Post::PERSISTENT_FIELDS.sort
post = HarpBlog::Post.new
expect(all_keys).to eq(post.persistent_fields.keys.sort)
end
end
describe '#fields' do
it "contains all expected fields" do
all_keys = HarpBlog::Post::FIELDS.sort
post = HarpBlog::Post.new
expect(all_keys).to eq(post.fields.keys.sort)
end
end
describe '#link?' do
it "returns true for link posts" do
post = HarpBlog::Post.new(link: @post_fields[:link])
expect(post.link?).to be_truthy
end
it "returns false for article posts" do
post = HarpBlog::Post.new
expect(post.link?).to be_falsy
end
end
describe '#draft?' do
it "returns true for draft posts" do
post = HarpBlog::Post.new(draft: true)
expect(post.draft?).to be_truthy
end
it "returns false for published posts" do
post = HarpBlog::Post.new
expect(post.draft?).to be_falsy
end
end
describe '#time' do
it "should be derived from the timestamp if necessary" do
timestamp = Time.now.to_i
post = HarpBlog::Post.new(timestamp: timestamp)
expect(post.time.to_i).to eq(timestamp)
end
end
describe '#timestamp' do
it "should be derived from the time if necessary" do
time = Time.now - 42
post = HarpBlog::Post.new(time: time)
expect(post.timestamp).to eq(time.to_i)
end
end
describe '#url' do
it "should be derived from the time and slug if necessary" do
post = HarpBlog::Post.new(@post_fields)
year = post.time.year.to_s
month = post.time.month
padded_month = month < 10 ? "0#{month}" : "#{month}"
expect(post.url).to eq("/posts/#{year}/#{padded_month}/#{@post_slug}")
end
end
describe '#id' do
it "should be generated for drafts if necessary" do
draft = HarpBlog::Post.new(@draft_fields)
expect(draft.id).to eq(@draft_fields[:id])
draft = HarpBlog::Post.new(@draft_fields.merge(id: nil))
expect(draft.id).to_not eq(@draft_fields[:id])
end
it "should be the slug for posts" do
post = HarpBlog::Post.new(@post_fields)
expect(post.id).to eq(post.slug)
end
end
describe '#slug' do
it "should be derived from the title if necessary" do
post = HarpBlog::Post.new(@post_fields)
expect(post.slug).to eq(@post_slug)
end
it "should be nil for drafts" do
draft = HarpBlog::Post.new(@draft_fields)
expect(draft.slug).to be_nil
end
it "should strip apostrophes" do
post = HarpBlog::Post.new(title: "sjs's post")
expect(post.slug).to eq('sjss-post')
end
it "should replace most non-word characters with dashes" do
post = HarpBlog::Post.new(title: 'foo/bår!baz_quüx42')
expect(post.slug).to eq('foo-bår-baz_quüx42')
end
it "should strip leading and trailing dashes" do
post = HarpBlog::Post.new(title: '!foo?bar!')
expect(post.slug).to eq('foo-bar')
end
it "should collapse runs of dashes" do
post = HarpBlog::Post.new(title: 'foo???bar')
expect(post.slug).to eq('foo-bar')
end
end
describe '#dir' do
it "returns the drafts dir for draft posts" do
post = HarpBlog::Post.new(draft: true)
expect(post.dir).to eq('drafts')
end
it "returns the dated dir for published posts" do
post = HarpBlog::Post.new
expect(post.dir).to eq("#{post.time.year}/#{post.padded_month}")
end
end
describe '#pad' do
it "should have a leading zero for integers 0 < n < 10" do
post = HarpBlog::Post.new
expect(post.pad(1)).to eq('01')
expect(post.pad(9)).to eq('09')
end
it "should not have a leading zero for integers n >= 10" do
post = HarpBlog::Post.new
expect(post.pad(10)).to eq('10')
expect(post.pad(12)).to eq('12')
end
end
end

View file

@ -1,359 +0,0 @@
require 'json'
require_relative './helpers'
require_relative '../harp_blog'
TEST_BLOG_PATH = File.expand_path('../../test-blog', __FILE__)
TEST_BLOG_ORIGIN_PATH = File.expand_path('../../test-blog-origin.git', __FILE__)
RSpec.configure do |c|
c.include Helpers
end
RSpec.describe HarpBlog do
before :each do
@test_blog_ref = git_sha(TEST_BLOG_PATH)
dry_run = false
@mock_title = 'fancy title'
@mock_title_finder = mock_title_finder(@mock_title)
@mock_version_finder = mock_version_finder(@test_blog_ref)
@blog = HarpBlog.new(TEST_BLOG_PATH, dry_run, @mock_title_finder, @mock_version_finder)
end
after :each do
git_reset_hard(TEST_BLOG_PATH, @test_blog_ref)
force = true
@blog.send(:git_push, force)
end
describe '#new' do
it "should optionally accept dry_run" do
expect(@blog).to be_truthy
blog = HarpBlog.new(TEST_BLOG_PATH)
expect(blog).to be_truthy
end
end
describe '#local_version' do
it "should expose the local version" do
expect(@blog.local_version).to eq(@test_blog_ref)
end
end
describe '#remote_version' do
it "should expose the remote version" do
expect(@blog.remote_version).to eq(@test_blog_ref)
end
end
describe '#dirty?' do
it "should specify whether or not there are unpublished changes" do
expect(@blog.dirty?).to be_falsy
@blog.create_post('title', 'body', nil)
expect(@blog.dirty?).to be_truthy
@mock_version_finder.version = @blog.local_version
expect(@blog.dirty?).to be_falsy
end
end
describe '#status' do
it "should expose the local and remote versions, and dirty state" do
status = @blog.status
expect(status['local-version']).to eq(@blog.local_version)
expect(status['remote-version']).to eq(@blog.remote_version)
expect(status['dirty']).to eq(@blog.dirty?)
end
end
describe '#years' do
it "should return all of the years with posts" do
# yup, if I don't blog for an entire year that's a bug!
years = (2006..Date.today.year).to_a.map(&:to_s)
expect(@blog.years).to eq(years)
end
end
describe '#months' do
it "should return all of the years and months with posts" do
months = [
["2006", "02"], ["2006", "03"], ["2006", "04"], ["2006", "05"], ["2006", "06"], ["2006", "07"], ["2006", "08"], ["2006", "09"], ["2006", "12"],
["2007", "03"], ["2007", "04"], ["2007", "05"], ["2007", "06"], ["2007", "07"], ["2007", "08"], ["2007", "09"], ["2007", "10"],
["2008", "01"], ["2008", "02"], ["2008", "03"],
["2009", "11"],
["2010", "01"], ["2010", "11"],
["2011", "11"], ["2011", "12"],
["2012", "01"],
["2013", "03"], ["2013", "09"],
]
expect(@blog.months.first(months.length)).to eq(months)
end
end
describe '#posts_for_month' do
it "should return the correct number of posts" do
expect(@blog.posts_for_month('2006', '02').length).to eq(12)
end
it "should sort the posts by publish time" do
timestamps = @blog.posts_for_month('2006', '02').map(&:timestamp)
expect(increasing?(timestamps)).to be_truthy
end
end
describe '#posts_for_year' do
it "should return the correct number of posts" do
expect(@blog.posts_for_year('2006').length).to eq(31)
end
it "should sort the posts by publish time" do
timestamps = @blog.posts_for_year('2006').map(&:timestamp)
expect(increasing?(timestamps)).to be_truthy
end
end
describe '#drafts' do
it "returns the correct number of posts" do
expect(@blog.drafts.length).to eq(2)
end
it "should sort the posts by publish time" do
timestamps = @blog.drafts.map(&:timestamp)
expect(increasing?(timestamps)).to be_truthy
end
end
describe '#get_post' do
it "should return complete posts" do
first_post_path = File.join(TEST_BLOG_PATH, 'public/posts/2006/02/first-post.md')
post = @blog.get_post('2006', '02', 'first-post')
expect(post).to be_truthy
expect(post.author).to eq('Sami J. Samhuri')
expect(post.title).to eq('First Post!')
expect(post.slug).to eq('first-post')
expect(post.timestamp).to eq(1139368860)
expect(post.date).to eq('8th February, 2006')
expect(post.url).to eq('/posts/2006/02/first-post')
expect(post.link).to eq(nil)
expect(post.link?).to be_falsy
expect(post.tags).to eq(['life'])
expect(post.body).to eq(File.read(first_post_path))
end
it "should return nil if the post does not exist" do
post = @blog.get_post('2005', '01', 'anything')
expect(post).to be(nil)
end
end
describe '#get_draft' do
it "should return complete posts" do
id = 'some-draft-id'
title = 'new draft'
body = "blah blah blah\n"
@blog.create_post(title, body, nil, id: id, draft: true)
draft = @blog.get_draft(id)
expect(draft).to be_truthy
expect(draft.title).to eq(title)
expect(draft.url).to eq("/posts/drafts/#{id}")
expect(draft.draft?).to be_truthy
expect(draft.body).to eq(body)
end
it "should return nil if the post does not exist" do
draft = @blog.get_draft('does-not-exist')
expect(draft).to be(nil)
end
end
describe '#create_post' do
it "should create a link post when a link is given" do
title = 'test post'
body = 'check this out'
link = 'https://samhuri.net'
post = @blog.create_post(title, body, link)
expect(post).to be_truthy
expect(post.link?).to be_truthy
expect(post.title).to eq(title)
expect(post.body).to eq(body)
expect(post.link).to eq(link)
expect(post.time.to_date).to eq(Date.today)
end
it "should create an article post when no link is given" do
title = 'test post'
body = 'check this out'
post = @blog.create_post(title, body, nil)
expect(post).to be_truthy
expect(post.link?).to be_falsy
expect(post.title).to eq(title)
expect(post.body).to eq(body)
expect(post.link).to eq(nil)
expect(post.time.to_date).to eq(Date.today)
end
it "should create a draft post" do
title = 'test draft'
body = 'check this out'
post = @blog.create_post(title, body, nil, draft: true)
expect(post).to be_truthy
expect(post.draft?).to be_truthy
expect(post.dir).to eq('drafts')
end
it "should create a post that can be fetched immediately" do
title = 'fetch now'
body = 'blah blah blah'
post = @blog.create_post(title, body, nil)
expect(post).to be_truthy
today = Date.today
year = today.year.to_s
month = post.pad(today.month)
fetched_post = @blog.get_post(year, month, post.slug)
expect(fetched_post.url).to eq(post.url)
end
it "should create a draft that can be fetched immediately" do
id = 'another-draft-id'
title = 'fetch now'
body = 'blah blah blah'
draft = @blog.create_post(title, body, nil, id: id, draft: true)
expect(draft).to be_truthy
fetched_draft = @blog.get_draft(draft.id)
expect(draft.url).to eq(fetched_draft.url)
end
it "should fetch titles if necessary" do
post = @blog.create_post(nil, nil, 'https://samhuri.net')
expect(post.title).to eq(@mock_title)
@blog.delete_post(post.time.year.to_s, post.padded_month, post.slug)
post = @blog.create_post(" \t\n", nil, 'https://samhuri.net')
expect(post.title).to eq(@mock_title)
end
end
describe '#update_post' do
it "should immediately reflect changes when fetched" do
post = @blog.get_post('2006', '02', 'first-post')
title = 'new title'
body = "new body\n"
link = 'new link'
@blog.update_post(post, title, body, link)
# new slug, new data
post = @blog.get_post('2006', '02', 'first-post')
expect(post.title).to eq(title)
expect(post.body).to eq(body)
expect(post.link).to eq(link)
end
end
describe '#delete_post' do
it "should delete existing posts" do
post = @blog.get_post('2006', '02', 'first-post')
expect(post).to be_truthy
@blog.delete_post('2006', '02', 'first-post')
post = @blog.get_post('2006', '02', 'first-post')
expect(post).to eq(nil)
end
it "should do nothing for non-existent posts" do
post = @blog.get_post('2006', '02', 'first-post')
expect(post).to be_truthy
@blog.delete_post('2006', '02', 'first-post')
@blog.delete_post('2006', '02', 'first-post')
end
end
describe '#delete_draft' do
it "should delete existing drafts" do
id = 'bunk-draft-id'
title = 'new draft'
body = 'blah blah blah'
existing_draft = @blog.create_post(title, body, nil, id: id, draft: true)
draft = @blog.get_draft(existing_draft.id)
expect(draft).to be_truthy
@blog.delete_draft(draft.id)
draft = @blog.get_draft(draft.id)
expect(draft).to eq(nil)
end
it "should do nothing for non-existent posts" do
id = 'missing-draft-id'
title = 'new draft'
body = 'blah blah blah'
existing_draft = @blog.create_post(title, body, nil, id: id, draft: true)
draft = @blog.get_draft(existing_draft.id)
expect(draft).to be_truthy
@blog.delete_draft(draft.id)
expect(@blog.get_draft(existing_draft.id)).to be_nil
@blog.delete_draft(draft.id)
end
end
describe '#publish_post' do
it "should publish drafts" do
id = 'this-draft-is-a-keeper'
title = 'a-shiny-new-post'
body = 'blah blah blah'
link = 'https://samhuri.net'
draft = @blog.create_post(title, body, link, id: id, draft: true)
post = @blog.publish_post(draft)
expect(post).to be_truthy
expect(post.id).to eq(post.slug)
expect(post.draft?).to be_falsy
expect(post.title).to eq(title)
expect(post.body).to eq(body)
expect(post.link).to eq(link)
missing_draft = @blog.get_draft(draft.id)
expect(missing_draft).to eq(nil)
fetched_post = @blog.get_post(post.time.year.to_s, post.padded_month, post.slug)
expect(fetched_post).to be_truthy
end
it "should raise an error for published posts" do
post = @blog.get_post('2006', '02', 'first-post')
expect { @blog.publish_post(post) }.to raise_error
end
end
describe '#unpublish_post' do
it "should unpublish posts" do
post = @blog.get_post('2006', '02', 'first-post')
draft = @blog.unpublish_post(post)
expect(draft).to be_truthy
expect(draft.id).to be_truthy
expect(draft.draft?).to be_truthy
expect(draft.title).to eq(post.title)
expect(draft.body).to eq(post.body)
expect(draft.link).to eq(post.link)
missing_post = @blog.get_post(post.time.year.to_s, post.padded_month, post.slug)
expect(missing_post).to eq(nil)
fetched_draft = @blog.get_draft(draft.id)
expect(fetched_draft).to be_truthy
end
it "should raise an error for drafts" do
title = 'a-shiny-new-post'
body = 'blah blah blah'
link = 'https://samhuri.net'
post = @blog.create_post(title, body, link, draft: true)
expect { @blog.unpublish_post(post) }.to raise_error
end
end
end

View file

@ -1,71 +0,0 @@
module Helpers
def increasing?(list)
comparisons(list).all? { |x| x && x <= 0 }
end
def decreasing?(list)
comparisons(list).all? { |x| x && x >= 0 }
end
def comparisons(list)
x = list.first
list.drop(1).map do |y|
x <=> y
end
end
def git_bare?(dir)
if File.exist?(File.join(dir, '.git'))
false
elsif File.exist?(File.join(dir, 'HEAD'))
true
else
raise "what is this dir? #{dir} #{Dir[dir + '/*'].join(', ')}"
end
end
def git_sha(dir)
if git_bare?(dir)
`cd '#{dir}' && cat "$(cut -d' ' -f2 HEAD)"`.strip
else
`cd '#{dir}' && git log -n1`.split[1].strip
end
end
def git_reset_hard(dir, ref = nil)
if git_bare?(dir)
raise 'git_reset_hard does not support bare repos ' + dir + ' -- ' + ref.to_s
else
args = ref ? "'#{ref}'" : ''
`cd '#{dir}' && git reset --hard #{args}`
end
end
class TitleFinder
attr_accessor :title
def initialize(title)
@title = title
end
def find_title(url) @title end
end
def mock_title_finder(title)
TitleFinder.new(title)
end
class VersionFinder
attr_accessor :version
def initialize(version)
@version = version
end
def find_version(url = nil)
@version
end
end
def mock_version_finder(version)
VersionFinder.new(version)
end
end

View file

@ -1,16 +0,0 @@
require 'htmlentities'
require 'open-uri'
class WebTitleFinder
def find_title(url)
body = open(url).read
lines = body.split(/[\r\n]+/)
title_line = lines.grep(/<title/).first.strip
html_title = title_line.gsub(/\s*<\/?title[^>]*>\s*/, '')
HTMLEntities.new.decode(html_title)
rescue
nil
end
end

View file

@ -1,11 +0,0 @@
require 'open-uri'
class WebVersionFinder
DEFAULT_URL = 'https://samhuri.net/version.txt'
def find_version(url = nil)
open(url || DEFAULT_URL).read.strip
end
end

6
site.json Normal file
View file

@ -0,0 +1,6 @@
{
"title": "samhuri.net",
"author": "Sami J. Samhuri",
"email": "sami@samhuri.net",
"url": "https://samhuri.net"
}