beachio-hammer 0.1.0.pre1
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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +35 -0
- data/PUBLISH.md +34 -0
- data/beachio-hammer.gemspec +27 -0
- data/exe/hammer +6 -0
- data/lib/beachio-hammer.rb +25 -0
- data/lib/hammer/build_command.rb +146 -0
- data/lib/hammer/builder.rb +195 -0
- data/lib/hammer/cli.rb +27 -0
- data/lib/hammer/content/builder.rb +77 -0
- data/lib/hammer/content/config.rb +68 -0
- data/lib/hammer/content/document.rb +115 -0
- data/lib/hammer/content/expression_evaluator.rb +113 -0
- data/lib/hammer/content/helpers.rb +96 -0
- data/lib/hammer/content/page_generator.rb +362 -0
- data/lib/hammer/content/parser.rb +463 -0
- data/lib/hammer/content/relation_resolver.rb +54 -0
- data/lib/hammer/content/template_context.rb +91 -0
- data/lib/hammer/css_parser.rb +178 -0
- data/lib/hammer/errors.rb +18 -0
- data/lib/hammer/ignore.rb +52 -0
- data/lib/hammer/output_formatter.rb +33 -0
- data/lib/hammer/project_resolver.rb +41 -0
- data/lib/hammer/sitemap_generator.rb +84 -0
- data/lib/hammer/tag_parser.rb +569 -0
- data/lib/hammer/version.rb +5 -0
- data/scripts/capture-goldens.sh +15 -0
- data/scripts/test-classic.rb +23 -0
- data/scripts/test-content.rb +20 -0
- data/scripts/tree-diff.rb +65 -0
- data/spec/parity/classic_spec.rb +23 -0
- data/spec/parity/content_spec.rb +23 -0
- data/spec/spec_helper.rb +10 -0
- metadata +103 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hammer
|
|
4
|
+
module Content
|
|
5
|
+
class Parser
|
|
6
|
+
def initialize(source_directory:)
|
|
7
|
+
@source_directory = source_directory
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def parse_markdown_file(path)
|
|
11
|
+
content = File.read(path)
|
|
12
|
+
metadata, body = extract_front_matter(content)
|
|
13
|
+
html_body = parse_markdown_body(body)
|
|
14
|
+
collection = extract_collection(path)
|
|
15
|
+
Document.new(
|
|
16
|
+
file_path: path,
|
|
17
|
+
metadata: metadata,
|
|
18
|
+
body: html_body,
|
|
19
|
+
content_type: :markdown,
|
|
20
|
+
collection: collection
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_yaml_file(path)
|
|
25
|
+
metadata = parse_simple_yaml(File.read(path))
|
|
26
|
+
Document.new(
|
|
27
|
+
file_path: path,
|
|
28
|
+
metadata: metadata,
|
|
29
|
+
body: "",
|
|
30
|
+
content_type: :yaml,
|
|
31
|
+
collection: extract_collection(path)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def extract_collection(path)
|
|
36
|
+
rel = path.delete_prefix("#{@source_directory}/")
|
|
37
|
+
parts = rel.split("/")
|
|
38
|
+
parts[1] if parts[0] == "content" && parts.length > 2
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_front_matter(content)
|
|
42
|
+
return [{}, content] unless content.start_with?("---\n")
|
|
43
|
+
|
|
44
|
+
rest = content[4..]
|
|
45
|
+
return [{}, content] unless (end_idx = rest.index("\n---"))
|
|
46
|
+
|
|
47
|
+
yaml_text = rest[0...end_idx]
|
|
48
|
+
body = rest[(end_idx + 4)..].sub(/\A\n?/, "")
|
|
49
|
+
[parse_simple_yaml(yaml_text), body]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_simple_yaml(text)
|
|
53
|
+
metadata = {}
|
|
54
|
+
current_key = nil
|
|
55
|
+
text.each_line do |line|
|
|
56
|
+
stripped = line.strip
|
|
57
|
+
next if stripped.empty?
|
|
58
|
+
|
|
59
|
+
if (m = stripped.match(/\A([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)\z/))
|
|
60
|
+
key = m[1]
|
|
61
|
+
value = m[2].strip
|
|
62
|
+
metadata[key] = parse_yaml_value(value)
|
|
63
|
+
current_key = key
|
|
64
|
+
elsif stripped.start_with?("- ") && current_key
|
|
65
|
+
metadata[current_key] ||= []
|
|
66
|
+
metadata[current_key] << parse_yaml_value(stripped[2..].strip)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
metadata
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_yaml_value(value)
|
|
73
|
+
return true if value == "true"
|
|
74
|
+
return false if value == "false"
|
|
75
|
+
return nil if value == "null" || value.empty?
|
|
76
|
+
return Integer(value) if value.match?(/\A-?\d+\z/)
|
|
77
|
+
return Float(value) if value.match?(/\A-?\d+\.\d+\z/)
|
|
78
|
+
if value.start_with?("[") && value.end_with?("]")
|
|
79
|
+
inner = value[1..-2]
|
|
80
|
+
return [] if inner.strip.empty?
|
|
81
|
+
|
|
82
|
+
return inner.split(",").map { |v| parse_yaml_value(v.strip.delete_prefix('"').delete_suffix('"')) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
value.delete_prefix('"').delete_suffix('"').delete_prefix("'").delete_suffix("'")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_markdown_body(markdown)
|
|
89
|
+
html = markdown.dup
|
|
90
|
+
|
|
91
|
+
html.gsub!(/```(\w+)?\s*\n([\s\S]*?)```/) do
|
|
92
|
+
lang = Regexp.last_match(1).to_s
|
|
93
|
+
code = Regexp.last_match(2)
|
|
94
|
+
code = code.gsub(/\A\n+/, "").gsub(/\n+\z/, "")
|
|
95
|
+
escaped = CGI.escapeHTML(code)
|
|
96
|
+
cls = lang.empty? ? "" : " class=\"language-#{lang}\""
|
|
97
|
+
"<pre><code#{cls}>#{escaped}</code></pre>"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
/^### (.*?)$/ => "<h3>\\1</h3>",
|
|
102
|
+
/^## (.*?)$/ => "<h2>\\1</h2>",
|
|
103
|
+
/^# (.*?)$/ => "<h1>\\1</h1>"
|
|
104
|
+
}.each { |pat, repl| html.gsub!(pat, repl) }
|
|
105
|
+
|
|
106
|
+
placeholders = []
|
|
107
|
+
html.gsub!(%r{</?[a-zA-Z][^>]*>}) do |tag|
|
|
108
|
+
token = "HAMMERHTMLTAGTOKEN#{placeholders.length}ZZEND"
|
|
109
|
+
placeholders << [token, tag]
|
|
110
|
+
token
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
html.gsub!(/\*\*(.+?)\*\*/, '<strong>\1</strong>')
|
|
114
|
+
html.gsub!(/__(.+?)__/, '<strong>\1</strong>')
|
|
115
|
+
html.gsub!(/\*(.+?)\*/, '<em>\1</em>')
|
|
116
|
+
html.gsub!(/_(.+?)_/, '<em>\1</em>')
|
|
117
|
+
|
|
118
|
+
placeholders.each { |token, tag| html.gsub!(token, tag) }
|
|
119
|
+
|
|
120
|
+
html.gsub!(/\[([^\]]+)\]\(([^)]+)\)/, '<a href="\2">\1</a>')
|
|
121
|
+
html.gsub!(/<(https?:\/\/[^>]+)>/, '<a href="\1">\1</a>')
|
|
122
|
+
html.gsub!(/!\[([^\]]*)\]\(([^)]+)\)/, '<img src="\2" alt="\1">')
|
|
123
|
+
html.gsub!(/`([^`]+)`/, '<code>\1</code>')
|
|
124
|
+
|
|
125
|
+
process_block_level_elements(html).then { |processed| wrap_paragraphs(processed) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
BLOCK_TAG_SET = %w[
|
|
129
|
+
div pre code section article aside header footer nav main figure blockquote
|
|
130
|
+
table thead tbody tfoot tr ul ol dl fieldset form style script
|
|
131
|
+
].freeze
|
|
132
|
+
|
|
133
|
+
WRAP_BLOCK_TAG_SET = (BLOCK_TAG_SET + %w[h1 h2 h3 h4 h5 h6 hr]).freeze
|
|
134
|
+
|
|
135
|
+
TAG_PATTERN = %r{<(/?)([a-zA-Z][a-zA-Z0-9]*)(?:\s[^>]*)?(/?)\s*>}i
|
|
136
|
+
|
|
137
|
+
def process_block_level_elements(html)
|
|
138
|
+
processed_lines = []
|
|
139
|
+
state = {
|
|
140
|
+
in_unordered_list: false,
|
|
141
|
+
in_ordered_list: false,
|
|
142
|
+
in_blockquote: false,
|
|
143
|
+
in_table: false
|
|
144
|
+
}
|
|
145
|
+
blockquote_content = []
|
|
146
|
+
table_rows = []
|
|
147
|
+
html_block_depth = 0
|
|
148
|
+
standalone_indented_block = []
|
|
149
|
+
|
|
150
|
+
flush_standalone = lambda do
|
|
151
|
+
next if standalone_indented_block.empty?
|
|
152
|
+
|
|
153
|
+
block_content = standalone_indented_block.join("\n")
|
|
154
|
+
processed_lines << %(<div class="indent">#{block_content}</div>)
|
|
155
|
+
standalone_indented_block.clear
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
close_blocks = lambda do
|
|
159
|
+
if state[:in_unordered_list]
|
|
160
|
+
processed_lines << "</ul>"
|
|
161
|
+
state[:in_unordered_list] = false
|
|
162
|
+
end
|
|
163
|
+
if state[:in_ordered_list]
|
|
164
|
+
processed_lines << "</ol>"
|
|
165
|
+
state[:in_ordered_list] = false
|
|
166
|
+
end
|
|
167
|
+
if state[:in_blockquote]
|
|
168
|
+
append_blockquote(processed_lines, blockquote_content)
|
|
169
|
+
state[:in_blockquote] = false
|
|
170
|
+
end
|
|
171
|
+
if state[:in_table]
|
|
172
|
+
append_table(processed_lines, table_rows)
|
|
173
|
+
state[:in_table] = false
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
close_other_lists = lambda do |keep|
|
|
178
|
+
if keep != :unordered && state[:in_unordered_list]
|
|
179
|
+
processed_lines << "</ul>"
|
|
180
|
+
state[:in_unordered_list] = false
|
|
181
|
+
end
|
|
182
|
+
if keep != :ordered && state[:in_ordered_list]
|
|
183
|
+
processed_lines << "</ol>"
|
|
184
|
+
state[:in_ordered_list] = false
|
|
185
|
+
end
|
|
186
|
+
if state[:in_blockquote]
|
|
187
|
+
append_blockquote(processed_lines, blockquote_content)
|
|
188
|
+
state[:in_blockquote] = false
|
|
189
|
+
end
|
|
190
|
+
if state[:in_table]
|
|
191
|
+
append_table(processed_lines, table_rows)
|
|
192
|
+
state[:in_table] = false
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
html.each_line(chomp: true) do |line|
|
|
197
|
+
trimmed = line.strip
|
|
198
|
+
|
|
199
|
+
depth_at_start = html_block_depth
|
|
200
|
+
depth_after = html_block_depth
|
|
201
|
+
line.scan(TAG_PATTERN) do |closing, tag_name, self_closing|
|
|
202
|
+
tag = tag_name.downcase
|
|
203
|
+
next unless BLOCK_TAG_SET.include?(tag)
|
|
204
|
+
|
|
205
|
+
if self_closing == "/"
|
|
206
|
+
next
|
|
207
|
+
elsif closing == "/"
|
|
208
|
+
depth_after = [0, depth_after - 1].max
|
|
209
|
+
else
|
|
210
|
+
depth_after += 1
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
html_block_depth = depth_after
|
|
214
|
+
in_protected_html = depth_at_start.positive? || trimmed.start_with?("<")
|
|
215
|
+
|
|
216
|
+
standalone_indented = !trimmed.empty? &&
|
|
217
|
+
!(state[:in_unordered_list] || state[:in_ordered_list]) &&
|
|
218
|
+
!in_protected_html &&
|
|
219
|
+
(line.start_with?(" ") || line.start_with?("\t"))
|
|
220
|
+
|
|
221
|
+
if standalone_indented
|
|
222
|
+
content = line.dup
|
|
223
|
+
content = content[1..] if content.start_with?("\t")
|
|
224
|
+
content = content[2..] if content.start_with?(" ")
|
|
225
|
+
standalone_indented_block << content.strip
|
|
226
|
+
next
|
|
227
|
+
else
|
|
228
|
+
flush_standalone.call
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if %w[--- *** ___].include?(trimmed)
|
|
232
|
+
close_blocks.call
|
|
233
|
+
processed_lines << "<hr>"
|
|
234
|
+
next
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if trimmed.start_with?("|") && trimmed.end_with?("|") && trimmed.length > 2
|
|
238
|
+
without_pipes = trimmed.delete("|").strip
|
|
239
|
+
is_separator = without_pipes.include?("-") &&
|
|
240
|
+
without_pipes.chars.all? { |c| ["-", ":", " "].include?(c) }
|
|
241
|
+
if is_separator
|
|
242
|
+
state[:in_table] = true if table_rows.any?
|
|
243
|
+
next
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
unless state[:in_table]
|
|
247
|
+
close_blocks.call
|
|
248
|
+
state[:in_table] = true
|
|
249
|
+
table_rows = []
|
|
250
|
+
end
|
|
251
|
+
table_rows << trimmed
|
|
252
|
+
next
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if trimmed.start_with?(">")
|
|
256
|
+
close_blocks.call
|
|
257
|
+
unless state[:in_blockquote]
|
|
258
|
+
state[:in_blockquote] = true
|
|
259
|
+
blockquote_content = []
|
|
260
|
+
end
|
|
261
|
+
blockquote_content << trimmed[1..].strip
|
|
262
|
+
next
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
if (m = line.match(/\A\s*[-*]\s+(.+)\z/))
|
|
266
|
+
close_other_lists.call(:unordered)
|
|
267
|
+
unless state[:in_unordered_list]
|
|
268
|
+
processed_lines << "<ul>"
|
|
269
|
+
state[:in_unordered_list] = true
|
|
270
|
+
end
|
|
271
|
+
processed_lines << "<li>#{m[1].strip}</li>"
|
|
272
|
+
next
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if (m = line.match(/\A\s*\d+\.\s+(.+)\z/))
|
|
276
|
+
close_other_lists.call(:ordered)
|
|
277
|
+
unless state[:in_ordered_list]
|
|
278
|
+
processed_lines << "<ol>"
|
|
279
|
+
state[:in_ordered_list] = true
|
|
280
|
+
end
|
|
281
|
+
processed_lines << "<li>#{m[1].strip}</li>"
|
|
282
|
+
next
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
if trimmed.empty?
|
|
286
|
+
if state[:in_table]
|
|
287
|
+
append_table(processed_lines, table_rows)
|
|
288
|
+
state[:in_table] = false
|
|
289
|
+
elsif state[:in_blockquote]
|
|
290
|
+
blockquote_content << ""
|
|
291
|
+
else
|
|
292
|
+
processed_lines << ""
|
|
293
|
+
end
|
|
294
|
+
next
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if (state[:in_unordered_list] || state[:in_ordered_list]) &&
|
|
298
|
+
(line.start_with?(" ") || line.start_with?("\t"))
|
|
299
|
+
last_index = processed_lines.rindex { |l| l.start_with?("<li>") }
|
|
300
|
+
if last_index
|
|
301
|
+
continuation = trimmed
|
|
302
|
+
last_item = processed_lines[last_index]
|
|
303
|
+
if last_item.end_with?("</li>")
|
|
304
|
+
item_content = last_item[0...-5]
|
|
305
|
+
processed_lines[last_index] = "#{item_content} #{continuation}</li>"
|
|
306
|
+
end
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
close_blocks.call
|
|
312
|
+
processed_lines << line
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
flush_standalone.call
|
|
316
|
+
close_blocks.call
|
|
317
|
+
processed_lines.join("\n")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def append_blockquote(processed_lines, blockquote_content)
|
|
321
|
+
blockquote_html = []
|
|
322
|
+
current_paragraph = []
|
|
323
|
+
|
|
324
|
+
blockquote_content.each do |content|
|
|
325
|
+
if content.empty?
|
|
326
|
+
unless current_paragraph.empty?
|
|
327
|
+
blockquote_html << "<p>#{current_paragraph.join(' ')}</p>"
|
|
328
|
+
current_paragraph.clear
|
|
329
|
+
end
|
|
330
|
+
else
|
|
331
|
+
current_paragraph << content
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
blockquote_html << "<p>#{current_paragraph.join(' ')}</p>" unless current_paragraph.empty?
|
|
336
|
+
blockquote_html << "<p></p>" if blockquote_html.empty?
|
|
337
|
+
processed_lines << "<blockquote>#{blockquote_html.join("\n")}</blockquote>"
|
|
338
|
+
blockquote_content.clear
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def append_table(processed_lines, table_rows)
|
|
342
|
+
return if table_rows.empty?
|
|
343
|
+
|
|
344
|
+
parsed_rows = table_rows.filter_map do |row|
|
|
345
|
+
cells = row.split("|").map(&:strip)
|
|
346
|
+
cells.shift if cells.first&.empty?
|
|
347
|
+
cells.pop if cells.last&.empty?
|
|
348
|
+
cells.empty? ? nil : cells
|
|
349
|
+
end
|
|
350
|
+
return if parsed_rows.empty?
|
|
351
|
+
|
|
352
|
+
table_html = ["<table>", "<thead>", "<tr>"]
|
|
353
|
+
parsed_rows.first.each { |cell| table_html << "<th>#{cell}</th>" }
|
|
354
|
+
table_html.concat(["</tr>", "</thead>"])
|
|
355
|
+
|
|
356
|
+
if parsed_rows.length > 1
|
|
357
|
+
table_html << "<tbody>"
|
|
358
|
+
parsed_rows[1..].each do |row|
|
|
359
|
+
table_html << "<tr>"
|
|
360
|
+
row.each { |cell| table_html << "<td>#{cell}</td>" }
|
|
361
|
+
table_html << "</tr>"
|
|
362
|
+
end
|
|
363
|
+
table_html << "</tbody>"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
table_html << "</table>"
|
|
367
|
+
processed_lines << table_html.join("\n")
|
|
368
|
+
table_rows.clear
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def wrap_paragraphs(html)
|
|
372
|
+
wrapped = []
|
|
373
|
+
current_paragraph = []
|
|
374
|
+
html_block_depth = 0
|
|
375
|
+
in_pre_block = false
|
|
376
|
+
|
|
377
|
+
html.each_line(chomp: true) do |line|
|
|
378
|
+
trimmed = in_pre_block ? line : line.strip
|
|
379
|
+
depth_at_start = html_block_depth
|
|
380
|
+
depth_after = html_block_depth
|
|
381
|
+
tag_matches = trimmed.scan(TAG_PATTERN)
|
|
382
|
+
|
|
383
|
+
tag_matches.each do |closing, tag_name, _self_closing|
|
|
384
|
+
tag = tag_name.downcase
|
|
385
|
+
if tag == "pre"
|
|
386
|
+
in_pre_block = closing != "/"
|
|
387
|
+
break
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
tag_matches.each do |closing, tag_name, self_closing|
|
|
392
|
+
tag = tag_name.downcase
|
|
393
|
+
next unless WRAP_BLOCK_TAG_SET.include?(tag)
|
|
394
|
+
|
|
395
|
+
if self_closing == "/"
|
|
396
|
+
next
|
|
397
|
+
elsif closing == "/"
|
|
398
|
+
depth_after = [0, depth_after - 1].max
|
|
399
|
+
else
|
|
400
|
+
depth_after += 1
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
html_block_depth = depth_after
|
|
404
|
+
|
|
405
|
+
is_inside_block = depth_at_start.positive? || depth_after.positive?
|
|
406
|
+
first_tag = tag_matches.first&.then { |m| m[1].downcase }
|
|
407
|
+
is_block_element = trimmed.start_with?("<") && first_tag && WRAP_BLOCK_TAG_SET.include?(first_tag)
|
|
408
|
+
|
|
409
|
+
if in_pre_block
|
|
410
|
+
unless current_paragraph.empty?
|
|
411
|
+
wrapped << current_paragraph.join(" ")
|
|
412
|
+
current_paragraph.clear
|
|
413
|
+
end
|
|
414
|
+
wrapped << line
|
|
415
|
+
elsif trimmed.empty?
|
|
416
|
+
unless current_paragraph.empty? && !is_inside_block
|
|
417
|
+
if !current_paragraph.empty? && !is_inside_block
|
|
418
|
+
wrapped << "<p>#{current_paragraph.join(' ')}</p>"
|
|
419
|
+
current_paragraph.clear
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
elsif is_block_element
|
|
423
|
+
unless current_paragraph.empty? && !is_inside_block
|
|
424
|
+
if !current_paragraph.empty? && !is_inside_block
|
|
425
|
+
wrapped << "<p>#{current_paragraph.join(' ')}</p>"
|
|
426
|
+
current_paragraph.clear
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
wrapped << trimmed
|
|
430
|
+
elsif trimmed.start_with?("<")
|
|
431
|
+
if is_inside_block
|
|
432
|
+
unless current_paragraph.empty?
|
|
433
|
+
wrapped << current_paragraph.join(" ")
|
|
434
|
+
current_paragraph.clear
|
|
435
|
+
end
|
|
436
|
+
wrapped << trimmed
|
|
437
|
+
else
|
|
438
|
+
current_paragraph << trimmed
|
|
439
|
+
end
|
|
440
|
+
elsif is_inside_block
|
|
441
|
+
unless current_paragraph.empty?
|
|
442
|
+
wrapped << current_paragraph.join(" ")
|
|
443
|
+
current_paragraph.clear
|
|
444
|
+
end
|
|
445
|
+
wrapped << trimmed
|
|
446
|
+
else
|
|
447
|
+
current_paragraph << trimmed
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
if !current_paragraph.empty? && html_block_depth.zero?
|
|
452
|
+
wrapped << "<p>#{current_paragraph.join(' ')}</p>"
|
|
453
|
+
elsif !current_paragraph.empty?
|
|
454
|
+
wrapped << current_paragraph.join(" ")
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
wrapped.join("\n")
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
require "cgi"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hammer
|
|
4
|
+
module Content
|
|
5
|
+
class RelationResolver
|
|
6
|
+
def initialize(collections:, config:)
|
|
7
|
+
@collections = collections
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def resolve_all!
|
|
12
|
+
@config.collections.each do |name, collection_config|
|
|
13
|
+
populate_relations(name, collection_config)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def populate_relations(collection_name, collection_config)
|
|
18
|
+
collection = @collections[collection_name]
|
|
19
|
+
return unless collection
|
|
20
|
+
|
|
21
|
+
fields = collection_config.fields || {}
|
|
22
|
+
collection.documents.each do |doc|
|
|
23
|
+
fields.each do |field_name, field|
|
|
24
|
+
next unless field.type == "relation"
|
|
25
|
+
next unless doc.metadata.key?(field_name)
|
|
26
|
+
|
|
27
|
+
raw = doc.metadata[field_name]
|
|
28
|
+
next unless raw.is_a?(String)
|
|
29
|
+
|
|
30
|
+
resolved = resolve_relation(raw, field, collection_name)
|
|
31
|
+
doc.metadata[field_name] = resolved if resolved
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve_relation(relation_value, field, _from_collection)
|
|
37
|
+
target_name = field.collection
|
|
38
|
+
return nil unless target_name
|
|
39
|
+
|
|
40
|
+
target = @collections[target_name]
|
|
41
|
+
return nil unless target
|
|
42
|
+
|
|
43
|
+
slug_value = relation_value.include?("/") ? relation_value.split("/").last : relation_value
|
|
44
|
+
value_field = field.value_field || "slug"
|
|
45
|
+
|
|
46
|
+
if value_field == "slug"
|
|
47
|
+
target.get_by_slug(slug_value)
|
|
48
|
+
else
|
|
49
|
+
target.documents.find { |d| d.metadata[value_field].to_s == slug_value }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hammer
|
|
4
|
+
module Content
|
|
5
|
+
class TemplateContext
|
|
6
|
+
attr_reader :site, :collections, :doc, :helpers, :variables
|
|
7
|
+
|
|
8
|
+
def initialize(site: {}, collections: {}, doc: nil, helpers: {}, variables: {})
|
|
9
|
+
@site = site
|
|
10
|
+
@collections = collections
|
|
11
|
+
@doc = doc
|
|
12
|
+
@helpers = helpers
|
|
13
|
+
@variables = variables
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def resolve(path)
|
|
17
|
+
parts = path.split(".")
|
|
18
|
+
root = parts.first
|
|
19
|
+
rest = parts[1..]
|
|
20
|
+
|
|
21
|
+
case root
|
|
22
|
+
when "doc" then resolve_doc(rest)
|
|
23
|
+
when "collections" then resolve_collections(rest)
|
|
24
|
+
when "site" then dig(@site, rest)
|
|
25
|
+
when "helpers" then rest.length == 1 ? @helpers[rest[0]] : nil
|
|
26
|
+
else
|
|
27
|
+
dig(@variables[root], rest[0..]) || nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve_doc(parts)
|
|
32
|
+
return @doc if parts.empty?
|
|
33
|
+
return nil unless @doc
|
|
34
|
+
|
|
35
|
+
if parts.length == 1
|
|
36
|
+
case parts[0]
|
|
37
|
+
when "body" then @doc.body
|
|
38
|
+
when "slug" then @doc.slug
|
|
39
|
+
when "date" then @doc.date
|
|
40
|
+
when "filePath" then @doc.file_path
|
|
41
|
+
when "collection" then @doc.collection
|
|
42
|
+
else @doc.metadata[parts[0]]
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
val = @doc.metadata[parts[0]]
|
|
46
|
+
dig(val, parts[1..])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def resolve_collections(parts)
|
|
51
|
+
return @collections if parts.empty?
|
|
52
|
+
|
|
53
|
+
coll = @collections[parts[0]]
|
|
54
|
+
return coll if parts.length == 1
|
|
55
|
+
|
|
56
|
+
coll
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dig(value, parts)
|
|
60
|
+
return value if parts.nil? || parts.empty?
|
|
61
|
+
|
|
62
|
+
current = value
|
|
63
|
+
parts.each do |part|
|
|
64
|
+
case current
|
|
65
|
+
when Document
|
|
66
|
+
current = resolve_document_property(current, part)
|
|
67
|
+
when Hash
|
|
68
|
+
current = current[part] || current[part.to_sym]
|
|
69
|
+
when Array
|
|
70
|
+
idx = Integer(part, exception: false)
|
|
71
|
+
current = idx ? current[idx] : nil
|
|
72
|
+
else
|
|
73
|
+
return nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
current
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_document_property(document, part)
|
|
80
|
+
case part
|
|
81
|
+
when "body" then document.body
|
|
82
|
+
when "slug" then document.slug
|
|
83
|
+
when "date" then document.date
|
|
84
|
+
when "filePath" then document.file_path
|
|
85
|
+
when "collection" then document.collection
|
|
86
|
+
else document.metadata[part]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|