prosemirror 0.0.1 → 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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/COOKBOOK.md +108 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +169 -12
  6. data/ROADMAP.md +211 -0
  7. data/lib/prosemirror/coerce.rb +17 -0
  8. data/lib/prosemirror/content_expression.rb +94 -0
  9. data/lib/prosemirror/document.rb +76 -0
  10. data/lib/prosemirror/html_renderer.rb +20 -0
  11. data/lib/prosemirror/json_parser.rb +125 -0
  12. data/lib/prosemirror/mark.rb +19 -0
  13. data/lib/prosemirror/mark_type.rb +60 -0
  14. data/lib/prosemirror/marks/basic/em.rb +13 -0
  15. data/lib/prosemirror/marks/basic/link.rb +20 -0
  16. data/lib/prosemirror/marks/basic/strong.rb +13 -0
  17. data/lib/prosemirror/marks/bold.rb +11 -0
  18. data/lib/prosemirror/marks/code.rb +11 -0
  19. data/lib/prosemirror/marks/highlight.rb +16 -0
  20. data/lib/prosemirror/marks/italic.rb +11 -0
  21. data/lib/prosemirror/marks/link.rb +21 -0
  22. data/lib/prosemirror/marks/strike.rb +9 -0
  23. data/lib/prosemirror/marks/subscript.rb +11 -0
  24. data/lib/prosemirror/marks/superscript.rb +11 -0
  25. data/lib/prosemirror/marks/underline.rb +9 -0
  26. data/lib/prosemirror/naming.rb +16 -0
  27. data/lib/prosemirror/node.rb +65 -0
  28. data/lib/prosemirror/node_type.rb +101 -0
  29. data/lib/prosemirror/nodes/basic/code_block.rb +19 -0
  30. data/lib/prosemirror/nodes/basic/image.rb +22 -0
  31. data/lib/prosemirror/nodes/basic/ordered_list.rb +19 -0
  32. data/lib/prosemirror/nodes/blockquote.rb +12 -0
  33. data/lib/prosemirror/nodes/bullet_list.rb +12 -0
  34. data/lib/prosemirror/nodes/code_block.rb +19 -0
  35. data/lib/prosemirror/nodes/doc.rb +10 -0
  36. data/lib/prosemirror/nodes/hard_break.rb +13 -0
  37. data/lib/prosemirror/nodes/heading.rb +14 -0
  38. data/lib/prosemirror/nodes/horizontal_rule.rb +11 -0
  39. data/lib/prosemirror/nodes/image.rb +21 -0
  40. data/lib/prosemirror/nodes/list_item.rb +11 -0
  41. data/lib/prosemirror/nodes/ordered_list.rb +18 -0
  42. data/lib/prosemirror/nodes/paragraph.rb +12 -0
  43. data/lib/prosemirror/nodes/table.rb +14 -0
  44. data/lib/prosemirror/nodes/table_cell.rb +23 -0
  45. data/lib/prosemirror/nodes/table_header.rb +13 -0
  46. data/lib/prosemirror/nodes/table_row.rb +11 -0
  47. data/lib/prosemirror/nodes/text.rb +10 -0
  48. data/lib/prosemirror/rendering.rb +70 -0
  49. data/lib/prosemirror/schema.rb +190 -0
  50. data/lib/prosemirror/text_serializer.rb +46 -0
  51. data/lib/prosemirror/type_definition.rb +119 -0
  52. data/lib/prosemirror/version.rb +1 -1
  53. data/lib/prosemirror.rb +33 -2
  54. metadata +101 -16
  55. data/.rubocop.yml +0 -13
  56. data/Gemfile +0 -12
  57. data/Rakefile +0 -16
  58. data/prosemirror.gemspec +0 -35
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # Extracts the text of a document, as ProseMirror's `textBetween` and Tiptap's `getText` do:
5
+ # textblocks and leaf blocks with text are separated by `block_separator`,
6
+ # and leaf nodes such as hard breaks are replaced by their `leaf_text`.
7
+ class TextSerializer
8
+ def initialize(schema, block_separator: "\n\n")
9
+ @schema = schema
10
+ @block_separator = block_separator
11
+ end
12
+
13
+ def call(root)
14
+ text = +""
15
+ first = true
16
+
17
+ root.descendants do |node|
18
+ node_type = @schema.node_type(node.type)
19
+ node_text = text_of(node, node_type)
20
+
21
+ if separated?(node_type, node_text)
22
+ text << @block_separator unless first
23
+ first = false
24
+ end
25
+ text << node_text
26
+ end
27
+
28
+ text
29
+ end
30
+
31
+ private
32
+
33
+ def text_of(node, node_type)
34
+ return node.text if node.text?
35
+ return "" unless node_type&.leaf?
36
+
37
+ node_type.text_for(node)
38
+ end
39
+
40
+ def separated?(node_type, node_text)
41
+ return false unless node_type&.block?
42
+
43
+ node_type.leaf? ? !node_text.empty? : @schema.textblock?(node_type)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # Class-level declarations shared by node types and mark types.
5
+ module TypeDefinition
6
+ # The name of the type, as a snake_case Symbol. It defaults to the class name (`BulletList` → `:bullet_list`),
7
+ # and subclasses of a concrete type keep its name.
8
+ def type_name(name = nil)
9
+ return @type_name = Naming.snake(name).to_sym if name
10
+
11
+ @type_name ||= concrete_superclass&.type_name || derived_type_name
12
+ end
13
+
14
+ # Other names accepted on input for this type, such as `:strong` for `:bold`.
15
+ def aliases(*names)
16
+ return @aliases = names.map { |n| Naming.snake(n).to_sym } unless names.empty?
17
+
18
+ inherited_setting(:@aliases, [])
19
+ end
20
+
21
+ # Declare an attribute. Same as Literal's `prop`, with two options:
22
+ # - `json:` the attribute name in JSON, when it differs from the property name (`"textAlign"`);
23
+ # - `aliases:` other JSON names accepted on input (`["order"]`).
24
+ def prop(name, type, kind = :keyword, json: nil, aliases: [], **, &)
25
+ attribute_names[name] = {json: (json || name).to_s, aliases: aliases.map(&:to_s)}.freeze
26
+ super(name, type, kind, **, &)
27
+ end
28
+
29
+ def attribute_names
30
+ @attribute_names ||= concrete_superclass ? concrete_superclass.attribute_names.dup : {}
31
+ end
32
+
33
+ # Build valid attributes from JSON attributes (a Hash with String or Symbol keys).
34
+ # Invalid values are converted when the meaning is clear, or replaced by their default.
35
+ # Returns nil when a required attribute is missing or invalid.
36
+ def build_attrs(given, report:)
37
+ given = given.is_a?(Hash) ? given.transform_keys(&:to_s) : {}
38
+
39
+ literal_properties.each_with_object({}) do |property, attrs|
40
+ json_name = attribute_names.dig(property.name, :json)
41
+ key = attribute_key(given, attribute_names.fetch(property.name))
42
+
43
+ if key
44
+ value = coerce_attribute(property, given[key])
45
+ next attrs[property.name] = value if property.type === value
46
+
47
+ report.call("invalid attribute #{json_name}: #{given[key].inspect}")
48
+ elsif !optional_attribute?(property)
49
+ report.call("missing attribute #{json_name}")
50
+ end
51
+ return nil unless optional_attribute?(property)
52
+
53
+ attrs[property.name] = default_attribute(property)
54
+ end
55
+ end
56
+
57
+ # The JSON attributes this type doesn't declare, with String keys, so that `to_h` gives them back.
58
+ def unknown_attrs(given)
59
+ return {} unless given.is_a?(Hash)
60
+
61
+ known = attribute_names.values.flat_map do |names|
62
+ [names[:json], *names[:aliases]].flat_map { |name| [name, Naming.snake(name), Naming.camel(name)] }
63
+ end
64
+ given.transform_keys(&:to_s).except(*known)
65
+ end
66
+
67
+ # Attributes as JSON, with their JSON names.
68
+ def serialize_attrs(attrs)
69
+ attrs.to_h { |name, value| [attribute_names.dig(name, :json) || name.to_s, value] }
70
+ end
71
+
72
+ def base_type?
73
+ false
74
+ end
75
+
76
+ private
77
+
78
+ def concrete_superclass
79
+ superclass unless superclass.base_type?
80
+ end
81
+
82
+ def derived_type_name
83
+ raise ArgumentError, "anonymous types need a type_name" if name.nil?
84
+
85
+ Naming.snake(name.split("::").last).to_sym
86
+ end
87
+
88
+ def inherited_setting(ivar, default)
89
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
90
+ return default if superclass.base_type?
91
+
92
+ superclass.send(:inherited_setting, ivar, default)
93
+ end
94
+
95
+ def attribute_key(given, names)
96
+ [names[:json], *names[:aliases]].each do |name|
97
+ [name, Naming.snake(name), Naming.camel(name)].each do |candidate|
98
+ return candidate if given.key?(candidate)
99
+ end
100
+ end
101
+ nil
102
+ end
103
+
104
+ def coerce_attribute(property, value)
105
+ property.coercion ? property.coerce(value, context: self) : value
106
+ rescue
107
+ value
108
+ end
109
+
110
+ def optional_attribute?(property)
111
+ # Checks whether the Literal type accepts nil, so `.nil?` would be wrong here.
112
+ property.default? || property.type === nil # standard:disable Style/NilComparison
113
+ end
114
+
115
+ def default_attribute(property)
116
+ property.default? ? property.default_value(self) : nil
117
+ end
118
+ end
119
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prosemirror
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/prosemirror.rb CHANGED
@@ -1,8 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "prosemirror/version"
3
+ require "json"
4
+ require "literal"
5
+ require "phlex"
6
+ require "zeitwerk"
4
7
 
