Yretek 🍃 Compone y publica, mis nuevos scripts

Ya no estoy usando Geminator, en su lugar estoy empleando dos scripts en Lua: compone y publica.

El primero sirve para componer el artículo añadiendo cabecera y pie de página al borrador, moviéndolo al directorio «programados».

El segundo comprueba si la fecha prevista de publicación coincide con la fecha de hoy y, de ser así, mueve el archivo al directorio «articulos» (sin acento por no dificultar la programación) y añade un enlace a los índices de mi web (yretek.com/index.gmi yretek.com/todo.gmi e yretek.com/articulos/index.gmi).

Lo hago en dos scripts por simplificar todo. La filosofía de un programa pequeño que haga una cosa, pero lo haga bien, me parece una magnífica idea. Además últimamente estaba editando y publicando en momentos diferentes así que tenía sentido. Y si quiero hacerlo en un solo momento basta con ```lua compone.lua && lua publica.lua```. Notaréis que no los he hecho auto-ejecutables, porque tampoco le veo la necesidad, la verdad.

Ambos tienen una licencia GNU-GPL, por tener buenos modales más que nada, porque están pensados para mi propia cápsula y necesidades. No obstante, si los consideras de utilidad no tengas reparo en cortar, pegar y modificarlos a gusto y necesidad.

He puesto los comentarios en inglés, porque francamente estar pasando del inglés al español es un lío. Sobre todo con los nombres de las variables. Pero vamos, está en un inglés muy sencillito.

Paso ahora a incluir el código de cada uno ---aunque la licencia incluye ambos aquí lo pongo solo una vez y al final del artículo--- junto con una breve explicación.

Compone.lua

-- Compone
--[[ 
  Adds header, footer to a draft file,
  building a "blog" gemtext entry
  The resulting file is then written on the ../programados folder

]]
-- Functions
function read_text_file(f_name)
-- Takes the contents from f_name Returns it as a string t_str
  local f_ile = io.open(f_name, 'r')
  local t_str = f_ile:read('*all')
  f_ile:close()
  return t_str
end

function write_text_file(f_name, texto)
  -- Writes texto on file f_name
  local f_ile = io.open(f_name, 'w')
  io.output(f_ile)
  io.write(texto)
  io.close(f_ile)
end

function ask_user(prompt_str)
-- prompts user prompt_str, gets answer
   io.stdout:write(prompt_str .. " > ")
   local answer_str = io.stdin:read()
   return answer_str
end

function is_file_here(file_name)
  local was_found = false
  i_file = io.open(file_name, i)
  if i_file~=nil then 
    io.close(i_file) 
    was_found = true 
  end
  return was_found
end

function ensures_dot_gmi(in_str)
-- Appends ".gmi" to in_str if in_str has no .gmi extension
  local out_str = in_str
  local ext = string.sub(in_str,-4,-1)
  if ext ~= ".gmi" then out_str = out_str..".gmi" end
  return out_str  
end

function split_paragraph(in_str)
-- Splits a paragraph into first and other_lines
   local i = string.find(in_str,'\n')
   if i ~= nil then
     local first_line = string.sub(in_str,1,i-1)
     local other_lines = string.sub(in_str,i+1,-1)
     return first_line, other_lines
   else
     return in_str, ""
   end
end  

function strip(in_str)
-- Strips spaces from in_str (right and left)
-- "  Hola mundo  " -> "Hola mundo"
   local r = string.find(in_str,"%s+\$")
   local out_str = in_str
   if r ~= nil then
     out_str = string.sub(in_str, 1, r-1)
   end
   local _, l = string.find(out_str,"^%s+")
   if l ~= nil then 
     out_str = string.sub(out_str, l+1,-1)
   end
   return out_str
end

function get_word_count(f_name)
  -- returns the word count from a file named f_name
  os.execute("wc -w " .. f_name .. " > .words.txt")
  local raw_wc_str = read_text_file(".words.txt")
  local wc_str = string.sub(raw_wc_str, string.find(raw_wc_str, "%d+%s"))
  return "* Palabras: "..wc_str.."\n"
end

--[[
·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-
 Main -----------------------------------------------
·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-
]]
scheduled_dir = "programados/"
today_str = os.date('%Y-%m-%d')

-- Gets draft file name from user
prompt_str = "¿Cuál es el archivo de borrador? %%% Para cancelar"
repeat
  draft_file_str = ask_user(prompt_str)
  if draft_file_str == "%%%" then os.exit() end
  prompt_str = "Fichero no encontrado.\nUse otro nombre.\n%%% detiene el programa."
