pte-interpolation-react 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # pte-interpolation-react
2
+
3
+ [![npm version](https://img.shields.io/npm/v/pte-interpolation-react.svg?style=flat-square)](https://www.npmjs.com/package/pte-interpolation-react)
4
+
5
+ React rendering library that resolves dynamic variable inline blocks in [Portable Text](https://portabletext.org/) to actual values. Wraps [`@portabletext/react`](https://github.com/portabletext/react-portabletext) with automatic variable substitution.
6
+
7
+ Part of [sanity-pte-interpolation](https://github.com/jordanl17/sanity-pte-interpolation). For adding variable picker inline blocks to Sanity Studio, see [`sanity-plugin-pte-interpolation`](https://www.npmjs.com/package/sanity-plugin-pte-interpolation).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install pte-interpolation-react @portabletext/react
13
+ ```
14
+
15
+ ### Peer dependencies
16
+
17
+ - `react` ^18.0.0 || ^19.0.0
18
+
19
+ ## Usage
20
+
21
+ ### Drop-in component (recommended)
22
+
23
+ `<InterpolatedPortableText>` is a drop-in replacement for `<PortableText>` that handles variable resolution automatically:
24
+
25
+ ```tsx
26
+ import {InterpolatedPortableText} from 'pte-interpolation-react'
27
+
28
+ function EmailPreview({body, recipient}) {
29
+ return (
30
+ <InterpolatedPortableText
31
+ value={body}
32
+ interpolationValues={{
33
+ firstName: recipient.firstName,
34
+ lastName: recipient.lastName,
35
+ email: recipient.email,
36
+ }}
37
+ />
38
+ )
39
+ }
40
+ ```
41
+
42
+ Variables render as `<span data-variable-key="firstName">Jo</span>`, making them easy to target with CSS for styling or highlighting.
43
+
44
+ ### With custom Portable Text components
45
+
46
+ Pass your own `components` prop and they will be merged with the interpolation types:
47
+
48
+ ```tsx
49
+ <InterpolatedPortableText
50
+ value={body}
51
+ interpolationValues={values}
52
+ components={{
53
+ marks: {
54
+ link: ({children, value}) => <a href={value.href}>{children}</a>,
55
+ },
56
+ }}
57
+ />
58
+ ```
59
+
60
+ ### Custom fallback for missing values
61
+
62
+ By default, unresolved variables render as `{variableKey}` (e.g. `{firstName}`). Provide a `fallback` function to customise this:
63
+
64
+ ```tsx
65
+ <InterpolatedPortableText
66
+ value={body}
67
+ interpolationValues={values}
68
+ fallback={(variableKey) => `[missing: ${variableKey}]`}
69
+ />
70
+ ```
71
+
72
+ ### Low-level API
73
+
74
+ If you need full control over component merging, use `createInterpolationComponents` directly with `<PortableText>` from `@portabletext/react`:
75
+
76
+ ```tsx
77
+ import {useMemo} from 'react'
78
+ import {PortableText} from '@portabletext/react'
79
+ import {createInterpolationComponents} from 'pte-interpolation-react'
80
+
81
+ function EmailPreview({body, values, customComponents}) {
82
+ const components = useMemo(() => {
83
+ const interpolation = createInterpolationComponents(values)
84
+ return {
85
+ ...customComponents,
86
+ types: {
87
+ ...customComponents.types,
88
+ ...interpolation.types,
89
+ },
90
+ }
91
+ }, [values, customComponents])
92
+
93
+ return <PortableText value={body} components={components} />
94
+ }
95
+ ```
96
+
97
+ ## Authoring Variables in Sanity Studio
98
+
99
+ This package handles the **rendering** side. To add the variable picker to Sanity Studio's Portable Text Editor, use [`sanity-plugin-pte-interpolation`](https://www.npmjs.com/package/sanity-plugin-pte-interpolation):
100
+
101
+ ```ts
102
+ import {interpolationVariables} from 'sanity-plugin-pte-interpolation'
103
+
104
+ defineField({
105
+ name: 'body',
106
+ type: 'array',
107
+ of: [
108
+ interpolationVariables([
109
+ {id: 'firstName', name: 'First name'},
110
+ {id: 'email', name: 'Email address'},
111
+ ]),
112
+ ],
113
+ })
114
+ ```
115
+
116
+ ## Data Shape
117
+
118
+ Variable blocks in stored Portable Text look like this:
119
+
120
+ ```json
121
+ {
122
+ "_type": "block",
123
+ "children": [
124
+ {"_type": "span", "text": "Hello, "},
125
+ {"_type": "pteInterpolationVariable", "variableKey": "firstName"},
126
+ {"_type": "span", "text": "!"}
127
+ ]
128
+ }
129
+ ```
130
+
131
+ The `variableKey` maps to the `id` defined in the Studio variable definitions and the keys in the `interpolationValues` record.
132
+
133
+ ## API Reference
134
+
135
+ ### `<InterpolatedPortableText>`
136
+
137
+ | Prop | Type | Description |
138
+ | --------------------- | --------------------------------- | -------------------------------------------------------------------------- |
139
+ | `value` | `PortableTextBlock[]` | Portable Text content from Sanity |
140
+ | `interpolationValues` | `Record<string, string>` | Map of variable IDs to their resolved values |
141
+ | `components` | `PortableTextComponents` | Optional custom Portable Text components (merged with interpolation types) |
142
+ | `fallback` | `(variableKey: string) => string` | Optional function for unresolved variables (defaults to `{variableKey}`) |
143
+
144
+ Also accepts all other props from `@portabletext/react`'s `<PortableText>`.
145
+
146
+ ### `createInterpolationComponents(values, fallback?)`
147
+
148
+ Returns a `PortableTextComponents` object with a `types.pteInterpolationVariable` renderer. Use this when you need manual control over component merging.
149
+
150
+ | Parameter | Type | Description |
151
+ | ---------- | --------------------------------- | ------------------------------------------ |
152
+ | `values` | `Record<string, string>` | Map of variable IDs to values |
153
+ | `fallback` | `(variableKey: string) => string` | Optional fallback for unresolved variables |
154
+
155
+ ### `VARIABLE_TYPE_PREFIX`
156
+
157
+ The constant `'pteInterpolationVariable'` - the `_type` string used for variable inline blocks. Exported for advanced use cases.
158
+
159
+ ## License
160
+
161
+ MIT
package/dist/index.cjs CHANGED
@@ -1,8 +1,56 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var jsxRuntime = require("react/jsx-runtime");
4
- function InterpolatedText({ blocks, values }) {
5
- return /* @__PURE__ */ jsxRuntime.jsx("pre", { children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: JSON.stringify({ blocks, values }, null, 2) }) });
3
+ var jsxRuntime = require("react/jsx-runtime"), react$1 = require("@portabletext/react"), react = require("react"), pteInterpolationCore = require("pte-interpolation-core");
4
+ function defaultFallback(variableKey) {
5
+ return `{${variableKey}}`;
6
6
  }
7
- exports.InterpolatedText = InterpolatedText;
7
+ function createInterpolationComponents(values, fallback = defaultFallback) {
8
+ function VariableComponent(props) {
9
+ const { variableKey } = props.value, resolvedValue = values[variableKey] !== void 0 ? values[variableKey] : fallback(variableKey);
10
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-variable-key": variableKey, children: resolvedValue });
11
+ }
12
+ return {
13
+ types: {
14
+ [pteInterpolationCore.VARIABLE_TYPE_PREFIX]: VariableComponent
15
+ }
16
+ };
17
+ }
18
+ function InterpolatedPortableText({
19
+ interpolationValues,
20
+ components: userComponents,
21
+ fallback,
22
+ ...rest
23
+ }) {
24
+ const mergedComponents = react.useMemo(() => {
25
+ const interpolationComponents = createInterpolationComponents(interpolationValues, fallback);
26
+ return userComponents ? {
27
+ ...userComponents,
28
+ types: {
29
+ ...userComponents.types,
30
+ ...interpolationComponents.types
31
+ }
32
+ } : interpolationComponents;
33
+ }, [interpolationValues, fallback, userComponents]);
34
+ return /* @__PURE__ */ jsxRuntime.jsx(react$1.PortableText, { ...rest, components: mergedComponents });
35
+ }
36
+ Object.defineProperty(exports, "VARIABLE_TYPE_PREFIX", {
37
+ enumerable: !0,
38
+ get: function() {
39
+ return pteInterpolationCore.VARIABLE_TYPE_PREFIX;
40
+ }
41
+ });
42
+ Object.defineProperty(exports, "extractVariableKeys", {
43
+ enumerable: !0,
44
+ get: function() {
45
+ return pteInterpolationCore.extractVariableKeys;
46
+ }
47
+ });
48
+ Object.defineProperty(exports, "interpolateToString", {
49
+ enumerable: !0,
50
+ get: function() {
51
+ return pteInterpolationCore.interpolateToString;
52
+ }
53
+ });
54
+ exports.InterpolatedPortableText = InterpolatedPortableText;
55
+ exports.createInterpolationComponents = createInterpolationComponents;
8
56
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../src/InterpolatedText.tsx"],"sourcesContent":["import type {InterpolatedTextProps} from './types'\n\n/** @public */\nexport function InterpolatedText({blocks, values}: InterpolatedTextProps) {\n return (\n <pre>\n <code>{JSON.stringify({blocks, values}, null, 2)}</code>\n </pre>\n )\n}\n"],"names":["jsx"],"mappings":";;;AAGO,SAAS,iBAAiB,EAAC,QAAQ,UAAgC;AACxE,SACEA,2BAAAA,IAAC,OAAA,EACC,UAAAA,2BAAAA,IAAC,QAAA,EAAM,UAAA,KAAK,UAAU,EAAC,QAAQ,OAAA,GAAS,MAAM,CAAC,GAAE,GACnD;AAEJ;;"}
1
+ {"version":3,"file":"index.cjs","sources":["../src/createInterpolationComponents.tsx","../src/InterpolatedPortableText.tsx"],"sourcesContent":["import type {PortableTextComponents, PortableTextTypeComponentProps} from '@portabletext/react'\n\nimport {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {\n InterpolationFallback,\n InterpolationValues,\n PteInterpolationVariableBlock,\n} from './types'\n\nfunction defaultFallback(variableKey: string): string {\n return `{${variableKey}}`\n}\n\n/** @public */\nexport function createInterpolationComponents(\n values: InterpolationValues,\n fallback: InterpolationFallback = defaultFallback,\n): PortableTextComponents {\n function VariableComponent(props: PortableTextTypeComponentProps<PteInterpolationVariableBlock>) {\n const {variableKey} = props.value\n const resolvedValue =\n values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)\n\n return <span data-variable-key={variableKey}>{resolvedValue}</span>\n }\n\n return {\n types: {\n [VARIABLE_TYPE_PREFIX]: VariableComponent,\n },\n }\n}\n","import {PortableText} from '@portabletext/react'\nimport {useMemo} from 'react'\n\nimport {createInterpolationComponents} from './createInterpolationComponents'\nimport type {InterpolatedPortableTextProps} from './types'\n\n/** @public */\nexport function InterpolatedPortableText({\n interpolationValues,\n components: userComponents,\n fallback,\n ...rest\n}: InterpolatedPortableTextProps) {\n const mergedComponents = useMemo(() => {\n const interpolationComponents = createInterpolationComponents(interpolationValues, fallback)\n\n if (!userComponents) {\n return interpolationComponents\n }\n\n return {\n ...userComponents,\n types: {\n ...userComponents.types,\n ...interpolationComponents.types,\n },\n }\n }, [interpolationValues, fallback, userComponents])\n\n return <PortableText {...rest} components={mergedComponents} />\n}\n"],"names":["jsx","VARIABLE_TYPE_PREFIX","useMemo","PortableText"],"mappings":";;;AASA,SAAS,gBAAgB,aAA6B;AACpD,SAAO,IAAI,WAAW;AACxB;AAGO,SAAS,8BACd,QACA,WAAkC,iBACV;AACxB,WAAS,kBAAkB,OAAsE;AAC/F,UAAM,EAAC,YAAA,IAAe,MAAM,OACtB,gBACJ,OAAO,WAAW,MAAM,SAAY,OAAO,WAAW,IAAI,SAAS,WAAW;AAEhF,WAAOA,2BAAAA,IAAC,QAAA,EAAK,qBAAmB,aAAc,UAAA,eAAc;AAAA,EAC9D;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,MACL,CAACC,yCAAoB,GAAG;AAAA,IAAA;AAAA,EAC1B;AAEJ;ACxBO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAAkC;AAChC,QAAM,mBAAmBC,MAAAA,QAAQ,MAAM;AACrC,UAAM,0BAA0B,8BAA8B,qBAAqB,QAAQ;AAE3F,WAAK,iBAIE;AAAA,MACL,GAAG;AAAA,MACH,OAAO;AAAA,QACL,GAAG,eAAe;AAAA,QAClB,GAAG,wBAAwB;AAAA,MAAA;AAAA,IAC7B,IARO;AAAA,EAUX,GAAG,CAAC,qBAAqB,UAAU,cAAc,CAAC;AAElD,SAAOF,2BAAAA,IAACG,QAAAA,cAAA,EAAc,GAAG,MAAM,YAAY,kBAAkB;AAC/D;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.d.cts CHANGED
@@ -1,16 +1,57 @@
1
+ import {extractVariableKeys} from 'pte-interpolation-core'
2
+ import {interpolateToString} from 'pte-interpolation-core'
3
+ import {InterpolationFallback} from 'pte-interpolation-core'
4
+ import {InterpolationValues} from 'pte-interpolation-core'
1
5
  import {JSX} from 'react/jsx-runtime'
2
- import type {PortableTextBlock} from '@portabletext/react'
6
+ import {PortableTextBlockLike} from 'pte-interpolation-core'
7
+ import {PortableTextChild} from 'pte-interpolation-core'
8
+ import type {PortableTextComponents} from '@portabletext/react'
9
+ import type {PortableTextProps} from '@portabletext/react'
10
+ import {VARIABLE_TYPE_PREFIX} from 'pte-interpolation-core'
3
11
 
4
12
  /** @public */
5
- export declare function InterpolatedText({blocks, values}: InterpolatedTextProps): JSX.Element
13
+ export declare function createInterpolationComponents(
14
+ values: InterpolationValues,
15
+ fallback?: InterpolationFallback,
16
+ ): PortableTextComponents
17
+
18
+ export {extractVariableKeys}
19
+
20
+ /** @public */
21
+ export declare function InterpolatedPortableText({
22
+ interpolationValues,
23
+ components: userComponents,
24
+ fallback,
25
+ ...rest
26
+ }: InterpolatedPortableTextProps): JSX.Element
6
27
 
7
28
  /** @public */
8
- export declare interface InterpolatedTextProps {
9
- blocks: PortableTextBlock[]
10
- values: InterpolationValues
29
+ export declare interface InterpolatedPortableTextProps extends Omit<
30
+ PortableTextProps,
31
+ 'components'
32
+ > {
33
+ interpolationValues: InterpolationValues
34
+ components?: PortableTextComponents
35
+ fallback?: InterpolationFallback
11
36
  }
12
37
 
38
+ export {interpolateToString}
39
+
40
+ export {InterpolationFallback}
41
+
42
+ export {InterpolationValues}
43
+
44
+ export {PortableTextBlockLike}
45
+
46
+ export {PortableTextChild}
47
+
13
48
  /** @public */
14
- export declare type InterpolationValues = Record<string, string>
49
+ export declare interface PteInterpolationVariableBlock {
50
+ _type: 'pteInterpolationVariable'
51
+ _key: string
52
+ variableKey: string
53
+ }
54
+
55
+ export {VARIABLE_TYPE_PREFIX}
15
56
 
16
57
  export {}
package/dist/index.d.ts CHANGED
@@ -1,16 +1,57 @@
1
+ import {extractVariableKeys} from 'pte-interpolation-core'
2
+ import {interpolateToString} from 'pte-interpolation-core'
3
+ import {InterpolationFallback} from 'pte-interpolation-core'
4
+ import {InterpolationValues} from 'pte-interpolation-core'
1
5
  import {JSX} from 'react/jsx-runtime'
2
- import type {PortableTextBlock} from '@portabletext/react'
6
+ import {PortableTextBlockLike} from 'pte-interpolation-core'
7
+ import {PortableTextChild} from 'pte-interpolation-core'
8
+ import type {PortableTextComponents} from '@portabletext/react'
9
+ import type {PortableTextProps} from '@portabletext/react'
10
+ import {VARIABLE_TYPE_PREFIX} from 'pte-interpolation-core'
3
11
 
4
12
  /** @public */
5
- export declare function InterpolatedText({blocks, values}: InterpolatedTextProps): JSX.Element
13
+ export declare function createInterpolationComponents(
14
+ values: InterpolationValues,
15
+ fallback?: InterpolationFallback,
16
+ ): PortableTextComponents
17
+
18
+ export {extractVariableKeys}
19
+
20
+ /** @public */
21
+ export declare function InterpolatedPortableText({
22
+ interpolationValues,
23
+ components: userComponents,
24
+ fallback,
25
+ ...rest
26
+ }: InterpolatedPortableTextProps): JSX.Element
6
27
 
7
28
  /** @public */
8
- export declare interface InterpolatedTextProps {
9
- blocks: PortableTextBlock[]
10
- values: InterpolationValues
29
+ export declare interface InterpolatedPortableTextProps extends Omit<
30
+ PortableTextProps,
31
+ 'components'
32
+ > {
33
+ interpolationValues: InterpolationValues
34
+ components?: PortableTextComponents
35
+ fallback?: InterpolationFallback
11
36
  }
12
37
 
38
+ export {interpolateToString}
39
+
40
+ export {InterpolationFallback}
41
+
42
+ export {InterpolationValues}
43
+
44
+ export {PortableTextBlockLike}
45
+
46
+ export {PortableTextChild}
47
+
13
48
  /** @public */
14
- export declare type InterpolationValues = Record<string, string>
49
+ export declare interface PteInterpolationVariableBlock {
50
+ _type: 'pteInterpolationVariable'
51
+ _key: string
52
+ variableKey: string
53
+ }
54
+
55
+ export {VARIABLE_TYPE_PREFIX}
15
56
 
16
57
  export {}
package/dist/index.js CHANGED
@@ -1,8 +1,45 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- function InterpolatedText({ blocks, values }) {
3
- return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: JSON.stringify({ blocks, values }, null, 2) }) });
2
+ import { PortableText } from "@portabletext/react";
3
+ import { useMemo } from "react";
4
+ import { VARIABLE_TYPE_PREFIX } from "pte-interpolation-core";
5
+ import { VARIABLE_TYPE_PREFIX as VARIABLE_TYPE_PREFIX2, extractVariableKeys, interpolateToString } from "pte-interpolation-core";
6
+ function defaultFallback(variableKey) {
7
+ return `{${variableKey}}`;
8
+ }
9
+ function createInterpolationComponents(values, fallback = defaultFallback) {
10
+ function VariableComponent(props) {
11
+ const { variableKey } = props.value, resolvedValue = values[variableKey] !== void 0 ? values[variableKey] : fallback(variableKey);
12
+ return /* @__PURE__ */ jsx("span", { "data-variable-key": variableKey, children: resolvedValue });
13
+ }
14
+ return {
15
+ types: {
16
+ [VARIABLE_TYPE_PREFIX]: VariableComponent
17
+ }
18
+ };
19
+ }
20
+ function InterpolatedPortableText({
21
+ interpolationValues,
22
+ components: userComponents,
23
+ fallback,
24
+ ...rest
25
+ }) {
26
+ const mergedComponents = useMemo(() => {
27
+ const interpolationComponents = createInterpolationComponents(interpolationValues, fallback);
28
+ return userComponents ? {
29
+ ...userComponents,
30
+ types: {
31
+ ...userComponents.types,
32
+ ...interpolationComponents.types
33
+ }
34
+ } : interpolationComponents;
35
+ }, [interpolationValues, fallback, userComponents]);
36
+ return /* @__PURE__ */ jsx(PortableText, { ...rest, components: mergedComponents });
4
37
  }
5
38
  export {
6
- InterpolatedText
39
+ InterpolatedPortableText,
40
+ VARIABLE_TYPE_PREFIX2 as VARIABLE_TYPE_PREFIX,
41
+ createInterpolationComponents,
42
+ extractVariableKeys,
43
+ interpolateToString
7
44
  };
8
45
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/InterpolatedText.tsx"],"sourcesContent":["import type {InterpolatedTextProps} from './types'\n\n/** @public */\nexport function InterpolatedText({blocks, values}: InterpolatedTextProps) {\n return (\n <pre>\n <code>{JSON.stringify({blocks, values}, null, 2)}</code>\n </pre>\n )\n}\n"],"names":[],"mappings":";AAGO,SAAS,iBAAiB,EAAC,QAAQ,UAAgC;AACxE,SACE,oBAAC,OAAA,EACC,UAAA,oBAAC,QAAA,EAAM,UAAA,KAAK,UAAU,EAAC,QAAQ,OAAA,GAAS,MAAM,CAAC,GAAE,GACnD;AAEJ;"}
1
+ {"version":3,"file":"index.js","sources":["../src/createInterpolationComponents.tsx","../src/InterpolatedPortableText.tsx"],"sourcesContent":["import type {PortableTextComponents, PortableTextTypeComponentProps} from '@portabletext/react'\n\nimport {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {\n InterpolationFallback,\n InterpolationValues,\n PteInterpolationVariableBlock,\n} from './types'\n\nfunction defaultFallback(variableKey: string): string {\n return `{${variableKey}}`\n}\n\n/** @public */\nexport function createInterpolationComponents(\n values: InterpolationValues,\n fallback: InterpolationFallback = defaultFallback,\n): PortableTextComponents {\n function VariableComponent(props: PortableTextTypeComponentProps<PteInterpolationVariableBlock>) {\n const {variableKey} = props.value\n const resolvedValue =\n values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)\n\n return <span data-variable-key={variableKey}>{resolvedValue}</span>\n }\n\n return {\n types: {\n [VARIABLE_TYPE_PREFIX]: VariableComponent,\n },\n }\n}\n","import {PortableText} from '@portabletext/react'\nimport {useMemo} from 'react'\n\nimport {createInterpolationComponents} from './createInterpolationComponents'\nimport type {InterpolatedPortableTextProps} from './types'\n\n/** @public */\nexport function InterpolatedPortableText({\n interpolationValues,\n components: userComponents,\n fallback,\n ...rest\n}: InterpolatedPortableTextProps) {\n const mergedComponents = useMemo(() => {\n const interpolationComponents = createInterpolationComponents(interpolationValues, fallback)\n\n if (!userComponents) {\n return interpolationComponents\n }\n\n return {\n ...userComponents,\n types: {\n ...userComponents.types,\n ...interpolationComponents.types,\n },\n }\n }, [interpolationValues, fallback, userComponents])\n\n return <PortableText {...rest} components={mergedComponents} />\n}\n"],"names":[],"mappings":";;;;;AASA,SAAS,gBAAgB,aAA6B;AACpD,SAAO,IAAI,WAAW;AACxB;AAGO,SAAS,8BACd,QACA,WAAkC,iBACV;AACxB,WAAS,kBAAkB,OAAsE;AAC/F,UAAM,EAAC,YAAA,IAAe,MAAM,OACtB,gBACJ,OAAO,WAAW,MAAM,SAAY,OAAO,WAAW,IAAI,SAAS,WAAW;AAEhF,WAAO,oBAAC,QAAA,EAAK,qBAAmB,aAAc,UAAA,eAAc;AAAA,EAC9D;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,MACL,CAAC,oBAAoB,GAAG;AAAA,IAAA;AAAA,EAC1B;AAEJ;ACxBO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAAkC;AAChC,QAAM,mBAAmB,QAAQ,MAAM;AACrC,UAAM,0BAA0B,8BAA8B,qBAAqB,QAAQ;AAE3F,WAAK,iBAIE;AAAA,MACL,GAAG;AAAA,MACH,OAAO;AAAA,QACL,GAAG,eAAe;AAAA,QAClB,GAAG,wBAAwB;AAAA,MAAA;AAAA,IAC7B,IARO;AAAA,EAUX,GAAG,CAAC,qBAAqB,UAAU,cAAc,CAAC;AAElD,SAAO,oBAAC,cAAA,EAAc,GAAG,MAAM,YAAY,kBAAkB;AAC/D;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pte-interpolation-react",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "React components for rendering Portable Text with interpolated values",
@@ -40,13 +40,18 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@portabletext/react": "^6.0.2"
43
+ "@portabletext/react": "^6.0.2",
44
+ "pte-interpolation-core": "1.1.0"
44
45
  },
45
46
  "peerDependencies": {
46
47
  "react": "^18.0.0 || ^19.0.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@sanity/pkg-utils": "^10.4.4",
51
+ "@types/react": "^19.2.14",
52
+ "@types/react-dom": "^19.2.3",
53
+ "react": "^19.2.4",
54
+ "react-dom": "^19.2.4",
50
55
  "rimraf": "^6.1.3",
51
56
  "typescript": "~5.9.3"
52
57
  },
@@ -0,0 +1,31 @@
1
+ import {PortableText} from '@portabletext/react'
2
+ import {useMemo} from 'react'
3
+
4
+ import {createInterpolationComponents} from './createInterpolationComponents'
5
+ import type {InterpolatedPortableTextProps} from './types'
6
+
7
+ /** @public */
8
+ export function InterpolatedPortableText({
9
+ interpolationValues,
10
+ components: userComponents,
11
+ fallback,
12
+ ...rest
13
+ }: InterpolatedPortableTextProps) {
14
+ const mergedComponents = useMemo(() => {
15
+ const interpolationComponents = createInterpolationComponents(interpolationValues, fallback)
16
+
17
+ if (!userComponents) {
18
+ return interpolationComponents
19
+ }
20
+
21
+ return {
22
+ ...userComponents,
23
+ types: {
24
+ ...userComponents.types,
25
+ ...interpolationComponents.types,
26
+ },
27
+ }
28
+ }, [interpolationValues, fallback, userComponents])
29
+
30
+ return <PortableText {...rest} components={mergedComponents} />
31
+ }
@@ -0,0 +1,204 @@
1
+ import type {PortableTextComponents} from '@portabletext/react'
2
+ import {renderToStaticMarkup} from 'react-dom/server'
3
+ import {describe, expect, it} from 'vitest'
4
+
5
+ import {VARIABLE_TYPE_PREFIX} from '../constants'
6
+ import {InterpolatedPortableText} from '../InterpolatedPortableText'
7
+ import {
8
+ emptyBlocksContent,
9
+ multiBlockContent,
10
+ multipleVariablesBlock,
11
+ plainTextBlock,
12
+ singleVariableBlock,
13
+ styledTextWithVariableBlock,
14
+ } from './fixtures'
15
+
16
+ describe('InterpolatedPortableText', () => {
17
+ it('renders single variable resolved inline', () => {
18
+ const html = renderToStaticMarkup(
19
+ <InterpolatedPortableText
20
+ value={singleVariableBlock}
21
+ interpolationValues={{firstName: 'Jordan'}}
22
+ />,
23
+ )
24
+ expect(html).toContain('Hello, ')
25
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
26
+ expect(html).toContain('!')
27
+ })
28
+
29
+ it('renders multiple variables in one block', () => {
30
+ const html = renderToStaticMarkup(
31
+ <InterpolatedPortableText
32
+ value={multipleVariablesBlock}
33
+ interpolationValues={{
34
+ firstName: 'Jordan',
35
+ lastName: 'Lawrence',
36
+ email: 'jordan@example.com',
37
+ }}
38
+ />,
39
+ )
40
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
41
+ expect(html).toContain('<span data-variable-key="lastName">Lawrence</span>')
42
+ expect(html).toContain('<span data-variable-key="email">jordan@example.com</span>')
43
+ })
44
+
45
+ it('renders plain text unchanged', () => {
46
+ const html = renderToStaticMarkup(
47
+ <InterpolatedPortableText value={plainTextBlock} interpolationValues={{}} />,
48
+ )
49
+ expect(html).toContain('No variables here.')
50
+ })
51
+
52
+ it('renders fallback for missing values', () => {
53
+ const html = renderToStaticMarkup(
54
+ <InterpolatedPortableText value={singleVariableBlock} interpolationValues={{}} />,
55
+ )
56
+ expect(html).toContain('{firstName}')
57
+ })
58
+
59
+ it('uses custom fallback prop', () => {
60
+ const html = renderToStaticMarkup(
61
+ <InterpolatedPortableText
62
+ value={singleVariableBlock}
63
+ interpolationValues={{}}
64
+ fallback={(key) => `[${key}]`}
65
+ />,
66
+ )
67
+ expect(html).toContain('[firstName]')
68
+ })
69
+
70
+ it('preserves user components alongside interpolation types', () => {
71
+ const userComponents: PortableTextComponents = {
72
+ block: {
73
+ normal: ({children}) => <div data-testid="custom-block">{children}</div>,
74
+ },
75
+ }
76
+
77
+ const html = renderToStaticMarkup(
78
+ <InterpolatedPortableText
79
+ value={singleVariableBlock}
80
+ interpolationValues={{firstName: 'Jordan'}}
81
+ components={userComponents}
82
+ />,
83
+ )
84
+ expect(html).toContain('data-testid="custom-block"')
85
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
86
+ })
87
+
88
+ it('renders variables across multiple blocks', () => {
89
+ const html = renderToStaticMarkup(
90
+ <InterpolatedPortableText
91
+ value={multiBlockContent}
92
+ interpolationValues={{
93
+ firstName: 'Jordan',
94
+ email: 'jordan@example.com',
95
+ }}
96
+ />,
97
+ )
98
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
99
+ expect(html).toContain('<span data-variable-key="email">jordan@example.com</span>')
100
+ expect(html).toContain('Dear ')
101
+ expect(html).toContain('Your email is ')
102
+ })
103
+
104
+ it('interpolation types take precedence over user types with same key', () => {
105
+ const userComponents: PortableTextComponents = {
106
+ types: {
107
+ [VARIABLE_TYPE_PREFIX]: () => <span>user-override</span>,
108
+ },
109
+ }
110
+
111
+ const html = renderToStaticMarkup(
112
+ <InterpolatedPortableText
113
+ value={singleVariableBlock}
114
+ interpolationValues={{firstName: 'Jordan'}}
115
+ components={userComponents}
116
+ />,
117
+ )
118
+ expect(html).not.toContain('user-override')
119
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
120
+ })
121
+
122
+ it('renders user custom types alongside interpolation types', () => {
123
+ const customWidgetBlock = [
124
+ {
125
+ _type: 'block',
126
+ _key: 'block-1',
127
+ style: 'normal' as const,
128
+ markDefs: [],
129
+ children: [
130
+ {_type: 'span', _key: 'span-1', text: 'Hello ', marks: []},
131
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
132
+ ],
133
+ },
134
+ {
135
+ _type: 'customWidget',
136
+ _key: 'widget-1',
137
+ label: 'Click me',
138
+ },
139
+ ]
140
+
141
+ const userComponents: PortableTextComponents = {
142
+ types: {
143
+ customWidget: ({value}: {value: {label: string}}) => (
144
+ <button data-testid="widget">{value.label}</button>
145
+ ),
146
+ },
147
+ }
148
+
149
+ const html = renderToStaticMarkup(
150
+ <InterpolatedPortableText
151
+ value={customWidgetBlock}
152
+ interpolationValues={{firstName: 'Jordan'}}
153
+ components={userComponents}
154
+ />,
155
+ )
156
+ expect(html).toContain('data-testid="widget"')
157
+ expect(html).toContain('Click me')
158
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
159
+ })
160
+
161
+ it('preserves user mark components', () => {
162
+ const userComponents: PortableTextComponents = {
163
+ marks: {
164
+ strong: ({children}) => <strong data-testid="custom-strong">{children}</strong>,
165
+ },
166
+ }
167
+
168
+ const html = renderToStaticMarkup(
169
+ <InterpolatedPortableText
170
+ value={styledTextWithVariableBlock}
171
+ interpolationValues={{firstName: 'Jordan'}}
172
+ components={userComponents}
173
+ />,
174
+ )
175
+ expect(html).toContain('data-testid="custom-strong"')
176
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
177
+ })
178
+
179
+ it('renders without errors when value array is empty', () => {
180
+ const html = renderToStaticMarkup(
181
+ <InterpolatedPortableText value={emptyBlocksContent} interpolationValues={{}} />,
182
+ )
183
+ expect(html).toBe('')
184
+ })
185
+
186
+ it('default fallback renders exactly {variableKey} format', () => {
187
+ const html = renderToStaticMarkup(
188
+ <InterpolatedPortableText value={singleVariableBlock} interpolationValues={{}} />,
189
+ )
190
+ expect(html).toContain('<span data-variable-key="firstName">{firstName}</span>')
191
+ })
192
+
193
+ it('renders styled text marks alongside variables', () => {
194
+ const html = renderToStaticMarkup(
195
+ <InterpolatedPortableText
196
+ value={styledTextWithVariableBlock}
197
+ interpolationValues={{firstName: 'Jordan'}}
198
+ />,
199
+ )
200
+ expect(html).toContain('<strong>Welcome </strong>')
201
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
202
+ expect(html).toContain('<em> aboard</em>')
203
+ })
204
+ })
@@ -0,0 +1,115 @@
1
+ import {PortableText} from '@portabletext/react'
2
+ import {renderToStaticMarkup} from 'react-dom/server'
3
+ import {describe, expect, it} from 'vitest'
4
+
5
+ import {VARIABLE_TYPE_PREFIX} from '../constants'
6
+ import {createInterpolationComponents} from '../createInterpolationComponents'
7
+ import {
8
+ consecutiveVariablesBlock,
9
+ multipleVariablesBlock,
10
+ singleVariableBlock,
11
+ variableOnlyBlock,
12
+ } from './fixtures'
13
+
14
+ describe('createInterpolationComponents', () => {
15
+ it('returns object with types.pteInterpolationVariable component', () => {
16
+ const components = createInterpolationComponents({firstName: 'Jordan'})
17
+ expect(components.types).toBeDefined()
18
+ expect(components.types).toHaveProperty('pteInterpolationVariable')
19
+ })
20
+
21
+ it('resolves variable value from the values map', () => {
22
+ const components = createInterpolationComponents({firstName: 'Jordan'})
23
+ const html = renderToStaticMarkup(
24
+ <PortableText value={singleVariableBlock} components={components} />,
25
+ )
26
+ expect(html).toContain('Jordan')
27
+ expect(html).toContain('data-variable-key="firstName"')
28
+ })
29
+
30
+ it('renders {variableKey} fallback for missing values', () => {
31
+ const components = createInterpolationComponents({})
32
+ const html = renderToStaticMarkup(
33
+ <PortableText value={singleVariableBlock} components={components} />,
34
+ )
35
+ expect(html).toContain('{firstName}')
36
+ })
37
+
38
+ it('renders empty string when value is explicitly ""', () => {
39
+ const components = createInterpolationComponents({firstName: ''})
40
+ const html = renderToStaticMarkup(
41
+ <PortableText value={singleVariableBlock} components={components} />,
42
+ )
43
+ expect(html).toContain('<span data-variable-key="firstName"></span>')
44
+ expect(html).not.toContain('{firstName}')
45
+ })
46
+
47
+ it('calls custom fallback function for missing values', () => {
48
+ const customFallback = (variableKey: string) => `[MISSING: ${variableKey}]`
49
+ const components = createInterpolationComponents({}, customFallback)
50
+ const html = renderToStaticMarkup(
51
+ <PortableText value={singleVariableBlock} components={components} />,
52
+ )
53
+ expect(html).toContain('[MISSING: firstName]')
54
+ })
55
+
56
+ it('renders inside <span> with data-variable-key attribute', () => {
57
+ const components = createInterpolationComponents({
58
+ firstName: 'Jordan',
59
+ lastName: 'Lawrence',
60
+ email: 'jordan@example.com',
61
+ })
62
+ const html = renderToStaticMarkup(
63
+ <PortableText value={multipleVariablesBlock} components={components} />,
64
+ )
65
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
66
+ expect(html).toContain('<span data-variable-key="lastName">Lawrence</span>')
67
+ expect(html).toContain('<span data-variable-key="email">jordan@example.com</span>')
68
+ })
69
+
70
+ it('returns components with only a types key', () => {
71
+ const components = createInterpolationComponents({firstName: 'Jordan'})
72
+ expect(Object.keys(components)).toEqual(['types'])
73
+ })
74
+
75
+ it('registers component under VARIABLE_TYPE_PREFIX key', () => {
76
+ const components = createInterpolationComponents({firstName: 'Jordan'})
77
+ expect(Object.keys(components.types!)).toEqual([VARIABLE_TYPE_PREFIX])
78
+ })
79
+
80
+ it('falls back for all variables when values map is empty', () => {
81
+ const components = createInterpolationComponents({})
82
+ const html = renderToStaticMarkup(
83
+ <PortableText value={multipleVariablesBlock} components={components} />,
84
+ )
85
+ expect(html).toContain('{firstName}')
86
+ expect(html).toContain('{lastName}')
87
+ expect(html).toContain('{email}')
88
+ })
89
+
90
+ it('escapes HTML special characters in values', () => {
91
+ const components = createInterpolationComponents({firstName: '<script>alert("xss")</script>'})
92
+ const html = renderToStaticMarkup(
93
+ <PortableText value={singleVariableBlock} components={components} />,
94
+ )
95
+ expect(html).not.toContain('<script>')
96
+ expect(html).toContain('&lt;script&gt;')
97
+ })
98
+
99
+ it('renders consecutive variables without interference', () => {
100
+ const components = createInterpolationComponents({firstName: 'Jordan', lastName: 'Lawrence'})
101
+ const html = renderToStaticMarkup(
102
+ <PortableText value={consecutiveVariablesBlock} components={components} />,
103
+ )
104
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
105
+ expect(html).toContain('<span data-variable-key="lastName">Lawrence</span>')
106
+ })
107
+
108
+ it('renders a variable as the only child in a block', () => {
109
+ const components = createInterpolationComponents({firstName: 'Jordan'})
110
+ const html = renderToStaticMarkup(
111
+ <PortableText value={variableOnlyBlock} components={components} />,
112
+ )
113
+ expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
114
+ })
115
+ })
@@ -0,0 +1,106 @@
1
+ import type {PortableTextBlock} from '@portabletext/react'
2
+
3
+ export const singleVariableBlock: PortableTextBlock[] = [
4
+ {
5
+ _type: 'block',
6
+ _key: 'block-1',
7
+ style: 'normal',
8
+ markDefs: [],
9
+ children: [
10
+ {_type: 'span', _key: 'span-1', text: 'Hello, ', marks: []},
11
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
12
+ {_type: 'span', _key: 'span-2', text: '!', marks: []},
13
+ ],
14
+ },
15
+ ]
16
+
17
+ export const multipleVariablesBlock: PortableTextBlock[] = [
18
+ {
19
+ _type: 'block',
20
+ _key: 'block-1',
21
+ style: 'normal',
22
+ markDefs: [],
23
+ children: [
24
+ {_type: 'span', _key: 'span-1', text: 'Name: ', marks: []},
25
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
26
+ {_type: 'span', _key: 'span-2', text: ' ', marks: []},
27
+ {_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'lastName'},
28
+ {_type: 'span', _key: 'span-3', text: ', Email: ', marks: []},
29
+ {_type: 'pteInterpolationVariable', _key: 'var-3', variableKey: 'email'},
30
+ ],
31
+ },
32
+ ]
33
+
34
+ export const plainTextBlock: PortableTextBlock[] = [
35
+ {
36
+ _type: 'block',
37
+ _key: 'block-1',
38
+ style: 'normal',
39
+ markDefs: [],
40
+ children: [{_type: 'span', _key: 'span-1', text: 'No variables here.', marks: []}],
41
+ },
42
+ ]
43
+
44
+ export const variableOnlyBlock: PortableTextBlock[] = [
45
+ {
46
+ _type: 'block',
47
+ _key: 'block-1',
48
+ style: 'normal',
49
+ markDefs: [],
50
+ children: [{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'}],
51
+ },
52
+ ]
53
+
54
+ export const consecutiveVariablesBlock: PortableTextBlock[] = [
55
+ {
56
+ _type: 'block',
57
+ _key: 'block-1',
58
+ style: 'normal',
59
+ markDefs: [],
60
+ children: [
61
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
62
+ {_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'lastName'},
63
+ ],
64
+ },
65
+ ]
66
+
67
+ export const styledTextWithVariableBlock: PortableTextBlock[] = [
68
+ {
69
+ _type: 'block',
70
+ _key: 'block-1',
71
+ style: 'normal',
72
+ markDefs: [],
73
+ children: [
74
+ {_type: 'span', _key: 'span-1', text: 'Welcome ', marks: ['strong']},
75
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
76
+ {_type: 'span', _key: 'span-2', text: ' aboard', marks: ['em']},
77
+ ],
78
+ },
79
+ ]
80
+
81
+ export const emptyBlocksContent: PortableTextBlock[] = []
82
+
83
+ export const multiBlockContent: PortableTextBlock[] = [
84
+ {
85
+ _type: 'block',
86
+ _key: 'block-1',
87
+ style: 'normal',
88
+ markDefs: [],
89
+ children: [
90
+ {_type: 'span', _key: 'span-1', text: 'Dear ', marks: []},
91
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
92
+ {_type: 'span', _key: 'span-2', text: ',', marks: []},
93
+ ],
94
+ },
95
+ {
96
+ _type: 'block',
97
+ _key: 'block-2',
98
+ style: 'normal',
99
+ markDefs: [],
100
+ children: [
101
+ {_type: 'span', _key: 'span-3', text: 'Your email is ', marks: []},
102
+ {_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'email'},
103
+ {_type: 'span', _key: 'span-4', text: '.', marks: []},
104
+ ],
105
+ },
106
+ ]
@@ -0,0 +1,87 @@
1
+ import {describe, expect, it} from 'vitest'
2
+ import {
3
+ extractVariableKeys,
4
+ interpolateToString,
5
+ VARIABLE_TYPE_PREFIX,
6
+ } from 'pte-interpolation-react'
7
+ import type {
8
+ InterpolationFallback,
9
+ InterpolationValues,
10
+ PortableTextBlockLike,
11
+ PortableTextChild,
12
+ PteInterpolationVariableBlock,
13
+ } from 'pte-interpolation-react'
14
+ import {singleVariableBlock, multipleVariablesBlock, plainTextBlock} from './fixtures'
15
+
16
+ describe('re-exports from pte-interpolation-core', () => {
17
+ describe('VARIABLE_TYPE_PREFIX', () => {
18
+ it('equals pteInterpolationVariable', () => {
19
+ expect(VARIABLE_TYPE_PREFIX).toBe('pteInterpolationVariable')
20
+ })
21
+ })
22
+
23
+ describe('extractVariableKeys', () => {
24
+ it('extracts variable keys from blocks', () => {
25
+ const keys = extractVariableKeys(singleVariableBlock)
26
+ expect(keys).toEqual(['firstName'])
27
+ })
28
+
29
+ it('extracts multiple variable keys', () => {
30
+ const keys = extractVariableKeys(multipleVariablesBlock)
31
+ expect(keys).toEqual(['firstName', 'lastName', 'email'])
32
+ })
33
+
34
+ it('returns empty array for plain text', () => {
35
+ const keys = extractVariableKeys(plainTextBlock)
36
+ expect(keys).toEqual([])
37
+ })
38
+ })
39
+
40
+ describe('interpolateToString', () => {
41
+ it('interpolates variable values into text', () => {
42
+ const result = interpolateToString(singleVariableBlock, {firstName: 'Alice'})
43
+ expect(result).toBe('Hello, Alice!')
44
+ })
45
+
46
+ it('uses default fallback for missing values', () => {
47
+ const result = interpolateToString(singleVariableBlock, {})
48
+ expect(result).toBe('Hello, {firstName}!')
49
+ })
50
+
51
+ it('uses custom fallback for missing values', () => {
52
+ const customFallback: InterpolationFallback = (variableKey) => `[${variableKey}]`
53
+ const result = interpolateToString(singleVariableBlock, {}, customFallback)
54
+ expect(result).toBe('Hello, [firstName]!')
55
+ })
56
+ })
57
+
58
+ describe('type exports', () => {
59
+ it('InterpolationValues type works as a record of strings', () => {
60
+ const values: InterpolationValues = {firstName: 'Alice', lastName: 'Smith'}
61
+ expect(values).toEqual({firstName: 'Alice', lastName: 'Smith'})
62
+ })
63
+
64
+ it('PortableTextBlockLike type is compatible with fixture blocks', () => {
65
+ const blocks: PortableTextBlockLike[] = singleVariableBlock
66
+ expect(blocks).toHaveLength(1)
67
+ })
68
+
69
+ it('PortableTextChild type is compatible with block children', () => {
70
+ const child: PortableTextChild = {
71
+ _type: 'span',
72
+ _key: 'span-1',
73
+ text: 'Hello',
74
+ }
75
+ expect(child._type).toBe('span')
76
+ })
77
+
78
+ it('PteInterpolationVariableBlock type is compatible with variable blocks', () => {
79
+ const variableBlock: PteInterpolationVariableBlock = {
80
+ _type: 'pteInterpolationVariable',
81
+ _key: 'var-1',
82
+ variableKey: 'firstName',
83
+ }
84
+ expect(variableBlock.variableKey).toBe('firstName')
85
+ })
86
+ })
87
+ })
@@ -0,0 +1 @@
1
+ export {VARIABLE_TYPE_PREFIX} from 'pte-interpolation-core'
@@ -0,0 +1,32 @@
1
+ import type {PortableTextComponents, PortableTextTypeComponentProps} from '@portabletext/react'
2
+
3
+ import {VARIABLE_TYPE_PREFIX} from './constants'
4
+ import type {
5
+ InterpolationFallback,
6
+ InterpolationValues,
7
+ PteInterpolationVariableBlock,
8
+ } from './types'
9
+
10
+ function defaultFallback(variableKey: string): string {
11
+ return `{${variableKey}}`
12
+ }
13
+
14
+ /** @public */
15
+ export function createInterpolationComponents(
16
+ values: InterpolationValues,
17
+ fallback: InterpolationFallback = defaultFallback,
18
+ ): PortableTextComponents {
19
+ function VariableComponent(props: PortableTextTypeComponentProps<PteInterpolationVariableBlock>) {
20
+ const {variableKey} = props.value
21
+ const resolvedValue =
22
+ values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)
23
+
24
+ return <span data-variable-key={variableKey}>{resolvedValue}</span>
25
+ }
26
+
27
+ return {
28
+ types: {
29
+ [VARIABLE_TYPE_PREFIX]: VariableComponent,
30
+ },
31
+ }
32
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,11 @@
1
- export {InterpolatedText} from './InterpolatedText'
2
- export type {InterpolatedTextProps, InterpolationValues} from './types'
1
+ export {InterpolatedPortableText} from './InterpolatedPortableText'
2
+ export {createInterpolationComponents} from './createInterpolationComponents'
3
+ export {VARIABLE_TYPE_PREFIX} from './constants'
4
+ export {extractVariableKeys, interpolateToString} from 'pte-interpolation-core'
5
+ export type {
6
+ InterpolatedPortableTextProps,
7
+ InterpolationFallback,
8
+ InterpolationValues,
9
+ PteInterpolationVariableBlock,
10
+ } from './types'
11
+ export type {PortableTextBlockLike, PortableTextChild} from 'pte-interpolation-core'
package/src/types.ts CHANGED
@@ -1,10 +1,18 @@
1
- import type {PortableTextBlock} from '@portabletext/react'
1
+ import type {PortableTextComponents, PortableTextProps} from '@portabletext/react'
2
+ import type {InterpolationFallback, InterpolationValues} from 'pte-interpolation-core'
3
+
4
+ export type {InterpolationFallback, InterpolationValues} from 'pte-interpolation-core'
2
5
 
3
6
  /** @public */
4
- export type InterpolationValues = Record<string, string>
7
+ export interface PteInterpolationVariableBlock {
8
+ _type: 'pteInterpolationVariable'
9
+ _key: string
10
+ variableKey: string
11
+ }
5
12
 
6
13
  /** @public */
7
- export interface InterpolatedTextProps {
8
- blocks: PortableTextBlock[]
9
- values: InterpolationValues
14
+ export interface InterpolatedPortableTextProps extends Omit<PortableTextProps, 'components'> {
15
+ interpolationValues: InterpolationValues
16
+ components?: PortableTextComponents
17
+ fallback?: InterpolationFallback
10
18
  }
@@ -1,10 +0,0 @@
1
- import type {InterpolatedTextProps} from './types'
2
-
3
- /** @public */
4
- export function InterpolatedText({blocks, values}: InterpolatedTextProps) {
5
- return (
6
- <pre>
7
- <code>{JSON.stringify({blocks, values}, null, 2)}</code>
8
- </pre>
9
- )
10
- }
@@ -1,9 +0,0 @@
1
- import {describe, expect, it} from 'vitest'
2
- import {InterpolatedText} from '../InterpolatedText'
3
-
4
- describe('InterpolatedText', () => {
5
- it('should be defined', () => {
6
- expect(InterpolatedText).toBeDefined()
7
- expect(typeof InterpolatedText).toBe('function')
8
- })
9
- })