prosemirror 0.1.0 → 0.3.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 +16 -0
- data/README.md +69 -46
- data/ROADMAP.md +14 -10
- data/TYPES.md +218 -0
- data/lib/prosemirror/content_match.rb +241 -0
- data/lib/prosemirror/html/builder.rb +154 -0
- data/lib/prosemirror/html/element.rb +84 -0
- data/lib/prosemirror/html/entities.rb +87 -0
- data/lib/prosemirror/html/entities.txt +2231 -0
- data/lib/prosemirror/html/selector.rb +95 -0
- data/lib/prosemirror/html/splitter.rb +68 -0
- data/lib/prosemirror/html/text.rb +23 -0
- data/lib/prosemirror/html_parser.rb +503 -0
- data/lib/prosemirror/json_parser.rb +1 -12
- data/lib/prosemirror/mark_type.rb +11 -0
- data/lib/prosemirror/markdown/builder.rb +200 -0
- data/lib/prosemirror/marks/basic/code.rb +16 -0
- data/lib/prosemirror/marks/basic/em.rb +4 -0
- data/lib/prosemirror/marks/basic/link.rb +1 -0
- data/lib/prosemirror/marks/basic/strong.rb +4 -0
- data/lib/prosemirror/marks/bold.rb +8 -0
- data/lib/prosemirror/marks/code.rb +5 -0
- data/lib/prosemirror/marks/highlight.rb +3 -1
- data/lib/prosemirror/marks/italic.rb +4 -0
- data/lib/prosemirror/marks/link.rb +1 -0
- data/lib/prosemirror/marks/strike.rb +5 -0
- data/lib/prosemirror/marks/subscript.rb +2 -0
- data/lib/prosemirror/marks/superscript.rb +2 -0
- data/lib/prosemirror/marks/underline.rb +3 -0
- data/lib/prosemirror/node.rb +12 -0
- data/lib/prosemirror/node_type.rb +22 -0
- data/lib/prosemirror/nodes/basic/code_block.rb +1 -0
- data/lib/prosemirror/nodes/basic/image.rb +1 -0
- data/lib/prosemirror/nodes/basic/ordered_list.rb +4 -1
- data/lib/prosemirror/nodes/blockquote.rb +1 -0
- data/lib/prosemirror/nodes/bullet_list.rb +1 -0
- data/lib/prosemirror/nodes/code_block.rb +6 -1
- data/lib/prosemirror/nodes/hard_break.rb +2 -0
- data/lib/prosemirror/nodes/heading.rb +1 -0
- data/lib/prosemirror/nodes/horizontal_rule.rb +1 -0
- data/lib/prosemirror/nodes/image.rb +1 -0
- data/lib/prosemirror/nodes/list_item.rb +1 -0
- data/lib/prosemirror/nodes/ordered_list.rb +20 -2
- data/lib/prosemirror/nodes/paragraph.rb +1 -0
- data/lib/prosemirror/nodes/table.rb +1 -0
- data/lib/prosemirror/nodes/table_cell.rb +12 -2
- data/lib/prosemirror/nodes/table_header.rb +2 -0
- data/lib/prosemirror/nodes/table_row.rb +1 -0
- data/lib/prosemirror/parse_rule.rb +50 -0
- data/lib/prosemirror/schema.rb +87 -15
- data/lib/prosemirror/type_definition.rb +34 -4
- data/lib/prosemirror/utf8.rb +16 -0
- data/lib/prosemirror/version.rb +1 -1
- data/lib/prosemirror.rb +22 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a11c83675790681267f7d5282882f967154892b408d8965e528b1c34d166876
|
|
4
|
+
data.tar.gz: 14ae8422a61cbe0be055d6ef7590a10981014430978089e14b2805ebf3100be2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02ac22023a65ba9631752cf6c42b9895cfb4be99acc4d2b1922d0a9742e5d61c7d084455b0c6aec2f0c80400531870113b2ceba519f0a7a4af701de7e192c1f0
|
|
7
|
+
data.tar.gz: 8adae01f43c5ea57367db4d77605ceb28b4d606829b905440a716c64dc483c82a105898878349bb79f1fd73df4459651e9172d67546ee1d23968fd8cd3538801
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-10-24
|
|
4
|
+
|
|
5
|
+
- Import Markdown with `Prosemirror.from_markdown` (or `schema.parse_markdown`). Needs the [`commonmarker`](https://github.com/gjtorikian/commonmarker) gem, which isn't installed with this gem. GitHub Flavored Markdown tables, strikethrough and autolinks are supported, and the schema's parse rules apply, so custom types can match Markdown too. Raw HTML goes through HTML import when the herb gem is installed. YAML front matter is dropped.
|
|
6
|
+
- Import plain text with `Prosemirror.from_text` (or `schema.parse_text`): blank lines separate paragraphs, and other line breaks become hard breaks.
|
|
7
|
+
- HTML, Markdown and text import accept nil and binary strings, and replace invalid UTF-8 with U+FFFD, as browsers do. HTML import raised on invalid UTF-8.
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
- Import HTML with `Prosemirror.from_html` (or `schema.parse_html`), as ProseMirror's `DOMParser` does. Needs the [`herb`](https://herb-tools.dev) gem, which isn't installed with this gem.
|
|
12
|
+
- Node and mark types declare how to recognize them in HTML with `parse_html`, like ProseMirror's parse rules, and attributes with `prop …, parse_html:`. The built-in types have Tiptap's rules.
|
|
13
|
+
- Table cells have an `align` attribute (`left`, `right` or `center`), rendered as a `text-align` style.
|
|
14
|
+
- `Schema.tiptap` sorts marks in Tiptap's order: `code` now comes before `italic`, and `strike` before `underline`.
|
|
15
|
+
- Tiptap's `code` mark can't be combined with other marks, as in Tiptap. `Schema.prosemirror` uses a new `Marks::Basic::Code`, which can.
|
|
16
|
+
- `TYPES.md` documents node and mark types: options, attributes, rendering helpers and parse rules.
|
|
17
|
+
- Custom types can pass Array, Hash or Date attribute values to Phlex outside Rails. Phlex 2.4.1 raised a `NameError` without the `date` library.
|
|
18
|
+
|
|
3
19
|
## [0.1.0] - 2026-10-23
|
|
4
20
|
|
|
5
21
|
- Render ProseMirror and Tiptap JSON to HTML (`to_html`) and text (`to_text`). In Rails, `render doc` works in ERB templates and ViewComponents, and `render doc.to_phlex` in Phlex components.
|
data/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# ProseMirror
|
|
2
2
|
|
|
3
|
-
Render [ProseMirror](https://prosemirror.net) and [Tiptap](https://tiptap.dev) documents to HTML and text, in Ruby.
|
|
3
|
+
Render [ProseMirror](https://prosemirror.net) and [Tiptap](https://tiptap.dev) documents to HTML and text, and import HTML, Markdown and text into them, in Ruby.
|
|
4
4
|
|
|
5
5
|
- HTML is built with [Phlex](https://www.phlex.fun) and escaped as it is written.
|
|
6
6
|
- Attributes are typed with [Literal](https://literal.fun).
|
|
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).
|
|
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.
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
@@ -48,6 +48,57 @@ doc.select { |node| node in { type: :heading, attrs: { level: 1 } } }
|
|
|
48
48
|
doc.content.first.text_content
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## HTML import
|
|
52
|
+
|
|
53
|
+
HTML import uses [Herb](https://herb-tools.dev)'s parser, which never raises on malformed HTML. Add it to your Gemfile:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
bundle add herb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
doc = Prosemirror.from_html("<h2>Hello</h2><p>World</p>")
|
|
61
|
+
doc.to_h # => { "type" => "doc", "content" => [{ "type" => "heading", ... }, ...] }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It works like ProseMirror's `DOMParser`, and for the built-in types gives the same JSON as Tiptap's `generateJSON`, apart from invalid attributes:
|
|
65
|
+
|
|
66
|
+
- each node and mark type recognizes its elements with its parse rules: `<b>`, `<strong>` and `font-weight: bold` all give `bold`;
|
|
67
|
+
- whitespace is collapsed, except in code blocks;
|
|
68
|
+
- content that doesn't fit where it is gets wrapped or moved: loose text becomes a paragraph, an `<li>` outside a list gets a list;
|
|
69
|
+
- elements without a rule are unwrapped, and `script`, `style` and similar are ignored;
|
|
70
|
+
- entities are decoded, and ERB tags (`<%= … %>`) are kept as text;
|
|
71
|
+
- invalid attributes are fixed and reported, as in [Invalid documents](#invalid-documents).
|
|
72
|
+
|
|
73
|
+
## Markdown import
|
|
74
|
+
|
|
75
|
+
Markdown import uses [commonmarker](https://github.com/gjtorikian/commonmarker)'s parser, written in Rust. Add it to your Gemfile:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
bundle add commonmarker
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
doc = Prosemirror.from_markdown("## Hello\n\nSome **bold** text.")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It reads GitHub Flavored Markdown: tables, strikethrough and autolinks. Each Markdown node becomes the element commonmarker renders for it, and the schema's parse rules apply as in HTML import, so custom types can match Markdown too: a type that matches `img` also gets Markdown images. For the built-in types, it gives the same JSON as rendering the Markdown to HTML and importing it in Tiptap, except that:
|
|
86
|
+
|
|
87
|
+
- code blocks lose their final newline, as in prosemirror-markdown;
|
|
88
|
+
- task list items (`- [ ] todo`) stay as list items starting with `[ ] ` or `[x] `.
|
|
89
|
+
|
|
90
|
+
Raw HTML in Markdown goes through HTML import when the herb gem is installed, and stays as text otherwise.
|
|
91
|
+
|
|
92
|
+
YAML front matter, between `---` lines at the start, is dropped. Footnotes aren't supported yet: `[^1]: Note` reads as a link definition, as in CommonMark. Markdown nested more than 1,000 levels deep raises `Prosemirror::Error`.
|
|
93
|
+
|
|
94
|
+
## Text import
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
doc = Prosemirror.from_text("First line\nsecond line\n\nNew paragraph")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Blank lines separate paragraphs, and other line breaks become hard breaks. Spaces are kept.
|
|
101
|
+
|
|
51
102
|
## Schemas
|
|
52
103
|
|
|
53
104
|
Two schemas are included:
|
|
@@ -87,53 +138,17 @@ Prosemirror.schema = Prosemirror::Schema.tiptap.with(ImageBlock)
|
|
|
87
138
|
|
|
88
139
|
In `view_template`, `node` is the node being rendered, and `render_content(nodes)` renders any of its descendants.
|
|
89
140
|
|
|
90
|
-
|
|
141
|
+
`parse_html` says which elements become this type when importing HTML, and `prop … parse_html:` how to read an attribute:
|
|
91
142
|
|
|
92
143
|
```ruby
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
See the [cookbook](COOKBOOK.md) for recipes, like skipping empty paragraphs.
|
|
97
|
-
|
|
98
|
-
### Content expressions
|
|
99
|
-
|
|
100
|
-
`content` says which children a node can have. It works like a regular expression, with node names or group names instead of characters:
|
|
101
|
-
|
|
102
|
-
| Expression | Meaning |
|
|
103
|
-
|---|---|
|
|
104
|
-
| `"text*"` | zero or more text nodes |
|
|
105
|
-
| `"inline*"` | zero or more nodes of the `inline` group (text, hard breaks…) |
|
|
106
|
-
| `"block+"` | one or more nodes of the `block` group |
|
|
107
|
-
| `"paragraph block*"` | a paragraph, then zero or more blocks |
|
|
108
|
-
| `"(table_cell \| table_header)*"` | zero or more cells or header cells |
|
|
109
|
-
| `"caption?"` | zero or one caption |
|
|
110
|
-
| `"cell{2}"`, `"cell{1,5}"`, `"cell{2,}"` | exactly two, one to five, two or more cells |
|
|
111
|
-
|
|
112
|
-
A node type joins a group with `group`, for example `group :block`. A node type without `content` has no children, like an image.
|
|
113
|
-
|
|
114
|
-
The built-in types use these groups: `block` for paragraphs, headings, lists, tables…, `inline` for text and hard breaks, and `list` for lists.
|
|
115
|
-
|
|
116
|
-
Today, content expressions are used to find the nodes that contain text, for `to_text`. Rendering doesn't check them. They will also be used by HTML import and validation. See [ProseMirror's guide](https://prosemirror.net/docs/guide/#schema.content_expressions) for details.
|
|
117
|
-
|
|
118
|
-
### Other options
|
|
144
|
+
class ImageBlock < Prosemirror::NodeType
|
|
145
|
+
prop :caption, _String?, parse_html: ->(element) { element.query("figcaption")&.text_content }
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| `content "inline*"` | the allowed children |
|
|
124
|
-
| `inline` | the node is inline, like text (also implied by `group :inline`) |
|
|
125
|
-
| `marks ""` | the marks allowed inside: names or groups, `"_"` for all, `""` for none |
|
|
126
|
-
| `code` | the node contains code |
|
|
127
|
-
| `whitespace :pre` | keep whitespace when importing HTML (implied by `code`) |
|
|
128
|
-
| `leaf_text "\n"` | the text of a node without children, for `to_text`; also takes a block called with the node |
|
|
129
|
-
| `type_name :image_block` | the type name, when it differs from the class name |
|
|
130
|
-
| `aliases :figure` | other type names accepted on input |
|
|
147
|
+
parse_html(tag: "figure.image") { |element| {src: element.query("img")&.[]("src")} }
|
|
148
|
+
end
|
|
149
|
+
```
|
|
131
150
|
|
|
132
|
-
|
|
133
|
-
|---|---|
|
|
134
|
-
| `spanning false` | render one element per node, instead of one for adjacent nodes with the same mark |
|
|
135
|
-
| `excludes "_"` | the marks it can't be combined with |
|
|
136
|
-
| `group`, `code`, `type_name`, `aliases` | as for node types |
|
|
151
|
+
[Node and mark types](TYPES.md) documents every option: groups, content expressions, allowed marks, attribute names in JSON, rendering helpers, and parse rules. See the [cookbook](COOKBOOK.md) for recipes, like skipping empty paragraphs.
|
|
137
152
|
|
|
138
153
|
### Changing built-in types
|
|
139
154
|
|
|
@@ -177,7 +192,15 @@ Prosemirror::Schema.tiptap(
|
|
|
177
192
|
|
|
178
193
|
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.
|
|
179
194
|
|
|
180
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
195
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
196
|
+
|
|
197
|
+
To release a new version, update the version number in `version.rb` and the changelog, and merge them into main. Then tag the version and push the tag:
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
git tag v0.3.0 && git push origin v0.3.0
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The [Push gem](.github/workflows/push_gem.yml) workflow publishes the gem to [rubygems.org](https://rubygems.org) with [trusted publishing](https://guides.rubygems.org/trusted-publishing/), after approval in the `release` environment.
|
|
181
204
|
|
|
182
205
|
## Contributing
|
|
183
206
|
|
data/ROADMAP.md
CHANGED
|
@@ -134,26 +134,30 @@ They differ in more than names:
|
|
|
134
134
|
|
|
135
135
|
## Milestones
|
|
136
136
|
|
|
137
|
-
### 0.1: Rendering
|
|
137
|
+
### 0.1: Rendering (released)
|
|
138
138
|
|
|
139
139
|
- `Node`, `Mark` and `Document` values, built from a Hash or a JSON string, with a nesting limit.
|
|
140
140
|
- Normalization as in ProseMirror: marks sorted by schema order, adjacent text with the same marks joined, empty text removed.
|
|
141
|
-
-
|
|
141
|
+
- Attributes a type doesn't declare kept in `unknown_attrs`: never rendered, and given back by `to_h`.
|
|
142
|
+
- `Schema`, `NodeType` and `MarkType`, with the options of ProseMirror's `NodeSpec` and `MarkSpec`: `content`, `group`, `inline`, `marks`, `whitespace`, `leaf_text`, `excludes`, `spanning`. `marks`, `whitespace` and `excludes` are declared, but only take effect with HTML import (0.2) and validation (0.4), as ProseMirror's `fromJSON` doesn't enforce them either.
|
|
142
143
|
- `Schema.tiptap`: `doc`, `paragraph`, `text`, `heading`, `blockquote`, `bullet_list`, `ordered_list`, `list_item`, `code_block`, `hard_break`, `horizontal_rule`, `image`, `table`, `table_row`, `table_header`, `table_cell`. Marks: `link`, `bold`, `italic`, `underline`, `strike`, `code`, `subscript`, `superscript`, `highlight`.
|
|
143
144
|
- `Schema.prosemirror`: exactly the nodes and marks of `prosemirror-schema-basic` and `prosemirror-schema-list`.
|
|
144
145
|
- `to_html`, with one tag for a mark shared by adjacent text (one `<a>` for a link with mixed formatting).
|
|
146
|
+
- `to_phlex` to render in Phlex components, and `render doc` in ERB templates and ViewComponents.
|
|
147
|
+
- `node` and `render_content` in node types, to render parts of a node's content.
|
|
145
148
|
- `to_text`, like Tiptap's `getText`.
|
|
146
|
-
- `case: :camel`, name aliases, allowed URL schemes, `resolve_url
|
|
147
|
-
- One JSON test fixture per node and mark.
|
|
149
|
+
- `case: :camel`, name aliases, allowed URL schemes, `resolve_url` (a URL it can't resolve is removed and reported).
|
|
150
|
+
- One JSON test fixture per node and mark, for both schemas.
|
|
151
|
+
- A [cookbook](COOKBOOK.md).
|
|
148
152
|
- Security tests, starting with every issue fixed in `prosemirror_to_html`:
|
|
149
153
|
- unescaped attribute values ([CVE-2025-64501](https://github.com/etaminstudio/prosemirror_to_html/security/advisories/GHSA-52c5-vh7f-26fx));
|
|
150
154
|
- attribute names from the document, like `onmouseover`, and `javascript:` URLs ([GHSA-6gj3-5qf2-wwvv](https://github.com/etaminstudio/prosemirror_to_html/security/advisories/GHSA-6gj3-5qf2-wwvv));
|
|
151
155
|
- a heading `level` like `1><script>`;
|
|
152
156
|
- other URL schemes, hidden schemes (`java\tscript:`), `style` injection, deep nesting.
|
|
153
157
|
|
|
154
|
-
### 0.2: HTML import
|
|
158
|
+
### 0.2: HTML import (released)
|
|
155
159
|
|
|
156
|
-
- `Prosemirror.from_html`, built on [Herb](https://herb-tools.dev), which doesn't raise on bad HTML
|
|
160
|
+
- `Prosemirror.from_html`, built on [Herb](https://herb-tools.dev), which doesn't raise on bad HTML.
|
|
157
161
|
- The importer also needs to:
|
|
158
162
|
- decode entities (`&`, `é`), with the [WHATWG entity list](https://html.spec.whatwg.org/entities.json);
|
|
159
163
|
- keep `<% %>` in content as text;
|
|
@@ -165,11 +169,11 @@ They differ in more than names:
|
|
|
165
169
|
- Old HTML mapped to the schema: `<b>` and `<i>` to `bold` and `italic`, `<strike>` to `strike`, `<tt>` to `code`. `<font>` and `<center>` are unwrapped.
|
|
166
170
|
- Tests: HTML from Tiptap gives the same JSON as Tiptap.
|
|
167
171
|
|
|
168
|
-
### 0.3: Markdown and text import
|
|
172
|
+
### 0.3: Markdown and text import (released)
|
|
169
173
|
|
|
170
|
-
- `Prosemirror.from_markdown`, built from [`commonmarker`](https://github.com/gjtorikian/commonmarker)'s syntax tree, without going through HTML
|
|
171
|
-
- GitHub Flavored Markdown: tables, strikethrough, autolinks
|
|
172
|
-
- Custom node types
|
|
174
|
+
- `Prosemirror.from_markdown`, built from [`commonmarker`](https://github.com/gjtorikian/commonmarker)'s syntax tree. commonmarker's parser is written in Rust. Each Markdown node becomes the element commonmarker renders for it, without going through an HTML string, and HTML import's parse rules build the document.
|
|
175
|
+
- GitHub Flavored Markdown: tables, strikethrough, autolinks. Task list items stay as text until task list nodes (0.6).
|
|
176
|
+
- Custom node types match Markdown with their parse rules.
|
|
173
177
|
- Raw HTML in Markdown goes through the HTML importer if Herb is installed.
|
|
174
178
|
- `Prosemirror.from_text`: blank lines make paragraphs, line breaks make hard breaks.
|
|
175
179
|
|
data/TYPES.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Node and mark types
|
|
2
|
+
|
|
3
|
+
Each node and mark of a document has a type, a Ruby class. A type declares its name, its attributes, what it can contain, how it renders to HTML, and how to recognize it when importing HTML. The options follow ProseMirror's [NodeSpec](https://prosemirror.net/docs/ref/#model.NodeSpec) and [MarkSpec](https://prosemirror.net/docs/ref/#model.MarkSpec).
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Callout < Prosemirror::NodeType
|
|
7
|
+
group :block
|
|
8
|
+
content "paragraph+"
|
|
9
|
+
|
|
10
|
+
prop :tone, _String?
|
|
11
|
+
|
|
12
|
+
parse_html(tag: "aside.callout") { |element| {tone: element["data-tone"]} }
|
|
13
|
+
|
|
14
|
+
def view_template(&) = aside(class: "callout", data_tone: @tone, &)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Prosemirror.schema = Prosemirror::Schema.tiptap.with(Callout)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A type is a [Phlex](https://www.phlex.fun) component, and its attributes are [Literal](https://literal.fun) properties. `Schema#with` adds a type to a schema, or replaces the type with the same name.
|
|
21
|
+
|
|
22
|
+
- [Names](#names)
|
|
23
|
+
- [Node type options](#node-type-options)
|
|
24
|
+
- [Mark type options](#mark-type-options)
|
|
25
|
+
- [Content expressions](#content-expressions)
|
|
26
|
+
- [Attributes](#attributes)
|
|
27
|
+
- [Rendering](#rendering)
|
|
28
|
+
- [Parse rules](#parse-rules)
|
|
29
|
+
- [Markdown](#markdown)
|
|
30
|
+
- [Subclassing built-in types](#subclassing-built-in-types)
|
|
31
|
+
|
|
32
|
+
## Names
|
|
33
|
+
|
|
34
|
+
| Option | Meaning |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `type_name :image_block` | The type name. Defaults to the class name in snake_case (`ImageBlock` → `image_block`). |
|
|
37
|
+
| `aliases :figure, :picture` | Other names accepted on input. `Bold` accepts `strong`. |
|
|
38
|
+
|
|
39
|
+
Type names are snake_case Symbols in Ruby. On input, camelCase works too (`imageBlock`). The schema's `case:` option chooses how they are written in JSON.
|
|
40
|
+
|
|
41
|
+
## Node type options
|
|
42
|
+
|
|
43
|
+
| Option | Default | Meaning |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `group :block, :list` | none | The groups of the type, used in content expressions and in `marks`. |
|
|
46
|
+
| `content "inline*"` | none: the node is a leaf | The children allowed, as a [content expression](#content-expressions). |
|
|
47
|
+
| `inline` | false | The node is inline, like text. Also implied by `group :inline`. |
|
|
48
|
+
| `marks "bold italic"` | all, in nodes with inline content | The marks allowed inside: names or groups separated by spaces, `"_"` for all, `""` for none. |
|
|
49
|
+
| `code` | false | The node holds code. Implies `whitespace :pre`. |
|
|
50
|
+
| `whitespace :pre` | `:normal` | Keep spaces and newlines when importing HTML. |
|
|
51
|
+
| `leaf_text "\n"` | `""` | The text of a leaf node in `to_text`. Also takes a block, called with the node. |
|
|
52
|
+
| `linebreak_replacement` | false | When importing HTML that keeps whitespace, newlines become nodes of this inline type, like `hard_break`. |
|
|
53
|
+
|
|
54
|
+
## Mark type options
|
|
55
|
+
|
|
56
|
+
| Option | Default | Meaning |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `group :font` | none | The groups of the type, used in `excludes` and in a node's `marks`. |
|
|
59
|
+
| `excludes "_"` | marks of the same type | The marks this one can't be combined with: names or groups, `"_"` for all. A link excludes other links, and Tiptap's `code` excludes all marks. |
|
|
60
|
+
| `spanning false` | true | Render one element per text node, instead of one for adjacent nodes with the same mark. |
|
|
61
|
+
| `code` | false | The mark holds code, as ProseMirror's `code`. Not used by this gem yet. |
|
|
62
|
+
|
|
63
|
+
The order of marks in the schema sorts the marks of a node, and nests their elements: the first mark is the outermost element.
|
|
64
|
+
|
|
65
|
+
## Content expressions
|
|
66
|
+
|
|
67
|
+
`content` says which children a node can have. It works like a regular expression, with node names or group names instead of characters:
|
|
68
|
+
|
|
69
|
+
| Expression | Meaning |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `"text*"` | Zero or more text nodes. |
|
|
72
|
+
| `"inline*"` | Zero or more nodes of the `inline` group: text, hard breaks… |
|
|
73
|
+
| `"block+"` | One or more nodes of the `block` group. |
|
|
74
|
+
| `"paragraph block*"` | A paragraph, then zero or more blocks. |
|
|
75
|
+
| `"(table_cell \| table_header)*"` | Zero or more cells or header cells. |
|
|
76
|
+
| `"caption?"` | Zero or one caption. |
|
|
77
|
+
| `"cell{2}"`, `"cell{1,5}"`, `"cell{2,}"` | Exactly two, one to five, two or more cells. |
|
|
78
|
+
|
|
79
|
+
The built-in types use these groups: `block` for paragraphs, headings, lists, tables…, `inline` for text and hard breaks, and `list` for lists.
|
|
80
|
+
|
|
81
|
+
HTML import follows content expressions, to wrap content that doesn't fit where it is, or move it up. `to_text` uses them to find the nodes that contain text. Rendering doesn't check them.
|
|
82
|
+
|
|
83
|
+
## Attributes
|
|
84
|
+
|
|
85
|
+
Attributes are declared with Literal's `prop`, as keyword properties, and are available as instance variables in `view_template`:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
prop :level, _Integer(1..6), default: 1, &Prosemirror::Coerce::INTEGER
|
|
89
|
+
prop :src, _String(length: 1..)
|
|
90
|
+
prop :alt, _String?
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **Type:** a Literal type. A value that doesn't match it is invalid (see [Invalid documents](README.md#invalid-documents)).
|
|
94
|
+
- **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.
|
|
95
|
+
- **Coercion:** the block converts values before they are checked. `Prosemirror::Coerce::INTEGER` turns `"2"` into `2`.
|
|
96
|
+
|
|
97
|
+
`prop` takes three more options:
|
|
98
|
+
|
|
99
|
+
| Option | Meaning |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `json: "textAlign"` | The name in JSON, when it differs from the property name. |
|
|
102
|
+
| `aliases: ["order"]` | Other JSON names accepted on input. |
|
|
103
|
+
| `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. |
|
|
104
|
+
|
|
105
|
+
Attributes that a type doesn't declare are kept in `unknown_attrs`. They are never rendered, and `to_h` gives them back.
|
|
106
|
+
|
|
107
|
+
## Rendering
|
|
108
|
+
|
|
109
|
+
A type renders with `view_template`, which yields to render its content. For a mark, the content is the marked text.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
def view_template(&) = blockquote(class: "quote", &)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Without `view_template`, the content is rendered without a wrapping element. These methods are also available:
|
|
116
|
+
|
|
117
|
+
| Method | Returns |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `node` | The node being rendered, a `Prosemirror::Node`. Not available in mark types. |
|
|
120
|
+
| `render_content(nodes)` | Renders any list of nodes, with their marks, for example only some of the node's children. |
|
|
121
|
+
| `safe_url(url)` | The URL after the schema's `resolve_url`, when its scheme is allowed, or nil. Use it for every URL from the document. |
|
|
122
|
+
| `prosemirror_schema` | The schema of the document being rendered. |
|
|
123
|
+
|
|
124
|
+
Phlex escapes text and attribute values, but doesn't check URLs: pass them through `safe_url`.
|
|
125
|
+
|
|
126
|
+
## Parse rules
|
|
127
|
+
|
|
128
|
+
`parse_html` says which elements become this type when importing HTML, like ProseMirror's [parse rules](https://prosemirror.net/docs/ref/#model.ParseRule). A type can have several rules. A type without rules is never created by HTML import.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
parse_html tag: "h1", attrs: {level: 1}
|
|
132
|
+
parse_html(tag: "b") { |element| element.style["font-weight"] != "normal" && {} }
|
|
133
|
+
parse_html(style: "font-weight") { |value| value.match?(/bold|[5-9]00/) && {} }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The block is called with the element, or with the value of the style for a style rule. It returns the attributes, `nil` for the defaults, or `false` when the rule doesn't match. A block with two parameters also gets the schema: `{ |element, schema| … }`.
|
|
137
|
+
|
|
138
|
+
The attributes are built from the rule's `attrs` or block, then from the values each `prop` reads from the element. Values read from the element take precedence, as in Tiptap.
|
|
139
|
+
|
|
140
|
+
| Option | Meaning |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `tag: "img[src]"` | A CSS selector the element must match. |
|
|
143
|
+
| `style: "font-style=italic"` | For mark types: a CSS property, or a property and value. |
|
|
144
|
+
| `attrs: {level: 1}` | Attributes to give the node or mark. |
|
|
145
|
+
| `priority: 60` | Rules with a higher priority are tried first. Defaults to 50. Rules with the same priority are tried in schema order, mark types first. |
|
|
146
|
+
| `consuming: false` | Let the following rules match the element too, for example to apply several marks. |
|
|
147
|
+
| `context: "list_item/"` | The parents the rule needs. See below. |
|
|
148
|
+
| `ignore: true` | Skip the element and its content. |
|
|
149
|
+
| `skip: true` | Skip the element, but parse its content. |
|
|
150
|
+
| `close_parent: true` | Close the current node: the content that follows goes after it. |
|
|
151
|
+
| `content_element: "figcaption"` | A selector, or a block called with the element, giving the descendant that holds the content. |
|
|
152
|
+
| `get_content: ->(element, schema) { … }` | Returns the nodes to use as content, instead of parsing the element's children. |
|
|
153
|
+
| `preserve_whitespace: true` | Keep spaces. Newlines become `hard_break` nodes where they fit, and spaces otherwise. With `:full`, newlines stay in the text. |
|
|
154
|
+
| `clear_mark: true` | For style rules: remove the marks of this type, as `font-weight: normal` does for bold. Also takes a block called with each mark. |
|
|
155
|
+
|
|
156
|
+
### Selectors
|
|
157
|
+
|
|
158
|
+
`tag` takes a list of compound selectors, separated by commas: a type (`p`, `*`), `#id`, `.class`, attribute selectors (`[href]`, `[a=b]`, `[a^=b]`, `[a$=b]`, `[a*=b]`, `[a~=b]`, `[a|=b]`), and `:not(…)`. Combinators like `div > p` aren't supported: use `context`, or check the element in the block.
|
|
159
|
+
|
|
160
|
+
### Context
|
|
161
|
+
|
|
162
|
+
`context` matches the nodes the content is being added to, from the innermost one:
|
|
163
|
+
|
|
164
|
+
| Context | Matches |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `"list_item/"` | Directly in a list item. |
|
|
167
|
+
| `"blockquote//"` | Anywhere inside a blockquote. |
|
|
168
|
+
| `"list_item/paragraph/"` | In a paragraph directly in a list item. |
|
|
169
|
+
| `"blockquote/ \| list_item/"` | One of several contexts. |
|
|
170
|
+
|
|
171
|
+
Group names work too.
|
|
172
|
+
|
|
173
|
+
### Elements
|
|
174
|
+
|
|
175
|
+
The element passed to rules is a `Prosemirror::HTML::Element`, with lowercase names, and attribute values with their character references decoded:
|
|
176
|
+
|
|
177
|
+
| Method | Returns |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `name` | The tag name, like `"img"`. |
|
|
180
|
+
| `element["src"]` | An attribute value, or nil. |
|
|
181
|
+
| `key?("data-id")` | Whether the attribute is present. |
|
|
182
|
+
| `classes` | The classes, as an Array. |
|
|
183
|
+
| `style` | The declarations of the `style` attribute, as a Hash with lowercase properties: `{"font-weight" => "bold"}`. |
|
|
184
|
+
| `text_content` | The text of the element and its descendants. |
|
|
185
|
+
| `children`, `element_children`, `first_element_child` | The children: elements and texts, elements only, the first element. |
|
|
186
|
+
| `query("img")` | The first descendant matching a selector, or nil. |
|
|
187
|
+
| `matches?("p.lead")` | Whether the element matches a selector. |
|
|
188
|
+
| `parent` | The parent element. |
|
|
189
|
+
|
|
190
|
+
## Markdown
|
|
191
|
+
|
|
192
|
+
Markdown import builds the elements commonmarker renders for each Markdown node, and applies the same parse rules. To match Markdown, a type matches these elements:
|
|
193
|
+
|
|
194
|
+
| Markdown | Element |
|
|
195
|
+
|---|---|
|
|
196
|
+
| Paragraph, heading, blockquote, thematic break | `p`, `h1`…`h6`, `blockquote`, `hr` |
|
|
197
|
+
| Bullet list, ordered list, list item | `ul`, `ol` (with `start` when it isn't 1), `li` |
|
|
198
|
+
| Code block | `pre` holding a `code` with a `language-…` class from the info string |
|
|
199
|
+
| Table | `table`, `tr`, `th` for the first row, `td`, with a `text-align` style for aligned columns |
|
|
200
|
+
| Emphasis, strong, strikethrough, inline code | `em`, `strong`, `s`, `code` |
|
|
201
|
+
| Link, image | `a` with `href` and `title`, `img` with `src`, `alt` and `title` |
|
|
202
|
+
| Hard line break | `br` |
|
|
203
|
+
|
|
204
|
+
Raw HTML in Markdown goes through HTML import when the herb gem is installed, and stays as text otherwise.
|
|
205
|
+
|
|
206
|
+
## Subclassing built-in types
|
|
207
|
+
|
|
208
|
+
A subclass keeps the name, options, attributes and parse rules of its parent, and can change any of them. Register it with `Schema#with`, which replaces the built-in type with the same name:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class Paragraph < Prosemirror::Nodes::Paragraph
|
|
212
|
+
def view_template(&) = p(class: "text-lg", &)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
Prosemirror.schema = Prosemirror::Schema.tiptap.with(Paragraph)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A subclass that declares parse rules replaces the rules of its parent. See the [cookbook](COOKBOOK.md) for more examples.
|