prosemirror 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8aae1987528266c7bd7c4b3c3d8f7c82e74f74cf89ee9e7ac87b1732ff76634d
4
- data.tar.gz: d5cb877b264eb0d39cd40cd8b4909ec94b423ac439a6719df23c70eb6cde6182
3
+ metadata.gz: 76385bda3d9b696b80e976154bf40c5d33ad2b9ac79b48e5f3fa8a6f7bf224e8
4
+ data.tar.gz: 7acda4288b7bbb2d731a111143dc26b715177551476829ffa6416a7a298180c6
5
5
  SHA512:
6
- metadata.gz: 947eab5fdc041f059be90be81b94bc66bb1505118b983ed982100faf1ccf2748fdcf753f83384385af72e80cc610473e24eeca31aa6a404ed93f431a8d3138d2
7
- data.tar.gz: 4a56748f16cc7ac1af9ad1660518870dfded5f22b089e714417cf74afb970e0ec8b9f53d2d9e1adc82662efac63c449ba1dff10d07988ed8ca110413143c2452
6
+ metadata.gz: 7c9fdb3905684864d3662c4bdb25c2cee8efb055d59a64b28b4dd414b4a08759deae97a5f071ab605e8c94ce235356c30bb54c364053d5cc2d0b105eee151c0a
7
+ data.tar.gz: 42054ca794261591e076b654aac9068bf629f7aeb82d18a5eb51fdbf4602d95dd5199907a4caa5363ad883b6b1628490ab848f32310aea4dbcbd10eadd7b7577
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2026-10-24
4
+
5
+ - `resolve_urls:` resolves all the URLs of a document at once, before rendering it, to look up records in one query, where `resolve_url:` is called once per URL. Attributes holding URLs are declared with `prop … url: true`, like links' `href` and images' `src`, and `Document#urls` lists them.
6
+
3
7
  ## [0.6.0] - 2026-10-24
4
8
 
5
9
  - Tiptap's YouTube and Twitch embeds, in `Schema.tiptap`: `youtube` and `twitch`, with the video's URL in `src`, rendered as an iframe of its embed URL, built from the video's id. They are imported from Tiptap's markup, and from iframes of embed URLs without it. Subclasses set Twitch's `parent` domain and YouTube's nocookie host. Markdown export writes a link to the video, or the iframe with `html: true`.
data/COOKBOOK.md CHANGED
@@ -104,7 +104,26 @@ end
104
104
 
105
105
  ## Links to records with GlobalID
106
106
 
107
- Store links to records as GlobalIDs (`gid://app/Page/1`), so they keep working when a record's URL changes, and turn them into URLs when rendering:
107
+ Store links to records as GlobalIDs (`gid://app/Page/1`), so they keep working when a record's URL changes, and turn them into URLs when rendering. `resolve_urls` gets all the URLs of a document before rendering it, so its records load in one query:
108
+
109
+ ```ruby
110
+ Prosemirror::Schema.tiptap(
111
+ resolve_urls: lambda do |urls|
112
+ gids = urls.select { |url| url.start_with?("gid://") }
113
+ records = GlobalID::Locator.locate_many(gids, ignore_missing: true).index_by { |record| record.to_global_id.to_s }
114
+
115
+ urls.to_h do |url|
116
+ next [url, url] unless url.start_with?("gid://")
117
+
118
+ [url, records[url]&.then { |record| Rails.application.routes.url_helpers.polymorphic_path(record) }]
119
+ end
120
+ end
121
+ )
122
+ ```
123
+
124
+ URLs are resolved before the allowed URL schemes are checked, so `gid://` doesn't need to be allowed. When a record doesn't exist, its link is removed, its text is kept, and the problem goes to `on_invalid`.
125
+
126
+ `resolve_url` is simpler, but runs one query per link:
108
127
 
109
128
  ```ruby
110
129
  Prosemirror::Schema.tiptap(
@@ -116,5 +135,3 @@ Prosemirror::Schema.tiptap(
116
135
  end
117
136
  )
118
137
  ```
