utopia 1.9.11 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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