чување 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:
| M | README | | | 9 | +++------ |
| M | TODO | | | 7 | ++++--- |
| M | defs.h | | | 8 | +++++++- |
| M | index.html | | | 13 | ++++--------- |
| M | index.slw | | | 11 | +++-------- |
| M | slweb.1.in | | | 40 | ++++++++++++++++++++-------------------- |
| M | slweb.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 <<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");