until is_file_here(draft_file_str)
-- Gets to_publication date from user
repeat 
  prompt_str = "Fecha de publicación, enter para hoy"
  raw_date_str = ask_user(prompt_str)
until string.len(raw_date_str) == 10 or raw_date_str == ""
-- minimum validation

if raw_date_str == "" then date_str = today_str else date_str = raw_date_str end


raw_text_str = read_text_file(draft_file_str)

title_str, draft_text_str = split_paragraph(raw_text_str)
title_str = strip(title_str)

if title_str == "" then title_str = "Sin título" end
draft_text_str = strip(draft_text_str)
word_count_str = get_word_count(draft_file_str)

header_str = "# Yretek 🍃 " .. title_str .. "\n\n"
footer_title_str = "\n\n### Pie de página" .. "\n"
date_field_str = "* Fecha: " .. date_str .. "\n"
author_str = [[
~ Miguel de Luis Espinosa
=> mailto:yretek@proton.me yretek@proton.me

]]
footer_str = footer_title_str .. date_field_str .. word_count_str .. author_str

to_write_str = header_str .. draft_text_str .. footer_str

out_file_name = date_str .. "_" .. draft_file_str 
out_file_name = ensures_dot_gmi(out_file_name)
write_text_file(out_file_name, to_write_str)

-- a last look at the entry
os.execute("vim "..out_file_name) 
-- moves entry to its directory
os.execute("mv "..out_file_name.." "..scheduled_dir )

-- Ta da!

Explicación

Vale, esta es la secuencia del programa:

1. Pide al usuario el nombre del fichero de borrador.

2. Valida el input (determina si el fichero existe y si es así continua, si no repite la pregunta.)

3. Pide al usuario la fecha deseada de publicación (no tengo todavía una buena validación)

4. Asigna como título del artículo la primera línea del borrador.

5. Crea la cabecera (El símbolo .. en Lua concatena, o sea, una el texto, para un ordenador un texto se parece a una cadena en la que cada eslabón es un carácter, letra o número etc)

6. Crea el pie de página

7. Lo concatena todo, cabecera, contenido y pie en una sola cadena de texto.

8. Guarda el resultado anterior en un archivo.

9. Llama al mejor editor de texto del universo, o sea vim, para que yo pueda revisar el resultado.

10. Cuando termina lo mueve al directorio de "programados"

Publica.lua

Código

--[[ 
  Examines the contents of the "programados" folder, determines
  which are to be published the day this script is run.
  Those who are get moved to the "articulos" directory
  Links are then inserted into index files
  This is meant for my capsule only. Nevertheless the code is
  offered as reference.
  
  These are gemtext files which filenames follow the 
  format of yyyy-mm-dd_file_title.gmi
 
]]

function read_text_file(f_name)
-- Takes the contents from f_name
-- Returns it as a string t_str
  f_ile = io.open(f_name, 'r')
  t_str = f_ile:read('*all')
  f_ile:close()
  return t_str
end

function del_rpt_consecutive_lines(in_parg, p_line, out_parg)
  -- deletes consecutive repeated lines in a paragraph
  p_line = p_line or ""  -- previous line
  in_parg = in_parg or "" -- in paragraph
  out_parg = out_parg or "" -- out paragraph
  fl,ol = split_paragraph(in_parg)
  if p_line ~= fl then out_parg = out_parg..fl..'\n' end
  p_line = fl
  in_parg = ol
  if in_parg ~= "" then
    return del_rpt_consecutive_lines(in_parg, p_line, out_parg)
  else
    return out_parg
  end
end

function insert_in_str(target_str, mark_str, insert_str)
  -- inserta insert_str in "target_str" after mark_str 
  return string.gsub(target_str, 
                     mark_str,
                     mark_str..insert_str,
                             1)
end

function split_paragraph(in_str)
-- Splits a paragraph into first and other_lines
   local i = string.find(in_str,'\n')
   if i ~= nil then
     local first_line = string.sub(in_str,1,i-1)
     local other_lines = string.sub(in_str,i+1,-1)
     return first_line, other_lines
   else
     return in_str, ""
   end
end  

function get_title(f_name)
  local in_str = read_text_file(f_name)
  f_line, _ = split_paragraph(in_str)
  return string.sub(f_line,14,-1)
end

