[
  {
    "path": ".claude/hooks/check-on-stop.sh",
    "content": "#!/bin/bash\nINPUT=$(cat)\n\n# Prevent infinite loops — if we already triggered a continuation, skip\nif [ \"$(echo \"$INPUT\" | jq -r '.stop_hook_active')\" = \"true\" ]; then\n  exit 0\nfi\n\ncd \"$(dirname \"$0\")/../..\" || exit 0\n\nERRORS=\"\"\n\n# 1. Prettier (autoformat, don't block on this)\npnpm format > /dev/null 2>&1\n\n# 2. ESLint\nESLINT_OUTPUT=$(pnpm lint 2>&1)\nif [ $? -ne 0 ]; then\n  ERRORS=\"${ERRORS}ESLint failed:\\n${ESLINT_OUTPUT}\\n\\n\"\nfi\n\n# 3. TypeScript type checking\nTYPECHECK_OUTPUT=$(pnpm typecheck 2>&1)\nif [ $? -ne 0 ]; then\n  ERRORS=\"${ERRORS}TypeScript failed:\\n${TYPECHECK_OUTPUT}\\n\\n\"\nfi\n\n# 4. Tests\nTEST_OUTPUT=$(pnpm test 2>&1)\nif [ $? -ne 0 ]; then\n  ERRORS=\"${ERRORS}Tests failed:\\n${TEST_OUTPUT}\\n\\n\"\nfi\n\nif [ -n \"$ERRORS\" ]; then\n  echo -e \"$ERRORS\" >&2\n  exit 2\nfi\n\nexit 0\n"
  },
  {
    "path": ".claude/settings.json",
    "content": "{\n  \"hooks\": {\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"Write|Edit\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"jq -r '.tool_input.file_path' | xargs pnpm prettier --write 2>/dev/null; exit 0\"\n          }\n        ]\n      }\n    ],\n    \"Stop\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"bash .claude/hooks/check-on-stop.sh\"\n          }\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": ".claude/skills/triage/SKILL.md",
    "content": "---\nname: triage\ndescription: Triage and close GitHub issues on TalAter/annyang\nuser-invocable: true\nallowed-tools: Bash, Read, Grep, Glob, Agent\nargument-hint: [issue-number or \"list\"]\n---\n\n# GitHub Issue Triage for TalAter/annyang\n\nYou are helping triage and close GitHub issues on the **TalAter/annyang** repository.\n\n## How to post as the bot\n\nAll `gh` commands that interact with issues MUST use the bot script so comments are posted as `annyang-triage[bot]`, not as the repo owner:\n\n```bash\n./scripts/gh-bot issue comment <number> --repo TalAter/annyang --body \"<message>\"\n./scripts/gh-bot issue close <number> --repo TalAter/annyang\n./scripts/gh-bot issue close <number> --repo TalAter/annyang --reason \"not planned\"\n```\n\nNever use bare `gh` for issue comments or closes — always use `./scripts/gh-bot`.\n\n## Workflow\n\n1. **If given an issue number**: fetch it with `gh issue view <number> --repo TalAter/annyang --comments` (this read-only call can use regular `gh`)\n2. **If asked to list issues**: use `gh issue list --repo TalAter/annyang` (read-only, regular `gh` is fine). To sort by newest, use `-S \"sort:created-desc\"`. Note: `gh issue list` has no `--sort` flag — use the `-S` search query instead.\n3. **Read the issue and all comments carefully** before deciding on an action\n4. **Present your proposed comment and action to the user** before posting. Wait for approval.\n5. **Post using `./scripts/gh-bot`** once approved\n\n## Closing reasons\n\n- `gh issue close` — default, resolved/completed\n- `gh issue close --reason \"not planned\"` — out of scope, won't fix, not a bug\n\nChoose the appropriate reason. Most stale or out-of-scope issues should use \"not planned\".\n\n## Tone\n\nThese are real people who took time to file issues. Be kind, patient, and helpful.\n\n- **Keep comments to 1-3 sentences.** Concise but warm.\n- **Never dismissive.** Even if the issue is out of scope or a misunderstanding, acknowledge what they were trying to do.\n- **Explain \"why\" when closing.** When something is outside annyang's control (browser behavior, platform limitations, third-party APIs), briefly explain why so the user learns something useful.\n- **Don't pile on.** If the issue was already answered in the comments, just close it — no need to add another comment.\n- **Be helpful with links.** When pointing to another issue or resource, briefly say why it's relevant.\n- **End positively when natural.** \"Good luck with your project!\" or \"Hope that helps!\" — but only when it fits. Don't force it.\n\n### Do NOT\n\n- Use phrases like \"this is a support question, not a bug\" — it sounds dismissive\n- Be condescending about the user's level of knowledge\n- Apologize excessively — one brief acknowledgment is enough\n- Write walls of text — if it needs more than 3 sentences, something is wrong\n- Close without a comment unless the issue was already fully answered in existing comments\n\n## Formatting\n\n**Always** render issue numbers as clickable links: `[#123](https://github.com/TalAter/annyang/issues/123)`. This applies everywhere — proposals, summaries, references within comments, conversation text. Never write a bare `#123`.\n\n## After completing actions\n\nAlways finish with a concise summary of what was done, with linked issue numbers.\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# http://editorconfig.org\n\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nmax_line_length = 120\n"
  },
  {
    "path": ".gitattributes",
    "content": "demo/* linguist-documentation\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--- Provide a general summary of the issue in the Title above. -->\n\n## Expected Behavior\n<!--- If you're describing a bug, tell us what should happen. -->\n<!--- If you're suggesting a change/improvement, tell us how it should work. -->\n\n## Current Behavior\n<!--- If you are describing a bug, tell us what happens instead of the expected behavior. -->\n<!--- If you are suggesting a change/improvement, explain the difference from current behavior. -->\n\n## Possible Solution\n<!--- Not obligatory, but suggest a fix/reason for the bug, -->\n<!--- or ideas for how to implement the addition or change. -->\n\n## Steps to Reproduce (for bugs)\n<!--- Provide a link to a live example or an unambiguous set of steps to -->\n<!--- reproduce this bug. Include code to reproduce, if relevant. -->\n1.\n2.\n3.\n4.\n\n## Context\n<!--- How has this issue affected you? What are you trying to accomplish? -->\n<!--- Providing context helps us come up with a solution that is most useful in the real world. -->\n\n## Your Environment\n<!--- Include as many relevant details about the environment you experienced the bug in. -->\n* Version used:\n* Browser name and version:\n* Operating system and version (desktop or mobile):\n* Link to your project:\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--- Provide a general summary of your changes in the Title above -->\n\n## Description\n<!--- Describe your changes in detail -->\n\n## Motivation and Context\n<!--- Why is this change required? What problem does it solve? -->\n<!--- If it fixes an open issue, please link to the issue here. -->\n\n## How Has This Been Tested?\n<!--- Please describe in detail how you tested your changes. -->\n<!--- Include details of your testing environment, and the tests you ran to -->\n<!--- see how your change affects other areas of the code, etc. -->\n\n## Types of changes\n<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to change)\n\n## Checklist:\n<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->\n<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->\n- [ ] My code follows the code style of this project.\n- [ ] My change requires a change to the documentation.\n- [ ] I have updated the documentation accordingly.\n- [ ] I have read the **CONTRIBUTING** document.\n- [ ] I have added tests to cover my changes.\n- [ ] All new and existing tests passed.\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\nnpm-debug.log\n.idea\n.vscode\n.idx\n.DS_Store\ntest-manual/dist\ntest-manual/annyang.iife.min.js\nscripts\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"printWidth\": 120,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"endOfLine\": \"lf\",\n  \"insertPragma\": false,\n  \"requirePragma\": false,\n  \"arrowParens\": \"avoid\",\n  \"overrides\": [\n    {\n      \"files\": \"*.json\",\n      \"options\": {\n        \"parser\": \"json\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 3.0.0\n\n### New Features / Breaking Changes\n\n- **TypeScript types included** — Full type definitions ship with the package. `addCallback` enforces correct callback signatures per event type.\n- **ESM/CJS/IIFE module support** — Works with `import`, `require()`, and `<script>` tags.\n- **`getState()` method** — Returns `'idle'`, `'listening'`, or `'paused'`.\n- **`state` property** (on default export) — Getter that returns the current state.\n- **`addCallback` returns an unsubscribe function** — Previously returned `undefined`. Now returns a function you can call to remove that specific callback:\n  ```js\n  const unsub = annyang.addCallback('start', myFunc);\n  unsub(); // removes myFunc from 'start' callbacks\n  ```\n- **`trigger()` works independently of speech recognition** — `trigger()` can now be used regardless of whether annyang is listening, paused, aborted, or even in browsers that don't support speech recognition. If you need the previous behavior, check `isListening()` before calling `trigger()` or within the triggered command's callback.\n- **Safe to use in unsupported browsers** — Methods like `start()`, `addCommands()`, and `setLanguage()` no longer throw when SpeechRecognition is unavailable. Speech recognition simply won't activate, but command registration and `trigger()` still work.\n- **`if (annyang)` no longer detects browser support** — Starting in v3, the annyang object is always defined. Use `annyang.isSpeechRecognitionSupported()` instead:\n\n  ```js\n  // v2\n  if (annyang) {\n    annyang.start();\n  }\n\n  // v3\n  if (annyang.isSpeechRecognitionSupported()) {\n    annyang.start();\n  }\n  ```\n\n- **`init()` deprecated** — annyang initializes automatically when needed. Calling `init()` now logs a deprecation warning. Remove any calls to `init()`.\n- **String-based command callbacks removed** — Passing function names as strings (e.g. `{'hello': 'myFunc'}`) is no longer supported. Pass functions directly: `{'hello': myFunc}`.\n- **Duplicate command phrases now overwrite** — In v2, adding a command with the same phrase would register both callbacks. In v3, the new callback replaces the old one.\n\n### Internal\n\n- Switched bundler from Rollup to tsup\n- Migrated source to TypeScript with strict mode\n- Tests migrated to Vitest\n- `parseResults` refactored to use `for...of` with early return\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to annyang\n\nThank you for taking the time to get involved with annyang! :+1:\n\nThere are several ways you can help the project out:\n\n* [Contributing code](#contributing-code)\n* [Reporting Bugs](#reporting-bugs)\n* [Feature Requests and Ideas](#feature-requests-and-ideas)\n\n## How Can I Contribute?\n\n### Contributing Code\n\nA lot of annyang's functionality came from pull requests sent over GitHub. Here is how you can contribute too:\n\n- [x] Fork the repository from the [annyang GitHub page](https://github.com/TalAter/annyang).\n- [x] Clone a copy to your local machine with `$ git clone git@github.com:YOUR-GITHUB-USER-NAME/annyang.git`\n- [x] Make sure you have *node.js* and *pnpm* installed on your machine.\n- [x] Install all of annyang's development dependencies with pnpm. `$ cd annyang; pnpm install`\n- [x] Run the tests to make sure everything runs smoothly: `$ pnpm test`\n- [x] Add tests for your code. [See details below](#automated-testing).\n- [x] Code, code, code. Changes should be done in `/src/annyang.ts`.\n- [x] Run `$ pnpm test` and `$ pnpm lint` after making changes to verify that everything still works and the tests all pass.\n\n  :bulb: A great alternative to repeatedly running tests is to run `$ pnpm test:watch` once, and leave this process running. It will continuously run all the tests every time you make a change to one of annyang's files. :+1:\n- [x] Before committing your changes, the last step must always be running `$ pnpm test` and `$ pnpm lint`. This makes sure everything works.\n- [x] Once you've made sure all your changes work correctly and have been committed, push your local changes back to github with `$ git push -u origin master`\n- [x] Visit your fork on GitHub.com ([https://github.com/YOUR-USER-NAME/annyang](https://github.com/YOUR-USER-NAME/annyang)) and create a pull request for your changes.\n- [x] Makes sure your pull request describes exactly what you changed and if it relates to an open issue references that issue (just include the issue number in the title like this: #49)\n\n#### Important:\n\n* Make sure to run `pnpm install`, `pnpm test`, and `pnpm lint` and make sure all tasks completed successfully before committing.\n* Do not change the [API docs](https://github.com/TalAter/annyang/blob/master/docs/README.md) in `/docs/README.md` directly. This file is generated automatically, and your changes will be overwritten. Instead, update the relevant comments in `src/annyang.ts`\n* Do not update the version number yourself.\n* Please stick to the project's existing coding style. Coding styles don't need to have a consensus, they just need to be consistent :smile:.\n* Push your changes to a topic branch in your fork of the repository. Your branch should be based on the `master` branch.\n* When submitting [pull request](https://help.github.com/articles/using-pull-requests/), please elaborate as much as possible about the change, your motivation for the change, etc.\n\n#### Build Commands\n\n| Command | Description |\n|---|---|\n| `pnpm build` | Build ESM, CJS, and IIFE bundles with tsup |\n| `pnpm test` | Run tests with Vitest |\n| `pnpm test:watch` | Run tests in watch mode |\n| `pnpm lint` | Run ESLint |\n| `pnpm format` | Format code with Prettier |\n| `pnpm typecheck` | Check types with TypeScript |\n| `pnpm dev` | Build in watch mode |\n\n#### Automated Testing\n\nannyang is tested using [Vitest](https://vitest.dev/).\n\nPlease include tests for any changes you make:\n* If you found a bug, please write a test that fails because of that bug, then fix the bug so that the test passes.\n* If you are adding a new feature, write tests that thoroughly test every possible use of your code.\n* If you are changing existing functionality, make sure to update existing tests so they pass. (This is a last resort move. Whenever possible try to maintain backward compatibility)\n\nTo simulate Speech Recognition in the testing environment, annyang uses a mock object called [Corti](https://github.com/TalAter/Corti) which mocks the browser's SpeechRecognition object. Corti also adds a number of utility functions to the SpeechRecognition object which simulate user actions (e.g. `say('Hello there')`), and allow checking the SpeechRecognition's status (e.g. `isListening() === true`).\n\n### Reporting Bugs\n\nBugs are tracked as [GitHub issues](https://github.com/TalAter/annyang/issues). If you found a bug with annyang, the quickest way to get help would be to look through existing open and closed [GitHub issues](https://github.com/TalAter/annyang/issues?q=is%3Aissue). If the issue is already being discussed and hasn't been resolved yet, you can join the discussion and provide details about the problem you are having. If this is a new bug, please open a [new issue](https://github.com/TalAter/annyang/issues/new).\n\nWhen you are creating a bug report, please include as many details as possible.\n\nExplain the problem and include additional details to help maintainers reproduce the problem.\n\n* Use a clear and descriptive title for the issue to identify the problem.\n* Describe the exact steps which reproduce the problem. Share the relevant code to reproduce the issue if possible.\n* Try to isolate the issue as much as possible, reducing unrelated code until you get to the minimal amount of code in which the bug still reproduces. This is the most important step to help the community solve the issue.\n\n### Feature Requests and Ideas\n\nWe track discussions of new features, proposed changes, and other ideas as [GitHub issues](https://github.com/TalAter/annyang/issues). If you would like to discuss one of those, please first look through existing open and closed [GitHub issues](https://github.com/TalAter/annyang/issues?q=is%3Aissue) and see if there is already a discussion on this topic which you can join. If there isn't, please open a [new issue](https://github.com/TalAter/annyang/issues/new).\n\nWhen discussing new ideas or proposing changes, please take the time to be as descriptive as possible about the topic at hand. Please take the time to explain the issue you are facing, or the problem you propose to solve in as much detail as possible.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2024 Tal Ater\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# annyang!\n\nA tiny JavaScript Speech Recognition library that lets your users control your site with voice commands.\n\n**annyang** has no dependencies, weighs just 2 KB, and is free to use and modify under the MIT license.\n\n## Demo and Tutorial\n\n[Play with some live speech recognition demos](https://www.talater.com/annyang)\n\n## FAQ, Technical Documentation, and API Reference\n\n- [annyang Frequently Asked Questions](https://github.com/TalAter/annyang/blob/master/docs/FAQ.md)\n- [annyang API reference](https://github.com/TalAter/annyang/blob/master/docs/README.md)\n- [annyang tutorial](https://www.talater.com/annyang)\n- [CHANGELOG](https://github.com/TalAter/annyang/blob/master/CHANGELOG.md)\n\n## Install\n\n```sh\nnpm install annyang\n```\n\n## Hello World\n\nIt's as easy as installing annyang and defining the commands you want.\n\n### ESM (recommended)\n\n```js\nimport annyang from 'annyang';\n\nif (annyang.isSpeechRecognitionSupported()) {\n  // Let's define a command.\n  const commands = {\n    'hello': () => { alert('Hello world!'); },\n    'search for *term': (term) => { console.log(`Searching for ${term}`); },\n  };\n\n  // Add our commands to annyang\n  annyang.addCommands(commands);\n\n  // Start listening.\n  annyang.start();\n}\n```\n\n### Named imports\n\n```js\nimport { addCommands, start, isSpeechRecognitionSupported } from 'annyang';\n\nif (isSpeechRecognitionSupported()) {\n  addCommands({ 'hello': () => { alert('Hello world!'); } });\n  start();\n}\n```\n\n### CommonJS\n\n```js\nconst annyang = require('annyang');\n```\n\n### Script tag (IIFE)\n\n````html\n<script src=\"dist/annyang.iife.min.js\"></script>\n<script>\nif (annyang.isSpeechRecognitionSupported()) {\n  // Let's define a command.\n  const commands = {\n    'hello': () => { alert('Hello world!'); }\n  };\n\n  // Add our commands to annyang\n  annyang.addCommands(commands);\n\n  // Start listening.\n  annyang.start();\n}\n</script>\n````\n\n**Check out some [live speech recognition demos and advanced samples](https://www.talater.com/annyang), then read the full [API Docs](https://github.com/TalAter/annyang/blob/master/docs/README.md).**\n\n## Adding a GUI\n\nYou can easily add a GUI for the user to interact with Speech Recognition using [Speech KITT](https://github.com/TalAter/SpeechKITT).\n\nSpeech KITT makes it easy to add a graphical interface for the user to start or stop Speech Recognition and see its current status. KITT also provides clear visual hints to the user on how to interact with your site using their voice, providing instructions and sample commands.\n\nSpeech KITT is fully customizable and comes with many different themes, and instructions on how to create your own designs.\n\n[![Speech Recognition GUI with Speech KITT](https://raw.githubusercontent.com/TalAter/SpeechKITT/master/demo/speechkitt-demo.gif)](https://github.com/TalAter/SpeechKITT)\n\nFor help with setting up a GUI with KITT, check out the [Speech KITT page](https://github.com/TalAter/SpeechKITT).\n\n## Author\n\nTal Ater: [@TalAter](https://twitter.com/TalAter)\n\n## License\n\nLicensed under [MIT](https://github.com/TalAter/annyang/blob/master/LICENSE).\n"
  },
  {
    "path": "demo/css/main.css",
    "content": "/* ===== Design Tokens ===== */\n:root {\n  --bg: #0c0c0c;\n  --bg-alt: #151514;\n  --bg-code: #1a1917;\n  --text: #f5f2ed;\n  --text-body: #f5f2edbf;\n  --text-muted: #f5f2ed66;\n  --text-faint: #f5f2ed40;\n  --accent: #ff6b35;\n  --border: #f5f2ed0f;\n  --font-heading: 'DM Sans', sans-serif;\n  --font-body: 'Inter', sans-serif;\n  --font-mono: 'JetBrains Mono', monospace;\n}\n\n/* ===== Reset ===== */\n*,\n*::before,\n*::after {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\nhtml {\n  scroll-behavior: smooth;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nbody {\n  background: var(--bg);\n  color: var(--text-body);\n  font-family: var(--font-body);\n  font-size: 18px;\n  font-weight: 400;\n  line-height: 1.6;\n}\n\na {\n  color: var(--accent);\n  text-decoration: none;\n}\na:hover {\n  text-decoration: underline;\n}\n\n/* ===== Section Layout ===== */\nsection {\n  padding: 120px 140px;\n}\n\nsection.alt {\n  background: var(--bg-alt);\n}\n\n/* ===== Section Labels ===== */\n.section-label {\n  font-family: var(--font-mono);\n  font-size: 13px;\n  font-weight: 400;\n  text-transform: uppercase;\n  letter-spacing: 0.1em;\n  color: var(--accent);\n  margin-bottom: 24px;\n}\n\n/* ===== Typography ===== */\nh1 {\n  font-family: var(--font-heading);\n  font-weight: 900;\n  font-size: 120px;\n  line-height: 1;\n  letter-spacing: -0.03em;\n  color: var(--text);\n  margin-bottom: 32px;\n}\n\nh2 {\n  font-family: var(--font-heading);\n  font-weight: 900;\n  font-size: 56px;\n  line-height: 1.1;\n  letter-spacing: -0.02em;\n  color: var(--text);\n  margin-bottom: 24px;\n}\n\nh2.footer-heading {\n  font-size: 72px;\n  line-height: 1.05;\n}\n\n.hero-description {\n  font-size: 20px;\n  color: var(--text-body);\n  max-width: 540px;\n  margin-bottom: 48px;\n  line-height: 1.7;\n}\n\n.section-description {\n  font-size: 20px;\n  color: var(--text-body);\n  max-width: 640px;\n  margin-bottom: 32px;\n  line-height: 1.7;\n}\n\n.em-lead {\n  font-size: 28px;\n  font-weight: 600;\n  color: var(--text);\n  margin-bottom: 16px;\n  font-style: italic;\n}\n\n/* ===== Hero ===== */\n.hero {\n  position: relative;\n  overflow: hidden;\n  padding-top: 160px;\n  padding-bottom: 160px;\n}\n\n.hero-glow {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  width: 800px;\n  height: 800px;\n  background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);\n  pointer-events: none;\n}\n\n.hero-waveform {\n  position: absolute;\n  top: 120px;\n  left: 100px;\n  width: 600px;\n  height: 320px;\n  opacity: 0.12;\n  pointer-events: none;\n}\n\n.hero-stats {\n  font-family: var(--font-mono);\n  font-size: 13px;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--text-muted);\n  margin-bottom: 32px;\n}\n\n.hero-stats-dot {\n  margin: 0 12px;\n  color: var(--text-faint);\n}\n\n/* ===== Buttons ===== */\n.btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 8px;\n  font-family: var(--font-body);\n  font-size: 16px;\n  font-weight: 600;\n  border-radius: 100px;\n  padding: 14px 32px;\n  cursor: pointer;\n  transition: all 0.2s ease;\n  border: none;\n  text-decoration: none;\n}\n\n.btn:hover {\n  text-decoration: none;\n}\n\n.btn-primary {\n  background: var(--accent);\n  color: #fff;\n}\n\n.btn-primary:hover {\n  background: #e55a28;\n}\n\n.btn-secondary {\n  background: transparent;\n  color: var(--text);\n  border: 1px solid var(--text-faint);\n}\n\n.btn-secondary:hover {\n  border-color: var(--text-muted);\n}\n\n/* ===== Voice Instruction Sections ===== */\n.mic-icon {\n  width: 20px;\n  height: 20px;\n  margin-right: 4px;\n  vertical-align: middle;\n  opacity: 0.5;\n}\n\n.voice-instruction {\n  font-size: 20px;\n  color: var(--text-body);\n  margin-bottom: 12px;\n}\n\n.voice-instruction code {\n  font-family: var(--font-mono);\n  font-size: 18px;\n  color: var(--accent);\n}\n\n.response-text {\n  font-family: var(--font-heading);\n  font-weight: 900;\n  font-size: 80px;\n  line-height: 1.1;\n  color: var(--text);\n  margin-top: 40px;\n  display: none;\n}\n\n.response-text.visible {\n  display: block;\n}\n\n/* ===== Code Blocks ===== */\n.code-block {\n  background: var(--bg-code);\n  border-radius: 16px;\n  padding: 36px 40px;\n  overflow-x: auto;\n  margin-top: 32px;\n  max-width: 780px;\n}\n\n.code-block pre {\n  margin: 0;\n  font-family: var(--font-mono);\n  font-size: 15px;\n  line-height: 1.7;\n  color: var(--text-body);\n  white-space: pre;\n}\n\n/* Hand-painted syntax highlighting */\n.kw {\n  color: var(--accent);\n}\n.str {\n  color: #d4a574;\n}\n.cm {\n  color: var(--text-faint);\n}\n.fn {\n  color: #c9b8a8;\n}\n.op {\n  color: var(--text-muted);\n}\n\n/* ===== Gallery ===== */\n.gallery {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 12px;\n  margin-top: 32px;\n}\n\n.gallery-item {\n  width: 110px;\n  height: 110px;\n  border-radius: 8px;\n  border: 1px solid rgba(255, 107, 53, 0.15);\n  background: var(--bg-code);\n  overflow: hidden;\n}\n\n.gallery-item img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  display: block;\n}\n\n/* ===== TPS Report ===== */\n#tpsreport {\n  position: fixed;\n  right: 40px;\n  bottom: -600px;\n  width: 320px;\n  z-index: 100;\n  border-radius: 4px;\n  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);\n  transform: rotate(-12deg);\n  transition: bottom 0.6s ease;\n}\n\n#tpsreport.visible {\n  bottom: -80px;\n}\n\n/* ===== Stats Bar ===== */\n.stats-bar {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 0;\n  padding: 80px 140px;\n  background: var(--bg);\n}\n\n.stat {\n  text-align: center;\n  padding: 0 60px;\n}\n\n.stat-value {\n  font-family: var(--font-heading);\n  font-weight: 900;\n  font-size: 48px;\n  color: var(--accent);\n  line-height: 1.2;\n}\n\n.stat-label {\n  font-family: var(--font-mono);\n  font-size: 13px;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--text-muted);\n  margin-top: 8px;\n}\n\n.stat-divider {\n  width: 1px;\n  height: 60px;\n  background: var(--border);\n  flex-shrink: 0;\n}\n\n/* ===== Footer CTA ===== */\n.footer-cta {\n  position: relative;\n  overflow: hidden;\n  text-align: center;\n  padding: 140px 140px;\n}\n\n.footer-cta .hero-description {\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.footer-cta .btn-group {\n  display: flex;\n  justify-content: center;\n  gap: 16px;\n  flex-wrap: wrap;\n}\n\n.footer-wave {\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  width: 100%;\n  opacity: 0.08;\n  pointer-events: none;\n}\n\n.footer-arcs {\n  position: absolute;\n  top: 60px;\n  right: 60px;\n  opacity: 0.06;\n  pointer-events: none;\n}\n\n/* ===== Copyright ===== */\n.copyright {\n  border-top: 1px solid var(--border);\n  padding: 32px 140px;\n  text-align: center;\n  font-size: 14px;\n  color: var(--text-faint);\n}\n\n.copyright a {\n  color: var(--text-muted);\n}\n\n/* ===== Unsupported Banner ===== */\n#unsupported {\n  display: none;\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  background: #1a1917;\n  border-top: 1px solid var(--border);\n  padding: 24px 40px;\n  z-index: 200;\n  text-align: center;\n}\n\n#unsupported.visible {\n  display: block;\n}\n\n#unsupported h4 {\n  font-family: var(--font-heading);\n  font-weight: 900;\n  font-size: 18px;\n  color: var(--text);\n  margin-bottom: 8px;\n}\n\n#unsupported p {\n  font-size: 14px;\n  color: var(--text-muted);\n}\n\n/* ===== Flickr Loader ===== */\n#flickrLoader {\n  margin-top: 24px;\n  height: 24px;\n  font-family: var(--font-mono);\n  font-size: 14px;\n  color: var(--text-muted);\n}\n\n#flickrLoader p {\n  opacity: 0;\n  transition: opacity 0.2s ease;\n}\n\n#flickrLoader.visible p {\n  opacity: 1;\n}\n\n/* ===== Responsive ===== */\n@media (max-width: 1200px) {\n  section {\n    padding: 100px 80px;\n  }\n  .stats-bar {\n    padding: 60px 80px;\n  }\n  .footer-cta {\n    padding: 120px 80px;\n  }\n  .copyright {\n    padding: 32px 80px;\n  }\n  h1 {\n    font-size: 88px;\n  }\n  .hero-waveform {\n    width: 440px;\n    height: 240px;\n    left: 50px;\n  }\n  h2 {\n    font-size: 44px;\n  }\n  h2.footer-heading {\n    font-size: 56px;\n  }\n  .response-text {\n    font-size: 64px;\n  }\n  .stat {\n    padding: 0 40px;\n  }\n}\n\n@media (max-width: 768px) {\n  section {\n    padding: 80px 32px;\n  }\n  .stats-bar {\n    padding: 48px 32px;\n    flex-wrap: wrap;\n    gap: 32px;\n  }\n  .stat-divider {\n    display: none;\n  }\n  .stat {\n    padding: 0 24px;\n  }\n  .footer-cta {\n    padding: 80px 32px;\n  }\n  .copyright {\n    padding: 24px 32px;\n  }\n  h1 {\n    font-size: 56px;\n  }\n  .hero-waveform {\n    width: 280px;\n    height: 150px;\n    left: 10px;\n    top: 100px;\n  }\n  h2 {\n    font-size: 36px;\n  }\n  h2.footer-heading {\n    font-size: 44px;\n  }\n  .hero {\n    padding-top: 100px;\n    padding-bottom: 100px;\n  }\n  .hero-description {\n    font-size: 18px;\n  }\n  .response-text {\n    font-size: 48px;\n  }\n  .stat-value {\n    font-size: 36px;\n  }\n  .code-block {\n    padding: 24px;\n    border-radius: 12px;\n  }\n  .code-block pre {\n    font-size: 13px;\n  }\n  .footer-cta .btn-group {\n    flex-direction: column;\n    align-items: center;\n  }\n}\n\n@media (max-width: 480px) {\n  section {\n    padding: 60px 20px;\n  }\n  .stats-bar {\n    padding: 40px 20px;\n  }\n  .footer-cta {\n    padding: 60px 20px;\n  }\n  .copyright {\n    padding: 20px;\n  }\n  h1 {\n    font-size: 40px;\n    margin-bottom: 20px;\n  }\n  .hero-waveform {\n    width: 200px;\n    height: 110px;\n    left: 5px;\n    top: 80px;\n  }\n  h2 {\n    font-size: 28px;\n  }\n  h2.footer-heading {\n    font-size: 32px;\n  }\n  .hero {\n    padding-top: 80px;\n    padding-bottom: 80px;\n  }\n  .response-text {\n    font-size: 36px;\n  }\n  .stat-value {\n    font-size: 28px;\n  }\n  .code-block {\n    padding: 20px 16px;\n  }\n  .code-block pre {\n    font-size: 12px;\n  }\n  .em-lead {\n    font-size: 22px;\n  }\n  .gallery-item {\n    width: 90px;\n    height: 90px;\n  }\n  #tpsreport {\n    width: 160px;\n    right: 20px;\n  }\n  .btn {\n    padding: 12px 24px;\n    font-size: 15px;\n  }\n}\n"
  },
  {
    "path": "demo/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>annyang! Easily add speech recognition to your site</title>\n    <meta charset=\"utf-8\" />\n    <meta\n      name=\"description\"\n      content=\"annyang is a JavaScript SpeechRecognition library that makes adding voice commands to your site super-easy. Let your users control your site with their voice.\"\n    />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta property=\"og:image\" content=\"https://www.talater.com/annyang/images/icon_speech.png\" />\n    <meta property=\"og:title\" content=\"annyang! Easily add speech recognition to your site\" />\n    <meta property=\"og:url\" content=\"https://www.talater.com/annyang/\" />\n    <meta property=\"og:site_name\" content=\"annyang\" />\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=DM+Sans:wght@900&family=Inter:wght@400;600&family=JetBrains+Mono:wght@400&display=swap\"\n      rel=\"stylesheet\"\n    />\n    <link rel=\"stylesheet\" href=\"css/main.css\" />\n    <!-- Google tag (gtag.js) -->\n    <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-3QP312P56G\"></script>\n    <script>\n      window.dataLayer = window.dataLayer || [];\n      function gtag() {\n        dataLayer.push(arguments);\n      }\n      gtag('js', new Date());\n      gtag('config', 'G-3QP312P56G');\n    </script>\n  </head>\n  <body>\n    <!-- 1. Hero -->\n    <section class=\"hero\">\n      <div class=\"hero-glow\"></div>\n      <svg class=\"hero-waveform\" width=\"560\" height=\"300\" viewBox=\"0 0 560 300\" fill=\"none\">\n        <rect x=\"10\" y=\"95\" width=\"14\" height=\"110\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"40\" y=\"50\" width=\"14\" height=\"200\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"70\" y=\"15\" width=\"14\" height=\"270\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"100\" y=\"55\" width=\"14\" height=\"190\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"130\" y=\"90\" width=\"14\" height=\"120\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"160\" y=\"25\" width=\"14\" height=\"250\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"190\" y=\"0\" width=\"14\" height=\"300\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"220\" y=\"40\" width=\"14\" height=\"220\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"250\" y=\"80\" width=\"14\" height=\"140\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"280\" y=\"10\" width=\"14\" height=\"280\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"310\" y=\"45\" width=\"14\" height=\"210\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"340\" y=\"100\" width=\"14\" height=\"100\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"370\" y=\"35\" width=\"14\" height=\"230\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"400\" y=\"70\" width=\"14\" height=\"160\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"430\" y=\"105\" width=\"14\" height=\"90\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"460\" y=\"60\" width=\"14\" height=\"180\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"490\" y=\"95\" width=\"14\" height=\"110\" rx=\"7\" fill=\"#FF6B35\" />\n        <rect x=\"520\" y=\"120\" width=\"14\" height=\"60\" rx=\"7\" fill=\"#FF6B35\" />\n      </svg>\n      <p class=\"section-label\">Speech Recognition for the Web</p>\n      <h1>annyang!</h1>\n      <p class=\"hero-description\">\n        A tiny JavaScript library that adds voice commands to any project &mdash; websites, home automation,\n        accessibility tools, VR, drones, and more.\n      </p>\n      <div class=\"hero-stats\">\n        <span>2kb</span>\n        <span class=\"hero-stats-dot\">&middot;</span>\n        <span>Zero dependencies</span>\n        <span class=\"hero-stats-dot\">&middot;</span>\n        <span>MIT license</span>\n      </div>\n    </section>\n\n    <!-- 2. Hello -->\n    <section id=\"section_hello\" class=\"alt\">\n      <p class=\"em-lead\">Go ahead, try it&hellip;</p>\n      <p class=\"voice-instruction\">\n        <svg class=\"mic-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n          <rect x=\"9\" y=\"1\" width=\"6\" height=\"12\" rx=\"3\" />\n          <path d=\"M19 10v1a7 7 0 0 1-14 0v-1\" />\n          <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n          <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n        </svg>\n        Say <code>\"Hello!\"</code>\n      </p>\n      <p id=\"hello\" class=\"response-text\">annyang!</p>\n    </section>\n\n    <!-- 3. Image Search -->\n    <section id=\"section_image_search\">\n      <p class=\"em-lead\">Let's try something more interesting&hellip;</p>\n      <p class=\"voice-instruction\">\n        <svg class=\"mic-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n          <rect x=\"9\" y=\"1\" width=\"6\" height=\"12\" rx=\"3\" />\n          <path d=\"M19 10v1a7 7 0 0 1-14 0v-1\" />\n          <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n          <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n        </svg>\n        Say <code>\"Show me cute kittens!\"</code>\n      </p>\n      <p class=\"voice-instruction\">\n        <svg class=\"mic-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n          <rect x=\"9\" y=\"1\" width=\"6\" height=\"12\" rx=\"3\" />\n          <path d=\"M19 10v1a7 7 0 0 1-14 0v-1\" />\n          <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n          <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n        </svg>\n        Say <code>\"Show me Arches National Park!\"</code>\n      </p>\n      <p class=\"voice-instruction\">\n        <svg class=\"mic-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n          <rect x=\"9\" y=\"1\" width=\"6\" height=\"12\" rx=\"3\" />\n          <path d=\"M19 10v1a7 7 0 0 1-14 0v-1\" />\n          <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n          <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n        </svg>\n        Now go wild. Say <code>\"Show me&hellip;\"</code> and make your demands!\n      </p>\n      <div id=\"flickrGallery\" class=\"gallery\"></div>\n      <div id=\"flickrLoader\"><p></p></div>\n    </section>\n\n    <!-- 4. TPS Report -->\n    <section id=\"section_biz_use\" class=\"alt\">\n      <p class=\"em-lead\">That's cool, but in the real world it's not all kittens and hello world.</p>\n      <p class=\"voice-instruction\">\n        <svg class=\"mic-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n          <rect x=\"9\" y=\"1\" width=\"6\" height=\"12\" rx=\"3\" />\n          <path d=\"M19 10v1a7 7 0 0 1-14 0v-1\" />\n          <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n          <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n        </svg>\n        No problem, say <code>\"Show TPS report\"</code>\n      </p>\n      <img src=\"images/tpscover.jpg\" alt=\"TPS Report cover\" id=\"tpsreport\" />\n    </section>\n\n    <!-- 5. Code: Basic -->\n    <section id=\"section_code_sample_1\">\n      <p class=\"section-label\">How did you do that?</p>\n      <h2>Simple. Here's all the code.</h2>\n      <div class=\"code-block\">\n        <pre><span class=\"kw\">import</span> annyang <span class=\"kw\">from</span> <span class=\"str\">'annyang'</span><span class=\"op\">;</span>\n\n<span class=\"kw\">const</span> commands <span class=\"op\">= {</span>\n  <span class=\"str\">'hello'</span><span class=\"op\">:</span> <span class=\"op\">() =></span> <span class=\"fn\">alert</span><span class=\"op\">(</span><span class=\"str\">'Hello!'</span><span class=\"op\">),</span>\n  <span class=\"str\">'show tps report'</span><span class=\"op\">:</span> <span class=\"op\">() =></span> document<span class=\"op\">.</span><span class=\"fn\">getElementById</span><span class=\"op\">(</span><span class=\"str\">'tpsreport'</span><span class=\"op\">).</span><span class=\"fn\">show</span><span class=\"op\">()</span>\n<span class=\"op\">};</span>\n\nannyang<span class=\"op\">.</span><span class=\"fn\">addCommands</span><span class=\"op\">(</span>commands<span class=\"op\">);</span>\nannyang<span class=\"op\">.</span><span class=\"fn\">start</span><span class=\"op\">();</span></pre>\n      </div>\n    </section>\n\n    <!-- 6. Code: Advanced -->\n    <section id=\"section_code_sample_2\" class=\"alt\">\n      <p class=\"section-label\">What about more complicated commands?</p>\n      <p class=\"section-description\">\n        annyang understands commands with <strong>named variables</strong>, <strong>splats</strong>, and\n        <strong>optional words</strong>.\n      </p>\n      <div class=\"code-block\">\n        <pre><span class=\"kw\">const</span> commands <span class=\"op\">= {</span>\n  <span class=\"cm\">// Splats (*) capture multi-word text at the end of a command.</span>\n  <span class=\"cm\">// \"Show me Batman and Robin\" calls showFlickr('Batman and Robin')</span>\n  <span class=\"str\">'show me *query'</span><span class=\"op\">:</span> showFlickr<span class=\"op\">,</span>\n\n  <span class=\"cm\">// Named variables (:name) capture a single word anywhere.</span>\n  <span class=\"cm\">// \"calculate October stats\" calls calculateStats('October')</span>\n  <span class=\"str\">'calculate :month stats'</span><span class=\"op\">:</span> calculateStats<span class=\"op\">,</span>\n\n  <span class=\"cm\">// Optional words are wrapped in parentheses.</span>\n  <span class=\"cm\">// Matches both \"say hello friend\" and \"say hello to my little friend\"</span>\n  <span class=\"str\">'say hello (to my little) friend'</span><span class=\"op\">:</span> greeting\n<span class=\"op\">};</span>\n\nannyang<span class=\"op\">.</span><span class=\"fn\">addCommands</span><span class=\"op\">(</span>commands<span class=\"op\">);</span>\nannyang<span class=\"op\">.</span><span class=\"fn\">start</span><span class=\"op\">();</span></pre>\n      </div>\n    </section>\n\n    <!-- 7. Footer CTA -->\n    <section class=\"footer-cta alt\">\n      <svg\n        class=\"footer-arcs\"\n        width=\"200\"\n        height=\"200\"\n        viewBox=\"0 0 200 200\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <circle cx=\"100\" cy=\"100\" r=\"40\" stroke=\"#F5F2ED\" stroke-width=\"1\" />\n        <circle cx=\"100\" cy=\"100\" r=\"70\" stroke=\"#F5F2ED\" stroke-width=\"0.75\" />\n        <circle cx=\"100\" cy=\"100\" r=\"95\" stroke=\"#F5F2ED\" stroke-width=\"0.5\" />\n      </svg>\n      <svg class=\"footer-wave\" viewBox=\"0 0 1440 80\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path d=\"M0 40 Q180 0 360 40 T720 40 T1080 40 T1440 40\" stroke=\"#F5F2ED\" stroke-width=\"1\" fill=\"none\" />\n      </svg>\n      <h2 class=\"footer-heading\">Ready to get started?</h2>\n      <p class=\"hero-description\">Add voice commands to your site in minutes.</p>\n      <div class=\"btn-group\">\n        <a href=\"https://github.com/TalAter/annyang\" class=\"btn btn-primary\">View on GitHub</a>\n        <a href=\"https://github.com/TalAter/annyang/blob/master/docs/README.md\" class=\"btn btn-secondary\"\n          >API Documentation</a\n        >\n      </div>\n    </section>\n\n    <!-- 9. Copyright -->\n    <div class=\"copyright\">\n      <p>\n        &copy; 2026 <a href=\"https://www.talater.com\">Tal Ater</a> &middot; Free to use under the\n        <a href=\"https://raw.github.com/TalAter/annyang/master/LICENSE\">MIT license</a>\n      </p>\n      <p>\n        Tal Ater retains creative control, spin-off rights and theme park approval for Mr. Banana Grabber, Baby Banana\n        Grabber, and any other Banana Grabber family character that might emanate there from.\n      </p>\n    </div>\n\n    <!-- Unsupported browser banner -->\n    <div id=\"unsupported\">\n      <h4>It looks like your browser doesn't support speech recognition.</h4>\n      <p>annyang works with all browsers, progressively enhancing those that support the SpeechRecognition standard.</p>\n      <p>\n        annyang commands can even be triggered manually in unsupported browsers (e.g., &ldquo;<a\n          href=\"#\"\n          id=\"trigger-demo\"\n          >Show me snowboarding</a\n        >&rdquo;)\n      </p>\n      <p>\n        Please visit <a href=\"https://www.talater.com/annyang/\">talater.com/annyang</a> in a supported browser like\n        Chrome.\n      </p>\n    </div>\n\n    <script type=\"module\">\n      import annyang from './annyang.js';\n\n      const hello = () => {\n        const el = document.getElementById('hello');\n        el.classList.add('visible');\n        el.scrollIntoView({ behavior: 'smooth', block: 'center' });\n      };\n\n      const showFlickr = tag => {\n        const gallery = document.getElementById('flickrGallery');\n        const loader = document.getElementById('flickrLoader');\n\n        loader.querySelector('p').textContent = 'Searching for ' + tag;\n        loader.classList.add('visible');\n\n        const url =\n          'https://api.flickr.com/services/rest/?method=flickr.photos.search' +\n          '&api_key=a828a6571bb4f0ff8890f7a386d61975' +\n          '&sort=interestingness-desc&per_page=6&format=json&nojsoncallback=1' +\n          '&extras=url_q&media=photos' +\n          '&tags=' +\n          encodeURIComponent(tag);\n\n        fetch(url)\n          .then(r => r.json())\n          .then(data => {\n            loader.classList.remove('visible');\n            const photos = data.photos.photo;\n            photos.forEach(photo => {\n              if (!photo.url_q || photo.server === '31337' || photo.server === '0') return;\n              const div = document.createElement('div');\n              div.className = 'gallery-item';\n              const img = document.createElement('img');\n              img.src = photo.url_q;\n              img.alt = photo.title;\n              img.onerror = () => div.remove();\n              div.appendChild(img);\n              gallery.appendChild(div);\n            });\n          });\n\n        document.getElementById('section_image_search').scrollIntoView({ behavior: 'smooth' });\n      };\n\n      const showTPS = () => {\n        const tps = document.getElementById('tpsreport');\n        tps.classList.add('visible');\n        setTimeout(() => tps.classList.remove('visible'), 3000);\n      };\n\n      const getStarted = () => {\n        window.location.href = 'https://github.com/TalAter/annyang';\n      };\n\n      const commands = {\n        'hello (there)': hello,\n        'show me *tag': showFlickr,\n        'show :type report': showTPS,\n        \"let's get started\": getStarted,\n      };\n\n      annyang.debug();\n      annyang.addCommands(commands);\n      annyang.setLanguage('en');\n\n      if (annyang.isSpeechRecognitionSupported()) {\n        annyang.start();\n      } else {\n        document.getElementById('unsupported').classList.add('visible');\n        document.getElementById('trigger-demo').addEventListener('click', e => {\n          e.preventDefault();\n          annyang.trigger('show me snowboarding');\n        });\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "docs/FAQ.md",
    "content": "# Frequently Asked Questions\n\n- [What languages are supported?](#what-languages-are-supported)\n- [Why does the browser repeatedly ask for permission to use the microphone?](#why-does-the-browser-repeatedly-ask-for-permission-to-use-the-microphone)\n- [What can I do to make speech recognition results return faster?](#what-can-i-do-to-make-speech-recognition-results-return-faster)\n- [How can I contribute to annyang's development?](#how-can-i-contribute-to-annyangs-development)\n- [Why does Speech Recognition repeatedly starts and stops?](#why-does-speech-recognition-repeatedly-starts-and-stops)\n- [Can annyang work offline?](#can-annyang-work-offline)\n- [Which browsers are supported?](#which-browsers-are-supported)\n- [How does annyang work with and without speech recognition?](#how-does-annyang-work-with-and-without-speech-recognition)\n- [Can annyang be used to capture the full text spoken by the user?](#can-annyang-be-used-to-capture-the-full-text-spoken-by-the-user)\n- [Can I detect when the user starts and stops speaking?](#can-i-detect-when-the-user-starts-and-stops-speaking)\n- [Can annyang be used in Chromium or Electron?](#can-annyang-be-used-in-chromium-or-electron)\n- [Can annyang be used in Cordova?](#can-annyang-be-used-in-cordova)\n\n## What languages are supported?\n\nLanguage support is up to each browser. While there isn't an official list of supported languages in Chrome, here is a list based on [anecdotal evidence](http://stackoverflow.com/a/14302134/338039).\n\n- Afrikaans `af`\n- Basque `eu`\n- Bulgarian `bg`\n- Catalan `ca`\n- Arabic (Egypt) `ar-EG`\n- Arabic (Jordan) `ar-JO`\n- Arabic (Kuwait) `ar-KW`\n- Arabic (Lebanon) `ar-LB`\n- Arabic (Qatar) `ar-QA`\n- Arabic (UAE) `ar-AE`\n- Arabic (Morocco) `ar-MA`\n- Arabic (Iraq) `ar-IQ`\n- Arabic (Algeria) `ar-DZ`\n- Arabic (Bahrain) `ar-BH`\n- Arabic (Lybia) `ar-LY`\n- Arabic (Oman) `ar-OM`\n- Arabic (Saudi Arabia) `ar-SA`\n- Arabic (Tunisia) `ar-TN`\n- Arabic (Yemen) `ar-YE`\n- Czech `cs`\n- Dutch `nl-NL`\n- English (Australia) `en-AU`\n- English (Canada) `en-CA`\n- English (India) `en-IN`\n- English (New Zealand) `en-NZ`\n- English (South Africa) `en-ZA`\n- English(UK) `en-GB`\n- English(US) `en-US`\n- Finnish `fi`\n- French `fr-FR`\n- Galician `gl`\n- German `de-DE`\n- Greek `el-GR`\n- Hebrew `he`\n- Hungarian `hu`\n- Icelandic `is`\n- Italian `it-IT`\n- Indonesian `id`\n- Japanese `ja`\n- Korean `ko`\n- Latin `la`\n- Mandarin Chinese `zh-CN`\n- Traditional Taiwan `zh-TW`\n- Simplified China zh-CN `?`\n- Simplified Hong Kong `zh-HK`\n- Yue Chinese (Traditional Hong Kong) `zh-yue`\n- Malaysian `ms-MY`\n- Norwegian `no-NO`\n- Polish `pl`\n- Pig Latin `xx-piglatin`\n- Portuguese `pt-PT`\n- Portuguese (Brasil) `pt-br`\n- Romanian `ro-RO`\n- Russian `ru`\n- Serbian `sr-SP`\n- Slovak `sk`\n- Spanish (Argentina) `es-AR`\n- Spanish (Bolivia) `es-BO`\n- Spanish (Chile) `es-CL`\n- Spanish (Colombia) `es-CO`\n- Spanish (Costa Rica) `es-CR`\n- Spanish (Dominican Republic) `es-DO`\n- Spanish (Ecuador) `es-EC`\n- Spanish (El Salvador) `es-SV`\n- Spanish (Guatemala) `es-GT`\n- Spanish (Honduras) `es-HN`\n- Spanish (Mexico) `es-MX`\n- Spanish (Nicaragua) `es-NI`\n- Spanish (Panama) `es-PA`\n- Spanish (Paraguay) `es-PY`\n- Spanish (Peru) `es-PE`\n- Spanish (Puerto Rico) `es-PR`\n- Spanish (Spain) `es-ES`\n- Spanish (US) `es-US`\n- Spanish (Uruguay) `es-UY`\n- Spanish (Venezuela) `es-VE`\n- Swedish `sv-SE`\n- Turkish `tr`\n- Zulu `zu`\n\n## Why does the browser repeatedly ask for permission to use the microphone?\n\nChrome's speech recognition behaves differently based on the protocol used:\n\n- `https://` Asks for permission once and remembers the choice.\n\n- `http://` Asks for permission repeatedly **on every page load**. Results are also returned significantly slower in HTTP.\n\nFor a great user experience, don't compromise on anything less than HTTPS.\n\n## What can I do to make speech recognition results return faster?\n\nFirst, remember that because the actual speech-to-text processing is done in the cloud, a faster connection can mean faster results.\n\nSecond, when the speech recognition is in continuous mode, results are returned slower (the browser waits after you finish talking to see if there's anything else you'd like to add).\n\nTurning continuous mode off tends to make the browser return recognized results much faster.\n\nTo start annyang in non-continuous mode, you can pass `continuous: false` in the options object that `annyang.start()` accepts. You will most likely want to also turn on `autoRestart` if you do that. You can read more about both options in the [annyang API Docs](https://github.com/TalAter/annyang/blob/master/docs/README.md#startoptions)\n\nFor example:\n\n```javascript\nannyang.start({ autoRestart: true, continuous: false });\n```\n\nNote that these settings are already the default if you are using HTTPS. If you are using HTTP, continuous mode will be turned on by default (resulting in slower recognition) to prevent [repeated security notices](#why-does-the-browser-repeatedly-ask-for-permission-to-use-the-microphone).\n\n## How can I contribute to annyang's development?\n\nThere are three main ways for you to help. Check out the [CONTRIBUTING](https://github.com/TalAter/annyang/blob/master/CONTRIBUTING.md) guide for more details.\n\n## Why does Speech Recognition repeatedly starts and stops?\n\nThe most common reason for this is because you have opened more than one tab or window that uses Speech Recognition in your browser at the same time (e.g. if you open annyang's homepage in one tab, and the Speech Recognition app you are developing in another).\n\nWhen a browser detects that one tab has started Speech Recognition, it aborts all other Speech Recognition processes in other tabs. annyang detects when it is aborted by an external process and restarts itself. If you have two windows aborting each other, and restarting themselves you may experience Speech Recognition starting and stopping over and over again.\n\nAnother possible reason for this might be that you are offline.\n\n## Can annyang work offline?\n\nNo. annyang relies on the browser's own speech recognition engine. In Chrome, this engine performs recognition in the cloud.\n\n## Which browsers are supported?\n\nannyang works with all browsers that implement the Speech Recognition interface of the Web Speech API (such as Google Chrome, and Samsung Internet).\n\nTo check if the user's browser supports speech recognition, use `isSpeechRecognitionSupported()`:\n\n```javascript\nif (!annyang.isSpeechRecognitionSupported()) {\n  console.log('Speech Recognition is not supported');\n}\n```\n\nYou can find out the current state of browser support on [caniuse.com](https://caniuse.com/speech-recognition).\n\nEven in unsupported browsers, annyang is safe to use. You can register commands and trigger them programmatically with `trigger()`, which works independently of the speech recognition engine:\n\n```javascript\nannyang.addCommands({\n  'show help': () => showHelpOverlay(),\n  'go to :page': page => navigateTo(page),\n});\n\nif (annyang.isSpeechRecognitionSupported()) {\n  annyang.start(); // Voice input triggers commands\n} else {\n  // Provide an alternative input method\n  goButton.addEventListener('click', () => {\n    annyang.trigger('go to ' + pageInput.value);\n  });\n}\n```\n\n## How does annyang work with and without speech recognition?\n\n`isListening()`, `getState()`, and the `start`/`end`/`soundstart`/`error*` callbacks reflect the state of the browser's speech recognition engine — whether the microphone is active, paused, or off. You can use these to drive UI that shows the user whether the browser is listening. In unsupported browsers, these are safe to call but will always report that the engine is inactive (`isListening()` returns `false`, `getState()` returns `'idle'`).\n\n`trigger()` allows manual invocation of commands regardless of speech recognition support. The same commands registered with `addCommands()` can be matched either through speech recognition in a supported browser or programmatically via `trigger()`. This means you can use `trigger()` to provide a fallback input in unsupported browsers, or to invoke commands from your own code alongside speech recognition in supported ones. `trigger()` does not depend on the listening state — it works whether annyang is listening, paused, aborted, or was never started.\n\n## Can annyang be used to capture the full text spoken by the user?\n\nYes. You can listen to the `result` event which is triggered whenever speech is recognized. This event will fire with a list of possible phrases the user may have said, regardless of whether any of them matched an annyang command or not. You can even do this without registering any commands:\n\n```javascript\nannyang.addCallback('result', function (phrases) {\n  console.log('I think the user said: ', phrases[0]);\n  console.log('But then again, it could be any of the following: ', phrases);\n});\n```\n\nAlternatively, you may choose to only capture what the user said when it matches an annyang command (`resultMatch`), or when it does not match a command (`resultNoMatch`).\n\n```javascript\nannyang.addCallback('resultMatch', function (userSaid, commandText, phrases) {\n  console.log(userSaid); // sample output: 'hello'\n  console.log(commandText); // sample output: 'hello (there)'\n  console.log(phrases); // sample output: ['hello', 'halo', 'yellow', 'polo', 'hello kitty']\n});\n\nannyang.addCallback('resultNoMatch', function (phrases) {\n  console.log('I think the user said: ', phrases[0]);\n  console.log('But then again, it could be any of the following: ', phrases);\n});\n```\n\n## Can I detect when the user starts and stops speaking?\n\nYes. Sometimes.\n\nYou can detect when a sound is first detected by the microphone with the `soundstart` event. Unfortunately, due to a [bug in Chrome](https://bugs.chromium.org/p/chromium/issues/detail?id=572697&thanks=572697&ts=1451323087), this event will only fire once in every speech recognition session. If you are in non-continuous mode and annyang is restarting after every sentence recognized (the default in HTTPS), this will not be a problem. Because speech recognition will abort and restart, soundstart will fire again correctly.\n\nThe following code will detect when a user starts and stops speaking.\n\n```javascript\nannyang.addCallback('soundstart', function () {\n  console.log('sound detected');\n});\n\nannyang.addCallback('result', function () {\n  console.log('sound stopped');\n});\n```\n\n## Can annyang be used in Chromium or Electron?\n\nYes, however, you must create your own Chromium keys and are limited to 50 requests/day. To do this you'll need to provide your own keys at runtime by following the instructions for [Acquiring Keys](https://www.chromium.org/developers/how-tos/api-keys) in the Chromium developer docs.\n\n## Can annyang be used in Cordova?\n\nCrosswalk (the Chromium-based WebView for Cordova) has been discontinued. If your Cordova WebView supports the Web Speech API, annyang will work. Otherwise, consider using `trigger()` to invoke commands programmatically from a native speech recognition plugin.\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Quick Tutorial, Intro, and Demos\n\nThe quickest way to get started is to visit the [annyang homepage](https://www.talater.com/annyang/).\n\nFor a more in-depth look at annyang, read on.\n\n# API Reference\n**annyang**\n\n***\n\n# annyang\n\n## Functions\n\n### abort()\n\n> **abort**(): `void`\n\nDefined in: [annyang.ts:369](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L369)\n\nStop listening and turn off the mic.\n\nAlternatively, to only temporarily pause annyang responding to commands without stopping the SpeechRecognition engine or closing the mic, use pause() instead.\n\n#### Returns\n\n`void`\n\n#### See\n\n[pause()](#pause)\n\n***\n\n### addCallback()\n\n> **addCallback**\\<`T`\\>(`type`, `callback`, `context?`): () => `void`\n\nDefined in: [annyang.ts:457](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L457)\n\nAdd a callback function to be called in case one of the following events happens:\n\n* `start` - Fired as soon as the browser's Speech Recognition engine starts listening.\n\n* `soundstart` - Fired as soon as any sound (possibly speech) has been detected.\n\n    This will fire once per Speech Recognition starting. See https://is.gd/annyang_sound_start.\n\n* `error` - Fired when the browser's Speech Recognition engine returns an error, this generic error callback will be followed by more accurate error callbacks (both will fire if both are defined).\n\n    The Callback function will be called with the error event as the first argument.\n\n* `errorNetwork` - Fired when Speech Recognition fails because of a network error.\n\n    The Callback function will be called with the error event as the first argument.\n\n* `errorPermissionBlocked` - Fired when the browser blocks the permission request to use Speech Recognition.\n\n    The Callback function will be called with the error event as the first argument.\n\n* `errorPermissionDenied` - Fired when the user blocks the permission request to use Speech Recognition.\n\n    The Callback function will be called with the error event as the first argument.\n\n* `end` - Fired when the browser's Speech Recognition engine stops.\n\n* `result` - Fired as soon as some speech was identified. This generic callback will be followed by either the `resultMatch` or `resultNoMatch` callbacks.\n\n    The Callback functions for this event will be called with an array of possible phrases the user said as the first argument.\n\n* `resultMatch` - Fired when annyang was able to match between what the user said and a registered command.\n\n    The Callback functions for this event will be called with three arguments in the following order:\n\n    * The phrase the user said that matched a command.\n    * The command that was matched.\n    * An array of possible alternative phrases the user might have said.\n\n* `resultNoMatch` - Fired when what the user said didn't match any of the registered commands.\n\n    Callback functions for this event will be called with an array of possible phrases the user might have said as the first argument.\n\n#### Examples:\n````javascript\nannyang.addCallback('resultMatch', (userSaid, commandText, phrases) => {\n  console.log(userSaid); // sample output: 'hello'\n  console.log(commandText); // sample output: 'hello (there)'\n  console.log(phrases); // sample output: ['hello', 'halo', 'yellow', 'polo', 'hello kitty']\n});\n\n// Returns an unsubscribe function\nconst unsubscribe = annyang.addCallback('error', () => {\n  console.log('There was an error!');\n});\nunsubscribe(); // removes the callback\n````\n\n#### Type Parameters\n\n##### T\n\n`T` *extends* keyof `CallbackMap`\n\n#### Parameters\n\n##### type\n\n`T`\n\nName of event that will trigger this callback\n\n##### callback\n\n`CallbackMap`\\[`T`\\]\n\nThe function to call when event is triggered\n\n##### context?\n\n`object` = `undefined`\n\nOptional context for the callback function\n\n#### Returns\n\nA function that removes this callback when called\n\n> (): `void`\n\n##### Returns\n\n`void`\n\n***\n\n### addCommands()\n\n> **addCommands**(`commands`, `resetCommands?`): `void`\n\nDefined in: [annyang.ts:265](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L265)\n\nAdd commands that annyang will respond to.\nBy default this will add to the existing commands. Pass `true` as the second parameter to remove all existing commands first.\n\n#### Examples:\n````javascript\nconst commands1 = {'hello :name': helloFunction, 'howdy': helloFunction};\nconst commands2 = {'hi': helloFunction};\n\nannyang.addCommands(commands1);\nannyang.addCommands(commands2);\n// annyang will now listen for all three commands defined in commands1 and commands2\n\nannyang.addCommands(commands2, true);\n// annyang will now only listen for the command in commands2\n````\n\n#### Parameters\n\n##### commands\n\n`CommandsList`\n\nCommands that annyang should listen for\n\n##### resetCommands?\n\n`boolean` = `false`\n\nRemove all existing commands before adding new commands? *\n\n#### Returns\n\n`void`\n\n#### See\n\n[Commands Object](#commands-object)\n\n***\n\n### debug()\n\n> **debug**(`newState?`): `void`\n\nDefined in: [annyang.ts:569](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L569)\n\nTurn on the output of debug messages to the console.\n\n#### Parameters\n\n##### newState?\n\n`boolean` = `true`\n\nTurn debug messages on or off\n\n#### Returns\n\n`void`\n\n***\n\n### getSpeechRecognizer()\n\n> **getSpeechRecognizer**(): `SpeechRecognition` \\| `undefined`\n\nDefined in: [annyang.ts:601](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L601)\n\nReturns the instance of the browser's SpeechRecognition object used by annyang.\nUseful in case you want direct access to the browser's Speech Recognition engine.\n\n#### Returns\n\n`SpeechRecognition` \\| `undefined`\n\nSpeechRecognition The browser's Speech Recognizer instance currently used by annyang\n\n***\n\n### getState()\n\n> **getState**(): `AnnyangState`\n\nDefined in: [annyang.ts:544](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L544)\n\nReturns the current state of annyang.\n\n#### Returns\n\n`AnnyangState`\n\nThe current state\n\n***\n\n### ~~init()~~\n\n> **init**(): `void`\n\nDefined in: [annyang.ts:608](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L608)\n\n#### Returns\n\n`void`\n\n#### Deprecated\n\nannyang no longer requires manual initialization. It initializes automatically on `start()` or `addCommands()`. Remove any calls to `init()`.\n\n***\n\n### isListening()\n\n> **isListening**(): `boolean`\n\nDefined in: [annyang.ts:533](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L533)\n\nReturns true if speech recognition is currently on.\nReturns false if speech recognition is off or annyang is paused.\n\n#### Returns\n\n`boolean`\n\ntrue if SpeechRecognition is on and annyang is not paused\n\n***\n\n### isSpeechRecognitionSupported()\n\n> **isSpeechRecognitionSupported**(): `boolean`\n\nDefined in: [annyang.ts:232](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L232)\n\nIs SpeechRecognition supported in this environment?\n\n#### Returns\n\n`boolean`\n\ntrue if SpeechRecognition is supported by the browser\n\n***\n\n### pause()\n\n> **pause**(): `void`\n\nDefined in: [annyang.ts:383](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L383)\n\nPause listening. annyang will stop responding to commands (until the resume or start methods are called), without turning off the browser's SpeechRecognition engine or the mic.\n\nAlternatively, to stop the SpeechRecognition engine and close the mic, use abort() instead.\n\n#### Returns\n\n`void`\n\n#### See\n\n[abort()](#abort)\n\n***\n\n### removeCallback()\n\n> **removeCallback**(`type?`, `callback?`): `void`\n\nDefined in: [annyang.ts:512](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L512)\n\nRemove callbacks from events.\n\n- Pass an event name and a callback command to remove that callback command from that event type.\n- Pass just an event name to remove all callback commands from that event type.\n- Pass undefined as event name and a callback command to remove that callback command from all event types.\n- Pass no params to remove all callback commands from all event types.\n\n#### Examples:\n````javascript\nannyang.addCallback('start', myFunction1);\nannyang.addCallback('start', myFunction2);\nannyang.addCallback('end', myFunction1);\nannyang.addCallback('end', myFunction2);\n\n// Remove all callbacks from all events:\nannyang.removeCallback();\n\n// Remove all callbacks attached to end event:\nannyang.removeCallback('end');\n\n// Remove myFunction2 from being called on start:\nannyang.removeCallback('start', myFunction2);\n\n// Remove myFunction1 from being called on all events:\nannyang.removeCallback(undefined, myFunction1);\n````\n\n#### Parameters\n\n##### type?\n\nkeyof CallbackMap\n\nName of event type to remove callback from\n\n##### callback?\n\nThe callback function to remove\n\n() => `void` | () => `void` | () => `void` | (`phrases`) => `void` | (`userSaid`, `commandText`, `phrases`) => `void` | (`phrases`) => `void` | (`event`) => `void` | (`event`) => `void` | (`event`) => `void` | (`event`) => `void`\n\n#### Returns\n\n`void`\n\nundefined\n\n***\n\n### removeCommands()\n\n> **removeCommands**(`commandsToRemove?`): `void`\n\nDefined in: [annyang.ts:306](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L306)\n\nRemove existing commands. Called with a single phrase, an array of phrases, or with no params to remove all commands.\n\n#### Examples:\n````javascript\nconst commands = {'hello': helloFunction, 'howdy': helloFunction, 'hi': helloFunction};\n\n// Remove all existing commands\nannyang.removeCommands();\n\n// Add some commands\nannyang.addCommands(commands);\n\n// Don't respond to hello\nannyang.removeCommands('hello');\n\n// Don't respond to howdy or hi\nannyang.removeCommands(['howdy', 'hi']);\n````\n\n#### Parameters\n\n##### commandsToRemove?\n\nCommands to remove\n\n`string` | `string`[]\n\n#### Returns\n\n`void`\n\n***\n\n### resume()\n\n> **resume**(): `void`\n\nDefined in: [annyang.ts:391](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L391)\n\nResumes listening and restore command callback execution when a command is matched.\nIf SpeechRecognition was aborted (stopped), start it.\n\n#### Returns\n\n`void`\n\n***\n\n### setLanguage()\n\n> **setLanguage**(`language`): `void`\n\nDefined in: [annyang.ts:556](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L556)\n\nSet the language the user will speak in. If this method is not called, annyang defaults to 'en-US'.\n\n#### Parameters\n\n##### language\n\n`string`\n\nThe language (locale)\n\n#### Returns\n\n`void`\n\n#### See\n\n[Languages](https://github.com/TalAter/annyang/blob/master/docs/FAQ.md#what-languages-are-supported)\n\n***\n\n### start()\n\n> **start**(`options?`): `void`\n\nDefined in: [annyang.ts:340](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L340)\n\nStart listening.\nIt's a good idea to call this after adding some commands first (but not mandatory)\n\nReceives an optional options object which supports the following options:\n\n- `autoRestart`  (boolean) Should annyang restart itself if it is closed indirectly (e.g. because of silence or window conflicts)?\n- `continuous`   (boolean) Allow forcing continuous mode on or off. annyang is pretty smart about this, so only set this if you know what you're doing.\n- `paused`       (boolean) Start annyang in paused mode.\n\n#### Examples:\n````javascript\n// Start listening, don't restart automatically\nannyang.start({ autoRestart: false });\n// Start listening, don't restart automatically, stop recognition after first phrase recognized\nannyang.start({ autoRestart: false, continuous: false });\n````\n\n#### Parameters\n\n##### options?\n\n`StartOptions` = `{}`\n\nOptional options.\n\n#### Returns\n\n`void`\n\n***\n\n### trigger()\n\n> **trigger**(`sentences?`): `void`\n\nDefined in: [annyang.ts:591](https://github.com/TalAter/annyang/blob/17be9d5c272f8beb449c5bb269e947c996e5adff/src/annyang.ts#L591)\n\nMatch text against registered commands and fire the corresponding callbacks.\nWorks independently of the speech recognition engine — does not require `start()`, and works even in\nenvironments where SpeechRecognition is not supported.\n\nCan accept either a string containing a single sentence or an array containing multiple sentences to be checked\nin order until one of them matches a command (similar to the way Speech Recognition Alternatives are parsed)\n\n#### Examples:\n````javascript\nannyang.trigger('Time for some thrilling heroics');\nannyang.trigger(\n    ['Time for some thrilling heroics', 'Time for some thrilling aerobics']\n  );\n````\n\n#### Parameters\n\n##### sentences?\n\nA sentence as a string or an array of strings of possible sentences\n\n`string` | `string`[]\n\n#### Returns\n\n`void`\n\n# Good to Know\n\n## Commands Object\n\nannyang understands commands with `named variables`, `splats`, and `optional words`.\n\n- Use `named variables` for one-word arguments in your command.\n- Use `splats` to capture multi-word text at the end of your command (greedy).\n- Use `optional words` or phrases to define a part of the command as optional.\n\n#### Examples:\n````html\n<script>\nconst commands = {\n  // annyang will capture anything after a splat (*) and pass it to the function.\n  // For example saying \"Show me Batman and Robin\" will call showFlickr('Batman and Robin');\n  'show me *tag': showFlickr,\n\n  // A named variable is a one-word variable, that can fit anywhere in your command.\n  // For example saying \"calculate October stats\" will call calculateStats('October');\n  'calculate :month stats': calculateStats,\n\n  // By defining a part of the following command as optional, annyang will respond\n  // to both: \"say hello to my little friend\" as well as \"say hello friend\"\n  'say hello (to my little) friend': greeting\n};\n\nconst showFlickr = tag => {\n  const url = 'http://api.flickr.com/services/rest/?tags='+tag;\n  $.getJSON(url);\n}\n\nconst calculateStats = month => {\n  $('#stats').text('Statistics for '+month);\n}\n\nconst greeting = () => {\n  $('#greeting').text('Hello!');\n}\n</script>\n````\n\n### Using Regular Expressions in commands\nFor advanced commands, you can pass a regular expression object, instead of\na simple string command.\n\nThis is done by passing an object containing two properties: `regexp`, and\n`callback` instead of the function.\n\n#### Examples:\n````javascript\nconst calculateFunction = month => { console.log(month); }\nconst commands = {\n  // This example will accept any word as the \"month\"\n  'calculate :month stats': calculateFunction,\n  // This example will only accept months which are at the start of a quarter\n  'calculate :quarter stats': {'regexp': /^calculate (January|April|July|October) stats$/, 'callback': calculateFunction}\n}\n````\n"
  },
  {
    "path": "docs/api-footer.md",
    "content": "\n# Good to Know\n\n## Commands Object\n\nannyang understands commands with `named variables`, `splats`, and `optional words`.\n\n- Use `named variables` for one-word arguments in your command.\n- Use `splats` to capture multi-word text at the end of your command (greedy).\n- Use `optional words` or phrases to define a part of the command as optional.\n\n#### Examples:\n````html\n<script>\nconst commands = {\n  // annyang will capture anything after a splat (*) and pass it to the function.\n  // For example saying \"Show me Batman and Robin\" will call showFlickr('Batman and Robin');\n  'show me *tag': showFlickr,\n\n  // A named variable is a one-word variable, that can fit anywhere in your command.\n  // For example saying \"calculate October stats\" will call calculateStats('October');\n  'calculate :month stats': calculateStats,\n\n  // By defining a part of the following command as optional, annyang will respond\n  // to both: \"say hello to my little friend\" as well as \"say hello friend\"\n  'say hello (to my little) friend': greeting\n};\n\nconst showFlickr = tag => {\n  const url = 'http://api.flickr.com/services/rest/?tags='+tag;\n  $.getJSON(url);\n}\n\nconst calculateStats = month => {\n  $('#stats').text('Statistics for '+month);\n}\n\nconst greeting = () => {\n  $('#greeting').text('Hello!');\n}\n</script>\n````\n\n### Using Regular Expressions in commands\nFor advanced commands, you can pass a regular expression object, instead of\na simple string command.\n\nThis is done by passing an object containing two properties: `regexp`, and\n`callback` instead of the function.\n\n#### Examples:\n````javascript\nconst calculateFunction = month => { console.log(month); }\nconst commands = {\n  // This example will accept any word as the \"month\"\n  'calculate :month stats': calculateFunction,\n  // This example will only accept months which are at the start of a quarter\n  'calculate :quarter stats': {'regexp': /^calculate (January|April|July|October) stats$/, 'callback': calculateFunction}\n}\n````\n"
  },
  {
    "path": "docs/api-intro.md",
    "content": "# Quick Tutorial, Intro, and Demos\n\nThe quickest way to get started is to visit the [annyang homepage](https://www.talater.com/annyang/).\n\nFor a more in-depth look at annyang, read on.\n\n# API Reference\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import tseslint from 'typescript-eslint';\nimport eslintConfigPrettier from 'eslint-config-prettier';\n\nexport default tseslint.config(\n  {\n    ignores: ['dist/', 'node_modules/'],\n  },\n  ...tseslint.configs.recommended,\n  eslintConfigPrettier,\n  {\n    languageOptions: {\n      parserOptions: {\n        ecmaVersion: 2020,\n        sourceType: 'module',\n        projectService: true,\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n    rules: {\n      'no-console': 'off',\n      'max-len': ['error', { code: 120, ignoreComments: true }],\n    },\n  },\n  {\n    files: ['test/**/*.test.ts', 'test/**/*.js'],\n    languageOptions: {\n      parserOptions: {\n        projectService: false,\n      },\n    },\n    rules: {\n      'max-len': 'off',\n    },\n  },\n  {\n    files: ['src/annyang.ts'],\n    rules: {\n      'no-use-before-define': 'off',\n    },\n  },\n);\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"annyang\",\n  \"version\": \"3.0.0\",\n  \"description\": \"A JavaScript library for adding voice commands to your site, using speech recognition\",\n  \"keywords\": [\"speech\", \"recognition\", \"voice\", \"commands\", \"speechrecognition\"],\n  \"homepage\": \"https://www.talater.com/annyang/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/TalAter/annyang/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/TalAter/annyang.git\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"Tal Ater <tal@talater.com> (https://www.talater.com/)\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/annyang.d.ts\",\n        \"default\": \"./dist/annyang.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/annyang.d.cts\",\n        \"default\": \"./dist/annyang.cjs\"\n      }\n    }\n  },\n  \"main\": \"./dist/annyang.cjs\",\n  \"module\": \"./dist/annyang.js\",\n  \"types\": \"./dist/annyang.d.ts\",\n  \"files\": [\"dist\"],\n  \"sideEffects\": false,\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"lint\": \"eslint src test\",\n    \"format\": \"prettier --write src test package.json\",\n    \"format:check\": \"prettier --check src test package.json\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"docs\": \"typedoc && cat docs/api-intro.md docs/README.md docs/api-footer.md > docs/README.tmp && mv docs/README.tmp docs/README.md\",\n    \"test:manual\": \"pnpm build && cp dist/annyang.iife.min.js test-manual/ && npx esbuild test-manual/esm-app.js --bundle --alias:annyang=./dist/annyang.js --outfile=test-manual/dist/esm-app.js && npx esbuild test-manual/cjs-app.js --bundle --alias:annyang=./dist/annyang.cjs --outfile=test-manual/dist/cjs-app.js && npx http-server test-manual -p 8081 -c-1 -o\",\n    \"demo\": \"concurrently -n build,serve -c blue,green \\\"tsup --watch\\\" \\\"npx http-server . -p 8080 -c-1\\\"\",\n    \"prepublishOnly\": \"pnpm test && pnpm lint && pnpm typecheck && pnpm build && pnpm docs\"\n  },\n  \"devDependencies\": {\n    \"@types/dom-speech-recognition\": \"^0.0.7\",\n    \"concurrently\": \"^9.2.1\",\n    \"corti\": \"^2.1.0\",\n    \"eslint\": \"^10.0.2\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"prettier\": \"^3.8.1\",\n    \"tsup\": \"^8.5.1\",\n    \"typedoc\": \"^0.28.17\",\n    \"typedoc-plugin-markdown\": \"^4.10.0\",\n    \"typescript\": \"^5.9.3\",\n    \"typescript-eslint\": \"^8.56.1\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"packageManager\": \"pnpm@10.30.3\",\n  \"pnpm\": {\n    \"overrides\": {\n      \"flatted\": \">=3.4.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/annyang.ts",
    "content": "const MIN_RESTART_INTERVAL_MS = 1000;\nconst RESTART_WARNING_INTERVAL = 10;\n\nlet recognition: SpeechRecognition;\nlet listening: boolean = false;\nlet autoRestart: boolean = true;\nlet debugState: boolean = false;\nconst debugStyle: string = 'font-weight: bold; color: #00f;';\n\nexport interface CallbackMap {\n  start: () => void;\n  end: () => void;\n  soundstart: () => void;\n  result: (phrases: string[]) => void;\n  resultMatch: (userSaid: string, commandText: string, phrases: string[]) => void;\n  resultNoMatch: (phrases: string[]) => void;\n  error: (event: SpeechRecognitionErrorEvent) => void;\n  errorNetwork: (event: SpeechRecognitionErrorEvent) => void;\n  errorPermissionBlocked: (event: SpeechRecognitionErrorEvent) => void;\n  errorPermissionDenied: (event: SpeechRecognitionErrorEvent) => void;\n}\n\nexport type CallbackType = keyof CallbackMap;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyFunction = (...args: any[]) => void;\n\ninterface StoredCallback {\n  callback: AnyFunction;\n  context: object | undefined;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst commandsList: Map<string, { command: RegExp; callback: (...args: any[]) => void }> = new Map();\nconst callbacks: Map<CallbackType, StoredCallback[]> = new Map([\n  ['start', []],\n  ['error', []],\n  ['end', []],\n  ['soundstart', []],\n  ['result', []],\n  ['resultMatch', []],\n  ['resultNoMatch', []],\n  ['errorNetwork', []],\n  ['errorPermissionBlocked', []],\n  ['errorPermissionDenied', []],\n]);\nlet lastStartedAt: number = 0;\nlet autoRestartCount: number = 0;\nlet pauseListening: boolean = false;\n\n// The command matching code is a modified version of Backbone.Router by Jeremy Ashkenas, under the MIT license.\nconst optionalParam = /\\s*\\((.*?)\\)\\s*/g;\nconst optionalRegex = /(\\(\\?:[^)]+\\))\\?/g;\nconst namedParam = /(\\(\\?)?:\\w+/g;\nconst splatParam = /\\*\\w+/g;\nconst escapeRegExp = /[-{}[\\]+?.,\\\\^$|#]/g;\nconst commandToRegExp = (command: string) => {\n  const parsedCommand = command\n    .replace(escapeRegExp, '\\\\$&')\n    .replace(optionalParam, '(?:$1)?')\n    .replace(namedParam, (match, optional) => {\n      return optional ? match : '([^\\\\s]+)';\n    })\n    .replace(splatParam, '(.*?)')\n    .replace(optionalRegex, '\\\\s*$1?\\\\s*');\n  return new RegExp(`^${parsedCommand}$`, 'i');\n};\n\n// Get the SpeechRecognition object, accounting for possible browser prefixes\nconst getSpeechRecognition = () => globalThis.SpeechRecognition || globalThis.webkitSpeechRecognition;\n\n// Check if annyang is already initialized\nconst isInitialized = () => {\n  return recognition !== undefined;\n};\n\n// Method for logging to the console when debug mode is on\nconst logMessage = (text: string, extraParameters?: string | string[]) => {\n  if (debugState) {\n    if (text.indexOf('%c') === -1 && !extraParameters) {\n      console.log(text);\n    } else {\n      console.log(text, extraParameters || debugStyle);\n    }\n  }\n};\n\n// Add a command to the commands list\nconst registerCommand = (command: RegExp, callback: AnyFunction, originalPhrase: string) => {\n  commandsList.set(originalPhrase, { command, callback });\n  logMessage(`Command successfully loaded: %c${originalPhrase}`, debugStyle);\n};\n\n// This method receives an array of callbacks and invokes each of them\nconst invokeCallbacks = (callbacksArr: StoredCallback[] = [], ...args: unknown[]) => {\n  callbacksArr.forEach(cb => {\n    cb.callback.apply(cb.context, args);\n  });\n};\n\n// Initialize annyang\nconst init = () => {\n  if (!getSpeechRecognition()) {\n    return;\n  }\n\n  // Abort previous instances of recognition already running\n  if (recognition && recognition.abort) {\n    recognition.abort();\n  }\n\n  // initiate SpeechRecognition\n  recognition = new (getSpeechRecognition())();\n\n  // Set the max number of alternative transcripts to try and match with a command\n  recognition.maxAlternatives = 5;\n\n  // In HTTPS, turn off continuous mode for faster results.\n  // In HTTP,  turn on  continuous mode for much slower results, but no repeating security notices\n  recognition.continuous = globalThis.location.protocol === 'http:';\n\n  // Sets the language to the default 'en-US'. This can be changed with annyang.setLanguage()\n  recognition.lang = 'en-US';\n\n  recognition.onstart = () => {\n    listening = true;\n    invokeCallbacks(callbacks.get('start'));\n  };\n\n  recognition.onsoundstart = () => {\n    invokeCallbacks(callbacks.get('soundstart'));\n  };\n\n  recognition.onerror = event => {\n    invokeCallbacks(callbacks.get('error'), event);\n    switch (event.error) {\n      case 'network':\n        invokeCallbacks(callbacks.get('errorNetwork'), event);\n        break;\n      case 'not-allowed':\n      case 'service-not-allowed':\n        // if permission to use the mic is denied, turn off auto-restart\n        autoRestart = false;\n        // determine if permission was denied by user or automatically.\n        if (new Date().getTime() - lastStartedAt < 200) {\n          invokeCallbacks(callbacks.get('errorPermissionBlocked'), event);\n        } else {\n          invokeCallbacks(callbacks.get('errorPermissionDenied'), event);\n        }\n        break;\n      default:\n        break;\n    }\n  };\n\n  recognition.onend = () => {\n    listening = false;\n    invokeCallbacks(callbacks.get('end'));\n    // annyang will auto restart if it is closed automatically and not by user action.\n    if (autoRestart) {\n      // play nicely with the browser, and never restart annyang automatically more than once per second\n      const timeSinceLastStart = new Date().getTime() - lastStartedAt;\n      autoRestartCount += 1;\n      if (autoRestartCount % RESTART_WARNING_INTERVAL === 0) {\n        logMessage(\n          'Speech Recognition is repeatedly stopping and starting. See http://is.gd/annyang_restarts for tips.'\n        );\n      }\n      if (timeSinceLastStart < MIN_RESTART_INTERVAL_MS) {\n        setTimeout(() => {\n          start({ paused: pauseListening });\n        }, MIN_RESTART_INTERVAL_MS - timeSinceLastStart);\n      } else {\n        start({ paused: pauseListening });\n      }\n    }\n  };\n\n  recognition.onresult = (event: SpeechRecognitionEvent) => {\n    if (pauseListening) {\n      logMessage('Speech heard, but annyang is paused');\n      return;\n    }\n\n    // Map the results to an array\n    const SpeechRecognitionResults = event.results[event.resultIndex];\n    const results = Array.from(SpeechRecognitionResults, result => result.transcript);\n    parseResults(results);\n  };\n};\n\n// If annyang isn't initialized, initialize it\nconst initIfNeeded = () => {\n  if (!isInitialized()) {\n    init();\n  }\n};\n\nconst parseResults = (recognitionResults: string[]) => {\n  invokeCallbacks(callbacks.get('result'), recognitionResults);\n\n  // Log all recognition alternatives for debugging, regardless of match\n  for (const rawText of recognitionResults) {\n    logMessage(`Speech recognized: %c${rawText.trim()}`, debugStyle);\n  }\n\n  // Try to match each alternative to a command\n  for (const rawText of recognitionResults) {\n    const commandText = rawText.trim();\n    for (const [originalPhrase, currentCommand] of commandsList) {\n      const matchedCommand = currentCommand.command.exec(commandText);\n      if (matchedCommand) {\n        const parameters = matchedCommand.slice(1);\n        logMessage(`command matched: %c${originalPhrase}`, debugStyle);\n        if (parameters.length) {\n          logMessage('with parameters', parameters);\n        }\n        currentCommand.callback(...parameters);\n        invokeCallbacks(callbacks.get('resultMatch'), commandText, originalPhrase, recognitionResults);\n        return;\n      }\n    }\n  }\n  invokeCallbacks(callbacks.get('resultNoMatch'), recognitionResults);\n};\n\n/**\n * Is SpeechRecognition supported in this environment?\n *\n * @returns {boolean} true if SpeechRecognition is supported by the browser\n */\nconst isSpeechRecognitionSupported = () => !!getSpeechRecognition();\n\nexport type CommandCallback = (...args: string[]) => void;\n\nexport interface CommandsList {\n  [key: string]:\n    | CommandCallback\n    | {\n        regexp: RegExp;\n        callback: CommandCallback;\n      };\n}\n\n/**\n * Add commands that annyang will respond to.\n * By default this will add to the existing commands. Pass `true` as the second parameter to remove all existing commands first.\n *\n * #### Examples:\n * ````javascript\n * const commands1 = {'hello :name': helloFunction, 'howdy': helloFunction};\n * const commands2 = {'hi': helloFunction};\n *\n * annyang.addCommands(commands1);\n * annyang.addCommands(commands2);\n * // annyang will now listen for all three commands defined in commands1 and commands2\n *\n * annyang.addCommands(commands2, true);\n * // annyang will now only listen for the command in commands2\n * ````\n *\n * @param {Object} commands - Commands that annyang should listen for\n * @param {boolean} [resetCommands=false] - Remove all existing commands before adding new commands? * @see [Commands Object](#commands-object)\n */\nconst addCommands = (commands: CommandsList, resetCommands = false) => {\n  if (resetCommands) {\n    commandsList.clear();\n  }\n\n  for (const phrase of Object.keys(commands)) {\n    const cb = commands[phrase];\n\n    if (typeof cb === 'function') {\n      // convert command to regex then register the command\n      registerCommand(commandToRegExp(phrase), cb, phrase);\n    } else if (typeof cb === 'object' && cb.regexp instanceof RegExp) {\n      // register the command\n      registerCommand(new RegExp(cb.regexp.source, 'i'), cb.callback, phrase);\n    } else {\n      logMessage(`Can not register command: %c${phrase}`, debugStyle);\n    }\n  }\n};\n\n/**\n * Remove existing commands. Called with a single phrase, an array of phrases, or with no params to remove all commands.\n *\n * #### Examples:\n * ````javascript\n * const commands = {'hello': helloFunction, 'howdy': helloFunction, 'hi': helloFunction};\n *\n * // Remove all existing commands\n * annyang.removeCommands();\n *\n * // Add some commands\n * annyang.addCommands(commands);\n *\n * // Don't respond to hello\n * annyang.removeCommands('hello');\n *\n * // Don't respond to howdy or hi\n * annyang.removeCommands(['howdy', 'hi']);\n * ````\n * @param {string|string[]|undefined} [commandsToRemove] - Commands to remove\n */\nconst removeCommands = (commandsToRemove?: string | string[] | undefined) => {\n  if (commandsToRemove === undefined) {\n    commandsList.clear();\n  } else {\n    const commandsToRemoveArray = Array.isArray(commandsToRemove) ? commandsToRemove : [commandsToRemove];\n    commandsToRemoveArray.forEach(command => commandsList.delete(command));\n  }\n};\n\nexport interface StartOptions {\n  autoRestart?: boolean;\n  continuous?: boolean;\n  paused?: boolean;\n}\n\n/**\n * Start listening.\n * It's a good idea to call this after adding some commands first (but not mandatory)\n *\n * Receives an optional options object which supports the following options:\n *\n * - `autoRestart`  (boolean) Should annyang restart itself if it is closed indirectly (e.g. because of silence or window conflicts)?\n * - `continuous`   (boolean) Allow forcing continuous mode on or off. annyang is pretty smart about this, so only set this if you know what you're doing.\n * - `paused`       (boolean) Start annyang in paused mode.\n *\n * #### Examples:\n * ````javascript\n * // Start listening, don't restart automatically\n * annyang.start({ autoRestart: false });\n * // Start listening, don't restart automatically, stop recognition after first phrase recognized\n * annyang.start({ autoRestart: false, continuous: false });\n * ````\n * @param {Object} [options] - Optional options.\n */\nconst start = (options: StartOptions = {}) => {\n  if (!isSpeechRecognitionSupported()) {\n    return;\n  }\n  initIfNeeded();\n  pauseListening = !!options.paused;\n  if (options.autoRestart !== undefined) {\n    autoRestart = !!options.autoRestart;\n  } else {\n    autoRestart = true;\n  }\n  if (options.continuous !== undefined) {\n    recognition.continuous = !!options.continuous;\n  }\n\n  lastStartedAt = new Date().getTime();\n  try {\n    recognition.start();\n  } catch (e: unknown) {\n    logMessage(e instanceof Error ? e.message : String(e));\n  }\n};\n\n/**\n * Stop listening and turn off the mic.\n *\n * Alternatively, to only temporarily pause annyang responding to commands without stopping the SpeechRecognition engine or closing the mic, use pause() instead.\n * @see [pause()](#pause)\n */\nconst abort = () => {\n  autoRestart = false;\n  autoRestartCount = 0;\n  if (isInitialized()) {\n    recognition.abort();\n  }\n};\n\n/**\n * Pause listening. annyang will stop responding to commands (until the resume or start methods are called), without turning off the browser's SpeechRecognition engine or the mic.\n *\n * Alternatively, to stop the SpeechRecognition engine and close the mic, use abort() instead.\n * @see [abort()](#abort)\n */\nconst pause = () => {\n  pauseListening = true;\n};\n\n/**\n * Resumes listening and restore command callback execution when a command is matched.\n * If SpeechRecognition was aborted (stopped), start it.\n */\nconst resume = () => {\n  start();\n};\n\n/**\n * Add a callback function to be called in case one of the following events happens:\n *\n * * `start` - Fired as soon as the browser's Speech Recognition engine starts listening.\n *\n * * `soundstart` - Fired as soon as any sound (possibly speech) has been detected.\n *\n *     This will fire once per Speech Recognition starting. See https://is.gd/annyang_sound_start.\n *\n * * `error` - Fired when the browser's Speech Recognition engine returns an error, this generic error callback will be followed by more accurate error callbacks (both will fire if both are defined).\n *\n *     The Callback function will be called with the error event as the first argument.\n *\n * * `errorNetwork` - Fired when Speech Recognition fails because of a network error.\n *\n *     The Callback function will be called with the error event as the first argument.\n *\n * * `errorPermissionBlocked` - Fired when the browser blocks the permission request to use Speech Recognition.\n *\n *     The Callback function will be called with the error event as the first argument.\n *\n * * `errorPermissionDenied` - Fired when the user blocks the permission request to use Speech Recognition.\n *\n *     The Callback function will be called with the error event as the first argument.\n *\n * * `end` - Fired when the browser's Speech Recognition engine stops.\n *\n * * `result` - Fired as soon as some speech was identified. This generic callback will be followed by either the `resultMatch` or `resultNoMatch` callbacks.\n *\n *     The Callback functions for this event will be called with an array of possible phrases the user said as the first argument.\n *\n * * `resultMatch` - Fired when annyang was able to match between what the user said and a registered command.\n *\n *     The Callback functions for this event will be called with three arguments in the following order:\n *\n *     * The phrase the user said that matched a command.\n *     * The command that was matched.\n *     * An array of possible alternative phrases the user might have said.\n *\n * * `resultNoMatch` - Fired when what the user said didn't match any of the registered commands.\n *\n *     Callback functions for this event will be called with an array of possible phrases the user might have said as the first argument.\n *\n * #### Examples:\n * ````javascript\n * annyang.addCallback('resultMatch', (userSaid, commandText, phrases) => {\n *   console.log(userSaid); // sample output: 'hello'\n *   console.log(commandText); // sample output: 'hello (there)'\n *   console.log(phrases); // sample output: ['hello', 'halo', 'yellow', 'polo', 'hello kitty']\n * });\n *\n * // Returns an unsubscribe function\n * const unsubscribe = annyang.addCallback('error', () => {\n *   console.log('There was an error!');\n * });\n * unsubscribe(); // removes the callback\n * ````\n * @param {string} type - Name of event that will trigger this callback\n * @param {function} callback - The function to call when event is triggered\n * @param {Object} [context] - Optional context for the callback function\n * @returns {function} A function that removes this callback when called\n */\nconst addCallback = <T extends CallbackType>(\n  type: T,\n  callback: CallbackMap[T],\n  context: object | undefined = undefined\n): (() => void) => {\n  const callbacksOfType = callbacks.get(type);\n  if (typeof callback === 'function' && callbacksOfType) {\n    const entry: StoredCallback = {\n      callback: callback as AnyFunction,\n      context,\n    };\n    callbacksOfType.push(entry);\n    return () => {\n      const arr = callbacks.get(type);\n      if (arr) {\n        const idx = arr.indexOf(entry);\n        if (idx !== -1) arr.splice(idx, 1);\n      }\n    };\n  }\n  return () => {};\n};\n\n/**\n * Remove callbacks from events.\n *\n * - Pass an event name and a callback command to remove that callback command from that event type.\n * - Pass just an event name to remove all callback commands from that event type.\n * - Pass undefined as event name and a callback command to remove that callback command from all event types.\n * - Pass no params to remove all callback commands from all event types.\n *\n * #### Examples:\n * ````javascript\n * annyang.addCallback('start', myFunction1);\n * annyang.addCallback('start', myFunction2);\n * annyang.addCallback('end', myFunction1);\n * annyang.addCallback('end', myFunction2);\n *\n * // Remove all callbacks from all events:\n * annyang.removeCallback();\n *\n * // Remove all callbacks attached to end event:\n * annyang.removeCallback('end');\n *\n * // Remove myFunction2 from being called on start:\n * annyang.removeCallback('start', myFunction2);\n *\n * // Remove myFunction1 from being called on all events:\n * annyang.removeCallback(undefined, myFunction1);\n * ````\n *\n * @param type Name of event type to remove callback from\n * @param callback The callback function to remove\n * @returns undefined\n */\nconst removeCallback = (type?: CallbackType, callback?: CallbackMap[CallbackType]) => {\n  callbacks.forEach((callbacksArray, callbackType) => {\n    if (type === undefined || type === callbackType) {\n      if (callback === undefined) {\n        callbacks.get(callbackType)!.length = 0;\n      } else {\n        callbacks.set(\n          callbackType,\n          callbacksArray.filter(cb => cb.callback !== callback)\n        );\n      }\n    }\n  });\n};\n\n/**\n * Returns true if speech recognition is currently on.\n * Returns false if speech recognition is off or annyang is paused.\n *\n * @returns true if SpeechRecognition is on and annyang is not paused\n */\nconst isListening = () => {\n  return listening && !pauseListening;\n};\n\nexport type AnnyangState = 'idle' | 'listening' | 'paused';\n\n/**\n * Returns the current state of annyang.\n *\n * @returns {'idle' | 'listening' | 'paused'} The current state\n */\nconst getState = (): AnnyangState => {\n  if (!listening) return 'idle';\n  if (pauseListening) return 'paused';\n  return 'listening';\n};\n\n/**\n * Set the language the user will speak in. If this method is not called, annyang defaults to 'en-US'.\n *\n * @param {string} language - The language (locale)\n * @see [Languages](https://github.com/TalAter/annyang/blob/master/docs/FAQ.md#what-languages-are-supported)\n */\nconst setLanguage = (language: string): void => {\n  if (!isSpeechRecognitionSupported()) {\n    return;\n  }\n  initIfNeeded();\n  recognition.lang = language;\n};\n\n/**\n * Turn on the output of debug messages to the console.\n *\n * @param {boolean} [newState=true] - Turn debug messages on or off\n */\nconst debug = (newState: boolean = true): void => {\n  debugState = !!newState;\n};\n\n/**\n * Match text against registered commands and fire the corresponding callbacks.\n * Works independently of the speech recognition engine — does not require `start()`, and works even in\n * environments where SpeechRecognition is not supported.\n *\n * Can accept either a string containing a single sentence or an array containing multiple sentences to be checked\n * in order until one of them matches a command (similar to the way Speech Recognition Alternatives are parsed)\n *\n * #### Examples:\n * ````javascript\n * annyang.trigger('Time for some thrilling heroics');\n * annyang.trigger(\n *     ['Time for some thrilling heroics', 'Time for some thrilling aerobics']\n *   );\n * ````\n *\n * @param sentences - A sentence as a string or an array of strings of possible sentences\n */\nconst trigger = (sentences: string | string[] = []) => {\n  parseResults(Array.isArray(sentences) ? sentences : [sentences]);\n};\n\n/**\n * Returns the instance of the browser's SpeechRecognition object used by annyang.\n * Useful in case you want direct access to the browser's Speech Recognition engine.\n *\n * @returns SpeechRecognition The browser's Speech Recognizer instance currently used by annyang\n */\nconst getSpeechRecognizer = (): SpeechRecognition | undefined => {\n  return recognition;\n};\n\n/**\n * @deprecated annyang no longer requires manual initialization. It initializes automatically on `start()` or `addCommands()`. Remove any calls to `init()`.\n */\nconst initDeprecated = () => {\n  console.warn(\n    'annyang.init() is deprecated and no longer needed. ' +\n      'annyang initializes automatically on start() or addCommands(). Remove this call.'\n  );\n};\n\nexport {\n  abort,\n  addCallback,\n  addCommands,\n  debug,\n  getSpeechRecognizer,\n  getState,\n  initDeprecated as init,\n  isListening,\n  isSpeechRecognitionSupported,\n  pause,\n  removeCallback,\n  removeCommands,\n  resume,\n  setLanguage,\n  start,\n  trigger,\n};\n\nconst annyang = {\n  isSpeechRecognitionSupported,\n  addCommands,\n  removeCommands,\n  start,\n  abort,\n  pause,\n  resume,\n  addCallback,\n  removeCallback,\n  isListening,\n  setLanguage,\n  trigger,\n  debug,\n  getSpeechRecognizer,\n  getState,\n  init: initDeprecated,\n  get state() {\n    return getState();\n  },\n} as const;\n\nexport default annyang;\n"
  },
  {
    "path": "test/setupTests.js",
    "content": "import { vi, beforeAll, afterAll } from 'vitest';\nimport { SpeechRecognition } from 'corti';\n\nbeforeAll(() => {\n  vi.stubGlobal('SpeechRecognition', SpeechRecognition);\n  vi.stubGlobal('location', { protocol: 'https:' });\n});\n\nafterAll(() => {\n  vi.unstubAllGlobals();\n});\n"
  },
  {
    "path": "test/specs/annyang.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, it, test, vi, MockInstance } from 'vitest';\nimport type { CortiSpeechRecognition } from 'corti';\n\nimport annyangDefault from '../../src/annyang.ts';\nimport * as annyang from '../../src/annyang.ts';\nimport { isSpeechRecognitionSupported, start, isListening } from '../../src/annyang.ts';\n\nconst logFormatString = 'font-weight: bold; color: #00f;';\n\ntest('SpeechRecognition is mocked', () => {\n  expect(globalThis.SpeechRecognition).toBeDefined();\n  expect(globalThis.SpeechRecognition.prototype).toHaveProperty('say', expect.any(Function));\n});\n\ntest('Can import annyang as an object', () => {\n  expect(annyang).toBeDefined();\n  expect(annyang.isSpeechRecognitionSupported).toBeInstanceOf(Function);\n  expect(annyang.isSpeechRecognitionSupported()).toBe(true);\n});\n\ntest('Can import annyang as a default export', () => {\n  expect(annyangDefault).toBeDefined();\n  expect(annyangDefault.isSpeechRecognitionSupported).toBeInstanceOf(Function);\n  expect(annyangDefault.addCommands).toBeInstanceOf(Function);\n  expect(annyangDefault.start).toBeInstanceOf(Function);\n});\n\ntest('Default export has state getter', () => {\n  expect(annyangDefault.state).toBe('idle');\n  annyangDefault.start();\n  expect(annyangDefault.state).toBe('listening');\n  annyangDefault.pause();\n  expect(annyangDefault.state).toBe('paused');\n  annyangDefault.abort();\n  expect(annyangDefault.state).toBe('idle');\n});\n\ntest('Can import individual named exports from annyang', () => {\n  expect(isSpeechRecognitionSupported).toBeInstanceOf(Function);\n  expect(isSpeechRecognitionSupported()).toBe(true);\n  expect(isListening()).toBe(false);\n  start();\n  expect(isListening()).toBe(true);\n});\n\ndescribe('annyang', () => {\n  let logSpy!: MockInstance;\n\n  beforeEach(() => {\n    vi.useFakeTimers();\n    logSpy = vi.spyOn(console, 'log');\n    annyang.debug(false);\n    annyang.abort();\n    annyang.removeCommands();\n    annyang.removeCallback();\n  });\n\n  afterEach(() => {\n    vi.useRealTimers();\n    logSpy.mockRestore();\n  });\n\n  it('should recognize when Speech Recognition engine was aborted and abort annyang', () => {\n    annyang.start();\n    expect(annyang.isListening()).toBe(true);\n    annyang.getSpeechRecognizer().abort();\n    expect(annyang.isListening()).toBe(false);\n  });\n\n  it('should recognize when Speech Recognition engine is repeatedly aborted as soon as it is started and console.log about it once every 10 seconds', () => {\n    const recognition = annyang.getSpeechRecognizer();\n\n    const onStart = () => {\n      setTimeout(() => recognition.abort(), 1);\n    };\n\n    recognition.addEventListener('start', onStart);\n    annyang.debug();\n    annyang.start();\n    expect(logSpy).not.toHaveBeenCalled();\n    vi.advanceTimersByTime(10000);\n    expect(logSpy).toHaveBeenCalledTimes(1);\n    expect(logSpy).toHaveBeenCalledWith(\n      'Speech Recognition is repeatedly stopping and starting. See http://is.gd/annyang_restarts for tips.'\n    );\n    vi.advanceTimersByTime(10000);\n    expect(logSpy).toHaveBeenCalledTimes(2);\n\n    recognition.removeEventListener('start', onStart);\n  });\n\n  describe('isSpeechRecognitionSupported()', () => {\n    it('should be a function', () => {\n      expect(annyang.isSpeechRecognitionSupported).toBeInstanceOf(Function);\n    });\n    it('should return true when SpeechRecognition is available in globalThis', () => {\n      expect(annyang.isSpeechRecognitionSupported()).toBe(true);\n    });\n  });\n\n  describe('debug()', () => {\n    it('should be a function', () => {\n      expect(annyang.isSpeechRecognitionSupported).toBeInstanceOf(Function);\n    });\n    it('should turn on debug messages when called without a parameter', () => {\n      annyang.debug();\n      annyang.addCommands({ 'test command': () => {} });\n      expect(logSpy).toHaveBeenCalled();\n    });\n    it('should turn on debug messages when called with a truthy parameter', () => {\n      // @ts-expect-error testing invalid parameter\n      annyang.debug(11);\n      annyang.addCommands({ 'test command': () => {} });\n      expect(logSpy).toHaveBeenCalled();\n    });\n    it('should turn off debug messages when called with a falsy parameter', () => {\n      // @ts-expect-error testing invalid parameter\n      annyang.debug(0);\n      annyang.addCommands({ 'test command': () => {} });\n      expect(logSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('addCommands()', () => {\n    it('should be a function', () => {\n      expect(annyang.addCommands).toBeInstanceOf(Function);\n    });\n\n    it('should accept an object consisting of key (sentence) and value (callback function)', () => {\n      expect(() => {\n        annyang.addCommands({\n          'Time for some thrilling heroics': () => {},\n        });\n      }).not.toThrowError();\n    });\n\n    describe('command matching', () => {\n      let spyOnMatch!: MockInstance;\n\n      beforeEach(() => {\n        spyOnMatch = vi.fn();\n      });\n\n      it('should work when a command object with a single simple command is passed', () => {\n        annyang.addCommands({ 'Time for some thrilling heroics': spyOnMatch });\n        annyang.start();\n        (annyang.getSpeechRecognizer() as CortiSpeechRecognition).say('Time for some thrilling heroics');\n        expect(spyOnMatch).toHaveBeenCalledTimes(1);\n      });\n    });\n\n    describe('debug messages', () => {\n      it('should write to console each command that was successfully added when debug is on', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(true);\n\n        annyang.addCommands({\n          'Time for some thrilling heroics': () => {},\n        });\n\n        expect(logSpy).toHaveBeenCalledTimes(1);\n        expect(logSpy).toHaveBeenCalledWith(\n          'Command successfully loaded: %cTime for some thrilling heroics',\n          logFormatString\n        );\n\n        annyang.addCommands({\n          'That sounds like something out of science fiction': () => {},\n          'We should start dealing in those black-market beagles': () => {},\n        });\n\n        expect(logSpy).toHaveBeenCalledTimes(3);\n      });\n\n      it('should not write to console commands added when debug is off', () => {\n        annyang.debug(false);\n        annyang.addCommands({\n          'Time for some thrilling heroics': () => {},\n        });\n        annyang.addCommands({\n          'That sounds like something out of science fiction': () => {},\n          'We should start dealing in those black-market beagles': () => {},\n        });\n\n        expect(logSpy).not.toHaveBeenCalled();\n      });\n\n      it('should write to console when commands could not be added and debug is on', () => {\n        annyang.debug(true);\n        expect(logSpy).not.toHaveBeenCalled();\n\n        annyang.addCommands({\n          'Time for some thrilling heroics': 'not_a_function',\n        });\n\n        expect(logSpy).toHaveBeenCalledTimes(1);\n        expect(logSpy).toHaveBeenCalledWith(\n          'Can not register command: %cTime for some thrilling heroics',\n          logFormatString\n        );\n      });\n\n      it('should not write to console when commands could not be added but debug is off', () => {\n        annyang.debug(false);\n        annyang.addCommands({\n          'Time for some thrilling heroics': 'not_a_function',\n        });\n        expect(logSpy).not.toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('removeCommands()', () => {\n    let recognition;\n    let spyOnMatch1!: MockInstance;\n    let spyOnMatch2!: MockInstance;\n    let spyOnMatch3!: MockInstance;\n    let spyOnMatch4!: MockInstance;\n    let spyOnMatch5!: MockInstance;\n\n    beforeEach(() => {\n      spyOnMatch1 = vi.fn();\n      spyOnMatch2 = vi.fn();\n      spyOnMatch3 = vi.fn();\n      spyOnMatch4 = vi.fn();\n      spyOnMatch5 = vi.fn();\n      annyang.addCommands({\n        'Time for some (thrilling) heroics': spyOnMatch1,\n        'We should start dealing in those *merchandise': spyOnMatch2,\n        'That sounds like something out of science fiction': spyOnMatch3,\n        'too pretty': {\n          regexp: /We are just too pretty for God to let us die/,\n          callback: spyOnMatch4,\n        },\n        \"You can't take the :thing from me\": spyOnMatch5,\n      });\n      annyang.start({ continuous: true });\n      recognition = annyang.getSpeechRecognizer();\n    });\n\n    it('should be a function', () => {\n      expect(annyang.removeCommands).toBeInstanceOf(Function);\n    });\n\n    it('should remove a single command when its name is passed as a string in the first parameter', () => {\n      annyang.removeCommands('Time for some (thrilling) heroics');\n      annyang.start();\n      recognition.say('Time for some thrilling heroics');\n      recognition.say('We should start dealing in those black-market beagles');\n      recognition.say('That sounds like something out of science fiction');\n      recognition.say('We are just too pretty for God to let us die');\n      recognition.say(\"You can't take the sky from me\");\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n      expect(spyOnMatch2).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch3).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n\n    it('should remove multiple commands when their names are passed as an array in the first parameter', () => {\n      annyang.removeCommands([\n        'Time for some (thrilling) heroics',\n        'That sounds like something out of science fiction',\n      ]);\n      recognition.say('Time for some thrilling heroics');\n      recognition.say('We should start dealing in those black-market beagles');\n      recognition.say('That sounds like something out of science fiction');\n      recognition.say('We are just too pretty for God to let us die');\n      recognition.say(\"You can't take the sky from me\");\n\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n      expect(spyOnMatch2).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch3).not.toHaveBeenCalled();\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n\n    it('should remove all commands when called with no parameters', () => {\n      annyang.removeCommands();\n      recognition.say('Time for some heroics');\n      recognition.say('We should start dealing in those black-market beagles');\n      recognition.say('That sounds like something out of science fiction');\n      recognition.say('We are just too pretty for God to let us die');\n      recognition.say(\"You can't take the sky from me\");\n\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n      expect(spyOnMatch2).not.toHaveBeenCalled();\n      expect(spyOnMatch3).not.toHaveBeenCalled();\n      expect(spyOnMatch4).not.toHaveBeenCalled();\n      expect(spyOnMatch5).not.toHaveBeenCalled();\n    });\n\n    it('should remove a command with an optional word when its name is passed in the first parameter', () => {\n      annyang.removeCommands('Time for some (thrilling) heroics');\n      recognition.say('Time for some heroics');\n      recognition.say('We should start dealing in those black-market beagles');\n      recognition.say('That sounds like something out of science fiction');\n      recognition.say('We are just too pretty for God to let us die');\n      recognition.say(\"You can't take the sky from me\");\n\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n      expect(spyOnMatch2).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch3).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n\n    it('should remove a command with a named variable when its name is passed in the first parameter', () => {\n      annyang.removeCommands(\"You can't take the :thing from me\");\n      recognition.say('Time for some heroics');\n      recognition.say('We should start dealing in those black-market beagles');\n      recognition.say('That sounds like something out of science fiction');\n      recognition.say('We are just too pretty for God to let us die');\n      recognition.say(\"You can't take the sky from me\");\n\n      expect(spyOnMatch1).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch2).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch3).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch5).not.toHaveBeenCalled();\n    });\n\n    it('should remove a command with a splat when its name is passed as a parameter', () => {\n      annyang.removeCommands('We should start dealing in those *merchandise');\n      recognition.say('Time for some heroics');\n      recognition.say('We should start dealing in those black-market beagles');\n      recognition.say('That sounds like something out of science fiction');\n      recognition.say('We are just too pretty for God to let us die');\n      recognition.say(\"You can't take the sky from me\");\n\n      expect(spyOnMatch1).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch2).not.toHaveBeenCalled();\n      expect(spyOnMatch3).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n\n    it('should remove a regexp command when its name is passed as a parameter', () => {\n      annyang.removeCommands('too pretty');\n      recognition.say('Time for some heroics');\n      recognition.say('We should start dealing in those black-market beagles');\n      recognition.say('That sounds like something out of science fiction');\n      recognition.say('We are just too pretty for God to let us die');\n      recognition.say(\"You can't take the sky from me\");\n\n      expect(spyOnMatch1).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch2).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch3).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch4).not.toHaveBeenCalled();\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('addCallback()', () => {\n    it('should be a function', () => {\n      expect(annyang.addCallback).toBeInstanceOf(Function);\n    });\n\n    it('should return an unsubscribe function when a valid callback is added', () => {\n      const unsub = annyang.addCallback('start', () => {});\n      expect(unsub).toBeInstanceOf(Function);\n    });\n\n    it('should return a no-op function when called with invalid arguments', () => {\n      // @ts-expect-error testing invalid parameter\n      const unsub1 = annyang.addCallback();\n      expect(unsub1).toBeInstanceOf(Function);\n      // @ts-expect-error testing invalid parameter\n      const unsub2 = annyang.addCallback('blergh');\n      expect(unsub2).toBeInstanceOf(Function);\n      // @ts-expect-error testing invalid parameter\n      const unsub3 = annyang.addCallback('start');\n      expect(unsub3).toBeInstanceOf(Function);\n    });\n\n    it('should remove callback when unsubscribe function is called', () => {\n      const spy: MockInstance = vi.fn();\n      const unsub = annyang.addCallback('start', spy);\n\n      annyang.start();\n      expect(spy).toHaveBeenCalledTimes(1);\n\n      annyang.abort();\n      spy.mockClear();\n      unsub();\n\n      annyang.start();\n      expect(spy).not.toHaveBeenCalled();\n    });\n\n    it('should be able to register multiple callbacks to one event type', () => {\n      const spy1: MockInstance = vi.fn();\n      const spy2: MockInstance = vi.fn();\n\n      annyang.addCallback('start', spy1);\n      annyang.addCallback('start', spy2);\n\n      expect(spy1).not.toHaveBeenCalled();\n      expect(spy2).not.toHaveBeenCalled();\n\n      annyang.start();\n\n      expect(spy1).toHaveBeenCalledTimes(1);\n      expect(spy2).toHaveBeenCalledTimes(1);\n    });\n\n    it('should run callbacks with `this` being undefined by default', () => {\n      const spy1 = vi.fn();\n      const fn = function () {\n        spy1(this);\n      };\n      annyang.addCallback('start', fn);\n\n      annyang.start();\n      expect(spy1).toHaveBeenCalledWith(undefined);\n    });\n\n    it('should run callbacks in the scope where addCallback was called by default', () => {\n      let counter = 0;\n      const fn = function () {\n        counter += 1;\n      };\n      annyang.addCallback('start', fn);\n\n      annyang.start();\n      expect(counter).toEqual(1);\n    });\n\n    it('should run arrow function callbacks with `this` being the current scope in which addCallback was called', () => {\n      const spy1 = vi.fn();\n      const fn = () => {\n        spy1(this);\n      };\n      annyang.addCallback('start', fn);\n\n      annyang.start();\n      expect(spy1).toHaveBeenCalledWith(this);\n    });\n\n    it('should run callbacks with `this` being equal to the context given as the third parameter', () => {\n      const spy1 = vi.fn();\n      const obj = { counter: 0 };\n\n      const fn = function () {\n        spy1(this);\n        this.counter += 1;\n      };\n\n      annyang.addCallback('start', fn, obj);\n      annyang.start();\n\n      expect(spy1).toHaveBeenCalledWith(obj);\n      expect(obj.counter).toEqual(1);\n    });\n\n    it('should run arrow function callbacks with `this` being equal to the current context regardless of the context given as the third parameter', () => {\n      const spy1: MockInstance = vi.fn();\n\n      const fn = () => {\n        spy1(this);\n      };\n\n      annyang.addCallback('start', fn, { a: 1 });\n      annyang.start();\n\n      expect(spy1).toHaveBeenCalledWith(this);\n    });\n  });\n\n  describe('removeCallback()', () => {\n    let spy1!: MockInstance;\n    let spy2!: MockInstance;\n    let spy3!: MockInstance;\n    let spy4!: MockInstance;\n\n    beforeEach(() => {\n      spy1 = vi.fn();\n      spy2 = vi.fn();\n      spy3 = vi.fn();\n      spy4 = vi.fn();\n      annyang.addCallback('start', spy1);\n      annyang.addCallback('start', spy2);\n      annyang.addCallback('end', spy3);\n      annyang.addCallback('end', spy4);\n    });\n\n    it('should be a function', () => {\n      expect(annyang.removeCallback).toBeInstanceOf(Function);\n    });\n\n    it('should always return undefined', () => {\n      expect(annyang.removeCallback()).toEqual(undefined);\n      // @ts-expect-error testing invalid parameter\n      expect(annyang.removeCallback('blergh')).toEqual(undefined);\n      expect(annyang.removeCallback('start')).toEqual(undefined);\n      expect(annyang.removeCallback('start', () => {})).toEqual(undefined);\n    });\n\n    it('should delete all callbacks on all event types if passed undefined in both parameters', () => {\n      annyang.removeCallback();\n      annyang.start();\n      annyang.abort();\n\n      expect(spy1).not.toHaveBeenCalled();\n      expect(spy2).not.toHaveBeenCalled();\n      expect(spy3).not.toHaveBeenCalled();\n      expect(spy4).not.toHaveBeenCalled();\n    });\n\n    it('should delete all callbacks of given function on all event types if 1st parameter is undefined and second parameter is a function', () => {\n      annyang.addCallback('end', spy1);\n      annyang.removeCallback(undefined, spy1);\n      annyang.start();\n      annyang.abort();\n\n      expect(spy1).not.toHaveBeenCalled();\n      expect(spy2).toHaveBeenCalledTimes(1);\n      expect(spy3).toHaveBeenCalledTimes(1);\n      expect(spy4).toHaveBeenCalledTimes(1);\n    });\n\n    it('should delete all callbacks on an event type if passed an event name and no second parameter', () => {\n      annyang.removeCallback('start');\n      annyang.start();\n      annyang.abort();\n\n      expect(spy1).not.toHaveBeenCalled();\n      expect(spy2).not.toHaveBeenCalled();\n      expect(spy3).toHaveBeenCalledTimes(1);\n      expect(spy4).toHaveBeenCalledTimes(1);\n    });\n\n    it('should delete the callbacks on an event type matching the function passed as the second parameter', () => {\n      annyang.removeCallback('start', spy2);\n      annyang.start();\n      annyang.abort();\n\n      expect(spy1).toHaveBeenCalledTimes(1);\n      expect(spy2).not.toHaveBeenCalled();\n      expect(spy3).toHaveBeenCalledTimes(1);\n      expect(spy4).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('getSpeechRecognizer()', () => {\n    it('should be a function', () => {\n      expect(annyang.getSpeechRecognizer).toBeInstanceOf(Function);\n    });\n\n    it('should return the instance of SpeechRecognition used by annyang', () => {\n      const spyOnStart: MockInstance = vi.fn();\n      const recognition = annyang.getSpeechRecognizer();\n      expect(recognition).toBeInstanceOf(globalThis.SpeechRecognition);\n\n      // Make sure it's the one used by annyang\n      recognition.addEventListener('start', spyOnStart);\n      expect(spyOnStart).not.toHaveBeenCalled();\n      annyang.start();\n      expect(spyOnStart).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('start()', () => {\n    let recognition;\n    let spyOnStart1!: MockInstance;\n    let spyOnStart2!: MockInstance;\n\n    beforeEach(() => {\n      recognition = annyang.getSpeechRecognizer();\n      spyOnStart1 = vi.fn();\n      spyOnStart2 = vi.fn();\n      recognition.addEventListener('start', spyOnStart1);\n      annyang.addCallback('start', spyOnStart2);\n    });\n\n    it('should be a function', () => {\n      expect(annyang.start).toBeInstanceOf(Function);\n    });\n\n    it('should start annyang and SpeechRecognition if it was aborted', () => {\n      expect(spyOnStart1).not.toHaveBeenCalled();\n      expect(spyOnStart2).not.toHaveBeenCalled();\n      expect(annyang.isListening()).toBe(false);\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n      expect(spyOnStart1).toHaveBeenCalledTimes(1);\n      expect(spyOnStart2).toHaveBeenCalledTimes(1);\n    });\n\n    it('should resume annyang if it was paused', () => {\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n\n      annyang.pause();\n      expect(annyang.isListening()).toBe(false);\n\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n    });\n\n    it('should resume annyang if it was paused but not trigger start event', () => {\n      expect(spyOnStart1).not.toHaveBeenCalled();\n      expect(spyOnStart2).not.toHaveBeenCalled();\n\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n      expect(spyOnStart1).toHaveBeenCalledTimes(1);\n      expect(spyOnStart2).toHaveBeenCalledTimes(1);\n\n      annyang.pause();\n      expect(annyang.isListening()).toBe(false);\n\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n\n      expect(spyOnStart1).toHaveBeenCalledTimes(1);\n      expect(spyOnStart2).toHaveBeenCalledTimes(1);\n    });\n\n    it('should do nothing when annyang is already started and listening', () => {\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n\n      expect(() => {\n        annyang.start();\n      }).not.toThrowError();\n\n      expect(annyang.isListening()).toBe(true);\n\n      expect(spyOnStart1).toHaveBeenCalledTimes(1);\n      expect(spyOnStart2).toHaveBeenCalledTimes(1);\n    });\n\n    it('should accept an options object as its first argument', () => {\n      expect(() => {\n        // @ts-expect-error testing invalid parameter\n        annyang.start({ option: true });\n      }).not.toThrowError();\n    });\n\n    describe('options', () => {\n      describe('autoRestart', () => {\n        it('should cause annyang to restart after 1 second when Speech Recognition engine was aborted (when true)', () => {\n          annyang.start({ autoRestart: true });\n          recognition.abort();\n          expect(annyang.isListening()).toBe(false);\n          vi.advanceTimersByTime(999);\n          expect(annyang.isListening()).toBe(false);\n          vi.advanceTimersByTime(1);\n          expect(annyang.isListening()).toBe(true);\n        });\n\n        it('should cause annyang to not restart when Speech Recognition engine was aborted (when false)', () => {\n          annyang.start({ autoRestart: false });\n          recognition.abort();\n          expect(annyang.isListening()).toBe(false);\n          vi.advanceTimersByTime(10000);\n          expect(annyang.isListening()).toBe(false);\n        });\n\n        it('should default to true, even after an annyang.abort() call', () => {\n          annyang.start();\n          annyang.abort();\n          annyang.start();\n\n          expect(annyang.isListening()).toBe(true);\n          annyang.getSpeechRecognizer().abort();\n          expect(annyang.isListening()).toBe(false);\n          vi.advanceTimersByTime(20000);\n          expect(annyang.isListening()).toBe(true);\n        });\n      });\n\n      describe('paused', () => {\n        it('should cause annyang to start paused (when true)', () => {\n          annyang.start({ paused: true });\n          expect(annyang.isListening()).toBe(false);\n        });\n        it('should cause annyang to start not paused (when false)', () => {\n          annyang.start({ paused: false });\n          expect(annyang.isListening()).toBe(true);\n        });\n      });\n\n      describe('continuous', () => {\n        let spyOnEnd!: MockInstance;\n        let spyOnResult!: MockInstance;\n\n        beforeEach(() => {\n          spyOnEnd = vi.fn();\n          spyOnResult = vi.fn();\n          annyang.addCallback('end', spyOnEnd);\n          annyang.addCallback('result', spyOnResult);\n        });\n\n        it('should cause annyang to continuously listen to phrases even after matches are made (when true)', () => {\n          annyang.start({ continuous: true });\n          expect(spyOnResult).not.toHaveBeenCalled();\n          expect(spyOnEnd).not.toHaveBeenCalled();\n          recognition.say('Time for some thrilling heroics');\n          expect(spyOnResult).toHaveBeenCalledTimes(1);\n          expect(spyOnEnd).not.toHaveBeenCalled();\n          recognition.say('Time for some thrilling heroics');\n          expect(spyOnResult).toHaveBeenCalledTimes(2);\n          expect(spyOnEnd).not.toHaveBeenCalled();\n        });\n\n        it('should cause annyang to stop after the first recognized phrase whether it matches or not (when false)', () => {\n          annyang.start({ continuous: false });\n          expect(spyOnResult).not.toHaveBeenCalled();\n          expect(spyOnEnd).not.toHaveBeenCalled();\n          recognition.say('Time for some thrilling heroics');\n          expect(spyOnResult).toHaveBeenCalledTimes(1);\n          expect(spyOnEnd).toHaveBeenCalledTimes(1);\n          recognition.say('Time for some thrilling heroics');\n          expect(spyOnResult).toHaveBeenCalledTimes(1);\n          expect(spyOnEnd).toHaveBeenCalledTimes(1);\n        });\n      });\n    });\n\n    describe('deubg messages', () => {\n      it('should write a message to log when annyang is already started and debug is on', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(true);\n        annyang.start();\n        annyang.start();\n\n        expect(logSpy).toHaveBeenCalledTimes(1);\n        expect(logSpy).toHaveBeenCalledWith(\n          \"Failed to execute 'start' on 'SpeechRecognition': recognition has already started.\"\n        );\n      });\n\n      it('should not write a message to log when annyang is already started but debug is off', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(false);\n        annyang.start();\n        annyang.start();\n\n        expect(logSpy).not.toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('abort()', () => {\n    let spyOnEnd!: MockInstance;\n    let recognition;\n\n    beforeEach(() => {\n      spyOnEnd = vi.fn();\n      recognition = annyang.getSpeechRecognizer();\n      recognition.addEventListener('end', spyOnEnd);\n    });\n\n    it('should be a function', () => {\n      expect(annyang.abort).toBeInstanceOf(Function);\n    });\n\n    it('should stop SpeechRecognition and annyang if it is started', () => {\n      annyang.start();\n      expect(spyOnEnd).toHaveBeenCalledTimes(0);\n      expect(annyang.isListening()).toBe(true);\n      annyang.abort();\n      expect(spyOnEnd).toHaveBeenCalledTimes(1);\n      expect(annyang.isListening()).toBe(false);\n    });\n\n    it('should stop Speech Recognition and annyang if it is paused', () => {\n      annyang.start();\n      annyang.pause();\n      expect(spyOnEnd).toHaveBeenCalledTimes(0);\n      expect(annyang.isListening()).toBe(false);\n      annyang.abort();\n      expect(spyOnEnd).toHaveBeenCalledTimes(1);\n      expect(annyang.isListening()).toBe(false);\n    });\n\n    it('should do nothing when annyang is already stopped', () => {\n      annyang.start();\n      annyang.abort();\n      expect(spyOnEnd).toHaveBeenCalledTimes(1);\n      annyang.abort();\n      expect(spyOnEnd).toHaveBeenCalledTimes(1);\n    });\n\n    it('should not throw an error when called before annyang initializes', () => {\n      expect(() => {\n        annyang.abort();\n      }).not.toThrowError();\n    });\n  });\n\n  describe('pause()', () => {\n    let recognition;\n\n    beforeEach(() => {\n      annyang.start();\n      recognition = annyang.getSpeechRecognizer();\n    });\n\n    it('should be a function', () => {\n      expect(annyang.pause).toBeInstanceOf(Function);\n    });\n\n    it('should return undefined when called', () => {\n      expect(annyang.pause()).toEqual(undefined);\n    });\n\n    it('should cause commands not to fire even when a command phrase is matched', () => {\n      const spyOnMatch: MockInstance = vi.fn();\n      annyang.addCommands({\n        'Time for some thrilling heroics': spyOnMatch,\n      });\n      annyang.pause();\n      recognition.say('Time for some thrilling heroics');\n      expect(spyOnMatch).not.toHaveBeenCalled();\n    });\n\n    it(\"should not stop the browser's Speech Recognition engine\", () => {\n      expect(recognition.isStarted()).toBe(true);\n      annyang.pause();\n      expect(recognition.isStarted()).toBe(true);\n    });\n\n    it('should leave annyang paused if called after annyang.abort()', () => {\n      expect(annyang.isListening()).toBe(true);\n      annyang.abort();\n\n      expect(annyang.isListening()).toBe(false);\n      annyang.pause();\n\n      expect(annyang.isListening()).toBe(false);\n    });\n\n    it(\"should leave the browser's Speech Recognition off, if called after annyang.abort()\", () => {\n      expect(recognition.isStarted()).toBe(true);\n      annyang.abort();\n\n      expect(recognition.isStarted()).toBe(false);\n      annyang.pause();\n\n      expect(recognition.isStarted()).toBe(false);\n    });\n\n    describe('debug messages', () => {\n      beforeEach(() => {\n        annyang.pause();\n      });\n\n      it('should log a message if speech detected while paused and debug is on', () => {\n        annyang.debug();\n        expect(logSpy).not.toHaveBeenCalled();\n        recognition.say('Time for some thrilling heroics');\n        expect(logSpy).toHaveBeenCalledTimes(1);\n        expect(logSpy).toHaveBeenCalledWith('Speech heard, but annyang is paused');\n      });\n\n      it('should not log a message if speech detected while paused and debug is off', () => {\n        annyang.debug(false);\n        recognition.say('Time for some thrilling heroics');\n        expect(logSpy).not.toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('resume()', () => {\n    let recognition;\n\n    beforeEach(() => {\n      annyang.start();\n      recognition = annyang.getSpeechRecognizer();\n    });\n\n    it('should be a function', () => {\n      expect(annyang.resume).toBeInstanceOf(Function);\n    });\n\n    it('should return undefined when called', () => {\n      expect(annyang.resume()).toEqual(undefined);\n    });\n\n    it('should leave speech recognition on and turn annyang on, if called when annyang is paused', () => {\n      annyang.start();\n      annyang.pause();\n\n      expect(annyang.isListening()).toBe(false);\n      expect(recognition.isStarted()).toBe(true);\n      annyang.resume();\n\n      expect(annyang.isListening()).toBe(true);\n      expect(recognition.isStarted()).toBe(true);\n    });\n\n    it('should turn speech recognition and annyang on, if called when annyang is stopped', () => {\n      annyang.abort();\n\n      expect(annyang.isListening()).toBe(false);\n      expect(recognition.isStarted()).toBe(false);\n      annyang.resume();\n\n      expect(annyang.isListening()).toBe(true);\n      expect(recognition.isStarted()).toBe(true);\n    });\n\n    it('should leave speech recognition and annyang on, if called when annyang is listening', () => {\n      expect(annyang.isListening()).toBe(true);\n      expect(recognition.isStarted()).toBe(true);\n      annyang.resume();\n\n      expect(annyang.isListening()).toBe(true);\n      expect(recognition.isStarted()).toBe(true);\n    });\n\n    describe('debug messages', () => {\n      it('should log a message if debug is on, and resume was called when annyang is listening', () => {\n        annyang.debug(true);\n        annyang.resume();\n\n        expect(logSpy).toHaveBeenCalledTimes(1);\n        expect(logSpy).toHaveBeenCalledWith(\n          \"Failed to execute 'start' on 'SpeechRecognition': recognition has already started.\"\n        );\n      });\n\n      it('should not log a message if debug is off, and resume was called when annyang is listening', () => {\n        annyang.debug(false);\n        annyang.resume();\n\n        expect(logSpy).not.toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('setLanguage()', () => {\n    it('should be a function', () => {\n      expect(annyang.setLanguage).toBeInstanceOf(Function);\n    });\n\n    it('should return undefined when called', () => {\n      // @ts-expect-error testing invalid parameter\n      expect(annyang.setLanguage()).toEqual(undefined);\n    });\n\n    it('should set the Speech Recognition engine to the value passed', () => {\n      annyang.setLanguage('he');\n\n      expect(annyang.getSpeechRecognizer().lang).toEqual('he');\n    });\n  });\n\n  describe('isListening()', () => {\n    it('should be a function', () => {\n      expect(annyang.isListening).toBeInstanceOf(Function);\n    });\n\n    it('should return false when called before annyang starts', () => {\n      expect(annyang.isListening()).toBe(false);\n    });\n\n    it('should return true when called after annyang starts', () => {\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n    });\n\n    it('should return false when called after annyang aborts', () => {\n      annyang.start();\n      annyang.abort();\n      expect(annyang.isListening()).toBe(false);\n    });\n\n    it('should return false when called when annyang is paused', () => {\n      annyang.start();\n      annyang.pause();\n      expect(annyang.isListening()).toBe(false);\n    });\n\n    it('should return true when called after annyang is resumed', () => {\n      annyang.start();\n      annyang.pause();\n      annyang.resume();\n      expect(annyang.isListening()).toBe(true);\n    });\n\n    it('should return false when SpeechRecognition object is aborted directly', () => {\n      annyang.start();\n      expect(annyang.isListening()).toBe(true);\n      annyang.getSpeechRecognizer().abort();\n      expect(annyang.isListening()).toBe(false);\n    });\n  });\n\n  describe('trigger()', () => {\n    let spyOnCommand!: MockInstance;\n    let spyOnResult!: MockInstance;\n\n    beforeEach(() => {\n      spyOnCommand = vi.fn();\n      spyOnResult = vi.fn();\n      annyang.addCommands({\n        'Time for some thrilling heroics': spyOnCommand,\n      });\n      annyang.start();\n    });\n\n    it('should always return undefined', () => {\n      expect(annyang.trigger()).toEqual(undefined);\n      expect(annyang.trigger('Time for some thrilling heroics')).toEqual(undefined);\n      expect(annyang.trigger(['Time for some thrilling aerobics', 'Time for some thrilling heroics'])).toEqual(\n        undefined\n      );\n    });\n\n    it('should match a sentence passed as a string and execute it as if it was passed from Speech Recognition', () => {\n      expect(spyOnCommand).not.toHaveBeenCalled();\n      annyang.trigger('Time for some thrilling heroics');\n      expect(spyOnCommand).toHaveBeenCalledTimes(1);\n    });\n\n    it('should match a sentence passed as part of an array and execute it as if it was passed from Speech Recognition', () => {\n      expect(spyOnCommand).not.toHaveBeenCalled();\n      annyang.trigger(['Time for some thrilling aerobics', 'Time for some thrilling heroics']);\n      expect(spyOnCommand).toHaveBeenCalledTimes(1);\n    });\n\n    it('should trigger a result event', () => {\n      annyang.addCallback('result', spyOnResult);\n\n      expect(spyOnResult).not.toHaveBeenCalled();\n      annyang.trigger('Result but not a match');\n\n      expect(spyOnResult).toHaveBeenCalledTimes(1);\n    });\n\n    it('should trigger a resultMatch event if sentence matches a command', () => {\n      annyang.addCallback('resultMatch', spyOnResult);\n\n      expect(spyOnResult).not.toHaveBeenCalled();\n      annyang.trigger('Time for some thrilling heroics');\n\n      expect(spyOnResult).toHaveBeenCalledTimes(1);\n    });\n\n    it('should trigger a resultNoMatch event if sentence does not match a command', () => {\n      annyang.addCallback('resultNoMatch', spyOnResult);\n\n      expect(spyOnResult).not.toHaveBeenCalled();\n      annyang.trigger('Result but not a match');\n\n      expect(spyOnResult).toHaveBeenCalledTimes(1);\n    });\n\n    it('should trigger a matching command even if annyang is aborted or not started', () => {\n      annyang.addCallback('resultMatch', spyOnResult);\n      expect(spyOnResult).not.toHaveBeenCalled();\n      annyang.abort();\n      annyang.trigger('Time for some thrilling heroics');\n      expect(spyOnResult).toHaveBeenCalled();\n    });\n\n    it('should trigger a matching command even if annyang is paused', () => {\n      annyang.addCallback('resultMatch', spyOnResult);\n      expect(spyOnResult).not.toHaveBeenCalled();\n      annyang.pause();\n      annyang.trigger('Time for some thrilling heroics');\n      expect(spyOnResult).toHaveBeenCalled();\n    });\n  });\n\n  describe('events', () => {\n    describe('start', () => {\n      let spyOnStart!: MockInstance;\n\n      beforeEach(() => {\n        spyOnStart = vi.fn();\n        annyang.addCallback('start', spyOnStart);\n      });\n\n      it('should fire callback when annyang aborts', () => {\n        expect(spyOnStart).not.toHaveBeenCalled();\n        annyang.start();\n        expect(spyOnStart).toHaveBeenCalledTimes(1);\n      });\n\n      it('should not fire callback when annyang resumes from a paused state', () => {\n        expect(spyOnStart).not.toHaveBeenCalled();\n        annyang.start();\n        expect(spyOnStart).toHaveBeenCalledTimes(1);\n        annyang.pause();\n        annyang.start();\n        expect(spyOnStart).toHaveBeenCalledTimes(1);\n      });\n\n      it('should fire callback when annyang resumes from an aborted (stopped) state', () => {\n        expect(spyOnStart).not.toHaveBeenCalled();\n        annyang.start();\n        expect(spyOnStart).toHaveBeenCalledTimes(1);\n        annyang.abort();\n        annyang.start();\n        expect(spyOnStart).toHaveBeenCalledTimes(2);\n      });\n    });\n\n    describe('end', () => {\n      let spyOnEnd!: MockInstance;\n\n      beforeEach(() => {\n        spyOnEnd = vi.fn();\n        annyang.addCallback('end', spyOnEnd);\n      });\n\n      it('should fire callback when annyang aborts', () => {\n        annyang.start();\n        expect(spyOnEnd).toHaveBeenCalledTimes(0);\n        annyang.abort();\n        expect(spyOnEnd).toHaveBeenCalledTimes(1);\n      });\n\n      it('should not fire callback when annyang enters paused state', () => {\n        annyang.start();\n        annyang.pause();\n        expect(spyOnEnd).toHaveBeenCalledTimes(0);\n      });\n\n      it('should trigger when SpeechRecognition is directly aborted', () => {\n        annyang.start();\n        annyang.getSpeechRecognizer().abort();\n        expect(spyOnEnd).toHaveBeenCalledTimes(1);\n      });\n    });\n\n    describe('soundstart', () => {\n      let spyOnSoundStart!: MockInstance;\n\n      beforeEach(() => {\n        spyOnSoundStart = vi.fn();\n        annyang.addCallback('soundstart', spyOnSoundStart);\n      });\n\n      it('should fire callback when annyang detects sound', () => {\n        expect(spyOnSoundStart).toHaveBeenCalledTimes(0);\n        // Corti which is used to mock SpeechRecognition fires the soundstart event as soon as it starts\n        annyang.start();\n        expect(spyOnSoundStart).toHaveBeenCalledTimes(1);\n      });\n\n      it('should fire callback once when in continuous mode even when multiples phrases are said', () => {\n        // Corti which is used to mock SpeechRecognition fires the soundstart event as soon as it starts\n        annyang.start({ continuous: true });\n        const recognition = annyang.getSpeechRecognizer() as CortiSpeechRecognition;\n        expect(spyOnSoundStart).toHaveBeenCalledTimes(1);\n        recognition.say('Time for some thrilling heroics');\n        expect(spyOnSoundStart).toHaveBeenCalledTimes(1);\n        recognition.say('That sounds like something out of science fiction');\n        expect(spyOnSoundStart).toHaveBeenCalledTimes(1);\n      });\n\n      it('should fire callback multiple times in non-continuous mode with autorestart', () => {\n        annyang.start({ continuous: false, autoRestart: true });\n        const recognition = annyang.getSpeechRecognizer() as CortiSpeechRecognition;\n        recognition.say('Time for some thrilling heroics');\n        expect(spyOnSoundStart).toHaveBeenCalledTimes(1);\n        vi.advanceTimersByTime(1000);\n        recognition.say('That sounds like something out of science fiction');\n        expect(spyOnSoundStart).toHaveBeenCalledTimes(2);\n      });\n    });\n\n    describe('result', () => {\n      let spyOnResult!: MockInstance;\n      let recognition;\n\n      beforeEach(() => {\n        spyOnResult = vi.fn();\n        annyang.addCallback('result', spyOnResult);\n        annyang.addCommands({\n          'Time for some thrilling heroics': () => {},\n        });\n        annyang.start();\n        recognition = annyang.getSpeechRecognizer() as CortiSpeechRecognition;\n      });\n\n      it('should fire callback when a result is returned from Speech Recognition and a command was matched', () => {\n        expect(spyOnResult).not.toHaveBeenCalled();\n        recognition.say('Time for some thrilling heroics');\n        expect(spyOnResult).toHaveBeenCalledTimes(1);\n      });\n\n      it('should fire callback when a result is returned from Speech Recognition and a command was not matched', () => {\n        expect(spyOnResult).not.toHaveBeenCalled();\n        recognition.say('That sounds like something out of science fiction');\n        expect(spyOnResult).toHaveBeenCalledTimes(1);\n      });\n\n      it('should call the callback with the first argument containing an array of all possible Speech Recognition Alternatives the user may have said', () => {\n        expect(spyOnResult).not.toHaveBeenCalled();\n        recognition.say('That sounds like something out of science fiction');\n        expect(spyOnResult).toHaveBeenCalledTimes(1);\n        expect(spyOnResult).toHaveBeenCalledWith([\n          'That sounds like something out of science fiction',\n          'That sounds like something out of science fiction and so on',\n          'That sounds like something out of science fiction and so on and so forth',\n          'That sounds like something out of science fiction and so on and so forth and so on',\n          'That sounds like something out of science fiction and so on and so forth and so on and so forth',\n        ]);\n      });\n    });\n\n    describe('resultMatch', () => {\n      let spyOnResultMatch!: MockInstance;\n      let recognition;\n\n      beforeEach(() => {\n        spyOnResultMatch = vi.fn();\n        annyang.addCallback('resultMatch', spyOnResultMatch);\n        annyang.addCommands({\n          'Time for some (thrilling) heroics': () => {},\n        });\n        annyang.start();\n        recognition = annyang.getSpeechRecognizer() as CortiSpeechRecognition;\n      });\n\n      it('should fire callback when a result is returned from Speech Recognition and a command was matched', () => {\n        expect(spyOnResultMatch).not.toHaveBeenCalled();\n        recognition.say('Time for some thrilling heroics');\n        expect(spyOnResultMatch).toHaveBeenCalledTimes(1);\n      });\n\n      it('should not fire callback when a result is returned from Speech Recognition and a command was not matched', () => {\n        expect(spyOnResultMatch).not.toHaveBeenCalled();\n        recognition.say('That sounds like something out of science fiction');\n        expect(spyOnResultMatch).not.toHaveBeenCalled();\n      });\n\n      it('should call the callback with the first argument containing the phrase the user said that matched a command', () => {\n        expect(spyOnResultMatch).not.toHaveBeenCalled();\n        recognition.say('Time for some heroics');\n        expect(spyOnResultMatch).toHaveBeenCalledTimes(1);\n        expect(spyOnResultMatch).toHaveBeenCalledWith('Time for some heroics', expect.anything(), expect.anything());\n      });\n\n      it('should call the callback with the second argument containing the name of the matched command', () => {\n        expect(spyOnResultMatch).not.toHaveBeenCalled();\n        recognition.say('Time for some heroics');\n        expect(spyOnResultMatch).toHaveBeenCalledTimes(1);\n        expect(spyOnResultMatch).toHaveBeenCalledWith(\n          expect.anything(),\n          'Time for some (thrilling) heroics',\n          expect.anything()\n        );\n      });\n\n      it('should call the callback with the third argument containing an array of all possible Speech Recognition Alternatives the user may have said', () => {\n        expect(spyOnResultMatch).not.toHaveBeenCalled();\n        recognition.say('Time for some heroics');\n        expect(spyOnResultMatch).toHaveBeenCalledTimes(1);\n        expect(spyOnResultMatch).toHaveBeenCalledWith(expect.anything(), expect.anything(), [\n          'Time for some heroics',\n          'Time for some heroics and so on',\n          'Time for some heroics and so on and so forth',\n          'Time for some heroics and so on and so forth and so on',\n          'Time for some heroics and so on and so forth and so on and so forth',\n        ]);\n      });\n    });\n\n    describe('resultNoMatch', () => {\n      let spyOnResultNoMatch!: MockInstance;\n      let recognition;\n\n      beforeEach(() => {\n        spyOnResultNoMatch = vi.fn();\n        annyang.addCallback('resultNoMatch', spyOnResultNoMatch);\n        annyang.addCommands({\n          'Time for some (thrilling) heroics': () => {},\n        });\n        annyang.start();\n        recognition = annyang.getSpeechRecognizer() as CortiSpeechRecognition;\n      });\n\n      it('should not fire callback when a result is returned from Speech Recognition and a command was matched', () => {\n        expect(spyOnResultNoMatch).not.toHaveBeenCalled();\n        recognition.say('Time for some thrilling heroics');\n        expect(spyOnResultNoMatch).not.toHaveBeenCalled();\n      });\n\n      it('should fire callback when a result is returned from Speech Recognition and a command was not matched', () => {\n        expect(spyOnResultNoMatch).not.toHaveBeenCalled();\n        recognition.say('That sounds like something out of science fiction');\n        expect(spyOnResultNoMatch).toHaveBeenCalledTimes(1);\n      });\n\n      it('should call the callback with the first argument containing an array of all possible Speech Recognition Alternatives the user may have said', () => {\n        expect(spyOnResultNoMatch).not.toHaveBeenCalled();\n        recognition.say('That sounds like something out of science fiction');\n        expect(spyOnResultNoMatch).toHaveBeenCalledTimes(1);\n        expect(spyOnResultNoMatch).toHaveBeenCalledWith([\n          'That sounds like something out of science fiction',\n          'That sounds like something out of science fiction and so on',\n          'That sounds like something out of science fiction and so on and so forth',\n          'That sounds like something out of science fiction and so on and so forth and so on',\n          'That sounds like something out of science fiction and so on and so forth and so on and so forth',\n        ]);\n      });\n    });\n\n    // describe('error', () => {});\n    // describe('errorNetwork', () => {});\n    // describe('errorPermissionBlocked', () => {});\n    // describe('errorPermissionDenied', () => {});\n  });\n\n  describe('result matching', () => {\n    let spyOnMatch1!: MockInstance;\n    let spyOnMatch2!: MockInstance;\n    let spyOnMatch3!: MockInstance;\n    let spyOnMatch4!: MockInstance;\n    let spyOnMatch5!: MockInstance;\n    let recognition;\n\n    beforeEach(() => {\n      spyOnMatch1 = vi.fn();\n      spyOnMatch2 = vi.fn();\n      spyOnMatch3 = vi.fn();\n      spyOnMatch4 = vi.fn();\n      spyOnMatch5 = vi.fn();\n\n      annyang.addCommands({\n        'Time for some (thrilling) heroics': spyOnMatch1,\n        'That sounds like something out of science fiction and so on and so forth': spyOnMatch2,\n        \"You can't take the :thing from me\": spyOnMatch3,\n        'We should start dealing in those *merchandise': spyOnMatch4,\n      });\n\n      annyang.start({ continuous: true });\n      recognition = annyang.getSpeechRecognizer();\n    });\n\n    it('should match when phrase matches exactly', () => {\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n      recognition.say('Time for some heroics');\n      expect(spyOnMatch1).toHaveBeenCalledTimes(1);\n    });\n\n    it('should match commands with a named variable as the last word in the sentence', () => {\n      annyang.removeCommands();\n      annyang.addCommands({\n        \"You can't take the sky from :whom\": spyOnMatch5,\n      });\n      recognition.say(\"You can't take the sky from me\");\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n\n    it('should match commands with a named variable in the middle of the sentence', () => {\n      annyang.removeCommands();\n      annyang.addCommands({\n        \"You can't take the :thing from me\": spyOnMatch5,\n      });\n      recognition.say(\"You can't take the sky from me\");\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n\n    it('should not match commands with more than one word in the position of a named variable', () => {\n      recognition.say(\"You can't take the sky from me\");\n      expect(spyOnMatch3).toHaveBeenCalledTimes(1);\n      recognition.say(\"You can't take the stuff from me\");\n      expect(spyOnMatch3).toHaveBeenCalledTimes(2);\n      recognition.say(\"You can't take the sky and stuff from me\");\n      expect(spyOnMatch3).toHaveBeenCalledTimes(2);\n    });\n\n    it('should not match commands with nothing in the position of a named variable', () => {\n      recognition.say(\"You can't take the sky from me\");\n      expect(spyOnMatch3).toHaveBeenCalledTimes(1);\n      recognition.say(\"You can't take the stuff from me\");\n      expect(spyOnMatch3).toHaveBeenCalledTimes(2);\n      recognition.say(\"You can't take the from me\");\n      expect(spyOnMatch3).toHaveBeenCalledTimes(2);\n    });\n\n    it('should pass named variables to the callback function', () => {\n      recognition.say(\"You can't take the sky from me\");\n      expect(spyOnMatch3).toHaveBeenLastCalledWith('sky');\n      recognition.say(\"You can't take the stuff from me\");\n      expect(spyOnMatch3).toHaveBeenLastCalledWith('stuff');\n    });\n\n    it('should match commands with one or more words matched by splats', () => {\n      recognition.say('We should start dealing in those beagles');\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n      recognition.say('We should start dealing in those black-market beagles');\n      expect(spyOnMatch4).toHaveBeenCalledTimes(2);\n    });\n\n    it('should match commands with nothing matched by splats', () => {\n      recognition.say('We should start dealing in those');\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n    });\n\n    it('should pass what was captured by splats to the callback function', () => {\n      recognition.say('We should start dealing in those black-market beagles');\n      expect(spyOnMatch4).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch4).toHaveBeenCalledWith('black-market beagles');\n    });\n\n    it('should match commands with optional words when the word appears in the sentence', () => {\n      recognition.say('Time for some thrilling heroics');\n      expect(spyOnMatch1).toHaveBeenCalledTimes(1);\n    });\n\n    it('should match commands with optional words when the word does not appear in the sentence', () => {\n      recognition.say('Time for some heroics');\n      expect(spyOnMatch1).toHaveBeenCalledTimes(1);\n    });\n\n    it('should not match commands with optional words when a different word is in the sentence', () => {\n      recognition.say('Time for some gorram heroics');\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n    });\n\n    it('should not break when a command is removed by another command being called', () => {\n      const spyMal: MockInstance = vi.fn(() => {\n        annyang.removeCommands();\n      });\n      const spyWash: MockInstance = vi.fn(() => {\n        annyang.removeCommands('Mal');\n      });\n\n      const commands = {\n        Mal: spyMal,\n        Wash: spyWash,\n      };\n\n      annyang.removeCommands();\n      annyang.addCommands(commands);\n\n      expect(() => {\n        recognition.say('Mal');\n      }).not.toThrowError();\n\n      annyang.addCommands(commands, true);\n\n      expect(() => {\n        recognition.say('Wash');\n      }).not.toThrowError();\n      expect(spyMal).toHaveBeenCalledTimes(1);\n      expect(spyWash).toHaveBeenCalledTimes(1);\n    });\n\n    it('should not break when a command is added by another command being called', () => {\n      const spyZoe: MockInstance = vi.fn();\n\n      const spyMal: MockInstance = vi.fn(() => {\n        annyang.addCommands({ Zoe: spyZoe });\n      });\n\n      const commands = {\n        Mal: spyMal,\n      };\n\n      annyang.addCommands(commands, true);\n\n      expect(() => {\n        recognition.say('Mal');\n      }).not.toThrowError();\n\n      expect(() => {\n        recognition.say('Zoe');\n      }).not.toThrowError();\n\n      expect(spyMal).toHaveBeenCalledTimes(1);\n      expect(spyZoe).toHaveBeenCalledTimes(1);\n    });\n\n    it('should match a commands even if the matched phrase is not the first SpeechRecognitionAlternative', () => {\n      expect(spyOnMatch2).not.toHaveBeenCalled();\n      // Our SpeechRecognition mock will create SpeechRecognitionAlternatives that append \"and so on and so forth\" to the phrase said\n      recognition.say('That sounds like something out of science fiction');\n      expect(spyOnMatch2).toHaveBeenCalledTimes(1);\n    });\n\n    it('should overwrite previously defined commands in subsequent addCommands calls if the command phrase is already registered', () => {\n      annyang.addCommands({\n        'Time for some (thrilling) heroics': spyOnMatch5,\n      });\n\n      recognition.say('Time for some thrilling heroics');\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n    });\n\n    it('should not accept callbacks passed as string names (v3 breaking change)', () => {\n      annyang.removeCommands();\n      annyang.debug();\n      annyang.addCommands({\n        // @ts-expect-error testing removed feature\n        \"You can't take the sky from me\": 'spyOnMatch1',\n      });\n      recognition.say(\"You can't take the sky from me\");\n\n      expect(spyOnMatch1).not.toHaveBeenCalled();\n    });\n\n    it('should match commands passed as a command name and an object which consists of a regular expression and a callback', () => {\n      annyang.removeCommands();\n      annyang.addCommands({\n        'It is time': {\n          regexp: /\\w* for some thrilling.*/,\n          callback: spyOnMatch5,\n        },\n      });\n\n      recognition.say('Time for some thrilling heroics');\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n      recognition.say('I feel the need for some thrilling heroics');\n      expect(spyOnMatch5).toHaveBeenCalledTimes(2);\n    });\n\n    it('should pass variables from regular expression capturing groups to the callback function', () => {\n      annyang.removeCommands();\n      annyang.addCommands({\n        'It is time': {\n          regexp: /Time for some (\\w*) (\\w*)/,\n          callback: spyOnMatch5,\n        },\n      });\n      recognition.say('Time for some thrilling heroics');\n      expect(spyOnMatch5).toHaveBeenCalledTimes(1);\n      expect(spyOnMatch5).toHaveBeenCalledWith('thrilling', 'heroics');\n    });\n\n    describe('debug messages', () => {\n      it('should write to console when a command matches if debug is on', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(true);\n        recognition.say('Time for some thrilling heroics');\n        // 5 alternatives logged + 1 command matched = 6\n        expect(logSpy).toHaveBeenCalledTimes(6);\n        expect(logSpy).toHaveBeenLastCalledWith(\n          'command matched: %cTime for some (thrilling) heroics',\n          logFormatString\n        );\n      });\n\n      it('should write to console with argument matched when command with an argument matches if debug is on', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(true);\n        recognition.say(\"You can't take the sky from me\");\n        // 5 alternatives logged + 1 command matched + 1 parameters = 7\n        expect(logSpy).toHaveBeenCalledTimes(7);\n        expect(logSpy).toHaveBeenLastCalledWith('with parameters', ['sky']);\n      });\n\n      it('should not write to console when a command matches if debug is off', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(false);\n        recognition.say('Time for some thrilling heroics');\n        expect(logSpy).not.toHaveBeenCalled();\n      });\n\n      it('should write to console each speech recognition alternative that is recognized when a command matches', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(true);\n        recognition.say('Time for some thrilling heroics');\n\n        expect(logSpy).toHaveBeenNthCalledWith(\n          1,\n          'Speech recognized: %cTime for some thrilling heroics',\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          2,\n          'Speech recognized: %cTime for some thrilling heroics and so on',\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          3,\n          'Speech recognized: %cTime for some thrilling heroics and so on and so forth',\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          4,\n          'Speech recognized: %cTime for some thrilling heroics and so on and so forth and so on',\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          5,\n          'Speech recognized: %cTime for some thrilling heroics and so on and so forth and so on and so forth',\n          logFormatString\n        );\n      });\n\n      it('should write to console each speech recognition alternative that is recognized when no command matches', () => {\n        expect(logSpy).toHaveBeenCalledTimes(0);\n        annyang.debug(true);\n        recognition.say(\"Let's do some thrilling heroics\");\n\n        expect(logSpy).toHaveBeenNthCalledWith(\n          1,\n          \"Speech recognized: %cLet's do some thrilling heroics\",\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          2,\n          \"Speech recognized: %cLet's do some thrilling heroics and so on\",\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          3,\n          \"Speech recognized: %cLet's do some thrilling heroics and so on and so forth\",\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          4,\n          \"Speech recognized: %cLet's do some thrilling heroics and so on and so forth and so on\",\n          logFormatString\n        );\n        expect(logSpy).toHaveBeenNthCalledWith(\n          5,\n          \"Speech recognized: %cLet's do some thrilling heroics and so on and so forth and so on and so forth\",\n          logFormatString\n        );\n      });\n    });\n  });\n\n  describe('getState()', () => {\n    it('should return \"idle\" when annyang has not been started', () => {\n      expect(annyang.getState()).toBe('idle');\n    });\n\n    it('should return \"listening\" when annyang is started and not paused', () => {\n      annyang.start();\n      expect(annyang.getState()).toBe('listening');\n    });\n\n    it('should return \"paused\" when annyang is paused', () => {\n      annyang.start();\n      annyang.pause();\n      expect(annyang.getState()).toBe('paused');\n    });\n\n    it('should return \"idle\" after annyang is aborted', () => {\n      annyang.start();\n      annyang.abort();\n      expect(annyang.getState()).toBe('idle');\n    });\n  });\n\n  describe('addCallback() unsubscribe', () => {\n    it('should not affect other callbacks when one is unsubscribed', () => {\n      const spy1: MockInstance = vi.fn();\n      const spy2: MockInstance = vi.fn();\n\n      const unsub1 = annyang.addCallback('start', spy1);\n      annyang.addCallback('start', spy2);\n\n      unsub1();\n\n      annyang.start();\n      expect(spy1).not.toHaveBeenCalled();\n      expect(spy2).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('duplicate addCommands()', () => {\n    it('should overwrite the callback when the same command phrase is added again', () => {\n      const spy1: MockInstance = vi.fn();\n      const spy2: MockInstance = vi.fn();\n      const recognition = annyang.getSpeechRecognizer() as CortiSpeechRecognition;\n\n      annyang.addCommands({ hello: spy1 });\n      annyang.addCommands({ hello: spy2 });\n\n      annyang.start();\n      recognition.say('hello');\n\n      expect(spy1).not.toHaveBeenCalled();\n      expect(spy2).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "test/specs/issues.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport * as annyang from '../../src/annyang.ts';\n\ndescribe('Issues', () => {\n  beforeEach(() => {\n    vi.useFakeTimers();\n  });\n\n  afterEach(() => {\n    vi.useRealTimers();\n  });\n\n  describe('#193 - Speech recognition aborting while annyang is paused', () => {\n    it('should not unpause annyang on restart', () => {\n      annyang.start({ autoRestart: true, continuous: false });\n      annyang.pause();\n      annyang.getSpeechRecognizer().abort();\n      expect(annyang.isListening()).toBe(false);\n      vi.advanceTimersByTime(2000);\n      expect(annyang.isListening()).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "test/specs/no-speech-support.test.ts",
    "content": "/**\n * Tests for environments where SpeechRecognition is NOT available.\n * This file must NOT use the Corti setup file.\n *\n * Configured via vitest workspace project \"unsupported\" in vitest.config.js.\n */\nimport { afterEach, describe, expect, it, vi } from 'vitest';\nimport * as annyang from '../../src/annyang.ts';\nimport annyangDefault from '../../src/annyang.ts';\nimport { isSpeechRecognitionSupported } from '../../src/annyang.ts';\n\ndescribe('When SpeechRecognition is not supported', () => {\n  it('globalThis.SpeechRecognition should be undefined', () => {\n    expect(globalThis.SpeechRecognition).toBeUndefined();\n    expect(globalThis.webkitSpeechRecognition).toBeUndefined();\n  });\n\n  it('isSpeechRecognitionSupported() should return false (named export)', () => {\n    expect(isSpeechRecognitionSupported()).toBe(false);\n  });\n\n  it('isSpeechRecognitionSupported() should return false (namespace import)', () => {\n    expect(annyang.isSpeechRecognitionSupported()).toBe(false);\n  });\n\n  it('isSpeechRecognitionSupported() should return false (default export)', () => {\n    expect(annyangDefault.isSpeechRecognitionSupported()).toBe(false);\n  });\n\n  it('annyang object should still be defined', () => {\n    expect(annyang).toBeDefined();\n    expect(annyangDefault).toBeDefined();\n  });\n\n  it('annyang methods should still be accessible', () => {\n    expect(annyang.addCommands).toBeInstanceOf(Function);\n    expect(annyang.start).toBeInstanceOf(Function);\n    expect(annyang.abort).toBeInstanceOf(Function);\n    expect(annyang.pause).toBeInstanceOf(Function);\n    expect(annyang.resume).toBeInstanceOf(Function);\n  });\n\n  describe('Methods should not throw', () => {\n    afterEach(() => {\n      annyang.abort();\n      annyang.removeCommands();\n      annyang.removeCallback();\n    });\n\n    it('addCommands() should not throw', () => {\n      expect(() => annyang.addCommands({ 'test command': () => {} })).not.toThrow();\n    });\n\n    it('start() should not throw', () => {\n      expect(() => annyang.start()).not.toThrow();\n    });\n\n    it('setLanguage() should not throw', () => {\n      expect(() => annyang.setLanguage('en-US')).not.toThrow();\n    });\n  });\n\n  describe('State should reflect no speech engine', () => {\n    afterEach(() => {\n      annyang.abort();\n      annyang.removeCommands();\n      annyang.removeCallback();\n    });\n\n    it('isListening() should return false after start()', () => {\n      annyang.start();\n      expect(annyang.isListening()).toBe(false);\n    });\n\n    it('state should be idle after start()', () => {\n      annyang.start();\n      expect(annyangDefault.state).toBe('idle');\n    });\n  });\n\n  describe('trigger() should work without speech recognition', () => {\n    afterEach(() => {\n      annyang.abort();\n      annyang.removeCommands();\n      annyang.removeCallback();\n    });\n\n    it('should fire a matching command callback', () => {\n      const spy = vi.fn();\n      annyang.addCommands({ 'test command': spy });\n      annyang.trigger('test command');\n      expect(spy).toHaveBeenCalled();\n    });\n\n    it('should fire the result callback', () => {\n      const spy = vi.fn();\n      annyang.addCallback('result', spy);\n      annyang.trigger('anything');\n      expect(spy).toHaveBeenCalled();\n    });\n\n    it('should fire the resultMatch callback on match', () => {\n      const spy = vi.fn();\n      annyang.addCommands({ 'test command': () => {} });\n      annyang.addCallback('resultMatch', spy);\n      annyang.trigger('test command');\n      expect(spy).toHaveBeenCalled();\n    });\n\n    it('should fire the resultNoMatch callback on no match', () => {\n      const spy = vi.fn();\n      annyang.addCommands({ 'test command': () => {} });\n      annyang.addCallback('resultNoMatch', spy);\n      annyang.trigger('something else');\n      expect(spy).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "test-manual/cjs-app.js",
    "content": "const annyang = require('annyang');\n\nconst log = msg => {\n  document.getElementById('log').textContent += msg + '\\n';\n  console.log(msg);\n};\n\nannyang.addCommands({\n  hello: () => log('Command matched: hello'),\n});\nannyang.debug(true);\nannyang.start();\nlog('✓ annyang loaded via CJS require + bundler — say \"hello\"');\n"
  },
  {
    "path": "test-manual/cjs.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>annyang CJS test</title>\n  </head>\n  <body>\n    <h1>annyang — CJS require, bundled with esbuild</h1>\n    <pre id=\"log\"></pre>\n    <script src=\"dist/cjs-app.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "test-manual/esm-app.js",
    "content": "import annyang from 'annyang';\n\nconst log = msg => {\n  document.getElementById('log').textContent += msg + '\\n';\n  console.log(msg);\n};\n\nannyang.addCommands({\n  hello: () => log('Command matched: hello'),\n});\nannyang.debug(true);\nannyang.start();\nlog('✓ annyang loaded via ESM import + bundler — say \"hello\"');\n"
  },
  {
    "path": "test-manual/esm.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>annyang ESM test</title>\n  </head>\n  <body>\n    <h1>annyang — ESM import, bundled with esbuild</h1>\n    <pre id=\"log\"></pre>\n    <script src=\"dist/esm-app.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "test-manual/iife.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>annyang IIFE test</title>\n  </head>\n  <body>\n    <h1>annyang — IIFE (script tag)</h1>\n    <pre id=\"log\"></pre>\n    <script src=\"annyang.iife.min.js\"></script>\n    <script>\n      const log = msg => {\n        document.getElementById('log').textContent += msg + '\\n';\n        console.log(msg);\n      };\n      annyang.addCommands({\n        hello: () => log('Command matched: hello'),\n      });\n      annyang.debug(true);\n      annyang.start();\n      log('✓ annyang loaded via IIFE script tag — say \"hello\"');\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "test-manual/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>annyang manual tests</title>\n  </head>\n  <body>\n    <h1>annyang manual tests</h1>\n    <p>Open devtools console, then click a test. Say \"hello\" to trigger the command.</p>\n    <ul>\n      <li><a href=\"iife.html\">IIFE (script tag)</a></li>\n      <li><a href=\"esm.html\">ESM (import → esbuild bundle)</a></li>\n      <li><a href=\"cjs.html\">CJS (require → esbuild bundle)</a></li>\n    </ul>\n  </body>\n</html>\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"skipLibCheck\": false,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"lib\": [\"ES2020\", \"DOM\"]\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { defineConfig } from 'tsup';\n\nexport default defineConfig([\n  {\n    entry: ['src/annyang.ts'],\n    format: ['esm', 'cjs'],\n    dts: true,\n    clean: true,\n    sourcemap: true,\n  },\n  {\n    entry: ['src/annyang.ts'],\n    format: ['iife'],\n    globalName: 'annyang',\n    minify: true,\n    outExtension: () => ({ js: '.iife.min.js' }),\n  },\n]);\n"
  },
  {
    "path": "typedoc.json",
    "content": "{\n  \"$schema\": \"https://typedoc.org/schema.json\",\n  \"entryPoints\": [\"src/annyang.ts\"],\n  \"plugin\": [\"typedoc-plugin-markdown\"],\n  \"out\": \"docs\",\n  \"outputFileStrategy\": \"modules\",\n  \"cleanOutputDir\": false,\n  \"readme\": \"none\",\n  \"excludeNotDocumented\": true\n}\n"
  },
  {
    "path": "vitest.config.js",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    projects: [\n      {\n        test: {\n          name: 'supported',\n          setupFiles: './test/setupTests.js',\n          include: ['test/specs/annyang.test.ts', 'test/specs/issues.test.ts'],\n        },\n      },\n      {\n        test: {\n          name: 'unsupported',\n          include: ['test/specs/no-speech-support.test.ts'],\n        },\n      },\n    ],\n  },\n});\n"
  }
]