Статьи

Построение CMS: rubyPress

После создания базовой структуры системы управления контентом (CMS) и реального сервера, использующего Go и Node.js , вы готовы попробовать свои силы на другом языке.

На этот раз я использую язык Ruby для создания сервера. Я обнаружил, что, создавая одну и ту же программу на нескольких языках, вы начинаете получать новые идеи о лучших способах реализации программы. Вы также видите больше способов добавить функциональность в программу. Давайте начнем.

Для программирования на Ruby вам необходимо установить последнюю версию в вашей системе. В наши дни многие операционные системы уже предустановлены с Ruby (Linux и OS X), но обычно они имеют более старую версию. В этом руководстве предполагается, что у вас Ruby версии 2.4.

Самый простой способ обновления до последней версии ruby ​​- использовать RVM . Чтобы установить RVM в Linux или Mac OS X, введите в терминале следующее:

1
2
gpg —keyserver hkp://keys.gnupg.net —recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -sSL https://get.rvm.io |

Это создаст безопасное соединение для загрузки и установки RVM. Также устанавливается последняя стабильная версия Ruby. Вам придется перезагрузить вашу оболочку, чтобы завершить установку.

Для Windows вы можете скачать установщик Windows Ruby . В настоящее время этот пакет работает до Ruby 2.2.2, что прекрасно для запуска библиотек и сценариев из этого руководства.

Как только язык Ruby установлен правильно, вы можете установить библиотеки. В Ruby, как и в Go и Node, есть менеджер пакетов для установки сторонних библиотек. В терминале введите следующее:

1
2
3
4
gem install sinatra
gem install ruby-handlebars
gem install kramdown
gem install slim

Это устанавливает библиотеки Sinatra , Ruby Handlebars , Kramdown и Slim . Синатра — это фреймворк для веб-приложений. В Ruby Handlebars реализован движок шаблонов Handlebars в Ruby. Kramdown — это конвертер Markdown в HTML. Slim — это библиотека Jade для работы, но она не включает определения макросов Jade. Поэтому макросы, используемые в индексах новостей и блогов, теперь являются обычными Jade.

В верхнем каталоге создайте файл rubyPress.rb и добавьте следующий код. Я буду комментировать каждый раздел, как он добавлен в файл.

1
2
3
4
5
6
7
8
9
#
# Load the Libraries.
#
require ‘sinatra’ # http://www.sinatrarb.com/
require ‘ruby-handlebars’ # https://github.com/vincent-psarga/ruby-handlebars
require ‘kramdown’ # http://kramdown.gettalong.org
require ‘slim’ # http://slim-lang.com/
require ‘json’
require ‘date’

Первое, что нужно сделать, это загрузить библиотеки. В отличие от Node.js, они не загружаются в переменную. Библиотеки Ruby добавляют свои функции в область действия программы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#
# Setup the Handlebars engine.
#
$hbs = Handlebars::Handlebars.new
 
#
# HandleBars Helper: date
#
# Description: This helper returns the current date
# based on the format given.
#
$hbs.register_helper(‘date’) {|context, format|
    now = Date.today
    now.strftime(format)
}
 
#
# HandleBars Helper: cdate
#
# Description: This helper returns the given date
# based on the format given.
#
$hbs.register_helper(‘cdate’) {|context, date, format|
    day = Date.parse(date)
    day.strftime(format)
}
 
#
# HandleBars Helper: save
#
# Description: This helper expects a
# «|»
# is saved with the value for future
# expansions.
# value directly.
#
$hbs.register_helper(‘save’) {|context, name, text|
    #
    # If the text parameter isn’t there, then it is the
    # goPress format all combined into the name.
    # out.
    # Therefore, they need converted first.
    #
    name = String.try_convert(name)
    if name.count(«|») > 0
        parts = name.split(‘|’)
        name = parts[0]
        text = parts[1]
    end
 
    #
    # Register the new helper.
    #
    $hbs.register_helper(name) {|context, value|
        text
    }
 
    #
    # Return the text.
    #
    text
}

Библиотека Handlebars инициализируется с различными определенными вспомогательными функциями. Определены вспомогательные функции: date , cdate и save .

Вспомогательная функция date берет текущие дату и время и форматирует их в соответствии со строкой форматирования, переданной помощнику. cdate аналогичен за исключением того, что сначала проходит дата Помощник save позволяет вам указать name и value . Создает новый помощник с именем name и возвращает value . Это позволяет создавать переменные, которые указываются один раз и влияют на многие местоположения. Эта функция также принимает версию Go, которая ожидает строку с name ‘|’ как разделитель, так и value .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#
# Load Server Data.
#
$parts = {}
$parts = JSON.parse(File.read ‘./server.json’)
$styleDir = Dir.getwd + ‘/themes/styling/’ + $parts[‘CurrentStyling’]
$layoutDir = Dir.getwd + ‘/themes/layouts/’ + $parts[‘CurrentLayout’]
 
