django-richtext-link 0.3.0__tar.gz
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.
- django_richtext_link-0.3.0/.gitignore +61 -0
- django_richtext_link-0.3.0/AUTHORS +7 -0
- django_richtext_link-0.3.0/LICENSE +28 -0
- django_richtext_link-0.3.0/PKG-INFO +571 -0
- django_richtext_link-0.3.0/README.md +524 -0
- django_richtext_link-0.3.0/pyproject.toml +118 -0
- django_richtext_link-0.3.0/richtext_link/__init__.py +1 -0
- django_richtext_link-0.3.0/richtext_link/admin.py +133 -0
- django_richtext_link-0.3.0/richtext_link/apps.py +11 -0
- django_richtext_link-0.3.0/richtext_link/checks.py +231 -0
- django_richtext_link-0.3.0/richtext_link/conf.py +74 -0
- django_richtext_link-0.3.0/richtext_link/contrib/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/contrib/djangocms_text/__init__.py +103 -0
- django_richtext_link-0.3.0/richtext_link/fields.py +32 -0
- django_richtext_link-0.3.0/richtext_link/guards.py +70 -0
- django_richtext_link-0.3.0/richtext_link/link_index/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/link_index/admin.py +37 -0
- django_richtext_link-0.3.0/richtext_link/link_index/apps.py +43 -0
- django_richtext_link-0.3.0/richtext_link/link_index/conf.py +10 -0
- django_richtext_link-0.3.0/richtext_link/link_index/index.py +150 -0
- django_richtext_link-0.3.0/richtext_link/link_index/management/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/link_index/management/commands/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/link_index/management/commands/rebuild_richtext_link_index.py +20 -0
- django_richtext_link-0.3.0/richtext_link/link_index/migrations/0001_initial.py +70 -0
- django_richtext_link-0.3.0/richtext_link/link_index/migrations/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/link_index/models.py +61 -0
- django_richtext_link-0.3.0/richtext_link/link_model/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/link_model/admin.py +32 -0
- django_richtext_link-0.3.0/richtext_link/link_model/conf.py +23 -0
- django_richtext_link-0.3.0/richtext_link/link_model/models.py +110 -0
- django_richtext_link-0.3.0/richtext_link/management/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/management/commands/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/management/commands/richtext_link_editor_check.py +172 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/css/link_admin.css +81 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/css/richtext_link.css +241 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/js/link_admin_search.js +172 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/js/richtext-link-ckeditor5.js +434 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/js/richtext-link-core.js +334 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/js/richtext-link-djangocms-text.js +248 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/js/richtext-link-quill.js +253 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/js/richtext-link-tiptap.js +556 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/js/widget-init.js +216 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/vendor/quill/VENDOR.md +16 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/vendor/quill/quill.js +3 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/vendor/quill/quill.js.map +1 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/vendor/quill/quill.min.js +9 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/vendor/quill/quill.snow.css +10 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/vendor/tiptap/VENDOR.md +23 -0
- django_richtext_link-0.3.0/richtext_link/static/richtext_link/vendor/tiptap/tiptap-bundle.js +139 -0
- django_richtext_link-0.3.0/richtext_link/templates/richtext_link/widget.html +4 -0
- django_richtext_link-0.3.0/richtext_link/templatetags/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/templatetags/richtext_link_tags.py +33 -0
- django_richtext_link-0.3.0/richtext_link/tests/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/tests/get_ckeditor5.sh +11 -0
- django_richtext_link-0.3.0/richtext_link/tests/settings.py +160 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/admin.py +27 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/fixtures/test_app.json +35 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/migrations/0001_initial.py +134 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/migrations/0002_testmodel_richtext_ckeditor.py +21 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/migrations/0003_alter_testmodel_richtext_second.py +21 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/migrations/0004_testmodel_richtext_tiptap.py +19 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/migrations/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/models.py +82 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/templates/test_app/testmodel_detail.html +7 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_app/views.py +7 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_checks.py +162 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_djangocms_text.py +108 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_editor.py +365 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_editor_check.py +47 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_guards.py +62 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_link_index.py +173 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_search.py +93 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_templatetag.py +150 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_utils.py +57 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_verify.py +84 -0
- django_richtext_link-0.3.0/richtext_link/tests/test_widget.py +191 -0
- django_richtext_link-0.3.0/richtext_link/tests/urls.py +11 -0
- django_richtext_link-0.3.0/richtext_link/tests/utils/__init__.py +0 -0
- django_richtext_link-0.3.0/richtext_link/tests/utils/selenium_utils.py +101 -0
- django_richtext_link-0.3.0/richtext_link/utils.py +187 -0
- django_richtext_link-0.3.0/richtext_link/widgets.py +101 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.idea/
|
|
2
|
+
.vscode/
|
|
3
|
+
*.egg-info/
|
|
4
|
+
*.pyc
|
|
5
|
+
*.coverage
|
|
6
|
+
*coverage/
|
|
7
|
+
db.sqlite
|
|
8
|
+
dist/
|
|
9
|
+
docs/_build/
|
|
10
|
+
.env
|
|
11
|
+
|
|
12
|
+
# Byte-compiled / optimized / DLL files
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
|
|
16
|
+
# C extensions
|
|
17
|
+
*.so
|
|
18
|
+
|
|
19
|
+
# Distribution / packaging
|
|
20
|
+
.Python
|
|
21
|
+
env/
|
|
22
|
+
bin/
|
|
23
|
+
build/
|
|
24
|
+
develop-eggs/
|
|
25
|
+
dist/
|
|
26
|
+
eggs/
|
|
27
|
+
lib/
|
|
28
|
+
lib64/
|
|
29
|
+
parts/
|
|
30
|
+
sdist/
|
|
31
|
+
var/
|
|
32
|
+
*.egg-info/
|
|
33
|
+
.installed.cfg
|
|
34
|
+
*.egg
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.coverage
|
|
44
|
+
.cache
|
|
45
|
+
nosetests.xml
|
|
46
|
+
coverage.xml
|
|
47
|
+
|
|
48
|
+
# Translations
|
|
49
|
+
*.mo
|
|
50
|
+
|
|
51
|
+
# Django stuff:
|
|
52
|
+
*.log
|
|
53
|
+
*.pot
|
|
54
|
+
|
|
55
|
+
# test project artefacts
|
|
56
|
+
test_app_static/
|
|
57
|
+
|
|
58
|
+
# Sphinx documentation
|
|
59
|
+
docs/_build/
|
|
60
|
+
node_modules/
|
|
61
|
+
richtext_link/tests/test_app/static/test_ckeditor5/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Stähli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
----
|
|
24
|
+
|
|
25
|
+
Quill (https://quilljs.com), bundled unmodified in
|
|
26
|
+
richtext_link/static/richtext_link/vendor/quill/, is licensed under the
|
|
27
|
+
BSD 3-Clause License, Copyright (c) 2017 Slab, Inc., 2014 Jason Chen,
|
|
28
|
+
2013 salesforce.com. See the license notice in the bundled files.
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-richtext-link
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Model based linking for the Quill 2 rich text editor in Django - the link dialog is a plain Django admin form
|
|
5
|
+
Project-URL: Homepage, https://github.com/bnzk/django-richtext-link
|
|
6
|
+
Project-URL: Changelog, https://github.com/bnzk/django-richtext-link/blob/main/CHANGELOG.md
|
|
7
|
+
Author-email: Ben Stähli <bnzk@bnzk.ch>
|
|
8
|
+
License: The MIT License (MIT)
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Ben Stähli
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
|
20
|
+
all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
----
|
|
31
|
+
|
|
32
|
+
Quill (https://quilljs.com), bundled unmodified in
|
|
33
|
+
richtext_link/static/richtext_link/vendor/quill/, is licensed under the
|
|
34
|
+
BSD 3-Clause License, Copyright (c) 2017 Slab, Inc., 2014 Jason Chen,
|
|
35
|
+
2013 salesforce.com. See the license notice in the bundled files.
|
|
36
|
+
License-File: AUTHORS
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Keywords: django,editor,link,prosemirror,quill,richtext,tiptap,wysiwyg
|
|
39
|
+
Requires-Python: >=3.11
|
|
40
|
+
Requires-Dist: django
|
|
41
|
+
Provides-Extra: linkmodel
|
|
42
|
+
Requires-Dist: django-formfieldstash; extra == 'linkmodel'
|
|
43
|
+
Provides-Extra: templatetag
|
|
44
|
+
Requires-Dist: cssselect; extra == 'templatetag'
|
|
45
|
+
Requires-Dist: lxml; extra == 'templatetag'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# django-richtext-link
|
|
49
|
+
|
|
50
|
+
[](https://github.com/bnzk/django-richtext-link/actions/workflows/ci.yml)
|
|
51
|
+
[](https://pypi.python.org/pypi/django-richtext-link/)
|
|
52
|
+
[](https://pypi.python.org/pypi/django-richtext-link/)
|
|
53
|
+
[](https://pypi.python.org/pypi/django-richtext-link/)
|
|
54
|
+
|
|
55
|
+
Model based linking for rich text editors in Django - **TipTap 3** (default),
|
|
56
|
+
**Quill 2**, and bring-your-own **CKEditor 5** - the spiritual successor of
|
|
57
|
+
[django-ckeditor-link](https://github.com/bnzk/django-ckeditor-link).
|
|
58
|
+
|
|
59
|
+
The link dialog is a plain Django admin form: **your link model defines the
|
|
60
|
+
fields**, its validation, its widgets. Nothing is ever saved to the link table -
|
|
61
|
+
the link data lives as a json payload in a single `data-richtext-link`
|
|
62
|
+
attribute on the `<a>` tag (see "Stored format"), and the real `href` is
|
|
63
|
+
resolved at render time. Links to objects (fk to a page, a file, whatever)
|
|
64
|
+
stay correct when the target moves.
|
|
65
|
+
|
|
66
|
+
Included:
|
|
67
|
+
|
|
68
|
+
- a widget (`RichtextWidget`), form field (`RichtextFormField`) and model
|
|
69
|
+
field (`RichtextField`), storing **plain HTML** - TipTap 3.x and Quill 2.x
|
|
70
|
+
are bundled, no further dependencies, the `editor` config key picks one;
|
|
71
|
+
CKEditor 5 is supported bring-your-own (it is GPL/commercially licensed
|
|
72
|
+
and not bundled)
|
|
73
|
+
- TipTap extras: per block **classes** on paragraphs/headings
|
|
74
|
+
(`blockClasses`), `enterMode: "br"` (CKEditor ENTER_BR style)
|
|
75
|
+
- the django link dialog as editor modules/extensions (vanilla JS, usable
|
|
76
|
+
with your own TipTap or Quill build)
|
|
77
|
+
- `DjangoLinkAdmin`, rendering your link model's admin form in an iframe
|
|
78
|
+
dialog, with ajax `verify/` and `search/` endpoints (quick search over all
|
|
79
|
+
fk link targets, based on each target admin's `search_fields`)
|
|
80
|
+
- the `richtext_link_add_links` template filter, resolving stored links at
|
|
81
|
+
render time
|
|
82
|
+
- optional: abstract link model base (`richtext_link.link_model`)
|
|
83
|
+
- optional: a rebuildable link index (`richtext_link.link_index`) - "what
|
|
84
|
+
links here?", link reports, advisory delete protection
|
|
85
|
+
- content compatible with django-ckeditor-link and django-quilljs-link:
|
|
86
|
+
legacy `data-ckeditor-link` / `data-quilljs-link` markers are recognized
|
|
87
|
+
everywhere, and upgraded to `data-richtext-link` on the next editor save
|
|
88
|
+
|
|
89
|
+
## Quickstart
|
|
90
|
+
|
|
91
|
+
Install:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install django-richtext-link
|
|
95
|
+
# lxml + cssselect are needed for the template filter (and the link index):
|
|
96
|
+
pip install django-richtext-link[templatetag]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Settings:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
INSTALLED_APPS = [
|
|
103
|
+
...
|
|
104
|
+
"richtext_link",
|
|
105
|
+
# optional, the link index ("what links here?"):
|
|
106
|
+
"richtext_link.link_index",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
RICHTEXT_LINK_MODEL = "my_app.models.LinkModel"
|
|
110
|
+
|
|
111
|
+
# the link dialog is an iframe:
|
|
112
|
+
X_FRAME_OPTIONS = "SAMEORIGIN"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Define your link model. It is never saved - it provides the dialog form and
|
|
116
|
+
the link resolution. `get_link()` is required, `get_link_target()`,
|
|
117
|
+
`get_link_style()` (css class) and `get_link_attrs()` (extra attributes) are
|
|
118
|
+
optional:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
class LinkModel(models.Model):
|
|
122
|
+
external_url = models.URLField(blank=True, default="")
|
|
123
|
+
page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
|
|
124
|
+
|
|
125
|
+
def get_link(self):
|
|
126
|
+
if self.external_url:
|
|
127
|
+
return self.external_url
|
|
128
|
+
if self.page:
|
|
129
|
+
return self.page.get_absolute_url()
|
|
130
|
+
return ""
|
|
131
|
+
|
|
132
|
+
def get_link_target(self):
|
|
133
|
+
return "_blank" if self.external_url else ""
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or start from the included abstract base
|
|
137
|
+
(`richtext_link.link_model.models.LinkBase`, with free/external/mailto/phone
|
|
138
|
+
fields and a `link_type` dispatcher).
|
|
139
|
+
|
|
140
|
+
Register it with `DjangoLinkAdmin` (this also adds the `verify/` and
|
|
141
|
+
`search/` endpoints, no urls.py include needed):
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from richtext_link.admin import DjangoLinkAdmin
|
|
145
|
+
|
|
146
|
+
@admin.register(LinkModel)
|
|
147
|
+
class LinkModelAdmin(DjangoLinkAdmin):
|
|
148
|
+
pass # a custom form/widgets? just set form = ...
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use the field on your models:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from richtext_link.fields import RichtextField
|
|
155
|
+
|
|
156
|
+
class Article(models.Model):
|
|
157
|
+
body = RichtextField(default="", blank=True)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The editor's dialog urls are derived from `RICHTEXT_LINK_MODEL`'s admin
|
|
161
|
+
automatically. Render the content with resolved links:
|
|
162
|
+
|
|
163
|
+
```django
|
|
164
|
+
{% load richtext_link_tags %}
|
|
165
|
+
{{ article.body|richtext_link_add_links|safe }}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Settings
|
|
169
|
+
|
|
170
|
+
| Setting | Default | Description |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `RICHTEXT_LINK_MODEL` | `None` | Dotted path to your link model class. |
|
|
173
|
+
| `RICHTEXT_LINK_CONFIGS` | see below | Editor configs; `RichtextField(config_name="...")` picks one. |
|
|
174
|
+
| `RICHTEXT_LINK_ATTR_MODIFIERS` | `{}` | `{field: format_string}` - recombine link data at render time, mostly for MultiWidget fields (e.g. `{"cms_page": "{cms_page_2}"}`). |
|
|
175
|
+
| `RICHTEXT_LINK_SANITIZER` | `None` | Dotted path to your html sanitize callable - a startup system check verifies the stored link format survives it (see "Stored format & sanitizers"). |
|
|
176
|
+
| `RICHTEXT_LINK_LEGACY_CHECK` | `True` | Checks also verify the legacy link formats survive (content never resaved since django-ckeditor-link / 0.1 / 0.2). Set `False` once no such content is left. |
|
|
177
|
+
| `RICHTEXT_LINK_SAVE_GUARD` | `True` | Logs an error (`richtext_link` logger) when a save loses link data the stored value had (see "Stored format & sanitizers"). |
|
|
178
|
+
| `RICHTEXT_LINK_INDEX_PROTECT` | `False` | link_index: raise when deleting a link target. |
|
|
179
|
+
|
|
180
|
+
## Editors
|
|
181
|
+
|
|
182
|
+
Each config in `RICHTEXT_LINK_CONFIGS` selects its editor via the `editor`
|
|
183
|
+
key: `"tiptap"` (the default) or `"quilljs"`. The widget only loads the
|
|
184
|
+
assets of the configured editor.
|
|
185
|
+
|
|
186
|
+
### TipTap (default)
|
|
187
|
+
|
|
188
|
+
TipTap configs use django-richtext-link's own schema (TipTap itself is
|
|
189
|
+
headless and not JSON-configurable):
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
RICHTEXT_LINK_CONFIGS = {
|
|
193
|
+
"default": {
|
|
194
|
+
"editor": "tiptap",
|
|
195
|
+
"toolbar": [
|
|
196
|
+
["bold", "italic"],
|
|
197
|
+
["h2", "h3", "p"],
|
|
198
|
+
["bulletList", "orderedList"],
|
|
199
|
+
["djangolink", "clear"],
|
|
200
|
+
],
|
|
201
|
+
"headingLevels": [2, 3],
|
|
202
|
+
"enterMode": "p",
|
|
203
|
+
"blockClasses": {
|
|
204
|
+
"paragraph": [["", "Default"], ["intro", "Intro"]],
|
|
205
|
+
"heading": [["", "Default"], ["fancy", "Fancy"]],
|
|
206
|
+
},
|
|
207
|
+
# iframeUrl/verifyUrl are auto derived from RICHTEXT_LINK_MODEL's
|
|
208
|
+
# admin when not set; omit the key entirely for no link feature:
|
|
209
|
+
"djangolink": {},
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
| key | default | description |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `toolbar` | see above | button groups; items: `bold`, `italic`, `underline`, `strike`, `h1`…`h6`, `p`, `bulletList`, `orderedList`, `blockquote`, `djangolink`, `clear`, `undo`, `redo`, `blockClass` |
|
|
217
|
+
| `headingLevels` | `[2, 3]` | allowed heading levels |
|
|
218
|
+
| `enterMode` | `"p"` | `"p"`: Enter = new paragraph, Shift+Enter = `<br>`; `"br"`: Enter inserts `<br>` (CKEditor ENTER_BR style; lists/code keep their Enter behavior) |
|
|
219
|
+
| `blockClasses` | — | per node type `[value, label]` class choices, shown as a toolbar select; the `class` attribute round-trips through the stored HTML |
|
|
220
|
+
| `djangolink` | — | `{iframeUrl, verifyUrl, fallbackField, linkText, okLabel, cancelLabel, errorMessage}` |
|
|
221
|
+
|
|
222
|
+
`fallbackField` (default `"external"`) is where a plain `<a href>`'s url
|
|
223
|
+
lands when editing pre-django-link content; `linkText` (default `"link"`)
|
|
224
|
+
is inserted when nothing is selected.
|
|
225
|
+
|
|
226
|
+
### Quill
|
|
227
|
+
|
|
228
|
+
Quill configs are passed to `new Quill(el, config)` as is (minus our
|
|
229
|
+
`editor` key) - the `djangolink` module options live in `modules`:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
RICHTEXT_LINK_CONFIGS = {
|
|
233
|
+
"default": { ... }, # tiptap, as above
|
|
234
|
+
"legacy": {
|
|
235
|
+
"editor": "quilljs",
|
|
236
|
+
"theme": "snow",
|
|
237
|
+
"modules": {
|
|
238
|
+
"toolbar": [
|
|
239
|
+
["bold", "italic", "underline"],
|
|
240
|
+
[{"list": "ordered"}, {"list": "bullet"}],
|
|
241
|
+
["djangolink", "clean"],
|
|
242
|
+
],
|
|
243
|
+
"djangolink": {},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Note: Quill has no soft line breaks (`<br>`) and no per block classes -
|
|
250
|
+
that's why TipTap is the default.
|
|
251
|
+
|
|
252
|
+
### CKEditor 5 (bring your own)
|
|
253
|
+
|
|
254
|
+
django-richtext-link is MIT licensed and ships **no CKEditor code**:
|
|
255
|
+
CKEditor 5 is GPL-2.0-or-later (or commercially) licensed, and since v44
|
|
256
|
+
requires a `licenseKey` - `"GPL"` (the adapter's default, shows a small
|
|
257
|
+
"Powered by CKEditor" logo) or your commercial key. Self-host the official
|
|
258
|
+
`ckeditor5` npm package's `dist/browser/` files, or serve them via jsdelivr
|
|
259
|
+
(below); `cdn.ckeditor.com` is part of CKEditor's cloud plans.
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
RICHTEXT_LINK_CONFIGS = {
|
|
263
|
+
"default": { ... }, # tiptap, as above
|
|
264
|
+
"ckeditor": {
|
|
265
|
+
"editor": "ckeditor5",
|
|
266
|
+
# optional: the widget loads these for you (absolute url or static
|
|
267
|
+
# path); omit = load a build exposing window.CKEDITOR yourself
|
|
268
|
+
"assets": {
|
|
269
|
+
"js": "https://cdn.jsdelivr.net/npm/ckeditor5@48.3.1/dist/browser/ckeditor5.umd.js",
|
|
270
|
+
"css": "https://cdn.jsdelivr.net/npm/ckeditor5@48.3.1/dist/browser/ckeditor5.css",
|
|
271
|
+
},
|
|
272
|
+
"licenseKey": "GPL",
|
|
273
|
+
# plugin names resolved against window.CKEDITOR; default:
|
|
274
|
+
# ["Essentials", "Paragraph", "Heading", "Bold", "Italic", "List",
|
|
275
|
+
# "LinkEditing"] - LinkEditing (not "Link") on purpose: no LinkUI
|
|
276
|
+
# balloon interfering with the django link dialog
|
|
277
|
+
# "plugins": [...],
|
|
278
|
+
"enterMode": "p", # or "br": enter inserts <br>
|
|
279
|
+
"djangolink": {}, # urls auto derived, like tiptap
|
|
280
|
+
"ckeditor": { # native CKEditor 5 config, passed through as is
|
|
281
|
+
"toolbar": ["heading", "|", "bold", "italic", "|",
|
|
282
|
+
"bulletedList", "numberedList", "|", "djangolink"],
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Block classes work via CKEditor's native (free) **Style** feature - the
|
|
289
|
+
tiptap `blockClasses` equivalent is plain passthrough config: add `"Style"`
|
|
290
|
+
and `"GeneralHtmlSupport"` to `plugins`, `"style"` to the toolbar, and e.g.
|
|
291
|
+
`"style": {"definitions": [{"name": "Intro", "element": "p", "classes":
|
|
292
|
+
["intro"]}]}` to `ckeditor`. Do not `htmlSupport.allow` (or `disallow`)
|
|
293
|
+
`data-*` attributes on `a` - the adapter owns the marker attribute, and a
|
|
294
|
+
startup system check (`richtext_link.E006`) refuses such configs.
|
|
295
|
+
|
|
296
|
+
Links are edited via the `djangolink` toolbar button or double click.
|
|
297
|
+
|
|
298
|
+
### djangocms-text (TipTap)
|
|
299
|
+
|
|
300
|
+
For [djangocms-text](https://github.com/django-cms/djangocms-text) the bridge
|
|
301
|
+
plugs into its *bundled* TipTap editor (TipTap 3, same as our adapter) via
|
|
302
|
+
djangocms-text's contrib registry - not a `RICHTEXT_LINK_CONFIGS` editor, but
|
|
303
|
+
an `RTEConfig`:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
# settings.py
|
|
307
|
+
TEXT_EDITOR = "richtext_link.contrib.djangocms_text.rte_config"
|
|
308
|
+
RICHTEXT_LINK_MODEL = "myproject.models.Link"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
or instantiate `RichtextLinkRTEConfig(js=..., css=..., djangolink=...)` in
|
|
312
|
+
your project to add own editor assets (see the module docstring).
|
|
313
|
+
|
|
314
|
+
What it does: the toolbar `Link` button and clicks on links open the
|
|
315
|
+
django link dialog instead of djangocms-text's built-in link form, and the
|
|
316
|
+
marker attribute survives djangocms-text's schema (which would otherwise
|
|
317
|
+
strip it on save). Legacy `data-ckeditor-link`/`data-quilljs-link` content
|
|
318
|
+
is read and upgraded on the next save, as everywhere.
|
|
319
|
+
|
|
320
|
+
Rendering stays yours: djangocms-text does not run our resolution, so pipe
|
|
321
|
+
the output through the `richtext_link_add_links` template filter in your
|
|
322
|
+
text plugin templates (the stored fallback `href` covers anything that
|
|
323
|
+
slips through unfiltered). djangocms-text's inline editing mode carries no
|
|
324
|
+
custom editor configuration, so the dialog opens in field editors (plugin
|
|
325
|
+
modal, `HTMLField`) only - data preservation works everywhere.
|
|
326
|
+
|
|
327
|
+
## Stored format & sanitizers
|
|
328
|
+
|
|
329
|
+
A django link is stored as plain HTML with one attribute, the marker, whose
|
|
330
|
+
value is the json encoded link data (your link model's field values, sorted
|
|
331
|
+
keys, empty fields omitted):
|
|
332
|
+
|
|
333
|
+
```html
|
|
334
|
+
<a data-richtext-link='{"link_type":"page","page":"3"}'>read more</a>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The single attribute is the whole contract: whitelisting django links in a
|
|
338
|
+
sanitizer or html schema means allowing exactly `a[href, data-richtext-link]`
|
|
339
|
+
- e.g. nh3/bleach `attributes={"a": {"href", "data-richtext-link"}}`, or
|
|
340
|
+
DOMPurify `ADD_ATTR: ["data-richtext-link"]`.
|
|
341
|
+
|
|
342
|
+
Read forever, upgraded to the current format on the next editor save:
|
|
343
|
+
|
|
344
|
+
- `data-richtext-link="true"` plus one `data-<field>` attribute per field
|
|
345
|
+
(django-richtext-link 0.2)
|
|
346
|
+
- the same attribute format with the `data-quilljs-link` marker
|
|
347
|
+
(django-quilljs-link 0.1) or the `data-ckeditor-link` marker
|
|
348
|
+
(django-ckeditor-link)
|
|
349
|
+
|
|
350
|
+
Two rules keep the format safe:
|
|
351
|
+
|
|
352
|
+
1. **Never assemble the attribute with string formatting** (f-strings in
|
|
353
|
+
templates/migration scripts): a quote in a value breaks out of the
|
|
354
|
+
attribute. Produce content through an editor or lxml, or escape the
|
|
355
|
+
dumped json (`django.utils.html.escape`). From Python,
|
|
356
|
+
`richtext_link.utils.serialize_link_data(data)` gives the canonical
|
|
357
|
+
payload.
|
|
358
|
+
2. **Anything rewriting stored content must keep the marker attribute** - a
|
|
359
|
+
sanitizer allowlist missing the exception silently destroys link data on
|
|
360
|
+
the next save. Point `RICHTEXT_LINK_SANITIZER` at your sanitize callable
|
|
361
|
+
and a Django system check round trips a probe link through it at startup:
|
|
362
|
+
`runserver`/`migrate` refuse to start when the payload does not survive
|
|
363
|
+
(`richtext_link.E001-E004`; editor configs are checked too, `E005/E006`).
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
# settings.py - optional, strongly recommended when you sanitize content
|
|
367
|
+
RICHTEXT_LINK_SANITIZER = "myproject.utils.sanitize_richtext"
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
With `RICHTEXT_LINK_LEGACY_CHECK = True` (the default) the sanitizer probe
|
|
371
|
+
also covers the legacy formats - content saved before the json format and
|
|
372
|
+
never resaved must not lose its link data either. Set it to `False` once no
|
|
373
|
+
such content is left (the strict one-attribute allowlist is then enough).
|
|
374
|
+
|
|
375
|
+
**The editors are sanitizers too** - a strict Quill `formats` whitelist, a
|
|
376
|
+
CKEditor plugin setup or your own editor build can strip links the moment
|
|
377
|
+
content loads. Three guards cover that side:
|
|
378
|
+
|
|
379
|
+
- static config analysis at startup (part of the system checks above, e.g. a
|
|
380
|
+
Quill `formats` whitelist without `"link"`)
|
|
381
|
+
- every widget verifies at editor init that the loaded content's link data
|
|
382
|
+
survives the editor's parse/serialize round trip (legacy formats load
|
|
383
|
+
upgraded, so only real loss counts) - on loss: a loud console error plus a
|
|
384
|
+
visible warning under the editor, before anything can be saved
|
|
385
|
+
- `manage.py richtext_link_editor_check` - the same round trip with probe
|
|
386
|
+
links (current + legacy formats), driven through every configured editor
|
|
387
|
+
in headless firefox, exiting non-zero on loss. Run it in CI or predeploy,
|
|
388
|
+
next to `manage.py check --deploy` (needs selenium + geckodriver;
|
|
389
|
+
bring-your-own CKEditor configs without `assets` are reported as skipped)
|
|
390
|
+
|
|
391
|
+
**The last safety net** runs at save time, server side: a `pre_save`
|
|
392
|
+
comparison logs an error on the `richtext_link` logger whenever a saved
|
|
393
|
+
value lost link data the stored value had - specifically a link whose anchor
|
|
394
|
+
survived but whose payload vanished, the signature of a stripping sanitizer
|
|
395
|
+
or editor (deliberate link removals and dialog edits stay silent). Whatever
|
|
396
|
+
save path the other guards never saw - a foreign admin form, an api, a
|
|
397
|
+
custom sanitizer - the loss surfaces here. Wire the `richtext_link` logger
|
|
398
|
+
to your error reporting; `RICHTEXT_LINK_SAVE_GUARD = False` disables it.
|
|
399
|
+
|
|
400
|
+
What no check can see: content mangled outside of model saves (raw sql,
|
|
401
|
+
`queryset.update()`). The link index below is the tripwire for that -
|
|
402
|
+
`rebuild_richtext_link_index` after sanitizer or editor changes, and watch
|
|
403
|
+
the link count.
|
|
404
|
+
|
|
405
|
+
## Quick search in the link dialog
|
|
406
|
+
|
|
407
|
+
The dialog form gets a search field on top that searches **all foreign key
|
|
408
|
+
link targets at once** - every fk field of your link model whose target
|
|
409
|
+
model has a `ModelAdmin` with `search_fields`. Results are grouped per
|
|
410
|
+
field; picking one fills the fk field (and `link_type`, when your link model
|
|
411
|
+
has a matching one). No configuration needed - it appears as soon as there
|
|
412
|
+
is something searchable, and stays away otherwise.
|
|
413
|
+
|
|
414
|
+
On your `DjangoLinkAdmin` subclass: `link_search = False` disables it,
|
|
415
|
+
`link_search_limit` (default `10`) caps results per target. Permissions of
|
|
416
|
+
the target model admins are respected.
|
|
417
|
+
|
|
418
|
+
## Nicer link_type forms: django-formfieldstash
|
|
419
|
+
|
|
420
|
+
For `link_type` dispatcher style link models (like the contrib `LinkBase`),
|
|
421
|
+
[django-formfieldstash](https://github.com/bnzk/django-formfieldstash) makes
|
|
422
|
+
the dialog show only the field of the selected type. The contrib
|
|
423
|
+
`richtext_link.link_model.admin.LinkBaseAdmin` applies it automatically when
|
|
424
|
+
installed (and is a plain `DjangoLinkAdmin` when not):
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
pip install django-richtext-link[linkmodel]
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
INSTALLED_APPS = [
|
|
432
|
+
...
|
|
433
|
+
"formfieldstash", # so its static files are served
|
|
434
|
+
]
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from richtext_link.link_model.admin import LinkBaseAdmin
|
|
439
|
+
|
|
440
|
+
@admin.register(LinkModel)
|
|
441
|
+
class LinkModelAdmin(LinkBaseAdmin):
|
|
442
|
+
pass
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Using your own editor build
|
|
446
|
+
|
|
447
|
+
The Django side does not care where the editor comes from.
|
|
448
|
+
|
|
449
|
+
**TipTap**: load `richtext_link/js/richtext-link-core.js` and
|
|
450
|
+
`richtext_link/js/richtext-link-tiptap.js`, and expose your own
|
|
451
|
+
`@tiptap/*` imports as `window.RichtextLinkTiptap = {Editor, Extension,
|
|
452
|
+
Mark, Node, mergeAttributes, StarterKit, Link}` (what the vendored bundle
|
|
453
|
+
does, see `frontend/tiptap-entry.js`). Then either let
|
|
454
|
+
`RichtextLink.tiptap.createEditor(el, config, {content, onUpdate})` build
|
|
455
|
+
toolbar and all, or use `RichtextLink.tiptap.buildExtensions(config)` with
|
|
456
|
+
your own `Editor` setup.
|
|
457
|
+
|
|
458
|
+
**CKEditor 5**: anything exposing `window.CKEDITOR` with `ClassicEditor`,
|
|
459
|
+
`Plugin`, `ButtonView` and your plugins works (the UMD build does). Load
|
|
460
|
+
`richtext-link-core.js` + `richtext-link-ckeditor5.js`, then use
|
|
461
|
+
`RichtextLink.ckeditor5.createEditor(el, config, {content, onUpdate})`, or
|
|
462
|
+
`buildConfig`/`buildDjangoLinkPlugin` with your own `ClassicEditor.create`.
|
|
463
|
+
|
|
464
|
+
**Quill**: load `richtext-link-core.js` and `richtext-link-quill.js`, then:
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
import Quill from "quill";
|
|
468
|
+
window.RichtextLink.quill.register(Quill); // link blot + "djangolink" module + icon
|
|
469
|
+
|
|
470
|
+
new Quill(el, {
|
|
471
|
+
theme: "snow",
|
|
472
|
+
modules: {
|
|
473
|
+
toolbar: [["bold"], ["djangolink"]],
|
|
474
|
+
djangolink: {
|
|
475
|
+
iframeUrl: "/admin/my_app/linkmodel/add/",
|
|
476
|
+
verifyUrl: "/admin/my_app/linkmodel/verify/",
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Sync the editor's html into your form field - or use
|
|
483
|
+
`richtext_link/js/widget-init.js` as a reference.
|
|
484
|
+
|
|
485
|
+
## Link index: what links here?
|
|
486
|
+
|
|
487
|
+
With `richtext_link.link_index` in `INSTALLED_APPS` (and a `migrate`), an
|
|
488
|
+
index of all links in `RichtextField` contents is maintained on every save -
|
|
489
|
+
browsable in the admin (read only), queryable in code:
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
from richtext_link.link_index.index import get_inbound_links, register
|
|
493
|
+
|
|
494
|
+
get_inbound_links(some_page) # queryset of LinkIndexEntry targeting some_page
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
- the index is **derived data**: rebuild it anytime with
|
|
498
|
+
`./manage.py rebuild_richtext_link_index` (also needed after `loaddata`,
|
|
499
|
+
fixtures are skipped on purpose, and after `queryset.update()`, which
|
|
500
|
+
bypasses signals)
|
|
501
|
+
- models using `RichtextField` are picked up automatically; for plain
|
|
502
|
+
`TextField` + `RichtextWidget` combinations, call
|
|
503
|
+
`register(MyModel, ["my_field"])` (e.g. in `AppConfig.ready`)
|
|
504
|
+
- `RICHTEXT_LINK_INDEX_PROTECT = True` raises
|
|
505
|
+
`LinkedObjectProtectedError` when a link target is deleted - advisory
|
|
506
|
+
protection, based on the index
|
|
507
|
+
|
|
508
|
+
## Migrating from django-quilljs-link 0.1.0
|
|
509
|
+
|
|
510
|
+
Hard rename, **content needs no migration** - `data-quilljs-link` markers
|
|
511
|
+
are read forever and upgraded on the next editor save. In code:
|
|
512
|
+
|
|
513
|
+
- `pip uninstall django-quilljs-link`, install `django-richtext-link`
|
|
514
|
+
- `INSTALLED_APPS`: `quilljs_link*` → `richtext_link*`
|
|
515
|
+
- settings: `QUILLJS_LINK_*` → `RICHTEXT_LINK_*`; `QUILLJS_LINK_CONFIGS`
|
|
516
|
+
entries keep working as quill configs when you add `"editor": "quilljs"`
|
|
517
|
+
(the new default editor is TipTap)
|
|
518
|
+
- fields/widgets: `QuillField/QuillFormField/QuillWidget` →
|
|
519
|
+
`RichtextField/RichtextFormField/RichtextWidget`
|
|
520
|
+
- template filter: `{% load quilljs_link_tags %}` /
|
|
521
|
+
`|quilljs_link_add_links` → `{% load richtext_link_tags %}` /
|
|
522
|
+
`|richtext_link_add_links`
|
|
523
|
+
- link index: `./manage.py migrate` creates the new table, then run
|
|
524
|
+
`./manage.py rebuild_richtext_link_index` once; the old
|
|
525
|
+
`quilljs_link_index_linkindexentry` table stays behind - drop it manually
|
|
526
|
+
or leave it
|
|
527
|
+
|
|
528
|
+
## Migrating from django-ckeditor-link
|
|
529
|
+
|
|
530
|
+
Content works unchanged: the editors and the template filter recognize
|
|
531
|
+
`data-ckeditor-link` markers, links upgrade to `data-richtext-link` on the
|
|
532
|
+
next editor save. Rename settings (`CKEDITOR_LINK_MODEL` →
|
|
533
|
+
`RICHTEXT_LINK_MODEL` etc.), swap `RichTextField` for `RichtextField`, use
|
|
534
|
+
the `richtext_link_add_links` filter, keep `X_FRAME_OPTIONS = "SAMEORIGIN"`.
|
|
535
|
+
Plain pre-django-link links: the `fallbackField` option prefills a field of
|
|
536
|
+
your choice with the old `href` when such a link is edited.
|
|
537
|
+
|
|
538
|
+
One heads-up that applies to all generations of this package: the stored
|
|
539
|
+
`data-*` attributes are your link model's **field names**. Renaming a link
|
|
540
|
+
model field silently orphans that attribute in existing content - migrate
|
|
541
|
+
your content when renaming.
|
|
542
|
+
|
|
543
|
+
## Development / running the test project
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
git clone https://github.com/bnzk/django-richtext-link.git
|
|
547
|
+
cd django-richtext-link
|
|
548
|
+
pip install -r requirements_dev.txt
|
|
549
|
+
./manage.py migrate
|
|
550
|
+
./manage.py createsuperuser
|
|
551
|
+
./manage.py loaddata test_app
|
|
552
|
+
./manage.py rebuild_richtext_link_index
|
|
553
|
+
./manage.py runserver
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Then check `/admin/test_app/testmodel/2/change/` (TipTap + Quill + CKEditor
|
|
557
|
+
editors, dialog) and `/testmodel/2/` (rendered output). For the CKEditor 5
|
|
558
|
+
editor and its Selenium tests, run `richtext_link/tests/get_ckeditor5.sh`
|
|
559
|
+
once (downloads the GPL licensed build into a gitignored dir - it is not
|
|
560
|
+
part of this package). Tests: `./manage.py test`
|
|
561
|
+
(the Selenium editor tests need Firefox + geckodriver and are skipped
|
|
562
|
+
otherwise; set `RICHTEXT_LINK_SKIP_SELENIUM=1` to skip them explicitly), or
|
|
563
|
+
`tox`.
|
|
564
|
+
|
|
565
|
+
Vendored editors: Quill 2.x unmodified
|
|
566
|
+
(`richtext_link/static/richtext_link/vendor/quill/VENDOR.md`); TipTap 3.x as
|
|
567
|
+
our own esbuild bundle of unmodified packages
|
|
568
|
+
(`.../vendor/tiptap/VENDOR.md`) - TipTap ships no browser build, rebuild
|
|
569
|
+
with `cd frontend && npm ci && npm run build` (maintainers only, hosts
|
|
570
|
+
install nothing). django-richtext-link is MIT licensed, bundled Quill is
|
|
571
|
+
BSD-3-Clause, bundled TipTap is MIT.
|