How to use
Custom Tagged Template

Creating Custom Tagged Template Processors

In the wyw-in-js toolkit, you can create custom processors for handling tagged templates. This guide will walk you through the process of creating custom tagged template processors.

Overview

A tagged template processor is a class that extends the TaggedTemplateProcessor class from the @wyw-in-js/processor-utils package. This class provides a set of methods and properties that you can override to customize the behavior of the processor.

Creating a Processor

To create a processor, you need to define a class that extends TaggedTemplateProcessor. The constructor of your class should call the super method with the appropriate parameters.

import { TaggedTemplateProcessor } from '@wyw-in-js/processor-utils';
 
export default class MyProcessor extends TaggedTemplateProcessor {
  constructor(params: Params, ...args: TailProcessorParams) {
    super(params, ...args);
  }
}

The Params type is an array of Param types, which represent the different ways a custom processor can be called. Each Param type corresponds to a different call pattern:

  • CalleeParam: This represents a call where the processor is directly invoked with a template literal, like css``. The first element of the array is the string 'callee', and the second element is an Identifier or MemberExpression, which represents the processor being called (e.g., css).

  • CallParam: This represents a call where the processor is invoked with a function call, like styled(SomeTag)``. The first element of the array is the string 'call', and the rest of the elements are ExpressionValues, which represent the arguments passed to the function call.

  • MemberParam: This represents a call where the processor is invoked on a member, like styled.div``. The first element of the array is the string 'member', and the second element is a string, which represents the member being accessed (e.g., 'div').

  • TemplateParam: This represents a call where the processor is invoked with a template literal, like styled``. The first element of the array is the string 'template', and the second element is an array of TemplateElements or ExpressionValues, which represent the contents of the template literal.

In summary, the Params type is used to capture the different ways a custom processor can be invoked, and the specific arguments or members used in the invocation.

The styled.div call with a template literal color: red would be represented in the Params type as follows:

const params: Params = [
  ['callee', { /* Identifier or MemberExpression representing 'styled' */ }],
  ['member', 'div'],
  ['template', [
    { /* TemplateElement representing 'color: red' */ }
  ]]
];

In this example:

  • The first element of the Params array is a CalleeParam representing the styled function.
  • The second element is a MemberParam representing the div member of the styled object.
  • The third element is a TemplateParam representing the template literal color: red.

The actual Identifier, MemberExpression, and TemplateElement instances would be generated by a parser like Babel when parsing the source code.

isValidParams

The isValidParams function is a type guard that checks if the provided params array matches the structure defined by the constraints array. It returns a boolean indicating whether the params array is valid according to the constraints.

The constraints array is an array of ParamConstraint types, which can be a ParamName (i.e., 'callee', 'call', 'member', 'template'), an array of ParamName, or a wildcard '*'. The '...' in the constraints array indicates that any number of any type of params can follow.

The function iterates over the params and constraints arrays simultaneously. If it encounters a '...' in the constraints, it immediately returns true, as '...' allows any number of any params. If it encounters a '*', it checks if the corresponding param is defined. If it is not, it returns false. If the constraint is an array, it checks if the param's type is in the array. If it is not, it returns false. If the constraint is a ParamName, it checks if the param's type matches the ParamName. If it does not, it returns false.

If the function iterates over all the params and constraints without returning false, it returns true, indicating that the params array is valid according to the constraints.

Overriding Methods

The TaggedTemplateProcessor class provides several methods that you can override to customize the behavior of your processor.

get asSelector()

This getter should return a string that represents the CSS selector for the styles defined in the tagged template.

get value()

This getter should return an Expression that represents the value of the tagged template. This value will replace the tagged template at runtime.

For simple cases like css, it is just a string literal with a class name. For styled, it is an object with some metadata, such as the class name and information about extending.

Here is an example of how the value getter might be implemented in a class that extends TaggedTemplateProcessor:

public override get value(): ObjectExpression {
  const t = this.astService;
  return t.objectExpression([
    t.objectProperty(
      t.stringLiteral('className'),
      t.stringLiteral(this.className)
    ),
    t.objectProperty(
      t.stringLiteral('extends'),
      this.extendsNode
        ? t.callExpression(t.identifier(this.extendsNode), [])
        : t.nullLiteral()
    ),
  ]);
}

In this example, the value getter returns an ObjectExpression that includes the class name and information about extending. The extendsNode is a hypothetical property that represents the node that the current node extends. If extendsNode is not defined, a nullLiteral is returned.

addInterpolation(node: Expression, precedingCss: string, source: string, unit = '')

This method is called when an interpolation is found in the tagged template. It should return a string that represents the identifier of the interpolation.

doEvaltimeReplacement()

This method is called during the evaluation phase. It should replace the tagged template with the appropriate value.

doRuntimeReplacement()

This method is called during the runtime phase. It should replace the tagged template with the appropriate value.

extractRules(valueCache: ValueCache, cssText: string, loc?: SourceLocation | null)

This method is called to extract the CSS rules from the tagged template. It should return an object that represents the CSS rules.

toString()

This method should return a string that represents the source code of the tagged template.

Example

Here is an example of a custom processor that handles styled tagged templates:

import { TaggedTemplateProcessor, validateParams } from '@wyw-in-js/processor-utils';
import type { Params, TailProcessorParams } from '@wyw-in-js/processor-utils';
 
export default class StyledProcessor extends TaggedTemplateProcessor {
  constructor(params: Params, ...args: TailProcessorParams) {
    validateParams(params, ['callee', '*', '...'], TaggedTemplateProcessor.SKIP);
    super(params, ...args);
  }
 
  public override get asSelector(): string {
    return `.${this.className}`;
  }
 
  // ... other methods
}

In this example, the StyledProcessor class extends TaggedTemplateProcessor and overrides several methods to handle styled tagged templates. The asSelector getter returns a CSS class selector based on the className property of the processor.

Please refer to the css.ts and styled.ts files for more detailed examples of custom processors.