botframework-webchat-component 4.19.0 → 4.19.1-main.20260529.6bcfcee
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/dist/_dtsroll-chunks/CwOuXmoL-botframework-webchat-styles.react.d.ts +68 -0
- package/dist/botframework-webchat-component.component.d.mts +1 -1
- package/dist/botframework-webchat-component.component.d.ts +1 -1
- package/dist/botframework-webchat-component.component.js +1 -1
- package/dist/botframework-webchat-component.component.mjs +1 -1
- package/dist/botframework-webchat-component.d.mts +1 -3
- package/dist/botframework-webchat-component.d.ts +1 -3
- package/dist/botframework-webchat-component.decorator.js +1 -1
- package/dist/botframework-webchat-component.decorator.js.map +1 -1
- package/dist/botframework-webchat-component.decorator.mjs +1 -1
- package/dist/botframework-webchat-component.decorator.mjs.map +1 -1
- package/dist/botframework-webchat-component.hook.js +1 -1
- package/dist/botframework-webchat-component.hook.mjs +1 -1
- package/dist/botframework-webchat-component.internal.d.mts +2 -6
- package/dist/botframework-webchat-component.internal.d.ts +2 -6
- package/dist/botframework-webchat-component.internal.js +1 -1
- package/dist/botframework-webchat-component.internal.js.map +1 -1
- package/dist/botframework-webchat-component.internal.mjs +1 -1
- package/dist/botframework-webchat-component.js +1 -1
- package/dist/botframework-webchat-component.js.map +1 -1
- package/dist/botframework-webchat-component.mjs +1 -1
- package/dist/botframework-webchat-component.mjs.map +1 -1
- package/dist/chunk-4EA5WZBJ.mjs +80 -0
- package/dist/chunk-4EA5WZBJ.mjs.map +1 -0
- package/dist/{chunk-H5YR7OLF.js → chunk-B2XHGOQH.js} +2 -2
- package/dist/{chunk-H5YR7OLF.js.map → chunk-B2XHGOQH.js.map} +1 -1
- package/dist/chunk-C2RHHZZQ.mjs +2 -0
- package/dist/chunk-C2RHHZZQ.mjs.map +1 -0
- package/dist/{chunk-TYPS3H4I.mjs → chunk-FIPA3BLZ.mjs} +2 -2
- package/dist/{chunk-TYPS3H4I.mjs.map → chunk-FIPA3BLZ.mjs.map} +1 -1
- package/dist/{chunk-LVVCSDZ4.mjs → chunk-HWDSXHRJ.mjs} +2 -2
- package/dist/chunk-JCX7GSY7.js +2 -0
- package/dist/{chunk-2R7BJ63Z.js.map → chunk-JCX7GSY7.js.map} +1 -1
- package/dist/{chunk-U6OWCHTQ.js → chunk-K4QNZHM5.js} +2 -2
- package/dist/chunk-K4QNZHM5.js.map +1 -0
- package/dist/{chunk-MOJMHOVH.js → chunk-OJOV52AD.js} +2 -2
- package/dist/{chunk-MOJMHOVH.js.map → chunk-OJOV52AD.js.map} +1 -1
- package/dist/{chunk-GTOP3WPD.mjs → chunk-WPWJFSZC.mjs} +2 -2
- package/dist/chunk-WPWJFSZC.mjs.map +1 -0
- package/dist/chunk-ZUKHLDZN.js +80 -0
- package/dist/chunk-ZUKHLDZN.js.map +1 -0
- package/dist/{component-BtSxgJS5.d.mts → component-BeCWAilk.d.mts} +34 -43
- package/dist/{component-Fyy8iCRE.d.ts → component-BtDQyqSP.d.ts} +34 -43
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/package.json +12 -14
- package/src/ActivityStatus/SendStatus/private/SendFailedRetry.tsx +31 -0
- package/src/BasicToast.js +4 -5
- package/src/BasicToaster.js +2 -3
- package/src/Composer.tsx +0 -20
- package/src/Utils/InlineMarkdown.tsx +121 -0
- package/src/Utils/LocalizedString.tsx +123 -124
- package/src/Utils/addTargetBlankToHyperlinks.spec.ts +52 -0
- package/src/Utils/addTargetBlankToHyperlinks.ts +14 -0
- package/src/boot/internal.ts +7 -2
- package/src/hooks/internal/WebChatUIContext.ts +0 -2
- package/src/hooks/useRenderMarkdownAsHTML.ts +5 -3
- package/src/hooks/useStreamingMarkdownWithDefinitions.ts +2 -2
- package/src/private/renderMarkdownInline.ts +19 -0
- package/src/providers/CustomElements/customElements/CodeBlock.ts +2 -2
- package/dist/_dtsroll-chunks/Cha1SOtx-botframework-webchat-styles.react.d.ts +0 -38
- package/dist/chunk-2R7BJ63Z.js +0 -2
- package/dist/chunk-A4NDFSZM.mjs +0 -77
- package/dist/chunk-A4NDFSZM.mjs.map +0 -1
- package/dist/chunk-CNTMOACS.mjs +0 -2
- package/dist/chunk-CNTMOACS.mjs.map +0 -1
- package/dist/chunk-GTOP3WPD.mjs.map +0 -1
- package/dist/chunk-U6OWCHTQ.js.map +0 -1
- package/dist/chunk-VDF6GQAL.js +0 -77
- package/dist/chunk-VDF6GQAL.js.map +0 -1
- package/src/ActivityStatus/SendStatus/private/SendFailedRetry.js +0 -28
- package/src/Utils/InlineMarkdown.js +0 -154
- package/src/Utils/addTargetBlankToHyperlinksMarkdown.js +0 -28
- package/src/Utils/addTargetBlankToHyperlinksMarkdown.spec.js +0 -45
- package/src/Utils/betterLinks.ts +0 -157
- package/src/Utils/parseDocumentFragmentFromString.ts +0 -9
- package/src/Utils/serializeDocumentFragmentIntoString.ts +0 -3
- package/src/Utils/updateMarkdownAttrs.js +0 -10
- package/src/Utils/updateMarkdownAttrs.spec.js +0 -71
- package/src/Utils/walkMarkdownTokens.js +0 -15
- package/src/Utils/walkMarkdownTokens.spec.js +0 -18
- package/src/hooks/internal/useInternalMarkdownIt.js +0 -7
- package/src/hooks/internal/useInternalRenderMarkdownInline.js +0 -9
- /package/dist/{chunk-LVVCSDZ4.mjs.map → chunk-HWDSXHRJ.mjs.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botframework-webchat-component",
|
|
3
|
-
"version": "4.19.
|
|
3
|
+
"version": "4.19.1-main.20260529.6bcfcee",
|
|
4
4
|
"description": "React component of botframework-webchat",
|
|
5
5
|
"main": "./dist/botframework-webchat-component.js",
|
|
6
6
|
"types": "./dist/botframework-webchat-component.d.ts",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"precommit:eslint": "../../node_modules/.bin/eslint --report-unused-disable-directives --max-warnings 0",
|
|
127
127
|
"precommit:typecheck": "tsc --project ./src --emitDeclarationOnly false --esModuleInterop true --noEmit --pretty false",
|
|
128
128
|
"preversion": "../../scripts/npm/preversion.sh",
|
|
129
|
-
"start": "../../scripts/npm/notify-build.sh \"src\" \"../react-hooks/package.json\" \"../react-valibot/package.json\" \"../styles/package.json\""
|
|
129
|
+
"start": "../../scripts/npm/notify-build.sh \"src\" \"../component-better-link/package.json\" \"../react-hooks/package.json\" \"../react-valibot/package.json\" \"../styles/package.json\""
|
|
130
130
|
},
|
|
131
131
|
"pinDependencies": {
|
|
132
132
|
"@types/jest": [
|
|
@@ -141,10 +141,6 @@
|
|
|
141
141
|
"1",
|
|
142
142
|
"@>=2 does not support IE Mode"
|
|
143
143
|
],
|
|
144
|
-
"markdown-it": [
|
|
145
|
-
"13",
|
|
146
|
-
"markdown-it@14.1.0 has module field and it is breaking Webpack 4 because cross loading CJS and ESM"
|
|
147
|
-
],
|
|
148
144
|
"react": [
|
|
149
145
|
"16.8.6",
|
|
150
146
|
"using react@16.8.6 to make sure this is the minimum supported version"
|
|
@@ -166,6 +162,7 @@
|
|
|
166
162
|
},
|
|
167
163
|
"localDependencies": {
|
|
168
164
|
"@msinternal/botframework-webchat-base": "development",
|
|
165
|
+
"@msinternal/botframework-webchat-component-better-link": "development",
|
|
169
166
|
"@msinternal/botframework-webchat-react-hooks": "development",
|
|
170
167
|
"@msinternal/botframework-webchat-react-valibot": "development",
|
|
171
168
|
"@msinternal/botframework-webchat-styles": "development",
|
|
@@ -176,11 +173,12 @@
|
|
|
176
173
|
"@babel/preset-env": "^7.29.2",
|
|
177
174
|
"@babel/preset-react": "^7.28.5",
|
|
178
175
|
"@babel/preset-typescript": "^7.28.5",
|
|
179
|
-
"@msinternal/botframework-webchat-base": "4.19.
|
|
180
|
-
"@msinternal/botframework-webchat-
|
|
181
|
-
"@msinternal/botframework-webchat-react-
|
|
182
|
-
"@msinternal/botframework-webchat-
|
|
183
|
-
"@msinternal/botframework-webchat-
|
|
176
|
+
"@msinternal/botframework-webchat-base": "4.19.1-main.20260529.6bcfcee",
|
|
177
|
+
"@msinternal/botframework-webchat-component-better-link": "4.19.1-main.20260529.6bcfcee",
|
|
178
|
+
"@msinternal/botframework-webchat-react-hooks": "4.19.1-main.20260529.6bcfcee",
|
|
179
|
+
"@msinternal/botframework-webchat-react-valibot": "4.19.1-main.20260529.6bcfcee",
|
|
180
|
+
"@msinternal/botframework-webchat-styles": "4.19.1-main.20260529.6bcfcee",
|
|
181
|
+
"@msinternal/botframework-webchat-tsconfig": "4.19.1-main.20260529.6bcfcee",
|
|
184
182
|
"@types/dom-speech-recognition": "^0.0.9",
|
|
185
183
|
"@types/jest": "^29.5.14",
|
|
186
184
|
"@types/mdast": "^4.0.4",
|
|
@@ -196,16 +194,16 @@
|
|
|
196
194
|
"dependencies": {
|
|
197
195
|
"@emotion/css": "11.13.5",
|
|
198
196
|
"base64-js": "1.5.1",
|
|
199
|
-
"botframework-webchat-api": "4.19.
|
|
200
|
-
"botframework-webchat-core": "4.19.
|
|
197
|
+
"botframework-webchat-api": "4.19.1-main.20260529.6bcfcee",
|
|
198
|
+
"botframework-webchat-core": "4.19.1-main.20260529.6bcfcee",
|
|
201
199
|
"classnames": "2.5.1",
|
|
202
200
|
"compute-scroll-into-view": "1.0.20",
|
|
203
201
|
"deep-freeze-strict": "1.1.1",
|
|
204
|
-
"markdown-it": "13.0.2",
|
|
205
202
|
"math-random": "2.0.1",
|
|
206
203
|
"mdast-util-from-markdown": "2.0.3",
|
|
207
204
|
"memoize-one": "6.0.0",
|
|
208
205
|
"merge-refs": "2.0.0",
|
|
206
|
+
"micromark": "4.0.2",
|
|
209
207
|
"prop-types": "15.8.1",
|
|
210
208
|
"punycode": "2.3.1",
|
|
211
209
|
"react-chain-of-responsibility": "0.4.2",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
|
|
2
|
+
import { useLocalizer } from 'botframework-webchat-api/hook.js';
|
|
3
|
+
import React, { useCallback } from 'react';
|
|
4
|
+
import { function_, object, pipe, readonly, type InferInput } from 'valibot';
|
|
5
|
+
|
|
6
|
+
import InlineMarkdown from '../../../Utils/InlineMarkdown';
|
|
7
|
+
|
|
8
|
+
const MARKDOWN_REFERENCES = ['RETRY'];
|
|
9
|
+
|
|
10
|
+
const sendFailedRetryPropsSchema = pipe(
|
|
11
|
+
object({
|
|
12
|
+
onRetryClick: function_()
|
|
13
|
+
}),
|
|
14
|
+
readonly()
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
type SendFailedRetryProps = InferInput<typeof sendFailedRetryPropsSchema>;
|
|
18
|
+
|
|
19
|
+
const SendFailedRetry = (props: SendFailedRetryProps) => {
|
|
20
|
+
const { onRetryClick } = validateProps(sendFailedRetryPropsSchema, props);
|
|
21
|
+
|
|
22
|
+
const handleReference = useCallback(({ data }) => data === 'RETRY' && onRetryClick(), [onRetryClick]);
|
|
23
|
+
const localize = useLocalizer();
|
|
24
|
+
|
|
25
|
+
const sendFailedText = localize('ACTIVITY_STATUS_SEND_FAILED_RETRY');
|
|
26
|
+
|
|
27
|
+
return <InlineMarkdown markdown={sendFailedText} onReference={handleReference} references={MARKDOWN_REFERENCES} />;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default SendFailedRetry;
|
|
31
|
+
export { sendFailedRetryPropsSchema, type SendFailedRetryProps };
|
package/src/BasicToast.js
CHANGED
|
@@ -6,13 +6,13 @@ import classNames from 'classnames';
|
|
|
6
6
|
import PropTypes from 'prop-types';
|
|
7
7
|
import React, { useCallback, useMemo } from 'react';
|
|
8
8
|
|
|
9
|
+
import ScreenReaderText from './ScreenReaderText';
|
|
9
10
|
import DismissIcon from './Toast/DismissIcon';
|
|
10
11
|
import NotificationIcon from './Toast/NotificationIcon';
|
|
11
12
|
import randomId from './Utils/randomId';
|
|
12
|
-
import ScreenReaderText from './ScreenReaderText';
|
|
13
|
-
import useInternalRenderMarkdownInline from './hooks/internal/useInternalRenderMarkdownInline';
|
|
14
|
-
import useStyleSet from './hooks/useStyleSet';
|
|
15
13
|
import { useStyleToEmotionObject } from './hooks/internal/styleToEmotionObject';
|
|
14
|
+
import useStyleSet from './hooks/useStyleSet';
|
|
15
|
+
import renderMarkdownInline from './private/renderMarkdownInline';
|
|
16
16
|
|
|
17
17
|
const { useDismissNotification, useLocalizer } = hooks;
|
|
18
18
|
|
|
@@ -29,11 +29,10 @@ const BasicToast = ({ notification: { alt, id, level, message = '' } }) => {
|
|
|
29
29
|
const contentId = useMemo(() => `webchat__toast__${randomId()}`, []);
|
|
30
30
|
const localize = useLocalizer();
|
|
31
31
|
const dismissNotification = useDismissNotification();
|
|
32
|
-
const renderMarkdownInline = useInternalRenderMarkdownInline();
|
|
33
32
|
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
|
|
34
33
|
|
|
35
34
|
const handleDismiss = useCallback(() => dismissNotification(id), [dismissNotification, id]);
|
|
36
|
-
const html = useMemo(() => ({ __html: renderMarkdownInline(message) }), [message
|
|
35
|
+
const html = useMemo(() => ({ __html: renderMarkdownInline(message) }), [message]);
|
|
37
36
|
|
|
38
37
|
return (
|
|
39
38
|
<div
|
package/src/BasicToaster.js
CHANGED
|
@@ -9,9 +9,9 @@ import CollapseIcon from './Toast/CollapseIcon';
|
|
|
9
9
|
import ExpandIcon from './Toast/ExpandIcon';
|
|
10
10
|
import NotificationIcon from './Toast/NotificationIcon';
|
|
11
11
|
import randomId from './Utils/randomId';
|
|
12
|
-
import useInternalRenderMarkdownInline from './hooks/internal/useInternalRenderMarkdownInline';
|
|
13
12
|
import { useStyleToEmotionObject } from './hooks/internal/styleToEmotionObject';
|
|
14
13
|
import useStyleSet from './hooks/useStyleSet';
|
|
14
|
+
import renderMarkdownInline from './private/renderMarkdownInline';
|
|
15
15
|
import { useLiveRegion } from './providers/LiveRegionTwin';
|
|
16
16
|
|
|
17
17
|
const { useDebouncedNotifications, useLocalizer, useRenderToast } = hooks;
|
|
@@ -77,7 +77,6 @@ const BasicToaster = ({ className }) => {
|
|
|
77
77
|
const [expanded, setExpanded] = useState(false);
|
|
78
78
|
const localizeWithPlural = useLocalizer({ plural: true });
|
|
79
79
|
const renderToast = useRenderToast();
|
|
80
|
-
const renderMarkdownInline = useInternalRenderMarkdownInline();
|
|
81
80
|
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
|
|
82
81
|
|
|
83
82
|
const handleToggleExpand = useCallback(() => setExpanded(!expanded), [expanded, setExpanded]);
|
|
@@ -142,7 +141,7 @@ const BasicToaster = ({ className }) => {
|
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
return toAnnounce.length > 0 && <Fragment>{toAnnounce}</Fragment>;
|
|
145
|
-
}, [
|
|
144
|
+
}, [sortedNotifications]);
|
|
146
145
|
|
|
147
146
|
return (
|
|
148
147
|
<div
|
package/src/Composer.tsx
CHANGED
|
@@ -24,7 +24,6 @@ import { DecoratorComposer, type DecoratorMiddleware } from 'botframework-webcha
|
|
|
24
24
|
import { type LegacyActivityMiddleware, type Polymiddleware } from 'botframework-webchat-api/middleware.js';
|
|
25
25
|
import { StoreDebugAPIRegistry, type StoreDebugAPI } from 'botframework-webchat-core/internal.js';
|
|
26
26
|
import classNames from 'classnames';
|
|
27
|
-
import MarkdownIt from 'markdown-it';
|
|
28
27
|
import PropTypes from 'prop-types';
|
|
29
28
|
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
30
29
|
import { Composer as SayComposer } from 'react-say';
|
|
@@ -65,7 +64,6 @@ import CSSCustomPropertiesContainer from './Styles/CSSCustomPropertiesContainer'
|
|
|
65
64
|
import ComponentStylesheet from './stylesheet/ComponentStylesheet';
|
|
66
65
|
import { type ContextOf } from './types/ContextOf';
|
|
67
66
|
import { type FocusTranscriptInit } from './types/internal/FocusTranscriptInit';
|
|
68
|
-
import addTargetBlankToHyperlinksMarkdown from './Utils/addTargetBlankToHyperlinksMarkdown';
|
|
69
67
|
import downscaleImageToDataURL from './Utils/downscaleImageToDataURL';
|
|
70
68
|
import mapMap from './Utils/mapMap';
|
|
71
69
|
|
|
@@ -188,23 +186,9 @@ const ComposerCore = ({
|
|
|
188
186
|
const [referenceGrammarID] = useReferenceGrammarID();
|
|
189
187
|
const [styleOptions] = useStyleOptions();
|
|
190
188
|
const focusTranscriptCallbacksRef = useRef<((init: FocusTranscriptInit) => Promise<void>)[]>([]);
|
|
191
|
-
const internalMarkdownIt = useMemo(() => new MarkdownIt(), []);
|
|
192
189
|
const scrollToCallbacksRef = useRef([]);
|
|
193
190
|
const scrollToEndCallbacksRef = useRef([]);
|
|
194
191
|
|
|
195
|
-
const internalRenderMarkdownInline = useMemo(
|
|
196
|
-
() => markdown => {
|
|
197
|
-
const tree = internalMarkdownIt.parseInline(markdown);
|
|
198
|
-
|
|
199
|
-
// TODO: Use "betterLink" plugin.
|
|
200
|
-
// We should add rel="noopener noreferrer" and target="_blank"
|
|
201
|
-
const patchedTree = addTargetBlankToHyperlinksMarkdown(tree);
|
|
202
|
-
|
|
203
|
-
return internalMarkdownIt.renderer.render(patchedTree);
|
|
204
|
-
},
|
|
205
|
-
[internalMarkdownIt]
|
|
206
|
-
);
|
|
207
|
-
|
|
208
192
|
const styleToEmotionObject = useStyleToEmotionObject();
|
|
209
193
|
|
|
210
194
|
const patchedStyleSet = useMemo(
|
|
@@ -289,8 +273,6 @@ const ComposerCore = ({
|
|
|
289
273
|
dispatchScrollPosition,
|
|
290
274
|
dispatchTranscriptFocusByActivityKey,
|
|
291
275
|
focusTranscriptCallbacksRef,
|
|
292
|
-
internalMarkdownItState: [internalMarkdownIt],
|
|
293
|
-
internalRenderMarkdownInline,
|
|
294
276
|
nonce,
|
|
295
277
|
numTranscriptFocusObservers,
|
|
296
278
|
observeScrollPosition,
|
|
@@ -308,8 +290,6 @@ const ComposerCore = ({
|
|
|
308
290
|
dispatchScrollPosition,
|
|
309
291
|
dispatchTranscriptFocusByActivityKey,
|
|
310
292
|
focusTranscriptCallbacksRef,
|
|
311
|
-
internalMarkdownIt,
|
|
312
|
-
internalRenderMarkdownInline,
|
|
313
293
|
nonce,
|
|
314
294
|
numTranscriptFocusObservers,
|
|
315
295
|
observeScrollPosition,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* eslint react/no-danger: "off" */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
betterLinkDocumentMod,
|
|
5
|
+
parseDocumentFragmentFromString,
|
|
6
|
+
serializeDocumentFragmentIntoString,
|
|
7
|
+
stripParagraphContainer
|
|
8
|
+
} from '@msinternal/botframework-webchat-component-better-link';
|
|
9
|
+
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
|
|
10
|
+
import { useStyleOptions } from 'botframework-webchat-api/hook.js';
|
|
11
|
+
import { micromark } from 'micromark';
|
|
12
|
+
import React, { useCallback, useMemo } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
args,
|
|
15
|
+
array,
|
|
16
|
+
function_,
|
|
17
|
+
instance,
|
|
18
|
+
object,
|
|
19
|
+
optional,
|
|
20
|
+
parse,
|
|
21
|
+
pipe,
|
|
22
|
+
readonly,
|
|
23
|
+
string,
|
|
24
|
+
tuple,
|
|
25
|
+
union,
|
|
26
|
+
type InferInput
|
|
27
|
+
} from 'valibot';
|
|
28
|
+
|
|
29
|
+
import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
|
|
30
|
+
import createCustomEvent from './createCustomEvent';
|
|
31
|
+
|
|
32
|
+
const referenceEventSchema = union([instance(Event), object({ data: string() })]);
|
|
33
|
+
|
|
34
|
+
const inlineMarkdownPropsSchema = pipe(
|
|
35
|
+
object({
|
|
36
|
+
markdown: string(),
|
|
37
|
+
onReference: pipe(function_(), args(tuple([referenceEventSchema]))),
|
|
38
|
+
references: optional(array(string()))
|
|
39
|
+
}),
|
|
40
|
+
readonly()
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
type InlineMarkdownProps = InferInput<typeof inlineMarkdownPropsSchema>;
|
|
44
|
+
|
|
45
|
+
const InlineMarkdown = (props: InlineMarkdownProps) => {
|
|
46
|
+
const { markdown, onReference, references } = validateProps(inlineMarkdownPropsSchema, props);
|
|
47
|
+
|
|
48
|
+
const [{ accent }] = useStyleOptions();
|
|
49
|
+
const styleToClassName = useStyleToEmotionObject();
|
|
50
|
+
|
|
51
|
+
// We inlined the style here because this style is:
|
|
52
|
+
// 1. Internal to Web Chat
|
|
53
|
+
// 2. Not customizable from developers (other than setting `styleOptions.accent`)
|
|
54
|
+
const className = useMemo(
|
|
55
|
+
() =>
|
|
56
|
+
styleToClassName({
|
|
57
|
+
'& button[data-markdown-href]': {
|
|
58
|
+
appearance: 'none',
|
|
59
|
+
backgroundColor: 'transparent',
|
|
60
|
+
border: 0,
|
|
61
|
+
color: accent,
|
|
62
|
+
cursor: 'pointer',
|
|
63
|
+
fontFamily: 'inherit',
|
|
64
|
+
fontSize: 'inherit',
|
|
65
|
+
padding: 0
|
|
66
|
+
},
|
|
67
|
+
'@media screen and (forced-colors: active)': {
|
|
68
|
+
'& button[data-markdown-href]': {
|
|
69
|
+
color: 'LinkText',
|
|
70
|
+
textDecoration: 'underline'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}) + '',
|
|
74
|
+
[accent, styleToClassName]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const html = useMemo(() => {
|
|
78
|
+
let markdownWithLinkReferenceDefinitions = markdown;
|
|
79
|
+
|
|
80
|
+
if (references?.length) {
|
|
81
|
+
markdownWithLinkReferenceDefinitions += '\n\n';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const reference of references || []) {
|
|
85
|
+
markdownWithLinkReferenceDefinitions += `[${reference}]: #${reference}\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const documentFragment = parseDocumentFragmentFromString(micromark(markdownWithLinkReferenceDefinitions));
|
|
89
|
+
|
|
90
|
+
// Turn "<a href="#retry">Retry</a>" into "<button type="button" data-markdown-ref="#retry">Retry</button>"
|
|
91
|
+
betterLinkDocumentMod(documentFragment, href => {
|
|
92
|
+
if (href?.startsWith('#')) {
|
|
93
|
+
return { asButton: true, dataset: { markdownHref: href } };
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { __html: stripParagraphContainer(serializeDocumentFragmentIntoString(documentFragment)) };
|
|
98
|
+
}, [markdown, references]);
|
|
99
|
+
|
|
100
|
+
const handleClick = useCallback(
|
|
101
|
+
event => {
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
|
|
104
|
+
const href = event.target.getAttribute('value') ?? undefined;
|
|
105
|
+
const reference = href?.startsWith('#') ? href.slice(1) : href;
|
|
106
|
+
|
|
107
|
+
if (reference) {
|
|
108
|
+
const event = createCustomEvent('reference', { data: reference });
|
|
109
|
+
|
|
110
|
+
parse(referenceEventSchema, event);
|
|
111
|
+
onReference?.(event);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[onReference]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return <span className={className} dangerouslySetInnerHTML={html} onClick={handleClick} />;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default InlineMarkdown;
|
|
121
|
+
export { inlineMarkdownPropsSchema, type InlineMarkdownProps };
|
|
@@ -1,143 +1,142 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
betterLinkDocumentMod,
|
|
3
|
+
parseDocumentFragmentFromString,
|
|
4
|
+
serializeDocumentFragmentIntoString,
|
|
5
|
+
stripParagraphContainer,
|
|
6
|
+
type BetterLinkDocumentModDecoration
|
|
7
|
+
} from '@msinternal/botframework-webchat-component-better-link';
|
|
8
|
+
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
|
|
9
|
+
import { useLocalizer } from 'botframework-webchat-api/hook.js';
|
|
4
10
|
import { onErrorResumeNext } from 'botframework-webchat-core';
|
|
5
|
-
import
|
|
11
|
+
import { micromark } from 'micromark';
|
|
6
12
|
import React, { memo, useMemo } from 'react';
|
|
7
|
-
import
|
|
13
|
+
import {
|
|
14
|
+
array,
|
|
15
|
+
intersect,
|
|
16
|
+
is,
|
|
17
|
+
number,
|
|
18
|
+
object,
|
|
19
|
+
optional,
|
|
20
|
+
pipe,
|
|
21
|
+
readonly,
|
|
22
|
+
string,
|
|
23
|
+
tupleWithRest,
|
|
24
|
+
union,
|
|
25
|
+
type InferInput,
|
|
26
|
+
type InferOutput
|
|
27
|
+
} from 'valibot';
|
|
8
28
|
|
|
9
29
|
const allowedSchemes = ['data', 'http', 'https', 'ftp', 'mailto', 'sip', 'tel'];
|
|
10
30
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.filter((className: string | undefined) => className)
|
|
45
|
-
.join(' ');
|
|
46
|
-
|
|
47
|
-
ariaLabelSegments.push(externalLinkAlt);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// The first segment is textContent. Putting textContent is aria-label is useless.
|
|
51
|
-
if (ariaLabelSegments.length > 1) {
|
|
52
|
-
// If "aria-label" is already applied, do not overwrite it.
|
|
53
|
-
decoration.ariaLabel ??= (value: string) => value || ariaLabelSegments.join(' ');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (typeof linkOptions?.className === 'string') {
|
|
57
|
-
classes.add(linkOptions.className);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Resolve className
|
|
61
|
-
const classNamesString = Array.from(classes).join(' ');
|
|
62
|
-
if (linkOptions?.className && linkOptions?.className instanceof Function) {
|
|
63
|
-
decoration.className = linkOptions.className(classNamesString);
|
|
64
|
-
} else {
|
|
65
|
-
decoration.className = classNamesString;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// By default, Markdown-It will set "title" to the link title in link definition.
|
|
69
|
-
|
|
70
|
-
// However, "title" may be narrated by screen reader:
|
|
71
|
-
// - Edge
|
|
72
|
-
// - <a> will narrate "aria-label" but not "title"
|
|
73
|
-
// - <button> will narrate both "aria-label" and "title"
|
|
74
|
-
// - NVDA
|
|
75
|
-
// - <a> will narrate both "aria-label" and "title"
|
|
76
|
-
// - <button> will narrate both "aria-label" and "title"
|
|
77
|
-
|
|
78
|
-
// Title makes it very difficult to control narrations by the screen reader. Thus, we are disabling it in favor of "aria-label".
|
|
79
|
-
// This will not affect our accessibility compliance but UX. We could use a non-native tooltip or other forms of visual hint.
|
|
80
|
-
|
|
81
|
-
decoration.title ??= false;
|
|
82
|
-
|
|
83
|
-
return decoration;
|
|
84
|
-
};
|
|
31
|
+
const pluralPropsSchema = pipe(
|
|
32
|
+
object({
|
|
33
|
+
stringIds: object({
|
|
34
|
+
zero: optional(string()),
|
|
35
|
+
one: optional(string()),
|
|
36
|
+
two: optional(string()),
|
|
37
|
+
few: optional(string()),
|
|
38
|
+
many: optional(string()),
|
|
39
|
+
other: string()
|
|
40
|
+
}),
|
|
41
|
+
values: pipe(tupleWithRest([number()], union([string()])), readonly())
|
|
42
|
+
}),
|
|
43
|
+
readonly()
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const singularPropsSchema = pipe(
|
|
47
|
+
object({
|
|
48
|
+
stringIds: string(),
|
|
49
|
+
values: optional(pipe(array(string()), readonly()))
|
|
50
|
+
}),
|
|
51
|
+
readonly()
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const localizedStringPropsSchema = intersect([
|
|
55
|
+
pipe(
|
|
56
|
+
object({
|
|
57
|
+
className: optional(string()),
|
|
58
|
+
linkClassName: optional(string())
|
|
59
|
+
}),
|
|
60
|
+
readonly()
|
|
61
|
+
),
|
|
62
|
+
union([pluralPropsSchema, singularPropsSchema])
|
|
63
|
+
]);
|
|
85
64
|
|
|
86
|
-
|
|
65
|
+
type LocalizedStringProps = InferInput<typeof localizedStringPropsSchema>;
|
|
66
|
+
type PluralProps = InferOutput<typeof pluralPropsSchema>;
|
|
87
67
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
two?: string;
|
|
92
|
-
few?: string;
|
|
93
|
-
many?: string;
|
|
94
|
-
other: string;
|
|
95
|
-
};
|
|
68
|
+
function isPlural(props: InferOutput<typeof localizedStringPropsSchema>): props is PluralProps {
|
|
69
|
+
return is(pluralPropsSchema, props);
|
|
70
|
+
}
|
|
96
71
|
|
|
97
|
-
const
|
|
72
|
+
const LocalizedString = (props: LocalizedStringProps) => {
|
|
73
|
+
const { className, linkClassName } = validateProps(localizedStringPropsSchema, props);
|
|
98
74
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
values: readonly [number, ...(number | string)[]] | undefined;
|
|
102
|
-
}>;
|
|
75
|
+
const localizePlural = useLocalizer({ plural: true });
|
|
76
|
+
const localizeSingular = useLocalizer();
|
|
103
77
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
values?: readonly (number | string)[] | undefined;
|
|
107
|
-
}>;
|
|
78
|
+
// TODO: Add test.
|
|
79
|
+
const externalLinkAlt = localizeSingular('MARKDOWN_EXTERNAL_LINK_ALT');
|
|
108
80
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
linkClassName?: string | undefined;
|
|
112
|
-
onDecorateLink?: ((href: string, textContent: string) => LinkOptions | undefined) | undefined;
|
|
113
|
-
}> &
|
|
114
|
-
(SingularProps | PluralProps);
|
|
81
|
+
const html = useMemo(() => {
|
|
82
|
+
let localized: string;
|
|
115
83
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
84
|
+
if (isPlural(props)) {
|
|
85
|
+
const { stringIds, values } = props;
|
|
119
86
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const env = useMemo<BetterLinkEnv>(
|
|
124
|
-
() => ({
|
|
125
|
-
linkOptions: {
|
|
126
|
-
className: linkClassName
|
|
127
|
-
},
|
|
128
|
-
decorateLink: onDecorateLink
|
|
129
|
-
}),
|
|
130
|
-
[linkClassName, onDecorateLink]
|
|
131
|
-
);
|
|
87
|
+
localized = localizePlural(stringIds, ...values);
|
|
88
|
+
} else {
|
|
89
|
+
const { stringIds, values } = props;
|
|
132
90
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
91
|
+
localized = localizeSingular(stringIds, ...(values || []));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const documentFragment = parseDocumentFragmentFromString(micromark(localized));
|
|
95
|
+
|
|
96
|
+
betterLinkDocumentMod(documentFragment, (href, textContent) => {
|
|
97
|
+
const decoration: BetterLinkDocumentModDecoration = {
|
|
98
|
+
rel: 'noopener noreferrer',
|
|
99
|
+
target: '_blank',
|
|
100
|
+
wrapZeroWidthSpace: true
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const classNames = new Set<string>([linkClassName]);
|
|
104
|
+
const protocol = onErrorResumeNext<string>(() => new URL(href).protocol);
|
|
105
|
+
|
|
106
|
+
// For links that would be sanitized out, let's turn them into a button so we could handle them later.
|
|
107
|
+
if (!allowedSchemes.map(scheme => `${scheme}:`).includes(protocol)) {
|
|
108
|
+
// TODO: Add test.
|
|
109
|
+
decoration.asButton = true;
|
|
110
|
+
} else if (protocol === 'http:' || protocol === 'https:') {
|
|
111
|
+
// TODO: Add test.
|
|
112
|
+
decoration.ariaLabel = value => [value || textContent, externalLinkAlt].filter(Boolean).join(' ');
|
|
113
|
+
decoration.iconClassName = 'render-markdown__external-link-icon';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
decoration.className = Array.from(classNames).join(' ');
|
|
117
|
+
|
|
118
|
+
// However, "title" may be narrated by screen reader:
|
|
119
|
+
// - Edge
|
|
120
|
+
// - <a> will narrate "aria-label" but not "title"
|
|
121
|
+
// - <button> will narrate both "aria-label" and "title"
|
|
122
|
+
// - NVDA
|
|
123
|
+
// - <a> will narrate both "aria-label" and "title"
|
|
124
|
+
// - <button> will narrate both "aria-label" and "title"
|
|
125
|
+
|
|
126
|
+
// Title makes it very difficult to control narrations by the screen reader. Thus, we are disabling it in favor of "aria-label".
|
|
127
|
+
// This will not affect our accessibility compliance but UX. We could use a non-native tooltip or other forms of visual hint.
|
|
128
|
+
|
|
129
|
+
decoration.title = false;
|
|
130
|
+
|
|
131
|
+
return decoration;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return Object.freeze({ __html: stripParagraphContainer(serializeDocumentFragmentIntoString(documentFragment)) });
|
|
135
|
+
}, [externalLinkAlt, linkClassName, localizePlural, localizeSingular, props]);
|
|
139
136
|
|
|
137
|
+
// eslint-disable-next-line react/no-danger
|
|
140
138
|
return <span className={className} dangerouslySetInnerHTML={html} />;
|
|
141
139
|
};
|
|
142
140
|
|
|
143
141
|
export default memo(LocalizedString);
|
|
142
|
+
export { localizedStringPropsSchema, type LocalizedStringProps };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** @jest-environment @happy-dom/jest-environment */
|
|
2
|
+
/// <reference types="jest" />
|
|
3
|
+
|
|
4
|
+
import { micromark } from 'micromark';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
parseDocumentFragmentFromString,
|
|
8
|
+
serializeDocumentFragmentIntoString
|
|
9
|
+
} from '@msinternal/botframework-webchat-component-better-link';
|
|
10
|
+
import addTargetBlankToHyperlinks from './addTargetBlankToHyperlinks';
|
|
11
|
+
|
|
12
|
+
test('add to external links', () => {
|
|
13
|
+
const documentFragment = parseDocumentFragmentFromString(micromark('Hello, [Microsoft](https://microsoft.com/)!'));
|
|
14
|
+
|
|
15
|
+
addTargetBlankToHyperlinks(documentFragment);
|
|
16
|
+
|
|
17
|
+
const actual = serializeDocumentFragmentIntoString(documentFragment);
|
|
18
|
+
|
|
19
|
+
expect(actual).toBe(
|
|
20
|
+
'<p xmlns="http://www.w3.org/1999/xhtml">Hello, <a href="https://microsoft.com/" rel="noopener noreferrer" target="_blank">Microsoft</a>!</p>'
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("don't add for hashes", () => {
|
|
25
|
+
const documentFragment = parseDocumentFragmentFromString(micromark('Hello, [Microsoft](#microsoft)!'));
|
|
26
|
+
|
|
27
|
+
addTargetBlankToHyperlinks(documentFragment);
|
|
28
|
+
|
|
29
|
+
const actual = serializeDocumentFragmentIntoString(documentFragment);
|
|
30
|
+
|
|
31
|
+
expect(actual).toBe(`<p xmlns="http://www.w3.org/1999/xhtml">Hello, <a href="#microsoft">Microsoft</a>!</p>`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("don't add for searches", () => {
|
|
35
|
+
const documentFragment = parseDocumentFragmentFromString(micromark('Hello, [Microsoft](?q=microsoft)!'));
|
|
36
|
+
|
|
37
|
+
addTargetBlankToHyperlinks(documentFragment);
|
|
38
|
+
|
|
39
|
+
const actual = serializeDocumentFragmentIntoString(documentFragment);
|
|
40
|
+
|
|
41
|
+
expect(actual).toBe(`<p xmlns="http://www.w3.org/1999/xhtml">Hello, <a href="?q=microsoft">Microsoft</a>!</p>`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("don't add for cross references", () => {
|
|
45
|
+
const documentFragment = parseDocumentFragmentFromString(micromark('Hello, [Microsoft]!\n\n[Microsoft]: #microsoft'));
|
|
46
|
+
|
|
47
|
+
addTargetBlankToHyperlinks(documentFragment);
|
|
48
|
+
|
|
49
|
+
const actual = serializeDocumentFragmentIntoString(documentFragment);
|
|
50
|
+
|
|
51
|
+
expect(actual).toBe(`<p xmlns="http://www.w3.org/1999/xhtml">Hello, <a href="#microsoft">Microsoft</a>!</p>\n`);
|
|
52
|
+
});
|