botframework-webchat 4.16.1-main.20240405.136b5a4 → 4.17.0-main.20240408.d0aa541

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.
@@ -1,5 +1,5 @@
1
- import iterator from 'markdown-it-for-inline';
2
1
  import MarkdownIt from 'markdown-it';
2
+ import iterator from 'markdown-it-for-inline';
3
3
 
4
4
  // Put a transparent pixel instead of the "open in new window" icon, so developers can easily modify the icon in CSS.
5
5
  const TRANSPARENT_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
@@ -30,11 +30,19 @@ type Decoration = {
30
30
 
31
31
  /** Value of "title" attribute of the link. If set to `false`, remove existing attribute. */
32
32
  title?: AttributeSetter;
33
+
34
+ /** Wraps the link with zero-width space. */
35
+ wrapZeroWidthSpace?: boolean;
33
36
  };
34
37
 
35
38
  // This is used for parsing Markdown for external links.
36
39
  const internalMarkdownIt = new MarkdownIt();
37
40
 
41
+ const ZERO_WIDTH_SPACE_TOKEN = {
42
+ content: '\u200b',
43
+ type: 'text'
44
+ };
45
+
38
46
  function setTokenAttribute(attrs: Array<[string, string]>, name: string, value?: AttributeSetter) {
39
47
  const index = attrs.findIndex(entry => entry[0] === name);
40
48
 
@@ -61,52 +69,65 @@ const betterLink = (
61
69
  ): typeof MarkdownIt =>
62
70
  markdown.use(iterator, 'url_new_win', 'link_open', (tokens, index) => {
63
71
  const indexOfLinkCloseToken = tokens.indexOf(tokens.slice(index + 1).find(({ type }) => type === 'link_close'));
64
- const token = tokens[+index];
72
+ // eslint-disable-next-line no-magic-numbers
73
+ const updatedTokens = tokens.splice(index, ~indexOfLinkCloseToken ? indexOfLinkCloseToken - index + 1 : 2);
65
74
 
66
- const [, href] = token.attrs.find(([name]) => name === 'href');
67
- const nodesInLink = tokens.slice(index + 1, indexOfLinkCloseToken);
75
+ try {
76
+ const [linkOpenToken] = updatedTokens;
77
+ const linkCloseToken = updatedTokens[updatedTokens.length - 1];
68
78
 
69
- const textContent = nodesInLink
70
- .filter(({ type }) => type === 'text')
71
- .map(({ content }) => content)
72
- .join(' ');
79
+ const [, href] = linkOpenToken.attrs.find(([name]) => name === 'href');
80
+ const nodesInLink = updatedTokens.slice(1, updatedTokens.length - 1);
73
81
 
74
- const decoration = decorate(href, textContent);
82
+ const textContent = nodesInLink
83
+ .filter(({ type }) => type === 'text')
84
+ .map(({ content }) => content)
85
+ .join(' ');
75
86
 
76
- if (!decoration) {
77
- return;
78
- }
87
+ const decoration = decorate(href, textContent);
79
88
 
80
- const { ariaLabel, asButton, className, iconAlt, iconClassName, rel, target, title } = decoration;
89
+ if (!decoration) {
90
+ return;
91
+ }
81
92
 
82
- setTokenAttribute(token.attrs, 'aria-label', ariaLabel);
83
- setTokenAttribute(token.attrs, 'class', className);
84
- setTokenAttribute(token.attrs, 'title', title);
93
+ const { ariaLabel, asButton, className, iconAlt, iconClassName, rel, target, title, wrapZeroWidthSpace } =
94
+ decoration;
85
95
 
86
- if (iconClassName) {
87
- const iconTokens = internalMarkdownIt.parseInline(`![](${TRANSPARENT_GIF})`)[0].children;
96
+ setTokenAttribute(linkOpenToken.attrs, 'aria-label', ariaLabel);
97
+ setTokenAttribute(linkOpenToken.attrs, 'class', className);
98
+ setTokenAttribute(linkOpenToken.attrs, 'title', title);
88
99
 
89
- setTokenAttribute(iconTokens[0].attrs, 'class', iconClassName);
90
- setTokenAttribute(iconTokens[0].attrs, 'title', iconAlt);
100
+ if (iconClassName) {
101
+ const iconTokens = internalMarkdownIt.parseInline(`![](${TRANSPARENT_GIF})`)[0].children;
91
102
 
92
- // Add an icon before </a>.
93
- ~indexOfLinkCloseToken && tokens.splice(indexOfLinkCloseToken, 0, ...iconTokens);
94
- }
103
+ setTokenAttribute(iconTokens[0].attrs, 'class', iconClassName);
104
+ setTokenAttribute(iconTokens[0].attrs, 'title', iconAlt);
105
+
106
+ // Add an icon before </a>.
107
+ // eslint-disable-next-line no-magic-numbers
108
+ updatedTokens.splice(-1, 0, ...iconTokens);
109
+ }
95
110
 
96
- if (asButton) {
97
- setTokenAttribute(token.attrs, 'href', false);
111
+ if (asButton) {
112
+ setTokenAttribute(linkOpenToken.attrs, 'href', false);
98
113
 
99
- token.tag = 'button';
114
+ linkOpenToken.tag = 'button';
100
115
 
101
- setTokenAttribute(token.attrs, 'type', 'button');
102
- setTokenAttribute(token.attrs, 'value', href);
116
+ setTokenAttribute(linkOpenToken.attrs, 'type', 'button');
117
+ setTokenAttribute(linkOpenToken.attrs, 'value', href);
103
118
 
104
- if (~indexOfLinkCloseToken) {
105
- tokens[+indexOfLinkCloseToken].tag = 'button';
119
+ linkCloseToken.tag = 'button';
120
+ } else {
121
+ setTokenAttribute(linkOpenToken.attrs, 'rel', rel);
122
+ setTokenAttribute(linkOpenToken.attrs, 'target', target);
106
123
  }
107
- } else {
108
- setTokenAttribute(token.attrs, 'rel', rel);
109
- setTokenAttribute(token.attrs, 'target', target);
124
+
125
+ if (wrapZeroWidthSpace) {
126
+ updatedTokens.splice(0, 0, ZERO_WIDTH_SPACE_TOKEN);
127
+ updatedTokens.splice(Infinity, 0, ZERO_WIDTH_SPACE_TOKEN);
128
+ }
129
+ } finally {
130
+ tokens.splice(index, 0, ...updatedTokens);
110
131
  }
111
132
  });
112
133
 
@@ -2,9 +2,9 @@ import { onErrorResumeNext } from 'botframework-webchat-core';
2
2
  import MarkdownIt from 'markdown-it';
3
3
  import sanitizeHTML from 'sanitize-html';
4
4
 
5
- import { pre as respectCRLFPre } from './markdownItPlugins/respectCRLF';
6
5
  import ariaLabel, { post as ariaLabelPost, pre as ariaLabelPre } from './markdownItPlugins/ariaLabel';
7
6
  import betterLink from './markdownItPlugins/betterLink';
7
+ import { pre as respectCRLFPre } from './markdownItPlugins/respectCRLF';
8
8
  import iterateLinkDefinitions from './private/iterateLinkDefinitions';
9
9
 
10
10
  const SANITIZE_HTML_OPTIONS = Object.freeze({
@@ -88,7 +88,8 @@ export default function render(
88
88
  .use(betterLink, (href: string, textContent: string): BetterLinkDecoration | undefined => {
89
89
  const decoration: BetterLinkDecoration = {
90
90
  rel: 'noopener noreferrer',
91
- target: '_blank'
91
+ target: '_blank',
92
+ wrapZeroWidthSpace: true
92
93
  };
93
94
 
94
95
  const ariaLabelSegments: string[] = [textContent];
@@ -101,10 +102,12 @@ export default function render(
101
102
  linkDefinition.title || onErrorResumeNext(() => new URL(linkDefinition.url).host) || linkDefinition.url
102
103
  );
103
104
 
104
- linkDefinition.identifier === textContent && classes.add('webchat__render-markdown__pure-identifier');
105
+ // linkDefinition.identifier is uppercase, while linkDefinition.label is as-is.
106
+ linkDefinition.label === textContent && classes.add('webchat__render-markdown__pure-identifier');
105
107
  }
106
108
 
107
- if (protocol === 'cite:') {
109
+ // For links that would be sanitized out, let's turn them into a button so we could handle them later.
110
+ if (!SANITIZE_HTML_OPTIONS.allowedSchemes.map(scheme => `${scheme}:`).includes(protocol)) {
108
111
  decoration.asButton = true;
109
112
 
110
113
  classes.add('webchat__render-markdown__citation');