utopia 1.9.11 → 2.0.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.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +3 -2
  3. data/.gitignore +4 -1
  4. data/.rspec +1 -0
  5. data/.travis.yml +4 -0
  6. data/.yardopts +2 -0
  7. data/Gemfile +8 -1
  8. data/README.md +2 -2
  9. data/Rakefile +10 -10
  10. data/benchmarks/call_vs_check.rb +36 -0
  11. data/benchmarks/const_vs_hash.rb +33 -0
  12. data/documentation/Gemfile +5 -0
  13. data/documentation/Guardfile +20 -0
  14. data/documentation/config.ru +6 -13
  15. data/documentation/config/puma.rb +20 -0
  16. data/documentation/pages/_editor.xnode +64 -0
  17. data/documentation/pages/_heading.xnode +2 -2
  18. data/documentation/pages/_page.xnode +1 -2
  19. data/documentation/pages/errors/exception.xnode +3 -3
  20. data/documentation/pages/errors/file-not-found.xnode +3 -3
  21. data/documentation/pages/wiki/bower-integration/content.md +1 -1
  22. data/documentation/pages/wiki/content.md +6 -8
  23. data/documentation/pages/wiki/controller.rb +3 -3
  24. data/documentation/pages/wiki/edit.xnode +7 -19
  25. data/documentation/pages/wiki/middleware/content/content.md +4 -10
  26. data/documentation/pages/wiki/{controller → middleware/controller}/actions/content.md +0 -0
  27. data/documentation/pages/wiki/{controller → middleware/controller}/links.yaml +0 -0
  28. data/documentation/pages/wiki/{controller → middleware/controller}/rewrite/content.md +3 -3
  29. data/documentation/pages/wiki/show.xnode +4 -6
  30. data/documentation/pages/wiki/updating-utopia/content.md +55 -0
  31. data/documentation/pages/wiki/your-first-page/content.md +5 -3
  32. data/documentation/public/materials +1 -0
  33. data/lib/utopia.rb +3 -4
  34. data/lib/utopia/command.rb +4 -284
  35. data/lib/utopia/command/server.rb +115 -0
  36. data/lib/utopia/command/setup.rb +78 -0
  37. data/lib/utopia/command/site.rb +183 -0
  38. data/lib/utopia/content.rb +83 -59
  39. data/lib/utopia/content/{transaction.rb → document.rb} +116 -110
  40. data/lib/utopia/content/link.rb +7 -2
  41. data/lib/utopia/content/links.rb +2 -1
  42. data/lib/utopia/content/markup.rb +7 -2
  43. data/lib/utopia/{tags/deferred.rb → content/namespace.rb} +25 -6
  44. data/lib/utopia/content/node.rb +74 -76
  45. data/lib/utopia/content/response.rb +22 -3
  46. data/lib/utopia/content/tags.rb +66 -0
  47. data/lib/utopia/controller.rb +10 -18
  48. data/lib/utopia/controller/actions.rb +10 -0
  49. data/lib/utopia/controller/base.rb +2 -1
  50. data/lib/utopia/controller/respond.rb +1 -1
  51. data/lib/utopia/controller/rewrite.rb +8 -4
  52. data/lib/utopia/exceptions.rb +1 -0
  53. data/lib/utopia/exceptions/handler.rb +7 -2
  54. data/lib/utopia/exceptions/mailer.rb +33 -12
  55. data/lib/utopia/{tags/node.rb → extensions/array_split.rb} +11 -9
  56. data/lib/utopia/{tags/environment.rb → extensions/date_comparisons.rb} +24 -14
  57. data/lib/utopia/http.rb +2 -0
  58. data/lib/utopia/locale.rb +1 -0
  59. data/lib/utopia/localization.rb +37 -28
  60. data/lib/utopia/logger.rb +1 -0
  61. data/lib/utopia/logger/compact_formatter.rb +1 -0
  62. data/lib/utopia/middleware.rb +11 -1
  63. data/lib/utopia/path.rb +1 -0
  64. data/lib/utopia/path/matcher.rb +14 -2
  65. data/lib/utopia/redirection.rb +13 -16
  66. data/lib/utopia/session.rb +14 -6
  67. data/lib/utopia/setup.rb +3 -1
  68. data/lib/utopia/static.rb +11 -12
  69. data/lib/utopia/version.rb +1 -1
  70. data/setup/server/git/hooks/post-receive +0 -4
  71. data/setup/site/.gitignore +9 -0
  72. data/setup/site/.rspec +1 -0
  73. data/setup/site/Gemfile +4 -0
  74. data/setup/site/Guardfile +17 -0
  75. data/setup/site/Rakefile +2 -2
  76. data/setup/site/config.ru +5 -12
  77. data/setup/site/pages/_heading.xnode +2 -2
  78. data/setup/site/pages/_page.xnode +1 -1
  79. data/setup/site/pages/errors/exception.xnode +3 -3
  80. data/setup/site/pages/errors/file-not-found.xnode +3 -3
  81. data/setup/site/pages/welcome/index.xnode +3 -3
  82. data/setup/site/public/_static/site.css +4 -0
  83. data/setup/site/spec/spec_helper.rb +29 -0
  84. data/setup/site/tasks/deploy.rake +13 -0
  85. data/setup/site/tasks/development.rake +34 -0
  86. data/setup/site/tasks/environment.rake +17 -0
  87. data/spec/mock_node.rb +15 -0
  88. data/spec/spec_helper.rb +29 -0
  89. data/{lib/utopia/extensions/date.rb → spec/utopia/content/document_spec.rb} +31 -21
  90. data/spec/utopia/content/markup_spec.rb +2 -2
  91. data/spec/utopia/content/{tag_spec.rb → namespace_spec.rb} +17 -10
  92. data/spec/utopia/content/tags_spec.rb +80 -0
  93. data/spec/utopia/content_spec.rb +1 -1
  94. data/spec/utopia/content_spec.ru +1 -6
  95. data/spec/utopia/content_spec/_heading.xnode +1 -1
  96. data/spec/utopia/content_spec/content/test-partial.xnode +1 -1
  97. data/spec/utopia/content_spec/index.xnode +1 -1
  98. data/spec/utopia/controller/middleware_spec.ru +1 -3
  99. data/spec/utopia/controller/respond_spec.rb +2 -22
  100. data/spec/utopia/controller/respond_spec.ru +1 -5
  101. data/spec/utopia/controller/respond_spec/errors/file-not-found.xnode +7 -6
  102. data/spec/utopia/exceptions/handler_spec.ru +1 -2
  103. data/spec/utopia/exceptions/mailer_spec.ru +1 -2
  104. data/spec/utopia/extensions_spec.rb +2 -2
  105. data/spec/utopia/localization_spec.ru +1 -2
  106. data/spec/utopia/performance_spec.rb +2 -6
  107. data/spec/utopia/performance_spec/config.ru +5 -12
  108. data/spec/utopia/performance_spec/pages/_heading.xnode +2 -2
  109. data/spec/utopia/performance_spec/pages/_page.xnode +1 -1
  110. data/spec/utopia/performance_spec/pages/errors/exception.xnode +3 -3
  111. data/spec/utopia/performance_spec/pages/errors/file-not-found.xnode +3 -3
  112. data/spec/utopia/performance_spec/pages/welcome/index.xnode +3 -3
  113. data/spec/utopia/setup_spec.rb +79 -15
  114. data/utopia.gemspec +3 -3
  115. metadata +41 -27
  116. data/.simplecov +0 -9
  117. data/documentation/pages/welcome/index.xnode +0 -41
  118. data/lib/utopia/content/tag.rb +0 -90
  119. data/lib/utopia/extensions/array.rb +0 -29
  120. data/lib/utopia/tags/override.rb +0 -33
  121. data/setup/site/.simplecov +0 -9
  122. data/setup/site/tasks/test.rake +0 -10
  123. data/setup/site/tasks/utopia.rake +0 -41
  124. data/spec/utopia/controller/respond_spec/rewrite/controller.rb +0 -12
