clickwrap 0.0.0 → 0.1.0

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.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +90 -0
  3. data/CHANGELOG.md +612 -0
  4. data/README.md +830 -1204
  5. data/SECURITY.md +33 -0
  6. data/app/assets/stylesheets/clickwrap.css +241 -0
  7. data/app/controllers/clickwrap/application_controller.rb +79 -0
  8. data/app/controllers/clickwrap/captures_controller.rb +145 -0
  9. data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
  10. data/app/controllers/clickwrap/receipts_controller.rb +115 -0
  11. data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
  12. data/app/helpers/clickwrap/engine_helper.rb +97 -0
  13. data/app/views/clickwrap/captures/show.html.erb +34 -0
  14. data/app/views/clickwrap/receipts/index.html.erb +38 -0
  15. data/app/views/clickwrap/receipts/show.html.erb +91 -0
  16. data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
  17. data/app/views/clickwrap/shared/_fields.html.erb +100 -0
  18. data/app/views/clickwrap/shared/_statement.html.erb +105 -0
  19. data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
  20. data/config/locales/en.yml +160 -0
  21. data/config/locales/es.yml +138 -0
  22. data/config/routes.rb +41 -0
  23. data/exe/clickwrap +374 -0
  24. data/guides/README.md +30 -0
  25. data/guides/accessibility.md +249 -0
  26. data/guides/consent-and-lifecycle.md +299 -0
  27. data/guides/integrating.md +610 -0
  28. data/guides/integrity.md +212 -0
  29. data/guides/migrating.md +335 -0
  30. data/guides/naming.md +320 -0
  31. data/guides/organizations.md +320 -0
  32. data/guides/receipts-and-verification.md +415 -0
  33. data/guides/request-evidence.md +512 -0
  34. data/guides/retention-and-legal-holds.md +438 -0
  35. data/lib/clickwrap/actor_proxy.rb +147 -0
  36. data/lib/clickwrap/anonymous_actor.rb +47 -0
  37. data/lib/clickwrap/authority.rb +174 -0
  38. data/lib/clickwrap/canonical_json.rb +216 -0
  39. data/lib/clickwrap/capture/event_builder.rb +220 -0
  40. data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
  41. data/lib/clickwrap/capture.rb +650 -0
  42. data/lib/clickwrap/configuration.rb +1129 -0
  43. data/lib/clickwrap/controller_helpers.rb +758 -0
  44. data/lib/clickwrap/current_state.rb +282 -0
  45. data/lib/clickwrap/digest.rb +125 -0
  46. data/lib/clickwrap/doctor.rb +418 -0
  47. data/lib/clickwrap/document_definition.rb +255 -0
  48. data/lib/clickwrap/document_renderer.rb +83 -0
  49. data/lib/clickwrap/document_renderers/markdown.rb +175 -0
  50. data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
  51. data/lib/clickwrap/dsl/policy_builder.rb +462 -0
  52. data/lib/clickwrap/dsl/retention_builder.rb +89 -0
  53. data/lib/clickwrap/durable_commit_callback.rb +37 -0
  54. data/lib/clickwrap/engine.rb +184 -0
  55. data/lib/clickwrap/errors.rb +181 -0
  56. data/lib/clickwrap/form_builder_extensions.rb +341 -0
  57. data/lib/clickwrap/front_matter.rb +67 -0
  58. data/lib/clickwrap/identifier.rb +112 -0
  59. data/lib/clickwrap/import/external_receipt.rb +241 -0
  60. data/lib/clickwrap/import/fine_print.rb +290 -0
  61. data/lib/clickwrap/import/legacy.rb +450 -0
  62. data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
  63. data/lib/clickwrap/integrity/anchor.rb +130 -0
  64. data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
  65. data/lib/clickwrap/integrity/attestor.rb +221 -0
  66. data/lib/clickwrap/integrity/chain.rb +313 -0
  67. data/lib/clickwrap/integrity/timestamp.rb +143 -0
  68. data/lib/clickwrap/ip_geolocation/location.rb +112 -0
  69. data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
  70. data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
  71. data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
  72. data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
  73. data/lib/clickwrap/ip_geolocation.rb +16 -0
  74. data/lib/clickwrap/lifecycle.rb +534 -0
  75. data/lib/clickwrap/linter.rb +382 -0
  76. data/lib/clickwrap/localized_text.rb +101 -0
  77. data/lib/clickwrap/macros.rb +203 -0
  78. data/lib/clickwrap/models/application_record.rb +20 -0
  79. data/lib/clickwrap/models/chain_head.rb +79 -0
  80. data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
  81. data/lib/clickwrap/models/disposition_plan.rb +208 -0
  82. data/lib/clickwrap/models/document.rb +46 -0
  83. data/lib/clickwrap/models/document_version.rb +163 -0
  84. data/lib/clickwrap/models/event.rb +743 -0
  85. data/lib/clickwrap/models/event_document.rb +79 -0
  86. data/lib/clickwrap/models/event_statement.rb +92 -0
  87. data/lib/clickwrap/models/external_action.rb +150 -0
  88. data/lib/clickwrap/models/integrity_attestation.rb +90 -0
  89. data/lib/clickwrap/models/legal_hold.rb +81 -0
  90. data/lib/clickwrap/models/policy_revision.rb +115 -0
  91. data/lib/clickwrap/models/presentation.rb +59 -0
  92. data/lib/clickwrap/models/receipt_access.rb +53 -0
  93. data/lib/clickwrap/models/recording_sequence.rb +21 -0
  94. data/lib/clickwrap/models/request_evidence.rb +378 -0
  95. data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
  96. data/lib/clickwrap/models/statement_state.rb +130 -0
  97. data/lib/clickwrap/pending_receipt.rb +177 -0
  98. data/lib/clickwrap/policy.rb +283 -0
  99. data/lib/clickwrap/presentation_manifest.rb +210 -0
  100. data/lib/clickwrap/presenter.rb +716 -0
  101. data/lib/clickwrap/privacy.rb +419 -0
  102. data/lib/clickwrap/protected_outcome.rb +120 -0
  103. data/lib/clickwrap/receipt.rb +606 -0
  104. data/lib/clickwrap/receipt_html.rb +235 -0
  105. data/lib/clickwrap/receipt_verifier.rb +978 -0
  106. data/lib/clickwrap/reference.rb +44 -0
  107. data/lib/clickwrap/registration.rb +236 -0
  108. data/lib/clickwrap/registry.rb +54 -0
  109. data/lib/clickwrap/remediation_token.rb +155 -0
  110. data/lib/clickwrap/request_evidence_extractor.rb +590 -0
  111. data/lib/clickwrap/request_evidence_policy.rb +261 -0
  112. data/lib/clickwrap/retention/applier.rb +231 -0
  113. data/lib/clickwrap/retention/disposition.rb +221 -0
  114. data/lib/clickwrap/retention/planner.rb +502 -0
  115. data/lib/clickwrap/retention_class.rb +97 -0
  116. data/lib/clickwrap/reviewed_text.rb +28 -0
  117. data/lib/clickwrap/schema_requirements.rb +196 -0
  118. data/lib/clickwrap/services/authorize_external_action.rb +149 -0
  119. data/lib/clickwrap/services/load_policies.rb +69 -0
  120. data/lib/clickwrap/services/publish_documents.rb +251 -0
  121. data/lib/clickwrap/services/validate_policy_references.rb +166 -0
  122. data/lib/clickwrap/statement.rb +248 -0
  123. data/lib/clickwrap/subject_fingerprint.rb +28 -0
  124. data/lib/clickwrap/submission.rb +169 -0
  125. data/lib/clickwrap/system_actor.rb +31 -0
  126. data/lib/clickwrap/test_helpers.rb +676 -0
  127. data/lib/clickwrap/testing.rb +211 -0
  128. data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
  129. data/lib/clickwrap/verification.rb +504 -0
  130. data/lib/clickwrap/version.rb +12 -1
  131. data/lib/clickwrap/view_helpers.rb +190 -0
  132. data/lib/clickwrap/vocabulary.rb +294 -0
  133. data/lib/clickwrap.rb +497 -7
  134. data/lib/generators/clickwrap/document_generator.rb +164 -0
  135. data/lib/generators/clickwrap/hardening_generator.rb +177 -0
  136. data/lib/generators/clickwrap/install_generator.rb +1287 -0
  137. data/lib/generators/clickwrap/link_generator.rb +56 -0
  138. data/lib/generators/clickwrap/policy_generator.rb +118 -0
  139. data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
  140. data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
  141. data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
  142. data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
  143. data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
  144. data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
  145. data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
  146. data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
  147. data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
  148. data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
  149. data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
  150. data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
  151. data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
  152. data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
  153. data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
  154. data/lib/generators/clickwrap/views_generator.rb +101 -0
  155. data/lib/tasks/clickwrap.rake +569 -0
  156. metadata +311 -16
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ # Require the sanitizer implementation without loading the Action View helper
5
+ # facade. Requiring `rails-html-sanitizer` while Action View is booting can
6
+ # create a circular require; these are the library's implementation entrypoints
7
+ # and do not install unrelated helper methods.
8
+ require "loofah"
9
+ require "rails/html/scrubbers"
10
+ require "rails/html/sanitizer"
11
+
12
+ module Clickwrap
13
+ # Safe, dependency-free rendering for the text formats most legal documents
14
+ # use. It intentionally favors faithful, readable text over clever Markdown
15
+ # interpretation: source characters are escaped, then placed in a <pre>
16
+ # element. Applications that want richer Markdown may supply a renderer, but
17
+ # its HTML still passes through #sanitize_html before Clickwrap stores it.
18
+ class DocumentRenderer
19
+ NAME = "clickwrap_safe_document_renderer"
20
+ VERSION = "1"
21
+ SANITIZER_NAME = "rails_safe_list_sanitizer"
22
+ SANITIZER_VERSION = "1"
23
+
24
+ SAFE_TAGS = %w[
25
+ a abbr b blockquote br cite code dd del details div dl dt em h1 h2 h3 h4 h5 h6
26
+ hr i ins kbd li mark ol p pre q s samp small span strong sub summary sup table
27
+ tbody td tfoot th thead tr u ul var
28
+ ].freeze
29
+ SAFE_ATTRIBUTES = %w[href title lang dir class id colspan rowspan scope].freeze
30
+
31
+ def call(bytes, definition)
32
+ text = utf8_text!(bytes, definition)
33
+
34
+ html = case definition.media_type
35
+ when "text/html" then text
36
+ when "text/markdown", "text/plain", "application/json"
37
+ %(<pre class="clickwrap-document-source">#{ERB::Util.html_escape(text)}</pre>)
38
+ else
39
+ return nil
40
+ end
41
+
42
+ {
43
+ bytes: self.class.sanitize_html(html),
44
+ media_type: "text/html; charset=utf-8",
45
+ renderer_name: NAME,
46
+ renderer_version: VERSION,
47
+ sanitizer_name: SANITIZER_NAME,
48
+ sanitizer_version: SANITIZER_VERSION
49
+ }
50
+ end
51
+
52
+ def self.sanitize_html(html)
53
+ safe_list_sanitizer_class.new.sanitize(
54
+ html.to_s,
55
+ tags: SAFE_TAGS,
56
+ attributes: SAFE_ATTRIBUTES
57
+ ).to_s
58
+ end
59
+
60
+ def self.safe_list_sanitizer_class
61
+ return Rails::HTML5::SafeListSanitizer if defined?(Rails::HTML5::SafeListSanitizer)
62
+ return Rails::HTML4::SafeListSanitizer if defined?(Rails::HTML4::SafeListSanitizer)
63
+
64
+ raise ConfigurationError,
65
+ "Clickwrap could not find Rails::HTML5::SafeListSanitizer or " \
66
+ "Rails::HTML4::SafeListSanitizer. Install a rails-html-sanitizer version that " \
67
+ "provides one of those supported safe-list sanitizers."
68
+ end
69
+ private_class_method :safe_list_sanitizer_class
70
+
71
+ private
72
+
73
+ def utf8_text!(bytes, definition)
74
+ text = bytes.to_s.dup.force_encoding(Encoding::UTF_8)
75
+ return text if text.valid_encoding?
76
+
77
+ raise DefinitionError,
78
+ "Document #{definition} cannot be rendered as text because its bytes are not valid UTF-8. " \
79
+ "Store binary source with :active_storage (or a byte-preserving resolver) and provide " \
80
+ "a reviewed renderer for the representation people should be offered."
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clickwrap
4
+ module DocumentRenderers
5
+ # Optional Markdown rendering for the representation people are offered.
6
+ #
7
+ # The reference renderer deliberately escapes Markdown into a <pre> block:
8
+ # faithful, dependency-free, and ugly for a forty-page Terms document. This
9
+ # renderer produces real HTML instead, using whichever Markdown library the
10
+ # application already bundles — it is opt-in and it adds no dependency:
11
+ #
12
+ # Clickwrap.configure do |config|
13
+ # config.document_renderer = :markdown
14
+ # end
15
+ #
16
+ # Three engines are recognized, in order: Commonmarker (1.x and the older
17
+ # CommonMarker 0.x), Redcarpet, and Kramdown. An application whose Markdown
18
+ # dialect needs specific options can inject its own rendering instead and
19
+ # must name it, because the receipt records which renderer produced the
20
+ # offered bytes:
21
+ #
22
+ # config.document_renderer = Clickwrap::DocumentRenderers::Markdown.new(
23
+ # render_markdown_with: ->(markdown_text) { MyMarkdown.render(markdown_text) },
24
+ # engine_name: "my_markdown",
25
+ # engine_version: MyMarkdown::VERSION
26
+ # )
27
+ #
28
+ # A leading YAML front-matter block (the Jekyll/Sitepress convention) is
29
+ # stripped from the RENDERED representation only — the source digest still
30
+ # covers the exact file bytes, front matter included. Bundled engines'
31
+ # output always passes through the same safe-list sanitizer as the
32
+ # reference renderer, so a Markdown library's raw-HTML passthrough cannot
33
+ # smuggle markup into an offer. A host-supplied `render_markdown_with:`
34
+ # may additionally pass `sanitize_rendered_html: false` when its own pages
35
+ # serve the renderer's output verbatim: re-sanitizing reparses the HTML
36
+ # and changes its bytes, and a stored digest must describe exactly what
37
+ # readers were shown. The provenance records which choice was made.
38
+ # Non-Markdown documents delegate to the reference renderer unchanged.
39
+ class Markdown
40
+ NAME = "clickwrap_markdown_document_renderer"
41
+ VERSION = "1"
42
+
43
+ def initialize(render_markdown_with: nil, engine_name: nil, engine_version: nil,
44
+ sanitize_rendered_html: true)
45
+ @sanitize_rendered_html = sanitize_rendered_html != false
46
+
47
+ if !@sanitize_rendered_html && render_markdown_with.nil?
48
+ raise ConfigurationError,
49
+ "sanitize_rendered_html: false is only available with your own " \
50
+ "render_markdown_with. Clickwrap's bundled engines always sanitize; skipping " \
51
+ "sanitization is a promise about a rendering pipeline YOU own — that its " \
52
+ "output is exactly what your own pages already serve to readers."
53
+ end
54
+
55
+ if render_markdown_with
56
+ unless render_markdown_with.respond_to?(:call)
57
+ raise ConfigurationError,
58
+ "render_markdown_with must respond to call(markdown_text) and return HTML."
59
+ end
60
+ if engine_name.to_s.strip.empty?
61
+ raise ConfigurationError,
62
+ "A custom render_markdown_with needs an engine_name (and ideally an " \
63
+ "engine_version), because receipts record which renderer produced the " \
64
+ "bytes a person was offered."
65
+ end
66
+
67
+ @render_markdown = render_markdown_with
68
+ @engine_name = engine_name.to_s
69
+ @engine_version = engine_version.to_s.strip.empty? ? "unstated" : engine_version.to_s
70
+ else
71
+ @render_markdown, @engine_name, @engine_version = resolve_bundled_engine
72
+ end
73
+
74
+ @fallback = DocumentRenderer.new
75
+ end
76
+
77
+ attr_reader :engine_name, :engine_version
78
+
79
+ def call(bytes, definition)
80
+ return @fallback.call(bytes, definition) unless definition.media_type == "text/markdown"
81
+
82
+ text = utf8_text!(bytes, definition)
83
+ html = @render_markdown.call(FrontMatter.strip(text))
84
+
85
+ if @sanitize_rendered_html
86
+ {
87
+ bytes: DocumentRenderer.sanitize_html(html),
88
+ media_type: "text/html; charset=utf-8",
89
+ renderer_name: NAME,
90
+ renderer_version: "#{VERSION} (#{@engine_name} #{@engine_version})",
91
+ sanitizer_name: DocumentRenderer::SANITIZER_NAME,
92
+ sanitizer_version: DocumentRenderer::SANITIZER_VERSION
93
+ }
94
+ else
95
+ # Byte parity with the host's own pages is the point: re-sanitizing
96
+ # reparses and re-serializes the HTML, so the stored digest would
97
+ # describe bytes nobody was ever shown. The provenance says plainly
98
+ # that no sanitizer ran — the host's rendering pipeline owns safety.
99
+ {
100
+ bytes: html.to_s.dup.force_encoding(Encoding::UTF_8),
101
+ media_type: "text/html; charset=utf-8",
102
+ renderer_name: NAME,
103
+ renderer_version: "#{VERSION} (#{@engine_name} #{@engine_version})",
104
+ sanitizer_name: "none",
105
+ sanitizer_version: "host_renderer_output_stored_verbatim"
106
+ }
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def resolve_bundled_engine
113
+ commonmarker_engine || redcarpet_engine || kramdown_engine ||
114
+ raise(ConfigurationError,
115
+ "config.document_renderer = :markdown needs a Markdown library, and none of " \
116
+ "the ones Clickwrap recognizes (commonmarker, redcarpet, kramdown) is " \
117
+ "available in this application. Add one of them to your Gemfile, or supply " \
118
+ "your own rendering with " \
119
+ "Clickwrap::DocumentRenderers::Markdown.new(render_markdown_with:, engine_name:).")
120
+ end
121
+
122
+ def commonmarker_engine
123
+ attempt_require("commonmarker")
124
+
125
+ if defined?(::Commonmarker)
126
+ [->(text) { ::Commonmarker.to_html(text) }, "commonmarker", loaded_gem_version("commonmarker")]
127
+ elsif defined?(::CommonMarker)
128
+ [->(text) { ::CommonMarker.render_html(text) }, "commonmarker", loaded_gem_version("commonmarker")]
129
+ end
130
+ end
131
+
132
+ def redcarpet_engine
133
+ attempt_require("redcarpet")
134
+ return unless defined?(::Redcarpet::Markdown)
135
+
136
+ # A fresh renderer per call: Redcarpet's renderer objects carry state
137
+ # between renders and are not safe to share across threads.
138
+ render = lambda do |text|
139
+ ::Redcarpet::Markdown.new(
140
+ ::Redcarpet::Render::HTML.new(with_toc_data: true),
141
+ tables: true, autolink: true, fenced_code_blocks: true, strikethrough: true,
142
+ footnotes: true, no_intra_emphasis: true
143
+ ).render(text)
144
+ end
145
+ [render, "redcarpet", loaded_gem_version("redcarpet")]
146
+ end
147
+
148
+ def kramdown_engine
149
+ attempt_require("kramdown")
150
+ return unless defined?(::Kramdown::Document)
151
+
152
+ [->(text) { ::Kramdown::Document.new(text).to_html }, "kramdown", loaded_gem_version("kramdown")]
153
+ end
154
+
155
+ def attempt_require(name)
156
+ require name
157
+ rescue LoadError
158
+ nil
159
+ end
160
+
161
+ def loaded_gem_version(name)
162
+ Gem.loaded_specs[name]&.version&.to_s || "unstated"
163
+ end
164
+
165
+ def utf8_text!(bytes, definition)
166
+ text = bytes.to_s.dup.force_encoding(Encoding::UTF_8)
167
+ return text if text.valid_encoding?
168
+
169
+ raise DefinitionError,
170
+ "Document #{definition} cannot be rendered as Markdown because its bytes are " \
171
+ "not valid UTF-8."
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clickwrap
4
+ module DocumentRenderers
5
+ # Byte parity with the application's own pages, by construction.
6
+ #
7
+ # Content-file Rails apps (Sitepress and friends) render their public
8
+ # legal pages through the `markdown-rails` gem, whose initializer
9
+ # registers a renderer for `.md` templates:
10
+ #
11
+ # MarkdownRails.handle :md do
12
+ # ApplicationMarkdown.new
13
+ # end
14
+ #
15
+ # When the same files are also Clickwrap documents, what people accept and
16
+ # what the public page serves must be the SAME BYTES — same renderer, same
17
+ # options, no re-sanitization pass changing entities behind the digest.
18
+ # Wiring that by hand means a lambda, an engine name, gem versions, and a
19
+ # parity test, all copied between applications. This renderer says it once:
20
+ #
21
+ # Clickwrap.configure do |config|
22
+ # config.document_renderer = :markdown_rails
23
+ # end
24
+ #
25
+ # At render time it asks markdown-rails for the exact renderer object the
26
+ # application's own `.md` pages go through, renders through it, stores the
27
+ # output verbatim (`sanitizer_name: "none"` in the provenance — safety
28
+ # stays where it already lives, in the pipeline every public page trusts),
29
+ # and records honest provenance: the renderer class the application
30
+ # registered, plus the markdown-rails and Markdown-engine gem versions.
31
+ #
32
+ # The handler is resolved LAZILY, at first render, never at configuration
33
+ # time: Rails initializers run alphabetically, so `clickwrap.rb` usually
34
+ # runs before `markdown.rb`, and an eager lookup would capture the stock
35
+ # default renderer instead of the application's own.
36
+ #
37
+ # Honest bound: the renderer is called directly, without a view context —
38
+ # exactly how a frozen document must render, because a snapshot cannot
39
+ # depend on the request it happened to be published during. Markdown that
40
+ # calls Rails view helpers (image_tag and friends) fails loudly at publish;
41
+ # documents like that need the explicit lambda form of
42
+ # Clickwrap::DocumentRenderers::Markdown instead.
43
+ class MarkdownRails
44
+ HANDLER_EXTENSION = :md
45
+
46
+ def initialize
47
+ # The gem must be bundled for this choice to ever work, and a missing
48
+ # bundle is a configuration mistake worth failing the boot for. The
49
+ # HANDLER may legitimately not be registered yet — see the class
50
+ # comment — so that half of the check waits until render time.
51
+ #
52
+ # The handler file loads first on purpose: markdown-rails' engine runs
53
+ # an on_load(:action_view) hook that references its own Handler
54
+ # constant, and when Action View is already loaded (it is, here) that
55
+ # hook fires mid-require, before the entry file has declared the
56
+ # autoload the hook depends on.
57
+ begin
58
+ require "markdown-rails/handler"
59
+ require "markdown-rails"
60
+ rescue LoadError
61
+ raise ConfigurationError,
62
+ "config.document_renderer = :markdown_rails needs the markdown-rails gem, and " \
63
+ "it is not in this application's bundle. Add `gem \"markdown-rails\"` (the " \
64
+ "Sitepress content stack), or render through your own pipeline with " \
65
+ "Clickwrap::DocumentRenderers::Markdown.new(render_markdown_with:, engine_name:)."
66
+ end
67
+
68
+ @fallback = DocumentRenderer.new
69
+ end
70
+
71
+ def call(bytes, definition)
72
+ return @fallback.call(bytes, definition) unless definition.media_type == "text/markdown"
73
+
74
+ application_markdown_renderer_for(definition).call(bytes, definition)
75
+ end
76
+
77
+ # The provenance the application's registered renderer would produce
78
+ # right now — exposed so hosts and tests can ask "which renderer will my
79
+ # documents publish through?" in one call.
80
+ def engine_name
81
+ "markdown_rails/#{registered_renderer_class_name}"
82
+ end
83
+
84
+ def engine_version
85
+ %w[markdown-rails redcarpet commonmarker kramdown].filter_map do |gem_name|
86
+ spec = Gem.loaded_specs[gem_name]
87
+ "#{gem_name} #{spec.version}" if spec
88
+ end.join(" / ").presence || "unstated"
89
+ end
90
+
91
+ private
92
+
93
+ # A fresh delegate per call, resolved from the registry markdown-rails
94
+ # keeps: publish-time work, and late binding keeps development reloads of
95
+ # the application's renderer class honest.
96
+ def application_markdown_renderer_for(definition)
97
+ renderer = registered_renderer(definition)
98
+
99
+ Markdown.new(
100
+ render_markdown_with: ->(markdown_text) { renderer.renderer.render(markdown_text) },
101
+ engine_name: "markdown_rails/#{renderer.class.name}",
102
+ engine_version: engine_version,
103
+ sanitize_rendered_html: false
104
+ )
105
+ end
106
+
107
+ def registered_renderer(definition = nil)
108
+ handler = ::MarkdownRails::Handler.handler_for(HANDLER_EXTENSION)
109
+ if handler.nil?
110
+ raise ConfigurationError,
111
+ "config.document_renderer = :markdown_rails found no markdown-rails handler " \
112
+ "registered for .#{HANDLER_EXTENSION}#{" while rendering document #{definition}" if definition}. " \
113
+ "Register one in config/initializers/markdown.rb — " \
114
+ "`MarkdownRails.handle :md { ApplicationMarkdown.new }` — so Clickwrap can " \
115
+ "render documents through the exact pipeline your own pages use."
116
+ end
117
+
118
+ handler.create_renderer
119
+ end
120
+
121
+ def registered_renderer_class_name
122
+ registered_renderer.class.name
123
+ end
124
+ end
125
+ end
126
+ end