wagtail-tinymce-table 0.2.1__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.
- wagtail_tinymce_table-0.2.1/MANIFEST.in +3 -0
- wagtail_tinymce_table-0.2.1/PKG-INFO +429 -0
- wagtail_tinymce_table-0.2.1/README.md +411 -0
- wagtail_tinymce_table-0.2.1/__init__.py +3 -0
- wagtail_tinymce_table-0.2.1/apps.py +8 -0
- wagtail_tinymce_table-0.2.1/blocks.py +62 -0
- wagtail_tinymce_table-0.2.1/core/__init__.py +0 -0
- wagtail_tinymce_table-0.2.1/core/table_block.py +220 -0
- wagtail_tinymce_table-0.2.1/pyproject.toml +50 -0
- wagtail_tinymce_table-0.2.1/setup.cfg +4 -0
- wagtail_tinymce_table-0.2.1/static/wagtailtinymce/js/tinymce-adapter.js +99 -0
- wagtail_tinymce_table-0.2.1/tests/test_replace_cell_text.py +128 -0
- wagtail_tinymce_table-0.2.1/tests/test_table_block.py +471 -0
- wagtail_tinymce_table-0.2.1/tests/test_tinymce_block.py +193 -0
- wagtail_tinymce_table-0.2.1/wagtail_tinymce_table.egg-info/PKG-INFO +429 -0
- wagtail_tinymce_table-0.2.1/wagtail_tinymce_table.egg-info/SOURCES.txt +23 -0
- wagtail_tinymce_table-0.2.1/wagtail_tinymce_table.egg-info/dependency_links.txt +1 -0
- wagtail_tinymce_table-0.2.1/wagtail_tinymce_table.egg-info/requires.txt +10 -0
- wagtail_tinymce_table-0.2.1/wagtail_tinymce_table.egg-info/top_level.txt +1 -0
- wagtail_tinymce_table-0.2.1/widgets.py +35 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wagtail-tinymce-table
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: TinyMCE-powered StreamField blocks for Wagtail, including a table block with Excel paste support.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/ogcio/wagtail-tinymce-table
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: wagtail>=4.0
|
|
10
|
+
Requires-Dist: django-tinymce>=5.0
|
|
11
|
+
Requires-Dist: bleach>=6.0
|
|
12
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
13
|
+
Requires-Dist: lxml>=4.9
|
|
14
|
+
Requires-Dist: wagtail-localize>=1.5
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-django; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# wagtail-tinymce
|
|
20
|
+
|
|
21
|
+
A set of [Wagtail](https://wagtail.org/) StreamField blocks that embed a [TinyMCE](https://www.tiny.cloud/) editor directly inside the page editor. The headline feature is `TinyMCETableBlock`: a fully-featured table block that lets editors paste tables straight from Excel or Google Sheets, create header-only or footer-only tables, and merge cells — all without needing Wagtail's built-in table block.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Paste from Excel / Google Sheets** — TinyMCE's clipboard handling converts spreadsheet data to clean HTML tables automatically.
|
|
28
|
+
- **Header rows (`<thead>`) and footer rows (`<tfoot>`)** — a custom *Footer Row* toolbar button toggles the selected row between `<tbody>` and `<tfoot>`. Header rows use TinyMCE's built-in *Row Header* button.
|
|
29
|
+
- **Header-less tables** — nothing forces a header row; plain body tables work fine.
|
|
30
|
+
- **Cell merging and splitting** — `colspan`/`rowspan` via the built-in *Merge Cells* and *Split Cell* buttons.
|
|
31
|
+
- **`wagtail-localize` integration** — `TinyMCETableBlock` implements `get_translatable_segments` and `restore_translated_segments` so table content is fully translatable, including header cells (`<th>`), footer cells, empty cells, and merged cells.
|
|
32
|
+
- **HTML sanitization** — all output is passed through [bleach](https://bleach.readthedocs.io/) with a strict allowlist before being stored.
|
|
33
|
+
- **Customisable** — override `custom_mce_config`, `allowed_tags`, `allowed_attributes`, or pass `menubar_options` / `toolbar_options` per block instance.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
| Package | Minimum version |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Python | 3.9 |
|
|
42
|
+
| Django | 4.2 |
|
|
43
|
+
| Wagtail | 4.0 |
|
|
44
|
+
| django-tinymce | 5.0 |
|
|
45
|
+
| bleach | 6.0 |
|
|
46
|
+
| beautifulsoup4 | 4.12 |
|
|
47
|
+
| lxml | 4.9 |
|
|
48
|
+
|
|
49
|
+
`wagtail-localize` ≥ 1.5 is a **required** dependency and is installed automatically.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
`wagtail-localize` is a **required** dependency — it is always installed automatically. No extra flags are needed.
|
|
56
|
+
|
|
57
|
+
### From PyPI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install wagtail-tinymce
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### From Git (`master` branch — TinyMCE 7, django-tinymce ≥ 5.0)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install git+https://github.com/ogcio/wagtail-tinymce-table.git@master
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
In a `requirements.txt` file (PEP 508):
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
wagtail-tinymce @ git+https://github.com/ogcio/wagtail-tinymce-table.git@master
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Legacy version (TinyMCE 6, django-tinymce ≥ 3.5)
|
|
76
|
+
|
|
77
|
+
If your project requires (TinyMCE 6), install the tag instead:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install git+https://github.com/ogcio/wagtail-tinymce-table.git@v0.1.0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
In a `requirements.txt` file (PEP 508):
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
wagtail-tinymce @ git+https://github.com/ogcio/wagtail-tinymce-table.git@v0.1.0
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
Add `"wagtailtinymce"` and `"tinymce"` to `INSTALLED_APPS`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
INSTALLED_APPS = [
|
|
95
|
+
...
|
|
96
|
+
"tinymce",
|
|
97
|
+
"wagtailtinymce",
|
|
98
|
+
]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Add a minimal TinyMCE configuration to your Django settings (required by `django-tinymce`):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
TINYMCE_DEFAULT_CONFIG = {}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Add `STATIC_URL` if it is not already set:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
STATIC_URL = "/static/"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Run `collectstatic` so the Wagtail telepath adapter script is served:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python manage.py collectstatic
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Quick start
|
|
122
|
+
|
|
123
|
+
### Complete working example
|
|
124
|
+
|
|
125
|
+
The snippets below show every file you need to add or edit in a standard Wagtail project to get a table block working end-to-end.
|
|
126
|
+
|
|
127
|
+
#### 1. `settings.py`
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
INSTALLED_APPS = [
|
|
131
|
+
# --- Wagtail core ---
|
|
132
|
+
"wagtail.contrib.forms",
|
|
133
|
+
"wagtail.contrib.redirects",
|
|
134
|
+
"wagtail.embeds",
|
|
135
|
+
"wagtail.sites",
|
|
136
|
+
"wagtail.users",
|
|
137
|
+
"wagtail.snippets",
|
|
138
|
+
"wagtail.documents",
|
|
139
|
+
"wagtail.images",
|
|
140
|
+
"wagtail.search",
|
|
141
|
+
"wagtail.admin",
|
|
142
|
+
"wagtail",
|
|
143
|
+
# --- Django ---
|
|
144
|
+
"django.contrib.admin",
|
|
145
|
+
"django.contrib.auth",
|
|
146
|
+
"django.contrib.contenttypes",
|
|
147
|
+
"django.contrib.sessions",
|
|
148
|
+
"django.contrib.messages",
|
|
149
|
+
"django.contrib.staticfiles",
|
|
150
|
+
# --- Third-party ---
|
|
151
|
+
"tinymce", # django-tinymce must come before wagtailtinymce
|
|
152
|
+
"wagtailtinymce", # this package
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# Required by django-tinymce (may be an empty dict if you rely on block-level config)
|
|
156
|
+
TINYMCE_DEFAULT_CONFIG = {}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### 2. `myapp/models.py`
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from wagtail.models import Page
|
|
163
|
+
from wagtail.fields import StreamField
|
|
164
|
+
from wagtail.admin.panels import FieldPanel
|
|
165
|
+
|
|
166
|
+
from wagtailtinymce.core.table_block import TinyMCETableBlock
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TableDemoPage(Page):
|
|
170
|
+
"""A page that contains one or more TinyMCE table blocks."""
|
|
171
|
+
|
|
172
|
+
body = StreamField(
|
|
173
|
+
[
|
|
174
|
+
("table", TinyMCETableBlock()),
|
|
175
|
+
],
|
|
176
|
+
blank=True,
|
|
177
|
+
use_json_field=True,
|
|
178
|
+
verbose_name="Page body",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
content_panels = Page.content_panels + [
|
|
182
|
+
FieldPanel("body"),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
class Meta:
|
|
186
|
+
verbose_name = "Table demo page"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### 3. `myapp/templates/myapp/table_demo_page.html`
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
{% extends "base.html" %}
|
|
193
|
+
{% load wagtailcore_tags %}
|
|
194
|
+
|
|
195
|
+
{% block content %}
|
|
196
|
+
<article>
|
|
197
|
+
<h1>{{ page.title }}</h1>
|
|
198
|
+
|
|
199
|
+
{% for block in page.body %}
|
|
200
|
+
{% if block.block_type == "table" %}
|
|
201
|
+
{# The block value is already sanitised HTML — render it directly #}
|
|
202
|
+
<div class="table-wrapper">
|
|
203
|
+
{{ block.value }}
|
|
204
|
+
</div>
|
|
205
|
+
{% endif %}
|
|
206
|
+
{% endfor %}
|
|
207
|
+
</article>
|
|
208
|
+
{% endblock %}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> **Tip:** use `{% include_block page.body %}` instead of the manual loop if you do not need to wrap individual blocks in extra markup.
|
|
212
|
+
|
|
213
|
+
#### 4. Optional: add basic table styles
|
|
214
|
+
|
|
215
|
+
The block stores a plain `<table>` element. Add CSS so it displays nicely:
|
|
216
|
+
|
|
217
|
+
```css
|
|
218
|
+
/* static/css/content.css (load this in your base template) */
|
|
219
|
+
.table-wrapper {
|
|
220
|
+
overflow-x: auto; /* horizontal scroll on small screens */
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.table-wrapper table {
|
|
224
|
+
border-collapse: collapse;
|
|
225
|
+
width: 100%;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.table-wrapper th,
|
|
229
|
+
.table-wrapper td {
|
|
230
|
+
border: 1px solid #d1d5db;
|
|
231
|
+
padding: 0.5rem 0.75rem;
|
|
232
|
+
text-align: left;
|
|
233
|
+
vertical-align: top;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.table-wrapper thead th {
|
|
237
|
+
background-color: #f3f4f6;
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.table-wrapper tfoot td {
|
|
242
|
+
background-color: #f9fafb;
|
|
243
|
+
font-style: italic;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### Table block (minimal)
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from wagtail.models import Page
|
|
253
|
+
from wagtail.fields import StreamField
|
|
254
|
+
from wagtail.admin.panels import FieldPanel
|
|
255
|
+
from wagtailtinymce.core.table_block import TinyMCETableBlock
|
|
256
|
+
|
|
257
|
+
class MyPage(Page):
|
|
258
|
+
body = StreamField(
|
|
259
|
+
[("table", TinyMCETableBlock())],
|
|
260
|
+
blank=True,
|
|
261
|
+
use_json_field=True,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
content_panels = Page.content_panels + [
|
|
265
|
+
FieldPanel("body"),
|
|
266
|
+
]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Render the block in a template as you would any other StreamField block:
|
|
270
|
+
|
|
271
|
+
```html
|
|
272
|
+
{% load wagtailcore_tags %}
|
|
273
|
+
{% include_block page.body %}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Generic TinyMCE block
|
|
277
|
+
|
|
278
|
+
`TinyMCEBlock` is the base class. Use it directly when you need a full rich-text editor without the table-specific toolbar:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from wagtailtinymce.blocks import TinyMCEBlock
|
|
282
|
+
|
|
283
|
+
class MyPage(Page):
|
|
284
|
+
body = StreamField(
|
|
285
|
+
[("rich_text", TinyMCEBlock())],
|
|
286
|
+
blank=True,
|
|
287
|
+
use_json_field=True,
|
|
288
|
+
)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Customisation
|
|
294
|
+
|
|
295
|
+
### Per-instance toolbar / menubar
|
|
296
|
+
|
|
297
|
+
Pass `toolbar_options` or `menubar_options` when declaring the block:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
TinyMCETableBlock(
|
|
301
|
+
toolbar_options="bold italic | table tablemergecells",
|
|
302
|
+
menubar_options="", # hide the menubar
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Subclassing for a project-wide config
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from wagtailtinymce.core.table_block import TinyMCETableBlock
|
|
310
|
+
|
|
311
|
+
class MyTableBlock(TinyMCETableBlock):
|
|
312
|
+
custom_mce_config = {
|
|
313
|
+
**TinyMCETableBlock.custom_mce_config,
|
|
314
|
+
"language": "ga", # Irish
|
|
315
|
+
"content_css": "/static/css/editor.css",
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
allowed_tags = TinyMCETableBlock.allowed_tags + ["figure", "figcaption"]
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Disabling sanitization
|
|
322
|
+
|
|
323
|
+
Set `sanitize_input=False` if you need to allow arbitrary HTML (only do this when the editor is trusted):
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
TinyMCETableBlock(sanitize_input=False)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Toolbar reference
|
|
332
|
+
|
|
333
|
+
The default `TinyMCETableBlock` toolbar groups are:
|
|
334
|
+
|
|
335
|
+
| Group | Buttons |
|
|
336
|
+
|---|---|
|
|
337
|
+
| Formatting | `bold` `italic` `link` `unlink` |
|
|
338
|
+
| Table structure | `table` `tablecaption` `tablecolheader` `tablerowheader` `tablefooterrow`* |
|
|
339
|
+
| Cell operations | `tablecellprops` `tablemergecells` `tablesplitcells` |
|
|
340
|
+
| Row operations | `tableinsertrowbefore` `tableinsertrowafter` `tabledeleterow` |
|
|
341
|
+
| Column operations | `tableinsertcolbefore` `tableinsertcolafter` `tabledeletecol` |
|
|
342
|
+
|
|
343
|
+
\* `tablefooterrow` is a custom button added by this package. Clicking it moves the selected row into `<tfoot>` (or back to `<tbody>` if it is already a footer row).
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## `wagtail-localize` integration
|
|
348
|
+
|
|
349
|
+
When `wagtail-localize` is installed, `TinyMCETableBlock` implements the segment protocol so translators see each non-empty cell as an individual string segment.
|
|
350
|
+
|
|
351
|
+
**Behaviour:**
|
|
352
|
+
- Empty cells are skipped and do not consume a segment index.
|
|
353
|
+
- Duplicate cell values are extracted once and restored to all matching cells.
|
|
354
|
+
- `<th>` (header) cells, `<tbody>` cells, and `<tfoot>` cells are all included.
|
|
355
|
+
- Cells that contain a nested table are skipped entirely.
|
|
356
|
+
- Merged cells (`colspan`/`rowspan`) are treated as a single cell.
|
|
357
|
+
|
|
358
|
+
No configuration is needed. `wagtail-localize` is installed automatically with the package, and Wagtail Localize will pick up the segments automatically.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Project structure
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
wagtailtinymce/
|
|
366
|
+
├── __init__.py # version
|
|
367
|
+
├── apps.py # Django AppConfig
|
|
368
|
+
├── blocks.py # TinyMCEBlock (base class)
|
|
369
|
+
├── widgets.py # WagtailTinyMCE widget + telepath adapter
|
|
370
|
+
├── core/
|
|
371
|
+
│ └── table_block.py # TinyMCETableBlock
|
|
372
|
+
└── static/
|
|
373
|
+
└── wagtailtinymce/js/
|
|
374
|
+
└── tinymce-adapter.js # Wagtail telepath registration
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Running the tests
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
pip install "wagtail-tinymce[dev]" # adds pytest + pytest-django
|
|
383
|
+
pytest
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The test suite has **75 tests** covering:
|
|
387
|
+
|
|
388
|
+
- `_replace_cell_text` helper (simple and compound cell paths)
|
|
389
|
+
- `TinyMCETableBlock` configuration (allowed tags, toolbar, TinyMCE `setup` callback)
|
|
390
|
+
- `get_translatable_segments` (empty cells, duplicates, `<th>`, `<tfoot>`, merged cells, round-trip)
|
|
391
|
+
- `restore_translated_segments` (index correctness, compound cells, `<tfoot>`, duplicates)
|
|
392
|
+
- `TinyMCEBlock.sanitize` (XSS, allowed tags/attributes, inline formatting)
|
|
393
|
+
- `TinyMCEBlock.value_from_form` (`SafeData`, bypass mode)
|
|
394
|
+
- `WagtailTinyMCE` widget (config merging and overrides)
|
|
395
|
+
- `WagtailTinyMCEAdapter` (telepath registration)
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Changelog
|
|
400
|
+
|
|
401
|
+
### 0.2.1
|
|
402
|
+
|
|
403
|
+
#### Bug fixes
|
|
404
|
+
- **Header rows now produce `<th>` elements.** The default TinyMCE `table_header_type` value `"section"` moved rows into `<thead>` but kept `<td>` cells. Setting it to `"sectionCells"` correctly converts cells to `<th>` as well.
|
|
405
|
+
- **Table `<caption>` text is now included in translatable segments.** The caption is extracted before row cells and restored in the same position, so it appears correctly in the Wagtail Localize translation UI.
|
|
406
|
+
|
|
407
|
+
### 0.2.0
|
|
408
|
+
|
|
409
|
+
#### Breaking changes
|
|
410
|
+
- **Requires `django-tinymce >= 5.0`** (previously 3.5). This bundles **TinyMCE 7.8** instead of TinyMCE 6.x. If you cannot upgrade `django-tinymce`, pin to the `v0.1.0` git tag.
|
|
411
|
+
- **`wagtail-localize >= 1.5` is now a required dependency** (previously optional).
|
|
412
|
+
|
|
413
|
+
#### Packaging
|
|
414
|
+
- Fixed build backend from `setuptools.backends.legacy:build` to `setuptools.build_meta` for compatibility with older setuptools versions.
|
|
415
|
+
- Fixed `package-dir` mapping so `pip install git+…` correctly installs the `wagtailtinymce` module (previously the package was silently installed empty).
|
|
416
|
+
|
|
417
|
+
### 0.1.0
|
|
418
|
+
|
|
419
|
+
- Initial release.
|
|
420
|
+
- `TinyMCEBlock` and `TinyMCETableBlock` StreamField blocks.
|
|
421
|
+
- Excel / Google Sheets paste support via TinyMCE's clipboard plugin.
|
|
422
|
+
- Custom *Footer Row* toolbar button for `<tfoot>` support.
|
|
423
|
+
- `wagtail-localize` segment extraction and restoration, with fixes for empty cells, merged cells with rich content, and `<th>` cells.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Licence
|
|
428
|
+
|
|
429
|
+
MIT
|