noumenon 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.travis.yml +0 -4
- data/.yardopts +5 -0
- data/Gemfile +1 -3
- data/README.md +57 -80
- data/Rakefile +17 -6
- data/bin/noumenon +6 -0
- data/features/dynamic_template_rendering.feature +107 -0
- data/features/generator/site_generator.feature +25 -0
- data/features/mounted_applications.feature +30 -0
- data/features/static_template_rendering.feature +43 -0
- data/features/step_definitions/asset_steps.rb +7 -0
- data/features/step_definitions/content_steps.rb +7 -0
- data/features/step_definitions/generator_steps.rb +22 -0
- data/features/step_definitions/request_steps.rb +31 -0
- data/features/step_definitions/theme_steps.rb +19 -0
- data/features/support/env.rb +38 -0
- data/features/support/theme/theme.yml +5 -0
- data/features/theme_assets.feature +22 -0
- data/generators/repository/index.yml +3 -0
- data/generators/site/Gemfile +3 -0
- data/generators/site/config.ru +7 -0
- data/generators/theme/assets/style.css +1 -0
- data/generators/theme/layouts/default.nou.html +23 -0
- data/generators/theme/templates/default.nou.html +12 -0
- data/generators/theme/theme.yml +5 -0
- data/lib/noumenon/cli.rb +27 -0
- data/lib/noumenon/core.rb +70 -77
- data/lib/noumenon/repository/file_system.rb +102 -0
- data/lib/noumenon/repository.rb +39 -0
- data/lib/noumenon/spec/example_app.rb +19 -6
- data/lib/noumenon/spec/theme_helpers.rb +66 -0
- data/lib/noumenon/spec.rb +4 -7
- data/lib/noumenon/string_extensions.rb +21 -0
- data/lib/noumenon/template.rb +113 -72
- data/lib/noumenon/theme/assets_middleware.rb +21 -0
- data/lib/noumenon/theme.rb +106 -0
- data/lib/noumenon/version.rb +3 -1
- data/lib/noumenon.rb +68 -100
- data/noumenon.gemspec +13 -9
- data/spec/noumenon/repository/file_system_spec.rb +115 -0
- data/spec/noumenon/repository_spec.rb +40 -0
- data/spec/noumenon/template_spec.rb +9 -7
- data/spec/noumenon/theme_spec.rb +129 -0
- data/spec/noumenon_spec.rb +24 -80
- data/spec/spec_helper.rb +5 -14
- data/spec/support/file_matchers.rb +45 -0
- data/spec/support/templates/basic_template.html +1 -0
- data/spec/{fixtures/themes/example_without_layout → support}/templates/template_with_fields.html +1 -1
- metadata +143 -62
- data/lib/noumenon/configuration.rb +0 -28
- data/lib/noumenon/spec/fixtures.rb +0 -34
- data/spec/fixtures/fixture_specs/test +0 -1
- data/spec/fixtures/missing_application/mounted_app/config.yml +0 -1
- data/spec/fixtures/static_example/directory_with_index/index.yml +0 -1
- data/spec/fixtures/static_example/found.html +0 -1
- data/spec/fixtures/static_example/liquid_example.html +0 -1
- data/spec/fixtures/static_example/mounted_app/config.yml +0 -1
- data/spec/fixtures/static_example/template_with_substitutions.html +0 -1
- data/spec/fixtures/static_example/templates/basic_example.yml +0 -1
- data/spec/fixtures/static_example/templates/with_fields.yml +0 -2
- data/spec/fixtures/themes/example/assets/example.txt +0 -1
- data/spec/fixtures/themes/example/templates/layout.html +0 -3
- data/spec/fixtures/themes/example_without_layout/templates/basic_template.html +0 -1
- data/spec/fixtures/themes/example_without_layout/templates/fields.html +0 -1
- data/spec/fixtures/themes/external_theme/assets/example.txt +0 -1
- data/spec/fixtures/themes/unregistered/lib/unregistered.rb +0 -1
- data/spec/noumenon/config_spec.rb +0 -29
- data/spec/noumenon/core_spec.rb +0 -105
- data/spec/noumenon/spec/example_app_spec.rb +0 -14
- data/spec/noumenon/spec/fixtures_spec.rb +0 -41
- data/spec/noumenon/spec_spec.rb +0 -7
- data/watchr.rb +0 -2
data/lib/noumenon/template.rb
CHANGED
@@ -1,88 +1,129 @@
|
|
1
|
+
require 'noumenon'
|
1
2
|
require 'liquid'
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
4
|
+
# Templates specify the structure and presentation of a particular piece of content in
|
5
|
+
# a Noumenon site, and are usually provided by a theme.
|
6
|
+
#
|
7
|
+
# A template is split into two parts, the metadata, which defines what data is needed for
|
8
|
+
# the template, and a Liquid template, which defines how the data should be presented. When
|
9
|
+
# loaded from a file, a template will look like this:
|
10
|
+
#
|
11
|
+
# title:
|
12
|
+
# label: Page title
|
13
|
+
# required: true
|
14
|
+
# type: string
|
15
|
+
# help: The title displayed at the top of the page
|
16
|
+
# author:
|
17
|
+
# label: Author
|
18
|
+
# required: false
|
19
|
+
# type: string
|
20
|
+
# help: The author of the article. If not provided, no byline will be shown.
|
21
|
+
# body:
|
22
|
+
# label: Body text
|
23
|
+
# required: true
|
24
|
+
# type: text
|
25
|
+
# help: The main article body. Will be processed with Textile for formatting.
|
26
|
+
# ---
|
27
|
+
# <h1>{{ title }}</h1>
|
28
|
+
# {% if author %}
|
29
|
+
# <p class="byline">By {{ author }}</p>
|
30
|
+
# {% endif %}
|
31
|
+
# {{ body | textilize }}
|
32
|
+
#
|
33
|
+
# And can be rendered like this:
|
34
|
+
#
|
35
|
+
# Template.from_file("/path/to/template").render("title" => "An Example Page", "author" => "Jon Wood", "body" => "This is an article...")
|
36
|
+
#
|
37
|
+
# If any required fields are missing from the data provided then a MissingContentError will be raised.
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
class Noumenon::Template
|
41
|
+
# Indicates one or more required fields were not provided to the template.
|
6
42
|
#
|
7
|
-
#
|
8
|
-
# the template, and a Liquid template, which defines how the data should be presented. When
|
9
|
-
# loaded from a file, a template will look like this:
|
43
|
+
# The missing fields are listed in the error message.
|
10
44
|
#
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
#
|
15
|
-
# help: The title displayed at the top of the page
|
16
|
-
# author:
|
17
|
-
# label: Author
|
18
|
-
# required: false
|
19
|
-
# type: string
|
20
|
-
# help: The author of the article. If not provided, no byline will be shown.
|
21
|
-
# body:
|
22
|
-
# label: Body text
|
23
|
-
# required: true
|
24
|
-
# type: text
|
25
|
-
# help: The main article body. Will be processed with Textile for formatting.
|
26
|
-
# ---
|
27
|
-
# <h1>{{ title }}</h1>
|
28
|
-
# {% if author %}
|
29
|
-
# <p class="byline">By {{ author }}</p>
|
30
|
-
# {% endif %}
|
31
|
-
# {{ body | textilize }}
|
32
|
-
#
|
33
|
-
# And can be rendered like this:
|
45
|
+
# @api public
|
46
|
+
class MissingContentError < StandardError; end
|
47
|
+
|
48
|
+
# Indicates the requested template could not be found.
|
34
49
|
#
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
# The location this template was loaded from.
|
43
|
-
attr_accessor :source
|
50
|
+
# @api public
|
51
|
+
class NotFoundError < StandardError; end
|
52
|
+
|
53
|
+
# The location this template was loaded from.
|
54
|
+
# @api public
|
55
|
+
attr_accessor :source
|
44
56
|
|
45
|
-
|
46
|
-
|
57
|
+
# The template view.
|
58
|
+
# @api public
|
59
|
+
attr_accessor :content
|
47
60
|
|
48
|
-
|
49
|
-
|
61
|
+
# The fields used by this template.
|
62
|
+
# @api public
|
63
|
+
attr_accessor :fields
|
64
|
+
|
65
|
+
# Loads a template from the specified path.
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
#
|
69
|
+
# @param [ String, #to_s ] path the file to load the template from
|
70
|
+
# @raise [ Noumenon::Template::NotFoundError ] the template could not be found
|
71
|
+
# @return [ Noumenon::Template ] the loaded template
|
72
|
+
# @api public
|
73
|
+
def self.from_file(path)
|
74
|
+
raise NotFoundError.new("No template could be found at #{path}.") unless File.exists?(path)
|
50
75
|
|
51
|
-
|
52
|
-
#
|
53
|
-
# If the file does not exist then a NotFoundError will be raised.
|
54
|
-
def self.from_file(path)
|
55
|
-
raise NotFoundError.new("No template could be found at #{path}.") unless File.exists?(path)
|
56
|
-
|
57
|
-
content = File.read(path)
|
76
|
+
content = File.read(path)
|
58
77
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
self.new(path, content, fields)
|
78
|
+
parts = content.split("\n---\n")
|
79
|
+
if parts.size == 1
|
80
|
+
fields = {}
|
81
|
+
else
|
82
|
+
fields = YAML.load(parts.first)
|
83
|
+
content = parts.last
|
68
84
|
end
|
69
85
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
86
|
+
self.new(path, content, fields)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Creates a new Template instance.
|
90
|
+
#
|
91
|
+
# @api public
|
92
|
+
#
|
93
|
+
# @param [ #to_s ] source the location the template was loaded from
|
94
|
+
# @param [ #to_s ] content the Liquid template to render
|
95
|
+
# @param [ Hash, #each ] fields the list of fields used within the template
|
96
|
+
#
|
97
|
+
# @example Loading a template from a database
|
98
|
+
# fields = load_field_rows.inject({}) do |fields, row|
|
99
|
+
# fields[row[:name]] = { "required" => row[:required], "type" => row[:type] }
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# Noumenon::Template.new("mysql://localhost/database/table#row_32", row[:content], fields)
|
103
|
+
#
|
104
|
+
# @api public
|
105
|
+
def initialize(source = nil, content = nil, fields = {})
|
106
|
+
@source = source
|
107
|
+
@content = content
|
108
|
+
@fields = fields
|
109
|
+
end
|
110
|
+
|
111
|
+
# Renders the template.
|
112
|
+
#
|
113
|
+
# @param [ Hash, #each ] page_content the content to provide to the template
|
114
|
+
# @raise [ Noumenon::Template::MissingContentError ] one or more required fields were not provided
|
115
|
+
# @return [ #to_s ] the rendered template
|
116
|
+
# @api public
|
117
|
+
def render(page_content = {})
|
118
|
+
fields_from_page = page_content.stringify_keys
|
75
119
|
|
76
|
-
|
77
|
-
|
78
|
-
missing_fields
|
79
|
-
|
80
|
-
missing_fields << key if field["required"] && !page_content.key?(key)
|
81
|
-
end
|
120
|
+
missing_fields = []
|
121
|
+
fields.each do |key, field|
|
122
|
+
missing_fields << key if field["required"] && !fields_from_page.key?(key)
|
123
|
+
end
|
82
124
|
|
83
|
-
|
125
|
+
raise MissingContentError.new("The following fields were missing from your content: #{missing_fields.sort.join(", ")}") unless missing_fields.empty?
|
84
126
|
|
85
|
-
|
86
|
-
end
|
127
|
+
Liquid::Template.parse(content).render(fields_from_page)
|
87
128
|
end
|
88
129
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'noumenon/theme'
|
2
|
+
require 'sinatra'
|
3
|
+
|
4
|
+
# Handles requests for assets within loaded themes.
|
5
|
+
#
|
6
|
+
# Any request to /themes/Theme Name/* will be served from the theme's assets directory,
|
7
|
+
# with existing files being served, and non-existant files generating a 404.
|
8
|
+
#
|
9
|
+
# The middleware is automatically added to the stack if you're running using Noumenon.server
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
class Noumenon::Theme::AssetsMiddleware < Sinatra::Base
|
13
|
+
get '/themes/:theme/*' do |theme, asset|
|
14
|
+
halt 404, "File Not Found" unless Noumenon::Theme.themes.key? theme
|
15
|
+
|
16
|
+
asset_path = File.join(Noumenon::Theme.themes[theme].path, "assets", asset)
|
17
|
+
halt 404, "File Not Found" unless File.exist?(asset_path)
|
18
|
+
|
19
|
+
send_file(asset_path)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'noumenon'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
# Provides access to a theme and it's contents.
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
class Noumenon::Theme
|
8
|
+
autoload :AssetsMiddleware, 'noumenon/theme/assets_middleware'
|
9
|
+
|
10
|
+
# The specified theme could not be loaded, either because the path does not exist, or it does not
|
11
|
+
# contain a theme.yml file.
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
class NotFoundError < RuntimeError; end
|
15
|
+
|
16
|
+
# Load a theme from the specified path. Metadata about the theme will be laoded from "theme.yml",
|
17
|
+
# which should have the format shown in the example.
|
18
|
+
#
|
19
|
+
# If the theme is loaded succesfully it will also be registered in the theme list.
|
20
|
+
#
|
21
|
+
# @param [ String, #to_s ] path The path to load from.
|
22
|
+
# @raise [ Noumenon::Theme::NotFoundError ]
|
23
|
+
# @return [ Noumenon::Theme ] A populated theme.
|
24
|
+
# @see Noumenon::Theme.themes
|
25
|
+
def self.load(path)
|
26
|
+
unless File.exist?("#{path}/theme.yml")
|
27
|
+
raise NotFoundError.new("No theme was found at #{path}. Did you create a theme.yml file?")
|
28
|
+
end
|
29
|
+
|
30
|
+
description = YAML.load(File.read("#{path}/theme.yml"))
|
31
|
+
theme = Noumenon::Theme.new(path, description)
|
32
|
+
|
33
|
+
themes[theme.name] = theme
|
34
|
+
theme
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [ Hash ] A hash containing any loaded themes, keyed by their name.
|
38
|
+
# @api public
|
39
|
+
def self.themes
|
40
|
+
@themes ||= {}
|
41
|
+
end
|
42
|
+
|
43
|
+
# The path the theme was loaded from.
|
44
|
+
# @api public
|
45
|
+
attr_reader :path
|
46
|
+
|
47
|
+
# The name to refer to this theme by. Loaded from theme.yml.
|
48
|
+
# @api public
|
49
|
+
attr_accessor :name
|
50
|
+
|
51
|
+
# The author of this theme. Loaded from theme.yml.
|
52
|
+
# @api public
|
53
|
+
attr_accessor :author
|
54
|
+
|
55
|
+
# The email address to contact regarding this theme. Loaded from theme.yml.
|
56
|
+
# @api public
|
57
|
+
attr_accessor :email
|
58
|
+
|
59
|
+
# The copyright line to attribute this theme to. Loaded from theme.yml.
|
60
|
+
# @api public
|
61
|
+
attr_accessor :copyright
|
62
|
+
|
63
|
+
# The license this theme is distributed under. Loaded from theme.yml.
|
64
|
+
# @api public
|
65
|
+
attr_accessor :license
|
66
|
+
|
67
|
+
# Create a new them instance.
|
68
|
+
#
|
69
|
+
# @param [ String, #to_s ] path The path the theme was loaded from.
|
70
|
+
# @param [ Hash ] description Any metadata to attach to theme.
|
71
|
+
# @option description [ String ] :name The human readable name of the theme.
|
72
|
+
# @option description [ String ] :author The author of the theme.
|
73
|
+
# @option description [ String ] :email The email address to content regarding this theme.
|
74
|
+
# @option description [ String ] :copyright The copyright line to attribute this theme to.
|
75
|
+
# @option description [ String ] :license The license this theme is distributed under.
|
76
|
+
# @api public
|
77
|
+
def initialize(path, description = {})
|
78
|
+
@path = path
|
79
|
+
|
80
|
+
description.each do |name, value|
|
81
|
+
if respond_to? "#{name}="
|
82
|
+
send "#{name}=", value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Load a template from the theme's templates directory.
|
88
|
+
#
|
89
|
+
# @param [ String, #to_s ] name The path to load the template from. This path will be prefixed with "templates/"
|
90
|
+
# @return [ Noumenon::Template ] The loaded template.
|
91
|
+
# @raise [ Noumenon::Template::NotFoundError ]
|
92
|
+
# @api public
|
93
|
+
def template(name)
|
94
|
+
Noumenon::Template.from_file File.join(path, "templates", name)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Load a template from the theme's layouts directory.
|
98
|
+
#
|
99
|
+
# @param [ String, #to_s ] name The path to load the template from. This path will be prefixed with "layouts/"
|
100
|
+
# @return [ Noumenon::Template ] The loaded template.
|
101
|
+
# @raise [ Noumenon::Template::NotFoundError ]
|
102
|
+
# @api public
|
103
|
+
def layout(name)
|
104
|
+
Noumenon::Template.from_file File.join(path, "layouts", name)
|
105
|
+
end
|
106
|
+
end
|
data/lib/noumenon/version.rb
CHANGED
data/lib/noumenon.rb
CHANGED
@@ -1,109 +1,77 @@
|
|
1
|
-
require 'noumenon/
|
2
|
-
require 'noumenon/
|
3
|
-
require '
|
1
|
+
require 'noumenon/version'
|
2
|
+
require 'noumenon/string_extensions'
|
3
|
+
require 'facets'
|
4
|
+
require 'rack/builder'
|
4
5
|
|
6
|
+
# @api public
|
5
7
|
module Noumenon
|
6
|
-
|
7
|
-
|
8
|
+
autoload :Core, 'noumenon/core'
|
9
|
+
autoload :Repository, 'noumenon/repository'
|
10
|
+
autoload :Template, 'noumenon/template'
|
11
|
+
autoload :Theme, 'noumenon/theme'
|
12
|
+
autoload :Spec, 'noumenon/spec'
|
8
13
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
# it. The "application" attribute sets the class that will handle that URL, for example:
|
28
|
-
#
|
29
|
-
# application: "Noumenon::Applications::ContactForm"
|
30
|
-
#
|
31
|
-
# Example:
|
32
|
-
#
|
33
|
-
# require 'noumenon'
|
34
|
-
# Noumenon::Core.set :content_repository_path, "/srv/sites/example.org"
|
35
|
-
#
|
36
|
-
# run Noumenon.boot
|
37
|
-
def boot
|
38
|
-
Rack::Builder.new do
|
39
|
-
Noumenon.themes.each do |name, details|
|
40
|
-
map("/themes/#{name}") { run Rack::File.new(File.join(details[:path], "assets")) }
|
41
|
-
end
|
42
|
-
|
43
|
-
Dir.glob(File.join(Noumenon.config.content_repository_path, "**/config.yml")).each do |path|
|
44
|
-
config = YAML.load(File.read(path))
|
45
|
-
|
46
|
-
if config["application"]
|
47
|
-
begin
|
48
|
-
url = path.gsub(Noumenon.config.content_repository_path, "").split("/")[0..-2].join("/")
|
49
|
-
app = Noumenon.constantize(config["application"]).new
|
50
|
-
|
51
|
-
map(url) { run app }
|
52
|
-
rescue NameError => e
|
53
|
-
raise MissingApplicationError.new("The application #{config["application"]} has not been loaded.")
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
map("/") { run Core.new }
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
# Attempts to convert a string into a constant.
|
63
|
-
#
|
64
|
-
# If the constant could not be found then NameError will be raised.
|
65
|
-
def constantize(name)
|
66
|
-
mod = Module
|
67
|
-
name.split("::").each do |mod_name|
|
68
|
-
mod = mod.const_get(mod_name.to_sym)
|
69
|
-
end
|
70
|
-
|
71
|
-
mod
|
14
|
+
# Starts Noumenon serving, this will usually be called from a config.ru file
|
15
|
+
# something like this:
|
16
|
+
#
|
17
|
+
# Noumenon.content_repository = Noumenon::Repository::FileSystem.new("/home/noumenon/content")
|
18
|
+
# Noumenon.theme = Noumenon::Theme.load("/home/noumenon/theme")
|
19
|
+
#
|
20
|
+
# run Noumenon.server
|
21
|
+
#
|
22
|
+
# While you can also just use an instance Noumenon::Core, keep in mind that it won't
|
23
|
+
# have the full Rack middleware stack, and so some functionality such as serving assets
|
24
|
+
# from themes won't be available.
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
# @return [ Rack::Builder ] the Rack stack to serve a Noumenon site
|
28
|
+
def self.server
|
29
|
+
Rack::Builder.new do
|
30
|
+
use Noumenon::Theme::AssetsMiddleware
|
31
|
+
run Noumenon::Core
|
72
32
|
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the current content repository.
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
# @return [ Noumenon::Repository, #get, #put ]
|
39
|
+
def self.content_repository
|
40
|
+
@content_repository
|
41
|
+
end
|
73
42
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
43
|
+
# Sets the repository to load site content from.
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
# @param [ Noumenon::Repository, #get, #put ] repository the repository to use
|
47
|
+
def self.content_repository=(repository)
|
48
|
+
@content_repository = repository
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the current theme
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
# @return [ Noumenon::Theme ] the current theme
|
55
|
+
def self.theme
|
56
|
+
@theme
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the current theme.
|
60
|
+
#
|
61
|
+
# If provided with a [ Noumenon::Theme ] object then it will set that, otherwise
|
62
|
+
# it will convert the argument to a string, and attempt to find a loaded theme with
|
63
|
+
# that name.
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
# @param [ Noumenon::Theme, #to_s ] theme either a theme object, or the name of a loaded theme
|
67
|
+
# @return [ Noumenon::Theme, nil ] the specified theme, or nil if it could not be found
|
68
|
+
def self.theme=(theme)
|
69
|
+
if theme.is_a? Noumenon::Theme
|
70
|
+
@theme = theme
|
71
|
+
else
|
72
|
+
self.theme = Noumenon::Theme.themes[theme]
|
95
73
|
end
|
96
74
|
|
97
|
-
|
98
|
-
def themes
|
99
|
-
@themes ||= {}
|
100
|
-
end
|
101
|
-
|
102
|
-
# Registers a theme for use by Noumenon.
|
103
|
-
#
|
104
|
-
# Any files in the assets/ directory below the path provided will be accessible from /themes/theme_name.
|
105
|
-
def register_theme(name, path)
|
106
|
-
themes[name] = { :path => path }
|
107
|
-
end
|
75
|
+
@theme
|
108
76
|
end
|
109
77
|
end
|
data/noumenon.gemspec
CHANGED
@@ -9,10 +9,10 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.authors = ["Jon Wood"]
|
10
10
|
s.email = ["jon@blankpad.net"]
|
11
11
|
s.homepage = "https://github.com/Noumenon"
|
12
|
-
s.summary = %q{
|
12
|
+
s.summary = %q{A flexible content management system.}
|
13
13
|
s.description = <<EOF
|
14
|
-
Noumenon is a content management system designed to
|
15
|
-
|
14
|
+
Noumenon is a content management system designed to support being extended with Sinatra
|
15
|
+
applications.
|
16
16
|
|
17
17
|
It's currently in an early stage of development, but right now you can create a basic
|
18
18
|
static site using templates from a theme which specify the structure and presentation
|
@@ -22,9 +22,6 @@ templates.
|
|
22
22
|
Future development will include an end-user friendly web interface for editing and
|
23
23
|
creating content, while retaining the ability for developers and designers to manage
|
24
24
|
the site's presentation using the tools they're most comfortable with.
|
25
|
-
|
26
|
-
See http://github.com/Noumenon/example-app/ for a really bare bones example of how
|
27
|
-
Noumenon works.
|
28
25
|
EOF
|
29
26
|
|
30
27
|
s.rubyforge_project = "noumenon"
|
@@ -34,9 +31,16 @@ EOF
|
|
34
31
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
35
32
|
s.require_paths = ["lib"]
|
36
33
|
|
34
|
+
s.add_dependency "facets", ">= 2.9.1"
|
35
|
+
s.add_dependency "liquid", ">= 2.2"
|
37
36
|
s.add_dependency "sinatra", ">= 1.2.3"
|
38
|
-
|
39
|
-
|
37
|
+
|
38
|
+
s.add_development_dependency "aruba", ">= 0.3.6"
|
39
|
+
s.add_development_dependency "capybara", ">= 1.0.0.beta1"
|
40
|
+
s.add_development_dependency "cucumber", ">= 0.10.2"
|
41
|
+
s.add_development_dependency "rake", ">= 0.9.0"
|
42
|
+
s.add_development_dependency "rdiscount", ">= 1.6.8"
|
40
43
|
s.add_development_dependency "rspec", ">= 2.5.0"
|
41
|
-
s.add_development_dependency "
|
44
|
+
s.add_development_dependency "thor", ">= 0.14.6"
|
45
|
+
s.add_development_dependency "yard", ">= 0.7.1"
|
42
46
|
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Noumenon::Repository::FileSystem do
|
4
|
+
let(:content_path) { File.join(File.dirname(__FILE__), "..", "..", "..", "..", "tmp", "content") }
|
5
|
+
|
6
|
+
around do |example|
|
7
|
+
FileUtils.mkdir_p content_path
|
8
|
+
example.run
|
9
|
+
FileUtils.rm_r content_path
|
10
|
+
end
|
11
|
+
|
12
|
+
subject do
|
13
|
+
Noumenon::Repository::FileSystem.new path: content_path
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "initialization" do
|
17
|
+
context "when no path option is provided" do
|
18
|
+
it "should raise an ArgumentError" do
|
19
|
+
lambda { Noumenon::Repository::FileSystem.new }.should raise_error ArgumentError
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should provide details of the error of how to resolve the error" do
|
23
|
+
begin
|
24
|
+
Noumenon::Repository::FileSystem.new
|
25
|
+
rescue ArgumentError => e
|
26
|
+
e.to_s.should == "You must provide a path to the content repository: Noumenon::Repository::FileSystem.new(path: '/tmp')"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it { should respond_to(:put) }
|
33
|
+
|
34
|
+
describe "putting content in the repository" do
|
35
|
+
context "when writing to a top level file" do
|
36
|
+
it "creates a YAML file at the specified path" do
|
37
|
+
lambda { subject.put "example", { template: "static" } }.should create_file File.join(content_path, "example.yml")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "writes the provided hash to the specified path" do
|
41
|
+
subject.put "example", { template: "static" }
|
42
|
+
|
43
|
+
YAML.load(File.read(File.join(content_path, "example.yml"))).should == { template: "static" }
|
44
|
+
end
|
45
|
+
|
46
|
+
it "symbolises any field keys" do
|
47
|
+
subject.put "example", { "template" => "static" }
|
48
|
+
YAML.load(File.read(File.join(content_path, "example.yml"))).should == { template: "static" }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when writing to a sub-directory" do
|
53
|
+
context "without an existing YAML file" do
|
54
|
+
it "creates the tree leading to the file" do
|
55
|
+
lambda { subject.put "sub_directory/example", { template: "static" } }.should create_file File.join(content_path, "sub_directory", "example.yml")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when a file in the path already exists" do
|
60
|
+
before(:each) { subject.put "sub_directory", { example: true } }
|
61
|
+
|
62
|
+
it "creates the tree to the file" do
|
63
|
+
lambda { subject.put "sub_directory/example", { template: "static" } }.should create_directory File.join(content_path, "sub_directory")
|
64
|
+
end
|
65
|
+
|
66
|
+
it "it moves the existing file into an index.yml file in the path" do
|
67
|
+
lambda { subject.put "sub_directory/example", { template: "static" } }.should move_file({
|
68
|
+
from: File.join(content_path, "sub_directory.yml"),
|
69
|
+
to: File.join(content_path, "sub_directory", "index.yml")
|
70
|
+
})
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "when the directory specified already exists" do
|
75
|
+
before(:each) { FileUtils.mkdir File.join(content_path, "sub_directory") }
|
76
|
+
|
77
|
+
it "creates the file as index.yml within the directory" do
|
78
|
+
lambda { subject.put "sub_directory", { template: "static" } }.should create_file File.join(content_path, "sub_directory", "index.yml")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "retrieving content from the repository" do
|
85
|
+
context "when the item does not exist" do
|
86
|
+
it "returns nil" do
|
87
|
+
subject.get("not_found").should be_nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when the item exists" do
|
92
|
+
it "returns it's content in hash form" do
|
93
|
+
subject.put "example", { template: "static" }
|
94
|
+
subject.get("example").should == { template: "static" }
|
95
|
+
end
|
96
|
+
|
97
|
+
it "supports items with string based keys" do
|
98
|
+
File.open(File.join(content_path, "example.yml"), "w") { |f| f.print("template: foo") }
|
99
|
+
subject.get("example").should == { template: "foo" }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context "when the item is a directory" do
|
104
|
+
it "returns the contents of index.yml if it exists" do
|
105
|
+
subject.put "example/index", { template: "static" }
|
106
|
+
subject.get("example").should == { template: "static" }
|
107
|
+
end
|
108
|
+
|
109
|
+
it "returns nil if no index.yml exists" do
|
110
|
+
subject.put "example/page", { template: "static" }
|
111
|
+
subject.get("example").should be_nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|