|
3 | 3 | # Released under the MIT License. |
4 | 4 | # Copyright, 2009-2025, by Samuel Williams. |
5 | 5 |
|
6 | | -require_relative "middleware" |
7 | | -require_relative "localization" |
8 | | - |
9 | | -require_relative "content/links" |
10 | | -require_relative "content/node" |
11 | | -require_relative "content/markup" |
12 | | -require_relative "content/tags" |
13 | | - |
14 | | -require "xrb/template" |
15 | | - |
16 | | -require "concurrent/map" |
17 | | - |
18 | | -require "traces/provider" |
| 6 | +require_relative "content/middleware" |
19 | 7 |
|
20 | 8 | module Utopia |
21 | | - # A middleware which serves dynamically generated content based on markup files. |
22 | | - class Content |
23 | | - CONTENT_NAMESPACE = "content".freeze |
24 | | - UTOPIA_NAMESPACE = "utopia".freeze |
25 | | - DEFERRED_TAG_NAME = "utopia:deferred".freeze |
26 | | - CONTENT_TAG_NAME = "utopia:content".freeze |
27 | | - |
28 | | - # @param root [String] The content root where pages will be generated from. |
29 | | - # @param namespaces [Hash<String,Library>] Tag namespaces for dynamic tag lookup. |
30 | | - def initialize(app, root: Utopia::default_root, namespaces: {}) |
31 | | - @app = app |
32 | | - @root = root |
33 | | - |
34 | | - @template_cache = Concurrent::Map.new |
35 | | - @node_cache = Concurrent::Map.new |
36 | | - |
37 | | - @links = Links.new(@root) |
38 | | - |
39 | | - @namespaces = namespaces |
40 | | - |
41 | | - # Default content namespace for dynamic path based lookup: |
42 | | - @namespaces[CONTENT_NAMESPACE] ||= self.method(:content_tag) |
43 | | - |
44 | | - # The core namespace for utopia specific functionality: |
45 | | - @namespaces[UTOPIA_NAMESPACE] ||= Tags |
46 | | - end |
47 | | - |
48 | | - def freeze |
49 | | - return self if frozen? |
50 | | - |
51 | | - @root.freeze |
52 | | - @namespaces.values.each(&:freeze) |
53 | | - @namespaces.freeze |
54 | | - |
55 | | - super |
56 | | - end |
57 | | - |
58 | | - attr :root |
59 | | - |
60 | | - # TODO we should remove this method and expose `@links` directly. |
61 | | - def links(path, **options) |
62 | | - @links.index(path, **options) |
63 | | - end |
64 | | - |
65 | | - def fetch_template(path) |
66 | | - @template_cache.fetch_or_store(path.to_s) do |
67 | | - XRB::Template.load_file(path) |
68 | | - end |
69 | | - end |
70 | | - |
71 | | - # Look up a named tag such as `<entry />` or `<content:page>...` |
72 | | - def lookup_tag(qualified_name, node) |
73 | | - namespace, name = XRB::Tag.split(qualified_name) |
74 | | - |
75 | | - if library = @namespaces[namespace] |
76 | | - library.call(name, node) |
77 | | - end |
78 | | - end |
79 | | - |
80 | | - # @param path [Path] 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. |
81 | | - def lookup_node(path, locale = nil) |
82 | | - resolve_link( |
83 | | - @links.for(path, locale) |
84 | | - ) |
85 | | - end |
86 | | - |
87 | | - def resolve_link(link) |
88 | | - if full_path = link&.full_path(@root) |
89 | | - if File.exist?(full_path) |
90 | | - return Node.new(self, link.path, link.path, full_path) |
91 | | - end |
92 | | - end |
93 | | - end |
94 | | - |
95 | | - def respond(link, request) |
96 | | - if node = resolve_link(link) |
97 | | - attributes = request.env.fetch(VARIABLES_KEY, {}).to_hash |
98 | | - |
99 | | - return node.process!(request, attributes) |
100 | | - elsif redirect_uri = link[:uri] |
101 | | - return [307, {HTTP::LOCATION => redirect_uri}, []] |
102 | | - end |
103 | | - end |
104 | | - |
105 | | - def call(env) |
106 | | - request = Rack::Request.new(env) |
107 | | - path = Path.create(request.path_info) |
108 | | - |
109 | | - # Check if the request is to a non-specific index. This only works for requests with a given name: |
110 | | - basename = path.basename |
111 | | - directory_path = File.join(@root, path.dirname.components, basename) |
112 | | - |
113 | | - # If the request for /foo/bar is actually a directory, rewrite it to /foo/bar/index: |
114 | | - if File.directory? directory_path |
115 | | - index_path = [basename, INDEX] |
116 | | - |
117 | | - return [307, {HTTP::LOCATION => path.dirname.join(index_path).to_s}, []] |
118 | | - end |
119 | | - |
120 | | - locale = env[Localization::CURRENT_LOCALE_KEY] |
121 | | - if link = @links.for(path, locale) |
122 | | - if response = self.respond(link, request) |
123 | | - return response |
124 | | - end |
125 | | - end |
126 | | - |
127 | | - return @app.call(env) |
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 | | - full_path = node.parent_path + name |
170 | | - |
171 | | - name = full_path.pop |
172 | | - |
173 | | - # If the current node is called 'foo', we can't lookup 'foo' in the current directory or we will have infinite recursion. |
174 | | - while full_path.last == name |
175 | | - full_path.pop |
176 | | - end |
177 | | - |
178 | | - cache_key = full_path + name |
179 | | - |
180 | | - @node_cache.fetch_or_store(cache_key) do |
181 | | - lookup_content(name, full_path) |
182 | | - end |
183 | | - end |
184 | | - end |
185 | | - |
186 | | - Traces::Provider(Content) do |
187 | | - def respond(link, request) |
188 | | - attributes = { |
189 | | - "link.key" => link.key, |
190 | | - "link.href" => link.href |
191 | | - } |
192 | | - |
193 | | - Traces.trace("utopia.content.respond", attributes: attributes) {super} |
| 9 | + module Content |
| 10 | + def self.new(...) |
| 11 | + Middleware.new(...) |
194 | 12 | end |
195 | 13 | end |
196 | 14 | end |
0 commit comments