prosemirror 0.0.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +20 -0
- data/COOKBOOK.md +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +155 -12
- data/ROADMAP.md +215 -0
- data/TYPES.md +201 -0
- data/lib/prosemirror/coerce.rb +17 -0
- data/lib/prosemirror/content_expression.rb +94 -0
- data/lib/prosemirror/content_match.rb +241 -0
- data/lib/prosemirror/document.rb +76 -0
- data/lib/prosemirror/html/builder.rb +146 -0
- data/lib/prosemirror/html/element.rb +84 -0
- data/lib/prosemirror/html/entities.rb +87 -0
- data/lib/prosemirror/html/entities.txt +2231 -0
- data/lib/prosemirror/html/selector.rb +95 -0
- data/lib/prosemirror/html/splitter.rb +68 -0
- data/lib/prosemirror/html/text.rb +23 -0
- data/lib/prosemirror/html_parser.rb +498 -0
- data/lib/prosemirror/html_renderer.rb +20 -0
- data/lib/prosemirror/json_parser.rb +114 -0
- data/lib/prosemirror/mark.rb +19 -0
- data/lib/prosemirror/mark_type.rb +71 -0
- data/lib/prosemirror/marks/basic/code.rb +16 -0
- data/lib/prosemirror/marks/basic/em.rb +17 -0
- data/lib/prosemirror/marks/basic/link.rb +21 -0
- data/lib/prosemirror/marks/basic/strong.rb +17 -0
- data/lib/prosemirror/marks/bold.rb +19 -0
- data/lib/prosemirror/marks/code.rb +16 -0
- data/lib/prosemirror/marks/highlight.rb +18 -0
- data/lib/prosemirror/marks/italic.rb +15 -0
- data/lib/prosemirror/marks/link.rb +22 -0
- data/lib/prosemirror/marks/strike.rb +14 -0
- data/lib/prosemirror/marks/subscript.rb +13 -0
- data/lib/prosemirror/marks/superscript.rb +13 -0
- data/lib/prosemirror/marks/underline.rb +12 -0
- data/lib/prosemirror/naming.rb +16 -0
- data/lib/prosemirror/node.rb +77 -0
- data/lib/prosemirror/node_type.rb +123 -0
- data/lib/prosemirror/nodes/basic/code_block.rb +20 -0
- data/lib/prosemirror/nodes/basic/image.rb +23 -0
- data/lib/prosemirror/nodes/basic/ordered_list.rb +22 -0
- data/lib/prosemirror/nodes/blockquote.rb +13 -0
- data/lib/prosemirror/nodes/bullet_list.rb +13 -0
- data/lib/prosemirror/nodes/code_block.rb +24 -0
- data/lib/prosemirror/nodes/doc.rb +10 -0
- data/lib/prosemirror/nodes/hard_break.rb +15 -0
- data/lib/prosemirror/nodes/heading.rb +15 -0
- data/lib/prosemirror/nodes/horizontal_rule.rb +12 -0
- data/lib/prosemirror/nodes/image.rb +22 -0
- data/lib/prosemirror/nodes/list_item.rb +12 -0
- data/lib/prosemirror/nodes/ordered_list.rb +36 -0
- data/lib/prosemirror/nodes/paragraph.rb +13 -0
- data/lib/prosemirror/nodes/table.rb +15 -0
- data/lib/prosemirror/nodes/table_cell.rb +33 -0
- data/lib/prosemirror/nodes/table_header.rb +15 -0
- data/lib/prosemirror/nodes/table_row.rb +12 -0
- data/lib/prosemirror/nodes/text.rb +10 -0
- data/lib/prosemirror/parse_rule.rb +50 -0
- data/lib/prosemirror/rendering.rb +70 -0
- data/lib/prosemirror/schema.rb +241 -0
- data/lib/prosemirror/text_serializer.rb +46 -0
- data/lib/prosemirror/type_definition.rb +149 -0
- data/lib/prosemirror/version.rb +1 -1
- data/lib/prosemirror.rb +43 -2
- metadata +113 -16
- data/.rubocop.yml +0 -13
- data/Gemfile +0 -12
- data/Rakefile +0 -16
- data/prosemirror.gemspec +0 -35
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Nodes
|
|
5
|
+
module Basic
|
|
6
|
+
# ProseMirror's image, an inline node. Without an allowed `src`, it renders nothing.
|
|
7
|
+
class Image < NodeType
|
|
8
|
+
group :inline
|
|
9
|
+
inline
|
|
10
|
+
|
|
11
|
+
prop :src, _String(length: 1..)
|
|
12
|
+
prop :alt, _String?
|
|
13
|
+
prop :title, _String?
|
|
14
|
+
parse_html tag: "img[src]"
|
|
15
|
+
|
|
16
|
+
def view_template
|
|
17
|
+
src = safe_url(@src)
|
|
18
|
+
img(src: src, alt: @alt, title: @title) if src
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Nodes
|
|
5
|
+
module Basic
|
|
6
|
+
# ProseMirror's ordered list. Tiptap's `start` attribute is read as `order`.
|
|
7
|
+
class OrderedList < NodeType
|
|
8
|
+
group :block, :list
|
|
9
|
+
content "list_item+"
|
|
10
|
+
|
|
11
|
+
prop :order, Integer, default: 1, aliases: ["start"],
|
|
12
|
+
parse_html: ->(element) { element.key?("start") ? element["start"].to_i : 1 }, &Coerce::INTEGER
|
|
13
|
+
|
|
14
|
+
parse_html tag: "ol"
|
|
15
|
+
|
|
16
|
+
def view_template(&)
|
|
17
|
+
ol(start: (@order unless @order == 1), &)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Nodes
|
|
5
|
+
# Tiptap's code block. `prosemirror-markdown`'s `params` attribute is read as `language`.
|
|
6
|
+
class CodeBlock < NodeType
|
|
7
|
+
group :block
|
|
8
|
+
content "text*"
|
|
9
|
+
marks ""
|
|
10
|
+
code
|
|
11
|
+
|
|
12
|
+
# From the `language-*` class of the `<code>` inside `<pre>`.
|
|
13
|
+
prop :language, _String?(/\A[\w#+.-]+\z/), aliases: ["params"], parse_html: lambda { |element|
|
|
14
|
+
element.first_element_child&.classes&.find { |name| name.start_with?("language-") }&.delete_prefix("language-")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parse_html tag: "pre", preserve_whitespace: :full
|
|
18
|
+
|
|
19
|
+
def view_template(&)
|
|
20
|
+
pre { code(class: @language && "language-#{@language}", &) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Nodes
|
|
5
|
+
class Heading < NodeType
|
|
6
|
+
group :block
|
|
7
|
+
content "inline*"
|
|
8
|
+
|
|
9
|
+
prop :level, _Integer(1..6), default: 1, &Coerce::INTEGER
|
|
10
|
+
(1..6).each { |level| parse_html tag: "h#{level}", attrs: {level:} }
|
|
11
|
+
|
|
12
|
+
def view_template(&) = tag(:"h#{@level}", &)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Nodes
|
|
5
|
+
# Tiptap's image, a block node. Without an allowed `src`, it renders nothing.
|
|
6
|
+
class Image < NodeType
|
|
7
|
+
group :block
|
|
8
|
+
|
|
9
|
+
prop :src, _String(length: 1..)
|
|
10
|
+
prop :alt, _String?
|
|
11
|
+
prop :title, _String?
|
|
12
|
+
prop :width, _Integer?(1..), &Coerce::INTEGER
|
|
13
|
+
prop :height, _Integer?(1..), &Coerce::INTEGER
|
|
14
|
+
parse_html tag: %(img[src]:not([src^="data:"]))
|
|
15
|
+
|
|
16
|
+
def view_template
|
|
17
|
+
src = safe_url(@src)
|
|
18
|
+
img(src: src, alt: @alt, title: @title, width: @width, height: @height) if src
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Nodes
|
|
5
|
+
# Tiptap's ordered list. ProseMirror's `order` attribute is read as `start`.
|
|
6
|
+
class OrderedList < NodeType
|
|
7
|
+
# CSS list styles, used by Google Docs and Word, and their `type`.
|
|
8
|
+
LIST_STYLE_TYPES = {
|
|
9
|
+
"upper-roman" => "I", "lower-roman" => "i", "upper-alpha" => "A", "upper-latin" => "A",
|
|
10
|
+
"lower-alpha" => "a", "lower-latin" => "a"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
group :block, :list
|
|
14
|
+
content "list_item+"
|
|
15
|
+
|
|
16
|
+
prop :start, Integer, default: 1, aliases: ["order"],
|
|
17
|
+
parse_html: ->(element) { element.key?("start") ? element["start"].to_i : 1 }, &Coerce::INTEGER
|
|
18
|
+
prop :type, _Union?("1", "a", "A", "i", "I"), parse_html: ->(element) { OrderedList.type_from_html(element) }
|
|
19
|
+
|
|
20
|
+
parse_html tag: "ol"
|
|
21
|
+
|
|
22
|
+
# From the `type` attribute, or the list style of the list or of its first item.
|
|
23
|
+
def self.type_from_html(element)
|
|
24
|
+
element["type"] || list_style_type(element) || element.query("li")&.then { |item| list_style_type(item) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.list_style_type(element)
|
|
28
|
+
LIST_STYLE_TYPES[element.style["list-style-type"].to_s.downcase]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def view_template(&)
|
|
32
|
+
ol(start: (@start unless @start == 1), type: @type, &)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Nodes
|
|
5
|
+
class TableCell < NodeType
|
|
6
|
+
ALIGNMENTS = %w[left right center].freeze
|
|
7
|
+
|
|
8
|
+
content "block+"
|
|
9
|
+
|
|
10
|
+
prop :colspan, _Integer(1..), default: 1, &Coerce::INTEGER
|
|
11
|
+
prop :rowspan, _Integer(1..), default: 1, &Coerce::INTEGER
|
|
12
|
+
prop :colwidth, _Array?(_Integer(1..)),
|
|
13
|
+
parse_html: ->(element) { element["colwidth"]&.split(",")&.map(&:to_i) }
|
|
14
|
+
# From the `text-align` style, or the old `align` attribute.
|
|
15
|
+
prop :align, _Union?(*ALIGNMENTS), parse_html: lambda { |element|
|
|
16
|
+
(element.style["text-align"] || element["align"]).to_s.strip.downcase.then { |value| value if ALIGNMENTS.include?(value) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
parse_html tag: "td"
|
|
20
|
+
|
|
21
|
+
def view_template(&)
|
|
22
|
+
td(**cell_attributes, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def cell_attributes
|
|
28
|
+
{colspan: (@colspan unless @colspan == 1), rowspan: (@rowspan unless @rowspan == 1),
|
|
29
|
+
style: @align && "text-align: #{@align}"}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
# How to recognize a node or a mark in HTML, as ProseMirror's ParseRule:
|
|
5
|
+
# https://prosemirror.net/docs/ref/#model.ParseRule
|
|
6
|
+
#
|
|
7
|
+
# - `tag`: a CSS selector the element must match (see HTML::Selector), or
|
|
8
|
+
# - `style`: a CSS property (`"font-style"`), or a property and value (`"font-style=italic"`), for marks;
|
|
9
|
+
# - `attrs`: attributes to give the node or mark;
|
|
10
|
+
# - `get_attrs`: a block called with the element (or the style's value), returning attributes, nil for
|
|
11
|
+
# the defaults, or false when the rule doesn't match;
|
|
12
|
+
# - `priority` (default 50): rules with a higher priority are tried first;
|
|
13
|
+
# - `consuming` (default true): false lets the following rules match the element too;
|
|
14
|
+
# - `context`: the parents the rule needs, as `"list_item/"` or `"blockquote//"`;
|
|
15
|
+
# - `ignore`: skip the element and its content;
|
|
16
|
+
# - `skip`: skip the element, but parse its content;
|
|
17
|
+
# - `close_parent`: close the current node;
|
|
18
|
+
# - `content_element`: a selector for the descendant holding the content;
|
|
19
|
+
# - `get_content`: a block called with the element and the schema, returning the nodes to use as content;
|
|
20
|
+
# - `preserve_whitespace`: true to keep spaces, `:full` to also keep newlines;
|
|
21
|
+
# - `clear_mark`: for style rules, true to remove the type's marks, or a block called with each mark.
|
|
22
|
+
ParseRule = Data.define(
|
|
23
|
+
:type, :tag, :style, :attrs, :get_attrs, :priority, :consuming, :context, :ignore, :skip,
|
|
24
|
+
:close_parent, :content_element, :get_content, :preserve_whitespace, :clear_mark
|
|
25
|
+
) do
|
|
26
|
+
def initialize(type:, tag: nil, style: nil, attrs: nil, get_attrs: nil, priority: 50, consuming: true,
|
|
27
|
+
context: nil, ignore: false, skip: false, close_parent: false, content_element: nil, get_content: nil,
|
|
28
|
+
preserve_whitespace: nil, clear_mark: nil)
|
|
29
|
+
raise ArgumentError, "a parse rule needs a tag or a style" if tag.nil? == style.nil?
|
|
30
|
+
|
|
31
|
+
HTML::Selector.parse(tag) if tag
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def node?
|
|
36
|
+
type < NodeType
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The style's property, without the value.
|
|
40
|
+
def style_property
|
|
41
|
+
style&.split("=", 2)&.first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether a style rule applies to this property and value.
|
|
45
|
+
def style_matches?(property, value)
|
|
46
|
+
name, expected = style.split("=", 2)
|
|
47
|
+
name == property && (expected.nil? || expected == value)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
# Renders nodes and their marks. Included in the HTML renderer, and in node and mark types
|
|
5
|
+
# so that they can render any part of their content.
|
|
6
|
+
module Rendering
|
|
7
|
+
# Render a list of nodes with their marks. A mark shared by adjacent nodes is rendered once.
|
|
8
|
+
def render_content(nodes)
|
|
9
|
+
render_fragment(nodes, 0)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The schema of the document being rendered.
|
|
13
|
+
def prosemirror_schema
|
|
14
|
+
@_prosemirror_schema
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The URL, when its scheme is allowed, after the schema's `resolve_url`. Otherwise nil.
|
|
18
|
+
def safe_url(url)
|
|
19
|
+
prosemirror_schema.safe_url(url)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def render_fragment(nodes, depth)
|
|
25
|
+
index = 0
|
|
26
|
+
while index < nodes.size
|
|
27
|
+
mark = nodes[index].marks[depth]
|
|
28
|
+
unless mark
|
|
29
|
+
render_node(nodes[index])
|
|
30
|
+
index += 1
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
last = last_index_with_mark(nodes, index, depth, mark)
|
|
35
|
+
render_marked(mark, nodes[index..last], depth)
|
|
36
|
+
index = last + 1
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def last_index_with_mark(nodes, index, depth, mark)
|
|
41
|
+
return index unless prosemirror_schema.mark_type(mark.type)&.spanning?
|
|
42
|
+
|
|
43
|
+
index += 1 while index + 1 < nodes.size && nodes[index + 1].marks[depth] == mark
|
|
44
|
+
index
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_marked(mark, nodes, depth)
|
|
48
|
+
mark_type = prosemirror_schema.mark_type(mark.type)
|
|
49
|
+
return render_fragment(nodes, depth + 1) unless mark_type
|
|
50
|
+
|
|
51
|
+
render(prepare(mark_type.new(**mark.attrs))) { render_fragment(nodes, depth + 1) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_node(node)
|
|
55
|
+
return plain(node.text) if node.text?
|
|
56
|
+
|
|
57
|
+
node_type = prosemirror_schema.node_type(node.type)
|
|
58
|
+
return render_content(node.content) unless node_type
|
|
59
|
+
|
|
60
|
+
component = prepare(node_type.new(**node.attrs))
|
|
61
|
+
component.instance_variable_set(:@_prosemirror_node, node)
|
|
62
|
+
render(component) { render_content(node.content) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def prepare(component)
|
|
66
|
+
component.instance_variable_set(:@_prosemirror_schema, prosemirror_schema)
|
|
67
|
+
component
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|