├── .eslintrc.js ├── .github └── workflows │ ├── deploy-docs.yaml │ ├── node.js.yml │ └── test-deploy-docs.yaml ├── .gitignore ├── .prettierignore ├── .release-it.json ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── architecture.png ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── develop │ │ ├── _category_.yml │ │ ├── api │ │ │ ├── _category_.yml │ │ │ ├── classes │ │ │ │ ├── Bluehawk.md │ │ │ │ ├── Document.md │ │ │ │ ├── RootParser.md │ │ │ │ └── _category_.yml │ │ │ ├── index.md │ │ │ ├── interfaces │ │ │ │ ├── ActionArgs.md │ │ │ │ ├── AnyTag.md │ │ │ │ ├── BlockCommentTokenConfiguration.md │ │ │ │ ├── BlockTagNode.md │ │ │ │ ├── CheckArgs.md │ │ │ │ ├── CheckResult.md │ │ │ │ ├── CopyArgs.md │ │ │ │ ├── EmphasizeRange.md │ │ │ │ ├── EmphasizeSourceAttributes.md │ │ │ │ ├── IParser.md │ │ │ │ ├── IVisitor.md │ │ │ │ ├── LanguageSpecification.md │ │ │ │ ├── LineTagNode.md │ │ │ │ ├── ListStatesArgs.md │ │ │ │ ├── ListTagArgs.md │ │ │ │ ├── ParseResult.md │ │ │ │ ├── PayloadQuery.md │ │ │ │ ├── PluginArgs.md │ │ │ │ ├── ProcessOptions.md │ │ │ │ ├── ProcessRequest.md │ │ │ │ ├── Project.md │ │ │ │ ├── PushParserPayload.md │ │ │ │ ├── PushParserTokenConfiguration.md │ │ │ │ ├── SnipArgs.md │ │ │ │ ├── VisitorResult.md │ │ │ │ └── _category_.yml │ │ │ ├── modules.md │ │ │ └── namespaces │ │ │ │ ├── _category_.yml │ │ │ │ ├── getBluehawk.md │ │ │ │ └── tokens.md │ │ └── plugins.md │ ├── guides │ │ ├── _category_.yml │ │ ├── code-snippets.md │ │ ├── continuous-integration.md │ │ └── tutorials.md │ ├── install.md │ ├── intro.md │ ├── reference │ │ ├── _category_.yml │ │ ├── cli.md │ │ └── tags.md │ └── use-cases.md ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ ├── HomepageFeatures.js │ │ └── HomepageFeatures.module.css │ └── css │ │ └── custom.css └── static │ ├── .nojekyll │ └── img │ ├── docusaurus.png │ ├── favicon.ico │ ├── logo.svg │ ├── tutorial │ ├── docsVersionDropdown.png │ └── localeDropdown.png │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ └── undraw_docusaurus_tree.svg ├── integrationTests ├── .gitignore ├── README.md ├── copy │ ├── expected │ │ ├── state1 │ │ │ └── sample.txt │ │ └── state2 │ │ │ └── sample.txt │ ├── input │ │ └── sample.txt │ └── test.sh ├── logLevel │ ├── expected │ │ ├── logLevel0 │ │ ├── logLevel1 │ │ ├── logLevel2 │ │ └── logLevel3 │ ├── input │ │ ├── normalFile.txt │ │ └── sample.txt │ └── test.sh ├── runTests.sh └── snip │ ├── expected │ ├── sample.snippet.c-test.c │ ├── sample.snippet.cpp-test.cpp │ ├── sample.snippet.failing-cpp-test.cpp │ ├── sample.snippet.go-test.go │ ├── sample.snippet.multi-line-block-comment.cpp │ ├── sample.snippet.php-octothorp-test.php │ ├── sample.snippet.php-slash-test.php │ ├── sample.snippet.php-test.php │ ├── sample.snippet.python-test.py │ ├── sample.snippet.ruby-test.rb │ ├── sample.snippet.rust-test.rs │ ├── sample.snippet.sc-test.sc │ ├── sample.snippet.scala-test.scala │ ├── sample.snippet.single-line-block-comment.cpp │ └── sample.snippet.test-one.txt │ ├── input │ ├── sample.c │ ├── sample.cpp │ ├── sample.go │ ├── sample.php │ ├── sample.py │ ├── sample.rb │ ├── sample.rs │ ├── sample.sc │ ├── sample.scala │ └── sample.txt │ └── test.sh ├── jest.config.js ├── package-lock.json ├── package.json ├── patches └── chevrotain+7.1.2.patch ├── src ├── bluehawk │ ├── BluehawkError.ts │ ├── Document.test.ts │ ├── Document.ts │ ├── Location.ts │ ├── OnBinaryFileFunction.ts │ ├── OnErrorFunction.ts │ ├── Plugin.ts │ ├── Range.ts │ ├── actions │ │ ├── ActionArgs.ts │ │ ├── ActionReporter.ts │ │ ├── ConsoleActionReporter.test.ts │ │ ├── ConsoleActionReporter.ts │ │ ├── check.ts │ │ ├── copy.test.ts │ │ ├── copy.ts │ │ ├── index.ts │ │ ├── listStates.ts │ │ ├── listTags.ts │ │ ├── printJsonResult.ts │ │ ├── snip.test.ts │ │ └── snip.ts │ ├── bluehawk.test.ts │ ├── bluehawk.ts │ ├── getBluehawk.ts │ ├── index.ts │ ├── io │ │ ├── System.ts │ │ ├── messageHandler.test.ts │ │ └── messageHandler.ts │ ├── options.ts │ ├── parser │ │ ├── ErrorMessageProvider.ts │ │ ├── LanguageSpecification.ts │ │ ├── ParseResult.ts │ │ ├── ParserStore.test.ts │ │ ├── ParserStore.ts │ │ ├── RootParser.ts │ │ ├── TagNode.ts │ │ ├── extractTagNamesFromTokens.ts │ │ ├── flatten.test.ts │ │ ├── flatten.ts │ │ ├── index.ts │ │ ├── lexer │ │ │ ├── index.ts │ │ │ ├── lexer.test.ts │ │ │ ├── makeAttributeListMode.ts │ │ │ ├── makeBlockCommentTokens.ts │ │ │ ├── makeLexer.ts │ │ │ ├── makeLineCommentToken.ts │ │ │ ├── makePayloadPattern.ts │ │ │ ├── makePushParserTokens.ts │ │ │ ├── makeRootMode.ts │ │ │ ├── makeStringLiteralToken.ts │ │ │ ├── tokenCategoryFilter.ts │ │ │ └── tokens.ts │ │ ├── locationFromToken.ts │ │ ├── makeAttributesConformToJsonSchemaRule.test.ts │ │ ├── makeAttributesConformToJsonSchemaRule.ts │ │ ├── makeParser.ts │ │ ├── parser.test.ts │ │ ├── validator.test.ts │ │ ├── validator.ts │ │ └── visitor │ │ │ ├── index.ts │ │ │ ├── innerOffsetToOuterLocation.ts │ │ │ ├── jsonAttributeList.test.ts │ │ │ ├── jsonErrorToVisitorError.ts │ │ │ ├── makeCstVisitor.ts │ │ │ ├── multilang.test.ts │ │ │ └── visitor.test.ts │ ├── processor │ │ ├── Processor.test.ts │ │ ├── Processor.ts │ │ └── index.ts │ ├── project │ │ ├── Project.ts │ │ ├── index.ts │ │ ├── loadProjectPaths.test.ts │ │ └── loadProjectPaths.ts │ └── tags │ │ ├── EmphasizeTag.ts │ │ ├── RemoveTag.ts │ │ ├── ReplaceTag.ts │ │ ├── SnippetTag.ts │ │ ├── StateRemoveTag.ts │ │ ├── StateTag.ts │ │ ├── Tag.ts │ │ ├── UncommentTag.ts │ │ ├── emphasizeTag.test.ts │ │ ├── index.ts │ │ ├── removeMetaRange.test.ts │ │ ├── removeMetaRange.ts │ │ ├── removeTag.test.ts │ │ ├── replaceTag.test.ts │ │ ├── snippetTag.test.ts │ │ ├── stateRemoveTag.test.ts │ │ ├── stateTag.test.ts │ │ └── uncommentTag.test.ts ├── cli │ ├── cli.ts │ ├── commandModules │ │ ├── check.ts │ │ ├── copy.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── list │ │ │ ├── index.ts │ │ │ ├── states.ts │ │ │ └── tags.ts │ │ └── snip.ts │ └── index.ts ├── index.ts └── main.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | root: true, 6 | parser: "@typescript-eslint/parser", 7 | plugins: ["@typescript-eslint", "eslint-plugin-tsdoc"], 8 | extends: [ 9 | "eslint:recommended", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | ], 14 | rules: { 15 | "tsdoc/syntax": "warn", 16 | }, 17 | ignorePatterns: ["**/build/*"], 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | deploy: 9 | name: Deploy to GitHub Pages 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: 20.x 16 | cache: npm 17 | - name: Install Bluehawk dependencies 18 | run: | 19 | npm ci 20 | - name: Build website 21 | working-directory: docs 22 | run: | 23 | npm ci 24 | npm run build 25 | 26 | # Popular action to deploy to GitHub Pages: 27 | # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus 28 | - name: Deploy to GitHub Pages 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | # Build output to publish to the `gh-pages` branch: 33 | publish_dir: ./docs/build 34 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [21.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | - run: cd integrationTests && ./runTests.sh 31 | -------------------------------------------------------------------------------- /.github/workflows/test-deploy-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Test docs deployment 2 | 3 | on: 4 | # runs on PR to main branch 5 | push: 6 | branches: ["!main"] 7 | pull_request: 8 | branches: [main] 9 | jobs: 10 | test-deploy: 11 | name: Test deployment 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 20.x 18 | cache: npm 19 | - name: Install Bluehawk dependencies 20 | run: | 21 | npm ci 22 | - name: Test build 23 | working-directory: docs 24 | run: | 25 | npm ci 26 | npm run build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | **/output 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | .parcel-cache 82 | 83 | # Next.js build output 84 | .next 85 | out 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # yarn v2 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .yarn/install-state.gz 120 | .pnp.* 121 | 122 | # TS build 123 | build/ 124 | 125 | # vscode 126 | .vscode/ 127 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | test/input 3 | test/expected 4 | test/output -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "pushRepo": "upstream" 4 | }, 5 | "github": { 6 | "release": true, 7 | "tokenRef": "RELEASE_IT_GITHUB_TOKEN" 8 | }, 9 | "hooks": { 10 | "before:init": ["npm run clean", "npm run test"], 11 | "after:bump": "npm run build" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.3.0", 6 | "configurations": [ 7 | { 8 | "name": "Run Tests", 9 | "request": "launch", 10 | "runtimeArgs": ["run", "test"], 11 | "runtimeExecutable": "npm", 12 | "cwd": "${workspaceFolder}", 13 | "skipFiles": ["/**"], 14 | "type": "node" 15 | }, 16 | { 17 | "name": "Run TS Tests", 18 | "request": "launch", 19 | "runtimeArgs": ["run", "ts-test"], 20 | "runtimeExecutable": "npm", 21 | "skipFiles": ["/**"], 22 | "type": "node" 23 | }, 24 | { 25 | "name": "Run JS Tests", 26 | "request": "launch", 27 | "runtimeArgs": ["run", "js-test"], 28 | "runtimeExecutable": "npm", 29 | "skipFiles": ["/**"], 30 | "type": "node" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "restructuredtext.confPath": "" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 (May 20, 2021) 4 | 5 | The 1.0.0 release contains the following breaking changes: 6 | 7 | - Change the flag for specifying where generated files are deposited from `-d/--destination` 8 | to `-o/--output`. 9 | - Remove the `code-block` alias for the `snippet` tag. Now you may only use `snippet`. 10 | 11 | The release also includes a variety of internal API changes, bug fixes, 12 | and reporting improvements. 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Run Bluehawk from Source 2 | 3 | To build and run Bluehawk from source, clone this repo and install dependencies: 4 | 5 | ```sh 6 | npm install 7 | ``` 8 | 9 | To build, run: 10 | 11 | ``` 12 | npm run build 13 | ``` 14 | 15 | If compilation is successful, you can run bluehawk like so: 16 | 17 | ```sh 18 | node build/src/main.js snip -o 19 | ``` 20 | 21 | Which you can alias as: 22 | 23 | ```sh 24 | alias bluehawk-dev="node /path/to/bluehawk/build/src/main.js" 25 | ``` 26 | 27 | # Running Tests 28 | 29 | This project uses Jest to test. 30 | 31 | To run all tests, use: 32 | 33 | ```sh 34 | npm test 35 | ``` 36 | 37 | To run the tests and get verbose output for all unit tests, use: 38 | 39 | ```sh 40 | npm run verbose 41 | ``` 42 | 43 | Additionally, you can get a Jest coverage report with: 44 | 45 | ```sh 46 | npm run coverage 47 | ``` 48 | 49 | You can also run tests with breakpoints in VS Code with F5. See .vscode/launch.json. 50 | 51 | # Architecture 52 | 53 | ![Graphical overview of the Bluehawk architecture](https://raw.githubusercontent.com/mongodb-university/Bluehawk/main/architecture.png "Bluehawk Architecture") 54 | 55 | Bluehawk has three major components: 56 | 57 | - **Client:** loads files to be parsed and processed. Implements listeners that 58 | decide what to do with results (e.g. save to file). Can add custom tags 59 | and language specifications (i.e. comment syntax). The main client is the CLI, 60 | but you can use Bluehawk as a library and implement your own clients. 61 | - **Parser:** finds tags in a source file and diagnoses markup errors. 62 | - **Processor:** executes tags on a source file to produce transformed documents. 63 | 64 | ## File Language-Specific Tokens 65 | 66 | The lexer can receive custom tokens for a given language to define comment 67 | syntax. For example, plaintext has no comments, bash only has line comments 68 | (#)[†](https://stackoverflow.com/questions/32126653/how-does-end-work-in-bash-to-create-a-multi-line-comment-block), 69 | and C++ has line (//) and block (`/*`, `*/`) comments. Bluehawk is comment aware so 70 | that it can correctly strip comments (when needed) and diagnose when tags 71 | are halfway in a block comment. 72 | 73 | ## Tag Tokens 74 | 75 | ":snippet-start:", ":remove-start:", etc. are not keywords. Instead, the 76 | lexer and parser detect [tag], [tag]-start, and [tag]-end. It is up 77 | to the visitor to determine whether the -start and -end tag names match and 78 | if the tag name is understood by Bluehawk. This keeps the lexer and parser 79 | simpler and allows for extensibility of Bluehawk as users could eventually 80 | provide their own tags. 81 | 82 | ## Attribute Lists 83 | 84 | The original Bluehawk spec document included the ability to provide a JSON 85 | object after a block tag to configure the block tag's attributes. The 86 | lexer has "modes" so after it encounters a block tag, it goes into an 87 | attribute mode, which will either accept the tag identifier (i.e. 88 | :some-tag-start: this-is-the-identifier) or the attribute list JSON. 89 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 MongoDB, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bluehawk 2 | 3 | [![npm version](https://badge.fury.io/js/bluehawk.svg)](https://badge.fury.io/js/bluehawk) 4 | 5 | Bluehawk is a markup processor for extracting and manipulating arbitrary code. 6 | With Bluehawk, you can: 7 | 8 | - Extract code examples for use in documentation 9 | - Generate formatted code examples for use in documentation 10 | - Replace "finished" code with "todo" code for a branch in a tutorial repo 11 | 12 | > 💡 See our [Documentation](https://mongodb-university.github.io/Bluehawk/) or 13 | > [open an issue](https://github.com/mongodb-university/Bluehawk/issues/new) 14 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/Bluehawk/6c5f2612111c3f6813b66fe95d5d05f07ff9e9e1/architecture.png -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | # /docs/develop/api <- note must not include these generated files in the .gitignore 11 | # for the plugin to work properly :/ 12 | 13 | # Misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a documentation-focused static website generator. 4 | 5 | The website also uses the plugin [docusaurus-plugin-typedoc](https://www.npmjs.com/package/docusaurus-plugin-typedoc) 6 | to generate the reference documentation in the `docs/api` directory. 7 | 8 | ## Installation 9 | 10 | From the root of the Bluehawk repo: 11 | 12 | ```shell 13 | npm install 14 | cd docs 15 | npm install 16 | ``` 17 | 18 | ## Local Development 19 | 20 | From the `/docs` folder: 21 | 22 | ```shell 23 | npm start 24 | ``` 25 | 26 | This tag starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 27 | 28 | ## Build 29 | 30 | From the `/docs` folder: 31 | 32 | ```shell 33 | npm run build 34 | ``` 35 | 36 | This tag generates static content into the `build` directory and can be served using any static contents hosting service. 37 | 38 | ## Deployment 39 | 40 | The docs autodeploy with a Github action. If you need to manually deploy the docs, run this tag from the `/docs` folder: 41 | 42 | ```shell 43 | GIT_USER= USE_SSH=true npm run deploy 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/develop/_category_.yml: -------------------------------------------------------------------------------- 1 | position: 6 2 | label: "Develop" -------------------------------------------------------------------------------- /docs/docs/develop/api/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "API" -------------------------------------------------------------------------------- /docs/docs/develop/api/classes/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Classes" 2 | position: 3 -------------------------------------------------------------------------------- /docs/docs/develop/api/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "index" 3 | title: "bluehawk" 4 | slug: "/develop/api/" 5 | sidebar_label: "Readme" 6 | sidebar_position: 0 7 | custom_edit_url: null 8 | --- 9 | 10 | # Bluehawk 11 | 12 | [![npm version](https://badge.fury.io/js/bluehawk.svg)](https://badge.fury.io/js/bluehawk) 13 | 14 | Bluehawk is a markup processor for extracting and manipulating arbitrary code. 15 | With Bluehawk, you can: 16 | 17 | - Extract code examples for use in documentation 18 | - Generate formatted code examples for use in documentation 19 | - Replace "finished" code with "todo" code for a branch in a tutorial repo 20 | 21 | > 💡 See our [Documentation](https://mongodb-university.github.io/Bluehawk/) or 22 | > [open an issue](https://github.com/mongodb-university/Bluehawk/issues/new) 23 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/ActionArgs.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "ActionArgs" 3 | title: "Interface: ActionArgs" 4 | sidebar_label: "ActionArgs" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - **`ActionArgs`** 12 | 13 | ↳ [`CheckArgs`](CheckArgs) 14 | 15 | ↳ [`CopyArgs`](CopyArgs) 16 | 17 | ↳ [`ListStatesArgs`](ListStatesArgs) 18 | 19 | ↳ [`ListTagArgs`](ListTagArgs) 20 | 21 | ↳ [`SnipArgs`](SnipArgs) 22 | 23 | ## Properties 24 | 25 | ### logLevel 26 | 27 | • `Optional` **logLevel**: `LogLevel` 28 | 29 | #### Defined in 30 | 31 | [src/bluehawk/actions/ActionArgs.ts:3](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L3) 32 | 33 | ___ 34 | 35 | ### waitForListeners 36 | 37 | • `Optional` **waitForListeners**: `boolean` 38 | 39 | #### Defined in 40 | 41 | [src/bluehawk/actions/ActionArgs.ts:4](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L4) 42 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/AnyTag.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "AnyTag" 3 | title: "Interface: AnyTag" 4 | sidebar_label: "AnyTag" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - `Tag` 12 | 13 | ↳ **`AnyTag`** 14 | 15 | ## Properties 16 | 17 | ### attributesSchema 18 | 19 | • `Optional` **attributesSchema**: `AnySchema` 20 | 21 | #### Inherited from 22 | 23 | Tag.attributesSchema 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/tags/Tag.ts:15](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/Tag.ts#L15) 28 | 29 | ___ 30 | 31 | ### description 32 | 33 | • `Optional` **description**: `string` 34 | 35 | #### Inherited from 36 | 37 | Tag.description 38 | 39 | #### Defined in 40 | 41 | [src/bluehawk/tags/Tag.ts:12](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/Tag.ts#L12) 42 | 43 | ___ 44 | 45 | ### name 46 | 47 | • **name**: `string` 48 | 49 | #### Inherited from 50 | 51 | Tag.name 52 | 53 | #### Defined in 54 | 55 | [src/bluehawk/tags/Tag.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/Tag.ts#L9) 56 | 57 | ___ 58 | 59 | ### rules 60 | 61 | • `Optional` **rules**: `Rule`[] 62 | 63 | #### Inherited from 64 | 65 | Tag.rules 66 | 67 | #### Defined in 68 | 69 | [src/bluehawk/tags/Tag.ts:19](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/Tag.ts#L19) 70 | 71 | ___ 72 | 73 | ### supportsBlockMode 74 | 75 | • **supportsBlockMode**: `boolean` 76 | 77 | #### Defined in 78 | 79 | [src/bluehawk/tags/Tag.ts:26](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/Tag.ts#L26) 80 | 81 | ___ 82 | 83 | ### supportsLineMode 84 | 85 | • **supportsLineMode**: `boolean` 86 | 87 | #### Defined in 88 | 89 | [src/bluehawk/tags/Tag.ts:27](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/Tag.ts#L27) 90 | 91 | ## Methods 92 | 93 | ### process 94 | 95 | ▸ **process**(`request`): `NotPromise` 96 | 97 | #### Parameters 98 | 99 | | Name | Type | 100 | | :------ | :------ | 101 | | `request` | [`ProcessRequest`](ProcessRequest)<[`AnyTagNode`](../modules#anytagnode)\> | 102 | 103 | #### Returns 104 | 105 | `NotPromise` 106 | 107 | #### Defined in 108 | 109 | [src/bluehawk/tags/Tag.ts:28](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/Tag.ts#L28) 110 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/BlockCommentTokenConfiguration.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "BlockCommentTokenConfiguration" 3 | title: "Interface: BlockCommentTokenConfiguration" 4 | sidebar_label: "BlockCommentTokenConfiguration" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### canNest 12 | 13 | • **canNest**: `boolean` 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/lexer/makeBlockCommentTokens.ts:6](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makeBlockCommentTokens.ts#L6) 18 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/BlockTagNode.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "BlockTagNode" 3 | title: "Interface: BlockTagNode" 4 | sidebar_label: "BlockTagNode" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - `TagNode` 12 | 13 | ↳ **`BlockTagNode`** 14 | 15 | ## Properties 16 | 17 | ### associatedTokens 18 | 19 | • **associatedTokens**: `IToken`[] 20 | 21 | #### Inherited from 22 | 23 | TagNode.associatedTokens 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/parser/TagNode.ts:26](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L26) 28 | 29 | ___ 30 | 31 | ### attributes 32 | 33 | • **attributes**: `TagNodeAttributes` 34 | 35 | #### Overrides 36 | 37 | TagNode.attributes 38 | 39 | #### Defined in 40 | 41 | [src/bluehawk/parser/TagNode.ts:61](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L61) 42 | 43 | ___ 44 | 45 | ### children 46 | 47 | • **children**: [`AnyTagNode`](../modules#anytagnode)[] 48 | 49 | #### Overrides 50 | 51 | TagNode.children 52 | 53 | #### Defined in 54 | 55 | [src/bluehawk/parser/TagNode.ts:60](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L60) 56 | 57 | ___ 58 | 59 | ### contentRange 60 | 61 | • **contentRange**: `Range` 62 | 63 | #### Overrides 64 | 65 | TagNode.contentRange 66 | 67 | #### Defined in 68 | 69 | [src/bluehawk/parser/TagNode.ts:59](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L59) 70 | 71 | ___ 72 | 73 | ### id 74 | 75 | • `Optional` **id**: `string`[] 76 | 77 | #### Inherited from 78 | 79 | TagNode.id 80 | 81 | #### Defined in 82 | 83 | [src/bluehawk/parser/TagNode.ts:33](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L33) 84 | 85 | ___ 86 | 87 | ### inContext 88 | 89 | • **inContext**: `TagNodeContext` 90 | 91 | #### Inherited from 92 | 93 | TagNode.inContext 94 | 95 | #### Defined in 96 | 97 | [src/bluehawk/parser/TagNode.ts:29](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L29) 98 | 99 | ___ 100 | 101 | ### lineComments 102 | 103 | • **lineComments**: `IToken`[] 104 | 105 | #### Inherited from 106 | 107 | TagNode.lineComments 108 | 109 | #### Defined in 110 | 111 | [src/bluehawk/parser/TagNode.ts:23](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L23) 112 | 113 | ___ 114 | 115 | ### lineRange 116 | 117 | • **lineRange**: `Range` 118 | 119 | #### Inherited from 120 | 121 | TagNode.lineRange 122 | 123 | #### Defined in 124 | 125 | [src/bluehawk/parser/TagNode.ts:19](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L19) 126 | 127 | ___ 128 | 129 | ### newlines 130 | 131 | • **newlines**: `IToken`[] 132 | 133 | #### Inherited from 134 | 135 | TagNode.newlines 136 | 137 | #### Defined in 138 | 139 | [src/bluehawk/parser/TagNode.ts:22](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L22) 140 | 141 | ___ 142 | 143 | ### range 144 | 145 | • **range**: `Range` 146 | 147 | #### Inherited from 148 | 149 | TagNode.range 150 | 151 | #### Defined in 152 | 153 | [src/bluehawk/parser/TagNode.ts:15](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L15) 154 | 155 | ___ 156 | 157 | ### tagName 158 | 159 | • **tagName**: `string` 160 | 161 | #### Inherited from 162 | 163 | TagNode.tagName 164 | 165 | #### Defined in 166 | 167 | [src/bluehawk/parser/TagNode.ts:10](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L10) 168 | 169 | ___ 170 | 171 | ### type 172 | 173 | • **type**: ``"block"`` 174 | 175 | #### Overrides 176 | 177 | TagNode.type 178 | 179 | #### Defined in 180 | 181 | [src/bluehawk/parser/TagNode.ts:58](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L58) 182 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/CheckArgs.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "CheckArgs" 3 | title: "Interface: CheckArgs" 4 | sidebar_label: "CheckArgs" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - [`ActionArgs`](ActionArgs) 12 | 13 | ↳ **`CheckArgs`** 14 | 15 | ## Properties 16 | 17 | ### ignore 18 | 19 | • `Optional` **ignore**: `string` \| `string`[] 20 | 21 | #### Defined in 22 | 23 | [src/bluehawk/actions/check.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/check.ts#L9) 24 | 25 | ___ 26 | 27 | ### json 28 | 29 | • `Optional` **json**: `boolean` 30 | 31 | #### Defined in 32 | 33 | [src/bluehawk/actions/check.ts:10](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/check.ts#L10) 34 | 35 | ___ 36 | 37 | ### logLevel 38 | 39 | • `Optional` **logLevel**: `LogLevel` 40 | 41 | #### Inherited from 42 | 43 | [ActionArgs](ActionArgs).[logLevel](ActionArgs#loglevel) 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/actions/ActionArgs.ts:3](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L3) 48 | 49 | ___ 50 | 51 | ### paths 52 | 53 | • **paths**: `string`[] 54 | 55 | #### Defined in 56 | 57 | [src/bluehawk/actions/check.ts:8](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/check.ts#L8) 58 | 59 | ___ 60 | 61 | ### waitForListeners 62 | 63 | • `Optional` **waitForListeners**: `boolean` 64 | 65 | #### Inherited from 66 | 67 | [ActionArgs](ActionArgs).[waitForListeners](ActionArgs#waitforlisteners) 68 | 69 | #### Defined in 70 | 71 | [src/bluehawk/actions/ActionArgs.ts:4](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L4) 72 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/CheckResult.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "CheckResult" 3 | title: "Interface: CheckResult" 4 | sidebar_label: "CheckResult" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/CopyArgs.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "CopyArgs" 3 | title: "Interface: CopyArgs" 4 | sidebar_label: "CopyArgs" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - [`ActionArgs`](ActionArgs) 12 | 13 | ↳ **`CopyArgs`** 14 | 15 | ## Properties 16 | 17 | ### ignore 18 | 19 | • `Optional` **ignore**: `string` \| `string`[] 20 | 21 | #### Defined in 22 | 23 | [src/bluehawk/actions/copy.ts:12](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/copy.ts#L12) 24 | 25 | ___ 26 | 27 | ### logLevel 28 | 29 | • `Optional` **logLevel**: `LogLevel` 30 | 31 | #### Inherited from 32 | 33 | [ActionArgs](ActionArgs).[logLevel](ActionArgs#loglevel) 34 | 35 | #### Defined in 36 | 37 | [src/bluehawk/actions/ActionArgs.ts:3](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L3) 38 | 39 | ___ 40 | 41 | ### output 42 | 43 | • **output**: `string` 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/actions/copy.ts:10](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/copy.ts#L10) 48 | 49 | ___ 50 | 51 | ### rootPath 52 | 53 | • **rootPath**: `string` 54 | 55 | #### Defined in 56 | 57 | [src/bluehawk/actions/copy.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/copy.ts#L9) 58 | 59 | ___ 60 | 61 | ### state 62 | 63 | • `Optional` **state**: `string` 64 | 65 | #### Defined in 66 | 67 | [src/bluehawk/actions/copy.ts:11](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/copy.ts#L11) 68 | 69 | ___ 70 | 71 | ### waitForListeners 72 | 73 | • `Optional` **waitForListeners**: `boolean` 74 | 75 | #### Inherited from 76 | 77 | [ActionArgs](ActionArgs).[waitForListeners](ActionArgs#waitforlisteners) 78 | 79 | #### Defined in 80 | 81 | [src/bluehawk/actions/ActionArgs.ts:4](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L4) 82 | 83 | ## Methods 84 | 85 | ### onBinaryFile 86 | 87 | ▸ `Optional` **onBinaryFile**(`path`): `void` \| `Promise`<`void`\> 88 | 89 | Hook for additional work after a binary file is processed. 90 | 91 | #### Parameters 92 | 93 | | Name | Type | 94 | | :------ | :------ | 95 | | `path` | `string` | 96 | 97 | #### Returns 98 | 99 | `void` \| `Promise`<`void`\> 100 | 101 | #### Defined in 102 | 103 | [src/bluehawk/actions/copy.ts:17](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/copy.ts#L17) 104 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/EmphasizeRange.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "EmphasizeRange" 3 | title: "Interface: EmphasizeRange" 4 | sidebar_label: "EmphasizeRange" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### end 12 | 13 | • **end**: `Object` 14 | 15 | #### Type declaration 16 | 17 | | Name | Type | 18 | | :------ | :------ | 19 | | `column` | `number` | 20 | | `line` | `number` | 21 | 22 | #### Defined in 23 | 24 | [src/bluehawk/tags/EmphasizeTag.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/EmphasizeTag.ts#L9) 25 | 26 | ___ 27 | 28 | ### start 29 | 30 | • **start**: `Object` 31 | 32 | #### Type declaration 33 | 34 | | Name | Type | 35 | | :------ | :------ | 36 | | `column` | `number` | 37 | | `line` | `number` | 38 | 39 | #### Defined in 40 | 41 | [src/bluehawk/tags/EmphasizeTag.ts:5](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/EmphasizeTag.ts#L5) 42 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/EmphasizeSourceAttributes.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "EmphasizeSourceAttributes" 3 | title: "Interface: EmphasizeSourceAttributes" 4 | sidebar_label: "EmphasizeSourceAttributes" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### ranges 12 | 13 | • **ranges**: [`EmphasizeRange`](EmphasizeRange)[] 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/tags/EmphasizeTag.ts:16](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/tags/EmphasizeTag.ts#L16) 18 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/IParser.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "IParser" 3 | title: "Interface: IParser" 4 | sidebar_label: "IParser" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### languageSpecification 12 | 13 | • **languageSpecification**: [`LanguageSpecification`](LanguageSpecification) 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/makeParser.ts:15](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/makeParser.ts#L15) 18 | 19 | ## Methods 20 | 21 | ### parse 22 | 23 | ▸ **parse**(`source`): [`ParseResult`](ParseResult) 24 | 25 | #### Parameters 26 | 27 | | Name | Type | 28 | | :------ | :------ | 29 | | `source` | [`Document`](../classes/Document) | 30 | 31 | #### Returns 32 | 33 | [`ParseResult`](ParseResult) 34 | 35 | #### Defined in 36 | 37 | [src/bluehawk/parser/makeParser.ts:16](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/makeParser.ts#L16) 38 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/IVisitor.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "IVisitor" 3 | title: "Interface: IVisitor" 4 | sidebar_label: "IVisitor" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### parser 12 | 13 | • **parser**: [`RootParser`](../classes/RootParser) 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/visitor/makeCstVisitor.ts:26](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/visitor/makeCstVisitor.ts#L26) 18 | 19 | ## Methods 20 | 21 | ### visit 22 | 23 | ▸ **visit**(`node`, `source`): [`VisitorResult`](VisitorResult) 24 | 25 | #### Parameters 26 | 27 | | Name | Type | 28 | | :------ | :------ | 29 | | `node` | `CstNode` | 30 | | `source` | [`Document`](../classes/Document) | 31 | 32 | #### Returns 33 | 34 | [`VisitorResult`](VisitorResult) 35 | 36 | #### Defined in 37 | 38 | [src/bluehawk/parser/visitor/makeCstVisitor.ts:27](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/visitor/makeCstVisitor.ts#L27) 39 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/LanguageSpecification.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "LanguageSpecification" 3 | title: "Interface: LanguageSpecification" 4 | sidebar_label: "LanguageSpecification" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### blockComments 12 | 13 | • `Optional` **blockComments**: [`RegExp`, `RegExp`][] 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/LanguageSpecification.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/LanguageSpecification.ts#L9) 18 | 19 | ___ 20 | 21 | ### languageId 22 | 23 | • **languageId**: `string` 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/parser/LanguageSpecification.ts:3](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/LanguageSpecification.ts#L3) 28 | 29 | ___ 30 | 31 | ### lineComments 32 | 33 | • `Optional` **lineComments**: `RegExp`[] 34 | 35 | #### Defined in 36 | 37 | [src/bluehawk/parser/LanguageSpecification.ts:6](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/LanguageSpecification.ts#L6) 38 | 39 | ___ 40 | 41 | ### parserPushers 42 | 43 | • `Optional` **parserPushers**: { `endNewParserAfterPopToken?`: `boolean` ; `languageId`: `string` ; `patterns`: [`RegExp`, `RegExp`] ; `startNewParserOnPushToken?`: `boolean` }[] 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/parser/LanguageSpecification.ts:22](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/LanguageSpecification.ts#L22) 48 | 49 | ___ 50 | 51 | ### stringLiterals 52 | 53 | • `Optional` **stringLiterals**: { `multiline`: `boolean` ; `pattern`: `RegExp` }[] 54 | 55 | #### Defined in 56 | 57 | [src/bluehawk/parser/LanguageSpecification.ts:13](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/LanguageSpecification.ts#L13) 58 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/LineTagNode.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "LineTagNode" 3 | title: "Interface: LineTagNode" 4 | sidebar_label: "LineTagNode" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - `TagNode` 12 | 13 | ↳ **`LineTagNode`** 14 | 15 | ## Properties 16 | 17 | ### associatedTokens 18 | 19 | • **associatedTokens**: `IToken`[] 20 | 21 | #### Inherited from 22 | 23 | TagNode.associatedTokens 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/parser/TagNode.ts:26](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L26) 28 | 29 | ___ 30 | 31 | ### attributes 32 | 33 | • **attributes**: `undefined` 34 | 35 | #### Overrides 36 | 37 | TagNode.attributes 38 | 39 | #### Defined in 40 | 41 | [src/bluehawk/parser/TagNode.ts:53](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L53) 42 | 43 | ___ 44 | 45 | ### children 46 | 47 | • **children**: `undefined` 48 | 49 | #### Overrides 50 | 51 | TagNode.children 52 | 53 | #### Defined in 54 | 55 | [src/bluehawk/parser/TagNode.ts:52](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L52) 56 | 57 | ___ 58 | 59 | ### contentRange 60 | 61 | • **contentRange**: `undefined` 62 | 63 | #### Overrides 64 | 65 | TagNode.contentRange 66 | 67 | #### Defined in 68 | 69 | [src/bluehawk/parser/TagNode.ts:51](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L51) 70 | 71 | ___ 72 | 73 | ### id 74 | 75 | • **id**: `undefined` 76 | 77 | #### Overrides 78 | 79 | TagNode.id 80 | 81 | #### Defined in 82 | 83 | [src/bluehawk/parser/TagNode.ts:50](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L50) 84 | 85 | ___ 86 | 87 | ### inContext 88 | 89 | • **inContext**: `TagNodeContext` 90 | 91 | #### Inherited from 92 | 93 | TagNode.inContext 94 | 95 | #### Defined in 96 | 97 | [src/bluehawk/parser/TagNode.ts:29](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L29) 98 | 99 | ___ 100 | 101 | ### lineComments 102 | 103 | • **lineComments**: `IToken`[] 104 | 105 | #### Inherited from 106 | 107 | TagNode.lineComments 108 | 109 | #### Defined in 110 | 111 | [src/bluehawk/parser/TagNode.ts:23](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L23) 112 | 113 | ___ 114 | 115 | ### lineRange 116 | 117 | • **lineRange**: `Range` 118 | 119 | #### Inherited from 120 | 121 | TagNode.lineRange 122 | 123 | #### Defined in 124 | 125 | [src/bluehawk/parser/TagNode.ts:19](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L19) 126 | 127 | ___ 128 | 129 | ### newlines 130 | 131 | • **newlines**: `IToken`[] 132 | 133 | #### Inherited from 134 | 135 | TagNode.newlines 136 | 137 | #### Defined in 138 | 139 | [src/bluehawk/parser/TagNode.ts:22](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L22) 140 | 141 | ___ 142 | 143 | ### range 144 | 145 | • **range**: `Range` 146 | 147 | #### Inherited from 148 | 149 | TagNode.range 150 | 151 | #### Defined in 152 | 153 | [src/bluehawk/parser/TagNode.ts:15](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L15) 154 | 155 | ___ 156 | 157 | ### tagName 158 | 159 | • **tagName**: `string` 160 | 161 | #### Inherited from 162 | 163 | TagNode.tagName 164 | 165 | #### Defined in 166 | 167 | [src/bluehawk/parser/TagNode.ts:10](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L10) 168 | 169 | ___ 170 | 171 | ### type 172 | 173 | • **type**: ``"line"`` 174 | 175 | #### Overrides 176 | 177 | TagNode.type 178 | 179 | #### Defined in 180 | 181 | [src/bluehawk/parser/TagNode.ts:49](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/TagNode.ts#L49) 182 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/ListStatesArgs.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "ListStatesArgs" 3 | title: "Interface: ListStatesArgs" 4 | sidebar_label: "ListStatesArgs" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - [`ActionArgs`](ActionArgs) 12 | 13 | ↳ **`ListStatesArgs`** 14 | 15 | ## Properties 16 | 17 | ### ignore 18 | 19 | • `Optional` **ignore**: `string` \| `string`[] 20 | 21 | #### Defined in 22 | 23 | [src/bluehawk/actions/listStates.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/listStates.ts#L9) 24 | 25 | ___ 26 | 27 | ### json 28 | 29 | • `Optional` **json**: `boolean` 30 | 31 | #### Defined in 32 | 33 | [src/bluehawk/actions/listStates.ts:8](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/listStates.ts#L8) 34 | 35 | ___ 36 | 37 | ### logLevel 38 | 39 | • `Optional` **logLevel**: `LogLevel` 40 | 41 | #### Inherited from 42 | 43 | [ActionArgs](ActionArgs).[logLevel](ActionArgs#loglevel) 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/actions/ActionArgs.ts:3](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L3) 48 | 49 | ___ 50 | 51 | ### paths 52 | 53 | • **paths**: `string`[] 54 | 55 | #### Defined in 56 | 57 | [src/bluehawk/actions/listStates.ts:7](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/listStates.ts#L7) 58 | 59 | ___ 60 | 61 | ### waitForListeners 62 | 63 | • `Optional` **waitForListeners**: `boolean` 64 | 65 | #### Inherited from 66 | 67 | [ActionArgs](ActionArgs).[waitForListeners](ActionArgs#waitforlisteners) 68 | 69 | #### Defined in 70 | 71 | [src/bluehawk/actions/ActionArgs.ts:4](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L4) 72 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/ListTagArgs.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "ListTagArgs" 3 | title: "Interface: ListTagArgs" 4 | sidebar_label: "ListTagArgs" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - [`ActionArgs`](ActionArgs) 12 | 13 | ↳ **`ListTagArgs`** 14 | 15 | ## Properties 16 | 17 | ### json 18 | 19 | • `Optional` **json**: `boolean` 20 | 21 | #### Defined in 22 | 23 | [src/bluehawk/actions/listTags.ts:6](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/listTags.ts#L6) 24 | 25 | ___ 26 | 27 | ### logLevel 28 | 29 | • `Optional` **logLevel**: `LogLevel` 30 | 31 | #### Inherited from 32 | 33 | [ActionArgs](ActionArgs).[logLevel](ActionArgs#loglevel) 34 | 35 | #### Defined in 36 | 37 | [src/bluehawk/actions/ActionArgs.ts:3](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L3) 38 | 39 | ___ 40 | 41 | ### waitForListeners 42 | 43 | • `Optional` **waitForListeners**: `boolean` 44 | 45 | #### Inherited from 46 | 47 | [ActionArgs](ActionArgs).[waitForListeners](ActionArgs#waitforlisteners) 48 | 49 | #### Defined in 50 | 51 | [src/bluehawk/actions/ActionArgs.ts:4](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L4) 52 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/ParseResult.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "ParseResult" 3 | title: "Interface: ParseResult" 4 | sidebar_label: "ParseResult" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### errors 12 | 13 | • **errors**: `BluehawkError`[] 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/ParseResult.ts:7](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/ParseResult.ts#L7) 18 | 19 | ___ 20 | 21 | ### languageSpecification 22 | 23 | • `Optional` **languageSpecification**: [`LanguageSpecification`](LanguageSpecification) 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/parser/ParseResult.ts:10](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/ParseResult.ts#L10) 28 | 29 | ___ 30 | 31 | ### source 32 | 33 | • **source**: [`Document`](../classes/Document) 34 | 35 | #### Defined in 36 | 37 | [src/bluehawk/parser/ParseResult.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/ParseResult.ts#L9) 38 | 39 | ___ 40 | 41 | ### tagNodes 42 | 43 | • **tagNodes**: [`AnyTagNode`](../modules#anytagnode)[] 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/parser/ParseResult.ts:8](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/ParseResult.ts#L8) 48 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/PayloadQuery.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "PayloadQuery" 3 | title: "Interface: PayloadQuery" 4 | sidebar_label: "PayloadQuery" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### groups 12 | 13 | • **groups**: `Object` 14 | 15 | #### Index signature 16 | 17 | ▪ [groupName: `string`]: `IToken`[] 18 | 19 | #### Defined in 20 | 21 | [src/bluehawk/parser/lexer/makePayloadPattern.ts:11](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePayloadPattern.ts#L11) 22 | 23 | ___ 24 | 25 | ### offset 26 | 27 | • **offset**: `number` 28 | 29 | #### Defined in 30 | 31 | [src/bluehawk/parser/lexer/makePayloadPattern.ts:9](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePayloadPattern.ts#L9) 32 | 33 | ___ 34 | 35 | ### result 36 | 37 | • **result**: `RegExpExecArray` 38 | 39 | #### Defined in 40 | 41 | [src/bluehawk/parser/lexer/makePayloadPattern.ts:14](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePayloadPattern.ts#L14) 42 | 43 | ___ 44 | 45 | ### text 46 | 47 | • **text**: `string` 48 | 49 | #### Defined in 50 | 51 | [src/bluehawk/parser/lexer/makePayloadPattern.ts:8](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePayloadPattern.ts#L8) 52 | 53 | ___ 54 | 55 | ### tokens 56 | 57 | • **tokens**: `IToken`[] 58 | 59 | #### Defined in 60 | 61 | [src/bluehawk/parser/lexer/makePayloadPattern.ts:10](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePayloadPattern.ts#L10) 62 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/PluginArgs.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "PluginArgs" 3 | title: "Interface: PluginArgs" 4 | sidebar_label: "PluginArgs" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | The arguments passed from the CLI to a plugin's register() function. 10 | 11 | ## Properties 12 | 13 | ### bluehawk 14 | 15 | • **bluehawk**: [`Bluehawk`](../classes/Bluehawk) 16 | 17 | The [Bluehawk](../classes/Bluehawk) instance that a plugin can use to add Bluehawk commands, 18 | languages, and listeners. 19 | 20 | #### Defined in 21 | 22 | [src/bluehawk/Plugin.ts:49](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/Plugin.ts#L49) 23 | 24 | ___ 25 | 26 | ### bluehawkVersion 27 | 28 | • **bluehawkVersion**: `string` 29 | 30 | The current semantic version string of Bluehawk. 31 | 32 | #### Defined in 33 | 34 | [src/bluehawk/Plugin.ts:60](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/Plugin.ts#L60) 35 | 36 | ___ 37 | 38 | ### yargs 39 | 40 | • **yargs**: `Argv`<{}\> 41 | 42 | The [yargs](https://yargs.js.org/) instance that a plugin can modify to add 43 | CLI commands and options. 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/Plugin.ts:55](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/Plugin.ts#L55) 48 | 49 | ___ 50 | 51 | ### yargsVersion 52 | 53 | • **yargsVersion**: `string` 54 | 55 | The current semantic version string of Yargs. 56 | 57 | #### Defined in 58 | 59 | [src/bluehawk/Plugin.ts:65](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/Plugin.ts#L65) 60 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/ProcessOptions.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "ProcessOptions" 3 | title: "Interface: ProcessOptions" 4 | sidebar_label: "ProcessOptions" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### stripUnknownTags 12 | 13 | • `Optional` **stripUnknownTags**: `boolean` 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/processor/Processor.ts:12](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/processor/Processor.ts#L12) 18 | 19 | ___ 20 | 21 | ### waitForListeners 22 | 23 | • **waitForListeners**: `boolean` 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/processor/Processor.ts:11](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/processor/Processor.ts#L11) 28 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/ProcessRequest.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "ProcessRequest" 3 | title: "Interface: ProcessRequest" 4 | sidebar_label: "ProcessRequest" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Type parameters 10 | 11 | | Name | Type | 12 | | :------ | :------ | 13 | | `TagNodeType` | [`AnyTagNode`](../modules#anytagnode) | 14 | 15 | ## Properties 16 | 17 | ### document 18 | 19 | • **document**: [`Document`](../classes/Document) 20 | 21 | The document to be edited by the processor. 22 | 23 | Tag processors may edit the document text directly using MagicString 24 | functionality. Avoid converting the text to a non-MagicString string, as the 25 | edit history must be retained for subsequent edits and listener processing 26 | to work as expected. 27 | 28 | Tag processors may safely modify the attributes of the document under 29 | their own tag name's key (e.g. a tag named "myTag" may freely 30 | edit `document.attributes["myTag"]`). Attributes are useful for passing 31 | meta information from the tag to an eventual listener. 32 | 33 | #### Defined in 34 | 35 | [src/bluehawk/processor/Processor.ts:34](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/processor/Processor.ts#L34) 36 | 37 | ___ 38 | 39 | ### tagNode 40 | 41 | • **tagNode**: `TagNodeType` 42 | 43 | The specific tag to process. 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/processor/Processor.ts:39](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/processor/Processor.ts#L39) 48 | 49 | ___ 50 | 51 | ### tagNodes 52 | 53 | • **tagNodes**: [`AnyTagNode`](../modules#anytagnode)[] 54 | 55 | The overall result's tag nodes being processed by the processor. 56 | 57 | #### Defined in 58 | 59 | [src/bluehawk/processor/Processor.ts:44](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/processor/Processor.ts#L44) 60 | 61 | ## Methods 62 | 63 | ### fork 64 | 65 | ▸ **fork**(`args`): `void` 66 | 67 | Process the given Bluehawk result, optionally under an alternative id, and 68 | emit the file. 69 | 70 | #### Parameters 71 | 72 | | Name | Type | 73 | | :------ | :------ | 74 | | `args` | `ForkArgs` | 75 | 76 | #### Returns 77 | 78 | `void` 79 | 80 | #### Defined in 81 | 82 | [src/bluehawk/processor/Processor.ts:50](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/processor/Processor.ts#L50) 83 | 84 | ___ 85 | 86 | ### stopPropagation 87 | 88 | ▸ **stopPropagation**(): `void` 89 | 90 | Call this to stop the processor from continuing into the tag node's 91 | children. 92 | 93 | #### Returns 94 | 95 | `void` 96 | 97 | #### Defined in 98 | 99 | [src/bluehawk/processor/Processor.ts:56](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/processor/Processor.ts#L56) 100 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/Project.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "Project" 3 | title: "Interface: Project" 4 | sidebar_label: "Project" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### ignore 12 | 13 | • `Optional` **ignore**: `string` \| `string`[] 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/project/Project.ts:7](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/project/Project.ts#L7) 18 | 19 | ___ 20 | 21 | ### rootPath 22 | 23 | • **rootPath**: `string` 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/project/Project.ts:4](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/project/Project.ts#L4) 28 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/PushParserPayload.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "PushParserPayload" 3 | title: "Interface: PushParserPayload" 4 | sidebar_label: "PushParserPayload" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### endToken 12 | 13 | • **endToken**: `TokenType` 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:16](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L16) 18 | 19 | ___ 20 | 21 | ### fullText 22 | 23 | • **fullText**: `string` 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:12](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L12) 28 | 29 | ___ 30 | 31 | ### includePopTokenInSubstring 32 | 33 | • **includePopTokenInSubstring**: `boolean` 34 | 35 | #### Defined in 36 | 37 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:15](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L15) 38 | 39 | ___ 40 | 41 | ### includePushTokenInSubstring 42 | 43 | • **includePushTokenInSubstring**: `boolean` 44 | 45 | #### Defined in 46 | 47 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:14](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L14) 48 | 49 | ___ 50 | 51 | ### parserId 52 | 53 | • **parserId**: `string` 54 | 55 | #### Defined in 56 | 57 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:13](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L13) 58 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/PushParserTokenConfiguration.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "PushParserTokenConfiguration" 3 | title: "Interface: PushParserTokenConfiguration" 4 | sidebar_label: "PushParserTokenConfiguration" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### includePopTokenInSubstring 12 | 13 | • **includePopTokenInSubstring**: `boolean` 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:8](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L8) 18 | 19 | ___ 20 | 21 | ### includePushTokenInSubstring 22 | 23 | • **includePushTokenInSubstring**: `boolean` 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:7](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L7) 28 | 29 | ___ 30 | 31 | ### parserId 32 | 33 | • **parserId**: `string` 34 | 35 | #### Defined in 36 | 37 | [src/bluehawk/parser/lexer/makePushParserTokens.ts:6](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/lexer/makePushParserTokens.ts#L6) 38 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/SnipArgs.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "SnipArgs" 3 | title: "Interface: SnipArgs" 4 | sidebar_label: "SnipArgs" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Hierarchy 10 | 11 | - [`ActionArgs`](ActionArgs) 12 | 13 | ↳ **`SnipArgs`** 14 | 15 | ## Properties 16 | 17 | ### format 18 | 19 | • `Optional` **format**: `Format` \| `Format`[] 20 | 21 | #### Defined in 22 | 23 | [src/bluehawk/actions/snip.ts:17](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/snip.ts#L17) 24 | 25 | ___ 26 | 27 | ### id 28 | 29 | • `Optional` **id**: `string` \| `string`[] 30 | 31 | #### Defined in 32 | 33 | [src/bluehawk/actions/snip.ts:15](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/snip.ts#L15) 34 | 35 | ___ 36 | 37 | ### ignore 38 | 39 | • `Optional` **ignore**: `string` \| `string`[] 40 | 41 | #### Defined in 42 | 43 | [src/bluehawk/actions/snip.ts:16](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/snip.ts#L16) 44 | 45 | ___ 46 | 47 | ### logLevel 48 | 49 | • `Optional` **logLevel**: `LogLevel` 50 | 51 | #### Inherited from 52 | 53 | [ActionArgs](ActionArgs).[logLevel](ActionArgs#loglevel) 54 | 55 | #### Defined in 56 | 57 | [src/bluehawk/actions/ActionArgs.ts:3](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L3) 58 | 59 | ___ 60 | 61 | ### output 62 | 63 | • **output**: `string` 64 | 65 | #### Defined in 66 | 67 | [src/bluehawk/actions/snip.ts:13](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/snip.ts#L13) 68 | 69 | ___ 70 | 71 | ### paths 72 | 73 | • **paths**: `string`[] 74 | 75 | #### Defined in 76 | 77 | [src/bluehawk/actions/snip.ts:12](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/snip.ts#L12) 78 | 79 | ___ 80 | 81 | ### state 82 | 83 | • `Optional` **state**: `string` 84 | 85 | #### Defined in 86 | 87 | [src/bluehawk/actions/snip.ts:14](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/snip.ts#L14) 88 | 89 | ___ 90 | 91 | ### waitForListeners 92 | 93 | • `Optional` **waitForListeners**: `boolean` 94 | 95 | #### Inherited from 96 | 97 | [ActionArgs](ActionArgs).[waitForListeners](ActionArgs#waitforlisteners) 98 | 99 | #### Defined in 100 | 101 | [src/bluehawk/actions/ActionArgs.ts:4](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/actions/ActionArgs.ts#L4) 102 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/VisitorResult.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "VisitorResult" 3 | title: "Interface: VisitorResult" 4 | sidebar_label: "VisitorResult" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Properties 10 | 11 | ### errors 12 | 13 | • **errors**: `BluehawkError`[] 14 | 15 | #### Defined in 16 | 17 | [src/bluehawk/parser/visitor/makeCstVisitor.ts:21](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/visitor/makeCstVisitor.ts#L21) 18 | 19 | ___ 20 | 21 | ### tagNodes 22 | 23 | • **tagNodes**: [`AnyTagNode`](../modules#anytagnode)[] 24 | 25 | #### Defined in 26 | 27 | [src/bluehawk/parser/visitor/makeCstVisitor.ts:22](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/parser/visitor/makeCstVisitor.ts#L22) 28 | -------------------------------------------------------------------------------- /docs/docs/develop/api/interfaces/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Interfaces" 2 | position: 4 -------------------------------------------------------------------------------- /docs/docs/develop/api/namespaces/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Namespaces" 2 | position: 1 -------------------------------------------------------------------------------- /docs/docs/develop/api/namespaces/getBluehawk.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "getBluehawk" 3 | title: "Namespace: getBluehawk" 4 | sidebar_label: "getBluehawk" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Functions 10 | 11 | ### reset 12 | 13 | ▸ **reset**(): `Promise`<[`Bluehawk`](../classes/Bluehawk)\> 14 | 15 | Resets the bluehawk instance (for unit testing). 16 | 17 | #### Returns 18 | 19 | `Promise`<[`Bluehawk`](../classes/Bluehawk)\> 20 | 21 | #### Defined in 22 | 23 | [src/bluehawk/getBluehawk.ts:124](https://github.com/mongodben/Bluehawk/blob/be77c09/src/bluehawk/getBluehawk.ts#L124) 24 | -------------------------------------------------------------------------------- /docs/docs/develop/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "plugins" 3 | title: "Plugins" 4 | sidebar_label: "Plugins" 5 | sidebar_position: 4 6 | custom_edit_url: null 7 | --- 8 | 9 | You can add tags, CLI commands, and listeners by creating a JS file or node 10 | project that implements the register() function: 11 | 12 | ```js 13 | // myPlugin.js 14 | exports.register = (bluehawk) => { 15 | // Register a new tag, :my-tag: 16 | bluehawk.registerTag("my-tag", { 17 | rules: [], 18 | process: (request) => { 19 | // Execute tag 20 | }, 21 | }); 22 | 23 | // Register a document listener 24 | bluehawk.subscribe((finishedDocument) => { 25 | // Do something with finishedDocument 26 | }); 27 | }; 28 | ``` 29 | 30 | Usage: 31 | 32 | ```shell 33 | bluehawk --plugin ./myPlugin source.txt 34 | ``` 35 | 36 | You can pass the --plugin flag multiple times to load different plugins or create a plugin that is composed of other plugins. 37 | -------------------------------------------------------------------------------- /docs/docs/guides/_category_.yml: -------------------------------------------------------------------------------- 1 | position: 4 2 | label: "Guides" -------------------------------------------------------------------------------- /docs/docs/guides/continuous-integration.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "continuous-integration" 3 | title: "Continuous Integration" 4 | slug: "/continuous-integration/" 5 | sidebar_label: "Continuous Integration" 6 | sidebar_position: 3 7 | custom_edit_url: null 8 | --- 9 | 10 | Because the Bluehawk CLI is a package you can 11 | [install with `npm`](/install#usage-as-a-module), you can use it in your 12 | continuous integration workflows. 13 | 14 | In the Realm documentation team, we use it in a couple of different ways: 15 | 16 | - Check for Bluehawk errors when we push updates to files that might use Bluehawk 17 | - Use Bluehawk to update tutorial artifact repositories when we push updates to tutorials 18 | 19 | ## Check for Bluehawk Errors 20 | 21 | In the Realm documentatation GitHub repository, we have a 22 | [GitHub Workflow](https://github.com/mongodb/docs-realm/blob/master/.github/workflows/bluehawk.yml) 23 | that uses [bluehawk check](/reference/cli#check) to check for errors when 24 | we push to specific directories. The command we execute in the workflow is: 25 | 26 | ```shell 27 | npx bluehawk check -i "*.md" -i "*.properties" -i "*.lock" examples tutorial 28 | ``` 29 | 30 | This does a few things: 31 | 32 | - Uses `bluehawk check` to tell us if there are Bluehawk errors in our files 33 | - Ignores files of certain types: 34 | - Markdown files 35 | - Properties files 36 | - Lock files 37 | - Checks for updates in the `examples` or `tutorial` directories, which are the 38 | two directories where we use Bluehawk 39 | 40 | This way, if we push a file to GitHub in one of these directories, and the 41 | file has Bluehawk errors, the check will fail and we won't merge the PR. 42 | This is similar to checking for build errors when we generate our documentation. 43 | 44 | ## Use Bluehawk in GitHub Workflows and Actions 45 | 46 | We also use Bluehawk in our CI in more complex ways. Our 47 | [Tutorials guide](/tutorials) explains how we use `bluehawk state` to generate 48 | code examples for our tutorials. 49 | 50 | We also use Bluehawk in GitHub Workflows and Actions that automatically 51 | update artifact repositories when we push changes to our tutorials. We 52 | explain this in more depth in 53 | [Tutorials -> Automatically Update Tutorial Code](/tutorials#automatically-update-tutorial-code), 54 | but the gist of it is this: 55 | 56 | - When we push updates to the `tutorial` directory, we run `bluehawk check` to 57 | make sure there are no Bluehawk errors 58 | - Then, we use Bluehawk to copy code examples for each `state` to git branches 59 | in artifact repositories. The git branch names match the `states` in our code 60 | examples. 61 | 62 | This lets us update tutorial code in a single place - the `tutorial` directory 63 | in our documentation repository. Then, using Bluehawk, we generate updated 64 | code examples for our documentation. This CI also copies the updated code to 65 | our tutorial repositories that developers can clone and run on their machines. 66 | 67 | All of this reduces the need to manually update code in multiple places - 68 | which makes maintenance faster, and reduces the need to remember to copy 69 | code to all of the places. 70 | -------------------------------------------------------------------------------- /docs/docs/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "install" 3 | title: "Install Bluehawk" 4 | sidebar_label: "Install" 5 | sidebar_position: 0 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Install 10 | 11 | Install the CLI globally: 12 | 13 | ```sh 14 | npm install -g bluehawk 15 | ``` 16 | 17 | ## Usage as a Module 18 | 19 | ```sh 20 | npm install bluehawk 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "intro" 3 | title: "Bluehawk" 4 | sidebar_label: "Introduction" 5 | sidebar_position: -1 6 | slug: / 7 | --- 8 | 9 | :::note Bluehawk 1.0.0 is here 🎉 10 | 11 | Bluehawk 1.0.0 has been released! Future releases will adhere to [semantic versioning](https://semver.org/). 12 | 13 | ::: 14 | 15 | Bluehawk is a markup processor for extracting and manipulating arbitrary code. 16 | With Bluehawk, you can: 17 | 18 | - Extract code examples for use in documentation 19 | - Generate formatted code examples for use in documentation 20 | - Replace "finished" code with "todo" code for a branch in a tutorial repo 21 | 22 | ## Install 23 | 24 | Install Bluehawk with [NPM](https://www.npmjs.com/): 25 | 26 | ``` 27 | npm install -g bluehawk 28 | ``` 29 | 30 | ## Example 31 | 32 | Say you're documenting a library. To provide code examples for library functionality, 33 | you're forced to copy & paste snippets of code from test cases you've written into 34 | your documentation. Every time an API changes, or you want to improve an example, or 35 | you want to fix a bug, you have to copy & paste those snippets again. Sooner or later 36 | you'll miss a line, or forget to copy and paste a change from your tests to the 37 | documentation, or forget to update a line highlight... because you're trying to 38 | maintain equivalent code snippets in two places at once. 39 | 40 | What if there was a better way? What if you could write your examples in one place, 41 | and let a tool take care of removing your assertions and setup and copying the 42 | examples into your documentation? Bluehawk does exactly that. 43 | 44 | ## How to Use Bluehawk 45 | 46 | To use Bluehawk: 47 | 48 | 1. You add special comments, called [tags](reference/tags), to code blocks or lines of source code. 49 | 2. Use the [Bluehawk CLI](reference/cli) to read the input files and generate output files based on the tags. 50 | 3. Include the output files that the Bluehawk CLI generated in your documentation. 51 | 52 | For examples of how the Realm Docs team uses Bluehawk in workflows, see our guides: 53 | 54 | - [Extract Code Snippets](code-snippets/) 55 | - [Create Checkpointed Tutorials](tutorials) 56 | - [Bluehawk in Continuous Integration](continuous-integration) 57 | 58 | ### Videos 59 | 60 | How do you use Bluehawk in workflows? Here are a couple of short video 61 | overviews of how the MongoDB Developer Education team uses Bluehawk to create 62 | code examples: 63 | 64 | #### Extract and Generate Code Examples 65 | 66 | 67 | 68 | #### Generate Code for Tutorial Apps 69 | 70 | 71 | 72 | ## Contributing 73 | 74 | For more information about how to run, build, or test Bluehawk yourself, see [CONTRIBUTING.md](https://github.com/mongodb-university/Bluehawk/blob/main/CONTRIBUTING.md). 75 | -------------------------------------------------------------------------------- /docs/docs/reference/_category_.yml: -------------------------------------------------------------------------------- 1 | position: 5 2 | label: "Reference" -------------------------------------------------------------------------------- /docs/docs/reference/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "cli" 3 | title: "CLI" 4 | sidebar_label: "CLI" 5 | sidebar_position: 2 6 | custom_edit_url: null 7 | --- 8 | 9 | ## Commands 10 | 11 | Use commands to generate different kinds of output with Bluehawk, including 12 | code blocks, full files of code, and even error checks. 13 | 14 | ### Snip 15 | 16 | ``` 17 | bluehawk snip --output 18 | ``` 19 | 20 | Output "snippet files" that contain only the content of `snippet` or 21 | `snippet` Bluehawk tags, named in the format 22 | `.snippet..`. 23 | By default, this command generates snippets 24 | that omit all `state` tag contents. However, 25 | you can use the `--state` flag to generate snippet files that include 26 | content from a single state that you specify. 27 | 28 | ### Copy 29 | 30 | ``` 31 | bluehawk copy --output 32 | ``` 33 | 34 | Output full bluehawk-processed input files, in their original directory 35 | structure, to output directory. Binary files are copied without 36 | Bluehawk processing. You can use the `--ignore` flag to add gitignore-style 37 | ignore patterns that omit matched files from output. 38 | By default, this command generates output files that omit all `state`. 39 | However, you can use the `--state` flag to generate output files that 40 | include content from a single state that you specify. 41 | If you would like to rename files as you copy them, use 42 | the `--rename` flag. The `--rename` flag takes a JSON 43 | string as an argument. The JSON must represent an object whose keys are filenames that are to be renamed and whose values are the new names of those files. 44 | For example, ` --rename '{"test.txt":"test_new.txt"}'` changes the name of any file names `test.txt` to `test_new.txt`. The `--rename` flag cannot accept a JSON 45 | object whose keys or values contain a path. If you 46 | require this functionality, please submit a pull 47 | request or issue on Github. 48 | 49 | ### Check 50 | 51 | ``` 52 | bluehawk check 53 | ``` 54 | 55 | Generates non-zero output if processing any input files generates a Bluehawk 56 | error, zero output otherwise. Does not generate any files: instead, `check` 57 | outputs directly to the command line. 58 | 59 | ## Flags 60 | 61 | You can use flags to tweak the output of Bluehawk. 62 | 63 | ### Ignore 64 | 65 | Pass a pattern to the `--ignore` flag to omit any file that matches that 66 | pattern from Bluehawk's input files. Bluehawk will not process or generate 67 | output for any ignored file. You can use the `ignore` flag multiple times 68 | in a single Bluehawk execution to ignore multiple patterns. `.gitignore` files 69 | in the input directory tree are automatically used as ignore patterns. 70 | 71 | ### State 72 | 73 | Pass a state's id to the `--state` flag to include only the contents of that 74 | state, and no other states, in the generated output. 75 | 76 | ### Format 77 | 78 | Pass the name of a markup syntax to the `--format` flag when generating snippets 79 | to generate a formatted version of that snippet in the specified markup syntax. 80 | This command currently supports the following options: 81 | 82 | - `rst`: [ReStructuredText](https://en.wikipedia.org/wiki/ReStructuredText) syntax 83 | - `md`: [Markdown fenced codeblock](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks) syntax 84 | using backticks (`). Markdown format does not support the [emphasize tag](./tags#emphasize). 85 | - `docusaurus`: Docusaurus syntax with [comment highlighting](https://docusaurus.io/docs/markdown-features/code-blocks#highlighting-with-comments) 86 | -------------------------------------------------------------------------------- /docs/docs/use-cases.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: "use-cases" 3 | title: "Use Cases" 4 | sidebar_label: "Use Cases" 5 | sidebar_position: 3 6 | custom_edit_url: null 7 | --- 8 | 9 | ### Tested Code Examples 10 | 11 | Imagine you want to paste some code from a unit test into your docs. You can 12 | mark up the unit test source file like this with Bluehawk tags like 13 | `:snippet-start:`, `:snippet-end:`, `:remove-start:`, and `:remove-end:`: 14 | 15 | ```swift 16 | // SomeTest.swift 17 | 18 | // ... more tests ... 19 | func someTest() { 20 | // :snippet-start: some-example 21 | let person = getPerson() 22 | // :remove-start: // remove test boilerplate from the code block 23 | XCTAssert(person.name != "Keith") 24 | // :remove-end: 25 | person.doSomething { 26 | person.doSomethingElse() 27 | } 28 | // :snippet-end: 29 | } 30 | // ... more tests ... 31 | ``` 32 | 33 | Running Bluehawk with the `snip` command on this file will produce a snippet 34 | file called `SomeTest.snippet.some-example.swift` that looks something like this: 35 | 36 | ```swift 37 | let person = getPerson() 38 | person.doSomething { 39 | person.doSomethingElse() 40 | } 41 | ``` 42 | 43 | You can now import this snippet into your documentation. Now you have the 44 | benefit of tested examples that are still easy to read in the docs. 45 | 46 | Bluehawk markup can go into any source file, so you don't need to rig every unit 47 | test framework you use up to also extract code examples. Just use Bluehawk with 48 | the unit test framework that suits your language and your project. Heck, you don't 49 | even need a unit test framework. Use Bluehawk in your app or bash script that you 50 | run to make sure everything's still more or less working. 51 | 52 | ### Checkpointed Tutorials 53 | 54 | Suppose you have a tutorial repo that learners can clone to follow along with 55 | your tutorial from a certain starting point, say a "start" branch. You also want 56 | learners to be able to check out a "final" branch so they can see the finished 57 | project. As the tutorial developer, you would have to maintain these two state 58 | branches, which can be tedious and error prone. 59 | 60 | To manage this process, you can use Bluehawk to mark up your tutorial source and 61 | indicate different states or checkpoints with the `:state-start:` and 62 | `:state-end:` tags: 63 | 64 | ```swift 65 | // WelcomeViewController.swift 66 | 67 | // ... more code ... 68 | // :snippet-start: sign-up 69 | @objc func signUp() { 70 | // :state-start: final 71 | setLoading(true); 72 | app.emailPasswordAuth.registerUser(email: email!, password: password!, completion: { [weak self](error) in 73 | DispatchQueue.main.async { 74 | self!.setLoading(false); 75 | ... 76 | } 77 | }) 78 | // :state-end: 79 | // :state-start: start 80 | // TODO: Use the app's emailPasswordAuth to registerUser with the email and password. 81 | // When registered, call signIn(). 82 | // :state-end: 83 | } 84 | // :snippet-end: 85 | // ... more code ... 86 | ``` 87 | 88 | Running `bluehawk copy` on this file with `--state start` results in a copy of 89 | `WelcomeViewController.swift` that looks something like this: 90 | 91 | ```swift 92 | // WelcomeViewController.swift 93 | 94 | // ... more code ... 95 | @objc func signUp() { 96 | // TODO: Use the app's emailPasswordAuth to registerUser with the email and password. 97 | // When registered, call signIn(). 98 | } 99 | // ... more code ... 100 | ``` 101 | 102 | Notice that you still have all of the boilerplate, but no final implementation 103 | code. Only the "TODO" is left. 104 | 105 | Using the `--state final` flag produces another version of 106 | `WelcomeViewController.swift` that has the boilerplate and the final 107 | implementation code, but no "TODO": 108 | 109 | ```swift 110 | // WelcomeViewController.swift 111 | 112 | // ... more code ... 113 | @objc func signUp() { 114 | setLoading(true); 115 | app.emailPasswordAuth.registerUser(email: email!, password: password!, completion: { [weak self](error) in 116 | DispatchQueue.main.async { 117 | self!.setLoading(false); 118 | ... 119 | } 120 | }) 121 | } 122 | // ... more code ... 123 | ``` 124 | 125 | You can run Bluehawk on an entire directory, and each file in the repo will be 126 | copied or transformed to the output directory. This makes it easy to copy one state 127 | of the entire tutorial source into another repo that learners can clone. 128 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require("prism-react-renderer/themes/github"); 5 | const darkCodeTheme = require("prism-react-renderer/themes/dracula"); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: "Bluehawk", 10 | tagline: "Markup processor for extracting and manipulating code", 11 | url: "https://mongodb-university.github.io", 12 | baseUrl: "/Bluehawk/", 13 | onBrokenLinks: "warn", 14 | onBrokenMarkdownLinks: "warn", 15 | organizationName: "mongodb-university", // Usually your GitHub org/user name. 16 | projectName: "Bluehawk", // Usually your repo name. 17 | plugins: [ 18 | [ 19 | "docusaurus-plugin-typedoc", 20 | 21 | // Plugin / TypeDoc options 22 | { 23 | entryPoints: ["../src/index.ts"], 24 | tsconfig: "../tsconfig.json", 25 | out: "develop/api" 26 | }, 27 | ], 28 | ], 29 | presets: [ 30 | [ 31 | "@docusaurus/preset-classic", 32 | /** @type {import('@docusaurus/preset-classic').Options} */ 33 | ({ 34 | docs: { 35 | sidebarPath: require.resolve("./sidebars.js"), 36 | routeBasePath: "/", 37 | // Please change this to your repo. 38 | editUrl: 39 | "https://github.com/mongodb-university/Bluehawk/blob/main/docs/", 40 | }, 41 | theme: { 42 | customCss: require.resolve("./src/css/custom.css"), 43 | }, 44 | }), 45 | ], 46 | ], 47 | 48 | themeConfig: 49 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 50 | ({ 51 | navbar: { 52 | title: "Bluehawk Docs", 53 | items: [ 54 | { 55 | href: "https://github.com/mongodb-university/Bluehawk", 56 | label: "GitHub", 57 | position: "right", 58 | }, 59 | ], 60 | }, 61 | footer: { 62 | style: "dark", 63 | links: [ 64 | { 65 | title: "Docs", 66 | items: [ 67 | { 68 | label: "Intro", 69 | to: "/", 70 | }, 71 | { 72 | label: "Tags", 73 | to: "/commands", 74 | }, 75 | { 76 | label: "CLI", 77 | to: "/cli", 78 | }, 79 | { 80 | label: "API", 81 | to: "api", 82 | }, 83 | ], 84 | }, 85 | { 86 | title: "More", 87 | items: [ 88 | { 89 | label: "Github Repo", 90 | href: "https://github.com/mongodb-university/Bluehawk", 91 | }, 92 | { 93 | label: "Contributing Guidelines", 94 | href: "https://github.com/mongodb-university/Bluehawk/blob/main/CONTRIBUTING.md", 95 | }, 96 | { 97 | label: "Submit Issue", 98 | href: "https://github.com/mongodb-university/Bluehawk/issues", 99 | }, 100 | ], 101 | }, 102 | ], 103 | }, 104 | prism: { 105 | additionalLanguages: ["java", "swift"], 106 | theme: lightCodeTheme, 107 | darkTheme: darkCodeTheme, 108 | }, 109 | }), 110 | }; 111 | 112 | module.exports = config; 113 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "2.0.0-beta.20", 18 | "@docusaurus/preset-classic": "2.0.0-beta.20", 19 | "@mdx-js/react": "^1.6.21", 20 | "@svgr/webpack": "^5.5.0", 21 | "clsx": "^1.1.1", 22 | "file-loader": "^6.2.0", 23 | "prism-react-renderer": "^1.2.1", 24 | "react": "^17.0.1", 25 | "react-dom": "^17.0.1", 26 | "url-loader": "^4.1.1" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "docusaurus-plugin-typedoc": "^0.16.5", 42 | "typedoc": "^0.22.10", 43 | "typedoc-plugin-markdown": "^3.11.7", 44 | "typescript": "^4.5.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | mainSidebar: [ 18 | "intro", 19 | "install", 20 | "use-cases", 21 | { 22 | type: "category", 23 | label: "Guides", 24 | items: [ 25 | { 26 | type: "autogenerated", 27 | dirName: "guides", 28 | }, 29 | ], 30 | collapsed: false, 31 | }, 32 | { 33 | type: "category", 34 | label: "Reference", 35 | items: [ 36 | { 37 | type: "autogenerated", 38 | dirName: "reference", 39 | }, 40 | ], 41 | collapsed: false, 42 | }, 43 | { 44 | type: "category", 45 | label: "Develop", 46 | items: [ 47 | { 48 | type: "autogenerated", 49 | dirName: "develop", 50 | }, 51 | ], 52 | }, 53 | ], 54 | 55 | // But you can create a sidebar manually 56 | /* 57 | tutorialSidebar: [ 58 | { 59 | type: 'category', 60 | label: 'Tutorial', 61 | items: ['hello'], 62 | }, 63 | ], 64 | */ 65 | }; 66 | 67 | module.exports = sidebars; 68 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import styles from "./HomepageFeatures.module.css"; 4 | 5 | const FeatureList = [ 6 | { 7 | title: "Easy to Use", 8 | Svg: require("../../static/img/undraw_docusaurus_mountain.svg").default, 9 | description: ( 10 | <> 11 | Docusaurus was designed from the ground up to be easily installed and 12 | used to get your website up and running quickly. 13 | 14 | ), 15 | }, 16 | { 17 | title: "Focus on What Matters", 18 | Svg: require("../../static/img/undraw_docusaurus_tree.svg").default, 19 | description: ( 20 | <> 21 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 22 | ahead and move your docs into the docs directory. 23 | 24 | ), 25 | }, 26 | { 27 | title: "Powered by React", 28 | Svg: require("../../static/img/undraw_docusaurus_react.svg").default, 29 | description: ( 30 | <> 31 | Extend or customize your website layout by reusing React. Docusaurus can 32 | be extended while reusing the same header and footer. 33 | 34 | ), 35 | }, 36 | ]; 37 | 38 | function Feature({ Svg, title, description }) { 39 | return ( 40 |
41 |
42 | 43 |
44 |
45 |

