markdown_record 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +63 -43
- data/app/helpers/markdown_record/application_helper.rb +23 -0
- data/app/helpers/markdown_record/controller_helpers.rb +1 -1
- data/app/helpers/markdown_record/view_helpers.rb +9 -3
- data/{lib → app/models}/markdown_record/base.rb +10 -0
- data/{lib → app/models}/markdown_record/content_fragment.rb +17 -2
- data/lib/generators/markdown_record_generator.rb +6 -0
- data/lib/markdown_record/configuration.rb +4 -0
- data/lib/markdown_record/file_sorting/base.rb +25 -0
- data/lib/markdown_record/file_sorting/date_sorter.rb +14 -0
- data/lib/markdown_record/file_sorting/sem_ver_sorter.rb +47 -0
- data/lib/markdown_record/{association.rb → models/association.rb} +20 -3
- data/lib/markdown_record/{associations.rb → models/associations.rb} +17 -9
- data/lib/markdown_record/{content_associations.rb → models/content_associations.rb} +4 -2
- data/lib/markdown_record/models/filtering.rb +13 -0
- data/lib/markdown_record/models/filters/array_filter.rb +11 -0
- data/lib/markdown_record/models/filters/base_filter.rb +35 -0
- data/lib/markdown_record/models/filters/hash_filter.rb +81 -0
- data/lib/markdown_record/models/filters/nil_filter.rb +11 -0
- data/lib/markdown_record/models/filters/range_filter.rb +11 -0
- data/lib/markdown_record/models/filters/regexp_filter.rb +11 -0
- data/lib/markdown_record/models/filters/symbol_filter.rb +14 -0
- data/lib/markdown_record/models/filters.rb +22 -0
- data/lib/markdown_record/models/model_inflator.rb +65 -0
- data/lib/markdown_record/path_utilities.rb +20 -11
- data/lib/markdown_record/rendering/content_dsl/attribute.rb +22 -0
- data/lib/markdown_record/{content_dsl → rendering/content_dsl}/directory_fragment.rb +3 -3
- data/lib/markdown_record/{content_dsl → rendering/content_dsl}/disable.rb +2 -2
- data/lib/markdown_record/{content_dsl → rendering/content_dsl}/enable.rb +2 -2
- data/lib/markdown_record/{content_dsl → rendering/content_dsl}/end_attribute.rb +3 -3
- data/lib/markdown_record/{content_dsl → rendering/content_dsl}/end_model.rb +3 -3
- data/lib/markdown_record/{content_dsl → rendering/content_dsl}/fragment.rb +3 -3
- data/lib/markdown_record/rendering/content_dsl/model.rb +22 -0
- data/lib/markdown_record/rendering/content_dsl/scope.rb +22 -0
- data/lib/markdown_record/{content_dsl → rendering/content_dsl}/use_layout.rb +3 -3
- data/lib/markdown_record/rendering/content_dsl.rb +45 -0
- data/lib/markdown_record/rendering/html_renderer.rb +22 -0
- data/lib/markdown_record/{indexer.rb → rendering/indexer.rb} +0 -10
- data/lib/markdown_record/rendering/json_renderer.rb +20 -0
- data/lib/markdown_record/rendering/nodes/html_base.rb +95 -0
- data/lib/markdown_record/rendering/nodes/html_directory.rb +51 -0
- data/lib/markdown_record/rendering/nodes/html_file.rb +48 -0
- data/lib/markdown_record/rendering/nodes/json_base.rb +55 -0
- data/lib/markdown_record/rendering/nodes/json_directory.rb +69 -0
- data/lib/markdown_record/rendering/nodes/json_file.rb +172 -0
- data/lib/markdown_record/{rendering.rb → rendering/rendering.rb} +1 -1
- data/lib/markdown_record/{validator.rb → rendering/validator.rb} +4 -4
- data/lib/markdown_record/version.rb +1 -1
- data/lib/markdown_record.rb +21 -14
- data/templates/demo/content/10_custom_models_and_associations.md.erb +27 -7
- data/templates/demo/content/11_controller_helpers.md.erb +2 -2
- data/templates/demo/content/12_configuration.md.erb +22 -3
- data/templates/demo/content/13_sandbox/1_foo.md +2 -1
- data/templates/demo/content/13_sandbox/2_sandbox_nested/1_bar.md +4 -1
- data/templates/demo/content/1_home.md.erb +30 -13
- data/templates/demo/content/2_installation.md.erb +61 -46
- data/templates/demo/content/3_rendering_basics.md.erb +3 -3
- data/templates/demo/content/4_content_dsl.md.erb +22 -11
- data/templates/demo/content/5_routes.md.erb +10 -7
- data/templates/demo/content/6_model_basics.md.erb +11 -11
- data/templates/demo/content/7_content_frags.md.erb +9 -6
- data/templates/demo/content/8_erb_syntax_and_view_helpers.md.erb +32 -11
- data/templates/demo/content/9_layouts.md.erb +3 -3
- data/templates/demo/layouts/_global_layout.html.erb +1 -0
- data/templates/demo/models/dsl_command.rb +6 -0
- data/templates/demo/models/section.rb +5 -0
- data/templates/render_content.thor +2 -1
- data/templates/tests/assets/images/ruby-logo.png +0 -0
- data/templates/tests/content/1_test_files_home.md.erb +31 -0
- data/templates/tests/content/2_content_dsl_tests/1_content_dsl.md.erb +162 -0
- data/templates/tests/content/2_content_dsl_tests/2_nested_directory/1_associations.md.erb +83 -0
- data/templates/tests/layouts/_concatenated_layout.html.erb +12 -0
- data/templates/tests/layouts/_custom_layout.html.erb +14 -0
- data/templates/tests/layouts/_file_layout.html.erb +15 -0
- data/templates/tests/layouts/_global_layout.html.erb +116 -0
- metadata +53 -34
- data/app/models/markdown_record/demo/dsl_command.rb +0 -10
- data/app/models/markdown_record/demo/section.rb +0 -9
- data/app/models/markdown_record/tests/child_model.rb +0 -15
- data/app/models/markdown_record/tests/fake_active_record_model.rb +0 -13
- data/app/models/markdown_record/tests/model.rb +0 -15
- data/app/models/markdown_record/tests/other_child_model.rb +0 -15
- data/lib/markdown_record/cli.rb +0 -54
- data/lib/markdown_record/content_dsl/attribute.rb +0 -22
- data/lib/markdown_record/content_dsl/model.rb +0 -23
- data/lib/markdown_record/content_dsl/render_format.rb +0 -22
- data/lib/markdown_record/content_dsl/render_strategy.rb +0 -22
- data/lib/markdown_record/content_dsl.rb +0 -37
- data/lib/markdown_record/html_renderer.rb +0 -194
- data/lib/markdown_record/json_renderer.rb +0 -270
- data/lib/markdown_record/model_inflator.rb +0 -107
- data/lib/markdown_record/routes_renderer.rb +0 -0
- /data/lib/markdown_record/{file_saver.rb → rendering/file_saver.rb} +0 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
module MarkdownRecord
|
2
|
+
module Rendering
|
3
|
+
module Nodes
|
4
|
+
class HtmlBase
|
5
|
+
include ::MarkdownRecord::PathUtilities
|
6
|
+
include ::MarkdownRecord::ContentDsl
|
7
|
+
|
8
|
+
attr_reader :name
|
9
|
+
attr_reader :rendered_html
|
10
|
+
attr_reader :processed_html
|
11
|
+
|
12
|
+
HTML_SUBSTITUTIONS = {
|
13
|
+
/<!---/ => "<!--",
|
14
|
+
/<!---/ => "<!--"
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize(pathname, markdown, options)
|
18
|
+
@markdown = markdown
|
19
|
+
@pathname = pathname
|
20
|
+
@options = options
|
21
|
+
@name = @pathname.relative_path_from(MarkdownRecord.config.content_root.parent).to_s
|
22
|
+
@sorter = MarkdownRecord.config.filename_sorter
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(file_saver)
|
26
|
+
@rendered_html = ""
|
27
|
+
process_html
|
28
|
+
finalize_html
|
29
|
+
save(file_saver)
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_html
|
33
|
+
@processed_html = render_erbs(@rendered_html)
|
34
|
+
end
|
35
|
+
|
36
|
+
def finalize_html
|
37
|
+
return unless @processed_html.present?
|
38
|
+
|
39
|
+
locals = erb_locals_from_path(@name)
|
40
|
+
final_html = remove_html_dsl_command(@processed_html)
|
41
|
+
final_html = render_erb(global_layout, locals.merge(html: final_html)) if global_layout
|
42
|
+
|
43
|
+
HTML_SUBSTITUTIONS.each do |find, replace|
|
44
|
+
final_html = final_html.gsub(find, replace)
|
45
|
+
end
|
46
|
+
|
47
|
+
final_html = final_html.squeeze("\n")
|
48
|
+
@final_html = HtmlBeautifier.beautify(final_html)
|
49
|
+
end
|
50
|
+
|
51
|
+
def save(file_saver)
|
52
|
+
return unless @final_html.present?
|
53
|
+
|
54
|
+
path = clean_path(@name)
|
55
|
+
file_saver.save_to_file(@final_html, "#{path}.html", @options)
|
56
|
+
end
|
57
|
+
|
58
|
+
def render_erbs(html)
|
59
|
+
processed_html = html.gsub(/<p>(\<%(\S|\s)*?%\>)<\/p>/){ CGI.unescapeHTML($1) }
|
60
|
+
processed_html = processed_html.gsub(/(\<%(\S|\s)*?%\>)/){ CGI.unescapeHTML($1) }
|
61
|
+
locals = erb_locals_from_path(name)
|
62
|
+
processed_html = render_erb(processed_html, locals) if name.ends_with?(".md.erb")
|
63
|
+
processed_html = render_erb(layout, locals.merge(html: processed_html)) if layout
|
64
|
+
processed_html
|
65
|
+
end
|
66
|
+
|
67
|
+
def layout
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def render_erb(html, locals)
|
72
|
+
render_controller = ::MarkdownRecord.config.render_controller || ::ApplicationController
|
73
|
+
rendered_erb = render_controller.render(
|
74
|
+
inline: html,
|
75
|
+
locals: locals
|
76
|
+
).to_str
|
77
|
+
rendered_erb
|
78
|
+
end
|
79
|
+
|
80
|
+
def global_layout
|
81
|
+
global_layout_path = ::MarkdownRecord.config.global_layout_path
|
82
|
+
@global_layout ||= global_layout_path ? load_layout(global_layout_path) : nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def load_layout(path)
|
86
|
+
File.read(::MarkdownRecord.config.layout_directory.join(path))
|
87
|
+
end
|
88
|
+
|
89
|
+
def sort_value
|
90
|
+
@sorter.path_to_sort_value(@name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module MarkdownRecord
|
2
|
+
module Rendering
|
3
|
+
module Nodes
|
4
|
+
class HtmlDirectory < MarkdownRecord::Rendering::Nodes::HtmlBase
|
5
|
+
|
6
|
+
def render(file_saver)
|
7
|
+
children.each do |child|
|
8
|
+
child.render(file_saver)
|
9
|
+
end
|
10
|
+
|
11
|
+
concatenate_html
|
12
|
+
process_html
|
13
|
+
finalize_html
|
14
|
+
|
15
|
+
if @options[:concat]
|
16
|
+
save(file_saver)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def concatenate_html
|
21
|
+
return @rendered_html if @rendered_html.present?
|
22
|
+
|
23
|
+
sorted_children = children.sort_by(&:sort_value)
|
24
|
+
@rendered_html = sorted_children.map(&:concatenated_html).compact.join("\r\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
def concatenated_html
|
28
|
+
@rendered_html
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def children
|
34
|
+
@children ||= Dir.new(@pathname).children.map do |child|
|
35
|
+
pathname = @pathname.join(child)
|
36
|
+
if pathname.directory?
|
37
|
+
self.class.new(pathname, @markdown, @options)
|
38
|
+
else
|
39
|
+
MarkdownRecord::Rendering::Nodes::HtmlFile.new(pathname, @markdown, @options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def layout
|
45
|
+
concatenated_layout_path = ::MarkdownRecord.config.concatenated_layout_path
|
46
|
+
concatenated_layout_path ? load_layout(concatenated_layout_path) : nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module MarkdownRecord
|
2
|
+
module Rendering
|
3
|
+
module Nodes
|
4
|
+
class HtmlFile < MarkdownRecord::Rendering::Nodes::HtmlBase
|
5
|
+
|
6
|
+
def render(file_saver)
|
7
|
+
render_html
|
8
|
+
process_html
|
9
|
+
finalize_html
|
10
|
+
|
11
|
+
if @options[:deep]
|
12
|
+
save(file_saver)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def render_html
|
17
|
+
html = @markdown.render(raw_content)
|
18
|
+
html = remove_json_dsl_commands(html)
|
19
|
+
@rendered_html = html
|
20
|
+
end
|
21
|
+
|
22
|
+
def raw_content
|
23
|
+
@raw_content ||= File.read(@pathname)
|
24
|
+
end
|
25
|
+
|
26
|
+
def concatenated_html
|
27
|
+
@processed_html
|
28
|
+
end
|
29
|
+
|
30
|
+
def layout
|
31
|
+
@layout ||= custom_layout(@rendered_html) || file_layout
|
32
|
+
end
|
33
|
+
|
34
|
+
def custom_layout(html)
|
35
|
+
cust_layout = use_layout_dsl(html)
|
36
|
+
return nil unless cust_layout
|
37
|
+
|
38
|
+
load_layout(cust_layout)
|
39
|
+
end
|
40
|
+
|
41
|
+
def file_layout
|
42
|
+
file_layout_path = ::MarkdownRecord.config.file_layout_path
|
43
|
+
@file_layout ||= file_layout_path ? load_layout(file_layout_path) : nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module MarkdownRecord
|
2
|
+
module Rendering
|
3
|
+
module Nodes
|
4
|
+
class JsonBase
|
5
|
+
include ::MarkdownRecord::PathUtilities
|
6
|
+
include ::MarkdownRecord::ContentDsl
|
7
|
+
|
8
|
+
attr_reader :name
|
9
|
+
attr_reader :json_models
|
10
|
+
attr_reader :concatenated
|
11
|
+
|
12
|
+
def initialize(pathname, options)
|
13
|
+
@pathname = pathname
|
14
|
+
@options = options
|
15
|
+
@name = @pathname.relative_path_from(MarkdownRecord.config.content_root.parent).to_s
|
16
|
+
@json_models = {}
|
17
|
+
@concatenated = nil
|
18
|
+
@scope = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def render(file_saver, inherited_scope = nil)
|
22
|
+
# This method defines an interface and should be overridden by a subclass.
|
23
|
+
add_content_fragment
|
24
|
+
save(file_saver)
|
25
|
+
end
|
26
|
+
|
27
|
+
def fragment_meta
|
28
|
+
# This method defines an interface and should be overridden by a subclass.
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
|
32
|
+
def scope
|
33
|
+
@scope
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_content_fragment
|
37
|
+
return unless @options[:render_content_fragment_json]
|
38
|
+
|
39
|
+
content_fragment_hash = fragment_attributes_from_path(name).merge("meta" => fragment_meta, "concatenated" => concatenated, "__scope__" => scope)
|
40
|
+
|
41
|
+
@json_models["markdown_record/content_fragment"] ||= []
|
42
|
+
@json_models["markdown_record/content_fragment"] << content_fragment_hash
|
43
|
+
end
|
44
|
+
|
45
|
+
def save(file_saver)
|
46
|
+
fragments = @json_models.slice("markdown_record/content_fragment")
|
47
|
+
non_fragments = @json_models.except("markdown_record/content_fragment")
|
48
|
+
path = clean_path(@name)
|
49
|
+
file_saver.save_to_file(non_fragments.to_json, "#{path}.json", @options)
|
50
|
+
file_saver.save_to_file(fragments.to_json, "#{path}.json", @options, true)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module MarkdownRecord
|
2
|
+
module Rendering
|
3
|
+
module Nodes
|
4
|
+
class JsonDirectory < MarkdownRecord::Rendering::Nodes::JsonBase
|
5
|
+
|
6
|
+
def initialize(pathname, options, top_level = false)
|
7
|
+
super(pathname, options)
|
8
|
+
@concatenated = true
|
9
|
+
@top_level = top_level
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(file_saver, inherited_scope = nil)
|
13
|
+
# Only override scope if this directory
|
14
|
+
# doesn't have its own scope defined
|
15
|
+
# First call to scope will search files
|
16
|
+
# and get the scope if defined.
|
17
|
+
@scope = inherited_scope unless scope
|
18
|
+
|
19
|
+
children.each do |child|
|
20
|
+
child.render(file_saver, scope)
|
21
|
+
end
|
22
|
+
|
23
|
+
concatenate_json
|
24
|
+
save(file_saver) if @options[:concat] || (@options[:concat_top_level] && @top_level)
|
25
|
+
end
|
26
|
+
|
27
|
+
def fragment_meta
|
28
|
+
@fragment_meta ||= children.select {|c| c.respond_to?(:directory_meta) }.reduce({}) { |meta, child| meta.merge(child.directory_meta) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def scope
|
32
|
+
@scope ||= children.select {|c| c.respond_to?(:directory_scope) }.find(&:directory_scope)&.directory_scope
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def concatenate_json
|
38
|
+
return unless @options[:concat] || (@options[:concat_top_level] && @top_level)
|
39
|
+
|
40
|
+
children.each do |child|
|
41
|
+
@json_models.merge!(child.json_models) do |key, oldval, newval|
|
42
|
+
oldval + newval
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# IMPORTANT: Unattach from children's json_models
|
47
|
+
# This node's json_models may share references
|
48
|
+
# after the merge, so we need to detach them
|
49
|
+
@json_models.keys.each do |k|
|
50
|
+
@json_models[k] = @json_models[k].dup
|
51
|
+
end
|
52
|
+
|
53
|
+
add_content_fragment
|
54
|
+
end
|
55
|
+
|
56
|
+
def children
|
57
|
+
@children ||= Dir.new(@pathname).children.map do |child|
|
58
|
+
pathname = @pathname.join(child)
|
59
|
+
if pathname.directory?
|
60
|
+
self.class.new(pathname, @options)
|
61
|
+
else
|
62
|
+
MarkdownRecord::Rendering::Nodes::JsonFile.new(pathname, @options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module MarkdownRecord
|
2
|
+
module Rendering
|
3
|
+
module Nodes
|
4
|
+
class JsonFile < MarkdownRecord::Rendering::Nodes::JsonBase
|
5
|
+
|
6
|
+
def initialize(...)
|
7
|
+
super(...)
|
8
|
+
@concatenated = false
|
9
|
+
end
|
10
|
+
|
11
|
+
def render(file_saver, inherited_scope = nil)
|
12
|
+
@scope ||= inherited_scope
|
13
|
+
render_json
|
14
|
+
apply_scope
|
15
|
+
save(file_saver) if @options[:deep]
|
16
|
+
end
|
17
|
+
|
18
|
+
def fragment_meta
|
19
|
+
@fragment_meta ||= fragment_dsl(raw_content)
|
20
|
+
@fragment_meta ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def scope
|
24
|
+
@scope ||= scope_dsl(raw_content)
|
25
|
+
end
|
26
|
+
|
27
|
+
def directory_scope
|
28
|
+
@directory_scope ||= scope
|
29
|
+
end
|
30
|
+
|
31
|
+
def directory_meta
|
32
|
+
@directory_meta ||= directory_fragment_dsl(raw_content)
|
33
|
+
@directory_meta ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def apply_scope
|
39
|
+
@json_models.each do |klass, array|
|
40
|
+
next if klass == MarkdownRecord::ContentFragment.name.underscore
|
41
|
+
array.each do |model|
|
42
|
+
model["scope"] = scope
|
43
|
+
model["scoped_id"] = to_scoped_id(scope, model["id"])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def raw_content
|
49
|
+
@raw_content ||= File.read(@pathname)
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_json
|
53
|
+
return unless @options[:deep] || @options[:concat_top_level]
|
54
|
+
|
55
|
+
filename, subdirectory = full_path_to_parts(name)
|
56
|
+
|
57
|
+
described_models = []
|
58
|
+
enabled = true
|
59
|
+
|
60
|
+
raw_content.split(HTML_COMMENT_REGEX).each do |text|
|
61
|
+
dsl_command = HTML_COMMENT_REGEX =~ text
|
62
|
+
|
63
|
+
if dsl_command
|
64
|
+
enabled = false if disable_dsl(text)
|
65
|
+
enabled = true if enable_dsl(text)
|
66
|
+
end
|
67
|
+
|
68
|
+
if dsl_command && enabled
|
69
|
+
extract_model(text, described_models)
|
70
|
+
extract_attribute(text, described_models)
|
71
|
+
pop_attribute(text,described_models)
|
72
|
+
pop_model(text, described_models)
|
73
|
+
else
|
74
|
+
append_to_attribute(text, described_models)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
described_models.each do |model|
|
79
|
+
finalize_attribute(model)
|
80
|
+
end
|
81
|
+
|
82
|
+
add_content_fragment if @options[:deep]
|
83
|
+
end
|
84
|
+
|
85
|
+
def extract_model(text, described_models)
|
86
|
+
model = model_dsl(text)
|
87
|
+
|
88
|
+
if model
|
89
|
+
return unless model["type"].present?
|
90
|
+
return unless model["id"].present?
|
91
|
+
|
92
|
+
filename, subdirectory = full_path_to_parts(name)
|
93
|
+
|
94
|
+
model["subdirectory"] = subdirectory
|
95
|
+
model["filename"] = filename
|
96
|
+
|
97
|
+
# reset "type" to not have a prefix, in order to ensure
|
98
|
+
# consistent results between the internal only :klass filter
|
99
|
+
# which is used as an index in a hash (and has the prefix removed
|
100
|
+
# for consistency and deterministic behavior)
|
101
|
+
# and the externally filterable :type field.
|
102
|
+
model["type"] = model["type"].delete_prefix("::").strip
|
103
|
+
|
104
|
+
@json_models[model["type"]] ||= []
|
105
|
+
@json_models[model["type"]] << model
|
106
|
+
|
107
|
+
described_models.push(model)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def extract_attribute(text, described_models)
|
112
|
+
attribute, type = attribute_dsl(text)
|
113
|
+
type ||= "md"
|
114
|
+
|
115
|
+
if described_models.last && attribute
|
116
|
+
model = described_models.last
|
117
|
+
model[:described_attribute_type] = type
|
118
|
+
model[:described_attribute] = attribute
|
119
|
+
model[model[:described_attribute]] = []
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def append_to_attribute(text, described_models)
|
124
|
+
return if text.match(HTML_COMMENT_REGEX)
|
125
|
+
|
126
|
+
described_models.each do |model|
|
127
|
+
next unless model[:described_attribute].present?
|
128
|
+
|
129
|
+
model[model[:described_attribute]] ||= []
|
130
|
+
model[model[:described_attribute]] << text
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def pop_attribute(text, described_models)
|
135
|
+
if described_models.last && end_attribute_dsl(text)
|
136
|
+
finalize_attribute(described_models.last)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def pop_model(text, described_models)
|
141
|
+
if end_model_dsl(text)
|
142
|
+
described_models.pop
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def finalize_attribute(model)
|
147
|
+
return if model[:described_attribute].nil?
|
148
|
+
|
149
|
+
attribute_name = model.delete(:described_attribute)
|
150
|
+
attribute = model[attribute_name].join("\n")
|
151
|
+
type = model.delete(:described_attribute_type)
|
152
|
+
|
153
|
+
case type
|
154
|
+
when "html"
|
155
|
+
renderer = Redcarpet::Render::HTML.new
|
156
|
+
attribute = Redcarpet::Markdown.new(renderer).render(attribute)
|
157
|
+
when "md"
|
158
|
+
attribute = attribute.strip
|
159
|
+
when "int"
|
160
|
+
attribute = attribute.strip.gsub("\n", "").to_i
|
161
|
+
when "float"
|
162
|
+
attribute = attribute.strip.gsub("\n", "").to_f
|
163
|
+
when "string"
|
164
|
+
attribute = Redcarpet::Markdown.new(::Redcarpet::Render::StripDown).render(attribute).strip
|
165
|
+
end
|
166
|
+
|
167
|
+
model[attribute_name] = attribute
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -19,7 +19,7 @@ module MarkdownRecord
|
|
19
19
|
|
20
20
|
def validate_filenames(val)
|
21
21
|
if val.is_a?(Hash)
|
22
|
-
temp_keys = val.keys.map { |v|
|
22
|
+
temp_keys = val.keys.map { |v| remove_prefix(v) }
|
23
23
|
|
24
24
|
dups = temp_keys.group_by{|e| clean_path(e)}.keep_if{|_, e| e.length > 1}
|
25
25
|
if dups.any?
|
@@ -33,8 +33,8 @@ module MarkdownRecord
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def validate_models
|
36
|
-
@json
|
37
|
-
ids = array.map { |o| o["id"] }
|
36
|
+
@json.each do |klass, array|
|
37
|
+
ids = array.map { |o| [o["id"], o["scope"]] }
|
38
38
|
dups = ids.group_by{|e| e}.keep_if{|_, e| e.length > 1}
|
39
39
|
if dups.any?
|
40
40
|
raise ::MarkdownRecord::Errors::DuplicateIdError.new(klass, dups.keys.first)
|
@@ -43,7 +43,7 @@ module MarkdownRecord
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def validate_fragments
|
46
|
-
frags = @json["
|
46
|
+
frags = @json["markdown_record/content_fragment"]
|
47
47
|
frags.each do |frag|
|
48
48
|
parent_id = frag.dig("meta", "parent_id")
|
49
49
|
if parent_id
|
data/lib/markdown_record.rb
CHANGED
@@ -2,20 +2,27 @@ require "markdown_record/version"
|
|
2
2
|
require "markdown_record/engine"
|
3
3
|
require "markdown_record/configuration"
|
4
4
|
require "markdown_record/path_utilities"
|
5
|
-
require "markdown_record/
|
6
|
-
require "markdown_record/
|
7
|
-
require "markdown_record/
|
8
|
-
require "markdown_record/
|
9
|
-
require "markdown_record/
|
10
|
-
require "markdown_record/
|
11
|
-
require "markdown_record/
|
12
|
-
require "markdown_record/
|
13
|
-
require "markdown_record/
|
14
|
-
require "markdown_record/
|
15
|
-
require "markdown_record/
|
16
|
-
require "markdown_record/
|
17
|
-
require "markdown_record/
|
18
|
-
require "markdown_record/
|
5
|
+
require "markdown_record/file_sorting/base"
|
6
|
+
require "markdown_record/file_sorting/date_sorter"
|
7
|
+
require "markdown_record/file_sorting/sem_ver_sorter"
|
8
|
+
require "markdown_record/rendering/validator"
|
9
|
+
require "markdown_record/rendering/rendering"
|
10
|
+
require "markdown_record/rendering/content_dsl"
|
11
|
+
require "markdown_record/rendering/indexer"
|
12
|
+
require "markdown_record/rendering/file_saver"
|
13
|
+
require "markdown_record/rendering/json_renderer"
|
14
|
+
require "markdown_record/rendering/html_renderer"
|
15
|
+
require "markdown_record/rendering/nodes/json_base"
|
16
|
+
require "markdown_record/rendering/nodes/json_file"
|
17
|
+
require "markdown_record/rendering/nodes/json_directory"
|
18
|
+
require "markdown_record/rendering/nodes/html_base"
|
19
|
+
require "markdown_record/rendering/nodes/html_file"
|
20
|
+
require "markdown_record/rendering/nodes/html_directory"
|
21
|
+
require "markdown_record/models/filtering"
|
22
|
+
require "markdown_record/models/model_inflator"
|
23
|
+
require "markdown_record/models/association"
|
24
|
+
require "markdown_record/models/associations"
|
25
|
+
require "markdown_record/models/content_associations"
|
19
26
|
|
20
27
|
module MarkdownRecord
|
21
28
|
def self.config
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<!--fragment { "author": "Bryant Morrill", "
|
2
|
-
|
1
|
+
<!--fragment { "author": "Bryant Morrill", "relative_parent_id": "home", "name": "Custom Models and Associations" } -->
|
2
|
+
<!--model { "type": "section", "id": 9, "name": "Custom Models and Associations" } -->
|
3
3
|
|
4
4
|
# Custom Models and Associations
|
5
5
|
|
@@ -43,24 +43,44 @@ end
|
|
43
43
|
Once the associations are defined, you will want to make sure your JSON objects in your markdown have the required fields. For the example above, the DslCommand model will automatically be given a `section_id` field that will need to be populated in your markdown like so:
|
44
44
|
|
45
45
|
```html
|
46
|
-
<!---model { "type": "
|
46
|
+
<!---model { "type": "dsl_command", "id": 6, "name": "directory_fragment", "section_id": 1 } -->
|
47
47
|
```
|
48
48
|
|
49
49
|
Then you can use the association in your code just like ActiveRecord associations:
|
50
50
|
|
51
51
|
```ruby
|
52
|
-
|
52
|
+
Section.find(3).dsl_commands.all
|
53
53
|
=>
|
54
|
-
[#<
|
54
|
+
[#<DslCommand section_id: 1, description: "...", ...>, ...]
|
55
55
|
```
|
56
56
|
|
57
57
|
```ruby
|
58
|
-
|
59
|
-
=> #<
|
58
|
+
DslCommand.find(1).section
|
59
|
+
=> #<Section filename: "content_dsl", id: 3, name: "Content DSL", subdirectory: "content", type: "section">
|
60
60
|
```
|
61
61
|
|
62
62
|
`has_many_content` will define a method that returns an `MarkdownRecord::Association` object, which you can then chain queries onto. `belongs_to_content` and `has_one_content` will define methods that returns a single MarkdownContent model.
|
63
63
|
|
64
|
+
### Scopes
|
65
|
+
|
66
|
+
If a model is defined within a scope (using the `scope` Content DSL command), then all its associations are assumed to be within that scope, and must point to other models that are within that scope.
|
67
|
+
|
68
|
+
Furthermore, when querying for models that are defined within a scope without using associations, you will need to provide the scope like so:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
DslCommand.find(1, "v_0_1_3")
|
72
|
+
=> #<DslCommand description: "model is the main command...", scope: "v_0_1_3", scoped_id: "v_0_1_3:s:1", section_id: 3, subdirectory: "content", type: "dsl_command">
|
73
|
+
```
|
74
|
+
|
75
|
+
Or
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
DslCommand.where(:section_id => 3, :scope => "v_0_1_3").all
|
79
|
+
=> [#<DslCommand >,...]
|
80
|
+
````
|
81
|
+
|
82
|
+
Association methods do not need the `scope` provided, since they are assumed to be in the same scope as the model instance you call them on.
|
83
|
+
|
64
84
|
### Associations on ActiveRecord Models
|
65
85
|
|
66
86
|
You can easily integrate MarkdownRecord and ActiveRecord models together using the `MarkdownRecord::ContentAssociations` module. Just include it in your ActiveRecord model like so:
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<!--fragment { "author": "Bryant Morrill", "
|
2
|
-
|
1
|
+
<!--fragment { "author": "Bryant Morrill", "relative_parent_id": "home", "name": "Controller Helpers" } -->
|
2
|
+
<!--model { "type": "section", "id": 10, "name": "Controller Helpers" } -->
|
3
3
|
|
4
4
|
# Controller Helpers
|
5
5
|
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<!--fragment { "author": "Bryant Morrill", "
|
2
|
-
|
1
|
+
<!--fragment { "author": "Bryant Morrill", "relative_parent_id": "home", "name": "Configuration" } -->
|
2
|
+
<!--model { "type": "section", "id": 11, "name": "Configuration" } -->
|
3
3
|
|
4
4
|
# Configuration
|
5
5
|
|
@@ -23,4 +23,23 @@ You can configure `MarkdownRecord` in the initializer that is generated upon ins
|
|
23
23
|
- `mount_path`: the path where the MarkdownRecord engine is mounted inside the host application. It is `mdr` by default.
|
24
24
|
- `render_content_fragment_json`: a boolean value that determines whether MarkdownRecord renders content fragments and saves them to the `rendered_content_root` in JSON files with the `_fragments` suffix. If this is set to `false`, then MarkdownRecord will render your markdown content each time MarkdownRecord models queries are executed. This is set to `true` by default.
|
25
25
|
- `render_controller`: the controller used to render ERB content during the rendering process. If this isn't set, then the host application's `ApplicationController` will be used. By default this is `nil`.
|
26
|
-
- `ignore_numeric_prefix`: configures whether or not MarkdownRecord ignores the numeric prefixes of filenames. The default value is `true`. When it is set to `false`, the numeric prefixes will be included in the `filename` attribute of models, the `id` attribute of content fragments, and the `content_path` parameter of requests to the routes for rendered content.
|
26
|
+
- `ignore_numeric_prefix`: configures whether or not MarkdownRecord ignores the numeric prefixes of filenames. The default value is `true`. When it is set to `false`, the numeric prefixes will be included in the `filename` attribute of models, the `id` attribute of content fragments, and the `content_path` parameter of requests to the routes for rendered content.
|
27
|
+
- `filename_sorter`: the file sorting instance to be used to organized concatenated content. The default value is `MarkdownRecord::FileSorting::Base.new`
|
28
|
+
|
29
|
+
## File sorting
|
30
|
+
|
31
|
+
MarkdownRecord uses an instance of `MarkdownRecord::FileSorting::Base` by default to sort files in order to determine concatenation order of the markdown content it renders. This class is also responsible for trimming the numeric prefix prefixes off filenames. However, two additional classes are available for this purpose to allow the use of different kinds of prefixes and sort orders. They are:
|
32
|
+
|
33
|
+
- `MarkdownRecord::FileSorting::DateSorter`: this class sorts files by a date prefix. By default it expects to find a prefix matching the regular expression: `/^(\d\d\d\d?_\d\d?_\d\d?)/` and parses that prefix using the data pattern: `"%Y_%m_%d"`. However you can override both of these upon instantiation like so:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
MarkdownRecord::FileSorting::DateSorter.new(<regexp>, <date_pattern_string>)
|
37
|
+
````
|
38
|
+
|
39
|
+
- `MarkdownRecord::FileSorting::SemVerSorter`: this class sorts files by a semantive version prefix. By default it expects to find a prefix matching the regular expression: `/(\d+(?:_\d+)*)(?:_|$)/`. This allows any number of version parts, including just a single number, which would make it functionally equivalent to the default file sorter `MarkdownRecord::FileSorting::Base`. However, you can override the regex used like so:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
MarkdownRecord::FileSorting::SemVerSorter.new(<regexp>)
|
43
|
+
````
|
44
|
+
|
45
|
+
You may also implement your own file sorter. It only needs to implement a `to_sort_value(value)` method which returns an object that responds to `<=>`.
|