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 +4 -4
- data/CHANGELOG.md +4 -0
- data/COOKBOOK.md +20 -3
- data/MIGRATING.md +131 -0
- data/README.md +3 -1
- data/ROADMAP.md +5 -0
- data/TYPES.md +2 -1
- data/lib/prosemirror/document.rb +18 -0
- data/lib/prosemirror/html_renderer.rb +2 -0
- data/lib/prosemirror/marks/basic/link.rb +1 -1
- data/lib/prosemirror/marks/link.rb +1 -1
- data/lib/prosemirror/nodes/basic/image.rb +1 -1
- data/lib/prosemirror/nodes/image.rb +1 -1
- data/lib/prosemirror/rendering.rb +3 -2
- data/lib/prosemirror/schema.rb +25 -3
- data/lib/prosemirror/type_definition.rb +11 -4
- data/lib/prosemirror/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76385bda3d9b696b80e976154bf40c5d33ad2b9ac79b48e5f3fa8a6f7bf224e8
|
|
4
|
+
data.tar.gz: 7acda4288b7bbb2d731a111143dc26b715177551476829ffa6416a7a298180c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 `©` 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
|
|
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.
|
data/lib/prosemirror/document.rb
CHANGED
|
@@ -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"
|
|
@@ -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
|
data/lib/prosemirror/schema.rb
CHANGED
|
@@ -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
|
|
198
|
-
|
|
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 =
|
|
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
|
-
|
|
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] }
|
data/lib/prosemirror/version.rb
CHANGED
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.
|
|
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
|