├── .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 | foo bar 2 | -------------------------------------------------------------------------------- /text-runner-engine/src/parsers/fixtures/image-active/input.md: -------------------------------------------------------------------------------- 1 | foo bar 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 |

alt text

-------------------------------------------------------------------------------- /text-runner-engine/src/parsers/fixtures/image-passive/input.md: -------------------------------------------------------------------------------- 1 | ![alt text](foo.png) 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 | 2 | 3 |
h1h2
d1d2
-------------------------------------------------------------------------------- /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 | 2 | 3 | 4 | 5 |
123
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 | 9 | 10 | 11 | 12 |
123
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: tags 2 | 3 | Background: 4 | Given the source code contains the HelloWorld action 5 | 6 | Scenario: 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/ol.feature: -------------------------------------------------------------------------------- 1 | Feature: active OL tags 2 | 3 | Background: 4 | Given the source code contains the HelloWorld action 5 | 6 | Scenario: ordered list tag 7 | Given the source code contains a file "1.md" with content: 8 | """ 9 |
    10 |
  1. one
  2. 11 |
12 | """ 13 | When calling Text-Runner 14 | Then it runs these actions: 15 | | FILENAME | LINE | ACTION | 16 | | 1.md | 1 | hello-world | 17 | 18 | Scenario: LI tag inside an OL 19 | Given the source code contains a file "1.md" with content: 20 | """ 21 |
    22 |
  1. one
  2. 23 |
24 | """ 25 | When calling Text-Runner 26 | Then it runs these actions: 27 | | FILENAME | LINE | ACTION | 28 | | 1.md | 2 | hello-world | 29 | -------------------------------------------------------------------------------- /text-runner-features/tag-types/p.feature: -------------------------------------------------------------------------------- 1 | Feature: active P tags 2 | 3 | Scenario:

htmlblocks 4 | Given the source code contains a file "1.md" with content: 5 | """ 6 |

7 | foo 8 | bar 9 |

10 | """ 11 | When running Text-Runner 12 | -------------------------------------------------------------------------------- /text-runner-features/tag-types/pre.feature: -------------------------------------------------------------------------------- 1 | Feature: active pre tags 2 | 3 | Background: 4 | Given the source code contains the HelloWorld action 5 | 6 | Scenario: pre tag 7 | Given the source code contains a file "1.md" with content: 8 | """ 9 |
10 |       foo
11 |       
12 | """ 13 | When calling Text-Runner 14 | Then it runs these actions: 15 | | FILENAME | LINE | ACTION | 16 | | 1.md | 1 | hello-world | 17 | -------------------------------------------------------------------------------- /text-runner-features/tag-types/strong.feature: -------------------------------------------------------------------------------- 1 | Feature: active strong tags 2 | 3 | Background: 4 | Given the source code contains the HelloWorld action 5 | 6 | Scenario: strong 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/summary.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/sup.feature: -------------------------------------------------------------------------------- 1 | Feature: active SUP tags 2 | 3 | Background: 4 | Given the source code contains the HelloWorld action 5 | 6 | Scenario: active 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/ul.feature: -------------------------------------------------------------------------------- 1 | Feature: active UL tags 2 | 3 | Background: 4 | Given the source code contains the HelloWorld action 5 | 6 | Scenario: UL tag 7 | Given the source code contains a file "1.md" with content: 8 | """ 9 |
    10 |
  • one
  • 11 |
12 | """ 13 | When calling Text-Runner 14 | Then it runs these actions: 15 | | FILENAME | LINE | ACTION | 16 | | 1.md | 1 | hello-world | 17 | 18 | Scenario: unordered list item tag 19 | Given the source code contains a file "1.md" with content: 20 | """ 21 |
    22 |
  • one
  • 23 |
24 | """ 25 | When calling Text-Runner 26 | Then it runs these actions: 27 | | FILENAME | LINE | ACTION | 28 | | 1.md | 2 | hello-world | 29 | -------------------------------------------------------------------------------- /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 | "systemTmp": false 6 | } 7 | -------------------------------------------------------------------------------- /textrun-action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textrun-action", 3 | "version": "0.3.1", 4 | "license": "ISC", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "files": [ 8 | "dist/**/*.js" 9 | ], 10 | "scripts": { 11 | "build": "tsc -p tsconfig-build.json", 12 | "doc": "text-runner --format=dot", 13 | "fix": "eslint --fix --ignore-pattern=dist/ . && dprint fmt && sort-package-json --quiet", 14 | "lint": "dprint check && sort-package-json --check --quiet && eslint --ignore-pattern=dist/ . && depcheck --config=../.depcheckrc", 15 | "reset": "rm -rf dist && yarn run build" 16 | }, 17 | "dependencies": { 18 | "text-runner-engine": "7.1.2" 19 | }, 20 | "devDependencies": { 21 | "text-runner": "7.1.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /textrun-action/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner" 2 | 3 | import { nameFull } from "./name-full.js" 4 | import { nameShort } from "./name-short.js" 5 | 6 | export const textrunActions: textRunner.exports.TextrunActions = { 7 | nameFull, 8 | nameShort 9 | } 10 | -------------------------------------------------------------------------------- /textrun-action/src/name-full.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import * as textRunner from "text-runner-engine" 3 | 4 | export async function nameFull(action: textRunner.actions.Args): Promise { 5 | const want = action.region.text() 6 | action.name(`verify full name of action "${want}"`) 7 | const wantStd = textRunner.actions.name(want) 8 | const pkgJsonPath = action.configuration.sourceDir.joinStr("package.json") 9 | const pkgJson: textRunner.exports.PackageJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8")) 10 | const exportsPath = action.configuration.sourceDir.joinStr(pkgJson.exports) 11 | const main: textRunner.exports.IndexFile = await import(exportsPath) 12 | const allNames = Object.keys(main.textrunActions) 13 | const allNamesStd = allNames.map(textRunner.actions.name) 14 | if (!allNamesStd.includes(wantStd)) { 15 | throw new Error(`This module does not export action "${want}. Found [${allNames.join(", ")}] `) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /textrun-action/src/name-short.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import * as textRunner from "text-runner-engine" 3 | 4 | export async function nameShort(action: textRunner.actions.Args): Promise { 5 | const want = action.region.text() 6 | action.name(`verify short name of action "${want}"`) 7 | const wantStd = textRunner.actions.name(want) 8 | const pkgJsonPath = action.configuration.sourceDir.joinStr("package.json") 9 | const pkgJson: textRunner.exports.PackageJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8")) 10 | const exportsPath = action.configuration.sourceDir.joinStr(pkgJson.exports) 11 | const main: textRunner.exports.IndexFile = await import(exportsPath) 12 | const allNames = Object.keys(main.textrunActions) 13 | const allNamesStd = allNames.map(textRunner.actions.name) 14 | if (!allNamesStd.includes(wantStd)) { 15 | throw new Error(`This module does not export action "${want}. Found [${allNames.join(", ")}] `) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /textrun-action/text-runner/test-setup.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import * as textRunner from "text-runner" 3 | 4 | export default async function testSetup(action: textRunner.actions.Args): Promise { 5 | const codeBlocks = action.region.nodesOfTypes("code") 6 | if (codeBlocks.length !== 2) { 7 | throw new Error(`Expected 2 code blocks, got ${codeBlocks.length}`) 8 | } 9 | const pkgName = action.region.nodesFor(codeBlocks[0]).text() 10 | const action1 = action.region.nodesFor(codeBlocks[1]).text() 11 | await fs.writeFile( 12 | action.configuration.workspace.joinStr("package.json"), 13 | `\ 14 | { 15 | "name": "${pkgName}", 16 | "version": "0.0.0", 17 | "type": "module", 18 | "exports": "./index.js" 19 | }` 20 | ) 21 | await fs.writeFile( 22 | action.configuration.workspace.joinStr("index.js"), 23 | `\ 24 | export const textrunActions = { 25 | "${action1}": () => { console.log(1) } 26 | }` 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /textrun-action/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", "./text-runner/*.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 | -------------------------------------------------------------------------------- /textrun-action/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", "./text-runner/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /textrun-extension/cucumber.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast" 3 | } 4 | -------------------------------------------------------------------------------- /textrun-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textrun-extension", 3 | "version": "0.3.1", 4 | "description": "Text-Runner actions for verifying the documentation of Text-Runner extensions", 5 | "license": "ISC", 6 | "type": "module", 7 | "exports": "./dist/index.js", 8 | "files": [ 9 | "dist/**/*.js", 10 | "dist/**/*.d.ts" 11 | ], 12 | "scripts": { 13 | "build": "tsc -p tsconfig-build.json", 14 | "cuke": "cucumber-js --format=progress", 15 | "doc": "text-runner", 16 | "fix": "eslint --fix --ignore-pattern=dist/ . && dprint fmt && sort-package-json --quiet", 17 | "lint": "dprint check && sort-package-json --check --quiet && eslint --ignore-pattern=dist/ . && depcheck", 18 | "reset": "rm -rf dist && yarn run build", 19 | "unit": "node --test --import tsx 'src/**/*.test.ts'" 20 | }, 21 | "dependencies": { 22 | "text-runner-engine": "7.1.2" 23 | }, 24 | "devDependencies": { 25 | "shared-cucumber-steps": "*", 26 | "text-runner": "7.1.2", 27 | "textrun-action": "0.3.1", 28 | "tsx": "4.19.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /textrun-extension/src/actions/run-textrunner.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner-engine" 2 | import * as util from "util" 3 | 4 | /** runs Text-Runner in the workspace */ 5 | export async function runTextrunner(action: textRunner.actions.Args): Promise { 6 | action.name("Running Text-Runner in workspace") 7 | const command = new textRunner.commands.Run({ 8 | emptyWorkspace: false, 9 | sourceDir: action.configuration.workspace.joinStr(action.region[0].attributes["dir"] || "."), 10 | workspace: "." 11 | }) 12 | const activityCollector = new textRunner.ActivityCollector(command) 13 | await command.execute() 14 | for (const result of activityCollector.results()) { 15 | action.log(util.inspect(result, false, Infinity)) 16 | if (result.error) { 17 | throw result.error 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /textrun-extension/src/helpers/call-args.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { callArgs } from "./call-args.js" 5 | 6 | suite("callArgs", () => { 7 | test("on Windows", () => { 8 | const have = callArgs("bin/text-runner dynamic", "win32") 9 | assert.deepEqual(have, ["cmd", "/c", "bin\\text-runner dynamic"]) 10 | }) 11 | test("on Linux", () => { 12 | const have = callArgs("bin/text-runner dynamic", "linux") 13 | assert.deepEqual(have, ["sh", "-c", "bin/text-runner dynamic"]) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /textrun-extension/src/helpers/call-args.ts: -------------------------------------------------------------------------------- 1 | export function callArgs(command: string, platform: NodeJS.Platform): string[] { 2 | if (platform === "win32") { 3 | return ["cmd", "/c", command.replace(/\//g, "\\")] 4 | } else { 5 | return ["sh", "-c", command] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /textrun-extension/src/index.ts: -------------------------------------------------------------------------------- 1 | import { runTextrunner } from "./actions/run-textrunner.js" 2 | import { runnableRegion } from "./actions/runnable-region.js" 3 | import { callArgs } from "./helpers/call-args.js" 4 | 5 | const textrunActions = { runnableRegion, runTextrunner } 6 | 7 | export { callArgs, textrunActions } 8 | -------------------------------------------------------------------------------- /textrun-extension/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 | "tmp" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /textrun-extension/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 | -------------------------------------------------------------------------------- /textrun-extension/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 | -------------------------------------------------------------------------------- /textrun-javascript/cucumber.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast" 3 | } 4 | -------------------------------------------------------------------------------- /textrun-javascript/features/run-javascript.feature: -------------------------------------------------------------------------------- 1 | Feature: running inline regions of Javascript 2 | 3 | Scenario: missing code block 4 | Given the source code contains a file "1.md" with content: 5 | """ 6 | 7 | 8 | """ 9 | When calling Text-Runner 10 | Then it runs these actions: 11 | | FILENAME | LINE | ACTION | STATUS | ERROR TYPE | ERROR MESSAGE | 12 | | 1.md | 1 | javascript/runnable | failed | UserError | no JavaScript code found | 13 | 14 | 15 | Scenario: multiple code blocks 16 | Given the source code contains a file "1.md" with content: 17 | """ 18 | 19 | 20 | ``` 21 | let a = 1; 22 | ``` 23 | 24 | ``` 25 | let a = 2; 26 | ``` 27 | 28 | """ 29 | When calling Text-Runner 30 | Then it runs these actions: 31 | | FILENAME | LINE | ACTION | ACTIVITY | 32 | | 1.md | 1 | javascript/runnable | run JavaScript | 33 | -------------------------------------------------------------------------------- /textrun-javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textrun-javascript", 3 | "version": "0.3.1", 4 | "license": "ISC", 5 | "type": "module", 6 | "exports": "./dist/index.js", 7 | "files": [ 8 | "dist/**/*.js" 9 | ], 10 | "scripts": { 11 | "build": "tsc -p tsconfig-build.json", 12 | "cuke": "cucumber-js --format=progress", 13 | "doc": "text-runner --format=dot", 14 | "fix": "eslint --fix --ignore-pattern=dist/ . && dprint fmt && sort-package-json --quiet", 15 | "lint": "dprint check && sort-package-json --check --quiet && eslint --ignore-pattern=dist/ . && depcheck", 16 | "reset": "rm -rf dist && yarn run build", 17 | "unit": "node --test --import tsx 'src/**/*.test.ts'" 18 | }, 19 | "dependencies": { 20 | "text-runner-engine": "7.1.2" 21 | }, 22 | "devDependencies": { 23 | "shared-cucumber-steps": "*", 24 | "text-runner": "7.1.2", 25 | "textrun-extension": "0.3.1", 26 | "textrun-npm": "0.3.1", 27 | "tsx": "4.19.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /textrun-javascript/src/actions/non-runnable.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner-engine" 2 | 3 | /** checks the given JavaScript code for syntax errors. */ 4 | export function nonRunnable(action: textRunner.actions.Args): void { 5 | const code = action.region.text().trim() 6 | if (code.length === 0) { 7 | throw new Error("no JavaScript code found") 8 | } 9 | try { 10 | new Function(code) 11 | } catch (e) { 12 | throw new Error(`invalid Javascript: ${textRunner.errorMessage(e)}`) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/append-async-callback.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { appendAsyncCallback } from "./append-async-callback.js" 5 | 6 | suite("appendAsyncCallback", () => { 7 | test("synchronous code", () => { 8 | const give = `\ 9 | console.log(123)` 10 | const have = appendAsyncCallback(give) 11 | const want = `\ 12 | console.log(123); 13 | __finished();` 14 | assert.equal(have, want) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/append-async-callback.ts: -------------------------------------------------------------------------------- 1 | export function appendAsyncCallback(code: string): string { 2 | return `${code.trim()};\n__finished();` 3 | } 4 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/has-callback-placeholder.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { hasCallbackPlaceholder } from "./has-callback-placeholder.js" 5 | 6 | suite("hasCallbackPlaceholder", () => { 7 | const tests = { 8 | 'await fs.writeFile("foo", "bar")': false, 9 | 'fs.writeFile("foo", "bar", () => {\n // ...\n})': true, 10 | 'fs.writeFile("foo", "bar", )': true, 11 | 'fs.writeFile("foo", "bar", function() {\n // ...\n})': true 12 | } 13 | for (const [give, want] of Object.entries(tests)) { 14 | test(`${give} --> ${want}`, () => { 15 | assert.equal(hasCallbackPlaceholder(give), want) 16 | }) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/has-callback-placeholder.ts: -------------------------------------------------------------------------------- 1 | /** hasCallbackPlaceholder returns whether the given code block contains a callback placeholder. */ 2 | export function hasCallbackPlaceholder(code: string): boolean { 3 | return code.includes("") || code.includes("// ...") 4 | } 5 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/replace-async-callback.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { replaceAsyncCallback } from "./replace-async-callback.js" 5 | 6 | suite("replaceAsyncCallback", () => { 7 | test("", () => { 8 | const give = 'fs.writeFile("foo", "bar", )' 9 | const want = 'fs.writeFile("foo", "bar", __finished)' 10 | assert.equal(replaceAsyncCallback(give), want) 11 | }) 12 | test("// ...", () => { 13 | const give = `\ 14 | fs.writeFile("foo", "bar", () => { 15 | // ... 16 | })` 17 | const want = `\ 18 | fs.writeFile("foo", "bar", () => { 19 | __finished(); 20 | })` 21 | assert.equal(replaceAsyncCallback(give), want) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/replace-async-callback.ts: -------------------------------------------------------------------------------- 1 | export function replaceAsyncCallback(code: string): string { 2 | return code.replace("", "__finished").replace(/\/\/\s*\.\.\./g, "__finished();") 3 | } 4 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/replace-require-local-module.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { replaceRequireLocalModule } from "./replace-require-local-module.js" 5 | 6 | suite("replaceRequireLocalModule", () => { 7 | test("double-quotes", () => { 8 | const give = 'const foo = import(".")' 9 | const want = "const foo = import(process.cwd())" 10 | assert.equal(replaceRequireLocalModule(give), want) 11 | }) 12 | test("single-quotes", () => { 13 | const give = "const foo = import('.')" 14 | const want = "const foo = import(process.cwd())" 15 | assert.equal(replaceRequireLocalModule(give), want) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/replace-require-local-module.ts: -------------------------------------------------------------------------------- 1 | /** replaceRequireLocalModule makes sure "require('.') works as expected even if running in a temp workspace. */ 2 | export function replaceRequireLocalModule(code: string): string { 3 | return code.replace(/import\(['"].['"]\)/, "import(process.cwd())") 4 | } 5 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/replace-variable-declarations.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { replaceVariableDeclarations } from "./replace-variable-declarations.js" 5 | 6 | suite("replaceVariableDeclarations", () => { 7 | const tests = { 8 | "const foo = 123": "global.foo = 123", 9 | "let foo = 123": "global.foo = 123", 10 | "this.foo = 123": "global.foo = 123", 11 | "var foo = 123": "global.foo = 123" 12 | } 13 | for (const [give, want] of Object.entries(tests)) { 14 | test(`${give} --> ${want}`, () => { 15 | assert.equal(replaceVariableDeclarations(give), want) 16 | }) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /textrun-javascript/src/helpers/replace-variable-declarations.ts: -------------------------------------------------------------------------------- 1 | /** replaceVariableDeclarations makes variable declarations persist across code regions. */ 2 | export function replaceVariableDeclarations(code: string): string { 3 | return code 4 | .replace(/\bconst /g, "global.") 5 | .replace(/\blet /g, "global.") 6 | .replace(/\bvar /g, "global.") 7 | .replace(/\bthis\./g, "global.") 8 | } 9 | -------------------------------------------------------------------------------- /textrun-javascript/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner" 2 | 3 | import { nonRunnable } from "./actions/non-runnable.js" 4 | import { runnable } from "./actions/runnable.js" 5 | 6 | export const textrunActions: textRunner.exports.TextrunActions = { 7 | nonRunnable, 8 | runnable 9 | } 10 | -------------------------------------------------------------------------------- /textrun-javascript/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 | -------------------------------------------------------------------------------- /textrun-javascript/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 | -------------------------------------------------------------------------------- /textrun-make/cucumber.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast" 3 | } 4 | -------------------------------------------------------------------------------- /textrun-make/src/actions/command.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { getMakeTargets, trimDollar } from "./command.js" 5 | 6 | suite("getMakeTargets", () => { 7 | test("exact match", () => { 8 | const have = getMakeTargets("make foo") 9 | assert.deepEqual(have, ["foo"]) 10 | }) 11 | test("match inside block", () => { 12 | const give = "$ echo start\n$ make foo\n$echo done" 13 | const have = getMakeTargets(give) 14 | assert.deepEqual(have, ["foo"]) 15 | }) 16 | test("empty block", () => { 17 | assert.deepEqual(getMakeTargets(""), []) 18 | }) 19 | }) 20 | 21 | suite("trimDollar", () => { 22 | const tests = { 23 | "$ foo": "foo", 24 | "$ foo": "foo", 25 | foo: "foo" 26 | } 27 | for (const [give, want] of Object.entries(tests)) { 28 | test(`${give} --> ${want}`, () => { 29 | assert.equal(want, trimDollar(give)) 30 | }) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /textrun-make/src/actions/target.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as textRunner from "text-runner-engine" 4 | 5 | import { makefileTargets } from "../helpers/makefile-targets.js" 6 | 7 | /** verifies that the Makefile in the sourceDir contains the enclosed target */ 8 | export async function target(action: textRunner.actions.Args): Promise { 9 | const target = action.region.text().trim() 10 | if (target === "") { 11 | throw new Error("Empty make target") 12 | } 13 | action.name(`make target ${color.cyan(target)}`) 14 | const makePath = action.configuration.sourceDir.joinStr(action.region[0].attributes["dir"] || ".", "Makefile") 15 | const makefile = await fs.readFile(makePath, "utf8") 16 | const targets = makefileTargets(makefile) 17 | if (!targets.includes(target)) { 18 | throw new Error( 19 | `Makefile does not contain target ${color.cyan(target)} but these ones: ${color.cyan(targets.join(", "))}` 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /textrun-make/src/helpers/makefile-targets.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { makefileTargets } from "./makefile-targets.js" 5 | 6 | suite("makefileTargets", () => { 7 | test("with tabs", () => { 8 | const give = `\ 9 | foo: # builds the foo target 10 | \techo building foo 11 | 12 | bar: 13 | \techo building bar 14 | ` 15 | assert.deepEqual(makefileTargets(give), ["bar", "foo"]) 16 | }) 17 | 18 | test("line contains column", () => { 19 | const give = `\ 20 | help: # shows all available Make commands 21 | \tcat Makefile | grep '^[^ ]*:' | grep -v '.PHONY' | grep -v help | sed 's/:.*#/#/' | column -s "#" -t 22 | 23 | lint: # lints the code base 24 | ` 25 | assert.deepEqual(makefileTargets(give), ["help", "lint"]) 26 | }) 27 | 28 | test("Makefile contains hidden targets", () => { 29 | const give = `\ 30 | .PHONY: test 31 | 32 | foo:` 33 | assert.deepEqual(makefileTargets(give), ["foo"]) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /textrun-make/src/helpers/makefile-targets.ts: -------------------------------------------------------------------------------- 1 | /** provides the targets in the given Makefile content, sorted alphabetically */ 2 | export function makefileTargets(text: string): string[] { 3 | return text 4 | .split("\n") 5 | .filter((line: string) => !line.startsWith("\t")) 6 | .filter((line: string) => !line.startsWith(".")) 7 | .filter((line: string) => line.includes(":")) 8 | .map((line: string) => line.split(":")[0]) 9 | .map((line: string) => line.trim()) 10 | .filter((line: string) => line.length > 0) 11 | .sort() 12 | } 13 | -------------------------------------------------------------------------------- /textrun-make/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner" 2 | 3 | import { command } from "./actions/command.js" 4 | import { target } from "./actions/target.js" 5 | 6 | export const textrunActions: textRunner.exports.TextrunActions = { 7 | command, 8 | target 9 | } 10 | -------------------------------------------------------------------------------- /textrun-make/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 | "tmp" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /textrun-make/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 | -------------------------------------------------------------------------------- /textrun-make/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 | -------------------------------------------------------------------------------- /textrun-npm/cucumber.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast" 3 | } 4 | -------------------------------------------------------------------------------- /textrun-npm/src/actions/package-json.ts: -------------------------------------------------------------------------------- 1 | /** data structure of the package.json file as needed by this codebase */ 2 | export interface PackageJson { 3 | bin: Record 4 | name: string 5 | scripts: Record 6 | } 7 | -------------------------------------------------------------------------------- /textrun-npm/src/actions/script-name.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as textRunner from "text-runner-engine" 4 | 5 | import { PackageJson } from "./package-json.js" 6 | 7 | export async function scriptName(action: textRunner.actions.Args): Promise { 8 | const want = action.region.text().trim() 9 | if (want === "") { 10 | throw new Error("No script name specified") 11 | } 12 | action.name(`npm package has script ${color.cyan(want)}`) 13 | const packageJsonPath = action.configuration.sourceDir.joinStr("package.json") 14 | const pkgText = await fs.readFile(packageJsonPath, "utf-8") 15 | const pkgData: PackageJson = JSON.parse(pkgText) 16 | if (!Object.keys(pkgData.scripts).includes(want)) { 17 | throw new Error(`package.json does not have a "${want}" script`) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /textrun-npm/src/helpers/starts-with-npm-run.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { startsWithNpmRun } from "./starts-with-npm-run.js" 5 | 6 | suite("startsWithNpmRun", () => { 7 | const tests = { 8 | "npm run test": true 9 | } 10 | for (const [give, want] of Object.entries(tests)) { 11 | test(`${give} --> ${want}`, () => { 12 | assert.equal(want, startsWithNpmRun(give)) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /textrun-npm/src/helpers/starts-with-npm-run.ts: -------------------------------------------------------------------------------- 1 | export function startsWithNpmRun(text: string): boolean { 2 | return text.startsWith("npm run ") 3 | } 4 | -------------------------------------------------------------------------------- /textrun-npm/src/helpers/trim-dollar.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { trimDollar } from "./trim-dollar.js" 5 | 6 | suite("trimDollar", () => { 7 | const tests = { 8 | "$ foo": "foo", 9 | "$ foo": "foo", 10 | foo: "foo" 11 | } 12 | for (const [give, want] of Object.entries(tests)) { 13 | test(`${give} --> ${want}`, () => { 14 | assert.equal(want, trimDollar(give)) 15 | }) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /textrun-npm/src/helpers/trim-dollar.ts: -------------------------------------------------------------------------------- 1 | /** trims the leading dollar from the given command */ 2 | export function trimDollar(text: string): string { 3 | return text.replace(/^\$?\s*/, "") 4 | } 5 | -------------------------------------------------------------------------------- /textrun-npm/src/helpers/trim-npm-run.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { trimNpmRun } from "./trim-npm-run.js" 5 | 6 | suite("trimNpmRun", () => { 7 | const tests = { 8 | "npm run test": "test" 9 | } 10 | for (const [give, want] of Object.entries(tests)) { 11 | test(`${give} --> ${want}`, () => { 12 | assert.equal(want, trimNpmRun(give)) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /textrun-npm/src/helpers/trim-npm-run.ts: -------------------------------------------------------------------------------- 1 | export function trimNpmRun(call: string): string { 2 | return call.substring(8) 3 | } 4 | -------------------------------------------------------------------------------- /textrun-npm/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner" 2 | 3 | import { exportedExecutable } from "./actions/exported-executable.js" 4 | import { install } from "./actions/install.js" 5 | import { installedExecutable } from "./actions/installed-executable.js" 6 | import { scriptCall } from "./actions/script-call.js" 7 | import { scriptName } from "./actions/script-name.js" 8 | 9 | export const textrunActions: textRunner.exports.TextrunActions = { 10 | exportedExecutable, 11 | install, 12 | installedExecutable, 13 | scriptCall, 14 | scriptName 15 | } 16 | -------------------------------------------------------------------------------- /textrun-npm/text-runner/bundled-executable.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import * as path from "path" 3 | import * as textRunner from "text-runner" 4 | 5 | /** creates a binary with the given name in the workspace */ 6 | export default async function bundledExecutable(action: textRunner.actions.Args): Promise { 7 | const name = action.region.text() 8 | const filePath = action.configuration.workspace.joinStr(name) 9 | const dirPath = path.dirname(filePath) 10 | await fs.mkdir(dirPath) 11 | await fs.writeFile(filePath, "") 12 | } 13 | -------------------------------------------------------------------------------- /textrun-npm/text-runner/create-npm-executable.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import * as textRunner from "text-runner" 3 | 4 | /** creates a binary with the given name in the workspace */ 5 | export default async function createNPMExecutable(action: textRunner.actions.Args): Promise { 6 | const name = action.region.text() 7 | await fs.mkdir(action.configuration.workspace.joinStr("node_modules")) 8 | await fs.mkdir(action.configuration.workspace.joinStr("node_modules", ".bin")) 9 | const filePath = action.configuration.workspace.joinStr("node_modules", ".bin", name) 10 | await fs.writeFile(filePath, "", { mode: 0o744 }) 11 | } 12 | -------------------------------------------------------------------------------- /textrun-npm/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", "./text-runner/*.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 | -------------------------------------------------------------------------------- /textrun-npm/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", "./text-runner/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /textrun-repo/cucumber.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast" 3 | } 4 | -------------------------------------------------------------------------------- /textrun-repo/src/existing-file.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as textRunner from "text-runner-engine" 4 | 5 | export async function existingFile(action: textRunner.actions.Args): Promise { 6 | const fileName = action.region.text() 7 | action.name(`document mentions source code file ${color.cyan(fileName)}`) 8 | const fullPath = action.configuration.sourceDir.joinStr(fileName) 9 | action.log(`ls ${fullPath}`) 10 | try { 11 | var stats = await fs.stat(fullPath) 12 | } catch (err) { 13 | if (!textRunner.isFsError(err)) { 14 | throw err 15 | } 16 | if (err.code === "ENOENT") { 17 | throw new Error(`file not found: ${color.cyan(fileName)}`) 18 | } else { 19 | throw err 20 | } 21 | } 22 | if (stats.isDirectory()) { 23 | throw new Error(`expected ${color.cyan(fileName)} to be a file but is a directory`) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /textrun-repo/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner" 2 | 3 | import { executable } from "./executable.js" 4 | import { existingFileContent } from "./existing-file-content.js" 5 | import { existingFile } from "./existing-file.js" 6 | 7 | export const textrunActions: textRunner.exports.TextrunActions = { 8 | executable, 9 | existingFile, 10 | existingFileContent 11 | } 12 | -------------------------------------------------------------------------------- /textrun-repo/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 | "tmp" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /textrun-repo/text-runner/new-executable.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import * as path from "path" 3 | import * as textRunner from "text-runner" 4 | 5 | export async function newExecutable(action: textRunner.actions.Args): Promise { 6 | const name = action.region.text() 7 | const dirName = path.dirname(name) 8 | await fs.mkdir(path.join(action.configuration.workspace.platformified(), dirName)) 9 | await fs.writeFile(path.join(action.configuration.workspace.platformified(), name), "", { mode: 0o744 }) 10 | } 11 | -------------------------------------------------------------------------------- /textrun-repo/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", "./text-runner/*.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 | -------------------------------------------------------------------------------- /textrun-repo/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", "./text-runner/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /textrun-shell/cucumber.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast" 3 | } 4 | -------------------------------------------------------------------------------- /textrun-shell/features/exec/README.md: -------------------------------------------------------------------------------- 1 | The "global-tool" example is run as part of the documentation tests. 2 | -------------------------------------------------------------------------------- /textrun-shell/features/exec/multiple-commands.feature: -------------------------------------------------------------------------------- 1 | Feature: running multiple console commands 2 | 3 | Scenario: running multiple console commands 4 | Given the source code contains a file "running-multiple-commands.md" with content: 5 | """ 6 |
 7 |       mkdir one
 8 |       mkdir two
 9 |       
10 | """ 11 | When calling Text-Runner 12 | Then it runs these actions: 13 | | FILENAME | LINE | ACTION | ACTIVITY | 14 | | running-multiple-commands.md | 1 | shell/command | running console command: mkdir one && mkdir two | 15 | And the test workspace now contains a directory "one" 16 | And the test workspace now contains a directory "two" 17 | -------------------------------------------------------------------------------- /textrun-shell/features/exec/preceding-dollar-sign.feature: -------------------------------------------------------------------------------- 1 | Feature: marking console commands with preceding dollar signs 2 | 3 | Scenario: running console commands with dollar signs 4 | Given the source code contains a file "running-with-dollar-sign.md" with content: 5 | """ 6 |
 7 |       $ mkdir one
 8 |       $ mkdir two
 9 |       
10 | """ 11 | When calling Text-Runner 12 | Then it runs these actions: 13 | | FILENAME | LINE | ACTION | ACTIVITY | 14 | | running-with-dollar-sign.md | 1 | shell/command | running console command: mkdir one && mkdir two | 15 | And the test workspace now contains a directory "one" 16 | And the test workspace now contains a directory "two" 17 | -------------------------------------------------------------------------------- /textrun-shell/features/verify-console-command-output/verify-console-command-output.feature: -------------------------------------------------------------------------------- 1 | @smoke 2 | Feature: verifying the output of the last console command 3 | 4 | Scenario: verifying the output of a console command 5 | Given the source code contains a file "verify-output.md" with content: 6 | """ 7 | 8 | 9 | ``` 10 | echo one 11 | echo two 12 | echo three 13 | ``` 14 | 15 | 16 | 17 | 18 | ``` 19 | one 20 | three 21 | ``` 22 | 23 | """ 24 | When calling Text-Runner 25 | Then it runs these actions: 26 | | FILENAME | LINE | ACTION | ACTIVITY | 27 | | verify-output.md | 1 | shell/command | running console command: echo one && echo two && echo three | 28 | | verify-output.md | 10 | shell/command-output | verifying the output of the last run console command | 29 | -------------------------------------------------------------------------------- /textrun-shell/src/actions/server-output.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner-engine" 2 | 3 | import { CurrentServer } from "../helpers/current-server.js" 4 | 5 | /** 6 | * The "start-output" action waits until the currently running console command 7 | * produces the given output. 8 | */ 9 | export async function serverOutput(action: textRunner.actions.Args): Promise { 10 | action.name("verifying the output of the long-running process") 11 | const expectedOutput = action.region.textInNodeOfType("fence") 12 | const expectedLines = expectedOutput 13 | .split("\n") 14 | .map((line: string) => line.trim()) 15 | .filter((line: string) => line) 16 | const process = CurrentServer.instance().process 17 | if (!process) { 18 | throw new Error("Cannot verify process output since no process has been started") 19 | } 20 | for (const line of expectedLines) { 21 | action.log(`waiting for output: ${line}`) 22 | await process.output.waitForText(line) 23 | } 24 | action.log(process.output.fullText()) 25 | } 26 | -------------------------------------------------------------------------------- /textrun-shell/src/actions/server.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import * as observableProcess from "observable-process" 3 | import * as textRunner from "text-runner-engine" 4 | import * as trExt from "textrun-extension" 5 | 6 | import { CurrentServer } from "../helpers/current-server.js" 7 | import { trimDollar } from "../helpers/trim-dollar.js" 8 | 9 | /** 10 | * The "start" action runs the given commands on the console. 11 | * It leaves the command running. 12 | */ 13 | export function server(action: textRunner.actions.Args): void { 14 | const commandsToRun = action.region 15 | .text() 16 | .split("\n") 17 | .map((line: string) => line.trim()) 18 | .filter((line: string) => line.length > 0) 19 | .map(trimDollar) 20 | .join(" && ") 21 | action.name(`starting a server process: ${color.bold(color.cyan(commandsToRun))}`) 22 | CurrentServer.instance().set( 23 | observableProcess.start(trExt.callArgs(commandsToRun, process.platform), { 24 | cwd: action.configuration.workspace.platformified() 25 | }) 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /textrun-shell/src/actions/stop-server.ts: -------------------------------------------------------------------------------- 1 | import { endChildProcesses } from "end-child-processes" 2 | import * as textRunner from "text-runner-engine" 3 | 4 | import { CurrentServer } from "../helpers/current-server.js" 5 | 6 | /** 7 | * The "stop" action stops the currently running server process, 8 | * started by the "start" action. 9 | */ 10 | export async function stopServer(action: textRunner.actions.Args): Promise { 11 | action.name("stopping the long-running process") 12 | if (!CurrentServer.instance().hasProcess()) { 13 | throw new Error("No running process found") 14 | } 15 | await CurrentServer.instance().kill() 16 | await endChildProcesses() 17 | CurrentServer.instance().reset() 18 | } 19 | -------------------------------------------------------------------------------- /textrun-shell/src/helpers/configuration.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { Configuration } from "./configuration.js" 5 | 6 | suite("Configuration", () => { 7 | test("pathMapper", () => { 8 | const configFileContent = { globals: {} } 9 | const config = new Configuration(configFileContent) 10 | const pathMapper = config.pathMapper() 11 | assert.ok(pathMapper) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /textrun-shell/src/helpers/current-command.ts: -------------------------------------------------------------------------------- 1 | import * as observableProcess from "observable-process" 2 | 3 | /** CurrentCommand provides global access to the currently running console command. */ 4 | export class CurrentCommand { 5 | static instance(): observableProcess.FinishedProcess { 6 | if (!instance) { 7 | throw new Error("no instance") 8 | } 9 | return instance 10 | } 11 | 12 | static set(process: observableProcess.FinishedProcess): void { 13 | instance = process 14 | } 15 | } 16 | 17 | let instance: null | observableProcess.FinishedProcess = null 18 | -------------------------------------------------------------------------------- /textrun-shell/src/helpers/current-server.ts: -------------------------------------------------------------------------------- 1 | import * as observableProcess from "observable-process" 2 | 3 | /** CurrentServer provides global access to the currently running server process. */ 4 | export class CurrentServer { 5 | process: null | observableProcess.RunningProcess 6 | constructor() { 7 | this.process = null 8 | } 9 | 10 | static instance(): CurrentServer { 11 | return instance 12 | } 13 | 14 | hasProcess(): boolean { 15 | return this.process != null 16 | } 17 | 18 | async kill(): Promise { 19 | await this.process?.kill() 20 | } 21 | 22 | reset(): void { 23 | this.process = null 24 | } 25 | 26 | set(process: observableProcess.RunningProcess): void { 27 | this.process = process 28 | } 29 | } 30 | 31 | const instance = new CurrentServer() 32 | -------------------------------------------------------------------------------- /textrun-shell/src/helpers/path-mapper.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { PathMapper } from "./path-mapper.js" 5 | 6 | suite("PathMapper", () => { 7 | const mappings = { 8 | bar: "/three/four/bar", 9 | foo: "/one/two/foo" 10 | } 11 | const pathMapper = new PathMapper(mappings) 12 | const tests = { 13 | "bar README.md": "/three/four/bar README.md", 14 | "baz --online": "baz --online", 15 | "foo -b": "/one/two/foo -b" 16 | } 17 | // the method will be used as a higher-order function in the production code 18 | const globalizePath = pathMapper.globalizePathFunc() 19 | for (const [give, want] of Object.entries(tests)) { 20 | test(`makePath "${give}" --> "${want}"`, () => { 21 | const have = globalizePath(give) 22 | assert.equal(have, want) 23 | }) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /textrun-shell/src/helpers/path-mapper.ts: -------------------------------------------------------------------------------- 1 | import { PathMappings } from "./configuration.js" 2 | 3 | /** Absolutifier makes */ 4 | export class PathMapper { 5 | private mappings: PathMappings 6 | 7 | constructor(mappings: PathMappings) { 8 | this.mappings = mappings 9 | } 10 | 11 | globalizePathFunc(): (x: string) => string { 12 | return this.globalizePath.bind(this) 13 | } 14 | 15 | /** converts the given executable filename into its full path */ 16 | private globalizePath(command: string): string { 17 | const words = command.split(" ") 18 | const hit = this.mappings[words[0]] 19 | if (!hit) { 20 | return command 21 | } 22 | words[0] = hit 23 | return words.join(" ") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /textrun-shell/src/helpers/trim-dollar.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai" 2 | import { suite, test } from "node:test" 3 | 4 | import { trimDollar } from "./trim-dollar.js" 5 | 6 | suite("trimDollar", () => { 7 | const tests = { 8 | "$ foo": "foo", 9 | "$ foo": "foo", 10 | foo: "foo" 11 | } 12 | for (const [give, want] of Object.entries(tests)) { 13 | test(`${give} --> ${want}`, () => { 14 | assert.equal(want, trimDollar(give)) 15 | }) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /textrun-shell/src/helpers/trim-dollar.ts: -------------------------------------------------------------------------------- 1 | /** trims the leading dollar from the given command */ 2 | export function trimDollar(text: string): string { 3 | return text.replace(/^\$?\s*/, "") 4 | } 5 | -------------------------------------------------------------------------------- /textrun-shell/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner" 2 | 3 | import { commandOutput } from "./actions/command-output.js" 4 | import { commandWithInput } from "./actions/command-with-input.js" 5 | import { command } from "./actions/command.js" 6 | import { serverOutput } from "./actions/server-output.js" 7 | import { server } from "./actions/server.js" 8 | import { stopServer } from "./actions/stop-server.js" 9 | 10 | export const textrunActions: textRunner.exports.TextrunActions = { 11 | command, 12 | commandOutput, 13 | commandWithInput, 14 | server, 15 | serverOutput, 16 | stopServer 17 | } 18 | -------------------------------------------------------------------------------- /textrun-shell/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 | } 7 | -------------------------------------------------------------------------------- /textrun-shell/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, "node_modules", ".bin", "text-runner") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /textrun-shell/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 | -------------------------------------------------------------------------------- /textrun-shell/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 | -------------------------------------------------------------------------------- /textrun-workspace/cucumber.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: "--import '../shared/cucumber-steps/dist/*.js' --fail-fast" 3 | } 4 | -------------------------------------------------------------------------------- /textrun-workspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textrun-workspace", 3 | "version": "0.3.1", 4 | "description": "Text-Runner actions for the Text-Runner workspace", 5 | "license": "ISC", 6 | "type": "module", 7 | "exports": "./dist/index.js", 8 | "files": [ 9 | "dist/**/*.js" 10 | ], 11 | "scripts": { 12 | "build": "tsc -p tsconfig-build.json", 13 | "cuke": "cucumber-js --format=progress", 14 | "doc": "text-runner", 15 | "fix": "eslint --fix --ignore-pattern=dist/ . && dprint fmt && sort-package-json --quiet", 16 | "lint": "dprint check && sort-package-json --check --quiet && eslint --ignore-pattern=dist/ . && depcheck", 17 | "reset": "rm -rf dist && yarn run build" 18 | }, 19 | "dependencies": { 20 | "assert-no-diff": "4.1.0", 21 | "colorette": "2.0.20", 22 | "text-runner-engine": "7.1.2" 23 | }, 24 | "devDependencies": { 25 | "shared-cucumber-steps": "*", 26 | "text-runner": "7.1.2", 27 | "textrun-action": "0.3.1", 28 | "textrun-extension": "0.3.1", 29 | "tsx": "4.19.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /textrun-workspace/src/actions/additional-file-content.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as path from "path" 4 | import * as textRunner from "text-runner-engine" 5 | 6 | export async function additionalFileContent(action: textRunner.actions.Args): Promise { 7 | const fileName = action.region.textInNodeOfType("em", "strong") 8 | const fileRelPath = path.join(action.region[0].attributes["dir"] || ".", fileName) 9 | action.name(`append to file ${color.cyan(fileRelPath)}`) 10 | const content = action.region.textInNodeOfType("fence", "code") 11 | const fullPath = action.configuration.workspace.joinStr(fileRelPath) 12 | action.log(content) 13 | await fs.appendFile(fullPath, content) 14 | } 15 | -------------------------------------------------------------------------------- /textrun-workspace/src/actions/empty-file.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as path from "path" 4 | import * as textRunner from "text-runner-engine" 5 | 6 | export async function emptyFile(action: textRunner.actions.Args): Promise { 7 | const fileName = action.region.text().trim() 8 | if (fileName === "") { 9 | throw new textRunner.UserError("No filename given", "") 10 | } 11 | const filePath = path.join(action.region[0].attributes["dir"] ?? ".", fileName) 12 | action.name(`create file ${color.cyan(filePath)}`) 13 | const fullPath = action.configuration.workspace.joinStr(filePath) 14 | action.log(`create file ${filePath}`) 15 | await fs.mkdir(path.dirname(fullPath), { recursive: true }) 16 | await fs.writeFile(fullPath, "") 17 | } 18 | -------------------------------------------------------------------------------- /textrun-workspace/src/actions/existing-directory.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as path from "path" 4 | import * as textRunner from "text-runner-engine" 5 | 6 | /** 7 | * The "directory" action verifies that the test workspace 8 | * contains the given directory. 9 | */ 10 | export async function existingDirectory(action: textRunner.actions.Args): Promise { 11 | const dirName = action.region.text() 12 | const dirRelName = path.join(action.region[0].attributes["dir"] || ".", dirName) 13 | action.name(`directory ${color.cyan(dirRelName)} exists in the workspace`) 14 | const fullPath = action.configuration.workspace.joinStr(dirRelName) 15 | try { 16 | var stats = await fs.lstat(fullPath) 17 | } catch (err) { 18 | throw new Error(`directory ${color.cyan(color.bold(dirRelName))} does not exist in the workspace`) 19 | } 20 | if (!stats.isDirectory()) { 21 | throw new Error(`${color.cyan(dirRelName)} exists but is not a directory`) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /textrun-workspace/src/actions/existing-file.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as path from "path" 4 | import * as textRunner from "text-runner-engine" 5 | 6 | export async function existingFile(action: textRunner.actions.Args): Promise { 7 | const fileName = action.region.text() 8 | const fileRelPath = path.join(action.region[0].attributes["dir"] || ".", fileName) 9 | action.name(`verify existence of file ${color.cyan(fileRelPath)}`) 10 | const fullPath = action.configuration.workspace.joinStr(fileRelPath) 11 | try { 12 | await fs.stat(fullPath) 13 | } catch (e) { 14 | if (!textRunner.isFsError(e)) { 15 | throw e 16 | } 17 | if (e.code === "ENOENT") { 18 | const files = await fs.readdir(action.configuration.sourceDir.platformified()) 19 | throw new textRunner.UserError( 20 | `file not found: ${fileRelPath}`, 21 | `the workspace has these files: ${files.join(", ")}` 22 | ) 23 | } else { 24 | throw e 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /textrun-workspace/src/actions/new-directory.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import { promises as fs } from "fs" 3 | import * as path from "path" 4 | import * as textRunner from "text-runner-engine" 5 | 6 | export async function newDirectory(action: textRunner.actions.Args): Promise { 7 | const dirName = action.region.text().trim() 8 | if (!dirName) { 9 | throw new Error("empty directory name given") 10 | } 11 | const dirRelName = path.join(action.region[0].attributes["dir"] || ".", dirName) 12 | action.name(`create directory ${color.cyan(dirRelName)}`) 13 | const fullPath = action.configuration.workspace.joinStr(dirRelName) 14 | await fs.mkdir(fullPath, { recursive: true }) 15 | } 16 | -------------------------------------------------------------------------------- /textrun-workspace/src/actions/working-dir.ts: -------------------------------------------------------------------------------- 1 | import * as color from "colorette" 2 | import * as textRunner from "text-runner-engine" 3 | 4 | /** The "cd" action changes the current working directory to the one given in the hyperlink or code block. */ 5 | export function workingDir(action: textRunner.actions.Args): void { 6 | const directory = action.region.text() 7 | action.name(`changing into the ${color.cyan(directory)} directory`) 8 | const fullPath = action.configuration.workspace.joinStr(directory) 9 | action.log(`cd ${fullPath}`) 10 | try { 11 | process.chdir(fullPath) 12 | } catch (e) { 13 | if (!textRunner.isFsError(e)) { 14 | throw e 15 | } 16 | if (e.code === "ENOENT") { 17 | throw new Error(`directory ${directory} not found`) 18 | } 19 | throw e 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /textrun-workspace/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as textRunner from "text-runner" 2 | 3 | import { additionalFileContent } from "./actions/additional-file-content.js" 4 | import { emptyFile } from "./actions/empty-file.js" 5 | import { existingDirectory } from "./actions/existing-directory.js" 6 | import { existingFileWithContent } from "./actions/existing-file-with-content.js" 7 | import { existingFile } from "./actions/existing-file.js" 8 | import { newDirectory } from "./actions/new-directory.js" 9 | import { newFile } from "./actions/new-file.js" 10 | import { workingDir } from "./actions/working-dir.js" 11 | 12 | export const textrunActions: textRunner.exports.TextrunActions = { 13 | additionalFileContent, 14 | emptyFile, 15 | existingDirectory, 16 | existingFile, 17 | existingFileWithContent, 18 | newDirectory, 19 | newFile, 20 | workingDir 21 | } 22 | -------------------------------------------------------------------------------- /textrun-workspace/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 | "tmp" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /textrun-workspace/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 | -------------------------------------------------------------------------------- /textrun-workspace/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 | -------------------------------------------------------------------------------- /tools/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevgo/text-runner/8aa3f5d9fbf24776eae6516b96ebde4863ad33a2/tools/.gitkeep -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "cache": false 7 | }, 8 | "cuke": { 9 | "dependsOn": ["build"], 10 | "cache": false 11 | }, 12 | "doc": { 13 | "dependsOn": ["build"], 14 | "cache": false 15 | }, 16 | "fix": { 17 | "cache": false 18 | }, 19 | "lint": { 20 | "cache": false 21 | }, 22 | "test": { 23 | "dependsOn": ["build", "unit", "cuke", "doc"], 24 | "cache": false 25 | }, 26 | "reset": { 27 | "cache": false 28 | }, 29 | "unit": { 30 | "dependsOn": ["build"], 31 | "cache": false 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------