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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/COOKBOOK.md +108 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +155 -12
  6. data/ROADMAP.md +215 -0
  7. data/TYPES.md +201 -0
  8. data/lib/prosemirror/coerce.rb +17 -0
  9. data/lib/prosemirror/content_expression.rb +94 -0
  10. data/lib/prosemirror/content_match.rb +241 -0
  11. data/lib/prosemirror/document.rb +76 -0
  12. data/lib/prosemirror/html/builder.rb +146 -0
  13. data/lib/prosemirror/html/element.rb +84 -0
  14. data/lib/prosemirror/html/entities.rb +87 -0
  15. data/lib/prosemirror/html/entities.txt +2231 -0
  16. data/lib/prosemirror/html/selector.rb +95 -0
  17. data/lib/prosemirror/html/splitter.rb +68 -0
  18. data/lib/prosemirror/html/text.rb +23 -0
  19. data/lib/prosemirror/html_parser.rb +498 -0
  20. data/lib/prosemirror/html_renderer.rb +20 -0
  21. data/lib/prosemirror/json_parser.rb +114 -0
  22. data/lib/prosemirror/mark.rb +19 -0
  23. data/lib/prosemirror/mark_type.rb +71 -0
  24. data/lib/prosemirror/marks/basic/code.rb +16 -0
  25. data/lib/prosemirror/marks/basic/em.rb +17 -0
  26. data/lib/prosemirror/marks/basic/link.rb +21 -0
  27. data/lib/prosemirror/marks/basic/strong.rb +17 -0
  28. data/lib/prosemirror/marks/bold.rb +19 -0
  29. data/lib/prosemirror/marks/code.rb +16 -0
  30. data/lib/prosemirror/marks/highlight.rb +18 -0
  31. data/lib/prosemirror/marks/italic.rb +15 -0
  32. data/lib/prosemirror/marks/link.rb +22 -0
  33. data/lib/prosemirror/marks/strike.rb +14 -0
  34. data/lib/prosemirror/marks/subscript.rb +13 -0
  35. data/lib/prosemirror/marks/superscript.rb +13 -0
  36. data/lib/prosemirror/marks/underline.rb +12 -0
  37. data/lib/prosemirror/naming.rb +16 -0
  38. data/lib/prosemirror/node.rb +77 -0
  39. data/lib/prosemirror/node_type.rb +123 -0
  40. data/lib/prosemirror/nodes/basic/code_block.rb +20 -0
  41. data/lib/prosemirror/nodes/basic/image.rb +23 -0
  42. data/lib/prosemirror/nodes/basic/ordered_list.rb +22 -0
  43. data/lib/prosemirror/nodes/blockquote.rb +13 -0
  44. data/lib/prosemirror/nodes/bullet_list.rb +13 -0
  45. data/lib/prosemirror/nodes/code_block.rb +24 -0
  46. data/lib/prosemirror/nodes/doc.rb +10 -0
  47. data/lib/prosemirror/nodes/hard_break.rb +15 -0
  48. data/lib/prosemirror/nodes/heading.rb +15 -0
  49. data/lib/prosemirror/nodes/horizontal_rule.rb +12 -0
  50. data/lib/prosemirror/nodes/image.rb +22 -0
  51. data/lib/prosemirror/nodes/list_item.rb +12 -0
  52. data/lib/prosemirror/nodes/ordered_list.rb +36 -0
  53. data/lib/prosemirror/nodes/paragraph.rb +13 -0
  54. data/lib/prosemirror/nodes/table.rb +15 -0
  55. data/lib/prosemirror/nodes/table_cell.rb +33 -0
  56. data/lib/prosemirror/nodes/table_header.rb +15 -0
  57. data/lib/prosemirror/nodes/table_row.rb +12 -0
  58. data/lib/prosemirror/nodes/text.rb +10 -0
  59. data/lib/prosemirror/parse_rule.rb +50 -0
  60. data/lib/prosemirror/rendering.rb +70 -0
  61. data/lib/prosemirror/schema.rb +241 -0
  62. data/lib/prosemirror/text_serializer.rb +46 -0
  63. data/lib/prosemirror/type_definition.rb +149 -0
  64. data/lib/prosemirror/version.rb +1 -1
  65. data/lib/prosemirror.rb +43 -2
  66. metadata +113 -16
  67. data/.rubocop.yml +0 -13
  68. data/Gemfile +0 -12
  69. data/Rakefile +0 -16
  70. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class Blockquote < NodeType
6
+ group :block
7
+ content "block+"
8
+ parse_html tag: "blockquote"
9
+
10
+ def view_template(&) = blockquote(&)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class BulletList < NodeType
6
+ group :block, :list
7
+ content "list_item+"
8
+ parse_html tag: "ul"
9
+
10
+ def view_template(&) = ul(&)
11
+ end
12
+ end
13
+ 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ # The top node of a document. It renders its content only.
6
+ class Doc < NodeType
7
+ content "block+"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class HardBreak < NodeType
6
+ group :inline
7
+ inline
8
+ leaf_text "\n"
9
+ linebreak_replacement
10
+ parse_html tag: "br"
11
+
12
+ def view_template = br
13
+ end
14
+ end
15
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class HorizontalRule < NodeType
6
+ group :block
7
+ parse_html tag: "hr"
8
+
9
+ def view_template = hr
10
+ end
11
+ end
12
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class ListItem < NodeType
6
+ content "paragraph block*"
7
+ parse_html tag: "li"
8
+
9
+ def view_template(&) = li(&)
10
+ end
11
+ end
12
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class Paragraph < NodeType
6
+ group :block
7
+ content "inline*"
8
+ parse_html tag: "p"
9
+
10
+ def view_template(&) = p(&)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class Table < NodeType
6
+ group :block
7
+ content "table_row+"
8
+ parse_html tag: "table"
9
+
10
+ def view_template(&)
11
+ table { tbody(&) }
12
+ end
13
+ end
14
+ end
15
+ 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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class TableHeader < TableCell
6
+ type_name :table_header
7
+
8
+ parse_html tag: "th"
9
+
10
+ def view_template(&)
11
+ th(**cell_attributes, &)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ class TableRow < NodeType
6
+ content "(table_cell | table_header)*"
7
+ parse_html tag: "tr"
8
+
9
+ def view_template(&) = tr(&)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ # Text. Text nodes are rendered by the renderer, not by this type.
6
+ class Text < NodeType
7
+ group :inline
8
+ end
9
+ end
10
+ 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