sfeed_json: add JSON output format tool - sfeed - RSS and Atom parser
 (HTM) git clone git://git.codemadness.org/sfeed
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit ced0dd7f8a01dedad3ba16c4cf209aea673c3e82
 (DIR) parent 3f1d323497e5aefce65a3758d1bc408130d9bbe0
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Mon, 10 Apr 2023 17:03:57 +0200
       
       sfeed_json: add JSON output format tool
       
       This outputs the TSV data to JSON.
       
       It uses a subset of JSON Feed 1.1:
       
               https://www.jsonfeed.org/version/1.1/
       
       Diffstat:
         M Makefile                            |       1 +
         M README                              |       1 +
         A sfeed_json.1                        |      48 +++++++++++++++++++++++++++++++
         A sfeed_json.c                        |     171 +++++++++++++++++++++++++++++++
       
       4 files changed, 221 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/Makefile b/Makefile
       @@ -51,6 +51,7 @@ BIN = \
                sfeed_frames\
                sfeed_gopher\
                sfeed_html\
       +        sfeed_json\
                sfeed_mbox\
                sfeed_opml_import\
                sfeed_plain\
 (DIR) diff --git a/README b/README
       @@ -186,6 +186,7 @@ sfeed_curses      - Format feed data (TSV) to a curses interface.
        sfeed_frames      - Format feed data (TSV) to HTML file(s) with frames.
        sfeed_gopher      - Format feed data (TSV) to Gopher files.
        sfeed_html        - Format feed data (TSV) to HTML.
       +sfeed_json        - Format feed data (TSV) to JSON Feed.
        sfeed_opml_export - Generate an OPML XML file from a sfeedrc config file.
        sfeed_opml_import - Generate a sfeedrc config file from an OPML XML file.
        sfeed_markread    - Mark items as read/unread, for use with sfeed_curses.
 (DIR) diff --git a/sfeed_json.1 b/sfeed_json.1
       @@ -0,0 +1,48 @@
       +.Dd March 8, 2023
       +.Dt SFEED_JSON 1
       +.Os
       +.Sh NAME
       +.Nm sfeed_json
       +.Nd format feed data to JSON Feed
       +.Sh SYNOPSIS
       +.Nm
       +.Op Ar
       +.Sh DESCRIPTION
       +.Nm
       +formats feed data (TSV) from
       +.Xr sfeed 1
       +from stdin or for each
       +.Ar file
       +to stdout as JSON Feed data.
       +If one or more
       +.Ar file
       +arguments are specified then the basename of the
       +.Ar file
       +is used as the feed name in the output.
       +If no
       +.Ar file
       +arguments are specified and so the data is read from stdin then the feed name
       +is empty.
       +If
       +.Nm
       +is reading from one or more
       +.Ar file
       +arguments it will prefix the entry title with "[feed name] ".
       +.Sh EXIT STATUS
       +.Ex -std
       +.Sh EXAMPLES
       +.Bd -literal
       +curl -s 'https://codemadness.org/atom.xml' | sfeed | sfeed_json
       +.Ed
       +.Sh SEE ALSO
       +.Xr sfeed 1 ,
       +.Xr sfeed_atom 1 ,
       +.Xr sfeed 5
       +.Sh STANDARDS
       +.Rs
       +.%T JSON Feed Version 1.1
       +.%U https://www.jsonfeed.org/version/1.1/
       +.%D Nov, 2022
       +.Re
       +.Sh AUTHORS
       +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
 (DIR) diff --git a/sfeed_json.c b/sfeed_json.c
       @@ -0,0 +1,171 @@
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +
       +#include "util.h"
       +
       +static char *line;
       +static size_t linesize;
       +static int firstitem = 1;
       +
       +/* Unescape / decode fields printed by string_print_encoded() */
       +static void
       +printcontent(const char *s)
       +{
       +        for (; *s; s++) {
       +                switch (*s) {
       +                case '\\':
       +                        s++;
       +                        switch (*s) {
       +                        case 'n':  fputs("\\n",  stdout); break;
       +                        case '\\': fputs("\\\\", stdout); break;
       +                        case 't':  fputs("\\t",  stdout); break;
       +                        }
       +                        break; /* ignore invalid escape sequence */
       +                case '"': fputs("\\\"", stdout); break;
       +                default:
       +                        putchar(*s);
       +                        break;
       +                }
       +        }
       +}
       +
       +static void
       +printfield(const char *s)
       +{
       +        for (; *s; s++) {
       +                if (*s == '\\')
       +                        fputs("\\\\", stdout);
       +                else if (*s == '"')
       +                        fputs("\\\"", stdout);
       +                else
       +                        putchar(*s);
       +        }
       +}
       +
       +static void
       +printfeed(FILE *fp, const char *feedname)
       +{
       +        char *fields[FieldLast], timebuf[32];
       +        struct tm parsedtm, *tm;
       +        time_t parsedtime;
       +        ssize_t linelen;
       +        int ch;
       +        char *p, *s;
       +
       +        while ((linelen = getline(&line, &linesize, fp)) > 0 &&
       +               !ferror(stdout)) {
       +                if (line[linelen - 1] == '\n')
       +                        line[--linelen] = '\0';
       +                parseline(line, fields);
       +
       +                if (!firstitem)
       +                        fputs(",\n", stdout);
       +                firstitem = 0;
       +
       +                fputs("{\n\t\"id\": \"", stdout);
       +                printfield(fields[FieldId]);
       +                fputs("\"", stdout);
       +
       +                parsedtime = 0;
       +                if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) &&
       +                    (tm = gmtime_r(&parsedtime, &parsedtm)) &&
       +                    strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%SZ", tm)) {
       +                        fputs(",\n\t\"date_published\": \"", stdout);
       +                        fputs(timebuf, stdout);
       +                        fputs("\"", stdout);
       +                }
       +
       +                fputs(",\n\t\"title\": \"", stdout);
       +                if (feedname[0]) {
       +                        fputs("[", stdout);
       +                        printfield(feedname);
       +                        fputs("] ", stdout);
       +                }
       +                printfield(fields[FieldTitle]);
       +                fputs("\"", stdout);
       +
       +                if (fields[FieldLink][0]) {
       +                        fputs(",\n\t\"url\": \"", stdout);
       +                        printfield(fields[FieldLink]);
       +                        fputs("\"", stdout);
       +                }
       +
       +                if (fields[FieldAuthor][0]) {
       +                        fputs(",\n\t\"authors\": [{\"name\": \"", stdout);
       +                        printfield(fields[FieldAuthor]);
       +                        fputs("\"}]", stdout);
       +                }
       +
       +                if (fields[FieldCategory][0]) {
       +                        fputs(",\n\t\"tags\": [", stdout);
       +
       +                        for (p = s = fields[FieldCategory]; ; s++) {
       +                                if (*s == '|' || *s == '\0') {
       +                                        if (p != fields[FieldCategory])
       +                                                fputs(", ", stdout);
       +                                        ch = *s;
       +                                        *s = '\0'; /* temporary NUL terminate */
       +                                        fputs("\"", stdout);
       +                                        printfield(p);
       +                                        fputs("\"", stdout);
       +                                        *s = ch; /* restore */
       +                                        p = s + 1;
       +                                }
       +                                if (*s == '\0')
       +                                        break;
       +                        }
       +                        fputs("]", stdout);
       +                }
       +
       +                if (fields[FieldEnclosure][0]) {
       +                        fputs(",\n\t\"attachments\": [{\"url:\": \"", stdout);
       +                        printfield(fields[FieldEnclosure]);
       +                        fputs("\"}]", stdout);
       +                }
       +
       +                if (!strcmp(fields[FieldContentType], "html"))
       +                        fputs(",\n\t\"content_html\": \"", stdout);
       +                else
       +                        fputs(",\n\t\"content_text\": \"", stdout);
       +                printcontent(fields[FieldContent]);
       +                fputs("\"\n}", stdout);
       +        }
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        FILE *fp;
       +        char *name;
       +        int i;
       +
       +        if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1)
       +                err(1, "pledge");
       +
       +        fputs("{\n"
       +              "\"version\": \"https://jsonfeed.org/version/1.1\",\n"
       +              "\"title\": \"Newsfeed\",\n"
       +              "\"items\": [\n", stdout);
       +
       +        if (argc == 1) {
       +                printfeed(stdin, "");
       +                checkfileerror(stdin, "<stdin>", 'r');
       +        } else {
       +                for (i = 1; i < argc; i++) {
       +                        if (!(fp = fopen(argv[i], "r")))
       +                                err(1, "fopen: %s", argv[i]);
       +                        name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i];
       +                        printfeed(fp, name);
       +                        checkfileerror(fp, argv[i], 'r');
       +                        checkfileerror(stdout, "<stdout>", 'w');
       +                        fclose(fp);
       +                }
       +        }
       +        fputs("]\n}\n", stdout);
       +
       +        checkfileerror(stdout, "<stdout>", 'w');
       +
       +        return 0;
       +}