/* * This file is part of libdom. * Licensed under the MIT License, * http://www.opensource.org/licenses/mit-license.php * Copyright 2022 Daniel Silverstone */ #include #include #include #include #include #include #include #include #include #include #include #include "core/element.h" #include "core/document.h" #include "utils/utils.h" #define DOM_TOKENLIST_GROW_INCREMENT 4 struct dom_tokenlist { uint32_t refcnt; dom_element *ele; dom_string *attr; dom_event_listener *listener; dom_string *last_set; bool needs_parse; /* Parsed content, for optimal access */ dom_string **entries; uint32_t len; uint32_t alloc; }; /* Handle a DOMAttrModified event which might be to do with our attribute */ static void _dom_tokenlist_handle_attrmodified(dom_event *evt, void *pw) { dom_mutation_event *mutevt = (dom_mutation_event *)evt; dom_tokenlist *list = (dom_tokenlist *)pw; dom_exception exc; dom_string *value; { dom_event_target *target; exc = dom_event_get_target(evt, &target); if (exc != DOM_NO_ERR) return; dom_node_unref(target); if (target != (dom_event_target *)list->ele) return; } { dom_string *attr; exc = dom_mutation_event_get_attr_name(mutevt, &attr); if (exc != DOM_NO_ERR) return; if (!dom_string_isequal(attr, list->attr)) { dom_string_unref(attr); return; } dom_string_unref(attr); } /* At this point we know that this is a mutation of our attribute on our * node */ exc = dom_mutation_event_get_new_value(mutevt, &value); if (exc != DOM_NO_ERR) return; if (list->last_set != NULL && dom_string_isequal(list->last_set, value)) { /* We've just seen the mutation event for one of our own set * operations */ dom_string_unref(value); return; } /* Mark that we need to re-parse the tokenlist on the next request */ list->needs_parse = true; dom_string_unref(value); } static dom_exception _dom_tokenlist_make_room(dom_tokenlist *list) { if (list->len == list->alloc) { uint32_t new_alloc = list->alloc + DOM_TOKENLIST_GROW_INCREMENT; dom_string **new_entries = realloc( list->entries, new_alloc * sizeof(dom_string *)); if (new_entries == NULL) return DOM_NO_MEM_ERR; list->alloc = new_alloc; list->entries = new_entries; } return DOM_NO_ERR; } static dom_exception _dom_tokenlist_reparse(dom_tokenlist *list) { dom_exception exc; dom_string *value; const char *pos; uint32_t remaining, check; uint32_t n_entries = 0; dom_string *temp; bool found; if (!list->needs_parse) return DOM_NO_ERR; /* Clean down the current entries */ while (list->len-- > 0) dom_string_unref(list->entries[list->len]); list->len = 0; /* Get the "new" attribute value */ exc = dom_element_get_attribute(list->ele, list->attr, &value); if (exc != DOM_NO_ERR) return exc; /* If there is no value, we're an empty list and we're done */ if (value == NULL) { list->needs_parse = false; return DOM_NO_ERR; } /* OK, there's something here to do, so let's do it... */ /* Count number of entries */ for (pos = dom_string_data(value), remaining = dom_string_length(value); remaining > 0;) { if (*pos != ' ') { while (*pos != ' ' && remaining > 0) { remaining--; pos++; } n_entries++; } else { while (*pos == ' ' && remaining > 0) { remaining--; pos++; } } } /* If there are no entries (all whitespace) just bail here */ if (n_entries == 0) { list->needs_parse = false; dom_string_unref(value); return DOM_NO_ERR; } /* If we need more room, reallocate the buffer */ if (list->alloc < n_entries) { dom_string **new_alloc = realloc( list->entries, n_entries * sizeof(dom_string *)); if (new_alloc == NULL) { dom_string_unref(value); return DOM_NO_MEM_ERR; } list->entries = new_alloc; list->alloc = n_entries; } /* And now parse those entries into the buffer */ for (pos = dom_string_data(value), remaining = dom_string_length(value), n_entries = 0; remaining > 0;) { if (*pos != ' ') { const char *s = pos; while (*pos != ' ' && remaining > 0) { pos++; remaining--; } exc = dom_string_create_interned((const uint8_t *)s, pos - s, &temp); if (exc != DOM_NO_ERR) { dom_string_unref(value); return exc; } found = false; for (check = 0; check < list->len; check++) { if (dom_string_isequal(temp, list->entries[check])) { found = true; break; } } if (found == true) { dom_string_unref(temp); } else { list->entries[list->len] = temp; list->len++; } } else { while (*pos == ' ' && remaining > 0) { pos++; remaining--; } } } dom_string_unref(value); list->needs_parse = false; return DOM_NO_ERR; } static dom_exception _dom_tokenlist_reify(dom_tokenlist *list) { dom_exception exc; uint32_t nchars = 0, n; char *buffer, *next; dom_string *output; if (list->len == 0) { if (list->last_set != NULL) { dom_string_unref(list->last_set); } list->last_set = dom_string_ref( list->ele->base.owner->_memo_empty); return dom_element_set_attribute(list->ele, list->attr, list->last_set); } for (n = 0; n < list->len; ++n) nchars += dom_string_length(list->entries[n]); buffer = calloc(1, nchars + list->len); if (buffer == NULL) return DOM_NO_MEM_ERR; for (next = buffer, n = 0; n < list->len; ++n) { uint32_t slen = dom_string_length(list->entries[n]); memcpy(next, dom_string_data(list->entries[n]), slen); next[slen] = ' '; next += slen + 1; } exc = dom_string_create_interned((const uint8_t *)buffer, nchars + list->len - 1, &output); free(buffer); if (exc != DOM_NO_ERR) return exc; if (list->last_set != NULL) { dom_string_unref(list->last_set); } list->last_set = output; return dom_element_set_attribute(list->ele, list->attr, list->last_set); } /**********************************************************************************/ /** * Create a tokenlist * * \param ele The element which owns the tokenlist attribute * \param attr The name of the attribute we are treating as a tokenlist * \param list The tokenlist output which is set on success * \return DOM_NO_ERR on success, DOM_NO_MEM_ERR on memory exhaustion * * The returned list will already be referenced, so the client need not * do so explicitly. The client must unref the list once finished with it. * * This list will take its own references to ::ele and ::attr */ dom_exception dom_tokenlist_create(dom_element *ele, dom_string *attr, dom_tokenlist **list) { dom_tokenlist *l; dom_exception exc; l = calloc(1, sizeof(dom_tokenlist)); if (l == NULL) return DOM_NO_MEM_ERR; l->refcnt = 1; l->ele = (dom_element *)dom_node_ref(ele); l->attr = dom_string_ref(attr); l->needs_parse = true; exc = dom_event_listener_create(_dom_tokenlist_handle_attrmodified, l, &l->listener); if (exc != DOM_NO_ERR) goto fail; exc = dom_event_target_add_event_listener( ele, ele->base.owner->_memo_domattrmodified, l->listener, false); if (exc != DOM_NO_ERR) goto fail; *list = l; return DOM_NO_ERR; fail: if (l->listener != NULL) dom_event_listener_unref(l->listener); dom_node_unref(l->ele); dom_string_unref(l->attr); free(l); return exc; } /** * Claim a ref on a tokenlist * * \param list The tokenlist to claim a ref on */ void dom_tokenlist_ref(dom_tokenlist *list) { assert(list != NULL); list->refcnt++; } /** * Release a ref on a tokenlist * * \param list The list to release the reference of * * If you release the last ref, this cleans up the tokenlist */ void dom_tokenlist_unref(dom_tokenlist *list) { assert(list != NULL); if (--list->refcnt > 0) return; if (list->alloc > 0) { while (list->len-- > 0) dom_string_unref(list->entries[list->len]); free(list->entries); } dom_event_target_remove_event_listener( list->ele, list->ele->base.owner->_memo_domattrmodified, list->listener, false); dom_event_listener_unref(list->listener); if (list->last_set != NULL) dom_string_unref(list->last_set); dom_string_unref(list->attr); dom_node_unref(list->ele); free(list); } /** * Get the length of the tokenlist * * \param list The list to get the length of * \param length Length of the list outputs here * \return DOM_NO_ERR on success, otherwise the failure code */ dom_exception dom_tokenlist_get_length(dom_tokenlist *list, uint32_t *length) { dom_exception exc; assert(list != NULL); exc = _dom_tokenlist_reparse(list); if (exc != DOM_NO_ERR) return exc; *length = list->len; return DOM_NO_ERR; } /** * Get a particular item from the tokenlist * * \param list The list to retrieve the item from * \param index The index of the item to retrieve * \param value The value of the item returns here * \return DOM_NO_ERR on success, otherwise the failure code */ dom_exception _dom_tokenlist_item(dom_tokenlist *list, uint32_t index, dom_string **value) { dom_exception exc; assert(list != NULL); exc = _dom_tokenlist_reparse(list); if (exc != DOM_NO_ERR) return exc; if (index >= list->len) { *value = NULL; return DOM_NO_ERR; } *value = dom_string_ref(list->entries[index]); return DOM_NO_ERR; } /** * Retrieve the value of the tokenlist as a string * * \param list The list to retrieve the value of * \param value The value of the list returns here * \return DOM_NO_ERR on success, otherwise the failure code */ dom_exception dom_tokenlist_get_value(dom_tokenlist *list, dom_string **value) { assert(list != NULL); return dom_element_get_attribute(list->ele, list->attr, value); } /** * Set the value of the tokenlist as a string * * \param list The list to set the value of * \param value The value to set * \return DOM_NO_ERR on success, otherwise the failure code * */ dom_exception dom_tokenlist_set_value(dom_tokenlist *list, dom_string *value) { assert(list != NULL); return dom_element_set_attribute(list->ele, list->attr, value); } /** * Check if the given value is in the tokenlist * * \param list The list to scan for the given value * \param value The value to look for in the token list * \param contains This will be set based on whether or not the value is present * \return DOM_NO_ERR on success, otherwise the failure code */ dom_exception dom_tokenlist_contains(dom_tokenlist *list, dom_string *value, bool *contains) { dom_exception exc; uint32_t n; assert(list != NULL); exc = _dom_tokenlist_reparse(list); if (exc != DOM_NO_ERR) return exc; *contains = false; for (n = 0; n < list->len; n++) { if (dom_string_isequal(value, list->entries[n])) { *contains = true; break; } } return DOM_NO_ERR; } /** * Add the given value to the tokenlist * * \param list The list to add to * \param value The value to add * \return DOM_NO_ERR on success, otherwise the failure code */ dom_exception dom_tokenlist_add(dom_tokenlist *list, dom_string *value) { dom_exception exc; bool present = false; assert(list != NULL); exc = dom_tokenlist_contains(list, value, &present); if (exc != DOM_NO_ERR) return exc; if (present == true) return DOM_NO_ERR; exc = _dom_tokenlist_make_room(list); if (exc != DOM_NO_ERR) return exc; list->entries[list->len++] = dom_string_ref(value); exc = _dom_tokenlist_reify(list); return exc; } /** * Remove the given value from the tokenlist * * \param list The list to remove from * \param value The value to remove * \return DOM_NO_ERR on success, otherwise the failure code */ dom_exception dom_tokenlist_remove(dom_tokenlist *list, dom_string *value) { dom_exception exc; uint32_t n, m; assert(list != NULL); exc = _dom_tokenlist_reparse(list); if (exc != DOM_NO_ERR) return false; for (n = 0; n < list->len; ++n) { if (dom_string_isequal(value, list->entries[n])) { dom_string_unref(list->entries[n]); for (m = n + 1; m < list->len; ++m) { list->entries[m - 1] = list->entries[m]; } list->len--; break; } } exc = _dom_tokenlist_reify(list); return exc; }