diff --git a/packages/intl/src/format/rich.spec.ts b/packages/intl/src/format/rich.spec.ts
index 01f76881..ca9ddbc9 100644
--- a/packages/intl/src/format/rich.spec.ts
+++ b/packages/intl/src/format/rich.spec.ts
@@ -139,6 +139,67 @@ describe('intl', () => {
'second'
])
})
+
+ it('should support nested calls in tag handlers', () => {
+ const nestedTags = {
+ ...tags,
+ wrap: (chunks: (string | Node)[]) => ({
+ type: 'wrap',
+ children: [...chunks, ...mapTags('inner', tags)]
+ })
+ }
+
+ expect(mapTags('outer and after', nestedTags)).toEqual([
+ {
+ type: 'wrap',
+ children: [
+ 'outer',
+ {
+ type: 'strong',
+ children: ['inner']
+ }
+ ]
+ },
+ ' and ',
+ {
+ type: 'strong',
+ children: ['after']
+ }
+ ])
+ })
+
+ it('should pass unique index to tag handlers', () => {
+ const indexedTags = {
+ strong: (chunks: unknown[], index: number) => ({
+ type: 'strong',
+ index,
+ children: chunks
+ }),
+ br: (_: unknown[], index: number) => ({
+ type: 'br',
+ index,
+ children: []
+ })
+ }
+
+ expect(mapTags('First
second', indexedTags)).toEqual([
+ {
+ type: 'strong',
+ index: 0,
+ children: ['First']
+ },
+ {
+ type: 'br',
+ index: 1,
+ children: []
+ },
+ {
+ type: 'strong',
+ index: 2,
+ children: ['second']
+ }
+ ])
+ })
})
})
})
diff --git a/packages/intl/src/format/rich.ts b/packages/intl/src/format/rich.ts
index c36dcb78..a318e4cc 100644
--- a/packages/intl/src/format/rich.ts
+++ b/packages/intl/src/format/rich.ts
@@ -2,7 +2,7 @@ import type { Format } from './types.js'
export type RichChunks = (string | T)[]
-export type RichTag = (chunks: RichChunks) => T
+export type RichTag = (chunks: RichChunks, index: number) => T
export type RichTags = Record>
@@ -72,6 +72,7 @@ interface RichFrame {
/**
* Maps rich-text tags in a string to chunks using provided tag handlers.
+ * Each handler call receives an unique index, usable as a key for framework nodes.
* Self-closing tags are mapped with empty chunks.
* Unknown or malformed tags are ignored as markup and only their text content is kept.
* @param input - Rich-text input string.
@@ -89,8 +90,12 @@ export function mapTags(
}
const stack = [root]
let lastIndex = 0
+ let index = 0
let match: RegExpExecArray | null
+ // reset the shared regex state possibly clobbered by an interrupted outer call
+ tagRegex.lastIndex = 0
+
while (match = tagRegex.exec(input)) {
const [, closing, tag, selfClosing] = match
const current = stack[stack.length - 1]
@@ -99,16 +104,18 @@ export function mapTags(
current.chunks.push(input.slice(lastIndex, match.index))
}
+ lastIndex = tagRegex.lastIndex
+
if (tag in tags) {
if (closing) {
if (current.tag === tag) {
stack.pop()
stack[stack.length - 1].chunks.push(
- tags[tag](current.chunks)
+ tags[tag](current.chunks, index++)
)
}
} else if (selfClosing) {
- current.chunks.push(tags[tag]([]))
+ current.chunks.push(tags[tag]([], index++))
} else {
stack.push({
tag,
@@ -117,7 +124,8 @@ export function mapTags(
}
}
- lastIndex = tagRegex.lastIndex
+ // restore the shared regex state possibly clobbered by a nested call in a tag handler
+ tagRegex.lastIndex = lastIndex
}
if (lastIndex < input.length) {
diff --git a/website/src/content/docs/intl/api.mdx b/website/src/content/docs/intl/api.mdx
index 73c03d4d..464c2689 100644
--- a/website/src/content/docs/intl/api.mdx
+++ b/website/src/content/docs/intl/api.mdx
@@ -410,17 +410,18 @@ $t().message({
### `rich(tags)` / `rich(fallback, tags)`
-Maps lightweight tags inside translated strings to rich chunks.
+Maps lightweight tags inside translated strings to rich chunks. Self-closing tags like `
` are supported and mapped with empty chunks. Each tag handler also receives an unique index, usable as a key for framework nodes.
```ts
const [$t] = messages('docs', {
- message: rich('Read the docs', {
- link: chunks => {chunks}
+ message: rich('Read the docs
today', {
+ link: (chunks, i) => {chunks},
+ br: (_, i) =>
})
})
$t().message
-// ['Read ', the docs]
+// ['Read ', the docs,
, 'today']
```
### `markup(tags)` / `markup(fallback, tags)`