8
+ loader = Zeitwerk::Loader.for_gem
9
+ loader.inflector.inflect("html_renderer" => "HTMLRenderer", "json_parser" => "JSONParser")
10
+ loader.setup
11
+
12
+ # Render ProseMirror and Tiptap documents.
5
13
  module Prosemirror
6
14
  class Error < StandardError; end
7
- # Your code goes here...
15
+
16
+ # Raised in strict mode, and passed to `on_invalid`, when a document doesn't match its schema.
17
+ class InvalidDocument < Error
18
+ attr_reader :path
19
+
20
+ def initialize(message, path: nil)
21
+ @path = path
22
+ super(path ? "#{path}: #{message}" : message)
23
+ end
24
+ end
25
+
26
+ class << self
27
+ attr_writer :schema
28
+
29
+ # The schema used when none is given. Defaults to `Schema.tiptap`.
30
+ def schema
31
+ @schema ||= Schema.tiptap
32
+ end
33
+
34
+ # Build a document from ProseMirror JSON, given as a Hash or a String.
35
+ def parse(json, schema: self.schema)
36
+ schema.parse(json)
37
+ end
38
+ end
8
39
  end
metadata CHANGED
@@ -1,38 +1,124 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prosemirror
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hans Lemuet
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-07-03 00:00:00.000000000 Z
12
- dependencies: []
13
- description:
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: literal
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: phlex
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.4'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.4'
40
+ - !ruby/object:Gem::Dependency
41
+ name: zeitwerk
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.6'
54
+ description: Render ProseMirror and Tiptap JSON documents to safe HTML or plain text,
55
+ with typed attributes and custom node types.
14
56
  email:
15
57
  - hans@etamin.studio
16
58
  executables: []
17
59
  extensions: []
18
60
  extra_rdoc_files: []
19
61
  files:
20
- - ".rubocop.yml"
21
62
  - CHANGELOG.md
22
- - Gemfile
63
+ - COOKBOOK.md
64
+ - LICENSE.txt
23
65
  - README.md
24
- - Rakefile
66
+ - ROADMAP.md
25
67
  - lib/prosemirror.rb
68
+ - lib/prosemirror/coerce.rb
69
+ - lib/prosemirror/content_expression.rb
70
+ - lib/prosemirror/document.rb
71
+ - lib/prosemirror/html_renderer.rb
72
+ - lib/prosemirror/json_parser.rb
73
+ - lib/prosemirror/mark.rb
74
+ - lib/prosemirror/mark_type.rb
75
+ - lib/prosemirror/marks/basic/em.rb
76
+ - lib/prosemirror/marks/basic/link.rb
77
+ - lib/prosemirror/marks/basic/strong.rb
78
+ - lib/prosemirror/marks/bold.rb
79
+ - lib/prosemirror/marks/code.rb
80
+ - lib/prosemirror/marks/highlight.rb
81
+ - lib/prosemirror/marks/italic.rb
82
+ - lib/prosemirror/marks/link.rb
83
+ - lib/prosemirror/marks/strike.rb
84
+ - lib/prosemirror/marks/subscript.rb
85
+ - lib/prosemirror/marks/superscript.rb
86
+ - lib/prosemirror/marks/underline.rb
87
+ - lib/prosemirror/naming.rb
88
+ - lib/prosemirror/node.rb
89
+ - lib/prosemirror/node_type.rb
90
+ - lib/prosemirror/nodes/basic/code_block.rb
91
+ - lib/prosemirror/nodes/basic/image.rb
92
+ - lib/prosemirror/nodes/basic/ordered_list.rb
93
+ - lib/prosemirror/nodes/blockquote.rb
94
+ - lib/prosemirror/nodes/bullet_list.rb
95
+ - lib/prosemirror/nodes/code_block.rb
96
+ - lib/prosemirror/nodes/doc.rb
97
+ - lib/prosemirror/nodes/hard_break.rb
98
+ - lib/prosemirror/nodes/heading.rb
99
+ - lib/prosemirror/nodes/horizontal_rule.rb
100
+ - lib/prosemirror/nodes/image.rb
101
+ - lib/prosemirror/nodes/list_item.rb
102
+ - lib/prosemirror/nodes/ordered_list.rb
103
+ - lib/prosemirror/nodes/paragraph.rb
104
+ - lib/prosemirror/nodes/table.rb
105
+ - lib/prosemirror/nodes/table_cell.rb
106
+ - lib/prosemirror/nodes/table_header.rb
107
+ - lib/prosemirror/nodes/table_row.rb
108
+ - lib/prosemirror/nodes/text.rb
109
+ - lib/prosemirror/rendering.rb
110
+ - lib/prosemirror/schema.rb
111
+ - lib/prosemirror/text_serializer.rb
112
+ - lib/prosemirror/type_definition.rb
26
113
  - lib/prosemirror/version.rb
