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, likecss``
. 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, likestyled(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, likestyled.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, likestyled``
. 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 aCalleeParam
representing thestyled
function. - The second element is a
MemberParam
representing thediv
member of thestyled
object. - The third element is a
TemplateParam
representing the template literalcolor: 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.