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,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # Builds nodes from ProseMirror JSON, and normalizes them as ProseMirror does:
5
+ # marks sorted in schema order, adjacent text with the same marks joined, empty text removed.
6
+ # Invalid parts are fixed and reported to the schema (which raises in strict mode).
7
+ class JSONParser
8
+ MAX_DEPTH = 100
9
+
10
+ def initialize(schema)
11
+ @schema = schema
12
+ end
13
+
14
+ def parse(json)
15
+ hash = json.is_a?(String) ? JSON.parse(json, max_nesting: (MAX_DEPTH * 2) + 2) : json
16
+ raise ArgumentError, "expected a Hash or a JSON String, got #{json.class}" unless hash.is_a?(Hash)
17
+
18
+ root = build_node(hash, "doc", 0) || Node.new(type: @schema.top_node)
19
+ return root if root.type == @schema.top_node
20
+
21
+ @schema.report("the top node is #{root.type}, not #{@schema.top_node}", path: "doc")
22
+ Node.new(type: @schema.top_node, content: [root])
23
+ rescue JSON::NestingError
24
+ raise InvalidDocument, "the document is nested more than #{MAX_DEPTH} levels deep"
25
+ end
26
+
27
+ private
28
+
29
+ def build_node(hash, path, depth)
30
+ return invalid("not a node: #{hash.inspect}", path) unless hash.is_a?(Hash)
31
+ return invalid("nested more than #{MAX_DEPTH} levels deep", path) if depth > MAX_DEPTH
32
+
33
+ name = fetch(hash, "type")
34
+ return invalid("missing node type", path) unless name.is_a?(String) || name.is_a?(Symbol)
35
+ return build_text(hash, path) if name.to_s == "text"
36
+
37
+ node_type = @schema.node_type(name)
38
+ return build_unknown(hash, name, path, depth) unless node_type
39
+
40
+ given = fetch(hash, "attrs")
41
+ attrs = node_type.build_attrs(given, report: reporter(path))
42
+ return invalid("removed #{name} node", path) unless attrs
43
+
44
+ Node.new(type: node_type.type_name, attrs: attrs, unknown_attrs: node_type.unknown_attrs(given),
45
+ content: build_content(hash, path, depth), marks: build_marks(fetch(hash, "marks"), path))
46
+ end
47
+
48
+ def build_text(hash, path)
49
+ text = fetch(hash, "text")
50
+ return invalid("text node without text", path) unless text.is_a?(String)
51
+ return nil if text.empty?
52
+
53
+ Node.new(type: :text, text: text, marks: build_marks(fetch(hash, "marks"), path))
54
+ end
55
+
56
+ def build_unknown(hash, name, path, depth)
57
+ invalid("unknown node type #{name}", path)
58
+ attrs = fetch(hash, "attrs")
59
+ Node.new(type: Naming.snake(name), attrs: attrs.is_a?(Hash) ? attrs.transform_keys(&:to_sym) : {},
60
+ content: build_content(hash, path, depth), marks: build_marks(fetch(hash, "marks"), path))
61
+ end
62
+
63
+ def build_content(hash, path, depth)
64
+ content = fetch(hash, "content")
65
+ return [] if content.nil?
66
+ return invalid("content is not a list", path) || [] unless content.is_a?(Array)
67
+
68
+ nodes = content.each_with_index.filter_map do |child, index|
69
+ build_node(child, "#{path}.content[#{index}]", depth + 1)
70
+ end
71
+ Node.join_text(nodes)
72
+ end
73
+
74
+ def build_marks(marks, path)
75
+ return [] if marks.nil?
76
+ return invalid("marks is not a list", path) || [] unless marks.is_a?(Array)
77
+
78
+ built = marks.each_with_index.filter_map { |mark, index| build_mark(mark, "#{path}.marks[#{index}]") }
79
+ built.uniq.sort_by.with_index { |mark, index| [@schema.mark_rank(mark.type), index] }
80
+ end
81
+
82
+ def build_mark(hash, path)
83
+ return invalid("not a mark: #{hash.inspect}", path) unless hash.is_a?(Hash)
84
+
85
+ name = fetch(hash, "type")
86
+ return invalid("missing mark type", path) unless name.is_a?(String) || name.is_a?(Symbol)
87
+
88
+ mark_type = @schema.mark_type(name)
89
+ unless mark_type
90
+ invalid("unknown mark type #{name}", path)
91
+ attrs = fetch(hash, "attrs")
92
+ return Mark.new(type: Naming.snake(name), attrs: attrs.is_a?(Hash) ? attrs.transform_keys(&:to_sym) : {})
93
+ end
94
+
95
+ given = fetch(hash, "attrs")
96
+ attrs = mark_type.build_attrs(given, report: reporter(path))
97
+ return invalid("removed #{name} mark", path) unless attrs
98
+
99
+ Mark.new(type: mark_type.type_name, attrs: attrs, unknown_attrs: mark_type.unknown_attrs(given))
100
+ end
101
+
102
+ def fetch(hash, key)
103
+ hash.key?(key) ? hash[key] : hash[key.to_sym]
104
+ end
105
+
106
+ def reporter(path)
107
+ ->(message) { @schema.report(message, path: path) }
108
+ end
109
+
110
+ def invalid(message, path)
111
+ @schema.report(message, path: path)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # A mark applied to a node, such as `bold` or `link`. Marks are immutable values.
5
+ #
6
+ # mark in { type: :link, attrs: { href: } }
7
+ #
8
+ # As for nodes, `unknown_attrs` holds the attributes the mark's type doesn't declare.
9
+ class Mark < Literal::Data
10
+ prop :type, Symbol, &Coerce::SYMBOL
11
+ prop :attrs, _Hash(Symbol, _Any?), default: {}.freeze, &Coerce::FROZEN
12
+ prop :unknown_attrs, _Hash(String, _Any?), default: {}.freeze, &Coerce::FROZEN
13
+
14
+ # A copy with some properties changed.
15
+ def with(**changes)
16
+ self.class.new(**to_h, **changes)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # The base class of mark types. A mark type is a Phlex component: `view_template` renders one element
5
+ # and yields to render the marked content. Attributes are Literal properties.
6
+ #
7
+ # class Bold < Prosemirror::MarkType
8
+ # def view_template(&) = strong(&)
9
+ # end
10
+ #
11
+ # The options follow ProseMirror's MarkSpec: https://prosemirror.net/docs/ref/#model.MarkSpec
12
+ class MarkType < Phlex::HTML
13
+ extend Literal::Properties
14
+ extend TypeDefinition
15
+ include Rendering
16
+
17
+ class << self
18
+ def base_type?
19
+ self == MarkType
20
+ end
21
+
22
+ # The marks this mark can't coexist with: mark or group names, `"_"` for all.
23
+ # Defaults to marks of the same type.
24
+ def excludes(spec = nil)
25
+ return @excludes = spec if spec
26
+
27
+ inherited_setting(:@excludes, nil)
28
+ end
29
+
30
+ # Whether this mark can't coexist with marks of the other type.
31
+ def excludes?(other)
32
+ case excludes
33
+ when nil then other.type_name == type_name
34
+ when "_" then true
35
+ else
36
+ names = excludes.split.map(&:to_sym)
37
+ names.include?(other.type_name) || names.intersect?(other.group)
38
+ end
39
+ end
40
+
41
+ def group(*names)
42
+ return @group = names.flat_map { |n| n.to_s.split }.map(&:to_sym) unless names.empty?
43
+
44
+ inherited_setting(:@group, [])
45
+ end
46
+
47
+ # Whether one element can wrap several adjacent nodes with this mark. Defaults to true.
48
+ def spanning(value = nil)
49
+ return @spanning = value unless value.nil?
50
+
51
+ inherited_setting(:@spanning, true)
52
+ end
53
+
54
+ def spanning?
55
+ spanning
56
+ end
57
+
58
+ def code(value = true)
59
+ @code = value
60
+ end
61
+
62
+ def code?
63
+ inherited_setting(:@code, false)
64
+ end
65
+ end
66
+
67
+ def view_template
68
+ yield if block_given?
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ module Basic
6
+ # ProseMirror's inline code, which can be combined with other marks.
7
+ class Code < MarkType
8
+ code
9
+
10
+ parse_html tag: "code"
11
+
12
+ def view_template(&) = code(&)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ module Basic
6
+ class Em < MarkType
7
+ aliases :italic
8
+ parse_html tag: "i"
9
+ parse_html tag: "em"
10
+ parse_html style: "font-style=italic"
11
+ parse_html style: "font-style=normal", clear_mark: true
12
+
13
+ def view_template(&) = em(&)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ module Basic
6
+ # ProseMirror's link. Without an allowed `href`, its text is rendered without a link.
7
+ class Link < MarkType
8
+ prop :href, _String(length: 1..)
9
+ prop :title, _String?
10
+ parse_html(tag: "a[href]") { |element, schema| schema.allowed_url?(element["href"]) && {} }
11
+
12
+ def view_template(&)
13
+ href = safe_url(@href)
14
+ return yield unless href
15
+
16
+ a(href: href, title: @title, &)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ module Basic
6
+ class Strong < MarkType
7
+ aliases :bold
8
+ parse_html tag: "strong"
9
+ parse_html(tag: "b") { |element| element.style["font-weight"] != "normal" && {} }
10
+ parse_html style: "font-weight=400", clear_mark: true
11
+ parse_html(style: "font-weight") { |value| value.match?(Bold::BOLD) && {} }
12
+
13
+ def view_template(&) = strong(&)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ class Bold < MarkType
6
+ # The `font-weight` values that mean bold.
7
+ BOLD = /\A(bold(er)?|[5-9]\d{2,})\z/
8
+
9
+ aliases :strong
10
+
11
+ parse_html tag: "strong"
12
+ parse_html(tag: "b") { |element| element.style["font-weight"] != "normal" && {} }
13
+ parse_html style: "font-weight=400", clear_mark: true
14
+ parse_html(style: "font-weight") { |value| value.match?(BOLD) && {} }
15
+
16
+ def view_template(&) = strong(&)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ # Tiptap's inline code, which can't be combined with other marks.
6
+ class Code < MarkType
7
+ code
8
+ excludes "_"
9
+
10
+ parse_html tag: "code"
11
+ parse_html tag: "tt"
12
+
13
+ def view_template(&) = code(&)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ # Tiptap's highlight. `color` must be a hex, named, rgb() or hsl() color, so it is safe in a style.
6
+ class Highlight < MarkType
7
+ COLOR = %r{\A(\#\h{3,8}|[a-z]+|(rgb|hsl)a?\([\d\s.,%/]+\))\z}i
8
+
9
+ prop :color, _String?(COLOR), parse_html: ->(element) { element["data-color"] || element.style["background-color"] }
10
+
11
+ parse_html tag: "mark"
12
+
13
+ def view_template(&)
14
+ mark(data_color: @color, style: @color && "background-color: #{@color}; color: inherit", &)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ class Italic < MarkType
6
+ aliases :em
7
+ parse_html tag: "em"
8
+ parse_html(tag: "i") { |element| element.style["font-style"] != "normal" && {} }
9
+ parse_html style: "font-style=normal", clear_mark: true
10
+ parse_html style: "font-style=italic"
11
+
12
+ def view_template(&) = em(&)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ # Tiptap's link. Without an allowed `href`, its text is rendered without a link.
6
+ class Link < MarkType
7
+ prop :href, _String(length: 1..)
8
+ prop :target, _String?
9
+ prop :rel, _String?
10
+ prop :css_class, _String?, json: "class"
11
+ prop :title, _String?
12
+ parse_html(tag: "a[href]") { |element, schema| schema.allowed_url?(element["href"]) && {} }
13
+
14
+ def view_template(&)
15
+ href = safe_url(@href)
16
+ return yield unless href
17
+
18
+ a(href: href, target: @target, rel: @rel, class: @css_class, title: @title, &)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ class Strike < MarkType
6
+ parse_html tag: "s"
7
+ parse_html tag: "del"
8
+ parse_html tag: "strike"
9
+ parse_html(style: "text-decoration", consuming: false) { |value| value.include?("line-through") ? {} : false }
10
+
11
+ def view_template(&) = s(&)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ class Subscript < MarkType
6
+ excludes "superscript"
7
+ parse_html tag: "sub"
8
+ parse_html(style: "vertical-align") { |value| (value == "sub") ? {} : false }
9
+
10
+ def view_template(&) = sub(&)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ class Superscript < MarkType
6
+ excludes "subscript"
7
+ parse_html tag: "sup"
8
+ parse_html(style: "vertical-align") { |value| (value == "super") ? {} : false }
9
+
10
+ def view_template(&) = sup(&)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Marks
5
+ class Underline < MarkType
6
+ parse_html tag: "u"
7
+ parse_html(style: "text-decoration", consuming: false) { |value| value.include?("underline") ? {} : false }
8
+
9
+ def view_template(&) = u(&)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # Converts names between snake_case (`bullet_list`) and camelCase (`bulletList`).
5
+ module Naming
6
+ module_function
7
+
8
+ def snake(name)
9
+ name.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase
10
+ end
11
+
12
+ def camel(name)
13
+ snake(name).gsub(/_([a-z\d])/) { Regexp.last_match(1).upcase }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # A node of a document. Nodes are immutable values, and support pattern matching:
5
+ #
6
+ # node in { type: :heading, attrs: { level: 1 } }
7
+ #
8
+ # Text nodes have the type `:text` and their content in `text`.
9
+ #
10
+ # `attrs` holds the attributes the node's type declares. `unknown_attrs` holds the others, as written
11
+ # in JSON (for example from a Tiptap extension the schema doesn't know): they are never rendered,
12
+ # and `to_h` gives them back unchanged.
13
+ class Node < Literal::Data
14
+ prop :type, Symbol, &Coerce::SYMBOL
15
+ prop :attrs, _Hash(Symbol, _Any?), default: {}.freeze, &Coerce::FROZEN
16
+ prop :unknown_attrs, _Hash(String, _Any?), default: {}.freeze, &Coerce::FROZEN
17
+ prop :content, _Array(Node), default: [].freeze, &Coerce::FROZEN
18
+ prop :marks, _Array(Mark), default: [].freeze, &Coerce::FROZEN
19
+ prop :text, _String?, &Coerce::FROZEN
20
+
21
+ # The nodes with adjacent text nodes that have the same marks joined, as in ProseMirror.
22
+ def self.join_text(nodes)
23
+ nodes.each_with_object([]) do |node, joined|
24
+ previous = joined.last
25
+ if previous&.text? && node.text? && previous.marks == node.marks
26
+ joined[-1] = previous.with(text: previous.text + node.text)
27
+ else
28
+ joined << node
29
+ end
30
+ end
31
+ end
32
+
33
+ # A copy with some properties changed.
34
+ def with(**changes)
35
+ self.class.new(**to_h, **changes)
36
+ end
37
+
38
+ def text?
39
+ @type == :text
40
+ end
41
+
42
+ def first_child
43
+ @content.first
44
+ end
45
+
46
+ def last_child
47
+ @content.last
48
+ end
49
+
50
+ def child_count
51
+ @content.size
52
+ end
53
+
54
+ # All text in this node and its descendants, joined without separators.
55
+ def text_content
56
+ return @text if text?
57
+
58
+ descendants.select(&:text?).map(&:text).join
59
+ end
60
+
61
+ # Iterate over all descendants, depth first, without the node itself.
62
+ def descendants
63
+ return enum_for(:descendants) unless block_given?
64
+
65
+ stack = @content.reverse
66
+ until stack.empty?
67
+ node = stack.pop
68
+ yield node
69
+ stack.concat(node.content.reverse)
70
+ end
71
+ end
72
+
73
+ def same_markup?(other)
74
+ @type == other.type && @attrs == other.attrs && @unknown_attrs == other.unknown_attrs && @marks == other.marks
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # The base class of node types. A node type is a Phlex component: `view_template` renders a node,
5
+ # and yields to render its content. Attributes are Literal properties, available as instance variables.
6
+ #
7
+ # class Heading < Prosemirror::NodeType
8
+ # group :block
9
+ # content "inline*"
10
+ # prop :level, _Integer(1..6), default: 1
11
+ #
12
+ # def view_template(&) = tag(:"h#{@level}", &)
13
+ # end
14
+ #
15
+ # The options follow ProseMirror's NodeSpec: https://prosemirror.net/docs/ref/#model.NodeSpec
16
+ class NodeType < Phlex::HTML
17
+ extend Literal::Properties
18
+ extend TypeDefinition
19
+ include Rendering
20
+
21
+ class << self
22
+ def base_type?
23
+ self == NodeType
24
+ end
25
+
26
+ # The content expression, such as `"block+"`. Nodes without one are leaves.
27
+ def content(expression = nil)
28
+ return @content = ContentExpression.new(expression) if expression
29
+
30
+ inherited_setting(:@content, nil)
31
+ end
32
+
33
+ # The groups this type belongs to, such as `:block` or `:inline`.
34
+ def group(*names)
35
+ return @group = names.flat_map { |n| n.to_s.split }.map(&:to_sym) unless names.empty?
36
+
37
+ inherited_setting(:@group, [])
38
+ end
39
+
40
+ def inline(value = true)
41
+ @inline = value
42
+ end
43
+
44
+ def inline?
45
+ inherited_setting(:@inline, false) || group.include?(:inline)
46
+ end
47
+
48
+ def block?
49
+ !inline?
50
+ end
51
+
52
+ def leaf?
53
+ content.nil?
54
+ end
55
+
56
+ # The marks allowed inside this node: mark or group names, `"_"` for all, `""` for none.
57
+ def marks(spec = nil)
58
+ return @marks = spec if spec
59
+
60
+ inherited_setting(:@marks, nil)
61
+ end
62
+
63
+ # `:pre` to keep whitespace when parsing. Implied by `code`.
64
+ def whitespace(value = nil)
65
+ return @whitespace = value if value
66
+
67
+ inherited_setting(:@whitespace, code? ? :pre : :normal)
68
+ end
69
+
70
+ def code(value = true)
71
+ @code = value
72
+ end
73
+
74
+ def code?
75
+ inherited_setting(:@code, false)
76
+ end
77
+
78
+ # The text of a leaf node in `to_text`, as a String or a block called with the node.
79
+ def leaf_text(text = nil, &block)
80
+ return @leaf_text = text || block if text || block
81
+
82
+ inherited_setting(:@leaf_text, nil)
83
+ end
84
+
85
+ def text_for(node)
86
+ value = leaf_text
87
+ value.respond_to?(:call) ? value.call(node).to_s : value.to_s
88
+ end
89
+
90
+ # This inline node stands for a newline, as ProseMirror's `linebreakReplacement`: when importing
91
+ # HTML whose whitespace is preserved, newlines become nodes of this type.
92
+ def linebreak_replacement(value = true)
93
+ @linebreak_replacement = value
94
+ end
95
+
96
+ def linebreak_replacement?
97
+ inherited_setting(:@linebreak_replacement, false)
98
+ end
99
+
100
+ # Whether marks of this type can be applied inside this node, according to its `marks` option.
101
+ def allows_mark?(mark_type, schema)
102
+ case marks
103
+ when nil then schema.content_match(self).inline_content?
104
+ when "_" then true
105
+ when "" then false
106
+ else
107
+ names = marks.split.map(&:to_sym)
108
+ names.include?(mark_type.type_name) || names.intersect?(mark_type.group)
109
+ end
110
+ end
111
+ end
112
+
113
+ # The node being rendered.
114
+ def node
115
+ @_prosemirror_node
116
+ end
117
+
118
+ # Render the node's content when no `view_template` is defined.
119
+ def view_template
120
+ yield if block_given?
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module Nodes
5
+ module Basic
6
+ # ProseMirror's code block, without attributes.
7
+ class CodeBlock < NodeType
8
+ group :block
9
+ content "text*"
10
+ marks ""
11
+ code
12
+ parse_html tag: "pre", preserve_whitespace: :full
13
+
14
+ def view_template(&)
15
+ pre { code(&) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end