#
# Load the layouts and styles defaults.
#
$parts[«layout»] = File.read $layoutDir + ‘/template.html’
$parts[«404»] = File.read $styleDir + ‘/404.html’
$parts[«footer»] = File.read $styleDir + ‘/footer.html’
$parts[«header»] = File.read $styleDir + ‘/header.html’
$parts[«sidebar»] = File.read $styleDir + ‘/sidebar.html’
 
#
# Load all the page parts in the parts directory.
#
Dir.entries($parts[«Sitebase»] + ‘/parts/’).select {|f|
    if !File.directory?
        $parts[File.basename(f, «.*»)] = File.read $parts[«Sitebase»] + ‘/parts/’ + f
    end
}
 
#
# Setup server defaults:
#
port = $parts[«ServerAddress»].split(«:»)[2]
set :port, port

Следующая часть кода предназначена для загрузки кэшируемых элементов веб-сайта. Это все в стилях и макете вашей темы, а также элементы в подкаталоге parts . Глобальная переменная $parts сначала загружается из файла server.json . Затем эта информация используется для загрузки соответствующих элементов для заданного макета и темы. Механизм шаблонов Handlebars использует эту информацию для заполнения шаблонов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#
# Define the routes for the CMS.
#
get ‘/’ do
  page «main»
end
 
get ‘/favicon.ico’, :provides => ‘ico’ do
    File.read «#{$parts[‘Sitebase’]}/images/favicon.ico»
end
 
get ‘/stylesheets.css’, :provides => ‘css’ do
    File.read «#{$parts[«Sitebase»]}/css/final/final.css»
end
 
get ‘/scripts.js’, :provides => ‘js’ do
    File.read «#{$parts[«Sitebase»]}/js/final/final.js»
end
 
get ‘/images/:image’, :provides => ‘image’ do
    File.read «#{$parts[‘Sitebase’]}/images/#{parms[‘image’]}»
end
 
get ‘/posts/blogs/:blog’ do
    post ‘blogs’, params[‘blog’], ‘index’
end
 
get ‘/posts/blogs/:blog/:post’ do
    post ‘blogs’, params[‘blog’], params[‘post’]
end
 
get ‘/posts/news/:news’ do
    post ‘news’, params[‘news’], ‘index’
end
 
get ‘/posts/news/:news/:post’ do
    post ‘news’, params[‘news’], params[‘post’]
end
 
get ‘/:page’ do
    page params[‘page’]
end

Следующий раздел содержит определения для всех маршрутов. Sinatra — это полноценный REST- совместимый сервер. Но для этой CMS я буду использовать только глагол get . Каждый маршрут берет элементы из маршрута для передачи в функции для создания правильной страницы. В Синатре имя, которому предшествует двоеточие, указывает участок маршрута, который необходимо передать обработчику маршрута. Эти элементы находятся в хэш-таблице параметров.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#
# Various functions used in the making of the server:
#
 
#
# Function: page
#
# Description: This function is for processing a page
# in the CMS.
#
# Inputs:
# pg The page name to lookup
#
def page(pg)
    processPage $parts[«layout»], «#{$parts[«Sitebase»]}/pages/#{pg}»
end

Функция page получает имя страницы из маршрута и передает макет в переменной $parts вместе с полным путем к файлу страницы, необходимому для функции processPage . Функция processPage берет эту информацию и создает соответствующую страницу, которую затем возвращает. В Ruby выходные данные последней функции являются возвращаемым значением для функции.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
#
# Function: post
#
# Description: This function is for processing a post type
# page in the CMS.
# post type pages.
#
# Inputs:
# type The type of the post
# cat The category of the post (blog, news)
# post The actual page of the post
#
def post(type, cat, post)
    processPage $parts[«layout»], «#{$parts[«Sitebase»]}/posts/#{type}/#{cat}/#{post}»
end