function make_link(article_name, title_str)
  -- Makes a suscribible link line in the shape of
  -- gemini://whatever/folder/file.gmi 2022-01-01 Title
  folder_url  = "gemini://yretek.com/articulos/" 
  full_url = folder_url .. article_name
  return "=> " .. full_url .. " " .. title_str
end

function update_index(index_f_name, insert_point, link_str)
  local old_index = read_text_file(index_f_name)
  local new_index = insert_in_str(old_index, 
                             insert_point, 
                             link_str ) 
  new_index = del_rpt_consecutive_lines(new_index)
  write_text_file(index_f_name, new_index)
end

function update_indexes(link_str)
  update_index("../index.gmi", "## Artículos\n", link_str)
  update_index("../articulos/index.gmi","·\n", link_str)
  update_index("../todo.gmi","·\n", link_str)
end

function write_text_file(f_name, texto)
  -- Writes texto on file f_name
  local f_ile = io.open(f_name, 'w')
  io.output(f_ile)
  io.write(texto)
  io.close(f_ile)
end
-- .-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-

-- backing up old indexes
os.execute("cp ../index.gmi backup/index_prncpl.bak")
os.execute("cp ../articulos/index.gmi backup/index_artcls.bak")
os.execute("cp ../todo.gmi backup/todo.bak")

local scheduled_dir = "programados/"
local today_str = os.date('%Y-%m-%d')
local link_str = ""
local articulos = "../articulos/"

local scheduled_dir_contents = io.popen("ls "..scheduled_dir):read("*a")
print(scheduled_dir_contents)

-- Table to store the lines
local scheduled_dir_lines = {}

-- Iterate over the lines in the string and store them in the table
for line in scheduled_dir_contents:gmatch("[^\r\n]+") do
  table.insert(scheduled_dir_lines, line)
end

-- Examine entries
for i, article_name in ipairs(scheduled_dir_lines) do
  to_pub_date = string.sub(article_name,1,10)
  print(to_pub_date, i, article_name)
  if to_pub_date == today_str then
    title_str = " "..to_pub_date..get_title(scheduled_dir..article_name)
    link_str = make_link(article_name, title_str).."\n"
    print("Let's go", article_name)
    -- mv artcl to articulos folder
    os.execute("mv "..scheduled_dir..article_name.." ".."../articulos/" )
    update_indexes(link_str)
    
  else
    print("No go", article_name)
  end
end

Explicación

Este es mucho más sencillo. Básicamente lee los archivos que aparecen en el directorio programados. Uno por uno va extrayendo la fecha de publicación. Esto es sencillo porque todos los archivos tiene la estructura yyyy-mm-dd_nombre_archivo.gmi Donde yyyy es el año con cuatro dígtios, mm el mes y dd el día del mes. Vamos, la estructura usual en Gemini. Luego comprueba si coincide con la fecha de hoy y, si es así, da el mensaje "Let's go" y pasa a la segunda parte. Si no coincide imprime en pantalla "No go"y va al siguiente archivo hasta terminar el contenido de la carpeta.

La segunda parte comienza por determinar el título mediante la función get_title. Esta lo único que hace es aprovechar que los títulos de mis artículos empiezan siempre en la primera línea del archivo y en la columna catorce (nota que internamente los smileis cuentan como más de un caracter). Después, make_link crea el enlace. Finalmente mueve el archivo al directorio articulos (sin acento… ) e incluye los enlaces en los índices.

Y eso es todo. Creo que sabiendo un poquito de Lua se entiende todo muy bien. Y si no, pues tengo correo, o preguntad a alguien que sepa más, ¿vale?

Cierre

Estos programas pueden sufrir cambios, pero su última versión estará en

Compone.lua

Publica.lua

Nota: en la práctica los ejecuto en local, desde mi propio ordenador, y revisado que está todo bien subo el resultado al servidor.

Y con esto termino, mostrando a continuación la licencia, por aquello de los buenos modales. Recuerda programar en abierto, sé amable.

Licencia

·-·-· Licence ·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-
  This program is free software: you can redistribute it 
  and/or modify it under the terms of the GNU 
  General Public License as published by the Free Software 
  Foundation, either version 3 of the License, or (at your option) 
  any later version.
    This program is distributed in the hope that it will be 
  useful, but WITHOUT ANY WARRANTY; without even the implied 
  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
  See the GNU General Public License for more details.
  
  For A copy of the GNU General Public License along see 
  <https://www.gnu.org/licenses/>.

Pie de página

~ Miguel de Luis Espinosa

yretek@proton.me