| #define INCLUDE_MUSTACHE_IMPLEMENTATION 1 |
| #include <mustache_parser.h> |
| |
| #include <fiobj_ary.h> |
| #include <fiobj_hash.h> |
| #include <fiobj_mustache.h> |
| #include <fiobj_str.h> |
| |
| /** |
| * Loads a mustache template, converting it into an opaque instruction array. |
| * |
| * Returns a pointer to the instruction array. |
| * |
| * The `folder` argument should contain the template's root folder which would |
| * also be used to search for any required partial templates. |
| * |
| * The `filename` argument should contain the template's file name. |
| */ |
| mustache_s *fiobj_mustache_load(fio_str_info_s filename) { |
| return mustache_load(.filename = filename.data, .filename_len = filename.len); |
| } |
| |
| /** Free the mustache template */ |
| void fiobj_mustache_free(mustache_s *mustache) { mustache_free(mustache); } |
| |
| /** |
| * Renders a template into an existing FIOBJ String (`dest`'s end), using the |
| * information in the `data` object. |
| * |
| * Returns FIOBJ_INVALID if an error occured and a FIOBJ String on success. |
| */ |
| FIOBJ fiobj_mustache_build2(FIOBJ dest, mustache_s *mustache, FIOBJ data) { |
| mustache_build(mustache, .udata1 = (void *)dest, .udata2 = (void *)data); |
| return dest; |
| } |
| |
| /** |
| * Creates a FIOBJ String containing the rendered template using the information |
| * in the `data` object. |
| * |
| * Returns FIOBJ_INVALID if an error occured and a FIOBJ String on success. |
| */ |
| FIOBJ fiobj_mustache_build(mustache_s *mustache, FIOBJ data) { |
| if (!mustache) |
| return FIOBJ_INVALID; |
| return fiobj_mustache_build2(fiobj_str_buf(mustache->u.read_only.data_length), |
| mustache, data); |
| } |
| |
| /* ***************************************************************************** |
| Mustache Callbacks |
| ***************************************************************************** */ |
| |
| /** HTML ecape table, created using the following Ruby Script: |
| a = [] |
| 256.times {|i| a[i] = "&\#x#{ i < 16 ? "0#{i.to_s(16)}" : i.to_s(16)};"} |
| ('a'.ord..'z'.ord).each {|i| a[i] = i.chr } |
| ('A'.ord..'Z'.ord).each {|i| a[i] = i.chr } |
| ('0'.ord..'9'.ord).each {|i| a[i] = i.chr } |
| a['<'.ord] = "<" |
| a['>'.ord] = ">" |
| a['&'.ord] = "&" |
| a['"'.ord] = """ |
| |
| b = a.map {|s| s.length } |
| puts "static char *html_escape_strs[] = {", a.to_s.slice(1..-2) ,"};", |
| "static uint8_t html_escape_len[] = {", b.to_s.slice(1..-2),"};" |
| */ |
| static const char *html_escape_strs[] = { |
| "�", "", "", "", "", "", "", |
| "", "", "	", "
", "", "", "
", |
| "", "", "", "", "", "", "", |
| "", "", "", "", "", "", "", |
| "", "", "", "", " ", "!", """, |
| "#", "$", "%", "&", "'", "(", ")", |
| "*", "+", ",", "-", ".", "/", "0", |
| "1", "2", "3", "4", "5", "6", "7", |
| "8", "9", ":", ";", "<", "=", ">", |
| "?", "@", "A", "B", "C", "D", "E", |
| "F", "G", "H", "I", "J", "K", "L", |
| "M", "N", "O", "P", "Q", "R", "S", |
| "T", "U", "V", "W", "X", "Y", "Z", |
| "[", "\", "]", "^", "_", "`", "a", |
| "b", "c", "d", "e", "f", "g", "h", |
| "i", "j", "k", "l", "m", "n", "o", |
| "p", "q", "r", "s", "t", "u", "v", |
| "w", "x", "y", "z", "{", "|", "}", |
| "~", "", "€", "", "‚", "ƒ", "„", |
| "…", "†", "‡", "ˆ", "‰", "Š", "‹", |
| "Œ", "", "Ž", "", "", "‘", "’", |
| "“", "”", "•", "–", "—", "˜", "™", |
| "š", "›", "œ", "", "ž", "Ÿ", " ", |
| "¡", "¢", "£", "¤", "¥", "¦", "§", |
| "¨", "©", "ª", "«", "¬", "­", "®", |
| "¯", "°", "±", "²", "³", "´", "µ", |
| "¶", "·", "¸", "¹", "º", "»", "¼", |
| "½", "¾", "¿", "À", "Á", "Â", "Ã", |
| "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", |
| "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", |
| "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", |
| "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", |
| "à", "á", "â", "ã", "ä", "å", "æ", |
| "ç", "è", "é", "ê", "ë", "ì", "í", |
| "î", "ï", "ð", "ñ", "ò", "ó", "ô", |
| "õ", "ö", "÷", "ø", "ù", "ú", "û", |
| "ü", "ý", "þ", "ÿ"}; |
| static uint8_t html_escape_len[] = { |
| 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, |
| 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, |
| 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 4, 6, 4, 6, 6, 1, 1, 1, 1, 1, 1, 1, |
| 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 6, |
| 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
| 1, 1, 1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, |
| 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, |
| 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, |
| 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, |
| 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, |
| 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}; |
| |
| static inline FIOBJ fiobj_mustache_find_obj(mustache_section_s *section, |
| const char *name, |
| uint32_t name_len) { |
| FIOBJ key = fiobj_str_tmp(); |
| fiobj_str_write(key, name, name_len); |
| FIOBJ o = FIOBJ_INVALID; |
| do { |
| if (!FIOBJ_TYPE_IS((FIOBJ)section->udata2, FIOBJ_T_HASH)) |
| continue; |
| o = fiobj_hash_get((FIOBJ)section->udata2, key); |
| section = section->parent; |
| } while (o == FIOBJ_INVALID && section); |
| return o; |
| } |
| /** |
| * Called when an argument name was detected in the current section. |
| * |
| * A conforming implementation will search for the named argument both in the |
| * existing section and all of it's parents (walking backwards towards the root) |
| * until a value is detected. |
| * |
| * A missing value should be treated the same as an empty string. |
| * |
| * A conforming implementation will output the named argument's value (either |
| * HTML escaped or not, depending on the `escape` flag) as a string. |
| */ |
| static int mustache_on_arg(mustache_section_s *section, const char *name, |
| uint32_t name_len, unsigned char escape) { |
| FIOBJ o = fiobj_mustache_find_obj(section, name, name_len); |
| if (!o) |
| return 0; |
| if (!escape || !FIOBJ_TYPE_IS(o, FIOBJ_T_STRING)) { |
| fiobj_str_join((FIOBJ)section->udata1, o); |
| return 0; |
| } |
| /* TODO: html escape */ |
| fio_str_info_s str = fiobj_obj2cstr(o); |
| if (!str.len) |
| return 0; |
| fio_str_info_s i = fiobj_obj2cstr(o); |
| i.capa = fiobj_str_capa_assert((FIOBJ)section->udata1, i.len + str.len + 64); |
| do { |
| if (i.len + 6 >= i.capa) |
| i.capa = fiobj_str_capa_assert((FIOBJ)section->udata1, i.capa + 64); |
| i.len = fiobj_str_write((FIOBJ)section->udata1, |
| html_escape_strs[(uint8_t)str.data[0]], |
| html_escape_len[(uint8_t)str.data[0]]); |
| --str.len; |
| ++str.data; |
| } while (str.len); |
| (void)section; |
| (void)name; |
| (void)name_len; |
| (void)escape; |
| return 0; |
| } |
| |
| /** |
| * Called when simple template text (string) is detected. |
| * |
| * A conforming implementation will output data as a string (no escaping). |
| */ |
| static int mustache_on_text(mustache_section_s *section, const char *data, |
| uint32_t data_len) { |
| FIOBJ dest = (FIOBJ)section->udata1; |
| fiobj_str_write(dest, data, data_len); |
| return 0; |
| } |
| |
| /** |
| * Called for nested sections, must return the number of objects in the new |
| * subsection (depending on the argument's name). |
| * |
| * Arrays should return the number of objects in the array. |
| * |
| * `true` values should return 1. |
| * |
| * `false` values should return 0. |
| * |
| * A return value of -1 will stop processing with an error. |
| * |
| * Please note, this will handle both normal and inverted sections. |
| */ |
| static int32_t mustache_on_section_test(mustache_section_s *section, |
| const char *name, uint32_t name_len) { |
| FIOBJ o = fiobj_mustache_find_obj(section, name, name_len); |
| if (!o) |
| return 0; |
| if (FIOBJ_TYPE_IS(o, FIOBJ_T_ARRAY)) |
| return fiobj_ary_count(o); |
| return 1; |
| } |
| |
| /** |
| * Called when entering a nested section. |
| * |
| * `index` is a zero based index indicating the number of repetitions that |
| * occurred so far (same as the array index for arrays). |
| * |
| * A return value of -1 will stop processing with an error. |
| * |
| * Note: this is a good time to update the subsection's `udata` with the value |
| * of the array index. The `udata` will always contain the value or the parent's |
| * `udata`. |
| */ |
| static int mustache_on_section_start(mustache_section_s *section, |
| char const *name, uint32_t name_len, |
| uint32_t index) { |
| FIOBJ o = fiobj_mustache_find_obj(section, name, name_len); |
| if (!o) |
| return -1; |
| if (FIOBJ_TYPE_IS(o, FIOBJ_T_ARRAY)) |
| section->udata2 = (void *)fiobj_ary_index(o, index); |
| else |
| section->udata2 = (void *)o; |
| return 0; |
| } |
| |
| /** |
| * Called for cleanup in case of error. |
| */ |
| static void mustache_on_formatting_error(void *udata1, void *udata2) { |
| (void)udata1; |
| (void)udata2; |
| } |
| |
| /* ***************************************************************************** |
| Testing |
| ***************************************************************************** */ |
| |
| #if DEBUG |
| static inline void mustache_save2file(char const *filename, char const *data, |
| size_t length) { |
| int fd = open(filename, O_CREAT | O_RDWR, 0); |
| if (fd == -1) { |
| perror("Couldn't open / create file for template testing"); |
| exit(-1); |
| } |
| fchmod(fd, 0777); |
| if (pwrite(fd, data, length, 0) != (ssize_t)length) { |
| perror("Mustache template write error"); |
| exit(-1); |
| } |
| close(fd); |
| } |
| |
| void fiobj_mustache_test(void) { |
| #define TEST_ASSERT(cond, ...) \ |
| if (!(cond)) { \ |
| fprintf(stderr, "* " __VA_ARGS__); \ |
| fprintf(stderr, "\n !!! Testing failed !!!\n"); \ |
| exit(-1); \ |
| } |
| |
| char const *template = "{{=<< >>=}}* Users:\r\n<<#users>><<id>>. <<& name>> " |
| "(<<name>>)\r\n<</users>>"; |
| char const *template_name = "mustache_test_template.mustache"; |
| mustache_save2file(template_name, template, strlen(template)); |
| // mustache_error_en err = MUSTACHE_OK; |
| mustache_s *m = |
| fiobj_mustache_load((fio_str_info_s){.data = (char *)template_name}); |
| unlink(template_name); |
| TEST_ASSERT(m, "fiobj_mustache_load failed.\n"); |
| FIOBJ data = fiobj_hash_new(); |
| FIOBJ key = fiobj_str_new("users", 5); |
| FIOBJ ary = fiobj_ary_new2(4); |
| fiobj_hash_set(data, key, ary); |
| fiobj_free(key); |
| for (int i = 0; i < 4; ++i) { |
| FIOBJ id = fiobj_str_buf(4); |
| fiobj_str_write_i(id, i); |
| FIOBJ name = fiobj_str_buf(4); |
| fiobj_str_write(name, "User ", 5); |
| fiobj_str_write_i(name, i); |
| FIOBJ usr = fiobj_hash_new2(2); |
| key = fiobj_str_new("id", 2); |
| fiobj_hash_set(usr, key, id); |
| fiobj_free(key); |
| key = fiobj_str_new("name", 4); |
| fiobj_hash_set(usr, key, name); |
| fiobj_free(key); |
| fiobj_ary_push(ary, usr); |
| } |
| key = fiobj_mustache_build(m, data); |
| fiobj_free(data); |
| TEST_ASSERT(key, "fiobj_mustache_build failed!\n"); |
| fprintf(stderr, "%s\n", fiobj_obj2cstr(key).data); |
| fiobj_free(key); |
| } |
| |
| #endif |