@@ -0,0 +1,115 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'setup'
22
+
23
+ module Utopia
24
+ module Command
25
+ # Server setup commands.
26
+ class Server < Samovar::Command
27
+ # Create a server.
28
+ class Create < Samovar::Command
29
+ self.description = "Create a remote Utopia website suitable for deployment using git."
30
+
31
+ def invoke(parent)
32
+ destination_root = parent.root
33
+
34
+ FileUtils.mkdir_p File.join(destination_root, "public")
35
+
36
+ Update.new.invoke(parent)
37
+
38
+ # Print out helpful git remote add message:
39
+ hostname = `hostname`.chomp
40
+ puts "Now add the git remote to your local repository:\n\tgit remote add production ssh://#{hostname}#{destination_root}"
41
+ puts "Then push to it:\n\tgit push --set-upstream production master"
42
+ end
43
+ end
44
+
45
+ # Update a server.
46
+ class Update < Samovar::Command
47
+ self.description = "Update the git hooks in an existing server repository."
48
+
49
+ def invoke(parent)
50
+ destination_root = parent.root
51
+
52
+ Dir.chdir(destination_root) do
53
+ # It's okay to call this on an existing repo, it will only update config as required to enable --shared.
54
+ # --shared allows multiple users to access the site with the same group.
55
+ system("git", "init", "--shared") or fail "could not initialize repository"
56
+
57
+ system("git", "config", "receive.denyCurrentBranch", "ignore") or fail "could not set configuration"
58
+ system("git", "config", "core.worktree", destination_root) or fail "could not set configuration"
59
+
60
+ # In theory, to convert from non-shared to shared:
61
+ # chgrp -R <group-name> . # Change files and directories' group
62
+ # chmod -R g+w . # Change permissions
63
+ # chmod g-w .git/objects/pack/* # Git pack files should be immutable
64
+ # chmod g+s `find . -type d` # New files get group id of directory
65
+ end
66
+
67
+ Setup::Server.update_default_environment(destination_root)
68
+
69
+ # Copy git hooks:
70
+ system("cp", "-r", File.join(Setup::Server::ROOT, 'git', 'hooks'), File.join(destination_root, '.git')) or fail "could not copy git hooks"
71
+ # finally set everything in the .git directory to be group writable
72
+ system("chmod", "-Rf", "g+w", File.join(destination_root, '.git')) or fail "could not update permissions of .git directory"
73
+ end
74
+ end
75
+
76
+ # Set environment variables within the server deployment.
77
+ class Environment < Samovar::Command
78
+ self.description = "Update environment variables in config/environment.yaml"
79
+
80
+ many :variables, "A list of environment KEY=VALUE pairs to set."
81
+
82
+ def invoke(parent)
83
+ return if variables.empty?
84
+
85
+ destination_root = parent.root
86
+
87
+ Setup::Server.environment(destination_root) do |store|
88
+ variables.each do |variable|
89
+ key, value = variable.split('=', 2)
90
+
91
+ if value
92
+ puts "ENV[#{key.inspect}] will default to #{value.inspect} unless otherwise specified."
93
+ store[key] = value
94
+ else
95
+ puts "ENV[#{key.inspect}] will be unset unless otherwise specified."
96
+ store.delete(key)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ self.description = "Manage server deployments."
104
+
105
+ nested '<command>',
106
+ 'create' => Create,
107
+ 'update' => Update,
108
+ 'environment' => Environment
109
+
110
+ def invoke(parent)
111
+ @command.invoke(parent)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,78 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'fileutils'
22
+ require 'find'
23
+
24
+ require 'yaml/store'
25
+
26
+ require 'samovar'
27
+ require 'securerandom'
28
+
29
+ module Utopia
30
+ module Command
31
+ # The command for client/server setup.
32
+ module Setup
33
+ # This path must point to utopia/setup in the gem source.
34
+ BASE = File.expand_path("../../../setup", __dir__)
35
+
36
+ # Helpers for setting up a local site.
37
+ module Site
38
+ # Configuration files which should be installed/updated:
39
+ CONFIGURATION_FILES = ['.bowerrc', '.gitignore', 'config.ru', 'config/environment.rb', 'Gemfile', 'Guardfile', 'Rakefile', 'tasks/bower.rake', 'tasks/deploy.rake', 'tasks/development.rake', 'tasks/environment.rake', 'tasks/log.rake']
40
+
41
+ # Directories that should exist:
42
+ DIRECTORIES = ["config", "lib", "pages", "public", "tasks"]
43
+
44
+ # Directories that should be removed during upgrade process:
45
+ OLD_PATHS = ["access_log", "cache", "tmp", 'tasks/test.rake', 'tasks/utopia.rake']
46
+
47
+ # The root directory of the template site:
48
+ ROOT = File.join(BASE, 'site')
49
+ end
50
+
51
+ # Helpers for setting up the server deployment.
52
+ module Server
53
+ # The root directory of the template server deployment:
54
+ ROOT = File.join(BASE, 'server')
55
+
56
+ # Setup `config/environment.yaml` according to specified options.
57
+ def self.environment(root)
58
+ environment_path = File.join(root, 'config/environment.yaml')
59
+ FileUtils.mkpath File.dirname(environment_path)
60
+
61
+ store = YAML::Store.new(environment_path)
62
+
63
+ store.transaction do
64
+ yield store
65
+ end
66
+ end
67
+
68
+ # Set some useful defaults for the environment.
69
+ def self.update_default_environment(root)
70
+ environment(root) do |store|
71
+ store['RACK_ENV'] ||= 'production'
72
+ store['UTOPIA_SESSION_SECRET'] ||= SecureRandom.hex(40)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,183 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'setup'
22
+
23
+ module Utopia
24
+ module Command
25
+ # Local site setup commands.
26
+ class Site < Samovar::Command
27
+ # Create a local site.
28
+ class Create < Samovar::Command
29
+ self.description = "Create a new local Utopia website using the default template."
30
+ # self.example = "utopia --in www.example.com site create"
31
+
32
+ def invoke(parent)
33
+ destination_root = parent.root
34
+
35
+ $stderr.puts "Setting up initial site in #{destination_root} for Utopia v#{Utopia::VERSION}..."
36
+
37
+ Setup::Site::DIRECTORIES.each do |directory|
38
+ FileUtils.mkdir_p(File.join(destination_root, directory))
39
+ end
40
+
41
+ Find.find(Setup::Site::ROOT) do |source_path|
42
+ # What is this doing?
43
+ destination_path = File.join(destination_root, source_path[Setup::Site::ROOT.size..-1])
44
+
45
+ if File.directory?(source_path)
46
+ FileUtils.mkdir_p(destination_path)
47
+ else
48
+ unless File.exist? destination_path
49
+ FileUtils.copy_entry(source_path, destination_path)
50
+ end
51
+ end
52
+ end
53
+
54
+ Setup::Site::CONFIGURATION_FILES.each do |configuration_file|
55
+ destination_path = File.join(destination_root, configuration_file)
56
+
57
+ buffer = File.read(destination_path).gsub('$UTOPIA_VERSION', Utopia::VERSION)
58
+
59
+ File.open(destination_path, "w") { |file| file.write(buffer) }
60
+ end
61
+
62
+ Dir.chdir(destination_root) do
63
+ puts "Setting up site in #{destination_root}..."
64
+
65
+ if `which bundle`.strip != ''
66
+ puts "Generating initial package list with bundle..."
67
+ system("bundle", "install", "--binstubs") or fail "could not install bundled gems"
68
+ end
69
+
70
+ if `which git`.strip == ""
71
+ $stderr.puts "Now is a good time to learn about git: http://git-scm.com/"
72
+ elsif !File.exist?('.git')
73
+ puts "Setting up git repository..."
74
+ system("git", "init") or fail "could not create git repository"
75
+ system("git", "add", ".") or fail "could not add all files"
76
+ system("git", "commit", "-q", "-m", "Initial Utopia v#{Utopia::VERSION} site.") or fail "could not commit files"
77
+ end
78
+ end
79
+
80
+ name = `git config user.name || whoami`.chomp
81
+
82
+ puts
83
+ puts " #{name},".ljust(78)
84
+ puts "Thank you for using Utopia!".center(78)
85
+ puts "We sincerely hope that Utopia helps to".center(78)
86
+ puts "make your life easier and more enjoyable.".center(78)
87
+ puts ""
88
+ puts "To start the development server, run:".center(78)
89
+ puts "rake server".center(78)
90
+ puts ""
91
+ puts "For extreme productivity, please consult the online documentation".center(78)
92
+ puts "https://github.com/ioquatix/utopia".center(78)
93
+ puts " ~ Samuel. ".rjust(78)
94
+ end
95
+ end
96
+
97
+ # Update a local site.
98
+ class Update < Samovar::Command
99
+ self.description = "Upgrade an existing site to use the latest configuration files from the template."
100
+
101
+ # Move legacy `pages/_static` to `public/_static`.
102
+ def move_static!
103
+ if File.lstat("public/_static").symlink?
104
+ FileUtils.rm_f "public/_static"
105
+ end
106
+
107
+ if File.directory?("pages/_static") and !File.exist?("public/_static")
108
+ system("git", "mv", "pages/_static", "public/")
109
+ end
110
+ end
111
+
112
+ def invoke(parent)
113
+ destination_root = parent.root
114
+ branch_name = "utopia-upgrade-#{Utopia::VERSION}"
115
+
116
+ $stderr.puts "Upgrading #{destination_root}..."
117
+
118
+ Dir.chdir(destination_root) do
119
+ system('git', 'checkout', '-b', branch_name) or fail "could not change branch"
120
+ end
121
+
122
+ Setup::Site::DIRECTORIES.each do |directory|
123
+ FileUtils.mkdir_p(File.join(destination_root, directory))
124
+ end
125
+
126
+ Setup::Site::OLD_PATHS.each do |path|
127
+ path = File.join(destination_root, path)
128
+ $stderr.puts "\tRemoving #{path}..."
129
+ FileUtils.rm_rf(path)
130
+ end
131
+
132
+ Setup::Site::CONFIGURATION_FILES.each do |configuration_file|
133
+ source_path = File.join(Setup::Site::ROOT, configuration_file)
134
+ destination_path = File.join(destination_root, configuration_file)
135
+
136
+ $stderr.puts "Updating #{destination_path}..."
137
+
138
+ FileUtils.copy_entry(source_path, destination_path)
139
+ buffer = File.read(destination_path).gsub('$UTOPIA_VERSION', Utopia::VERSION)
140
+ File.open(destination_path, "w") { |file| file.write(buffer) }
141
+ end
142
+
143
+ begin
144
+ Dir.chdir(destination_root) do
145
+ # Stage any files that have been changed or removed:
146
+ system("git", "add", "-u") or fail "could not add files"
147
+
148
+ # Stage any new files that we have explicitly added:
149
+ system("git", "add", *Setup::Site::CONFIGURATION_FILES) or fail "could not add files"
150
+
151
+ move_static!
152
+
153
+ # Commit all changes:
154
+ system("git", "commit", "-m", "Upgrade to utopia #{Utopia::VERSION}.") or fail "could not commit changes"
155
+
156
+ # Checkout master..
157
+ system("git", "checkout", "master") or fail "could not checkout master"
158
+
159
+ # and merge:
160
+ system("git", "merge", "--squash", "--no-commit", branch_name) or fail "could not merge changes"
161
+ end
162
+ rescue RuntimeError
163
+ $stderr.puts "** Detected error with upgrade, reverting changes. Some new files may still exist in tree. **"
164
+
165
+ system("git", "checkout", "master")
166
+ ensure
167
+ system("git", "branch", "-D", branch_name)
168
+ end
169
+ end
170
+ end
171
+
172
+ nested '<command>',
173
+ 'create' => Create,
174
+ 'update' => Update
175
+
176
+ self.description = "Manage local utopia sites."
177
+
178
+ def invoke(parent)
179
+ @command.invoke(parent)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -23,94 +23,65 @@ require_relative 'localization'
23
23
 