Функция post аналогична функции page , за исключением того, что она работает для всех страниц типа записей. Эта функция ожидает type поста, категорию post сам post . Это создаст адрес для правильной страницы для отображения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#
# Function: figurePage
#
# Description: This function is to figure out the page
# type (ie: markdown, HTML, jade, etc), read
# the contents, and translate it to HTML.
#
# Inputs:
# page The address of the page
# without its extension.
#
def figurePage(page)
    result = «»
 
    if File.exist?
        #
        # It’s an HTML file.
        #
        result = File.read page + «.html»
    elsif File.exist?
        #
        # It’s a markdown file.
        #
        result = Kramdown::Document.new(File.read page + «.md»).to_html
 
        #
        # Fix the fancy quotes from Kramdown.
        # the Handlebars parser.
        #
        result.gsub!(«“»,»\»»)
        result.gsub!(«”»,»\»»)
    elsif File.exist?
        #
        # It’s a jade file.
        # macros.
        # Also, we have to render any Handlebars first
        # since the Slim engine dies on them.
        #
        File.write(«./tmp.txt»,$hbs.compile(File.read page + «.amber»).call($parts))
        result = Slim::Template.new(«./tmp.txt»).render()
    else
        #
        # Doesn’t exist.
        #
        result = $parts[«404»]
    end
 
    #
    # Return the results.
    #
    return result
end

Функция processPage использует функцию processPage для чтения содержимого страницы из файловой системы. Эта функция получает полный путь к файлу без расширения. Затем figurePage проверяет файл с заданным именем с расширением html для чтения HTML-файла. Второй вариант — расширение md для файла Markdown.

Наконец, он проверяет amber расширение для файла Jade. Помните: Amber — это имя библиотеки для обработки файлов синтаксиса Jade в Go. Я сохранил то же самое для функциональности. HTML-файл просто передается обратно, в то время как все файлы Markdown и Jade перед преобразованием возвращаются в HTML.

Если файл не найден, пользователь получит страницу 404 . Таким образом, ваша страница «страница не найдена» выглядит как любая другая страница, за исключением содержимого.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#
# Function: processPage
#
# Description: The function processes a page by getting
# its contents, combining with all the page
# parts using Handlebars, and processing the
# shortcodes.
#
# Inputs:
# layout The layout structure for the page
# page The complete path to the desired
# page without its extension.
#
def processPage(layout, page)
    #
    # Get the page contents and name.
    #
    $parts[«content»] = figurePage page
    $parts[«PageName»] = File.basename page
 
    #
    # Run the page through Handlebars engine.
    #
    begin
        pageHB = $hbs.compile(layout).call($parts)
    rescue
        pageHB = «
Render Error
 
«
    end
 
    #
    # Run the page through the shortcodes processor.
    #
    pageSH = processShortCodes pageHB
 
    #
    # Run the page through the Handlebar engine again.
    #
    begin
        pageFinal = $hbs.compile(pageSH).call($parts)
    rescue
        pageFinal = «
Render Error
 
» + pageSH
    end
 
    #
    # Return the results.
    #
    return pageFinal
end

Функция processPage выполняет все расширения шаблона для данных страницы. Он начинается с вызова функции figurePage для получения содержимого страницы. Затем он обрабатывает макет, переданный ему с помощью Handlebars, чтобы развернуть шаблон.

