# 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 def initialize(site:, post:, front_matter: {}) @site = site @post = post # los errores tienen que ser un hash para que # ActiveModel pueda traer los errores normalmente @errors = {} @front_matter = front_matter new_post! unless @post 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 @post.try :path || File.join(@site.path, '_posts', basename_from_front_matter) end def id get_metadata 'slug' end alias :slug :id alias :to_s :id def basename_changed? @post.basename != basename_from_front_matter end def data @post.data end def content @post.content end private def new_post opts = { site: @site, collection: @site.posts } @post = ::Jekyll::Document.new(path, opts) @site.posts.docs << @post @site.posts.docs.sort! @post end # Obtiene metadatos def get_metadata(name) new? ? @front_matter.dig(name) : @post.data[name] 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 @errors << 'El archivo destino ya existe' return end FileUtils.mv @post.path, path replace_post! path end def replace_post!(path) @site.posts.docs.delete @post new_post end # Obtiene el nombre del archivo a partir de los datos que le # pasemos def basename_from_front_matter date = @front_matter[:date].strftime('%F') title = ::Jekyll::Utils.slugify(@front_matter[:title]) ext = @post.try :ext || 'markdown' "#{date}-#{title}.#{ext}" 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, # porque Jekyll trabaja con cadenas. def merge_data_with_front_matter! @data.merge! Hash[@front_matter.map { |k, v| [k.to_s, v] }] 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? @errors << 'No se pudo escribir el archivo' false end def full_content "#{@data.to_yaml}---\n\n#{@content}" end end