AmendHub

Download:

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;