{title}

46 |

{description}

47 |
48 |
49 | ); 50 | } 51 | 52 | export default function HomepageFeatures() { 53 | return ( 54 |
55 |
56 |
57 | {FeatureList.map((props, idx) => ( 58 | 59 | ))} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #1e88e5; 10 | --ifm-color-primary-dark: #187bd1; 11 | --ifm-color-primary-darker: #1774c5; 12 | --ifm-color-primary-darkest: #135fa3; 13 | --ifm-color-primary-light: #3594e8; 14 | --ifm-color-primary-lighter: #419ae9; 15 | --ifm-color-primary-lightest: #64aded; 16 | --ifm-code-font-size: 95%; 17 | } 18 | 19 | .docusaurus-highlight-code-line { 20 | background-color: rgba(0, 0, 0, 0.1); 21 | display: block; 22 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 23 | padding: 0 var(--ifm-pre-padding); 24 | } 25 | 26 | html[data-theme="dark"] .docusaurus-highlight-code-line { 27 | background-color: rgba(0, 0, 0, 0.3); 28 | } 29 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/Bluehawk/6c5f2612111c3f6813b66fe95d5d05f07ff9e9e1/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/Bluehawk/6c5f2612111c3f6813b66fe95d5d05f07ff9e9e1/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/Bluehawk/6c5f2612111c3f6813b66fe95d5d05f07ff9e9e1/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/tutorial/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/Bluehawk/6c5f2612111c3f6813b66fe95d5d05f07ff9e9e1/docs/static/img/tutorial/docsVersionDropdown.png -------------------------------------------------------------------------------- /docs/static/img/tutorial/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/Bluehawk/6c5f2612111c3f6813b66fe95d5d05f07ff9e9e1/docs/static/img/tutorial/localeDropdown.png -------------------------------------------------------------------------------- /integrationTests/.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | 3 | -------------------------------------------------------------------------------- /integrationTests/README.md: -------------------------------------------------------------------------------- 1 | # CLI Integration Tests 2 | 3 | These tests run the CLI and ensure that a CLI command on an "input" directory 4 | results in an "output" directory that perfectly matches the "expected" 5 | directory. 6 | 7 | More thorough testing of actions should be done in unit tests. This is just 8 | another layer of assurance that the CLI is not broken when you make changes. 9 | 10 | ## Run All Tests 11 | 12 | To run the tests: 13 | 14 | ```sh 15 | ./runTests.sh 16 | ``` 17 | 18 | This does the following: 19 | 20 | - Builds the latest Bluehawk executable 21 | - Runs all "test.sh" files found in subdirectories 22 | - Checksum-compares the contents of each test's "output" and "expected" directories 23 | 24 | ## Run a Specific Test 25 | 26 | The "runTests.sh" script optionally takes one argument. This can be the 27 | directory name that contains a test.sh file. 28 | 29 | The following example will only run the "snip" test: 30 | 31 | ```sh 32 | ./runTests.sh snip 33 | ``` 34 | 35 | ## Add a Test 36 | 37 | To create a test, copy an existing test as a template: 38 | 39 | ```sh 40 | cp -R ./snip myNewTest 41 | ``` 42 | 43 | Within your new test directory, create input files (if needed) in a directory 44 | called "input". 45 | 46 | Create the expected result file(s) and place them in a directory called 47 | "expected". 48 | 49 | Update the command in "myNewTest/test.sh" to the command you want to test. The 50 | `$BLUEHAWK` environment variable will be set to the current development build. 51 | 52 | Ensure the output of your command is sent to a directory called "output". 53 | 54 | Use the parent directory's "runTests.sh" script to run your test. 55 | -------------------------------------------------------------------------------- /integrationTests/copy/expected/state1/sample.txt: -------------------------------------------------------------------------------- 1 | Hi, this is a sample text. 2 | 3 | This text should be in state1 4 | 5 | That's all, folks! 6 | 7 | -------------------------------------------------------------------------------- /integrationTests/copy/expected/state2/sample.txt: -------------------------------------------------------------------------------- 1 | Hi, this is a sample text. 2 | 3 | This text should be in state2 4 | 5 | That's all, folks! 6 | 7 | -------------------------------------------------------------------------------- /integrationTests/copy/input/sample.txt: -------------------------------------------------------------------------------- 1 | Hi, this is a sample text. 2 | 3 | :state-start: state1 4 | This text should be in state1 5 | :state-end: 6 | :state-start: state2 7 | This text should be in state2 8 | :state-end: 9 | 10 | That's all, folks! 11 | 12 | -------------------------------------------------------------------------------- /integrationTests/copy/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | cd "$(dirname "$0")" 4 | 5 | rm -rf output 6 | mkdir output 7 | 8 | $BLUEHAWK copy --state state1 -o ./output/state1 input 9 | $BLUEHAWK copy --state state2 -o ./output/state2 input 10 | -------------------------------------------------------------------------------- /integrationTests/logLevel/expected/logLevel0: -------------------------------------------------------------------------------- 1 | Processed 3 files: 2 | - 0 binary files 3 | - 3 text files 4 | - 2 errors 5 | - 0 files written 6 | -------------------------------------------------------------------------------- /integrationTests/logLevel/expected/logLevel1: -------------------------------------------------------------------------------- 1 | bluehawk errors on path/to/project/input/sample.txt: 2 | (parser) Line 11:1 - 11:1(162) blockTag: After Newline, expected TagEnd but found EOF 3 | bluehawk errors on path/to/project/input/sample.txt: 4 | (parser) Line 11:1 - 11:1(162) blockTag: After Newline, expected TagEnd but found EOF 5 | Processed 3 files: 6 | - 0 binary files 7 | - 3 text files 8 | - 2 errors 9 | - 0 files written 10 | -------------------------------------------------------------------------------- /integrationTests/logLevel/expected/logLevel2: -------------------------------------------------------------------------------- 1 | bluehawk errors on path/to/project/input/sample.txt: 2 | (parser) Line 11:1 - 11:1(162) blockTag: After Newline, expected TagEnd but found EOF 3 | bluehawk errors on path/to/project/input/sample.txt: 4 | (parser) Line 11:1 - 11:1(162) blockTag: After Newline, expected TagEnd but found EOF 5 | state not found: notfound 6 | Processed 3 files: 7 | - 0 binary files 8 | - 3 text files 9 | - 2 errors 10 | - 0 files written 11 | -------------------------------------------------------------------------------- /integrationTests/logLevel/expected/logLevel3: -------------------------------------------------------------------------------- 1 | parsed file: path/to/project/input/normalFile.txt 2 | bluehawk errors on path/to/project/input/sample.txt: 3 | (parser) Line 11:1 - 11:1(162) blockTag: After Newline, expected TagEnd but found EOF 4 | bluehawk errors on path/to/project/input/sample.txt: 5 | (parser) Line 11:1 - 11:1(162) blockTag: After Newline, expected TagEnd but found EOF 6 | state not found: notfound 7 | Processed 3 files: 8 | - 0 binary files 9 | - 3 text files 10 | - 2 errors 11 | - 0 files written 12 | -------------------------------------------------------------------------------- /integrationTests/logLevel/input/normalFile.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/Bluehawk/6c5f2612111c3f6813b66fe95d5d05f07ff9e9e1/integrationTests/logLevel/input/normalFile.txt -------------------------------------------------------------------------------- /integrationTests/logLevel/input/sample.txt: -------------------------------------------------------------------------------- 1 | Hi, this is a sample text. 2 | 3 | 4 | 5 | :snippet-start: test-one 6 | This text should be in test-one 7 | 8 | Yes I forgot to close this snippet to force an error. 9 | 10 | That's all, folks! 11 | 12 | -------------------------------------------------------------------------------- /integrationTests/logLevel/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | cd "$(dirname "$0")" 4 | 5 | rm -rf output 6 | mkdir output 7 | 8 | # These are meant to cause errors, so don't fail out the script. 9 | set +e 10 | 11 | for LOGLEVEL in 0 1 2 3 12 | do 13 | # - Ignore the actual output, just look at the logs 14 | # - Use a state that is not actually found to trigger the state not found warning 15 | # - Set the log level to each possible level 16 | # - Redirect stderr to stdout 17 | # - Pipe to sed, delete any instances of your computer name 18 | # - Pipe to a file 19 | $BLUEHAWK snip -o output/ignoreThis --state notfound --logLevel $LOGLEVEL input 2>&1 | sed -e "s!$(pwd)!path/to/project!gi" > output/logLevel$LOGLEVEL 20 | done 21 | set -e 22 | -------------------------------------------------------------------------------- /integrationTests/runTests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Local build setup 4 | pushd "$(dirname "$0")" > /dev/null 5 | pushd .. > /dev/null 6 | PROJECT_ROOT="`pwd`" 7 | npm install 8 | npm run build 9 | popd > /dev/null 10 | export BLUEHAWK="node $PROJECT_ROOT/build/src/main" 11 | 12 | function checksum() { 13 | pushd "$1" > /dev/null 14 | CHECKSUM_RESULT=`find . -type f ! -name ".DS_Store" -exec shasum {} + | sort | shasum` 15 | popd > /dev/null 16 | } 17 | 18 | # Select tests 19 | if [ -z "$1" ] 20 | then 21 | TESTS=`find . -name "test.sh"` 22 | else 23 | TESTS="$1"/test.sh 24 | fi 25 | 26 | # Run tests 27 | for TEST in $TESTS 28 | do 29 | TEST_DIR="$(dirname "$TEST")" 30 | "$TEST" 31 | checksum ./"$TEST_DIR"/expected 32 | EXPECTED_CHECKSUM=$CHECKSUM_RESULT 33 | checksum ./"$TEST_DIR"/output 34 | OUTPUT_CHECKSUM=$CHECKSUM_RESULT 35 | if [ "$EXPECTED_CHECKSUM" != "$OUTPUT_CHECKSUM" ] 36 | then 37 | echo "Test failed: $TEST_DIR" 38 | echo "Compare $TEST_DIR/expected and $TEST_DIR/output to debug." 39 | exit 1 40 | else 41 | echo "✅ Test passed: $TEST_DIR" 42 | fi 43 | done 44 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.c-test.c: -------------------------------------------------------------------------------- 1 | #include 2 | int main() { 3 | printf("Hello, World!"); 4 | return 0; 5 | } 6 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.cpp-test.cpp: -------------------------------------------------------------------------------- 1 | auto something = SomeClass::someProperty; 2 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.failing-cpp-test.cpp: -------------------------------------------------------------------------------- 1 | auto something = SomeClass::state::something; 2 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.go-test.go: -------------------------------------------------------------------------------- 1 | import "fmt" 2 | 3 | func main() { 4 | fmt.Println("hello world") 5 | } 6 | 7 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.multi-line-block-comment.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Here's some block comment on multiple lines. 3 | */ 4 | auto something = SomeClass::state::something; 5 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.php-octothorp-test.php: -------------------------------------------------------------------------------- 1 | # Here's a comment with an octothorp 2 | 5 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.php-slash-test.php: -------------------------------------------------------------------------------- 1 | // Here's a comment with slashes 2 | 5 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.php-test.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.python-test.py: -------------------------------------------------------------------------------- 1 | print("This line will be printed.") 2 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.ruby-test.rb: -------------------------------------------------------------------------------- 1 | puts "Hello World" 2 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.rust-test.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello World!"); 3 | } 4 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.sc-test.sc: -------------------------------------------------------------------------------- 1 | object Hello { 2 | def main(args: Array[String]) = { 3 | println("Hello, world") 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.scala-test.scala: -------------------------------------------------------------------------------- 1 | object Hello { 2 | def main(args: Array[String]) = { 3 | println("Hello, world") 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.single-line-block-comment.cpp: -------------------------------------------------------------------------------- 1 | /* Here's some block comment on a single line */ 2 | auto something = SomeClass::state::something; 3 | -------------------------------------------------------------------------------- /integrationTests/snip/expected/sample.snippet.test-one.txt: -------------------------------------------------------------------------------- 1 | This text should be in test-one 2 | -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.c: -------------------------------------------------------------------------------- 1 | // :snippet-start: c-test 2 | #include 3 | int main() { 4 | // printf() displays the string inside quotation // :remove: 5 | printf("Hello, World!"); 6 | return 0; 7 | } 8 | // :snippet-end: -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.cpp: -------------------------------------------------------------------------------- 1 | // :snippet-start: cpp-test 2 | auto something = SomeClass::someProperty; 3 | // :snippet-end: 4 | 5 | // :snippet-start: failing-cpp-test 6 | auto something = SomeClass::state::something; 7 | // :snippet-end: 8 | 9 | // :snippet-start: multi-line-block-comment 10 | /* 11 | * Here's some block comment on multiple lines. 12 | */ 13 | auto something = SomeClass::state::something; 14 | // :snippet-end: 15 | 16 | // :snippet-start: single-line-block-comment 17 | /* Here's some block comment on a single line */ 18 | auto something = SomeClass::state::something; 19 | // :snippet-end: -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // :snippet-start: go-test 4 | import "fmt" 5 | 6 | func main() { 7 | // To FMT or LOG, *that* is the question // :remove: 8 | fmt.Println("hello world") 9 | } 10 | 11 | // :snippet-end: 12 | -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.php: -------------------------------------------------------------------------------- 1 | // :snippet-start: php-test 2 | # Sometimes comments start with octothorps :remove: 3 | 6 | // :snippet-end: 7 | 8 | # :snippet-start: php-octothorp-test 9 | # Here's a comment with an octothorp 10 | #And one without a space just for testing :remove: 11 | 14 | # :snippet-end: 15 | 16 | // :snippet-start: php-slash-test 17 | // Here's a comment with slashes 18 | //and one without a space just because :remove: 19 | 22 | // :snippet-end: 23 | -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.py: -------------------------------------------------------------------------------- 1 | # :snippet-start: python-test 2 | # My comment is the coolest, for real. :remove: 3 | print("This line will be printed.") 4 | # :snippet-end: -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.rb: -------------------------------------------------------------------------------- 1 | # :snippet-start: ruby-test 2 | # Here's a comment :remove: 3 | puts "Hello World" 4 | # :snippet-end: -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.rs: -------------------------------------------------------------------------------- 1 | // :snippet-start: rust-test 2 | fn main() { 3 | // Print text to the console. This is a boring comment. :remove: 4 | println!("Hello World!"); 5 | } 6 | // :snippet-end: -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.sc: -------------------------------------------------------------------------------- 1 | // :snippet-start: sc-test 2 | object Hello { 3 | // Arrrrrrrrrrgs! :remove: 4 | def main(args: Array[String]) = { 5 | println("Hello, world") 6 | } 7 | } 8 | // :snippet-end: -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.scala: -------------------------------------------------------------------------------- 1 | // :snippet-start: scala-test 2 | object Hello { 3 | // Why do we pass arguments - that sounds confrontational. :remove: 4 | def main(args: Array[String]) = { 5 | println("Hello, world") 6 | } 7 | } 8 | // :snippet-end: 9 | -------------------------------------------------------------------------------- /integrationTests/snip/input/sample.txt: -------------------------------------------------------------------------------- 1 | Hi, this is a sample text. 2 | 3 | :snippet-start: test-one 4 | This text should be in test-one 5 | :snippet-end: 6 | 7 | That's all, folks! 8 | 9 | -------------------------------------------------------------------------------- /integrationTests/snip/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | cd "$(dirname "$0")" 4 | 5 | rm -rf output 6 | mkdir output 7 | 8 | $BLUEHAWK snip -o ./output input 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testRegex: "\\.test\\.ts$", 4 | testEnvironment: "node", 5 | collectCoverage: true, 6 | coverageReporters: ["text", "html"], 7 | coverageDirectory: "/coverage", 8 | modulePathIgnorePatterns: ["/build"], 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluehawk", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/mongodb-university/Bluehawk" 6 | }, 7 | "version": "1.5.0", 8 | "description": "A markup language and tool for extracting code examples, checkpointing tutorials, and dynamically transforming text", 9 | "keywords": [ 10 | "docs", 11 | "docs-as-code", 12 | "docs tools", 13 | "tutorials", 14 | "code examples", 15 | "code snippets" 16 | ], 17 | "homepage": "https://github.com/mongodb-university/Bluehawk#readme", 18 | "bugs": { 19 | "url": "https://github.com/mongodb-university/Bluehawk/issues" 20 | }, 21 | "main": "./build/src", 22 | "bin": { 23 | "bluehawk": "./build/src/main.js" 24 | }, 25 | "scripts": { 26 | "clean": "rm -rf build", 27 | "build": "tsc -b", 28 | "watch": "tsc -b -w", 29 | "test": "jest", 30 | "verbose": "jest --verbose", 31 | "coverage": "jest --coverage", 32 | "integration": "./integrationTests/runTests.sh", 33 | "docs": "cd docs && npm i && npm run build", 34 | "release": "release-it" 35 | }, 36 | "author": "MongoDB Developer Education Team", 37 | "license": "Apache-2.0", 38 | "dependencies": { 39 | "ajv": "^8", 40 | "chevrotain": "^10", 41 | "ignore": "^5", 42 | "isbinaryfile": "^5", 43 | "magic-string": "^0.27", 44 | "memfs": "^3", 45 | "source-map": "^0.7", 46 | "yargs": "^17" 47 | }, 48 | "devDependencies": { 49 | "@babel/preset-typescript": "^7.0.0", 50 | "@types/jest": "^29", 51 | "@types/node": "^18", 52 | "@types/yargs": "^17", 53 | "@typescript-eslint/eslint-plugin": "^5", 54 | "eslint": "^8", 55 | "eslint-config-prettier": "^8", 56 | "eslint-plugin-tsdoc": "^0.2.0", 57 | "jest": "^29", 58 | "prettier": "^2", 59 | "release-it": "^16.1.5", 60 | "ts-jest": "^29", 61 | "typedoc": "^0.23", 62 | "typescript": "^4" 63 | }, 64 | "prettier": { 65 | "semi": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /patches/chevrotain+7.1.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/chevrotain/lib/chevrotain.d.ts b/node_modules/chevrotain/lib/chevrotain.d.ts 2 | index 032cd50..8279b78 100644 3 | --- a/node_modules/chevrotain/lib/chevrotain.d.ts 4 | +++ b/node_modules/chevrotain/lib/chevrotain.d.ts 5 | @@ -161,7 +161,7 @@ declare abstract class BaseParser { 6 | * A Parsing DSL method use to consume a single Token. 7 | * In EBNF terms this is equivalent to a Terminal. 8 | * 9 | - * A Token will be consumed, IFF the next token in the token vector matches . 10 | + * A Token will be consumed, IFF the next token in the token vector matches `tokType`. 11 | * otherwise the parser may attempt to perform error recovery (if enabled). 12 | * 13 | * The index in the method name indicates the unique occurrence of a terminal consumption 14 | -------------------------------------------------------------------------------- /src/bluehawk/BluehawkError.ts: -------------------------------------------------------------------------------- 1 | import { Location } from "./Location"; 2 | 3 | export interface BluehawkError { 4 | component: "lexer" | "parser" | "visitor" | "validator" | "processor"; 5 | message: string; 6 | location: Location; 7 | sourcePath?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/bluehawk/Document.test.ts: -------------------------------------------------------------------------------- 1 | import { Document } from "./Document"; 2 | 3 | describe("Document", () => { 4 | it("maps original locations to new", async () => { 5 | const document = new Document({ 6 | path: "", 7 | text: `line1 8 | line2 9 | line3 10 | line4 11 | `, 12 | }); 13 | document.text.remove(14, 20); // line3 14 | expect(document.text.toString()).toBe(`line1 15 | line2 16 | line4 17 | `); 18 | 19 | expect( 20 | await document.getNewLocationFor({ 21 | column: 1, 22 | line: 1, // Not affected 23 | }) 24 | ).toStrictEqual({ 25 | column: 1, 26 | line: 1, // No change 27 | }); 28 | 29 | expect( 30 | await document.getNewLocationFor({ 31 | column: 3, 32 | line: 4, // Line above was removed... 33 | }) 34 | ).toStrictEqual({ 35 | column: 3, 36 | line: 3, // ...so line is 1 less 37 | }); 38 | 39 | expect( 40 | await document.getNewLocationFor({ 41 | column: 1, 42 | line: 6, // Outside of range 43 | }) 44 | ).toBeUndefined(); 45 | 46 | expect( 47 | await document.getNewLocationFor({ 48 | column: 1, 49 | line: 3, // line was deleted 50 | }) 51 | ).toStrictEqual({ 52 | column: 1, 53 | line: 3, // :shrug: 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/bluehawk/Location.ts: -------------------------------------------------------------------------------- 1 | export interface Location { 2 | line: number; 3 | column: number; 4 | offset: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/bluehawk/OnBinaryFileFunction.ts: -------------------------------------------------------------------------------- 1 | export type OnBinaryFileFunction = (file: string) => void | Promise; 2 | -------------------------------------------------------------------------------- /src/bluehawk/OnErrorFunction.ts: -------------------------------------------------------------------------------- 1 | import { BluehawkError } from "./BluehawkError"; 2 | 3 | export type OnErrorFunction = ( 4 | filePath: string, 5 | errors: BluehawkError[] 6 | ) => void | Promise; 7 | 8 | // A standard implementation of OnErrorFunction 9 | export const logErrorsToConsole: OnErrorFunction = (filePath, errors) => { 10 | console.error( 11 | `Error in ${filePath}:\n${errors 12 | .map( 13 | (error) => 14 | ` at line ${error.location.line} col ${error.location.column}: ${error.message}` 15 | ) 16 | .join("\n")}` 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/bluehawk/Plugin.ts: -------------------------------------------------------------------------------- 1 | import * as Path from "path"; 2 | import yargs, { Argv } from "yargs"; 3 | import { Bluehawk } from "./bluehawk"; 4 | 5 | /** 6 | A plugin is a Node module that exports a register() function. 7 | 8 | A plugin can be a simple JS file or a transpiled Node module, as long as it 9 | exports the register() function: 10 | 11 | ```js 12 | // MyPlugin.js 13 | exports.register = (args) => { 14 | // Add a new CLI option 15 | args.yargs.option("myNewOption", { string: true }); 16 | 17 | // Add Bluehawk listener 18 | args.bluehawk.subscribe((result) => { 19 | console.log("Plugin called for file", result.document.path); 20 | }); 21 | }; 22 | ``` 23 | 24 | You can then call `bluehawk --plugin /path/to/MyPlugin.js` to use the plugin. 25 | */ 26 | export type Plugin = { 27 | /** 28 | The CLI calls this function to allow the plugin to modify the main 29 | [[Bluehawk]] instance or the CLI itself. 30 | */ 31 | register(args: PluginArgs): Promise | void; 32 | }; 33 | 34 | export type LoadedPlugin = Plugin & { 35 | /** 36 | The path to the plugin as provided to the CLI by the --plugin option. 37 | */ 38 | path: string; 39 | }; 40 | 41 | /** 42 | The arguments passed from the CLI to a plugin's register() function. 43 | */ 44 | export interface PluginArgs { 45 | /** 46 | The [[Bluehawk]] instance that a plugin can use to add Bluehawk commands, 47 | languages, and listeners. 48 | */ 49 | bluehawk: Bluehawk; 50 | 51 | /** 52 | The [yargs](https://yargs.js.org/) instance that a plugin can modify to add 53 | CLI commands and options. 54 | */ 55 | yargs: Argv; 56 | 57 | /** 58 | The current semantic version string of Bluehawk. 59 | */ 60 | bluehawkVersion: string; 61 | 62 | /** 63 | The current semantic version string of Yargs. 64 | */ 65 | yargsVersion: string; 66 | } 67 | 68 | /** 69 | Load the given plugin(s). 70 | */ 71 | export const loadPlugins = async ( 72 | path: string | string[] | undefined 73 | ): Promise => { 74 | if (path === undefined) { 75 | return []; 76 | } 77 | 78 | if (Array.isArray(path)) { 79 | const plugins = await Promise.all(path.map((path) => loadPlugins(path))); 80 | return plugins.flat(); 81 | } 82 | 83 | // Convert potentially relative path (from user's cwd) to absolute path -- as 84 | // import() expects relative paths from Bluehawk bin directory 85 | const absolutePath = Path.resolve(path); 86 | const { register } = await import(absolutePath); 87 | 88 | if (typeof register !== "function") { 89 | throw new Error( 90 | `loading plugin '${path}': expected function register(args) to be exported` 91 | ); 92 | } 93 | 94 | return [ 95 | { 96 | path, 97 | register, 98 | }, 99 | ]; 100 | }; 101 | 102 | /** 103 | Loads a directory as yargs commands while supporting TypeScript for development. 104 | See yargs.commandDir(). 105 | */ 106 | export function commandDir( 107 | argv: yargs.Argv, 108 | directory: string, 109 | options?: yargs.RequireDirectoryOptions 110 | ): yargs.Argv { 111 | // Centralize the workaround for commandDir with TS 112 | return argv.commandDir(directory, { 113 | extensions: process.env.NODE_ENV === "development" ? ["js", "ts"] : ["js"], 114 | exclude: /^(?:index|.*\.test)\.[jt]s$/, 115 | visit(commandModule) { 116 | return commandModule.default; 117 | }, 118 | ...options, 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /src/bluehawk/Range.ts: -------------------------------------------------------------------------------- 1 | import { Location } from "./Location"; 2 | 3 | export interface Range { 4 | start: Location; 5 | end: Location; 6 | } 7 | -------------------------------------------------------------------------------- /src/bluehawk/actions/ActionArgs.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from "./ActionReporter"; 2 | export interface ActionArgs { 3 | logLevel?: LogLevel; 4 | waitForListeners?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/bluehawk/actions/ActionReporter.ts: -------------------------------------------------------------------------------- 1 | import { ParseResult } from "./../"; 2 | import { BluehawkError } from "./../BluehawkError"; 3 | 4 | /** 5 | Creates a type with a required ActionReporter field. 6 | */ 7 | export type WithActionReporter = T & { reporter: ActionReporter }; 8 | 9 | /** 10 | Handles various events for user information. 11 | */ 12 | export interface ActionReporter { 13 | logLevel: LogLevel; 14 | readonly errorCount: number; 15 | 16 | // Info 17 | onBinaryFile(event: BinaryFileEvent): void; 18 | onFileParsed(event: FileParsedEvent): void; 19 | onFileWritten(event: FileWrittenEvent): void; 20 | onStatesFound(event: StatesFoundEvent): void; 21 | 22 | // Warnings 23 | onStateNotFound(event: StateNotFoundEvent): void; 24 | onIdsUnused(event: IdsUnusedEvent): void; 25 | onParserNotFound(event: ParserNotFoundEvent): void; 26 | 27 | // Errors 28 | onFileError(event: FileErrorEvent): void; 29 | onWriteFailed(event: WriteFailedEvent): void; 30 | onBluehawkErrors(event: BluehawkErrorsEvent): void; 31 | 32 | /** 33 | Request the summary of all things reported so far. 34 | 35 | Users should call this after an action is complete. 36 | */ 37 | printSummary(): void; 38 | } 39 | 40 | export enum LogLevel { 41 | None = 0, 42 | Error, 43 | Warning, 44 | Info, 45 | } 46 | 47 | export type FileEvent = { 48 | inputPath: string; 49 | }; 50 | 51 | export type BinaryFileEvent = FileEvent; 52 | 53 | export type FileParsedEvent = FileEvent & { 54 | parseResult: ParseResult; 55 | }; 56 | 57 | export type FileWrittenEvent = FileEvent & { 58 | outputPath: string; 59 | type: "text" | "binary"; 60 | }; 61 | 62 | export type StatesFoundEvent = { 63 | action: string; 64 | paths: string[]; 65 | statesFound: string[]; 66 | }; 67 | 68 | export type StateNotFoundEvent = { 69 | paths: string[]; 70 | state: string; 71 | }; 72 | 73 | export type IdsUnusedEvent = { 74 | paths: string[]; 75 | ids: string[]; 76 | }; 77 | 78 | export type ParserNotFoundEvent = FileEvent & { 79 | error: Error; 80 | }; 81 | 82 | export type FileErrorEvent = FileEvent & { 83 | error: Error; 84 | }; 85 | 86 | export type WriteFailedEvent = FileWrittenEvent & { 87 | error: Error; 88 | }; 89 | 90 | export type BluehawkErrorsEvent = FileEvent & { 91 | errors: BluehawkError[]; 92 | }; 93 | -------------------------------------------------------------------------------- /src/bluehawk/actions/ConsoleActionReporter.test.ts: -------------------------------------------------------------------------------- 1 | import { ActionReporter, LogLevel } from "./ActionReporter"; 2 | import { Document } from "./../Document"; 3 | import { ConsoleActionReporter } from "./ConsoleActionReporter"; 4 | 5 | const dummyReport = (reporter: ActionReporter) => { 6 | reporter.onBinaryFile({ 7 | inputPath: "test", 8 | }); 9 | reporter.onBluehawkErrors({ 10 | inputPath: "test", 11 | errors: [], 12 | }); 13 | reporter.onFileError({ 14 | inputPath: "test", 15 | error: new Error(), 16 | }); 17 | reporter.onFileParsed({ 18 | inputPath: "foo", 19 | parseResult: { 20 | errors: [], 21 | source: {} as Document, 22 | tagNodes: [], 23 | }, 24 | }); 25 | reporter.onFileWritten({ 26 | outputPath: "foo", 27 | inputPath: "bar", 28 | type: "text", 29 | }); 30 | reporter.onIdsUnused({ 31 | ids: [], 32 | paths: [], 33 | }); 34 | reporter.onParserNotFound({ 35 | error: new Error(), 36 | inputPath: "test", 37 | }); 38 | reporter.onStateNotFound({ 39 | paths: [], 40 | state: "foo", 41 | }); 42 | reporter.onStatesFound({ 43 | action: "", 44 | paths: [], 45 | statesFound: [], 46 | }); 47 | reporter.onWriteFailed({ 48 | type: "text", 49 | outputPath: "foo", 50 | inputPath: "bar", 51 | error: new Error(), 52 | }); 53 | }; 54 | 55 | class ConsoleCounter { 56 | log = 0; 57 | warn = 0; 58 | error = 0; 59 | 60 | constructor() { 61 | console.log = () => { 62 | ++this.log; 63 | }; 64 | console.warn = () => { 65 | ++this.warn; 66 | }; 67 | console.error = () => { 68 | ++this.error; 69 | }; 70 | } 71 | } 72 | 73 | describe("ConsoleActionReporter", () => { 74 | const originalConsole = { ...console }; 75 | afterEach(() => { 76 | console.log = originalConsole.log; 77 | console.warn = originalConsole.warn; 78 | console.error = originalConsole.error; 79 | }); 80 | 81 | it("allows log level setting", () => { 82 | const reporter = new ConsoleActionReporter(); 83 | 84 | let counter = new ConsoleCounter(); 85 | reporter.logLevel = LogLevel.Info; 86 | dummyReport(reporter); 87 | expect(counter.log).toBeGreaterThan(0); 88 | expect(counter.warn).toBeGreaterThan(0); 89 | expect(counter.error).toBeGreaterThan(0); 90 | 91 | counter = new ConsoleCounter(); 92 | reporter.logLevel = LogLevel.Warning; 93 | dummyReport(reporter); 94 | expect(counter.log).toBe(0); 95 | expect(counter.warn).toBeGreaterThan(0); 96 | expect(counter.error).toBeGreaterThan(0); 97 | 98 | counter = new ConsoleCounter(); 99 | reporter.logLevel = LogLevel.Error; 100 | dummyReport(reporter); 101 | expect(counter.log).toBe(0); 102 | expect(counter.warn).toBe(0); 103 | expect(counter.error).toBeGreaterThan(0); 104 | 105 | counter = new ConsoleCounter(); 106 | reporter.logLevel = LogLevel.None; 107 | dummyReport(reporter); 108 | expect(counter.log).toBe(0); 109 | expect(counter.warn).toBe(0); 110 | expect(counter.error).toBe(0); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/bluehawk/actions/ConsoleActionReporter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionReporter, 3 | BluehawkErrorsEvent, 4 | FileErrorEvent, 5 | FileEvent, 6 | FileParsedEvent, 7 | FileWrittenEvent, 8 | IdsUnusedEvent, 9 | LogLevel, 10 | ParserNotFoundEvent, 11 | StateNotFoundEvent, 12 | StatesFoundEvent, 13 | WriteFailedEvent, 14 | } from "./ActionReporter"; 15 | 16 | export class ConsoleActionReporter implements ActionReporter { 17 | _count = { 18 | binaryFiles: 0, 19 | textFiles: 0, 20 | filesWritten: 0, 21 | errors: 0, 22 | }; 23 | 24 | logLevel: LogLevel = LogLevel.Info; 25 | 26 | constructor(args?: { logLevel?: LogLevel }) { 27 | if (args?.logLevel !== undefined) { 28 | this.logLevel = args.logLevel; 29 | } 30 | } 31 | 32 | get errorCount(): number { 33 | return this._count.errors; 34 | } 35 | 36 | onBinaryFile = (event: FileEvent): void => { 37 | ++this._count.binaryFiles; 38 | if (this.logLevel >= LogLevel.Info) { 39 | console.log(`found binary file: ${event.inputPath}`); 40 | } 41 | }; 42 | 43 | onFileParsed = (event: FileParsedEvent): void => { 44 | ++this._count.textFiles; 45 | if (this.logLevel >= LogLevel.Info) { 46 | console.log(`parsed file: ${event.inputPath}`); 47 | } 48 | }; 49 | 50 | onFileWritten = (event: FileWrittenEvent): void => { 51 | ++this._count.filesWritten; 52 | if (this.logLevel >= LogLevel.Info) { 53 | console.log( 54 | `wrote ${event.type} file based on ${event.inputPath} -> ${event.outputPath}` 55 | ); 56 | } 57 | }; 58 | 59 | onStatesFound = (event: StatesFoundEvent): void => { 60 | if (this.logLevel >= LogLevel.Info) { 61 | console.log(`found states: ${event.statesFound.join(", ")}`); 62 | } 63 | }; 64 | 65 | onStateNotFound = (event: StateNotFoundEvent): void => { 66 | if (this.logLevel >= LogLevel.Warning) { 67 | console.warn(`state not found: ${event.state}`); 68 | } 69 | }; 70 | 71 | onIdsUnused = (event: IdsUnusedEvent): void => { 72 | if (this.logLevel >= LogLevel.Warning) { 73 | console.warn(`ids not used: ${event.ids.join(", ")}`); 74 | } 75 | }; 76 | 77 | onParserNotFound = (event: ParserNotFoundEvent): void => { 78 | if (this.logLevel >= LogLevel.Warning) { 79 | console.warn( 80 | `parser not found for file ${event.inputPath}: ${event.error.message}` 81 | ); 82 | } 83 | }; 84 | 85 | onFileError = (event: FileErrorEvent): void => { 86 | ++this._count.errors; 87 | if (this.logLevel >= LogLevel.Error) { 88 | console.error(`file error: ${event.inputPath}: ${event.error.message}`); 89 | } 90 | }; 91 | 92 | onWriteFailed = (event: WriteFailedEvent): void => { 93 | ++this._count.errors; 94 | if (this.logLevel >= LogLevel.Error) { 95 | console.error( 96 | `failed to write file ${event.inputPath} -> ${event.outputPath}: ${event.error.message}` 97 | ); 98 | } 99 | }; 100 | 101 | onBluehawkErrors = (event: BluehawkErrorsEvent): void => { 102 | ++this._count.textFiles; 103 | this._count.errors += event.errors.length; 104 | if (this.logLevel >= LogLevel.Error) { 105 | console.error( 106 | `bluehawk errors on ${event.inputPath}:\n${event.errors 107 | .map((error) => { 108 | return `(${error.component}) Line ${error.location.line}:${error.location.column} - ${error.message}`; 109 | }) 110 | .join("\n")}` 111 | ); 112 | } 113 | }; 114 | 115 | printSummary = (): void => { 116 | const { binaryFiles, errors, textFiles, filesWritten } = this._count; 117 | console.log(`Processed ${binaryFiles + textFiles} files: 118 | - ${binaryFiles} binary files 119 | - ${textFiles} text files 120 | - ${errors} errors 121 | - ${filesWritten} files written`); 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /src/bluehawk/actions/check.ts: -------------------------------------------------------------------------------- 1 | import { WithActionReporter } from "./ActionReporter"; 2 | import { getBluehawk } from "../../bluehawk"; 3 | import { BluehawkError } from "../../bluehawk/BluehawkError"; 4 | import { ActionArgs } from "./ActionArgs"; 5 | import { printJsonResult } from "./printJsonResult"; 6 | 7 | export interface CheckArgs extends ActionArgs { 8 | paths: string[]; 9 | ignore?: string | string[]; 10 | json?: boolean; 11 | } 12 | 13 | export interface CheckResult {} 14 | 15 | export const check = async ( 16 | args: WithActionReporter 17 | ): Promise => { 18 | const { ignore, json, paths, waitForListeners, reporter } = args; 19 | const bluehawk = await getBluehawk(); 20 | const fileToErrorMap = new Map(); 21 | 22 | const addErrors = (filePath: string, errors: BluehawkError[]) => { 23 | reporter.onBluehawkErrors({ 24 | errors, 25 | inputPath: filePath, 26 | }); 27 | const existingErrors = fileToErrorMap.get(filePath) ?? []; 28 | fileToErrorMap.set(filePath, [...existingErrors, ...errors]); 29 | }; 30 | 31 | // Define the handler for generating snippet files. 32 | bluehawk.subscribe(({ parseResult }) => { 33 | const { errors, source } = parseResult; 34 | if (errors.length !== 0) { 35 | addErrors(source.path, errors); 36 | } 37 | }); 38 | 39 | // Run through all given source paths and process them. 40 | await bluehawk.parseAndProcess(paths, { 41 | ignore, 42 | onErrors: addErrors, 43 | waitForListeners: waitForListeners ?? false, 44 | reporter, 45 | }); 46 | 47 | if (json) { 48 | const errorsByPath = Object.fromEntries(Array.from(fileToErrorMap)); 49 | printJsonResult(args, errorsByPath); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/bluehawk/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./check"; 2 | export * from "./copy"; 3 | export * from "./listStates"; 4 | export * from "./listTags"; 5 | export * from "./snip"; 6 | export * from "./ActionArgs"; 7 | -------------------------------------------------------------------------------- /src/bluehawk/actions/listStates.ts: -------------------------------------------------------------------------------- 1 | import { WithActionReporter } from "./ActionReporter"; 2 | import { getBluehawk } from "../../bluehawk"; 3 | import { ActionArgs } from "./ActionArgs"; 4 | import { printJsonResult } from "./printJsonResult"; 5 | 6 | export interface ListStatesArgs extends ActionArgs { 7 | paths: string[]; 8 | json?: boolean; 9 | ignore?: string | string[]; 10 | } 11 | 12 | export const listStates = async ( 13 | args: WithActionReporter 14 | ): Promise => { 15 | const { ignore, json, paths, waitForListeners, reporter } = args; 16 | const bluehawk = await getBluehawk(); 17 | 18 | const statesFound = new Set(); 19 | bluehawk.subscribe((result) => { 20 | const { document } = result; 21 | const { state } = document.attributes; 22 | if (state === undefined) { 23 | return; 24 | } 25 | statesFound.add(state as string); 26 | }); 27 | 28 | await bluehawk.parseAndProcess(paths, { 29 | reporter, 30 | ignore, 31 | waitForListeners: waitForListeners ?? false, 32 | }); 33 | 34 | if (json) { 35 | printJsonResult(args, { states: Array.from(statesFound) }); 36 | return; 37 | } 38 | 39 | reporter.onStatesFound({ 40 | action: "listStates", 41 | paths, 42 | statesFound: Array.from(statesFound), 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/bluehawk/actions/listTags.ts: -------------------------------------------------------------------------------- 1 | import { getBluehawk } from ".."; 2 | import { ActionArgs } from "./ActionArgs"; 3 | import { printJsonResult } from "./printJsonResult"; 4 | 5 | export interface ListTagArgs extends ActionArgs { 6 | json?: boolean; 7 | } 8 | 9 | export const listTags = async (args: ListTagArgs): Promise => { 10 | const { json } = args; 11 | const bluehawk = await getBluehawk(); 12 | 13 | const { processor } = bluehawk; 14 | const { processors } = processor; 15 | 16 | if (json) { 17 | const tags = Object.entries(processors).map(([registeredName, tag]) => { 18 | const aliasOf = registeredName !== tag.name ? tag.name : undefined; 19 | const name = registeredName; 20 | const { 21 | description, 22 | supportsBlockMode, 23 | supportsLineMode, 24 | attributesSchema, 25 | } = tag; 26 | return { 27 | name, 28 | aliasOf, 29 | description, 30 | supportsBlockMode, 31 | supportsLineMode, 32 | attributesSchema, 33 | }; 34 | }); 35 | printJsonResult(args, { tags }); 36 | return; 37 | } 38 | 39 | const tagsListText = Object.entries(processors) 40 | .map(([registeredName, tag]) => { 41 | const isAlias = registeredName !== tag.name; 42 | const name = isAlias 43 | ? `${registeredName} (alias of ${tag.name})` 44 | : tag.name; 45 | return `${name}\n${[ 46 | `${tag.description ?? "No description"}`, 47 | `block mode supported: ${tag.supportsBlockMode ? "yes" : "no"}`, 48 | `line mode supported: ${tag.supportsLineMode ? "yes" : "no"}`, 49 | `attributes schema: ${ 50 | tag.attributesSchema !== undefined 51 | ? JSON.stringify(tag.attributesSchema) 52 | : "N/A" 53 | }`, 54 | ] 55 | .map((s) => ` ${s}`) 56 | .join("\n")}`; 57 | }) 58 | .join("\n\n"); 59 | 60 | console.log(`available markup tags:\n\n${tagsListText}`); 61 | }; 62 | -------------------------------------------------------------------------------- /src/bluehawk/actions/printJsonResult.ts: -------------------------------------------------------------------------------- 1 | import { ActionArgs } from "./ActionArgs"; 2 | import { version } from "../../../package.json"; 3 | 4 | // Given the --json flag, prints results with additional useful information in a 5 | // uniform way 6 | export function printJsonResult( 7 | args: T, 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | result: Record 10 | ): void { 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | const { json, ...info } = args; // Copy the args for output, minus json 13 | console.log( 14 | JSON.stringify({ 15 | bluehawkVersion: version, 16 | ...info, 17 | ...result, 18 | }) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/bluehawk/getBluehawk.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Bluehawk, 3 | SnippetTag, 4 | ReplaceTag, 5 | RemoveTag, 6 | StateTag, 7 | StateRemoveTag, 8 | UncommentTag, 9 | EmphasizeTag, 10 | tokens, 11 | } from "."; 12 | import { 13 | makeBlockTag, 14 | IdRequiredAttributes, 15 | IdRequiredAttributesSchema, 16 | } from "./tags"; 17 | 18 | let bluehawk: Bluehawk | undefined = undefined; 19 | 20 | /** 21 | Returns a standard, shared Bluehawk instance. 22 | */ 23 | export const getBluehawk = async (): Promise => { 24 | if (bluehawk === undefined) { 25 | const StateUncommentTag = makeBlockTag({ 26 | name: "state-uncomment", 27 | description: "combines 'uncomment' and 'state'", 28 | shorthandArgsAttributeName: "id", 29 | attributesSchema: IdRequiredAttributesSchema, 30 | process(request) { 31 | UncommentTag.process(request); 32 | StateTag.process(request); 33 | }, 34 | }); 35 | 36 | bluehawk = new Bluehawk({ 37 | tags: [ 38 | RemoveTag, 39 | ReplaceTag, 40 | SnippetTag, 41 | StateTag, 42 | StateRemoveTag, 43 | StateUncommentTag, 44 | UncommentTag, 45 | EmphasizeTag, 46 | ], 47 | }); 48 | 49 | // Add all supported extensions here. 50 | bluehawk.addLanguage( 51 | [ 52 | ".c", 53 | ".cpp", 54 | ".cs", 55 | ".dart", 56 | ".go", 57 | ".gradle", 58 | ".groovy", 59 | ".gsh", 60 | ".gvy", 61 | ".gy", 62 | ".h", 63 | ".hpp", 64 | ".java", 65 | ".js", 66 | ".jsx", 67 | ".kt", 68 | ".m", 69 | ".mm", 70 | ".rs", 71 | ".sc", 72 | ".scala", 73 | ".swift", 74 | ".ts", 75 | ".tsx", 76 | ], 77 | { 78 | languageId: "C-like", 79 | blockComments: [[/\/\*/, /\*\//]], 80 | lineComments: [/\/\/ ?/], 81 | } 82 | ); 83 | 84 | bluehawk.addLanguage([".php"], { 85 | languageId: "PHP", 86 | lineComments: [/#|\/\//], 87 | blockComments: [[/\/\*/, /\*\//]], 88 | }); 89 | 90 | bluehawk.addLanguage([".py"], { 91 | languageId: "Python", 92 | lineComments: [/# ?/], 93 | stringLiterals: [ 94 | { 95 | pattern: tokens.PYTHON_STRING_LITERAL_PATTERN, 96 | multiline: true, 97 | }, 98 | ], 99 | }); 100 | 101 | bluehawk.addLanguage([".rb"], { 102 | languageId: "Ruby", 103 | lineComments: [/# ?/], 104 | }); 105 | 106 | bluehawk.addLanguage(["", ".txt", ".rst", ".md", ".json"], { 107 | languageId: "text", 108 | }); 109 | 110 | bluehawk.addLanguage([".yaml", ".sh"], { 111 | languageId: "bashlike", 112 | lineComments: [/# ?/], 113 | // String literal specification required as it's the only way to use # as 114 | // not-a-comment 115 | stringLiterals: [ 116 | { 117 | pattern: tokens.JSON_STRING_LITERAL_PATTERN, 118 | multiline: false, 119 | }, 120 | ], 121 | }); 122 | 123 | bluehawk.addLanguage([".xml", ".svg", ".html", ".htm", ".uxml", ".xaml"], { 124 | languageId: "xml", 125 | blockComments: [[//]], 126 | }); 127 | } 128 | 129 | return bluehawk; 130 | }; 131 | 132 | /** 133 | Resets the bluehawk instance (for unit testing). 134 | */ 135 | getBluehawk.reset = (): Promise => { 136 | bluehawk = undefined; 137 | return getBluehawk(); 138 | }; 139 | -------------------------------------------------------------------------------- /src/bluehawk/index.ts: -------------------------------------------------------------------------------- 1 | export { Bluehawk } from "./bluehawk"; 2 | export { Document } from "./Document"; 3 | export { OnBinaryFileFunction } from "./OnBinaryFileFunction"; 4 | export { getBluehawk } from "./getBluehawk"; 5 | export * from "./Plugin"; 6 | export * from "./io/System"; 7 | export * from "./tags"; 8 | export * from "./parser"; 9 | export * from "./processor"; 10 | export * from "./project"; 11 | export * from "./actions"; 12 | export * from "./options"; 13 | export * from "./actions/ActionReporter"; 14 | export * from "./actions/ConsoleActionReporter"; 15 | -------------------------------------------------------------------------------- /src/bluehawk/io/System.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-explicit-any: 0 */ 2 | /* eslint @typescript-eslint/explicit-module-boundary-types: 0 */ 3 | import fs from "fs"; 4 | import { createFsFromVolume, DirectoryJSON, Volume } from "memfs"; 5 | 6 | // Mockable fs implementation. Use this instead of fs directly. 7 | export const System = { 8 | fs: fs.promises, 9 | 10 | // You can use this in a jest file like so: 11 | /* 12 | beforeEach(System.useMemfs); 13 | */ 14 | useMemfs() { 15 | // Use an empty fs so the state is not retained between test files 16 | System.useJsonFs({}); 17 | }, 18 | 19 | // You can use this in a jest test like so: 20 | /* 21 | System.useJsonFs({ 22 | "/path/to/foo.txt": "this is the content of foo.txt" 23 | }) 24 | */ 25 | useJsonFs(directoryJson: DirectoryJSON) { 26 | const fsFromVolume = createFsFromVolume(Volume.fromJSON(directoryJson)); 27 | System.fs = fsFromVolume.promises as unknown as typeof fs.promises; 28 | }, 29 | 30 | useRealfs() { 31 | System.fs = fs.promises; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/bluehawk/io/messageHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MessageHandler, 3 | WarningMessage, 4 | ImportantMessage, 5 | InfoMessage, 6 | ErrorMessage, 7 | } from "./messageHandler"; 8 | 9 | describe("MessageHandler", () => { 10 | let handler1: MessageHandler; 11 | let handler2: MessageHandler; 12 | beforeAll(() => { 13 | handler1 = MessageHandler.getMessageHandler(); 14 | handler2 = MessageHandler.getMessageHandler(); 15 | }); 16 | 17 | it("handles accepting error information", () => { 18 | const messageContent = "foo bar biz baz"; 19 | const expectedErrorObject = new ErrorMessage(messageContent); 20 | handler1.addError(messageContent); 21 | expect(handler1.errorList.length).toEqual(1); 22 | expect(handler1.errorList.shift()).toEqual(expectedErrorObject); 23 | }); 24 | 25 | it("handles information messages", () => { 26 | const messageContent = "foo bar biz baz"; 27 | const expectedInformationObject = new InfoMessage(messageContent); 28 | const expectedWarningMessage = new WarningMessage(messageContent); 29 | const expectedImportantMessage = new ImportantMessage(messageContent); 30 | handler1.addImportant(messageContent); 31 | handler1.addWarning(messageContent); 32 | handler1.addInformational(messageContent); 33 | expect(handler1.infoList.length).toBe(3); 34 | expect(handler1.infoList).toContainEqual(expectedInformationObject); 35 | expect(handler1.infoList).toContainEqual(expectedImportantMessage); 36 | expect(handler1.infoList).toContainEqual(expectedWarningMessage); 37 | }); 38 | it("is a singleton", () => { 39 | expect(handler1 === handler2).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/bluehawk/io/messageHandler.ts: -------------------------------------------------------------------------------- 1 | export class MessageHandler { 2 | private static instance: MessageHandler; 3 | private _errorsList: ErrorMessage[] = []; 4 | private _infoList: NonErrorMessage[] = []; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-empty-function 7 | private constructor() {} 8 | 9 | public static getMessageHandler(): MessageHandler { 10 | if (!MessageHandler.instance) { 11 | MessageHandler.instance = new MessageHandler(); 12 | } 13 | return MessageHandler.instance; 14 | } 15 | 16 | get errorList(): ErrorMessage[] { 17 | return this._errorsList; 18 | } 19 | 20 | get infoList(): NonErrorMessage[] { 21 | return this._infoList; 22 | } 23 | 24 | public addWarning(...text: string[]): void { 25 | this._infoList.push(new WarningMessage(...text)); 26 | } 27 | 28 | public addError(...text: string[]): void { 29 | this._errorsList.push(new ErrorMessage(...text)); 30 | } 31 | 32 | public addImportant(...text: string[]): void { 33 | this._infoList.push(new ImportantMessage(...text)); 34 | } 35 | 36 | public addInformational(...text: string[]): void { 37 | this._infoList.push(new InfoMessage(...text)); 38 | } 39 | } 40 | 41 | class DiagnosticMessage { 42 | messageType = ""; 43 | prefix = ""; 44 | message: string[]; 45 | constructor(...message: string[]) { 46 | this.message = message; 47 | } 48 | } 49 | 50 | export class WarningMessage extends DiagnosticMessage { 51 | messageType = "warning"; 52 | prefix = "\n⚠️\t"; 53 | } 54 | 55 | export class ImportantMessage extends DiagnosticMessage { 56 | messageType = "important"; 57 | prefix = "\n❗\t"; 58 | } 59 | 60 | export class InfoMessage extends DiagnosticMessage { 61 | messageType = "info"; 62 | } 63 | 64 | export class ErrorMessage extends DiagnosticMessage { 65 | messageType = "error"; 66 | } 67 | 68 | type NonErrorMessage = WarningMessage | ImportantMessage | InfoMessage; 69 | -------------------------------------------------------------------------------- /src/bluehawk/options.ts: -------------------------------------------------------------------------------- 1 | import { Argv, Options } from "yargs"; 2 | 3 | function option( 4 | yargs: Argv, 5 | key: K, 6 | options: O 7 | ) { 8 | return yargs.option(key, options).check((argv) => { 9 | if (options.once && Array.isArray(argv[key])) { 10 | throw new Error(`Cannot accept option '${key}' multiple times`); 11 | } 12 | return true; 13 | }); 14 | } 15 | 16 | export function withOutputOption( 17 | yargs: Argv 18 | ): Argv { 19 | return option(yargs, "output", { 20 | alias: "o", 21 | string: true, 22 | describe: "the output directory", 23 | required: true, 24 | once: true, 25 | }).check((argv) => !Array.isArray(argv.state)); 26 | } 27 | 28 | export function withIgnoreOption( 29 | yargs: Argv 30 | ): Argv { 31 | return option(yargs, "ignore", { 32 | alias: "i", 33 | string: true, 34 | describe: 35 | "ignore a certain file pattern (like gitignore) when traversing project files. Provide paths relative to project root.", 36 | }); 37 | } 38 | 39 | export function withStateOption( 40 | yargs: Argv 41 | ): Argv { 42 | return option(yargs, "state", { 43 | string: true, 44 | describe: "select snippets etc. from the given state", 45 | once: true, 46 | }); 47 | } 48 | 49 | export function withRenameOption( 50 | yargs: Argv 51 | ): Argv { 52 | return option(yargs, "rename", { 53 | string: true, 54 | describe: "rename files during copy", 55 | once: true, 56 | }); 57 | } 58 | 59 | export function withIdOption( 60 | yargs: Argv 61 | ): Argv { 62 | return option(yargs, "id", { 63 | string: true, 64 | describe: "select snippets with a specific id", 65 | }); 66 | } 67 | 68 | export function withGenerateFormattedCodeSnippetsOption( 69 | yargs: Argv 70 | ): Argv { 71 | return option(yargs, "format", { 72 | requiresArg: true, 73 | choices: ["rst", "docusaurus"], 74 | alias: "f", 75 | string: true, 76 | describe: 77 | "generate code snippets with markup formatting, e.g. emphasized lines, specified language", 78 | once: true, 79 | }); 80 | } 81 | 82 | export function withJsonOption( 83 | yargs: Argv 84 | ): Argv { 85 | return option(yargs, "json", { 86 | boolean: true, 87 | describe: "output as json", 88 | once: true, 89 | }); 90 | } 91 | 92 | export function withLogLevelOption( 93 | yargs: Argv 94 | ): Argv { 95 | return option(yargs, "logLevel", { 96 | number: true, 97 | describe: 98 | "set the reporter log level. 0=none, 1=errors only, 2=warnings & above, 3=all", 99 | once: true, 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /src/bluehawk/parser/ErrorMessageProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultParserErrorProvider, 3 | IParserErrorMessageProvider, 4 | IToken, 5 | TokenType, 6 | } from "chevrotain"; 7 | 8 | export class ErrorMessageProvider implements IParserErrorMessageProvider { 9 | buildMismatchTokenMessage({ 10 | expected, 11 | actual, 12 | previous, 13 | ruleName, 14 | }: { 15 | expected: TokenType; 16 | actual: IToken; 17 | previous: IToken; 18 | ruleName: string; 19 | }): string { 20 | return `${previous.startLine}:${previous.startColumn}(${previous.startOffset}) ${ruleName}: After ${previous.tokenType.name}, expected ${expected.name} but found ${actual.tokenType.name}`; 21 | } 22 | 23 | buildNotAllInputParsedMessage(options: { 24 | firstRedundant: IToken; 25 | ruleName: string; 26 | }): string { 27 | return `${options.firstRedundant.startLine}:${options.firstRedundant.startColumn}(${options.firstRedundant.startOffset}) ${options.ruleName}: expecting EOF but found ${options.firstRedundant.tokenType.name}`; 28 | } 29 | 30 | buildNoViableAltMessage(options: { 31 | expectedPathsPerAlt: TokenType[][][]; 32 | actual: IToken[]; 33 | previous: IToken; 34 | customUserDescription: string; 35 | ruleName: string; 36 | }): string { 37 | return `${options.previous.startLine}:${options.previous.startColumn}(${ 38 | options.previous.startOffset 39 | }) ${ 40 | options.ruleName 41 | }: expecting one of these possible token sequences: ${options.expectedPathsPerAlt 42 | .map((alt) => 43 | alt.map((path) => path.map((a) => a.name).join()).join(" -> ") 44 | ) 45 | .join(" | ")} `; 46 | } 47 | 48 | buildEarlyExitMessage(options: { 49 | expectedIterationPaths: TokenType[][]; 50 | actual: IToken[]; 51 | previous: IToken; 52 | customUserDescription: string; 53 | ruleName: string; 54 | }): string { 55 | return `${options.previous.startLine}:${options.previous.startColumn}(${ 56 | options.previous.startOffset 57 | }) ${defaultParserErrorProvider.buildEarlyExitMessage(options)}`; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/bluehawk/parser/LanguageSpecification.ts: -------------------------------------------------------------------------------- 1 | export interface LanguageSpecification { 2 | // The ID of the specified language. 3 | languageId: string; 4 | 5 | // The single line comment patterns. 6 | lineComments?: RegExp[]; 7 | 8 | // The start and end patterns for block comments. 9 | blockComments?: [RegExp, RegExp][]; 10 | 11 | // String literals as a pattern. This prevents accidentally catching other 12 | // tokens that actually appear within a string. 13 | stringLiterals?: { 14 | pattern: RegExp; 15 | // Set to true if the pattern can match across multiple lines. 16 | multiline: boolean; 17 | }[]; 18 | 19 | // Patterns for pushing a different language parser onto the stack. For 20 | // example, a PHP environment typically starts off as an HTML parser and only 21 | // parses PHP within tags. 22 | parserPushers?: { 23 | // The language ID of the parser to push. 24 | languageId: string; 25 | // The push and pop patterns 26 | patterns: [RegExp, RegExp]; 27 | // If the pushed parser should receive the push and pop tokens themselves, 28 | // set these to true. Undefined is considered false. 29 | startNewParserOnPushToken?: boolean; 30 | endNewParserAfterPopToken?: boolean; 31 | }[]; 32 | } 33 | -------------------------------------------------------------------------------- /src/bluehawk/parser/ParseResult.ts: -------------------------------------------------------------------------------- 1 | import { AnyTagNode } from "./TagNode"; 2 | import { Document } from "../Document"; 3 | import { BluehawkError } from "../BluehawkError"; 4 | import { LanguageSpecification } from "./LanguageSpecification"; 5 | 6 | export interface ParseResult { 7 | errors: BluehawkError[]; 8 | tagNodes: AnyTagNode[]; 9 | source: Document; 10 | languageSpecification?: LanguageSpecification; 11 | } 12 | -------------------------------------------------------------------------------- /src/bluehawk/parser/ParserStore.test.ts: -------------------------------------------------------------------------------- 1 | import { LanguageSpecification } from "."; 2 | import { ParserStore } from "./ParserStore"; 3 | 4 | describe("ParserStore", () => { 5 | it("throws on unknown extension", () => { 6 | const parserStore = new ParserStore(); 7 | expect(() => { 8 | parserStore.getParser({ extension: "unknown" }); 9 | }).toThrowError( 10 | "no parser available for extension 'unknown'. Did you add it with addLanguage()?" 11 | ); 12 | }); 13 | 14 | it("reuses parsers", () => { 15 | const languageSpec1: LanguageSpecification = { 16 | languageId: "test", 17 | }; 18 | const languageSpec2: LanguageSpecification = { 19 | languageId: "test2", 20 | }; 21 | const parserStore = new ParserStore(); 22 | const parser1a = parserStore.getParser(languageSpec1); 23 | expect(parser1a).toBeDefined(); 24 | const parser2 = parserStore.getParser(languageSpec2); 25 | expect(parser2).toBeDefined(); 26 | expect(parser1a).not.toStrictEqual(parser2); 27 | const parser1b = parserStore.getParser(languageSpec1); 28 | expect(parser1a).toStrictEqual(parser1b); 29 | }); 30 | 31 | it("allows adding single extensions", () => { 32 | const parserStore = new ParserStore(); 33 | parserStore.addLanguage("a", { 34 | languageId: "test", 35 | }); 36 | expect(() => { 37 | // Still throw despite one added 38 | parserStore.getParser({ extension: "unknown" }); 39 | }).toThrow(); 40 | const p = parserStore.getParser({ extension: "a" }); 41 | expect(p).toBeDefined(); 42 | }); 43 | 44 | it("allows adding extensions", () => { 45 | const parserStore = new ParserStore(); 46 | parserStore.addLanguage(["a", ".b", ""], { 47 | languageId: "test", 48 | }); 49 | expect(() => { 50 | // Still throw despite some added 51 | parserStore.getParser({ extension: "unknown" }); 52 | }).toThrow(); 53 | 54 | // Ignores first dots in extensions 55 | const pa = parserStore.getParser({ extension: "a" }); 56 | expect(pa).toBeDefined(); 57 | const pdota = parserStore.getParser({ extension: ".a" }); 58 | expect(pdota).toBeDefined(); 59 | const p = parserStore.getParser({ extension: "" }); 60 | expect(p).toBeDefined(); 61 | const pdot = parserStore.getParser({ extension: "." }); 62 | expect(pdot).toBeDefined(); 63 | const pb = parserStore.getParser({ extension: "b" }); 64 | expect(pb).toBeDefined(); 65 | const pdotb = parserStore.getParser({ extension: ".b" }); 66 | expect(pdotb).toBeDefined(); 67 | expect(pb).toStrictEqual(pa); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/bluehawk/parser/ParserStore.ts: -------------------------------------------------------------------------------- 1 | import { LanguageSpecification, IParser, makeParser } from "./"; 2 | 3 | // Maintains a map of file extensions corresponding to language specifications 4 | // that the store can use to provide language-specific parsers. 5 | export class ParserStore { 6 | // Specify the special patterns for a given language. 7 | addLanguage = ( 8 | forFileExtension: string | string[], 9 | languageSpecification: LanguageSpecification 10 | ): void => { 11 | const extensions = !Array.isArray(forFileExtension) 12 | ? [forFileExtension] 13 | : forFileExtension; 14 | extensions 15 | // Accept extensions with our without leading '.' but cut off the '.' 16 | .map(normalize) 17 | .forEach((extension) => { 18 | if (this._languageSpecs.has(extension)) { 19 | console.warn( 20 | `overriding language specification for extension '${extension}'` 21 | ); 22 | } 23 | this._languageSpecs.set(extension, languageSpecification); 24 | }); 25 | }; 26 | 27 | // Returns the parser for the given language specification or the language 28 | // specification corresponding to the given extension that was added with 29 | // addLanguage(). Throws if no language specification can be found. Creates a 30 | // parser if needed. 31 | getParser = ( 32 | documentOrLanguageSpecification: 33 | | { extension: string } 34 | | LanguageSpecification 35 | ): IParser => { 36 | const { extension } = documentOrLanguageSpecification as { 37 | extension?: string; 38 | }; 39 | const normalizedExtension = extension && normalize(extension); 40 | const spec = 41 | normalizedExtension === undefined 42 | ? (documentOrLanguageSpecification as LanguageSpecification) 43 | : this._languageSpecs.get(normalizedExtension); 44 | 45 | if (spec === undefined) { 46 | throw new Error( 47 | `no parser available for extension '${normalizedExtension}'. Did you add it with addLanguage()?` 48 | ); 49 | } 50 | 51 | const { languageId } = spec; 52 | 53 | if (!this._parsers.has(languageId)) { 54 | // Lazy-load the parser 55 | const parser = makeParser(spec); 56 | this._parsers.set(languageId, parser); 57 | } 58 | 59 | const parser = this._parsers.get(languageId); 60 | if (parser === undefined) { 61 | throw new Error(`could not load parser for language ID '${languageId}'`); 62 | } 63 | return parser; 64 | }; 65 | 66 | getDefaultParser = (): IParser => { 67 | return this.getParser({ 68 | languageId: "__default__", 69 | }); 70 | }; 71 | 72 | private _languageSpecs = new Map< 73 | string /* file extension */, 74 | LanguageSpecification 75 | >(); 76 | private _parsers = new Map(); 77 | } 78 | 79 | // Remove a leading dot from the extension and make it lowercase for case 80 | // insensitivity 81 | function normalize(extension: string): string { 82 | return ( 83 | /^\./.test(extension) ? extension.substring(1) : extension 84 | ).toLowerCase(); 85 | } 86 | -------------------------------------------------------------------------------- /src/bluehawk/parser/extractTagNamesFromTokens.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "assert"; 2 | import { IToken } from "chevrotain"; 3 | import { TAG_START_PATTERN, TAG_END_PATTERN } from "./lexer/tokens"; 4 | 5 | export function extractTagNamesFromTokens( 6 | startToken: IToken, 7 | endToken: IToken 8 | ): [string, string] { 9 | const startPatternResult = TAG_START_PATTERN.exec(startToken.image); 10 | assert(startPatternResult !== null); 11 | assert(startPatternResult.length > 1); 12 | const tagName = startPatternResult[1]; 13 | const endPatternResult = TAG_END_PATTERN.exec(endToken.image); 14 | assert(endPatternResult !== null); 15 | assert(endPatternResult.length > 1); 16 | const endTagName = endPatternResult[1]; 17 | return [tagName, endTagName]; 18 | } 19 | -------------------------------------------------------------------------------- /src/bluehawk/parser/flatten.test.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from "./flatten"; 2 | 3 | describe("flatten", () => { 4 | interface Node { 5 | value: string; 6 | children: Node[]; 7 | } 8 | it("flattens a hierarchical structure", () => { 9 | const root: Node = { 10 | value: "a", 11 | children: [ 12 | { 13 | value: "b", 14 | children: [ 15 | { 16 | value: "c", 17 | children: [], 18 | }, 19 | ], 20 | }, 21 | { 22 | value: "d", 23 | children: [], 24 | }, 25 | ], 26 | }; 27 | const flattened = flatten(root); 28 | expect(flattened.map((node) => node.value)).toStrictEqual([ 29 | "a", 30 | "b", 31 | "c", 32 | "d", 33 | ]); 34 | }); 35 | 36 | it("handles empty structures", () => { 37 | const root: Node = { 38 | value: "a", 39 | children: [], 40 | }; 41 | const flattened = flatten(root); 42 | expect(flattened.map((node) => node.value)).toStrictEqual(["a"]); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/bluehawk/parser/flatten.ts: -------------------------------------------------------------------------------- 1 | // Flattens a hierarchical structure 2 | export function flatten(node: T): T[] { 3 | return (node.children ?? []).reduce( 4 | (acc, cur) => [...acc, ...flatten(cur)], 5 | [node] 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/bluehawk/parser/index.ts: -------------------------------------------------------------------------------- 1 | export { ParseResult } from "./ParseResult"; 2 | export { LineTagNode, BlockTagNode, AnyTagNode } from "./TagNode"; 3 | export * from "./flatten"; 4 | export * from "./lexer"; 5 | export { LanguageSpecification } from "./LanguageSpecification"; 6 | export { RootParser } from "./RootParser"; 7 | export * from "./visitor"; 8 | export * from "./makeParser"; 9 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./makeBlockCommentTokens"; 2 | export * from "./makeLineCommentToken"; 3 | export * from "./makePayloadPattern"; 4 | export * from "./makePushParserTokens"; 5 | export * as tokens from "./tokens"; 6 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makeAttributeListMode.ts: -------------------------------------------------------------------------------- 1 | import { TokenType } from "chevrotain"; 2 | import { 3 | AttributeListEnd, 4 | AttributeListStart, 5 | BlockCommentEnd, 6 | BlockCommentStart, 7 | JsonStringLiteral, 8 | LineComment, 9 | Newline, 10 | Space, 11 | Text, 12 | } from "./tokens"; 13 | import { tokenCategoryFilter } from "./tokenCategoryFilter"; 14 | 15 | /* 16 | Bluehawk uses AttributeListMode to parse the attribute list of a tag: 17 | 18 | :snippet-start: { 19 | "id": "some-id", 20 | "attribute1": 1 21 | } 22 | 23 | */ 24 | // Attribute lists are basically JSON objects that allow comments. 25 | export function makeAttributeListMode( 26 | languageTokens: TokenType[] 27 | ): TokenType[] { 28 | const commentTokens = tokenCategoryFilter(languageTokens, [ 29 | BlockCommentStart, 30 | BlockCommentEnd, 31 | LineComment, 32 | ]); 33 | return [ 34 | AttributeListStart, 35 | AttributeListEnd, 36 | JsonStringLiteral, 37 | ...commentTokens, // Attribute list JSON must be able to exist in commented lines 38 | Newline, 39 | Space, 40 | Text, 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makeBlockCommentTokens.ts: -------------------------------------------------------------------------------- 1 | import { createToken, TokenType } from "chevrotain"; 2 | import { makePayloadPattern } from "./makePayloadPattern"; 3 | import { BlockCommentStart, BlockCommentEnd } from "./tokens"; 4 | 5 | export interface BlockCommentTokenConfiguration { 6 | canNest: boolean; 7 | } 8 | 9 | export function makeBlockCommentTokens( 10 | startPattern: RegExp, 11 | endPattern: RegExp, 12 | configuration?: BlockCommentTokenConfiguration 13 | ): [TokenType, TokenType] { 14 | const tokens: [TokenType, TokenType] = [ 15 | createToken({ 16 | name: "BlockCommentStart", 17 | label: `BlockCommentStart(${startPattern.source})`, 18 | categories: [BlockCommentStart], 19 | pattern: makePayloadPattern(startPattern, () => ({ 20 | ...(configuration ?? {}), 21 | endToken: tokens[1], 22 | })), 23 | line_breaks: false, 24 | }), 25 | createToken({ 26 | name: "BlockCommentEnd", 27 | label: `BlockCommentEnd(${endPattern.source})`, 28 | categories: [BlockCommentEnd], 29 | pattern: endPattern, 30 | line_breaks: false, 31 | }), 32 | ]; 33 | return tokens; 34 | } 35 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makeLexer.ts: -------------------------------------------------------------------------------- 1 | import { Lexer, TokenType } from "chevrotain"; 2 | import { makeAttributeListMode } from "./makeAttributeListMode"; 3 | import { makeRootMode } from "./makeRootMode"; 4 | import { 5 | PopParser, 6 | Newline, 7 | Space, 8 | Text, 9 | AttributeListStart, 10 | Identifier, 11 | BlockCommentEnd, 12 | } from "./tokens"; 13 | import { tokenCategoryFilter } from "./tokenCategoryFilter"; 14 | 15 | // Generates a Lexer for the given language comment patterns. 16 | export function makeLexer(languageTokens: TokenType[]): Lexer { 17 | const modes = { 18 | RootMode: makeRootMode(languageTokens), 19 | // After a tag start tag, there may be an attributes list or ID until the 20 | // end of line or a block comment end. 21 | TagAttributesMode: [ 22 | AttributeListStart, 23 | Identifier, 24 | Space, 25 | ...TagAttributesPopTokens(languageTokens), 26 | ], 27 | AttributeListMode: makeAttributeListMode(languageTokens), 28 | AltParserMode: [ 29 | ...tokenCategoryFilter(languageTokens, [PopParser]), 30 | Newline, 31 | Space, 32 | Text, 33 | ], 34 | }; 35 | return new Lexer({ 36 | modes, 37 | defaultMode: "RootMode", 38 | }); 39 | } 40 | 41 | // Helper function for generating tag attribute pop tokens 42 | function TagAttributesPopTokens(languageTokens: TokenType[]): TokenType[] { 43 | const popTokens: TokenType[] = [{ ...Newline, POP_MODE: true }]; 44 | // if block comments are defined, add them as pop tokens 45 | if (tokenCategoryFilter(languageTokens, [BlockCommentEnd]).length !== 0) { 46 | popTokens.push({ 47 | ...tokenCategoryFilter(languageTokens, [BlockCommentEnd])[0], 48 | POP_MODE: true, 49 | }); 50 | } 51 | return popTokens; 52 | } 53 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makeLineCommentToken.ts: -------------------------------------------------------------------------------- 1 | import { createToken, TokenType } from "chevrotain"; 2 | import { LineComment } from "./tokens"; 3 | 4 | export function makeLineCommentToken(pattern: RegExp): TokenType { 5 | return createToken({ 6 | name: "LineComment", 7 | label: `LineComment(${pattern.source})`, 8 | pattern, 9 | categories: [LineComment], 10 | line_breaks: false, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makePayloadPattern.ts: -------------------------------------------------------------------------------- 1 | import { ICustomPattern, IToken } from "chevrotain"; 2 | import { strict as assert } from "assert"; 3 | 4 | // You can use custom payloads to store the full text and other information in 5 | // the token. This allows a CST visitor to operate on the full document. See 6 | // http://sap.github.io/chevrotain/docs/guide/custom_token_patterns.html#custom-payloads 7 | export interface PayloadQuery { 8 | text: string; 9 | offset: number; 10 | tokens: IToken[]; 11 | groups: { 12 | [groupName: string]: IToken[]; 13 | }; 14 | result: RegExpExecArray; 15 | } 16 | 17 | // Creates a CustomPattern with the given RegExp and getPayload callback 18 | // function, which is called whenever the token is encountered by the lexer. 19 | // It should return the payload for the token in the given circumstance. 20 | export function makePayloadPattern( 21 | pattern: RegExp, 22 | getPayload: (query: PayloadQuery) => PayloadType 23 | ): ICustomPattern { 24 | assert( 25 | pattern.sticky, 26 | "Custom pattern MUST be sticky (e.g. `y` in `/example/y`)" 27 | ); 28 | return { 29 | exec: ( 30 | text: string, 31 | offset: number, 32 | tokens: IToken[], 33 | groups: { [groupName: string]: IToken[] } 34 | ): RegExpExecArray | null => { 35 | // start the search at the offset 36 | pattern.lastIndex = offset; 37 | 38 | const result = pattern.exec(text); 39 | if (result) { 40 | // Store the payload options in the regex result as the payload. 41 | // Chevrotain will pick it up. 42 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 43 | (result as any).payload = getPayload({ 44 | text, 45 | offset, 46 | tokens, 47 | groups, 48 | result, 49 | }); 50 | } 51 | return result; 52 | }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makePushParserTokens.ts: -------------------------------------------------------------------------------- 1 | import { createToken, TokenType } from "chevrotain"; 2 | import { PayloadQuery, makePayloadPattern } from "./makePayloadPattern"; 3 | import { PopParser, PushParser } from "./tokens"; 4 | 5 | export interface PushParserTokenConfiguration { 6 | parserId: string; 7 | includePushTokenInSubstring: boolean; 8 | includePopTokenInSubstring: boolean; 9 | } 10 | 11 | export interface PushParserPayload { 12 | fullText: string; 13 | parserId: string; 14 | includePushTokenInSubstring: boolean; 15 | includePopTokenInSubstring: boolean; 16 | endToken: TokenType; 17 | } 18 | 19 | export function makePushParserTokens( 20 | pushPattern: RegExp, 21 | popPattern: RegExp, 22 | configuration: PushParserTokenConfiguration 23 | ): [TokenType, TokenType] { 24 | const tokens: [TokenType, TokenType] = [ 25 | createToken({ 26 | name: "PushParser", 27 | label: `PushParser(${pushPattern})`, 28 | categories: [PushParser], 29 | pattern: makePayloadPattern( 30 | pushPattern, 31 | ({ text }: PayloadQuery): PushParserPayload => ({ 32 | ...configuration, 33 | fullText: text, 34 | endToken: tokens[1], 35 | }) 36 | ), 37 | push_mode: "AltParserMode", 38 | line_breaks: false, 39 | }), 40 | createToken({ 41 | name: "PopParser", 42 | label: `PopParser(${popPattern})`, 43 | categories: [PopParser], 44 | pattern: popPattern, 45 | pop_mode: true, 46 | line_breaks: false, 47 | }), 48 | ]; 49 | return tokens; 50 | } 51 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makeRootMode.ts: -------------------------------------------------------------------------------- 1 | import { TokenType } from "chevrotain"; 2 | import { tokenCategoryFilter } from "./tokenCategoryFilter"; 3 | import { 4 | BlockCommentEnd, 5 | BlockCommentStart, 6 | Tag, 7 | TagEnd, 8 | TagStart, 9 | LineComment, 10 | Newline, 11 | PopParser, 12 | PushParser, 13 | Space, 14 | StringLiteral, 15 | Text, 16 | } from "./tokens"; 17 | 18 | // RootMode is the default parser and lexer mode for a given language. 19 | export function makeRootMode(languageTokens: TokenType[]): Array { 20 | return [ 21 | TagStart, 22 | TagEnd, 23 | Tag, 24 | ...tokenCategoryFilter(languageTokens, [ 25 | BlockCommentStart, 26 | BlockCommentEnd, 27 | LineComment, 28 | PushParser, 29 | StringLiteral, 30 | ]), 31 | Space, 32 | Newline, 33 | Text, 34 | 35 | // "Abstract" tokens 36 | PushParser, 37 | PopParser, 38 | LineComment, 39 | BlockCommentStart, 40 | BlockCommentEnd, 41 | StringLiteral, 42 | ]; 43 | } 44 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/makeStringLiteralToken.ts: -------------------------------------------------------------------------------- 1 | import { createToken, TokenType } from "chevrotain"; 2 | import { StringLiteral } from "./tokens"; 3 | 4 | export function makeStringLiteralToken( 5 | pattern: RegExp, 6 | isMultiline: boolean 7 | ): TokenType { 8 | return createToken({ 9 | name: "StringLiteral", 10 | label: `StringLiteral(${pattern.source})`, 11 | pattern, 12 | categories: [StringLiteral], 13 | line_breaks: isMultiline, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/tokenCategoryFilter.ts: -------------------------------------------------------------------------------- 1 | import { TokenType } from "chevrotain"; 2 | 3 | // Returns the subset of given tokens that are in at least one of the given 4 | // categories. 5 | export function tokenCategoryFilter( 6 | tokens: TokenType[], 7 | categories: TokenType[] 8 | ): TokenType[] { 9 | return tokens.filter( 10 | (token) => 11 | token.CATEGORIES?.find((category) => categories.includes(category)) !== 12 | undefined 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/bluehawk/parser/lexer/tokens.ts: -------------------------------------------------------------------------------- 1 | import { createToken, Lexer } from "chevrotain"; 2 | import { PayloadQuery, makePayloadPattern } from "./makePayloadPattern"; 3 | 4 | /* 5 | It is idiomatic in Python to use a multi-line string literal 6 | (triple quoted-string) for both string assignment 7 | and as a multi-line comment block. Bluehawk treats multi-line 8 | string literals in Python as strings rather than block comments. 9 | */ 10 | export const PYTHON_STRING_LITERAL_PATTERN = 11 | /((? ({ 56 | fullText: text, 57 | })), 58 | push_mode: "AttributeListMode", 59 | line_breaks: false, 60 | }); 61 | 62 | const Space = createToken({ 63 | name: "Spaces", 64 | pattern: /[^\S\r\n]/, 65 | group: Lexer.SKIPPED, 66 | }); 67 | 68 | const Newline = createToken({ 69 | name: "Newline", 70 | pattern: /(\r\n|\r|\n)/, 71 | line_breaks: true, 72 | }); 73 | 74 | const Text = createToken({ 75 | name: "Text", 76 | pattern: /\S/, 77 | group: Lexer.SKIPPED, 78 | }); 79 | 80 | // Shared patterns with captures for use in CstVisitor. Please ensure they stay 81 | // aligned (both in the editor for at-a-glance error checking and as regexes). 82 | // TODO: Allow any amount of non-newline white space (/[^\S\r\n]*/) to be 83 | // included before or after the actual tag name to make stripping it out 84 | // much easier. 85 | const TAG_START_PATTERN /**/ = /(? { 11 | const validate = ajv.compile(schema); 12 | return ( 13 | { attributes, range, tagName }: AnyTagNode, 14 | result: ValidateCstResult 15 | ) => { 16 | if (validate(attributes)) { 17 | return; 18 | } 19 | if (validate.errors?.length === 1 && attributes === undefined) { 20 | // Provide an exception for attributes being 'undefined' instead of null 21 | const { instancePath, params, message } = validate.errors[0]; 22 | if ( 23 | instancePath === "" && 24 | params.type === "null" && 25 | message === "must be null" 26 | ) { 27 | return; 28 | } 29 | } 30 | result.errors.push({ 31 | component: "validator", 32 | location: range.start, 33 | message: ajv.errorsText(validate.errors, { 34 | separator: "\n", 35 | dataVar: `attribute list for '${tagName}' tag`, 36 | }), 37 | }); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/bluehawk/parser/makeParser.ts: -------------------------------------------------------------------------------- 1 | import { TagProcessors } from "./../processor/Processor"; 2 | import { Document } from "../Document"; 3 | import { LanguageSpecification } from "./LanguageSpecification"; 4 | import { 5 | makeBlockCommentTokens, 6 | makeLineCommentToken, 7 | makePushParserTokens, 8 | } from "./lexer"; 9 | import { makeStringLiteralToken } from "./lexer/makeStringLiteralToken"; 10 | import { ParseResult } from "./ParseResult"; 11 | import { RootParser } from "./RootParser"; 12 | import { makeCstVisitor } from "./visitor"; 13 | import { validateTags } from "./validator"; 14 | 15 | // Complete parser (lexicon, syntax, and semantics). 16 | export interface IParser { 17 | languageSpecification: LanguageSpecification; 18 | parse(args: { source: Document; tagProcessors?: TagProcessors }): ParseResult; 19 | } 20 | 21 | // Token patterns are required to be sticky (y flag) 22 | const makeSticky = (re: RegExp): RegExp => { 23 | if (re.sticky) { 24 | return re; 25 | } 26 | return new RegExp(re, re.flags + "y"); 27 | }; 28 | 29 | export function makeParser( 30 | languageSpecification: LanguageSpecification 31 | ): IParser { 32 | const { lineComments, blockComments, stringLiterals, parserPushers } = 33 | languageSpecification; 34 | const languageTokens = [ 35 | ...(lineComments ?? []).map(makeSticky).map(makeLineCommentToken), 36 | ...(blockComments ?? []) 37 | .map(([startPattern, endPattern]) => 38 | makeBlockCommentTokens(makeSticky(startPattern), makeSticky(endPattern)) 39 | ) 40 | .flat(), 41 | ...(stringLiterals ?? []).map(({ pattern, multiline }) => 42 | makeStringLiteralToken(makeSticky(pattern), multiline) 43 | ), 44 | ...(parserPushers ?? []) 45 | .map( 46 | ({ 47 | languageId, 48 | patterns, 49 | startNewParserOnPushToken, 50 | endNewParserAfterPopToken, 51 | }) => 52 | makePushParserTokens(patterns[0], patterns[1], { 53 | parserId: languageId, 54 | includePushTokenInSubstring: startNewParserOnPushToken ?? false, 55 | includePopTokenInSubstring: endNewParserAfterPopToken ?? false, 56 | }) 57 | ) 58 | .flat(), 59 | ]; 60 | const syntaxProcessor = new RootParser(languageTokens); 61 | const semanticsProcessor = makeCstVisitor(syntaxProcessor); 62 | return { 63 | parse({ source, tagProcessors }) { 64 | // ⚠️ Caller is responsible for making sure this parser is appropriate for 65 | // the source language 66 | const parseResult = syntaxProcessor.parse(source.text.original); 67 | if (parseResult.cst === undefined) { 68 | return { 69 | tagNodes: [], 70 | errors: parseResult.errors, 71 | source, 72 | languageSpecification, 73 | }; 74 | } 75 | const visitorResult = semanticsProcessor.visit(parseResult.cst, source); 76 | 77 | const validateErrors = 78 | tagProcessors !== undefined 79 | ? validateTags(visitorResult.tagNodes, tagProcessors) 80 | : []; 81 | return { 82 | errors: [ 83 | ...parseResult.errors, 84 | ...visitorResult.errors, 85 | ...validateErrors, 86 | ], 87 | tagNodes: visitorResult.tagNodes, 88 | source, 89 | languageSpecification, 90 | }; 91 | }, 92 | languageSpecification, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/bluehawk/parser/validator.ts: -------------------------------------------------------------------------------- 1 | import { TagProcessors } from "./../processor/Processor"; 2 | import { AnyTagNode } from "../parser"; 3 | import { BluehawkError } from "../BluehawkError"; 4 | import { flatten } from "../parser"; 5 | import { makeAttributesConformToJsonSchemaRule } from "./makeAttributesConformToJsonSchemaRule"; 6 | import { mapShorthandArgsToAttributes } from "../tags/Tag"; 7 | 8 | export interface ValidateCstResult { 9 | errors: BluehawkError[]; 10 | tagsById: Map; 11 | } 12 | 13 | // A rule function is registered for a given tag name and checks that the 14 | // given tag node conforms to the specification of that tag. Errors can 15 | // be reported into the result.errors array. 16 | export type Rule = (tagNode: AnyTagNode, result: ValidateCstResult) => void; 17 | 18 | export function validateTags( 19 | tagNodes: AnyTagNode[], 20 | tagProcessorMap: TagProcessors 21 | ): BluehawkError[] { 22 | const validateResult: ValidateCstResult = { 23 | errors: [], 24 | tagsById: new Map(), 25 | }; 26 | flatten({ 27 | children: tagNodes, 28 | } as AnyTagNode).forEach((tagNode) => { 29 | const tag = tagProcessorMap[tagNode.tagName]; 30 | if (tag === undefined) { 31 | // TODO: warn unknown tag 32 | return; 33 | } 34 | 35 | const { shorthandArgsAttributeName } = tag; 36 | if (shorthandArgsAttributeName !== undefined) { 37 | // Apply the tag's shorthand rename rule to the tag node's attribute list 38 | mapShorthandArgsToAttributes({ 39 | tagNode, 40 | shorthandArgsAttributeName, 41 | }); 42 | } else if (tagNode.shorthandArgs !== undefined) { 43 | validateResult.errors.push({ 44 | component: "validator", 45 | location: tagNode.range.start, 46 | message: `'${ 47 | tagNode.tagName 48 | }' does not accept shorthand args ('${tagNode.shorthandArgs.join( 49 | " " 50 | )}') because the tag definition does not have shorthandArgsAttributeName set.`, 51 | }); 52 | return; 53 | } 54 | 55 | if (tagNode.type === "block" && !tag.supportsBlockMode) { 56 | validateResult.errors.push({ 57 | component: "validator", 58 | location: tagNode.range.start, 59 | message: `'${tagNode.tagName}' cannot be used in block mode (i.e. with -start and -end)`, 60 | }); 61 | return; 62 | } 63 | 64 | if (tagNode.type === "line" && !tag.supportsLineMode) { 65 | validateResult.errors.push({ 66 | component: "validator", 67 | location: tagNode.range.start, 68 | message: `'${tagNode.tagName}' cannot be used in single line mode (i.e. without -start and -end around a block)`, 69 | }); 70 | return; 71 | } 72 | 73 | if (tag.attributesSchema !== undefined) { 74 | const attributeSchemaValidator = makeAttributesConformToJsonSchemaRule( 75 | tag.attributesSchema 76 | ); 77 | attributeSchemaValidator(tagNode, validateResult); 78 | } 79 | 80 | tag.rules?.forEach((rule) => { 81 | rule(tagNode, validateResult); 82 | }); 83 | }); 84 | return validateResult.errors; 85 | } 86 | 87 | // standard rule implementations 88 | 89 | export const idIsUnique: Rule = ( 90 | tagNode: AnyTagNode, 91 | result: ValidateCstResult 92 | ) => { 93 | if (tagNode.attributes?.id !== undefined) { 94 | // if the tag id already exists in the set of tag ids, create a duplicate error 95 | const union = [ 96 | ...new Set([ 97 | ...Array.from(result.tagsById.keys()), 98 | ...tagNode.attributes.id, 99 | ]), 100 | ]; 101 | // if the union of the seen ids and the current node's ids has fewer elements than the length of those in total... duplicate 102 | if (union.length != result.tagsById.size + tagNode.attributes.id.length) { 103 | // (result.tagsById.has(tagNode.attributes.id)) { 104 | result.errors.push({ 105 | component: "validator", 106 | location: tagNode.range.start, 107 | message: `duplicate ID '${tagNode.attributes.id}' found`, 108 | }); 109 | } else { 110 | // otherwise, add the tag id to the set 111 | tagNode.attributes.id.forEach((id: string) => { 112 | result.tagsById.set(id, tagNode); 113 | }); 114 | } 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /src/bluehawk/parser/visitor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./makeCstVisitor"; 2 | -------------------------------------------------------------------------------- /src/bluehawk/parser/visitor/innerOffsetToOuterLocation.ts: -------------------------------------------------------------------------------- 1 | import { Location } from "../../Location"; 2 | 3 | // Convenience function to transform an offset within an inner string 4 | // (substring) that appears in the outer text at the given location. 5 | export function innerOffsetToOuterLocation( 6 | innerOffset: number, 7 | innerText: string, 8 | innerTextOuterLocation: Location 9 | ): Location { 10 | const location = innerTextOuterLocation; 11 | // Offset is easy to calculate... 12 | location.offset += innerOffset; 13 | 14 | // ...but line and column get trickier. Calculate actual position of 15 | // the error in the overall document by looking at line lengths 16 | const re = /(\r\n|\r|\n)/g; // account for weird newlines 17 | 18 | // Scan through the JSON string from line to line and adjust the error 19 | // location according to the offset 20 | for ( 21 | let result = re.exec(innerText), lastIndex = 0; 22 | result && innerOffset > 0; 23 | result = re.exec(innerText) 24 | ) { 25 | const lineLength = result.index - lastIndex; 26 | lastIndex = result.index; 27 | if (innerOffset >= lineLength) { 28 | // Consume part of the position value and roll over to the next line. 29 | location.column = 1; 30 | ++location.line; 31 | innerOffset -= lineLength; 32 | continue; 33 | } 34 | 35 | // Consume the remainder of the position. 36 | location.column += innerOffset - 1; 37 | } 38 | return location; 39 | } 40 | 41 | export function innerLocationToOuterLocation( 42 | innerLocation: Location, 43 | innerText: string, 44 | innerTextOuterLocation: Location 45 | ): Location { 46 | // TODO: This might be possible to do without iterating line by line over the 47 | // inner text 48 | return innerOffsetToOuterLocation( 49 | innerLocation.offset, 50 | innerText, 51 | innerTextOuterLocation 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/bluehawk/parser/visitor/jsonErrorToVisitorError.ts: -------------------------------------------------------------------------------- 1 | import { innerOffsetToOuterLocation } from "./innerOffsetToOuterLocation"; 2 | import { Location } from "../../Location"; 3 | import { BluehawkError } from "../../BluehawkError"; 4 | 5 | const component = "visitor"; 6 | 7 | // Convert a JSON.parse() error to visitor error. Updates the error location to 8 | // document space and cleans up the message, if possible. 9 | export function jsonErrorToVisitorError( 10 | error: Error, 11 | json: string, 12 | locationInDocument: Location // The location of the start of the json that caused the error 13 | ): BluehawkError { 14 | const location = locationInDocument; 15 | 16 | // Try to convert to a more helpful diagnostic 17 | const match = /^(.*) at position ([0-9]+)$/.exec(error.message); 18 | const message = match 19 | ? match[1] 20 | : error.message ?? "Unknown issue parsing attribute JSON"; 21 | 22 | const position = match && parseInt(match[2], 10); 23 | if (!position) { 24 | return { component, location, message }; 25 | } 26 | 27 | return { 28 | component, 29 | location: innerOffsetToOuterLocation(position, json, locationInDocument), 30 | message: message, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/bluehawk/processor/index.ts: -------------------------------------------------------------------------------- 1 | export { Listener, ProcessRequest, ProcessOptions } from "./Processor"; 2 | -------------------------------------------------------------------------------- /src/bluehawk/project/Project.ts: -------------------------------------------------------------------------------- 1 | // Project represents a directory of files to be processed by bluehawk. 2 | export interface Project { 3 | // The root path into the project. 4 | rootPath: string; 5 | 6 | // An optional collection of patterns of files to be ignored, a la gitignore. 7 | ignore?: string | string[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/bluehawk/project/index.ts: -------------------------------------------------------------------------------- 1 | export { Project } from "./Project"; 2 | export { loadProjectPaths } from "./loadProjectPaths"; 3 | -------------------------------------------------------------------------------- /src/bluehawk/project/loadProjectPaths.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import ignore from "ignore"; 3 | import { Project } from "./Project"; 4 | import { System } from "../io/System"; 5 | 6 | async function traverse( 7 | absolutePath: string, 8 | projectRoot: string, 9 | ignores: string[] 10 | ): Promise { 11 | const ig = ignore(); 12 | ig.add(ignores); 13 | const stats = await System.fs.lstat(absolutePath); 14 | const relativePath = path.relative(projectRoot, absolutePath); 15 | if (stats.isFile()) { 16 | if (absolutePath === projectRoot) { 17 | // special handling -- when run on individual files, bluehawk path already contains file name 18 | return [absolutePath]; 19 | } 20 | if (ig.ignores(relativePath)) { 21 | return []; 22 | } 23 | return [absolutePath]; 24 | } 25 | if (!stats.isDirectory()) { 26 | return []; 27 | } 28 | 29 | try { 30 | const gitignore = await System.fs.readFile( 31 | path.join(absolutePath, ".gitignore"), 32 | "utf8" 33 | ); 34 | const gitignores = gitignore 35 | .split(/\r\n|\r|\n/) 36 | .filter((line) => !(line.startsWith("#") || line.trim().length == 0)); 37 | ignores.push(...gitignores); 38 | ig.add(ignores); 39 | } catch { 40 | // no gitignore -- oh well 41 | } 42 | const files = await System.fs.readdir(absolutePath); 43 | const promises = files.map(async (file): Promise => { 44 | const absoluteSubpath = path.join(absolutePath, file); 45 | const relativeSubpath = path.relative(projectRoot, absoluteSubpath); 46 | if (ig.ignores(relativeSubpath)) { 47 | return []; 48 | } 49 | return await traverse(absoluteSubpath, projectRoot, [...ignores]); 50 | }); 51 | return (await Promise.all(promises)).flat(); 52 | } 53 | 54 | // Given a source entry point path, load all paths to files within, mindful of 55 | // .gitignore. 56 | export async function loadProjectPaths(project: Project): Promise { 57 | const projectRoot = path.resolve(project.rootPath); 58 | const ignores = Array.isArray(project.ignore) 59 | ? project.ignore 60 | : typeof project.ignore === "string" 61 | ? [project.ignore] 62 | : []; 63 | 64 | return traverse(projectRoot, projectRoot, ignores); 65 | } 66 | -------------------------------------------------------------------------------- /src/bluehawk/tags/EmphasizeTag.ts: -------------------------------------------------------------------------------- 1 | import { locationFromToken } from "../parser/locationFromToken"; 2 | import { makeBlockOrLineTag, NoAttributes, NoAttributesSchema } from "./Tag"; 3 | 4 | export interface EmphasizeRange { 5 | start: { 6 | line: number; 7 | column: number; 8 | }; 9 | end: { 10 | line: number; 11 | column: number; 12 | }; 13 | } 14 | 15 | export interface EmphasizeSourceAttributes { 16 | ranges: EmphasizeRange[]; 17 | } 18 | 19 | export const EmphasizeTag = makeBlockOrLineTag({ 20 | name: "emphasize", 21 | description: 22 | "identify line(s) to highlight (see `bluehawk snip --format` tag)", 23 | attributesSchema: NoAttributesSchema, 24 | 25 | process({ tagNode, document }) { 26 | if (document.attributes["emphasize"] === undefined) { 27 | document.attributes["emphasize"] = { ranges: [] }; 28 | } 29 | 30 | let range: EmphasizeRange; 31 | 32 | switch (tagNode.type) { 33 | case "block": { 34 | const lastNewline = tagNode.newlines[tagNode.newlines.length - 1]; 35 | if (lastNewline === undefined) { 36 | throw new Error(`lastNewline unexpectedly undefined!`); 37 | } 38 | range = { 39 | start: tagNode.contentRange.start, 40 | end: locationFromToken(lastNewline), 41 | }; 42 | break; 43 | } 44 | case "line": 45 | range = { 46 | start: tagNode.lineRange.start, 47 | end: tagNode.range.end, 48 | }; 49 | break; 50 | } 51 | 52 | document.attributes["emphasize"]["ranges"].push(range); 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /src/bluehawk/tags/RemoveTag.ts: -------------------------------------------------------------------------------- 1 | import { makeBlockOrLineTag, NoAttributes, NoAttributesSchema } from "./Tag"; 2 | 3 | export const RemoveTag = makeBlockOrLineTag({ 4 | name: "remove", 5 | description: "deletes line(s) from the result", 6 | attributesSchema: NoAttributesSchema, 7 | process({ tagNode, document, stopPropagation }) { 8 | const { lineRange } = tagNode; 9 | document.text.remove(lineRange.start.offset, lineRange.end.offset); 10 | stopPropagation(); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/bluehawk/tags/ReplaceTag.ts: -------------------------------------------------------------------------------- 1 | import { makeBlockTag } from "./Tag"; 2 | 3 | type ReplaceTagAttributes = { 4 | terms: { 5 | // Replace '' with '' 6 | [searchTerm: string]: /* replaceTerm */ string; 7 | }; 8 | }; 9 | 10 | export const ReplaceTag = makeBlockTag({ 11 | name: "replace", 12 | description: 13 | "given 'terms' object in the attribute list, replaces term keys with corresponding values within the block", 14 | attributesSchema: { 15 | type: "object", 16 | required: ["terms"], 17 | properties: { 18 | terms: { 19 | type: "object", 20 | minProperties: 1, 21 | additionalProperties: { type: "string" }, 22 | required: [], 23 | }, 24 | }, 25 | }, 26 | 27 | process({ tagNode, document }) { 28 | const attributes = tagNode.attributes as ReplaceTagAttributes; 29 | const { text } = document; 30 | 31 | const { contentRange } = tagNode; 32 | const contentLength = contentRange.end.offset - contentRange.start.offset; 33 | const { terms } = attributes; 34 | Object.entries(terms).forEach(([searchTerm, replaceTerm]) => { 35 | if (contentLength < searchTerm.length) { 36 | // It will be impossible to find the search term within this block 37 | return; 38 | } 39 | const indexOf = (s: string, term: string, index: number): number => { 40 | return s.indexOf(term, index); 41 | }; 42 | const { original } = text; 43 | for ( 44 | let position = indexOf(original, searchTerm, contentRange.start.offset); 45 | position !== -1 && 46 | position < contentRange.end.offset - searchTerm.length; 47 | position = indexOf(original, searchTerm, position + 1) 48 | ) { 49 | text.overwrite(position, position + searchTerm.length, replaceTerm); 50 | } 51 | }); 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /src/bluehawk/tags/SnippetTag.ts: -------------------------------------------------------------------------------- 1 | import MagicString from "magic-string"; 2 | import { BlockTagNode } from "../parser/TagNode"; 3 | import { ProcessRequest } from "../processor/Processor"; 4 | import { idIsUnique } from "../parser/validator"; 5 | import { strict as assert } from "assert"; 6 | import { 7 | makeBlockTag, 8 | IdRequiredAttributes, 9 | IdRequiredAttributesSchema, 10 | } from "./Tag"; 11 | 12 | function dedentRange( 13 | s: MagicString, 14 | { contentRange }: BlockTagNode 15 | ): MagicString { 16 | if (contentRange === undefined) { 17 | return s; 18 | } 19 | 20 | // Find minimum indentation in the content block 21 | const re = /(.*)(?:\r|\n|\r\n)/y; 22 | let minimumIndentation: number | undefined = undefined; 23 | re.lastIndex = contentRange.start.offset; 24 | for ( 25 | let match = re.exec(s.original); 26 | match && re.lastIndex <= contentRange.end.offset; 27 | match = re.exec(s.original) 28 | ) { 29 | const line = match[1]; 30 | if (line.length === 0) { 31 | continue; 32 | } 33 | const indentMatch = line.match(/^\s+/); 34 | if (!indentMatch) { 35 | // No indentation 36 | minimumIndentation = 0; 37 | break; 38 | } 39 | const indentLength = indentMatch[0].length; 40 | if (minimumIndentation === undefined || indentLength < minimumIndentation) { 41 | minimumIndentation = indentLength; 42 | } 43 | } 44 | 45 | // In this case ambiguity between 0 and null is ok 46 | if (!minimumIndentation) { 47 | return s; 48 | } 49 | 50 | // Dedent lines 51 | re.lastIndex = contentRange.start.offset; 52 | for ( 53 | let match = re.exec(s.original), offset = contentRange.start.offset; 54 | match && re.lastIndex <= contentRange.end.offset; 55 | match = re.exec(s.original), offset 56 | ) { 57 | const line = match[1]; 58 | s.remove(offset, offset + Math.min(line.length, minimumIndentation)); 59 | offset = re.lastIndex; 60 | } 61 | 62 | return s; 63 | } 64 | 65 | export const SnippetTag = makeBlockTag({ 66 | name: "snippet", 67 | description: "identifies snippets for extraction into standalone files", 68 | attributesSchema: IdRequiredAttributesSchema, 69 | shorthandArgsAttributeName: "id", 70 | rules: [idIsUnique], 71 | process: (request: ProcessRequest): void => { 72 | const { tagNode, document, fork } = request; 73 | const { contentRange } = tagNode; 74 | 75 | if (document.attributes["snippet"] !== undefined) { 76 | // Nested snippet. Its output will be the same as unnested. 77 | return; 78 | } 79 | 80 | // Copy text to new working copy 81 | const clonedSnippet = document.text.snip( 82 | contentRange.start.offset, 83 | contentRange.end.offset 84 | ); 85 | 86 | // Dedent 87 | dedentRange(clonedSnippet, tagNode); 88 | 89 | // ID is required and enforced by the validator, so it should exist 90 | assert( 91 | tagNode.attributes.id !== undefined && tagNode.attributes.id.length > 0 92 | ); 93 | 94 | // Fork subset code block to another file 95 | fork({ 96 | document, 97 | tagNodes: tagNode.children ?? [], 98 | newPath: document.pathWithInfix(`snippet.${tagNode.attributes.id[0]}`), 99 | newText: clonedSnippet, 100 | newAttributes: { 101 | snippet: tagNode.attributes.id[0], 102 | }, 103 | }); 104 | }, 105 | }); 106 | -------------------------------------------------------------------------------- /src/bluehawk/tags/StateRemoveTag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeBlockTag, 3 | IdsRequiredAttributesSchema, 4 | IdsRequiredAttributes, 5 | } from "./Tag"; 6 | import { RemoveTag } from "./RemoveTag"; 7 | import { conditionalForkWithState } from "./StateTag"; 8 | 9 | export const StateRemoveTag = makeBlockTag({ 10 | name: "state-remove", 11 | description: 12 | "given a state name(s) as tag ids, identifies blocks that should not appear in the given state's version of the file", 13 | attributesSchema: IdsRequiredAttributesSchema, 14 | shorthandArgsAttributeName: "id", 15 | process(request) { 16 | conditionalForkWithState(request); 17 | const { tagNode, document } = request; 18 | const stateAttribute = document.attributes["state"]; 19 | // Strip all matching states 20 | if (tagNode.attributes.id.includes(stateAttribute)) { 21 | RemoveTag.process(request); 22 | } 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/bluehawk/tags/StateTag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeBlockTag, 3 | IdsRequiredAttributes, 4 | IdsRequiredAttributesSchema, 5 | } from "./Tag"; 6 | import { RemoveTag } from "./RemoveTag"; 7 | import { BlockTagNode } from "../parser"; 8 | import { ProcessRequest } from "../processor/Processor"; 9 | 10 | export const StateTag = makeBlockTag({ 11 | name: "state", 12 | description: 13 | "given a state name(s) as tag ids, identifies blocks that should only appear in the given state's version of the file", 14 | attributesSchema: IdsRequiredAttributesSchema, 15 | shorthandArgsAttributeName: "id", 16 | process(request) { 17 | conditionalForkWithState(request); 18 | const { tagNode, document } = request; 19 | const stateAttribute = document.attributes["state"]; 20 | // Strip all other states 21 | if (!tagNode.attributes.id.includes(stateAttribute)) { 22 | RemoveTag.process(request); 23 | } 24 | }, 25 | }); 26 | 27 | /** 28 | If we are not processing in a state file, fork a file for each 29 | state listed in our tag node. 30 | */ 31 | export const conditionalForkWithState = ( 32 | request: ProcessRequest 33 | ) => { 34 | const { tagNode, fork, document, tagNodes } = request; 35 | const stateAttribute = document.attributes["state"]; 36 | if (stateAttribute === undefined) { 37 | tagNode.attributes.id.forEach((id: string) => { 38 | fork({ 39 | document, 40 | tagNodes, 41 | newModifiers: { 42 | state: id, 43 | }, 44 | newAttributes: { 45 | // Set the state attribute for next time StateTag is invoked on the 46 | // new file 47 | state: id, 48 | }, 49 | }); 50 | }); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/bluehawk/tags/UncommentTag.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "assert"; 2 | import { IToken } from "chevrotain"; 3 | import { AnyTagNode, flatten } from "../parser"; 4 | import { makeBlockTag, NoAttributes, NoAttributesSchema } from "./Tag"; 5 | 6 | export const UncommentTag = makeBlockTag({ 7 | name: "uncomment", 8 | description: 9 | "removes up to one line comment token from every line in its range", 10 | attributesSchema: NoAttributesSchema, 11 | process(request) { 12 | const { tagNode, document } = request; 13 | const { text } = document; 14 | 15 | // Get all line comments in the hierarchy 16 | const lineComments = flatten(tagNode as AnyTagNode) 17 | .reduce((acc, cur) => [...acc, ...cur.lineComments], [] as IToken[]) 18 | .sort((a, b) => a.startOffset - b.startOffset); 19 | 20 | // TODO: it would be possible to warn here of uncommented lines 21 | lineComments 22 | .filter( 23 | // Remove all but the first LineComment on each line 24 | (lineComment, i) => 25 | i === 0 || lineComments[i - 1].startLine !== lineComment.startLine 26 | ) 27 | .filter(({ startColumn, startOffset }) => { 28 | // Do not delete line comments if they are not the first thing in the 29 | // line (after whitespace) 30 | assert(startColumn); 31 | return ( 32 | startColumn === 1 || 33 | text.snip(startOffset - (startColumn - 1), startOffset).isEmpty() 34 | ); 35 | }) 36 | .forEach((lineComment) => { 37 | assert(lineComment.endOffset !== undefined); 38 | // Delete the line comment 39 | text.remove(lineComment.startOffset, lineComment.endOffset + 1); 40 | }); 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /src/bluehawk/tags/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Tag"; 2 | export * from "./RemoveTag"; 3 | export * from "./ReplaceTag"; 4 | export * from "./SnippetTag"; 5 | export * from "./StateTag"; 6 | export * from "./StateRemoveTag"; 7 | export * from "./UncommentTag"; 8 | export * from "./removeMetaRange"; 9 | export * from "./EmphasizeTag"; 10 | -------------------------------------------------------------------------------- /src/bluehawk/tags/removeMetaRange.test.ts: -------------------------------------------------------------------------------- 1 | import { Bluehawk } from "../bluehawk"; 2 | import { Document } from "../Document"; 3 | import { makeBlockOrLineTag, NoAttributes, NoAttributesSchema } from "./Tag"; 4 | import { removeMetaRange } from "./removeMetaRange"; 5 | 6 | describe("removeMetaRange", () => { 7 | const bluehawk = new Bluehawk(); 8 | bluehawk.registerTag( 9 | makeBlockOrLineTag({ 10 | attributesSchema: NoAttributesSchema, 11 | name: "strip-this", 12 | process({ document, tagNode }) { 13 | removeMetaRange(document.text, tagNode); 14 | }, 15 | }) 16 | ); 17 | bluehawk.addLanguage("js", { 18 | languageId: "javascript", 19 | blockComments: [[/\/\*/, /\*\//]], 20 | lineComments: [/\/\/ ?/], 21 | }); 22 | 23 | it("behaves on block tags", async () => { 24 | // Note that it completely deletes the line on which the -end tag is 25 | // found. 26 | const source = new Document({ 27 | text: `const bar = "foo"; 28 | // :strip-this-start: { 29 | // "a": 1, 30 | // "b": [1, 2, 3, 31 | // 4, 5, 6]} 32 | const baz = "bar"; 33 | not this 34 | // :strip-this-end: 35 | :not-this: 36 | const qux = "baz"; 37 | `, 38 | path: "strip.test.js", 39 | }); 40 | 41 | const parseResult = bluehawk.parse(source); 42 | const files = await bluehawk.process(parseResult); 43 | expect(files["strip.test.js"].document.text.toString()).toBe( 44 | `const bar = "foo"; 45 | const baz = "bar"; 46 | not this 47 | :not-this: 48 | const qux = "baz"; 49 | ` 50 | ); 51 | }); 52 | it("behaves on line tags", async () => { 53 | const source = new Document({ 54 | text: `const bar = "foo"; 55 | const baz = "bar"; // :strip-this: 56 | :strip-this: :not-this: 57 | const qux = "baz"; // not this :strip-this: 58 | `, 59 | path: "strip.test.js", 60 | }); 61 | 62 | const parseResult = bluehawk.parse(source); 63 | const files = await bluehawk.process(parseResult); 64 | expect(files["strip.test.js"].document.text.toString()).toBe( 65 | `const bar = "foo"; 66 | const baz = "bar"; 67 | :not-this: 68 | const qux = "baz"; // not this 69 | ` 70 | ); 71 | }); 72 | 73 | it("behaves with doubled-up line tags", async () => { 74 | const source = new Document({ 75 | text: `abc 76 | def // :strip-this: :strip-this: 77 | ghi // :strip-this: // :strip-this: 78 | jkl //// :strip-this: // :strip-this: 79 | `, 80 | path: "strip.test.js", 81 | }); 82 | 83 | const parseResult = bluehawk.parse(source); 84 | const files = await bluehawk.process(parseResult); 85 | expect(files["strip.test.js"].document.text.toString()).toBe( 86 | `abc 87 | def 88 | ghi 89 | jkl // 90 | ` 91 | ); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/bluehawk/tags/removeMetaRange.ts: -------------------------------------------------------------------------------- 1 | import MagicString from "magic-string"; 2 | 3 | import { AnyTagNode, BlockTagNode, LineTagNode } from "../parser"; 4 | 5 | function removeMetaRangeForLineTagNode( 6 | s: MagicString, 7 | tagNode: LineTagNode 8 | ): MagicString { 9 | s.remove(tagNode.range.start.offset, tagNode.range.end.offset + 1); 10 | return s; 11 | } 12 | 13 | function removeMetaRangeForBlockTagNode( 14 | s: MagicString, 15 | tagNode: BlockTagNode 16 | ): MagicString { 17 | const { lineRange, contentRange } = tagNode; 18 | s.remove(lineRange.start.offset, contentRange.start.offset); 19 | s.remove(contentRange.end.offset, lineRange.end.offset); 20 | return s; 21 | } 22 | 23 | // Removes all within the range except the content range. 24 | // Ideal for stripping tag tags and attribute lists. 25 | export function removeMetaRange( 26 | s: MagicString, 27 | tagNode: AnyTagNode 28 | ): MagicString { 29 | tagNode.associatedTokens.forEach((token) => 30 | s.remove(token.startOffset, (token.endOffset ?? token.startOffset) + 1) 31 | ); 32 | 33 | switch (tagNode.type) { 34 | case "line": 35 | return removeMetaRangeForLineTagNode(s, tagNode as LineTagNode); 36 | case "block": 37 | return removeMetaRangeForBlockTagNode(s, tagNode as BlockTagNode); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/bluehawk/tags/removeTag.test.ts: -------------------------------------------------------------------------------- 1 | import { Bluehawk } from "../bluehawk"; 2 | import { Document } from "../Document"; 3 | import { RemoveTag } from "./RemoveTag"; 4 | import MagicString from "magic-string"; 5 | 6 | describe("remove tag", () => { 7 | const bluehawk = new Bluehawk(); 8 | bluehawk.registerTag(RemoveTag); 9 | bluehawk.addLanguage("js", { 10 | languageId: "javascript", 11 | blockComments: [[/\/\*/, /\*\//]], 12 | lineComments: [/\/\/ ?/], 13 | }); 14 | 15 | test("magic string allows double delete", () => { 16 | // Check assumptions about magic string. At time of writing, magic string 17 | // docs claim that "removing the same content twice, or making removals that 18 | // partially overlap, will cause an error." In practice, this doesn't seem 19 | // to be the case. In the interest of keeping processor code simple, 20 | // Bluehawk relies on being able to delete the same thing multiple times. 21 | const s = new MagicString(`line 1 22 | line 2 23 | line 3`); 24 | expect(s.length()).toBe(s.original.length); 25 | const lines = s.original.split("\n"); 26 | s.remove(0, lines[0].length + 1); 27 | expect(s.length()).toBe(s.original.length - lines[0].length - 1); 28 | s.remove(0, lines[0].length + 1); // Delete again -- no error 29 | expect(s.length()).toBe(s.original.length - lines[0].length - 1); 30 | expect(s.toString()).toBe(`line 2 31 | line 3`); 32 | s.remove(3, 6); // Delete inner field again -- no error 33 | expect(s.length()).toBe(s.original.length - lines[0].length - 1); 34 | expect(s.toString()).toBe(`line 2 35 | line 3`); 36 | s.remove(0, lines[0].length + lines[1].length + 2); // Delete overlapping field 37 | expect(s.length()).toBe( 38 | s.original.length - lines[0].length - lines[1].length - 2 39 | ); 40 | expect(s.toString()).toBe(`line 3`); 41 | }); 42 | 43 | it("strips text", async () => { 44 | const singleInput = `const bar = "foo" 45 | 46 | // :remove-start: 47 | describe("some stuff", () => { 48 | it("foos the bar", () => { 49 | expect(true).toBeTruthy(); 50 | }); 51 | }); 52 | // :remove-end: 53 | console.log(bar); 54 | `; 55 | 56 | const source = new Document({ 57 | text: singleInput, 58 | path: "test.js", 59 | }); 60 | 61 | const parseResult = bluehawk.parse(source); 62 | const files = await bluehawk.process(parseResult); 63 | expect(files["test.js"].document.text.toString()).toBe(`const bar = "foo" 64 | 65 | console.log(bar); 66 | `); 67 | }); 68 | 69 | it("nests", async () => { 70 | const source = new Document({ 71 | text: `a 72 | :remove-start: 73 | b 74 | :remove-start: 75 | c 76 | :remove-end: 77 | d 78 | :remove-end: 79 | e 80 | `, 81 | path: "test.js", 82 | }); 83 | 84 | const parseResult = bluehawk.parse(source); 85 | const files = await bluehawk.process(parseResult); 86 | expect(files["test.js"].document.text.toString()).toBe( 87 | `a 88 | e 89 | ` 90 | ); 91 | }); 92 | 93 | it("requires no attributes", () => { 94 | const input = `// :remove-start: { 95 | "id": "hey" 96 | } 97 | // :remove-end: 98 | `; 99 | 100 | const source = new Document({ 101 | text: input, 102 | path: "test.js", 103 | }); 104 | 105 | const parseResult = bluehawk.parse(source); 106 | expect(parseResult.errors[0].message).toStrictEqual( 107 | "attribute list for 'remove' tag must be null" 108 | ); 109 | }); 110 | 111 | it("works as a line tag", async () => { 112 | const input = `leave this 113 | this should be removed // :remove: 114 | and leave this 115 | this should also be removed // :remove: // do it 116 | but not this 117 | `; 118 | 119 | const source = new Document({ 120 | text: input, 121 | path: "test.js", 122 | }); 123 | 124 | const parseResult = bluehawk.parse(source); 125 | const files = await bluehawk.process(parseResult); 126 | expect(files["test.js"].document.text.toString()).toBe( 127 | `leave this 128 | and leave this 129 | but not this 130 | ` 131 | ); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /src/bluehawk/tags/stateRemoveTag.test.ts: -------------------------------------------------------------------------------- 1 | import { doesNotReject } from "assert"; 2 | import { fail } from "yargs"; 3 | import { Bluehawk } from "../bluehawk"; 4 | import { Document } from "../Document"; 5 | import { StateRemoveTag } from "./StateRemoveTag"; 6 | import { StateTag } from "./StateTag"; 7 | 8 | describe("remove state tag", () => { 9 | const bluehawk = new Bluehawk(); 10 | bluehawk.registerTag(StateRemoveTag); 11 | bluehawk.registerTag(StateTag); 12 | bluehawk.addLanguage("js", { 13 | languageId: "javascript", 14 | blockComments: [[/\/\*/, /\*\//]], 15 | lineComments: [/\/\/ ?/], 16 | }); 17 | 18 | it("strips text", async () => { 19 | const input = `t0 20 | // :state-start: s1 s2 21 | t1 22 | // :state-end: 23 | // :state-remove-start: s1 24 | t2 25 | // :state-remove-end: 26 | `; 27 | 28 | const source = new Document({ 29 | text: input, 30 | path: "test.js", 31 | }); 32 | 33 | const parseResult = bluehawk.parse(source); 34 | const files = await bluehawk.process(parseResult); 35 | expect(files["test.js"].document.text.toString()).toBe(`t0 36 | t2 37 | `); 38 | expect(files["test.js?state=s1"].document.text.toString()).toBe(`t0 39 | t1 40 | `); 41 | expect(files["test.js?state=s2"].document.text.toString()).toBe(`t0 42 | t1 43 | t2 44 | `); 45 | }); 46 | 47 | it("nests", async () => { 48 | const input = `1 49 | // :state-remove-start: s1 50 | 2 51 | // :state-remove-start: s1 s2 52 | 3 53 | // :state-remove-start: s1 s2 s3 54 | 4 55 | // :state-remove-end: 56 | 5 57 | // :state-remove-end: 58 | 6 59 | // :state-remove-end: 60 | 7 61 | `; 62 | const source = new Document({ 63 | text: input, 64 | path: "test.js", 65 | }); 66 | const parseResult = bluehawk.parse(source); 67 | const files = await bluehawk.process(parseResult); 68 | 69 | expect(files["test.js"].document.text.toString()).toBe(`1 70 | 2 71 | 3 72 | 4 73 | 5 74 | 6 75 | 7 76 | `); 77 | expect(files["test.js?state=s1"].document.text.toString()).toBe(`1 78 | 7 79 | `); 80 | expect(files["test.js?state=s2"].document.text.toString()).toBe(`1 81 | 2 82 | 6 83 | 7 84 | `); 85 | expect(files["test.js?state=s3"].document.text.toString()).toBe(`1 86 | 2 87 | 3 88 | 5 89 | 6 90 | 7 91 | `); 92 | }); 93 | it("behaves well when mixed with state", async () => { 94 | const input = `a 95 | // :state-remove-start: s0 96 | b 97 | // :state-remove-start: s1 98 | c 99 | // :state-start: s2 s3 100 | d 101 | // :state-start: s3 102 | e 103 | // :state-end: 104 | f 105 | // :state-end: 106 | g 107 | // :state-remove-end: 108 | h 109 | // :state-remove-end: 110 | i 111 | `; 112 | const source = new Document({ 113 | text: input, 114 | path: "test.js", 115 | }); 116 | const parseResult = bluehawk.parse(source); 117 | const files = await bluehawk.process(parseResult); 118 | expect(files["test.js"].document.text.toString()).toBe(`a 119 | b 120 | c 121 | g 122 | h 123 | i 124 | `); 125 | expect(files["test.js?state=s0"].document.text.toString()).toBe(`a 126 | i 127 | `); 128 | expect(files["test.js?state=s1"].document.text.toString()).toBe(`a 129 | b 130 | h 131 | i 132 | `); 133 | expect(files["test.js?state=s2"].document.text.toString()).toBe(`a 134 | b 135 | c 136 | d 137 | f 138 | g 139 | h 140 | i 141 | `); 142 | expect(files["test.js?state=s3"].document.text.toString()).toBe(`a 143 | b 144 | c 145 | d 146 | e 147 | f 148 | g 149 | h 150 | i 151 | `); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /src/bluehawk/tags/uncommentTag.test.ts: -------------------------------------------------------------------------------- 1 | import { Bluehawk } from "../bluehawk"; 2 | import { Document } from "../Document"; 3 | import { UncommentTag } from "./UncommentTag"; 4 | import { RemoveTag } from "./RemoveTag"; 5 | 6 | describe("uncomment tag", () => { 7 | const bluehawk = new Bluehawk(); 8 | bluehawk.registerTag(UncommentTag); 9 | bluehawk.registerTag(RemoveTag); 10 | bluehawk.addLanguage(["js"], { 11 | languageId: "javascript", 12 | blockComments: [[/\/\*/, /\*\//]], 13 | lineComments: [/\/\/ ?/], 14 | }); 15 | 16 | it("uncomments", async () => { 17 | const source = new Document({ 18 | text: `// :uncomment-start: 19 | //comment 20 | no comment 21 | //// double comment 22 | // :uncomment-end: 23 | `, 24 | path: "uncomment.test.js", 25 | }); 26 | 27 | const parseResult = bluehawk.parse(source); 28 | const files = await bluehawk.process(parseResult); 29 | expect(files["uncomment.test.js"].document.text.toString()).toBe( 30 | `comment 31 | no comment 32 | // double comment 33 | ` 34 | ); 35 | }); 36 | 37 | it("handles one space after comments", async () => { 38 | const source = new Document({ 39 | text: `// :uncomment-start: 40 | // comment 41 | no comment 42 | // // double comment 43 | // :uncomment-end: 44 | `, 45 | path: "uncomment.test.js", 46 | }); 47 | 48 | const parseResult = bluehawk.parse(source); 49 | const files = await bluehawk.process(parseResult); 50 | expect(files["uncomment.test.js"].document.text.toString()).toBe( 51 | `comment 52 | no comment 53 | // double comment 54 | ` 55 | ); 56 | }); 57 | 58 | it("nests", async () => { 59 | const source = new Document({ 60 | text: `// :uncomment-start: 61 | // comment 62 | // // :uncomment-start: 63 | no comment 64 | // // double comment 65 | // // :uncomment-end: 66 | // :uncomment-end: 67 | `, 68 | path: "uncomment.test.js", 69 | }); 70 | 71 | const parseResult = bluehawk.parse(source); 72 | const files = await bluehawk.process(parseResult); 73 | expect(files["uncomment.test.js"].document.text.toString()).toBe( 74 | `comment 75 | no comment 76 | // double comment 77 | ` 78 | ); 79 | }); 80 | 81 | it("uncomments only at the beginning of the line (after whitespace if any)", async () => { 82 | const source = new Document({ 83 | text: `// :uncomment-start: 84 | a// 85 | leave me alone // comment 86 | // uncomment this 87 | leave this alone // // double comment 88 | // uncomment this // double comment 89 | \t\t// uncomment this 90 | \t\tnot this // though 91 | // :uncomment-end: 92 | `, 93 | path: "uncomment.test.js", 94 | }); 95 | 96 | const parseResult = bluehawk.parse(source); 97 | const files = await bluehawk.process(parseResult); 98 | expect(files["uncomment.test.js"].document.text.toString()).toBe( 99 | ` a// 100 | leave me alone // comment 101 | uncomment this 102 | leave this alone // // double comment 103 | uncomment this // double comment 104 | \t\tuncomment this 105 | \t\tnot this // though 106 | ` 107 | ); 108 | }); 109 | 110 | it("pairs with remove", async () => { 111 | const source = new Document({ 112 | text: `// :uncomment-start: 113 | // comment 114 | // :remove-start: 115 | // // // // 116 | no comment 117 | a // a 118 | \t\t // spaced out comment 119 | // :remove-end: 120 | // // double comment 121 | // :uncomment-end: 122 | `, 123 | path: "uncomment.test.js", 124 | }); 125 | 126 | const parseResult = bluehawk.parse(source); 127 | const files = await bluehawk.process(parseResult); 128 | expect(files["uncomment.test.js"].document.text.toString()).toBe( 129 | `comment 130 | // double comment 131 | ` 132 | ); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import * as Path from "path"; 2 | import * as yargs from "yargs"; 3 | import { getBluehawk, loadPlugins, commandDir } from "../bluehawk"; 4 | import { version as yargsVersion } from "yargs/package.json"; 5 | import { version as bluehawkVersion } from "../../package.json"; 6 | 7 | export async function run(): Promise { 8 | const preArgv = await yargs.option("plugin", { 9 | string: true, 10 | describe: "add a plugin", 11 | }).argv; 12 | 13 | const mainArgv = commandDir( 14 | yargs.help(), 15 | Path.join(__dirname, "commandModules") 16 | ) 17 | .demandCommand() 18 | .recommendCommands() 19 | .strict(); 20 | 21 | const plugins = await loadPlugins(preArgv.plugin); 22 | 23 | const bluehawk = await getBluehawk(); 24 | 25 | const pluginPromises = plugins.map(async (plugin) => { 26 | const { path, register } = plugin; 27 | try { 28 | await register({ 29 | bluehawk, 30 | bluehawkVersion, 31 | yargs: mainArgv, 32 | yargsVersion, 33 | }); 34 | } catch (error) { 35 | error.message = `Plugin '${path}' register() failed with error: ${error.message}`; 36 | throw error; 37 | } 38 | }); 39 | 40 | await Promise.all(pluginPromises); 41 | 42 | // Accessing this property executes CLI 43 | mainArgv.argv; 44 | } 45 | -------------------------------------------------------------------------------- /src/cli/commandModules/check.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleActionReporter } from "./../../bluehawk/actions/ConsoleActionReporter"; 2 | import { CommandModule } from "yargs"; 3 | import { 4 | withIgnoreOption, 5 | withJsonOption, 6 | withLogLevelOption, 7 | ActionArgs, 8 | CheckArgs, 9 | check, 10 | } from "../.."; 11 | 12 | const commandModule: CommandModule< 13 | ActionArgs & { paths: string[] }, 14 | CheckArgs 15 | > = { 16 | command: "check ", 17 | builder(yargs) { 18 | return withLogLevelOption(withJsonOption(withIgnoreOption(yargs))); 19 | }, 20 | async handler(args) { 21 | const reporter = new ConsoleActionReporter(args); 22 | await check({ ...args, reporter }); 23 | reporter.printSummary(); 24 | process.exit(reporter.errorCount > 0 ? 1 : 0); 25 | }, 26 | aliases: [], 27 | describe: 28 | "validate bluehawk markup without outputting any files. Exits with non-zero code if errors are found.", 29 | }; 30 | 31 | export default commandModule; 32 | -------------------------------------------------------------------------------- /src/cli/commandModules/copy.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleActionReporter } from "./../../bluehawk/actions/ConsoleActionReporter"; 2 | import { CommandModule, Arguments, Argv } from "yargs"; 3 | import { 4 | withOutputOption, 5 | withStateOption, 6 | withIgnoreOption, 7 | withLogLevelOption, 8 | withRenameOption, 9 | ActionArgs, 10 | CopyArgs, 11 | CopyArgsCli, 12 | copy, 13 | } from "../.."; 14 | 15 | const commandModule: CommandModule< 16 | ActionArgs & { rootPath: string }, 17 | CopyArgsCli 18 | > = { 19 | command: "copy ", 20 | builder: (yargs): Argv => { 21 | return withLogLevelOption( 22 | withIgnoreOption( 23 | withStateOption(withRenameOption(withOutputOption(yargs))) 24 | ) 25 | ); 26 | }, 27 | handler: async (args: Arguments) => { 28 | const reporter = new ConsoleActionReporter(args); 29 | let parsedRename = undefined; 30 | // parse rename argument to record if specified 31 | if (typeof args.rename === "string") { 32 | try { 33 | parsedRename = JSON.parse(args.rename) as Record; 34 | } catch (SyntaxError) { 35 | throw "Unable to parse 'rename' argument. Ensure your 'rename' argument is valid JSON."; 36 | } 37 | } 38 | const argsParsed: CopyArgs = { ...args, rename: parsedRename }; 39 | await copy({ ...argsParsed, reporter }); 40 | reporter.printSummary(); 41 | process.exit(reporter.errorCount > 0 ? 1 : 0); 42 | }, 43 | aliases: [], 44 | describe: 45 | "clone source project to output directory with Bluehawk commands processed", 46 | }; 47 | 48 | export default commandModule; 49 | -------------------------------------------------------------------------------- /src/cli/commandModules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./check"; 2 | export * from "./copy"; 3 | export * from "./list"; 4 | export * from "./snip"; 5 | -------------------------------------------------------------------------------- /src/cli/commandModules/list.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from "yargs"; 2 | import { commandDir } from "../../bluehawk"; 3 | import * as path from "path"; 4 | 5 | const commandModule: yargs.CommandModule = { 6 | command: "list", 7 | builder(yargs) { 8 | return commandDir(yargs, path.join(__dirname, "list")); 9 | }, 10 | handler() { 11 | yargs.showHelp(); 12 | }, 13 | aliases: [], 14 | describe: "list info", 15 | }; 16 | 17 | export default commandModule; 18 | -------------------------------------------------------------------------------- /src/cli/commandModules/list/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./tags"; 2 | export * from "./states"; 3 | -------------------------------------------------------------------------------- /src/cli/commandModules/list/states.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleActionReporter } from "./../../../bluehawk/actions/ConsoleActionReporter"; 2 | import { CommandModule } from "yargs"; 3 | import { 4 | withIgnoreOption, 5 | withJsonOption, 6 | withLogLevelOption, 7 | } from "../../../bluehawk/options"; 8 | import { ActionArgs, ListStatesArgs, listStates } from "../../../bluehawk"; 9 | 10 | const commandModule: CommandModule< 11 | ActionArgs & { paths: string[] }, 12 | ListStatesArgs 13 | > = { 14 | command: "states ", 15 | builder(argv) { 16 | return withLogLevelOption(withJsonOption(withIgnoreOption(argv))); 17 | }, 18 | async handler(args) { 19 | const reporter = new ConsoleActionReporter(args); 20 | await listStates({ ...args, reporter }); 21 | reporter.printSummary(); 22 | process.exit(reporter.errorCount > 0 ? 1 : 0); 23 | }, 24 | aliases: [], 25 | describe: "list states used in the given project", 26 | }; 27 | 28 | export default commandModule; 29 | -------------------------------------------------------------------------------- /src/cli/commandModules/list/tags.ts: -------------------------------------------------------------------------------- 1 | import { CommandModule } from "yargs"; 2 | import { ActionArgs, ListTagArgs, listTags, withJsonOption } from "../../.."; 3 | 4 | const commandModule: CommandModule = { 5 | command: "tags", 6 | builder(args) { 7 | return withJsonOption(args); 8 | }, 9 | handler: async (args) => { 10 | await listTags(args); 11 | }, 12 | aliases: [], 13 | describe: "list available tags", 14 | }; 15 | 16 | export default commandModule; 17 | -------------------------------------------------------------------------------- /src/cli/commandModules/snip.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleActionReporter } from "./../../bluehawk/actions/ConsoleActionReporter"; 2 | import { CommandModule, Arguments, Argv } from "yargs"; 3 | import { 4 | withOutputOption, 5 | withStateOption, 6 | withIdOption, 7 | withIgnoreOption, 8 | withGenerateFormattedCodeSnippetsOption, 9 | withLogLevelOption, 10 | } from "../../bluehawk/options"; 11 | import { ActionArgs, SnipArgs, snip } from "../../bluehawk"; 12 | 13 | const commandModule: CommandModule = 14 | { 15 | command: "snip ", 16 | builder: (yargs): Argv => { 17 | return withLogLevelOption( 18 | withIgnoreOption( 19 | withStateOption( 20 | withIdOption( 21 | withOutputOption(withGenerateFormattedCodeSnippetsOption(yargs)) 22 | ) 23 | ) 24 | ) 25 | ); 26 | }, 27 | handler: async (args: Arguments) => { 28 | const reporter = new ConsoleActionReporter(args); 29 | await snip({ ...args, reporter }); 30 | reporter.printSummary(); 31 | process.exit(reporter.errorCount > 0 ? 1 : 0); 32 | }, 33 | aliases: [], 34 | describe: "extract snippets", 35 | }; 36 | 37 | export default commandModule; 38 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from "./cli"; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bluehawk"; 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { run } from "./cli"; 4 | 5 | run().catch((err) => { 6 | console.error(err); 7 | process.exit(1); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build", 4 | "allowJs": false, 5 | "target": "ES5", 6 | "esModuleInterop": true, 7 | "lib": ["ES2020"], 8 | "downlevelIteration": true, 9 | "strictNullChecks": true, 10 | "strict": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "resolveJsonModule": true, 14 | "useUnknownInCatchVariables": false 15 | }, 16 | "sourceMap": true, 17 | "include": ["./src/**/*.ts"], 18 | "exclude": ["**/*.test.ts"] 19 | } 20 | --------------------------------------------------------------------------------