react-email 4.0.0-alpha.6 → 4.0.0-alpha.8
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/CHANGELOG.md +13 -0
- package/dist/cli/index.js +18 -13
- package/dist/cli/index.mjs +26 -21
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +14 -13
- package/dist/preview/.next/build-manifest.json +3 -3
- package/dist/preview/.next/cache/.rscinfo +1 -1
- package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +3 -3
- package/dist/preview/.next/required-server-files.json +3 -3
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
- package/dist/preview/.next/server/app/page.js +1 -1
- package/dist/preview/.next/server/app/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +133 -25
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/chunks/42.js +1 -0
- package/dist/preview/.next/server/chunks/600.js +3 -3
- package/dist/preview/.next/server/chunks/{171.js → 816.js} +6 -6
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.json +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/pages-manifest.json +1 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/static/chunks/287-7864b805e6bdc854.js +1 -0
- package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
- package/dist/preview/.next/static/chunks/683-b769e5d91bdf9a82.js +1 -0
- package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
- package/dist/preview/.next/static/chunks/app/layout-7dee682873546401.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-a610d641c64448cc.js +1 -0
- package/dist/preview/.next/static/chunks/{main-app-c2e686acf8d370d7.js → main-app-256b213b179a95cc.js} +1 -1
- package/dist/preview/.next/static/css/e68ebc9bb8f7b3f4.css +3 -0
- package/dist/preview/.next/trace +26 -26
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/page.ts +84 -0
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/package.json +1 -1
- package/src/actions/email-validation/check-compatibility.ts +16 -5
- package/src/actions/email-validation/check-images.spec.tsx +13 -11
- package/src/actions/email-validation/check-images.ts +6 -0
- package/src/actions/email-validation/check-links.spec.tsx +23 -11
- package/src/actions/email-validation/check-links.ts +6 -0
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
- package/src/actions/render-email-by-path.tsx +2 -2
- package/src/app/env.ts +3 -0
- package/src/app/preview/[...slug]/page.tsx +24 -11
- package/src/app/preview/[...slug]/preview.tsx +15 -12
- package/src/components/code-container.tsx +90 -71
- package/src/components/code.tsx +106 -42
- package/src/components/icons/icon-info.tsx +18 -0
- package/src/components/icons/icon-reload.tsx +13 -14
- package/src/components/logo.tsx +3 -2
- package/src/components/resizable-wrapper.tsx +1 -4
- package/src/components/sidebar/file-tree-directory-children.tsx +13 -2
- package/src/components/sidebar/file-tree-directory.tsx +26 -18
- package/src/components/sidebar/file-tree.tsx +2 -2
- package/src/components/sidebar/sidebar.tsx +16 -19
- package/src/components/toolbar/code-preview-line-link.tsx +40 -0
- package/src/components/toolbar/compatibility.tsx +113 -0
- package/src/components/toolbar/linter.tsx +69 -111
- package/src/components/toolbar/results.tsx +5 -2
- package/src/components/toolbar/spam-assassin.tsx +31 -20
- package/src/components/toolbar/toolbar-button.tsx +4 -2
- package/src/components/toolbar/use-cached-state.ts +2 -2
- package/src/components/toolbar.tsx +152 -30
- package/src/components/tooltip-content.tsx +1 -1
- package/src/components/topbar/view-size-controls.tsx +1 -2
- package/src/components/topbar.tsx +1 -20
- package/src/contexts/fragment-identifier.tsx +46 -0
- package/src/hooks/use-fragment-identifier.ts +14 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
- package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
- package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
- package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -2
- package/src/utils/get-email-component.ts +1 -1
- package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
- package/src/utils/get-line-and-column-from-offset.ts +11 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/linting.ts +5 -30
- package/src/utils/load-stream.ts +15 -0
- package/src/utils/sanitize.ts +6 -0
- package/dist/preview/.next/server/chunks/833.js +0 -1
- package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +0 -1
- package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +0 -1
- package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +0 -1
- package/dist/preview/.next/static/css/d7df9cfc3e182163.css +0 -3
- package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
- package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
- package/src/components/icons/icon-scanner.tsx +0 -19
- package/src/components/icons/icon-scissors.tsx +0 -19
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_ssgManifest.js +0 -0
|
@@ -3,13 +3,15 @@ import * as Tabs from '@radix-ui/react-tabs';
|
|
|
3
3
|
import { LayoutGroup } from 'framer-motion';
|
|
4
4
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
5
5
|
import { use, useEffect } from 'react';
|
|
6
|
+
import type { CompatibilityCheckingResult } from '../actions/email-validation/check-compatibility';
|
|
6
7
|
import { isBuilding } from '../app/env';
|
|
7
8
|
import { PreviewContext } from '../contexts/preview';
|
|
8
9
|
import { cn } from '../utils';
|
|
9
10
|
import { IconArrowDown } from './icons/icon-arrow-down';
|
|
11
|
+
import { IconCheck } from './icons/icon-check';
|
|
12
|
+
import { IconInfo } from './icons/icon-info';
|
|
10
13
|
import { IconReload } from './icons/icon-reload';
|
|
11
|
-
import {
|
|
12
|
-
import { IconScissors } from './icons/icon-scissors';
|
|
14
|
+
import { Compatibility, useCompatibility } from './toolbar/compatibility';
|
|
13
15
|
import { Linter, type LintingRow, useLinter } from './toolbar/linter';
|
|
14
16
|
import {
|
|
15
17
|
SpamAssassin,
|
|
@@ -19,11 +21,12 @@ import {
|
|
|
19
21
|
import { ToolbarButton } from './toolbar/toolbar-button';
|
|
20
22
|
import { useCachedState } from './toolbar/use-cached-state';
|
|
21
23
|
|
|
22
|
-
export type ToolbarTabValue = 'linter' | 'spam-assassin';
|
|
24
|
+
export type ToolbarTabValue = 'linter' | 'compatibility' | 'spam-assassin';
|
|
23
25
|
|
|
24
26
|
const ToolbarInner = ({
|
|
25
27
|
serverLintingRows,
|
|
26
28
|
serverSpamCheckingResult,
|
|
29
|
+
serverCompatibilityResults,
|
|
27
30
|
|
|
28
31
|
markup,
|
|
29
32
|
reactMarkup,
|
|
@@ -54,30 +57,42 @@ const ToolbarInner = ({
|
|
|
54
57
|
} else {
|
|
55
58
|
params.set('toolbar-panel', newValue);
|
|
56
59
|
}
|
|
57
|
-
router.push(`${pathname}?${params.toString()}`);
|
|
60
|
+
router.push(`${pathname}?${params.toString()}${location.hash}`);
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
const [cachedSpamCheckingResult, setCachedSpamCheckingResult] =
|
|
61
64
|
useCachedState<SpamCheckingResult>(
|
|
62
65
|
`spam-assassin-${emailSlug.replaceAll('/', '-')}`,
|
|
63
66
|
);
|
|
64
|
-
const [spamCheckingResult, { load: loadSpamChecking }] =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const [spamCheckingResult, { load: loadSpamChecking, loading: spamLoading }] =
|
|
68
|
+
useSpamAssassin({
|
|
69
|
+
markup,
|
|
70
|
+
plainText,
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
initialResult: serverSpamCheckingResult ?? cachedSpamCheckingResult,
|
|
73
|
+
});
|
|
70
74
|
|
|
71
75
|
const [cachedLintingRows, setCachedLintingRows] = useCachedState<
|
|
72
76
|
LintingRow[]
|
|
73
77
|
>(`linter-${emailSlug.replaceAll('/', '-')}`);
|
|
74
|
-
const [lintingRows, { load: loadLinting }] = useLinter({
|
|
75
|
-
reactMarkup,
|
|
76
|
-
emailPath,
|
|
78
|
+
const [lintingRows, { load: loadLinting, loading: lintLoading }] = useLinter({
|
|
77
79
|
markup,
|
|
78
80
|
|
|
79
81
|
initialRows: serverLintingRows ?? cachedLintingRows,
|
|
80
82
|
});
|
|
83
|
+
const [cachedCompatibilityResults, setCachedCompatibilityResults] =
|
|
84
|
+
useCachedState<CompatibilityCheckingResult[]>(
|
|
85
|
+
`compatibility-${emailSlug.replaceAll('/', '-')}`,
|
|
86
|
+
);
|
|
87
|
+
const [
|
|
88
|
+
compatibilityCheckingResults,
|
|
89
|
+
{ load: loadCompatibility, loading: compatibilityLoading },
|
|
90
|
+
] = useCompatibility({
|
|
91
|
+
emailPath,
|
|
92
|
+
reactMarkup,
|
|
93
|
+
|
|
94
|
+
initialResults: serverCompatibilityResults ?? cachedCompatibilityResults,
|
|
95
|
+
});
|
|
81
96
|
|
|
82
97
|
if (!isBuilding) {
|
|
83
98
|
useEffect(() => {
|
|
@@ -87,6 +102,9 @@ const ToolbarInner = ({
|
|
|
87
102
|
|
|
88
103
|
const spamCheckingResult = await loadSpamChecking();
|
|
89
104
|
setCachedSpamCheckingResult(spamCheckingResult);
|
|
105
|
+
|
|
106
|
+
const compatibilityCheckingResults = await loadCompatibility();
|
|
107
|
+
setCachedCompatibilityResults(compatibilityCheckingResults);
|
|
90
108
|
})();
|
|
91
109
|
}, []);
|
|
92
110
|
}
|
|
@@ -95,49 +113,76 @@ const ToolbarInner = ({
|
|
|
95
113
|
<div
|
|
96
114
|
data-toggled={toggled}
|
|
97
115
|
className={cn(
|
|
98
|
-
'
|
|
99
|
-
'
|
|
116
|
+
'absolute bottom-0 left-0 right-0',
|
|
117
|
+
'bg-black group/toolbar text-xs text-slate-11 h-52 transition-transform',
|
|
118
|
+
'data-[toggled=false]:translate-y-[170px]',
|
|
100
119
|
)}
|
|
101
120
|
>
|
|
102
121
|
<Tabs.Root
|
|
103
|
-
value={activeTab}
|
|
122
|
+
value={activeTab ?? ''}
|
|
104
123
|
onValueChange={(newValue) => {
|
|
105
124
|
setActivePanelValue(newValue as ToolbarTabValue);
|
|
106
125
|
}}
|
|
107
126
|
asChild
|
|
108
127
|
>
|
|
109
128
|
<div className="flex flex-col h-full">
|
|
110
|
-
<Tabs.List className="flex gap-4 px-
|
|
129
|
+
<Tabs.List className="flex gap-4 px-4 border-b border-solid border-slate-6 h-10 w-full flex-shrink-0">
|
|
111
130
|
<LayoutGroup id="toolbar">
|
|
112
|
-
<Tabs.Trigger asChild value="spam-assassin">
|
|
113
|
-
<ToolbarButton active={activeTab === 'spam-assassin'}>
|
|
114
|
-
<IconScissors />
|
|
115
|
-
Spam Assassin
|
|
116
|
-
</ToolbarButton>
|
|
117
|
-
</Tabs.Trigger>
|
|
118
131
|
<Tabs.Trigger asChild value="linter">
|
|
119
132
|
<ToolbarButton active={activeTab === 'linter'}>
|
|
120
|
-
<IconScanner />
|
|
121
133
|
Linter
|
|
122
134
|
</ToolbarButton>
|
|
123
135
|
</Tabs.Trigger>
|
|
136
|
+
<Tabs.Trigger asChild value="compatibility">
|
|
137
|
+
<ToolbarButton active={activeTab === 'compatibility'}>
|
|
138
|
+
Compatibility
|
|
139
|
+
</ToolbarButton>
|
|
140
|
+
</Tabs.Trigger>
|
|
141
|
+
<Tabs.Trigger asChild value="spam-assassin">
|
|
142
|
+
<ToolbarButton active={activeTab === 'spam-assassin'}>
|
|
143
|
+
Spam
|
|
144
|
+
</ToolbarButton>
|
|
145
|
+
</Tabs.Trigger>
|
|
124
146
|
</LayoutGroup>
|
|
125
|
-
<div className="flex gap-
|
|
147
|
+
<div className="flex gap-0.5 ml-auto">
|
|
148
|
+
<ToolbarButton
|
|
149
|
+
delayDuration={0}
|
|
150
|
+
tooltip={
|
|
151
|
+
(activeTab === 'linter' &&
|
|
152
|
+
'The Linter tab checks all the images and links for common issues like missing alt text, broken URLs, insecure HTTP methods, and more.') ||
|
|
153
|
+
(activeTab === 'spam-assassin' &&
|
|
154
|
+
'The Spam tab will look at the content and use a robust scoring framework to determine if the email is likely to be spam. Powered by SpamAssassin.') ||
|
|
155
|
+
(activeTab === 'compatibility' &&
|
|
156
|
+
'The Compatibility tab shows how well the HTML/CSS is supported across mail clients like Outlook, Gmail, etc. Powered by Can I Email.') ||
|
|
157
|
+
'Info'
|
|
158
|
+
}
|
|
159
|
+
>
|
|
160
|
+
<IconInfo size={24} />
|
|
161
|
+
</ToolbarButton>
|
|
126
162
|
{isBuilding ? null : (
|
|
127
163
|
<ToolbarButton
|
|
128
164
|
tooltip="Reload"
|
|
165
|
+
disabled={lintLoading || spamLoading}
|
|
129
166
|
onClick={async () => {
|
|
130
167
|
if (activeTab === undefined) {
|
|
131
168
|
setActivePanelValue('linter');
|
|
132
169
|
}
|
|
133
170
|
if (activeTab === 'spam-assassin') {
|
|
134
171
|
await loadSpamChecking();
|
|
135
|
-
} else {
|
|
172
|
+
} else if (activeTab === 'linter') {
|
|
136
173
|
await loadLinting();
|
|
174
|
+
} else if (activeTab === 'compatibility') {
|
|
175
|
+
await loadCompatibility();
|
|
137
176
|
}
|
|
138
177
|
}}
|
|
139
178
|
>
|
|
140
|
-
<IconReload
|
|
179
|
+
<IconReload
|
|
180
|
+
size={24}
|
|
181
|
+
className={cn({
|
|
182
|
+
'animate-spin opacity-60 animate-spin-fast':
|
|
183
|
+
lintLoading || spamLoading,
|
|
184
|
+
})}
|
|
185
|
+
/>
|
|
141
186
|
</ToolbarButton>
|
|
142
187
|
)}
|
|
143
188
|
<ToolbarButton
|
|
@@ -150,17 +195,65 @@ const ToolbarInner = ({
|
|
|
150
195
|
}
|
|
151
196
|
}}
|
|
152
197
|
>
|
|
153
|
-
<IconArrowDown
|
|
198
|
+
<IconArrowDown
|
|
199
|
+
size={24}
|
|
200
|
+
className="transition-transform group-data-[toggled=false]/toolbar:rotate-180"
|
|
201
|
+
/>
|
|
154
202
|
</ToolbarButton>
|
|
155
203
|
</div>
|
|
156
204
|
</Tabs.List>
|
|
157
205
|
|
|
158
|
-
<div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2">
|
|
206
|
+
<div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2 pt-3">
|
|
159
207
|
<Tabs.Content value="linter">
|
|
160
|
-
|
|
208
|
+
{lintLoading ? (
|
|
209
|
+
<div className="animate-pulse text-slate-11 text-sm pt-1">
|
|
210
|
+
Running linting...
|
|
211
|
+
</div>
|
|
212
|
+
) : lintingRows?.length === 0 ? (
|
|
213
|
+
<div className="flex flex-col items-center justify-center pt-8">
|
|
214
|
+
<SuccessIcon />
|
|
215
|
+
<SuccessTitle>All good</SuccessTitle>
|
|
216
|
+
<SuccessDescription>
|
|
217
|
+
No linting issues found.
|
|
218
|
+
</SuccessDescription>
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<Linter rows={lintingRows ?? []} />
|
|
222
|
+
)}
|
|
223
|
+
</Tabs.Content>
|
|
224
|
+
<Tabs.Content value="compatibility">
|
|
225
|
+
{compatibilityLoading ? (
|
|
226
|
+
<div className="animate-pulse text-slate-11 text-sm pt-1">
|
|
227
|
+
Running compatibility check...
|
|
228
|
+
</div>
|
|
229
|
+
) : compatibilityCheckingResults?.length === 0 ? (
|
|
230
|
+
<div className="flex flex-col items-center justify-center py-8 px-4 my-4">
|
|
231
|
+
<SuccessIcon />
|
|
232
|
+
<SuccessTitle>Great compatibility</SuccessTitle>
|
|
233
|
+
<SuccessDescription>
|
|
234
|
+
It should render properly everywhere.
|
|
235
|
+
</SuccessDescription>
|
|
236
|
+
</div>
|
|
237
|
+
) : (
|
|
238
|
+
<Compatibility results={compatibilityCheckingResults ?? []} />
|
|
239
|
+
)}
|
|
161
240
|
</Tabs.Content>
|
|
162
241
|
<Tabs.Content value="spam-assassin">
|
|
163
|
-
|
|
242
|
+
{spamLoading ? (
|
|
243
|
+
<div className="animate-pulse text-slate-11 text-sm pt-1">
|
|
244
|
+
Running spam check...
|
|
245
|
+
</div>
|
|
246
|
+
) : spamCheckingResult?.isSpam === false ? (
|
|
247
|
+
<div className="flex flex-col items-center justify-center py-4 px-4 my-4">
|
|
248
|
+
<SuccessIcon />
|
|
249
|
+
<SuccessTitle>10/10</SuccessTitle>
|
|
250
|
+
<SuccessDescription>
|
|
251
|
+
Your email is clean of abuse indicators.
|
|
252
|
+
</SuccessDescription>
|
|
253
|
+
</div>
|
|
254
|
+
) : (
|
|
255
|
+
<SpamAssassin result={spamCheckingResult} />
|
|
256
|
+
)}
|
|
164
257
|
</Tabs.Content>
|
|
165
258
|
</div>
|
|
166
259
|
</div>
|
|
@@ -169,14 +262,42 @@ const ToolbarInner = ({
|
|
|
169
262
|
);
|
|
170
263
|
};
|
|
171
264
|
|
|
265
|
+
const SuccessIcon = () => {
|
|
266
|
+
return (
|
|
267
|
+
<div className="relative mb-8 flex items-center justify-center">
|
|
268
|
+
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-green-300/20 opacity-80 to-emerald-500/30 blur-md absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
269
|
+
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-green-400/80 opacity-10 to-emerald-600/80 absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-lg" />
|
|
270
|
+
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-green-400 to-emerald-600 flex items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-[inset_0_1px_1px_rgba(255,255,255,0.4)]">
|
|
271
|
+
<IconCheck size={24} className="text-white drop-shadow-sm" />
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const SuccessTitle = ({ children }) => {
|
|
278
|
+
return (
|
|
279
|
+
<h3 className="text-slate-12 font-medium text-base mb-1">{children}</h3>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const SuccessDescription = ({ children }) => {
|
|
284
|
+
return (
|
|
285
|
+
<p className="text-slate-11 text-sm text-center max-w-[300px]">
|
|
286
|
+
{children}
|
|
287
|
+
</p>
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
172
291
|
interface ToolbarProps {
|
|
173
292
|
serverSpamCheckingResult: SpamCheckingResult | undefined;
|
|
174
293
|
serverLintingRows: LintingRow[] | undefined;
|
|
294
|
+
serverCompatibilityResults: CompatibilityCheckingResult[] | undefined;
|
|
175
295
|
}
|
|
176
296
|
|
|
177
297
|
export const Toolbar = ({
|
|
178
298
|
serverLintingRows,
|
|
179
299
|
serverSpamCheckingResult,
|
|
300
|
+
serverCompatibilityResults,
|
|
180
301
|
}: ToolbarProps) => {
|
|
181
302
|
const { emailPath, emailSlug, renderedEmailMetadata } = use(PreviewContext)!;
|
|
182
303
|
|
|
@@ -192,6 +313,7 @@ export const Toolbar = ({
|
|
|
192
313
|
plainText={plainText}
|
|
193
314
|
serverLintingRows={serverLintingRows}
|
|
194
315
|
serverSpamCheckingResult={serverSpamCheckingResult}
|
|
316
|
+
serverCompatibilityResults={serverCompatibilityResults}
|
|
195
317
|
/>
|
|
196
318
|
);
|
|
197
319
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
3
2
|
import { motion } from 'framer-motion';
|
|
4
3
|
import * as React from 'react';
|
|
@@ -124,7 +123,7 @@ const PresetMenuItem = ({
|
|
|
124
123
|
onClick={() => onSelect(dimensions)}
|
|
125
124
|
>
|
|
126
125
|
{name}
|
|
127
|
-
<span className="flex h-fit items-center rounded-full bg-slate-6 px-
|
|
126
|
+
<span className="flex h-fit items-center rounded-full bg-slate-6 px-2 py-1 font-medium text-slate-11 text-xs">
|
|
128
127
|
{dimensions.width}x{dimensions.height}
|
|
129
128
|
</span>
|
|
130
129
|
</DropdownMenu.Item>
|
|
@@ -16,7 +16,7 @@ export const Topbar = ({ emailTitle, children }: TopbarProps) => {
|
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<Tooltip.Provider>
|
|
19
|
-
<header className="relative flex h-[3.3125rem] items-center justify-between gap-3 border-slate-6 border-b px-3">
|
|
19
|
+
<header className="relative flex h-[3.3125rem] items-center justify-between gap-3 border-slate-6 border-b px-3 py-2">
|
|
20
20
|
<div className="relative flex w-fit items-center gap-3">
|
|
21
21
|
<Tooltip>
|
|
22
22
|
<Tooltip.Trigger asChild>
|
|
@@ -40,25 +40,6 @@ export const Topbar = ({ emailTitle, children }: TopbarProps) => {
|
|
|
40
40
|
</div>
|
|
41
41
|
<div className="flex w-full items-center justify-between gap-3 lg:w-fit lg:justify-start">
|
|
42
42
|
{children}
|
|
43
|
-
{/* {setViewWidth && setViewHeight && viewWidth && viewHeight ? ( */}
|
|
44
|
-
{/* <ViewSizeControls */}
|
|
45
|
-
{/* setViewHeight={setViewHeight} */}
|
|
46
|
-
{/* setViewWidth={setViewWidth} */}
|
|
47
|
-
{/* viewHeight={viewHeight} */}
|
|
48
|
-
{/* viewWidth={viewWidth} */}
|
|
49
|
-
{/* /> */}
|
|
50
|
-
{/* ) : null} */}
|
|
51
|
-
{/* {activeView && setActiveView ? ( */}
|
|
52
|
-
{/* <ActiveViewToggleGroup */}
|
|
53
|
-
{/* activeView={activeView} */}
|
|
54
|
-
{/* setActiveView={setActiveView} */}
|
|
55
|
-
{/* /> */}
|
|
56
|
-
{/* ) : null} */}
|
|
57
|
-
{/* {markup ? ( */}
|
|
58
|
-
{/* <div className="flex justify-end"> */}
|
|
59
|
-
{/* <Send markup={markup} /> */}
|
|
60
|
-
{/* </div> */}
|
|
61
|
-
{/* ) : null} */}
|
|
62
43
|
</div>
|
|
63
44
|
</header>
|
|
64
45
|
</Tooltip.Provider>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
3
|
+
import { createContext, use, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export const FragmentIdentifierContext = createContext<
|
|
6
|
+
| {
|
|
7
|
+
identifier: string | undefined;
|
|
8
|
+
|
|
9
|
+
update(value: string): void;
|
|
10
|
+
}
|
|
11
|
+
| undefined
|
|
12
|
+
>(undefined);
|
|
13
|
+
|
|
14
|
+
export const useFragmentIdentifier = () => {
|
|
15
|
+
const value = use(FragmentIdentifierContext);
|
|
16
|
+
return value?.identifier;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const FragmentIdentifierProvider = ({
|
|
20
|
+
children,
|
|
21
|
+
}: { children: React.ReactNode }) => {
|
|
22
|
+
const [fragmentIdentifier, setFragmentIdentifier] = useState<string>();
|
|
23
|
+
const pathname = usePathname();
|
|
24
|
+
const searchParams = useSearchParams();
|
|
25
|
+
|
|
26
|
+
const update = () => {
|
|
27
|
+
setFragmentIdentifier(location.hash);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
update();
|
|
32
|
+
}, [pathname, searchParams]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<FragmentIdentifierContext.Provider
|
|
36
|
+
value={{
|
|
37
|
+
identifier: fragmentIdentifier,
|
|
38
|
+
update(value: string) {
|
|
39
|
+
setFragmentIdentifier(value);
|
|
40
|
+
},
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</FragmentIdentifierContext.Provider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export const useFragmentIdentifier = () => {
|
|
5
|
+
const pathname = usePathname();
|
|
6
|
+
const searchParams = useSearchParams();
|
|
7
|
+
const [fragmentIdentifier, setFragmentIdentifier] = useState<string>();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setFragmentIdentifier(global.location?.hash);
|
|
11
|
+
}, [pathname, searchParams]);
|
|
12
|
+
|
|
13
|
+
return fragmentIdentifier;
|
|
14
|
+
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="
|
|
3
|
+
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel Logo" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img alt="alanturing's profile picture" height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="Arrow indicating invitation" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img alt="Enigma team logo" height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px"><tbody><tr><td><a href="https://vercel.com" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>  </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>  ​</i><![endif]--></span></a></td></tr></tbody></table><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com</a></p><hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-bottom:16px;margin-top:16px">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`getObjectVariables() 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"buttonStyle": [
|
|
6
|
+
Node {
|
|
7
|
+
"computed": false,
|
|
8
|
+
"end": 91,
|
|
9
|
+
"key": Node {
|
|
10
|
+
"end": 84,
|
|
11
|
+
"loc": SourceLocation {
|
|
12
|
+
"end": Position {
|
|
13
|
+
"column": 14,
|
|
14
|
+
"index": 84,
|
|
15
|
+
"line": 5,
|
|
16
|
+
},
|
|
17
|
+
"filename": undefined,
|
|
18
|
+
"identifierName": "borderRadius",
|
|
19
|
+
"start": Position {
|
|
20
|
+
"column": 2,
|
|
21
|
+
"index": 72,
|
|
22
|
+
"line": 5,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
"name": "borderRadius",
|
|
26
|
+
"start": 72,
|
|
27
|
+
"type": "Identifier",
|
|
28
|
+
},
|
|
29
|
+
"loc": SourceLocation {
|
|
30
|
+
"end": Position {
|
|
31
|
+
"column": 21,
|
|
32
|
+
"index": 91,
|
|
33
|
+
"line": 5,
|
|
34
|
+
},
|
|
35
|
+
"filename": undefined,
|
|
36
|
+
"identifierName": undefined,
|
|
37
|
+
"start": Position {
|
|
38
|
+
"column": 2,
|
|
39
|
+
"index": 72,
|
|
40
|
+
"line": 5,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
"method": false,
|
|
44
|
+
"shorthand": false,
|
|
45
|
+
"start": 72,
|
|
46
|
+
"type": "ObjectProperty",
|
|
47
|
+
"value": Node {
|
|
48
|
+
"end": 91,
|
|
49
|
+
"extra": {
|
|
50
|
+
"raw": "'5px'",
|
|
51
|
+
"rawValue": "5px",
|
|
52
|
+
},
|
|
53
|
+
"loc": SourceLocation {
|
|
54
|
+
"end": Position {
|
|
55
|
+
"column": 21,
|
|
56
|
+
"index": 91,
|
|
57
|
+
"line": 5,
|
|
58
|
+
},
|
|
59
|
+
"filename": undefined,
|
|
60
|
+
"identifierName": undefined,
|
|
61
|
+
"start": Position {
|
|
62
|
+
"column": 16,
|
|
63
|
+
"index": 86,
|
|
64
|
+
"line": 5,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
"start": 86,
|
|
68
|
+
"type": "StringLiteral",
|
|
69
|
+
"value": "5px",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`getUsedStyleProperties() 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"location": SourceLocation {
|
|
7
|
+
"end": Position {
|
|
8
|
+
"column": 21,
|
|
9
|
+
"index": 91,
|
|
10
|
+
"line": 5,
|
|
11
|
+
},
|
|
12
|
+
"filename": undefined,
|
|
13
|
+
"identifierName": undefined,
|
|
14
|
+
"start": Position {
|
|
15
|
+
"column": 2,
|
|
16
|
+
"index": 72,
|
|
17
|
+
"line": 5,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"name": "borderRadius",
|
|
21
|
+
"value": "5px",
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import { getObjectVariables } from './get-object-variables';
|
|
3
|
+
|
|
4
|
+
test('getObjectVariables()', () => {
|
|
5
|
+
const reactCode = `
|
|
6
|
+
<Button style={buttonStyle}>Click me</Button>
|
|
7
|
+
|
|
8
|
+
const buttonStyle = {
|
|
9
|
+
borderRadius: '5px',
|
|
10
|
+
};
|
|
11
|
+
`;
|
|
12
|
+
const ast = parse(reactCode, {
|
|
13
|
+
strictMode: false,
|
|
14
|
+
errorRecovery: true,
|
|
15
|
+
sourceType: 'unambiguous',
|
|
16
|
+
plugins: ['jsx', 'typescript', 'decorators'],
|
|
17
|
+
});
|
|
18
|
+
expect(getObjectVariables(ast)).toMatchSnapshot();
|
|
19
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import { getObjectVariables } from './get-object-variables';
|
|
3
|
+
import { getUsedStyleProperties } from './get-used-style-properties';
|
|
4
|
+
|
|
5
|
+
test('getUsedStyleProperties()', async () => {
|
|
6
|
+
const reactCode = `
|
|
7
|
+
<Button style={buttonStyle}>Click me</Button>
|
|
8
|
+
|
|
9
|
+
const buttonStyle = {
|
|
10
|
+
borderRadius: '5px',
|
|
11
|
+
};
|
|
12
|
+
`;
|
|
13
|
+
const ast = parse(reactCode, {
|
|
14
|
+
strictMode: false,
|
|
15
|
+
errorRecovery: true,
|
|
16
|
+
sourceType: 'unambiguous',
|
|
17
|
+
plugins: ['jsx', 'typescript', 'decorators'],
|
|
18
|
+
});
|
|
19
|
+
const objectVariables = getObjectVariables(ast);
|
|
20
|
+
expect(
|
|
21
|
+
await getUsedStyleProperties(ast, reactCode, '', objectVariables),
|
|
22
|
+
).toMatchSnapshot();
|
|
23
|
+
});
|
|
@@ -6,8 +6,8 @@ export const getCssPropertyWithValue = (title: string) => {
|
|
|
6
6
|
if (match) {
|
|
7
7
|
const [_full, propertyName, propertyValue] = match;
|
|
8
8
|
return {
|
|
9
|
-
name: propertyName
|
|
10
|
-
value: propertyValue
|
|
9
|
+
name: propertyName!,
|
|
10
|
+
value: propertyValue!,
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
return undefined;
|
|
@@ -12,7 +12,7 @@ import type { EmailTemplate as EmailComponent } from './types/email-template';
|
|
|
12
12
|
import type { ErrorObject } from './types/error-object';
|
|
13
13
|
|
|
14
14
|
const EmailComponentModule = z.object({
|
|
15
|
-
default: z.
|
|
15
|
+
default: z.any(),
|
|
16
16
|
render: z.function(),
|
|
17
17
|
reactEmailCreateReactElement: z.function(),
|
|
18
18
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getLineAndColumnFromOffset } from './get-line-and-column-from-offset';
|
|
2
|
+
|
|
3
|
+
test('getLineAndColumnFromOffset()', () => {
|
|
4
|
+
const content = `export default function MyEmail() {
|
|
5
|
+
return <div className="testing classes to make sure this is not removed" id="my-div" aria-label="my beautiful div">
|
|
6
|
+
inside the div, should also stay unchanged
|
|
7
|
+
</div>;
|
|
8
|
+
}`;
|
|
9
|
+
const offset = content.indexOf('className');
|
|
10
|
+
expect(getLineAndColumnFromOffset(offset, content)).toEqual([2, 15]);
|
|
11
|
+
});
|