draftail-text-utils 0.1.7__py3-none-any.whl

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.
Files changed (39) hide show
  1. draftail_text_utils/__init__.py +4 -0
  2. draftail_text_utils/apps.py +7 -0
  3. draftail_text_utils/conf.py +482 -0
  4. draftail_text_utils/migrations/__init__.py +0 -0
  5. draftail_text_utils/rich_text/__init__.py +0 -0
  6. draftail_text_utils/rich_text/base.py +267 -0
  7. draftail_text_utils/rich_text/font_family.py +61 -0
  8. draftail_text_utils/rich_text/font_size.py +94 -0
  9. draftail_text_utils/rich_text/highlight_color.py +99 -0
  10. draftail_text_utils/rich_text/text_alignment.py +70 -0
  11. draftail_text_utils/rich_text/text_color.py +95 -0
  12. draftail_text_utils/static/draftail_text_utils/css/draftail_text_utils.css +5 -0
  13. draftail_text_utils/static/draftail_text_utils/css/font_family.css +57 -0
  14. draftail_text_utils/static/draftail_text_utils/css/font_size.css +102 -0
  15. draftail_text_utils/static/draftail_text_utils/css/highlight_color.css +117 -0
  16. draftail_text_utils/static/draftail_text_utils/css/text_alignment.css +72 -0
  17. draftail_text_utils/static/draftail_text_utils/css/text_color.css +117 -0
  18. draftail_text_utils/static/draftail_text_utils/js/font_family.js +149 -0
  19. draftail_text_utils/static/draftail_text_utils/js/font_size.js +408 -0
  20. draftail_text_utils/static/draftail_text_utils/js/font_size_entity.js +39 -0
  21. draftail_text_utils/static/draftail_text_utils/js/highlight_color.js +350 -0
  22. draftail_text_utils/static/draftail_text_utils/js/highlight_color_entity.js +43 -0
  23. draftail_text_utils/static/draftail_text_utils/js/text_alignment.js +139 -0
  24. draftail_text_utils/static/draftail_text_utils/js/text_color.js +343 -0
  25. draftail_text_utils/static/draftail_text_utils/js/text_color_entity.js +42 -0
  26. draftail_text_utils/templates/draftail_text_utils/icons/align-center.svg +1 -0
  27. draftail_text_utils/templates/draftail_text_utils/icons/align-justify.svg +1 -0
  28. draftail_text_utils/templates/draftail_text_utils/icons/align-left.svg +1 -0
  29. draftail_text_utils/templates/draftail_text_utils/icons/align-right.svg +1 -0
  30. draftail_text_utils/templates/draftail_text_utils/icons/font.svg +1 -0
  31. draftail_text_utils/templates/draftail_text_utils/icons/highlighter.svg +1 -0
  32. draftail_text_utils/templates/draftail_text_utils/icons/palette.svg +1 -0
  33. draftail_text_utils/templates/draftail_text_utils/icons/text-height.svg +1 -0
  34. draftail_text_utils/templatetags/__init__.py +0 -0
  35. draftail_text_utils/templatetags/draftail_text_utils_tags.py +29 -0
  36. draftail_text_utils/wagtail_hooks.py +266 -0
  37. draftail_text_utils-0.1.7.dist-info/METADATA +63 -0
  38. draftail_text_utils-0.1.7.dist-info/RECORD +39 -0
  39. draftail_text_utils-0.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,343 @@
