rynz 1.0.0__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.
rynz/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ #!/bin/python
2
+
3
+ #
4
+ # ███╗ ██╗██╗██╗ ██╗ █████╗ ██████╗ ███████╗
5
+ # ████╗ ██║██║██║ ██║██╔══██╗██╔══██╗██╔════╝
6
+ # ██╔██╗ ██║██║███████║███████║██████╔╝███████╗
7
+ # ██║╚██╗██║██║██╔══██║██╔══██║██╔══██╗╚════██║
8
+ # ██║ ╚████║██║██║ ██║██║ ██║██║ ██║███████║
9
+ # ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
10
+ # DRAFTED BY [https://nih.ar] ON 14-04-2025
11
+ # SOURCE [__init__.py] LAST MODIFIED ON 28-04-2025.
12
+ #
13
+
14
+ """
15
+ Module for versioning of the rynz package.
16
+
17
+ This module defines the version number for the rynz static site generator
18
+ and provides a function to retrieve the current version.
19
+ """
20
+
21
+ __version__ = "1.0.0"
22
+
23
+ def get_version():
24
+ """Returns the current version of the rynz package."""
25
+ return __version__
rynz/builder.py ADDED
@@ -0,0 +1,242 @@
1
+ #!/bin/python
2
+
3
+ #
4
+ # ███╗ ██╗██╗██╗ ██╗ █████╗ ██████╗ ███████╗
5
+ # ████╗ ██║██║██║ ██║██╔══██╗██╔══██╗██╔════╝
6
+ # ██╔██╗ ██║██║███████║███████║██████╔╝███████╗
7
+ # ██║╚██╗██║██║██╔══██║██╔══██║██╔══██╗╚════██║
8
+ # ██║ ╚████║██║██║ ██║██║ ██║██║ ██║███████║
9
+ # ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
10
+ # DRAFTED BY [https://nih.ar] ON 28-04-2025
11
+ # SOURCE [builder.py] LAST MODIFIED ON 28-04-2025.
12
+ #
13
+
14
+ from shutil import copytree, rmtree
15
+ from os import path, makedirs, sep
16
+ from glob import glob
17
+ from yaml import safe_load
18
+ from jinja2 import Environment, FileSystemLoader
19
+ from markdown2 import markdown
20
+ from time import time
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+
25
+ # Initialize console for colorful output
26
+ console = Console()
27
+
28
+
29
+ def print_message(message, style="info"):
30
+ """
31
+ Prints a styled message using the Rich console.
32
+
33
+ Args:
34
+ message (str): The message to display.
35
+ style (str): Message style ("success", "error", or "info").
36
+ """
37
+ styles = {
38
+ "success": "bold green",
39
+ "error": "bold red",
40
+ "info": "yellow"
41
+ }
42
+ console.print(message, style=styles.get(style, "yellow"))
43
+
44
+ def buildRynz():
45
+ """
46
+ Builds the static site by converting Markdown notes into HTML pages,
47
+ using Jinja2 templates and configuration from config.yml.
48
+ """
49
+ start_time = time()
50
+ total_notes = 0
51
+ failed_notes = 0
52
+ skipped_notes = []
53
+
54
+ console.rule("[bold green]🔨 Starting Build Process")
55
+
56
+ def readmd(filepath):
57
+ """
58
+ Reads and returns the raw content of a markdown file.
59
+
60
+ Args:
61
+ filepath (str): Path to the markdown file.
62
+
63
+ Returns:
64
+ str: File content as a string.
65
+ """
66
+ try:
67
+ with open(filepath, 'r', encoding='utf8', errors='ignore') as data:
68
+ return data.read()
69
+ except Exception as e:
70
+ print_message(f"❌ Error reading {filepath}: {e}", "error")
71
+ return ""
72
+
73
+
74
+ def create_page(template, post_detail, md_content, filename):
75
+ """
76
+ Renders and writes a page from a Jinja2 template and given content.
77
+ Handles both single notes and special pages like index.html and RSS feeds.
78
+
79
+ Args:
80
+ template (str): Template filename.
81
+ post_detail (dict): Frontmatter details for the post.
82
+ md_content (str): Markdown content to render.
83
+ filename (str): Output filename.
84
+
85
+ Returns:
86
+ str: Rendered file name or an empty string in case of error.
87
+ """
88
+
89
+ try:
90
+ env = Environment(loader=FileSystemLoader(searchpath='./'))
91
+ template_obj = env.get_template(template)
92
+
93
+ # Initialize variables
94
+ post_title = ""
95
+ post_subtitle = ""
96
+ post_date = ""
97
+ post_meta = ""
98
+ posts_list = ""
99
+ last_date = ""
100
+ post_data = filename.split(sep)
101
+ post_path = home_path
102
+
103
+ # Determine post details based on the filename
104
+ if filename == "index.html":
105
+ post_file = filename
106
+ posts_list = posts
107
+ elif filename.endswith(".xml"):
108
+ post_file = filename
109
+ posts_list = posts
110
+ last_date = posts_list[0].get('date')
111
+ elif post_detail is None:
112
+ post_file = filename.replace('.md', '.html')
113
+ else:
114
+ post_title = post_detail.get("title", "")
115
+ post_subtitle = post_detail.get("subtitle", "")
116
+ post_date = post_detail.get("date", "")
117
+ post_meta = post_detail.get("meta", "")
118
+ post_data = filename.split(sep)
119
+ post_path = path.join(home_path)
120
+ post_file = post_data[2].replace('.md', '.html')
121
+ post_data = post_data[1]
122
+ makedirs(post_path, exist_ok=True)
123
+
124
+ output_filepath = path.join(post_path, post_file)
125
+ with open(output_filepath, 'w', encoding='utf8', errors='ignore') as output_file:
126
+ output_file.write(
127
+ template_obj.render(
128
+ title=title,
129
+ post_title=post_title,
130
+ post_subtitle=post_subtitle,
131
+ date=post_date,
132
+ metad=post_meta,
133
+ url=path.join(url, post_file),
134
+ article=markdown(md_content),
135
+ posts=posts_list,
136
+ home=home_md,
137
+ header=markdown(readmd(header_md)),
138
+ footer=markdown(readmd(footer_md)),
139
+ last_date=last_date,
140
+ config=config
141
+ )
142
+ )
143
+ return post_file
144
+ except Exception as e:
145
+ print_message(f"❌ Error creating page {filename}: {e}", "error")
146
+ return ""
147
+
148
+ try:
149
+ # Load configuration
150
+ with open('config.yml', encoding='utf8', errors='ignore') as conf_file:
151
+ config = safe_load(conf_file.read())
152
+
153
+ # Set config values as globals
154
+ for key, val in config.items():
155
+ globals()[key] = val
156
+
157
+ # Ensure all required configuration keys are present
158
+ required_keys = [
159
+ 'home_path', 'resource_path', 'content_path', 'note_template',
160
+ 'home_template', 'feed_template', 'home_md', 'header_md',
161
+ 'footer_md', 'title', 'url'
162
+ ]
163
+ for key in required_keys:
164
+ if key not in config:
165
+ print_message(f"❌ Missing config key: {key}", "error")
166
+ return
167
+
168
+ # Reset output directory
169
+ if path.exists(home_path):
170
+ rmtree(home_path)
171
+ copytree(resource_path, home_path)
172
+
173
+ posts = []
174
+
175
+ # Process each note
176
+ for note_path in glob(path.join(content_path, "note", "*.md")):
177
+ try:
178
+ yaml_lines = []
179
+ frontmatter, markdown_content = '', ''
180
+
181
+ # Read frontmatter and body
182
+ with open(note_path, encoding='utf8', errors='ignore') as infile:
183
+ for line in infile:
184
+ if line.startswith('---'):
185
+ for line in infile:
186
+ if line.startswith('---'):
187
+ break
188
+ yaml_lines.append(line)
189
+ frontmatter = ''.join(yaml_lines)
190
+ markdown_content = ''.join(infile)
191
+ break
192
+
193
+ post_detail = safe_load(frontmatter)
194
+
195
+ if post_detail is not None:
196
+ # Build the note page
197
+ post_url = create_page(
198
+ note_template, post_detail, markdown_content, note_path
199
+ )
200
+
201
+ # Add to posts list for homepage/rss
202
+ post_entry = post_detail
203
+ post_entry.update({'url': '/' + post_url})
204
+ post_entry.update({'note': markdown(markdown_content)})
205
+ posts.append(post_entry)
206
+
207
+ total_notes += 1
208
+ print_message(f"✔ Built: {post_url}", "success")
209
+ else:
210
+ # If no valid frontmatter found
211
+ skipped_notes.append(note_path)
212
+ print_message(f"⚠️ Skipped: No frontmatter in {note_path}", "info")
213
+ except Exception:
214
+ failed_notes += 1
215
+ print_message(f"❌ Error in: {note_path}", "error")
216
+ console.print_exception()
217
+
218
+ # Sort posts by latest date
219
+ posts = sorted(posts, key=lambda post: post.get('date', ''), reverse=True)
220
+
221
+ # Build index.html and RSS feed
222
+ create_page(home_template, None, readmd(home_md), "index.html")
223
+ create_page(feed_template, None, readmd(home_md), "rss.xml")
224
+
225
+ # Build summary table
226
+ console.rule("[bold cyan]✅ Build Summary")
227
+ summary = Table(show_header=True, header_style="bold magenta")
228
+ summary.add_column("Metric")
229
+ summary.add_column("Count", justify="right")
230
+ summary.add_row("Notes Processed", str(total_notes))
231
+ summary.add_row("Skipped Notes", str(len(skipped_notes)))
232
+ summary.add_row("Failed Notes", str(failed_notes))
233
+ summary.add_row("Time Taken (s)", f"{(time() - start_time):.2f}")
234
+ console.print(summary)
235
+
236
+ except Exception:
237
+ console.print(Panel(
238
+ "[bold red]Something went wrong while building your site![/bold red]\n\n"
239
+ "[italic]Make sure you're inside a valid Rynz directory.[/italic]",
240
+ title="❌ Build Failed"
241
+ ))
242
+ console.print_exception()
rynz/creator.py ADDED
@@ -0,0 +1,410 @@
1
+ #!/bin/python3
2
+
3
+ #
4
+ # ███╗ ██╗██╗██╗ ██╗ █████╗ ██████╗ ███████╗
5
+ # ████╗ ██║██║██║ ██║██╔══██╗██╔══██╗██╔════╝
6
+ # ██╔██╗ ██║██║███████║███████║██████╔╝███████╗
7
+ # ██║╚██╗██║██║██╔══██║██╔══██║██╔══██╗╚════██║
8
+ # ██║ ╚████║██║██║ ██║██║ ██║██║ ██║███████║
9
+ # ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
10
+ # DRAFTED BY [https://nih.ar] ON 18-04-2021.
11
+ # SOURCE [creator.py] LAST MODIFIED ON 15-04-2025.
12
+ #
13
+
14
+ from os import mkdir, path, makedirs
15
+ from datetime import datetime
16
+ import sys
17
+ from rich.console import Console
18
+
19
+ # Initialize console for colorful output
20
+ console = Console()
21
+
22
+
23
+ def print_message(message, style="info", timestamp=False):
24
+ """
25
+ Prints a styled message using the Rich console.
26
+
27
+ Args:
28
+ message (str): The message to display.
29
+ style (str): Message style ("success", "error", or "info").
30
+ timestamp (bool): Whether to include a timestamp in the message.
31
+ """
32
+ styles = {
33
+ "success": "bold green",
34
+ "error": "bold red",
35
+ "info": "yellow"
36
+ }
37
+ if timestamp:
38
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
39
+ message = f"[{current_time}] {message}"
40
+ console.print(message, style=styles.get(style, "yellow"))
41
+
42
+
43
+ def get_input(prompt, default_value, validator=None):
44
+ """
45
+ Prompts user for input with an optional default and validator.
46
+
47
+ Args:
48
+ prompt (str): The input prompt.
49
+ default_value (str): Default input value if the user provides none.
50
+ validator (callable): A function to validate input (optional).
51
+
52
+ Returns:
53
+ str: The validated user input.
54
+ """
55
+ while True:
56
+ user_input = input(prompt).strip() or default_value
57
+ if validator and not validator(user_input):
58
+ print_message(
59
+ "Invalid input. Please try again.", "error", timestamp=True
60
+ )
61
+ else:
62
+ return user_input
63
+
64
+
65
+ def is_valid_url(url):
66
+ """
67
+ Checks if the given string contains a basic valid domain structure.
68
+
69
+ Args:
70
+ url (str): The URL string to validate.
71
+
72
+ Returns:
73
+ bool: True if the URL is valid, False otherwise.
74
+ """
75
+ return "." in url
76
+
77
+
78
+ def safe_create_directory(directory):
79
+ """
80
+ Safely creates a directory if it doesn't already exist.
81
+
82
+ Args:
83
+ directory (str): Path to the directory to create.
84
+
85
+ Exits:
86
+ On failure to create the directory.
87
+ """
88
+ try:
89
+ if not path.exists(directory):
90
+ makedirs(directory)
91
+ print_message(
92
+ f"Created directory: {directory}", "success", timestamp=True
93
+ )
94
+ else:
95
+ print_message(
96
+ f"Directory already exists: {directory}", "info", timestamp=True
97
+ )
98
+ except Exception as e:
99
+ print_message(
100
+ f"❌ Error creating directory {directory}: {e}", "error", timestamp=True
101
+ )
102
+ sys.exit(1)
103
+
104
+
105
+ def createConfig(rynzName):
106
+ """
107
+ Creates the default configuration file `config.yml`.
108
+
109
+ Args:
110
+ rynzName (str): The name of the project directory.
111
+ """
112
+ try:
113
+ with open(path.join(rynzName, 'config.yml'), 'w') as f:
114
+ title = rynzName
115
+ url = 'yourdomain.tld'
116
+
117
+ conf_data = f"""
118
+ # Mandatory Configuration
119
+ title: {title}
120
+ url: {url}
121
+ note_template: template/note_template.html
122
+ home_template: template/home_template.html
123
+ feed_template: template/feed_template.xml
124
+ home_path: public
125
+ content_path: content
126
+ resource_path: static
127
+ home_md: content/home.md
128
+ header_md: content/header.md
129
+ footer_md: content/footer.md
130
+
131
+ # Optional Configuration
132
+ site-title: {title}
133
+ css: demo.css
134
+ desc: Write anything that human and machine can understand.
135
+ mail: some@mail.com
136
+ """
137
+ f.write(conf_data)
138
+ print_message("config.yml created.", "success", timestamp=True)
139
+ except Exception as e:
140
+ print_message(
141
+ f"❌ Error creating config.yml: {e}", "error", timestamp=True
142
+ )
143
+
144
+
145
+ def createTemplate(rynzName, template_type, file_content):
146
+ """
147
+ Creates a template file with the provided content.
148
+
149
+ Args:
150
+ rynzName (str): The name of the project directory.
151
+ template_type (str): The name of the template file.
152
+ file_content (str): The content to write to the template file.
153
+ """
154
+ try:
155
+ template_file_path = path.join(rynzName, 'template', template_type)
156
+ with open(template_file_path, 'w') as f:
157
+ f.write(file_content)
158
+ print_message(
159
+ f"{template_type} created.", "success", timestamp=True
160
+ )
161
+ except Exception as e:
162
+ print_message(
163
+ f"❌ Error creating {template_type}: {e}", "error", timestamp=True
164
+ )
165
+
166
+ # Template: Home Page
167
+ home_template_content = """
168
+ <!DOCTYPE html>
169
+ <html lang="en-IN" data-theme="dark">
170
+ <head>
171
+ <meta charset="utf-8">
172
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
173
+ <meta name="viewport" content="width=device-width, initial-scale=1">
174
+ <meta name="description" content=" {{ config.get('desc') }} ">
175
+ <title>{{ config.get('site-title') }}</title>
176
+ <link rel="stylesheet" href="{{ config.get('css') }}">
177
+ <link rel="alternate" type="application/atom+xml" title="Recent blog posts" href="/rss.xml">
178
+ </head>
179
+ <body>
180
+ <header>
181
+ <h1><a href="/">{{ title | lower }}</a></h1>
182
+ {{ header }}
183
+ </header>
184
+ <section>
185
+ {{ article }}
186
+ <ul>
187
+ {% for post in posts %}
188
+ {% if "note" in post.tags %}
189
+ <li>{{ post.date.strftime('%d %m %Y') }} ; <a href="{{ post.url }}">{{ post.title | lower }}</a></li>
190
+ {% endif %}
191
+ {% endfor %}
192
+ {% if nextpage %} <a href="{{ nextpage }}">Older Notes >> </a> {% endif %}
193
+ </ul>
194
+ </section>
195
+ <footer>
196
+ {{ footer }}
197
+ </footer>
198
+ </body>
199
+ </html>
200
+ """
201
+
202
+ # Template: Note Page
203
+ note_template_content = """
204
+ <!DOCTYPE html>
205
+ <html lang="en" data-theme="dark">
206
+ <head>
207
+ <meta charset="utf-8">
208
+ <title>{{ post_title }}</title>
209
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
210
+ <meta name="viewport" content="width=device-width, initial-scale=1">
211
+ <meta name="description" content=" {{ post_subtitle }} ">
212
+ <link rel="stylesheet" href="{{ config.get('css') }}">
213
+ <link rel="alternate" type="application/atom+xml" title="Recent blog posts" href="/rss.xml">
214
+ </head>
215
+ <body>
216
+ <header>
217
+ <h1>{{ post_title }}</h1>
218
+ </header>
219
+ <article>
220
+ {{ article }}
221
+ {% if date %}
222
+ <p># Last updated on <time>{{ date.strftime('%d %b %Y') }}.</time></p>
223
+ {% endif %}
224
+ </article>
225
+ <footer>
226
+ {{ footer }}
227
+ </footer>
228
+ </body>
229
+ </html>
230
+ """
231
+
232
+ # Template: Feed (RSS)
233
+ feed_template_content = """
234
+ <?xml version="1.0" encoding="UTF-8"?>
235
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
236
+ <channel>
237
+ <title>{{ title }}</title>
238
+ <atom:link href="{{ url }}" rel="self" type="application/rss+xml" />
239
+ <link>{{ url }}</link>
240
+ <description>{{ subtitle }}</description>
241
+ <lastBuildDate>{{ last_date.strftime('%a, %d %b %Y %H:%M:%S GMT') }}</lastBuildDate>
242
+ <language>en-IN</language>
243
+ <image>
244
+ <url>{{ config.get('url') }}/{{ config.get('pht') }}</url>
245
+ <title>{{ title }}</title>
246
+ <link>{{ url }}</link>
247
+ <width>32</width>
248
+ <height>32</height>
249
+ </image>
250
+ {% for post in posts %}
251
+ {% if "note" in post.tags %}
252
+ <item>
253
+ <title>{{ post.title }}</title>
254
+ <link>{{ config.get('url') }}{{ post.url }}</link>
255
+ <pubDate>{{ post.date.strftime('%a, %d %b %Y %H:%M:%S GMT') }}</pubDate>
256
+ <guid isPermaLink="false">{{ config.get('url') }}{{ post.url }}</guid>
257
+ <description><![CDATA[{{ post.subtitle }} - {{ post.note }} ]]></description>
258
+ </item>
259
+ {% endif %}
260
+ {% endfor %}
261
+ </channel>
262
+ </rss>
263
+ """
264
+
265
+ # Create content/header/footer/note
266
+ def createContent(rynzName):
267
+ """
268
+ Creates initial content, including `header.md`, `footer.md`, and `home.md`.
269
+
270
+ Args:
271
+ rynzName (str): The name of the project directory.
272
+ """
273
+ try:
274
+ safe_create_directory(path.join(rynzName, 'content', 'note'))
275
+
276
+ with open(path.join(rynzName, 'content', 'header.md'), 'w') as f:
277
+ f.write(
278
+ "* [Home](/)\n* [Other Links](/)\n"
279
+ "* [Edit content/header.md](/)"
280
+ )
281
+ print_message("header.md created.", "success", timestamp=True)
282
+
283
+ with open(path.join(rynzName, 'content', 'footer.md'), 'w') as f:
284
+ f.write(
285
+ "* [Home](/)\n* [RSS](/)\n"
286
+ "* [Edit content/footer.md](/)\n"
287
+ "* powered by [Rynz](https://rynz.de)"
288
+ )
289
+ print_message("footer.md created.", "success", timestamp=True)
290
+
291
+ with open(path.join(rynzName, 'content', 'home.md'), 'w') as f:
292
+ f.write(
293
+ "Welcome to Rynz.\nThis is a sample homepage which can be "
294
+ "edited at /content/home.md"
295
+ )
296
+ print_message("home.md created.", "success", timestamp=True)
297
+
298
+ # Ensure the note directory exists before calling createNote
299
+ note_dir = path.join(rynzName, 'content', 'note')
300
+ safe_create_directory(note_dir)
301
+
302
+ # Generate a sample note
303
+ createNote("notepage1.md", rynzName)
304
+
305
+ except Exception as e:
306
+ print_message(f"❌ Error creating content: {e}", "error", timestamp=True)
307
+
308
+
309
+ def createNote(noteName, rynzName=''):
310
+ """
311
+ Creates a markdown note file with sample content.
312
+
313
+ Args:
314
+ noteName (str): Name of the note file to create.
315
+ rynzName (str): Parent directory for the note file. Defaults to ''.
316
+ """
317
+ try:
318
+ note_dir = path.join(rynzName, 'content', 'note')
319
+
320
+ # Ensure the directory exists
321
+ if not path.exists(note_dir):
322
+ makedirs(note_dir)
323
+
324
+ if not noteName.lower().endswith('.md'):
325
+ noteName += '.md'
326
+
327
+ with open(path.join(note_dir, noteName), 'w') as f:
328
+ conf_data = (
329
+ """---
330
+ title: "Sample Note Page."
331
+ subtitle: "Sample subtitle"
332
+ date: {t}
333
+ tags: [note,generic]
334
+ ---
335
+
336
+ # This is a sample note page which can be edited/renamed at /content/note/{noteName}
337
+ # This is heading 1
338
+
339
+ ## This is heading 2
340
+
341
+ ### This is heading 3
342
+
343
+ #### This is heading 4
344
+
345
+ ##### This is heading 5
346
+
347
+ ###### This is heading 6
348
+
349
+ **Bold**
350
+
351
+ *italic*
352
+
353
+ * list
354
+ * list
355
+
356
+ 1. ordered list
357
+ 2. ordered list
358
+
359
+ [link](/)
360
+
361
+ Sample paragraph is written like this with lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
362
+
363
+ """).format(t=datetime.now().strftime("%Y-%m-%d"), noteName=noteName)
364
+
365
+ f.write(conf_data)
366
+ print_message(
367
+ f"{noteName} is created at {path.join(note_dir, noteName)}",
368
+ "success",
369
+ timestamp=True
370
+ )
371
+
372
+ except Exception as e:
373
+ print_message(
374
+ f"While generating page {noteName}, some issue occurred: {e}",
375
+ "error",
376
+ timestamp=True
377
+ )
378
+
379
+
380
+ def createRynz(rynzName):
381
+ """
382
+ Initializes a Rynz project with all necessary directories and files.
383
+
384
+ Args:
385
+ rynzName (str): The name of the project directory.
386
+ """
387
+ try:
388
+ safe_create_directory(rynzName)
389
+ safe_create_directory(path.join(rynzName, 'template'))
390
+ safe_create_directory(path.join(rynzName, 'static'))
391
+ safe_create_directory(path.join(rynzName, 'content'))
392
+ safe_create_directory(path.join(rynzName, 'content', 'note'))
393
+
394
+ createConfig(rynzName)
395
+ createTemplate(rynzName, 'home_template.html', home_template_content)
396
+ createTemplate(rynzName, 'note_template.html', note_template_content)
397
+ createTemplate(rynzName, 'feed_template.xml', feed_template_content)
398
+ createContent(rynzName)
399
+
400
+ print_message(
401
+ f"Rynz project {rynzName} created successfully!", "success",
402
+ timestamp=True
403
+ )
404
+
405
+ except Exception as e:
406
+ print_message(
407
+ f"❌ Error creating Rynz project {rynzName}: {e}", "error",
408
+ timestamp=True
409
+ )
410
+ sys.exit(1)