noumenon 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|