24
24
  require_relative 'content/node'
25
25
  require_relative 'content/markup'
26
+ require_relative 'content/tags'
26
27
 
27
28
  require 'trenni/template'
28
29
 
29
30
  require 'concurrent/map'
30
31
 
31
32
  module Utopia
33
+ # A middleware which serves dynamically generated content based on markup files.
32
34
  class Content
33
35
  INDEX = 'index'.freeze
34
36
 
35
- def initialize(app, **options)
37
+ CONTENT_NAMESPACE = 'content'.freeze
38
+ UTOPIA_NAMESPACE = 'utopia'.freeze
39
+ DEFERRED_TAG_NAME = 'utopia:deferred'.freeze
40
+ CONTENT_TAG_NAME = 'utopia:content'.freeze
41
+
42
+ # @param root [String] The content root where pages will be generated from.
43
+ # @param namespaces [Hash<String,Library>] Tag namespaces for dynamic tag lookup.
44
+ def initialize(app, root: Utopia::default_root, namespaces: {})
36
45
  @app = app
46
+ @root = root
37
47
 
38
- @root = File.expand_path(options[:root] || Utopia::default_root)
48
+ @template_cache = Concurrent::Map.new
49
+ @node_cache = Concurrent::Map.new
39
50
 
40
- if options[:cache_templates]
41
- @template_cache = Concurrent::Map.new
42
- else
43
- @template_cache = nil
44
- end
51
+ @namespaces = namespaces
45
52
 
