fix(core): preserve Interpolate children semantics (#7103)

* fix(core): preserve Interpolate children semantics

* fix

* fix again
This commit is contained in:
Joshua Chen 2022-04-03 15:16:30 +08:00 committed by GitHub
parent c7c0ee4e7c
commit 85f47fd8f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 54 additions and 86 deletions

View file

@ -9,7 +9,6 @@ import React, {isValidElement, type ReactNode} from 'react';
import type {
InterpolateProps,
InterpolateValues,
ExtractInterpolatePlaceholders,
} from '@docusaurus/Interpolate';
/*
@ -18,8 +17,6 @@ We don't ship a markdown parser nor a feature-complete i18n library on purpose.
More details here: https://github.com/facebook/docusaurus/pull/4295
*/
const ValueFoundMarker = '{}'; // does not care much
// If all the values are plain strings, then interpolate returns a simple string
export function interpolate<Str extends string>(
text: Str,
@ -36,46 +33,26 @@ export function interpolate<Str extends string, Value extends ReactNode>(
text: Str,
values?: InterpolateValues<Str, Value>,
): ReactNode {
const elements: (Value | string)[] = [];
const processedText = text.replace(
// eslint-disable-next-line prefer-named-capture-group
/\{(\w+)\}/g,
(match, key: ExtractInterpolatePlaceholders<Str>) => {
const value = values?.[key];
if (typeof value !== 'undefined') {
const element = isValidElement(value)
? value
: // For non-React elements: basic primitive->string conversion
String(value);
elements.push(element);
return ValueFoundMarker;
// eslint-disable-next-line prefer-named-capture-group
const segments = text.split(/(\{\w+\})/).map((seg, index) => {
// Odd indices (1, 3, 5...) of the segments are (potentially) interpolatable
if (index % 2 === 1) {
const value = values?.[seg.slice(1, -1) as keyof typeof values];
if (value !== undefined) {
return value;
}
return match; // no match? add warning?
},
);
// No interpolation to be done: just return the text
if (elements.length === 0) {
return text;
// No match: add warning? There's no way to "escape" interpolation though
}
return seg;
});
if (segments.some((seg) => isValidElement(seg))) {
return segments
.map((seg, index) =>
isValidElement(seg) ? React.cloneElement(seg, {key: index}) : seg,
)
.filter((seg) => seg !== '');
}
// Basic string interpolation: returns interpolated string
if (elements.every((el): el is string => typeof el === 'string')) {
return processedText
.split(ValueFoundMarker)
.reduce<string>(
(str, value, index) => str.concat(value).concat(elements[index] ?? ''),
'',
);
}
// JSX interpolation: returns ReactNode
return processedText.split(ValueFoundMarker).map((value, index) => (
<React.Fragment key={index}>
{value}
{elements[index]}
</React.Fragment>
));
return segments.join('');
}
export default function Interpolate<Str extends string>({
@ -83,9 +60,10 @@ export default function Interpolate<Str extends string>({
values,
}: InterpolateProps<Str>): JSX.Element {
if (typeof children !== 'string') {
console.warn('Illegal <Interpolate> children', children);
throw new Error(
'The Docusaurus <Interpolate> component only accept simple string values',
`The Docusaurus <Interpolate> component only accept simple string values. Received: ${
isValidElement(children) ? 'React element' : typeof children
}`,
);
}
return <>{interpolate(children, values)}</>;

View file

@ -130,7 +130,7 @@ describe('<Interpolate>', () => {
</Interpolate>,
),
).toThrowErrorMatchingInlineSnapshot(
`"The Docusaurus <Interpolate> component only accept simple string values"`,
`"The Docusaurus <Interpolate> component only accept simple string values. Received: React element"`,
);
});
});

View file

@ -8,62 +8,48 @@ exports[`<Interpolate> acceptance test 1`] = `
<span>
today
</span>,
"? Another {unprovidedValue}!",
"? Another ",
"{unprovidedValue}",
"!",
]
`;
exports[`interpolate acceptance test 1`] = `
[
<React.Fragment>
Hello
Sébastien
</React.Fragment>,
<React.Fragment>
how are you
<span>
today
</span>
</React.Fragment>,
<React.Fragment>
? Another {unprovidedValue}!
</React.Fragment>,
"Hello ",
"Sébastien",
" how are you ",
<span>
today
</span>,
"? Another ",
"{unprovidedValue}",
"!",
]
`;
exports[`interpolate placeholders with JSX values 1`] = `
[
<React.Fragment>
Hello
<b>
Sébastien
</b>
</React.Fragment>,
<React.Fragment>
how are you
<span>
today
</span>
</React.Fragment>,
<React.Fragment>
?
</React.Fragment>,
"Hello ",
<b>
Sébastien
</b>,
" how are you ",
<span>
today
</span>,
"?",
]
`;
exports[`interpolate placeholders with mixed vales 1`] = `
[
<React.Fragment>
Hello
Sébastien
</React.Fragment>,
<React.Fragment>
how are you
<span>
today
</span>
</React.Fragment>,
<React.Fragment>
?
</React.Fragment>,
"Hello ",
"Sébastien",
" how are you ",
<span>
today
</span>,
"?",
]
`;

View file

@ -7,6 +7,7 @@
import React from 'react';
import Layout from '@theme/Layout';
import Interpolate from '@docusaurus/Interpolate';
import ErrorBoundaryTestButton from '@site/src/components/ErrorBoundaryTestButton';
@ -22,6 +23,9 @@ export default function ErrorBoundaryTests(): JSX.Element {
Crash inside layout
</ErrorBoundaryTestButton>
</div>
<Interpolate values={{foo: <span>FooFoo</span>, bar: <b>BarBar</b>}}>
{'{foo} is {bar}'}
</Interpolate>
</main>
</Layout>
</>