pte-interpolation-core 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jordan Lawrence
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # pte-interpolation-core
2
+
3
+ [![npm version](https://img.shields.io/npm/v/pte-interpolation-core.svg?style=flat-square)](https://www.npmjs.com/package/pte-interpolation-core)
4
+
5
+ Framework-agnostic utilities for [Portable Text](https://portabletext.org/) variable interpolation. Extract variable keys from PTE blocks and resolve them to plain strings - with zero dependencies.
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). For React rendering with rich text output, see [`pte-interpolation-react`](https://www.npmjs.com/package/pte-interpolation-react).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install pte-interpolation-core
13
+ ```
14
+
15
+ No peer dependencies required.
16
+
17
+ ## When to Use
18
+
19
+ Use this package when you need plain string output from interpolated Portable Text - without a React dependency. Common use cases include:
20
+
21
+ - Email templates (server-side rendering)
22
+ - PDF generation
23
+ - Node.js scripts and background jobs
24
+ - SMS or push notification text
25
+ - Any non-React framework (Vue, Svelte, Angular, etc.)
26
+
27
+ If you are rendering in React and want rich text output, use [`pte-interpolation-react`](https://www.npmjs.com/package/pte-interpolation-react) instead - it re-exports everything from this package, so you get both APIs without installing core separately.
28
+
29
+ ## Usage
30
+
31
+ ### Extract variable keys
32
+
33
+ `extractVariableKeys` returns the unique variable keys found in an array of PTE blocks, in first-occurrence order:
34
+
35
+ ```ts
36
+ import {extractVariableKeys} from 'pte-interpolation-core'
37
+
38
+ const keys = extractVariableKeys(blocks)
39
+ // ['firstName', 'email']
40
+ ```
41
+
42
+ ### Interpolate to a plain string
43
+
44
+ `interpolateToString` resolves PTE blocks to a plain string, substituting variable blocks with the provided values:
45
+
46
+ ```ts
47
+ import {interpolateToString} from 'pte-interpolation-core'
48
+
49
+ const text = interpolateToString(blocks, {
50
+ firstName: 'Alice',
51
+ email: 'alice@example.com',
52
+ })
53
+ // "Hello, Alice! Your email is alice@example.com."
54
+ ```
55
+
56
+ ### Custom fallback for missing values
57
+
58
+ By default, unresolved variables render as `{variableKey}` (e.g. `{firstName}`). Provide a fallback function to customise this:
59
+
60
+ ```ts
61
+ const text = interpolateToString(blocks, {}, (variableKey) => `[${variableKey}]`)
62
+ // "Hello, [firstName]! Your email is [email]."
63
+ ```
64
+
65
+ ## Authoring Variables in Sanity Studio
66
+
67
+ This package handles **resolution** of variable blocks that already exist in Portable Text. 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):
68
+
69
+ ```ts
70
+ import {interpolationVariables} from 'sanity-plugin-pte-interpolation'
71
+
72
+ defineField({
73
+ name: 'body',
74
+ type: 'array',
75
+ of: [
76
+ interpolationVariables([
77
+ {id: 'firstName', name: 'First name'},
78
+ {id: 'email', name: 'Email address'},
79
+ ]),
80
+ ],
81
+ })
82
+ ```
83
+
84
+ ## Data Shape
85
+
86
+ Variable blocks in stored Portable Text look like this:
87
+
88
+ ```json
89
+ {
90
+ "_type": "block",
91
+ "children": [
92
+ {"_type": "span", "text": "Hello, "},
93
+ {"_type": "pteInterpolationVariable", "variableKey": "firstName"},
94
+ {"_type": "span", "text": "!"}
95
+ ]
96
+ }
97
+ ```
98
+
99
+ The `variableKey` maps to the `id` defined in the Studio variable definitions and the keys in the values record passed to `interpolateToString`.
100
+
101
+ ## API Reference
102
+
103
+ ### `extractVariableKeys(blocks)`
104
+
105
+ Returns the unique variable keys from an array of PTE blocks, in first-occurrence order.
106
+
107
+ | Parameter | Type | Description |
108
+ | --------- | ------------------------- | ---------------------------- |
109
+ | `blocks` | `PortableTextBlockLike[]` | Portable Text blocks to scan |
110
+
111
+ Returns `string[]`.
112
+
113
+ ### `interpolateToString(blocks, values, fallback?)`
114
+
115
+ Resolves PTE blocks to a plain string, replacing variable blocks with the corresponding values. Multiple blocks are joined with newlines.
116
+
117
+ | Parameter | Type | Description |
118
+ | ---------- | --------------------------------- | ------------------------------------------------------------------------ |
119
+ | `blocks` | `PortableTextBlockLike[]` | Portable Text blocks to resolve |
120
+ | `values` | `Record<string, string>` | Map of variable IDs to their resolved values |
121
+ | `fallback` | `(variableKey: string) => string` | Optional function for unresolved variables (defaults to `{variableKey}`) |
122
+
123
+ Returns `string`.
124
+
125
+ ### `VARIABLE_TYPE_PREFIX`
126
+
127
+ The constant `'pteInterpolationVariable'` - the `_type` string used for variable inline blocks in stored Portable Text. Exported for advanced use cases.
128
+
129
+ ### Types
130
+
131
+ | Type | Description |
132
+ | ------------------------------- | ----------------------------------------------------- |
133
+ | `PortableTextBlockLike` | Minimal block shape with optional `children` array |
134
+ | `PortableTextChild` | A child node within a block (span or variable) |
135
+ | `PteInterpolationVariableBlock` | A variable inline block with `variableKey` |
136
+ | `InterpolationValues` | `Record<string, string>` - variable ID to value map |
137
+ | `InterpolationFallback` | `(variableKey: string) => string` - fallback function |
138
+
139
+ ## License
140
+
141
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ const VARIABLE_TYPE_PREFIX = "pteInterpolationVariable";
4
+ function extractVariableKeys(blocks) {
5
+ const seen = /* @__PURE__ */ new Set();
6
+ return blocks.reduce((keys, block) => (block.children ?? []).reduce((accumulated, child) => {
7
+ if (child._type !== VARIABLE_TYPE_PREFIX) return accumulated;
8
+ const variableKey = child.variableKey;
9
+ return typeof variableKey != "string" || seen.has(variableKey) ? accumulated : (seen.add(variableKey), [...accumulated, variableKey]);
10
+ }, keys), []);
11
+ }
12
+ function defaultFallback(variableKey) {
13
+ return `{${variableKey}}`;
14
+ }
15
+ function interpolateToString(blocks, values, fallback = defaultFallback) {
16
+ return blocks.map((block) => (block.children ?? []).map((child) => {
17
+ if (child._type === VARIABLE_TYPE_PREFIX) {
18
+ const variableKey = child.variableKey;
19
+ return values[variableKey] !== void 0 ? values[variableKey] : fallback(variableKey);
20
+ }
21
+ return typeof child.text == "string" ? child.text : "";
22
+ }).join("")).join(`
23
+ `);
24
+ }
25
+ exports.VARIABLE_TYPE_PREFIX = VARIABLE_TYPE_PREFIX;
26
+ exports.extractVariableKeys = extractVariableKeys;
27
+ exports.interpolateToString = interpolateToString;
28
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/constants.ts","../src/extractVariableKeys.ts","../src/interpolateToString.ts"],"sourcesContent":["/** @public */\nexport const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'\n","import {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {PortableTextBlockLike} from './types'\n\n/** @public */\nexport function extractVariableKeys(blocks: PortableTextBlockLike[]): string[] {\n const seen = new Set<string>()\n\n return blocks.reduce<string[]>((keys, block) => {\n const children = block.children ?? []\n\n return children.reduce((accumulated, child) => {\n if (child._type !== VARIABLE_TYPE_PREFIX) return accumulated\n\n const variableKey = child.variableKey\n if (typeof variableKey !== 'string') return accumulated\n if (seen.has(variableKey)) return accumulated\n\n seen.add(variableKey)\n return [...accumulated, variableKey]\n }, keys)\n }, [])\n}\n","import {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {InterpolationFallback, InterpolationValues, PortableTextBlockLike} from './types'\n\nfunction defaultFallback(variableKey: string): string {\n return `{${variableKey}}`\n}\n\n/** @public */\nexport function interpolateToString(\n blocks: PortableTextBlockLike[],\n values: InterpolationValues,\n fallback: InterpolationFallback = defaultFallback,\n): string {\n return blocks\n .map((block) => {\n const children = block.children ?? []\n\n return children\n .map((child) => {\n if (child._type === VARIABLE_TYPE_PREFIX) {\n const variableKey = child.variableKey as string\n return values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)\n }\n\n return typeof child.text === 'string' ? child.text : ''\n })\n .join('')\n })\n .join('\\n')\n}\n"],"names":[],"mappings":";;AACO,MAAM,uBAAuB;ACG7B,SAAS,oBAAoB,QAA2C;AAC7E,QAAM,2BAAW,IAAA;AAEjB,SAAO,OAAO,OAAiB,CAAC,MAAM,WACnB,MAAM,YAAY,CAAA,GAEnB,OAAO,CAAC,aAAa,UAAU;AAC7C,QAAI,MAAM,UAAU,qBAAsB,QAAO;AAEjD,UAAM,cAAc,MAAM;AAE1B,WADI,OAAO,eAAgB,YACvB,KAAK,IAAI,WAAW,IAAU,eAElC,KAAK,IAAI,WAAW,GACb,CAAC,GAAG,aAAa,WAAW;AAAA,EACrC,GAAG,IAAI,GACN,EAAE;AACP;AClBA,SAAS,gBAAgB,aAA6B;AACpD,SAAO,IAAI,WAAW;AACxB;AAGO,SAAS,oBACd,QACA,QACA,WAAkC,iBAC1B;AACR,SAAO,OACJ,IAAI,CAAC,WACa,MAAM,YAAY,CAAA,GAGhC,IAAI,CAAC,UAAU;AACd,QAAI,MAAM,UAAU,sBAAsB;AACxC,YAAM,cAAc,MAAM;AAC1B,aAAO,OAAO,WAAW,MAAM,SAAY,OAAO,WAAW,IAAI,SAAS,WAAW;AAAA,IACvF;AAEA,WAAO,OAAO,MAAM,QAAS,WAAW,MAAM,OAAO;AAAA,EACvD,CAAC,EACA,KAAK,EAAE,CACX,EACA,KAAK;AAAA,CAAI;AACd;;;;"}
@@ -0,0 +1,42 @@
1
+ /** @public */
2
+ export declare function extractVariableKeys(blocks: PortableTextBlockLike[]): string[]
3
+
4
+ /** @public */
5
+ export declare function interpolateToString(
6
+ blocks: PortableTextBlockLike[],
7
+ values: InterpolationValues,
8
+ fallback?: InterpolationFallback,
9
+ ): string
10
+
11
+ /** @public */
12
+ export declare type InterpolationFallback = (variableKey: string) => string
13
+
14
+ /** @public */
15
+ export declare type InterpolationValues = Record<string, string>
16
+
17
+ /** @public */
18
+ export declare interface PortableTextBlockLike {
19
+ _type: string
20
+ _key?: string
21
+ children?: PortableTextChild[]
22
+ }
23
+
24
+ /** @public */
25
+ export declare interface PortableTextChild {
26
+ _type: string
27
+ _key?: string
28
+ text?: string
29
+ variableKey?: string
30
+ }
31
+
32
+ /** @public */
33
+ export declare interface PteInterpolationVariableBlock {
34
+ _type: 'pteInterpolationVariable'
35
+ _key: string
36
+ variableKey: string
37
+ }
38
+
39
+ /** @public */
40
+ export declare const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'
41
+
42
+ export {}
@@ -0,0 +1,42 @@
1
+ /** @public */
2
+ export declare function extractVariableKeys(blocks: PortableTextBlockLike[]): string[]
3
+
4
+ /** @public */
5
+ export declare function interpolateToString(
6
+ blocks: PortableTextBlockLike[],
7
+ values: InterpolationValues,
8
+ fallback?: InterpolationFallback,
9
+ ): string
10
+
11
+ /** @public */
12
+ export declare type InterpolationFallback = (variableKey: string) => string
13
+
14
+ /** @public */
15
+ export declare type InterpolationValues = Record<string, string>
16
+
17
+ /** @public */
18
+ export declare interface PortableTextBlockLike {
19
+ _type: string
20
+ _key?: string
21
+ children?: PortableTextChild[]
22
+ }
23
+
24
+ /** @public */
25
+ export declare interface PortableTextChild {
26
+ _type: string
27
+ _key?: string
28
+ text?: string
29
+ variableKey?: string
30
+ }
31
+
32
+ /** @public */
33
+ export declare interface PteInterpolationVariableBlock {
34
+ _type: 'pteInterpolationVariable'
35
+ _key: string
36
+ variableKey: string
37
+ }
38
+
39
+ /** @public */
40
+ export declare const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'
41
+
42
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ const VARIABLE_TYPE_PREFIX = "pteInterpolationVariable";
2
+ function extractVariableKeys(blocks) {
3
+ const seen = /* @__PURE__ */ new Set();
4
+ return blocks.reduce((keys, block) => (block.children ?? []).reduce((accumulated, child) => {
5
+ if (child._type !== VARIABLE_TYPE_PREFIX) return accumulated;
6
+ const variableKey = child.variableKey;
7
+ return typeof variableKey != "string" || seen.has(variableKey) ? accumulated : (seen.add(variableKey), [...accumulated, variableKey]);
8
+ }, keys), []);
9
+ }
10
+ function defaultFallback(variableKey) {
11
+ return `{${variableKey}}`;
12
+ }
13
+ function interpolateToString(blocks, values, fallback = defaultFallback) {
14
+ return blocks.map((block) => (block.children ?? []).map((child) => {
15
+ if (child._type === VARIABLE_TYPE_PREFIX) {
16
+ const variableKey = child.variableKey;
17
+ return values[variableKey] !== void 0 ? values[variableKey] : fallback(variableKey);
18
+ }
19
+ return typeof child.text == "string" ? child.text : "";
20
+ }).join("")).join(`
21
+ `);
22
+ }
23
+ export {
24
+ VARIABLE_TYPE_PREFIX,
25
+ extractVariableKeys,
26
+ interpolateToString
27
+ };
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/constants.ts","../src/extractVariableKeys.ts","../src/interpolateToString.ts"],"sourcesContent":["/** @public */\nexport const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'\n","import {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {PortableTextBlockLike} from './types'\n\n/** @public */\nexport function extractVariableKeys(blocks: PortableTextBlockLike[]): string[] {\n const seen = new Set<string>()\n\n return blocks.reduce<string[]>((keys, block) => {\n const children = block.children ?? []\n\n return children.reduce((accumulated, child) => {\n if (child._type !== VARIABLE_TYPE_PREFIX) return accumulated\n\n const variableKey = child.variableKey\n if (typeof variableKey !== 'string') return accumulated\n if (seen.has(variableKey)) return accumulated\n\n seen.add(variableKey)\n return [...accumulated, variableKey]\n }, keys)\n }, [])\n}\n","import {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {InterpolationFallback, InterpolationValues, PortableTextBlockLike} from './types'\n\nfunction defaultFallback(variableKey: string): string {\n return `{${variableKey}}`\n}\n\n/** @public */\nexport function interpolateToString(\n blocks: PortableTextBlockLike[],\n values: InterpolationValues,\n fallback: InterpolationFallback = defaultFallback,\n): string {\n return blocks\n .map((block) => {\n const children = block.children ?? []\n\n return children\n .map((child) => {\n if (child._type === VARIABLE_TYPE_PREFIX) {\n const variableKey = child.variableKey as string\n return values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)\n }\n\n return typeof child.text === 'string' ? child.text : ''\n })\n .join('')\n })\n .join('\\n')\n}\n"],"names":[],"mappings":"AACO,MAAM,uBAAuB;ACG7B,SAAS,oBAAoB,QAA2C;AAC7E,QAAM,2BAAW,IAAA;AAEjB,SAAO,OAAO,OAAiB,CAAC,MAAM,WACnB,MAAM,YAAY,CAAA,GAEnB,OAAO,CAAC,aAAa,UAAU;AAC7C,QAAI,MAAM,UAAU,qBAAsB,QAAO;AAEjD,UAAM,cAAc,MAAM;AAE1B,WADI,OAAO,eAAgB,YACvB,KAAK,IAAI,WAAW,IAAU,eAElC,KAAK,IAAI,WAAW,GACb,CAAC,GAAG,aAAa,WAAW;AAAA,EACrC,GAAG,IAAI,GACN,EAAE;AACP;AClBA,SAAS,gBAAgB,aAA6B;AACpD,SAAO,IAAI,WAAW;AACxB;AAGO,SAAS,oBACd,QACA,QACA,WAAkC,iBAC1B;AACR,SAAO,OACJ,IAAI,CAAC,WACa,MAAM,YAAY,CAAA,GAGhC,IAAI,CAAC,UAAU;AACd,QAAI,MAAM,UAAU,sBAAsB;AACxC,YAAM,cAAc,MAAM;AAC1B,aAAO,OAAO,WAAW,MAAM,SAAY,OAAO,WAAW,IAAI,SAAS,WAAW;AAAA,IACvF;AAEA,WAAO,OAAO,MAAM,QAAS,WAAW,MAAM,OAAO;AAAA,EACvD,CAAC,EACA,KAAK,EAAE,CACX,EACA,KAAK;AAAA,CAAI;AACd;"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "pte-interpolation-core",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Framework-agnostic utilities for Portable Text variable interpolation",
7
+ "keywords": [
8
+ "portable-text",
9
+ "interpolation",
10
+ "variables"
11
+ ],
12
+ "homepage": "https://github.com/jordanl17/sanity-pte-interpolation#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/jordanl17/sanity-pte-interpolation.git",
16
+ "directory": "packages/pte-interpolation-core"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/jordanl17/sanity-pte-interpolation/issues"
20
+ },
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "src",
35
+ "dist"
36
+ ],
37
+ "browserslist": "extends @sanity/browserslist-config",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "devDependencies": {
42
+ "@sanity/pkg-utils": "^10.4.4",
43
+ "rimraf": "^6.1.3",
44
+ "typescript": "~5.9.3"
45
+ },
46
+ "scripts": {
47
+ "build": "pkg-utils build --strict --check --clean",
48
+ "clean": "rimraf dist",
49
+ "link-watch": "pkg-utils watch --tsconfig tsconfig.build.json",
50
+ "type-check": "tsc --noEmit",
51
+ "yalc:publish": "npx yalc publish"
52
+ }
53
+ }
@@ -0,0 +1,46 @@
1
+ import {describe, expect, it} from 'vitest'
2
+ import {extractVariableKeys} from '../extractVariableKeys'
3
+ import {
4
+ blockWithNoChildren,
5
+ blockWithNonStringVariableKey,
6
+ consecutiveVariablesBlock,
7
+ duplicateVariableBlock,
8
+ emptyBlocksContent,
9
+ multipleVariablesBlock,
10
+ plainTextBlock,
11
+ singleVariableBlock,
12
+ } from './fixtures'
13
+
14
+ describe('extractVariableKeys', () => {
15
+ it('returns an empty array for empty blocks', () => {
16
+ expect(extractVariableKeys(emptyBlocksContent)).toEqual([])
17
+ })
18
+
19
+ it('returns an empty array when no variables exist', () => {
20
+ expect(extractVariableKeys(plainTextBlock)).toEqual([])
21
+ })
22
+
23
+ it('extracts a single variable key', () => {
24
+ expect(extractVariableKeys(singleVariableBlock)).toEqual(['firstName'])
25
+ })
26
+
27
+ it('extracts multiple variable keys in order', () => {
28
+ expect(extractVariableKeys(multipleVariablesBlock)).toEqual(['firstName', 'lastName', 'email'])
29
+ })
30
+
31
+ it('extracts consecutive variable keys', () => {
32
+ expect(extractVariableKeys(consecutiveVariablesBlock)).toEqual(['firstName', 'lastName'])
33
+ })
34
+
35
+ it('deduplicates variable keys preserving first-occurrence order', () => {
36
+ expect(extractVariableKeys(duplicateVariableBlock)).toEqual(['firstName'])
37
+ })
38
+
39
+ it('handles blocks with no children property', () => {
40
+ expect(extractVariableKeys(blockWithNoChildren)).toEqual([])
41
+ })
42
+
43
+ it('skips children with non-string variableKey', () => {
44
+ expect(extractVariableKeys(blockWithNonStringVariableKey)).toEqual([])
45
+ })
46
+ })
@@ -0,0 +1,94 @@
1
+ import type {PortableTextBlockLike} from '../types'
2
+
3
+ export const singleVariableBlock: PortableTextBlockLike[] = [
4
+ {
5
+ _type: 'block',
6
+ _key: 'block-1',
7
+ children: [
8
+ {_type: 'span', _key: 'span-1', text: 'Hello, '},
9
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
10
+ {_type: 'span', _key: 'span-2', text: '!'},
11
+ ],
12
+ },
13
+ ]
14
+
15
+ export const multipleVariablesBlock: PortableTextBlockLike[] = [
16
+ {
17
+ _type: 'block',
18
+ _key: 'block-1',
19
+ children: [
20
+ {_type: 'span', _key: 'span-1', text: 'Name: '},
21
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
22
+ {_type: 'span', _key: 'span-2', text: ' '},
23
+ {_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'lastName'},
24
+ {_type: 'span', _key: 'span-3', text: ', Email: '},
25
+ {_type: 'pteInterpolationVariable', _key: 'var-3', variableKey: 'email'},
26
+ ],
27
+ },
28
+ ]
29
+
30
+ export const plainTextBlock: PortableTextBlockLike[] = [
31
+ {
32
+ _type: 'block',
33
+ _key: 'block-1',
34
+ children: [{_type: 'span', _key: 'span-1', text: 'No variables here.'}],
35
+ },
36
+ ]
37
+
38
+ export const consecutiveVariablesBlock: PortableTextBlockLike[] = [
39
+ {
40
+ _type: 'block',
41
+ _key: 'block-1',
42
+ children: [
43
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
44
+ {_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'lastName'},
45
+ ],
46
+ },
47
+ ]
48
+
49
+ export const multiBlockContent: PortableTextBlockLike[] = [
50
+ {
51
+ _type: 'block',
52
+ _key: 'block-1',
53
+ children: [
54
+ {_type: 'span', _key: 'span-1', text: 'Dear '},
55
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
56
+ {_type: 'span', _key: 'span-2', text: ','},
57
+ ],
58
+ },
59
+ {
60
+ _type: 'block',
61
+ _key: 'block-2',
62
+ children: [
63
+ {_type: 'span', _key: 'span-3', text: 'Your email is '},
64
+ {_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'email'},
65
+ {_type: 'span', _key: 'span-4', text: '.'},
66
+ ],
67
+ },
68
+ ]
69
+
70
+ export const duplicateVariableBlock: PortableTextBlockLike[] = [
71
+ {
72
+ _type: 'block',
73
+ _key: 'block-1',
74
+ children: [
75
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
76
+ {_type: 'span', _key: 'span-1', text: ' and '},
77
+ {_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'firstName'},
78
+ ],
79
+ },
80
+ ]
81
+
82
+ export const blockWithNoChildren: PortableTextBlockLike[] = [{_type: 'block', _key: 'block-1'}]
83
+
84
+ export const blockWithNonStringVariableKey: PortableTextBlockLike[] = [
85
+ {
86
+ _type: 'block',
87
+ _key: 'block-1',
88
+ children: [
89
+ {_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 123 as unknown as string},
90
+ ],
91
+ },
92
+ ]
93
+
94
+ export const emptyBlocksContent: PortableTextBlockLike[] = []
@@ -0,0 +1,59 @@
1
+ import {describe, expect, it} from 'vitest'
2
+ import {interpolateToString} from '../interpolateToString'
3
+ import {
4
+ consecutiveVariablesBlock,
5
+ emptyBlocksContent,
6
+ multiBlockContent,
7
+ multipleVariablesBlock,
8
+ plainTextBlock,
9
+ singleVariableBlock,
10
+ } from './fixtures'
11
+
12
+ describe('interpolateToString', () => {
13
+ it('returns an empty string for empty blocks', () => {
14
+ expect(interpolateToString(emptyBlocksContent, {})).toBe('')
15
+ })
16
+
17
+ it('returns plain text unchanged', () => {
18
+ expect(interpolateToString(plainTextBlock, {})).toBe('No variables here.')
19
+ })
20
+
21
+ it('resolves a single variable from values', () => {
22
+ expect(interpolateToString(singleVariableBlock, {firstName: 'Alice'})).toBe('Hello, Alice!')
23
+ })
24
+
25
+ it('uses default fallback for missing variables', () => {
26
+ expect(interpolateToString(singleVariableBlock, {})).toBe('Hello, {firstName}!')
27
+ })
28
+
29
+ it('uses a custom fallback for missing variables', () => {
30
+ const customFallback = (variableKey: string) => `[${variableKey}]`
31
+ expect(interpolateToString(singleVariableBlock, {}, customFallback)).toBe('Hello, [firstName]!')
32
+ })
33
+
34
+ it('uses the value when it is an empty string', () => {
35
+ expect(interpolateToString(singleVariableBlock, {firstName: ''})).toBe('Hello, !')
36
+ })
37
+
38
+ it('resolves consecutive variables', () => {
39
+ expect(
40
+ interpolateToString(consecutiveVariablesBlock, {firstName: 'Alice', lastName: 'Smith'}),
41
+ ).toBe('AliceSmith')
42
+ })
43
+
44
+ it('joins multiple blocks with newlines', () => {
45
+ expect(
46
+ interpolateToString(multiBlockContent, {firstName: 'Alice', email: 'alice@example.com'}),
47
+ ).toBe('Dear Alice,\nYour email is alice@example.com.')
48
+ })
49
+
50
+ it('resolves multiple variables in one block', () => {
51
+ expect(
52
+ interpolateToString(multipleVariablesBlock, {
53
+ firstName: 'Alice',
54
+ lastName: 'Smith',
55
+ email: 'alice@example.com',
56
+ }),
57
+ ).toBe('Name: Alice Smith, Email: alice@example.com')
58
+ })
59
+ })
@@ -0,0 +1,2 @@
1
+ /** @public */
2
+ export const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'
@@ -0,0 +1,22 @@
1
+ import {VARIABLE_TYPE_PREFIX} from './constants'
2
+ import type {PortableTextBlockLike} from './types'
3
+
4
+ /** @public */
5
+ export function extractVariableKeys(blocks: PortableTextBlockLike[]): string[] {
6
+ const seen = new Set<string>()
7
+
8
+ return blocks.reduce<string[]>((keys, block) => {
9
+ const children = block.children ?? []
10
+
11
+ return children.reduce((accumulated, child) => {
12
+ if (child._type !== VARIABLE_TYPE_PREFIX) return accumulated
13
+
14
+ const variableKey = child.variableKey
15
+ if (typeof variableKey !== 'string') return accumulated
16
+ if (seen.has(variableKey)) return accumulated
17
+
18
+ seen.add(variableKey)
19
+ return [...accumulated, variableKey]
20
+ }, keys)
21
+ }, [])
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export {VARIABLE_TYPE_PREFIX} from './constants'
2
+ export {extractVariableKeys} from './extractVariableKeys'
3
+ export {interpolateToString} from './interpolateToString'
4
+ export type {
5
+ InterpolationFallback,
6
+ InterpolationValues,
7
+ PortableTextBlockLike,
8
+ PortableTextChild,
9
+ PteInterpolationVariableBlock,
10
+ } from './types'
@@ -0,0 +1,30 @@
1
+ import {VARIABLE_TYPE_PREFIX} from './constants'
2
+ import type {InterpolationFallback, InterpolationValues, PortableTextBlockLike} from './types'
3
+
4
+ function defaultFallback(variableKey: string): string {
5
+ return `{${variableKey}}`
6
+ }
7
+
8
+ /** @public */
9
+ export function interpolateToString(
10
+ blocks: PortableTextBlockLike[],
11
+ values: InterpolationValues,
12
+ fallback: InterpolationFallback = defaultFallback,
13
+ ): string {
14
+ return blocks
15
+ .map((block) => {
16
+ const children = block.children ?? []
17
+
18
+ return children
19
+ .map((child) => {
20
+ if (child._type === VARIABLE_TYPE_PREFIX) {
21
+ const variableKey = child.variableKey as string
22
+ return values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)
23
+ }
24
+
25
+ return typeof child.text === 'string' ? child.text : ''
26
+ })
27
+ .join('')
28
+ })
29
+ .join('\n')
30
+ }
package/src/types.ts ADDED
@@ -0,0 +1,27 @@
1
+ /** @public */
2
+ export interface PortableTextChild {
3
+ _type: string
4
+ _key?: string
5
+ text?: string
6
+ variableKey?: string
7
+ }
8
+
9
+ /** @public */
10
+ export interface PortableTextBlockLike {
11
+ _type: string
12
+ _key?: string
13
+ children?: PortableTextChild[]
14
+ }
15
+
16
+ /** @public */
17
+ export interface PteInterpolationVariableBlock {
18
+ _type: 'pteInterpolationVariable'
19
+ _key: string
20
+ variableKey: string
21
+ }
22
+
23
+ /** @public */
24
+ export type InterpolationValues = Record<string, string>
25
+
26
+ /** @public */
27
+ export type InterpolationFallback = (variableKey: string) => string