├── .depcheckrc
├── .github
├── prosebot.yml
└── workflows
│ ├── ci.yml
│ └── windows.yml
├── .gitignore
├── .tool-versions
├── .vscode
└── launch.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Makefile
├── README.md
├── documentation
├── DEVELOPMENT.md
├── built-in-actions.md
├── external-actions.md
├── how-it-works.md
├── installation.md
├── logo.png
├── logo.sketch
├── logo2.png
├── logo_800_dark.png
├── logo_800_light.png
├── package.json
├── qna.md
├── related-tools.md
├── text-runner.jsonc
├── text-runner.schema.json
├── text-runner
│ ├── action-arg.ts
│ ├── all-action-args.ts
│ ├── ast-node-attributes.ts
│ ├── ast-node-list-methods.ts
│ └── textrunner-command.ts
├── textrun-shell.js
└── user-defined-actions.md
├── dprint.json
├── eslint.config.mjs
├── examples
├── .gitignore
├── README.md
├── custom-action-commonjs
│ ├── custom-action.md
│ ├── package.json
│ └── text-runner
│ │ └── hello-world.js
├── custom-action-esm
│ ├── custom-action.md
│ ├── package.json
│ └── text-runner
│ │ └── hello-world.js
├── custom-action-typescript
│ ├── 1.md
│ ├── package.json
│ └── text-runner
│ │ └── hello-world.ts
├── global-tool
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── tool
│ │ └── tool.cmd
│ └── textrun-shell.js
└── text-runner.jsonc
├── lerna.json
├── package.json
├── shared
└── cucumber-steps
│ ├── package.json
│ ├── src
│ ├── env.ts
│ ├── given-steps.ts
│ ├── helpers
│ │ ├── compare-execute-result-line.test.ts
│ │ ├── compare-execute-result-line.ts
│ │ ├── execute-cli.ts
│ │ ├── index.ts
│ │ ├── make-full-path.test.ts
│ │ ├── make-full-path.ts
│ │ ├── standardize-path.test.ts
│ │ ├── standardize-path.ts
│ │ ├── verify-ran-only-test-cli.ts
│ │ ├── workspace.test.ts
│ │ └── workspace.ts
│ ├── then-steps.ts
│ ├── when-steps.ts
│ └── world.ts
│ ├── tsconfig-build.json
│ ├── tsconfig.json
│ └── watermelon.gif
├── test
├── README.md
├── extension
│ ├── package.json
│ └── tsconfig.json
├── features_0
│ ├── package.json
│ └── tsconfig.json
├── features_1
│ ├── package.json
│ └── tsconfig.json
├── features_10
│ ├── package.json
│ └── tsconfig.json
├── features_11
│ ├── package.json
│ └── tsconfig.json
├── features_12
│ ├── package.json
│ └── tsconfig.json
├── features_13
│ ├── package.json
│ └── tsconfig.json
├── features_14
│ ├── package.json
│ └── tsconfig.json
├── features_15
│ ├── package.json
│ └── tsconfig.json
├── features_2
│ ├── package.json
│ └── tsconfig.json
├── features_3
│ ├── package.json
│ └── tsconfig.json
├── features_4
│ ├── package.json
│ └── tsconfig.json
├── features_5
│ ├── package.json
│ └── tsconfig.json
├── features_6
│ ├── package.json
│ └── tsconfig.json
├── features_7
│ ├── package.json
│ └── tsconfig.json
├── features_8
│ ├── package.json
│ └── tsconfig.json
├── features_9
│ ├── package.json
│ └── tsconfig.json
├── javascript
│ ├── package.json
│ └── tsconfig.json
├── make
│ ├── package.json
│ └── tsconfig.json
├── npm
│ ├── package.json
│ └── tsconfig.json
├── repo
│ ├── package.json
│ └── tsconfig.json
├── shell
│ ├── package.json
│ └── tsconfig.json
├── tsconfig.json
└── workspace
│ ├── package.json
│ └── tsconfig.json
├── text-runner-cli
├── bin
│ └── text-runner
├── package.json
├── src
│ ├── api.test.ts
│ ├── api.ts
│ ├── cmdline.test.ts
│ ├── cmdline.ts
│ ├── commands
│ │ ├── help.ts
│ │ ├── index.ts
│ │ ├── instantiate.ts
│ │ ├── scaffold.ts
│ │ ├── setup.ts
│ │ └── version.ts
│ ├── config-file.ts
│ ├── configuration.test.ts
│ ├── configuration.ts
│ ├── formatters
│ │ ├── detailed-formatter.ts
│ │ ├── dot-formatter.ts
│ │ ├── index.ts
│ │ ├── instantiate.test.ts
│ │ ├── instantiate.ts
│ │ ├── print-code-frame.ts
│ │ ├── print-summary.ts
│ │ ├── print-user-error.ts
│ │ ├── progress-formatter.ts
│ │ └── summary-formatter.ts
│ ├── helpers
│ │ ├── all-keys.test.ts
│ │ ├── all-keys.ts
│ │ ├── camelize.test.ts
│ │ ├── camelize.ts
│ │ └── index.ts
│ ├── main.ts
│ └── start.ts
├── tsconfig-build.json
└── tsconfig.json
├── text-runner-engine
├── DEVELOPMENT.md
├── README.md
├── dprint.json
├── package.json
├── src
│ ├── actions
│ │ ├── actions.test.ts
│ │ ├── actions.ts
│ │ ├── built-in
│ │ │ ├── check-image.ts
│ │ │ ├── check-link.ts
│ │ │ └── test.ts
│ │ ├── export.ts
│ │ ├── external-action-manager.ts
│ │ ├── finder.test.ts
│ │ ├── finder.ts
│ │ ├── index.ts
│ │ ├── name.test.ts
│ │ └── name.ts
│ ├── activities
│ │ ├── extract-dynamic.test.ts
│ │ ├── extract-dynamic.ts
│ │ ├── extract-images-and-links.test.ts
│ │ ├── extract-images-and-links.ts
│ │ ├── index.ts
│ │ ├── normalize-action-name.test.ts
│ │ └── normalize-action-name.ts
│ ├── ast
│ │ ├── index.ts
│ │ ├── node-list.test.ts
│ │ ├── node-list.ts
│ │ ├── node.test.ts
│ │ └── node.ts
│ ├── commands
│ │ ├── command.ts
│ │ ├── debug.ts
│ │ ├── dynamic.ts
│ │ ├── index.ts
│ │ ├── run.ts
│ │ ├── static.ts
│ │ └── unused.ts
│ ├── configuration
│ │ ├── data.ts
│ │ ├── defaults.test.ts
│ │ ├── defaults.ts
│ │ ├── index.ts
│ │ ├── publication.test.ts
│ │ ├── publication.ts
│ │ ├── publications.test.ts
│ │ └── publications.ts
│ ├── errors
│ │ ├── error.test.ts
│ │ ├── error.ts
│ │ ├── node-error.ts
│ │ └── user-error.ts
│ ├── events
│ │ └── index.ts
│ ├── filesystem
│ │ ├── absolute-dir.test.ts
│ │ ├── absolute-dir.ts
│ │ ├── absolute-file.test.ts
│ │ ├── absolute-file.ts
│ │ ├── full-dir.ts
│ │ ├── full-file.ts
│ │ ├── full-link.test.ts
│ │ ├── full-link.ts
│ │ ├── full-path.test.ts
│ │ ├── full-path.ts
│ │ ├── get-filenames.test.ts
│ │ ├── get-filenames.ts
│ │ ├── has-directory.ts
│ │ ├── index.ts
│ │ ├── is-markdown-file.ts
│ │ ├── location.ts
│ │ ├── relative-dir.ts
│ │ ├── relative-link.test.ts
│ │ ├── relative-link.ts
│ │ ├── source-dir.test.ts
│ │ ├── source-dir.ts
│ │ ├── unknown-link.test.ts
│ │ └── unknown-link.ts
│ ├── helpers
│ │ ├── add-leading-dot-unless-empty.test.ts
│ │ ├── add-leading-dot-unless-empty.ts
│ │ ├── add-leading-slash.test.ts
│ │ ├── add-leading-slash.ts
│ │ ├── add-trailing-slash.test.ts
│ │ ├── add-trailing-slash.ts
│ │ ├── index.ts
│ │ ├── is-external-link.test.ts
│ │ ├── is-external-link.ts
│ │ ├── is-link-to-anchor-in-other-file.test.ts
│ │ ├── is-link-to-anchor-in-other-file.ts
│ │ ├── is-link-to-anchor-in-same-file.test.ts
│ │ ├── is-link-to-anchor-in-same-file.ts
│ │ ├── is-mailto-link.test.ts
│ │ ├── is-mailto-link.ts
│ │ ├── remove-double-slash.test.ts
│ │ ├── remove-double-slash.ts
│ │ ├── remove-leading-slash.test.ts
│ │ ├── remove-leading-slash.ts
│ │ ├── remove-trailing-colon.test.ts
│ │ ├── remove-trailing-colon.ts
│ │ ├── straighten-link.test.ts
│ │ ├── straighten-link.ts
│ │ ├── trim-all-line-ends.test.ts
│ │ ├── trim-all-line-ends.ts
│ │ ├── trim-extension.test.ts
│ │ ├── trim-extension.ts
│ │ ├── unixify.test.ts
│ │ └── unixify.ts
│ ├── link-targets
│ │ ├── find.ts
│ │ ├── index.ts
│ │ ├── list.test.ts
│ │ ├── list.ts
│ │ ├── target-url.test.ts
│ │ └── target-url.ts
│ ├── parsers
│ │ ├── fixtures
│ │ │ ├── anchor
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── code-active
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── code-passive
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── complex-example
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── em-active
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── em-passive
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── heading-active
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── heading-passive
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── image-active
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── image-passive
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── linebreak
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── link-passive
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── pre-active
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── pre-embedded
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── pre-passive
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── strong-active
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── strong-passive
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ ├── table
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ │ └── text
│ │ │ │ ├── input.html
│ │ │ │ ├── input.md
│ │ │ │ └── result.json
│ │ ├── html
│ │ │ ├── fixtures
│ │ │ │ ├── anchor-open
│ │ │ │ │ ├── input.html
│ │ │ │ │ └── result.json
│ │ │ │ └── table
│ │ │ │ │ ├── input.html
│ │ │ │ │ └── result.json
│ │ │ ├── html-parser.test.ts
│ │ │ ├── html-parser.ts
│ │ │ ├── index.ts
│ │ │ ├── parse-html-files.test.ts
│ │ │ └── parse-html-files.ts
│ │ ├── index.ts
│ │ ├── markdown
│ │ │ ├── closing-tag-parser.test.ts
│ │ │ ├── closing-tag-parser.ts
│ │ │ ├── fixtures
│ │ │ │ ├── active-region
│ │ │ │ │ ├── input.md
│ │ │ │ │ └── result.json
│ │ │ │ ├── bullet_list
│ │ │ │ │ ├── input.md
│ │ │ │ │ └── result.json
│ │ │ │ ├── complex-2
│ │ │ │ │ ├── input.md
│ │ │ │ │ └── result.json
│ │ │ │ └── user-input
│ │ │ │ │ ├── input.md
│ │ │ │ │ └── result.json
│ │ │ ├── index.ts
│ │ │ ├── md-parser.test.ts
│ │ │ ├── md-parser.ts
│ │ │ ├── open-node-tracker.test.ts
│ │ │ ├── open-node-tracker.ts
│ │ │ └── parse.ts
│ │ ├── tag-mapper.test.ts
│ │ └── tag-mapper.ts
│ ├── run
│ │ ├── activity-collector.ts
│ │ ├── index.ts
│ │ ├── name-refiner.test.ts
│ │ ├── name-refiner.ts
│ │ ├── output-collector.test.ts
│ │ ├── output-collector.ts
│ │ ├── parallel.ts
│ │ ├── run-activity.ts
│ │ ├── sequential.ts
│ │ ├── stopwatch.test.ts
│ │ └── stopwatch.ts
│ ├── text-runner.ts
│ └── workspace
│ │ ├── create.ts
│ │ └── index.ts
├── text-runner.jsonc
├── textrun-shell.js
├── tsconfig-build.json
└── tsconfig.json
├── text-runner-features
├── actions
│ ├── custom
│ │ ├── README.md
│ │ └── skipping.feature
│ ├── multiple-callbacks.feature
│ └── unknown.feature
├── block-syntax
│ └── block-type-casing.feature
├── commands
│ ├── debug-command.feature
│ ├── help-command.feature
│ ├── scaffold-command.feature
│ ├── unknown-command.feature
│ ├── unused.feature
│ └── version-command.feature
├── configuration-file
│ ├── config-filename.feature
│ ├── generate.feature
│ └── no-config-file.feature
├── configuration-options
│ ├── default-file.feature
│ ├── empty-workspace.feature
│ ├── online.feature
│ ├── publications.feature
│ ├── region-marker.feature
│ ├── show-skipped.feature
│ └── use-system-tmp-dir.feature
├── cucumber.cjs
├── empty-files
│ ├── empty-directory.feature
│ ├── empty-file.feature
│ └── non-actionable-tutorial.feature
├── formatters
│ ├── elapsed-time.feature
│ ├── select.feature
│ └── signals.feature
├── images
│ ├── html-images.feature
│ └── markdown-images.feature
├── links
│ ├── empty-links.feature
│ ├── external-websites.feature
│ ├── ignored-links.feature
│ ├── links-to-html-anchors.feature
│ ├── local-filesystem.feature
│ └── mailto.feature
├── package.json
├── specifying-files
│ ├── default-behavior.feature
│ ├── excluding-files.feature
│ ├── glob-syntax.feature
│ ├── ignoring-dependencies.feature
│ ├── ignoring-workspace.feature
│ ├── run-single-file.feature
│ └── run-subdirectory.feature
└── tag-types
│ ├── abbr.feature
│ ├── anchor.feature
│ ├── b.feature
│ ├── blockquote.feature
│ ├── br.feature
│ ├── center.feature
│ ├── code.feature
│ ├── details.feature
│ ├── div.feature
│ ├── em.feature
│ ├── footnote.feature
│ ├── h1.feature
│ ├── h2.feature
│ ├── h3.feature
│ ├── h4.feature
│ ├── h5.feature
│ ├── h6.feature
│ ├── hr.feature
│ ├── i.feature
│ ├── img.feature
│ ├── kbd.feature
│ ├── link.feature
│ ├── marquee.feature
│ ├── ol.feature
│ ├── p.feature
│ ├── pre.feature
│ ├── strong.feature
│ ├── summary.feature
│ ├── sup.feature
│ ├── table.feature
│ └── ul.feature
├── text-runner.jsonc
├── textrun-action
├── README.md
├── package.json
├── src
│ ├── index.ts
│ ├── name-full.ts
│ └── name-short.ts
├── text-runner
│ └── test-setup.ts
├── tsconfig-build.json
└── tsconfig.json
├── textrun-extension
├── README.md
├── cucumber.cjs
├── features
│ ├── run-textrunner.feature
│ └── runnable-region.feature
├── package.json
├── src
│ ├── actions
│ │ ├── run-textrunner.ts
│ │ └── runnable-region.ts
│ ├── helpers
│ │ ├── call-args.test.ts
│ │ └── call-args.ts
│ └── index.ts
├── text-runner.jsonc
├── tsconfig-build.json
└── tsconfig.json
├── textrun-javascript
├── README.md
├── cucumber.cjs
├── features
│ ├── run-javascript.feature
│ └── validate-javascript.feature
├── package.json
├── src
│ ├── actions
│ │ ├── non-runnable.ts
│ │ └── runnable.ts
│ ├── helpers
│ │ ├── append-async-callback.test.ts
│ │ ├── append-async-callback.ts
│ │ ├── has-callback-placeholder.test.ts
│ │ ├── has-callback-placeholder.ts
│ │ ├── replace-async-callback.test.ts
│ │ ├── replace-async-callback.ts
│ │ ├── replace-require-local-module.test.ts
│ │ ├── replace-require-local-module.ts
│ │ ├── replace-variable-declarations.test.ts
│ │ └── replace-variable-declarations.ts
│ └── index.ts
├── tsconfig-build.json
└── tsconfig.json
├── textrun-make
├── README.md
├── cucumber.cjs
├── features
│ ├── command.feature
│ └── target.feature
├── package.json
├── src
│ ├── actions
│ │ ├── command.test.ts
│ │ ├── command.ts
│ │ └── target.ts
│ ├── helpers
│ │ ├── makefile-targets.test.ts
│ │ └── makefile-targets.ts
│ └── index.ts
├── text-runner.jsonc
├── tsconfig-build.json
└── tsconfig.json
├── textrun-npm
├── README.md
├── cucumber.cjs
├── features
│ ├── exported-executable.feature
│ ├── install.feature
│ ├── installed-executable.feature
│ ├── package-json-script-call.feature
│ └── package-json-script-name.feature
├── package.json
├── src
│ ├── actions
│ │ ├── exported-executable.ts
│ │ ├── install.ts
│ │ ├── installed-executable.ts
│ │ ├── package-json.ts
│ │ ├── script-call.ts
│ │ └── script-name.ts
│ ├── helpers
│ │ ├── starts-with-npm-run.test.ts
│ │ ├── starts-with-npm-run.ts
│ │ ├── trim-dollar.test.ts
│ │ ├── trim-dollar.ts
│ │ ├── trim-npm-run.test.ts
│ │ └── trim-npm-run.ts
│ └── index.ts
├── text-runner
│ ├── bundled-executable.ts
│ └── create-npm-executable.ts
├── tsconfig-build.json
└── tsconfig.json
├── textrun-repo
├── README.md
├── cucumber.cjs
├── features
│ ├── executable.feature
│ ├── existing-file-content.feature
│ └── existing-file.feature
├── package.json
├── src
│ ├── executable.ts
│ ├── existing-file-content.ts
│ ├── existing-file.ts
│ └── index.ts
├── text-runner.jsonc
├── text-runner
│ └── new-executable.ts
├── tsconfig-build.json
└── tsconfig.json
├── textrun-shell
├── README.md
├── cucumber.cjs
├── features
│ ├── exec
│ │ ├── README.md
│ │ ├── basic-usage.feature
│ │ ├── multiple-commands.feature
│ │ ├── preceding-dollar-sign.feature
│ │ └── user-input.feature
│ ├── start-stop-process
│ │ └── basic.feature
│ ├── verify-console-command-output
│ │ └── verify-console-command-output.feature
│ └── verify-process-output
│ │ └── verify-process-output.feature
├── package.json
├── src
│ ├── actions
│ │ ├── command-output.ts
│ │ ├── command-with-input.ts
│ │ ├── command.ts
│ │ ├── server-output.ts
│ │ ├── server.ts
│ │ └── stop-server.ts
│ ├── helpers
│ │ ├── configuration.test.ts
│ │ ├── configuration.ts
│ │ ├── current-command.ts
│ │ ├── current-server.ts
│ │ ├── path-mapper.test.ts
│ │ ├── path-mapper.ts
│ │ ├── trim-dollar.test.ts
│ │ └── trim-dollar.ts
│ └── index.ts
├── text-runner.jsonc
├── textrun-shell.js
├── tsconfig-build.json
└── tsconfig.json
├── textrun-workspace
├── README.md
├── cucumber.cjs
├── features
│ ├── additional-file-content.feature
│ ├── empty-file.feature
│ ├── existing-directory.feature
│ ├── existing-file.feature
│ ├── new-directory.feature
│ ├── new-file.feature
│ └── working-dir.feature
├── package.json
├── src
│ ├── actions
│ │ ├── additional-file-content.ts
│ │ ├── empty-file.ts
│ │ ├── existing-directory.ts
│ │ ├── existing-file-with-content.ts
│ │ ├── existing-file.ts
│ │ ├── new-directory.ts
│ │ ├── new-file.ts
│ │ └── working-dir.ts
│ └── index.ts
├── text-runner.jsonc
├── tsconfig-build.json
└── tsconfig.json
├── tools
└── .gitkeep
├── tsconfig.json
├── turbo.json
└── yarn.lock
/.depcheckrc:
--------------------------------------------------------------------------------
1 | ignores:
2 | - "@cucumber/cucumber"
3 | - chai
4 | - documentation
5 | - custom-action-commonjs
6 | - custom-action-esm
7 | - custom-action-typescript
8 | - shared-cucumber-steps
9 | - text-runner
10 | - text-runner-engine
11 | - text-runner-features
12 | - textrun-action
13 | - textrun-extension
14 | - textrun-javascript
15 | - textrun-make
16 | - textrun-npm
17 | - textrun-repo
18 | - textrun-shell
19 | - textrun-workspace
20 | - tsx
21 |
--------------------------------------------------------------------------------
/.github/prosebot.yml:
--------------------------------------------------------------------------------
1 | writeGood: true
2 | alex: false
3 | spellchecker: false
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | node-version: [22.x, 23.x]
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: make setup
21 | - run: make test
22 | - run: make fix
23 | - name: Indicate formatting issues
24 | run: git diff HEAD --exit-code --color
25 |
--------------------------------------------------------------------------------
/.github/workflows/windows.yml:
--------------------------------------------------------------------------------
1 | name: windows
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | windows:
11 | runs-on: windows-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-node@v3
15 | with:
16 | node-version: "22"
17 | - run: yarn
18 | - run: cd text-runner-engine && yarn run build
19 | - run: cd text-runner-cli && yarn run build
20 | - run: cd shared\cucumber-steps && yarn run build
21 | - run: cd text-runner-features && yarn run cuke:smoke
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .turbo
2 | dist
3 | node_modules
4 | npm-debug.log
5 | test-dir
6 | tmp
7 | tools
8 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | scc 3.5.0
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Text-Runner
2 |
3 | First and foremost, thank you! We appreciate every contribution no matter how
4 | big or small. Your time is valuable and your support means a lot to us.
5 |
6 | If you have a question, want to share an idea, or report a problem please
7 | [open an issue](https://github.com/kevgo/text-runner/issues/new). To get started
8 | coding, please see our [developer guide](documentation/DEVELOPMENT.md).
9 |
--------------------------------------------------------------------------------
/documentation/external-actions.md:
--------------------------------------------------------------------------------
1 | # External actions
2 |
3 | Text-Runner can use actions stored in NPM modules. This allows sharing actions
4 | between projects.
5 |
--------------------------------------------------------------------------------
/documentation/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevgo/text-runner/8aa3f5d9fbf24776eae6516b96ebde4863ad33a2/documentation/logo.png
--------------------------------------------------------------------------------
/documentation/logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevgo/text-runner/8aa3f5d9fbf24776eae6516b96ebde4863ad33a2/documentation/logo.sketch
--------------------------------------------------------------------------------
/documentation/logo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevgo/text-runner/8aa3f5d9fbf24776eae6516b96ebde4863ad33a2/documentation/logo2.png
--------------------------------------------------------------------------------
/documentation/logo_800_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevgo/text-runner/8aa3f5d9fbf24776eae6516b96ebde4863ad33a2/documentation/logo_800_dark.png
--------------------------------------------------------------------------------
/documentation/logo_800_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevgo/text-runner/8aa3f5d9fbf24776eae6516b96ebde4863ad33a2/documentation/logo_800_light.png
--------------------------------------------------------------------------------
/documentation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "documentation",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "ISC",
6 | "type": "module",
7 | "scripts": {
8 | "doc": "text-runner",
9 | "fix": "dprint fmt && sort-package-json --quiet",
10 | "lint": "dprint check && sort-package-json --check --quiet && depcheck --config=../.depcheckrc"
11 | },
12 | "devDependencies": {
13 | "assert-no-diff": "4.1.0",
14 | "custom-action-commonjs": "0.0.0",
15 | "custom-action-esm": "0.0.0",
16 | "custom-action-typescript": "0.0.0",
17 | "text-runner": "7.1.2",
18 | "text-runner-engine": "7.1.2",
19 | "textrun-extension": "0.3.1",
20 | "textrun-make": "0.3.1",
21 | "textrun-npm": "0.3.1",
22 | "textrun-shell": "0.3.1",
23 | "textrun-workspace": "0.3.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/documentation/qna.md:
--------------------------------------------------------------------------------
1 | # Q & A
2 |
3 | ### Does this replace other testing frameworks like [Cucumber](https://cucumber.io) or [Gauge](https://gauge.org)?
4 |
5 | No. Text-Runner complements these frameworks. Text-Runner is to make sure
6 | end-user facing documentation is correct. Cucumber and Gauge are for more
7 | fine-grained BDD. In particular, they are great for documenting all possible
8 | failure scenarios which would make end-user facing documentation unreadable.
9 |
10 | ### Does this replace unit testing?
11 |
12 | No. Text-Runner is for end-to-end testing.
13 |
14 | ### I don't want to add a `package.json` file to my root folder
15 |
16 | No problem, you can put it in the `text-runner` folder and call TextRunner from
17 | the root directory of your code base via:
18 |
19 | ```
20 | text-runner/node_modules/.bin/text-runner
21 | ```
22 |
23 | Remember to run `npm install` inside the `text-runner` directory as well.
24 |
--------------------------------------------------------------------------------
/documentation/text-runner.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/kevgo/text-runner/refs/heads/main/documentation/text-runner.schema.json",
3 | "exclude": [
4 | "examples"
5 | ],
6 | "format": "dot"
7 | }
8 |
--------------------------------------------------------------------------------
/documentation/text-runner/action-arg.ts:
--------------------------------------------------------------------------------
1 | import * as textRunner from "text-runner"
2 |
3 | export default function actionArg(action: textRunner.actions.Args): void {
4 | const documented = action.region.text()
5 | const allExisting = Object.keys(action).sort()
6 | for (const existing of allExisting) {
7 | if (documented === existing) {
8 | return
9 | }
10 | }
11 | throw new textRunner.UserError(
12 | `"${documented}" is not an attribute of action`,
13 | `The attributes are ${allExisting.join(", ")}`,
14 | action.location
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/documentation/text-runner/all-action-args.ts:
--------------------------------------------------------------------------------
1 | import * as assertNoDiff from "assert-no-diff"
2 | import * as textRunner from "text-runner"
3 |
4 | export default function allActionArgs(action: textRunner.actions.Args): void {
5 | const ignore = action.region[0].attributes.ignore
6 | const documented = action.region.textInNodesOfType("strong").sort().map(textRunner.helpers.removeTrailingColon)
7 | const existing = Object.keys(action)
8 | .sort()
9 | .filter(tool => tool !== ignore)
10 | assertNoDiff.trimmedLines(documented.join("\n"), existing.join("\n"))
11 | }
12 |
--------------------------------------------------------------------------------
/documentation/text-runner/ast-node-attributes.ts:
--------------------------------------------------------------------------------
1 | import * as assertNoDiff from "assert-no-diff"
2 | import * as textRunner from "text-runner"
3 |
4 | export default function astNodeAttributes(action: textRunner.actions.Args): void {
5 | const documented = action.region
6 | .textInNodesOfType("strong")
7 | .sort()
8 | .map(textRunner.helpers.removeTrailingColon)
9 | .join("\n")
10 | const existing = Object.keys(textRunner.ast.Node.scaffold()).sort().join("\n")
11 | assertNoDiff.chars(documented, existing)
12 | }
13 |
--------------------------------------------------------------------------------
/documentation/text-runner/ast-node-list-methods.ts:
--------------------------------------------------------------------------------
1 | import * as assertNoDiff from "assert-no-diff"
2 | import * as textRunner from "text-runner"
3 |
4 | export default function astNodeListMethods(action: textRunner.actions.Args): void {
5 | const ignore = (action.region[0].attributes["ignore"] || "").split(",").filter(s => s)
6 | ignore.push("constructor")
7 | const documented = action.region
8 | .textInNodesOfType("strong")
9 | .map(textRunner.helpers.removeTrailingColon)
10 | .map(upToOpenParen)
11 | .sort()
12 | const existing = Object.getOwnPropertyNames(textRunner.ast.NodeList.prototype)
13 | .filter(s => !ignore.includes(s))
14 | .sort()
15 | assertNoDiff.json(documented, existing)
16 | }
17 |
18 | function upToOpenParen(text: string): string {
19 | return text.substring(0, text.indexOf("("))
20 | }
21 |
--------------------------------------------------------------------------------
/documentation/text-runner/textrunner-command.ts:
--------------------------------------------------------------------------------
1 | import * as textRunner from "text-runner"
2 |
3 | export default function textrunnerCommand(action: textRunner.actions.Args): void {
4 | const documented = action.region.text().replace("text-runner ", "")
5 | action.name(`Text-Runner command: ${documented}`)
6 | const existing = Object.keys(textRunner.commands).map(s => s.toLowerCase())
7 | if (!existing.includes(documented)) {
8 | throw new textRunner.UserError(
9 | `No text-runner command: ${documented}`,
10 | `Commands are: ${existing.join(", ")}`,
11 | action.location
12 | )
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/documentation/textrun-shell.js:
--------------------------------------------------------------------------------
1 | import * as path from "path"
2 | import * as url from "url"
3 |
4 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url))
5 |
6 | export default {
7 | globals: {
8 | "text-runner": path.join(__dirname, "node_modules", ".bin", "text-runner")
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/dprint.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript": {
3 | "lineWidth": 120,
4 | "semiColons": "asi",
5 | "arrowFunction.useParentheses": "preferNone",
6 | "trailingCommas": "never",
7 | "quoteStyle": "preferDouble"
8 | },
9 | "json": {
10 | "trailingCommas": "never"
11 | },
12 | "markdown": {
13 | "textWrap": "always",
14 | "lineWidth": 80
15 | },
16 | "markup": {},
17 | "yaml": {},
18 | "excludes": [
19 | "documentation",
20 | "examples",
21 | "shared",
22 | "text-runner-cli",
23 | "text-runner-engine",
24 | "text-runner-features",
25 | "tools"
26 | ],
27 | "plugins": [
28 | "https://plugins.dprint.dev/typescript-0.94.0.wasm",
29 | "https://plugins.dprint.dev/json-0.20.0.wasm",
30 | "https://plugins.dprint.dev/markdown-0.18.0.wasm",
31 | "https://plugins.dprint.dev/g-plane/markup_fmt-v0.19.0.wasm",
32 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | yarn.lock
2 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Text-Runner example applications
2 |
3 | This folder contains working code that demonstrates various Text-Runner
4 | features. They are used in documentation as well as the feature specs.
5 |
--------------------------------------------------------------------------------
/examples/custom-action-commonjs/custom-action.md:
--------------------------------------------------------------------------------
1 | Let's run a few custom actions:
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/custom-action-commonjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-action-commonjs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "module": "commonjs",
6 | "scripts": {
7 | "doc": "text-runner --format=dot",
8 | "fix": "dprint fmt && sort-package-json --quiet",
9 | "lint": "dprint check && sort-package-json --check --quiet"
10 | },
11 | "devDependencies": {
12 | "text-runner": "7.1.2",
13 | "tsx": "4.19.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/custom-action-commonjs/text-runner/hello-world.js:
--------------------------------------------------------------------------------
1 | const util = require("util")
2 | const delay = util.promisify(setTimeout)
3 |
4 | function helloWorldSync(action) {
5 | action.log("Greetings from the 2222 sync action!")
6 | }
7 |
8 | async function helloWorldAsync(action) {
9 | await delay(1)
10 | action.log("Greetings from the async action!")
11 | await delay(1)
12 | }
13 |
14 | function helloWorldCallback(action, done) {
15 | setTimeout(() => {
16 | action.log("Greetings from the callback action!")
17 | setTimeout(done, 1)
18 | }, 1)
19 | }
20 |
21 | function helloWorldPromise(action) {
22 | return new Promise(resolve => {
23 | setTimeout(() => {
24 | action.log("Greetings from the promise-based action!")
25 | setTimeout(resolve, 1)
26 | }, 1)
27 | })
28 | }
29 |
30 | module.exports = {
31 | helloWorldSync,
32 | helloWorldAsync,
33 | helloWorldCallback,
34 | helloWorldPromise
35 | }
36 |
--------------------------------------------------------------------------------
/examples/custom-action-esm/custom-action.md:
--------------------------------------------------------------------------------
1 | Let's run a few custom actions:
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/custom-action-esm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-action-esm",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "doc": "text-runner --format=dot",
8 | "fix": "dprint fmt && sort-package-json --quiet",
9 | "lint": "dprint check && sort-package-json --check --quiet"
10 | },
11 | "devDependencies": {
12 | "text-runner": "7.1.2",
13 | "tsx": "4.19.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/custom-action-esm/text-runner/hello-world.js:
--------------------------------------------------------------------------------
1 | import util from "util"
2 | const delay = util.promisify(setTimeout)
3 |
4 | export function helloWorldSync(action) {
5 | action.log("Greetings from the sync action!")
6 | }
7 |
8 | export async function helloWorldAsync(action) {
9 | await delay(1)
10 | action.log("Greetings from the async action!")
11 | await delay(1)
12 | }
13 |
14 | export function helloWorldCallback(action, done) {
15 | setTimeout(() => {
16 | action.log("Greetings from the callback action!")
17 | setTimeout(done, 1)
18 | }, 1)
19 | }
20 |
21 | export function helloWorldPromise(action) {
22 | return new Promise(resolve => {
23 | setTimeout(() => {
24 | action.log("Greetings from the promise-based action!")
25 | setTimeout(resolve, 1)
26 | }, 1)
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/examples/custom-action-typescript/1.md:
--------------------------------------------------------------------------------
1 | Let's run a custom action:
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/examples/custom-action-typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-action-typescript",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "doc": "text-runner --format=dot",
8 | "fix": "dprint fmt && sort-package-json --quiet",
9 | "lint": "dprint check && sort-package-json --check --quiet"
10 | },
11 | "devDependencies": {
12 | "text-runner": "7.1.2",
13 | "tsx": "4.19.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/custom-action-typescript/text-runner/hello-world.ts:
--------------------------------------------------------------------------------
1 | import * as textRunner from "text-runner"
2 |
3 | export default function HelloWorld(action: textRunner.actions.Args): void {
4 | action.log("Hello World from TypeScript!")
5 | }
6 |
--------------------------------------------------------------------------------
/examples/global-tool/README.md:
--------------------------------------------------------------------------------
1 | # Example: calling global tools
2 |
3 | This example codebase contains a global tool aptly named `tool`. This file is
4 | the documentation for this tool. We want to give usage examples like this one:
5 |
6 | ```md
7 | You run the tool with a number as an argument. Here is an example:
8 |
9 |
10 | tool 123
11 |
12 | ```
13 |
--------------------------------------------------------------------------------
/examples/global-tool/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "global-tool-example",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "doc": "text-runner --format=dot",
8 | "fix": "dprint fmt && sort-package-json --quiet",
9 | "lint": "dprint check && sort-package-json --check --quiet"
10 | },
11 | "dependencies": {
12 | "text-runner": "7.1.2",
13 | "textrun-shell": "0.3.1"
14 | },
15 | "devDependencies": {
16 | "tsx": "4.19.3"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/global-tool/public/tool:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "tool called"
3 |
--------------------------------------------------------------------------------
/examples/global-tool/public/tool.cmd:
--------------------------------------------------------------------------------
1 | echo "tool called"
2 |
--------------------------------------------------------------------------------
/examples/global-tool/textrun-shell.js:
--------------------------------------------------------------------------------
1 | import path from "path"
2 | import * as url from "url"
3 |
4 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url))
5 |
6 | export default {
7 | globals: {
8 | tool: path.join(__dirname, "public", "tool")
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/text-runner.jsonc:
--------------------------------------------------------------------------------
1 | // This file exists so that Text-Runner when run from these examples finds it and stops searching in parent directories.
2 | // If it wouldn't exist, Text-Runner would use the text-runner.jsonc file in the root directory,
3 | // which contains things that don't make sense here.
4 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "independent",
3 | "npmClient": "yarn",
4 | "loglevel": "error"
5 | }
6 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shared-cucumber-steps",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "ISC",
6 | "type": "module",
7 | "scripts": {
8 | "build": "tsc -p tsconfig-build.json",
9 | "fix": "eslint --fix --ignore-pattern=dist/ . && dprint fmt && sort-package-json --quiet",
10 | "lint": "dprint check && sort-package-json --check --quiet && eslint --ignore-pattern=dist/ . && depcheck --config=../../.depcheckrc",
11 | "reset": "rm -rf dist && yarn run build",
12 | "unit": "node --test --import tsx 'src/**/*.test.ts'"
13 | },
14 | "devDependencies": {
15 | "@types/ps-tree": "1.1.6",
16 | "array-flatten": "3.0.0",
17 | "assert-no-diff": "4.1.0",
18 | "end-child-processes": "2.0.3",
19 | "globby": "14.1.0",
20 | "observable-process": "8.0.0",
21 | "ps-tree": "1.2.0",
22 | "strip-ansi": "7.1.0",
23 | "text-runner-engine": "7.1.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/env.ts:
--------------------------------------------------------------------------------
1 | import * as cucumber from "@cucumber/cucumber"
2 | import { endChildProcesses } from "end-child-processes"
3 |
4 | import * as workspace from "./helpers/workspace.js"
5 | import { TRWorld } from "./world.js"
6 |
7 | cucumber.BeforeAll(async () => {
8 | await workspace.backup()
9 | })
10 |
11 | cucumber.After({ timeout: 20_000 }, async function(this: TRWorld) {
12 | await endChildProcesses()
13 | await workspace.restore()
14 | })
15 |
16 | cucumber.Before({ tags: "@debug" }, function(this: TRWorld) {
17 | this.debug = true
18 | })
19 |
20 | cucumber.After({ tags: "@debug" }, function(this: TRWorld) {
21 | this.debug = false
22 | })
23 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/helpers/compare-execute-result-line.ts:
--------------------------------------------------------------------------------
1 | import { ExecuteResultLine } from "../then-steps.js"
2 |
3 | export function compareExecuteResultLine(a: ExecuteResultLine, b: ExecuteResultLine): number {
4 | if (!a.filename || !b.filename || !a.line || !b.line) {
5 | return 0
6 | }
7 | if (a.filename > b.filename) {
8 | return 1
9 | } else if (a.filename < b.filename) {
10 | return -1
11 | } else {
12 | return a.line - b.line
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./compare-execute-result-line.js"
2 | export * from "./execute-cli.js"
3 | export * from "./standardize-path.js"
4 | export * from "./verify-ran-only-test-cli.js"
5 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/helpers/make-full-path.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path"
2 | import * as url from "url"
3 |
4 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url))
5 |
6 | export function makeFullPath(command: string, platform: string): string {
7 | if (command.startsWith("text-runner")) {
8 | return command.replace(/^text-runner/, fullTextRunPath(platform))
9 | } else {
10 | return `${fullTextRunPath(platform)} ${command}`
11 | }
12 | }
13 |
14 | function fullTextRunPath(platform: string): string {
15 | let result = path.join(__dirname, "..", "..", "..", "..", "node_modules", ".bin", "text-runner")
16 | if (platform === "win32") {
17 | result += ".cmd"
18 | }
19 | return result
20 | }
21 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/helpers/standardize-path.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { standardizePath } from "./standardize-path.js"
5 |
6 | suite("standardizePath", () => {
7 | test("unix path", () => {
8 | assert.equal(standardizePath("foo/bar"), "foo/bar")
9 | })
10 | test("windows path", () => {
11 | assert.equal(standardizePath("foo\\bar"), "foo/bar")
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/helpers/standardize-path.ts:
--------------------------------------------------------------------------------
1 | export function standardizePath(filePath: string): string {
2 | return filePath.replace(/\\/g, "/")
3 | }
4 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/helpers/workspace.test.ts:
--------------------------------------------------------------------------------
1 | import { strict as assert } from "assert"
2 | import { suite, test } from "node:test"
3 |
4 | import { dirPath } from "./workspace.js"
5 |
6 | suite("dirPath", () => {
7 | const __dirname = "/home/foo/text-runner/shared/cucumber-steps/src/helpers"
8 | const tests = {
9 | "/home/foo/text-runner/text-runner-cli": "/home/foo/text-runner/test/cli",
10 | "/home/foo/text-runner/text-runner-engine": "/home/foo/text-runner/test/engine",
11 | "/home/foo/text-runner/text-runner-features": "/home/foo/text-runner/test/features_1",
12 | "/home/foo/text-runner/textrun-action": "/home/foo/text-runner/test/action",
13 | "/home/foo/text-runner/textrun-extension": "/home/foo/text-runner/test/extension"
14 | }
15 | for (const [give, want] of Object.entries(tests)) {
16 | test(`${give} --> ${want}`, () => {
17 | const have = dirPath(give, __dirname, "1")
18 | assert.equal(have, want)
19 | })
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/src/world.ts:
--------------------------------------------------------------------------------
1 | import * as cucumber from "@cucumber/cucumber"
2 | import * as observableProcess from "observable-process"
3 | import * as textRunner from "text-runner-engine"
4 |
5 | export class TRWorld extends cucumber.World {
6 | /** exception thrown at the last API call returned */
7 | apiException: textRunner.UserError | undefined
8 |
9 | /** result of the last API call */
10 | apiResults = new textRunner.ActivityResults([], "")
11 |
12 | /** whether debug mode is enabled */
13 | debug = false
14 |
15 | /** statistics about the subshell process after it finished */
16 | finishedProcess: observableProcess.FinishedProcess | undefined
17 | }
18 |
19 | cucumber.setWorldConstructor(TRWorld)
20 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/tsconfig-build.json:
--------------------------------------------------------------------------------
1 | // configuration for the compiler: ignore tests for increased speed
2 | // tests are type-checked and compiled when running the unit tests
3 | {
4 | "extends": "./tsconfig.json",
5 | "exclude": ["./src/**/*.test.ts"],
6 | "compilerOptions": {
7 | "outDir": "./dist",
8 | "declaration": true, /* Generates corresponding '.d.ts' file. */
9 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/tsconfig.json:
--------------------------------------------------------------------------------
1 | // configuration for the IDE: includes test code for global code navigation and refactoring
2 | {
3 | "extends": "../../tsconfig.json",
4 | "include": ["./src/**/*"]
5 | }
6 |
--------------------------------------------------------------------------------
/shared/cucumber-steps/watermelon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevgo/text-runner/8aa3f5d9fbf24776eae6516b96ebde4863ad33a2/shared/cucumber-steps/watermelon.gif
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | This folder contains workspaces for end-to-end tests. The current ESM
2 | implementation requires `text-runner` and `typescript` installed into a
3 | `node_modules` folder. Since it takes too much time to `yarn install` this for
4 | every end-to-end test, the workspaces in which tests happen are now part of the
5 | Yarn multi-workspace setup in this monorepo.
6 |
--------------------------------------------------------------------------------
/test/extension/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-extension",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "tsx": "4.19.3",
9 | "typescript": "5.8.2"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/extension/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_0/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-0",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "tsx": "4.19.3",
9 | "typescript": "5.8.2"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/features_0/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-1",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "tsx": "4.19.3",
9 | "typescript": "5.8.2"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/features_1/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_10/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-10",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "tsx": "4.19.3",
9 | "typescript": "5.7.3"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/features_10/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_11/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-11",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "tsx": "4.19.3",
9 | "typescript": "5.7.3"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/features_11/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_12/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-12",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.7.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_12/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_13/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-13",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.7.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_13/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_14/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-14",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.7.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_14/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_15/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-15",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.7.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_15/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-2",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_2/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-3",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_4/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-4",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_4/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_5/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-5",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_5/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_6/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-6",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_6/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_7/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-7",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_7/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_8/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-8",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.7.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_8/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/features_9/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-features-9",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.7.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/features_9/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/javascript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-javascript",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/javascript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/make/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-make",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/make/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/npm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-npm",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/npm/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/repo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-repo",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/repo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/shell/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-shell",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/shell/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "node16"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/workspace/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-workspace",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "text-runner": "7.1.2",
8 | "typescript": "5.8.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/workspace/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/text-runner-cli/bin/text-runner:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S node --experimental-strip-types --no-warnings
2 |
3 | import { start } from "../dist/start.js"
4 | start()
5 |
--------------------------------------------------------------------------------
/text-runner-cli/src/api.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 | import * as textRunner from "text-runner"
4 |
5 | suite("JS API export", () => {
6 | test("exports", () => {
7 | assert.exists(textRunner.commands)
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/text-runner-cli/src/api.ts:
--------------------------------------------------------------------------------
1 | export * from "text-runner-engine"
2 |
--------------------------------------------------------------------------------
/text-runner-cli/src/commands/index.ts:
--------------------------------------------------------------------------------
1 | export { instantiate } from "./instantiate.js"
2 | export * from "./scaffold.js"
3 |
4 | /** returns a list of all available commands */
5 | export function names(): string[] {
6 | return ["debug", "dynamic", "help", "run", "unused", "scaffold", "setup", "static", "version"]
7 | }
8 |
--------------------------------------------------------------------------------
/text-runner-cli/src/commands/setup.ts:
--------------------------------------------------------------------------------
1 | import * as color from "colorette"
2 | import { EventEmitter } from "events"
3 | import * as textRunner from "text-runner-engine"
4 |
5 | import * as configFile from "../config-file.js"
6 | import * as config from "../configuration.js"
7 |
8 | export class SetupCommand implements textRunner.commands.Command {
9 | config: config.Data
10 | emitter: EventEmitter
11 |
12 | constructor(config: config.Data) {
13 | this.config = config
14 | this.emitter = new EventEmitter()
15 | }
16 |
17 | emit(name: textRunner.events.Name, payload: textRunner.events.Args): void {
18 | this.emitter.emit(name, payload)
19 | }
20 |
21 | async execute(): Promise {
22 | await configFile.create(this.config)
23 | this.emit("output", `Created configuration file ${color.cyan("text-runner.jsonc")} with default values`)
24 | }
25 |
26 | on(name: textRunner.events.Name, handler: textRunner.events.Handler): this {
27 | this.emitter.on(name, handler)
28 | return this
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/text-runner-cli/src/commands/version.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "events"
2 | import { promises as fs } from "fs"
3 | import * as path from "path"
4 | import * as textRunner from "text-runner-engine"
5 | import * as url from "url"
6 |
7 | export class VersionCommand implements textRunner.commands.Command {
8 | emitter: EventEmitter
9 |
10 | constructor() {
11 | this.emitter = new EventEmitter()
12 | }
13 |
14 | emit(name: textRunner.events.Name, payload: textRunner.events.Args): void {
15 | this.emitter.emit(name, payload)
16 | }
17 |
18 | async execute(): Promise {
19 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url))
20 | const fileContent = await fs.readFile(path.join(__dirname, "../../package.json"), "utf-8")
21 | const pkg = JSON.parse(fileContent)
22 | this.emit("output", `TextRunner v${pkg.version}`)
23 | }
24 |
25 | on(name: textRunner.events.Name, handler: textRunner.events.Handler): this {
26 | this.emitter.on(name, handler)
27 | return this
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/text-runner-cli/src/formatters/index.ts:
--------------------------------------------------------------------------------
1 | import * as textRunner from "text-runner-engine"
2 |
3 | export { instantiate } from "./instantiate.js"
4 | export { printSummary } from "./print-summary.js"
5 | export { printUserError } from "./print-user-error.js"
6 |
7 | /** FinishArgs defines the arguments provided to the `finish` method. */
8 | export interface FinishArgs {
9 | readonly results: textRunner.ActivityResults
10 | }
11 |
12 | /** Formatter defines the interface that Formatters must implement. */
13 | export interface Formatter {
14 | finish(args: FinishArgs): void
15 | }
16 |
17 | /** Names defines the names of all built-in formatters */
18 | export type Names = "detailed" | "dot" | "progress" | "summary"
19 |
--------------------------------------------------------------------------------
/text-runner-cli/src/formatters/instantiate.ts:
--------------------------------------------------------------------------------
1 | import * as textRunner from "text-runner-engine"
2 |
3 | import { DetailedFormatter } from "./detailed-formatter.js"
4 | import { DotFormatter } from "./dot-formatter.js"
5 | import * as formatters from "./index.js"
6 | import { ProgressFormatter } from "./progress-formatter.js"
7 | import { SummaryFormatter } from "./summary-formatter.js"
8 |
9 | /** creates an instance of the formatter with the given name */
10 | export function instantiate(name: formatters.Names, command: textRunner.commands.Command): formatters.Formatter {
11 | switch (name) {
12 | case "detailed":
13 | return new DetailedFormatter(command)
14 | case "dot":
15 | return new DotFormatter(command)
16 | case "progress":
17 | return new ProgressFormatter(command)
18 | case "summary":
19 | return new SummaryFormatter(command)
20 | default:
21 | throw new textRunner.UserError(
22 | `Unknown formatter: ${name}`,
23 | "Available formatters are: detailed, dot, progress, summary"
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/text-runner-cli/src/formatters/print-code-frame.ts:
--------------------------------------------------------------------------------
1 | import * as babel from "@babel/code-frame"
2 | import * as fs from "fs"
3 | import * as textRunner from "text-runner-engine"
4 |
5 | type PrintFunc = (arg: string) => boolean | void
6 |
7 | export function printCodeFrame(output: PrintFunc, location: textRunner.files.Location | undefined): void {
8 | if (!location) {
9 | return
10 | }
11 |
12 | const fileContent = fs.readFileSync(location.absoluteFilePath().platformified(), "utf8")
13 | output(babel.codeFrameColumns(fileContent, { start: { line: location.line } }, { forceColor: true }))
14 | }
15 |
--------------------------------------------------------------------------------
/text-runner-cli/src/formatters/print-summary.ts:
--------------------------------------------------------------------------------
1 | import * as color from "colorette"
2 | import * as textRunner from "text-runner-engine"
3 |
4 | export function printSummary(results: textRunner.ActivityResults): void {
5 | let text = "\n"
6 | let colorFn: color.Color
7 | const errorCount = results.errorCount()
8 | if (errorCount === 0) {
9 | colorFn = color.green
10 | text += color.green("Success! ")
11 | } else {
12 | colorFn = color.red
13 | text += color.red(`${errorCount} errors, `)
14 | }
15 | text += colorFn(`${results.length} activities, ${results.duration}`)
16 | console.log(color.bold(text))
17 | }
18 |
--------------------------------------------------------------------------------
/text-runner-cli/src/formatters/print-user-error.ts:
--------------------------------------------------------------------------------
1 | import * as color from "colorette"
2 | import * as textRunner from "text-runner-engine"
3 |
4 | import * as helpers from "../helpers/index.js"
5 |
6 | /** prints the given error to the console */
7 | export function printUserError(err: textRunner.UserError): void {
8 | if (err.location) {
9 | console.log(color.red(`${err.location.file.unixified()}:${err.location.line} -- ${err.message || ""}`))
10 | } else {
11 | console.log(color.red(err.message))
12 | }
13 | if (err.guidance) {
14 | console.log()
15 | console.log(err.guidance)
16 | console.log()
17 | }
18 | helpers.printCodeFrame(console.log, err.location)
19 | }
20 |
--------------------------------------------------------------------------------
/text-runner-cli/src/helpers/all-keys.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai"
2 | import { test } from "node:test"
3 |
4 | import * as helpers from "./index.js"
5 |
6 | test("allKeys()", () => {
7 | const obj1 = { a: "1" }
8 | const obj2 = { b: "1" }
9 | const obj3 = { c: "1" }
10 | const actual = helpers.allKeys(obj1, obj2, obj3)
11 | expect(actual).to.eql(["a", "b", "c"])
12 | })
13 |
--------------------------------------------------------------------------------
/text-runner-cli/src/helpers/all-keys.ts:
--------------------------------------------------------------------------------
1 | /** allKeys returns all the keys of all objects. */
2 | export function allKeys(...args: Record[]): string[] {
3 | const result = new Set()
4 | for (const arg of args) {
5 | for (const key of Object.keys(arg)) {
6 | result.add(key)
7 | }
8 | }
9 | return Array.from(result)
10 | }
11 |
--------------------------------------------------------------------------------
/text-runner-cli/src/helpers/camelize.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import * as helpers from "./index.js"
5 |
6 | suite("camelize", () => {
7 | const tests = {
8 | foo: "foo",
9 | "one-two-three": "oneTwoThree",
10 | oneTwoThree: "oneTwoThree"
11 | }
12 | for (const [give, want] of Object.entries(tests)) {
13 | test(`${give} --> ${want}`, () => {
14 | assert.equal(helpers.camelize(give), want)
15 | })
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/text-runner-cli/src/helpers/camelize.ts:
--------------------------------------------------------------------------------
1 | export function camelize(text: string): string {
2 | return text.replace(/^([A-Z])|[\s-_]+(\w)/g, (_match: string, p1: string, p2: string) => {
3 | if (p2) {
4 | return p2.toUpperCase()
5 | } else {
6 | return p1.toLowerCase()
7 | }
8 | })
9 | }
10 |
--------------------------------------------------------------------------------
/text-runner-cli/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export { printCodeFrame } from "../formatters/print-code-frame.js"
2 | export { allKeys } from "./all-keys.js"
3 | export { camelize } from "./camelize.js"
4 |
--------------------------------------------------------------------------------
/text-runner-cli/src/start.ts:
--------------------------------------------------------------------------------
1 | import cliCursor from "cli-cursor"
2 |
3 | import { main } from "./main.js"
4 |
5 | export async function start() {
6 | cliCursor.hide()
7 | const errorCount = await main(process.argv)
8 | process.exit(errorCount)
9 | }
10 |
--------------------------------------------------------------------------------
/text-runner-cli/tsconfig-build.json:
--------------------------------------------------------------------------------
1 | // configuration for the compiler: ignore tests for increased speed
2 | // tests are type-checked and compiled when running the unit tests
3 | {
4 | "extends": "./tsconfig.json",
5 | "exclude": ["./src/**/*.test.ts"],
6 | "compilerOptions": {
7 | "outDir": "./dist",
8 | "declaration": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/text-runner-cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | // configuration for the IDE: includes test code for global code navigation and refactoring
2 | {
3 | "extends": "../tsconfig.json",
4 | "include": ["./src/**/*.ts"]
5 | }
6 |
--------------------------------------------------------------------------------
/text-runner-engine/dprint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../dprint.json"],
3 | "excludes": ["fixtures"]
4 | }
5 |
--------------------------------------------------------------------------------
/text-runner-engine/src/actions/built-in/test.ts:
--------------------------------------------------------------------------------
1 | import { Args } from "../index.js"
2 |
3 | export function test(action: Args): void {
4 | action.log(action.region.text())
5 | }
6 |
--------------------------------------------------------------------------------
/text-runner-engine/src/actions/export.ts:
--------------------------------------------------------------------------------
1 | import * as actions from "./index.js"
2 |
3 | export type Action = CbAction | PromiseAction | SyncAction
4 |
5 | export type CbAction = (action: actions.Args, done: DoneFunction) => void
6 |
7 | /** continuous-passing-style callback function */
8 | export type DoneFunction = (err?: Error) => void
9 |
10 | /** expected file structure of "index.js" files exporting Text-Runner actions */
11 | export interface IndexFile {
12 | textrunActions: TextrunActions
13 | }
14 | /** elements of "package.json" files used here */
15 | export interface PackageJson {
16 | exports: string
17 | }
18 | export type PromiseAction = (action: actions.Args) => Promise
19 | export type SyncAction = (action: actions.Args) => void
20 |
21 | /** data format of exported actions by npm modules */
22 | export type TextrunActions = Record
23 |
--------------------------------------------------------------------------------
/text-runner-engine/src/actions/name.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { name } from "./name.js"
5 |
6 | suite("getActionName()", () => {
7 | const tests = {
8 | "/users/foo/text-runner/text-runner/cdBack.js": "cd-back"
9 | }
10 | for (const [give, want] of Object.entries(tests)) {
11 | test(give, () => {
12 | assert.equal(name(give), want)
13 | })
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/text-runner-engine/src/actions/name.ts:
--------------------------------------------------------------------------------
1 | import slugify from "@sindresorhus/slugify"
2 | import * as path from "path"
3 |
4 | export function name(filepath: string): string {
5 | return slugify(path.basename(filepath, path.extname(filepath)))
6 | }
7 |
--------------------------------------------------------------------------------
/text-runner-engine/src/activities/extract-images-and-links.ts:
--------------------------------------------------------------------------------
1 | import * as ast from "../ast/index.js"
2 | import { List } from "./index.js"
3 |
4 | /** extracts activities that check images and links from the given ActivityLists */
5 | export function extractImagesAndLinks(ASTs: ast.NodeList[]): List {
6 | const result: List = []
7 | for (const AST of ASTs) {
8 | for (const node of AST) {
9 | switch (node.type) {
10 | case "image": {
11 | const nodes = new ast.NodeList()
12 | nodes.push(node)
13 | result.push({
14 | actionName: "check-image",
15 | document: AST,
16 | location: node.location,
17 | region: nodes
18 | })
19 | break
20 | }
21 |
22 | case "link_open":
23 | result.push({
24 | actionName: "check-link",
25 | document: AST,
26 | location: node.location,
27 | region: AST.nodesFor(node)
28 | })
29 | break
30 | }
31 | }
32 | }
33 | return result
34 | }
35 |
--------------------------------------------------------------------------------
/text-runner-engine/src/activities/index.ts:
--------------------------------------------------------------------------------
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 | /**
7 | * Activity is an action instance.
8 | * A particular action that we are going to perform
9 | * on a particular region of a particular document.
10 | */
11 | export interface Activity {
12 | readonly actionName: string
13 | document: ast.NodeList
14 | readonly location: files.Location
15 | readonly region: ast.NodeList
16 | }
17 |
18 | /** a list of activities */
19 | export type List = Activity[]
20 |
21 | /** scaffoldActivity creates a test Activity from the given data */
22 | export function scaffold(data: Partial = {}): Activity {
23 | return {
24 | actionName: data.actionName || "foo",
25 | document: new ast.NodeList(),
26 | location: files.Location.scaffold(),
27 | region: new ast.NodeList()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/text-runner-engine/src/activities/normalize-action-name.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import * as files from "../filesystem/index.js"
5 | import { normalizeActionName } from "./normalize-action-name.js"
6 |
7 | suite("normalizeActionName", () => {
8 | const tests = {
9 | "demo/hello-world": "demo/hello-world",
10 | "demo/HelloWorld": "demo/hello-world",
11 | "demo/helloWorld": "demo/hello-world",
12 | "hello-world": "hello-world",
13 | helloWorld: "hello-world",
14 | HelloWorld: "hello-world"
15 | }
16 | for (const [give, want] of Object.entries(tests)) {
17 | test(give, () => {
18 | const location = new files.Location(new files.SourceDir(""), new files.FullFilePath(""), 1)
19 | assert.equal(normalizeActionName(give, location), want)
20 | })
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/text-runner-engine/src/activities/normalize-action-name.ts:
--------------------------------------------------------------------------------
1 | import slugify from "@sindresorhus/slugify"
2 |
3 | import { UserError } from "../errors/user-error.js"
4 | import * as files from "../filesystem/index.js"
5 |
6 | export function normalizeActionName(actionName: string, location: files.Location): string {
7 | const parts = actionName.split("/")
8 | if (parts.length === 1) {
9 | return slugify(actionName)
10 | }
11 | if (parts.length > 2) {
12 | throw new UserError(`Illegal activity name: "${actionName}" contains ${parts.length} slashes`, "", location)
13 | }
14 | return parts[0] + "/" + slugify(parts[1])
15 | }
16 |
--------------------------------------------------------------------------------
/text-runner-engine/src/ast/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./node-list.js"
2 | export * from "./node.js"
3 |
--------------------------------------------------------------------------------
/text-runner-engine/src/commands/command.ts:
--------------------------------------------------------------------------------
1 | import * as events from "../events/index.js"
2 |
3 | /** Command describes a Text-Runner command */
4 | export interface Command {
5 | emit(event: events.Name, payload: events.Args): void
6 | /** executes this command */
7 | execute(): Promise
8 | on(event: events.Name, handler: events.Handler): void
9 | }
10 |
--------------------------------------------------------------------------------
/text-runner-engine/src/commands/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./command.js"
2 | export * from "./debug.js"
3 | export * from "./dynamic.js"
4 | export * from "./run.js"
5 | export * from "./static.js"
6 | export * from "./unused.js"
7 |
--------------------------------------------------------------------------------
/text-runner-engine/src/configuration/defaults.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import * as config from "./index.js"
5 |
6 | suite("addDefaults", () => {
7 | test("no input", async () => {
8 | const have = await config.addDefaults({})
9 | assert.strictEqual(have.files, "**/*.md")
10 | const want = "text-runner-engine/tmp"
11 | if (!have.workspace.unixified().endsWith(want)) {
12 | throw new Error(`expected ${have.workspace.unixified()} to end with ${want}`)
13 | }
14 | })
15 | test("input", async () => {
16 | const have = await config.addDefaults({ files: "1.md", regionMarker: "foo" })
17 | assert.strictEqual(have.files, "1.md")
18 | assert.strictEqual(have.regionMarker, "foo")
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/text-runner-engine/src/configuration/index.ts:
--------------------------------------------------------------------------------
1 | export type { APIData, CompleteAPIData, Data } from "./data.js"
2 | export { addDefaults, defaults } from "./defaults.js"
3 | export { Publication } from "./publication.js"
4 | export { Publications } from "./publications.js"
5 |
--------------------------------------------------------------------------------
/text-runner-engine/src/errors/error.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { errorMessage } from "./error.js"
5 | import { UserError } from "./user-error.js"
6 |
7 | suite("errorMessage", () => {
8 | test("Error", () => {
9 | const give = new Error("hello")
10 | const want = "hello"
11 | const have = errorMessage(give)
12 | assert.equal(have, want)
13 | })
14 |
15 | test("number", () => {
16 | const give = 123
17 | const want = "123"
18 | const have = errorMessage(give)
19 | assert.equal(have, want)
20 | })
21 |
22 | test("UserError", () => {
23 | const give = new UserError("hello", "world")
24 | const want = "hello"
25 | const have = errorMessage(give)
26 | assert.equal(have, want)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/text-runner-engine/src/errors/error.ts:
--------------------------------------------------------------------------------
1 | export function errorMessage(err: unknown): string {
2 | if (err instanceof Error) {
3 | return err.message
4 | } else {
5 | return String(err)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/text-runner-engine/src/errors/node-error.ts:
--------------------------------------------------------------------------------
1 | /** type guard that indicates errors of filesystem operations */
2 | export function isFsError(arg: unknown): arg is NodeJS.ErrnoException {
3 | return arg instanceof Error && "code" in arg
4 | }
5 |
--------------------------------------------------------------------------------
/text-runner-engine/src/errors/user-error.ts:
--------------------------------------------------------------------------------
1 | import * as files from "../filesystem/index.js"
2 |
3 | /**
4 | * Represents a UserError that has not been printed via the formatter.
5 | * This happens for user errors before the formatter could be instantiated
6 | */
7 | export class UserError extends Error {
8 | /** optional longer user-facing guidance on how to resolve the error */
9 | readonly guidance: string
10 | readonly location: files.Location | undefined
11 |
12 | constructor(message: string, guidance: string, location?: files.Location) {
13 | super(message)
14 | this.name = "UserError"
15 | this.guidance = guidance
16 | this.location = location
17 | }
18 | }
19 |
20 | export function isUserError(arg: unknown): arg is UserError {
21 | return arg instanceof Error && arg.name === "UserError"
22 | }
23 |
--------------------------------------------------------------------------------
/text-runner-engine/src/filesystem/full-dir.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path"
2 |
3 | /** represents the full path to a directory inside the document base, i.e. from the document root */
4 | export class FullDir {
5 | value: string
6 |
7 | constructor(value: string) {
8 | this.value = value
9 | }
10 |
11 | /**
12 | * Returns the path in the platform-specific format,
13 | * i.e. using '\' on Windows and '/' everywhere else
14 | */
15 | platformified(): string {
16 | return this.value.replace(/\//g, path.sep)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/text-runner-engine/src/filesystem/has-directory.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 |
3 | export async function hasDirectory(dirname: string): Promise {
4 | try {
5 | const stats = await fs.stat(dirname)
6 | return stats.isDirectory()
7 | } catch (e) {
8 | return false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/text-runner-engine/src/filesystem/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./absolute-dir.js"
2 | export * from "./absolute-file.js"
3 | export * from "./full-dir.js"
4 | export * from "./full-file.js"
5 | export * from "./full-link.js"
6 | export * from "./full-path.js"
7 | export * from "./get-filenames.js"
8 | export * from "./has-directory.js"
9 | export * from "./is-markdown-file.js"
10 | export * from "./location.js"
11 | export * from "./relative-dir.js"
12 | export * from "./relative-link.js"
13 | export * from "./source-dir.js"
14 | export * from "./unknown-link.js"
15 |
--------------------------------------------------------------------------------
/text-runner-engine/src/filesystem/is-markdown-file.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 |
3 | export async function isMarkdownFile(filepath: string): Promise {
4 | try {
5 | const fileStats = await fs.stat(filepath)
6 | return filepath.endsWith(".md") && fileStats.isFile()
7 | } catch (e) {
8 | return false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/text-runner-engine/src/filesystem/relative-dir.ts:
--------------------------------------------------------------------------------
1 | /** represents a relative path to another directory */
2 | export class RelativeDir {
3 | value: string
4 |
5 | constructor(value: string) {
6 | this.value = value
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/text-runner-engine/src/filesystem/relative-link.ts:
--------------------------------------------------------------------------------
1 | import * as configuration from "../configuration/index.js"
2 | import * as files from "./index.js"
3 |
4 | /**
5 | * A link relative to the current location,
6 | * i.e. a link not starting with '/'
7 | */
8 | export class RelativeLink {
9 | readonly value: string
10 |
11 | constructor(publicPath: string) {
12 | this.value = publicPath
13 | }
14 |
15 | /**
16 | * Assuming this relative link is in the given file,
17 | * returns the absolute links that point to the same target as this relative link.
18 | */
19 | absolutify(containingLocation: files.Location, publications: configuration.Publications): files.FullLink {
20 | const urlOfDir = containingLocation.file.directory().publicPath(publications)
21 | return urlOfDir.append(this)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/text-runner-engine/src/filesystem/unknown-link.ts:
--------------------------------------------------------------------------------
1 | import * as configuration from "../configuration/index.js"
2 | import * as helpers from "../helpers/index.js"
3 | import * as files from "./index.js"
4 |
5 | /**
6 | * A link that isn't known yet whether it is relative or absolute
7 | */
8 | export class UnknownLink {
9 | private readonly value: string
10 |
11 | constructor(publicPath: string) {
12 | this.value = helpers.removeDoubleSlash(helpers.unixify(publicPath))
13 | }
14 |
15 | absolutify(containingLocation: files.Location, publications: configuration.Publications): files.FullLink {
16 | if (this.isAbsolute()) {
17 | return new files.FullLink(this.value)
18 | }
19 | return new files.RelativeLink(this.value).absolutify(containingLocation, publications)
20 | }
21 |
22 | /**
23 | * Returns whether this link is an absolute link
24 | */
25 | isAbsolute(): boolean {
26 | return this.value.startsWith("/")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/add-leading-dot-unless-empty.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { addLeadingDotUnlessEmpty } from "./add-leading-dot-unless-empty.js"
5 |
6 | suite("addLeadingDotUnlessEmpty", () => {
7 | const tests = {
8 | "": "",
9 | ".foo": ".foo",
10 | foo: ".foo"
11 | }
12 | for (const [give, want] of Object.entries(tests)) {
13 | test(`${give} ==> ${want}`, () => {
14 | assert.equal(addLeadingDotUnlessEmpty(give), want)
15 | })
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/add-leading-dot-unless-empty.ts:
--------------------------------------------------------------------------------
1 | export function addLeadingDotUnlessEmpty(text: string): string {
2 | if (text === "") {
3 | return text
4 | }
5 | if (text.startsWith(".")) {
6 | return text
7 | }
8 | return "." + text
9 | }
10 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/add-leading-slash.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { addLeadingSlash } from "./add-leading-slash.js"
5 |
6 | suite("addLeadingSlash", () => {
7 | const tests = {
8 | "/foo": "/foo",
9 | foo: "/foo"
10 | }
11 | for (const [give, want] of Object.entries(tests)) {
12 | test(`${give} ==> ${want}`, () => {
13 | assert.equal(addLeadingSlash(give), want)
14 | })
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/add-leading-slash.ts:
--------------------------------------------------------------------------------
1 | export function addLeadingSlash(filepath: string): string {
2 | if (filepath[0] === "/") {
3 | return filepath
4 | } else {
5 | return "/" + filepath
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/add-trailing-slash.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { addTrailingSlash } from "./add-trailing-slash.js"
5 |
6 | suite("addTrailingSlash", () => {
7 | const tests = {
8 | foo: "foo/",
9 | "foo/": "foo/"
10 | }
11 | for (const [give, want] of Object.entries(tests)) {
12 | test(`${give} ==> ${want}`, () => {
13 | assert.equal(addTrailingSlash(give), want)
14 | })
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/add-trailing-slash.ts:
--------------------------------------------------------------------------------
1 | export function addTrailingSlash(text: string): string {
2 | if (text.endsWith("/")) {
3 | return text
4 | }
5 | return text + "/"
6 | }
7 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "../actions/name.js"
2 | export * from "./add-leading-dot-unless-empty.js"
3 | export * from "./add-leading-slash.js"
4 | export * from "./add-trailing-slash.js"
5 | export * from "./is-external-link.js"
6 | export * from "./is-link-to-anchor-in-other-file.js"
7 | export * from "./is-link-to-anchor-in-same-file.js"
8 | export * from "./is-mailto-link.js"
9 | export * from "./remove-double-slash.js"
10 | export * from "./remove-leading-slash.js"
11 | export * from "./remove-trailing-colon.js"
12 | export * from "./straighten-link.js"
13 | export * from "./trim-all-line-ends.js"
14 | export * from "./trim-extension.js"
15 | export * from "./unixify.js"
16 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-external-link.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { isExternalLink } from "./is-external-link.js"
5 |
6 | suite("isExternalLink", () => {
7 | test("link without protocol", () => {
8 | assert.isTrue(isExternalLink("//foo.com"))
9 | })
10 | test("link with protocol", () => {
11 | assert.isTrue(isExternalLink("http://foo.com"))
12 | })
13 | test("absolute link", () => {
14 | assert.isFalse(isExternalLink("/one/two.md"))
15 | })
16 | test("link to file in same dir", () => {
17 | assert.isFalse(isExternalLink("one.md"))
18 | })
19 | test("relative link", () => {
20 | assert.isFalse(isExternalLink("../one.md"))
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-external-link.ts:
--------------------------------------------------------------------------------
1 | import * as url from "url"
2 |
3 | export function isExternalLink(target: string): boolean {
4 | return target.startsWith("//") || !!url.parse(target).protocol
5 | }
6 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-link-to-anchor-in-other-file.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { isLinkToAnchorInOtherFile } from "./is-link-to-anchor-in-other-file.js"
5 |
6 | suite("isLinkToAnchorInOtherFile()", () => {
7 | const tests = {
8 | "#foo": false,
9 | "foo.md": false,
10 | "foo.md#bar": true,
11 | "https://foo.com/bar": false,
12 | "https://foo.com/bar#baz": false
13 | }
14 | for (const [give, want] of Object.entries(tests)) {
15 | test(`${give} is ${want}`, () => {
16 | assert.equal(isLinkToAnchorInOtherFile(give), want)
17 | })
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-link-to-anchor-in-other-file.ts:
--------------------------------------------------------------------------------
1 | export function isLinkToAnchorInOtherFile(target: string): boolean {
2 | return !target.startsWith("#") && !target.includes("://") && target.includes("#")
3 | }
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-link-to-anchor-in-same-file.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { isLinkToAnchorInSameFile } from "./is-link-to-anchor-in-same-file.js"
5 |
6 | suite("isLinkToAnchorInSameFile", () => {
7 | test("anchor in same file", () => {
8 | assert.isTrue(isLinkToAnchorInSameFile("#foo"))
9 | })
10 | test("anchor in other file", () => {
11 | assert.isFalse(isLinkToAnchorInSameFile("foo#bar"))
12 | })
13 | test("other file", () => {
14 | assert.isFalse(isLinkToAnchorInSameFile("foo.md"))
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-link-to-anchor-in-same-file.ts:
--------------------------------------------------------------------------------
1 | export function isLinkToAnchorInSameFile(target: string): boolean {
2 | return target.startsWith("#")
3 | }
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-mailto-link.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { isMailtoLink } from "./is-mailto-link.js"
5 |
6 | suite("isMailtoLink", () => {
7 | const tests = {
8 | foo: false,
9 | "mailto:foo@bar.com": true
10 | }
11 | for (const [give, want] of Object.entries(tests)) {
12 | test(`${give} ==> ${want}`, () => {
13 | assert.equal(isMailtoLink(give), want)
14 | })
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/is-mailto-link.ts:
--------------------------------------------------------------------------------
1 | export function isMailtoLink(target: string): boolean {
2 | return target.startsWith("mailto:")
3 | }
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/remove-double-slash.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { removeDoubleSlash } from "./remove-double-slash.js"
5 |
6 | suite("removeDoubleSlash", () => {
7 | const tests = {
8 | "/foo//bar/": "/foo/bar/"
9 | }
10 | for (const [give, want] of Object.entries(tests)) {
11 | test(`${give} ==> ${want}`, () => {
12 | assert.equal(removeDoubleSlash(give), want)
13 | })
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/remove-double-slash.ts:
--------------------------------------------------------------------------------
1 | const doubleSlashRE = /\/+/g
2 |
3 | /** Replaces multiple occurrences of '/' with a single slash */
4 | export function removeDoubleSlash(text: string): string {
5 | return text.replace(doubleSlashRE, "/")
6 | }
7 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/remove-leading-slash.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { removeLeadingSlash } from "./remove-leading-slash.js"
5 |
6 | suite("removeLeadingSlash", () => {
7 | const tests = {
8 | "/foo/bar/": "foo/bar/",
9 | "\\foo\\bar\\": "foo\\bar\\",
10 | "foo/bar/": "foo/bar/"
11 | }
12 | for (const [give, want] of Object.entries(tests)) {
13 | test(`${give} ==> ${want}`, () => {
14 | assert.equal(removeLeadingSlash(give), want)
15 | })
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/remove-leading-slash.ts:
--------------------------------------------------------------------------------
1 | export function removeLeadingSlash(text: string): string {
2 | if (!text.startsWith("/") && !text.startsWith("\\")) {
3 | return text
4 | }
5 | return text.slice(1)
6 | }
7 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/remove-trailing-colon.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { removeTrailingColon } from "./remove-trailing-colon.js"
5 |
6 | suite("removeTrailingColon", () => {
7 | const tests = {
8 | foo: "foo",
9 | "foo:": "foo"
10 | }
11 | for (const [give, want] of Object.entries(tests)) {
12 | test(`${give} ==> ${want}`, () => {
13 | assert.equal(removeTrailingColon(give), want)
14 | })
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/remove-trailing-colon.ts:
--------------------------------------------------------------------------------
1 | export function removeTrailingColon(text: string): string {
2 | if (text.endsWith(":")) {
3 | return text.substring(0, text.length - 1)
4 | } else {
5 | return text
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/straighten-link.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { straightenLink } from "./straighten-link.js"
5 |
6 | suite("straightenLink", () => {
7 | const tests = {
8 | "/foo": "/foo",
9 | "/one/../two": "/two",
10 | "/one/./././two/./": "/one/two/",
11 | "/one//../two": "/two",
12 | "/one/two/../three/../four": "/one/four",
13 | "/one/two/three/../../four": "/one/four"
14 | }
15 | for (const [give, want] of Object.entries(tests)) {
16 | test(`${give} ==> ${want}`, () => {
17 | assert.equal(straightenLink(give), want)
18 | })
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/straighten-link.ts:
--------------------------------------------------------------------------------
1 | const replaceDoubleSlashRE = /\/\//g
2 | const replaceDotRE = /\/\.\//
3 | const replaceDotDotRE = /\/[^/]+\/\.\.\//
4 |
5 | /** Removes intermediate directory expressions from the given link */
6 | export function straightenLink(link: string): string {
7 | let result = link.replace(replaceDoubleSlashRE, "/")
8 | while (result.includes("/./")) {
9 | result = result.replace(replaceDotRE, "/")
10 | }
11 | while (replaceDotDotRE.test(result)) {
12 | result = result.replace(replaceDotDotRE, "/")
13 | }
14 | return result
15 | }
16 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/trim-all-line-ends.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { trimAllLineEnds } from "./trim-all-line-ends.js"
5 |
6 | suite("trimAllLineEnds", () => {
7 | const tests = {
8 | hello: "hello",
9 | "one \n two ": "one\n two"
10 | }
11 | for (const [give, want] of Object.entries(tests)) {
12 | test(`${give} --> ${want}`, () => {
13 | assert.equal(trimAllLineEnds(give), want)
14 | })
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/trim-all-line-ends.ts:
--------------------------------------------------------------------------------
1 | /** Removes all whitespace at the end of each line in the given multi-line string */
2 | export function trimAllLineEnds(text: string): string {
3 | return text.replace(/[ ]+$/gm, "")
4 | }
5 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/trim-extension.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { trimExtension } from "./trim-extension.js"
5 | import { unixify } from "./unixify.js"
6 |
7 | suite("trimExtension()", () => {
8 | const tests = {
9 | "/one/two/three.ts": "/one/two/three"
10 | }
11 | for (const [give, want] of Object.entries(tests)) {
12 | test(`${give} ==> ${want}`, () => {
13 | assert.equal(unixify(trimExtension(give)), want)
14 | })
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/trim-extension.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path"
2 |
3 | export function trimExtension(filePath: string): string {
4 | return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)))
5 | }
6 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/unixify.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { unixify } from "./unixify.js"
5 |
6 | suite("unifixy", () => {
7 | const tests = {
8 | "/foo/bar/": "/foo/bar/",
9 | "/foo\\bar/": "/foo/bar/",
10 | "\\foo\\bar\\": "/foo/bar/"
11 | }
12 | for (const [give, want] of Object.entries(tests)) {
13 | test(`${give} ==> ${want}`, () => {
14 | assert.equal(unixify(give), want)
15 | })
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/text-runner-engine/src/helpers/unixify.ts:
--------------------------------------------------------------------------------
1 | const re = /\\/g
2 |
3 | /** Converts the given Windows path into a Unix path */
4 | export function unixify(text: string): string {
5 | return text.replace(re, "/")
6 | }
7 |
--------------------------------------------------------------------------------
/text-runner-engine/src/link-targets/find.ts:
--------------------------------------------------------------------------------
1 | import * as ast from "../ast/index.js"
2 | import { List } from "./list.js"
3 |
4 | export function find(nodeLists: ast.NodeList[]): List {
5 | const linkTargetList = new List()
6 | for (const nodeList of nodeLists) {
7 | linkTargetList.addNodeList(nodeList)
8 | }
9 | return linkTargetList
10 | }
11 |
--------------------------------------------------------------------------------
/text-runner-engine/src/link-targets/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./find.js"
2 | export * from "./list.js"
3 |
4 | /**
5 | * Target is a position in a Markdown file that links can point to:
6 | * headers or anchors
7 | */
8 | export interface Target {
9 | readonly level?: number
10 | readonly name: string
11 | readonly text?: string
12 | readonly type: Types
13 | }
14 |
15 | /** types of link targets */
16 | export type Types = "anchor" | "heading"
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/link-targets/target-url.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { targetURL } from "./target-url.js"
5 |
6 | suite("targetURL", () => {
7 | const tests = {
8 | CamelCase: "camelcase",
9 | "foo/bar-baz": "foobar-baz",
10 | hello: "hello",
11 | "identity & access": "identity--access"
12 | }
13 | for (const [give, want] of Object.entries(tests)) {
14 | test(`${give} --> ${want}`, () => {
15 | assert.equal(targetURL(give), want)
16 | })
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/text-runner-engine/src/link-targets/target-url.ts:
--------------------------------------------------------------------------------
1 | // @ts-expect-error: no types available
2 | import anchor from "anchor-markdown-header"
3 |
4 | export function targetURL(targetName: string): string {
5 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call
6 | const link = anchor(targetName, "github.com") as string
7 | return link.substring(link.indexOf("#") + 1, link.indexOf(")"))
8 | }
9 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/anchor/input.html:
--------------------------------------------------------------------------------
1 | A foo walks into a bar
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/anchor/input.md:
--------------------------------------------------------------------------------
1 | A foo walks into a bar
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/anchor/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "anchor_open",
12 | "tag": "a",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {
17 | "name": "foo",
18 | "type": "bar"
19 | }
20 | },
21 | {
22 | "type": "text",
23 | "tag": "",
24 | "file": "input.*",
25 | "line": 1,
26 | "content": "A foo walks into a bar",
27 | "attributes": {}
28 | },
29 | {
30 | "type": "anchor_close",
31 | "tag": "/a",
32 | "file": "input.*",
33 | "line": 1,
34 | "content": "",
35 | "attributes": {}
36 | },
37 | {
38 | "type": "paragraph_close",
39 | "tag": "/p",
40 | "file": "input.*",
41 | "line": 1,
42 | "content": "",
43 | "attributes": {}
44 | }
45 | ]
46 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/code-active/input.html:
--------------------------------------------------------------------------------
1 | code block
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/code-active/input.md:
--------------------------------------------------------------------------------
1 | code block
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/code-active/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "code_open",
12 | "tag": "code",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {
17 | "type": "foo"
18 | }
19 | },
20 | {
21 | "type": "text",
22 | "tag": "",
23 | "file": "input.*",
24 | "line": 1,
25 | "content": "code block",
26 | "attributes": {}
27 | },
28 | {
29 | "type": "code_close",
30 | "tag": "/code",
31 | "file": "input.*",
32 | "line": 1,
33 | "content": "",
34 | "attributes": {}
35 | },
36 | {
37 | "type": "paragraph_close",
38 | "tag": "/p",
39 | "file": "input.*",
40 | "line": 1,
41 | "content": "",
42 | "attributes": {}
43 | }
44 | ]
45 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/code-passive/input.html:
--------------------------------------------------------------------------------
1 | code block
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/code-passive/input.md:
--------------------------------------------------------------------------------
1 | `code block`
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/code-passive/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "code_open",
12 | "tag": "code",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {}
17 | },
18 | {
19 | "type": "text",
20 | "tag": "",
21 | "file": "input.*",
22 | "line": 1,
23 | "content": "code block",
24 | "attributes": {}
25 | },
26 | {
27 | "type": "code_close",
28 | "tag": "/code",
29 | "file": "input.*",
30 | "line": 1,
31 | "content": "",
32 | "attributes": {}
33 | },
34 | {
35 | "type": "paragraph_close",
36 | "tag": "/p",
37 | "file": "input.*",
38 | "line": 1,
39 | "content": "",
40 | "attributes": {}
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/complex-example/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello world!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/complex-example/input.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello **world**!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/em-active/input.html:
--------------------------------------------------------------------------------
1 | world
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/em-active/input.md:
--------------------------------------------------------------------------------
1 | world
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/em-active/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "em_open",
12 | "tag": "em",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {
17 | "type": "foo"
18 | }
19 | },
20 | {
21 | "type": "text",
22 | "tag": "",
23 | "file": "input.*",
24 | "line": 1,
25 | "content": "world",
26 | "attributes": {}
27 | },
28 | {
29 | "type": "em_close",
30 | "tag": "/em",
31 | "file": "input.*",
32 | "line": 1,
33 | "content": "",
34 | "attributes": {}
35 | },
36 | {
37 | "type": "paragraph_close",
38 | "tag": "/p",
39 | "file": "input.*",
40 | "line": 1,
41 | "content": "",
42 | "attributes": {}
43 | }
44 | ]
45 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/em-passive/input.html:
--------------------------------------------------------------------------------
1 | hello
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/em-passive/input.md:
--------------------------------------------------------------------------------
1 | _hello_
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/em-passive/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "em_open",
12 | "tag": "em",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {}
17 | },
18 | {
19 | "type": "text",
20 | "tag": "",
21 | "file": "input.*",
22 | "line": 1,
23 | "content": "hello",
24 | "attributes": {}
25 | },
26 | {
27 | "type": "em_close",
28 | "tag": "/em",
29 | "file": "input.*",
30 | "line": 1,
31 | "content": "",
32 | "attributes": {}
33 | },
34 | {
35 | "type": "paragraph_close",
36 | "tag": "/p",
37 | "file": "input.*",
38 | "line": 1,
39 | "content": "",
40 | "attributes": {}
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/heading-active/input.html:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/heading-active/input.md:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/heading-active/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "h1_open",
4 | "tag": "h1",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {
9 | "type": "foo"
10 | }
11 | },
12 | {
13 | "type": "text",
14 | "tag": "",
15 | "file": "input.*",
16 | "line": 1,
17 | "content": "Hello",
18 | "attributes": {}
19 | },
20 | {
21 | "type": "h1_close",
22 | "tag": "/h1",
23 | "file": "input.*",
24 | "line": 1,
25 | "content": "",
26 | "attributes": {}
27 | }
28 | ]
29 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/heading-passive/input.html:
--------------------------------------------------------------------------------
1 | Hello
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/heading-passive/input.md:
--------------------------------------------------------------------------------
1 | # Hello
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/heading-passive/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "h1_open",
4 | "tag": "h1",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "text",
12 | "tag": "",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "Hello",
16 | "attributes": {}
17 | },
18 | {
19 | "type": "h1_close",
20 | "tag": "/h1",
21 | "file": "input.*",
22 | "line": 1,
23 | "content": "",
24 | "attributes": {}
25 | }
26 | ]
27 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/image-active/input.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/image-active/input.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/image-active/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "image",
4 | "tag": "img",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {
9 | "src": "foo.png",
10 | "alt": "foo bar",
11 | "width": "100",
12 | "height": "200",
13 | "type": "bar"
14 | }
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/image-passive/input.html:
--------------------------------------------------------------------------------
1 | 
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/image-passive/input.md:
--------------------------------------------------------------------------------
1 | 
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/image-passive/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "image",
12 | "tag": "img",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {
17 | "src": "foo.png",
18 | "alt": "alt text"
19 | }
20 | },
21 | {
22 | "type": "paragraph_close",
23 | "tag": "/p",
24 | "file": "input.*",
25 | "line": 1,
26 | "content": "",
27 | "attributes": {}
28 | }
29 | ]
30 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/linebreak/input.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/linebreak/input.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/linebreak/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "attributes": {},
4 | "content": "",
5 | "file": "input.*",
6 | "line": 1,
7 | "tag": "br",
8 | "type": "linebreak"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/link-passive/input.html:
--------------------------------------------------------------------------------
1 | link title
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/link-passive/input.md:
--------------------------------------------------------------------------------
1 | [link title](link-target.md)
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/link-passive/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "attributes": {},
4 | "content": "",
5 | "file": "input.*",
6 | "line": 1,
7 | "tag": "p",
8 | "type": "paragraph_open"
9 | },
10 | {
11 | "attributes": { "href": "link-target.md" },
12 | "content": "",
13 | "file": "input.*",
14 | "line": 1,
15 | "tag": "a",
16 | "type": "link_open"
17 | },
18 | {
19 | "attributes": {},
20 | "content": "link title",
21 | "file": "input.*",
22 | "line": 1,
23 | "tag": "",
24 | "type": "text"
25 | },
26 | {
27 | "attributes": {},
28 | "content": "",
29 | "file": "input.*",
30 | "line": 1,
31 | "tag": "/a",
32 | "type": "link_close"
33 | },
34 | {
35 | "attributes": {},
36 | "content": "",
37 | "file": "input.*",
38 | "line": 1,
39 | "tag": "/p",
40 | "type": "paragraph_close"
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-active/input.html:
--------------------------------------------------------------------------------
1 |
2 | my code
3 |
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-active/input.md:
--------------------------------------------------------------------------------
1 |
2 | my code
3 |
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-active/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "fence_open",
4 | "tag": "pre",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {
9 | "type": "foo"
10 | }
11 | },
12 | {
13 | "type": "text",
14 | "tag": "",
15 | "file": "input.*",
16 | "line": 2,
17 | "content": "my code",
18 | "attributes": {}
19 | },
20 | {
21 | "type": "fence_close",
22 | "tag": "/pre",
23 | "file": "input.*",
24 | "line": 3,
25 | "content": "",
26 | "attributes": {}
27 | }
28 | ]
29 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-embedded/input.html:
--------------------------------------------------------------------------------
1 | a list with an
2 |
3 | indented code block
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-embedded/input.md:
--------------------------------------------------------------------------------
1 | - a list with an
2 |
3 | indented code block
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-passive/input.html:
--------------------------------------------------------------------------------
1 |
2 | my code
3 |
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-passive/input.md:
--------------------------------------------------------------------------------
1 | ```
2 | my code
3 | ```
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/pre-passive/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "fence_open",
4 | "tag": "pre",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "text",
12 | "tag": "",
13 | "file": "input.*",
14 | "line": 2,
15 | "content": "my code",
16 | "attributes": {}
17 | },
18 | {
19 | "type": "fence_close",
20 | "tag": "/pre",
21 | "file": "input.*",
22 | "line": 3,
23 | "content": "",
24 | "attributes": {}
25 | }
26 | ]
27 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/strong-active/input.html:
--------------------------------------------------------------------------------
1 | world
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/strong-active/input.md:
--------------------------------------------------------------------------------
1 | world
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/strong-active/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "strong_open",
12 | "tag": "strong",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {
17 | "type": "foo"
18 | }
19 | },
20 | {
21 | "type": "text",
22 | "tag": "",
23 | "file": "input.*",
24 | "line": 1,
25 | "content": "world",
26 | "attributes": {}
27 | },
28 | {
29 | "type": "strong_close",
30 | "tag": "/strong",
31 | "file": "input.*",
32 | "line": 1,
33 | "content": "",
34 | "attributes": {}
35 | },
36 | {
37 | "type": "paragraph_close",
38 | "tag": "/p",
39 | "file": "input.*",
40 | "line": 1,
41 | "content": "",
42 | "attributes": {}
43 | }
44 | ]
45 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/strong-passive/input.html:
--------------------------------------------------------------------------------
1 | world
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/strong-passive/input.md:
--------------------------------------------------------------------------------
1 | **world**
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/strong-passive/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "strong_open",
12 | "tag": "strong",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "",
16 | "attributes": {}
17 | },
18 | {
19 | "type": "text",
20 | "tag": "",
21 | "file": "input.*",
22 | "line": 1,
23 | "content": "world",
24 | "attributes": {}
25 | },
26 | {
27 | "type": "strong_close",
28 | "tag": "/strong",
29 | "file": "input.*",
30 | "line": 1,
31 | "content": "",
32 | "attributes": {}
33 | },
34 | {
35 | "type": "paragraph_close",
36 | "tag": "/p",
37 | "file": "input.*",
38 | "line": 1,
39 | "content": "",
40 | "attributes": {}
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/table/input.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/table/input.md:
--------------------------------------------------------------------------------
1 | | h1 | h2 |
2 | | --- | --- |
3 | | d1 | d2 |
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/text/input.html:
--------------------------------------------------------------------------------
1 | Hello world!
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/text/input.md:
--------------------------------------------------------------------------------
1 | Hello world!
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/fixtures/text/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "paragraph_open",
4 | "tag": "p",
5 | "file": "input.*",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {}
9 | },
10 | {
11 | "type": "text",
12 | "tag": "",
13 | "file": "input.*",
14 | "line": 1,
15 | "content": "Hello world!",
16 | "attributes": {}
17 | },
18 | {
19 | "type": "paragraph_close",
20 | "tag": "/p",
21 | "file": "input.*",
22 | "line": 1,
23 | "content": "",
24 | "attributes": {}
25 | }
26 | ]
27 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/html/fixtures/anchor-open/input.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/html/fixtures/anchor-open/result.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "anchor_open",
4 | "tag": "a",
5 | "file": "input.html",
6 | "line": 1,
7 | "content": "",
8 | "attributes": {
9 | "name": "foo",
10 | "type": "bar"
11 | }
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/html/fixtures/table/input.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/html/html-parser.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 | import * as parse5 from "parse5"
4 |
5 | import { standardizeHTMLAttributes } from "./html-parser.js"
6 |
7 | suite("standardizeHTMLAttributes", () => {
8 | test("values", () => {
9 | const input: parse5.Token.Attribute[] = [
10 | { name: "one", value: "1" },
11 | { name: "two", value: "2" }
12 | ]
13 | const expected = { one: "1", two: "2" }
14 | assert.deepEqual(standardizeHTMLAttributes(input), expected)
15 | })
16 |
17 | test("empty", () => {
18 | assert.deepEqual(standardizeHTMLAttributes([]), {})
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/html/index.ts:
--------------------------------------------------------------------------------
1 | export { Parser } from "./html-parser.js"
2 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/html/parse-html-files.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 |
3 | import * as ast from "../../ast/index.js"
4 | import * as files from "../../filesystem/index.js"
5 | import { TagMapper } from "../tag-mapper.js"
6 | import { Parser } from "./html-parser.js"
7 |
8 | /** returns the standard AST for the HTML files with the given paths */
9 | export async function parseHTMLFiles(
10 | filenames: files.FullFilePath[],
11 | sourceDir: files.SourceDir,
12 | tagMapper: TagMapper
13 | ): Promise {
14 | const result = []
15 | const parser = new Parser(tagMapper)
16 | for (const filename of filenames) {
17 | const content = await fs.readFile(sourceDir.joinFullFile(filename).platformified(), {
18 | encoding: "utf8"
19 | })
20 | result.push(parser.parse(content, new files.Location(sourceDir, filename, 1)))
21 | }
22 | return Promise.all(result)
23 | }
24 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/index.ts:
--------------------------------------------------------------------------------
1 | export * as html from "./html/index.js"
2 | export * as markdown from "./markdown/index.js"
3 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/markdown/fixtures/active-region/input.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | creating a file with name _one.txt_ and content `Hello world!`
4 |
5 |
6 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/markdown/fixtures/bullet_list/input.md:
--------------------------------------------------------------------------------
1 | * one
2 | * two
3 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/markdown/fixtures/complex-2/input.md:
--------------------------------------------------------------------------------
1 | Subscribe to our
2 | release feed to never miss a new
3 | release!
4 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/markdown/fixtures/user-input/input.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ```
4 | $ read foo
5 | $ echo You entered: $foo
6 | ```
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/markdown/index.ts:
--------------------------------------------------------------------------------
1 | export { MarkdownParser } from "./md-parser.js"
2 | export * from "./parse.js"
3 |
--------------------------------------------------------------------------------
/text-runner-engine/src/parsers/markdown/parse.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 |
3 | import * as ast from "../../ast/index.js"
4 | import * as files from "../../filesystem/index.js"
5 | import { MarkdownParser } from "./md-parser.js"
6 |
7 | /** returns the standard AST for the Markdown files given as paths relative to the given sourceDir */
8 | export async function parse(filenames: files.FullFilePath[], sourceDir: files.SourceDir): Promise {
9 | const result: ast.NodeList[] = []
10 | const parser = new MarkdownParser()
11 | for (const filename of filenames) {
12 | const content = await fs.readFile(sourceDir.joinStr(filename.platformified()), {
13 | encoding: "utf8"
14 | })
15 | result.push(parser.parse(content, sourceDir, filename))
16 | }
17 | return result
18 | }
19 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./activity-collector.js"
2 | export * from "./parallel.js"
3 | export * from "./sequential.js"
4 | export * from "./stopwatch.js"
5 |
6 | /** LogFn defines the signature of the "log" function available to actions */
7 | export type LogFn = (message?: any, ...optionalParams: any[]) => void
8 |
9 | /** signature of the method that allows actions to set a refined name for the current test step */
10 | export type RefineNameFn = (newName: string) => void
11 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/name-refiner.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import { NameRefiner } from "./name-refiner.js"
5 |
6 | suite("NameRefiner", () => {
7 | test("no refinements", () => {
8 | const refiner = new NameRefiner("original name")
9 | assert.equal(refiner.finalName(), "original name")
10 | })
11 |
12 | test("with refinements", () => {
13 | const refiner = new NameRefiner("original name")
14 | const refineFn = refiner.refineFn()
15 | refineFn("new name")
16 | refineFn("another name")
17 | assert.equal(refiner.finalName(), "another name")
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/name-refiner.ts:
--------------------------------------------------------------------------------
1 | import { RefineNameFn } from "./index.js"
2 |
3 | /** allows refining the current name of a test step */
4 | export class NameRefiner {
5 | // eslint-disable-next-line no-empty-function
6 | constructor(private name: string) {}
7 |
8 | /** returns the refined name */
9 | finalName(): string {
10 | return this.name
11 | }
12 |
13 | /** returns a function that can be called to refine the name */
14 | refineFn(): RefineNameFn {
15 | return this.refineName.bind(this)
16 | }
17 |
18 | /** updates the currently stored name to the given name */
19 | private refineName(newName: string) {
20 | this.name = newName
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/output-collector.ts:
--------------------------------------------------------------------------------
1 | import * as util from "util"
2 |
3 | import * as run from "./index.js"
4 |
5 | /** simulates console.log to collect output from a running action */
6 | export class OutputCollector {
7 | /** collects the received output */
8 | content: string[] = []
9 |
10 | /** appends to the output with a newline */
11 | log(...args: any[]): void {
12 | const stringified: string[] = []
13 | for (const arg of args) {
14 | if (typeof arg === "string") {
15 | stringified.push(arg)
16 | } else {
17 | stringified.push(util.inspect(arg, false, Infinity))
18 | }
19 | }
20 | this.content.push(stringified.join(" ") + "\n")
21 | }
22 |
23 | /** returns the "log" function to be used by actions */
24 | logFn(): run.LogFn {
25 | return this.log.bind(this)
26 | }
27 |
28 | /** returns the currently accumulated output */
29 | toString(): string {
30 | return this.content.join("")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/parallel.ts:
--------------------------------------------------------------------------------
1 | import * as actions from "../actions/index.js"
2 | import * as activities from "../activities/index.js"
3 | import * as commands from "../commands/index.js"
4 | import * as configuration from "../configuration/index.js"
5 | import * as linkTargets from "../link-targets/index.js"
6 | import { runActivity } from "./run-activity.js"
7 |
8 | /**
9 | * Executes the given activities in parallel.
10 | * Returns the errors they produce.
11 | */
12 | export function parallel(
13 | activities: activities.List,
14 | actionFinder: actions.Finder,
15 | targets: linkTargets.List,
16 | configuration: configuration.Data,
17 | emitter: commands.Command
18 | ): Promise[] {
19 | const result: Promise[] = []
20 | for (const activity of activities) {
21 | result.push(runActivity(activity, actionFinder, configuration, targets, emitter))
22 | }
23 | return result
24 | }
25 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/sequential.ts:
--------------------------------------------------------------------------------
1 | import * as actions from "../actions/index.js"
2 | import * as activities from "../activities/index.js"
3 | import * as commands from "../commands/index.js"
4 | import * as configuration from "../configuration/index.js"
5 | import * as linkTargets from "../link-targets/index.js"
6 | import { runActivity } from "./run-activity.js"
7 |
8 | export async function sequential(
9 | activities: activities.List,
10 | actionFinder: actions.Finder,
11 | configuration: configuration.Data,
12 | linkTargets: linkTargets.List,
13 | emitter: commands.Command
14 | ): Promise {
15 | for (const activity of activities) {
16 | const abort = await runActivity(activity, actionFinder, configuration, linkTargets, emitter)
17 | if (abort) {
18 | return
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/stopwatch.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai"
2 | import { suite, test } from "node:test"
3 |
4 | import * as run from "./index.js"
5 |
6 | suite("StopWatch", () => {
7 | test("less than 1s", () => {
8 | const stopWatch = new run.StopWatch()
9 | // @ts-ignore: access private member
10 | stopWatch.startTime -= 200
11 | assert.match(stopWatch.duration(), /2\d\dms/)
12 | })
13 |
14 | test("more than 1s", () => {
15 | const stopWatch = new run.StopWatch()
16 | // @ts-ignore: access private member
17 | stopWatch.startTime -= 2000
18 | assert.equal(stopWatch.duration(), "2s")
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/text-runner-engine/src/run/stopwatch.ts:
--------------------------------------------------------------------------------
1 | /** StopWatch allows to measure the difference between time periods */
2 | export class StopWatch {
3 | /** the time when this StopWatch was started */
4 | private startTime: number
5 |
6 | constructor() {
7 | this.startTime = new Date().getTime()
8 | }
9 |
10 | /**
11 | * Duration returns a human-readable description of the difference
12 | * between the current time and when this StopWatch was created.
13 | */
14 | duration(): string {
15 | const endTime = new Date().getTime()
16 | const milliseconds = endTime - this.startTime
17 | if (milliseconds > 1000) {
18 | return `${Math.round(milliseconds / 1000)}s`
19 | }
20 | return `${milliseconds}ms`
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/text-runner-engine/src/text-runner.ts:
--------------------------------------------------------------------------------
1 | export * as exports from "./actions/export.js"
2 | export * as actions from "./actions/index.js"
3 | export * as activities from "./activities/index.js"
4 | export * as ast from "./ast/index.js"
5 | export * as commands from "./commands/index.js"
6 | export * as configuration from "./configuration/index.js"
7 | export { errorMessage } from "./errors/error.js"
8 | export { isFsError } from "./errors/node-error.js"
9 | export { isUserError, UserError } from "./errors/user-error.js"
10 | export * as events from "./events/index.js"
11 | export * as files from "./filesystem/index.js"
12 | export * as helpers from "./helpers/index.js"
13 | export * as parsers from "./parsers/index.js"
14 | export { ActivityCollector, ActivityResults } from "./run/activity-collector.js"
15 |
--------------------------------------------------------------------------------
/text-runner-engine/src/workspace/create.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 |
3 | import * as configuration from "../configuration/index.js"
4 |
5 | /** creates the temp directory to run the tests in */
6 | export async function create(config: configuration.Data): Promise {
7 | const path = config.workspace.platformified()
8 | if (config.emptyWorkspace) {
9 | await fs.rm(path, { force: true, recursive: true })
10 | }
11 | await fs.mkdir(path, { recursive: true })
12 | }
13 |
--------------------------------------------------------------------------------
/text-runner-engine/src/workspace/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./create.js"
2 |
--------------------------------------------------------------------------------
/text-runner-engine/text-runner.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/kevgo/text-runner/refs/heads/main/documentation/text-runner.schema.json",
3 | "files": "**/*.md",
4 | "format": "dot",
5 | "exclude": [
6 | "examples",
7 | "src/parsers/fixtures",
8 | "src/parsers/markdown/fixtures",
9 | "tmp"
10 | ],
11 | "systemTmp": false
12 | }
13 |
--------------------------------------------------------------------------------
/text-runner-engine/textrun-shell.js:
--------------------------------------------------------------------------------
1 | import path from "path"
2 | import * as url from "url"
3 |
4 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url))
5 |
6 | export default {
7 | globals: {
8 | "text-runner": path.join(__dirname, "bin", "text-runner")
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/text-runner-engine/tsconfig-build.json:
--------------------------------------------------------------------------------
1 | // configuration for the compiler: ignore tests for increased speed
2 | // tests are type-checked and compiled when running the unit tests
3 | {
4 | "extends": "./tsconfig.json",
5 | "exclude": ["./src/**/*.test.ts"],
6 | "compilerOptions": {
7 | "outDir": "./dist",
8 | "declaration": true, /* Generates corresponding '.d.ts' file. */
9 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/text-runner-engine/tsconfig.json:
--------------------------------------------------------------------------------
1 | // configuration for the IDE: includes test code for global code navigation and refactoring
2 | {
3 | "extends": "../tsconfig.json",
4 | "include": ["./src/**/*.ts"]
5 | }
6 |
--------------------------------------------------------------------------------
/text-runner-features/actions/custom/README.md:
--------------------------------------------------------------------------------
1 | The examples are executed as documentation tests for these codebases. They don't
2 | need to be executed again here.
3 |
--------------------------------------------------------------------------------
/text-runner-features/actions/multiple-callbacks.feature:
--------------------------------------------------------------------------------
1 | Feature: multiple callbacks
2 |
3 | Scenario: synchronous action
4 | Given the source code contains a file "1.md" with content:
5 | """
6 |
7 |
8 | """
9 | And the source code contains a file "text-runner/multiple-callbacks.js" with content:
10 | """
11 | export default function (_, done) {
12 | done();
13 | done();
14 | }
15 | """
16 | When calling Text-Runner
17 | Then it runs these actions:
18 | | FILENAME | LINE | ACTION | STATUS |
19 | | 1.md | 1 | multiple-callbacks | success |
20 |
--------------------------------------------------------------------------------
/text-runner-features/actions/unknown.feature:
--------------------------------------------------------------------------------
1 | @smoke
2 | Feature: unknown action types
3 |
4 | Scenario: using an unknown action type
5 | Given the source code contains a file "1.md" with content:
6 | """
7 |
8 |
9 | """
10 | When calling Text-Runner
11 | Then it runs these actions:
12 | | FILENAME | LINE | STATUS | ERROR TYPE | ERROR MESSAGE | GUIDANCE |
13 | | 1.md | 1 | failed | UserError | unknown action: zonk | No custom actions defined.\n\nTo create a new "zonk" action,\nrun "text-runner scaffold zonk" |
14 | And the error provides the guidance:
15 | """
16 | No custom actions defined.
17 |
18 | To create a new "zonk" action,
19 | run "text-runner scaffold zonk"
20 | """
21 |
--------------------------------------------------------------------------------
/text-runner-features/commands/help-command.feature:
--------------------------------------------------------------------------------
1 | @cli
2 | Feature: help command
3 |
4 | Scenario:
5 | When running "text-runner help"
6 | Then it prints:
7 | """
8 | USAGE: .*
9 |
10 | COMMANDS
11 | """
12 |
--------------------------------------------------------------------------------
/text-runner-features/commands/unknown-command.feature:
--------------------------------------------------------------------------------
1 | @smoke
2 | @cli
3 | Feature: unknown command
4 |
5 | Scenario: running an unknown command via the CLI
6 | When trying to run "text-runner zonk"
7 | Then the test fails with:
8 | | ERROR MESSAGE | file or directory does not exist: zonk |
9 | | EXIT CODE | 1 |
10 |
--------------------------------------------------------------------------------
/text-runner-features/commands/unused.feature:
--------------------------------------------------------------------------------
1 | @cli
2 | Feature: show unused steps
3 |
4 | Scenario: the code base contains unused steps
5 | Given my workspace contains testable documentation
6 | And the source code contains the HelloWorld action
7 | When running "text-runner unused"
8 | Then it prints:
9 | """
10 | Unused activities:
11 | - hello-world
12 | """
13 |
--------------------------------------------------------------------------------
/text-runner-features/commands/version-command.feature:
--------------------------------------------------------------------------------
1 | @cli
2 | Feature: display the version
3 |
4 | Scenario: displaying the version
5 | When running "text-runner version"
6 | Then it prints:
7 | """
8 | TextRunner v\d+\.\d+\.\d+
9 | """
10 |
--------------------------------------------------------------------------------
/text-runner-features/configuration-file/no-config-file.feature:
--------------------------------------------------------------------------------
1 | Feature: Configuration file is optional
2 |
3 | Scenario: running without a configuration file
4 | Given I am in a directory that contains documentation without a configuration file
5 | When calling Text-Runner
6 | Then it runs without errors
7 |
--------------------------------------------------------------------------------
/text-runner-features/cucumber.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast",
3 | }
4 |
--------------------------------------------------------------------------------
/text-runner-features/empty-files/empty-directory.feature:
--------------------------------------------------------------------------------
1 | Feature: failing on empty directory
2 |
3 | Scenario: running inside an empty directory
4 | When calling Text-Runner
5 | Then it runs these actions:
6 | | STATUS | MESSAGE |
7 | | warning | no Markdown files found |
8 |
--------------------------------------------------------------------------------
/text-runner-features/empty-files/empty-file.feature:
--------------------------------------------------------------------------------
1 | Feature: running empty files
2 |
3 | Scenario: a documentation consisting of an empty file
4 | Given the workspace contains an empty file "empty.md"
5 | When calling Text-Runner
6 | Then it runs these actions:
7 | | STATUS | MESSAGE |
8 | | warning | no activities found |
9 |
--------------------------------------------------------------------------------
/text-runner-features/empty-files/non-actionable-tutorial.feature:
--------------------------------------------------------------------------------
1 | Feature: Fail on non-actionable Markdown
2 |
3 | Scenario: documentation with no actions
4 | Given the source code contains a file "1.md" with content:
5 | """
6 | Just text here, nothing to do!
7 | """
8 | When calling Text-Runner
9 | Then it runs these actions:
10 | | STATUS | MESSAGE |
11 | | warning | no activities found |
12 |
--------------------------------------------------------------------------------
/text-runner-features/formatters/elapsed-time.feature:
--------------------------------------------------------------------------------
1 | @cli
2 | Feature: display total test time
3 |
4 | Scenario: displaying the elapsed test time
5 | Given my workspace contains testable documentation
6 | When running Text-Runner
7 | Then it prints:
8 | """
9 | \d activities, \d+m?s
10 | """
11 |
--------------------------------------------------------------------------------
/text-runner-features/formatters/signals.feature:
--------------------------------------------------------------------------------
1 | @cli
2 | Feature: Formatter signals
3 |
4 | Scenario Outline: checking output of various formatters
5 | Given the source code contains a file "error.md" with content:
6 | """
7 |
8 | throw new Error("BOOM!")
9 |
10 | """
11 | When trying to run "text-runner --format="
12 | Then it signals:
13 | | FILENAME | error.md |
14 | | ERROR | BOOM! |
15 |
16 | Examples:
17 | | FORMATTER |
18 | | detailed |
19 | | dot |
20 | | progress |
21 | | summary |
22 |
--------------------------------------------------------------------------------
/text-runner-features/links/empty-links.feature:
--------------------------------------------------------------------------------
1 | Feature: recognize empty links
2 |
3 | Scenario: empty link
4 | Given the source code contains a file "1.md" with content:
5 | """
6 | An [empty link to an anchor]()
7 | """
8 | When calling Text-Runner
9 | Then it runs these actions:
10 | | FILENAME | LINE | ACTION | ACTIVITY | STATUS | ERROR TYPE | ERROR MESSAGE |
11 | | 1.md | 1 | check-link | Check link | failed | UserError | link without target |
12 |
--------------------------------------------------------------------------------
/text-runner-features/links/mailto.feature:
--------------------------------------------------------------------------------
1 | @online
2 | Feature: ignoring mailto links
3 |
4 | Scenario: mailto link
5 | Given the source code contains a file "1.md" with content:
6 | """
7 | A [working external link](mailto:foo@acme.com)
8 | """
9 | When calling Text-Runner
10 | Then it runs these actions:
11 | | FILENAME | LINE | ACTION | ACTIVITY |
12 | | 1.md | 1 | check-link | link to mailto:foo@acme.com |
13 |
--------------------------------------------------------------------------------
/text-runner-features/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-runner-features",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "ISC",
6 | "type": "module",
7 | "scripts": {
8 | "cuke": "cucumber-js --tags='(not @online) and (not @todo)' --format=progress --parallel=`node -e 'console.log(os.cpus().length)'` '**/*.feature'",
9 | "cuke:api": "cucumber-js --tags='(not @online) and (not @todo) and (not @cli)' --format=progress --parallel=`node -e 'console.log(os.cpus().length)'` '**/*.feature'",
10 | "cuke:cli": "cucumber-js --tags='(not @online) and (not @todo) and (@cli)' --format=progress --parallel=`node -e 'console.log(os.cpus().length)'` '**/*.feature'",
11 | "cuke:online": "cucumber-js --tags='(not @todo) and @online' --format=progress --parallel=`node -e 'console.log(os.cpus().length)`",
12 | "cuke:smoke": "cucumber-js --tags=@smoke --format=progress '**/*.feature'"
13 | },
14 | "devDependencies": {
15 | "shared-cucumber-steps": "0.0.0",
16 | "text-runner-engine": "7.1.2",
17 | "textrun-javascript": "0.3.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/text-runner-features/specifying-files/default-behavior.feature:
--------------------------------------------------------------------------------
1 | Feature: finding documentation files to run
2 |
3 | Scenario: the current directory contains Markdown files
4 | Given a runnable file "1.md"
5 | When calling Text-Runner
6 | Then it executes 1 test
7 |
8 | Scenario: the Markdown files are located in a subdirectory
9 | Given a runnable file "foo/1.md"
10 | When calling Text-Runner
11 | Then it executes 1 test
12 |
--------------------------------------------------------------------------------
/text-runner-features/specifying-files/glob-syntax.feature:
--------------------------------------------------------------------------------
1 | @smoke
2 | Feature: finding files in certain directories only
3 |
4 | Background:
5 | Given a runnable file "readme.md"
6 | And a runnable file "bar/1.md"
7 | And a runnable file "foo/1.md"
8 | And a runnable file "foo/2.md"
9 | And the configuration file:
10 | """
11 | {
12 | "files": "*.md"
13 | }
14 | """
15 |
16 | @cli
17 | Scenario: selecting files via CLI
18 | When running "text-runner foo/*.md"
19 | Then it runs only the tests in:
20 | | foo/1.md |
21 | | foo/2.md |
22 |
23 |
24 | Scenario: selecting files via API
25 | When calling:
26 | """
27 | command = new textRunner.commands.Run({...config, files: 'foo/*.md'})
28 | observer = new MyObserverClass(command)
29 | await command.execute()
30 | """
31 | Then it runs these actions:
32 | | FILENAME |
33 | | foo/1.md |
34 | | foo/2.md |
35 |
--------------------------------------------------------------------------------
/text-runner-features/specifying-files/ignoring-dependencies.feature:
--------------------------------------------------------------------------------
1 | Feature: ignoring dependencies
2 |
3 | Scenario: a code base with a node_modules folder
4 | Given a runnable file "creator.md"
5 | And a broken file "node_modules/zonk/broken.md"
6 | When calling Text-Runner
7 | Then it runs these actions:
8 | | FILENAME |
9 | | creator.md |
10 |
--------------------------------------------------------------------------------
/text-runner-features/specifying-files/ignoring-workspace.feature:
--------------------------------------------------------------------------------
1 | Feature: ignoring workspace files
2 |
3 | Scenario: a code base with existing workspace files
4 | Given the source code contains a file "source.md" with content:
5 | """
6 |
7 | """
8 | And the workspace contains a file "workspace.md" with content:
9 | """
10 |
11 | """
12 | When calling:
13 | """
14 | command = new textRunner.commands.Run({...config, emptyWorkspace: false})
15 | observer = new MyObserverClass(command)
16 | await command.execute()
17 | """
18 | Then it runs these actions:
19 | | FILENAME |
20 | | source.md |
21 |
--------------------------------------------------------------------------------
/text-runner-features/specifying-files/run-subdirectory.feature:
--------------------------------------------------------------------------------
1 | Feature: testing all docs in a subfolder
2 |
3 | Background:
4 | Given a runnable file "commands/foo.md"
5 | Given a runnable file "commands/bar/baz.md"
6 | And a runnable file "readme.md"
7 |
8 | @cli
9 | Scenario: testing all files in a subfolder via CLI
10 | When running "text-runner commands"
11 | Then it runs only the tests in:
12 | | commands/foo.md |
13 | | commands/bar/baz.md |
14 |
15 | Scenario: testing all files in a subfolder via API
16 | When calling:
17 | """
18 | command = new textRunner.commands.Run({...config, files: 'commands'})
19 | observer = new MyObserverClass(command)
20 | await command.execute()
21 | """
22 | Then it runs these actions:
23 | | FILENAME |
24 | | commands/bar/baz.md |
25 | | commands/foo.md |
26 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/abbr.feature:
--------------------------------------------------------------------------------
1 | Feature: active ABBR tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: active ABBR tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/anchor.feature:
--------------------------------------------------------------------------------
1 | Feature: active anchor tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: anchor tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
16 | Scenario: anchor block
17 | Given the source code contains a file "1.md" with content:
18 | """
19 |
20 | hello
21 |
22 | """
23 | When calling Text-Runner
24 | Then it runs these actions:
25 | | FILENAME | LINE | ACTION |
26 | | 1.md | 1 | hello-world |
27 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/b.feature:
--------------------------------------------------------------------------------
1 | Feature: active bold tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: bold tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/blockquote.feature:
--------------------------------------------------------------------------------
1 | Feature: active blockquote tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: blockquote tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/br.feature:
--------------------------------------------------------------------------------
1 | Feature: active BR tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: bold tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 |
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/center.feature:
--------------------------------------------------------------------------------
1 | Feature: active CENTER tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: active CENTER tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/code.feature:
--------------------------------------------------------------------------------
1 | Feature: active code tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: code tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/details.feature:
--------------------------------------------------------------------------------
1 | Feature: active code tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: code tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/div.feature:
--------------------------------------------------------------------------------
1 | Feature: div tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: code tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/em.feature:
--------------------------------------------------------------------------------
1 | Feature: active em tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: em tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/footnote.feature:
--------------------------------------------------------------------------------
1 | Feature: footnotes
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: code tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo[^1]
10 |
11 |
12 | [^1]: footnote text
13 | """
14 | When calling Text-Runner
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/h1.feature:
--------------------------------------------------------------------------------
1 | Feature: active h1 tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: H1 tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/h2.feature:
--------------------------------------------------------------------------------
1 | Feature: active h2 tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: H2 tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/h3.feature:
--------------------------------------------------------------------------------
1 | Feature: active h3 tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: H3 tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/h4.feature:
--------------------------------------------------------------------------------
1 | Feature: active h4 tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: H4 tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/h5.feature:
--------------------------------------------------------------------------------
1 | Feature: active h5 tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: H5 tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/h6.feature:
--------------------------------------------------------------------------------
1 | Feature: active h6 tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: H6 tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/hr.feature:
--------------------------------------------------------------------------------
1 | Feature: HR tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: active HR tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 |
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/i.feature:
--------------------------------------------------------------------------------
1 | Feature: active i tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: italic tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | hello
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/img.feature:
--------------------------------------------------------------------------------
1 | Feature: active img tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: image tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 |
10 | """
11 | And the workspace contains an image "watermelon.gif"
12 | When calling Text-Runner
13 | Then it runs these actions:
14 | | FILENAME | LINE | ACTION |
15 | | 1.md | 1 | hello-world |
16 | | 1.md | 1 | check-image |
17 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/kbd.feature:
--------------------------------------------------------------------------------
1 | Feature: KBD tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: code tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 | foo
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/link.feature:
--------------------------------------------------------------------------------
1 | Feature: active link tags
2 |
3 | Background:
4 | Given the source code contains the HelloWorld action
5 |
6 | Scenario: link tag
7 | Given the source code contains a file "1.md" with content:
8 | """
9 |
10 | """
11 | When calling Text-Runner
12 | Then it runs these actions:
13 | | FILENAME | LINE | ACTION |
14 | | 1.md | 1 | hello-world |
15 | | 1.md | 1 | check-link |
16 |
--------------------------------------------------------------------------------
/text-runner-features/tag-types/marquee.feature:
--------------------------------------------------------------------------------
1 | Feature: