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; +}