Browse Source

New error system, building a tree of posts now works in theory

master
mort 9 years ago
parent
commit
03e5f0a8a9

+ 2
- 2
Makefile View File

@@ -1,10 +1,10 @@
appname = cms

build:
gcc -o $(appname) src/*.c -std=c99
gcc -o3 -o $(appname) src/*.c -std=c99

debug:
gcc -o $(appname) src/*.c -std=c99 -DDEBUG=1
gcc -o3 -o $(appname) src/*.c -std=c99 -DDEBUG=1

install:
mv $(appname) /usr/bin/$(appname)

+ 3
- 0
resources/pages/0001-example/0001-example View File

@@ -0,0 +1,3 @@
Example Post

<p>This post is an example.</p>

+ 1
- 0
resources/pages/0001-example/page View File

@@ -0,0 +1 @@
Example Page

+ 0
- 7
resources/placeholder View File

@@ -1,7 +0,0 @@
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.

+ 0
- 8
resources/subdirtest/placeholder View File

@@ -1,8 +0,0 @@
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.
This is placeholder text.

+ 4
- 0
resources/theme/html/article.html View File

@@ -0,0 +1,4 @@
<article>
<header>
<h1>{title}</h1>
</article>

+ 15
- 0
resources/theme/html/index.html View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/_style.css">
<title>{title}</title>
</head>
<body>
{menu}

<div id="articles">
{articles}
</div>
</body>
</html>

+ 8
- 0
resources/theme/html/menu-page.html View File

@@ -0,0 +1,8 @@
<div class="page">
<span class="name">
{name}
</span>
<div class="sub">
{sub}
</div>
</div>

+ 5
- 0
resources/theme/html/menu.html View File

@@ -0,0 +1,5 @@
<header class="menu">
<nav class="menu-nav">
{pages}
</nav>
</header>

+ 92
- 0
src/cms_build.c View File

@@ -0,0 +1,92 @@
#include "cms_build.h"
#include "cms_err.h"
#include "cms_files.h"
#include "cms_util.h"
#include "cms_page.h"
#include "cms_post.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

cms_err* cms_build_make_tree(cms_page* root, char* path, char* dirname)
{
DIR* dp = opendir(path);
if (dp == NULL)
return cms_err_from_std_err(errno);

struct dirent* ep;

struct stat* st = malloc(sizeof(struct stat));
if (st == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

while (ep = readdir(dp))
{
if (ep->d_name[0] == '.')
continue;

char* entpath = cms_util_path_join(path, ep->d_name);
stat(entpath, st);

//Entry is directory, recurse
if (S_ISDIR(st->st_mode))
{
cms_page* sub = cms_page_create();
if (sub == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

cms_err* err;
err = cms_build_make_tree(sub, entpath, ep->d_name);
if (err)
return err;

err = cms_page_add_sub(root, sub);
if (err)
return err;
}

//Entry is the file which contains metadata
//about a page, parse
else if (strcmp(ep->d_name, CMS_FILE_PAGE) == 0)
{
char* content = cms_util_file_read(entpath);
if (content == NULL)
return cms_err_create(CMS_ERR_FILEREAD, entpath);

cms_err* err = cms_page_parse(root, content, dirname);
free(content);
if (err)
return err;
}

//Entry is a post, read it and add to page
else
{
cms_post* post = cms_post_create();

char* content = cms_util_file_read(entpath);
if (content == NULL)
return cms_err_create(CMS_ERR_FILEREAD, entpath);

cms_err* err;
err = cms_post_parse(post, content, ep->d_name);
free(content);
if (err)
return err;

err = cms_page_add_post(root, post);
if (err)
return err;
}

free(entpath);
}

closedir(dp);
free(st);

return cms_err_create(CMS_ERR_NONE, NULL);
}

+ 9
- 0
src/cms_build.h View File

@@ -0,0 +1,9 @@
#ifndef CMS_BUILD_H
#define CMS_BUILD_H

#include "cms_err.h"
#include "cms_page.h"

cms_err* cms_build_make_tree(cms_page* root, char* path, char* dirname);

#endif

+ 67
- 16
src/cms_err.c View File

@@ -1,5 +1,6 @@
#include "cms_err.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

@@ -7,9 +8,9 @@
#include <error.h>
#endif

static char* get_error_message(cms_err err)
static char* get_error_message(cms_err* err)
{
switch (err)
switch (err->code)
{
case CMS_ERR_NONE:
return "";
@@ -27,6 +28,8 @@ static char* get_error_message(cms_err err)
return "Not a directory.";
case CMS_ERR_FILEEXISTS:
return "File exists.";
case CMS_ERR_FILEREAD:
return "Failed to read file.";
case CMS_ERR_DIREXISTS:
return "Directory exists.";
case CMS_ERR_PERM:
@@ -38,24 +41,72 @@ static char* get_error_message(cms_err err)
}
}

void _cms_err_panic(cms_err err, char* msg, const char* file, int line)
cms_err* _cms_err_create(cms_err_code code, char* msg, const char* file, int line)
{
//We want to just return NULL if there is no error
if (code == CMS_ERR_NONE)
return NULL;

cms_err* err = malloc(sizeof(cms_err));
if (err == NULL)
{
fprintf(stderr, "Failed to allocate memory.\n");
exit(1);
}

err->code = code;

if (msg == NULL)
{
err->msg = NULL;
}
else
{
size_t msglen = strlen(msg) + 1;
err->msg = malloc(msglen * sizeof(char));
if (err->msg == NULL)
{
fprintf(stderr, "Failed to allocate memory.\n");
exit(1);
}

memcpy(err->msg, msg, msglen * sizeof(char));
}

err->file = file;
err->line = line;

return err;
}

void cms_err_free(cms_err* err)
{
//The error can be null, in which case, we don't want to do anything
if (err == NULL)
return;

free(err->msg);
free(err);
}

void cms_err_panic(cms_err* err)
{
if (!err)
return;

#ifdef DEBUG
fprintf(stderr, "File %s, line %i:\n\t", file, line);
fprintf(stderr, "File %s, line %i:\n\t", err->file, err->line);
#endif

if (msg == NULL)
if (err->msg == NULL)
fprintf(stderr, "Error: %s\n", get_error_message(err));
else
fprintf(stderr, "Error: %s: %s\n", msg, get_error_message(err));
fprintf(stderr, "Error: %s: %s\n", err->msg, get_error_message(err));

exit(1);
}

cms_err cms_err_from_std_err(int err)
cms_err* cms_err_from_std_err(int err)
{
#ifdef DEBUG
error(0, err, "converting to cms_err");
@@ -64,22 +115,22 @@ cms_err cms_err_from_std_err(int err)
switch (err)
{
case EACCES:
return CMS_ERR_PERM;
return cms_err_create(CMS_ERR_PERM, NULL);
case EEXIST:
return CMS_ERR_FILEEXISTS;
return cms_err_create(CMS_ERR_FILEEXISTS, NULL);
case EFAULT:
return CMS_ERR_PERM;
return cms_err_create(CMS_ERR_PERM, NULL);
case EISDIR:
return CMS_ERR_NOTFILE;
return cms_err_create(CMS_ERR_NOTFILE, NULL);
case ENOENT:
return CMS_ERR_NOENT;
return cms_err_create(CMS_ERR_NOENT, NULL);
case ENOMEM:
return CMS_ERR_ALLOC;
return cms_err_create(CMS_ERR_ALLOC, NULL);
case ENOTDIR:
return CMS_ERR_NOTDIR;
return cms_err_create(CMS_ERR_NOTDIR, NULL);
case EROFS:
return CMS_ERR_PERM;
return cms_err_create(CMS_ERR_PERM, NULL);
default:
return CMS_ERR_UNKNOWN;
return cms_err_create(CMS_ERR_UNKNOWN, NULL);
}
}

+ 19
- 5
src/cms_err.h View File

@@ -1,9 +1,9 @@
#ifndef CMS_ERR_H
#define CMS_ERR_H

typedef enum cms_err
typedef enum cms_err_code
{
CMS_ERR_NONE,
CMS_ERR_NONE = 0,
CMS_ERR_UNKNOWN,
CMS_ERR_ALLOC,
CMS_ERR_PARSE,
@@ -11,16 +11,30 @@ typedef enum cms_err
CMS_ERR_NOTFILE,
CMS_ERR_NOTDIR,
CMS_ERR_FILEEXISTS,
CMS_ERR_FILEREAD,
CMS_ERR_DIREXISTS,
CMS_ERR_PERM,
CMS_ERR_INITED,
CMS_ERR_NOTINITED
} cms_err_code;

typedef struct cms_err
{
cms_err_code code;
char* msg;
const char* file;
int line;
} cms_err;

void _cms_err_panic(cms_err err, char* msg, const char* file, int line);
#define cms_err_panic(err, msg) _cms_err_panic(err, msg, __FILE__, __LINE__)
//Create an error. Returns NULL if the error code is CMS_ERR_NONE.
cms_err* _cms_err_create(cms_err_code code, char* msg, const char* file, int line);
#define cms_err_create(code, msg) _cms_err_create(code, msg, __FILE__, __LINE__)

void cms_err_free(cms_err* err);

void cms_err_panic(cms_err* err);

cms_err cms_err_from_std_err(int err);
cms_err* cms_err_from_std_err(int err);

#endif


+ 7
- 0
src/cms_files.h View File

@@ -7,4 +7,11 @@
//File which tells us if a directory is already initiated.
#define CMS_FILE_INITED ".cmsinited"

//The name of the dir which specifies the title and
//other metadata of a page
#define CMS_FILE_PAGE "page"

//Directory containing the user's resources.
#define CMS_FILE_ROOT "pages"

#endif

+ 37
- 28
src/cms_page.c View File

@@ -7,28 +7,33 @@
#include <sys/types.h>
#include <errno.h>

#define PREFIX_LENGTH 5

cms_page* cms_page_create()
{
cms_page* page = malloc(sizeof(cms_page));
if (page == NULL)
cms_err_panic(CMS_ERR_ALLOC, NULL);
page->numposts = 0;
page->numsubs = 0;
page->posts = NULL;
page->subs = NULL;
return page;
}

cms_err cms_page_parse(cms_page* page, char* str)
cms_err* cms_page_parse(cms_page* page, char* str, char* slugstr)
{
//Adding 1 because strlen() returns the length without \0
size_t len = strlen(str) + 1;

page->_str = malloc(len * sizeof(char));

if (page->_str == 0)
return CMS_ERR_ALLOC;
if (page->_str == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

memcpy(page->_str, str, len * sizeof(char));

//The page's title will be the first line.
page->title = page->_str;

size_t line = 0;
//Replace newlines with \0
for (size_t i = 0; i < len; ++i)
{
char c = str[i];
@@ -36,45 +41,49 @@ cms_err cms_page_parse(cms_page* page, char* str)
switch (c)
{
case '\n':
line += 1;
if (line == 1)
page->slug = (page->_str + i + 1);

case '\r':
page->_str[i] = '\0';
break;
}

if (line == 2)
break;
}

if (line == 2)
return CMS_ERR_NONE;
else
return CMS_ERR_PARSE;
//Strip out the leading "xxxx-" from slugstr (the filename)
//to get the real slug
size_t slugstrlen = strlen(slugstr);
page->slug = malloc((slugstrlen + 1 - PREFIX_LENGTH) * sizeof(char));
if (page->slug == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

memcpy(page->slug, slugstr + PREFIX_LENGTH, (slugstrlen - PREFIX_LENGTH));

//Add \0 to the end of the string
page->slug[slugstrlen - PREFIX_LENGTH] = '\0';

return cms_err_create(CMS_ERR_NONE, NULL);
}

cms_err cms_page_add_post(cms_page* page, cms_post* post)
cms_err* cms_page_add_post(cms_page* page, cms_post* post)
{
page->numposts += 1;
page->posts = realloc(page->posts, page->numposts * sizeof(cms_post));

page->posts = realloc(page->posts, page->numposts * sizeof(cms_post));
if (page->posts == NULL)
return CMS_ERR_ALLOC;
return cms_err_create(CMS_ERR_ALLOC, NULL);

page->posts[page->numposts - 1] = *post;

return CMS_ERR_NONE;
return cms_err_create(CMS_ERR_NONE, NULL);
}

cms_err cms_page_create_tree(cms_page* root, const char* path)
cms_err* cms_page_add_sub(cms_page* page, cms_page* sub)
{
DIR* dp = opendir(path);
if (dp == NULL)
return cms_err_from_std_err(errno);
page->numsubs += 1;
page->subs = realloc(page->subs, page->numsubs * sizeof(cms_page));
if (page->subs == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

closedir(dp);
page->subs[page->numsubs - 1] = *sub;

return CMS_ERR_NONE;
return cms_err_create(CMS_ERR_NONE, NULL);
}


+ 6
- 14
src/cms_page.h View File

@@ -1,8 +1,8 @@
#ifndef CMS_PAGE_H
#define CMS_PAGE_H

#include "cms_post.h"
#include "cms_err.h"
#include "cms_post.h"
#include <stddef.h>

typedef struct cms_page
@@ -12,24 +12,16 @@ typedef struct cms_page
char* slug;
cms_post* posts;
size_t numposts;
struct cms_page* subs;
size_t numsubs;
} cms_page;

cms_page* cms_page_create();

// 1: allloc error
// 2: parse error
// 3: unknown
cms_err cms_page_parse(cms_page* page, char* str);
cms_err* cms_page_parse(cms_page* page, char* str, char* slugstr);

//1: alloc error
//2: unknown
cms_err cms_page_add_post(cms_page* page, cms_post* post);
cms_err* cms_page_add_post(cms_page* page, cms_post* post);

//1: alloc error
//2: permission denied
//3: dir doesn't exist
//4: not a directory
//5: unknown
cms_err cms_page_create_tree(cms_page* root, const char* path);
cms_err* cms_page_add_sub(cms_page* page, cms_page* sub);

#endif

+ 30
- 9
src/cms_post.c View File

@@ -1,22 +1,29 @@
#include "cms_post.h"
#include "cms_err.h"
#include <stdlib.h>
#include <stddef.h>
#include <string.h>

#define PREFIX_LENGTH 5

cms_post* cms_post_create()
{
cms_post* post = malloc(sizeof(cms_post));
return post;
}

int cms_post_parse(cms_post* post, char* str)
cms_err* cms_post_parse(cms_post* post, char* str, char* slugstr)
{
//Adding 1 because strlen() returns the length without \0
size_t len = strlen(str) + 1;

post->_str = malloc(len * sizeof(char));
if (post->_str == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

memcpy(post->_str, str, len * sizeof(char));

//The post's title will be the first line.
post->title = post->_str;

size_t line = 0;
@@ -28,22 +35,36 @@ int cms_post_parse(cms_post* post, char* str)
{
case '\n':
line += 1;
if (line == 1)
post->slug = (post->_str + i + 1);
else if (line == 2)
post->markdown = (post->_str + i + 1);
if (line == 2)
{
post->html = (post->_str + i + 1);
}

//falls through

case '\r':
post->_str[i] = '\0';
break;
}

if (line == 3)
if (line == 2)
break;
}

if (line == 3)
return 0;
//Strip out the leading "xxxx-" from slugstr (the filename)
//to get the real slug
size_t slugstrlen = strlen(slugstr);
post->slug = malloc((slugstrlen + 1 - PREFIX_LENGTH) * sizeof(char));
if (post->slug == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

memcpy(post->slug, slugstr + PREFIX_LENGTH, (slugstrlen - PREFIX_LENGTH));

//Add \0 to the end of the string
post->slug[slugstrlen - PREFIX_LENGTH] = '\0';

if (line >= 2)
return cms_err_create(CMS_ERR_NONE, NULL);
else
return 1;
return cms_err_create(CMS_ERR_PARSE, slugstr);
}

+ 4
- 2
src/cms_post.h View File

@@ -1,16 +1,18 @@
#ifndef CMS_POST_H
#define CMS_POST_H

#include "cms_err.h"

typedef struct cms_post
{
char* _str;
char* title;
char* slug;
char* markdown;
char* html;
} cms_post;

cms_post* cms_post_create();

int cms_post_parse(cms_post* post, char* str);
cms_err* cms_post_parse(cms_post* post, char* str, char* slugstr);

#endif

+ 51
- 22
src/cms_util.c View File

@@ -1,3 +1,5 @@
#include "cms_util.h"
#include "cms_err.h"
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
@@ -9,8 +11,6 @@
#include <sys/types.h>
#include <errno.h>

#include "cms_util.h"

int cms_util_file_exists(char* fname)
{
int f = open(fname, O_RDONLY);
@@ -26,7 +26,7 @@ int cms_util_file_exists(char* fname)
return 1;
}

cms_err cms_util_file_create(char* fname)
cms_err* cms_util_file_create(char* fname)
{
int f = open(fname, O_CREAT, 0777);
close(f);
@@ -34,10 +34,10 @@ cms_err cms_util_file_create(char* fname)
if (f == -1)
return cms_err_from_std_err(errno);
else
return CMS_ERR_NONE;
return cms_err_create(CMS_ERR_NONE, NULL);
}

cms_err cms_util_file_copy(char* fname1, char* fname2)
cms_err* cms_util_file_copy(char* fname1, char* fname2)
{
int f1 = open(fname1, O_RDONLY);
if (f1 == -1)
@@ -47,29 +47,54 @@ cms_err cms_util_file_copy(char* fname1, char* fname2)
if (f1 == -1)
return cms_err_from_std_err(errno);

struct stat* s = malloc(sizeof(struct stat));
if (s == NULL)
return CMS_ERR_ALLOC;
struct stat* st = malloc(sizeof(struct stat));
if (st == NULL)
return cms_err_create(CMS_ERR_ALLOC, NULL);

fstat(f1, s);
fstat(f1, st);

void* buf = malloc(s->st_size);
void* buf = malloc(st->st_size);
if (buf == NULL)
return CMS_ERR_ALLOC;
return cms_err_create(CMS_ERR_ALLOC, NULL);

read(f1, buf, s->st_size);
write(f2, buf, s->st_size);
read(f1, buf, st->st_size);
write(f2, buf, st->st_size);

close(f1);
close(f2);

free(s);
free(st);
free(buf);

return CMS_ERR_NONE;
return cms_err_create(CMS_ERR_NONE, NULL);
}

char* cms_util_file_read(char* fname)
{
int file = open(fname, O_RDONLY);
if (file == -1)
return NULL;

struct stat* st = malloc(sizeof(struct stat));
if (st == NULL)
return NULL;

fstat(file, st);

char* buf = malloc(st->st_size + 1);
if (buf == NULL)
return NULL;

read(file, buf, st->st_size + 1);
buf[st->st_size] = '\0';

close(file);
free(st);

return buf;
}

cms_err cms_util_dir_copy_recursive(char* dir1, char* dir2)
cms_err* cms_util_dir_copy_recursive(char* dir1, char* dir2)
{
if (mkdir(dir2, 0777) == -1 && errno != EEXIST)
return cms_err_from_std_err(errno);
@@ -82,7 +107,7 @@ cms_err cms_util_dir_copy_recursive(char* dir1, char* dir2)

struct stat* st = malloc(sizeof(struct stat));
if (st == NULL)
return CMS_ERR_ALLOC;
return cms_err_create(CMS_ERR_ALLOC, NULL);

while (ep = readdir(dp))
{
@@ -97,13 +122,17 @@ cms_err cms_util_dir_copy_recursive(char* dir1, char* dir2)

if (S_ISDIR(st->st_mode))
{
cms_err err = cms_util_dir_copy_recursive(path1, path2);
cms_err* err = cms_util_dir_copy_recursive(path1, path2);
free(path1);
free(path2);
if (err)
return err;
}
else
{
cms_err err = cms_util_file_copy(path1, path2);
cms_err* err = cms_util_file_copy(path1, path2);
free(path1);
free(path2);
if (err)
return err;
}
@@ -112,7 +141,7 @@ cms_err cms_util_dir_copy_recursive(char* dir1, char* dir2)
closedir(dp);
free(st);

return CMS_ERR_NONE;
return cms_err_create(CMS_ERR_NONE, NULL);
}

char* cms_util_path_join(char* str1, char* str2)
@@ -125,9 +154,9 @@ char* cms_util_path_join(char* str1, char* str2)
len1 -= 1;
}

char* path = malloc((len1 + len2 + 1) * sizeof(char));
char* path = malloc((len1 + len2 + 2) * sizeof(char));
if (path == NULL)
cms_err_panic(CMS_ERR_ALLOC, "");
cms_err_panic(cms_err_create(CMS_ERR_ALLOC, NULL));

if (path == NULL)
return NULL;

+ 6
- 3
src/cms_util.h View File

@@ -7,13 +7,16 @@
int cms_util_file_exists(char* fname);

//Create a file
cms_err cms_util_file_create(char* fname);
cms_err* cms_util_file_create(char* fname);

//Copy a file
cms_err cms_util_file_copy(char* fname1, char* fname2);
cms_err* cms_util_file_copy(char* fname1, char* fname2);

//Read a file into a string
char* cms_util_file_read(char* fname);

//Recursively copy a directory
cms_err cms_util_dir_copy_recursive(char* dir1, char* dir2);
cms_err* cms_util_dir_copy_recursive(char* dir1, char* dir2);

//Join together two paths
char* cms_util_path_join(char* str1, char* str2);

+ 33
- 6
src/main.c View File

@@ -1,15 +1,18 @@
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#include "cms_util.h"
#include "cms_err.h"
#include "cms_files.h"
#include "cms_page.h"
#include "cms_build.h"

int main(int argc, char** argv)
{
if (argc < 2)
{
fprintf(stderr, "Usage: %s <init|deinit|build>\n", argv[0]);
fprintf(stderr, "Usage: %s <init|build>\n", argv[0]);
return 1;
}

@@ -27,21 +30,23 @@ int main(int argc, char** argv)
//Get the path of .cmsinited, which tells us
//whether or not the directory is already inited
char* initedPath = cms_util_path_join(dirname, CMS_FILE_INITED);
if (initedPath == NULL)
cms_err_panic(cms_err_create(CMS_ERR_ALLOC, NULL));

//Panic if the directory is already initiated
if (cms_util_file_exists(initedPath))
cms_err_panic(CMS_ERR_INITED, NULL);
cms_err_panic(cms_err_create(CMS_ERR_INITED, NULL));

//Copy files from resources
cms_err err;
cms_err* err;
err = cms_util_dir_copy_recursive(CMS_FILE_RESOURCES, dirname);
if (err)
cms_err_panic(err, dirname);
cms_err_panic(err);

//Create .cmsinited file
err = cms_util_file_create(initedPath);
if (err)
cms_err_panic(err, initedPath);
cms_err_panic(err);
}

//Build
@@ -61,6 +66,28 @@ int main(int argc, char** argv)

//Panic if the directory isn't initiated
if (!cms_util_file_exists(initedPath))
cms_err_panic(CMS_ERR_NOTINITED, NULL);
cms_err_panic(cms_err_create(CMS_ERR_NOTINITED, NULL));

cms_page* root = cms_page_create();
if (root == NULL)
cms_err_panic(cms_err_create(CMS_ERR_ALLOC, NULL));

char* path = cms_util_path_join(dirname, CMS_FILE_ROOT);
if (path == NULL)
cms_err_panic(cms_err_create(CMS_ERR_ALLOC, NULL));

//Build tree of pages and posts
cms_err* err;
err = cms_build_make_tree(root, path, NULL);
free(path);
if (err)
cms_err_panic(err);
}

//Nothing, print usage
else
{
fprintf(stderr, "Usage: %s <init|build>\n", argv[0]);
return 1;
}
}

Loading…
Cancel
Save