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 { import type {
InterpolateProps, InterpolateProps,
InterpolateValues, InterpolateValues,
ExtractInterpolatePlaceholders,
} from '@docusaurus/Interpolate'; } 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 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 // If all the values are plain strings, then interpolate returns a simple string
export function interpolate<Str extends string>( export function interpolate<Str extends string>(
text: Str, text: Str,
@ -36,46 +33,26 @@ export function interpolate<Str extends string, Value extends ReactNode>(
text: Str, text: Str,
values?: InterpolateValues<Str, Value>, values?: InterpolateValues<Str, Value>,
): ReactNode { ): ReactNode {
const elements: (Value | string)[] = []; // eslint-disable-next-line prefer-named-capture-group
const segments = text.split(/(\{\w+\})/).map((seg, index) => {
const processedText = text.replace( // Odd indices (1, 3, 5...) of the segments are (potentially) interpolatable
// eslint-disable-next-line prefer-named-capture-group if (index % 2 === 1) {
/\{(\w+)\}/g, const value = values?.[seg.slice(1, -1) as keyof typeof values];
(match, key: ExtractInterpolatePlaceholders<Str>) => { if (value !== undefined) {
const value = values?.[key]; return value;
if (typeof value !== 'undefined') {
const element = isValidElement(value)
? value
: // For non-React elements: basic primitive->string conversion
String(value);
elements.push(element);
return ValueFoundMarker;
} }
return match; // no match? add warning? // No match: add warning? There's no way to "escape" interpolation though
}, }
); return seg;
});
// No interpolation to be done: just return the text if (segments.some((seg) => isValidElement(seg))) {
if (elements.length === 0) { return segments
return text; .map((seg, index) =>
isValidElement(seg) ? React.cloneElement(seg, {key: index}) : seg,
)
.filter((seg) => seg !== '');
} }
// Basic string interpolation: returns interpolated string return segments.join('');
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>
));
} }
export default function Interpolate<Str extends string>({ export default function Interpolate<Str extends string>({
@ -83,9 +60,10 @@ export default function Interpolate<Str extends string>({
values, values,
}: InterpolateProps<Str>): JSX.Element { }: InterpolateProps<Str>): JSX.Element {
if (typeof children !== 'string') { if (typeof children !== 'string') {
console.warn('Illegal <Interpolate> children', children);
throw new Error( 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)}</>; return <>{interpolate(children, values)}</>;

View file

@ -130,7 +130,7 @@ describe('<Interpolate>', () => {
</Interpolate>, </Interpolate>,
), ),
).toThrowErrorMatchingInlineSnapshot( ).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> <span>
today today
</span>, </span>,
"? Another {unprovidedValue}!", "? Another ",
"{unprovidedValue}",
"!",
] ]
`; `;
exports[`interpolate acceptance test 1`] = ` exports[`interpolate acceptance test 1`] = `
[ [
<React.Fragment> "Hello ",
Hello "Sébastien",
Sébastien " how are you ",
</React.Fragment>, <span>
<React.Fragment> today
how are you </span>,
<span> "? Another ",
today "{unprovidedValue}",
</span> "!",
</React.Fragment>,
<React.Fragment>
? Another {unprovidedValue}!
</React.Fragment>,
] ]
`; `;
exports[`interpolate placeholders with JSX values 1`] = ` exports[`interpolate placeholders with JSX values 1`] = `
[ [
<React.Fragment> "Hello ",
Hello <b>
<b> Sébastien
Sébastien </b>,
</b> " how are you ",
</React.Fragment>, <span>
<React.Fragment> today
how are you </span>,
<span> "?",
today
</span>
</React.Fragment>,
<React.Fragment>
?
</React.Fragment>,
] ]
`; `;
exports[`interpolate placeholders with mixed vales 1`] = ` exports[`interpolate placeholders with mixed vales 1`] = `
[ [
<React.Fragment> "Hello ",
Hello "Sébastien",
Sébastien " how are you ",
</React.Fragment>, <span>
<React.Fragment> today
how are you </span>,
<span> "?",
today
</span>
</React.Fragment>,
<React.Fragment>
?
</React.Fragment>,
] ]
`; `;

View file

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