sutty/app/models/post.rb

216 lines
4.9 KiB
Ruby
Raw Normal View History

2018-01-29 22:19:10 +00:00
# frozen_string_literal: true
require 'jekyll/utils'
# Esta clase representa un post en un sitio jekyll e incluye métodos
# para modificarlos y crear nuevos
class Post
attr_accessor :content, :front_matter
attr_reader :post, :site, :errors
REJECT_FROM_DATA = %w[excerpt slug draft date ext].freeze
# Trabajar con posts. Si estamos creando uno nuevo, el **site** y
# el **front_matter** son necesarios, sino, **site** y **post**.
# XXX chequear que se den las condiciones
2018-02-02 22:20:31 +00:00
def initialize(site:, post: nil, front_matter: {})
raise ArgumentError, I18n.t('posts.errors.site') unless site.is_a?(Site)
raise ArgumentError, I18n.t('posts.errors.post') unless post.is_a?(Jekyll::Document)
2018-01-29 22:19:10 +00:00
@site = site
@post = post
2018-01-31 20:29:27 +00:00
# los errores tienen que ser un hash para que
# ActiveModel pueda traer los errores normalmente
@errors = {}
2018-01-29 22:19:10 +00:00
@front_matter = front_matter
2018-02-02 22:20:31 +00:00
# Crea un post nuevo si no especificamos el post
new_post unless @post
2018-01-29 22:19:10 +00:00
load_data! unless new?
end
# Si el archivo destino no existe, el post es nuevo
def new?
!File.exist? @post.path
end
# Guarda los cambios
def save
merge_data_with_front_matter!
clean_content!
return unless write
return unless detect_file_rename!
# Vuelve a leer el post para tomar los cambios
@post.read
true
end
alias :save! :save
def title
get_metadata 'title'
end
def date
get_metadata 'date'
end
def tags
get_metadata 'tags'
end
def categories
get_metadata 'categories'
end
alias :category :categories
def path
2018-02-02 22:20:31 +00:00
basename_changed? ? File.join(@site.path, '_posts', basename_from_front_matter) : @post.try(:path)
2018-01-29 22:19:10 +00:00
end
def id
get_metadata 'slug'
end
alias :slug :id
alias :to_s :id
def basename_changed?
@post.basename != basename_from_front_matter
end
2018-01-30 15:20:19 +00:00
def data
@post.data
end
def content
2018-02-02 22:20:31 +00:00
@content ||= @post.content
end
# imita Model.update_attributes de ActiveRecord
def update_attributes(attrs)
attrs.each_pair do |k,i|
# el contenido no es metadata
if k == 'content'
@content = i
else
set_metadata k.to_sym, i
end
end
2018-01-30 15:20:19 +00:00
end
2018-01-29 22:19:10 +00:00
private
def new_post
2018-02-02 22:20:31 +00:00
opts = { site: @site, collection: @site.jekyll.posts }
@post = Jekyll::Document.new(path, opts)
@site.jekyll.posts.docs << @post
@site.jekyll.posts.docs.sort!
2018-01-29 22:19:10 +00:00
@post
end
# Obtiene metadatos
def get_metadata(name)
2018-02-02 22:20:31 +00:00
@front_matter.key?(name) ? @front_matter.dig(name) : @post.data[name]
end
def set_metadata(name, value)
@front_matter[name] = value
2018-01-29 22:19:10 +00:00
end
# Cambiar el nombre del archivo si cambió el título o la fecha.
# Como Jekyll no tiene métodos para modificar un Document, lo
# engañamos eliminando la instancia de @post y recargando otra.
def detect_file_rename!
return true unless basename_changed?
if File.exist? path
2018-02-02 22:20:31 +00:00
add_error path: I18n.t('posts.errors.path')
2018-01-29 22:19:10 +00:00
return
end
FileUtils.mv @post.path, path
replace_post! path
end
def replace_post!(path)
2018-02-02 22:20:31 +00:00
@site.jekyll.posts.docs.delete @post
2018-01-29 22:19:10 +00:00
new_post
end
# Obtiene el nombre del archivo a partir de los datos que le
# pasemos
def basename_from_front_matter
2018-02-02 22:20:31 +00:00
_date = @front_matter[:date]
date = _date.respond_to?(:strftime) ? _date.strftime('%F') : _date
title = get_metadata 'slug' || Jekyll::Utils.slugify(@front_matter[:title])
ext = get_metadata 'ext' || '.markdown'
2018-01-29 22:19:10 +00:00
2018-02-02 22:20:31 +00:00
"#{date}-#{title}#{ext}"
2018-01-29 22:19:10 +00:00
end
# Toma los datos del front matter local y los mueve a los datos
# que van a ir al post. Si hay símbolos se convierten a cadenas,
2018-02-02 22:20:31 +00:00
# porque Jekyll trabaja con cadenas. Se excluyen otros datos que no
# van en el frontmatter
2018-01-29 22:19:10 +00:00
def merge_data_with_front_matter!
2018-02-02 22:20:31 +00:00
@data.merge! Hash[@front_matter.map do |k, v|
[k.to_s, v] unless REJECT_FROM_DATA.include? k
end.compact]
2018-01-29 22:19:10 +00:00
end
# Carga una copia de los datos del post original excluyendo datos
# que no nos interesan
def load_data!
@data ||= @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
# Guarda los cambios en el archivo destino
def write
r = 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
return true if r.zero?
2018-02-02 22:20:31 +00:00
add_error file: I18n.t('posts.errors.file')
2018-01-29 22:19:10 +00:00
false
end
def full_content
"#{@data.to_yaml}---\n\n#{@content}"
end
2018-02-02 22:20:31 +00:00
def add_error(hash)
hash.each_pair do |k,i|
if @errors.key?(k)
@errors[k] = [@errors[k], i]
else
@errors[k] = i
end
end
@errors
end
2018-01-29 22:19:10 +00:00
end