# All files / src/variables VariableProcessor.ts

68.25% Statements 43/63
20% Branches 5/25
72.22% Functions 13/18
69.35% Lines 43/62

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    2737x

    7x
    7x
    7x
    7x
    7x

    7x

    83x

    83x

    83x

    83x

    83x
    89x
    89x

    99x

    9x

    9x

    9x

    2x
    2x
    5x

    224x

    224x

    224x
    224x

    224x

    224x

    224x
    236x
    236x

    224x

    224x
    224x

    224x
    224x

    224x

    224x

    224x

    224x

    224x
    224x
    224x

    224x

     import cheerio from 'cheerio';

    import { ATTRIB_CWF } from '../constants';
    import { PageSources } from '../Page/PageSources';
    import { VariableRenderer } from './VariableRenderer';
    import * as logger from '../utils/logger';
    import * as urlUtil from '../utils/urlUtil';
    import type { Context } from '../html/Context';
    import { MbNode } from '../utils/node';

    /**
     * All variable extraction and rendering is done here.
     *
     * There are four broad categories of methods here
     * 1. Site level variable extraction and storage methods
     *    These methods provide interfaces to store a particular (sub)site's variables,
     *    rendering them with the same (sub)site's previously extracted variables
     *    before doing so in {@link userDefinedVariablesMap}.
     *
     * 2. Site level variable rendering methods
     *    These methods use the processed variables in {@link userDefinedVariablesMap}
     *    to render provided string content that belongs to a provided content file path.
     *    Using the content file path, we are able to get the appropriate (sub)site's (that
     *    the content belongs to) variables and then render them with it.
     *
     * 3. Page level variable extraction and storage methods
     *    These methods are similar to (1), but extract variables from a page,
     *    declared by <variable>s or <import>s.
     *    There are also some utility methods here facilitating extraction of <variable>s
     *    the are nested within an <include> element, and inline variables ('var-xx') declared
     *    as attributes in the <include> itself.
     *
     * 4. Combined page and site variable rendering methods
     *    These methods are similar to (2), but in addition to the site variables, they
     *    render the content with the page variables extracted from (3) as well.
     */
    export class VariableProcessor {
      /**
       * Map of sites' root paths to their variables
       */
      userDefinedVariablesMap: {
        [rootPath: string]: Record<string, any>,
      } = {};

      /**
       * Map of sites' root paths to the respective VariableRenderer instances
       */
      variableRendererMap: {
        [rootPath: string]: VariableRenderer
      } = {};

      constructor(
        private rootPath: string,
        /**
         * Set of sites' root paths, for resolving the provided file path in
         * rendering methods to the appropriate (sub)site's root path.
         */
        private baseUrlMap: Set<string>,
      ) {
        // Set up userDefinedVariablesMap and variableRendererMap
        this.baseUrlMap.forEach((siteRootPath) => {
          this.userDefinedVariablesMap[siteRootPath] = {};
          this.variableRendererMap[siteRootPath] = new VariableRenderer(siteRootPath);
        });
      }

      /*
       * --------------------------------------------------
       * Utility methods
       */

      invalidateCache() {
        Object.values(this.variableRendererMap).forEach(variableRenderer => variableRenderer.invalidateCache());
      }

      /*
       * --------------------------------------------------
       * Site level variable storage methods
       */

      /**
       * Adds a variable for a site with the specified name and value.
       * @param site path of the site, as reflected by the Site instance's baseUrlMap
       * @param name of the variable to add
       * @param value of the variable
       */
      addUserDefinedVariable(site: string, name: string, value: any) {
        this.userDefinedVariablesMap[site][name] = value;
      }

      /**
       * Renders the variable in addition to adding it, unlike {@link addUserDefinedVariable}.
       * This is to allow using previously declared site variables in site variables declared later on.
       */
      renderAndAddUserDefinedVariable(site: string, name: string | undefined, value: any) {
        Iif (name === undefined) {
          logger.warn(
            `Variable with no name detected! It will be ignored. Value: ${value}. Site: ${site}.`,
          );
          logger.warn('Check _markbind/variables.md for variable declarations.');
          return;
        }

        const renderedVal = this.variableRendererMap[site].renderString(value, this.userDefinedVariablesMap[site],
                                                                        new PageSources());
        this.addUserDefinedVariable(site, name, renderedVal);
      }

      /**
       * Version of {@link addUserDefinedVariable} that adds to all sites.
       */
      addUserDefinedVariableForAllSites(name: string, value: any) {
        Object.keys(this.userDefinedVariablesMap)
          .forEach(base => this.addUserDefinedVariable(base, name, value));
      }

      resetUserDefinedVariablesMap() {
        this.userDefinedVariablesMap = {};
        this.baseUrlMap.forEach((siteRootPath) => {
          this.userDefinedVariablesMap[siteRootPath] = {};
        });
      }

      /**
       * Retrieves the appropriate **(sub)site** variables of the supplied file path.
       * @param contentFilePath to look up.
       * @return {*} The appropriate (closest upwards) site variables map
       */
      private getParentSiteVariables(contentFilePath: string) {
        const parentSitePath = urlUtil.getParentSiteAbsolutePath(contentFilePath, this.rootPath,
                                                                 this.baseUrlMap);
        return this.userDefinedVariablesMap[parentSitePath];
      }

      /*
       * --------------------------------------------------
       * Site level variable rendering methods
       */

      /**
       * Renders content belonging to a specified file path with the appropriate site's variables,
       * with an optional set of lower and higher priority variables than the site variables to be rendered.
       * @param contentFilePath of the specified content to render
       * @param {PageSources} pageSources to add dependencies found during nunjucks rendering to
       * @param lowerPriorityVariables than the site variables, if any
       */
      renderWithSiteVariables(
        contentFilePath: string,
        pageSources: PageSources,
        lowerPriorityVariables: Record<string, any> = {},
      ) {
        const userDefinedVariables = this.getParentSiteVariables(contentFilePath);
        const parentSitePath = urlUtil.getParentSiteAbsolutePath(contentFilePath, this.rootPath,
                                                                 this.baseUrlMap);

        return this.variableRendererMap[parentSitePath].renderFile(contentFilePath, {
          ...lowerPriorityVariables,
          ...userDefinedVariables,
        }, pageSources);
      }

      /*
       * --------------------------------------------------
       * Page level variable storage methods
       */

      /**
       * Extracts variables specified as <include var-xx="..."> in include elements.
       * @param includeElement to extract inline variables from
       */
      private static extractIncludeInlineVariables(includeElement: MbNode) {
        const includeInlineVariables: Record<string, any> = {};

        Object.entries(includeElement.attribs || {}).forEach(([attribute, val]) => {
          if (!attribute.startsWith('var-')) {
            return;
          }
          const variableName = attribute.slice(4);
          includeInlineVariables[variableName] = val;
        });

        return includeInlineVariables;
      }

      /**
       * Extracts variables specified as <variable> in include elements.
       * @param includeElement to search child nodes for
       */
      private static extractIncludeChildElementVariables(includeElement: MbNode) {
        if (!(includeElement.children && includeElement.children.length)) {
          return {};
        }
        const includeChildVariables: Record<string, string> = {};

        includeElement.children.forEach((child) => {
          Iif (child.name !== 'variable' && child.name !== 'span') {
            return;
          }
          const variableName = child.attribs?.name || child.attribs?.id;
          Iif (!variableName) {
            logger.warn(`Missing 'name' or 'id' in variable for ${includeElement.attribs.src}'s include in ${
              includeElement.attribs[ATTRIB_CWF]}.\n`);
            return;
          }
          Iif (!includeChildVariables[variableName]) {
            includeChildVariables[variableName] = cheerio(child).html() || '';
          }
        });

        return includeChildVariables;
      }

      /**
       * Extract variables from an <include> element.
       * See https://markbind.org/userGuide/reusingContents.html#specifying-variables-in-an-include for usage.
       * It is a subroutine for {@link renderIncludeFile}
       * @param includeElement include element to extract variables from
       */
      private static extractIncludeVariables(includeElement: MbNode) {
        const includeInlineVariables = VariableProcessor.extractIncludeInlineVariables(includeElement);
        const includeChildVariables = VariableProcessor.extractIncludeChildElementVariables(includeElement);

        return {
          ...includeChildVariables,
          ...includeInlineVariables,
        };
      }

      /*
       * --------------------------------------------------
       * Combined (both site and page variables) rendering methods
       */

      /**
       * Renders an <include> file with the supplied context, returning the rendered
       * content and new context with respect to the child content.
       * @param filePath of the included file source
       * @param pageSources to add dependencies found during nunjucks rendering to
       * @param node of the include element
       * @param context object containing the parent <include> (if any) variables for the current
       *        context, which has greater priority than the extracted include variables of the current context.
       * @param asIfAt where the included file should be rendered from
       * @return object containing the nunjucks-processed content, and a new {@link Context} object
       */
      renderIncludeFile(
        filePath: string,
        pageSources: PageSources,
        node: MbNode,
        context: Context,
        asIfAt: string,
      ) {
        // Extract included variables from the include element, merging with the parent context variables
        const includeVariables = VariableProcessor.extractIncludeVariables(node);

        const variables = {
          ...includeVariables,
          ...context.variables,
        };

        // Render the included content with the current <include>'s <variable>s and any parent <include>'s
        const renderedContent = this.renderWithSiteVariables(filePath, pageSources, variables);

        const childContext = context.clone();
        childContext.cwf = asIfAt;
        childContext.variables = variables;

        return {
          nunjucksProcessed: renderedContent,
          childContext,
        };
      }
    }