@jsenv/core 27.5.0 → 27.5.3
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/js/html_supervisor_installer.js +372 -289
- package/dist/main.js +63 -18
- package/package.json +2 -1
- package/src/dev/start_dev_server.js +5 -3
- package/src/plugins/html_supervisor/client/error_formatter.js +305 -0
- package/src/plugins/html_supervisor/client/error_overlay.js +60 -268
- package/src/plugins/html_supervisor/client/html_supervisor_installer.js +23 -32
- package/src/plugins/html_supervisor/jsenv_plugin_html_supervisor.js +53 -15
- package/src/test/execute_test_plan.js +1 -2
package/dist/main.js
CHANGED
|
@@ -12035,7 +12035,8 @@ const jsenvPluginHtmlSupervisor = ({
|
|
|
12035
12035
|
logs = false,
|
|
12036
12036
|
measurePerf = false,
|
|
12037
12037
|
errorOverlay = true,
|
|
12038
|
-
openInEditor = true
|
|
12038
|
+
openInEditor = true,
|
|
12039
|
+
errorBaseUrl
|
|
12039
12040
|
}) => {
|
|
12040
12041
|
const htmlSupervisorSetupFileUrl = new URL("./js/html_supervisor_setup.js", import.meta.url).href;
|
|
12041
12042
|
const htmlSupervisorInstallerFileUrl = new URL("./js/html_supervisor_installer.js", import.meta.url).href;
|
|
@@ -12045,26 +12046,67 @@ const jsenvPluginHtmlSupervisor = ({
|
|
|
12045
12046
|
dev: true,
|
|
12046
12047
|
test: true
|
|
12047
12048
|
},
|
|
12048
|
-
serve: request => {
|
|
12049
|
-
if (
|
|
12050
|
-
|
|
12049
|
+
serve: (request, context) => {
|
|
12050
|
+
if (request.ressource.startsWith("/__open_in_editor__/")) {
|
|
12051
|
+
const file = request.ressource.slice("/__open_in_editor__/".length);
|
|
12052
|
+
|
|
12053
|
+
if (!file) {
|
|
12054
|
+
return {
|
|
12055
|
+
status: 400,
|
|
12056
|
+
body: "Missing file in url"
|
|
12057
|
+
};
|
|
12058
|
+
}
|
|
12059
|
+
|
|
12060
|
+
const launch = requireFromJsenv("launch-editor");
|
|
12061
|
+
launch(fileURLToPath(file), () => {// ignore error for now
|
|
12062
|
+
});
|
|
12063
|
+
return {
|
|
12064
|
+
status: 200,
|
|
12065
|
+
headers: {
|
|
12066
|
+
"cache-control": "no-store"
|
|
12067
|
+
}
|
|
12068
|
+
};
|
|
12051
12069
|
}
|
|
12052
12070
|
|
|
12053
|
-
|
|
12071
|
+
if (request.ressource.startsWith("/__get_code_frame__/")) {
|
|
12072
|
+
const url = request.ressource.slice("/__get_code_frame__/".length);
|
|
12073
|
+
const match = url.match(/:([0-9]+):([0-9]+)$/);
|
|
12074
|
+
|
|
12075
|
+
if (!match) {
|
|
12076
|
+
return {
|
|
12077
|
+
status: 400,
|
|
12078
|
+
body: "Missing line and column in url"
|
|
12079
|
+
};
|
|
12080
|
+
}
|
|
12081
|
+
|
|
12082
|
+
const file = url.slice(0, match.index);
|
|
12083
|
+
const line = parseInt(match[1]);
|
|
12084
|
+
const column = parseInt(match[2]);
|
|
12085
|
+
const urlInfo = context.urlGraph.getUrlInfo(file);
|
|
12054
12086
|
|
|
12055
|
-
|
|
12087
|
+
if (!urlInfo) {
|
|
12088
|
+
return {
|
|
12089
|
+
status: 404
|
|
12090
|
+
};
|
|
12091
|
+
}
|
|
12092
|
+
|
|
12093
|
+
const codeFrame = stringifyUrlSite({
|
|
12094
|
+
url: file,
|
|
12095
|
+
line,
|
|
12096
|
+
column,
|
|
12097
|
+
content: urlInfo.originalContent
|
|
12098
|
+
});
|
|
12056
12099
|
return {
|
|
12057
|
-
status:
|
|
12058
|
-
|
|
12100
|
+
status: 200,
|
|
12101
|
+
headers: {
|
|
12102
|
+
"content-type": "text/plain",
|
|
12103
|
+
"content-length": Buffer.byteLength(codeFrame)
|
|
12104
|
+
},
|
|
12105
|
+
body: codeFrame
|
|
12059
12106
|
};
|
|
12060
12107
|
}
|
|
12061
12108
|
|
|
12062
|
-
|
|
12063
|
-
launch(fileURLToPath(file), () => {// ignore error for now
|
|
12064
|
-
});
|
|
12065
|
-
return {
|
|
12066
|
-
status: 200
|
|
12067
|
-
};
|
|
12109
|
+
return null;
|
|
12068
12110
|
},
|
|
12069
12111
|
transformUrlContent: {
|
|
12070
12112
|
html: ({
|
|
@@ -12193,6 +12235,7 @@ const jsenvPluginHtmlSupervisor = ({
|
|
|
12193
12235
|
import { installHtmlSupervisor } from ${htmlSupervisorInstallerFileReference.generatedSpecifier}
|
|
12194
12236
|
installHtmlSupervisor(${JSON.stringify({
|
|
12195
12237
|
rootDirectoryUrl: context.rootDirectoryUrl,
|
|
12238
|
+
errorBaseUrl,
|
|
12196
12239
|
logs,
|
|
12197
12240
|
measurePerf,
|
|
12198
12241
|
errorOverlay,
|
|
@@ -23641,14 +23684,15 @@ const startDevServer = async ({
|
|
|
23641
23684
|
handleSIGINT = true,
|
|
23642
23685
|
logLevel = "info",
|
|
23643
23686
|
omegaServerLogLevel = "warn",
|
|
23644
|
-
port = 3456,
|
|
23645
23687
|
protocol = "http",
|
|
23646
|
-
acceptAnyIp,
|
|
23647
23688
|
// it's better to use http1 by default because it allows to get statusText in devtools
|
|
23648
23689
|
// which gives valuable information when there is errors
|
|
23649
23690
|
http2 = false,
|
|
23650
23691
|
certificate,
|
|
23651
23692
|
privateKey,
|
|
23693
|
+
host,
|
|
23694
|
+
port = 3456,
|
|
23695
|
+
acceptAnyIp,
|
|
23652
23696
|
keepProcessAlive = true,
|
|
23653
23697
|
services,
|
|
23654
23698
|
rootDirectoryUrl,
|
|
@@ -23786,11 +23830,12 @@ const startDevServer = async ({
|
|
|
23786
23830
|
logLevel: omegaServerLogLevel,
|
|
23787
23831
|
keepProcessAlive,
|
|
23788
23832
|
acceptAnyIp,
|
|
23789
|
-
port,
|
|
23790
23833
|
protocol,
|
|
23791
23834
|
http2,
|
|
23792
23835
|
certificate,
|
|
23793
23836
|
privateKey,
|
|
23837
|
+
host,
|
|
23838
|
+
port,
|
|
23794
23839
|
services,
|
|
23795
23840
|
rootDirectoryUrl,
|
|
23796
23841
|
scenario: "dev",
|
|
@@ -25670,7 +25715,7 @@ const executeTestPlan = async ({
|
|
|
25670
25715
|
keepRunning = false,
|
|
25671
25716
|
cooldownBetweenExecutions = 0,
|
|
25672
25717
|
gcBetweenExecutions = logMemoryHeapUsage,
|
|
25673
|
-
coverageEnabled = process.argv.includes("--
|
|
25718
|
+
coverageEnabled = process.argv.includes("--coverage"),
|
|
25674
25719
|
coverageConfig = {
|
|
25675
25720
|
"./src/": true
|
|
25676
25721
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "27.5.
|
|
3
|
+
"version": "27.5.3",
|
|
4
4
|
"description": "Tool to develop, test and build js projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"performances": "node --expose-gc ./scripts/performance/generate_performance_report.mjs --log --once",
|
|
51
51
|
"file-size": "node ./scripts/file_size/file_size.mjs --log",
|
|
52
52
|
"start_file_server": "node ./scripts/dev/start_file_server.mjs",
|
|
53
|
+
"generate-dev-errors-snapshot-files": "node --conditions=development ./tests/dev_server/errors/generate_snapshot_files.mjs",
|
|
53
54
|
"prettier": "prettier --write .",
|
|
54
55
|
"playwright-install": "npx playwright install-deps && npx playwright install",
|
|
55
56
|
"certificate-install": "node ./scripts/dev/install_certificate_authority.mjs",
|
|
@@ -15,14 +15,15 @@ export const startDevServer = async ({
|
|
|
15
15
|
handleSIGINT = true,
|
|
16
16
|
logLevel = "info",
|
|
17
17
|
omegaServerLogLevel = "warn",
|
|
18
|
-
port = 3456,
|
|
19
18
|
protocol = "http",
|
|
20
|
-
acceptAnyIp,
|
|
21
19
|
// it's better to use http1 by default because it allows to get statusText in devtools
|
|
22
20
|
// which gives valuable information when there is errors
|
|
23
21
|
http2 = false,
|
|
24
22
|
certificate,
|
|
25
23
|
privateKey,
|
|
24
|
+
host,
|
|
25
|
+
port = 3456,
|
|
26
|
+
acceptAnyIp,
|
|
26
27
|
keepProcessAlive = true,
|
|
27
28
|
services,
|
|
28
29
|
|
|
@@ -148,11 +149,12 @@ export const startDevServer = async ({
|
|
|
148
149
|
logLevel: omegaServerLogLevel,
|
|
149
150
|
keepProcessAlive,
|
|
150
151
|
acceptAnyIp,
|
|
151
|
-
port,
|
|
152
152
|
protocol,
|
|
153
153
|
http2,
|
|
154
154
|
certificate,
|
|
155
155
|
privateKey,
|
|
156
|
+
host,
|
|
157
|
+
port,
|
|
156
158
|
services,
|
|
157
159
|
|
|
158
160
|
rootDirectoryUrl,
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
export const formatError = (
|
|
2
|
+
error,
|
|
3
|
+
{
|
|
4
|
+
rootDirectoryUrl,
|
|
5
|
+
errorBaseUrl,
|
|
6
|
+
openInEditor,
|
|
7
|
+
url,
|
|
8
|
+
line,
|
|
9
|
+
column,
|
|
10
|
+
codeFrame,
|
|
11
|
+
requestedRessource,
|
|
12
|
+
reportedBy,
|
|
13
|
+
},
|
|
14
|
+
) => {
|
|
15
|
+
let { message, stack } = normalizeErrorParts(error)
|
|
16
|
+
let codeFramePromiseReference = { current: null }
|
|
17
|
+
let tip = formatTip({ reportedBy, requestedRessource })
|
|
18
|
+
let errorUrlSite
|
|
19
|
+
|
|
20
|
+
const resolveUrlSite = ({ url, line, column }) => {
|
|
21
|
+
const inlineUrlMatch = url.match(/@L([0-9]+)\-L([0-9]+)\.[\w]+$/)
|
|
22
|
+
if (inlineUrlMatch) {
|
|
23
|
+
const htmlUrl = url.slice(0, inlineUrlMatch.index)
|
|
24
|
+
const tagLine = parseInt(inlineUrlMatch[1])
|
|
25
|
+
const tagColumn = parseInt(inlineUrlMatch[2])
|
|
26
|
+
url = htmlUrl
|
|
27
|
+
line = tagLine + parseInt(line) - 1
|
|
28
|
+
column = tagColumn + parseInt(column)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let urlObject = new URL(url)
|
|
32
|
+
if (urlObject.origin === window.origin) {
|
|
33
|
+
urlObject = new URL(
|
|
34
|
+
`${urlObject.pathname.slice(1)}${urlObject.search}`,
|
|
35
|
+
rootDirectoryUrl,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
if (urlObject.href.startsWith("file:")) {
|
|
39
|
+
const atFsIndex = urlObject.pathname.indexOf("/@fs/")
|
|
40
|
+
if (atFsIndex > -1) {
|
|
41
|
+
const afterAtFs = urlObject.pathname.slice(atFsIndex + "/@fs/".length)
|
|
42
|
+
url = new URL(afterAtFs, "file:///").href
|
|
43
|
+
} else {
|
|
44
|
+
url = urlObject.href
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
url = urlObject.href
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
url,
|
|
52
|
+
line,
|
|
53
|
+
column,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const generateClickableText = (text) => {
|
|
58
|
+
const textWithHtmlLinks = makeLinksClickable(text, {
|
|
59
|
+
createLink: (url, { line, column }) => {
|
|
60
|
+
const urlSite = resolveUrlSite({ url, line, column })
|
|
61
|
+
if (!errorUrlSite && text === stack) {
|
|
62
|
+
onErrorLocated(urlSite)
|
|
63
|
+
}
|
|
64
|
+
if (errorBaseUrl) {
|
|
65
|
+
if (urlSite.url.startsWith(rootDirectoryUrl)) {
|
|
66
|
+
urlSite.url = `${errorBaseUrl}${urlSite.url.slice(
|
|
67
|
+
rootDirectoryUrl.length,
|
|
68
|
+
)}`
|
|
69
|
+
} else {
|
|
70
|
+
urlSite.url = "file:///mocked_for_snapshots"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const urlWithLineAndColumn = formatUrlWithLineAndColumn(urlSite)
|
|
74
|
+
return {
|
|
75
|
+
href:
|
|
76
|
+
url.startsWith("file:") && openInEditor
|
|
77
|
+
? `javascript:window.fetch('/__open_in_editor__/${urlWithLineAndColumn}')`
|
|
78
|
+
: urlSite.url,
|
|
79
|
+
text: urlWithLineAndColumn,
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
return textWithHtmlLinks
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const onErrorLocated = (urlSite) => {
|
|
87
|
+
errorUrlSite = urlSite
|
|
88
|
+
if (codeFrame) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if (reportedBy !== "browser") {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
codeFramePromiseReference.current = (async () => {
|
|
95
|
+
const response = await window.fetch(
|
|
96
|
+
`/__get_code_frame__/${formatUrlWithLineAndColumn(urlSite)}`,
|
|
97
|
+
)
|
|
98
|
+
const codeFrame = await response.text()
|
|
99
|
+
const codeFrameClickable = generateClickableText(codeFrame)
|
|
100
|
+
return codeFrameClickable
|
|
101
|
+
})()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// error.stack is more reliable than url/line/column reported on window error events
|
|
105
|
+
// so use it only when error.stack is not available
|
|
106
|
+
if (
|
|
107
|
+
url &&
|
|
108
|
+
!stack &&
|
|
109
|
+
// ignore window.reportError() it gives no valuable info
|
|
110
|
+
!url.endsWith("html_supervisor_installer.js")
|
|
111
|
+
) {
|
|
112
|
+
onErrorLocated(resolveUrlSite({ url, line, column }))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let text
|
|
116
|
+
|
|
117
|
+
if (message && stack) {
|
|
118
|
+
text = `${generateClickableText(message)}\n${generateClickableText(stack)}`
|
|
119
|
+
} else if (stack) {
|
|
120
|
+
text = generateClickableText(stack)
|
|
121
|
+
} else {
|
|
122
|
+
text = generateClickableText(message)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (codeFrame) {
|
|
126
|
+
text += `\n\n${generateClickableText(codeFrame)}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
theme:
|
|
131
|
+
error && error.cause && error.cause.code === "PARSE_ERROR"
|
|
132
|
+
? "light"
|
|
133
|
+
: "dark",
|
|
134
|
+
title: "An error occured",
|
|
135
|
+
text,
|
|
136
|
+
codeFramePromise: codeFramePromiseReference.current,
|
|
137
|
+
tip: `${tip}
|
|
138
|
+
<br />
|
|
139
|
+
Click outside to close.`,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const formatUrlWithLineAndColumn = ({ url, line, column }) => {
|
|
144
|
+
return line === undefined && column === undefined
|
|
145
|
+
? url
|
|
146
|
+
: column === undefined
|
|
147
|
+
? `${url}:${line}`
|
|
148
|
+
: `${url}:${line}:${column}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const normalizeErrorParts = (error) => {
|
|
152
|
+
if (error === undefined) {
|
|
153
|
+
return {
|
|
154
|
+
message: "undefined",
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (error === null) {
|
|
158
|
+
return {
|
|
159
|
+
message: "null",
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (typeof error === "string") {
|
|
163
|
+
return {
|
|
164
|
+
message: error,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (error instanceof Error) {
|
|
168
|
+
if (error.name === "SyntaxError") {
|
|
169
|
+
return {
|
|
170
|
+
message: error.message,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (error.cause && error.cause.code === "PARSE_ERROR") {
|
|
174
|
+
if (error.messageHTML) {
|
|
175
|
+
return {
|
|
176
|
+
message: error.messageHTML,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
message: error.message,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// stackTrace formatted by V8
|
|
184
|
+
if (Error.captureStackTrace) {
|
|
185
|
+
return {
|
|
186
|
+
message: error.message,
|
|
187
|
+
stack: getErrorStackWithoutErrorMessage(error),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
message: error.message,
|
|
192
|
+
stack: error.stack ? ` ${error.stack}` : null,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (typeof error === "object") {
|
|
196
|
+
return error
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
message: JSON.stringify(error),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const getErrorStackWithoutErrorMessage = (error) => {
|
|
204
|
+
let stack = error.stack
|
|
205
|
+
const messageInStack = `${error.name}: ${error.message}`
|
|
206
|
+
if (stack.startsWith(messageInStack)) {
|
|
207
|
+
stack = stack.slice(messageInStack.length)
|
|
208
|
+
}
|
|
209
|
+
const nextLineIndex = stack.indexOf("\n")
|
|
210
|
+
if (nextLineIndex > -1) {
|
|
211
|
+
stack = stack.slice(nextLineIndex + 1)
|
|
212
|
+
}
|
|
213
|
+
return stack
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const formatTip = ({ reportedBy, requestedRessource }) => {
|
|
217
|
+
if (reportedBy === "browser") {
|
|
218
|
+
return `Reported by the browser while executing <code>${window.location.pathname}${window.location.search}</code>.`
|
|
219
|
+
}
|
|
220
|
+
return `Reported by the server while serving <code>${requestedRessource}</code>`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const makeLinksClickable = (string, { createLink = (url) => url }) => {
|
|
224
|
+
// normalize line breaks
|
|
225
|
+
string = string.replace(/\n/g, "\n")
|
|
226
|
+
string = escapeHtml(string)
|
|
227
|
+
// render links
|
|
228
|
+
string = stringToStringWithLink(string, {
|
|
229
|
+
transform: (url, { line, column }) => {
|
|
230
|
+
const { href, text } = createLink(url, { line, column })
|
|
231
|
+
return link({ href, text })
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
return string
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const escapeHtml = (string) => {
|
|
238
|
+
return string
|
|
239
|
+
.replace(/&/g, "&")
|
|
240
|
+
.replace(/</g, "<")
|
|
241
|
+
.replace(/>/g, ">")
|
|
242
|
+
.replace(/"/g, """)
|
|
243
|
+
.replace(/'/g, "'")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// `Error: yo
|
|
247
|
+
// at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
|
|
248
|
+
// at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
|
|
249
|
+
// at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
|
|
250
|
+
// at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
|
|
251
|
+
// debugger
|
|
252
|
+
// })
|
|
253
|
+
const stringToStringWithLink = (
|
|
254
|
+
source,
|
|
255
|
+
{
|
|
256
|
+
transform = (url) => {
|
|
257
|
+
return {
|
|
258
|
+
href: url,
|
|
259
|
+
text: url,
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
} = {},
|
|
263
|
+
) => {
|
|
264
|
+
return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
|
|
265
|
+
let linkHTML = ""
|
|
266
|
+
|
|
267
|
+
const lastChar = match[match.length - 1]
|
|
268
|
+
|
|
269
|
+
// hotfix because our url regex sucks a bit
|
|
270
|
+
const endsWithSeparationChar = lastChar === ")" || lastChar === ":"
|
|
271
|
+
if (endsWithSeparationChar) {
|
|
272
|
+
match = match.slice(0, -1)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/
|
|
276
|
+
const lineAndColumMatch = match.match(lineAndColumnPattern)
|
|
277
|
+
if (lineAndColumMatch) {
|
|
278
|
+
const lineAndColumnString = lineAndColumMatch[0]
|
|
279
|
+
const lineNumber = lineAndColumMatch[1]
|
|
280
|
+
const columnNumber = lineAndColumMatch[2]
|
|
281
|
+
linkHTML = transform(match.slice(0, -lineAndColumnString.length), {
|
|
282
|
+
line: lineNumber,
|
|
283
|
+
column: columnNumber,
|
|
284
|
+
})
|
|
285
|
+
} else {
|
|
286
|
+
const linePattern = /:([0-9]+)$/
|
|
287
|
+
const lineMatch = match.match(linePattern)
|
|
288
|
+
if (lineMatch) {
|
|
289
|
+
const lineString = lineMatch[0]
|
|
290
|
+
const lineNumber = lineMatch[1]
|
|
291
|
+
linkHTML = transform(match.slice(0, -lineString.length), {
|
|
292
|
+
line: lineNumber,
|
|
293
|
+
})
|
|
294
|
+
} else {
|
|
295
|
+
linkHTML = transform(match, {})
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (endsWithSeparationChar) {
|
|
299
|
+
return `${linkHTML}${lastChar}`
|
|
300
|
+
}
|
|
301
|
+
return linkHTML
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const link = ({ href, text = href }) => `<a href="${href}">${text}</a>`
|