dato_dast 0.0.1

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.projections.json +4 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +6 -0
  6. data/CODE_OF_CONDUCT.md +128 -0
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +58 -0
  9. data/README.md +1082 -0
  10. data/Rakefile +8 -0
  11. data/TODO.md +0 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/dato_dast.gemspec +29 -0
  15. data/lib/dato_dast/configuration.rb +121 -0
  16. data/lib/dato_dast/errors/block_field_missing.rb +15 -0
  17. data/lib/dato_dast/errors/block_node_missing_render_function.rb +15 -0
  18. data/lib/dato_dast/errors/invalid_block_structure_type.rb +6 -0
  19. data/lib/dato_dast/errors/invalid_blocks_configuration.rb +18 -0
  20. data/lib/dato_dast/errors/invalid_marks_configuration.rb +15 -0
  21. data/lib/dato_dast/errors/invalid_nodes.rb +15 -0
  22. data/lib/dato_dast/errors/invalid_types_configuration.rb +15 -0
  23. data/lib/dato_dast/errors/missing_render_value_function.rb +15 -0
  24. data/lib/dato_dast/errors.rb +8 -0
  25. data/lib/dato_dast/extensions/middleman.rb +35 -0
  26. data/lib/dato_dast/html_tag.rb +74 -0
  27. data/lib/dato_dast/marks.rb +12 -0
  28. data/lib/dato_dast/nodes/attributed_quote.rb +20 -0
  29. data/lib/dato_dast/nodes/base.rb +113 -0
  30. data/lib/dato_dast/nodes/block.rb +124 -0
  31. data/lib/dato_dast/nodes/blockquote.rb +6 -0
  32. data/lib/dato_dast/nodes/code.rb +35 -0
  33. data/lib/dato_dast/nodes/generic.rb +21 -0
  34. data/lib/dato_dast/nodes/heading.rb +14 -0
  35. data/lib/dato_dast/nodes/inline_item.rb +14 -0
  36. data/lib/dato_dast/nodes/item_link.rb +29 -0
  37. data/lib/dato_dast/nodes/link.rb +57 -0
  38. data/lib/dato_dast/nodes/list.rb +9 -0
  39. data/lib/dato_dast/nodes/list_item.rb +6 -0
  40. data/lib/dato_dast/nodes/paragraph.rb +6 -0
  41. data/lib/dato_dast/nodes/root.rb +6 -0
  42. data/lib/dato_dast/nodes/span.rb +21 -0
  43. data/lib/dato_dast/nodes/thematic_break.rb +9 -0
  44. data/lib/dato_dast/nodes.rb +29 -0
  45. data/lib/dato_dast/version.rb +5 -0
  46. data/lib/dato_dast.rb +39 -0
  47. metadata +119 -0
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/TODO.md ADDED
File without changes
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "dato_dast"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dato_dast.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/dato_dast/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "dato_dast"
7
+ spec.version = DatoDast::VERSION
8
+ spec.authors = ["John DeWyze"]
9
+ spec.email = ["john@dewyze.dev"]
10
+
11
+ spec.summary = "Gem for converting DatoCMS Structured Text to Html"
12
+ spec.description = "This gem provides a way to convert DatoCMS structured text to Html, as well as an extension for Middleman."
13
+ spec.homepage = "https://github.com/dewyze/dato_dast"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
15
+
16
+ spec.metadata["homepage_uri"] = "https://github.com/dewyze/dato_dast"
17
+ spec.metadata["source_code_uri"] = "https://github.com/dewyze/dato_dast"
18
+ spec.metadata["changelog_uri"] = "https://github.com/dewyze/dato_dast/blob/main/CHANGELOG.md."
19
+
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "activesupport"
28
+ spec.add_development_dependency "pry-byebug"
29
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatoDast
4
+ class Configuration
5
+ TYPE_CONFIG = {
6
+ Nodes::Block.type => { "node" => Nodes::Block },
7
+ Nodes::Blockquote.type => { "tag" => "blockquote", "node" => Nodes::AttributedQuote },
8
+ Nodes::Code.type => { "tag" => "code", "node" => Nodes::Code, "wrappers" => ["pre"] },
9
+ Nodes::Generic.type => { "node" => Nodes::Generic },
10
+ Nodes::Heading.type => { "tag" => ->(node) { "h#{node.level}" }, "node" => Nodes::Heading },
11
+ Nodes::ItemLink.type => { "tag" => "a", "node" => Nodes::ItemLink, "url_key" => :slug },
12
+ Nodes::Link.type => { "tag" => "a", "node" => Nodes::Link },
13
+ Nodes::List.type => { "tag" => ->(node) { node.style == "bulleted" ? "ul" : "ol" }, "node" => Nodes::List },
14
+ Nodes::ListItem.type => { "tag" => "li", "node" => Nodes::ListItem },
15
+ Nodes::Paragraph.type => { "tag" => "p", "node" => Nodes::Paragraph },
16
+ Nodes::Root.type => { "tag" => "div", "node" => Nodes::Root },
17
+ Nodes::Span.type => { "node" => Nodes::Span },
18
+ Nodes::ThematicBreak.type => { "tag" => "hr", "node" => Nodes::ThematicBreak },
19
+ }.freeze
20
+
21
+ MARK_CONFIG = {
22
+ Marks::CODE => { "tag" => "code" },
23
+ Marks::EMPHASIS => { "tag" => "em" },
24
+ Marks::HIGHLIGHT => { "tag" => "mark" },
25
+ Marks::STRIKETHROUGH => { "tag" => "strike" },
26
+ Marks::STRONG => { "tag" => "strong" },
27
+ Marks::UNDERLINE => { "tag" => "u" },
28
+ }.freeze
29
+
30
+ BLOCK_RENDER_KEYS = ["node", "render_value", "structure"].freeze
31
+
32
+ attr_reader :blocks, :host, :marks, :types
33
+ attr_accessor :highlight, :item_links, :smart_links
34
+
35
+ def initialize
36
+ @blocks = {}
37
+ @highlight = true
38
+ @host = nil
39
+ @item_links = {}
40
+ @marks = MARK_CONFIG.transform_values { |value| value.dup }
41
+ @smart_links = true
42
+ @types = TYPE_CONFIG.transform_values { |value| value.dup }
43
+ end
44
+
45
+ def host=(new_host)
46
+ uri = URI(new_host)
47
+
48
+ if uri.host.present?
49
+ @host = uri.host
50
+ else
51
+ @host = uri.to_s
52
+ end
53
+ end
54
+
55
+ def blocks=(new_blocks)
56
+ validate_blocks_configuration(new_blocks)
57
+
58
+ @blocks = new_blocks
59
+ end
60
+
61
+ def marks=(new_marks)
62
+ validate_marks_configuration(new_marks)
63
+
64
+ @marks.merge!(new_marks)
65
+ end
66
+
67
+ def types=(new_types)
68
+ validate_types(new_types)
69
+
70
+ @types.merge!(new_types)
71
+ end
72
+
73
+ def add_wrapper(type, wrapper)
74
+ wrappers = Array.wrap(@types[type]["wrappers"])
75
+ wrappers << wrapper
76
+ @types[type]["wrappers"] = wrappers
77
+ end
78
+
79
+ private
80
+
81
+ def validate_blocks_configuration(blocks_config)
82
+ invalid_blocks = []
83
+
84
+ blocks_config.each do |block, block_config|
85
+ next if block_config.is_a?(Proc)
86
+
87
+ intersection = block_config.keys & BLOCK_RENDER_KEYS
88
+ invalid_blocks << block unless intersection.length == 1
89
+ end
90
+
91
+ raise Errors::InvalidBlocksConfiguration.new(invalid_blocks) if invalid_blocks.present?
92
+ end
93
+
94
+ def validate_types(types_config)
95
+ invalid_configs = []
96
+ invalid_nodes = []
97
+
98
+ types_config.each do |type, type_config|
99
+ node = type_config["node"]
100
+ if node
101
+ invalid_nodes << type unless node.instance_methods.include?(:render)
102
+ else
103
+ invalid_configs << type
104
+ end
105
+ end
106
+
107
+ raise Errors::InvalidTypesConfiguration.new(invalid_configs) if invalid_configs.present?
108
+ raise Errors::InvalidNodes.new(invalid_nodes) if invalid_nodes.present?
109
+ end
110
+
111
+ def validate_marks_configuration(marks_config)
112
+ invalid_marks = []
113
+
114
+ marks_config.each do |mark, mark_config|
115
+ invalid_marks << mark unless mark_config.keys.include?("tag")
116
+ end
117
+
118
+ raise Errors::InvalidMarksConfiguration.new(invalid_marks) if invalid_marks.present?
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,15 @@
1
+ module DatoDast
2
+ module Errors
3
+ class BlockFieldMissing < StandardError
4
+ MESSAGE = <<~MSG.strip
5
+ A structure type of 'field' requires the block to have the specified field.
6
+
7
+ The following block configuration is invalid:
8
+ MSG
9
+
10
+ def initialize(item_type)
11
+ super(MESSAGE + " " + item_type)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module DatoDast
2
+ module Errors
3
+ class BlockNodeMissingRenderFunction < StandardError
4
+ MESSAGE = <<~MSG.strip
5
+ A node class provided for a block must have a 'render' method.
6
+
7
+ The node object for the following block item type is invalid:
8
+ MSG
9
+
10
+ def initialize(keys)
11
+ super(MESSAGE + " " + keys.join(", "))
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ module DatoDast
2
+ module Errors
3
+ class InvalidBlockStructureType < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ module DatoDast
2
+ module Errors
3
+ class InvalidBlocksConfiguration < StandardError
4
+ MESSAGE = <<~MSG.strip
5
+ A block configuration requires exactly one of the following keys:
6
+ - "node"
7
+ - "render_value"
8
+ - "structure"
9
+
10
+ The following block configurations are invalid:
11
+ MSG
12
+
13
+ def initialize(keys)
14
+ super(MESSAGE + " " + keys.join(", "))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module DatoDast
2
+ module Errors
3
+ class InvalidMarksConfiguration < StandardError
4
+ MESSAGE = <<~MSG.strip
5
+ A mark configuration requires only the "tag" key.
6
+
7
+ The following mark configurations are invalid:
8
+ MSG
9
+
10
+ def initialize(keys)
11
+ super(MESSAGE + " " + keys.join(", "))
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module DatoDast
2
+ module Errors
3
+ class InvalidNodes < StandardError
4
+ MESSAGE = <<~MSG.strip
5
+ A node class must have a 'render' instance method.
6
+
7
+ The node objects for the following types are invalid:
8
+ MSG
9
+
10
+ def initialize(keys)
11
+ super(MESSAGE + " " + keys.join(", "))
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module DatoDast
2
+ module Errors
3
+ class InvalidTypesConfiguration < StandardError
4
+ MESSAGE = <<~MSG.strip
5
+ A type configuration requires the "node" key.
6
+
7
+ The following type configurations are invalid:
8
+ MSG
9
+
10
+ def initialize(keys)
11
+ super(MESSAGE + " " + keys.join(", "))
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module DatoDast
2
+ module Errors
3
+ class MissingRenderValueFunction < StandardError
4
+ MESSAGE = <<~MSG.strip
5
+ A structure type of 'value' requires a render value function.
6
+
7
+ The following block configuration is invalid:
8
+ MSG
9
+
10
+ def initialize(item_type)
11
+ super(MESSAGE + " " + item_type)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ require "dato_dast/errors/block_field_missing"
2
+ require "dato_dast/errors/block_node_missing_render_function"
3
+ require "dato_dast/errors/invalid_block_structure_type"
4
+ require "dato_dast/errors/invalid_blocks_configuration"
5
+ require "dato_dast/errors/invalid_marks_configuration"
6
+ require "dato_dast/errors/invalid_nodes"
7
+ require "dato_dast/errors/invalid_types_configuration"
8
+ require "dato_dast/errors/missing_render_value_function"
@@ -0,0 +1,35 @@
1
+ require "middleman-core"
2
+
3
+ module DatoDast
4
+ module Extensions
5
+ class Middleman < Middleman::Extension
6
+ option :blocks, {}, "Configuration hash for blocks"
7
+ option :host, nil, "Host for your site, used in conjunction with 'smart_links' option"
8
+ option :highlight, true, "Toggle whether to attempt to higlight code blocks"
9
+ option :item_links, {}, "Configuration hash item links types and the url field"
10
+ option :marks, {}, "Configuration hash for a given mark"
11
+ option :smart_links, true, "Open Link items in new windows and ItemLinks in the same window"
12
+ option :types, {}, "Configuration hash for a given block node type"
13
+
14
+ def after_configuration
15
+ DatoDast.configure do |config|
16
+ config.blocks = options[:blocks]
17
+ config.highlight = options[:highlight]
18
+ config.host = options[:host]
19
+ config.item_links = options[:item_links]
20
+ config.marks = options[:marks]
21
+ config.smart_links = options[:smart_links]
22
+ config.types = options[:types]
23
+ end
24
+ end
25
+
26
+ helpers do
27
+ def structured_text(object, config = nil)
28
+ DatoDast.structured_text(object, config)
29
+ end
30
+ end
31
+ end
32
+
33
+ ::Middleman::Extensions.register(:dato_dast, DatoDast::Extensions::Middleman)
34
+ end
35
+ end
@@ -0,0 +1,74 @@
1
+ module DatoDast
2
+ class HtmlTag
3
+ EMPTY = ""
4
+ NEWLINE = "\n"
5
+
6
+ def self.parse(tag, object = nil)
7
+ case tag
8
+ when String
9
+ new(tag)
10
+ when Hash
11
+ html_tag = tag["tag"]
12
+ css_class = tag["css_class"]
13
+ meta = tag["meta"]
14
+
15
+ HtmlTag.new(html_tag, { "css_class" => css_class, "meta" => meta, "object" => object })
16
+ when HtmlTag
17
+ tag
18
+ when nil
19
+ HtmlTag.new(nil)
20
+ else
21
+ nil
22
+ end
23
+ end
24
+
25
+ def initialize(tag, options = {})
26
+ @tag = tag
27
+ @css_class = options["css_class"] || ""
28
+ @meta = options["meta"] || {}
29
+ @object = options["object"]
30
+ end
31
+
32
+ def open
33
+ return EMPTY unless tag
34
+
35
+ "<#{tag}#{css_class}#{meta}>" + NEWLINE
36
+ end
37
+
38
+ def close
39
+ return EMPTY unless tag
40
+
41
+ NEWLINE + "</#{tag}>"
42
+ end
43
+
44
+ private
45
+
46
+ def tag
47
+ if @tag.is_a?(Proc)
48
+ @tag.call(@object)
49
+ else
50
+ @tag
51
+ end
52
+ end
53
+
54
+ def css_class
55
+ return "" if @css_class.blank?
56
+
57
+ klass = @css_class.is_a?(Proc) ? @css_class.call(@object) : @css_class
58
+
59
+ " class=\"#{klass}\""
60
+ end
61
+
62
+ def meta
63
+ return "" if @meta.blank?
64
+
65
+ if @meta.is_a?(Proc)
66
+ " " + @meta.call(@object)
67
+ else
68
+ @meta.reduce("") do |html, pair|
69
+ html + " #{pair["id"]}=\"#{pair["value"]}\""
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatoDast
4
+ class Marks
5
+ CODE = "code"
6
+ EMPHASIS = "emphasis"
7
+ HIGHLIGHT = "highlight"
8
+ STRIKETHROUGH = "strikethrough"
9
+ STRONG = "strong"
10
+ UNDERLINE = "underline"
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module DatoDast
2
+ module Nodes
3
+ class AttributedQuote < Base
4
+ def attribution
5
+ @node["attribution"]
6
+ end
7
+
8
+ def render
9
+ <<~HTML
10
+ <figure>
11
+ #{super}
12
+ <figcaption>
13
+ #{attribution}
14
+ </figcaption>
15
+ </figure>
16
+ HTML
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,113 @@
1
+ module DatoDast
2
+ module Nodes
3
+ class Base
4
+ EMPTY = ""
5
+ NEWLINE = "\n"
6
+
7
+ def self.type
8
+ name.demodulize.camelize(:lower)
9
+ end
10
+
11
+ def initialize(node, links = [], blocks = [], config = nil)
12
+ @node = node
13
+ @links = links
14
+ @blocks = blocks
15
+ @config = config
16
+ end
17
+
18
+ def config
19
+ @config ||= DatoDast.configuration
20
+ end
21
+
22
+ def type
23
+ @node["type"]
24
+ end
25
+
26
+ def children
27
+ @node["children"]
28
+ end
29
+
30
+ def wrappers
31
+ @node["wrappers"] || Array.wrap(node_config["wrappers"])
32
+ end
33
+
34
+ def tag
35
+ if node_config && node_config["tag"].is_a?(Proc)
36
+ node_config["tag"].call(proc_object)
37
+ else
38
+ @node["tag"] || node_config["tag"]
39
+ end
40
+ end
41
+
42
+ def css_class
43
+ if node_config && node_config["css_class"].is_a?(Proc)
44
+ node_config["css_class"].call(proc_object)
45
+ else
46
+ @node["css_class"] || node_config["css_class"]
47
+ end
48
+ end
49
+
50
+ def meta
51
+ if node_config && node_config["meta"].is_a?(Proc)
52
+ node_config["meta"].call(proc_object)
53
+ else
54
+ @node["meta"] || node_config["meta"]
55
+ end
56
+ end
57
+
58
+ def node_config
59
+ @node_config ||= config.types[type]
60
+ end
61
+
62
+ def tag_info
63
+ {
64
+ "tag" => tag,
65
+ "css_class" => css_class,
66
+ "meta" => meta,
67
+ }
68
+ end
69
+
70
+ def render
71
+ open_wrappers +
72
+ html_tag.open +
73
+ render_value +
74
+ html_tag.close +
75
+ close_wrappers
76
+ end
77
+
78
+ def render_value
79
+ render_children
80
+ end
81
+
82
+ def render_children
83
+ return EMPTY unless children.present?
84
+
85
+ children.map do |child|
86
+ Nodes.wrap(child, @links, @blocks, config).render
87
+ end.join("\n").gsub(/\n+/, "\n")
88
+ end
89
+
90
+ private
91
+
92
+ def html_tag
93
+ @html_tag ||= HtmlTag.parse(tag_info)
94
+ end
95
+
96
+ def open_wrappers
97
+ wrapper_tags.map(&:open).join("")
98
+ end
99
+
100
+ def close_wrappers
101
+ wrapper_tags.reverse.map(&:close).join("")
102
+ end
103
+
104
+ def wrapper_tags
105
+ @wrapper_tags ||= wrappers.map { |wrappers| HtmlTag.parse(wrappers, proc_object) }
106
+ end
107
+
108
+ def proc_object
109
+ self
110
+ end
111
+ end
112
+ end
113
+ end