portable_text 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.
- checksums.yaml +7 -0
- data/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +85 -0
- data/LICENSE +21 -0
- data/README.md +357 -0
- data/Rakefile +15 -0
- data/lib/portable_text/block_types/base.rb +19 -0
- data/lib/portable_text/block_types/block.rb +6 -0
- data/lib/portable_text/block_types/image.rb +6 -0
- data/lib/portable_text/block_types/list.rb +64 -0
- data/lib/portable_text/block_types/null.rb +6 -0
- data/lib/portable_text/block_types/span.rb +12 -0
- data/lib/portable_text/config.rb +27 -0
- data/lib/portable_text/errors/unimplemented_error.rb +9 -0
- data/lib/portable_text/errors/unknown_serializer_error.rb +9 -0
- data/lib/portable_text/html/base_component.rb +19 -0
- data/lib/portable_text/html/block_types/block.rb +32 -0
- data/lib/portable_text/html/block_types/image.rb +17 -0
- data/lib/portable_text/html/block_types/list.rb +29 -0
- data/lib/portable_text/html/block_types/null.rb +13 -0
- data/lib/portable_text/html/block_types/span.rb +84 -0
- data/lib/portable_text/html/config.rb +48 -0
- data/lib/portable_text/html/configured.rb +11 -0
- data/lib/portable_text/html/mark_defs/base.rb +14 -0
- data/lib/portable_text/html/mark_defs/link.rb +13 -0
- data/lib/portable_text/html/mark_defs/null.rb +13 -0
- data/lib/portable_text/html/rendering.rb +7 -0
- data/lib/portable_text/html/serializer.rb +17 -0
- data/lib/portable_text/mark_defs/base.rb +10 -0
- data/lib/portable_text/mark_defs/link.rb +7 -0
- data/lib/portable_text/mark_defs/null.rb +6 -0
- data/lib/portable_text/plain/serializer.rb +34 -0
- data/lib/portable_text/serializer.rb +97 -0
- data/lib/portable_text/version.rb +5 -0
- data/lib/portable_text.rb +20 -0
- data/sig/portable_text.rbs +4 -0
- metadata +196 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# Base component for PortableText HTML components
|
2
|
+
# It overrides Dry::Initializer option and params to allow using them without triggering weird errors
|
3
|
+
# See https://github.com/orgs/phlex-ruby/discussions/553
|
4
|
+
|
5
|
+
module PortableText
|
6
|
+
module Html
|
7
|
+
class BaseComponent < Phlex::HTML
|
8
|
+
extend Dry::Initializer
|
9
|
+
|
10
|
+
def self.option(*args, **kwargs, &block)
|
11
|
+
super(*args, reader: false, **kwargs, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.param(*args, **kwargs, &block)
|
15
|
+
super(*args, reader: false, **kwargs, &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PortableText
|
2
|
+
module Html
|
3
|
+
module BlockTypes
|
4
|
+
class Block < Html::BaseComponent
|
5
|
+
include Configured
|
6
|
+
|
7
|
+
param :block
|
8
|
+
delegate :style, :children, :mark_defs, :list_item, to: :@block
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
node_style = node.fetch(:node)
|
12
|
+
node_arguments = node.except(:node)
|
13
|
+
|
14
|
+
send(node_style, **node_arguments) do
|
15
|
+
children.each do |child|
|
16
|
+
render block_type(:span).new(child, mark_defs: mark_defs)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def node
|
24
|
+
return @node if defined?(@node)
|
25
|
+
return @node = config.block.styles.fetch(:li) if list_item.present?
|
26
|
+
|
27
|
+
@node = config.block.styles.fetch(style&.to_sym, { node: :p })
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module PortableText
|
2
|
+
module Html
|
3
|
+
module BlockTypes
|
4
|
+
class Image < Html::BaseComponent
|
5
|
+
param :image
|
6
|
+
|
7
|
+
def view_template
|
8
|
+
if @image.asset.key?("url")
|
9
|
+
img(src: @image.asset["url"])
|
10
|
+
else
|
11
|
+
div { "Please provide a url for this image" }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PortableText
|
2
|
+
module Html
|
3
|
+
module BlockTypes
|
4
|
+
class List < Html::BaseComponent
|
5
|
+
include Configured
|
6
|
+
|
7
|
+
param :list
|
8
|
+
delegate :items, :list_type, to: :@list
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
node_style = node.fetch(:node)
|
12
|
+
node_arguments = node.except(:node)
|
13
|
+
|
14
|
+
send(node_style, **node_arguments) do
|
15
|
+
items.each do |block|
|
16
|
+
render block_type(block.type).new(block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def node
|
24
|
+
@node ||= config.block.list_types.fetch(list_type.to_sym, { node: :ul })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module PortableText
|
2
|
+
module Html
|
3
|
+
module BlockTypes
|
4
|
+
class Span < Html::BaseComponent
|
5
|
+
include Configured
|
6
|
+
|
7
|
+
param :span
|
8
|
+
option :mark_defs, default: proc { [] }
|
9
|
+
delegate :marks, :text, to: :@span
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
root = create_nodes
|
13
|
+
visit(root)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Visit a node and render it
|
19
|
+
# If the node is a mark definition, render the appropriate mark definition
|
20
|
+
# Otherwise, render the node with the appropriate mark decorator
|
21
|
+
# If the node has a child, visit the child
|
22
|
+
# Else render the node as plain text
|
23
|
+
# -------------
|
24
|
+
# Mark definitions and mark decorators are defined in the configuration
|
25
|
+
# If a mark definition is not found, a Null mark definition is rendered along with an error message
|
26
|
+
# if a mark is not found, a span tag is rendered
|
27
|
+
def visit(node)
|
28
|
+
return plain(node.value) if node.child.nil?
|
29
|
+
|
30
|
+
if matching_mark_def?(node.value)
|
31
|
+
annotation = @mark_defs.find { |mark_def| mark_def.key == node.value }
|
32
|
+
|
33
|
+
render annotation_klass(annotation).new(annotation) do
|
34
|
+
visit(node.child)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
decorator = config.span.marks.fetch(node.value.to_sym, { node: :span })
|
38
|
+
mark_node = decorator.fetch(:node)
|
39
|
+
node_arguments = decorator.except(:node)
|
40
|
+
|
41
|
+
send(mark_node, **node_arguments) { visit(node.child) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def annotation_klass(annotation)
|
46
|
+
config.block.mark_defs.fetch(annotation.type.to_sym, Html::MarkDefs::Null)
|
47
|
+
end
|
48
|
+
|
49
|
+
def matching_mark_def?(mark)
|
50
|
+
return false unless @mark_defs.present?
|
51
|
+
|
52
|
+
@mark_defs.map(&:key).include?(mark)
|
53
|
+
end
|
54
|
+
|
55
|
+
class Node
|
56
|
+
attr_accessor :value, :child
|
57
|
+
|
58
|
+
def initialize(value:, child: nil)
|
59
|
+
@value = value
|
60
|
+
@child = child
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Create a linked list of nodes to make it easier to traverse the tree of marks
|
65
|
+
# Marks are traversed in the marks order and the text is the last node
|
66
|
+
def create_nodes
|
67
|
+
nodes = marks + [text]
|
68
|
+
|
69
|
+
root = Node.new(value: nodes.first)
|
70
|
+
return root if nodes.size == 1
|
71
|
+
|
72
|
+
current_node = root
|
73
|
+
|
74
|
+
nodes[1..].each do |mark|
|
75
|
+
current_node.child = Node.new(value: mark)
|
76
|
+
current_node = current_node.child
|
77
|
+
end
|
78
|
+
|
79
|
+
root
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module PortableText
|
2
|
+
module Html
|
3
|
+
class Config
|
4
|
+
extend Dry::Configurable
|
5
|
+
|
6
|
+
# Default settings
|
7
|
+
# These can be overridden
|
8
|
+
# Example: PortableText::Html.config.block.types.merge!({ block: MyCustomBlock })
|
9
|
+
|
10
|
+
setting :block do
|
11
|
+
setting :types, default: {
|
12
|
+
block: Html::BlockTypes::Block,
|
13
|
+
image: Html::BlockTypes::Image,
|
14
|
+
list: Html::BlockTypes::List,
|
15
|
+
span: Html::BlockTypes::Span
|
16
|
+
}
|
17
|
+
|
18
|
+
setting :styles, default: {
|
19
|
+
h1: { node: :h1 },
|
20
|
+
h2: { node: :h2 },
|
21
|
+
h3: { node: :h3 },
|
22
|
+
h4: { node: :h4 },
|
23
|
+
h5: { node: :h5 },
|
24
|
+
h6: { node: :h6 },
|
25
|
+
blockquote: { node: :blockquote },
|
26
|
+
normal: { node: :p },
|
27
|
+
li: { node: :li }
|
28
|
+
}
|
29
|
+
|
30
|
+
setting :mark_defs, default: {
|
31
|
+
link: Html::MarkDefs::Link
|
32
|
+
}
|
33
|
+
|
34
|
+
setting :list_types, default: {
|
35
|
+
bullet: { node: :ul },
|
36
|
+
numeric: { node: :ol }
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
setting :span do
|
41
|
+
setting :marks, default: {
|
42
|
+
strong: { node: :strong },
|
43
|
+
em: { node: :em }
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module PortableText
|
2
|
+
module Html
|
3
|
+
class Serializer < Html::BaseComponent
|
4
|
+
include Configured
|
5
|
+
|
6
|
+
param :blocks
|
7
|
+
|
8
|
+
def content(**_options) = self
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
@blocks.each do |block|
|
12
|
+
render block_type(block.type).new(block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PortableText
|
2
|
+
module Plain
|
3
|
+
class Serializer
|
4
|
+
def initialize(blocks)
|
5
|
+
@blocks = blocks
|
6
|
+
end
|
7
|
+
|
8
|
+
def content(**_params)
|
9
|
+
visit(@blocks)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def visit(nodes)
|
15
|
+
nodes.map do |block|
|
16
|
+
case block.type
|
17
|
+
when "block"
|
18
|
+
block.children.map(&:text).join(" ").squeeze.strip
|
19
|
+
when "list"
|
20
|
+
visit(block.items)
|
21
|
+
when "image"
|
22
|
+
if block.asset.present?
|
23
|
+
block.asset["url"]
|
24
|
+
else
|
25
|
+
"image url not found"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
"Block #{block.type} found"
|
29
|
+
end
|
30
|
+
end.join("\n")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module PortableText
|
2
|
+
class Serializer
|
3
|
+
attr_reader :content, :blocks, :to, :converted
|
4
|
+
|
5
|
+
def initialize(content:, to: :html)
|
6
|
+
@content = content
|
7
|
+
@blocks = []
|
8
|
+
@to = to
|
9
|
+
@converted = false
|
10
|
+
end
|
11
|
+
|
12
|
+
# After conversion, the Portable Text content is serialized to the desired format.
|
13
|
+
def render(**options)
|
14
|
+
convert!
|
15
|
+
|
16
|
+
serializer.new(blocks).content(**options)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Converts the Portable Text content into a collection of blocks converted to ruby objects
|
20
|
+
# along with their children and markDefs.
|
21
|
+
# Object parameters are symbolized and camelCase keys are converted to snake_case.
|
22
|
+
# This method is idempotent.
|
23
|
+
def convert!
|
24
|
+
return if converted
|
25
|
+
|
26
|
+
content.each do |block_params|
|
27
|
+
params = block_params.transform_keys(&:to_sym)
|
28
|
+
params[:children] = create_children(params[:children])
|
29
|
+
params[:markDefs] = create_mark_defs(params[:markDefs])
|
30
|
+
|
31
|
+
block = block_klass(params.fetch(:_type)).new(**params)
|
32
|
+
add_block(block)
|
33
|
+
end
|
34
|
+
|
35
|
+
@converted = true
|
36
|
+
end
|
37
|
+
|
38
|
+
# The serializer is determined by the `to` parameter, which defaults to `:html`.
|
39
|
+
# The serializer must be defined in the PortableText configuration.
|
40
|
+
def serializer
|
41
|
+
config.serializers.fetch(to) { raise Errors::UnknownSerializerError }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def config = PortableText.config
|
47
|
+
|
48
|
+
def block_klass(type)
|
49
|
+
config.block.types.fetch(type.to_sym, BlockTypes::Null)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Adds a block to the blocks collection.
|
53
|
+
# If the block is a list item, it will be added to the last list block if it exists.
|
54
|
+
# Else a new list block will be created.
|
55
|
+
def add_block(block)
|
56
|
+
return blocks.push(block) unless block.list_item?
|
57
|
+
|
58
|
+
last_block = blocks.last
|
59
|
+
|
60
|
+
if last_block&.list?
|
61
|
+
last_block.add(block)
|
62
|
+
else
|
63
|
+
blocks.push(
|
64
|
+
block_klass(:list).new(
|
65
|
+
items: [block],
|
66
|
+
level: block.level,
|
67
|
+
parent: nil
|
68
|
+
)
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_children(children)
|
74
|
+
return [] if children.blank?
|
75
|
+
|
76
|
+
children.map do |child|
|
77
|
+
block_klass(:span).new(**child.transform_keys(&:to_sym))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_mark_defs(mark_defs)
|
82
|
+
return [] if mark_defs.blank?
|
83
|
+
|
84
|
+
inflector = Dry::Inflector.new
|
85
|
+
|
86
|
+
mark_defs.map do |mark_def|
|
87
|
+
mark_def.transform_keys!(&:to_sym)
|
88
|
+
mark_type = inflector.underscore(mark_def[:_type]).to_sym
|
89
|
+
|
90
|
+
config.block.mark_defs.fetch(
|
91
|
+
mark_type,
|
92
|
+
MarkDefs::Null
|
93
|
+
).new(**mark_def.merge(_type: mark_type))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext"
|
5
|
+
require "dry/configurable"
|
6
|
+
require "dry/inflector"
|
7
|
+
require "dry/initializer"
|
8
|
+
require "zeitwerk"
|
9
|
+
require "phlex"
|
10
|
+
|
11
|
+
module PortableText
|
12
|
+
def self.config = Config.config
|
13
|
+
|
14
|
+
module Html
|
15
|
+
def self.config = Html::Config.config
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
loader = Zeitwerk::Loader.for_gem
|
20
|
+
loader.setup
|