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
@@ -0,0 +1,23 @@
|
|
1
|
+
title:
|
2
|
+
type: string
|
3
|
+
description: The title to put in the browser's title bar.
|
4
|
+
required: false
|
5
|
+
|
6
|
+
content:
|
7
|
+
type: string
|
8
|
+
description: The content from the page template.
|
9
|
+
required: true
|
10
|
+
---
|
11
|
+
<!doctype html>
|
12
|
+
<html>
|
13
|
+
<head>
|
14
|
+
<title>{{ title }}</title>
|
15
|
+
<link rel="stylesheet" href="/themes/Your%20Theme/styles.css" />
|
16
|
+
</head>
|
17
|
+
|
18
|
+
<body>
|
19
|
+
<section id="content">
|
20
|
+
{{ content }}
|
21
|
+
</section>
|
22
|
+
</body>
|
23
|
+
</html>
|
data/lib/noumenon/cli.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
class Noumenon::Cli < Thor
|
4
|
+
include Thor::Actions
|
5
|
+
|
6
|
+
source_root File.expand_path("../../../generators", __FILE__)
|
7
|
+
|
8
|
+
desc "site PATH", "Generate a new site at PATH"
|
9
|
+
def site(path)
|
10
|
+
say_status :site, path
|
11
|
+
directory "site", path
|
12
|
+
invoke :theme, [ File.join(path, "theme") ]
|
13
|
+
invoke :repository, [ File.join(path, "content") ]
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "theme PATH", "Generate a new theme at PATH"
|
17
|
+
def theme(path)
|
18
|
+
say_status :theme, path
|
19
|
+
directory "theme", path
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "repository PATH", "Generate a new content repository at PATH"
|
23
|
+
def repository(path)
|
24
|
+
say_status :repository, path
|
25
|
+
directory "repository", path
|
26
|
+
end
|
27
|
+
end
|
data/lib/noumenon/core.rb
CHANGED
@@ -1,93 +1,86 @@
|
|
1
|
+
require 'noumenon'
|
1
2
|
require 'sinatra'
|
2
|
-
require 'liquid'
|
3
|
-
require 'yaml'
|
4
3
|
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
# The core Noumenon web application, responsible for loading content from the repository,
|
5
|
+
# and then either rendering it with the appropriate template, or passing it to the specified
|
6
|
+
# application.
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
class Noumenon::Core < Sinatra::Base
|
10
|
+
# Renders a content item within it's template and layout.
|
9
11
|
#
|
10
|
-
#
|
11
|
-
# using #locate_content, which will then be rendered using the template specified by the content
|
12
|
-
# file.
|
12
|
+
# If the template or layout is not specified then it will be set to "default".
|
13
13
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
14
|
+
# @param [ Hash ] page The content item to render.
|
15
|
+
# @option page [ String ] :template The template to use when rendering the item.
|
16
|
+
# @option page [ String ] :layout The layout to put the template within.
|
17
|
+
# @return [ String ] The rendered page, or an error message if any templates could not be rendered.
|
18
|
+
# @api public
|
19
|
+
def render_page(page)
|
20
|
+
page[:template] ||= "default"
|
21
|
+
page[:layout] ||= "default"
|
22
|
+
|
23
|
+
begin
|
24
|
+
template = Noumenon.theme.template("#{page[:template]}.nou.html")
|
25
|
+
content = template.render(page)
|
26
|
+
|
27
|
+
wrap_with_layout(content, page)
|
28
|
+
rescue Noumenon::Template::NotFoundError => e
|
29
|
+
halt 500, "<h1>Missing Template</h1><p>The template '#{page[:template]}' does not exist within the current theme.</p>"
|
30
|
+
rescue Noumenon::Template::MissingContentError => e
|
31
|
+
halt 500, "<h1>Missing Fields</h1><p>#{e}</p>"
|
30
32
|
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Convenience method to access the current content repository.
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
# @return [ Noumenon::Repository ] the current content repository
|
40
|
+
def content
|
41
|
+
Noumenon.content_repository
|
42
|
+
end
|
43
|
+
|
44
|
+
get "*" do |path|
|
45
|
+
page = content.get(path)
|
31
46
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# layout: If set to a string, an attempt will be made to use that layout.
|
40
|
-
# If set to false no layout will be used.
|
41
|
-
# If set to true (the default) the default layout will be used.
|
42
|
-
def render_template(path, locals = {}, options = {})
|
43
|
-
options[:layout] = "layout" if options[:layout] == true || options[:layout].nil?
|
44
|
-
|
45
|
-
template = Template.from_file(path)
|
46
|
-
body = template.render(locals)
|
47
|
-
|
48
|
-
if options[:layout]
|
49
|
-
begin
|
50
|
-
body = render_template(theme_path("templates/#{options[:layout]}.html"), { 'body' => body }, :layout => false)
|
51
|
-
rescue Noumenon::Template::NotFoundError => ignore_missing_layouts
|
52
|
-
end
|
47
|
+
unless page
|
48
|
+
# Search up the tree until we either hit the top, and 404, or find a mounted application.
|
49
|
+
path = path.split("/")
|
50
|
+
while path.size > 0
|
51
|
+
path.pop
|
52
|
+
page = content.get(path.join("/"))
|
53
|
+
break if page && page[:type] == "application"
|
53
54
|
end
|
54
55
|
|
55
|
-
|
56
|
+
halt 404, "<h1>Page Not Found</h1>" unless page
|
57
|
+
path = path.join("/")
|
56
58
|
end
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# If no template was specified then the template "default" will be used.
|
62
|
-
def render_content(path)
|
63
|
-
content = File.read(content_path(path))
|
64
|
-
item = YAML.load( content )
|
60
|
+
case page[:type]
|
61
|
+
when "application"
|
62
|
+
app = page[:application].constantize
|
65
63
|
|
66
|
-
|
64
|
+
# Rewrite PATH_INFO so that the child application can listen to URLs assuming it's at the root.
|
65
|
+
env["PATH_INFO"].gsub!("#{path}", "")
|
66
|
+
env["PATH_INFO"] = "/" if env["PATH_INFO"] == ""
|
67
|
+
app.new(page).call(env)
|
68
|
+
else
|
69
|
+
render_page(page)
|
67
70
|
end
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if File.exists?(content_path(path)) && File.exists?(content_path("#{path}/index.yml"))
|
81
|
-
return "#{path}/index.yml"
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def wrap_with_layout(content, page)
|
75
|
+
begin
|
76
|
+
layout = Noumenon.theme.layout("#{page[:layout]}.nou.html")
|
77
|
+
return layout.render( page.merge(content: content) )
|
78
|
+
rescue Noumenon::Template::NotFoundError => e
|
79
|
+
if page[:layout] == "default"
|
80
|
+
return content
|
81
|
+
else
|
82
|
+
halt 500, "<h1>Missing Layout</h1><p>The layout '#{page[:layout]}' does not exist within the current theme.</p>"
|
82
83
|
end
|
83
84
|
end
|
84
|
-
|
85
|
-
nil
|
86
85
|
end
|
87
|
-
|
88
|
-
get '*' do |path|
|
89
|
-
path = locate_content_for(path)
|
90
|
-
path ? render_content(path) : [ 404, "Not Found" ]
|
91
|
-
end
|
92
|
-
end
|
93
86
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'noumenon/repository'
|
2
|
+
require 'yaml'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
# A content repository which uses YAML files within the filesystem for storage.
|
6
|
+
#
|
7
|
+
# @example The structure of a filesystem repository
|
8
|
+
# index.yml
|
9
|
+
# |- about
|
10
|
+
# |- index.yml
|
11
|
+
# |- team.yml
|
12
|
+
# |- company.yml
|
13
|
+
# contact.yml
|
14
|
+
#
|
15
|
+
# @example A piece of content
|
16
|
+
# template: team_member
|
17
|
+
# name: Jon Wood
|
18
|
+
# position: Head Honcho of Awesomeness
|
19
|
+
# bio: Jon's just awesome, we'll leave it at that.
|
20
|
+
#
|
21
|
+
# @api public
|
22
|
+
class Noumenon::Repository::FileSystem < Noumenon::Repository
|
23
|
+
# @param [ Hash ] options A hash of options.
|
24
|
+
# @option options [ String ] :path The path to access the repository at.
|
25
|
+
# @api public
|
26
|
+
def initialize(options = {})
|
27
|
+
unless options.key? :path
|
28
|
+
raise ArgumentError.new("You must provide a path to the content repository: Noumenon::Repository::FileSystem.new(path: '/tmp')")
|
29
|
+
end
|
30
|
+
|
31
|
+
super options
|
32
|
+
end
|
33
|
+
|
34
|
+
# Loads a piece of content from the repository.
|
35
|
+
#
|
36
|
+
# @param [ String ] path The path to load from.
|
37
|
+
# @param [ Boolean ] check_for_index Whether sub-directories of the same name should be checked for an index.yml file
|
38
|
+
# @return [ Hash, #each, nil ] The piece of content, or nil if it could not be found.
|
39
|
+
# @api public
|
40
|
+
def get(path, check_for_index = true)
|
41
|
+
file = repository_path("#{path}.yml")
|
42
|
+
|
43
|
+
if File.exist?(file)
|
44
|
+
YAML.load(File.read(file)).symbolize_keys
|
45
|
+
elsif check_for_index
|
46
|
+
return get("#{path}/index", false)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Saves a piece of content to the repsitory.
|
51
|
+
#
|
52
|
+
# @see Noumenon::Repository#put
|
53
|
+
def put(path, content = {})
|
54
|
+
create_tree(path)
|
55
|
+
|
56
|
+
path = File.join(path, "index") if File.exist?(repository_path(path))
|
57
|
+
File.open(repository_path("#{path}.yml"), "w") { |f| f.print content.symbolize_keys.to_yaml }
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
# Return the on-disk path to a repository item.
|
62
|
+
def repository_path(path)
|
63
|
+
File.join(options[:path], path)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates any neccesary directories, and moves files that conflict with newly created
|
67
|
+
# directories to index.yml
|
68
|
+
def create_tree(path)
|
69
|
+
path_on_disk = File.dirname(repository_path(path))
|
70
|
+
|
71
|
+
unless File.exists?(path_on_disk)
|
72
|
+
FileUtils.mkdir_p(path_on_disk)
|
73
|
+
create_indexes(path)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Moves conflicting YAML files into directory/index.yml
|
78
|
+
#
|
79
|
+
# For example given
|
80
|
+
#
|
81
|
+
# /foo
|
82
|
+
# /foo.yml
|
83
|
+
#
|
84
|
+
# /foo.yml will be moved to /foo/index.yml
|
85
|
+
def create_indexes(path)
|
86
|
+
sub_path = options[:path]
|
87
|
+
|
88
|
+
path.split("/").each do |directory|
|
89
|
+
move_to_index File.join(sub_path, directory)
|
90
|
+
sub_path = File.join(sub_path, directory)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def move_to_index(path)
|
95
|
+
path_on_disk = path
|
96
|
+
yaml_file = "#{path_on_disk}.yml"
|
97
|
+
|
98
|
+
if File.exist? yaml_file
|
99
|
+
FileUtils.mv yaml_file, File.join("#{path_on_disk}/index.yml")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'noumenon'
|
2
|
+
|
3
|
+
# The base class for content repositories. This class is predominantly present just to document the
|
4
|
+
# expected API for a repository.
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
class Noumenon::Repository
|
8
|
+
autoload :FileSystem, 'noumenon/repository/file_system'
|
9
|
+
|
10
|
+
# Provides access to options set on initialization.
|
11
|
+
# @api public
|
12
|
+
attr_reader :options
|
13
|
+
|
14
|
+
# Create a new Repository instance.
|
15
|
+
#
|
16
|
+
# @param [ Hash, #each ] options A hash of options to use when configuring the repository.
|
17
|
+
# @api public
|
18
|
+
def initialize(options = {})
|
19
|
+
@options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
# Save an item in the repository. If an item already exists then it should be overwritten.
|
23
|
+
#
|
24
|
+
# @param [ #to_s ] path The path the save the content at.
|
25
|
+
# @param [ Hash, #each ] content A hash of key/value pairs to use as the content.
|
26
|
+
# @api public
|
27
|
+
def put(path, content)
|
28
|
+
raise NotImplementedError.new("This repository type does not support updating it's contents.")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Load an item from the repository. If the item does not exist then `nil` should be returned.
|
32
|
+
#
|
33
|
+
# @param [ #to_s ] path The path to laod content from
|
34
|
+
# @return [ Hash, #each, nil ] Either the hash stored at the specified path, or nil if no content was found.
|
35
|
+
# @api public
|
36
|
+
def get(path)
|
37
|
+
raise NotImplementedError.new("This repository type does not support reading it's contents.")
|
38
|
+
end
|
39
|
+
end
|
@@ -1,9 +1,22 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
require 'noumenon'
|
2
|
+
|
3
|
+
class Noumenon::Spec::ExampleApp
|
4
|
+
def initialize(options = {})
|
5
|
+
@options = { message: "This is a mounted application" }.merge(options.symbolize_keys)
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
case env["PATH_INFO"]
|
10
|
+
when "/"
|
11
|
+
respond 200, "<h1>#{@options[:message]}</h1>"
|
12
|
+
when "/sub"
|
13
|
+
respond 200, "<h1>This is a sub-page</h1>"
|
14
|
+
else
|
15
|
+
respond 404, "Page Not Found"
|
7
16
|
end
|
8
17
|
end
|
18
|
+
|
19
|
+
def respond(status, content)
|
20
|
+
[ status, { "Content-Type" => "text/html" }, StringIO.new(content) ]
|
21
|
+
end
|
9
22
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Noumenon
|
4
|
+
module Spec
|
5
|
+
# Methods which can be used in RSpec based tests to create and use themes that will be recreated on
|
6
|
+
# each test run.
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
module ThemeHelpers
|
10
|
+
# The path to the current theme.
|
11
|
+
# @return [ String ]
|
12
|
+
# @api public
|
13
|
+
def theme_path
|
14
|
+
File.join(File.dirname(__FILE__), "..", "tmp", "example_theme")
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a new theme directory at #theme_path.
|
18
|
+
#
|
19
|
+
# @param [ Hash, nil ] description Either the description to write to theme.yml, or nil to prevent
|
20
|
+
# creation of the theme.yml file.
|
21
|
+
# @return [ Noumenon::Theme, nil ] If a description was provided the theme will be loaded, otherwise
|
22
|
+
# nil will be returned.
|
23
|
+
def create_theme(description = {})
|
24
|
+
FileUtils.mkdir_p theme_path
|
25
|
+
FileUtils.mkdir_p File.join(theme_path, "templates")
|
26
|
+
FileUtils.mkdir_p File.join(theme_path, "layouts")
|
27
|
+
FileUtils.mkdir_p File.join(theme_path, "assets")
|
28
|
+
|
29
|
+
if description
|
30
|
+
File.open(File.join(theme_path, "theme.yml"), "w") do |f|
|
31
|
+
f.print description.to_yaml
|
32
|
+
end
|
33
|
+
|
34
|
+
return Noumenon::Theme.load(theme_path)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates a theme with the provided description, runs the block, and then deletes the theme again.
|
39
|
+
#
|
40
|
+
# Useful in RSpec around blocks:
|
41
|
+
#
|
42
|
+
# @example Using with RSpec
|
43
|
+
# describe "theme stuff" do
|
44
|
+
# include Noumenon::Spec::ThemeHelpers
|
45
|
+
#
|
46
|
+
# around do |ex|
|
47
|
+
# with_temporary_theme(name: "My Theme") do
|
48
|
+
# ex.run
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# it "should have the right name" do
|
53
|
+
# theme = Noumenon::Theme.load(theme_path)
|
54
|
+
# theme.name.should == "My Theme"
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
def with_temporary_theme(description = {})
|
58
|
+
create_theme(description)
|
59
|
+
|
60
|
+
yield
|
61
|
+
|
62
|
+
FileUtils.rm_r theme_path
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/noumenon/spec.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
|
-
require 'noumenon
|
2
|
-
require 'noumenon/spec/example_app'
|
1
|
+
require 'noumenon'
|
3
2
|
|
4
|
-
module Noumenon
|
5
|
-
|
6
|
-
|
7
|
-
include Noumenon::Spec::Fixtures
|
8
|
-
end
|
3
|
+
module Noumenon::Spec
|
4
|
+
autoload :ExampleApp, 'noumenon/spec/example_app'
|
5
|
+
autoload :ThemeHelpers, 'noumenon/spec/theme_helpers'
|
9
6
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Adds some additional methods to String objects.
|
2
|
+
#
|
3
|
+
# @api public
|
4
|
+
module Noumenon::StringExtensions
|
5
|
+
# Attempts to convert a string into a constant.
|
6
|
+
#
|
7
|
+
# If the constant could not be found then a NameError will be raised.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# "Foo::Bar".constantize # => Foo::Bar
|
11
|
+
def constantize
|
12
|
+
mod = Module
|
13
|
+
split("::").each do |mod_name|
|
14
|
+
mod = mod.const_get(mod_name.to_sym)
|
15
|
+
end
|
16
|
+
|
17
|
+
mod
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
String.class_eval { include Noumenon::StringExtensions }
|