applyPostProcess = (postProcess, value, shared = {}) => {
  shared.data ??= {};
  shared.uiSettings ??= {};

  if (!postProcess) { return value; }
  const postProcessors = Array.isArray(postProcess) ? postProcess : [postProcess]; // ensure array
  for (const postProcessor of postProcessors) {
    let postProcessorName = postProcessor;
    let args = {};
    // object processors (processors with one or more field(s))
    if (typeof postProcessor !== 'string') {
      // check if the (object) post-processor has a key matching the name of an existing post-processor
      const argKeys = Object.keys(postProcessor);
      const possiblePostProcessorName = argKeys?.find(key => customFilters[key]);
      if (typeof possiblePostProcessorName === 'string') {
        postProcessorName = possiblePostProcessorName;

        // the arg specifying the processor's name is renamed to 'param'
        args = { ...postProcessor, param: postProcessor[possiblePostProcessorName] };
        delete args[possiblePostProcessorName];

        // resolve values from settings, data, or escaping
        args = resolvePostProcessorArgs(args, value, shared);
      } else {
        // not a valid postProcessor -- stringified for better debugging
        console.warn(`Invalid postProcess ${JSON.stringify(postProcessor)}`);
        continue;
      }
    }

    // in order of preference: first try using a custom filter, then an existing vue-filter, or none (no filter found)
    const customFilterFunc = customFilters[postProcessorName];
    const defaultFilterFunc = filters[postProcessorName];
    if (customFilterFunc) {
      // value used in a post-processor can be overridden with the 'value' field
      const inputValue = Object.keys(args)?.includes('value') ? args.value : value;
      value = customFilterFunc(inputValue, args, shared);
    } else if (defaultFilterFunc) {
      value = defaultFilterFunc(value);
    } else {
      console.warn(`No such postProcess ${postProcessorName}`);
    }
  }
  return value;
}
Runs a value through a set of named functions or 'filters'
  parameterless PostProcessors are strings
  object PostProcessors are identified by the key for their primary argument ('param'),
    with the option of additional arguments

@param {PostProcessor|PostProcessor[]} postProcess
@param {*} value
@param {Object<string,*>} shared
@returns {*} the formatted displayValue (or intermediate value during recursive post-processing)