slweb

Једноставни генератор статичких веб страна
Дневник | Датотеке | Референце | ПРОЧИТАЈМЕ | ЛИЦЕНЦА

чување 6d10baa549a69a3b307ddc420d7c7637a6864894
родитељ 500f6e677b5d97a91302bb7e15de2e6373a5827f
Аутор: Страхиња Радић <contact@strahinja.org>
Датум:   Wed, 13 Jan 2021 16:27:50 +0100

Finished the initial work on csv templating directive; changed the syntax of incdir directive, adding dirname as first parameter

Signed-off-by: Страхиња Радић <contact@strahinja.org>

Diffstat:
MREADME | 9+++------
MTODO | 7++++---
Mdefs.h | 8+++++++-
Mindex.html | 13++++---------
Mindex.slw | 11+++--------
Mslweb.1.in | 40++++++++++++++++++++--------------------
Mslweb.c | 264++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
измењених датотека: 7, додавања: 283(+), брисања: 69(-)

diff --git a/README b/README @@ -20,18 +20,15 @@ git(1) is, aside from cloning the repository, required to use the directive $ git clone https://github.com/Strahinja/slweb.git $ cd slweb +$ su Then, if you have djb redo: -$ doas redo install +# redo install if you don't: -$ doas ./do install - - or with sudo(1) instead of doas(1): - -$ sudo ./do install +# ./do install Examples diff --git a/TODO b/TODO @@ -1,7 +1,7 @@ TODO ==== - [~] Add csv templating directive + [x] Add csv templating directive [x] Move fprintf(output...) to a separate function @@ -9,9 +9,10 @@ "capture blocks" - no need to do that for all of them, only the last one - [ ] Open CSV, for each row in CSV skipping the first one + [x] Open CSV, for each row in CSV skipping the first one output the current capture block substituting the variable - escapes $1..$9 (keeping it simple with 1-digit) with row fields + escapes $1..$9 (keeping it simple with 1-digit) with row fields; + don't skip header line [x] Update documentation diff --git a/defs.h b/defs.h @@ -38,7 +38,7 @@ #include <uniwidth.h> #define PROGRAMNAME "slweb" -#define VERSION "0.3.3-beta" +#define VERSION "0.3.4" #define COPYRIGHTYEAR "2020, 2021" #define BUFSIZE 1024 @@ -75,6 +75,7 @@ typedef struct #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-const-variable" const UBYTE MAX_HEADING_LEVEL = 4; +const UBYTE MAX_CSV_REGISTERS = 9; const ULONG ST_NONE = 0; const ULONG ST_YAML = 1; @@ -95,6 +96,11 @@ const ULONG ST_IMAGE = 1 << 14; const ULONG ST_IMAGE_SECOND_ARG = 1 << 15; const ULONG ST_MACRO_BODY = 1 << 16; const ULONG ST_CSV_BODY = 1 << 17; + +const UBYTE ST_CS_NONE = 0; +const UBYTE ST_CS_HEADER = 1; +const UBYTE ST_CS_REGISTER = 2; +const UBYTE ST_CS_QUOTE = 3; #pragma GCC diagnostic pop #endif /* __DEFS_H */ diff --git a/index.html b/index.html @@ -28,24 +28,19 @@ the directive <code>{git-log}</code>.</p> <pre> $ git clone https://github.com/Strahinja/slweb.git $ cd slweb +$ su </pre> <p>Then, if you have <a href="https://github.com/apenwarr/redo">apenwarr/redo</a>:</p> <pre> -$ doas redo install +# redo install </pre> <p>if you don't:</p> <pre> -$ doas ./do install -</pre> - -<p>or with <strong>sudo</strong>(1):</p> - -<pre> -$ sudo ./do install +# ./do install </pre> <h2>Examples</h2> @@ -128,7 +123,7 @@ this program. If not, see &lt;<a href="https://www.gnu.org/licenses">https://www <div id="git-log"> Previous commit: -index.slw 61d9851 2021-01-11 22:19:12 +0100 (Страхиња Радић) (HEAD -> master, origin/master, origin/HEAD) +index.slw cb25093 2021-01-13 16:42:37 +0100 (Страхиња Радић) (HEAD -> master, tag: v0.3.4) </div><!--git-log--> diff --git a/index.slw b/index.slw @@ -22,24 +22,19 @@ the directive `{git-log}`. ``` $ git clone https://github.com/Strahinja/slweb.git $ cd slweb +$ su ``` Then, if you have [apenwarr/redo][5]: ``` -$ doas redo install +# redo install ``` if you don't: ``` -$ doas ./do install -``` - -or with **sudo**(1): - -``` -$ sudo ./do install +# ./do install ``` ## Examples diff --git a/slweb.1.in b/slweb.1.in @@ -61,9 +61,8 @@ Only add the contents of the \fC<body>\fP tag, skipping \fC<html>\fP and .TQ .BI \-\-basedir " directory" .br -Set the base directory as a reference point to normalize paths in includes and -.I incdir -directive (the argument to +Set the base directory as a reference point to normalize paths in includes (the +argument to .B --relative-to option for the .BR realpath (1) @@ -180,12 +179,12 @@ Directive \fC{csv "csvfile"}{/csv}\fP marks a template. Whatever is between .B slweb and collected as a template. Then, file \fIcsvfile.csv\fP will be read and for each of its lines the collected template will be output, substituting each -occurence of \fC$\f[CI]n\fR (where \fIn\fP is between 1 and 9, inclusive) with -the corresponding +occurence of a register mark \fC$\f[CI]n\fR (where \fIn\fP is between 1 and 9, +inclusive) with the corresponding .SM CSV field of the read line. .SM CSV -file needs to use semicolons (;) as delimiters (or set +file needs to use semicolons (;) or commas (,) as delimiters (or set .B csv-delimiter as a .SM YAML @@ -193,7 +192,8 @@ variable in the calling .I .slw file) and double quotation marks (") as field boundaries. First line in the .SM CSV -file is skipped as a header line. +file is parsed as a header and associated with register marks \fC$#1\fP to +\fC$#9\fP. The symbol $ is represented as \fC$$\fP. . .IP "" 8 For example, having a file \fIsales.csv\fP: @@ -201,7 +201,7 @@ For example, having a file \fIsales.csv\fP: .CDS 8 "Item";"Q1 2020";"Q2 2020";"Q3 2020";"Q4 2020" "Toothpick";"15.2";"12";"16.4";"10" -"Pot";"1.2";"2";"5";"3.5" +"Teapot";"1.2";"2";"5";"3.5" .CDE . .IP "" 4 @@ -210,8 +210,8 @@ We can write .CDS 8 {csv "sales"} {.sales-segment} -Item {q}$1{/q} sales by quarter for the last year in -thousands of units sold were as follows: +$#1 {q}$1{/q} sales by quarter for the last year in +thousands of units sold (for $$) were as follows: {.q1}$2{/.q1} {.q2}$3{/.q2} {.q3}$4{/.q3} {.q4}$5{/.q4} {/.sales-segment} @@ -224,13 +224,13 @@ to get: .CDS 8 <div class="sales-segment"> Item <q>Toothpick</q> sales by quarter for the last year in -thousands of units sold were as follows: +thousands of units sold (for $) were as follows: <div class="q1">15.2</div> <div class="q2">12</div> <div class="q3">16.4</div> <div class="q4">10</div> </div> <div class="sales-segment"> -Item <q>Pot</q> sales by quarter for the last year in -thousands of units sold were as follows: +Item <q>Teapot</q> sales by quarter for the last year in +thousands of units sold (for $) were as follows: <div class="q1">1.2</div> <div class="q2">2</div> <div class="q3">5</div> <div class="q4">3.5</div> </div> @@ -294,7 +294,7 @@ nor can they contain macro calls, and doing so will produce an error. . .IP \[bu] .BR "Subdirectory inclusion (blogging directive)" . -The command \fC{incdir \fInum\fC =\fImacroname\fC}\fP +The command \fC{incdir "\fIdirname\rC" \fInum\fC =\fImacroname\fC}\fP .RI ( num and .I macroname @@ -308,8 +308,8 @@ the directive. . .IP \n+[list]. For every subdirectory of -.IR basedir , -up to +.IR dirname , +relative to current directory and up to .I num (if present) or 5 (if omitted) total subdirectories, a \fC<li>\fP tag will be inserted into the \fCul\fP tag. @@ -326,7 +326,7 @@ macro .I macroname (for example, this can be used to include custom .SM SVG -files as arrows). +markup as arrows). . .IP \n+[list]. After the \fC<summary>\fP tag a \fC<div>\fP tag will be inserted into @@ -350,9 +350,9 @@ about the current commit would be impossible). . .IP \[bu] 4 .BR csv-delimiter . -Changes the delimiter used for parsing of +Changes the delimiter used for .I .csv -files. By default, this will be a semicolon (;). See +file parsing. By default, this will be a semicolon\~(;). See .BR "CSV templating" . . .IP \[bu] @@ -395,7 +395,7 @@ attribute of permalinks generated by the directive. This variable is more useful in individual .I .slw files inside the subdirectories of the -.I basedir +.I dirname provided to the .I incdir directive. diff --git a/slweb.c b/slweb.c @@ -24,6 +24,7 @@ static size_t colno = 1; static char* input_filename = NULL; static char* input_dirname = NULL; static char* basedir = NULL; +static char* incdir = NULL; static KeyValue* vars = NULL; static KeyValue* pvars = NULL; static size_t vars_count = 0; @@ -35,6 +36,7 @@ static KeyValue* plinks = NULL; static size_t links_count = 0; static uint8_t* csv_template = NULL; static size_t csv_template_len = 0; +static char* csv_filename = NULL; static ULONG state = ST_NONE; @@ -72,7 +74,8 @@ error(int code, uint8_t* fmt, ...) va_start(args, fmt); u8_vsnprintf(buf, sizeof(buf), (const char*)fmt, args); va_end(args); - fprintf(stderr, "%s:%lu:%lu: %s", PROGRAMNAME, lineno, colno, buf); + fprintf(stderr, "%s:%s:%lu:%lu: %s", PROGRAMNAME, input_filename, + lineno, colno, buf); return code; } @@ -318,6 +321,74 @@ process_git_log(FILE* output) } int +print_csv_row(FILE* output, uint8_t** csv_header, uint8_t** csv_register) +{ + uint8_t* pcsv_template = csv_template; + UBYTE csv_state = ST_CS_NONE; + UBYTE num = 0; + + while (*pcsv_template) + { + switch (*pcsv_template) + { + case '$': + if (csv_state & ST_CS_REGISTER) + { + fprintf(output, "$"); + csv_state &= ST_CS_REGISTER; + } + else + csv_state |= ST_CS_REGISTER; + pcsv_template++; + break; + case '#': + if (csv_state & ST_CS_REGISTER) + { + if (csv_state & ST_CS_HEADER) + { + error(1, (uint8_t*)"csv: Invalid header register mark\n"); + csv_state &= ~(ST_CS_REGISTER | ST_CS_HEADER); + } + else + csv_state |= ST_CS_HEADER; + } + else + fprintf(output, "#"); + pcsv_template++; + break; + case '1': case '2': case '3': case '4': case '5': + case '6': case '7': case '8': case '9': + if (csv_state & ST_CS_HEADER) + { + num = *pcsv_template - '0'; + fprintf(output, "%s", csv_header[num-1]); + csv_state &= ~(ST_CS_REGISTER | ST_CS_HEADER); + } + else if (csv_state & ST_CS_REGISTER) + { + num = *pcsv_template - '0'; + fprintf(output, "%s", csv_register[num-1]); + csv_state &= ~ST_CS_REGISTER; + } + else + fprintf(output, "%c", *pcsv_template); + pcsv_template++; + break; + default: + if (csv_state & ST_CS_REGISTER) + { + error(1, (uint8_t*)"csv: Invalid register mark\n"); + csv_state &= ~ST_CS_REGISTER; + } + else + fprintf(output, "%c", *pcsv_template); + pcsv_template++; + } + } + return 0; +} + +int process_csv(uint8_t* token, FILE* output, BOOL read_yaml_macros_and_links, BOOL end_tag) { @@ -327,10 +398,137 @@ process_csv(uint8_t* token, FILE* output, BOOL read_yaml_macros_and_links, if (end_tag) { state &= ~ST_CSV_BODY; - /* - *fprintf(stderr, "%ld:%ld:csv: /end\n", lineno, colno); - *fprintf(stderr, "csv_template = {%s}\n", csv_template); - */ + + FILE* csv = fopen(csv_filename, "rt"); + size_t csv_lineno = 0; + uint8_t* bufline = NULL; + uint8_t* pbufline = NULL; + uint8_t* token = NULL; + uint8_t* ptoken = NULL; + UBYTE csv_state = ST_CS_NONE; + uint8_t* csv_header[MAX_CSV_REGISTERS]; + UBYTE current_header = 0; + uint8_t* csv_register[MAX_CSV_REGISTERS]; + UBYTE current_register = 0; + uint8_t* csv_delimiter = get_value(vars, vars_count, (uint8_t*)"csv-delimiter", NULL); + + if (!csv) + exit(error(ENOENT, (uint8_t*)"csv: No such file: %s\n", csv_filename)); + + CALLOC(bufline, uint8_t, BUFSIZE) + CALLOC(token, uint8_t, BUFSIZE) + for (UBYTE i = 0; i < MAX_CSV_REGISTERS; i++) + CALLOC(csv_header[i], uint8_t, BUFSIZE) + for (UBYTE i = 0; i < MAX_CSV_REGISTERS; i++) + CALLOC(csv_register[i], uint8_t, BUFSIZE) + + while (!feof(csv)) + { + uint8_t* eol = NULL; + if (!fgets((char*)bufline, BUFSIZE-1, csv)) + break; + eol = u8_strchr(bufline, (ucs4_t)'\n'); + if (eol) + *eol = '\0'; + + pbufline = bufline; + *token = '\0'; + ptoken = token; + current_register = 0; + while (*pbufline) + { + switch (*pbufline) + { + case '"': + if (csv_state & ST_CS_QUOTE) + csv_state &= ~ST_CS_QUOTE; + else + csv_state |= ST_CS_QUOTE; + pbufline++; + break; + case ';': + case ',': + if (csv_state & ST_CS_QUOTE) + *ptoken++ = *pbufline; + else + { + *ptoken = '\0'; + if (csv_lineno > 0) + { + if (current_register < MAX_CSV_REGISTERS) + u8_strncpy(csv_register[current_register++], token, + u8_strlen(token)+1); + } + else + { + if (current_header < MAX_CSV_REGISTERS) + u8_strncpy(csv_header[current_header++], token, + u8_strlen(token)+1); + } + *token = '\0'; + ptoken = token; + } + pbufline++; + break; + default: + if (csv_state & ST_CS_QUOTE) + *ptoken++ = *pbufline++; + else + { + if (csv_delimiter && *pbufline == *csv_delimiter) + { + *ptoken = '\0'; + if (csv_lineno > 0) + { + if (current_register < MAX_CSV_REGISTERS) + u8_strncpy(csv_register[current_register++], token, + u8_strlen(token)+1); + } + else + { + if (current_header < MAX_CSV_REGISTERS) + u8_strncpy(csv_header[current_header++], token, + u8_strlen(token)+1); + } + *token = '\0'; + ptoken = token; + pbufline++; + } + else + *ptoken++ = *pbufline++; + } + } + } + *ptoken = '\0'; + if (csv_lineno > 0) + { + if (current_register < MAX_CSV_REGISTERS) + u8_strncpy(csv_register[current_register++], token, + u8_strlen(token)+1); + } + else + { + if (current_header < MAX_CSV_REGISTERS) + u8_strncpy(csv_header[current_header++], token, + u8_strlen(token)+1); + } + *token = '\0'; + ptoken = token; + + if (csv_lineno > 0 && pbufline != bufline) + print_csv_row(output, csv_header, csv_register); + + for (UBYTE i = 0; i < MAX_CSV_REGISTERS; i++) + *csv_register[i] = 0; + csv_lineno++; + } + fclose(csv); + for (UBYTE i = MAX_CSV_REGISTERS; i > 0; i--) + free(csv_register[i-1]); + for (UBYTE i = MAX_CSV_REGISTERS; i > 0; i--) + free(csv_header[i-1]); + free(token); + free(bufline); free(csv_template); csv_template = NULL; @@ -345,14 +543,17 @@ process_csv(uint8_t* token, FILE* output, BOOL read_yaml_macros_and_links, uint8_t* saveptr = NULL; uint8_t* args = u8_strtok(token, (uint8_t*)" ", &saveptr); args = u8_strtok(NULL, (uint8_t*)" ", &saveptr); - /* - *if (args) - *{ - * fprintf(stderr, "%ld:%ld:csv: args=[%s]\n", - * lineno, colno, - * args); - *} - */ + if (!args) + exit(error(EINVAL, (uint8_t*)"csv: Arguments required\n")); + size_t args_len = u8_strlen(args); + if (*args != '"' || *(args + args_len - 1) != '"') + exit(error(EINVAL, (uint8_t*)"csv: First argument must be a string\n")); + if (!csv_filename) + CALLOC(csv_filename, uint8_t, BUFSIZE) + strncpy(csv_filename, input_dirname, strlen(input_dirname)+1); + strncat(csv_filename, "/", 2); + strncat(csv_filename, (char*)args+1, args_len-2); + strncat(csv_filename, ".csv", 5); } return 0; @@ -433,7 +634,7 @@ filter_subdirs(const struct dirent* node) char* nodename = NULL; CALLOC(nodename, char, BUFSIZE) - snprintf(nodename, BUFSIZE, "%s/%s", basedir, node->d_name); + snprintf(nodename, BUFSIZE, "%s/%s", incdir, node->d_name); if (lstat(nodename, &st) < 0 || !S_ISDIR(st.st_mode)) { @@ -489,7 +690,7 @@ process_incdir_subdir(const char* subdirname, FILE* output, BOOL details_open, char* abs_subdirname = NULL; CALLOC(abs_subdirname, char, BUFSIZE) - snprintf(abs_subdirname, BUFSIZE, "%s/%s", basedir, subdirname); + snprintf(abs_subdirname, BUFSIZE, "%s/%s", incdir, subdirname); if ((names_total = scandir(abs_subdirname, &namelist, &filter_slw, &reverse_alphacompare)) < 0) @@ -556,6 +757,7 @@ process_incdir(uint8_t* token, FILE* output, BOOL read_yaml_macros_and_links) uint8_t* saveptr = NULL; /* skipping the first token (incdir) */ uint8_t* arg = u8_strtok(token, (uint8_t*)" ", &saveptr); + size_t arg_len = 0; long num = 5; uint8_t* macro_body = NULL; struct dirent** namelist; @@ -563,9 +765,23 @@ process_incdir(uint8_t* token, FILE* output, BOOL read_yaml_macros_and_links) long names_output; BOOL details_open = TRUE; + arg = u8_strtok(NULL, (uint8_t*)" ", &saveptr); if (!arg) - return warning(1, (uint8_t*)"incdir: Arguments required\n"); + exit(error(1, (uint8_t*)"incdir: Arguments required\n")); + + arg_len = u8_strlen(arg); + + CALLOC(incdir, char, BUFSIZE) + if (*arg != '"' || *(arg + arg_len - 1) != '"') + exit(error(1, (uint8_t*)"incdir: First argument not string\n")); + + strncpy(incdir, (char*)(arg+1), arg_len-2); + + arg = u8_strtok(NULL, (uint8_t*)" ", &saveptr); + if (!arg) + exit(error(1, (uint8_t*)"incdir: Second argument required\n")); + if (*arg == '=') macro_body = get_value(macros, macros_count, arg+1, NULL); else @@ -574,21 +790,24 @@ process_incdir(uint8_t* token, FILE* output, BOOL read_yaml_macros_and_links) while (parg && *parg) { if (*parg < '0' || *parg > '9') - return warning(1, (uint8_t*)"incdir: Non-numeric argument\n"); + exit(error(1, (uint8_t*)"incdir: Non-numeric argument\n")); parg++; } num = strtol((char*)arg, NULL, 10); if (errno) - return warning(errno, (uint8_t*)"incdir: Invalid num\n"); + exit(error(errno, (uint8_t*)"incdir: Invalid num\n")); arg = u8_strtok(NULL, (uint8_t*)" ", &saveptr); - if (*arg != '=') - return warning(1, (uint8_t*)"incdir: Second argument not macro\n"); - macro_body = get_value(macros, macros_count, arg+1, NULL); + if (arg) + { + if (*arg != '=') + exit(error(1, (uint8_t*)"incdir: Third argument not macro\n")); + macro_body = get_value(macros, macros_count, arg+1, NULL); + } } print_output(output, "<ul class=\"incdir\">\n"); - if (scandir(basedir, &namelist, &filter_subdirs, + if (scandir(incdir, &namelist, &filter_subdirs, &reverse_alphacompare) < 0) { perror("scandir"); @@ -606,6 +825,7 @@ process_incdir(uint8_t* token, FILE* output, BOOL read_yaml_macros_and_links) names_output++; } free(namelist); + free(incdir); print_output(output, "</ul>\n");