@jsenv/test 2.1.3 → 3.0.1
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/README.md +2 -1
- package/dist/js/exception.js +908 -0
- package/dist/js/execute_using_dynamic_import.js +76 -167
- package/dist/js/v8_coverage.js +4 -482
- package/dist/jsenv_test.js +4608 -2917
- package/dist/node_child_process_controlled.mjs +95 -76
- package/dist/node_worker_thread_controlled.mjs +72 -54
- package/package.json +23 -14
- package/src/coverage/generate_coverage.js +186 -0
- package/src/coverage/list_files_not_covered.js +2 -2
- package/src/coverage/missing_coverage.js +2 -2
- package/src/coverage/report_coverage_as_html.js +45 -0
- package/src/coverage/report_coverage_as_json.js +20 -0
- package/src/coverage/{coverage_reporter_text_log.js → report_coverage_in_console.js} +13 -7
- package/src/coverage/v8_and_istanbul.js +4 -4
- package/src/coverage/v8_coverage.js +2 -2
- package/src/execution/exception.js +151 -68
- package/src/execution/execute.js +24 -11
- package/src/execution/execute_test_plan.js +834 -750
- package/src/execution/format_error_for_terminal.js +187 -0
- package/src/execution/github_annotation_from_error.js +3 -3
- package/src/execution/junit_xml_file/report_as_junit_xml.js +149 -0
- package/src/execution/junit_xml_file/xml_generator.js +141 -0
- package/src/execution/reporters/reporter_list.js +687 -0
- package/src/execution/run.js +92 -30
- package/src/execution/web_server/start_using_command.js +2 -2
- package/src/execution/web_server_param.js +5 -5
- package/src/helpers/cpu_usage.js +142 -0
- package/src/helpers/cpu_usage.test_manual.js +7 -0
- package/src/main.js +8 -0
- package/src/runtime_browsers/chromium.js +1 -3
- package/src/runtime_browsers/firefox.js +0 -3
- package/src/runtime_browsers/middleware_istanbul.js +2 -2
- package/src/runtime_browsers/using_playwright.js +296 -220
- package/src/runtime_browsers/webkit.js +0 -3
- package/src/runtime_node/execute_using_dynamic_import.js +75 -28
- package/src/runtime_node/node_child_process.js +250 -139
- package/src/runtime_node/node_child_process_controlled.mjs +94 -75
- package/src/runtime_node/node_worker_thread.js +206 -120
- package/src/runtime_node/node_worker_thread_controlled.mjs +70 -53
- package/src/coverage/coverage_reporter_html_directory.js +0 -32
- package/src/coverage/coverage_reporter_json_file.js +0 -17
- package/src/coverage/report_to_coverage.js +0 -198
- package/src/execution/execution_colors.js +0 -10
- package/src/execution/execution_steps.js +0 -65
- package/src/execution/logs_file_execution.js +0 -495
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { inspectFileContent } from "@jsenv/inspect";
|
|
3
|
+
import { urlToFileSystemPath } from "@jsenv/urls";
|
|
4
|
+
import { ANSI } from "@jsenv/log";
|
|
5
|
+
|
|
6
|
+
export const formatErrorForTerminal = (
|
|
7
|
+
error,
|
|
8
|
+
{ rootDirectoryUrl, mainFileRelativeUrl, mockFluctuatingValues, tryColors },
|
|
9
|
+
) => {
|
|
10
|
+
if (!error.stack) {
|
|
11
|
+
return error.message;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let ansiSupported = ANSI.supported;
|
|
15
|
+
if (!tryColors) {
|
|
16
|
+
ANSI.supported = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let text = "";
|
|
20
|
+
write_code_frame: {
|
|
21
|
+
if (
|
|
22
|
+
error.site &&
|
|
23
|
+
error.site.url &&
|
|
24
|
+
error.site.url.startsWith("file:") &&
|
|
25
|
+
typeof error.site.line === "number"
|
|
26
|
+
) {
|
|
27
|
+
const content = readFileSync(new URL(error.site.url), "utf8");
|
|
28
|
+
text += inspectFileContent({
|
|
29
|
+
content,
|
|
30
|
+
line: error.site.line,
|
|
31
|
+
column: error.site.column,
|
|
32
|
+
linesAbove: 2,
|
|
33
|
+
linesBelow: 0,
|
|
34
|
+
lineMaxWidth: process.stdout.columns,
|
|
35
|
+
format: (string, type) => {
|
|
36
|
+
return {
|
|
37
|
+
line_number_aside: () => ANSI.color(string, ANSI.GREY),
|
|
38
|
+
char: () => string,
|
|
39
|
+
marker_overflow_left: () => ANSI.color(string, ANSI.GREY),
|
|
40
|
+
marker_overflow_right: () => ANSI.color(string, ANSI.GREY),
|
|
41
|
+
marker_line: () => ANSI.color(string, ANSI.RED),
|
|
42
|
+
marker_column: () => ANSI.color(string, ANSI.RED),
|
|
43
|
+
}[type]();
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
text += `\n`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
text += `${error.name}: ${error.message}`;
|
|
50
|
+
write_stack: {
|
|
51
|
+
const stringifyUrlSite = ({ url, line, column, urlIsMain }) => {
|
|
52
|
+
let urlAsPath = String(url).startsWith("file:")
|
|
53
|
+
? urlToFileSystemPath(url)
|
|
54
|
+
: url;
|
|
55
|
+
if (mockFluctuatingValues) {
|
|
56
|
+
const rootDirectoryPath = urlToFileSystemPath(rootDirectoryUrl);
|
|
57
|
+
urlAsPath = urlAsPath.replace(rootDirectoryPath, "[mock]");
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
urlAsPath = urlAsPath.replace(/\\/g, "/");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (urlIsMain) {
|
|
63
|
+
urlAsPath = ANSI.effect(urlAsPath, ANSI.BOLD);
|
|
64
|
+
}
|
|
65
|
+
if (typeof line === "number" && typeof column === "number") {
|
|
66
|
+
return `${urlAsPath}:${line}:${column}`;
|
|
67
|
+
}
|
|
68
|
+
if (typeof line === "number") {
|
|
69
|
+
return `${urlAsPath}:${line}`;
|
|
70
|
+
}
|
|
71
|
+
return urlAsPath;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let stackTrace = "";
|
|
75
|
+
const stackFrames = error.stackFrames;
|
|
76
|
+
if (stackFrames && stackFrames.length) {
|
|
77
|
+
let atLeastOneNonNative = false;
|
|
78
|
+
let lastStackFrameForMain;
|
|
79
|
+
const mainFileUrl = new URL(mainFileRelativeUrl, rootDirectoryUrl).href;
|
|
80
|
+
for (const stackFrame of stackFrames) {
|
|
81
|
+
if (stackFrame.url === mainFileUrl) {
|
|
82
|
+
lastStackFrameForMain = stackFrame;
|
|
83
|
+
}
|
|
84
|
+
if (!stackFrame.native) {
|
|
85
|
+
atLeastOneNonNative = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const stackFrame of stackFrames) {
|
|
90
|
+
if (atLeastOneNonNative && stackFrame.native) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let stackFrameString = stackFrame.raw;
|
|
94
|
+
stackFrameString = replaceUrls(
|
|
95
|
+
stackFrameString,
|
|
96
|
+
({ url, line, column }) => {
|
|
97
|
+
const replacement = stringifyUrlSite({
|
|
98
|
+
url,
|
|
99
|
+
urlIsMain: stackFrame === lastStackFrameForMain,
|
|
100
|
+
line,
|
|
101
|
+
column,
|
|
102
|
+
});
|
|
103
|
+
return replacement;
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
if (stackTrace) stackTrace += "\n";
|
|
107
|
+
stackTrace += stackFrameString;
|
|
108
|
+
}
|
|
109
|
+
} else if (error.stackTrace) {
|
|
110
|
+
stackTrace = replaceUrls(error.stackTrace, ({ url, line, column }) => {
|
|
111
|
+
const replacement = stringifyUrlSite({
|
|
112
|
+
url,
|
|
113
|
+
line,
|
|
114
|
+
column,
|
|
115
|
+
});
|
|
116
|
+
return replacement;
|
|
117
|
+
});
|
|
118
|
+
} else if (error.site) {
|
|
119
|
+
stackTrace += ` ${stringifyUrlSite({
|
|
120
|
+
url: error.site.url,
|
|
121
|
+
line: error.site.line,
|
|
122
|
+
column: error.site.column,
|
|
123
|
+
})}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (stackTrace) {
|
|
127
|
+
text += `\n${stackTrace}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!tryColors) {
|
|
132
|
+
ANSI.supported = ansiSupported;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return text;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// `Error: yo
|
|
139
|
+
// at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
|
|
140
|
+
// at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
|
|
141
|
+
// at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
|
|
142
|
+
// at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
|
|
143
|
+
// debugger
|
|
144
|
+
// })
|
|
145
|
+
const replaceUrls = (source, replace) => {
|
|
146
|
+
return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
|
|
147
|
+
let replacement = "";
|
|
148
|
+
const lastChar = match[match.length - 1];
|
|
149
|
+
|
|
150
|
+
// hotfix because our url regex sucks a bit
|
|
151
|
+
const endsWithSeparationChar = lastChar === ")" || lastChar === ":";
|
|
152
|
+
if (endsWithSeparationChar) {
|
|
153
|
+
match = match.slice(0, -1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/;
|
|
157
|
+
const lineAndColumMatch = match.match(lineAndColumnPattern);
|
|
158
|
+
if (lineAndColumMatch) {
|
|
159
|
+
const lineAndColumnString = lineAndColumMatch[0];
|
|
160
|
+
const lineString = lineAndColumMatch[1];
|
|
161
|
+
const columnString = lineAndColumMatch[2];
|
|
162
|
+
replacement = replace({
|
|
163
|
+
url: match.slice(0, -lineAndColumnString.length),
|
|
164
|
+
line: lineString ? parseInt(lineString) : null,
|
|
165
|
+
column: columnString ? parseInt(columnString) : null,
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
const linePattern = /:([0-9]+)$/;
|
|
169
|
+
const lineMatch = match.match(linePattern);
|
|
170
|
+
if (lineMatch) {
|
|
171
|
+
const lineString = lineMatch[0];
|
|
172
|
+
replacement = replace({
|
|
173
|
+
url: match.slice(0, -lineString.length),
|
|
174
|
+
line: lineString ? parseInt(lineString) : null,
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
replacement = replace({
|
|
178
|
+
url: match,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (endsWithSeparationChar) {
|
|
183
|
+
return `${replacement}${lastChar}`;
|
|
184
|
+
}
|
|
185
|
+
return replacement;
|
|
186
|
+
});
|
|
187
|
+
};
|
|
@@ -2,14 +2,14 @@ import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
|
|
|
2
2
|
|
|
3
3
|
export const githubAnnotationFromError = (
|
|
4
4
|
error,
|
|
5
|
-
{ rootDirectoryUrl,
|
|
5
|
+
{ rootDirectoryUrl, execution },
|
|
6
6
|
) => {
|
|
7
7
|
const annotation = {
|
|
8
8
|
annotation_level: "failure",
|
|
9
|
-
path:
|
|
9
|
+
path: execution.fileRelativeUrl,
|
|
10
10
|
start_line: 1,
|
|
11
11
|
end_line: 1,
|
|
12
|
-
title: `Error while executing ${
|
|
12
|
+
title: `Error while executing ${execution.fileRelativeUrl} on ${execution.runtimeName}@${execution.runtimeVersion}`,
|
|
13
13
|
};
|
|
14
14
|
const exception = asException(error, { rootDirectoryUrl });
|
|
15
15
|
if (exception.site && typeof exception.site.line === "number") {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// run prettier on it
|
|
2
|
+
|
|
3
|
+
// https://github.com/testmoapp/junitxml
|
|
4
|
+
// https://llg.cubic.org/docs/junit/
|
|
5
|
+
|
|
6
|
+
import { assertAndNormalizeFileUrl, writeFileSync } from "@jsenv/filesystem";
|
|
7
|
+
|
|
8
|
+
import { formatErrorForTerminal } from "../format_error_for_terminal.js";
|
|
9
|
+
import { createXmlGenerator } from "./xml_generator.js";
|
|
10
|
+
|
|
11
|
+
export const reportAsJunitXml = async (
|
|
12
|
+
testPlanResult,
|
|
13
|
+
fileUrl,
|
|
14
|
+
{ mockFluctuatingValues } = {},
|
|
15
|
+
) => {
|
|
16
|
+
fileUrl = assertAndNormalizeFileUrl(fileUrl);
|
|
17
|
+
|
|
18
|
+
const createRootNode = createXmlGenerator({
|
|
19
|
+
rootNodeName: "testsuite",
|
|
20
|
+
canReceiveChildNames: ["testsuite", "testcase", "properties"],
|
|
21
|
+
canReceiveContentNames: ["failure", "error", "system-out", "system-err"],
|
|
22
|
+
});
|
|
23
|
+
const testSuite = createRootNode({
|
|
24
|
+
time: mockFluctuatingValues ? "[mock]" : testPlanResult.timings.end,
|
|
25
|
+
timestamp: mockFluctuatingValues
|
|
26
|
+
? "[mock]"
|
|
27
|
+
: new Date(testPlanResult.timings.origin).toISOString().slice(0, -5), // Date and time of when the test run was executed
|
|
28
|
+
tests: testPlanResult.counters.planified,
|
|
29
|
+
skipped:
|
|
30
|
+
testPlanResult.counters.aborted + testPlanResult.counters.cancelled,
|
|
31
|
+
failures: testPlanResult.counters.timedout + testPlanResult.counters.failed, // Total number of failed tests
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
for (const fileRelativeUrl of Object.keys(testPlanResult.results)) {
|
|
35
|
+
const fileResults = testPlanResult.results[fileRelativeUrl];
|
|
36
|
+
for (const group of Object.keys(fileResults)) {
|
|
37
|
+
const executionResult = fileResults[group];
|
|
38
|
+
const testCase = testSuite.createNode("testcase", {
|
|
39
|
+
file: fileRelativeUrl,
|
|
40
|
+
name: group,
|
|
41
|
+
time: mockFluctuatingValues ? "[mock]" : executionResult.timings.end, // duration of this execution
|
|
42
|
+
timestamp: mockFluctuatingValues
|
|
43
|
+
? "[mock]"
|
|
44
|
+
: new Date(executionResult.timings.origin).toISOString().slice(0, -5), // Date and time of when the test run was executed
|
|
45
|
+
tests: 1, // Total number of tests for this execution
|
|
46
|
+
});
|
|
47
|
+
testSuite.appendChild(testCase);
|
|
48
|
+
|
|
49
|
+
if (executionResult.status === "aborted") {
|
|
50
|
+
testCase.attributes.skipped = 1;
|
|
51
|
+
const skipped = testCase.createNode("skipped", {
|
|
52
|
+
message: "Execution was aborted",
|
|
53
|
+
});
|
|
54
|
+
testSuite.appendChild(skipped);
|
|
55
|
+
}
|
|
56
|
+
if (executionResult.status === "timedout") {
|
|
57
|
+
testCase.attributes.failures = 1;
|
|
58
|
+
const failure = testCase.createNode("failure", {
|
|
59
|
+
message: `Execution timeout after ${executionResult.params.allocatedMs}ms"`,
|
|
60
|
+
type: "timeout",
|
|
61
|
+
});
|
|
62
|
+
testSuite.appendChild(failure);
|
|
63
|
+
}
|
|
64
|
+
if (executionResult.status === "failed") {
|
|
65
|
+
const [error] = executionResult.errors;
|
|
66
|
+
if (
|
|
67
|
+
error &&
|
|
68
|
+
typeof error.name === "string" &&
|
|
69
|
+
error.name.includes("AssertionError")
|
|
70
|
+
) {
|
|
71
|
+
testCase.attributes.failures = 1;
|
|
72
|
+
const failure = testCase.createNode("failure", {
|
|
73
|
+
type: error.name,
|
|
74
|
+
});
|
|
75
|
+
testSuite.appendChild(failure);
|
|
76
|
+
failure.setContent(
|
|
77
|
+
formatErrorForTerminal(error, {
|
|
78
|
+
rootDirectoryUrl: testPlanResult.rootDirectoryUrl,
|
|
79
|
+
mainFileRelativeUrl: fileRelativeUrl,
|
|
80
|
+
mockFluctuatingValues,
|
|
81
|
+
tryColors: false,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
testCase.attributes.errors = 1;
|
|
86
|
+
const errorNode = testCase.createNode("error", {
|
|
87
|
+
type: error.name,
|
|
88
|
+
});
|
|
89
|
+
testSuite.appendChild(errorNode);
|
|
90
|
+
errorNode.setContent(
|
|
91
|
+
formatErrorForTerminal(error, {
|
|
92
|
+
rootDirectoryUrl: testPlanResult.rootDirectoryUrl,
|
|
93
|
+
mainFileRelativeUrl: fileRelativeUrl,
|
|
94
|
+
mockFluctuatingValues,
|
|
95
|
+
tryColors: false,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const groupInfo = testPlanResult.groups[group];
|
|
102
|
+
const properties = {
|
|
103
|
+
runtimeName: groupInfo.runtimeName,
|
|
104
|
+
runtimeVersion: mockFluctuatingValues
|
|
105
|
+
? "[mock]"
|
|
106
|
+
: groupInfo.runtimeVersion,
|
|
107
|
+
};
|
|
108
|
+
const propertiesNode = testCase.createNode("properties");
|
|
109
|
+
testCase.appendChild(propertiesNode);
|
|
110
|
+
for (const propertyName of Object.keys(properties)) {
|
|
111
|
+
const propertyNode = propertiesNode.createNode("property", {
|
|
112
|
+
name: propertyName,
|
|
113
|
+
value: properties[propertyName],
|
|
114
|
+
});
|
|
115
|
+
propertiesNode.appendChild(propertyNode);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { consoleCalls } = executionResult;
|
|
119
|
+
let stdout = "";
|
|
120
|
+
let stderr = "";
|
|
121
|
+
for (const consoleCall of consoleCalls) {
|
|
122
|
+
if (groupInfo.runtimeType === "node") {
|
|
123
|
+
if (consoleCall.type === "error") {
|
|
124
|
+
stdout += consoleCall.text;
|
|
125
|
+
} else {
|
|
126
|
+
stderr += consoleCall.text;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
stdout += consoleCall.text;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (stdout.length) {
|
|
133
|
+
const systemOut = testSuite.createNode("system-out");
|
|
134
|
+
testCase.appendChild(systemOut);
|
|
135
|
+
systemOut.setContent(stdout);
|
|
136
|
+
}
|
|
137
|
+
if (stderr.length) {
|
|
138
|
+
const systemErr = testSuite.createNode("system-err");
|
|
139
|
+
testCase.appendChild(systemErr);
|
|
140
|
+
systemErr.setContent(stderr);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let junitXmlFileContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
146
|
+
${testSuite.renderAsString()}`;
|
|
147
|
+
|
|
148
|
+
writeFileSync(new URL(fileUrl), junitXmlFileContent);
|
|
149
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import he from "he";
|
|
2
|
+
|
|
3
|
+
export const createXmlGenerator = ({
|
|
4
|
+
rootNodeName,
|
|
5
|
+
canSelfCloseNames = [],
|
|
6
|
+
canReceiveChildNames = [],
|
|
7
|
+
canReceiveContentNames = [],
|
|
8
|
+
}) => {
|
|
9
|
+
const createNode = (name, attributes = {}) => {
|
|
10
|
+
const canSelfClose = canSelfCloseNames.includes(name);
|
|
11
|
+
const canReceiveChild = canReceiveChildNames.includes(name);
|
|
12
|
+
const canReceiveContent = canReceiveContentNames.includes(name);
|
|
13
|
+
|
|
14
|
+
const children = [];
|
|
15
|
+
|
|
16
|
+
const node = {
|
|
17
|
+
name,
|
|
18
|
+
content: "",
|
|
19
|
+
children,
|
|
20
|
+
attributes,
|
|
21
|
+
canSelfClose,
|
|
22
|
+
createNode,
|
|
23
|
+
appendChild: (childNode) => {
|
|
24
|
+
if (!canReceiveChild) {
|
|
25
|
+
throw new Error(`cannot appendChild into ${name}`);
|
|
26
|
+
}
|
|
27
|
+
children.push(childNode);
|
|
28
|
+
return childNode;
|
|
29
|
+
},
|
|
30
|
+
setContent: (value) => {
|
|
31
|
+
if (!canReceiveContent) {
|
|
32
|
+
throw new Error(`cannot setContent on ${name}`);
|
|
33
|
+
}
|
|
34
|
+
node.content = value;
|
|
35
|
+
},
|
|
36
|
+
renderAsString: () => {
|
|
37
|
+
const renderNode = (node, { depth }) => {
|
|
38
|
+
let nodeString = "";
|
|
39
|
+
nodeString += `<${node.name}`;
|
|
40
|
+
|
|
41
|
+
write_attributes: {
|
|
42
|
+
const attributeNames = Object.keys(node.attributes);
|
|
43
|
+
if (attributeNames.length) {
|
|
44
|
+
let attributesSingleLine = "";
|
|
45
|
+
let attributesMultiLine = "";
|
|
46
|
+
|
|
47
|
+
for (const attributeName of attributeNames) {
|
|
48
|
+
let attributeValue = node.attributes[attributeName];
|
|
49
|
+
if (typeof attributeValue === "number") {
|
|
50
|
+
attributeValue = round(attributeValue);
|
|
51
|
+
}
|
|
52
|
+
if (attributeName === "viewBox") {
|
|
53
|
+
attributeValue = attributeValue
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((v) => round(parseFloat(v.trim())))
|
|
56
|
+
.join(", ");
|
|
57
|
+
}
|
|
58
|
+
attributesSingleLine += ` ${attributeName}="${attributeValue}"`;
|
|
59
|
+
attributesMultiLine += `\n `;
|
|
60
|
+
attributesMultiLine += " ".repeat(depth);
|
|
61
|
+
attributesMultiLine += `${attributeName}="${attributeValue}"`;
|
|
62
|
+
}
|
|
63
|
+
attributesMultiLine += "\n";
|
|
64
|
+
attributesMultiLine += " ".repeat(depth);
|
|
65
|
+
|
|
66
|
+
if (attributesSingleLine.length < 100) {
|
|
67
|
+
nodeString += attributesSingleLine;
|
|
68
|
+
} else {
|
|
69
|
+
nodeString += attributesMultiLine;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let innerHTML = "";
|
|
75
|
+
if (node.content) {
|
|
76
|
+
const contentEncoded = he.encode(node.content, { decimal: false });
|
|
77
|
+
innerHTML += contentEncoded;
|
|
78
|
+
}
|
|
79
|
+
write_children: {
|
|
80
|
+
if (node.children.length > 0) {
|
|
81
|
+
for (const child of node.children) {
|
|
82
|
+
innerHTML += "\n ";
|
|
83
|
+
innerHTML += " ".repeat(depth);
|
|
84
|
+
innerHTML += renderNode(child, {
|
|
85
|
+
depth: depth + 1,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
innerHTML += "\n";
|
|
89
|
+
innerHTML += " ".repeat(depth);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (innerHTML === "") {
|
|
93
|
+
if (node.canSelfClose) {
|
|
94
|
+
nodeString += `/>`;
|
|
95
|
+
} else {
|
|
96
|
+
nodeString += `></${node.name}>`;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
nodeString += `>`;
|
|
100
|
+
nodeString += innerHTML;
|
|
101
|
+
nodeString += `</${node.name}>`;
|
|
102
|
+
}
|
|
103
|
+
return nodeString;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return renderNode(node, {
|
|
107
|
+
depth: 0,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return node;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (rootNodeAttributes) => createNode(rootNodeName, rootNodeAttributes);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const createSvgRootNode = createXmlGenerator({
|
|
119
|
+
rootNodeName: "svg",
|
|
120
|
+
canSelfCloseNames: ["path", "rect", "circle"],
|
|
121
|
+
canReceiveChildNames: ["svg", "foreignObject", "g"],
|
|
122
|
+
canReceiveContentNames: ["text", "style"],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// not used for now and needs to configure canReceiveChildNames and so on...
|
|
126
|
+
export const createXmlRootNode = createXmlGenerator({
|
|
127
|
+
rootNodeName: "xml",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// not used for now and needs to configure canReceiveChildNames and so on...
|
|
131
|
+
export const createHtmlRootNode = createXmlGenerator({
|
|
132
|
+
rootNodeName: "html",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Round: Make number values smaller in output
|
|
136
|
+
// Eg: 14.23734 becomes 14.24
|
|
137
|
+
// Credit @Chris Martin: https://stackoverflow.com/a/43012696/2816869
|
|
138
|
+
const round = (x) => {
|
|
139
|
+
const rounded = Number(`${Math.round(`${x}e2`)}e-2`);
|
|
140
|
+
return rounded;
|
|
141
|
+
};
|