gout

A static git page generator
git clone https://git.bracken.jp/gout.git
Log | Files | Refs | README | LICENSE

format.c (7320B)


      1 #include "format.h"
      2 
      3 #include <err.h>
      4 #include <stdlib.h>
      5 #include <string.h>
      6 #include <time.h>
      7 
      8 void print_time(FILE* out, time_t time, int timezone_offset) {
      9   // Reject any offset > 24 hours.
     10   if (timezone_offset < -1440 || timezone_offset > 1440) {
     11     warnx("invalid timezone offset: %d", timezone_offset);
     12     return;
     13   }
     14   time_t local_time = time + (timezone_offset * 60);
     15   struct tm* time_in = gmtime(&local_time);
     16   if (!time_in) {
     17     return;
     18   }
     19 
     20   char formatted_time[32];
     21   if (!strftime(formatted_time, sizeof(formatted_time), "%a, %e %b %Y %H:%M:%S",
     22                 time_in)) {
     23     err(1, "strftime");
     24   }
     25 
     26   char timezone_sign = timezone_offset < 0 ? '-' : '+';
     27   int timezone_hours = (timezone_offset < 0 ? -1 : 1) * timezone_offset / 60;
     28   int timezone_mins = (timezone_offset < 0 ? -1 : 1) * timezone_offset % 60;
     29   char out_str[64];
     30   if (snprintf(out_str, sizeof(out_str), "%s %c%02d%02d", formatted_time,
     31                timezone_sign, timezone_hours, timezone_mins) < 0) {
     32     err(1, "snprintf");
     33   }
     34   fprintf(out, "%s", out_str);
     35 }
     36 
     37 void print_time_z(FILE* out, time_t time) {
     38   struct tm* time_in = gmtime(&time);
     39   if (!time_in) {
     40     return;
     41   }
     42 
     43   char formatted_time[32];
     44   if (!strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%dT%H:%M:%SZ",
     45                 time_in)) {
     46     err(1, "strftime");
     47   }
     48   fprintf(out, "%s", formatted_time);
     49 }
     50 
     51 void print_time_short(FILE* out, time_t time) {
     52   struct tm* time_in = gmtime(&time);
     53   if (!time_in) {
     54     return;
     55   }
     56 
     57   char formatted_time[32];
     58   if (!strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%d %H:%M",
     59                 time_in)) {
     60     err(1, "strftime");
     61   }
     62   fprintf(out, "%s", formatted_time);
     63 }
     64 
     65 void print_percent_encoded(FILE* out, const char* str) {
     66   static const char* hex_chars = "0123456789ABCDEF";
     67 
     68   size_t str_len = strlen(str);
     69   for (size_t i = 0; i < str_len; i++) {
     70     unsigned char uc = str[i];
     71     // NOTE: do not encode '/' for paths or ",-."
     72     if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || uc == '[' ||
     73         uc == ']') {
     74       fprintf(out, "%%%c%c", hex_chars[(uc >> 4) & 0x0f], hex_chars[uc & 0x0f]);
     75     } else {
     76       fprintf(out, "%c", uc);
     77     }
     78   }
     79 }
     80 
     81 void print_xml_encoded(FILE* out, const char* str) {
     82   print_xml_encoded_len(out, str, -1, true);
     83 }
     84 
     85 void print_xml_encoded_len(FILE* out,
     86                            const char* str,
     87                            ssize_t str_len,
     88                            bool output_crlf) {
     89   size_t len = (str_len >= 0) ? (size_t)str_len : strlen(str);
     90   for (size_t i = 0; i < len && str[i] != '\0'; i++) {
     91     char c = str[i];
     92     switch (c) {
     93       case '<':
     94         fprintf(out, "&lt;");
     95         break;
     96       case '>':
     97         fprintf(out, "&gt;");
     98         break;
     99       case '\'':
    100         fprintf(out, "&#39;");
    101         break;
    102       case '&':
    103         fprintf(out, "&amp;");
    104         break;
    105       case '"':
    106         fprintf(out, "&quot;");
    107         break;
    108       case '\r':
    109       case '\n':
    110         if (output_crlf) {
    111           fprintf(out, "%c", c);
    112         }
    113         break;
    114       default:
    115         fprintf(out, "%c", c);
    116     }
    117   }
    118 }
    119 
    120 void print_gopher_text(FILE* out, const char* str, bool output_lf) {
    121   print_gopher_text_len(out, str, -1, output_lf);
    122 }
    123 
    124 void print_gopher_text_len(FILE* out,
    125                            const char* str,
    126                            ssize_t str_len,
    127                            bool output_lf) {
    128   size_t len = (str_len >= 0) ? (size_t)str_len : strlen(str);
    129   bool start_of_line = true;
    130   for (size_t i = 0; i < len && str[i] != '\0'; i++) {
    131     char c = str[i];
    132     if (output_lf && start_of_line && c == '[') {
    133       fprintf(out, "[|");
    134     } else if (c == '\t') {
    135       fprintf(out, "        ");
    136     } else if (c == '\r' || (!output_lf && c == '\n')) {
    137       /* Ignore. */
    138     } else {
    139       fprintf(out, "%c", c);
    140     }
    141     start_of_line = (c == '\n');
    142   }
    143 }
    144 
    145 void print_gopher_link(FILE* out, const char* str) {
    146   for (size_t i = 0; str[i] != '\0'; i++) {
    147     char c = str[i];
    148     if (c == '|') {
    149       fprintf(out, "\\|");
    150     } else if (c == '\t') {
    151       fprintf(out, "        ");
    152     } else if (c == '\r' || c == '\n') {
    153       // Ignore.
    154     } else {
    155       fprintf(out, "%c", c);
    156     }
    157   }
    158 }
    159 
    160 static size_t utf8_char_length(const char* str) {
    161   unsigned char c = (unsigned char)*str;
    162   if (c < 0x80) {
    163     return 1;
    164   }
    165   if ((c & 0xe0) == 0xc0) {
    166     return 2;
    167   }
    168   if ((c & 0xf0) == 0xe0) {
    169     return 3;
    170   }
    171   if ((c & 0xf8) == 0xf0) {
    172     return 4;
    173   }
    174   // Invalid UTF-8 sequence. Treat as single byte.
    175   return 1;
    176 }
    177 
    178 static bool is_zwj(const char* str) {
    179   return str[0] == '\xe2' && str[1] == '\x80' && str[2] == '\x8d';
    180 }
    181 
    182 static bool is_variation_selector(const char* str) {
    183   return str[0] == '\xef' && str[1] == '\xb8' && str[2] == '\x8f';
    184 }
    185 
    186 static size_t get_char_width(wchar_t c) {
    187   // ASCII characters in the range [0-31] and [127-255] are control characters
    188   // or non-printable characters
    189   if ((c >= 0 && c <= 31) || (c >= 127 && c <= 255)) {
    190     return 1;
    191   }
    192 
    193   // Emoji range.
    194   if (c >= 0x1F300 && c <= 0x1F6FF) {
    195     return 2;
    196   }
    197 
    198   // Extended pictographic characters.
    199   if (c >= 0x1F900 && c <= 0x1F9FF) {
    200     return 2;
    201   }
    202 
    203   // Hiragana and Katakana ranges.
    204   if ((c >= 0x3040 && c <= 0x309F) || (c >= 0x30A0 && c <= 0x30FF)) {
    205     return 2;
    206   }
    207 
    208   // Kanji range.
    209   if (c >= 0x4E00 && c <= 0x9FFF) {
    210     return 2;
    211   }
    212   return 1;
    213 }
    214 
    215 // TODO(cbracken): There has to be a better way.
    216 void print_gopher_link_padded(FILE* out,
    217                               const char* str,
    218                               size_t width,
    219                               char pad_char) {
    220   size_t str_len = strlen(str);
    221   size_t char_count = 0;
    222   size_t last_char_width = 0;
    223   size_t byte_count = 0;
    224   while (byte_count < str_len) {
    225     size_t pos = byte_count;
    226     size_t char_length = utf8_char_length(&str[byte_count]);
    227     byte_count += char_length;
    228     if (char_count == width - 1 && byte_count < str_len) {
    229       fprintf(out, "\u2026");
    230       last_char_width = 1;
    231       char_count += last_char_width;
    232     } else if (char_count < width) {
    233       if (str[pos] == '|') {
    234         fprintf(out, "\\|");
    235         char_count++;
    236         last_char_width = 1;
    237       } else if (str[pos] == '\t') {
    238         for (size_t i = 0; i < 8 && char_count < width; i++) {
    239           fprintf(out, " ");
    240           char_count++;
    241         }
    242         last_char_width = 1;
    243       } else if (str[pos] == '\r' || str[pos] == '\n') {
    244         // Ignore.
    245         last_char_width = 0;
    246       } else if (char_length == 3 && is_zwj(&str[pos])) {
    247         fprintf(out, "\xe2\x80\x8d");
    248         char_count -= last_char_width;
    249         last_char_width = 0;
    250       } else if (char_length == 3 && is_variation_selector(&str[pos])) {
    251         fprintf(out, "\xef\xb8\x8f");
    252         char_count -= last_char_width;
    253         last_char_width = 0;
    254       } else {
    255         for (size_t i = pos; i < byte_count; i++) {
    256           fprintf(out, "%c", str[i]);
    257         }
    258         wchar_t wide_char;
    259         if (mbtowc(&wide_char, &str[pos], MB_CUR_MAX) >= 0) {
    260           last_char_width = get_char_width(wide_char);
    261         } else {
    262           last_char_width = 1;
    263         }
    264         char_count += last_char_width;
    265       }
    266     }
    267   }
    268   if (pad_char != '\0') {
    269     while (char_count < width) {
    270       fprintf(out, "%c", pad_char);
    271       char_count++;
    272     }
    273   }
    274 }