diff --git a/.eslintignore b/.eslintignore index 03546876e2..7b1522852f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,11 @@ docs/examples/ # 3rd party src/interpreter/plugin/3rdparty +# A byte-identical copy of handsontable/license-key's vendor/entitlement-key-reader - see +# PROVENANCE.md in that directory. Linting it would mean editing it, and editing it is the one +# thing it must not have; upstream runs strict tsc on it instead. +src/license/handsontable-license-key-parser/*.ts + # Configurations *.config.js karma.* diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 98f8337238..9898efe1b2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,7 +20,7 @@ ### Checklist: -- [ ] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/guide/contributing.html) and I confirm that my code follows the code style of this project. +- [ ] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html) and I confirm that my code follows the code style of this project. - [ ] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [ ] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. - [ ] My change is compatible with Microsoft Excel. diff --git a/.github/workflows/vendored-parser.yml b/.github/workflows/vendored-parser.yml new file mode 100644 index 0000000000..7202b0e694 --- /dev/null +++ b/.github/workflows/vendored-parser.yml @@ -0,0 +1,42 @@ +name: Vendored parser + +# The entitlement-key reader under src/license/handsontable-license-key-parser/ is a verbatim +# copy of a private upstream directory. This job is the drift check that copy's integration guide +# asks for: it fails when any byte differs from the pinned tag, when a file shadows the copy, or +# when upstream has released a newer tag. See PROVENANCE.md in that directory. +# +# It needs LICENSE_KEY_REPO_TOKEN - a read-only token for handsontable/license-key - as a repository +# secret. Without it the check fails on purpose ("could not verify" is not "verified"), so this job +# stays red until the secret is set. Pull requests from forks never receive secrets and are skipped. + +on: + pull_request: + types: [ opened, reopened, synchronize ] + paths: + - 'src/license/handsontable-license-key-parser/**' + - 'script/check-license-key-parser-drift.js' + - '.github/workflows/vendored-parser.yml' + push: + branches: [ master, develop ] + schedule: + - cron: '17 6 * * 1-5' # weekday mornings: a newer upstream tag is news even when nothing here changed + +permissions: + contents: read + +jobs: + drift: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598 # https://github.com/actions/checkout/releases/tag/v2.0.0 + - name: Setup Node.js 22 + uses: actions/setup-node@56899e050abffc08c2b3b61f3ec6a79a9dc3223d # https://github.com/actions/setup-node/releases/tag/v1.4.4 + with: + node-version: '22' + - name: Compare the vendored reader with upstream + run: node script/check-license-key-parser-drift.js + env: + LICENSE_KEY_REPO_TOKEN: ${{ secrets.LICENSE_KEY_REPO_TOKEN }} diff --git a/.typedoc.ts b/.typedoc.ts index 0001ced876..d7d1f28152 100644 --- a/.typedoc.ts +++ b/.typedoc.ts @@ -7,6 +7,7 @@ module.exports = { "./src/dependencyTransformers/**", "./src/DependencyGraph/**", "./src/ColumnSearch/**", + "./src/license/handsontable-license-key-parser/**", ], "mode": "file", "out": "./typedoc", diff --git a/AGENTS.md b/AGENTS.md index 99b25536ca..c81e681c12 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,19 @@ Instructions for AI coding agents (Cursor, Claude Code, Codex, Aider, and any ot Whatever you do, start by reading entire [DEV_DOCS.md](DEV_DOCS.md). Only then proceed to your task. +## Never publish sensitive information + +Never write any of the following into a commit message, branch name, pull request title or description, GitHub issue or comment, code comment, changelog entry, or documentation page: + +- client, customer, and partner names, or details that identify them indirectly (their domains, deployments, or the wording of their reports) +- personal data of any kind — names, e-mail addresses, phone numbers, user accounts, IP addresses +- credentials and secrets — API keys, tokens, passwords, license keys, private URLs +- internal-only material — contents of private repositories and internal tickets, unreleased plans, contract and pricing details + +Describe the change on its own technical terms instead: write "fix an off-by-one error in `SUMIFS` when the criteria range is empty", not "fix the bug reported by \". An internal ticket identifier such as `HF-123` is fine on its own; the contents of that ticket are not. + +If a change cannot be described without such information, stop and ask the user how to proceed. + ## Other important resources - the repository [README.md](README.md) — high-level project description and quick install/usage diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ecffc1c4..b68c77419b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +## [3.5.0] - 2026-10-09 + +### Added + +- Added support for the new license key format. A proprietary key can now grant a subset of the library: a function your key does not include evaluates to a `#LIC!` error, and the matching parts of the API throw a `LicenseCapabilityMissingError`. `getAvailableFunctions()` and `getFunctionDetails()` describe only the functions your key includes. [#1728](https://github.com/handsontable/hyperformula/pull/1728) + +### Changed + +- Changed the `VERSION` function to return only the HyperFormula version (e.g. `HyperFormula v3.4.0`), without the license key status. [#1728](https://github.com/handsontable/hyperformula/pull/1728) +- Changed the API methods gated by the license key to throw a `LicenseCapabilityMissingError` when the key is missing or invalid, when a classic key has expired, or when a trial key is past its grace period. The gated methods are the ones that edit cells, rows, columns, and sheets, `copy()`, `cut()`, `paste()`, `undo()`, `redo()`, `batch()`, `suspendEvaluation()`, and the methods that add, change, or remove named expressions. Building an engine with named expressions throws the same error, and the matching `isItPossibleTo*()` methods, `isThereSomethingToUndo()`, and `isThereSomethingToRedo()` return `false`. [#1728](https://github.com/handsontable/hyperformula/pull/1728) +- Changed the `MAXPOOL` and `MEDIANPOOL` functions to accept a stride greater than the window size in a cell, where they returned the `#VALUE!` error before. The windows then skip the rows and columns between them, as `calculateFormula()` already did. [#1718](https://github.com/handsontable/hyperformula/pull/1718) + +### Fixed + +- Fixed the validation of classic (25-character) license keys depending on the time zone: east of UTC, a key that expired the day before the build was released was still accepted, and west of UTC, the console message printed an expiry date one day too early. [#1728](https://github.com/handsontable/hyperformula/pull/1728) +- Fixed the `AVERAGEIF` function returning a division-by-zero error when the calculated average was `0`. [#1733](https://github.com/handsontable/hyperformula/pull/1733) +- Fixed the localized names of `VSTACK` and `HSTACK` in 14 language packs to match Microsoft Excel. [#1748](https://github.com/handsontable/hyperformula/pull/1748) +- Fixed the `MAXPOOL` and `MEDIANPOOL` functions throwing an uncaught `TypeError` or `RangeError`. They now return the `#VALUE!` error when the window is larger than the range or the range dimensions, reduced by the window size, are not whole multiples of the stride, and the `#NUM!` error when the window size or the stride is not a positive integer. [#1718](https://github.com/handsontable/hyperformula/pull/1718) +- Fixed the `VAR`, `STDEV`, `DEVSQ`, `COVARIANCE.P`, `COVARIANCE.S`, `SLOPE`, `STEYX`, `DVAR` and `DSTDEV` functions, their variants, and the matching `SUBTOTAL` modes losing precision on data with a large mean and a small spread. [#1784](https://github.com/handsontable/hyperformula/pull/1784) +- Fixed a bug where `SLOPE` and `STEYX` returned `#NUM!` or an arbitrary number instead of `#DIV/0!` when all the x values are equal. [#1784](https://github.com/handsontable/hyperformula/pull/1784) +- Fixed a bug where `STEYX` returned `#NUM!` or `0` instead of the standard error for points that lie almost on a line. [#1784](https://github.com/handsontable/hyperformula/pull/1784) +- Fixed a bug where `VAR`, `STDEV`, `DVAR`, `DSTDEV`, `COVARIANCE.P`, `COVARIANCE.S`, `SLOPE`, `STEYX`, their variants, and the matching `SUBTOTAL` modes returned an error or `0` for very large or very small values although the result was within the range of numbers, and `DEVSQ` returned `0` for very small values. [#1784](https://github.com/handsontable/hyperformula/pull/1784) +- Fixed the `MOD` function returning a remainder with the sign of the dividend instead of the sign of the divisor, which made the results differ from Excel and Google Sheets for arguments with opposite signs (e.g. `=MOD(-3, 12)` now returns `9` instead of `-3`). [#1747](https://github.com/handsontable/hyperformula/issues/1747) +- Fixed a bug where moving or pasting a formula with an undefined name to another sheet incorrectly added an empty global named expression. [#1728](https://github.com/handsontable/hyperformula/pull/1728) + ## [3.4.0] - 2026-08-10 ### Added @@ -242,7 +267,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Removed - Removed all polyfills from the CommonJS build and the ES modules build. In the UMD build, kept only the polyfills - required by the [supported browsers](https://hyperformula.handsontable.com/guide/supported-browsers.html). + required by the [supported browsers](https://hyperformula.handsontable.com/docs/guide/supported-browsers.html). [#1011](https://github.com/handsontable/hyperformula/issues/1011) ## [2.0.1] - 2022-06-14 @@ -261,9 +286,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), For more information on this release, see: -- [Release notes](https://hyperformula.handsontable.com/guide/release-notes.html) +- [Release notes](https://hyperformula.handsontable.com/docs/guide/release-notes.html) - [Blog post](https://handsontable.com/blog/articles/2022/04/whats-new-in-hyperformula-2.0.0) -- [Migration guide](https://hyperformula.handsontable.com/guide/migration-from-1.0-to-2.0.html) +- [Migration guide](https://hyperformula.handsontable.com/docs/guide/migration-from-1.x-to-2.0.html) ### Added diff --git a/DEV_DOCS.md b/DEV_DOCS.md index 13630fd4cb..daa15138f5 100644 --- a/DEV_DOCS.md +++ b/DEV_DOCS.md @@ -23,7 +23,9 @@ Canonical reference for everyone working on the HyperFormula source code: mainta │ │ └── plugin/ # Built-in spreadsheet function plugins │ ├── DependencyGraph/ # Cell dependency tracking and recalculation order │ ├── CrudOperations.ts # Create/read/update/delete operations on sheets and cells -│ └── i18n/ # Function-name translations per language +│ ├── i18n/ # Function-name translations per language +│ └── license/ # License key reading, capability tables, and license gating +│ └── handsontable-license-key-parser/ # Vendored key reader; do not edit ├── test/ # Test suite ├── docs/ # Public documentation portal (VuePress) │ ├── guide/ # Markdown guides (building, contributing, usage…) @@ -51,6 +53,13 @@ Canonical reference for everyone working on the HyperFormula source code: mainta - `src/interpreter/` — formula evaluation engine - `src/DependencyGraph/` — cell dependency tracking and recalculation order - `src/CrudOperations.ts` — create/read/update/delete operations on sheets and cells +- `src/license/` — license key handling: reads the key, maps its capability tokens to functions (`functionCapabilities.ts`) and public API features (`featureCapabilities.ts`), and gates function calls and API methods + +### License key reader (`src/license/handsontable-license-key-parser/`) + +A copy of the entitlement-key reader published by the `handsontable/license-key` repository, taken whole from a tagged release. Don't edit it: fix the problem upstream and take the copy again, as described in `PROVENANCE.md` in that directory. ESLint ignores the directory. + +`npm run check:license-key-parser-drift` compares the copy with the pinned upstream commit and fails on any difference or on a newer upstream tag. It needs read access to the upstream repository, so it runs in its own CI workflow rather than in `npm run test` or `npm run lint`. ### Function plugins (`src/interpreter/plugin/`) @@ -125,8 +134,9 @@ Adding a built-in function is similar to adding a [custom function](docs/guide/c 2. Add function metadata to `implementedFunctions`. 3. Implement the function method. 4. Add a catalogue entry to `src/interpreter/functionMetadata/categories/.ts` (see below). -5. Add translations to all language files in `src/i18n/languages/`. -6. Add tests in `test/unit/interpreter/`. +5. Add the function to the license capability table in `src/license/functionCapabilities.ts` (see below). +6. Add translations to all language files in `src/i18n/languages/`. +7. Add tests in `test/unit/interpreter/`. ### The function metadata catalogue @@ -153,17 +163,35 @@ It does **not** turn ordinary English into identifiers. A parameter's own descri Note what the drift warning does **not** cover: **optionality is not cross-checked.** The catalogue authors no optionality of its own — a parameter's `optional` flag is derived entirely from `optionalArg`/`defaultValue` in `implementedFunctions` — so a description that calls an argument optional can sit next to `optional: false` with nothing failing. When a function accepts a call that arity alone does not express (`SHEET()`, `ROW()`, and anything else served by `runFunctionWithReferenceArgument`'s zero-argument path), the plugin must declare `optionalArg: true` explicitly, or the public API will advertise the argument as required. `ROW`, `COLUMN`, `SHEET` and `SHEETS` all declare it; `ISFORMULA` takes the same path and correctly does not, because its zero-argument call is an error rather than a shorthand. -Descriptions must describe **HyperFormula's** behaviour, not Excel's. Much of the catalogue was seeded from a hand-written page that documented Excel, and HyperFormula deliberately deviates in places (`INT` truncates toward zero, `MOD` takes the sign of the dividend, `ISEVEN`/`ISODD` do not truncate, `CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1). Verify a claim against the implementation before authoring it, and record any deviation in [the list of differences](docs/guide/list-of-differences.md). +Descriptions must describe **HyperFormula's** behaviour, not Excel's. Much of the catalogue was seeded from a hand-written page that documented Excel, and HyperFormula deliberately deviates in places (`INT` truncates toward zero, `ISEVEN`/`ISODD` do not truncate, `CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1). Verify a claim against the implementation before authoring it, and record any deviation in [the list of differences](docs/guide/list-of-differences.md). + +### The license capability table + +`src/license/functionCapabilities.ts` decides which license keys can call a built-in function. Add every new built-in either to its group in `FUNCTION_GROUPS` or, if it belongs to no group, to `UNGROUPED_FUNCTIONS`. An ungrouped function is granted only by `fun:all` and by its own `fun:` token. Both lists use canonical names: an alias needs no entry, because it is gated as its canonical function. + +A built-in missing from the table escapes the entitlement check: every key that lets formulas evaluate can call it, including a key that grants no functions. The completeness check in the private test suite (`unit/license/capability-registry.spec.ts`) fails on such a function, but the public smoke tests do not. + +Group names are part of the license key format, so never rename a group or remove a function from one. Adding a function to a group grants it to every key that already carries that group's token. ## Internationalization and function translations HyperFormula supports internationalization and provides localized function names for all built-in languages. Translation files live in `src/i18n/languages/`. New functions must include translations for all built-in languages. +Only add a localized function name after confirming that Microsoft Excel ships that exact name. If an authoritative source does not provide a localized name, keep the English name instead of translating or inferring one. + When looking for the valid translations for new functions, try these sources: - https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889 - http://dolf.trieschnigg.nl/excel/index.php +For recent Excel functions that are absent from those sources, use Microsoft's localized alphabetical function list: + +```text +https://support.microsoft.com//office/excel-functions-alphabetical-b3944572-255d-4efb-bb96-c6d90033e188 +``` + +Find the link whose URL contains `functions/-function`, then follow it and confirm that the individual function page uses the same localized name in its title and formula syntax. Check each locale independently because Excel keeps some function names in English. If the alphabetical list and the individual page disagree, do not copy the list entry without additional product verification. + For languages not officially supported by Microsoft Excel, the two sources above do not apply. For these languages, use Google Sheets as the reference. Switch the `hl` query parameter to the target locale, for example: - https://support.google.com/docs/table/25273?hl=id (Indonesian) diff --git a/DOCS_CONTENT_GUIDE.md b/DOCS_CONTENT_GUIDE.md index 899c402fe8..08d160f128 100644 --- a/DOCS_CONTENT_GUIDE.md +++ b/DOCS_CONTENT_GUIDE.md @@ -220,6 +220,13 @@ and hallucinate the parts you omit. description: Register and use your own functions in HyperFormula. --- ``` +- Add a `tags` frontmatter **list** to make a page findable by words it does not use in +its title or its `##`/`###` headings — the search box matches those three things only, +never the body text (see `docs/guide/setup-coding-agent.md`, tagged `skills`, `Cursor`, +`MCP`). Keep it to at most 10 tags a page, each one a word a reader would really type, and +leave a query to the page that answers it best — a tag ten pages claim is decided by file +order, not relevance, once the 10-result limit bites. A plain string instead of a list breaks +the search box site-wide. - Use containers for asides: `::: tip`, `::: warning`, `::: danger` … `:::`. Put essential steps in the body, not hidden inside a tip. - Use **relative links** between guide pages (`./named-expressions.md`) and link to diff --git a/README.md b/README.md index f1a7b08467..2a3070c8cd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ --- - HyperFormula is a headless spreadsheet built in TypeScript, serving as both a parser and evaluator of spreadsheet formulas. It can be integrated into your browser or utilized as a service with Node.js as your back-end technology. ## What HyperFormula can be used for? @@ -39,36 +38,36 @@ HyperFormula doesn't assume any existing user interface, making it a general-pur ## Features -- [Function syntax compatible with Microsoft Excel](https://hyperformula.handsontable.com/guide/compatibility-with-microsoft-excel.html) and [Google Sheets](https://hyperformula.handsontable.com/guide/compatibility-with-google-sheets.html) +- [Function syntax compatible with Microsoft Excel](https://hyperformula.handsontable.com/docs/guide/compatibility-with-microsoft-excel.html) and [Google Sheets](https://hyperformula.handsontable.com/docs/guide/compatibility-with-google-sheets.html) - High-speed parsing and evaluation of spreadsheet formulas -- [A library of ~400 built-in functions](https://hyperformula.handsontable.com/guide/built-in-functions.html) -- [Support for custom functions](https://hyperformula.handsontable.com/guide/custom-functions.html) -- [Support for Node.js](https://hyperformula.handsontable.com/guide/server-side-installation.html#install-with-npm-or-yarn) -- [Support for undo/redo](https://hyperformula.handsontable.com/guide/undo-redo.html) -- [Support for CRUD operations](https://hyperformula.handsontable.com/guide/basic-operations.html) -- [Support for clipboard](https://hyperformula.handsontable.com/guide/clipboard-operations.html) -- [Support for named expressions](https://hyperformula.handsontable.com/guide/named-expressions.html) -- [Support for data sorting](https://hyperformula.handsontable.com/guide/sorting-data.html) -- [Support for formula localization with 17 built-in languages](https://hyperformula.handsontable.com/guide/i18n-features.html) +- [A library of over 400 built-in functions](https://hyperformula.handsontable.com/docs/guide/built-in-functions.html) +- [Support for custom functions](https://hyperformula.handsontable.com/docs/guide/custom-functions.html) +- [Support for Node.js](https://hyperformula.handsontable.com/docs/guide/server-side-installation.html#install-with-npm-or-yarn) +- [Support for undo/redo](https://hyperformula.handsontable.com/docs/guide/undo-redo.html) +- [Support for CRUD operations](https://hyperformula.handsontable.com/docs/guide/basic-operations.html) +- [Support for clipboard](https://hyperformula.handsontable.com/docs/guide/clipboard-operations.html) +- [Support for named expressions](https://hyperformula.handsontable.com/docs/guide/named-expressions.html) +- [Support for data sorting](https://hyperformula.handsontable.com/docs/guide/sorting-data.html) +- [Support for formula localization with 18 built-in languages](https://hyperformula.handsontable.com/docs/guide/i18n-features.html) - Easy integration with any front-end or back-end application - GPLv3 or a [commercial license](https://handsontable.com/get-a-quote) - Maintained by the team that stands behind the [Handsontable](https://handsontable.com/) data grid ## Documentation -- [Client-side installation](https://hyperformula.handsontable.com/guide/client-side-installation.html) -- [Server-side installation](https://hyperformula.handsontable.com/guide/server-side-installation.html) -- [Basic usage](https://hyperformula.handsontable.com/guide/basic-usage.html) -- [Configuration options](https://hyperformula.handsontable.com/guide/configuration-options.html) -- [List of built-in functions](https://hyperformula.handsontable.com/guide/built-in-functions.html) -- [API Reference](https://hyperformula.handsontable.com/api/) +- [Client-side installation](https://hyperformula.handsontable.com/docs/guide/client-side-installation.html) +- [Server-side installation](https://hyperformula.handsontable.com/docs/guide/server-side-installation.html) +- [Basic usage](https://hyperformula.handsontable.com/docs/guide/basic-usage.html) +- [Configuration options](https://hyperformula.handsontable.com/docs/guide/configuration-options.html) +- [List of built-in functions](https://hyperformula.handsontable.com/docs/guide/built-in-functions.html) +- [API Reference](https://hyperformula.handsontable.com/docs/api/) ## Integrations -- [Integration with React](https://hyperformula.handsontable.com/guide/integration-with-react.html#demo) -- [Integration with Angular](https://hyperformula.handsontable.com/guide/integration-with-angular.html#demo) -- [Integration with Vue](https://hyperformula.handsontable.com/guide/integration-with-vue.html#demo) -- [Integration with Svelte](https://hyperformula.handsontable.com/guide/integration-with-svelte.html#demo) +- [Integration with React](https://hyperformula.handsontable.com/docs/guide/integration-with-react.html#demo) +- [Integration with Angular](https://hyperformula.handsontable.com/docs/guide/integration-with-angular.html#demo) +- [Integration with Vue](https://hyperformula.handsontable.com/docs/guide/integration-with-vue.html#demo) +- [Integration with Svelte](https://hyperformula.handsontable.com/docs/guide/integration-with-svelte.html#demo) ## Installation and usage @@ -102,11 +101,13 @@ hf.setCellContents({ sheet: sheetId, row: 0, col: 0 }, [['Monthly Payment', '=PM console.log(`${hf.getCellValue({ sheet: sheetId, row: 0, col: 0 })}: ${hf.getCellValue({ sheet: sheetId, row: 0, col: 1 })}`); ``` -[Run this code in StackBlitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.4.x/mortgage-calculator) +[Run this code in StackBlitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.5.x/mortgage-calculator) + +HyperFormula ships an official Claude skill and machine-readable docs, so your AI coding agent can scaffold, configure, and debug HyperFormula correctly. To install the skill in Claude Code, or to point Cursor, GitHub Copilot, or another agent at the docs, see [Set up your coding agent](https://hyperformula.handsontable.com/docs/guide/setup-coding-agent.html). You can also connect any MCP-capable agent to the first-party [docs MCP server](https://handsontable.com/docs/javascript-data-grid/docs-mcp-server/) — semantic search over the full HyperFormula and Handsontable knowledge base: docs guides, API reference, code recipes, release notes, blog posts, and GitHub issues, always current with the latest release. ## Contributing -Contributions are welcome, but before you make them, please read the [Contributing Guide](https://hyperformula.handsontable.com/guide/contributing.html) and accept the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). +Contributions are welcome, but before you make them, please read the [Contributing Guide](https://hyperformula.handsontable.com/docs/guide/contributing.html) and accept the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). ## License diff --git a/codecov.yml b/codecov.yml index acc230a501..9610511d8d 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,6 +6,13 @@ coverage: round: down precision: 2 +# src/license/handsontable-license-key-parser/ is a byte-identical copy of handsontable/license-key's +# vendor/entitlement-key-reader (see PROVENANCE.md there). Upstream tests it; HyperFormula only calls +# part of it and may not edit it, so its coverage says nothing about this repository. It is left in the +# local jest report on purpose - only the codecov statuses ignore it. +ignore: + - "src/license/handsontable-license-key-parser/**" + comment: layout: "reach, diff, flags, files" behavior: new diff --git a/docs/.vuepress/components/CodingAgentWizard.vue b/docs/.vuepress/components/CodingAgentWizard.vue index 6eb24284a4..a91d3ba87d 100644 --- a/docs/.vuepress/components/CodingAgentWizard.vue +++ b/docs/.vuepress/components/CodingAgentWizard.vue @@ -40,8 +40,8 @@ export default { { id: 'cursor', label: 'Cursor', - snippet: 'Add to your AGENTS.md / rules file:\nHyperFormula docs (LLM-friendly): https://hyperformula.handsontable.com/docs/llms-full.txt', - note: 'Cursor has no Claude-skill installer yet — point it at the full docs corpus instead.', + snippet: 'Add to .cursor/mcp.json:\n{\n "mcpServers": {\n "handsontable-docs": { "url": "https://docs-assistant.handsontable.com/mcp" }\n }\n}', + note: 'Connects Cursor to the first-party docs MCP server (docs guides, API reference, code recipes, release notes, blog posts, and GitHub issues). Alternatively, point a rules file at the full docs corpus: https://hyperformula.handsontable.com/docs/llms-full.txt.', }, { id: 'copilot', @@ -53,7 +53,7 @@ export default { id: 'other', label: 'Other / API', snippet: 'curl -s https://hyperformula.handsontable.com/docs/llms-full.txt', - note: 'Fetch the full corpus, or upload the skill folder from handsontable/handsontable-skills to the Claude API.', + note: 'Fetch the full corpus, upload the skill folder from handsontable/handsontable-skills to the Claude API, or connect any MCP-capable agent to https://docs-assistant.handsontable.com/mcp.', }, ], }; diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 3f6b6864d1..5caadb41a7 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -4,6 +4,77 @@ const footnotePlugin = require('markdown-it-footnote'); const searchBoxPlugin = require('./plugins/search-box'); const examples = require('./plugins/examples/examples'); const HyperFormula = require('../../dist/hyperformula.full'); +const fs = require('fs'); +const path = require('path'); + +// HF-282: count HF built-in languages from `src/i18n/languages` (source of truth): one module +// per shipped language, next to the `index.ts` export barrel. Counted by listing the directory +// rather than by matching `export {default as xxYY}` lines in the barrel, so the total cannot +// depend on how those lines are punctuated: `export { default as ukUA }` is the same export to +// TypeScript, but a regex anchored on the brace spacing misses it and then quietly publishes a +// stale total with every check below still green. Read once at config load — no dist/ +// dependency, so it resolves identically in `docs:dev` and `docs:build`. +const languagesDir = path.resolve(__dirname, '../../src/i18n/languages'); +const languageCodes = fs.readdirSync(languagesDir) + .filter((file) => file.endsWith('.ts') && !file.endsWith('.d.ts') && file !== 'index.ts') + .map((file) => path.basename(file, '.ts')); +const languagesCount = languageCodes.length; +if (!languagesCount) { + throw new Error(`HF-282: derived languagesCount is 0 — no language modules found in ${languagesDir}; check the path in docs/.vuepress/config.js.`); +} +// Listing the directory counts what is *present*, and only the barrel decides what actually +// ships, so a module nobody re-exported would overstate the total. Cross-check by language code +// (no brace matching, so barrel formatting stays irrelevant) and fail loudly on the mismatch. +const languagesBarrel = fs.readFileSync(path.join(languagesDir, 'index.ts'), 'utf8'); +const unexportedLanguages = languageCodes.filter((code) => !new RegExp(`\\bdefault as ${code}\\b`).test(languagesBarrel)); +if (unexportedLanguages.length) { + throw new Error(`HF-282: language modules present in src/i18n/languages/ but not re-exported from index.ts, so they do not ship and must not be counted: ${unexportedLanguages.join(', ')} — add them to the barrel, or remove the files.`); +} + +// HF-282: the root README.md is rendered by GitHub and npm, not VuePress, so it cannot +// use the `{{ $page.languagesCount }}` interpolation and states the count literally. +// Assert it against the count derived above so it cannot rot unnoticed — it already did once, when +// the Indonesian pack (#1674) left the README saying 17. The function count needs no +// such check: "over 400" stays true as functions are added. +// +// The claim under test is one specific line: the features bullet linking to the i18n guide. Matched +// there rather than loose against the whole file, because `String.match` without /g returns the +// first hit anywhere — inside a fenced example, or in a sentence about an older release — and would +// then report a number from a line that was never the claim, sending the reader to correct text +// that is already right. Fenced blocks are skipped for the same reason. +const readmeLanguagesClaims = []; +let inReadmeFence = false; +for (const line of fs.readFileSync(path.resolve(__dirname, '../../README.md'), 'utf8').split('\n')) { + if (/^\s*(```|~~~)/.test(line)) { + inReadmeFence = !inReadmeFence; + continue; + } + if (inReadmeFence || !line.startsWith('- ') || !line.includes('guide/i18n-features')) { + continue; + } + const match = line.match(/(\d+) built-in languages/); + if (match) { + readmeLanguagesClaims.push(match[1]); + } +} +if (readmeLanguagesClaims.length !== 1) { + throw new Error(`HF-282: expected exactly one README.md features bullet linking to the i18n guide and stating " built-in languages", found ${readmeLanguagesClaims.length} — if the wording changed on purpose, update this check in docs/.vuepress/config.js.`); +} +if (Number(readmeLanguagesClaims[0]) !== languagesCount) { + throw new Error(`HF-282: README.md says ${readmeLanguagesClaims[0]} built-in languages but src/i18n/languages/ ships ${languagesCount} — update README.md.`); +} + +// HF-282: the function total. Derived from `getAvailableFunctions` on a default-config engine — +// the same API, and the same engine options, as `script/generate-builtin-functions-doc.ts`, so the +// total and the rows of the page it heads cannot describe different function sets. Default-config +// is the point: `functionPlugins` restrictions would give a narrower count than the generated +// table. The GPLv3 key only keeps the build quiet — a keyless engine logs a missing-key warning — +// but see the LICENSE_KEY note in that script for why it has to stay the fully-entitled one. +// Built once here, not per page: `extendPageData` runs for every page and the count is invariant. +const functionsCount = HyperFormula + .buildEmpty({language: 'enGB', licenseKey: 'gpl-v3'}) + .getAvailableFunctions().length; + const includeCodeSnippet = require('./plugins/markdown-it-include-code-snippet'); const mdCompanions = require('./plugins/md-companions'); @@ -112,11 +183,9 @@ module.exports = { // inject current HF releaseDate as {{ $page.releaseDate }} variable $page.releaseDate = HyperFormula.releaseDate // inject current HF function count as {{ $page.functionsCount }} variable - // This total and the rows of the built-in functions page come from two different sources: the count below, - // and getAvailableFunctions via script/renderBuiltinFunctionsTable.ts. They agree today (423 each) and must - // be kept in step by hand, or the page prints a total that contradicts the number of rows under it. The - // renderer throws on the one divergence it can see from its side; this side cannot detect any. - $page.functionsCount = HyperFormula.getRegisteredFunctionNames('enGB').length + $page.functionsCount = functionsCount + // inject current HF built-in language count as {{ $page.languagesCount }} variable + $page.languagesCount = languagesCount if (searchPattern.test($page.path) || generatedPagePattern.test($page.path)) { $page.frontmatter.editLink = false diff --git a/docs/.vuepress/plugins/md-companions/index.js b/docs/.vuepress/plugins/md-companions/index.js index a1a89362df..35d5ba0c04 100644 --- a/docs/.vuepress/plugins/md-companions/index.js +++ b/docs/.vuepress/plugins/md-companions/index.js @@ -13,7 +13,7 @@ const { stripVuePressSyntax } = require('./strip'); */ function resolvePageVars(md, page) { return md.replace( - /\{\{\s*\$page\.(version|buildDate|buildDateURIEncoded|releaseDate|functionsCount)\s*\}\}/g, + /\{\{\s*\$page\.(version|buildDate|buildDateURIEncoded|releaseDate|functionsCount|languagesCount)\s*\}\}/g, (m, key) => (page && page[key] != null ? String(page[key]) : m) ); } diff --git a/docs/api-ref-readme.md b/docs/api-ref-readme.md index 69f252437f..dc647c341c 100644 --- a/docs/api-ref-readme.md +++ b/docs/api-ref-readme.md @@ -43,7 +43,7 @@ For example, subscribing to `sheetAdded` event: const hfInstance = HyperFormula.buildFromSheets({ MySheet1: [ ['1'] ], MySheet2: [ ['10'] ], -}); +}, { licenseKey: 'gpl-v3' }); const handler = ( ) => { console.log('baz') } diff --git a/docs/guide/advanced-usage.md b/docs/guide/advanced-usage.md index 7ccb1f64c1..21934c647e 100644 --- a/docs/guide/advanced-usage.md +++ b/docs/guide/advanced-usage.md @@ -1,3 +1,11 @@ +--- +tags: + - buildEmpty + - simpleCellAddressFromString + - getSheetValues + - cross-sheet +--- + # Advanced usage ::: tip diff --git a/docs/guide/ai-sdk.md b/docs/guide/ai-sdk.md index e38f89c524..7eb0b7c593 100644 --- a/docs/guide/ai-sdk.md +++ b/docs/guide/ai-sdk.md @@ -1,3 +1,14 @@ +--- +tags: + - AI agents + - LLM + - tool calling + - deterministic + - createSpreadsheetTools + - OpenAI + - what-if analysis +--- + # HyperFormula AI SDK for Vercel A [Vercel AI SDK](https://sdk.vercel.ai/docs) tool that gives your agents deterministic spreadsheet and formula computation — backed by HyperFormula's Excel-compatible engine. @@ -13,7 +24,7 @@ If you'd like to try it, [join the early access list](https://2fmjvg.share-eu1.h - **Evaluate formulas deterministically** — your agent runs any Excel-compatible formula through HyperFormula instead of asking the LLM to do math. Results are exact, reproducible, and auditable. - **Read and write cells and ranges** — the agent inspects, populates, or modifies sheet data through typed tool calls. - **Trace dependencies** — precedents and dependents are surfaced so the agent can explain how every value was derived. -- **400+ built-in functions out of the box** — the agent has access to the full Excel-compatible function set (`SUM`, `VLOOKUP`, `IRR`, `INDEX/MATCH`, and the rest), no implementation work required. +- **{{ $page.functionsCount }} built-in functions out of the box** — the agent has access to the full Excel-compatible function set (`SUM`, `VLOOKUP`, `IRR`, `INDEX/MATCH`, and the rest), no implementation work required. ## Example @@ -30,7 +41,7 @@ const hf = HyperFormula.buildFromArray([ ['Revenue', 100], ['Cost', 60], ['Profit', '=B1-B2'], -]); +], { licenseKey: 'gpl-v3' }); // Pass the spreadsheet tools straight into generateText. const result = await generateText({ diff --git a/docs/guide/arrays.md b/docs/guide/arrays.md index dc02b87b60..a049ad2283 100644 --- a/docs/guide/arrays.md +++ b/docs/guide/arrays.md @@ -1,3 +1,13 @@ +--- +tags: + - ARRAYFORMULA + - useArrayArithmetic + - ARRAY_CONSTRAIN + - spilling + - arrayRowSeparator + - arrayColumnSeparator +--- + # Array formulas Use array formulas to perform an operation (or call a function) on multiple cells at a time. diff --git a/docs/guide/basic-operations.md b/docs/guide/basic-operations.md index f6c25fd7cb..b49f000bc7 100644 --- a/docs/guide/basic-operations.md +++ b/docs/guide/basic-operations.md @@ -1,3 +1,16 @@ +--- +tags: + - CRUD + - insert rows + - delete rows + - setCellContents + - SimpleCellAddress + - A1 notation + - workbook + - sheet tabs + - NoSheetWithIdError +--- + # Basic operations HyperFormula can perform efficient **CRUD** operations on the workbook. @@ -343,7 +356,7 @@ easily check if that action is allowed, and if it is not, throw an error. // an instance with some example data const hfInstance = HyperFormula.buildFromArray([ ['1', '2'], -]); +], { licenseKey: 'gpl-v3' }); // a variable used to carry the message for the user let messageUsedInUI; @@ -382,7 +395,7 @@ const hf = HyperFormula.buildFromArray([ [1], ['=SUM(A1:A2)'], ['=COUNTBLANK(A1:A3)'], -]); +], { licenseKey: 'gpl-v3' }); // insert an empty row between the row 0 and the row 1 const changes = hf.addRows(0, [1, 1]); diff --git a/docs/guide/basic-usage.md b/docs/guide/basic-usage.md index d1b392c9ec..b3cd196604 100644 --- a/docs/guide/basic-usage.md +++ b/docs/guide/basic-usage.md @@ -1,3 +1,12 @@ +--- +tags: + - getting started + - quickstart + - tutorial + - buildFromArray + - buildFromSheets +--- + # Basic usage ::: tip diff --git a/docs/guide/batch-operations.md b/docs/guide/batch-operations.md index 54596320ae..c113c7af8d 100644 --- a/docs/guide/batch-operations.md +++ b/docs/guide/batch-operations.md @@ -1,3 +1,12 @@ +--- +tags: + - batching + - isEvaluationSuspended + - EvaluationSuspendedError + - bulk updates + - recalculation +--- + # Batch operations HyperFormula offers a built-in feature for doing batch operations. @@ -21,7 +30,7 @@ operation together with their absolute addresses and new values. const hfInstance = HyperFormula.buildFromSheets({ MySheet1: [ ['1'] ], MySheet2: [ ['10'] ], -}); +}, { licenseKey: 'gpl-v3' }); // multiple operations in a single callback will trigger evaluation only once // and only one set of changes will be returned as a combined result of all @@ -49,7 +58,7 @@ operation together with their absolute addresses and new values. const hfInstance = HyperFormula.buildFromSheets({ MySheet1: [ ['1'] ], MySheet2: [ ['10'] ], -}); +}, { licenseKey: 'gpl-v3' }); // suspend the evaluation hfInstance.suspendEvaluation(); @@ -73,7 +82,7 @@ When you need to check if the evaluation is suspended you can call the [`isEvaluationSuspended`](../api/classes/hyperformula.md#isevaluationsuspended) method. ```javascript -const hfInstance = HyperFormula.buildEmpty(); +const hfInstance = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' }); // suspend the evaluation hfInstance.suspendEvaluation(); diff --git a/docs/guide/branding.md b/docs/guide/branding.md index 159219f7f8..0271cc46e9 100644 --- a/docs/guide/branding.md +++ b/docs/guide/branding.md @@ -1,3 +1,11 @@ +--- +tags: + - brand book + - brand guidelines + - brand assets + - press kit +--- + # Branding ## Our logo diff --git a/docs/guide/building.md b/docs/guide/building.md index 7d20b71a27..bbe9a0b5d8 100644 --- a/docs/guide/building.md +++ b/docs/guide/building.md @@ -1,3 +1,14 @@ +--- +tags: + - UMD + - ESM + - bundles + - TypeScript typings + - Jest + - Karma + - ESLint +--- + # Building The build process uses Webpack and Babel, as well as npm tasks diff --git a/docs/guide/built-in-functions.tmpl.md b/docs/guide/built-in-functions.tmpl.md index 2507820769..7d03cf4df1 100644 --- a/docs/guide/built-in-functions.tmpl.md +++ b/docs/guide/built-in-functions.tmpl.md @@ -1,3 +1,11 @@ +--- +tags: + - supported functions + - function syntax + - function categories + - statistics +--- + # Built-in functions