Source: personalizePage.js

/**
 * Personalize the page.
 *
 * This is done by using the options object as a blueprint for what attributes to set on which DOM nodes.
 *
 * @example Set a `src` attribute on an `img` tag
 *
 * window.lr_analytics.personalizePage({
 *   items: [
 *     {
 *       selector: 'img',
 *       attribute: 'src',
 *       source: 'https://example.org/img'
 *     }
 *   ]
 * });
 *
 * @example Remove the `hidden` attribute on a DOM node
 * window.lr_analytics.personalizePage({
 *   items: [
 *     {
 *       selector: 'div',
 *       attribute: 'hidden',
 *       source: false
 *     }
 *   ]
 * });
 *
 * @example Add a css class to a DOM node after modifying it
 * window.lr_analytics.personalizePage({
 *   items: [
 *     {
 *       selector: '#personalize-link',
 *       attribute: 'href',
 *       source: 'https://some-new-link.com',
 *       addClass: ['highlight']
 *     }
 *   ]
 * });
 *
 *
 * @param {Object} options - Object specifying how to personalize the page.
 * @param {Object[]} options.items - List of DOM nodes whose content we want to swap out.
 * @param {string} options.items[].selector - CSS selector for the node
 * @param {*} options.items[].source - The value to set the attribute to. If it is a function, it will be evaluated and the result will be used
 * @param {string} options.items[].attribute - The name of the node attribute to set (e.g. `src` for images and scripts).
 * @param {string[]} options.items[].addClass - List of css classes to add to the node.
 * @param {string[]} options.items[].removeClass - List of css classes to remove from the node.
 */
export function personalizePage({ items }) {
  if (!items) {
    throw new Error("`options.items` is required");
  }

  if (!Array.isArray(items)) {
    throw new Error("`options.items` must be an array");
  }

  // see the eslint no-prototype-builtin rule for the reason behind calling `hasOwnProperty` in
  // such an odd manner
  if (
    !(
      items.every((x) => Object.prototype.hasOwnProperty.call(x, "selector")) &&
      items.every((x) => Object.prototype.hasOwnProperty.call(x, "source")) &&
      items.every((x) => Object.prototype.hasOwnProperty.call(x, "attribute"))
    )
  ) {
    throw new Error(
      "one of the objects is `options.items` has an invalid shape",
    );
  }

  for (const item of items) {
    // This will only throw if the selector is invalid.
    const node = document.querySelector(item.selector);

    if (!node) {
      // skip personalization if we can't find the node
      return;
    }

    if (typeof item.source === "function") {
      node[item.attribute] = item.source();
    } else {
      node[item.attribute] = item.source;
    }

    const { addClass = [], removeClass = [] } = item;

    // Personalizing a page sometimes invovles adding/removing css classes to indicate
    // that a node has been personalized
    node.classList.add(...addClass);
    node.classList.remove(...removeClass);
  }
}