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,4 @@
1
+ VERSION = (0, 1, 0)
2
+ __version__ = ".".join(map(str, VERSION))
3
+
4
+ default_app_config = "draftail_text_utils.apps.DraftailTextUtilsAppConfig"
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DraftailTextUtilsAppConfig(AppConfig):
5
+ label = "draftail_text_utils"
6
+ name = "draftail_text_utils"
7
+ verbose_name = "Draftail Text Utils"
@@ -0,0 +1,482 @@
1
+ """
2
+ Configuration for draftail_text_utils via Django settings.
3
+
4
+ Usage in your Django settings:
5
+
6
+ DRAFTAIL_TEXT_UTILS = {
7
+ # ---- Font size ----
8
+ "FONT_SIZES": {
9
+ "MIN": 8,
10
+ "MAX": 100,
11
+ "STEP": 1,
12
+ "PRESETS": [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 30, 36, 48, 60, 72, 96],
13
+ },
14
+ # ---- Color palette ----
15
+ # Option A: point to an existing module with a DRAFTAIL_COLORS list
16
+ "COLORS": {
17
+ "MODULE": None, # e.g. "apps.core.models"
18
+ },
19
+ # Option B: provide an explicit palette (ignored if MODULE is set)
20
+ "COLOR_PALETTE": None, # falls back to built-in defaults
21
+ # ---- Font families ----
22
+ # Option A: point to an existing module with a DRAFTAIL_FONT_FAMILIES list
23
+ # or a SiteDesignSettings / TypographyBlock with font definitions.
24
+ "FONT_FAMILIES": {
25
+ "MODULE": None, # e.g. "apps.core.models"
26
+ },
27
+ # Option B: provide an explicit list (ignored if MODULE is set):
28
+ # [{"label": "Roboto", "value": "'Roboto', sans-serif"}, ...]
29
+ # ---- Font URL stylesheets ----
30
+ # Option A: point to an existing module with a DRAFTAIL_FONT_URLS list,
31
+ # or a TypographyBlock / SiteDesignSettings with a font_url field.
32
+ "FONT_URLS": {
33
+ "MODULE": None, # e.g. "apps.core.models"
34
+ },
35
+ # Option B: provide an explicit list of CSS URLs:
36
+ # ["https://fonts.bunny.net/css?family=...", ...]
37
+ # ---- Feature toggles ----
38
+ "FEATURES": {
39
+ "TEXT_COLOR": True,
40
+ "HIGHLIGHT_COLOR": True,
41
+ "FONT_FAMILY": True,
42
+ "FONT_SIZE": True,
43
+ "TEXT_ALIGNMENT": True,
44
+ },
45
+ }
46
+ """
47
+
48
+ import importlib
49
+ import logging
50
+
51
+ from django.conf import settings
52
+ from wagtailtraverse import traverse_value
53
+
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+
58
+ DEFAULT_FONT_SIZES = {
59
+ "MIN": 8,
60
+ "MAX": 100,
61
+ "STEP": 1,
62
+ "PRESETS": [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 30, 36, 48, 60, 72, 96],
63
+ }
64
+
65
+ DEFAULT_COLORS = [
66
+ {"key": "transparent", "label": "Transparent", "value": "#00000000"},
67
+ {"key": "black", "label": "Black", "value": "#000000"},
68
+ {"key": "white", "label": "White", "value": "#FFFFFF"},
69
+ {"key": "gray", "label": "Gray", "value": "#6B7280"},
70
+ {"key": "red", "label": "Red", "value": "#EF4444"},
71
+ {"key": "orange", "label": "Orange", "value": "#F97316"},
72
+ {"key": "yellow", "label": "Yellow", "value": "#EAB308"},
73
+ {"key": "green", "label": "Green", "value": "#22C55E"},
74
+ {"key": "teal", "label": "Teal", "value": "#14B8A6"},
75
+ {"key": "blue", "label": "Blue", "value": "#3B82F6"},
76
+ {"key": "indigo", "label": "Indigo", "value": "#6366F1"},
77
+ {"key": "purple", "label": "Purple", "value": "#A855F7"},
78
+ {"key": "pink", "label": "Pink", "value": "#EC4899"},
79
+ ]
80
+
81
+ DEFAULT_FEATURES = {
82
+ "TEXT_COLOR": True,
83
+ "HIGHLIGHT_COLOR": True,
84
+ "FONT_FAMILY": True,
85
+ "FONT_SIZE": True,
86
+ "TEXT_ALIGNMENT": True,
87
+ }
88
+
89
+
90
+ def _user_settings():
91
+ return getattr(settings, "DRAFTAIL_TEXT_UTILS", {})
92
+
93
+
94
+ def _deep_merge(default, override):
95
+ result = default.copy()
96
+ for key, value in override.items():
97
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
98
+ result[key] = _deep_merge(result[key], value)
99
+ else:
100
+ result[key] = value
101
+ return result
102
+
103
+
104
+ FULL_DEFAULTS = {
105
+ "FONT_SIZES": DEFAULT_FONT_SIZES,
106
+ "COLORS": {},
107
+ "COLOR_PALETTE": None,
108
+ "FONT_FAMILIES": None,
109
+ "FONT_URLS": None,
110
+ "FEATURES": DEFAULT_FEATURES,
111
+ }
112
+
113
+
114
+ def _merged_config():
115
+ user = _user_settings()
116
+ return _deep_merge(FULL_DEFAULTS.copy(), user)
117
+
118
+
119
+ def get_setting(key, default=None):
120
+ config = _merged_config()
121
+ return config.get(key, default)
122
+
123
+
124
+ def get_font_sizes():
125
+ return get_setting("FONT_SIZES", DEFAULT_FONT_SIZES)
126
+
127
+
128
+ def get_font_size_presets():
129
+ sizes = get_font_sizes()
130
+ return sizes.get("PRESETS", DEFAULT_FONT_SIZES["PRESETS"])
131
+
132
+
133
+ def get_feature_toggles():
134
+ return get_setting("FEATURES", DEFAULT_FEATURES)
135
+
136
+
137
+ def feature_enabled(name):
138
+ return get_feature_toggles().get(name, True)
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # wagtailtraverse-based extraction utilities
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ def _extract_struct_from_field(field_value):
147
+ """
148
+ Extract a plain dict from a Wagtail StreamValue / BoundBlock / StructValue,
149
+ or return the value itself if it is already a dict/list.
150
+ Returns an empty dict if extraction fails.
151
+ """
152
+ if not field_value:
153
+ return {}
154
+
155
+ if isinstance(field_value, dict):
156
+ return field_value
157
+
158
+ if isinstance(field_value, list):
159
+ if field_value and hasattr(field_value[0], "block"):
160
+ for _path, bound_block in traverse_value(field_value):
161
+ return bound_block.value or {}
162
+ return {}
163
+
164
+ try:
165
+ for _path, bound_block in traverse_value(field_value):
166
+ return bound_block.value or {}
167
+ except Exception:
168
+ logger.exception("Failed to extract struct from field %s", field_value)
169
+
170
+ return {}
171
+
172
+
173
+ def _find_design_settings_model(module):
174
+ """Find a SiteDesignSettings-style model class in a module."""
175
+ for attr_name in dir(module):
176
+ attr = getattr(module, attr_name)
177
+ if isinstance(attr, type) and hasattr(attr, "_meta"):
178
+ try:
179
+ if attr._meta.label and "SiteDesignSettings" in attr.__name__:
180
+ return attr
181
+ except (AttributeError, KeyError):
182
+ continue
183
+ return None
184
+
185
+
186
+ def _find_typography_block(module):
187
+ """Find a TypographyBlock / TypographyStreamBlock class in a module."""
188
+ for attr_name in dir(module):
189
+ attr = getattr(module, attr_name)
190
+ if isinstance(attr, type) and "Typography" in getattr(attr, "__name__", ""):
191
+ return attr
192
+ return None
193
+
194
+
195
+ def _normalise_font_urls(font_urls):
196
+ """Normalise font_urls to a list, handling string or single value input."""
197
+ if not font_urls:
198
+ return []
199
+ if isinstance(font_urls, str):
200
+ return [font_urls]
201
+ return list(font_urls)
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Color palette loading
206
+ # ---------------------------------------------------------------------------
207
+
208
+
209
+ def load_color_palette():
210
+ colors_config = get_setting("COLORS", {})
211
+ module_path = colors_config.get("MODULE")
212
+
213
+ if module_path:
214
+ try:
215
+ module = importlib.import_module(module_path)
216
+ if hasattr(module, "DRAFTAIL_COLORS"):
217
+ return module.DRAFTAIL_COLORS
218
+ if hasattr(module, "COLOR_PALETTE"):
219
+ raw = module.COLOR_PALETTE
220
+ if isinstance(raw, dict):
221
+ return [
222
+ {"key": k, "label": k.replace("_", " ").title(), "value": v}
223
+ for k, v in raw.items()
224
+ ]
225
+ return raw
226
+ except (ImportError, AttributeError):
227
+ pass
228
+
229
+ result = load_colors_from_module_settings(module_path)
230
+ if result is not None:
231
+ return result
232
+
233
+ explicit = get_setting("COLOR_PALETTE")
234
+ if explicit is not None:
235
+ return explicit
236
+
237
+ return DEFAULT_COLORS
238
+
239
+
240
+ def load_colors_from_module_settings(module_path):
241
+ try:
242
+ module = importlib.import_module(module_path)
243
+ except ImportError:
244
+ return None
245
+
246
+ palette = getattr(module, "LIGHT_MODE_DEFAULT_COLORS", None)
247
+ if palette and isinstance(palette, dict):
248
+ return [
249
+ {"key": k, "label": k.replace("_", " ").title(), "value": v}
250
+ for k, v in palette.items()
251
+ ]
252
+
253
+ model_class = _find_design_settings_model(module)
254
+ if model_class:
255
+ try:
256
+ instance = model_class.load()
257
+ cp_struct = _extract_struct_from_field(instance.color_palette)
258
+ light = cp_struct.get("light", {})
259
+ if light:
260
+ return [
261
+ {"key": k, "label": k.replace("_", " ").title(), "value": v}
262
+ for k, v in light.items()
263
+ ]
264
+ except Exception:
265
+ logger.warning(
266
+ "Failed to load colours from module %s", module_path, exc_info=True
267
+ )
268
+
269
+ return None
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Font family loading
274
+ # ---------------------------------------------------------------------------
275
+
276
+
277
+ def load_font_families():
278
+ value = get_setting("FONT_FAMILIES")
279
+
280
+ if isinstance(value, list):
281
+ return value
282
+
283
+ if isinstance(value, dict):
284
+ module_path = value.get("MODULE")
285
+ if module_path:
286
+ result = load_font_families_from_module_settings(module_path)
287
+ if result is not None:
288
+ return result
289
+
290
+ return []
291
+
292
+
293
+ def load_font_families_from_module_settings(module_path):
294
+ try:
295
+ module = importlib.import_module(module_path)
296
+ except ImportError:
297
+ return None
298
+
299
+ if hasattr(module, "DRAFTAIL_FONT_FAMILIES"):
300
+ return module.DRAFTAIL_FONT_FAMILIES
301
+
302
+ typography_block = _find_typography_block(module)
303
+ if typography_block:
304
+ families = _extract_font_families_from_typography_block(typography_block)
305
+ if families:
306
+ return families
307
+
308
+ model_class = _find_design_settings_model(module)
309
+ if model_class:
310
+ try:
311
+ instance = model_class.load()
312
+ families = _extract_font_families_from_typography_instance(instance)
313
+ if families:
314
+ return families
315
+ except Exception:
316
+ logger.warning(
317
+ "Failed to load font families from module %s",
318
+ module_path,
319
+ exc_info=True,
320
+ )
321
+
322
+ return None
323
+
324
+
325
+ def _extract_font_families_from_typography_block(typography_block):
326
+ families = []
327
+
328
+ child_blocks = getattr(typography_block, "child_blocks", None)
329
+ if child_blocks is None:
330
+ child_blocks = {}
331
+ try:
332
+ child_blocks = {f.name: f for f in typography_block.base_blocks.items()}
333
+ except (AttributeError, TypeError):
334
+ pass
335
+
336
+ for field_name in (
337
+ "font_family_heading",
338
+ "font_family_subheading",
339
+ "font_family_body",
340
+ ):
341
+ block = child_blocks.get(field_name)
342
+ if block is not None:
343
+ default = getattr(block, "default", None)
344
+ if default:
345
+ label = field_name.replace("font_family_", "").replace("_", " ").title()
346
+ families.append({"label": label, "value": str(default)})
347
+
348
+ custom_block = child_blocks.get("custom_fonts")
349
+ if custom_block is not None:
350
+ default_list = getattr(custom_block, "default", None)
351
+ if default_list:
352
+ for cf in default_list:
353
+ families.append(
354
+ {
355
+ "label": cf.get("label", cf.get("font_family", "")),
356
+ "value": cf.get("font_family", ""),
357
+ }
358
+ )
359
+
360
+ return families or None
361
+
362
+
363
+ def _extract_font_families_from_typography_instance(instance):
364
+ tp_struct = _extract_struct_from_field(instance.typography)
365
+ if not tp_struct:
366
+ return None
367
+
368
+ families = []
369
+ for key in (
370
+ "font_family_heading",
371
+ "font_family_subheading",
372
+ "font_family_body",
373
+ ):
374
+ value = tp_struct.get(key)
375
+ if value:
376
+ label = key.replace("font_family_", "").replace("_", " ").title()
377
+ families.append({"label": label, "value": value})
378
+
379
+ custom_fonts = tp_struct.get("custom_fonts", [])
380
+ for cf in custom_fonts:
381
+ families.append(
382
+ {
383
+ "label": cf.get("label", cf.get("font_family", "")),
384
+ "value": cf.get("font_family", ""),
385
+ }
386
+ )
387
+
388
+ return families or None
389
+
390
+
391
+ # ---------------------------------------------------------------------------
392
+ # Font URL loading
393
+ # ---------------------------------------------------------------------------
394
+
395
+
396
+ def load_font_urls():
397
+ value = get_setting("FONT_URLS")
398
+
399
+ if isinstance(value, list):
400
+ return value
401
+
402
+ if isinstance(value, dict):
403
+ module_path = value.get("MODULE")
404
+ if module_path:
405
+ result = load_font_urls_from_module_settings(module_path)
406
+ if result is not None:
407
+ return result
408
+
409
+ return []
410
+
411
+
412
+ def load_font_urls_from_module_settings(module_path):
413
+ try:
414
+ module = importlib.import_module(module_path)
415
+ except ImportError:
416
+ return None
417
+
418
+ if hasattr(module, "DRAFTAIL_FONT_URLS"):
419
+ return module.DRAFTAIL_FONT_URLS
420
+
421
+ typography_block = _find_typography_block(module)
422
+ if typography_block:
423
+ urls = _extract_font_urls_from_typography_block(typography_block)
424
+ if urls:
425
+ return urls
426
+
427
+ model_class = _find_design_settings_model(module)
428
+ if model_class:
429
+ try:
430
+ instance = model_class.load()
431
+ tp_struct = _extract_struct_from_field(instance.typography)
432
+ if tp_struct:
433
+ urls = _normalise_font_urls(tp_struct.get("font_url", []))
434
+ custom_fonts = tp_struct.get("custom_fonts", [])
435
+ for cf in custom_fonts:
436
+ url = cf.get("font_url")
437
+ if url:
438
+ urls.append(url)
439
+ if urls:
440
+ return urls
441
+ except Exception:
442
+ logger.warning(
443
+ "Failed to load font URLs from module %s",
444
+ module_path,
445
+ exc_info=True,
446
+ )
447
+
448
+ return None
449
+
450
+
451
+ def _extract_font_urls_from_typography_block(typography_block):
452
+ child_blocks = getattr(typography_block, "child_blocks", None)
453
+ if child_blocks is None:
454
+ try:
455
+ child_blocks = {f.name: f for f in typography_block.base_blocks.items()}
456
+ except (AttributeError, TypeError):
457
+ child_blocks = {}
458
+
459
+ urls = []
460
+ for field_name in (
461
+ "font_url",
462
+ "google_fonts_url",
463
+ "font_url_heading",
464
+ "font_url_body",
465
+ "font_url_subheading",
466
+ ):
467
+ block = child_blocks.get(field_name)
468
+ if block is not None:
469
+ default = getattr(block, "default", None)
470
+ if default:
471
+ urls.append(str(default))
472
+
473
+ custom_block = child_blocks.get("custom_fonts")
474
+ if custom_block is not None:
475
+ default_list = getattr(custom_block, "default", None)
476
+ if default_list:
477
+ for cf in default_list:
478
+ url = cf.get("font_url")
479
+ if url:
480
+ urls.append(url)
481
+
482
+ return list(dict.fromkeys(urls)) if urls else None
File without changes
File without changes