46
- @tags = options.fetch(:tags, {})
53
+ # Default content namespace for dynamic path based lookup:
54
+ @namespaces[CONTENT_NAMESPACE] ||= self.method(:content_tag)
47
55
 
48
- self.freeze
56
+ # The core namespace for utopia specific functionality:
57
+ @namespaces[UTOPIA_NAMESPACE] ||= Tags
49
58
  end
50
59
 
51
60
  def freeze
61
+ return self if frozen?
62
+
52
63
  @root.freeze
53
- @tags.freeze
64
+ @namespaces.values.each(&:freeze)
65
+ @namespaces.freeze
54
66
 
55
67
  super
56
68
  end
57
-
69
+
58
70
  attr :root
59
-
71
+
60
72
  def fetch_template(path)
61
- if @template_cache
62
- @template_cache.fetch_or_store(path.to_s) do
63
- Trenni::MarkupTemplate.load_file(path)
64
- end
65
- else
73
+ @template_cache.fetch_or_store(path.to_s) do
66
74
  Trenni::MarkupTemplate.load_file(path)
67
75
  end
68
76
  end
69
77
 
70
- # This function looks up a named tag in a given path. It's a hotspot and needs improvement.
71
- private def fetch_tag(name, parent_path)
72
- if String === name && name.index('/')
73
- name = Path.create(name)
74
- end
75
-
76
- if Path === name
77
- name = parent_path + name
78
- name_path = name.components.dup
79
- name_path[-1] += XNODE_EXTENSION
80
- else
81
- name_path = name + XNODE_EXTENSION
82
- end
78
+ # Look up a named tag such as `<entry />` or `<content:page>...`
79
+ def lookup_tag(qualified_name, node)
80
+ namespace, name = Trenni::Tag.split(qualified_name)
83
81
 
