jcs
/detritus
/amendments
/6
client: Implement Gemtext parsing/styling
jcs made amendment 6 about 1 year ago
--- client.c Mon Sep 30 21:36:32 2024
+++ client.c Tue Oct 1 12:13:41 2024
@@ -49,7 +49,9 @@ void shuffle_tls_read_plaintext(struct client *client)
size_t client_printf(struct client *client, const char *format, ...);
size_t client_debugf(struct client *client, const char *format, ...);
-void client_parse_gemtext(struct client *client);
+void client_parse_response(struct client *client);
+bool client_parse_header(struct client *client, char *str);
+void client_consume_input(struct client *client, size_t len);
Pattern fill_pattern;
@@ -140,7 +142,7 @@ client_init(void)
client->output_te = TEStylNew(&te_bounds, &bounds);
if (client->output_te == NULL)
panic("Out of memory allocating TE");
- TEAutoView(true, client->output_te);
+ TEAutoView(false, client->output_te);
(*(client->output_te))->caretHook = NullCaretHook;
/* scrollbar for output text */
@@ -203,6 +205,9 @@ client_cleanup(struct client *client)
_TCPAbort(&client->req->tcp_iopb, client->req->tcp_stream,
NULL, NULL, false);
+ if (client->req->links)
+ xfree(&client->req->links);
+
xfree(&client->req);
}
@@ -417,6 +422,7 @@ client_connect(struct client *client)
client->req->uri[len] = '\0';
HUnlock(te->hText);
HUnlock(client->uri_te);
+ TEDeactivate(client->uri_te);
/* TODO: support URIs with port? */
client->req->port = DEFAULT_GEMINI_PORT;
@@ -560,6 +566,7 @@ client_shuffle_data(struct client *client)
if (status & 0x10) {
/* tls has plaintext data for us */
progress(NULL);
+ TEActivate(client->uri_te);
shuffle_tls_read_plaintext(client);
status &= ~0x10;
}
@@ -714,8 +721,7 @@ shuffle_tls_send_plaintext(struct client *client, shor
client_debugf(client, "[Sent %ld bytes of plaintext to TLS]\r",
len);
} else {
- memmove(client->req->uri,
- client->req->uri + len,
+ memmove(client->req->uri, client->req->uri + len,
client->req->uri_len - len);
client->req->uri_len -= len;
client_debugf(client, "[Wrote %ld bytes of plaintext to "
@@ -753,7 +759,7 @@ shuffle_tls_read_plaintext(struct client *client)
}
if (client->req->input_len)
- client_parse_gemtext(client);
+ client_parse_response(client);
}
size_t
@@ -793,45 +799,161 @@ client_debugf(struct client *client, const char *forma
size_t
client_print(struct client *client, const char *str, size_t len)
{
- static char tstr[1024];
+ StScrpRec *scrp_rec;
+ ScrpSTElement *scrp_ele;
+ struct client_link *link;
short line_height;
- short was_len;
- size_t n = 0;
+ size_t n;
+ unsigned long style = STYLE_NONE;
+ if (client->req && client->req->style)
+ style = client->req->style;
+
line_height = CLIENT_FONT_SIZE + 3;
- HLock(client->output_te);
- was_len = (*(client->output_te))->teLength;
- HUnlock(client->output_te);
-
client_avoid_te_overflow(client, client->output_te, line_height);
- while (len) {
- if (*str == '\r' && *(str + 1) == '\n') {
- tstr[n++] = '\r';
+ if (client->scrp_rec_h == NULL) {
+ client->scrp_rec_h = xNewHandle(sizeof(short) +
+ (sizeof(ScrpSTElement) * CLIENT_SCRAP_ELEMENTS));
+ HLock(client->scrp_rec_h);
+ memset(*(client->scrp_rec_h), 0,
+ GetHandleSize(client->scrp_rec_h));
+ } else {
+ HLock(client->scrp_rec_h);
+ }
+
+ scrp_rec = (StScrpRec *)(*(client->scrp_rec_h));
+ scrp_rec->scrpNStyles = 1;
+ scrp_ele = &scrp_rec->scrpStyleTab[0];
+ scrp_ele->scrpHeight = line_height;
+ scrp_ele->scrpAscent = CLIENT_FONT_SIZE;
+ scrp_ele->scrpFont = CLIENT_FONT;
+ scrp_ele->scrpSize = CLIENT_FONT_SIZE;
+ scrp_ele->scrpFace = 0;
+
+ if (style & STYLE_BOLD)
+ scrp_ele->scrpFace |= bold | condense;
+ if (style & (STYLE_H1 | STYLE_H2 | STYLE_H3))
+ scrp_ele->scrpFace |= bold;
+ if (style & STYLE_ITALIC)
+ scrp_ele->scrpFace |= italic;
+ if (style & STYLE_LINK)
+ scrp_ele->scrpFace |= underline;
+ if (style & STYLE_H1) {
+ scrp_ele->scrpSize += 8;
+ scrp_ele->scrpHeight += 10;
+ scrp_ele->scrpAscent += 8;
+ } else if (style & STYLE_H2) {
+ scrp_ele->scrpSize += 4;
+ scrp_ele->scrpHeight += 6;
+ scrp_ele->scrpAscent += 4;
+ } else if (style & STYLE_H3) {
+ scrp_ele->scrpSize += 2;
+ scrp_ele->scrpHeight += 4;
+ scrp_ele->scrpAscent += 2;
+ }
+
+ TESetSelect(SHRT_MAX, SHRT_MAX, client->output_te);
+
+ if (style & STYLE_LINK) {
+ if (client->req->links_count == client->req->links_size) {
+ client->req->links_size += 256;
+ client->req->links = xreallocarray(client->req->links,
+ client->req->links_size, sizeof(struct client_link));
+ if (client->req->links == NULL) {
+ warn("Out of memory allocating links");
+ return 0;
+ }
+ memset(&client->req->links[client->req->links_count], 0,
+ sizeof(struct client_link) * 256);
+ }
+
+ link = &client->req->links[client->req->links_count++];
+
+ HLock(client->output_te);
+ link->pos = (*(client->output_te))->teLength;
+ HUnlock(client->output_te);
+
+ /* [<whitespace>]<URL>[<whitespace><title>] */
+
+ /* eat leading whitespace */
+ while (len && (str[0] == ' ' || str[0] == '\t')) {
str++;
len--;
- } else if (*str == '\n')
- tstr[n++] = '\r';
- else
- tstr[n++] = *str;
+ }
- str++;
- len--;
+ /* url, up to whitespace or end of str */
+ for (n = 0; n <= len; n++) {
+ if (!(n == len || str[n] == ' ' || str[n] == '\t' ||
+ str[n] == '\r'))
+ continue;
+
+ if (n == len)
+ n--;
+
+ link->link = xmalloc(n + 1);
+ if (link->link == NULL) {
+ warn("Out of memory allocating link");
+ return 0;
+ }
+ memcpy(link->link, str, n);
+ link->link[n] = '\0';
+ link->len = n;
+ str += n;
+ len -= n;
+ break;
+ }
- if (n == sizeof(tstr) || len == 0) {
- TESetSelect(SHRT_MAX, SHRT_MAX, client->output_te);
- TEInsert(tstr, n, client->output_te);
- if (len == 0)
- break;
- n = 0;
+ /* eat separating white space */
+ while (len && (str[0] == ' ' || str[0] == '\t')) {
+ str++;
+ len--;
}
+
+ /* optional title */
+ if (len > 1) {
+ /* len will include trailing \r */
+
+ link->title = xmalloc(len);
+ if (link->title == NULL) {
+ warn("Out of memory allocating link title");
+ return 0;
+ }
+ memcpy(link->title, str, len - 1);
+ link->title[len - 1] = '\0';
+ link->len = len - 1;
+
+ TEStylInsert(link->title, len - 1, client->scrp_rec_h,
+ client->output_te);
+
+ str += len - 1;
+ len = 1;
+ } else
+ TEStylInsert(link->link, len, client->scrp_rec_h,
+ client->output_te);
+
+ style &= ~(STYLE_LINK);
}
+
+ if (str[len - 1] == '\r' &&
+ (style & (STYLE_H1 | STYLE_H2 | STYLE_H3))) {
+ /* print newlines in a small size */
+ TEStylInsert(str, len - 1, client->scrp_rec_h, client->output_te);
+ scrp_ele->scrpHeight = line_height;
+ scrp_ele->scrpAscent = CLIENT_FONT_SIZE;
+ scrp_ele->scrpFont = CLIENT_FONT;
+ scrp_ele->scrpSize = CLIENT_FONT_SIZE;
+ TEStylInsert("\r", 1, client->scrp_rec_h, client->output_te);
+ } else
+ TEStylInsert(str, len, client->scrp_rec_h, client->output_te);
+
+ HUnlock(client->scrp_rec_h);
- if (was_len == 0) {
- SetCtlValue(client->output_te_scroller,
- GetCtlMin(client->output_te_scroller));
- }
+// if (was_len == 0) {
+// SetCtlValue(client->output_te_scroller,
+// GetCtlMin(client->output_te_scroller));
+// }
UpdateScrollbarForTE(client->win, client->output_te_scroller,
client->output_te, false);
@@ -905,91 +1027,223 @@ client_clear(struct client *client)
}
void
-client_parse_gemtext(struct client *client)
+client_parse_response(struct client *client)
{
- size_t n;
-
+ size_t n, trail, skip;
+ char c;
+
+handle_state:
switch (client->req->gem_state) {
case GEM_STATE_HEADER: {
short status;
for (n = 0; n < client->req->input_len; n++) {
- if (!(client->req->input[n] == '\n' && n &&
- client->req->input[n - 1] == '\r'))
+ c = client->req->input[n];
+
+ if (!(c == '\n' && n && client->req->input[n - 1] == '\r'))
continue;
client->req->input[n] = '\0';
client->req->input[n - 1] = '\0';
- client_debugf(client, "[Received header: %d]\r", status);
- if (!(client->req->input[0] >= '0' &&
- client->req->input[0] <= '9' &&
- client->req->input[1] >= '0' &&
- client->req->input[1] <= '9' &&
- client->req->input[2] == ' ')) {
- client_printf(client, "[Malformed response %s]\r",
- client->req->input);
+ if (!client_parse_header(client, client->req->input)) {
client->req->state = REQ_STATE_DISCONNECTED;
return;
}
- status = ((client->req->input[0] - '0') * 10) +
- (client->req->input[1] - '0');
+ if (strncmp(client->req->mime_type, "text/gemini", 11) == 0)
+ client->req->gem_state = GEM_STATE_GEMTEXT;
+ else
+ client->req->gem_state = GEM_STATE_DOWNLOAD;
- if (status >= 10 && status <= 19) {
- /* input, not supported */
- client_printf(client, "[Input not supported (%d)]\r",
- status);
- client->req->state = REQ_STATE_DISCONNECTED;
- return;
- } else if (status >= 20 && status <= 29) {
- /* success */
- client->req->gem_state = GEM_STATE_BODY;
- } else if (status >= 30 && status <= 39) {
- /* redirect */
- /* TODO */
- } else if (status >= 40 && status <= 49) {
- /* temp fail */
- client_printf(client, "[Temporary server failure (%d)]\r",
- status);
- client->req->state = REQ_STATE_DISCONNECTED;
- return;
- } else if (status >= 50 && status <= 59) {
- /* perm fail */
- client_printf(client, "[Permanent server failure (%d)]\r",
- status);
- client->req->state = REQ_STATE_DISCONNECTED;
- return;
- } else if (status >= 60 && status <= 69) {
- /* auth, not supported */
- client_printf(client, "[Auth not supported (%d)]\r",
- status);
- client->req->state = REQ_STATE_DISCONNECTED;
- return;
- } else {
- client_printf(client, "[Unsupported status %d]\r", status);
- client->req->state = REQ_STATE_DISCONNECTED;
- return;
+ client_consume_input(client, n + 1);
+
+ /* avoid a round trip through idle handler */
+ goto handle_state;
+ }
+ break;
+ }
+ case GEM_STATE_REDIRECT:
+ /* TODO */
+ break;
+ case GEM_STATE_DOWNLOAD:
+ /* TODO */
+ break;
+ case GEM_STATE_GEMTEXT:
+restart_parse:
+ trail = 0;
+ for (n = 0; n <= client->req->input_len; n++) {
+ if (n == client->req->input_len && n < 3) {
+ /*
+ * End of buffer with no newline, but not enough chars
+ * to determine what this line is, skip until we get more.
+ */
+ break;
}
+
+ if (!(n == client->req->input_len ||
+ client->req->input[n] == '\n'))
+ continue;
- strlcpy(client->req->mime_type, client->req->input + 3,
- sizeof(client->req->mime_type));
+ if (n < client->req->input_len &&
+ client->req->input[n] == '\n') {
+ client->req->input[n] = '\r';
+ if (client->req->input[n - 1] == '\r')
+ trail = 1;
+ }
- if (n == client->req->input_len - 1)
- client->req->input_len = 0;
- else {
- memmove(client->req->input, client->req->input + n + 1,
- sizeof(client->req->input) - (n + 1));
- client->req->input_len -= (n + 1);
+ if (client->req->input[0] == '=' &&
+ client->req->input[1] == '>') {
+ /* link */
+ client->req->style = STYLE_LINK;
+ skip = !!(client->req->input[2] == ' ');
+ client_print(client, client->req->input + 2 + skip,
+ n - trail - skip);
+ client_consume_input(client, n + 1);
+ client->req->style = STYLE_NONE;
+ goto restart_parse;
}
- break;
+
+ if (client->req->input[0] == '`' &&
+ client->req->input[1] == '`' &&
+ client->req->input[2] == '`') {
+ /* ``` toggle */
+ client->req->style = STYLE_PRE;
+ client_consume_input(client, n + 1);
+ goto restart_parse;
+ }
+
+ if (client->req->input[0] == '#' &&
+ client->req->input[1] == '#' &&
+ client->req->input[2] == '#') {
+ /* ### h3 */
+ client->req->style = STYLE_H3;
+ skip = !!(client->req->input[3] == ' ');
+ client_print(client, client->req->input + 3 + skip,
+ n - 2 - skip - trail);
+ client_consume_input(client, n + 1);
+ client->req->style = STYLE_NONE;
+ goto restart_parse;
+ }
+
+ if (client->req->input[0] == '#' &&
+ client->req->input[1] == '#') {
+ /* ## h2 */
+ client->req->style = STYLE_H2;
+ skip = !!(client->req->input[2] == ' ');
+ client_print(client, client->req->input + 2 + skip,
+ n - 1 - skip - trail);
+ client_consume_input(client, n + 1);
+ client->req->style = STYLE_NONE;
+ goto restart_parse;
+ }
+
+ if (client->req->input[0] == '#') {
+ /* # h1 */
+ client->req->style = STYLE_H1;
+ skip = !!(client->req->input[1] == ' ');
+ client_print(client, client->req->input + 1 + skip,
+ n - skip - trail);
+ client_consume_input(client, n + 1);
+ client->req->style = STYLE_NONE;
+ goto restart_parse;
+ }
+
+ if (client->req->input[0] == '*') {
+ /* * list item */
+ client->req->style = STYLE_LIST;
+ skip = !!(client->req->input[1] == ' ');
+ client_print(client, client->req->input + 1 + skip,
+ n - skip - trail);
+ client_consume_input(client, n + 1);
+ client->req->style = STYLE_NONE;
+ goto restart_parse;
+ }
+
+ if (client->req->input[0] == '>') {
+ /* > quote text */
+ client->req->style = STYLE_QUOTE;
+ skip = !!(client->req->input[1] == ' ');
+ client_print(client, client->req->input + 1 + skip,
+ n - skip - trail);
+ client_consume_input(client, n + 1);
+ client->req->style = STYLE_NONE;
+ goto restart_parse;
+ }
+
+ if (n == client->req->input_len) {
+ /* end of buffer with no start, probably a continuation */
+ client_print(client, client->req->input, n);
+ client_consume_input(client, n);
+ break;
+ }
+
+ /* just plain text */
+ client_print(client, client->req->input, n + 1 - trail);
+ client_consume_input(client, n + 1);
+ goto restart_parse;
}
break;
}
- case GEM_STATE_BODY:
- /* TODO */
- client_print(client, client->req->input, client->req->input_len);
+}
+
+void
+client_consume_input(struct client *client, size_t len)
+{
+ if (len == client->req->input_len)
client->req->input_len = 0;
- break;
+ else {
+ memmove(client->req->input, client->req->input + len,
+ sizeof(client->req->input) - len);
+ client->req->input_len -= len;
}
+}
+
+bool
+client_parse_header(struct client *client, char *str)
+{
+ short status;
+
+ client_debugf(client, "[Received header: %d]\r", str);
+ if (!(str[0] >= '0' && str[0] <= '9' &&
+ str[1] >= '0' && str[1] <= '9' && str[2] == ' ')) {
+ client_printf(client, "[Malformed response %s]\r", str);
+ return false;
+ }
+
+ status = ((str[0] - '0') * 10) + (str[1] - '0');
+
+ if (status >= 10 && status <= 19) {
+ /* input, not supported */
+ client_printf(client, "[Input not supported (%d)]\r", status);
+ return false;
+ }
+ if (status >= 20 && status <= 29) {
+ /* success */
+ strlcpy(client->req->mime_type, str + 3,
+ sizeof(client->req->mime_type));
+ return true;
+ }
+ if (status >= 30 && status <= 39) {
+ /* redirect */
+ /* TODO */
+ return false;
+ }
+ if (status >= 40 && status <= 49) {
+ /* temp fail */
+ client_printf(client, "[Temporary server failure (%d)]\r", status);
+ return false;
+ }
+ if (status >= 50 && status <= 59) {
+ /* perm fail */
+ client_printf(client, "[Permanent server failure (%d)]\r", status);
+ return false;
+ }
+ if (status >= 60 && status <= 69) {
+ /* auth, not supported */
+ client_printf(client, "[Auth not supported (%d)]\r", status);
+ return false;
+ }
+ client_printf(client, "[Unsupported status %d]\r", status);
+ return false;
}
--- client.h Mon Sep 30 17:56:27 2024
+++ client.h Tue Oct 1 11:41:30 2024
@@ -29,9 +29,29 @@ enum {
enum {
GEM_STATE_HEADER,
- GEM_STATE_BODY
+ GEM_STATE_GEMTEXT,
+ GEM_STATE_REDIRECT,
+ GEM_STATE_DOWNLOAD
};
+#define STYLE_NONE 0
+#define STYLE_BOLD (1UL << 0)
+#define STYLE_ITALIC (1UL << 1)
+#define STYLE_H1 (1UL << 2)
+#define STYLE_H2 (1UL << 3)
+#define STYLE_H3 (1UL << 4)
+#define STYLE_LINK (1UL << 5)
+#define STYLE_PRE (1UL << 6)
+#define STYLE_LIST (1UL << 7)
+#define STYLE_QUOTE (1UL << 8)
+
+struct client_link {
+ char *link;
+ char *title;
+ unsigned short pos;
+ unsigned short len;
+};
+
struct tcp_request {
short tls_id;
@@ -57,6 +77,11 @@ struct tcp_request {
size_t input_len;
char mime_type[64];
+ unsigned long style;
+
+ size_t links_count;
+ size_t links_size;
+ struct client_link *links;
};
struct client {
@@ -65,7 +90,8 @@ struct client {
ControlHandle go_button;
TEHandle output_te;
ControlHandle output_te_scroller;
-
+ Handle scrp_rec_h;
+#define CLIENT_SCRAP_ELEMENTS 20
TEHandle active_te;
struct tcp_request *req;