@ckeditor/ckeditor5-dev-manual-server 56.2.0
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/LICENSE.md +16 -0
- package/README.md +17 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +584 -0
- package/dist/manual-test-plugin/collect-pages.d.ts +6 -0
- package/dist/manual-test-plugin/parse-markdown.d.ts +6 -0
- package/dist/manual-test-plugin/plugin.d.ts +7 -0
- package/dist/manual-test-plugin/shell-html.d.ts +13 -0
- package/dist/manual-test-plugin/static-assets.d.ts +10 -0
- package/dist/manual-test-plugin/types.d.ts +18 -0
- package/dist/raw-plugin/plugin.d.ts +6 -0
- package/dist/refresh-plugin/plugin.d.ts +7 -0
- package/dist/utils.d.ts +20 -0
- package/package.json +38 -0
- package/theme/catalog.css +252 -0
- package/theme/catalog.html +57 -0
- package/theme/catalog.ts +162 -0
- package/theme/shell.css +361 -0
- package/theme/shell.html +46 -0
- package/theme/shell.ts +179 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Software License Agreement
|
|
2
|
+
==========================
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2003-2026, [CKSource](http://cksource.com) Holding sp. z o.o. All rights reserved.
|
|
5
|
+
|
|
6
|
+
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
|
|
7
|
+
|
|
8
|
+
Sources of Intellectual Property Included in CKEditor
|
|
9
|
+
-----------------------------------------------------
|
|
10
|
+
|
|
11
|
+
Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission.
|
|
12
|
+
|
|
13
|
+
Trademarks
|
|
14
|
+
----------
|
|
15
|
+
|
|
16
|
+
**CKEditor** is a trademark of [CKSource](http://cksource.com) Holding sp. z o.o. All other brand and product names are trademarks, registered trademarks or service marks of their respective holders.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CKEditor 5 manual server
|
|
2
|
+
=======================
|
|
3
|
+
|
|
4
|
+
[](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-manual-server)
|
|
5
|
+
[](https://app.circleci.com/pipelines/github/ckeditor/ckeditor5-dev?branch=master)
|
|
6
|
+
|
|
7
|
+
Used to extend Vite to create a manual test server for CKEditor 5.
|
|
8
|
+
|
|
9
|
+
More information about development tools packages can be found at the following URL: <https://github.com/ckeditor/ckeditor5-dev>.
|
|
10
|
+
|
|
11
|
+
## Changelog
|
|
12
|
+
|
|
13
|
+
See the [`CHANGELOG.md`](https://github.com/ckeditor/ckeditor5-dev/blob/master/packages/ckeditor5-dev-manual-server/CHANGELOG.md) file.
|
|
14
|
+
|
|
15
|
+
## License
|
|
16
|
+
|
|
17
|
+
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
export { manualTestsPlugin } from "./manual-test-plugin/plugin.js";
|
|
6
|
+
export type { ManualData } from "./manual-test-plugin/plugin.js";
|
|
7
|
+
export { refreshPlugin } from "./refresh-plugin/plugin.js";
|
|
8
|
+
export { rawHtmlPlugin } from "./raw-plugin/plugin.js";
|
|
9
|
+
export { stringifyValues } from "./utils.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { createReadStream, existsSync, globSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path, { extname, isAbsolute, posix, relative, resolve } from "node:path";
|
|
4
|
+
import { parse, serialize } from "parse5";
|
|
5
|
+
import { appendChild, createElement, getAttribute, getTextContent, hasAttribute, isElementNode, query, removeAttribute, removeNode, setAttribute, setTextContent } from "@parse5/tools";
|
|
6
|
+
import { unified } from "unified";
|
|
7
|
+
import remarkGfm from "remark-gfm";
|
|
8
|
+
import remarkParse from "remark-parse";
|
|
9
|
+
import remarkRehype from "remark-rehype";
|
|
10
|
+
import { toHtml } from "hast-util-to-html";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
//#region src/utils.ts
|
|
13
|
+
/**
|
|
14
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
15
|
+
* For licensing, see LICENSE.md.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Caches the computed value until the cache is invalidated.
|
|
19
|
+
*/
|
|
20
|
+
function cacheValue(compute) {
|
|
21
|
+
let cached = null;
|
|
22
|
+
return {
|
|
23
|
+
get() {
|
|
24
|
+
cached ||= { value: compute() };
|
|
25
|
+
return cached.value;
|
|
26
|
+
},
|
|
27
|
+
invalidate() {
|
|
28
|
+
cached = null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Stringifies the values of the given object.
|
|
34
|
+
*/
|
|
35
|
+
function stringifyValues(obj) {
|
|
36
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, JSON.stringify(value)]));
|
|
37
|
+
}
|
|
38
|
+
function toPublicFilePath(filePath, workspaceRoot) {
|
|
39
|
+
const relativeFilePath = relative(workspaceRoot, filePath);
|
|
40
|
+
if (!relativeFilePath.startsWith("..") && !isAbsolute(relativeFilePath)) return toPublicSpecifier(relativeFilePath);
|
|
41
|
+
return `/@fs/${toPosixPath(filePath)}`;
|
|
42
|
+
}
|
|
43
|
+
function toPublicSpecifier(relativeFilePath) {
|
|
44
|
+
return `/${stripLeadingSlash(toPosixPath(relativeFilePath))}`;
|
|
45
|
+
}
|
|
46
|
+
function toPosixPath(value) {
|
|
47
|
+
return value.replace(/\\/g, "/");
|
|
48
|
+
}
|
|
49
|
+
function stripLeadingSlash(value) {
|
|
50
|
+
return value.startsWith("/") ? value.slice(1) : value;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/manual-test-plugin/collect-pages.ts
|
|
54
|
+
/**
|
|
55
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
56
|
+
* For licensing, see LICENSE.md.
|
|
57
|
+
*/
|
|
58
|
+
function collectManualPages(patterns, workspaceRoot) {
|
|
59
|
+
const groupedFiles = /* @__PURE__ */ new Map();
|
|
60
|
+
const manualPages = [];
|
|
61
|
+
for (const relativeFilePath of matchFiles(patterns, workspaceRoot)) {
|
|
62
|
+
if (relativeFilePath.includes("/_utils/")) continue;
|
|
63
|
+
const parsedPath = parseManualTestAssetPath(relativeFilePath);
|
|
64
|
+
if (!parsedPath) continue;
|
|
65
|
+
const entryKey = `${parsedPath.packageRootPath}/${parsedPath.packageName}/${parsedPath.slug}`;
|
|
66
|
+
const matchedFiles = groupedFiles.get(entryKey) || {};
|
|
67
|
+
matchedFiles[parsedPath.extension] = relativeFilePath;
|
|
68
|
+
groupedFiles.set(entryKey, matchedFiles);
|
|
69
|
+
}
|
|
70
|
+
for (const matchedFiles of groupedFiles.values()) {
|
|
71
|
+
if (!matchedFiles.html) continue;
|
|
72
|
+
const scriptFilePath = matchedFiles.ts || matchedFiles.js;
|
|
73
|
+
if (!scriptFilePath) continue;
|
|
74
|
+
const parsedPath = parseManualTestAssetPath(scriptFilePath);
|
|
75
|
+
manualPages.push({
|
|
76
|
+
displayName: humanizeSlug(parsedPath.slug),
|
|
77
|
+
htmlFilePath: toPublicSpecifier(matchedFiles.html),
|
|
78
|
+
instructionsFilePath: matchedFiles.md ? toPublicSpecifier(matchedFiles.md) : void 0,
|
|
79
|
+
packageName: parsedPath.packageName,
|
|
80
|
+
scriptFilePath: toPublicSpecifier(scriptFilePath),
|
|
81
|
+
slug: parsedPath.slug
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
manualPages.sort((a, b) => a.packageName.localeCompare(b.packageName) || a.slug.localeCompare(b.slug));
|
|
85
|
+
return new Map(manualPages.map((entry) => [entry.htmlFilePath, entry]));
|
|
86
|
+
}
|
|
87
|
+
function parseManualTestAssetPath(filePath) {
|
|
88
|
+
const match = stripLeadingSlash(toPosixPath(filePath)).match(/^(?:(.*?)\/)?([^/]+)\/tests\/manual\/(.+)\.(html|js|md|ts)$/);
|
|
89
|
+
if (!match) return null;
|
|
90
|
+
return {
|
|
91
|
+
extension: match[4],
|
|
92
|
+
packageName: match[2],
|
|
93
|
+
packageRootPath: match[1] || "",
|
|
94
|
+
slug: match[3]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function humanizeSlug(slug) {
|
|
98
|
+
return slug.split("/").map((pathPart) => pathPart.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ")).join(" / ");
|
|
99
|
+
}
|
|
100
|
+
function matchFiles(patterns, workspaceRoot) {
|
|
101
|
+
return patterns.flatMap((pattern) => globSync(pattern, { cwd: workspaceRoot }).map((match) => toPosixPath(match)));
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/manual-test-plugin/static-assets.ts
|
|
105
|
+
/**
|
|
106
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
107
|
+
* For licensing, see LICENSE.md.
|
|
108
|
+
*/
|
|
109
|
+
const PROCESSED_MANUAL_TEST_EXTENSIONS = new Set([
|
|
110
|
+
".css",
|
|
111
|
+
".html",
|
|
112
|
+
".js",
|
|
113
|
+
".md",
|
|
114
|
+
".ts"
|
|
115
|
+
]);
|
|
116
|
+
const VITE_MODULE_QUERY_PARAMETERS = new Set([
|
|
117
|
+
"import",
|
|
118
|
+
"raw",
|
|
119
|
+
"url",
|
|
120
|
+
"worker",
|
|
121
|
+
"inline"
|
|
122
|
+
]);
|
|
123
|
+
function collectManualStaticAssets(patterns, workspaceRoot) {
|
|
124
|
+
return new Map(patterns.flatMap((pattern) => globSync(pattern, { cwd: workspaceRoot })).sort().filter((relativeFilePath) => statSync(resolve(workspaceRoot, relativeFilePath), { throwIfNoEntry: false })?.isFile()).filter(isManualStaticAssetPath).map((relativeFilePath) => [toPublicSpecifier(relativeFilePath), resolve(workspaceRoot, relativeFilePath)]));
|
|
125
|
+
}
|
|
126
|
+
function createManualStaticAssetsMiddleware(collectStaticAssets) {
|
|
127
|
+
return (request, response, next) => {
|
|
128
|
+
if (request.method != "GET" && request.method != "HEAD") {
|
|
129
|
+
next();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const publicPath = getManualStaticAssetPublicPath(request.url);
|
|
133
|
+
const filePath = publicPath && collectStaticAssets().get(publicPath);
|
|
134
|
+
const fileStats = filePath ? statSync(filePath, { throwIfNoEntry: false }) : void 0;
|
|
135
|
+
if (!filePath || !fileStats?.isFile()) {
|
|
136
|
+
next();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
response.statusCode = 200;
|
|
140
|
+
response.setHeader("Content-Length", fileStats.size);
|
|
141
|
+
response.setHeader("Content-Type", getContentType(extname(filePath)));
|
|
142
|
+
if (request.method == "HEAD") {
|
|
143
|
+
response.end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const fileStream = createReadStream(filePath);
|
|
147
|
+
fileStream.on("error", () => response.destroy());
|
|
148
|
+
fileStream.pipe(response);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function getManualStaticAssetPublicPath(requestUrl) {
|
|
152
|
+
const url = URL.parse(requestUrl || "", "http://localhost");
|
|
153
|
+
if (!url) return null;
|
|
154
|
+
if ([...url.searchParams.keys()].some((key) => VITE_MODULE_QUERY_PARAMETERS.has(key))) return null;
|
|
155
|
+
const pathname = decodePathname(url.pathname);
|
|
156
|
+
if (!pathname || !isManualStaticAssetPath(pathname)) return null;
|
|
157
|
+
return pathname;
|
|
158
|
+
}
|
|
159
|
+
function decodePathname(pathname) {
|
|
160
|
+
try {
|
|
161
|
+
return decodeURIComponent(pathname);
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function isManualStaticAssetPath(filePath) {
|
|
167
|
+
return extname(filePath) != "" && !PROCESSED_MANUAL_TEST_EXTENSIONS.has(extname(filePath));
|
|
168
|
+
}
|
|
169
|
+
function getContentType(extension) {
|
|
170
|
+
switch (extension) {
|
|
171
|
+
case ".avif": return "image/avif";
|
|
172
|
+
case ".css": return "text/css; charset=utf-8";
|
|
173
|
+
case ".gif": return "image/gif";
|
|
174
|
+
case ".ico": return "image/x-icon";
|
|
175
|
+
case ".jpg":
|
|
176
|
+
case ".jpeg": return "image/jpeg";
|
|
177
|
+
case ".json":
|
|
178
|
+
case ".map": return "application/json; charset=utf-8";
|
|
179
|
+
case ".mp3": return "audio/mpeg";
|
|
180
|
+
case ".mp4": return "video/mp4";
|
|
181
|
+
case ".png": return "image/png";
|
|
182
|
+
case ".svg": return "image/svg+xml";
|
|
183
|
+
case ".txt": return "text/plain; charset=utf-8";
|
|
184
|
+
case ".webp": return "image/webp";
|
|
185
|
+
case ".woff": return "font/woff";
|
|
186
|
+
case ".woff2": return "font/woff2";
|
|
187
|
+
default: return "application/octet-stream";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/manual-test-plugin/parse-markdown.ts
|
|
192
|
+
/**
|
|
193
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
194
|
+
* For licensing, see LICENSE.md.
|
|
195
|
+
*/
|
|
196
|
+
const markdownProcessor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true });
|
|
197
|
+
function loadRenderedInstructions(entry, workspaceRoot) {
|
|
198
|
+
if (!entry.instructionsFilePath) return "";
|
|
199
|
+
const markdown = readFileSync(resolve(workspaceRoot, entry.instructionsFilePath.slice(1)), "utf8").trim();
|
|
200
|
+
if (!markdown) return "";
|
|
201
|
+
return toHtml(markdownProcessor.runSync(markdownProcessor.parse(markdown)), { allowDangerousHtml: true });
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/manual-test-plugin/shell-html.ts
|
|
205
|
+
/**
|
|
206
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
207
|
+
* For licensing, see LICENSE.md.
|
|
208
|
+
*/
|
|
209
|
+
function createManualShellHtml({ entry, html, shellScriptPublicPath, shellTemplateFilePath, workspaceRoot }) {
|
|
210
|
+
const shellDocument = parse(readFileSync(shellTemplateFilePath, "utf8"));
|
|
211
|
+
const shellHead = getRequiredElementByTagName$1(shellDocument, "head");
|
|
212
|
+
const shellBody = getRequiredElementByTagName$1(shellDocument, "body");
|
|
213
|
+
const shellTitle = getRequiredElementByTagName$1(shellHead, "title");
|
|
214
|
+
const shellDataElement = query(shellBody, (candidate) => {
|
|
215
|
+
return isElementNode(candidate) && getAttribute(candidate, "id") == "manual-shell-data";
|
|
216
|
+
});
|
|
217
|
+
const shellScriptElement = getRequiredElementByAttribute(shellHead, "data-manual-shell-script");
|
|
218
|
+
const manualDocument = parse(html);
|
|
219
|
+
const manualHead = getRequiredElementByTagName$1(manualDocument, "head");
|
|
220
|
+
const manualBody = getRequiredElementByTagName$1(manualDocument, "body");
|
|
221
|
+
const manualTitle = findChildElement(manualHead, "title");
|
|
222
|
+
const testScriptElement = getRequiredElementByAttribute(shellHead, "data-manual-test-script");
|
|
223
|
+
const testScriptFileName = posix.basename(entry.scriptFilePath);
|
|
224
|
+
removeManualTestScriptNodes(manualDocument, testScriptFileName);
|
|
225
|
+
setTextContent(shellTitle, (manualTitle ? getTextContent(manualTitle) : "").trim() || entry.displayName);
|
|
226
|
+
setAttribute(shellScriptElement, "src", shellScriptPublicPath);
|
|
227
|
+
removeAttribute(shellScriptElement, "data-manual-shell-script");
|
|
228
|
+
setAttribute(testScriptElement, "src", `./${testScriptFileName}`);
|
|
229
|
+
removeAttribute(testScriptElement, "data-manual-test-script");
|
|
230
|
+
setTextContent(shellDataElement, stringifyHtmlScriptData(createManualData(entry, workspaceRoot)));
|
|
231
|
+
copyManualBodyAttributes(manualBody, shellBody);
|
|
232
|
+
appendChildren(shellHead, manualHead.childNodes.filter(shouldMoveManualHeadNode));
|
|
233
|
+
appendChild(shellBody, createManualTestContainer(manualBody));
|
|
234
|
+
return serialize(shellDocument);
|
|
235
|
+
}
|
|
236
|
+
function createManualData(entry, workspaceRoot) {
|
|
237
|
+
return {
|
|
238
|
+
displayName: entry.displayName,
|
|
239
|
+
instructionsHtml: loadRenderedInstructions(entry, workspaceRoot),
|
|
240
|
+
packageName: entry.packageName
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function stringifyHtmlScriptData(value) {
|
|
244
|
+
return JSON.stringify(value).split("<").join("\\u003c");
|
|
245
|
+
}
|
|
246
|
+
function copyManualBodyAttributes(manualBody, shellBody) {
|
|
247
|
+
for (const { name, value } of manualBody.attrs) {
|
|
248
|
+
if (name == "class") {
|
|
249
|
+
setAttribute(shellBody, "class", [getAttribute(shellBody, "class"), value].filter(Boolean).join(" "));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!hasAttribute(shellBody, name)) setAttribute(shellBody, name, value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function shouldMoveManualHeadNode(node) {
|
|
256
|
+
if (!isElementNode(node)) return true;
|
|
257
|
+
if (node.tagName == "title") return false;
|
|
258
|
+
if (node.tagName != "meta") return true;
|
|
259
|
+
return !getAttribute(node, "charset") && getAttribute(node, "name") != "viewport";
|
|
260
|
+
}
|
|
261
|
+
function appendChildren(parent, childNodes) {
|
|
262
|
+
for (const node of childNodes) {
|
|
263
|
+
removeNode(node);
|
|
264
|
+
appendChild(parent, node);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function createManualTestContainer(manualBody) {
|
|
268
|
+
const container = createElement("div", { class: "manual-test-container" });
|
|
269
|
+
appendChildren(container, [...manualBody.childNodes]);
|
|
270
|
+
return container;
|
|
271
|
+
}
|
|
272
|
+
function removeManualTestScriptNodes(parent, testScriptFileName) {
|
|
273
|
+
for (const node of [...parent.childNodes]) {
|
|
274
|
+
if (!isElementNode(node)) continue;
|
|
275
|
+
if (isManualTestScriptNode(node, testScriptFileName)) {
|
|
276
|
+
removeNode(node);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
removeManualTestScriptNodes(node, testScriptFileName);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function isManualTestScriptNode(node, testScriptFileName) {
|
|
283
|
+
if (node.tagName != "script") return false;
|
|
284
|
+
const source = getAttribute(node, "src");
|
|
285
|
+
if (!source) return false;
|
|
286
|
+
return getScriptModuleName(posix.basename(source.split("?")[0].split("#")[0])) == getScriptModuleName(testScriptFileName);
|
|
287
|
+
}
|
|
288
|
+
function getScriptModuleName(fileName) {
|
|
289
|
+
return fileName.replace(/\.[jt]s$/, "");
|
|
290
|
+
}
|
|
291
|
+
function getRequiredElementByTagName$1(root, tagName) {
|
|
292
|
+
return query(root, (candidate) => isElementNode(candidate) && candidate.tagName == tagName);
|
|
293
|
+
}
|
|
294
|
+
function getRequiredElementByAttribute(root, attributeName) {
|
|
295
|
+
return query(root, (candidate) => isElementNode(candidate) && hasAttribute(candidate, attributeName));
|
|
296
|
+
}
|
|
297
|
+
function findChildElement(root, tagName) {
|
|
298
|
+
return root.childNodes.find((node) => isElementNode(node) && node.tagName == tagName);
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/manual-test-plugin/plugin.ts
|
|
302
|
+
/**
|
|
303
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
304
|
+
* For licensing, see LICENSE.md.
|
|
305
|
+
*/
|
|
306
|
+
const MANUAL_ENTRIES_VIRTUAL_ID = "virtual:ckeditor5-manual-entries";
|
|
307
|
+
const MANUAL_THEME_ROOT = realpathSync(fileURLToPath(import.meta.resolve("@ckeditor/ckeditor5-dev-manual-server/theme")));
|
|
308
|
+
const MANUAL_CATALOG_FILE_PATH = resolve(MANUAL_THEME_ROOT, "catalog.html");
|
|
309
|
+
const MANUAL_CATALOG_SCRIPT_FILE_PATH = resolve(MANUAL_THEME_ROOT, "catalog.ts");
|
|
310
|
+
const MANUAL_SHELL_TEMPLATE_FILE_PATH = resolve(MANUAL_THEME_ROOT, "shell.html");
|
|
311
|
+
const MANUAL_SHELL_SCRIPT_FILE_PATH = resolve(MANUAL_THEME_ROOT, "shell.ts");
|
|
312
|
+
function manualTestsPlugin(manualTestPatterns) {
|
|
313
|
+
let workspaceRoot = process.cwd();
|
|
314
|
+
const manualPagePatterns = manualTestPatterns.map(toManualPagePattern);
|
|
315
|
+
const manualPagesCache = cacheValue(() => collectManualPages(manualPagePatterns, workspaceRoot));
|
|
316
|
+
const manualStaticAssetsCache = cacheValue(() => collectManualStaticAssets(manualTestPatterns, workspaceRoot));
|
|
317
|
+
const getManualPages = manualPagesCache.get;
|
|
318
|
+
const getManualStaticAssets = manualStaticAssetsCache.get;
|
|
319
|
+
const invalidateManualFileCaches = () => {
|
|
320
|
+
manualPagesCache.invalidate();
|
|
321
|
+
manualStaticAssetsCache.invalidate();
|
|
322
|
+
};
|
|
323
|
+
const getManualPageEntryForFile = (filePath) => {
|
|
324
|
+
return getManualPages().get(toPublicSpecifier(relative(workspaceRoot, filePath)));
|
|
325
|
+
};
|
|
326
|
+
const getManualCatalogPublicPath = () => toPublicFilePath(MANUAL_CATALOG_FILE_PATH, workspaceRoot);
|
|
327
|
+
const getManualCatalogScriptPublicPath = () => toPublicFilePath(MANUAL_CATALOG_SCRIPT_FILE_PATH, workspaceRoot);
|
|
328
|
+
const getManualShellScriptPublicPath = () => toPublicFilePath(MANUAL_SHELL_SCRIPT_FILE_PATH, workspaceRoot);
|
|
329
|
+
const getManualBuildInputs = () => [MANUAL_CATALOG_FILE_PATH, ...[...getManualPages().values()].map((entry) => resolve(workspaceRoot, entry.htmlFilePath.slice(1)))];
|
|
330
|
+
const resolvedVirtualModuleId = `\0${MANUAL_ENTRIES_VIRTUAL_ID}`;
|
|
331
|
+
const getClientEntries = () => [...getManualPages().values()].map((entry) => ({
|
|
332
|
+
displayName: entry.displayName,
|
|
333
|
+
href: entry.htmlFilePath,
|
|
334
|
+
packageName: entry.packageName,
|
|
335
|
+
slug: entry.slug
|
|
336
|
+
}));
|
|
337
|
+
const getManualEntriesJson = () => JSON.stringify(getClientEntries(), null, 2);
|
|
338
|
+
return {
|
|
339
|
+
name: "ckeditor5-manual-tests",
|
|
340
|
+
config() {
|
|
341
|
+
return { build: { rolldownOptions: { input: getManualBuildInputs() } } };
|
|
342
|
+
},
|
|
343
|
+
configResolved(config) {
|
|
344
|
+
workspaceRoot = config.root;
|
|
345
|
+
invalidateManualFileCaches();
|
|
346
|
+
config.build.rolldownOptions.input = getManualBuildInputs();
|
|
347
|
+
},
|
|
348
|
+
configureServer(server) {
|
|
349
|
+
const { memoryFiles } = server.environments.client;
|
|
350
|
+
useManualTestMiddlewares(server, getManualCatalogPublicPath, getManualStaticAssets);
|
|
351
|
+
useBundledDevManualHtmlSource(memoryFiles, getManualPages, getManualShellScriptPublicPath, () => workspaceRoot);
|
|
352
|
+
},
|
|
353
|
+
configurePreviewServer(server) {
|
|
354
|
+
useManualCatalogMiddleware(server, getManualCatalogPublicPath);
|
|
355
|
+
},
|
|
356
|
+
resolveId(source) {
|
|
357
|
+
if (source == MANUAL_ENTRIES_VIRTUAL_ID) return resolvedVirtualModuleId;
|
|
358
|
+
return null;
|
|
359
|
+
},
|
|
360
|
+
load(id) {
|
|
361
|
+
if (id == resolvedVirtualModuleId) return `export const manualTestEntries = ${getManualEntriesJson()};`;
|
|
362
|
+
return null;
|
|
363
|
+
},
|
|
364
|
+
generateBundle() {
|
|
365
|
+
for (const [publicPath, filePath] of getManualStaticAssets()) this.emitFile({
|
|
366
|
+
type: "asset",
|
|
367
|
+
fileName: stripLeadingSlash(publicPath),
|
|
368
|
+
source: readFileSync(filePath)
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
transformIndexHtml: {
|
|
372
|
+
order: "pre",
|
|
373
|
+
handler(html, context) {
|
|
374
|
+
if (toPosixPath(context.filename) == toPosixPath(MANUAL_CATALOG_FILE_PATH)) return html.replace("./catalog.ts", getManualCatalogScriptPublicPath());
|
|
375
|
+
const entry = getManualPageEntryForFile(context.filename);
|
|
376
|
+
if (!entry) return;
|
|
377
|
+
return createManualShellHtml({
|
|
378
|
+
entry,
|
|
379
|
+
html,
|
|
380
|
+
shellScriptPublicPath: getManualShellScriptPublicPath(),
|
|
381
|
+
shellTemplateFilePath: MANUAL_SHELL_TEMPLATE_FILE_PATH,
|
|
382
|
+
workspaceRoot
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function useManualTestMiddlewares(server, getManualCatalogPublicPath, getManualStaticAssets) {
|
|
389
|
+
server.middlewares.use(createManualStaticAssetsMiddleware(getManualStaticAssets));
|
|
390
|
+
useManualCatalogMiddleware(server, getManualCatalogPublicPath);
|
|
391
|
+
}
|
|
392
|
+
function useManualCatalogMiddleware(server, getManualCatalogPublicPath) {
|
|
393
|
+
server.middlewares.use((request, _response, next) => {
|
|
394
|
+
rewriteCatalogRequest(request, getManualCatalogPublicPath());
|
|
395
|
+
next();
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function useBundledDevManualHtmlSource(memoryFiles, getManualPages, getManualShellScriptPublicPath, getWorkspaceRoot) {
|
|
399
|
+
if (!memoryFiles) return;
|
|
400
|
+
const getBundledFile = memoryFiles.get.bind(memoryFiles);
|
|
401
|
+
memoryFiles.get = (filePath) => {
|
|
402
|
+
const file = getBundledFile(filePath);
|
|
403
|
+
const entry = getManualPages().get(toPublicSpecifier(filePath));
|
|
404
|
+
if (!file || !entry) return file;
|
|
405
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
406
|
+
const shellScriptPublicPath = getManualShellScriptPublicPath();
|
|
407
|
+
const bundledHtml = getFileSource(file);
|
|
408
|
+
return { source: mergeBundledAssetTags(createManualShellHtml({
|
|
409
|
+
entry,
|
|
410
|
+
html: readFileSync(resolve(workspaceRoot, stripLeadingSlash(entry.htmlFilePath)), "utf8"),
|
|
411
|
+
shellScriptPublicPath,
|
|
412
|
+
shellTemplateFilePath: MANUAL_SHELL_TEMPLATE_FILE_PATH,
|
|
413
|
+
workspaceRoot
|
|
414
|
+
}), bundledHtml, entry, shellScriptPublicPath) };
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function getFileSource(file) {
|
|
418
|
+
return typeof file.source == "string" ? file.source : Buffer.from(file.source).toString("utf8");
|
|
419
|
+
}
|
|
420
|
+
function toManualPagePattern(pattern) {
|
|
421
|
+
if (/\.(?:html|js|md|ts|\{[^}]*\})$/.test(pattern)) return pattern;
|
|
422
|
+
return `${pattern}.{html,js,md,ts}`;
|
|
423
|
+
}
|
|
424
|
+
function mergeBundledAssetTags(sourceHtml, bundledHtml, entry, shellScriptPublicPath) {
|
|
425
|
+
const sourceDocument = parse(sourceHtml);
|
|
426
|
+
const bundledDocument = parse(bundledHtml);
|
|
427
|
+
const sourceHead = getRequiredElementByTagName(sourceDocument, "head");
|
|
428
|
+
const bundledHead = getRequiredElementByTagName(bundledDocument, "head");
|
|
429
|
+
const testScriptFileName = posix.basename(entry.scriptFilePath);
|
|
430
|
+
const hasBundledShellAsset = bundledHead.childNodes.some((node) => isBundledModuleAsset(node, "shell"));
|
|
431
|
+
for (const node of [...sourceHead.childNodes]) if (isSourceTestScript(node, testScriptFileName) || hasBundledShellAsset && isSourceShellScript(node, shellScriptPublicPath)) removeNode(node);
|
|
432
|
+
for (const node of bundledHead.childNodes.filter(isBundledAssetTag)) {
|
|
433
|
+
removeNode(node);
|
|
434
|
+
appendChild(sourceHead, node);
|
|
435
|
+
}
|
|
436
|
+
return serialize(sourceDocument);
|
|
437
|
+
}
|
|
438
|
+
function isSourceTestScript(node, testScriptFileName) {
|
|
439
|
+
if (!isElementNode(node) || node.tagName != "script") return false;
|
|
440
|
+
const source = getAttribute(node, "src");
|
|
441
|
+
if (!source) return false;
|
|
442
|
+
return posix.basename(source) == testScriptFileName;
|
|
443
|
+
}
|
|
444
|
+
function isSourceShellScript(node, shellScriptPublicPath) {
|
|
445
|
+
return isElementNode(node) && node.tagName == "script" && getAttribute(node, "src") == shellScriptPublicPath;
|
|
446
|
+
}
|
|
447
|
+
function isBundledModuleAsset(node, moduleName) {
|
|
448
|
+
if (!isElementNode(node)) return false;
|
|
449
|
+
if (node.tagName == "script") return isBundledModulePath(getAttribute(node, "src"), moduleName);
|
|
450
|
+
return node.tagName == "link" && getAttribute(node, "rel") == "modulepreload" && isBundledModulePath(getAttribute(node, "href"), moduleName);
|
|
451
|
+
}
|
|
452
|
+
function isBundledModulePath(path, moduleName) {
|
|
453
|
+
return Boolean(path?.startsWith(`/assets/${moduleName}-`));
|
|
454
|
+
}
|
|
455
|
+
function isBundledAssetTag(node) {
|
|
456
|
+
if (!isElementNode(node)) return false;
|
|
457
|
+
return Boolean(node.tagName == "script" && getAttribute(node, "src")?.startsWith("/assets/") || node.tagName == "link" && getAttribute(node, "href")?.startsWith("/assets/"));
|
|
458
|
+
}
|
|
459
|
+
function rewriteCatalogRequest(request, manualCatalogPublicPath) {
|
|
460
|
+
const url = parseRequestUrl(request.url);
|
|
461
|
+
if (!url) return;
|
|
462
|
+
if (url.pathname == "/" || url.pathname == "/index.html") request.url = `${manualCatalogPublicPath}${url.search}`;
|
|
463
|
+
}
|
|
464
|
+
function parseRequestUrl(requestUrl) {
|
|
465
|
+
return URL.parse(requestUrl || "", "http://localhost");
|
|
466
|
+
}
|
|
467
|
+
function getRequiredElementByTagName(root, tagName) {
|
|
468
|
+
return query(root, (candidate) => isElementNode(candidate) && candidate.tagName == tagName);
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/refresh-plugin/plugin.ts
|
|
472
|
+
/**
|
|
473
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
474
|
+
* For licensing, see LICENSE.md.
|
|
475
|
+
*/
|
|
476
|
+
const MANUAL_REFRESH_EVENT_NAME = "ckeditor5-manual:refresh-available";
|
|
477
|
+
const isManualRefreshWrappedClient = Symbol("isManualRefreshWrappedClient");
|
|
478
|
+
function refreshPlugin() {
|
|
479
|
+
return {
|
|
480
|
+
name: "ckeditor5-manual-refresh",
|
|
481
|
+
apply: "serve",
|
|
482
|
+
configureServer(server) {
|
|
483
|
+
const clientEnvironment = server.environments.client;
|
|
484
|
+
wrapBundledDevClientSend(clientEnvironment.clients);
|
|
485
|
+
wrapBundledDevFullReloads(clientEnvironment);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function wrapBundledDevClientSend(clients) {
|
|
490
|
+
if (!clients) return;
|
|
491
|
+
const setupIfNeeded = clients.setupIfNeeded.bind(clients);
|
|
492
|
+
clients.setupIfNeeded = (client, clientId) => {
|
|
493
|
+
if (!client[isManualRefreshWrappedClient]) {
|
|
494
|
+
const send = client.send.bind(client);
|
|
495
|
+
client.send = (payload) => {
|
|
496
|
+
sendManualRefreshPayload(payload, send);
|
|
497
|
+
};
|
|
498
|
+
client[isManualRefreshWrappedClient] = true;
|
|
499
|
+
}
|
|
500
|
+
return setupIfNeeded(client, clientId);
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function wrapBundledDevFullReloads(clientEnvironment) {
|
|
504
|
+
if (!clientEnvironment.handleHmrOutput) return;
|
|
505
|
+
const handleHmrOutput = clientEnvironment.handleHmrOutput.bind(clientEnvironment);
|
|
506
|
+
clientEnvironment.handleHmrOutput = (client, files, hmrOutput, invalidateInformation) => {
|
|
507
|
+
if (hmrOutput.type == "FullReload" && shouldShowManualRefreshPromptForFiles(files)) {
|
|
508
|
+
clientEnvironment.devEngine?.ensureLatestBuildOutput().catch(() => {});
|
|
509
|
+
client.send({
|
|
510
|
+
type: "custom",
|
|
511
|
+
event: MANUAL_REFRESH_EVENT_NAME
|
|
512
|
+
});
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
return handleHmrOutput(client, files, hmrOutput, invalidateInformation);
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function sendManualRefreshPayload(payload, send) {
|
|
519
|
+
if (shouldShowManualRefreshPrompt(payload)) {
|
|
520
|
+
send({
|
|
521
|
+
type: "custom",
|
|
522
|
+
event: MANUAL_REFRESH_EVENT_NAME
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
send(payload);
|
|
527
|
+
}
|
|
528
|
+
function shouldShowManualRefreshPrompt(payload) {
|
|
529
|
+
if (payload.type == "update") return !isCssUpdatePayload(payload);
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
function isCssUpdatePayload(payload) {
|
|
533
|
+
return payload.type == "update" && payload.updates.length > 0 && payload.updates.every((update) => {
|
|
534
|
+
return update.type == "css-update" || update.type == "js-update" && update.acceptedPath.endsWith(".css");
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function shouldShowManualRefreshPromptForFiles(files) {
|
|
538
|
+
return files.some((file) => !file.endsWith(".css") && !file.endsWith(".html"));
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/raw-plugin/plugin.ts
|
|
542
|
+
/**
|
|
543
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
544
|
+
* For licensing, see LICENSE.md.
|
|
545
|
+
*/
|
|
546
|
+
const RAW_QUERY = "?ckeditor5-raw";
|
|
547
|
+
function rawHtmlPlugin() {
|
|
548
|
+
return {
|
|
549
|
+
name: "ckeditor5-raw",
|
|
550
|
+
enforce: "pre",
|
|
551
|
+
resolveId(source, importer) {
|
|
552
|
+
if (!importer || source.includes("?")) return null;
|
|
553
|
+
if (path.extname(source) != ".html") return null;
|
|
554
|
+
const filePath = resolveHtmlFilePath(source, importer);
|
|
555
|
+
if (filePath) return `${filePath}${RAW_QUERY}`;
|
|
556
|
+
return null;
|
|
557
|
+
},
|
|
558
|
+
async load(id) {
|
|
559
|
+
if (!id.endsWith(RAW_QUERY)) return null;
|
|
560
|
+
const source = await readFile(id.slice(0, -14), "utf8");
|
|
561
|
+
return {
|
|
562
|
+
code: `export default ${JSON.stringify(source)};`,
|
|
563
|
+
map: null
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function resolveHtmlFilePath(source, importer) {
|
|
569
|
+
if (!source.startsWith(".")) return null;
|
|
570
|
+
const filePath = path.resolve(path.dirname(getFilePathFromId(importer)), source);
|
|
571
|
+
return existsSync(filePath) ? toPosixPath(filePath) : null;
|
|
572
|
+
}
|
|
573
|
+
function getFilePathFromId(id) {
|
|
574
|
+
const queryIndex = id.indexOf("?");
|
|
575
|
+
return queryIndex >= 0 ? id.slice(0, queryIndex) : id;
|
|
576
|
+
}
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/index.ts
|
|
579
|
+
/**
|
|
580
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
581
|
+
* For licensing, see LICENSE.md.
|
|
582
|
+
*/
|
|
583
|
+
//#endregion
|
|
584
|
+
export { manualTestsPlugin, rawHtmlPlugin, refreshPlugin, stringifyValues };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
import type { ManualPageEntry } from "./types.js";
|
|
6
|
+
export declare function collectManualPages(patterns: Array<string>, workspaceRoot: string): Map<string, ManualPageEntry>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
import type { ManualPageEntry } from "./types.js";
|
|
6
|
+
export declare function loadRenderedInstructions(entry: ManualPageEntry, workspaceRoot: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
import type { Plugin } from "vite";
|
|
6
|
+
export type { ManualData } from "./types.js";
|
|
7
|
+
export declare function manualTestsPlugin(manualTestPatterns: Array<string>): Plugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
import type { ManualPageEntry } from "./types.js";
|
|
6
|
+
export interface CreateManualShellHtmlOptions {
|
|
7
|
+
entry: ManualPageEntry;
|
|
8
|
+
html: string;
|
|
9
|
+
shellScriptPublicPath: string;
|
|
10
|
+
shellTemplateFilePath: string;
|
|
11
|
+
workspaceRoot: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function createManualShellHtml({ entry, html, shellScriptPublicPath, shellTemplateFilePath, workspaceRoot }: CreateManualShellHtmlOptions): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
+
type ManualStaticAssetsMiddleware = (request: IncomingMessage, response: ServerResponse, next: () => void) => void;
|
|
7
|
+
export declare function collectManualStaticAssets(patterns: Array<string>, workspaceRoot: string): Map<string, string>;
|
|
8
|
+
export declare function createManualStaticAssetsMiddleware(collectStaticAssets: () => Map<string, string>): ManualStaticAssetsMiddleware;
|
|
9
|
+
export declare function getManualStaticAssetFilePath(requestUrl: string | undefined, staticAssets: Map<string, string>): string | null;
|
|
10
|
+
export {};
|