neruda 0.0.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/bin/pablo +135 -238
  3. data/lib/neruda/config.rb +137 -0
  4. data/lib/neruda/config/lisp_config.rb +254 -0
  5. data/lib/neruda/config/org-config.el +18 -0
  6. data/lib/neruda/config/ox-neruda.el +114 -0
  7. data/lib/neruda/emacs.rb +44 -0
  8. data/lib/neruda/index.rb +122 -0
  9. data/lib/neruda/index/atom_generator.rb +86 -0
  10. data/lib/neruda/index/org_generator.rb +115 -0
  11. data/lib/neruda/org_file.rb +299 -0
  12. data/lib/neruda/org_file/class_methods.rb +72 -0
  13. data/lib/neruda/org_file/extracter.rb +72 -0
  14. data/lib/neruda/org_file/htmlizer.rb +53 -0
  15. data/lib/neruda/preview.rb +55 -0
  16. data/lib/neruda/templater.rb +112 -0
  17. data/lib/neruda/utils.rb +212 -0
  18. data/lib/neruda/version.rb +6 -0
  19. data/lib/tasks/org.rake +84 -0
  20. data/lib/tasks/site.rake +86 -0
  21. data/lib/tasks/sync.rake +34 -0
  22. data/lib/tasks/tags.rake +19 -0
  23. data/locales/en.yml +37 -0
  24. data/locales/fr.yml +37 -0
  25. data/themes/default/css/htmlize.css +346 -0
  26. data/themes/default/css/style.css +153 -0
  27. data/themes/default/img/bottom.png +0 -0
  28. data/themes/default/img/tic.png +0 -0
  29. data/themes/default/img/top.png +0 -0
  30. metadata +153 -43
  31. data/README.md +0 -98
  32. data/docs/Rakefile.example +0 -4
  33. data/docs/config.yml.example +0 -17
  34. data/lib/assets/chapter.slim +0 -14
  35. data/lib/assets/index.slim +0 -13
  36. data/lib/assets/layout.slim +0 -17
  37. data/lib/assets/style.css +0 -199
  38. data/lib/neruda.rb +0 -106
  39. data/lib/neruda/chapter.rb +0 -26
  40. data/lib/neruda/url.rb +0 -14
  41. data/lib/tasks/book.rake +0 -60
  42. data/lib/tasks/capistrano/chapters.rake +0 -60
  43. data/lib/tasks/capistrano/sinatra.rake +0 -18
  44. data/lib/tasks/chapters.rake +0 -132
  45. data/lib/tasks/sinatra.rake +0 -36
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+ require 'neruda/version'
5
+
6
+ module Neruda
7
+ # This module contains utilitary methods to ease ~org-config.el~
8
+ # file generation
9
+ module LispConfig
10
+ # Fetch and return the last published version of Org.
11
+ #
12
+ # @return [String] the new x.x.x version string of Org
13
+ def org_last_version
14
+ return @org_version if @org_version
15
+ if File.exist?('tmp/__last_org_version__')
16
+ @org_version = IO.read('tmp/__last_org_version__')
17
+ return @org_version
18
+ end
19
+ index = URI('https://orgmode.org/index.html').open.read
20
+ last_ver = index.match(/https:\/\/orgmode\.org\/org-([0-9.]+)\.tar\.gz/)
21
+ # :nocov:
22
+ if last_ver.nil?
23
+ warn 'Org last version not found'
24
+ return nil
25
+ end
26
+ FileUtils.mkdir_p 'tmp'
27
+ IO.write('tmp/__last_org_version__', last_ver[1])
28
+ # :nocov:
29
+ @org_version = last_ver[1]
30
+ end
31
+
32
+ # Generate emacs lisp configuration file for Org and write it.
33
+ #
34
+ # This method saves the generated configuration in the file
35
+ # ~org-config.el~ at the root of your project, overwriting it if it
36
+ # existed already.
37
+ #
38
+ # @return [Integer] the length written (as returned by the
39
+ # underlying ~IO.write~ method call)
40
+ def write_org_lisp_config(with_tags: false)
41
+ projects = org_generate_projects(with_tags: with_tags)
42
+ workdir = Dir.pwd
43
+ content = IO.read(File.expand_path('./org-config.el', __dir__))
44
+ .gsub('__VERSION__', Neruda::VERSION)
45
+ .gsub('__WORK_DIR__', workdir)
46
+ .gsub('__NERUDA_DIR__', __dir__)
47
+ .gsub('__ORG_VER__', org_last_version)
48
+ .gsub('__ALL_PROJECTS__', all_projects(projects))
49
+ .gsub('__THEME_CONFIG__', org_default_theme_config)
50
+ .gsub('__ALL_PROJECTS_NAMES__', project_names(projects))
51
+ .gsub('__LONG_DATE_FMT__', r18n_full_datetime_format)
52
+ .gsub('__AUTHOR_EMAIL__', settings['author_email'] || '')
53
+ .gsub('__AUTHOR_NAME__', settings['author'])
54
+ IO.write("#{workdir}/org-config.el", content)
55
+ end
56
+
57
+ # Generate emacs directory variables file.
58
+ #
59
+ # This method generate the file ~.dir-locals.el~, which is
60
+ # responsible to load neruda Org settings when visiting an Org file
61
+ # of this neruda instance.
62
+ #
63
+ # @return [Integer] the length written (as returned by the
64
+ # underlying ~IO.write~ method call)
65
+ def write_dir_locals
66
+ workdir = Dir.pwd
67
+ IO.write(
68
+ "#{workdir}/.dir-locals.el",
69
+ "((org-mode . ((eval . (load-file \"#{workdir}/org-config.el\")))))"
70
+ )
71
+ end
72
+
73
+ private
74
+
75
+ def r18n_full_datetime_format
76
+ locale = R18n.get.locale
77
+ date_fmt = R18n.t.neruda.index.full_date_format(
78
+ date: locale.full_format
79
+ )
80
+ date_fmt = locale.year_format.sub('_', date_fmt)
81
+ time_fmt = locale.time_format.delete('_').strip
82
+ R18n.t.neruda.index.full_date_with_time_format(
83
+ date: date_fmt, time: time_fmt
84
+ )
85
+ end
86
+
87
+ def ruby_to_lisp_boolean(value)
88
+ return 't' if value == true
89
+ 'nil'
90
+ end
91
+
92
+ def project_names(projects)
93
+ names = projects.keys.map do |p|
94
+ ["\"#{p}\"", "\"#{p}-assets\""]
95
+ end.flatten
96
+ names << "\"theme-#{settings['theme']}\""
97
+ sources.each do |s|
98
+ next unless s['theme'] && s['theme'] != settings['theme']
99
+ theme = "\"theme-#{s['theme']}\""
100
+ next if names.include? theme
101
+ names << theme
102
+ end
103
+ names.join(' ')
104
+ end
105
+
106
+ def all_projects(projects)
107
+ projects.values.join("\n").strip
108
+ .gsub(/\n\s*\n/, "\n")
109
+ .gsub(/\n/, "\n ")
110
+ end
111
+
112
+ # Return the full path to the publication path of a given project
113
+ # configuration.
114
+ #
115
+ # @param project [Hash] a project configuration (as extracted from
116
+ # the ~sources~ key)
117
+ # @return [String] the full path to the target dir of this project
118
+ def publication_path(project)
119
+ publish_in = [Dir.pwd, settings['public_folder']]
120
+ publish_in << project['target'] unless project['target'] == '.'
121
+ publish_in.join('/')
122
+ end
123
+
124
+ def org_project(project_name, opts)
125
+ publish_in = publication_path(opts)
126
+ other_lines = [
127
+ format(':recursive %<value>s',
128
+ value: ruby_to_lisp_boolean(opts['recursive']))
129
+ ]
130
+ if opts['exclude']
131
+ other_lines << format(':exclude "%<value>s"',
132
+ value: opts['exclude'])
133
+ end
134
+ themeconf = org_theme_config(opts['theme']) || ''
135
+ <<~ORGPROJECT
136
+ ("#{project_name}"
137
+ :base-directory "#{opts['path']}"
138
+ :base-extension "org"
139
+ #{other_lines.join("\n ")}
140
+ :publishing-directory "#{publish_in}"
141
+ :publishing-function org-html-publish-to-html
142
+ :section-numbers nil
143
+ :with-toc nil
144
+ #{opts['org_headers']})
145
+ ("#{project_name}-assets"
146
+ :base-directory "#{opts['path']}"
147
+ :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|svg\\\\\\|pdf"
148
+ #{other_lines[0]}
149
+ :publishing-directory "#{publish_in}"
150
+ :publishing-function org-publish-attachment)
151
+ #{themeconf}
152
+ ORGPROJECT
153
+ end
154
+
155
+ def org_default_postamble
156
+ <<~POSTAMBLE
157
+ <p><span class="author">#{R18n.t.neruda.org.postamble.written_by}</span>
158
+ #{R18n.t.neruda.org.postamble.with_emacs}</p>
159
+ <p class="date">#{R18n.t.neruda.org.postamble.last_modification}</p>
160
+ <p class="validation">%v</p>
161
+ POSTAMBLE
162
+ end
163
+
164
+ def org_default_html_head
165
+ <<~HTMLHEAD
166
+ <link rel="stylesheet" type="text/css" media="screen"
167
+ href="__DOMAIN__/assets/__THEME__/css/style.css">
168
+ <link rel="stylesheet" type="text/css" media="screen"
169
+ href="__DOMAIN__/assets/__THEME__/css/htmlize.css">
170
+ __ATOM_FEED__
171
+ HTMLHEAD
172
+ end
173
+
174
+ def org_default_html_options
175
+ { 'html-head' => org_default_html_head,
176
+ 'html-postamble' => org_default_postamble,
177
+ 'html-head-include-default-style' => 't',
178
+ 'html-head-include-scripts' => 'nil' }
179
+ end
180
+
181
+ def expand_vars_in_html_head(head, project)
182
+ curtheme = project['theme'] || settings['theme']
183
+ # Head may be frozen when coming from settings
184
+ head = head.gsub('__THEME__', curtheme)
185
+ .gsub('__DOMAIN__', settings['domain'])
186
+ return head.gsub('__ATOM_FEED__', '') unless project['is_blog']
187
+ atomfeed = <<~ATOMFEED
188
+ <link rel="alternate" type="application/atom+xml" title="Atom 1.0"
189
+ href="#{settings['domain']}/feeds/index.xml" />
190
+ ATOMFEED
191
+ head.gsub('__ATOM_FEED__', atomfeed)
192
+ end
193
+
194
+ def build_project_org_headers(project)
195
+ orgtplopts = org_default_html_options.merge(
196
+ settings['org-html'] || {}, project['org-html'] || {}
197
+ )
198
+ orgtpl = []
199
+ orgtplopts.each do |k, v|
200
+ v = expand_vars_in_html_head(v, project) if k == 'html-head'
201
+ val = v.strip.gsub(/"/, '\"')
202
+ if ['t', 'nil', '1'].include? val
203
+ orgtpl << ":#{k} #{val}"
204
+ else
205
+ orgtpl << ":#{k} \"#{val}\""
206
+ end
207
+ end
208
+ orgtpl.join("\n ")
209
+ end
210
+
211
+ def org_generate_projects(with_tags: false)
212
+ projects = {}
213
+ projects_sources = sources
214
+ if with_tags
215
+ tags_conf = build_source('tags')
216
+ tags_conf['recursive'] = false
217
+ projects_sources << tags_conf
218
+ end
219
+ projects_sources.each do |opts|
220
+ opts['org_headers'] = build_project_org_headers(opts)
221
+ projects[opts['name']] = org_project(opts['name'], opts)
222
+ end
223
+ projects
224
+ end
225
+
226
+ def org_default_theme_config
227
+ org_theme_config(settings['theme']).split("\n").map do |line|
228
+ if line[0] == '('
229
+ line
230
+ else
231
+ " #{line}"
232
+ end
233
+ end.join("\n")
234
+ end
235
+
236
+ def org_theme_config(theme)
237
+ return nil if theme.nil?
238
+ workdir = Dir.pwd
239
+ if theme == 'default'
240
+ sourcedir = File.expand_path('../../../', __dir__)
241
+ else
242
+ sourcedir = workdir
243
+ end
244
+ <<~THEMECONFIG
245
+ ("theme-#{theme}"
246
+ :base-directory "#{sourcedir}/themes/#{theme}"
247
+ :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|js\\\\\\|css\\\\\\|otf\\\\\\|ttf\\\\\\|woff2?"
248
+ :recursive t
249
+ :publishing-directory "#{workdir}/#{settings['public_folder']}/assets/#{theme}"
250
+ :publishing-function org-publish-attachment)
251
+ THEMECONFIG
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,18 @@
1
+ ;; Add org-mode to load path
2
+ (add-to-list 'load-path (expand-file-name "org-__ORG_VER__/lisp" "__WORK_DIR__"))
3
+ ;; Load last version of htmlize.el
4
+ (load-file (expand-file-name "htmlize.el" "__WORK_DIR__"))
5
+
6
+ ;; Current project options
7
+ (setq neruda/version "__VERSION__"
8
+ neruda/current-work-dir "__WORK_DIR__"
9
+ user-mail-address "__AUTHOR_EMAIL__"
10
+ user-full-name "__AUTHOR_NAME__"
11
+ org-html-metadata-timestamp-format "__LONG_DATE_FMT__"
12
+ org-publish-project-alist
13
+ `(__ALL_PROJECTS__
14
+ __THEME_CONFIG__
15
+ ("website" :components (__ALL_PROJECTS_NAMES__))))
16
+
17
+ ;; Load neruda lib
18
+ (load-file (expand-file-name "ox-neruda.el" "__NERUDA_DIR__"))
@@ -0,0 +1,114 @@
1
+ ;;; ox-neruda.el --- Neruda Gem specific helpers for Org Export Engine -*- lexical-binding: t; -*-
2
+
3
+ ;; Copyright (C) 2011-2019 Free Software Foundation, Inc.
4
+
5
+ ;; Author: Étienne Deparis <etienne at depar dot is>
6
+ ;; Keywords: org, export
7
+
8
+ ;; This file is not part of GNU Emacs.
9
+
10
+ ;; GNU Emacs is free software: you can redistribute it and/or modify
11
+ ;; it under the terms of the GNU General Public License as published by
12
+ ;; the Free Software Foundation, either version 3 of the License, or
13
+ ;; (at your option) any later version.
14
+
15
+ ;; GNU Emacs is distributed in the hope that it will be useful,
16
+ ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ ;; GNU General Public License for more details.
19
+
20
+ ;; You should have received a copy of the GNU General Public License
21
+ ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
22
+
23
+ ;;; Commentary:
24
+
25
+ ;; This library implements specific helpers function, needed by the Ruby
26
+ ;; Gem Neruda, which offers an easy way to publish a static website
27
+ ;; using Org files as sources.
28
+
29
+ ;;; Code:
30
+
31
+ (require 'org)
32
+ (require 'ox-html)
33
+
34
+ ;;; Function Declarations
35
+
36
+ (defvar neruda/version ""
37
+ "Version of the current neruda installation")
38
+
39
+ (defvar neruda/current-work-dir nil
40
+ "Location of the current neruda website base directory.")
41
+
42
+ (defvar neruda/org-temp-dir nil
43
+ "Location of the local Org temporary directory (where to place
44
+ org timestamps and id locations).")
45
+
46
+ (defun neruda/org-html-format-spec (upstream info)
47
+ "Return format specification for preamble and postamble.
48
+ INFO is a plist used as a communication channel."
49
+ (let ((output (funcall upstream info)))
50
+ (push `(?A . ,(format "<span class=\"author\">%s</span>"
51
+ (org-export-data (plist-get info :author) info)))
52
+ output)
53
+ (push `(?k . ,(org-export-data (plist-get info :keywords) info)) output)
54
+ (push `(?K . ,(format "<ul class=\"keywords-list\">%s</ul>"
55
+ (mapconcat
56
+ (lambda (k) (format "<li class=\"keyword\">%s</li>" k))
57
+ (split-string (or (plist-get info :keywords) "") ",+ *")
58
+ "\n")))
59
+ output)
60
+ (push `(?l . ,(org-export-data (plist-get info :language) info)) output)
61
+ (push `(?n . ,(format "Neruda %s" neruda/version)) output)
62
+ (push `(?N . ,(format "<a href=\"https://git.umaneti.net/neruda/about/\">Neruda</a> %s" neruda/version)) output)
63
+ (push `(?x . ,(org-export-data (plist-get info :description) info)) output)
64
+ (push `(?X . ,(format "<p>%s</p>"
65
+ (org-export-data (plist-get info :description) info)))
66
+ output)))
67
+
68
+ (defun neruda/org-i18n-export (link description format)
69
+ "Export a i18n link"
70
+ (let* ((splitted-link (split-string link "|"))
71
+ (path (car splitted-link))
72
+ (desc (or description path))
73
+ (lang (cadr splitted-link)))
74
+ (pcase format
75
+ (`html (if lang
76
+ (format "<a href=\"%s\" hreflang=\"%s\">%s</a>"
77
+ path lang desc)
78
+ (format "<a href=\"%s\">%s</a>" path desc)))
79
+ (`latex (format "\\href{%s}{%s}" path desc))
80
+ (`ascii (format "%s (%s)" desc path))
81
+ (_ path))))
82
+
83
+ (defun neruda/org-i18n-follow (link)
84
+ "Visit a i18n link"
85
+ (browse-url (car (split-string link "|"))))
86
+
87
+ (org-link-set-parameters "i18n"
88
+ :export #'neruda/org-i18n-export
89
+ :follow #'neruda/org-i18n-follow)
90
+
91
+
92
+ ;;; Set configuration options
93
+
94
+ (setq neruda/org-temp-dir (expand-file-name "tmp" neruda/current-work-dir)
95
+ org-publish-timestamp-directory (expand-file-name "timestamps/" neruda/org-temp-dir)
96
+ org-id-locations-file (expand-file-name "id-locations.el" neruda/org-temp-dir)
97
+ make-backup-files nil
98
+ enable-local-variables :all
99
+ org-confirm-babel-evaluate nil
100
+ org-export-with-broken-links t
101
+ org-html-doctype "html5"
102
+ org-html-html5-fancy t
103
+ org-html-htmlize-output-type 'css
104
+ org-html-text-markup-alist '((bold . "<strong>%s</strong>")
105
+ (code . "<code>%s</code>")
106
+ (italic . "<em>%s</em>")
107
+ (strike-through . "<del>%s</del>")
108
+ (underline . "<span class=\"underline\">%s</span>")
109
+ (verbatim . "<code>%s</code>")))
110
+ (advice-add 'org-html-format-spec :around #'neruda/org-html-format-spec)
111
+
112
+ (provide 'ox-neruda)
113
+
114
+ ;;; ox-neruda.el ends here
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'neruda/config'
4
+
5
+ module Neruda
6
+ # Wraps Gnu/Emacs calls
7
+ class Emacs
8
+ def initialize(file_path: nil, verbose: false)
9
+ @file = file_path
10
+ @verbose = verbose
11
+ end
12
+
13
+ def publish
14
+ if @file.nil?
15
+ emacs_args = ['--eval \'(org-publish "website")\'']
16
+ else
17
+ emacs_args = ['-f org-publish-current-file']
18
+ end
19
+ call_emacs emacs_args
20
+ end
21
+
22
+ private
23
+
24
+ def emacs_command(arguments = [])
25
+ default_emacs = Neruda::Config.settings['emacs']
26
+ emacs_cmd = [default_emacs || 'emacs -Q --batch -nw']
27
+ emacs_cmd << '--eval \'(setq enable-dir-local-variables nil)\''
28
+ emacs_cmd << '--eval \'(setq inhibit-message t)\'' unless @verbose
29
+ emacs_cmd << '-l ./org-config.el'
30
+ emacs_cmd << "--eval '(find-file \"#{@file}\")'" unless @file.nil?
31
+ emacs_cmd.concat(arguments)
32
+ emacs_cmd.join(' ')
33
+ end
34
+
35
+ def call_emacs(arguments = [])
36
+ command = emacs_command arguments
37
+ if @verbose
38
+ warn command
39
+ return system(command, exception: true)
40
+ end
41
+ system command, out: '/dev/null', err: '/dev/null', exception: true
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'digest/md5'
5
+ require 'neruda/config'
6
+ require 'neruda/org_file'
7
+ require 'neruda/index/atom_generator'
8
+ require 'neruda/index/org_generator'
9
+
10
+ module Neruda
11
+ # Generates website indexes and atom feeds for all the org documents
12
+ # keywords.
13
+ class Index
14
+ attr_reader :date
15
+
16
+ include Neruda::IndexAtomGenerator
17
+ include Neruda::IndexOrgGenerator
18
+
19
+ def initialize
20
+ @pubdir = Neruda::Config.settings['public_folder']
21
+ @index = { 'index' => [] }
22
+ @projects = {}
23
+ @tags_names = {}
24
+ @date = DateTime.now
25
+ feed
26
+ sort!
27
+ end
28
+
29
+ def entries
30
+ @index.keys.reject { |k| k == 'index' }
31
+ end
32
+
33
+ def empty?
34
+ @index['index'].empty?
35
+ end
36
+
37
+ def write_all(verbose: true)
38
+ @index.each_key do |k|
39
+ write_org(k)
40
+ warn "Generated index file for #{k}" if verbose
41
+ write_atom(k)
42
+ warn "Generated atom feed for #{k}" if verbose
43
+ end
44
+ write_all_blog_home(verbose)
45
+ end
46
+
47
+ def sort_by(kind)
48
+ if [:name, :weight].include?(kind)
49
+ tags_sorted = sort_tags_by_name_and_weight["by_#{kind}".to_sym]
50
+ # Reverse in order to have most important or A near next prompt
51
+ # and avoid to scroll to find the beginning of the list.
52
+ return tags_sorted.map do |k|
53
+ @tags_names[k] + " (#{@index[k].length})"
54
+ end.reverse
55
+ end
56
+ raise ArgumentError, "#{kind} not in [:name, :weight]"
57
+ end
58
+
59
+ private
60
+
61
+ def feed
62
+ Neruda::Config.sources.each do |project|
63
+ next unless project['is_blog']
64
+ if project['recursive']
65
+ file_pattern = '**/*.org'
66
+ else
67
+ file_pattern = '*.org'
68
+ end
69
+ Dir.glob(file_pattern, base: project['path']).map do |s|
70
+ org_file = File.join(project['path'], s)
71
+ add_to_indexes(
72
+ Neruda::OrgFile.new(org_file, project: project)
73
+ )
74
+ end
75
+ end
76
+ end
77
+
78
+ def add_to_project_index(article)
79
+ project = article.project
80
+ @projects[project['name']] ||= []
81
+ @projects[project['name']] << article
82
+ end
83
+
84
+ def add_to_indexes(article)
85
+ @index['index'] << article
86
+ add_to_project_index article
87
+ article.keywords.each do |k|
88
+ slug = Neruda::OrgFile.slug k
89
+ @tags_names[slug] = k # Overwrite is permitted
90
+ @index[slug] ||= []
91
+ @index[slug] << article
92
+ end
93
+ end
94
+
95
+ def sort!
96
+ @index.each do |k, i|
97
+ @index[k] = i.sort { |a, b| b.timekey <=> a.timekey }
98
+ end
99
+ @projects.each do |k, i|
100
+ @projects[k] = i.sort { |a, b| b.timekey <=> a.timekey }
101
+ end
102
+ end
103
+
104
+ def sort_tags_by_name_and_weight
105
+ tags_sorted = {}
106
+ all_keys = entries
107
+ tags_sorted[:by_name] = all_keys.sort
108
+ tags_sorted[:by_weight] = all_keys.sort do |a, b|
109
+ @index[b].length <=> @index[a].length
110
+ end
111
+ tags_sorted
112
+ end
113
+
114
+ def save?
115
+ return true unless empty?
116
+ Neruda::Config.sources.each do |project|
117
+ return true if project['is_blog'] && Dir.exist?(project['path'])
118
+ end
119
+ false
120
+ end
121
+ end
122
+ end