84
- components = parent_path.components.dup
85
-
86
- while components.any?
87
- tag_path = File.join(root, components, name_path)
88
-
89
- if File.exist? tag_path
90
- return Node.new(self, Path[components] + name, parent_path + name, tag_path)
91
- end
92
-
93
- if String === name_path
94
- tag_path = File.join(root, components, '_' + name_path)
95
-
96
- if File.exist? tag_path
97
- return Node.new(self, Path[components] + name, parent_path + name, tag_path)
98
- end
99
- end
100
-
101
- components.pop
102
- end
103
-
104
- return nil
105
- end
106
-
107
- # Look up a named tag such as <entry />
108
- def lookup_tag(name, parent_path)
109
- if @tags.key? name
110
- return @tags[name]
82
+ if library = @namespaces[namespace]
83
+ library.call(name, node)
111
84
  end
112
-
113
- return fetch_tag(name, parent_path)
114
85
  end
115
86
 
116
87
  # The request_path is an absolute uri path, e.g. /foo/bar. If an xnode file exists on disk for this exact path, it is instantiated, otherwise nil.
@@ -155,5 +126,58 @@ module Utopia
155
126
 
156
127
  return @app.call(env)
157
128
  end
129
+
130
+ private
131
+
132
+ def lookup_content(name, parent_path)
133
+ if String === name && name.index('/')
134
+ name = Path.create(name)
135
+ end
136
+
137
+ if Path === name
138
+ name = parent_path + name
139
+ name_path = name.components.dup
140
+ name_path[-1] += XNODE_EXTENSION
141
+ else
142
+ name_path = name + XNODE_EXTENSION
143
+ end
144
+
145
+ components = parent_path.components.dup
146
+
147
+ while components.any?
148
+ tag_path = File.join(@root, components, name_path)
149
+
150
+ if File.exist? tag_path
151
+ return Node.new(self, Path[components] + name, parent_path + name, tag_path)
152
+ end
153
+
154
+ if String === name_path
155
+ tag_path = File.join(@root, components, '_' + name_path)
156
+
157
+ if File.exist? tag_path
158
+ return Node.new(self, Path[components] + name, parent_path + name, tag_path)
159
+ end
160
+ end
161
+
162
+ components.pop
163
+ end
164
+
165
+ return nil
166
+ end
167
+
168
+ def content_tag(name, node)
169
+ parent_path = node.parent_path
170
+
171
+ # If the current node is called 'foo', we can't lookup 'foo' in the current directory or we will have infinite recursion.
172
+ if name == node.name
173
+ parent_path = parent_path.dirname
174
+ end
175
+
176
+ cache_key = parent_path + name
177
+
178
+ @node_cache.fetch_or_store(cache_key) do
179
+ lookup_content(name, parent_path)
180
+ end
181
+ end
158
182
  end
159
183
  end