119
-
120
- `resolve_url` runs before the allowed URL schemes are checked, so `gid://` doesn't need to be allowed. When the record doesn't exist, the link is removed, its text is kept, and the problem goes to `on_invalid`. Each link runs one query.
data/MIGRATING.md ADDED
@@ -0,0 +1,131 @@
1
+ # Migrating from prosemirror_to_html and html_to_prosemirror
2
+
3
+ `prosemirror` replaces [`prosemirror_to_html`](https://github.com/etaminstudio/prosemirror_to_html) and [`html_to_prosemirror`](https://github.com/etaminstudio/html_to_prosemirror), which have the same maintainers. Rendering and import use one schema, so a custom type is declared once for both.
4
+
5
+ ```ruby
6
+ # Gemfile
7
+ gem "prosemirror"
8
+ gem "herb" # optional, only if you need HTML import
9
+ ```
10
+
11
+ ## Rendering, from prosemirror_to_html
12
+
13
+ ```ruby
14
+ # Before
15
+ ProsemirrorToHtml::Renderer.new.render(json)
16
+
17
+ # After
18
+ Prosemirror.parse(json).to_html
19
+ ```
20
+
21
+ `parse` takes the same input: a JSON string, or a Hash with String or Symbol keys. Type names can be in snake_case or camelCase.
22
+
23
+ ### Custom nodes and marks
24
+
25
+ A node type is a [Phlex](https://www.phlex.fun) component, and its attributes are declared with `prop`. It's registered in a schema instead of a renderer:
26
+
27
+ ```ruby
28
+ # Before
29
+ class Callout < ProsemirrorToHtml::Nodes::Node
30
+ @node_type = "callout"
31
+
32
+ def tag
33
+ [{tag: "aside", attrs: {class: "callout callout-#{@node[:attrs][:tone]}"}}]
34
+ end
35
+ end
36
+
37
+ renderer = ProsemirrorToHtml::Renderer.new.add_node(Callout)
38
+ renderer.render(json)
39
+
40
+ # After
41
+ class Callout < Prosemirror::NodeType
42
+ group :block
43
+ content "block+"
44
+ prop :tone, _String?
45
+
46
+ def view_template(&) = aside(class: ["callout", @tone && "callout-#{@tone}"], &)
47
+ end
48
+
49
+ schema = Prosemirror::Schema.tiptap.with(Callout)
50
+ Prosemirror.parse(json, schema:).to_html
51
+ ```
52
+
53
+ | prosemirror_to_html | prosemirror |
54
+ |---|---|
55
+ | `@node_type = "callout"` | The class name (`Callout` → `callout`), or `type_name :callout` |
56
+ | `@tag_name`, `def tag` | `def view_template(&)`, which yields to render the content |
57
+ | `@node[:attrs][:tone]` | `prop :tone, _String?`, then `@tone` |
58
+ | `def self_closing = true` | No `content`: the node is a leaf |
59
+ | `def text` | `view_template` renders whatever the node shows |
60
+ | `renderer.add_node(Callout)`, `add_mark` | `Prosemirror::Schema.tiptap.with(Callout)` |
61
+ | `renderer.replace_node(Old, New)` | A subclass of the built-in type, registered with `with`, which replaces it |
62
+ | `with_nodes([...])`, `with_marks([...])` | `Prosemirror::Schema.new(nodes: [...], marks: [...])` |
63
+
64
+ Mark types work the same way, with `Prosemirror::MarkType`. See [Node and mark types](TYPES.md) for everything a type can declare.
65
+
66
+ ### Differences in the HTML
67
+
68
+ prosemirror_to_html built HTML strings, then cleaned them up with Loofah. `prosemirror` builds the HTML with Phlex, which escapes text and attribute values, and cleans nothing up afterwards:
69
+
70
+ - **Only declared attributes are rendered.** prosemirror_to_html wrote every attribute of a heading, link, image, ordered list or table cell into the HTML, so `{"level": 2, "id": "intro"}` gave `<h2 id="intro">`. `prosemirror` renders the attributes each type declares. To render more, subclass the type (see [Changing built-in types](README.md#changing-built-in-types)). Other attributes are kept in `to_h`.
71
+ - **Unsafe URLs are dropped.** A link whose `href` isn't allowed renders as its text, instead of an `<a>` without `href`, and an image without an allowed `src` renders nothing. See [URLs](README.md#urls).
72
+ - **Custom types render as written.** Loofah removed elements and attributes it didn't know, like `<iframe>`, `<picture>` or `srcset`. They now render. Pass URLs from the document through `safe_url`.
73
+ - **Invalid attributes are fixed and reported** with `on_invalid`, instead of being written as they are. See [Invalid documents](README.md#invalid-documents).
74
+ - **HTML5:** strike is `<s>`, not `<strike>`. A code block's language is a `language-` class on `<code>`, and an ordered list starting at 1 has no `start`.
75
+ - **Marks are nested in schema order**, as Tiptap nests them, rather than in the order of the JSON: `<strong><em>`.
76
+ - **More types:** highlight, task lists, details, mentions, and YouTube and Twitch embeds render, where prosemirror_to_html rendered only their text, or nothing.
77
+
78
+ ## HTML import, from html_to_prosemirror
79
+
80
+ ```ruby
81
+ # Before
82
+ HtmlToProsemirror::Renderer.new.render(html) # snake_case
83
+ HtmlToProsemirror::Renderer.new(case_type: :lower_camel).render(html) # camelCase
84
+
85
+ # After
86
+ Prosemirror.from_html(html).to_h
87
+ Prosemirror.from_html(html, schema: Prosemirror::Schema.tiptap(case: :camel)).to_h
88
+ ```
89
+
90
+ `to_h` returns a Hash with String keys, where html_to_prosemirror returned Symbol keys. HTML import needs the [herb](https://herb-tools.dev) gem.
91
+
92
+ ### Differences in the JSON
93
+
94
+ The JSON follows the schema, as Tiptap's `generateJSON` does:
95
+
96
+ - **Documents are valid.** Text in a list item is wrapped in a paragraph, text at the top level too, and attributes have their types: a heading's `level` is `1`, not `"1"`.
97
+ - **Nested marks keep their text.** html_to_prosemirror dropped text in nested marks, like `<a><b>bold link</b></a>`.
98
+ - **Styles become marks:** `font-weight`, `font-style`, `text-decoration` and `vertical-align`, as in pasted Word or Google Docs content.
99
+ - **Whitespace is kept** as HTML defines it: newlines in `<pre>` stay, and character references like `&copy` are decoded.
100
+ - **More types:** strike, underline, subscript, superscript, highlight, tables, task lists, details, mentions and embeds.
101
+
102
+ ### The `user` node
103
+
104
+ html_to_prosemirror turned `<user-mention data-id="7">` into a `user` node. `prosemirror` has Tiptap's `mention` instead. To keep the `user` node, declare it:
105
+
106
+ ```ruby
107
+ class User < Prosemirror::NodeType
108
+ group :inline
109
+ inline
110
+ prop :id, _String?, parse_html: ->(element) { element["data-id"] }
111
+ parse_html tag: "user-mention"
112
+
113
+ def view_template = span(class: "user-mention", data_id: @id)
114
+ end
115
+
116
+ schema = Prosemirror::Schema.tiptap.with(User)
117
+ Prosemirror.from_html(html, schema:).to_h # {"type" => "user", "attrs" => {"id" => "7"}}
118
+ ```
119
+
120
+ ## Going further
121
+
122
+ Beyond what the legacy gems did, `prosemirror` can:
123
+
124
+ - write a document as [Markdown](README.md#markdown-export), useful for LLMs, or as [plain text](README.md#usage);
125
+ - import [Markdown](README.md#markdown-import) and [plain text](README.md#text-import), with the same schema as HTML import;
126
+ - [query and transform documents](README.md#transforming-documents): nodes are immutable values with pattern matching, and `transform` builds a new document, for example to [remove empty paragraphs before saving](COOKBOOK.md#remove-empty-paragraphs-before-saving);
127
+ - [check documents](README.md#transforming-documents) against the schema, and fix and report [invalid documents](README.md#invalid-documents);
128
+ - render inside Rails views and [Phlex components](README.md#usage), and [your own components](COOKBOOK.md#render-your-own-components) for custom nodes;
129
+ - resolve link URLs, for example [links to records with GlobalID](COOKBOOK.md#links-to-records-with-globalid), with [allowed URL schemes](README.md#urls);
130
+ - use Tiptap's [task lists, details, mentions, YouTube and Twitch embeds, and text alignment](README.md#schemas), or ProseMirror's own schema;
131
+ - declare custom types with [content expressions, attributes and parse rules](TYPES.md).
data/README.md CHANGED
@@ -7,7 +7,7 @@ Render [ProseMirror](https://prosemirror.net) and [Tiptap](https://tiptap.dev) d
7
7
  - Link and image URLs must use an allowed scheme.
8
8
  - No Rails dependency.
9
9
 
10
- This gem replaces [prosemirror_to_html](https://github.com/etaminstudio/prosemirror_to_html) and [html_to_prosemirror](https://github.com/etaminstudio/html_to_prosemirror). See the [roadmap](ROADMAP.md) for what comes next.
10
+ This gem replaces [prosemirror_to_html](https://github.com/etaminstudio/prosemirror_to_html) and [html_to_prosemirror](https://github.com/etaminstudio/html_to_prosemirror): see [Migrating](MIGRATING.md). See the [roadmap](ROADMAP.md) for what comes next.
11
11
 
12
12
  ## Installation
13
13
 
@@ -259,6 +259,8 @@ Prosemirror::Schema.tiptap(
259
259
  )
260
260
  ```
261
261
 
262
+ `resolve_urls:` gets all the URLs of a document at once, before rendering it, and returns a Hash from each URL to its rewritten URL, to look up records in one query (see the [cookbook](COOKBOOK.md#links-to-records-with-globalid)). A URL missing from the Hash, or mapped to nil, is removed and reported, as with `resolve_url`. The URLs are those of the attributes declared with `url: true`, like links' `href` and images' `src`, which `doc.urls` lists. URLs that types build while rendering, like YouTube's embed URLs, are resolved one at a time.
263
+
262
264
  ## Development
263
265
 
264
266
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests, and `rake` to also run [Standard](https://github.com/standardrb/standard). You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/ROADMAP.md CHANGED
@@ -205,6 +205,11 @@ They differ in more than names:
205
205
  - Text alignment, as an option: `Schema.tiptap(text_align: true)`.
206
206
  - GitHub Flavored Markdown task lists in Markdown import and export. Details are written as GitHub's `<details>` HTML.
207
207
 
208
+ ### 0.7: Resolving URLs at once (released)
209
+
210
+ - `resolve_urls:` resolves all the URLs of a document before rendering it, to look up records in one query. Types declare their URL attributes with `prop … url: true`, and `Document#urls` lists them.
211
+ - A [migration guide](MIGRATING.md) from `prosemirror_to_html` and `html_to_prosemirror`.
212
+
208
213
  ### 1.0
209
214
 
210
215
  - Stable API, documentation site, benchmarks.
data/TYPES.md CHANGED
@@ -95,12 +95,13 @@ prop :alt, _String?
95
95
  - **Default:** used when the attribute is missing or invalid. Literal needs a frozen value or a Proc: `default: "left".freeze`, or a file with `# frozen_string_literal: true`. An attribute that accepts nil defaults to nil. An attribute without a default is required: without a valid value, the node or mark is removed.
96
96
  - **Coercion:** the block converts values before they are checked. `Prosemirror::Coerce::INTEGER` turns `"2"` into `2`.
97
97
 
98
- `prop` takes three more options:
98
+ `prop` takes four more options:
99
99
 
100
100
  | Option | Meaning |
101
101
  |---|---|
102
102
  | `json: "textAlign"` | The name in JSON, when it differs from the property name. |
103
103
  | `aliases: ["order"]` | Other JSON names accepted on input. |
104
+ | `url: true` | The attribute holds a URL: `doc.urls` lists it, and the schema's `resolve_urls` resolves it with the document's other URLs before rendering. Render it with `safe_url`. |
104
105
  | `parse_html: ->(element) { … }` | How to read the attribute from an HTML element, when importing HTML. By default, the HTML attribute with the JSON name is read. |
105
106
 
106
107
  Attributes that a type doesn't declare are kept in `unknown_attrs`. They are never rendered, and `to_h` gives them back.
@@ -24,6 +24,15 @@ module Prosemirror
24
24
  root.descendants(&)
25
25
  end
26
26
 
27
+ # The URLs in the attributes of its nodes and marks declared with `url: true`, like links' `href` and
28
+ # images' `src`, without duplicates.
29
+ def urls
30
+ each_with_object(Set.new) do |node, urls|
31
+ add_urls(urls, schema.node_type(node.type), node.attrs)
32
+ node.marks.each { |mark| add_urls(urls, schema.mark_type(mark.type), mark.attrs) }
33
+ end.to_a
34
+ end
35
+
27
36
  # The document as HTML. The string is `html_safe` when ActiveSupport is loaded.
28
37
  def to_html
29
38
  html = to_phlex.call
@@ -110,5 +119,14 @@ module Prosemirror
110
119
  def hash
111
120
  root.hash
112
121
  end
122
+
123
+ private
124
+
125
+ def add_urls(urls, type, attrs)
126
+ type&.url_attributes&.each do |name|
127
+ value = attrs[name]
128
+ urls << value if value.is_a?(String)
129
+ end
130
+ end
113
131
  end
114
132
  end
@@ -13,7 +13,9 @@ module Prosemirror
13
13
  @_prosemirror_schema = document.schema
14
14
  end
15
15
 
16
+ # The document's URLs are resolved at once, with the schema's `resolve_urls`, for each rendering.
16
17
  def view_template
18
+ @_prosemirror_urls = prosemirror_schema.resolve_urls(@document.urls) if prosemirror_schema.resolve_urls?
17
19
  render_content([@document.root])
18
20
  end
19
21
  end
@@ -5,7 +5,7 @@ module Prosemirror
5
5
  module Basic
6
6
  # ProseMirror's link. Without an allowed `href`, its text is rendered without a link.
7
7
  class Link < MarkType
8
- prop :href, _String(length: 1..)
8
+ prop :href, _String(length: 1..), url: true
9
9
  prop :title, _String?
10
10
  parse_html(tag: "a[href]") { |element, schema| schema.allowed_url?(element["href"]) && {} }
11
11
 
@@ -4,7 +4,7 @@ module Prosemirror
4
4
  module Marks
5
5
  # Tiptap's link. Without an allowed `href`, its text is rendered without a link.
6
6
  class Link < MarkType
7
- prop :href, _String(length: 1..)
7
+ prop :href, _String(length: 1..), url: true
8
8
  prop :target, _String?
9
9
  prop :rel, _String?
10
10
  prop :css_class, _String?, json: "class"
@@ -8,7 +8,7 @@ module Prosemirror
8
8
  group :inline
9
9
  inline
10
10
 
11
- prop :src, _String(length: 1..)
11
+ prop :src, _String(length: 1..), url: true
12
12
  prop :alt, _String?
13
13
  prop :title, _String?
14
14
  parse_html tag: "img[src]"
@@ -6,7 +6,7 @@ module Prosemirror
6
6
  class Image < NodeType
7
7
  group :block
8
8
 
9
- prop :src, _String(length: 1..)
9
+ prop :src, _String(length: 1..), url: true
10
10
  prop :alt, _String?
11
11
  prop :title, _String?
12
12
  prop :width, _Integer?(1..), &Coerce::INTEGER
@@ -14,9 +14,9 @@ module Prosemirror
14
14
  @_prosemirror_schema
15
15
  end
16
16
 
17
- # The URL, when its scheme is allowed, after the schema's `resolve_url`. Otherwise nil.
17
+ # The URL, when its scheme is allowed, after the schema's `resolve_url` or `resolve_urls`. Otherwise nil.
18
18
  def safe_url(url)
19
- prosemirror_schema.safe_url(url)
19
+ prosemirror_schema.safe_url(url, @_prosemirror_urls)
20
20
  end
21
21
 
22
22
  private
@@ -64,6 +64,7 @@ module Prosemirror
64
64
 
65
65
  def prepare(component)
66
66
  component.instance_variable_set(:@_prosemirror_schema, prosemirror_schema)
67
+ component.instance_variable_set(:@_prosemirror_urls, @_prosemirror_urls)
67
68
  component
68
69
  end
69
70
  end
@@ -19,6 +19,9 @@ module Prosemirror
19
19
  end
20
20
  # Called with each URL before it is checked, to rewrite it.
21
21
  prop :resolve_url, _Callable?, reader: false
22
+ # Called with all the URLs of a document at once, before rendering it: returns a Hash from each URL to
23
+ # its rewritten URL, or nil. Instead of `resolve_url`, to look up records in one query.
24
+ prop :resolve_urls, _Callable?, reader: false, predicate: :public
22
25
  # Raise `InvalidDocument` instead of fixing invalid documents.
23
26
  prop :strict, _Boolean, default: false, reader: false, predicate: :public
24
27
  # Called with an `InvalidDocument` error for each problem fixed.
@@ -66,6 +69,7 @@ module Prosemirror
66
69
  @textblocks = @nodes.to_h { |type| [type, textblock_type?(type)] }
67
70
  @content_matches = @nodes.to_h { |type| [type, ContentMatch.compile(type.content, self)] }
68
71
  raise ArgumentError, "the schema has no #{@top_node} node type" unless node_type(@top_node)
72
+ raise ArgumentError, "resolve_url and resolve_urls can't be used together" if @resolve_url && @resolve_urls
69
73
  end
70
74
 
71
75
  # The start state of a node type's content expression.
@@ -174,6 +178,14 @@ module Prosemirror
174
178
 
175
179
  # Whether a URL's scheme is allowed, or the URL is relative. Browsers ignore leading control characters
176
180
  # and spaces, and tabs and newlines anywhere in a URL.
181
+ # The URLs resolved with `resolve_urls`, as a Hash, or nil without it.
182
+ def resolve_urls(urls)
183
+ return unless @resolve_urls
184
+ return {} if urls.empty?
185
+
186
+ @resolve_urls.call(urls).to_h
187
+ end
188
+
177
189
  def allowed_url?(url)
178
190
  return false unless url.is_a?(String)
179
191
 
@@ -194,11 +206,13 @@ module Prosemirror
194
206
  @textblocks.fetch(type, false)
195
207
  end
196
208
 
197
- # The URL after `resolve_url`, when its scheme is allowed. Otherwise nil, and the problem is reported.
198
- def safe_url(url)
209
+ # The URL after `resolve_url` or `resolve_urls`, when its scheme is allowed. Otherwise nil, and the
210
+ # problem is reported. `resolved_urls` holds URLs already resolved with `resolve_urls`: others are
211
+ # resolved one at a time.
212
+ def safe_url(url, resolved_urls = nil)
199
213
  return nil unless url.is_a?(String)
200
214
 
201
- resolved = @resolve_url ? @resolve_url.call(url) : url
215
+ resolved = resolve(url, resolved_urls)
202
216
  return report("URL could not be resolved: #{url.inspect}") unless resolved.is_a?(String)
203
217
  return report("URL scheme not allowed: #{resolved.inspect}") unless allowed_url?(resolved)
204
218
 
@@ -227,6 +241,14 @@ module Prosemirror
227
241
 
228
242
  private
229
243
 
244
+ def resolve(url, resolved_urls)
245
+ return @resolve_url.call(url) if @resolve_url
246
+ return url unless @resolve_urls
247
+
248
+ resolved_urls = resolve_urls([url]) unless resolved_urls&.key?(url)
249
+ resolved_urls[url]
250
+ end
251
+
230
252
  def lookup(types)
231
253
  types.each_with_object({}) do |type, table|
232
254
  [type.type_name, *type.aliases].each { |name| table[name] ||= type }
@@ -23,14 +23,16 @@ module Prosemirror
23
23
  # - `json:` the attribute name in JSON, when it differs from the property name (`"textAlign"`);
24
24
  # - `aliases:` other JSON names accepted on input (`["order"]`);
25
25
  # - `parse_html:` a block reading the attribute from an HTML element, when importing HTML. By default,
26
- # the HTML attribute with the JSON name is read.
27
- def prop(name, type, kind = :keyword, json: nil, aliases: [], parse_html: nil, **, &)
26
+ # the HTML attribute with the JSON name is read;
27
+ # - `url: true` for an attribute holding a URL, which `Document#urls` lists and `resolve_urls` resolves
28
+ # before rendering.
29
+ def prop(name, type, kind = :keyword, json: nil, aliases: [], parse_html: nil, url: false, **, &)
28
30
  json = (json || name).to_s
29
31
  aliases = aliases.map(&:to_s)
30
32
  # The keys accepted in JSON: each name as written, in snake_case and in camelCase.
31
33
  keys = [json, *aliases].flat_map { |key| [key, Naming.snake(key), Naming.camel(key)] }.uniq.map(&:freeze).freeze
32
- attribute_names[name] = {json:, aliases:, parse_html:, keys:}.freeze
33
- @known_keys = nil
34
+ attribute_names[name] = {json:, aliases:, parse_html:, keys:, url:}.freeze
35
+ @known_keys = @url_attributes = nil
34
36
  super(name, type, kind, **, &)
35
37
  end
36
38
 
@@ -108,6 +110,11 @@ module Prosemirror
108
110
  @known_keys ||= attribute_names.each_value.flat_map { |names| names[:keys] }.to_set.freeze
109
111
  end
110
112
 
113
+ # The names of the attributes declared with `url: true`.
114
+ def url_attributes
115
+ @url_attributes ||= attribute_names.filter_map { |name, names| name if names[:url] }.freeze
116
+ end
117
+
111
118
  # Attributes as JSON, with their JSON names.
112
119
  def serialize_attrs(attrs)
113
120
  attrs.to_h { |name, value| [attribute_names.dig(name, :json) || name.to_s, value] }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prosemirror
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prosemirror
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hans Lemuet
@@ -62,6 +62,7 @@ files:
62
62
  - CHANGELOG.md
63
63
  - COOKBOOK.md
64
64
  - LICENSE.txt
65
+ - MIGRATING.md
65
66
  - README.md
66
67
  - ROADMAP.md
67
68
  - TYPES.md