Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
Después de crear una estructura básica del sistema de gestión de contenido (CMS) y el servidor real utilizando Go y Node.js, está listo para probar su mano en otro lenguaje.
Esta vez, estoy usando el lenguaje Ruby para crear el servidor. He descubierto que al crear el mismo programa en varios lenguajes, comienzas a obtener nuevas ideas sobre mejores formas de implementar el programa. También ve más formas de agregar funcionalidad al programa. Empecemos.
Configurar y cargar las bibliotecas
Para programar en Ruby, deberá tener instalada la última versión en su sistema. Muchos sistemas operativos vienen preinstalados con Ruby en la actualidad (Linux y OS X), pero generalmente tienen una versión anterior. Este tutorial asume que tienes Ruby versión 2.4.
La forma más fácil de actualizar a la última versión de ruby es usar RVM. Para instalar RVM en Linux o Mac OS X, escriba lo siguiente en una terminal:
1 |
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 |
2 |
curl -sSL https://get.rvm.io | bash -s stable |
Esto creará una conexión segura para descargar e instalar RVM. Esto instala la última versión estable de Ruby también. Deberás volver a cargar tu caparazón para finalizar la instalación.
Para Windows, puede descargar el instalador de Windows Ruby. Actualmente, este paquete corresponde a Ruby 2.2.2, que está bien para ejecutar las bibliotecas y los scripts en este tutorial.
Una vez que Ruby está instalado correctamente, ahora puede instalar las bibliotecas. Ruby, al igual que Go y Node, tiene un administrador de paquetes para instalar bibliotecas de terceros. En la terminal, escriba lo siguiente:
1 |
gem install sinatra
|
2 |
gem install ruby-handlebars
|
3 |
gem install kramdown
|
4 |
gem install slim
|
Esto instala las bibliotecas Sinatra, Ruby Handlebars, Kramdown y Slim. Sinatra es un marco de aplicacion. Ruby Handlebars implementa el motor de plantillas Handlebars en Ruby. Kramdown es un convertidor Markdown to HTML. Slim es una biblioteca similar a Jade, pero no incluye las definiciones macro de Jade. Por lo tanto, las macros utilizadas en los índices de publicaciones de Noticias y Blog ahora son Jade normales.
Creando el archivo rubyPress.rb
En el
directorio superior, crea el archivo rubyPress.rb
y agrega el siguiente
código. Voy a comentar acerca de cada sección a medida que se agrega al
archivo.
1 |
#
|
2 |
# Load the Libraries.
|
3 |
#
|
4 |
require 'sinatra' # http://www.sinatrarb.com/ |
5 |
require 'ruby-handlebars' # https://github.com/vincent-psarga/ruby-handlebars |
6 |
require 'kramdown' # http://kramdown.gettalong.org |
7 |
require 'slim' # http://slim-lang.com/ |
8 |
require 'json' |
9 |
require 'date' |
Lo primero que debe hacer es cargar las bibliotecas. A diferencia de Node.js, estos no se cargan en una variable. Las bibliotecas de Ruby agregan sus funciones al alcance del programa.
1 |
#
|
2 |
# Setup the Handlebars engine.
|
3 |
#
|
4 |
$hbs = Handlebars::Handlebars.new |
5 |
|
6 |
#
|
7 |
# HandleBars Helper: date
|
8 |
#
|
9 |
# Description: This helper returns the current date
|
10 |
# based on the format given.
|
11 |
#
|
12 |
$hbs.register_helper('date') {|context, format| |
13 |
now = Date.today |
14 |
now.strftime(format) |
15 |
}
|
16 |
|
17 |
#
|
18 |
# HandleBars Helper: cdate
|
19 |
#
|
20 |
# Description: This helper returns the given date
|
21 |
# based on the format given.
|
22 |
#
|
23 |
$hbs.register_helper('cdate') {|context, date, format| |
24 |
day = Date.parse(date) |
25 |
day.strftime(format) |
26 |
}
|
27 |
|
28 |
#
|
29 |
# HandleBars Helper: save
|
30 |
#
|
31 |
# Description: This helper expects a
|
32 |
# "|" where the name
|
33 |
# is saved with the value for future
|
34 |
# expansions. It also returns the
|
35 |
# value directly.
|
36 |
#
|
37 |
$hbs.register_helper('save') {|context, name, text| |
38 |
#
|
39 |
# If the text parameter isn't there, then it is the
|
40 |
# goPress format all combined into the name. Split it
|
41 |
# out. The parameters are not String objects.
|
42 |
# Therefore, they need converted first.
|
43 |
#
|
44 |
name = String.try_convert(name) |
45 |
if name.count("|") > 0 |
46 |
parts = name.split('|') |
47 |
name = parts[0] |
48 |
text = parts[1] |
49 |
end
|
50 |
|
51 |
#
|
52 |
# Register the new helper.
|
53 |
#
|
54 |
$hbs.register_helper(name) {|context, value| |
55 |
text
|
56 |
}
|
57 |
|
58 |
#
|
59 |
# Return the text.
|
60 |
#
|
61 |
text
|
62 |
}
|
La biblioteca Handlebars se inicializa con las diferentes funciones auxiliares definidas. Las funciones auxiliares definidas son date
, cdate
, y save
.
La función helper date
toma la fecha y hora actual, y la formatea de
acuerdo con la cadena de formato que se pasa al helper. cdate
es similar
excepto para pasar la fecha primero. save
Helper le permite especificar
un name
y un valu
. Crea un nuevo ayudante con el name
y devuelve
el value
. Esto le permite crear variables que se especifican una vez y
afectan a muchas ubicaciones. Esta función también toma la versión Go,
que espera una cadena con el name
, '|' como separador y value
.
1 |
#
|
2 |
# Load Server Data.
|
3 |
#
|
4 |
$parts = {} |
5 |
$parts = JSON.parse(File.read './server.json') |
6 |
$styleDir = Dir.getwd + '/themes/styling/' + $parts['CurrentStyling'] |
7 |
$layoutDir = Dir.getwd + '/themes/layouts/' + $parts['CurrentLayout'] |
8 |
|
9 |
#
|
10 |
# Load the layouts and styles defaults.
|
11 |
#
|
12 |
$parts["layout"] = File.read $layoutDir + '/template.html' |
13 |
$parts["404"] = File.read $styleDir + '/404.html' |
14 |
$parts["footer"] = File.read $styleDir + '/footer.html' |
15 |
$parts["header"] = File.read $styleDir + '/header.html' |
16 |
$parts["sidebar"] = File.read $styleDir + '/sidebar.html' |
17 |
|
18 |
#
|
19 |
# Load all the page parts in the parts directory.
|
20 |
#
|
21 |
Dir.entries($parts["Sitebase"] + '/parts/').select {|f| |
22 |
if !File.directory? f |
23 |
$parts[File.basename(f, ".*")] = File.read $parts["Sitebase"] + '/parts/' + f |
24 |
end
|
25 |
}
|
26 |
|
27 |
#
|
28 |
# Setup server defaults:
|
29 |
#
|
30 |
port = $parts["ServerAddress"].split(":")[2] |
31 |
set :port, port |
La siguiente parte del código es para cargar los elementos almacenables en caché del sitio web. Esto es todo en los estilos y el diseño de su tema, y los elementos en el subdirectorio parts
. Una variable global, $parts
, se carga primero desde el archivo
server.json
. Esa información se utiliza para cargar los elementos
adecuados para el diseño y el tema especificado. El motor de plantillas
de manillar utiliza esta información para completar las plantillas.
1 |
#
|
2 |
# Define the routes for the CMS.
|
3 |
#
|
4 |
get '/' do |
5 |
page "main" |
6 |
end
|
7 |
|
8 |
get '/favicon.ico', :provides => 'ico' do |
9 |
File.read "#{$parts['Sitebase']}/images/favicon.ico" |
10 |
end
|
11 |
|
12 |
get '/stylesheets.css', :provides => 'css' do |
13 |
File.read "#{$parts["Sitebase"]}/css/final/final.css" |
14 |
end
|
15 |
|
16 |
get '/scripts.js', :provides => 'js' do |
17 |
File.read "#{$parts["Sitebase"]}/js/final/final.js" |
18 |
end
|
19 |
|
20 |
get '/images/:image', :provides => 'image' do |
21 |
File.read "#{$parts['Sitebase']}/images/#{parms['image']}" |
22 |
end
|
23 |
|
24 |
get '/posts/blogs/:blog' do |
25 |
post 'blogs', params['blog'], 'index' |
26 |
end
|
27 |
|
28 |
get '/posts/blogs/:blog/:post' do |
29 |
post 'blogs', params['blog'], params['post'] |
30 |
end
|
31 |
|
32 |
get '/posts/news/:news' do |
33 |
post 'news', params['news'], 'index' |
34 |
end
|
35 |
|
36 |
get '/posts/news/:news/:post' do |
37 |
post 'news', params['news'], params['post'] |
38 |
end
|
39 |
|
40 |
get '/:page' do |
41 |
page params['page'] |
42 |
end
|
La siguiente sección contiene las definiciones para todas las rutas. Sinatra es un servidor completo que cumple con REST. Pero para este CMS, solo usaré el verbo get
. Cada ruta toma los elementos de la ruta para pasar a las funciones para
producir la página correcta. En Sinatra, un nombre precedido por dos
puntos especifica una sección de la ruta para pasar al controlador de
ruta. Estos artículos están en una tabla hash params
.
1 |
#
|
2 |
# Various functions used in the making of the server:
|
3 |
#
|
4 |
|
5 |
#
|
6 |
# Function: page
|
7 |
#
|
8 |
# Description: This function is for processing a page
|
9 |
# in the CMS.
|
10 |
#
|
11 |
# Inputs:
|
12 |
# pg The page name to lookup
|
13 |
#
|
14 |
def page(pg) |
15 |
processPage $parts["layout"], "#{$parts["Sitebase"]}/pages/#{pg}" |
16 |
end
|
La función
page
obtiene el nombre de una página de la ruta y pasa el diseño en
la variable $parts
junto con la ruta completa al archivo de página
necesario para la función processPage
. La función processPage
toma esta información y crea la página adecuada,
que luego devuelve. En Ruby, la salida de la última función es el valor
de retorno para la función.
1 |
#
|
2 |
# Function: post
|
3 |
#
|
4 |
# Description: This function is for processing a post type
|
5 |
# page in the CMS. All blog and news pages are
|
6 |
# post type pages.
|
7 |
#
|
8 |
# Inputs:
|
9 |
# type The type of the post
|
10 |
# cat The category of the post (blog, news)
|
11 |
# post The actual page of the post
|
12 |
#
|
13 |
def post(type, cat, post) |
14 |
processPage $parts["layout"], "#{$parts["Sitebase"]}/posts/#{type}/#{cat}/#{post}" |
15 |
end
|
La función post
es como la función page
, excepto que funciona para todas las páginas de tipo de publicación. Esta función espera el type
de publicación, la cat
egoría de publicación y la post
misma. Estos crearán la dirección para que se muestre la página correcta.
1 |
#
|
2 |
# Function: figurePage
|
3 |
#
|
4 |
# Description: This function is to figure out the page
|
5 |
# type (ie: markdown, HTML, jade, etc), read
|
6 |
# the contents, and translate it to HTML.
|
7 |
#
|
8 |
# Inputs:
|
9 |
# page The address of the page
|
10 |
# without its extension.
|
11 |
#
|
12 |
def figurePage(page) |
13 |
result = "" |
14 |
|
15 |
if File.exist? page + ".html" |
16 |
#
|
17 |
# It's an HTML file.
|
18 |
#
|
19 |
result = File.read page + ".html" |
20 |
elsif File.exist? page + ".md" |
21 |
#
|
22 |
# It's a markdown file.
|
23 |
#
|
24 |
result = Kramdown::Document.new(File.read page + ".md").to_html |
25 |
|
26 |
#
|
27 |
# Fix the fancy quotes from Kramdown. It kills
|
28 |
# the Handlebars parser.
|
29 |
#
|
30 |
result.gsub!("“","\"") |
31 |
result.gsub!("”","\"") |
32 |
elsif File.exist? page + ".amber" |
33 |
#
|
34 |
# It's a jade file. Slim doesn't support
|
35 |
# macros. Therefore, not as powerful as straight jade.
|
36 |
# Also, we have to render any Handlebars first
|
37 |
# since the Slim engine dies on them.
|
38 |
#
|
39 |
File.write("./tmp.txt",$hbs.compile(File.read page + ".amber").call($parts)) |
40 |
result = Slim::Template.new("./tmp.txt").render() |
41 |
else
|
42 |
#
|
43 |
# Doesn't exist. Give the 404 page.
|
44 |
#
|
45 |
result = $parts["404"] |
46 |
end
|
47 |
|
48 |
#
|
49 |
# Return the results.
|
50 |
#
|
51 |
return result |
52 |
end
|
La función figurePage
usa la función processPage
para leer el contenido de la página del sistema de archivos. Esta función recibe la ruta completa al archivo sin la extensión. FigurePage
luego prueba un archivo con el nombre dado con la extensión
html
para leer un archivo HTML. La segunda opción es para una extensión
md
para un archivo Markdown.
Por último, comprueba si hay una extensión
amber
para un archivo de Jade. Recuerde: Amber es el nombre de la
biblioteca para procesar los archivos de sintaxis de Jade en Go. Lo
mantuve igual para inter-funcionalidad. Un archivo HTML simplemente se
devuelve, mientras que todos los archivos Markdown y Jade se convierten a
HTML antes de volver.
Si no se encuentra un archivo, el usuario recibirá
la página 404
. De esta manera, su página "página no encontrada" se
parece a cualquier otra página excepto por los contenidos.
1 |
#
|
2 |
# Function: processPage
|
3 |
#
|
4 |
# Description: The function processes a page by getting
|
5 |
# its contents, combining with all the page
|
6 |
# parts using Handlebars, and processing the
|
7 |
# shortcodes.
|
8 |
#
|
9 |
# Inputs:
|
10 |
# layout The layout structure for the page
|
11 |
# page The complete path to the desired
|
12 |
# page without its extension.
|
13 |
#
|
14 |
def processPage(layout, page) |
15 |
#
|
16 |
# Get the page contents and name.
|
17 |
#
|
18 |
$parts["content"] = figurePage page |
19 |
$parts["PageName"] = File.basename page |
20 |
|
21 |
#
|
22 |
# Run the page through Handlebars engine.
|
23 |
#
|
24 |
begin
|
25 |
pageHB = $hbs.compile(layout).call($parts) |
26 |
rescue
|
27 |
pageHB = " |
28 |
Render Error
|
29 |
|
30 |
"
|
31 |
end
|
32 |
|
33 |
#
|
34 |
# Run the page through the shortcodes processor.
|
35 |
#
|
36 |
pageSH = processShortCodes pageHB |
37 |
|
38 |
#
|
39 |
# Run the page through the Handlebar engine again.
|
40 |
#
|
41 |
begin
|
42 |
pageFinal = $hbs.compile(pageSH).call($parts) |
43 |
rescue
|
44 |
pageFinal = " |
45 |
Render Error
|
46 |
|
47 |
" + pageSH |
48 |
end
|
49 |
|
50 |
#
|
51 |
# Return the results.
|
52 |
#
|
53 |
return pageFinal |
54 |
end
|
La función processPage
realiza todas las expansiones de plantilla en los datos de página. Comienza llamando a la función figurePage
para obtener los contenidos de la página. A continuación, procesa el diseño que se le pasó con Handlebars para expandir la plantilla.
Luego la función processShortCode
encontrará y procesará todos los
códigos cortos en la página. Los resultados se pasan a Handlebars por
segunda vez para procesar las macros dejadas por los shortcodes. El
usuario recibe los resultados finales.
1 |
#
|
2 |
# Function: processShortCodes
|
3 |
#
|
4 |
# Description: This function takes the page and processes
|
5 |
# all of the shortcodes in the page.
|
6 |
#
|
7 |
# Inputs:
|
8 |
# page The contents of the page to
|
9 |
# process.
|
10 |
#
|
11 |
def processShortCodes(page) |
12 |
#
|
13 |
# Initialize the result variable for returning.
|
14 |
#
|
15 |
result = "" |
16 |
|
17 |
#
|
18 |
# Find the first shortcode
|
19 |
#
|
20 |
scregFind = /\-\[([^\]]*)\]\-/ |
21 |
match1 = scregFind.match(page) |
22 |
if match1 != nil |
23 |
#
|
24 |
# We found one! get the text before it
|
25 |
# into the result variable and initialize
|
26 |
# the name, param, and contents variables.
|
27 |
#
|
28 |
name = "" |
29 |
param = "" |
30 |
contents = "" |
31 |
nameLine = match1[1] |
32 |
loc1 = scregFind =~ page |
33 |
result = page[0, loc1] |
34 |
|
35 |
#
|
36 |
# Separate out the nameLine into a shortcode
|
37 |
# name and parameters.
|
38 |
#
|
39 |
match2 = /(\w+)(.*)*/.match(nameLine) |
40 |
if match2.length == 2 |
41 |
#
|
42 |
# Just a name was found.
|
43 |
#
|
44 |
name = match2[1] |
45 |
else
|
46 |
#
|
47 |
# A name and parameter were found.
|
48 |
#
|
49 |
name = match2[1] |
50 |
param = match2[2] |
51 |
end
|
52 |
|
53 |
#
|
54 |
# Find the closing shortcode
|
55 |
#
|
56 |
rest = page[loc1+match1[0].length, page.length] |
57 |
regEnd = Regexp.new("\\-\\[\\/#{name}\\]\\-") |
58 |
match3 = regEnd.match(rest) |
59 |
if match3 != nil |
60 |
#
|
61 |
# Get the contents the tags enclose.
|
62 |
#
|
63 |
loc2 = regEnd =~ rest |
64 |
contents = rest[0, loc2] |
65 |
|
66 |
#
|
67 |
# Search the contents for shortcodes.
|
68 |
#
|
69 |
contents = processShortCodes(contents) |
70 |
|
71 |
#
|
72 |
# If the shortcode exists, run it and include
|
73 |
# the results. Otherwise, add the contents to
|
74 |
# the result.
|
75 |
#
|
76 |
if $shortcodes.include?(name) |
77 |
result += $shortcodes[name].call(param, contents) |
78 |
else
|
79 |
result += contents |
80 |
end
|
81 |
|
82 |
#
|
83 |
# process the shortcodes in the rest of the
|
84 |
# page.
|
85 |
#
|
86 |
rest = rest[loc2 + match3[0].length, page.length] |
87 |
result += processShortCodes(rest) |
88 |
else
|
89 |
#
|
90 |
# There wasn't a closure. Therefore, just
|
91 |
# send the page back.
|
92 |
#
|
93 |
result = page |
94 |
end
|
95 |
else
|
96 |
#
|
97 |
# No shortcodes. Just return the page.
|
98 |
#
|
99 |
result = page |
100 |
end
|
101 |
|
102 |
return result |
103 |
end
|
La función
processShortCodes
toma el texto dado, encuentra cada shortcode y ejecuta
el shortcode especificado con los argumentos y el contenido del
shortcode. Utilizo la rutina shortcode para procesar los contenidos de códigos cortos también.
Un
shortcode es una etiqueta similar a HTML que usa -[
y ]-
para delimitar
la etiqueta de apertura y -[/
y ]-
la etiqueta de cierre. La etiqueta
de apertura también contiene los parámetros para el código corto. Por lo
tanto, un shortcode de ejemplo sería:
1 |
-[box]- |
2 |
This is inside a box. |
3 |
-[/box]- |
Este
shortcode define el shortcode de box
sin ningún parámetro con los
contenidos de <p>This is inside a box.</p>
. El
código abreviado box
ajusta los contenidos en el HTML apropiado
para producir un cuadro alrededor del texto con el texto centrado en el
box. Si luego desea cambiar la forma en que se representa el box
,
solo debe cambiar la definición del código abreviado. Esto ahorra mucho
trabajo.
1 |
#
|
2 |
# Data Structure: $shortcodes
|
3 |
#
|
4 |
# Description: This data structure contains all
|
5 |
# the valid shortcodes names and the
|
6 |
# function. All shortcodes should
|
7 |
# receive the arguments and the
|
8 |
# that the shortcode encompasses.
|
9 |
#
|
10 |
$shortcodes = { |
11 |
"box" => lambda { |args, contents| |
12 |
return("#{contents}") |
13 |
},
|
14 |
'Column1'=> lambda { |args, contents| |
15 |
return("#{contents}") |
16 |
},
|
17 |
'Column2' => lambda { |args, contents| |
18 |
return("#{contents}") |
19 |
},
|
20 |
'Column1of3' => lambda { |args, contents| |
21 |
return("#{contents}") |
22 |
},
|
23 |
'Column2of3' => lambda { |args, contents| |
24 |
return("#{contents}") |
25 |
},
|
26 |
'Column3of3' => lambda { |args, contents| |
27 |
return("#{contents}") |
28 |
},
|
29 |
'php' => lambda { |args, contents| |
30 |
return("#{contents}") }, 'js' => lambda { |args, contents| return("#{contents}") }, "html" => lambda { |args, contents| return("#{contents}") }, 'css' => lambda {|args, contents| return("#{contents}") } } |
Lo último en el archivo es la tabla de hash $shortcodes
que contiene las rutinas shortcode. Estos son códigos cortos simples, pero puede crear otros códigos cortos para que sean lo más complejos que desee.
Todos los shortcodes tienen que aceptar dos parámetros: args
y
contents
. Estas cadenas contienen los parámetros del shortcode y los
contenidos que rodean a los códigos cortos. Como los códigos cortos
están dentro de una tabla hash, utilicé una función lambda para
definirlos. Una función lambda es una función sin nombre. La única forma
de ejecutar estas funciones es desde la matriz hash.
Ejecutando el Servidor
Una vez que haya creado el archivo rubyPress.rb
con los
contenidos anteriores, puede ejecutar el servidor con:
1 |
ruby rubyPress.rb |
Como el marco de Sinatra funciona con la estructura Ruby on Rails Rack, puede usar Pow para ejecutar el servidor. Pow configurará los archivos host de su sistema para ejecutar su servidor localmente de la misma manera que lo haría desde un sitio alojado. Puede instalar Pow with Powder usando los siguientes comandos en la línea de comando:
1 |
gem install powder
|
2 |
powder install
|
Powder es una rutina de línea de comandos para administrar sitios Pow en tu computadora. Para que Pow vea su sitio, debe crear un enlace suave al directorio de su proyecto en el directorio ~/.pow
Si el servidor está en el directorio /Users/test/Documents/rubyPress
, ejecutará los siguientes comandos:
1 |
cd ~/.pow
|
2 |
ln -s /Users/test/Documents/rubyPress rubyPress |
El ln -s
crea un soft link (enlace suave) al
directorio especificado primero, con el nombre especificado en segundo
lugar. Pow luego configurará un dominio en su sistema con el nombre del
enlace suave. En el ejemplo anterior, ir al sitio web
http://rubyPress.dev
en el navegador cargará la página desde el
servidor.
Para iniciar el servidor, escriba lo siguiente después de crear el enlace suave:
1 |
powder start |
1inicio en polvoPara volver a cargar el servidor después de hacer algunos cambios de código, escriba lo siguiente:
1 |
powder restart |



Ir al sitio web en
el navegador dará como resultado la imagen de arriba. Pow establecerá
el sitio en http://rubyPress.dev
. Independientemente del método que
utilice para iniciar el sitio, verá la misma página
resultante.
Conclusión
Bueno, lo has hecho. Otro CMS, pero esta vez en Ruby. Esta versión es la versión más corta de todos los CMS creados en esta serie. Experimente con el código y vea cómo puede ampliar este marco básico.