Затем функция processShortCode найдет и обработает все шорткоды на странице. Затем результаты передаются на руль второй раз для обработки любых макросов, оставленных шорткодами. Пользователь получает окончательные результаты.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
#
# Function: processShortCodes
#
# Description: This function takes the page and processes
# all of the shortcodes in the page.
#
# Inputs:
# page The contents of the page to
# process.
#
def processShortCodes(page)
    #
    # Initialize the result variable for returning.
    #
    result = «»
 
    #
    # Find the first shortcode
    #
    scregFind = /\-\[([^\]]*)\]\-/
    match1 = scregFind.match(page)
    if match1 != nil
        #
        # We found one!
        # into the result variable and initialize
        # the name, param, and contents variables.
        #
        name = «»
        param = «»
        contents = «»
        nameLine = match1[1]
        loc1 = scregFind =~ page
        result = page[0, loc1]
 
        #
        # Separate out the nameLine into a shortcode
        # name and parameters.
        #
        match2 = /(\w+)(.*)*/.match(nameLine)
        if match2.length == 2
            #
            # Just a name was found.
            #
            name = match2[1]
        else
            #
            # A name and parameter were found.
            #
            name = match2[1]
            param = match2[2]
        end
 
        #
        # Find the closing shortcode
        #
        rest = page[loc1+match1[0].length, page.length]
        regEnd = Regexp.new(«\\-\\[\\/#{name}\\]\\-«)
        match3 = regEnd.match(rest)
        if match3 != nil
            #
            # Get the contents the tags enclose.
            #
            loc2 = regEnd =~ rest
            contents = rest[0, loc2]
 
            #
            # Search the contents for shortcodes.
            #
            contents = processShortCodes(contents)
 
            #
            # If the shortcode exists, run it and include
            # the results.
            # the result.
            #
            if $shortcodes.include?(name)
                result += $shortcodes[name].call(param, contents)
            else
                result += contents
            end
 
            #
            # process the shortcodes in the rest of the
            # page.
            #
            rest = rest[loc2 + match3[0].length, page.length]
            result += processShortCodes(rest)
        else
            #
            # There wasn’t a closure.
            # send the page back.
            #
            result = page
        end
    else
        #
        # No shortcodes.
        #
        result = page
    end
 
    return result
end

Функция processShortCodes берет заданный текст, находит каждый шорткод и запускает указанный шорткод с аргументами и содержимым шорткода. Я использую подпрограмму шорткода для обработки содержимого шорткодов.

Шорткод — это HTML-подобный тег, который использует -[ и ]- для разграничения открывающего тега и -[/ and ]- закрывающего тега. Открывающий тег также содержит параметры для шорткода. Поэтому пример шорткода будет:

1
2
3
-[box]-
This is inside a box.
-[/box]-

Этот шорткод определяет шорткод box без каких-либо параметров с содержанием <p>This is inside a box.</p> . Шорткод box оборачивает содержимое в соответствующий HTML-код, чтобы создать текстовую рамку с текстом по центру блока. Если позже вы захотите изменить способ отображения box , вам нужно только изменить определение шорткода. Это экономит много работы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#
# Data Structure: $shortcodes
#
# Description: This data structure contains all
# the valid shortcodes names and the
# function.
# receive the arguments and the
# that the shortcode encompasses.
#
$shortcodes = {
    «box» => lambda { |args, contents|
        return(«
    },
   ‘Column1’=> lambda { |args, contents|
        return(«
   },
   ‘Column2’ => lambda { |args, contents|
      return(«
   },
   ‘Column1of3’ => lambda { |args, contents|
      return(«
   },
   ‘Column2of3’ => lambda { |args, contents|
      return(«
   },
   ‘Column3of3’ => lambda { |args, contents|
      return(«
   },
   ‘php’ => lambda { |args, contents|
      return(«
   },
   ‘js’ => lambda { |args, contents|
      return(«
   },
    «html» => lambda { |args, contents|
        return(«
    },
   ‘css’ => lambda {|args, contents|
      return(«
   }
}

Последняя вещь в файле — это хеш-таблица $shortcodes содержащая подпрограммы для шорткода. Это простые шорткоды, но вы можете создавать другие шорткоды, которые будут настолько сложными, насколько вы хотите.

Все шорткоды должны принимать два параметра: args и contents . Эти строки содержат параметры шорткода и содержимое, которое окружают шорткоды. Поскольку шорткоды находятся внутри хеш-таблицы, я использовал лямбда-функцию для их определения. Лямбда-функция — это функция без имени. Единственный способ запустить эти функции — из хеш-массива.

rubyPress.rb файл rubyPress.rb с указанным выше содержимым, вы можете запустить сервер с:

1
ruby rubyPress.rb

Поскольку платформа Sinatra работает со структурой Ruby on Rails Rack, вы можете использовать Pow для запуска сервера. Pow настроит хост-файлы вашей системы для локального запуска вашего сервера так же, как и с хост-сайта. Вы можете установить Pow с Powder, используя следующие команды в командной строке:

1
2
gem install powder
powder install

Powder — это программа командной строки для управления сайтами Pow на вашем компьютере. Чтобы Пау мог видеть ваш сайт, вы должны создать мягкую ссылку на каталог вашего проекта в каталоге ~/.pow . Если сервер находится в каталоге /Users/test/Documents/rubyPress , вы должны выполнить следующие команды:

1
2
cd ~/.pow
ln -s /Users/test/Documents/rubyPress rubyPress

ln -s создает мягкую ссылку на каталог, указанный первым, с именем, указанным во втором. Затем Pow настроит домен в вашей системе с именем программной ссылки. В приведенном выше примере, перейдя на веб-сайт http://rubyPress.dev в браузере загрузит страницу с сервера.

Чтобы запустить сервер, введите следующее после создания программной ссылки:

1
powder start

Чтобы перезагрузить сервер после внесения некоторых изменений в код, введите следующее:

1
powder restart
rubyPress Главная страница
rubyPress Главная страница

Переход на сайт в браузере приведет к изображению выше. Пау создаст сайт по адресу http://rubyPress.dev . Независимо от того, какой метод вы используете для запуска сайта, вы увидите одну и ту же страницу.

Ну, вы сделали это. Еще одна CMS, но на этот раз в Ruby. Эта версия является самой короткой версией из всех CMS, созданных в этой серии. Поэкспериментируйте с кодом и посмотрите, как вы можете расширить эту базовую среду.