embeddable_content 0.1.19

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 (141) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +62 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +15 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +49 -0
  8. data/Rakefile +22 -0
  9. data/app/assets/config/embeddable_content_manifest.js +0 -0
  10. data/app/assets/images/embeddable_content/.keep +0 -0
  11. data/app/assets/javascripts/desmos/support.js +12 -0
  12. data/app/assets/javascripts/geogebra/support.js +14 -0
  13. data/app/assets/stylesheets/embeddable_content/.keep +0 -0
  14. data/app/controllers/.keep +0 -0
  15. data/app/controllers/concerns/cms/embeddable_content_controller.rb +14 -0
  16. data/app/controllers/concerns/embeddable_content_controller.rb +85 -0
  17. data/app/helpers/.keep +0 -0
  18. data/app/mailers/.keep +0 -0
  19. data/app/models/.keep +0 -0
  20. data/app/models/concerns/provides_embeddable_content.rb +59 -0
  21. data/app/models/embeddable_model_config.rb +55 -0
  22. data/app/models/embedder_config.rb +45 -0
  23. data/app/models/embedding.rb +25 -0
  24. data/app/services/embeddable_content/desmos_files/doc_processor.rb +5 -0
  25. data/app/services/embeddable_content/desmos_files/node_processor.rb +7 -0
  26. data/app/services/embeddable_content/doc_processor.rb +63 -0
  27. data/app/services/embeddable_content/embedded_tag_info.rb +50 -0
  28. data/app/services/embeddable_content/embedded_tags.rb +30 -0
  29. data/app/services/embeddable_content/embedder.rb +89 -0
  30. data/app/services/embeddable_content/embedder_base.rb +68 -0
  31. data/app/services/embeddable_content/fragment_embedder.rb +30 -0
  32. data/app/services/embeddable_content/geogebra_files/doc_processor.rb +5 -0
  33. data/app/services/embeddable_content/geogebra_files/node_processor.rb +11 -0
  34. data/app/services/embeddable_content/html_tags/doc_processor.rb +11 -0
  35. data/app/services/embeddable_content/html_tags/node_processor.rb +26 -0
  36. data/app/services/embeddable_content/images/attributions_processor.rb +60 -0
  37. data/app/services/embeddable_content/images/doc_processor.rb +54 -0
  38. data/app/services/embeddable_content/images/image_downloader.rb +60 -0
  39. data/app/services/embeddable_content/images/img_tag_attributes.rb +125 -0
  40. data/app/services/embeddable_content/images/modal_dialog.rb +74 -0
  41. data/app/services/embeddable_content/images/node_processor.rb +91 -0
  42. data/app/services/embeddable_content/images/shared.rb +11 -0
  43. data/app/services/embeddable_content/node_processor.rb +75 -0
  44. data/app/services/embeddable_content/presentation_tags/doc_processor.rb +5 -0
  45. data/app/services/embeddable_content/presentation_tags/node_processor.rb +35 -0
  46. data/app/services/embeddable_content/record_node_processor.rb +90 -0
  47. data/app/services/embeddable_content/replacement_template_manager.rb +21 -0
  48. data/app/services/embeddable_content/sad_embedded_tags.rb +29 -0
  49. data/app/services/embeddable_content/scrubber.rb +32 -0
  50. data/app/services/embeddable_content/template_based.rb +19 -0
  51. data/app/services/embeddable_content/template_manager.rb +100 -0
  52. data/app/services/embeddable_content/tex/base_renderer.rb +33 -0
  53. data/app/services/embeddable_content/tex/canvas_renderer.rb +15 -0
  54. data/app/services/embeddable_content/tex/doc_processor.rb +55 -0
  55. data/app/services/embeddable_content/tex/mathjax_renderer.rb +11 -0
  56. data/app/services/embeddable_content/tex/mml_renderer.rb +11 -0
  57. data/app/services/embeddable_content/tex/schoology_string_renderer.rb +15 -0
  58. data/app/services/embeddable_content/tex/svg_renderer.rb +11 -0
  59. data/app/services/embeddable_content/token_replacement_map.rb +32 -0
  60. data/app/services/embeddable_content/tree_based_node_processor.rb +23 -0
  61. data/app/services/embeddable_content/video_links/doc_processor.rb +5 -0
  62. data/app/services/embeddable_content/video_links/node_processor.rb +46 -0
  63. data/app/services/embeddable_content/video_links/vimeo_player_settings.rb +19 -0
  64. data/app/services/embeddable_content/visual_element_node_processor.rb +13 -0
  65. data/app/services/embeddable_content/widget_files/doc_processor.rb +5 -0
  66. data/app/services/embeddable_content/widget_files/node_processor.rb +7 -0
  67. data/app/views/.keep +0 -0
  68. data/app/views/embeddable_content/replacements/desmos_files/_applet.html.slim +8 -0
  69. data/app/views/embeddable_content/replacements/desmos_files/_description.html.slim +6 -0
  70. data/app/views/embeddable_content/replacements/desmos_files/cc.html.slim +1 -0
  71. data/app/views/embeddable_content/replacements/desmos_files/cms.html.slim +1 -0
  72. data/app/views/embeddable_content/replacements/desmos_files/editable.html.slim +1 -0
  73. data/app/views/embeddable_content/replacements/desmos_files/exported.html.slim +1 -0
  74. data/app/views/embeddable_content/replacements/desmos_files/kiddom.html.slim +1 -0
  75. data/app/views/embeddable_content/replacements/desmos_files/print.html.slim +1 -0
  76. data/app/views/embeddable_content/replacements/desmos_files/qti.html.slim +1 -0
  77. data/app/views/embeddable_content/replacements/desmos_files/schoology.html.slim +1 -0
  78. data/app/views/embeddable_content/replacements/desmos_files/web.html.slim +1 -0
  79. data/app/views/embeddable_content/replacements/geogebra_files/_applet.html.slim +11 -0
  80. data/app/views/embeddable_content/replacements/geogebra_files/_description.html.slim +9 -0
  81. data/app/views/embeddable_content/replacements/geogebra_files/cc.html.slim +1 -0
  82. data/app/views/embeddable_content/replacements/geogebra_files/cms.html.slim +1 -0
  83. data/app/views/embeddable_content/replacements/geogebra_files/editable.html.slim +1 -0
  84. data/app/views/embeddable_content/replacements/geogebra_files/exported.html.slim +1 -0
  85. data/app/views/embeddable_content/replacements/geogebra_files/kiddom.html.slim +1 -0
  86. data/app/views/embeddable_content/replacements/geogebra_files/print.html.slim +1 -0
  87. data/app/views/embeddable_content/replacements/geogebra_files/qti.html.slim +1 -0
  88. data/app/views/embeddable_content/replacements/geogebra_files/schoology.html.slim +1 -0
  89. data/app/views/embeddable_content/replacements/geogebra_files/web.html.slim +1 -0
  90. data/app/views/embeddable_content/replacements/html_tags/editable.html.slim +16 -0
  91. data/app/views/embeddable_content/replacements/images/_button_close.html.slim +9 -0
  92. data/app/views/embeddable_content/replacements/images/_button_open.html.slim +9 -0
  93. data/app/views/embeddable_content/replacements/images/_image_embed.html.slim +14 -0
  94. data/app/views/embeddable_content/replacements/images/_modal_content.html.slim +30 -0
  95. data/app/views/embeddable_content/replacements/images/_modal_dialog.html.slim +17 -0
  96. data/app/views/embeddable_content/replacements/images/cc.html.slim +12 -0
  97. data/app/views/embeddable_content/replacements/images/cms.html.slim +1 -0
  98. data/app/views/embeddable_content/replacements/images/editable.html.slim +1 -0
  99. data/app/views/embeddable_content/replacements/images/exported.html.slim +1 -0
  100. data/app/views/embeddable_content/replacements/images/kiddom.html.slim +12 -0
  101. data/app/views/embeddable_content/replacements/images/print.html.slim +1 -0
  102. data/app/views/embeddable_content/replacements/images/qti.html.slim +6 -0
  103. data/app/views/embeddable_content/replacements/images/schoology.html.slim +12 -0
  104. data/app/views/embeddable_content/replacements/images/web.html.slim +1 -0
  105. data/app/views/embeddable_content/replacements/presentation_tags/_default.html.slim +6 -0
  106. data/app/views/embeddable_content/replacements/presentation_tags/print.html.slim +1 -0
  107. data/app/views/embeddable_content/replacements/status/warning.html.slim +12 -0
  108. data/app/views/embeddable_content/replacements/video_links/_caption.html.slim +13 -0
  109. data/app/views/embeddable_content/replacements/video_links/_description.html.slim +12 -0
  110. data/app/views/embeddable_content/replacements/video_links/_video_embed.html.slim +13 -0
  111. data/app/views/embeddable_content/replacements/video_links/_video_player.html.slim +6 -0
  112. data/app/views/embeddable_content/replacements/video_links/_vimeo_player.html.slim +6 -0
  113. data/app/views/embeddable_content/replacements/video_links/cc.html.slim +1 -0
  114. data/app/views/embeddable_content/replacements/video_links/cms.html.slim +1 -0
  115. data/app/views/embeddable_content/replacements/video_links/editable.html.slim +1 -0
  116. data/app/views/embeddable_content/replacements/video_links/exported.html.slim +1 -0
  117. data/app/views/embeddable_content/replacements/video_links/kiddom.html.slim +1 -0
  118. data/app/views/embeddable_content/replacements/video_links/print.html.slim +1 -0
  119. data/app/views/embeddable_content/replacements/video_links/qti.html.slim +1 -0
  120. data/app/views/embeddable_content/replacements/video_links/schoology.html.slim +1 -0
  121. data/app/views/embeddable_content/replacements/video_links/web.html.slim +1 -0
  122. data/app/views/embeddable_content/replacements/widget_files/_widget_omitted.html.slim +8 -0
  123. data/app/views/embeddable_content/replacements/widget_files/_widget_script.html.slim +13 -0
  124. data/app/views/embeddable_content/replacements/widget_files/cc.html.slim +1 -0
  125. data/app/views/embeddable_content/replacements/widget_files/cms.html.slim +1 -0
  126. data/app/views/embeddable_content/replacements/widget_files/editable.html.slim +1 -0
  127. data/app/views/embeddable_content/replacements/widget_files/exported.html.slim +1 -0
  128. data/app/views/embeddable_content/replacements/widget_files/kiddom.html.slim +1 -0
  129. data/app/views/embeddable_content/replacements/widget_files/print.html.slim +1 -0
  130. data/app/views/embeddable_content/replacements/widget_files/qti.html.slim +1 -0
  131. data/app/views/embeddable_content/replacements/widget_files/schoology.html.slim +1 -0
  132. data/app/views/embeddable_content/replacements/widget_files/web.html.slim +1 -0
  133. data/bin/rails +25 -0
  134. data/config/initializers/embeddable_content.rb +3 -0
  135. data/config/routes.rb +2 -0
  136. data/embeddable_content.gemspec +48 -0
  137. data/lib/embeddable_content/engine.rb +8 -0
  138. data/lib/embeddable_content/version.rb +3 -0
  139. data/lib/embeddable_content.rb +3 -0
  140. data/lib/tasks/embeddable_content_tasks.rake +4 -0
  141. metadata +309 -0