27
- - prosemirror.gemspec
28
114
  - sig/prosemirror.rbs
29
115
  homepage: https://github.com/etaminstudio/prosemirror
30
- licenses: []
116
+ licenses:
117
+ - MIT
31
118
  metadata:
32
- homepage_uri: https://github.com/etaminstudio/prosemirror
33
119
  source_code_uri: https://github.com/etaminstudio/prosemirror
34
120
  changelog_uri: https://github.com/etaminstudio/prosemirror/blob/main/CHANGELOG.md
35
- post_install_message:
121
+ rubygems_mfa_required: 'true'
36
122
  rdoc_options: []
37
123
  require_paths:
38
124
  - lib
@@ -40,15 +126,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
40
126
  requirements:
41
127
  - - ">="
42
128
  - !ruby/object:Gem::Version
43
- version: 2.6.0
129
+ version: 3.3.0
44
130
  required_rubygems_version: !ruby/object:Gem::Requirement
45
131
  requirements:
46
132
  - - ">="
47
133
  - !ruby/object:Gem::Version
48
134
  version: '0'
49
135
  requirements: []
50
- rubygems_version: 3.3.26
51
- signing_key:
136
+ rubygems_version: 4.0.20
52
137
  specification_version: 4
53
- summary: Convert HTML to ProseMirror JSON and back.
138
+ summary: Render ProseMirror and Tiptap documents to HTML and text.
54
139
  test_files: []
data/.rubocop.yml DELETED
@@ -1,13 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
11
-
12
- Layout/LineLength:
13
- Max: 120
data/Gemfile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- # Specify your gem's dependencies in prosemirror.gemspec
6
- gemspec
7
-
8
- gem "rake", "~> 13.0"
9
-
10
- gem "minitest", "~> 5.0"
11
-
12
- gem "rubocop", "~> 1.21"
data/Rakefile DELETED
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
5
-
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/test_*.rb"]
10
- end
11
-
12
- require "rubocop/rake_task"
13
-
14
- RuboCop::RakeTask.new
15
-
16
- task default: %i[test rubocop]
data/prosemirror.gemspec DELETED
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/prosemirror/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "prosemirror"
7
- spec.version = Prosemirror::VERSION
8
- spec.authors = ["Hans Lemuet"]
9
- spec.email = ["hans@etamin.studio"]
10
-
11
- spec.summary = "Convert HTML to ProseMirror JSON and back."
12
- spec.homepage = "https://github.com/etaminstudio/prosemirror"
13
- spec.required_ruby_version = ">= 2.6.0"
14
-
15
- spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/etaminstudio/prosemirror"
17
- spec.metadata["changelog_uri"] = "https://github.com/etaminstudio/prosemirror/blob/main/CHANGELOG.md"
18
-
19
- # Specify which files should be added to the gem when it is released.
20
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
- spec.files = Dir.chdir(__dir__) do
22
- `git ls-files -z`.split("\x0").reject do |f|
23
- (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
24
- end
25
- end
26
- spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
- spec.require_paths = ["lib"]
29
-
30
- # Uncomment to register a new dependency of your gem
31
- # spec.add_dependency "example-gem", "~> 1.0"
32
-
33
- # For more information and examples about making a new gem, check out our
34
- # guide at: https://bundler.io/guides/creating_gem.html
35
- end