text-runner-engine 7.1.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/README.md +36 -0
- package/dist/actions/actions.d.ts +11 -0
- package/dist/actions/actions.js +36 -0
- package/dist/actions/built-in/check-image.d.ts +4 -0
- package/dist/actions/built-in/check-image.js +59 -0
- package/dist/actions/built-in/check-link.d.ts +4 -0
- package/dist/actions/built-in/check-link.js +127 -0
- package/dist/actions/built-in/test.d.ts +3 -0
- package/dist/actions/built-in/test.js +4 -0
- package/dist/actions/export.d.ts +18 -0
- package/dist/actions/export.js +2 -0
- package/dist/actions/external-action-manager.d.ts +10 -0
- package/dist/actions/external-action-manager.js +44 -0
- package/dist/actions/finder.d.ts +29 -0
- package/dist/actions/finder.js +99 -0
- package/dist/actions/index.d.ts +37 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/name.d.ts +2 -0
- package/dist/actions/name.js +6 -0
- package/dist/activities/extract-dynamic.d.ts +5 -0
- package/dist/activities/extract-dynamic.js +29 -0
- package/dist/activities/extract-images-and-links.d.ts +5 -0
- package/dist/activities/extract-images-and-links.js +32 -0
- package/dist/activities/index.d.ts +20 -0
- package/dist/activities/index.js +14 -0
- package/dist/activities/normalize-action-name.d.ts +3 -0
- package/dist/activities/normalize-action-name.js +13 -0
- package/dist/ast/index.d.ts +3 -0
- package/dist/ast/index.js +3 -0
- package/dist/ast/node-list.d.ts +41 -0
- package/dist/ast/node-list.js +128 -0
- package/dist/ast/node.d.ts +49 -0
- package/dist/ast/node.js +53 -0
- package/dist/commands/command.d.ts +9 -0
- package/dist/commands/command.js +2 -0
- package/dist/commands/debug.d.ts +16 -0
- package/dist/commands/debug.js +122 -0
- package/dist/commands/dynamic.d.ts +13 -0
- package/dist/commands/dynamic.js +57 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/run.d.ts +14 -0
- package/dist/commands/run.js +66 -0
- package/dist/commands/static.d.ts +13 -0
- package/dist/commands/static.js +60 -0
- package/dist/commands/unused.d.ts +13 -0
- package/dist/commands/unused.js +43 -0
- package/dist/configuration/data.d.ts +46 -0
- package/dist/configuration/data.js +2 -0
- package/dist/configuration/defaults.d.ts +5 -0
- package/dist/configuration/defaults.js +50 -0
- package/dist/configuration/index.d.ts +5 -0
- package/dist/configuration/index.js +4 -0
- package/dist/configuration/publication.d.ts +42 -0
- package/dist/configuration/publication.js +51 -0
- package/dist/configuration/publications.d.ts +9 -0
- package/dist/configuration/publications.js +25 -0
- package/dist/errors/error.d.ts +2 -0
- package/dist/errors/error.js +9 -0
- package/dist/errors/node-error.d.ts +3 -0
- package/dist/errors/node-error.js +5 -0
- package/dist/errors/user-error.d.ts +13 -0
- package/dist/errors/user-error.js +16 -0
- package/dist/events/index.d.ts +49 -0
- package/dist/events/index.js +13 -0
- package/dist/filesystem/absolute-dir.d.ts +25 -0
- package/dist/filesystem/absolute-dir.js +42 -0
- package/dist/filesystem/absolute-file.d.ts +18 -0
- package/dist/filesystem/absolute-file.js +28 -0
- package/dist/filesystem/full-dir.d.ts +11 -0
- package/dist/filesystem/full-dir.js +15 -0
- package/dist/filesystem/full-file.d.ts +22 -0
- package/dist/filesystem/full-file.js +35 -0
- package/dist/filesystem/full-link.d.ts +40 -0
- package/dist/filesystem/full-link.js +80 -0
- package/dist/filesystem/full-path.d.ts +31 -0
- package/dist/filesystem/full-path.js +61 -0
- package/dist/filesystem/get-filenames.d.ts +15 -0
- package/dist/filesystem/get-filenames.js +73 -0
- package/dist/filesystem/has-directory.d.ts +2 -0
- package/dist/filesystem/has-directory.js +11 -0
- package/dist/filesystem/index.d.ts +15 -0
- package/dist/filesystem/index.js +15 -0
- package/dist/filesystem/is-markdown-file.d.ts +2 -0
- package/dist/filesystem/is-markdown-file.js +11 -0
- package/dist/filesystem/location.d.ts +17 -0
- package/dist/filesystem/location.js +26 -0
- package/dist/filesystem/relative-dir.d.ts +6 -0
- package/dist/filesystem/relative-dir.js +7 -0
- package/dist/filesystem/relative-link.d.ts +16 -0
- package/dist/filesystem/relative-link.js +18 -0
- package/dist/filesystem/source-dir.d.ts +19 -0
- package/dist/filesystem/source-dir.js +35 -0
- package/dist/filesystem/unknown-link.d.ts +15 -0
- package/dist/filesystem/unknown-link.js +23 -0
- package/dist/helpers/add-leading-dot-unless-empty.d.ts +2 -0
- package/dist/helpers/add-leading-dot-unless-empty.js +10 -0
- package/dist/helpers/add-leading-slash.d.ts +2 -0
- package/dist/helpers/add-leading-slash.js +9 -0
- package/dist/helpers/add-trailing-slash.d.ts +2 -0
- package/dist/helpers/add-trailing-slash.js +7 -0
- package/dist/helpers/index.d.ts +16 -0
- package/dist/helpers/index.js +16 -0
- package/dist/helpers/is-external-link.d.ts +2 -0
- package/dist/helpers/is-external-link.js +5 -0
- package/dist/helpers/is-link-to-anchor-in-other-file.d.ts +2 -0
- package/dist/helpers/is-link-to-anchor-in-other-file.js +4 -0
- package/dist/helpers/is-link-to-anchor-in-same-file.d.ts +2 -0
- package/dist/helpers/is-link-to-anchor-in-same-file.js +4 -0
- package/dist/helpers/is-mailto-link.d.ts +2 -0
- package/dist/helpers/is-mailto-link.js +4 -0
- package/dist/helpers/remove-double-slash.d.ts +3 -0
- package/dist/helpers/remove-double-slash.js +6 -0
- package/dist/helpers/remove-leading-slash.d.ts +2 -0
- package/dist/helpers/remove-leading-slash.js +7 -0
- package/dist/helpers/remove-trailing-colon.d.ts +2 -0
- package/dist/helpers/remove-trailing-colon.js +9 -0
- package/dist/helpers/straighten-link.d.ts +3 -0
- package/dist/helpers/straighten-link.js +15 -0
- package/dist/helpers/trim-all-line-ends.d.ts +3 -0
- package/dist/helpers/trim-all-line-ends.js +5 -0
- package/dist/helpers/trim-extension.d.ts +2 -0
- package/dist/helpers/trim-extension.js +5 -0
- package/dist/helpers/unixify.d.ts +3 -0
- package/dist/helpers/unixify.js +6 -0
- package/dist/link-targets/find.d.ts +4 -0
- package/dist/link-targets/find.js +9 -0
- package/dist/link-targets/index.d.ts +15 -0
- package/dist/link-targets/index.js +3 -0
- package/dist/link-targets/list.d.ts +18 -0
- package/dist/link-targets/list.js +74 -0
- package/dist/link-targets/target-url.d.ts +2 -0
- package/dist/link-targets/target-url.js +8 -0
- package/dist/parsers/html/html-parser.d.ts +28 -0
- package/dist/parsers/html/html-parser.js +153 -0
- package/dist/parsers/html/index.d.ts +2 -0
- package/dist/parsers/html/index.js +2 -0
- package/dist/parsers/html/parse-html-files.d.ts +6 -0
- package/dist/parsers/html/parse-html-files.js +16 -0
- package/dist/parsers/index.d.ts +3 -0
- package/dist/parsers/index.js +3 -0
- package/dist/parsers/markdown/closing-tag-parser.d.ts +13 -0
- package/dist/parsers/markdown/closing-tag-parser.js +29 -0
- package/dist/parsers/markdown/index.d.ts +3 -0
- package/dist/parsers/markdown/index.js +3 -0
- package/dist/parsers/markdown/md-parser.d.ts +50 -0
- package/dist/parsers/markdown/md-parser.js +316 -0
- package/dist/parsers/markdown/open-node-tracker.d.ts +14 -0
- package/dist/parsers/markdown/open-node-tracker.js +33 -0
- package/dist/parsers/markdown/parse.d.ts +5 -0
- package/dist/parsers/markdown/parse.js +15 -0
- package/dist/parsers/tag-mapper.d.ts +31 -0
- package/dist/parsers/tag-mapper.js +112 -0
- package/dist/run/activity-collector.d.ts +29 -0
- package/dist/run/activity-collector.js +35 -0
- package/dist/run/index.d.ts +9 -0
- package/dist/run/index.js +5 -0
- package/dist/run/name-refiner.d.ts +13 -0
- package/dist/run/name-refiner.js +20 -0
- package/dist/run/output-collector.d.ts +13 -0
- package/dist/run/output-collector.js +30 -0
- package/dist/run/parallel.d.ts +11 -0
- package/dist/run/parallel.js +13 -0
- package/dist/run/run-activity.d.ts +8 -0
- package/dist/run/run-activity.js +76 -0
- package/dist/run/sequential.d.ts +7 -0
- package/dist/run/sequential.js +10 -0
- package/dist/run/stopwatch.d.ts +12 -0
- package/dist/run/stopwatch.js +19 -0
- package/dist/text-runner.d.ts +15 -0
- package/dist/text-runner.js +15 -0
- package/dist/workspace/create.d.ts +4 -0
- package/dist/workspace/create.js +10 -0
- package/dist/workspace/index.d.ts +2 -0
- package/dist/workspace/index.js +2 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!-- logo is from: https://icons8.com/icon/40886/test -->
|
|
2
|
+
<img src="../documentation/logo2.png" align="right" valign="bottom">
|
|
3
|
+
|
|
4
|
+
# Text-Runner
|
|
5
|
+
|
|
6
|
+
> Test runner for text
|
|
7
|
+
|
|
8
|
+
Text-Runner makes rich text executable. An example is the documentation you are
|
|
9
|
+
reading right now. Text-Runner executes certain parts of it to verify that it
|
|
10
|
+
correctly describes how Text-Runner works. Text-Runner allows you to
|
|
11
|
+
|
|
12
|
+
- test your natural-language documentation
|
|
13
|
+
- make your end-to-end tests completely human-readable prose
|
|
14
|
+
- write computer programs in natural language
|
|
15
|
+
|
|
16
|
+
There are no limitations on the type of text that TextRunner can execute. It can
|
|
17
|
+
read and understand (if you tell it how) any form of plain text in any human
|
|
18
|
+
language as well as complex data in tables, bullet point lists, or embedded
|
|
19
|
+
images. This can be useful for things like
|
|
20
|
+
|
|
21
|
+
- **agile documentation:** your documentation always matches the behavior of the
|
|
22
|
+
product it describes
|
|
23
|
+
- **[semantic versioning](http://semver.org):** know whether a product change
|
|
24
|
+
affects documented behavior
|
|
25
|
+
- **[readme-driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html):**
|
|
26
|
+
documentation becomes your product prototype and drives the implementation
|
|
27
|
+
|
|
28
|
+
More information about:
|
|
29
|
+
|
|
30
|
+
- [how it works](../documentation/how-it-works.md)
|
|
31
|
+
- [installation](../documentation/installation.md)
|
|
32
|
+
- [built-in actions](../documentation/built-in-actions.md)
|
|
33
|
+
- [user-defined actions](../documentation/user-defined-actions.md)
|
|
34
|
+
- [external actions](../documentation/external-actions.md)
|
|
35
|
+
- [questions and answers](../documentation/qna.md)
|
|
36
|
+
- [related tools](../documentation/related-tools.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Action, FunctionRepo } from "./index.js";
|
|
2
|
+
export declare class Actions {
|
|
3
|
+
list: FunctionRepo;
|
|
4
|
+
constructor();
|
|
5
|
+
get(name: string): Action | undefined;
|
|
6
|
+
/** provides the names of all registered actions */
|
|
7
|
+
names(): string[];
|
|
8
|
+
register(fileBasedName: string, action: any): void;
|
|
9
|
+
size(): number;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import slugify from "@sindresorhus/slugify";
|
|
2
|
+
export class Actions {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.list = {};
|
|
5
|
+
}
|
|
6
|
+
get(name) {
|
|
7
|
+
return this.list[name];
|
|
8
|
+
}
|
|
9
|
+
/** provides the names of all registered actions */
|
|
10
|
+
names() {
|
|
11
|
+
return Object.keys(this.list).sort();
|
|
12
|
+
}
|
|
13
|
+
register(fileBasedName, action) {
|
|
14
|
+
if (typeof action === "function") {
|
|
15
|
+
this.list[fileBasedName] = action;
|
|
16
|
+
}
|
|
17
|
+
else if (typeof action.default === "function") {
|
|
18
|
+
this.list[fileBasedName] = action.default;
|
|
19
|
+
}
|
|
20
|
+
else if (typeof action === "object") {
|
|
21
|
+
for (const key of Object.keys(action)) {
|
|
22
|
+
if (typeof key !== "string") {
|
|
23
|
+
throw new Error(`Key "${key}" has unexpected type "${typeof key}", expected string`);
|
|
24
|
+
}
|
|
25
|
+
this.list[slugify(key)] = action[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw new Error(`Cannot use object of type "${typeof action}" as an exported action: ${action}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
size() {
|
|
33
|
+
return Object.keys(this.list).length;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import got, { HTTPError, TimeoutError } from "got";
|
|
3
|
+
import * as files from "../../filesystem/index.js";
|
|
4
|
+
/** The "checkImage" action checks for broken images. */
|
|
5
|
+
export async function checkImage(action) {
|
|
6
|
+
const node = action.region[0];
|
|
7
|
+
const imagePath = node.attributes.src;
|
|
8
|
+
if (!imagePath) {
|
|
9
|
+
throw new Error("image tag without source");
|
|
10
|
+
}
|
|
11
|
+
action.name(`image ${imagePath}`);
|
|
12
|
+
if (isRemoteImage(imagePath)) {
|
|
13
|
+
const result = await checkRemoteImage(imagePath, action);
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// local image here
|
|
18
|
+
const imageFullFile = new files.FullFilePath(imagePath.startsWith("/") ? imagePath : node.location.file.directory().joinStr(imagePath));
|
|
19
|
+
const result = await checkLocalImage(imageFullFile, action.configuration);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function checkLocalImage(image, c) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.stat(c.sourceDir.joinFullFile(image).platformified());
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
throw new Error(`image ${image.unixified()} does not exist`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function checkRemoteImage(url, action) {
|
|
32
|
+
if (!action.configuration.online) {
|
|
33
|
+
return action.SKIPPING;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await got(url, { timeout: { request: 2000 } });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err instanceof HTTPError && err.response.statusCode === 404) {
|
|
40
|
+
action.log(`image ${url} does not exist`);
|
|
41
|
+
}
|
|
42
|
+
else if (err instanceof TimeoutError) {
|
|
43
|
+
action.log(`image ${url} timed out`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
function isRemoteImage(imagePath) {
|
|
52
|
+
if (imagePath != null) {
|
|
53
|
+
return imagePath.startsWith("//") || imagePath.startsWith("http://") || imagePath.startsWith("https://");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=check-image.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import got, { HTTPError, TimeoutError } from "got";
|
|
3
|
+
import { errorMessage } from "../../errors/error.js";
|
|
4
|
+
import { UserError } from "../../errors/user-error.js";
|
|
5
|
+
import * as files from "../../filesystem/index.js";
|
|
6
|
+
import * as helpers from "../../helpers/index.js";
|
|
7
|
+
/** The "checkLink" action checks for broken hyperlinks. */
|
|
8
|
+
export async function checkLink(action) {
|
|
9
|
+
const target = action.region.nodeOfTypes("link_open").attributes.href;
|
|
10
|
+
if (target == null || target === "") {
|
|
11
|
+
throw new Error("link without target");
|
|
12
|
+
}
|
|
13
|
+
for (const ignoreLinkTarget of action.configuration.ignoreLinkTargets) {
|
|
14
|
+
if (ignoreLinkTarget.test(target)) {
|
|
15
|
+
return action.SKIPPING;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
action.name(`link to ${target}`);
|
|
19
|
+
if (helpers.isMailtoLink(target)) {
|
|
20
|
+
return action.SKIPPING;
|
|
21
|
+
}
|
|
22
|
+
if (helpers.isLinkToAnchorInSameFile(target)) {
|
|
23
|
+
const result = checkLinkToAnchorInSameFile(action.location.file, target, action);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
if (helpers.isLinkToAnchorInOtherFile(target)) {
|
|
27
|
+
const result = checkLinkToAnchorInOtherFile(action.location, target, action);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
if (helpers.isExternalLink(target)) {
|
|
31
|
+
const result = await checkExternalLink(target, action);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
await checkLinkToFilesystem(target, action);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
async function checkExternalLink(target, action) {
|
|
38
|
+
if (!action.configuration.online) {
|
|
39
|
+
return action.SKIPPING;
|
|
40
|
+
}
|
|
41
|
+
action.name(`link to external website ${target}`);
|
|
42
|
+
try {
|
|
43
|
+
await got(target, { timeout: { request: 2000 } });
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e instanceof HTTPError && e.response.statusCode === 404) {
|
|
47
|
+
throw new Error(`external website doesn't exist: ${target}`);
|
|
48
|
+
}
|
|
49
|
+
else if (e instanceof TimeoutError) {
|
|
50
|
+
action.log("timed out");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
action.log(`error while checking link to ${target}: ${errorMessage(e)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
function checkLinkToAnchorInOtherFile(containingLocation, target, action) {
|
|
59
|
+
const link = new files.UnknownLink(target);
|
|
60
|
+
const absoluteLink = link.absolutify(containingLocation, action.configuration.publications);
|
|
61
|
+
const anchorName = absoluteLink.anchor();
|
|
62
|
+
const fullPath = absoluteLink.localize(action.configuration.publications, action.configuration.defaultFile);
|
|
63
|
+
try {
|
|
64
|
+
var fullFile = fullPath.toFullFilePath();
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
throw new Error(`link to non-existing file ${fullPath.unixified()}`);
|
|
68
|
+
}
|
|
69
|
+
if (!action.linkTargets.hasFile(fullFile)) {
|
|
70
|
+
throw new Error(`link to anchor #${anchorName} in non-existing file ${helpers.removeLeadingSlash(fullFile.unixified())}`);
|
|
71
|
+
}
|
|
72
|
+
if (!action.linkTargets.hasAnchor(fullFile, anchorName)) {
|
|
73
|
+
throw new UserError(`link to non-existing anchor ${"#" + anchorName} in ${fullFile.unixified()}`, `File ${fullFile.unixified()} contains these anchors: ${action.linkTargets
|
|
74
|
+
.getAnchors(fullFile)
|
|
75
|
+
.map(anchor => `#${anchor}`)
|
|
76
|
+
.join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
if (action.linkTargets.anchorType(fullFile, anchorName) === "heading") {
|
|
79
|
+
action.name(`link to heading ${fullFile.unixified() + "#" + anchorName}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
action.name(`link to ${fullFile.unixified()}#${anchorName}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function checkLinkToAnchorInSameFile(file, target, action) {
|
|
86
|
+
const anchorName = target.substr(1);
|
|
87
|
+
if (!action.linkTargets.hasAnchor(file, anchorName)) {
|
|
88
|
+
throw new UserError(`link to non-existing local anchor ${target}`, `These local anchors exist: ${action.linkTargets
|
|
89
|
+
.getAnchors(file)
|
|
90
|
+
.map(anchor => `#${anchor}`)
|
|
91
|
+
.join(", ")}`, action.location);
|
|
92
|
+
}
|
|
93
|
+
if (action.linkTargets.anchorType(file, anchorName) === "heading") {
|
|
94
|
+
action.name(`link to local heading ${target}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
action.name(`link to #${anchorName}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function checkLinkToFilesystem(target, action) {
|
|
101
|
+
const unknownLink = new files.UnknownLink(decodeURI(target));
|
|
102
|
+
const absoluteLink = unknownLink.absolutify(action.location, action.configuration.publications);
|
|
103
|
+
const linkedFile = absoluteLink.localize(action.configuration.publications, action.configuration.defaultFile);
|
|
104
|
+
const fullPath = action.configuration.sourceDir.joinStr(linkedFile.unixified());
|
|
105
|
+
// We only check for directories if no defaultFile is set.
|
|
106
|
+
// Otherwise links to folders point to the default file.
|
|
107
|
+
if (!action.configuration.defaultFile) {
|
|
108
|
+
try {
|
|
109
|
+
const stats = await fs.stat(fullPath);
|
|
110
|
+
if (stats.isDirectory()) {
|
|
111
|
+
action.name(`link to local directory ${linkedFile.unixified()}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
// we can ignore errors here since we keep checking the file below
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
action.name(`link to local file ${linkedFile.unixified()}`);
|
|
120
|
+
try {
|
|
121
|
+
await fs.stat(fullPath);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
throw new Error(`link to non-existing local file ${linkedFile.unixified()}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=check-link.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as actions from "./index.js";
|
|
2
|
+
export type Action = CbAction | PromiseAction | SyncAction;
|
|
3
|
+
export type CbAction = (action: actions.Args, done: DoneFunction) => void;
|
|
4
|
+
/** continuous-passing-style callback function */
|
|
5
|
+
export type DoneFunction = (err?: Error) => void;
|
|
6
|
+
/** expected file structure of "index.js" files exporting Text-Runner actions */
|
|
7
|
+
export interface IndexFile {
|
|
8
|
+
textrunActions: TextrunActions;
|
|
9
|
+
}
|
|
10
|
+
/** elements of "package.json" files used here */
|
|
11
|
+
export interface PackageJson {
|
|
12
|
+
exports: string;
|
|
13
|
+
}
|
|
14
|
+
export type PromiseAction = (action: actions.Args) => Promise<void>;
|
|
15
|
+
export type SyncAction = (action: actions.Args) => void;
|
|
16
|
+
/** data format of exported actions by npm modules */
|
|
17
|
+
export type TextrunActions = Record<string, Action>;
|
|
18
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Activity } from "../activities/index.js";
|
|
2
|
+
import { Action, FunctionRepo } from "./index.js";
|
|
3
|
+
/** ExternalActionManager provides external actions from their own NPM modules */
|
|
4
|
+
export declare class ExternalActionManager {
|
|
5
|
+
actions: FunctionRepo;
|
|
6
|
+
constructor();
|
|
7
|
+
/** provides the external action with the given name */
|
|
8
|
+
get(activity: Activity): Promise<Action | null>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=external-action-manager.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as actions from "../actions/index.js";
|
|
2
|
+
import { UserError } from "../errors/user-error.js";
|
|
3
|
+
/** ExternalActionManager provides external actions from their own NPM modules */
|
|
4
|
+
export class ExternalActionManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.actions = {};
|
|
7
|
+
}
|
|
8
|
+
/** provides the external action with the given name */
|
|
9
|
+
async get(activity) {
|
|
10
|
+
const parts = activity.actionName.split("/");
|
|
11
|
+
if (parts.length === 1) {
|
|
12
|
+
// not an external action here --> ignore
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (parts.length > 2) {
|
|
16
|
+
throw new UserError(`Too many slashes in action name "${activity.actionName}"`, "Activities are only allowed to have one slash in them: to separate the npm module name from the action name", activity.location);
|
|
17
|
+
}
|
|
18
|
+
const moduleName = "textrun-" + parts[0];
|
|
19
|
+
const wantAction = actions.name(parts[1]);
|
|
20
|
+
try {
|
|
21
|
+
var module = await import(moduleName);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (!(e instanceof Error)) {
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
throw new UserError(`cannot load npm package "${moduleName}"`, e.message, activity.location);
|
|
28
|
+
}
|
|
29
|
+
const moduleActions = module.textrunActions;
|
|
30
|
+
if (!moduleActions) {
|
|
31
|
+
throw new UserError(`npm package "${moduleName}" does not contain any Text-Runner actions`, "", activity.location);
|
|
32
|
+
}
|
|
33
|
+
const names = [];
|
|
34
|
+
for (const [rawName, action] of Object.entries(moduleActions)) {
|
|
35
|
+
const name = actions.name(rawName);
|
|
36
|
+
if (name === wantAction) {
|
|
37
|
+
return action;
|
|
38
|
+
}
|
|
39
|
+
names.push(name);
|
|
40
|
+
}
|
|
41
|
+
throw new UserError(`npm package "${moduleName}" does not contain action "${wantAction}"`, `Found actions: ${names.join(", ")}`, activity.location);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=external-action-manager.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as activities from "../activities/index.js";
|
|
2
|
+
import * as files from "../filesystem/index.js";
|
|
3
|
+
import { Actions } from "./actions.js";
|
|
4
|
+
import { ExternalActionManager } from "./external-action-manager.js";
|
|
5
|
+
import { Action } from "./index.js";
|
|
6
|
+
/** ActionFinder provides runnable action instances for activities. */
|
|
7
|
+
export declare class Finder {
|
|
8
|
+
private readonly builtinActions;
|
|
9
|
+
private readonly customActions;
|
|
10
|
+
private readonly externalActions;
|
|
11
|
+
constructor(builtIn: Actions, custom: Actions, external: ExternalActionManager);
|
|
12
|
+
/** loads all actions */
|
|
13
|
+
static load(sourceDir: files.SourceDir): Promise<Finder>;
|
|
14
|
+
/** loads only the actions for dynamic tests */
|
|
15
|
+
static loadDynamic(sourceDir: files.SourceDir): Promise<Finder>;
|
|
16
|
+
/** loads only the actions for static tests */
|
|
17
|
+
static loadStatic(): Promise<Finder>;
|
|
18
|
+
/** actionFor provides the action function for the given Activity. */
|
|
19
|
+
actionFor(activity: activities.Activity): Promise<Action>;
|
|
20
|
+
/** customActionNames returns the names of all built-in actions. */
|
|
21
|
+
customActionNames(): string[];
|
|
22
|
+
/** errorUnknownAction signals that the given activity has no known action. */
|
|
23
|
+
private errorUnknownAction;
|
|
24
|
+
}
|
|
25
|
+
export declare function builtinActionFilePaths(): Promise<string[]>;
|
|
26
|
+
export declare function customActionFilePaths(dir: string): Promise<string[]>;
|
|
27
|
+
export declare function loadBuiltinActions(): Promise<Actions>;
|
|
28
|
+
export declare function loadCustomActions(dir: string): Promise<Actions>;
|
|
29
|
+
//# sourceMappingURL=finder.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import * as url from "url";
|
|
4
|
+
import * as actions from "../actions/index.js";
|
|
5
|
+
import { UserError } from "../errors/user-error.js";
|
|
6
|
+
import { Actions } from "./actions.js";
|
|
7
|
+
import { ExternalActionManager } from "./external-action-manager.js";
|
|
8
|
+
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
|
9
|
+
/** ActionFinder provides runnable action instances for activities. */
|
|
10
|
+
export class Finder {
|
|
11
|
+
constructor(builtIn, custom, external) {
|
|
12
|
+
this.builtinActions = builtIn;
|
|
13
|
+
this.customActions = custom;
|
|
14
|
+
this.externalActions = external;
|
|
15
|
+
}
|
|
16
|
+
/** loads all actions */
|
|
17
|
+
static async load(sourceDir) {
|
|
18
|
+
return new Finder(await loadBuiltinActions(), await loadCustomActions(sourceDir.joinStr("text-runner")), new ExternalActionManager());
|
|
19
|
+
}
|
|
20
|
+
/** loads only the actions for dynamic tests */
|
|
21
|
+
static async loadDynamic(sourceDir) {
|
|
22
|
+
return new Finder(new Actions(), await loadCustomActions(sourceDir.joinStr("text-runner")), new ExternalActionManager());
|
|
23
|
+
}
|
|
24
|
+
/** loads only the actions for static tests */
|
|
25
|
+
static async loadStatic() {
|
|
26
|
+
return new Finder(await loadBuiltinActions(), new Actions(), new ExternalActionManager());
|
|
27
|
+
}
|
|
28
|
+
/** actionFor provides the action function for the given Activity. */
|
|
29
|
+
async actionFor(activity) {
|
|
30
|
+
return (this.builtinActions.get(activity.actionName)
|
|
31
|
+
|| this.customActions.get(activity.actionName)
|
|
32
|
+
|| (await this.externalActions.get(activity))
|
|
33
|
+
|| this.errorUnknownAction(activity));
|
|
34
|
+
}
|
|
35
|
+
/** customActionNames returns the names of all built-in actions. */
|
|
36
|
+
customActionNames() {
|
|
37
|
+
return this.customActions.names();
|
|
38
|
+
}
|
|
39
|
+
/** errorUnknownAction signals that the given activity has no known action. */
|
|
40
|
+
errorUnknownAction(activity) {
|
|
41
|
+
const errorText = `unknown action: ${activity.actionName}`;
|
|
42
|
+
let guidance = "";
|
|
43
|
+
if (this.customActions.size() > 0) {
|
|
44
|
+
guidance += "User-defined actions:\n";
|
|
45
|
+
for (const actionName of this.customActions.names()) {
|
|
46
|
+
guidance += `* ${actionName}\n`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
guidance += "No custom actions defined.\n";
|
|
51
|
+
}
|
|
52
|
+
guidance += `\nTo create a new "${activity.actionName}" action,\n`;
|
|
53
|
+
guidance += `run "text-runner scaffold ${activity.actionName}"\n`;
|
|
54
|
+
throw new UserError(errorText, guidance, activity.location);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function builtinActionFilePaths() {
|
|
58
|
+
const result = [];
|
|
59
|
+
const builtinDir = path.join(__dirname, "built-in");
|
|
60
|
+
for (const file of await fs.readdir(builtinDir)) {
|
|
61
|
+
if (file.endsWith(".js") || (file.endsWith(".ts") && !file.endsWith(".d.ts"))) {
|
|
62
|
+
result.push(path.join(builtinDir, file));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
export async function customActionFilePaths(dir) {
|
|
68
|
+
try {
|
|
69
|
+
var files = await fs.readdir(dir);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
// it's okay if there is no dir with custom actions
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const result = [];
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (file.endsWith(".js") || file.endsWith(".ts")) {
|
|
78
|
+
result.push(path.join(dir, file));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
export async function loadBuiltinActions() {
|
|
84
|
+
const result = new Actions();
|
|
85
|
+
for (const filename of await builtinActionFilePaths()) {
|
|
86
|
+
const fileURL = url.pathToFileURL(filename);
|
|
87
|
+
result.register(actions.name(filename), await import(fileURL.href));
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
export async function loadCustomActions(dir) {
|
|
92
|
+
const result = new Actions();
|
|
93
|
+
for (const filename of await customActionFilePaths(dir)) {
|
|
94
|
+
const fileURL = url.pathToFileURL(filename);
|
|
95
|
+
result.register(actions.name(filename), await import(fileURL.href));
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=finder.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as ast from "../ast/index.js";
|
|
2
|
+
import * as config from "../configuration/index.js";
|
|
3
|
+
import * as files from "../filesystem/index.js";
|
|
4
|
+
import * as linkTargets from "../link-targets/index.js";
|
|
5
|
+
import * as run from "../run/index.js";
|
|
6
|
+
export { Finder } from "./finder.js";
|
|
7
|
+
export * from "./name.js";
|
|
8
|
+
/** A user-defined or built-in function that tests an active block */
|
|
9
|
+
export type Action = (params: Args) => Result;
|
|
10
|
+
export interface Args {
|
|
11
|
+
/** TextRunner configuration data derived from the config file and CLI switches */
|
|
12
|
+
readonly configuration: config.Data;
|
|
13
|
+
/** the AST nodes of the active region which the current action tests */
|
|
14
|
+
readonly document: ast.NodeList;
|
|
15
|
+
/** all link targets in the current documentation */
|
|
16
|
+
readonly linkTargets: linkTargets.List;
|
|
17
|
+
/** name of the file in which the currently tested active region is */
|
|
18
|
+
readonly location: files.Location;
|
|
19
|
+
/** allows printing test output to the user, behaves like console.log */
|
|
20
|
+
readonly log: run.LogFn;
|
|
21
|
+
/**
|
|
22
|
+
* Name allows to provide a more specific name for the current action.
|
|
23
|
+
*
|
|
24
|
+
* As an example, the generic step name `write file` could be refined to
|
|
25
|
+
* `write file "foo.yml"` once the name of the file to be written
|
|
26
|
+
* has been extracted from the document AST.
|
|
27
|
+
*/
|
|
28
|
+
readonly name: run.RefineNameFn;
|
|
29
|
+
/** the AST nodes of the active region which the current action tests */
|
|
30
|
+
readonly region: ast.NodeList;
|
|
31
|
+
/** return the action with this value to signal that it is being skipped */
|
|
32
|
+
readonly SKIPPING: 254;
|
|
33
|
+
}
|
|
34
|
+
export type FunctionRepo = Record<string, Action>;
|
|
35
|
+
/** the result of an action function */
|
|
36
|
+
export type Result = 254 | undefined;
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as ast from "../ast/index.js";
|
|
2
|
+
import { List } from "./index.js";
|
|
3
|
+
/** returns all activities found in the given ast.NodeLists */
|
|
4
|
+
export declare function extractDynamic(docs: ast.NodeList[], regionMarker: string): List;
|
|
5
|
+
//# sourceMappingURL=extract-dynamic.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { normalizeActionName } from "./normalize-action-name.js";
|
|
2
|
+
/** returns all activities found in the given ast.NodeLists */
|
|
3
|
+
export function extractDynamic(docs, regionMarker) {
|
|
4
|
+
const result = [];
|
|
5
|
+
for (const doc of docs) {
|
|
6
|
+
result.push(...extractFromAST(doc, regionMarker));
|
|
7
|
+
}
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
/** returns the activities contained in the given NodeList */
|
|
11
|
+
function extractFromAST(doc, regionMarker) {
|
|
12
|
+
const result = [];
|
|
13
|
+
for (const node of doc) {
|
|
14
|
+
if (isActiveBlockTag(node, regionMarker)) {
|
|
15
|
+
result.push({
|
|
16
|
+
actionName: normalizeActionName(node.attributes[regionMarker], node.location),
|
|
17
|
+
document: doc,
|
|
18
|
+
location: node.location,
|
|
19
|
+
region: doc.nodesFor(node)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
/** returns whether the given AstNode marks an active region in the document */
|
|
26
|
+
function isActiveBlockTag(node, activeAttributeName) {
|
|
27
|
+
return !!node.attributes[activeAttributeName];
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=extract-dynamic.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as ast from "../ast/index.js";
|
|
2
|
+
import { List } from "./index.js";
|
|
3
|
+
/** extracts activities that check images and links from the given ActivityLists */
|
|
4
|
+
export declare function extractImagesAndLinks(ASTs: ast.NodeList[]): List;
|
|
5
|
+
//# sourceMappingURL=extract-images-and-links.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as ast from "../ast/index.js";
|
|
2
|
+
/** extracts activities that check images and links from the given ActivityLists */
|
|
3
|
+
export function extractImagesAndLinks(ASTs) {
|
|
4
|
+
const result = [];
|
|
5
|
+
for (const AST of ASTs) {
|
|
6
|
+
for (const node of AST) {
|
|
7
|
+
switch (node.type) {
|
|
8
|
+
case "image": {
|
|
9
|
+
const nodes = new ast.NodeList();
|
|
10
|
+
nodes.push(node);
|
|
11
|
+
result.push({
|
|
12
|
+
actionName: "check-image",
|
|
13
|
+
document: AST,
|
|
14
|
+
location: node.location,
|
|
15
|
+
region: nodes
|
|
16
|
+
});
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case "link_open":
|
|
20
|
+
result.push({
|
|
21
|
+
actionName: "check-link",
|
|
22
|
+
document: AST,
|
|
23
|
+
location: node.location,
|
|
24
|
+
region: AST.nodesFor(node)
|
|
25
|
+
});
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=extract-images-and-links.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as ast from "../ast/index.js";
|
|
2
|
+
import * as files from "../filesystem/index.js";
|
|
3
|
+
export { extractDynamic } from "./extract-dynamic.js";
|
|
4
|
+
export { extractImagesAndLinks } from "./extract-images-and-links.js";
|
|
5
|
+
/**
|
|
6
|
+
* Activity is an action instance.
|
|
7
|
+
* A particular action that we are going to perform
|
|
8
|
+
* on a particular region of a particular document.
|
|
9
|
+
*/
|
|
10
|
+
export interface Activity {
|
|
11
|
+
readonly actionName: string;
|
|
12
|
+
document: ast.NodeList;
|
|
13
|
+
readonly location: files.Location;
|
|
14
|
+
readonly region: ast.NodeList;
|
|
15
|
+
}
|
|
16
|
+
/** a list of activities */
|
|
17
|
+
export type List = Activity[];
|
|
18
|
+
/** scaffoldActivity creates a test Activity from the given data */
|
|
19
|
+
export declare function scaffold(data?: Partial<Activity>): Activity;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|