mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 11:26:21 +00:00
editor prehistorico
This commit is contained in:
parent
3ac158cc93
commit
cbe46babdc
18 changed files with 322 additions and 47 deletions
1
Gemfile
1
Gemfile
|
@ -11,4 +11,5 @@ gem 'sinatra_warden'
|
|||
group :development do
|
||||
gem 'pry'
|
||||
gem 'rubocop'
|
||||
gem 'sinatra-contrib'
|
||||
end
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -4,6 +4,7 @@ GEM
|
|||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
ast (2.3.0)
|
||||
backports (3.8.0)
|
||||
coderay (1.1.2)
|
||||
colorator (1.1.0)
|
||||
email_address (0.1.3)
|
||||
|
@ -36,6 +37,7 @@ GEM
|
|||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
mercenary (0.3.6)
|
||||
method_source (0.8.2)
|
||||
multi_json (1.12.2)
|
||||
mustermann (1.0.1)
|
||||
netaddr (1.5.1)
|
||||
parallel (1.12.0)
|
||||
|
@ -80,6 +82,13 @@ GEM
|
|||
rack (~> 2.0)
|
||||
rack-protection (= 2.0.0)
|
||||
tilt (~> 2.0)
|
||||
sinatra-contrib (2.0.0)
|
||||
backports (>= 2.0)
|
||||
multi_json
|
||||
mustermann (~> 1.0)
|
||||
rack-protection (= 2.0.0)
|
||||
sinatra (= 2.0.0)
|
||||
tilt (>= 1.3, < 3)
|
||||
sinatra_warden (0.3.2)
|
||||
sinatra (>= 1.0.0)
|
||||
warden (~> 1.0)
|
||||
|
@ -101,6 +110,7 @@ DEPENDENCIES
|
|||
rubocop
|
||||
sass
|
||||
sinatra
|
||||
sinatra-contrib
|
||||
sinatra_warden
|
||||
|
||||
BUNDLED WITH
|
||||
|
|
26
README.md
26
README.md
|
@ -25,3 +25,29 @@ archivo `Gemfile`.
|
|||
```bash
|
||||
bundle install
|
||||
```
|
||||
|
||||
## Ideas
|
||||
|
||||
Sutty actua como una granja de sitios Jekyll. La idea principal es que
|
||||
una sola instancia de Sutty es capaz de gestionar muchos sitios Jekyll.
|
||||
|
||||
### Sin base de datos
|
||||
|
||||
Sutty es capaz de obtener toda la información necesaria a partir de la
|
||||
estructura de directorios.
|
||||
|
||||
### Sin autenticación
|
||||
|
||||
Al menos sin autenticación propia. Sutty es capaz de funcionar con
|
||||
mecanismos de autenticación diversos. Actualmente solo funciona con
|
||||
LDAP. Podría ni tener autenticación!
|
||||
|
||||
### Sin administradoras ni administradas
|
||||
|
||||
No hay una cuenta de administración que decide a quién corresponde un
|
||||
sitio o quién puede escribir o no. Las usuarias crean sus propios
|
||||
sitios e invitan a otras usuarias a co-gestionarlos.
|
||||
|
||||
### Múltiples versiones de Jekyll
|
||||
|
||||
Debe ser capaz de soportar diferentes versiones de Jekyll.
|
||||
|
|
14
lib/jekyll.rb
Normal file
14
lib/jekyll.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'jekyll'
|
||||
|
||||
# Monkeypatcheando Jekyll
|
||||
module Jekyll
|
||||
# Métodos agregados o modificados a Site
|
||||
class Site
|
||||
# Obtener un nombre para el sitio
|
||||
def name
|
||||
@name ||= File.basename(dest)
|
||||
end
|
||||
end
|
||||
end
|
40
lib/sutty.rb
40
lib/sutty.rb
|
@ -1,11 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
require_relative 'sutty/models/jekyll'
|
||||
require 'jekyll'
|
||||
require_relative 'jekyll'
|
||||
require_relative 'sutty/models/post'
|
||||
|
||||
# Sutty
|
||||
module Sutty
|
||||
|
||||
# La raíz
|
||||
def self.root
|
||||
@root ||= File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
||||
|
@ -13,7 +14,7 @@ module Sutty
|
|||
|
||||
# La configuración
|
||||
def self.settings
|
||||
@settings ||= YAML.load(File.read(Sutty.config_file))
|
||||
@settings ||= YAML.safe_load(File.read(Sutty.config_file))
|
||||
end
|
||||
|
||||
def self.config_file
|
||||
|
@ -24,8 +25,37 @@ module Sutty
|
|||
@sites_dir ||= File.join(Sutty.root, Sutty.settings['sites_dir'])
|
||||
end
|
||||
|
||||
# Comprueba que el directorio parezca ser de jekyll
|
||||
def self.jekyll?(dir)
|
||||
File.directory?(dir) && File.exist?(File.join(dir, '_config.yml'))
|
||||
end
|
||||
|
||||
def self.sites
|
||||
binding.pry
|
||||
@sites ||= Jekyll.all
|
||||
@sites ||= Dir.entries('_sites/').map do |j|
|
||||
# no queremos . ni .. ni archivos ocultos
|
||||
next if j.start_with? '.'
|
||||
|
||||
j = Sutty.path_from_name(j)
|
||||
next unless Sutty.jekyll? j
|
||||
|
||||
Dir.chdir(j) do
|
||||
config = ::Jekyll.configuration('source' => Dir.pwd)
|
||||
|
||||
# No necesitamos cargar plugins en este momento
|
||||
%w[plugins gems].each do |unneeded|
|
||||
config[unneeded] = [] if config.key? unneeded
|
||||
end
|
||||
|
||||
::Jekyll::Site.new(config)
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
|
||||
def self.find(name)
|
||||
Sutty.sites.select { |s| s.name == name }.first
|
||||
end
|
||||
|
||||
def self.path_from_name(name)
|
||||
File.realpath(File.join(Sutty.sites_dir, name))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
require 'rack-flash'
|
||||
require 'sinatra/base'
|
||||
require 'sinatra/reloader' if ENV['RACK_ENV'] == 'development'
|
||||
require 'sinatra_warden'
|
||||
require_relative 'login'
|
||||
require_relative 'site'
|
||||
require_relative '../sutty'
|
||||
|
||||
module Sutty
|
||||
|
@ -11,12 +13,17 @@ module Sutty
|
|||
class App < Sinatra::Base
|
||||
use Rack::Flash
|
||||
use Sutty::Login
|
||||
use Sutty::Site
|
||||
register Sinatra::Warden
|
||||
|
||||
configure :development do
|
||||
register Sinatra::Reloader
|
||||
end
|
||||
|
||||
set :root, Sutty.root
|
||||
|
||||
before do
|
||||
authorize! '/login'
|
||||
authorize! '/login' if ENV['RACK_ENV'] == 'production'
|
||||
end
|
||||
|
||||
get '/' do
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sutty
|
||||
# Un sitio jekyll
|
||||
class Jekyll
|
||||
# Devuelve todos los sitios dentro de sites_dir como instancias de
|
||||
# Jekyll
|
||||
def self.all
|
||||
Dir.entries(Sutty.sites_dir).map do |j|
|
||||
# no queremos . ni .. ni archivos ocultos
|
||||
next if j.start_with? '.'
|
||||
|
||||
_j = File.realpath(File.join(Sutty.sites_dir, j))
|
||||
next unless Jekyll.is_jekyll? _j
|
||||
|
||||
Jekyll.new(_j, j)
|
||||
end.compact
|
||||
end
|
||||
|
||||
# Comprueba que el directorio parezca ser de jekyll
|
||||
def self.is_jekyll?(dir)
|
||||
File.directory?(dir) && File.exists?(File.join(dir, '_config.yml'))
|
||||
end
|
||||
|
||||
def initialize(dir, name = nil)
|
||||
@root = dir
|
||||
@name = name if name
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= YAML.load(File.read(File.join(@root, '_config.yml')))
|
||||
end
|
||||
|
||||
def name
|
||||
@name ||= File.basename(@root)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
69
lib/sutty/models/post.rb
Normal file
69
lib/sutty/models/post.rb
Normal file
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
|
||||
module Sutty
|
||||
module Models
|
||||
# Un post
|
||||
class Post
|
||||
attr_accessor :content, :front_matter
|
||||
attr_reader :post, :site
|
||||
|
||||
REJECT_FROM_DATA = %w[excerpt slug draft date ext].freeze
|
||||
|
||||
def initialize(site, post = nil)
|
||||
@site = site
|
||||
@post = post
|
||||
end
|
||||
|
||||
def new?
|
||||
!@post.is_a? Jekyll::Document
|
||||
end
|
||||
|
||||
def save
|
||||
front_matter_from_data!
|
||||
clean_content!
|
||||
|
||||
return unless write.zero?
|
||||
|
||||
@post.read
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def front_matter_from_data!
|
||||
@front_matter ||= @post.data.reject do |key, _|
|
||||
REJECT_FROM_DATA.include? key
|
||||
end
|
||||
end
|
||||
|
||||
# Aplica limpiezas básicas del contenido
|
||||
def clean_content!
|
||||
@content = @content.delete("\r")
|
||||
end
|
||||
|
||||
def write
|
||||
File.open(@post.path, File::RDWR | File::CREAT, 0o640) do |f|
|
||||
# Bloquear el archivo para que no sea accedido por otro
|
||||
# proceso u otra editora
|
||||
f.flock(File::LOCK_EX)
|
||||
|
||||
# Empezar por el principio
|
||||
f.rewind
|
||||
|
||||
# Escribir
|
||||
f.write(full_content)
|
||||
|
||||
# Eliminar el resto
|
||||
f.flush
|
||||
f.truncate(f.pos)
|
||||
end
|
||||
end
|
||||
|
||||
def full_content
|
||||
"#{@front_matter.to_yaml}---\n\n#{@content}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
64
lib/sutty/post.rb
Normal file
64
lib/sutty/post.rb
Normal file
|
@ -0,0 +1,64 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'sinatra/base'
|
||||
require 'sinatra/namespace'
|
||||
require 'sinatra/reloader' if ENV['RACK_ENV'] == 'development'
|
||||
require 'sinatra_warden'
|
||||
require_relative 'login'
|
||||
require_relative '../sutty'
|
||||
|
||||
module Sutty
|
||||
# El gestor de posts
|
||||
class Post < Sinatra::Base
|
||||
use Sutty::Login
|
||||
register Sinatra::Warden
|
||||
register Sinatra::Namespace
|
||||
|
||||
set :root, Sutty.root
|
||||
|
||||
configure :development do
|
||||
register Sinatra::Reloader
|
||||
end
|
||||
|
||||
namespace '/sites/:name/posts' do
|
||||
before do
|
||||
authorize! '/login' if ENV['RACK_ENV'] == 'production'
|
||||
@site = Sutty.find(params['name'])
|
||||
@site.read if @site.posts.docs.empty?
|
||||
end
|
||||
|
||||
get do
|
||||
haml :'posts/index'
|
||||
end
|
||||
|
||||
namespace '/:basename' do
|
||||
before do
|
||||
@post = @site.posts.docs.select do |d|
|
||||
d.basename == params['basename']
|
||||
end.first
|
||||
end
|
||||
|
||||
get do
|
||||
haml :'posts/show'
|
||||
end
|
||||
|
||||
get '/edit' do
|
||||
haml :'posts/edit'
|
||||
end
|
||||
|
||||
post do
|
||||
post = Sutty::Models::Post.new(@site, @post)
|
||||
post.content = params[:post][:content]
|
||||
|
||||
if post.save
|
||||
flash[:success] = 'Artículo guardado con éxito'
|
||||
redirect to("/sites/#{@site.name}/posts/#{@post.basename}")
|
||||
else
|
||||
flash[:error] = 'Hubo un error al guardar el artículo'
|
||||
redirect back
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
39
lib/sutty/site.rb
Normal file
39
lib/sutty/site.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'sinatra/base'
|
||||
require 'sinatra/namespace'
|
||||
require 'sinatra/reloader' if ENV['RACK_ENV'] == 'development'
|
||||
require 'sinatra_warden'
|
||||
require_relative 'login'
|
||||
require_relative 'post'
|
||||
require_relative '../sutty'
|
||||
|
||||
module Sutty
|
||||
# El gestor de sitios
|
||||
class Site < Sinatra::Base
|
||||
use Sutty::Login
|
||||
register Sinatra::Warden
|
||||
register Sinatra::Namespace
|
||||
|
||||
set :root, Sutty.root
|
||||
set :read, false
|
||||
|
||||
configure :development do
|
||||
register Sinatra::Reloader
|
||||
end
|
||||
|
||||
namespace '/sites/:name' do
|
||||
before do
|
||||
authorize! '/login' if ENV['RACK_ENV'] == 'production'
|
||||
@site = Sutty.find(params['name'])
|
||||
@site.read if @site.posts.docs.empty?
|
||||
end
|
||||
|
||||
get do
|
||||
haml :'sites/show'
|
||||
end
|
||||
|
||||
use Sutty::Post
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,4 +5,8 @@
|
|||
%tbody
|
||||
- Sutty.sites.each do |site|
|
||||
%tr
|
||||
%td= site.name
|
||||
%td
|
||||
%a{href: '/sites/' + site.name}
|
||||
= site.name
|
||||
%td
|
||||
%a{href: 'http://' + site.name} visitar
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
%link{rel: 'stylesheet', type: 'text/css', href: '/assets/css/bootstrap.min.css'}
|
||||
%link{rel: 'stylesheet', type: 'text/css', href: '/stylesheets/sutty.css'}
|
||||
%body{class: @has_cover ? 'background-cover' : ''}
|
||||
.container
|
||||
.container-fluid
|
||||
= yield
|
||||
%footer.footer
|
||||
%p{style: @has_cover ? 'color: white' : ''}
|
||||
|
|
6
views/posts/_form.haml
Normal file
6
views/posts/_form.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
%form{method: 'post', action: "/sites/#{@site.name}/posts/#{@post.basename}"}
|
||||
.form-group
|
||||
%button.btn.btn-success{type: 'submit'} Guardar
|
||||
.form-group
|
||||
%textarea.form-control{name: 'post[content]', id: 'contents', autofocus: true}
|
||||
= @post.content
|
9
views/posts/edit.haml
Normal file
9
views/posts/edit.haml
Normal file
|
@ -0,0 +1,9 @@
|
|||
.row
|
||||
.col-2
|
||||
= haml :'sites/_nav'
|
||||
.col-10
|
||||
%h1
|
||||
Editando
|
||||
= @post.data['title']
|
||||
|
||||
= haml :'posts/_form'
|
13
views/posts/index.haml
Normal file
13
views/posts/index.haml
Normal file
|
@ -0,0 +1,13 @@
|
|||
.row
|
||||
.col-2
|
||||
= haml :'sites/_nav'
|
||||
.col-10
|
||||
%h1= @site.name
|
||||
|
||||
%table.table.table-striped
|
||||
%tbody
|
||||
- @site.posts.docs.each do |post|
|
||||
%tr
|
||||
%td
|
||||
%a{href: "/sites/#{@site.name}/posts/#{post.basename}"}= post.data['title']
|
||||
%td= post.date.strftime('%F')
|
12
views/posts/show.haml
Normal file
12
views/posts/show.haml
Normal file
|
@ -0,0 +1,12 @@
|
|||
.row
|
||||
.col-2
|
||||
= haml :'sites/_nav'
|
||||
.col-10
|
||||
%a.btn.btn-info{href: "/sites/#{@site.name}/posts/#{@post.basename}/edit"}
|
||||
Editar
|
||||
|
||||
%h1= @post.data['title']
|
||||
|
||||
.content
|
||||
:markdown
|
||||
#{@post.content}
|
5
views/sites/_nav.haml
Normal file
5
views/sites/_nav.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
%ul.nav.flex-column
|
||||
%li.nav-item
|
||||
%a.nav-link{href: "/sites/#{@site.name}/posts"}
|
||||
Posts
|
||||
%span.badge.badge-secondary= @site.posts.docs.count
|
5
views/sites/show.haml
Normal file
5
views/sites/show.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
.row
|
||||
.col-2
|
||||
= haml :'sites/_nav'
|
||||
.col-10
|
||||
%h1= @site.name
|
Loading…
Reference in a new issue