@@ -0,0 +1,30 @@
1
+ module EmbeddableContent
2
+ class EmbeddedTags
3
+ attr_reader :record, :attr, :embedded_models
4
+
5
+ def initialize(record, attr, embedded_models)
6
+ @record = record
7
+ @attr = attr
8
+ @embedded_models = embedded_models
9
+ end
10
+
11
+ def each
12
+ embedded_models.each do |model|
13
+ document.css(model.embeddable_tag_name).each do |dom_node|
14
+ info = EmbeddedTagInfo.new record, model, attr, dom_node
15
+ yield info if info.available?
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def document
23
+ @document ||= Nokogiri::HTML.fragment searchable_text
24
+ end
25
+
26
+ def searchable_text
27
+ record[attr].is_a?(Array) ? record[attr].join(' ') : record[attr]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,89 @@
1
+ module EmbeddableContent
2
+ class Embedder < EmbedderBase
3
+ NO_REPLACEMENTS_BY_DEFAULT = {}.freeze
4
+
5
+ attr_reader :html, :document
6
+
7
+ def initialize(target, options = {})
8
+ super EmbedderConfig.for_target(target), options
9
+ end
10
+
11
+ def image_catalog
12
+ @image_catalog ||= Set.new
13
+ end
14
+
15
+ def embed_content!(content)
16
+ @html = content.to_str.dup
17
+ @document = scrub parse_html
18
+ perform_replacements
19
+ run_doc_processors
20
+ write_embedded_html_to_tmp_file if debug_embedder?
21
+ @html
22
+ end
23
+
24
+ def rebuild_document
25
+ @document = parse_html
26
+ end
27
+
28
+ private
29
+
30
+ def parse_html
31
+ case scope&.to_sym
32
+ when :document then html_parser.parse html
33
+ when :fragment then html_parser.fragment html
34
+ else raise "Unrecognized parsing scope: #{scope}"
35
+ end
36
+ end
37
+
38
+ def run_doc_processors
39
+ doc_processors.each(&:process!)
40
+ end
41
+
42
+ def html_parser
43
+ @html_parser ||=
44
+ case output_format&.to_sym
45
+ when :html then Nokogiri::HTML
46
+ when :xml then Nokogiri::XML
47
+ else raise "Unrecognized output format: #{output_format}"
48
+ end
49
+ end
50
+
51
+ def scrub(document)
52
+ Scrubber.new(config).scrub document
53
+ end
54
+
55
+ def perform_replacements
56
+ replacements.each { |old, new| html.gsub! old, new }
57
+ end
58
+
59
+ def replacements
60
+ token_replacement_map[target] || NO_REPLACEMENTS_BY_DEFAULT
61
+ end
62
+
63
+ def token_replacement_map
64
+ @token_replacement_map ||=
65
+ TokenReplacementMap.new(replacement_map).target_regexp_map
66
+ end
67
+
68
+ def doc_processors
69
+ @doc_processors ||=
70
+ embeddings.map { |embedding| embedding.doc_processor_for self }
71
+ end
72
+
73
+ def embeddings
74
+ @embeddings ||= Embedding.all.in_run_order
75
+ end
76
+
77
+ def debug_embedder?
78
+ ENV['DEBUG_EMBEDDER'] == 'true'
79
+ end
80
+
81
+ def write_embedded_html_to_tmp_file
82
+ Rails.root.join('tmp').join(temp_filename).write html
83
+ end
84
+
85
+ def temp_filename
86
+ Time.current.strftime "embedded-#{target}-%Y-%m-%d-%H%M.html"
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,68 @@
1
+ require Rails.root.join 'lib/tasks/support/verbosity.rb'
2
+
3
+ module EmbeddableContent
4
+ class EmbedderBase
5
+ include HtmlService::Helpers
6
+ include Verbosity
7
+
8
+ attr_reader :config, :options
9
+
10
+ delegate :tex_output_format, :render_images?, :scope, :xml?,
11
+ :output_format, :fragment?, :remove_repaired_math_spans?,
12
+ :replacement_map, :all_other_targets, :aria_attrs?,
13
+ to: :config
14
+
15
+ def self.default_s3_bucket
16
+ ENV['AWS_S3_BUCKET']
17
+ end
18
+
19
+ def initialize(config, options = {})
20
+ @config = config
21
+ @options = options
22
+ show_options if show_options?
23
+ end
24
+
25
+ def target
26
+ @target ||= config.target.to_sym
27
+ end
28
+
29
+ def to_s
30
+ "#{self.class} for #{config.inspect}"
31
+ end
32
+
33
+ def s3_bucket
34
+ @s3_bucket ||= options[:s3_bucket] ||
35
+ self.class.default_s3_bucket
36
+ end
37
+
38
+ def ed_node
39
+ @ed_node ||= options[:ed_node]
40
+ end
41
+
42
+ def tree_node
43
+ @tree_node ||= ed_node&.root
44
+ end
45
+
46
+ def tree
47
+ @tree ||= tree_node&.ref
48
+ end
49
+
50
+ def locale
51
+ @locale ||= tree&.locale
52
+ end
53
+
54
+ private
55
+
56
+ def module_name
57
+ @module_name ||= self.class.name.deconstantize
58
+ end
59
+
60
+ def embedding_module
61
+ @embedding_module ||= module_name.demodulize
62
+ end
63
+
64
+ def route_keys
65
+ @route_keys ||= [embedding_module.underscore]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,30 @@
1
+ module EmbeddableContent
2
+ class FragmentEmbedder < EmbedderBase
3
+ attr_reader :fragment
4
+
5
+ def initialize(fragment)
6
+ super EmbedderConfig.for_target(:exported)
7
+ @fragment = fragment
8
+ end
9
+
10
+ def run
11
+ return '' if fragment.blank?
12
+
13
+ embedder.embed_content!(fragment)
14
+ end
15
+
16
+ private
17
+
18
+ def embedder
19
+ @embedder ||= Embedder.new config
20
+ end
21
+
22
+ def opts
23
+ @opts ||= { s3_bucket: s3_bucket }
24
+ end
25
+
26
+ def s3_bucket
27
+ ENV.fetch('OCX_EXPORT_DEST_S3_BUCKET', 'cms-im')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ module EmbeddableContent
2
+ module GeogebraFiles
3
+ class DocProcessor < EmbeddableContent::DocProcessor; end
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module EmbeddableContent
2
+ module GeogebraFiles
3
+ class NodeProcessor < EmbeddableContent::RecordNodeProcessor
4
+ include TemplateBased
5
+
6
+ def data_parameters
7
+ @data_parameters ||= record.full_configuration.to_json
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module EmbeddableContent
2
+ module HtmlTags
3
+ class DocProcessor < EmbeddableContent::DocProcessor
4
+ private
5
+
6
+ def node_selector
7
+ 'ul.c-unstructured'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ module EmbeddableContent
2
+ module HtmlTags
3
+ class NodeProcessor < EmbeddableContent::NodeProcessor
4
+ include TemplateBased
5
+
6
+ def data_rows
7
+ @data_rows ||= list_data.each_slice(num_columns).to_a
8
+ end
9
+
10
+ def node_selector_class
11
+ 'embedded-unstructured-data'
12
+ end
13
+
14
+ private
15
+
16
+ def list_data
17
+ @list_data ||= node.css('li').map(&:text)
18
+ end
19
+
20
+ DEFAULT_NUM_COLUMNS = 8
21
+ def num_columns
22
+ DEFAULT_NUM_COLUMNS
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ module EmbeddableContent
2
+ module Images
3
+ class AttributionsProcessor
4
+ attr_reader :image_processor
5
+
6
+ delegate :embedder, :target, :document, :image_catalog, to: :image_processor
7
+ delegate :locale, to: :embedder
8
+
9
+ TARGETS_NEEDING_ATTRIBUTIONS = %i[print].freeze
10
+ ATTRIBUTIONS_PARTIAL = 'export/shared/attributions_images'.freeze
11
+ ATTRIBUTIONS_NODE_ID = 'image-attributions-node'.freeze
12
+ ATTRIBUTIONS_NODE_SELECTOR = "div##{ATTRIBUTIONS_NODE_ID}".freeze
13
+
14
+ def initialize(image_processor)
15
+ @image_processor = image_processor
16
+ end
17
+
18
+ def process!
19
+ render_attributions if attributions_required?
20
+ end
21
+
22
+ private
23
+
24
+ def attributions_required?
25
+ TARGETS_NEEDING_ATTRIBUTIONS.include?(target) &&
26
+ attributions_node.present? &&
27
+ image_catalog.present? &&
28
+ attributions.present?
29
+ end
30
+
31
+ def attributions
32
+ @attributions =
33
+ image_catalog.select(&:attribution).compact
34
+ end
35
+
36
+ def attributions_node
37
+ @attributions_node ||= document.css(ATTRIBUTIONS_NODE_SELECTOR).first
38
+ end
39
+
40
+ def render_attributions
41
+ attributions_node.replace(attributions_fragment)
42
+ end
43
+
44
+ def attributions_html
45
+ I18n.with_locale(locale) { render_html }
46
+ end
47
+
48
+ def render_html
49
+ ApplicationController.renderer.render(
50
+ partial: ATTRIBUTIONS_PARTIAL,
51
+ locals: { image_files: image_catalog }
52
+ )
53
+ end
54
+
55
+ def attributions_fragment
56
+ @attributions_fragment ||= Nokogiri::HTML.fragment(attributions_html)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmbeddableContent
4
+ module Images
5
+ class DocProcessor < EmbeddableContent::DocProcessor
6
+ delegate :image_catalog, to: :embedder
7
+
8
+ CLASS_IMG_TAG_RELOCATED = 'img-tag-relocated-by-embedder'
9
+
10
+ private
11
+
12
+ def pre_process
13
+ move_img_tags_out_from_p_tags
14
+ end
15
+
16
+ def post_process
17
+ attributions_processor.process!
18
+ end
19
+
20
+ def attributions_processor
21
+ @attributions_processor ||=
22
+ EmbeddableContent::Images::AttributionsProcessor.new self
23
+ end
24
+
25
+ def node_selector
26
+ 'img'
27
+ end
28
+
29
+ def move_img_tags_out_from_p_tags
30
+ img_tags_inside_p_tags.each do |img_tag|
31
+ move_img_tag_out_from_p_tag img_tag
32
+ end
33
+ remove_affected_empty_p_tags
34
+ end
35
+
36
+ def remove_affected_empty_p_tags
37
+ document.css("p.#{CLASS_IMG_TAG_RELOCATED}").each do |p_tag|
38
+ remove_if_empty p_tag
39
+ end
40
+ end
41
+
42
+ def move_img_tag_out_from_p_tag(img_tag)
43
+ img_tag.parent.tap do |p_tag|
44
+ p_tag.add_previous_sibling img_tag
45
+ p_tag.add_class CLASS_IMG_TAG_RELOCATED
46
+ end
47
+ end
48
+
49
+ def img_tags_inside_p_tags
50
+ @img_tags_inside_p_tags ||= document.css 'p > img'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,60 @@
1
+ module EmbeddableContent
2
+ module Images
3
+ class ImageDownloader
4
+ attr_reader :attached_image
5
+
6
+ delegate :to_path, to: :image_file
7
+
8
+ def initialize(attached_image)
9
+ @attached_image = attached_image
10
+ end
11
+
12
+ def save
13
+ image_file.tap { |file| file.write raw_image_data }
14
+ end
15
+
16
+ def delete
17
+ image_file.delete if image_file.exist?
18
+ end
19
+
20
+ def raw_svg
21
+ @raw_svg ||= downloaded_image_data if svg_attached?
22
+ end
23
+
24
+ private
25
+
26
+ TMP_IMAGE_DIR = Rails.root.join('tmp').freeze
27
+
28
+ def image_file
29
+ @image_file ||= TMP_IMAGE_DIR.join filename
30
+ end
31
+
32
+ def raw_image_data
33
+ png_rendered_from_svg || downloaded_image_data
34
+ end
35
+
36
+ def png_rendered_from_svg
37
+ Script::SvgToPng.new(downloaded_image_data).to_png if svg_attached?
38
+ end
39
+
40
+ def downloaded_image_data
41
+ @downloaded_image_data ||=
42
+ attached_image.download.force_encoding 'utf-8'
43
+ end
44
+
45
+ def filename
46
+ @filename ||= "embedder-#{Time.now.to_f}.#{extension}"
47
+ end
48
+
49
+ def extension
50
+ @extension ||=
51
+ svg_attached? ? 'png' : attached_image.filename.extension
52
+ end
53
+
54
+ CONTENT_TYPE_SVG = 'image/svg+xml'.freeze
55
+ def svg_attached?
56
+ attached_image.content_type.eql? CONTENT_TYPE_SVG
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,125 @@
1
+ module EmbeddableContent
2
+ module Images
3
+ class ImgTagAttributes
4
+ include Images::Shared
5
+ attr_reader :node_processor
6
+
7
+ delegate :alt_text, :attached_file, :record, :aria_attrs?,
8
+ :cms_url, :s3_url, :s3_ttl_service_url, :target,
9
+ :node, to: :node_processor
10
+ delegate :image, to: :record
11
+ delegate :raw_svg, to: :image_downloader
12
+
13
+ def initialize(node_processor)
14
+ @node_processor = node_processor
15
+ end
16
+
17
+ def to_h
18
+ { alt: strip_tags(alt_text),
19
+ src: src,
20
+ height: height,
21
+ class: css_classes,
22
+ width: width }.merge(aria_attrs).compact
23
+ end
24
+
25
+ def src
26
+ case target
27
+ when :schoology, :cc, :kiddom then s3_url with_extension: false
28
+ when :cms then cms_url
29
+ when :editable then downloaded_file_url
30
+ when :exported, :qti then s3_url with_extension: false
31
+ when :print, :web then s3_ttl_service_url
32
+ end
33
+ end
34
+
35
+ def width
36
+ case target
37
+ when :editable then width_for_editable_target
38
+ when :cc, :kiddom, :qti, :schoology then image_width
39
+ end
40
+ end
41
+
42
+ # TODO: should this be abstracted?
43
+ def height
44
+ case target
45
+ when :editable then height_for_editable_target
46
+ when :cc, :kiddom, :qti, :schoology then image_height
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def aria_attrs
53
+ aria_attrs? ? { role: :image } : {}
54
+ end
55
+
56
+ def css_classes
57
+ node.classes.any? ? node.classes.join(' ') : nil
58
+ end
59
+
60
+ def svg_image?
61
+ image.attached? && image.content_type == 'image/svg+xml'
62
+ end
63
+
64
+ def use_original_svg_dimensions_for_converted_png_file?
65
+ svg_image? && svg_document.present?
66
+ end
67
+
68
+ def downloaded_file_url
69
+ downloaded_image.to_path
70
+ end
71
+
72
+ def downloaded_image
73
+ @downloaded_image ||= image_downloader.save
74
+ end
75
+
76
+ def image_downloader
77
+ @image_downloader ||= ImageDownloader.new attached_file
78
+ end
79
+
80
+ def height_for_editable_target
81
+ svg_height if use_original_svg_dimensions_for_converted_png_file?
82
+ end
83
+
84
+ def width_for_editable_target
85
+ svg_width if use_original_svg_dimensions_for_converted_png_file?
86
+ end
87
+
88
+ def svg_width
89
+ svg_document.root['width'] if svg_available?
90
+ end
91
+
92
+ def svg_height
93
+ svg_document.root['height'] if svg_available?
94
+ end
95
+
96
+ def svg_available?
97
+ svg_image? && svg_document.present?
98
+ end
99
+
100
+ def svg_document
101
+ @svg_document ||= Nokogiri::XML.parse raw_svg if raw_svg.present?
102
+ end
103
+
104
+ def image_width
105
+ image.metadata[:width] || image_dimensions[1]
106
+ end
107
+
108
+ def image_height
109
+ image.metadata[:height] || image_dimensions[2]
110
+ end
111
+
112
+ IMAGE_DIMENSION_SCRIPT = %(identify -ping -format "%[w]x%[h]").freeze
113
+ IMAGE_DIMENSION_REGEX = /(\d+)x(\d+)/.freeze
114
+
115
+ def run_image_dimension_script
116
+ `#{IMAGE_DIMENSION_SCRIPT} #{downloaded_file_url}`
117
+ end
118
+
119
+ def image_dimensions
120
+ @image_dimensions ||=
121
+ run_image_dimension_script.match IMAGE_DIMENSION_REGEX
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,74 @@
1
+ module EmbeddableContent
2
+ module Images
3
+ class ModalDialog
4
+ include Images::Shared
5
+
6
+ attr_reader :node_processor
7
+
8
+ delegate :record_css_id_for, :s3_ttl_service_url, :caption, :attribution,
9
+ :alt_text, to: :node_processor
10
+
11
+ def initialize(node_processor)
12
+ @node_processor = node_processor
13
+ end
14
+
15
+ def img_tag_attrs
16
+ { src: s3_ttl_service_url,
17
+ width: '80%',
18
+ alt: strip_tags(alt_text),
19
+ role: :image }
20
+ end
21
+
22
+ def attribution_display_text
23
+ [attribution.owner_and_title_info,
24
+ attribution.license.name,
25
+ attribution.via_reference].join('. ').strip + '.'
26
+ end
27
+
28
+ def container_attrs
29
+ { tabindex: '-1',
30
+ role: 'dialog',
31
+ 'aria-labelledby': labelled_by_id,
32
+ 'aria-describedby': described_by_id,
33
+ 'aria-label': 'Image information' }
34
+ end
35
+
36
+ def target_attrs
37
+ { 'id': target_div_id,
38
+ 'hidden': 'true',
39
+ 'aria-hidden': 'true' }
40
+ end
41
+
42
+ def close_button_attrs
43
+ { 'aria-hidden': 'true',
44
+ height: '18',
45
+ width: '18',
46
+ viewBox: '0 0 1200 1200' }
47
+ end
48
+
49
+ def description
50
+ node_processor.long_description
51
+ end
52
+
53
+ def labelled_by_id
54
+ record_css_id_for 'dialog-alt-text'
55
+ end
56
+
57
+ def described_by_id
58
+ record_css_id_for 'dialog-long-description'
59
+ end
60
+
61
+ def target_div_id
62
+ record_css_id_for 'modal-target'
63
+ end
64
+
65
+ def caption_id
66
+ record_css_id_for 'dialog-caption'
67
+ end
68
+
69
+ def attribution_id
70
+ record_css_id_for 'dialog-attribution'
71
+ end
72
+ end
73
+ end
74
+ end