1
+ // https://github.com/wagtail/draftail/blob/main/src/components/Toolbar/ToolbarButton.tsx
2
+ // export interface ToolbarButtonProps {
3
+ // name?: string;
4
+ // active?: boolean;
5
+ // label?: string | null;
6
+ // title?: string | null;
7
+ // icon?: IconProp | null;
8
+ // className?: string | null;
9
+ // tooltipDirection?: "up" | "down";
10
+ // onClick?: ((name: string) => void) | null;
11
+ // }
12
+
13
+ // CSS Color Component
14
+ // https://github.com/argyleink/css-color-component
15
+
16
+ (function () {
17
+ var React = window.React;
18
+ var RichUtils = window.DraftJS.RichUtils;
19
+ var Modifier = window.DraftJS.Modifier;
20
+ var EditorState = window.DraftJS.EditorState;
21
+ var ToolbarButton = window.Draftail.ToolbarButton;
22
+ var data = window.draftailTextUtils || {};
23
+
24
+ // Build options from config
25
+ var options = data.customTextColors || [];
26
+ var optMap = data.customTextColorStyleMap || {};
27
+
28
+ var savedSelection = null;
29
+
30
+ const entityType = 'TEXT_COLOR';
31
+ const controlType = 'text-color';
32
+ const control = JSON.parse(
33
+ document.getElementById(`draftail-plugin-control-${controlType}`)
34
+ .textContent,
35
+ );
36
+
37
+ function removeTextColorEntity(editorState) {
38
+ var selection = editorState.getSelection();
39
+ if (!selection.getHasFocus()) return editorState;
40
+
41
+ var contentWithoutEntity = Modifier.applyEntity(
42
+ editorState.getCurrentContent(),
43
+ selection,
44
+ null, // null removes entity
45
+ );
46
+
47
+ return EditorState.push(editorState, contentWithoutEntity, 'apply-entity');
48
+ }
49
+
50
+ function saveSelection(getEditorState) {
51
+ const selection = getEditorState().getSelection();
52
+ if (selection && !selection.isCollapsed()) {
53
+ savedSelection = selection;
54
+ }
55
+ }
56
+
57
+ function restoreSelection(editorState) {
58
+ if (savedSelection && !savedSelection.isCollapsed()) {
59
+ return EditorState.forceSelection(editorState, savedSelection);
60
+ }
61
+ return editorState;
62
+ }
63
+
64
+ // Helper to find active color
65
+ function getActiveColor(editorState) {
66
+ var selection = editorState.getSelection();
67
+ if (!selection.getHasFocus()) return null;
68
+
69
+ var contentState = editorState.getCurrentContent();
70
+ var startKey = selection.getStartKey();
71
+ var startOffset = selection.getStartOffset();
72
+ var blockWithEntityAt = contentState.getBlockForKey(startKey);
73
+
74
+ // Check entity at cursor position
75
+ var entityKey = blockWithEntityAt.getEntityAt(startOffset);
76
+ if (entityKey) {
77
+ var entity = contentState.getEntity(entityKey);
78
+ if (entity.getType() === entityType) {
79
+ return entity.getData().color;
80
+ }
81
+ }
82
+
83
+ // If selection is collapsed, also check character before (so styling after typing works)
84
+ if (selection.isCollapsed() && startOffset > 0) {
85
+ entityKey = blockWithEntityAt.getEntityAt(startOffset - 1);
86
+ if (entityKey) {
87
+ var entity = contentState.getEntity(entityKey);
88
+ if (entity.getType() === entityType) {
89
+ return entity.getData().color;
90
+ }
91
+ }
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ // The control component
98
+ var TextColorControl = class TextColorControl extends React.Component {
99
+ constructor(props) {
100
+ super(props); // getEditorState, onChange
101
+ this.state = {
102
+ isDropdownOpen: false,
103
+ inputValue: '#000000',
104
+ isInputFocused: false,
105
+ };
106
+ this.controlRef = React.createRef();
107
+ }
108
+
109
+ // lifecycle
110
+ componentDidMount() {
111
+ this.syncInputValue();
112
+ document.addEventListener('mousedown', this.handleClickOutside);
113
+
114
+ // Listen to CSS Color Component events
115
+ var inputEl = document.querySelector('#Draftail--text-color-input');
116
+ if (inputEl) {
117
+ inputEl.addEventListener('open', this.handleInputFocus);
118
+ inputEl.addEventListener('close', this.handleInputBlur);
119
+ inputEl.addEventListener('change', this.handleInputChange);
120
+ }
121
+ }
122
+
123
+ componentWillUnmount() {
124
+ document.removeEventListener('mousedown', this.handleClickOutside);
125
+
126
+ // Unsubscribe from CSS Color Component events
127
+ var inputEl = document.querySelector('#Draftail--text-color-input');
128
+ if (inputEl) {
129
+ inputEl.removeEventListener('open', this.handleInputFocus);
130
+ inputEl.removeEventListener('close', this.handleInputBlur);
131
+ inputEl.removeEventListener('change', this.handleInputChange);
132
+ }
133
+ }
134
+
135
+ componentDidUpdate() {
136
+ this.syncInputValue(); // always runs, but guarded internally
137
+ }
138
+
139
+ // helpers
140
+ getActiveColor() {
141
+ return getActiveColor(this.props.getEditorState());
142
+ }
143
+
144
+ syncInputValue() {
145
+ // While the user is editing, never overwrite their input
146
+ if (this.state.isInputFocused || this.state.isDropdownOpen) {
147
+ return;
148
+ }
149
+
150
+ var activeColor = this.getActiveColor();
151
+ var newValue = activeColor || '';
152
+
153
+ if (this.state.inputValue !== newValue) {
154
+ this.setState({ inputValue: newValue });
155
+ }
156
+ }
157
+
158
+ toggleDropdown(force) {
159
+ const willBeOpen =
160
+ typeof force === 'boolean' ? force : !this.state.isDropdownOpen;
161
+ if (willBeOpen && !this.state.isDropdownOpen) {
162
+ saveSelection(this.props.getEditorState);
163
+ }
164
+ this.setState({ isDropdownOpen: willBeOpen });
165
+ }
166
+
167
+ applyTextColor(newColor) {
168
+ var color = newColor === 'transparent' ? '#00000000' : newColor;
169
+ var editorState = restoreSelection(this.props.getEditorState());
170
+ var currentColor = this.getActiveColor(editorState);
171
+ editorState = removeTextColorEntity(editorState);
172
+
173
+ if (color === currentColor) {
174
+ // Re-sync the input value
175
+ this.props.onChange(editorState);
176
+ this.setState({ isDropdownOpen: false, inputValue: '#000000' });
177
+ this.syncInputValue();
178
+ return;
179
+ }
180
+
181
+ var selection = editorState.getSelection();
182
+
183
+ // Apply the entity to the selection
184
+ var contentWithEntity = editorState
185
+ .getCurrentContent()
186
+ .createEntity(entityType, 'MUTABLE', {
187
+ color,
188
+ });
189
+ var contentStateWithEntity = Modifier.applyEntity(
190
+ contentWithEntity,
191
+ selection,
192
+ contentWithEntity.getLastCreatedEntityKey(),
193
+ );
194
+ var newContent = EditorState.push(
195
+ editorState,
196
+ contentStateWithEntity,
197
+ 'apply-entity',
198
+ );
199
+ this.props.onChange(newContent);
200
+
201
+ // Re-sync the input value
202
+ this.setState({ isDropdownOpen: false, inputValue: String(color) });
203
+ this.syncInputValue();
204
+ }
205
+
206
+ handleInputFocus = () => {
207
+ saveSelection(this.props.getEditorState);
208
+ this.setState({ isInputFocused: true });
209
+ if (!this.state.isDropdownOpen) {
210
+ this.setState({ isDropdownOpen: true });
211
+ }
212
+ };
213
+
214
+ handleInputBlur = () => {
215
+ // When leaving the field, clear the focus flag and re‑sync (will clear if no style)
216
+ this.setState({ isInputFocused: false }, () => {
217
+ this.syncInputValue();
218
+ });
219
+ };
220
+
221
+ handleInputChange = (e) => {
222
+ this.applyTextColor(e.detail.value);
223
+ };
224
+
225
+ handleInputKeyDown = (e) => {
226
+ if (e.key === 'Escape') {
227
+ this.toggleDropdown(false);
228
+ this.syncInputValue();
229
+ return;
230
+ }
231
+
232
+ if (e.key === 'Enter') {
233
+ e.preventDefault();
234
+ this.applyTextColor(this.state.inputValue);
235
+ return;
236
+ }
237
+ };
238
+
239
+ handleClickOutside = (event) => {
240
+ if (
241
+ this.controlRef.current &&
242
+ !this.controlRef.current.contains(event.target)
243
+ ) {
244
+ this.setState({ isDropdownOpen: false, isInputFocused: false });
245
+ var inputEl = document.querySelector('#Draftail--text-color-input');
246
+ if (inputEl) {
247
+ inputEl.close();
248
+ }
249
+ }
250
+ };
251
+
252
+ createOptElement = (opt) => {
253
+ return React.createElement(
254
+ 'li',
255
+ {
256
+ key: `TEXT_COLOR_OPT_${opt.key.toUpperCase()}`,
257
+ className: 'Draftail--text-color-option-wrapper',
258
+ },
259
+ React.createElement(ToolbarButton, {
260
+ name: opt.value === 'transparent' ? '#00000000' : opt.value,
261
+ active: this.getActiveColor() === opt.value,
262
+ title: opt.label,
263
+ label: '\u2713',
264
+ tooltipDirection: 'up',
265
+ className: 'Draftail--text-color-option',
266
+ onClick: () => this.applyTextColor(opt.value),
267
+ }),
268
+ );
269
+ };
270
+
271
+ // render
272
+ render() {
273
+ const icon = `#icon-${control.icon}`;
274
+
275
+ var activeColor = this.getActiveColor();
276
+ var inputDisplay = this.state.inputValue;
277
+ var isDropdownExpanded = this.state.isDropdownOpen;
278
+
279
+ var input = React.createElement(
280
+ 'li',
281
+ {
282
+ 'key': 'TEXT_COLOR_CTRL_INPUT_WRAPPER',
283
+ 'className': 'Draftail--text-color-input-wrapper',
284
+ 'data-draftail-balloon': 'down',
285
+ 'aria-label': 'Select custom color',
286
+ },
287
+ React.createElement('color-input', {
288
+ key: 'TEXT_COLOR_CTRL_INPUT',
289
+ id: 'Draftail--text-color-input',
290
+ className: 'Draftail--text-color-input',
291
+ defaultValue: activeColor || '',
292
+ value: inputDisplay,
293
+ onKeyDown: this.handleInputKeyDown,
294
+ }),
295
+ );
296
+
297
+ var hr = React.createElement('hr', {
298
+ key: 'TEXT_COLOR_CTRL_HR',
299
+ className: 'Draftail--text-color-hr',
300
+ });
301
+
302
+ var dropdown = React.createElement(
303
+ 'ul',
304
+ {
305
+ 'id': 'Draftail--text-color-dropdown',
306
+ 'className': 'Draftail--text-color-dropdown',
307
+ 'aria-expanded': isDropdownExpanded,
308
+ },
309
+ options.map(this.createOptElement),
310
+ hr,
311
+ input,
312
+ );
313
+
314
+ // ── Toolbar button ──
315
+ var button = React.createElement(ToolbarButton, {
316
+ name: control.type.toUpperCase(),
317
+ active: !!activeColor,
318
+ title: control.label,
319
+ icon: icon,
320
+ tooltipDirection: 'up',
321
+ onClick: () => this.toggleDropdown(),
322
+ });
323
+
324
+ return React.createElement(
325
+ 'div',
326
+ {
327
+ key: 'TEXT_COLOR_CTRL',
328
+ className: 'Draftail--text-color-control',
329
+ ref: this.controlRef,
330
+ },
331
+ dropdown,
332
+ button,
333
+ );
334
+ }
335
+ };
336
+ window.draftail.registerPlugin(
337
+ {
338
+ type: control.type,
339
+ inline: TextColorControl,
340
+ },
341
+ 'controls',
342
+ );
343
+ })();
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Text color entity for Draftail.
3
+ * Registers an entity type (TEXT_COLOR) that stores an arbitrary text color.
4
+ * The entity is applied by `text-color` control when a custom text color is entered.
5
+ * See:`src/draftail_text_utils/static/draftail_text_utils/js/text_color.js`
6
+ */
7
+ (function () {
8
+ const React = window.React;
9
+ const ENTITY_TYPE = 'TEXT_COLOR';
10
+ const STYLE_KEY = 'style';
11
+ const DATASET_KEY = 'data-entity-type';
12
+
13
+ const TextColorDecorator = ({ children, contentState, entityKey }) => {
14
+ const entity = contentState.getEntity(entityKey);
15
+ const { color: entityColor } = entity.getData();
16
+ if (!entityColor) return children;
17
+
18
+ const color = entityColor == 'transparent' ? '#00000000' : entityColor;
19
+ return React.createElement(
20
+ 'span',
21
+ {
22
+ [STYLE_KEY]: { color },
23
+ [DATASET_KEY]: ENTITY_TYPE,
24
+ },
25
+ children,
26
+ );
27
+ };
28
+
29
+ const TextColorSource = ({ onClose }) => {
30
+ onClose();
31
+ return null;
32
+ };
33
+
34
+ window.draftail.registerPlugin(
35
+ {
36
+ type: ENTITY_TYPE,
37
+ source: TextColorSource,
38
+ decorator: TextColorDecorator,
39
+ },
40
+ 'entityTypes',
41
+ );
42
+ })();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" id="icon-align-center"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M352 64c0-17.7-14.3-32-32-32L128 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l192 0c17.7 0 32-14.3 32-32zm96 128c0-17.7-14.3-32-32-32L32 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l384 0c17.7 0 32-14.3 32-32zM0 448c0 17.7 14.3 32 32 32l384 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 416c-17.7 0-32 14.3-32 32zM352 320c0-17.7-14.3-32-32-32l-192 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l192 0c17.7 0 32-14.3 32-32z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" id="icon-align-justify"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M448 64c0-17.7-14.3-32-32-32L32 32C14.3 32 0 46.3 0 64S14.3 96 32 96l384 0c17.7 0 32-14.3 32-32zm0 256c0-17.7-14.3-32-32-32L32 288c-17.7 0-32 14.3-32 32s14.3 32 32 32l384 0c17.7 0 32-14.3 32-32zM0 192c0 17.7 14.3 32 32 32l384 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 160c-17.7 0-32 14.3-32 32zM448 448c0-17.7-14.3-32-32-32L32 416c-17.7 0-32 14.3-32 32s14.3 32 32 32l384 0c17.7 0 32-14.3 32-32z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" id="icon-align-left"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M288 64c0 17.7-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l224 0c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32L32 352c-17.7 0-32-14.3-32-32s14.3-32 32-32l224 0c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 224c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32L32 480c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" id="icon-align-right"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M448 64c0 17.7-14.3 32-32 32L192 96c-17.7 0-32-14.3-32-32s14.3-32 32-32l224 0c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32l-224 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l224 0c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 224c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32L32 480c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" id="icon-font"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M254 52.8C249.3 40.3 237.3 32 224 32s-25.3 8.3-30 20.8L57.8 416 32 416c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-1.8 0 18-48 159.6 0 18 48-1.8 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-25.8 0L254 52.8zM279.8 304l-111.6 0L224 155.1 279.8 304z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" id="icon-highlighter"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M315 315l158.4-215L444.1 70.6 229 229 315 315zm-187 5s0 0 0 0l0-71.7c0-15.3 7.2-29.6 19.5-38.6L420.6 8.4C428 2.9 437 0 446.2 0c11.4 0 22.4 4.5 30.5 12.6l54.8 54.8c8.1 8.1 12.6 19 12.6 30.5c0 9.2-2.9 18.2-8.4 25.6L334.4 396.5c-9 12.3-23.4 19.5-38.6 19.5L224 416l-25.4 25.4c-12.5 12.5-32.8 12.5-45.3 0l-50.7-50.7c-12.5-12.5-12.5-32.8 0-45.3L128 320zM7 466.3l63-63 70.6 70.6-31 31c-4.5 4.5-10.6 7-17 7L24 512c-13.3 0-24-10.7-24-24l0-4.7c0-6.4 2.5-12.5 7-17z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-palette"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M512 256c0 .9 0 1.8 0 2.7c-.4 36.5-33.6 61.3-70.1 61.3L344 320c-26.5 0-48 21.5-48 48c0 3.4 .4 6.7 1 9.9c2.1 10.2 6.5 20 10.8 29.9c6.1 13.8 12.1 27.5 12.1 42c0 31.8-21.6 60.7-53.4 62c-3.5 .1-7 .2-10.6 .2C114.6 512 0 397.4 0 256S114.6 0 256 0S512 114.6 512 256zM128 288a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm0-96a32 32 0 1 0 0-64 32 32 0 1 0 0 64zM288 96a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm96 96a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" id="icon-text-height"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M64 128l0-32 64 0 0 320-32 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l128 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-32 0 0-320 64 0 0 32c0 17.7 14.3 32 32 32s32-14.3 32-32l0-48c0-26.5-21.5-48-48-48L160 32 48 32C21.5 32 0 53.5 0 80l0 48c0 17.7 14.3 32 32 32s32-14.3 32-32zM502.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 192-32 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0 0-192 32 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64z"/></svg>
File without changes
@@ -0,0 +1,29 @@
1
+ from urllib.parse import urljoin
2
+
3
+ from django import template
4
+ from django.utils.html import mark_safe
5
+
6
+ from draftail_text_utils.conf import load_font_urls
7
+ from draftail_text_utils.wagtail_hooks import _feature_static
8
+
9
+
10
+ register = template.Library()
11
+
12
+
13
+ @register.simple_tag
14
+ def draftail_text_assets():
15
+ links = set()
16
+ preconnect_links = set()
17
+
18
+ # Load font stylesheets
19
+ for url in load_font_urls():
20
+ if url:
21
+ preconnect_links.add(f'<link rel="preconnect" href="{urljoin(url, "/")}">')
22
+ links.add(f'<link rel="stylesheet" href="{url}">')
23
+
24
+ # Load draftail stylesheets
25
+ links.add(
26
+ f'<link rel="stylesheet" href="{_feature_static("css/draftail_text_utils.css")}">'
27
+ )
28
+
29
+ return mark_safe("\n".join(links.union(preconnect_links))) # noqa: S308