doc_contract 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -2
- data/Rakefile +6 -5
- data/app/assets/javascripts/doc_contract/application.coffee +2 -0
- data/app/controllers/concerns/doc_contract/shared_template_and_instance_methods.rb +24 -7
- data/app/controllers/doc_contract/application_controller.rb +4 -1
- data/app/controllers/doc_contract/contract_instances_controller.rb +15 -13
- data/app/controllers/doc_contract/contract_templates_controller.rb +15 -26
- data/app/helpers/doc_contract/application_helper.rb +38 -17
- data/app/models/concerns/yaml_validator.rb +9 -3
- data/app/models/doc_contract/application_record.rb +27 -0
- data/app/models/doc_contract/contract_instance.rb +10 -3
- data/app/models/doc_contract/contract_template.rb +15 -4
- data/app/views/doc_contract/application/_button-preview-pdf.html.slim +1 -1
- data/app/views/doc_contract/application/_navigation.html.slim +7 -3
- data/app/views/doc_contract/application/main.html.slim +1 -1
- data/app/views/doc_contract/contract_instances/_form.html.slim +4 -1
- data/app/views/doc_contract/contract_instances/index.html.slim +2 -2
- data/app/views/doc_contract/contract_templates/index.html.slim +1 -1
- data/config/environment.rb +4 -0
- data/config/initializers/human_plural.rb +8 -5
- data/config/routes.rb +3 -2
- data/db/migrate/20220118150923_create_doc_contract_contract_templates.rb +1 -0
- data/db/migrate/20220121160034_add_markdown_to_doc_contract_contract_templates.rb +1 -0
- data/db/migrate/20220121165825_add_config_yml_to_doc_contract_contract_templates.rb +1 -0
- data/db/migrate/20220122151402_create_doc_contract_contract_instances.rb +1 -0
- data/db/migrate/20220125183437_add_titlepage_background_to_contract_templates.rb +1 -0
- data/lib/doc_contract/engine.rb +25 -10
- data/lib/doc_contract/handlebars.rb +45 -30
- data/lib/doc_contract/version.rb +3 -1
- data/lib/doc_contract.rb +4 -3
- data/lib/tasks/doc_contract_tasks.rake +1 -0
- metadata +17 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3a6c918f368226c070e81509c178e61d83f45cd1f239a126ae66a355f50c512
|
4
|
+
data.tar.gz: 7bb72324e156c3fc3451e7379b9ab642ce395808ca0d3c2117819fd0231a86c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23ab0131768cc5d6579447c141de09915656d3d1249d8f966f1e55daa8b455189e3d4793ef995169ddf56aecceea914826bbf2156277f301974762229a1821b1
|
7
|
+
data.tar.gz: 76096e9d5a43835fd96dd94ca9a5ddc75b85ab12c3ceb60c3e66655a55404e05d5fcdc6d9cd97105b631c4d88acbda606b8c06710d266c44af56b4b3d3596ea5
|
data/README.md
CHANGED
@@ -173,16 +173,17 @@ To put your own version of this page create a view having this path in the main
|
|
173
173
|
put the following in your `config/application.rb` file:
|
174
174
|
|
175
175
|
```ruby
|
176
|
-
config.doc_contract.show_readme_on_main_page = false
|
176
|
+
config.x.doc_contract.show_readme_on_main_page = false
|
177
177
|
```
|
178
178
|
|
179
179
|
### The link home content
|
180
180
|
The default value for `config/application.rb` is:
|
181
181
|
|
182
182
|
```ruby
|
183
|
-
config.doc_contract.link_home_content = '<i class="arrow left icon"></i> Back'
|
183
|
+
config.x.doc_contract.link_home_content = -> { '<i class="arrow left icon"></i> Back' }
|
184
184
|
```
|
185
185
|
To change for example the icon, see the options at the [fomantic-ui](https://fomantic-ui.com/elements/icon.html) site.
|
186
|
+
Note that the value is a `lambda` to allow the use of for example `I18n`.
|
186
187
|
|
187
188
|
### Screenshot
|
188
189
|
![Edit template scnreenshot](/spec/dummy/public/DocContract-template-edit.png)
|
data/Rakefile
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'bundler/setup'
|
2
3
|
|
3
|
-
APP_RAKEFILE = File.expand_path(
|
4
|
-
load
|
4
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
5
|
+
load 'rails/tasks/engine.rake'
|
5
6
|
|
6
|
-
load
|
7
|
+
load 'rails/tasks/statistics.rake'
|
7
8
|
|
8
|
-
require
|
9
|
+
require 'bundler/gem_tasks'
|
@@ -11,6 +11,8 @@ latex_template_infos =
|
|
11
11
|
</a>'
|
12
12
|
$ ->
|
13
13
|
$('.ui.dropdown').dropdown() # assume fomantic that has the clearable class option, a lot better than the below custom code
|
14
|
+
$('.message .close').on 'click', ->
|
15
|
+
$(@).closest('.message').transition('fade')
|
14
16
|
|
15
17
|
$('.preview-markdown').click ->
|
16
18
|
markdown = $('#contract_template_markdown').val()
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module DocContract
|
2
3
|
module SharedTemplateAndInstanceMethods
|
3
4
|
# POST /doc-contract/contract_templates/:id/processed_markdown?markdown=...
|
@@ -19,12 +20,17 @@ module DocContract
|
|
19
20
|
render json: {html: @record.processed_html}
|
20
21
|
end
|
21
22
|
|
22
|
-
|
23
|
+
# rubocop:disable Metrics/MethodLength
|
24
|
+
# for now keep as a whole story
|
25
|
+
def pdf
|
23
26
|
@record = record_class.find params[:id]
|
24
27
|
authorize! :show, @record
|
28
|
+
|
25
29
|
# Allow preview using the markdown parameter. Fallback to actual saved
|
26
30
|
# version when param is not given
|
27
31
|
@record.markdown = params[:markdown] if params[:markdown].present?
|
32
|
+
return redirect_to @record unless @record.markdown.present?
|
33
|
+
|
28
34
|
@tempfile_markdown = Tempfile.new(['pandoc-markdown', '.md'])
|
29
35
|
config_addittions = {}
|
30
36
|
if nbackground = @record.try(:titlepage_background) || @record.contract_template.titlepage_background.presence
|
@@ -36,31 +42,42 @@ module DocContract
|
|
36
42
|
@tempfile_markdown.close
|
37
43
|
@tempfile_pdf = Tempfile.create(['pandoc-pdf-', '.pdf'])
|
38
44
|
@tempfile_pdf.close
|
39
|
-
command = %(
|
40
|
-
|
45
|
+
command = %(
|
46
|
+
pandoc --template=vendor/assets/doc-contract/pandoc-templates/eisvogel.tex
|
47
|
+
--css=app/assets/stylesheets/doc_contract/pandoc.css #{@tempfile_markdown.path}
|
48
|
+
-o #{@tempfile_pdf.path}
|
49
|
+
).strip.gsub(/\n\s*/, ' ')
|
50
|
+
command << ' --variable top-level-division=chapter' if @record.config['book']
|
41
51
|
#command << " --variable titlepage=true" if false # done through YAML config
|
42
|
-
command <<
|
43
|
-
command <<
|
52
|
+
command << ' --listings' # if some conditions TODO: make this a database boolean
|
53
|
+
command << ' --filter pandoc-latex-environment' if template_markdown['pandoc-latex-environment:']
|
44
54
|
|
45
55
|
stdout, stderr, status = nil
|
46
56
|
Dir.chdir DocContract::Engine.root do
|
47
57
|
stdout, stderr, status = Open3.capture3(command)
|
48
58
|
end
|
59
|
+
|
49
60
|
if status.success?
|
50
61
|
send_file @tempfile_pdf.path
|
51
62
|
else
|
52
63
|
redirect_back fallback_location: record_class, flash: {error: stderr}
|
53
64
|
end
|
54
65
|
ensure
|
55
|
-
|
56
|
-
|
66
|
+
begin
|
67
|
+
@tempfile_markdown.unlink
|
68
|
+
@tempfile_pdf.unlink
|
69
|
+
rescue
|
70
|
+
nil
|
71
|
+
end
|
57
72
|
end
|
73
|
+
# rubocop:enable Metrics/MethodLength
|
58
74
|
|
59
75
|
private
|
60
76
|
|
61
77
|
def record_class
|
62
78
|
result = controller_path.classify.safe_constantize
|
63
79
|
raise "Cannot determine record_class from controller_path: #{controller_path}" unless result
|
80
|
+
|
64
81
|
result
|
65
82
|
end
|
66
83
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module DocContract
|
2
3
|
class ContractInstancesController < ApplicationController
|
3
4
|
include SharedTemplateAndInstanceMethods
|
@@ -7,23 +8,19 @@ module DocContract
|
|
7
8
|
def index
|
8
9
|
authorize! :index, record_class
|
9
10
|
@q = record_class.ransack query
|
10
|
-
@q.sorts =
|
11
|
+
@q.sorts = 'name asc' if @q.sorts.empty?
|
11
12
|
@records = @q.result.includes(:contract_template).page(params[:page]).per(100)
|
12
13
|
respond_with(@records)
|
13
14
|
end
|
14
15
|
|
15
16
|
def new
|
16
17
|
@record = record_class.new
|
17
|
-
|
18
|
-
|
19
|
-
def edit
|
20
|
-
@record = record_class.find(params[:id])
|
21
|
-
authorize! :edit, @record
|
18
|
+
authorize! :create, @record
|
22
19
|
end
|
23
20
|
|
24
21
|
def create
|
25
|
-
authorize! :create, record_class
|
26
22
|
@record = record_class.new record_params
|
23
|
+
authorize! :create, @record
|
27
24
|
if @record.save
|
28
25
|
redirect_to @record
|
29
26
|
else
|
@@ -31,9 +28,19 @@ module DocContract
|
|
31
28
|
end
|
32
29
|
end
|
33
30
|
|
31
|
+
def show
|
32
|
+
@record = record_class.find params[:id]
|
33
|
+
authorize! :show, @record
|
34
|
+
end
|
35
|
+
|
36
|
+
def edit
|
37
|
+
@record = record_class.find(params[:id])
|
38
|
+
authorize! :edit, @record
|
39
|
+
end
|
40
|
+
|
34
41
|
def update
|
35
42
|
@record = record_class.find params[:id]
|
36
|
-
authorize! :
|
43
|
+
authorize! :edit, @record
|
37
44
|
if @record.update record_params
|
38
45
|
redirect_to @record
|
39
46
|
else
|
@@ -41,11 +48,6 @@ module DocContract
|
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
44
|
-
def show
|
45
|
-
@record = record_class.find params[:id]
|
46
|
-
authorize! :show, @record
|
47
|
-
end
|
48
|
-
|
49
51
|
def destroy
|
50
52
|
@record = record_class.find params[:id]
|
51
53
|
authorize! :destroy, @record
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module DocContract
|
2
3
|
class ContractTemplatesController < ApplicationController
|
3
4
|
include SharedTemplateAndInstanceMethods
|
@@ -7,23 +8,19 @@ module DocContract
|
|
7
8
|
def index
|
8
9
|
authorize! :index, record_class
|
9
10
|
@q = record_class.ransack query
|
10
|
-
@q.sorts = '
|
11
|
+
@q.sorts = 'name asc' if @q.sorts.empty?
|
11
12
|
@records = @q.result.page(params[:page]).per(100)
|
12
13
|
respond_with(@records)
|
13
14
|
end
|
14
15
|
|
15
16
|
def new
|
16
17
|
@record = record_class.new
|
17
|
-
|
18
|
-
|
19
|
-
def edit
|
20
|
-
@record = record_class.find(params[:id])
|
21
|
-
authorize! :edit, @record
|
18
|
+
authorize! :create, @record
|
22
19
|
end
|
23
20
|
|
24
21
|
def create
|
25
|
-
authorize! :create, record_class
|
26
22
|
@record = record_class.new record_params
|
23
|
+
authorize! :create, @record
|
27
24
|
if @record.save
|
28
25
|
redirect_to @record, status: :see_other
|
29
26
|
else
|
@@ -31,9 +28,19 @@ module DocContract
|
|
31
28
|
end
|
32
29
|
end
|
33
30
|
|
31
|
+
def show
|
32
|
+
@record = record_class.find params[:id]
|
33
|
+
authorize! :show, @record
|
34
|
+
end
|
35
|
+
|
36
|
+
def edit
|
37
|
+
@record = record_class.find(params[:id])
|
38
|
+
authorize! :edit, @record
|
39
|
+
end
|
40
|
+
|
34
41
|
def update
|
35
42
|
@record = record_class.find params[:id]
|
36
|
-
authorize! :
|
43
|
+
authorize! :edit, @record
|
37
44
|
if @record.update record_params
|
38
45
|
redirect_to @record, status: :see_other
|
39
46
|
else
|
@@ -41,29 +48,11 @@ module DocContract
|
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
44
|
-
def show
|
45
|
-
@record = record_class.find params[:id]
|
46
|
-
authorize! :show, @record
|
47
|
-
end
|
48
|
-
|
49
51
|
def destroy
|
50
52
|
@record = record_class.find params[:id]
|
51
53
|
authorize! :destroy, @record
|
52
54
|
@record.destroy
|
53
55
|
redirect_to record_class, status: :see_other
|
54
56
|
end
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
def record_class
|
59
|
-
result = controller_path.classify.safe_constantize
|
60
|
-
raise "Cannot determine record_class from controller_path: #{controller_path}" unless result
|
61
|
-
|
62
|
-
result
|
63
|
-
end
|
64
|
-
|
65
|
-
def record_params
|
66
|
-
params.require(record_class.name.demodulize.underscore).permit!
|
67
|
-
end
|
68
57
|
end
|
69
58
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DocContract
|
2
4
|
module ApplicationHelper
|
3
5
|
# Return active or nil based on the given route spec
|
@@ -6,8 +8,9 @@ module DocContract
|
|
6
8
|
#NOTE: Taken from the dunlop-core gem. That is the best maintained version
|
7
9
|
def active_class(*route_specs)
|
8
10
|
options = route_specs.extract_options!
|
9
|
-
return nil if Array.wrap(options[:except]).any?{|exception| current_route_spec?(exception) }
|
10
|
-
return 'active' if route_specs.any?{|rs| current_route_spec?(rs, options) }
|
11
|
+
return nil if Array.wrap(options[:except]).any?{ |exception| current_route_spec?(exception) }
|
12
|
+
return 'active' if route_specs.any?{ |rs| current_route_spec?(rs, options) }
|
13
|
+
|
11
14
|
nil
|
12
15
|
end
|
13
16
|
|
@@ -19,17 +22,21 @@ module DocContract
|
|
19
22
|
# current_route_spec?('#show') #=> true if action_name is show, false otherwise
|
20
23
|
#NOTE: this helper is tested through the active_class helper
|
21
24
|
#NOTE: Taken from the dunlop-core gem. That is the best maintained version
|
22
|
-
def current_route_spec?(route_spec,
|
25
|
+
def current_route_spec?(route_spec, _options = {})
|
23
26
|
return route_spec.match([controller_path, action_name].join('#')) if route_spec.is_a?(Regexp)
|
27
|
+
|
24
28
|
controller, action = route_spec.split('#')
|
25
29
|
return action == params[:id] if controller_path == 'high_voltage/pages'
|
30
|
+
|
26
31
|
actual_controller_parts = controller_path.split('/')
|
27
32
|
if controller #and controller_path == controller
|
28
33
|
tested_controller_parts = controller.split('/')
|
29
34
|
return if tested_controller_parts.size > actual_controller_parts.size
|
35
|
+
|
30
36
|
if actual_controller_parts[0...tested_controller_parts.size] == tested_controller_parts
|
31
37
|
# controller spec matches
|
32
38
|
return true unless action
|
39
|
+
|
33
40
|
action_name == action
|
34
41
|
end
|
35
42
|
else
|
@@ -78,11 +85,11 @@ module DocContract
|
|
78
85
|
def page_title_for_resource(args)
|
79
86
|
options = args.extract_options!
|
80
87
|
model = args[1].respond_to?(:model_name) ? args[1] : args[1].class
|
81
|
-
if args.first == :index
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
88
|
+
title = if args.first == :index
|
89
|
+
t('action.index.label', models: model.model_name.human_plural).html_safe
|
90
|
+
else
|
91
|
+
t("action.#{args.first}.label", model: model.model_name.human).html_safe
|
92
|
+
end
|
86
93
|
if back_options = options[:back]
|
87
94
|
url =
|
88
95
|
case back_options
|
@@ -105,7 +112,7 @@ module DocContract
|
|
105
112
|
"#{from_item} - #{to_item} / #{records.total_count}"
|
106
113
|
end
|
107
114
|
|
108
|
-
def at(attribute_name, scope_model=nil)
|
115
|
+
def at(attribute_name, scope_model = nil)
|
109
116
|
scope_model ||= @scope_model
|
110
117
|
scope_model.human_attribute_name(attribute_name)
|
111
118
|
end
|
@@ -116,7 +123,7 @@ module DocContract
|
|
116
123
|
classes |= ['search']
|
117
124
|
classes << 'conditions-present' if @q.conditions.present?
|
118
125
|
content = capture(&blk)
|
119
|
-
content_tag(:tr, content,
|
126
|
+
content_tag(:tr, content, class: classes)
|
120
127
|
end
|
121
128
|
|
122
129
|
# This helper returns the link for showing a record inside a table
|
@@ -126,7 +133,11 @@ module DocContract
|
|
126
133
|
else
|
127
134
|
return unless can? :show, record
|
128
135
|
end
|
129
|
-
link_to(
|
136
|
+
link_to(
|
137
|
+
content_tag(:i, nil, class: 'folder open icon'),
|
138
|
+
path || record,
|
139
|
+
class: 'table-link show ui mini basic primary icon button'
|
140
|
+
)
|
130
141
|
end
|
131
142
|
|
132
143
|
# This helper returns the link for showing a record inside a table
|
@@ -136,7 +147,11 @@ module DocContract
|
|
136
147
|
else
|
137
148
|
return unless can? :download, record
|
138
149
|
end
|
139
|
-
link_to(
|
150
|
+
link_to(
|
151
|
+
content_tag(:i, nil, class: 'download icon'),
|
152
|
+
path || [:download, record],
|
153
|
+
class: 'table-link download ui mini violet icon button'
|
154
|
+
)
|
140
155
|
end
|
141
156
|
|
142
157
|
# This helper returns the link for editing a record inside a table
|
@@ -161,16 +176,22 @@ module DocContract
|
|
161
176
|
record_name ||= record.name if record.respond_to?(:name)
|
162
177
|
record_name ||= record.title if record.respond_to?(:title)
|
163
178
|
confirm_text << " #{record_name}" if record_name.present?
|
164
|
-
confirm_text <<
|
165
|
-
link_to(
|
179
|
+
confirm_text << '?'
|
180
|
+
link_to(
|
181
|
+
content_tag(:i, nil, class: 'trash icon'),
|
182
|
+
path || record,
|
183
|
+
method: :delete,
|
184
|
+
data: { confirm: confirm_text },
|
185
|
+
class: 'table-link destroy ui mini negative icon button'
|
186
|
+
)
|
166
187
|
end
|
167
188
|
|
168
189
|
# https://coderwall.com/p/7gqmog/display-flash-messages-with-semantic-ui-in-rails
|
169
190
|
def flash_class(level)
|
170
191
|
case level.to_sym
|
171
|
-
when :success then
|
172
|
-
when :error, :alert then
|
173
|
-
when :notice then
|
192
|
+
when :success then 'ui positive message'
|
193
|
+
when :error, :alert then 'ui negative message'
|
194
|
+
when :notice then 'ui info message'
|
174
195
|
else "ui #{level} message"
|
175
196
|
end
|
176
197
|
end
|
@@ -1,9 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class YamlValidator < ActiveModel::EachValidator
|
2
4
|
def validate_each(record, attribute, value)
|
3
5
|
return if value.blank?
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
|
7
|
+
object = begin
|
8
|
+
YAML.safe_load(value, permitted_classes: [Symbol, Date])
|
9
|
+
rescue # Psych::SyntaxError
|
10
|
+
nil
|
7
11
|
end
|
12
|
+
|
13
|
+
record.errors.add attribute, (options[:message] || 'is not valid YAML') unless object
|
8
14
|
end
|
9
15
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DocContract
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
5
|
+
|
6
|
+
self.abstract_class = true
|
7
|
+
|
8
|
+
RANSACKABLE_ATTRIBUTES = [].freeze
|
9
|
+
RANSACKABLE_ASSOCIATIONS = [].freeze
|
10
|
+
|
11
|
+
def self.ransackable_attributes(_auth_object = nil)
|
12
|
+
if const_defined? :RANSACKABLE_ATTRIBUTES
|
13
|
+
const_get :RANSACKABLE_ATTRIBUTES
|
14
|
+
else
|
15
|
+
RANSACKABLE_ATTRIBUTES
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.ransackable_associations(_auth_object = nil)
|
20
|
+
if const_defined? :RANSACKABLE_ASSOCIATIONS
|
21
|
+
const_get :RANSACKABLE_ASSOCIATIONS
|
22
|
+
else
|
23
|
+
RANSACKABLE_ASSOCIATIONS
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,10 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DocContract
|
2
4
|
class ContractInstance < ApplicationRecord
|
3
|
-
|
5
|
+
|
4
6
|
belongs_to :contract_template
|
5
7
|
|
8
|
+
validates :name, presence: true
|
9
|
+
|
10
|
+
RANSACKABLE_ATTRIBUTES = %w[name].freeze
|
11
|
+
RANSACKABLE_ASSOCIATIONS = %w[contract_template].freeze
|
12
|
+
|
6
13
|
def config
|
7
|
-
own_config = YAML.
|
14
|
+
own_config = YAML.safe_load(config_yml.to_s, permitted_classes: [Symbol, Date]) || {}
|
8
15
|
own_config.reverse_merge contract_template.config
|
9
16
|
end
|
10
17
|
|
@@ -27,7 +34,7 @@ module DocContract
|
|
27
34
|
config.merge! config_addittions unless config_addittions.blank?
|
28
35
|
result = DocContract::Handlebars.compile(markdown, config)
|
29
36
|
# prefix the markdown with the config (eisvogel pandoc)
|
30
|
-
"#{config.to_yaml}...\n\n"
|
37
|
+
"#{config.to_yaml}...\n\n#{result}"
|
31
38
|
end
|
32
39
|
|
33
40
|
def processed_html
|
@@ -1,13 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module DocContract
|
2
3
|
class ContractTemplate < ApplicationRecord
|
3
|
-
|
4
|
+
|
5
|
+
HANDLEBARS_REGEX = /{{((#)?[a-z._ ]+)}}/
|
6
|
+
|
7
|
+
RANSACKABLE_ATTRIBUTES = %w[name].freeze
|
8
|
+
#RANSACKABLE_ASSOCIATIONS = %w[].freeze
|
9
|
+
|
4
10
|
has_many :contract_instances, dependent: :delete_all
|
5
11
|
|
6
12
|
validates :name, presence: true
|
7
13
|
validates :config_yml, yaml: true
|
8
14
|
|
9
15
|
def config
|
10
|
-
YAML.
|
16
|
+
YAML.safe_load(config_yml.to_s, permitted_classes: [Symbol, Date]) || {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Do not fail when an instance trait tries to access fallback template properties
|
20
|
+
def contract_template
|
21
|
+
self
|
11
22
|
end
|
12
23
|
|
13
24
|
def processed_markdown(config_addittions = {})
|
@@ -21,7 +32,7 @@ module DocContract
|
|
21
32
|
config.merge! config_addittions unless config_addittions.blank?
|
22
33
|
result = DocContract::Handlebars.compile(markdown, config)
|
23
34
|
# prefix the markdown with the config (eisvogel pandoc)
|
24
|
-
"#{config.to_yaml}...\n\n"
|
35
|
+
"#{config.to_yaml}...\n\n#{result}"
|
25
36
|
end
|
26
37
|
|
27
38
|
def processed_html
|
@@ -31,7 +42,7 @@ module DocContract
|
|
31
42
|
end
|
32
43
|
|
33
44
|
def present_handlebars_expressions
|
34
|
-
markdown.scan(HANDLEBARS_REGEX).map(&:first).uniq
|
45
|
+
markdown.to_s.scan(HANDLEBARS_REGEX).map(&:first).uniq
|
35
46
|
end
|
36
47
|
end
|
37
48
|
end
|
@@ -1 +1 @@
|
|
1
|
-
= link_to 'PDF', polymorphic_path(@record, action: :
|
1
|
+
= link_to 'PDF', polymorphic_path(@record, action: :pdf), class: 'ui button'
|
@@ -1,9 +1,13 @@
|
|
1
1
|
.ui.inverted.menu.fixed
|
2
2
|
= link_to main_app.root_path, class: 'header item'
|
3
|
-
= Rails.application.config.doc_contract.link_home_content.to_s.html_safe
|
3
|
+
= Rails.application.config.x.doc_contract.link_home_content.call.to_s.html_safe
|
4
|
+
= link_to doc_contract.root_path, class: 'header item'
|
5
|
+
= Rails.application.config.x.doc_contract.application_title.call.to_s.html_safe
|
4
6
|
- if current_user.present?
|
5
|
-
|
6
|
-
|
7
|
+
- if can? :read, DocContract::ContractTemplate
|
8
|
+
= link_to DocContract::ContractTemplate.model_name.human_plural, [DocContract::ContractTemplate], class: ['item', active_class('doc_contract/contract_templates')]
|
9
|
+
- if can? :read, DocContract::ContractInstance
|
10
|
+
= link_to DocContract::ContractInstance.model_name.human_plural, [DocContract::ContractInstance], class: ['item', active_class('doc_contract/contract_instances')]
|
7
11
|
.right.menu
|
8
12
|
- if current_user.present?
|
9
13
|
.ui.dropdown.item
|
@@ -1,6 +1,6 @@
|
|
1
1
|
.ui.two.item.menu
|
2
2
|
= link_to DocContract::ContractTemplate.model_name.human_plural, [DocContract::ContractTemplate], class: ['item', active_class('doc_contract/contract_templates')]
|
3
3
|
= link_to DocContract::ContractInstance.model_name.human_plural, [DocContract::ContractInstance], class: ['item', active_class('doc_contract/contract_instances')]
|
4
|
-
- if Rails.application.config.doc_contract.show_readme_on_main_page
|
4
|
+
- if Rails.application.config.x.doc_contract.show_readme_on_main_page
|
5
5
|
hr
|
6
6
|
#readme_container== readme_html
|
@@ -4,7 +4,10 @@
|
|
4
4
|
tbody
|
5
5
|
tr
|
6
6
|
td= at :name
|
7
|
-
td
|
7
|
+
td
|
8
|
+
.ui.input.error= f.text_field :name
|
9
|
+
- if @record.errors[:name].present?
|
10
|
+
.ui.left.pointing.red.basic.label= @record.errors[:name].to_sentence
|
8
11
|
tr
|
9
12
|
td
|
10
13
|
= DocContract::ContractTemplate.model_name.human
|
@@ -6,8 +6,8 @@
|
|
6
6
|
table.ui.compact.top.attached.striped.table
|
7
7
|
thead
|
8
8
|
tr
|
9
|
-
th= at :
|
10
|
-
th= DocContract::ContractTemplate.model_name.human
|
9
|
+
th= sort_link @q, :name, at(:name), default_order: :asc
|
10
|
+
th= sort_link @q, :contract_template_name, DocContract::ContractTemplate.model_name.human
|
11
11
|
th.column-filter.actions
|
12
12
|
= search_result_info @records
|
13
13
|
= search_row class: 'ui mini form' do
|
@@ -1,16 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
class Name
|
5
|
+
|
3
6
|
def human_plural
|
4
7
|
# Try to find the plural name of a model. If none can be found try to find a non namespaced version and return that one
|
5
|
-
default =
|
6
|
-
if path.present? and path['/'] # non namespaced model a
|
7
|
-
unnamespaced_path = path.sub(
|
8
|
-
I18n.t(unnamespaced_path, default:
|
8
|
+
default = proc do |path|
|
9
|
+
if path.present? and path['/'] # non namespaced model a.b.c/d/e => a.b.c
|
10
|
+
unnamespaced_path = path.sub(%r{\.(\w+)/[\w\\]+}, '.\1')
|
11
|
+
I18n.t(unnamespaced_path, default: proc{ human.pluralize })
|
9
12
|
else
|
10
13
|
human.pluralize
|
11
14
|
end
|
12
15
|
end
|
13
|
-
I18n.t("#{@klass.i18n_scope}.models.plural.#{i18n_key}", default: default
|
16
|
+
I18n.t("#{@klass.i18n_scope}.models.plural.#{i18n_key}", default: default)
|
14
17
|
end
|
15
18
|
end
|
16
19
|
end
|
data/config/routes.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
DocContract::Engine.routes.draw do
|
2
3
|
resources :contract_templates do
|
3
4
|
member do
|
4
5
|
post :processed_markdown
|
5
6
|
post :processed_html
|
6
|
-
get :
|
7
|
+
get :pdf
|
7
8
|
end
|
8
9
|
end
|
9
10
|
resources :contract_instances do
|
10
11
|
member do
|
11
12
|
post :processed_markdown
|
12
13
|
post :processed_html
|
13
|
-
get :
|
14
|
+
get :pdf
|
14
15
|
end
|
15
16
|
end
|
16
17
|
root 'application#main'
|
data/lib/doc_contract/engine.rb
CHANGED
@@ -1,27 +1,42 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
require 'cancancan'
|
3
|
-
require '
|
3
|
+
require 'jquery-rails'
|
4
4
|
require 'kaminari'
|
5
|
+
require 'numbers_and_words'
|
6
|
+
require 'ransack'
|
7
|
+
require 'redcarpet'
|
8
|
+
|
5
9
|
module DocContract
|
6
10
|
class Engine < ::Rails::Engine
|
11
|
+
|
7
12
|
#isolate_namespace self.name.deconstantize.safe_constantize
|
8
13
|
isolate_namespace DocContract
|
9
|
-
|
14
|
+
|
15
|
+
initializer 'doc_contract.assets.precompile' do |main_app|
|
10
16
|
#app.config.assets.precompile << "config/engine_name_manifest.js"
|
11
|
-
main_app.config.assets.precompile <<
|
12
|
-
main_app.config.assets.precompile <<
|
13
|
-
|
14
|
-
|
15
|
-
|
17
|
+
main_app.config.assets.precompile << 'doc_contract/application.css'
|
18
|
+
main_app.config.assets.precompile << 'doc_contract/application.js'
|
19
|
+
|
20
|
+
# Engine specific config
|
21
|
+
# Allow setting false, so check for nil
|
22
|
+
main_app.config.x.doc_contract.show_readme_on_main_page = true if main_app.config.x.doc_contract.show_readme_on_main_page.nil?
|
23
|
+
main_app.config.x.doc_contract.application_title ||= -> { 'DOC-contract' }
|
24
|
+
main_app.config.x.doc_contract.link_home_content ||= -> { '<i class="arrow left icon"></i> Back' }
|
16
25
|
end
|
17
26
|
|
18
27
|
# add migrations to containing application
|
19
28
|
initializer 'doc_contract.append_migrations' do |app|
|
20
29
|
unless app.root.to_s.match root.to_s
|
21
|
-
config.paths[
|
22
|
-
app.config.paths[
|
30
|
+
config.paths['db/migrate'].expanded.each do |expanded_path|
|
31
|
+
app.config.paths['db/migrate'] << expanded_path
|
23
32
|
end
|
24
33
|
end
|
25
34
|
end
|
35
|
+
|
36
|
+
initializer 'doc_contract.ensure_set_field_error_proc' do |app|
|
37
|
+
app.config.action_view.field_error_proc ||= proc do |html_tag, _instance|
|
38
|
+
%|<div class="field_with_errors field error has-error">#{html_tag}</div>|.html_safe
|
39
|
+
end
|
40
|
+
end
|
26
41
|
end
|
27
42
|
end
|
@@ -1,25 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DocContract
|
2
4
|
class Handlebars
|
5
|
+
|
3
6
|
COMPILE_ATTRIBUTES = %w[
|
4
7
|
title subtitle
|
5
8
|
header-left header-center header-right
|
6
9
|
footer-left footer-center footer-right
|
7
|
-
]
|
10
|
+
].freeze
|
11
|
+
|
8
12
|
class DummyEscaper
|
13
|
+
|
9
14
|
def self.escape(value)
|
10
15
|
value
|
11
16
|
end
|
12
17
|
end
|
13
18
|
|
19
|
+
# rubocop:disable Metrics/MethodLength
|
14
20
|
def self.renderer
|
15
|
-
return
|
21
|
+
return @handlebars_renderer if @handlebars_renderer
|
22
|
+
|
16
23
|
renderer = ::Handlebars::Handlebars.new
|
17
24
|
renderer.set_escaper DummyEscaper # Do not escape HTML
|
18
|
-
currency_helper =
|
19
|
-
if Integer
|
20
|
-
|
25
|
+
currency_helper = proc do |_context, value, currency_symbol|
|
26
|
+
if value.is_a?(Integer) or value.to_i == value
|
27
|
+
format("#{currency_symbol} %.0f,-", value) #.gsub(/(\d)(?=\d{3}+,)/, '\1.')
|
21
28
|
else
|
22
|
-
|
29
|
+
format("#{currency_symbol} %.2f", value.to_f) #.gsub(/(\d)(?=\d{3}+,)/, '\1.')
|
23
30
|
end
|
24
31
|
end
|
25
32
|
#renderer.register_helper(:euro) do |context, value|
|
@@ -29,15 +36,15 @@ module DocContract
|
|
29
36
|
# sprintf("€ %.2f", value.to_f) #.gsub(/(\d)(?=\d{3}+,)/, '\1.')
|
30
37
|
# end
|
31
38
|
#end
|
32
|
-
eval_boolean =
|
33
|
-
blocks, comparables = values.partition{|v| v.is_a? ::Handlebars::Tree::Block}
|
39
|
+
eval_boolean = proc do |context, values, compare_lambda|
|
40
|
+
blocks, comparables = values.partition{ |v| v.is_a? ::Handlebars::Tree::Block }
|
34
41
|
true_blk, false_blk = blocks
|
35
|
-
comparables = comparables.map{|v| v.is_a?(Parslet::Slice) ? v.to_s : v}
|
36
|
-
if compare_lambda.arity == 1
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
42
|
+
comparables = comparables.map{ |v| v.is_a?(Parslet::Slice) ? v.to_s : v }
|
43
|
+
are_equal = if compare_lambda.arity == 1
|
44
|
+
compare_lambda.call(comparables)
|
45
|
+
else
|
46
|
+
compare_lambda.call(*comparables)
|
47
|
+
end
|
41
48
|
if true_blk.items.present? # assumed internal helper usecase without blocks
|
42
49
|
are_equal ? true_blk.fn(context) : false_blk.try(:fn, context)
|
43
50
|
else
|
@@ -47,31 +54,38 @@ module DocContract
|
|
47
54
|
renderer.register_helper(:euro) { |context, value| currency_helper.call context, value, '€' }
|
48
55
|
renderer.register_helper(:dollar) { |context, value| currency_helper.call context, value, '$' }
|
49
56
|
renderer.register_helper(:peso) { |context, value| currency_helper.call context, value, '$' }
|
50
|
-
renderer.register_helper(:plus) {|
|
51
|
-
renderer.register_helper(:sum)
|
52
|
-
renderer.register_helper(:eq)
|
57
|
+
renderer.register_helper(:plus) { |_context, value, addition, *_rest| value.to_f + addition.to_f }
|
58
|
+
renderer.register_helper(:sum) { |_context, *values, _bkl| (values.first.is_a?(Array) ? values.first : values).map(&:to_f).sum }
|
59
|
+
renderer.register_helper(:eq) { |context, *values| eval_boolean.call context, values, ->(vals){ vals.uniq.length <= 1 } }
|
53
60
|
renderer.register_helper(:neq) { |context, *values| eval_boolean.call context, values, ->(vals){ vals.uniq.length > 1 } }
|
54
|
-
renderer.register_helper(:gt)
|
55
|
-
renderer.register_helper(:includes)
|
56
|
-
renderer.register_helper(:map) do |
|
61
|
+
renderer.register_helper(:gt) { |context, *values| eval_boolean.call context, values, ->(a, b){ a > b } }
|
62
|
+
renderer.register_helper(:includes) { |context, *values| eval_boolean.call context, values, ->(vals){ vals.first.include? vals.last } }
|
63
|
+
renderer.register_helper(:map) do |_context, array, key|
|
57
64
|
return [] unless array.present?
|
58
|
-
|
65
|
+
|
66
|
+
array.map{ |e| e[key.to_s] || e[key.to_sym] }
|
59
67
|
end
|
60
|
-
renderer.register_helper(:upcase) {|
|
61
|
-
renderer.register_helper(:downcase) {|
|
62
|
-
renderer.register_helper(:to_sentence) do |
|
68
|
+
renderer.register_helper(:upcase) { |_context, value| value.to_s.upcase }
|
69
|
+
renderer.register_helper(:downcase) { |_context, value| value.to_s.downcase }
|
70
|
+
renderer.register_helper(:to_sentence) do |_context, array, nester|
|
63
71
|
case nester
|
64
72
|
when String, Parslet::Slice
|
65
|
-
array.map{|e| e[nester.to_s] || e[nester.to_sym]}.to_sentence
|
73
|
+
array.map{ |e| e[nester.to_s] || e[nester.to_sym] }.to_sentence
|
66
74
|
else
|
67
75
|
array.to_sentence
|
68
76
|
end
|
69
77
|
end
|
70
|
-
renderer.register_helper(:to_words) { |
|
71
|
-
|
78
|
+
renderer.register_helper(:to_words) { |_context, value| value.to_words }
|
79
|
+
renderer.register_helper(:first_present) do |_context, *args|
|
80
|
+
args.find(&:present?)
|
81
|
+
end
|
82
|
+
@handlebars_renderer = renderer
|
72
83
|
end
|
84
|
+
# rubocop:enable Metrics/MethodLength
|
73
85
|
|
74
86
|
def self.compile(text, object, process_attributes: true)
|
87
|
+
return '' unless text.present?
|
88
|
+
|
75
89
|
object ||= {}
|
76
90
|
if process_attributes
|
77
91
|
object.stringify_keys!
|
@@ -83,13 +97,14 @@ module DocContract
|
|
83
97
|
|
84
98
|
def self.enrich_object(object)
|
85
99
|
object['today'] = Date.today.iso8601 if object['today'].blank?
|
86
|
-
object.keys
|
100
|
+
iteration_keys = object.keys
|
101
|
+
iteration_keys.each do |k|
|
87
102
|
if k =~ /_items$/ and object[k].is_a?(Array)
|
88
|
-
amounts_sum = object[k].map{|o| (o[:amount] || o['amount']).to_f}.sum
|
103
|
+
amounts_sum = object[k].map{ |o| (o[:amount] || o['amount']).to_f }.sum
|
89
104
|
object["#{k}_total"] = amounts_sum
|
90
105
|
end
|
91
106
|
if COMPILE_ATTRIBUTES.include? k
|
92
|
-
#TODO check self reference, maybe even circular?
|
107
|
+
#TODO: check self reference, maybe even circular?
|
93
108
|
object[k] = compile(object[k], object, process_attributes: false)
|
94
109
|
end
|
95
110
|
end
|
data/lib/doc_contract/version.rb
CHANGED
data/lib/doc_contract.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'doc_contract/version'
|
3
|
+
require 'doc_contract/engine'
|
4
|
+
require 'doc_contract/handlebars'
|
4
5
|
require 'ruby-handlebars'
|
5
6
|
require 'open3'
|
6
7
|
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: doc_contract
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin ter Kuile
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-08-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: cancancan
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: jquery-rails
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: kaminari
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: numbers_and_words
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rails
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -81,7 +81,7 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: ransack
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
@@ -95,7 +95,7 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: redcarpet
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
@@ -109,13 +109,13 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: ruby-handlebars
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - ">="
|
116
116
|
- !ruby/object:Gem::Version
|
117
117
|
version: '0'
|
118
|
-
type: :
|
118
|
+
type: :runtime
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: coffee-rails
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - ">="
|
@@ -137,7 +137,7 @@ dependencies:
|
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
140
|
+
name: factory_bot_rails
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - ">="
|
@@ -151,7 +151,7 @@ dependencies:
|
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
|
-
name:
|
154
|
+
name: rspec-rails
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - ">="
|
@@ -187,6 +187,7 @@ files:
|
|
187
187
|
- app/controllers/doc_contract/contract_templates_controller.rb
|
188
188
|
- app/helpers/doc_contract/application_helper.rb
|
189
189
|
- app/models/concerns/yaml_validator.rb
|
190
|
+
- app/models/doc_contract/application_record.rb
|
190
191
|
- app/models/doc_contract/contract_instance.rb
|
191
192
|
- app/models/doc_contract/contract_template.rb
|
192
193
|
- app/views/doc_contract/application/_button-destroy-record.html.slim
|
@@ -217,6 +218,7 @@ files:
|
|
217
218
|
- app/views/doc_contract/kaminari/_paginator.html.slim
|
218
219
|
- app/views/doc_contract/kaminari/_prev_page.html.slim
|
219
220
|
- app/views/layouts/doc_contract/application.html.slim
|
221
|
+
- config/environment.rb
|
220
222
|
- config/initializers/human_plural.rb
|
221
223
|
- config/locales/en.yml
|
222
224
|
- config/routes.rb
|
@@ -262,7 +264,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
262
264
|
- !ruby/object:Gem::Version
|
263
265
|
version: '0'
|
264
266
|
requirements: []
|
265
|
-
rubygems_version: 3.
|
267
|
+
rubygems_version: 3.4.10
|
266
268
|
signing_key:
|
267
269
|
specification_version: 4
|
268
270
|
summary: Create nice and easy contracts based on pandoc
|