moosey-cms 0.5.0__tar.gz → 0.7.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.
Files changed (33) hide show
  1. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/PKG-INFO +3 -1
  2. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/README.md +2 -0
  3. moosey_cms-0.7.0/docs/filters.md +184 -0
  4. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/pyproject.toml +1 -1
  5. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/filters.py +52 -8
  6. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/main.py +25 -0
  7. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/.gitignore +0 -0
  8. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/.python-version +0 -0
  9. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/assets/example-1.jpeg +0 -0
  10. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/assets/example-2.jpeg +0 -0
  11. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/content/about.md +0 -0
  12. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/content/index.md +0 -0
  13. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/content/pages/features.md +0 -0
  14. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/content/posts/building-modern-apps.md +0 -0
  15. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/content/posts/index.md +0 -0
  16. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/main.py +0 -0
  17. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/templates/404.html +0 -0
  18. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/templates/components/sidebar.html +0 -0
  19. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/templates/index.html +0 -0
  20. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/templates/layout/base.html +0 -0
  21. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/templates/page.html +0 -0
  22. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/templates/post.html +0 -0
  23. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/example/templates/posts.html +0 -0
  24. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/__init__.py +0 -0
  25. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/cache.py +0 -0
  26. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/file_watcher.py +0 -0
  27. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/helpers.py +0 -0
  28. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/hot_reload_script.py +0 -0
  29. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/md.py +0 -0
  30. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/models.py +0 -0
  31. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/seo.py +0 -0
  32. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/src/moosey_cms/static/js/reload-script.js +0 -0
  33. {moosey_cms-0.5.0 → moosey_cms-0.7.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moosey-cms
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: cachetools>=6.2.4
@@ -259,6 +259,8 @@ Moosey CMS comes packed with a comprehensive library of Jinja2 filters to help y
259
259
 
260
260
  ---
261
261
 
262
+ [Read More On Filters](docs/filters.md) and how to use some interesting ones such as stripping comments.
263
+
262
264
  ## ⚙️ Configuration Reference
263
265
 
264
266
  The `init_cms` function accepts the following parameters:
@@ -242,6 +242,8 @@ Moosey CMS comes packed with a comprehensive library of Jinja2 filters to help y
242
242
 
243
243
  ---
244
244
 
245
+ [Read More On Filters](docs/filters.md) and how to use some interesting ones such as stripping comments.
246
+
245
247
  ## ⚙️ Configuration Reference
246
248
 
247
249
  The `init_cms` function accepts the following parameters:
@@ -0,0 +1,184 @@
1
+ <!--
2
+ Copyright (c) 2026 Anthony Mugendi
3
+
4
+ This software is released under the MIT License.
5
+ https://opensource.org/licenses/MIT
6
+ -->
7
+
8
+ # Template Filters
9
+
10
+ Moosey CMS comes equipped with a powerful suite of Jinja2 filters. These allow you to format data, manipulate text, and clean up HTML directly within your Markdown files or HTML templates.
11
+
12
+ ## Usage
13
+
14
+ Filters are applied using the pipe symbol (`|`). You can chain multiple filters together.
15
+
16
+ ```jinja
17
+ {{ variable | filter_name }}
18
+ {{ variable | filter1 | filter2 }}
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 🧹 HTML & Structure
24
+
25
+ ### `strip_comments`
26
+ **Type:** Block Filter
27
+ Removes HTML comments (`<!-- ... -->`) from the enclosed content. This is useful for keeping production code clean while leaving comments in for development.
28
+
29
+ **Arguments:**
30
+ * `enabled` (bool): If `False`, comments are preserved. Default is `True`.
31
+
32
+ **Usage:**
33
+ You typically wrap your entire `base.html` layout with this.
34
+
35
+ ```jinja
36
+ <!-- example/templates/layout/base.html -->
37
+
38
+ <!-- Only strip comments if not in development mode -->
39
+ {% filter strip_comments(enabled=(mode != 'development')) %}
40
+ <!DOCTYPE html>
41
+ <html>
42
+ <head>
43
+ <!-- This comment will vanish in production -->
44
+ <title>{{ title }}</title>
45
+ </head>
46
+ <body>
47
+ {{ content }}
48
+ </body>
49
+ </html>
50
+ {% endfilter %}
51
+ ```
52
+
53
+ ### `minify_html`
54
+ **Type:** Block Filter
55
+ Reduces file size by removing newlines, tabs, and extra spaces. It collapses multiple spaces into one and removes whitespace between HTML tags.
56
+
57
+ **Arguments:**
58
+ * `enabled` (bool): Default `True`.
59
+
60
+ **⚠️ Important Note:**
61
+ This filter is "aggressive." It does not detect `<pre>` or `<textarea>` tags. If you use code blocks where indentation must be preserved exactly, consider disabling this filter or handling those blocks separately.
62
+
63
+ **Usage Example:**
64
+
65
+ ```jinja
66
+ {% minify_html(enabled=(mode != 'development')) %}
67
+ <html>
68
+ ...
69
+ </html>
70
+ {% endfilter %}
71
+ ```
72
+
73
+ **Combined Usage Example:**
74
+
75
+ This is the recommended setup for your `base.html` file to ensure maximum performance in production while keeping development easy.
76
+
77
+ ```jinja
78
+ {% filter strip_comments(enabled=(mode != 'development')) | minify_html(enabled=(mode != 'development')) %}
79
+ <html>
80
+ ...
81
+ </html>
82
+ {% endfilter %}
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 📅 Date & Time
88
+
89
+ Assuming `date_obj` is a Python datetime object (e.g., from `date: 2026-01-21` in frontmatter).
90
+
91
+ | Filter | Description | Example Input | Output |
92
+ | :--- | :--- | :--- | :--- |
93
+ | **`fancy_date`** | Formats date with ordinal suffix. | `2026-01-21 18:00` | 21st Jan, 2026 at 6:00 PM |
94
+ | **`short_date`** | Standard clean date format. | `2026-01-21` | Jan 21, 2026 |
95
+ | **`iso_date`** | ISO 8601 format (good for meta tags). | `2026-01-21` | 2026-01-21 |
96
+ | **`time_only`** | Extracts just the time. | `2026-01-21 18:00` | 6:00 PM |
97
+ | **`relative_time`** | Human readable time difference. | `(Now - 2 hours)` | 2 hours ago |
98
+
99
+ **Usage:**
100
+ ```jinja
101
+ <time>{{ date.created | fancy_date }}</time>
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 📝 Text Manipulation
107
+
108
+ | Filter | Description | Example Input | Output |
109
+ | :--- | :--- | :--- | :--- |
110
+ | **`truncate_words`** | Cuts text after N words. | `{{ "one two three four" | truncate_words(2) }}` | one two... |
111
+ | **`excerpt`** | Smart truncation that tries to break at the end of a sentence. | *Long paragraph* | *First few sentences...* |
112
+ | **`title_case`** | Capitalizes words intelligently (skips "and", "the", etc). | `a tale of two cities` | A Tale of Two Cities |
113
+ | **`slugify`** | Converts text to URL-friendly format. | `Hello World!` | `hello-world` |
114
+ | **`smart_quotes`** | Converts straight quotes to curly quotes. | `"Hello"` | “Hello” |
115
+ | **`read_time`** | Calculates reading time (approx 200 wpm). | *500 words text* | 3 min read |
116
+
117
+ **Usage:**
118
+ ```jinja
119
+ <h1>{{ title | title_case }}</h1>
120
+ <p>{{ content | excerpt(150) }}</p>
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 💰 Currency & Finance
126
+
127
+ | Filter | Description | Arguments | Output |
128
+ | :--- | :--- | :--- | :--- |
129
+ | **`currency`** | Formats number with symbol. | `code` (default 'USD') | `$1,234.56` |
130
+ | **`compact_currency`** | Shortens large numbers. | `code` (default 'USD') | `$1.5M`, `$45K` |
131
+ | **`currency_name`** | Converts ISO code to name. | - | `KES` → `Kenyan Shilling` |
132
+
133
+ **Usage:**
134
+ ```jinja
135
+ <!-- Custom Currency -->
136
+ Price: {{ 4500 | currency('EUR') }}
137
+ <!-- Output: €4,500.00 -->
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 🌍 Geography & Locale
143
+
144
+ Requires valid ISO 3166-1 alpha-2 or alpha-3 codes.
145
+
146
+ | Filter | Description | Example Input | Output |
147
+ | :--- | :--- | :--- | :--- |
148
+ | **`country_flag`** | Converts country code to Emoji flag. | `US` | 🇺🇸 |
149
+ | **`country_name`** | Converts code to full name. | `DE` | Germany |
150
+ | **`language_name`** | Converts language code to name. | `fr` | French |
151
+
152
+ **Usage:**
153
+ ```jinja
154
+ <span>Made in {{ 'JP' | country_flag }} {{ 'JP' | country_name }}</span>
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 🔢 Numbers & Math
160
+
161
+ | Filter | Description | Example Input | Output |
162
+ | :--- | :--- | :--- | :--- |
163
+ | **`number_format`** | Adds thousand separators. | `10000` | `10,000` |
164
+ | **`percentage`** | Formats float as percent. | `50.5` | `50.5%` |
165
+ | **`ordinal`** | Adds ordinal suffix to integer. | `3` | `3rd` |
166
+
167
+ ---
168
+
169
+ ## 🛠 Utilities
170
+
171
+ | Filter | Description | Example Input | Output |
172
+ | :--- | :--- | :--- | :--- |
173
+ | **`filesize`** | Bytes to human readable size. | `1048576` | `1.0 MB` |
174
+ | **`yesno`** | Boolean to text. | `True` | `Yes` (or custom) |
175
+ | **`default_if_none`** | Fallback if value is None. | `None` | *(Default string)* |
176
+
177
+ **Usage:**
178
+ ```jinja
179
+ <!-- Custom Yes/No labels -->
180
+ Active: {{ is_active | yesno("Online", "Offline") }}
181
+
182
+ <!-- File Size -->
183
+ Download size: {{ 2500000 | filesize }}
184
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "moosey-cms"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -47,13 +47,15 @@ def iso_date(dt):
47
47
  return dt.strftime('%Y-%m-%d')
48
48
 
49
49
 
50
- def relative_time(dt):
50
+ def relative_time(dt, showAgo=True):
51
51
  """Format date as relative time (e.g., '2 hours ago', 'yesterday')"""
52
52
  if not dt:
53
53
  return ""
54
54
 
55
55
  now = datetime.now()
56
56
  diff = now - dt
57
+
58
+ ago = " ago" if showAgo else ""
57
59
 
58
60
  seconds = diff.total_seconds()
59
61
 
@@ -61,10 +63,10 @@ def relative_time(dt):
61
63
  return "just now"
62
64
  elif seconds < 3600:
63
65
  minutes = int(seconds / 60)
64
- return f"{minutes} minute{'s' if minutes != 1 else ''} ago"
66
+ return f"{minutes} minute{'s' if minutes != 1 else ''}{ago}"
65
67
  elif seconds < 86400:
66
68
  hours = int(seconds / 3600)
67
- return f"{hours} hour{'s' if hours != 1 else ''} ago"
69
+ return f"{hours} hour{'s' if hours != 1 else ''}{ago}"
68
70
  elif seconds < 172800:
69
71
  return "yesterday"
70
72
  elif seconds < 604800:
@@ -72,13 +74,13 @@ def relative_time(dt):
72
74
  return f"{days} days ago"
73
75
  elif seconds < 2592000:
74
76
  weeks = int(seconds / 604800)
75
- return f"{weeks} week{'s' if weeks != 1 else ''} ago"
77
+ return f"{weeks} week{'s' if weeks != 1 else ''}{ago}"
76
78
  elif seconds < 31536000:
77
79
  months = int(seconds / 2592000)
78
- return f"{months} month{'s' if months != 1 else ''} ago"
80
+ return f"{months} month{'s' if months != 1 else ''}{ago}"
79
81
  else:
80
82
  years = int(seconds / 31536000)
81
- return f"{years} year{'s' if years != 1 else ''} ago"
83
+ return f"{years} year{'s' if years != 1 else ''}{ago}"
82
84
 
83
85
 
84
86
  def time_only(dt):
@@ -90,7 +92,8 @@ def time_only(dt):
90
92
  formatted = formatted[1:]
91
93
  return formatted
92
94
 
93
-
95
+ def strptime(s, fmt):
96
+ return datetime.strptime(s, fmt)
94
97
  # ============================================================================
95
98
  # CURRENCY FILTERS
96
99
  # ============================================================================
@@ -472,6 +475,43 @@ def read_time(text: str) -> str:
472
475
  return "1 min read"
473
476
  return f"{minutes} min read"
474
477
 
478
+
479
+ # ============================================================================
480
+ # HTML UTILITIES
481
+ # ============================================================================
482
+
483
+ def strip_comments(text, enabled=True):
484
+ """
485
+ Removes HTML comments from the output.
486
+ Usage: {% filter strip_comments(enabled=True) %} ... {% endfilter %}
487
+ """
488
+ if not enabled or not text:
489
+ return text
490
+
491
+ # Regex: Matches <!-- followed by anything (including newlines) until -->
492
+ # The *? ensures it is non-greedy (stops at the first closing tag)
493
+ return re.sub(r'<!--[\s\S]*?-->', '', str(text))
494
+
495
+ def minify_html(text, enabled=True):
496
+ """
497
+ Minifies HTML by removing unnecessary whitespace and newlines.
498
+ WARNING: This is a regex-based minifier. It does not respect <pre> tags.
499
+ """
500
+ if not enabled or not text:
501
+ return text
502
+
503
+ text = str(text)
504
+
505
+ # 1. Normalize whitespace:
506
+ # Replace sequences of whitespace (tabs, newlines) with a single space
507
+ text = re.sub(r'\s+', ' ', text)
508
+
509
+ # 2. Remove space between tags:
510
+ # Turns "</div> <div..." into "</div><div..."
511
+ text = re.sub(r'>\s+<', '><', text)
512
+
513
+ return text.strip()
514
+
475
515
  # ============================================================================
476
516
  # REGISTRATION FUNCTION
477
517
  # ============================================================================
@@ -492,6 +532,7 @@ def register_filters(jinja_env):
492
532
  'short_date': short_date,
493
533
  'iso_date': iso_date,
494
534
  'relative_time': relative_time,
535
+ 'strptime': strptime,
495
536
  'time_only': time_only,
496
537
  'currency': currency,
497
538
  'compact_currency': compact_currency,
@@ -511,7 +552,10 @@ def register_filters(jinja_env):
511
552
  'filesize': filesize,
512
553
  'default_if_none': default_if_none,
513
554
  'yesno': yesno,
514
- 'read_time':read_time
555
+ 'read_time':read_time,
556
+ 'strip_comments': strip_comments,
557
+ 'minify_html': minify_html,
558
+
515
559
  }
516
560
 
517
561
  for name, func in filters_dict.items():
@@ -24,6 +24,30 @@ from .hot_reload_script import inject_script_middleware
24
24
 
25
25
  from fastapi import WebSocket, WebSocketDisconnect
26
26
 
27
+ from jinja2 import Environment, FileSystemLoader
28
+ from jinja2.ext import Extension
29
+ import re
30
+
31
+ class AutoRemoveCommentsExtension(Extension):
32
+ """Automatically removes HTML comments from all included files"""
33
+
34
+ def __init__(self, environment):
35
+ super().__init__(environment)
36
+
37
+ # Store original include function
38
+ original_include = environment.globals['include']
39
+
40
+ # Create wrapper that removes comments
41
+ def include_no_comments(template_name, **kwargs):
42
+ # Get the included template
43
+ included = environment.get_template(template_name)
44
+ rendered = included.render(**kwargs)
45
+ # Remove comments
46
+ return re.sub(r'<!--.*?-->', '', rendered, flags=re.DOTALL)
47
+
48
+ # Replace include function
49
+ environment.globals['include_no_comments'] = include_no_comments
50
+
27
51
 
28
52
  class ConnectionManager:
29
53
  def __init__(self):
@@ -81,6 +105,7 @@ def init_cms(
81
105
  # This ensures site_data is available in 404.html and base.html automatically
82
106
  templates.env.globals["site_data"] = site_data
83
107
  templates.env.globals["mode"] = mode
108
+
84
109
 
85
110
  # Register all custom filters once
86
111
  filters.register_filters(templates.env)
File without changes
File without changes
File without changes
File without changes