Skip to content

Calculate formula with row and col - #1720

Open
emadebManyDesigns wants to merge 29 commits into
handsontable:masterfrom
emadebManyDesigns:calculateFormulaWithRowAndCol
Open

Calculate formula with row and col#1720
emadebManyDesigns wants to merge 29 commits into
handsontable:masterfrom
emadebManyDesigns:calculateFormulaWithRowAndCol

Conversation

@emadebManyDesigns

@emadebManyDesigns emadebManyDesigns commented Jul 31, 2026

Copy link
Copy Markdown

Context

How did you test your changes?

Types of changes

  • Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore)
  • New feature or improvement (a non-breaking change that adds functionality)
  • Bug fix (a non-breaking change that fixes an issue)
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues:

  1. Fixes #1320...

Checklist:

  • I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project.
  • I have signed the Contributor License Agreement.
  • My change is compliant with the OpenDocument standard.
  • My change is compatible with Microsoft Excel.
  • My change is compatible with Google Sheets.
  • I described my changes in the CHANGELOG.md file.
  • My changes require a documentation update.
  • My changes require a migration guide.

Note

Low Risk
Changes are mostly documentation, VuePress build plugins, and CI checkout steps; the deploy-token test fetch is the main operational risk if secrets or paths are misconfigured.

Overview
This PR reshapes how contributors and coding agents work in the repo and how public docs are built, rather than changing the formula engine in the excerpt shown.

Developer & agent onboarding adds root AGENTS.md (with CLAUDE.md / CONTRIBUTING.md reduced to pointers), a large rewrite of DEV_DOCS.md (layout, Definition of Done, function-metadata catalogue rules, performance/testing guidance), .cursor/rules, and DOCS_CONTENT_GUIDE.md. context7.json scopes external doc indexing to docs/.

Built-in functions documentation stops committing docs/guide/built-in-functions.md. Prose moves to built-in-functions.tmpl.md with autogenerated category/function regions; .gitignore ignores the generated page and doc test snippets. docs/README.md documents docs:generate-function-docs and ties the page to getAvailableFunctions / getFunctionDetails and src/interpreter/functionMetadata/.

VuePress / LLM consumption adds the md-companions plugin (llms-full.txt, per-page .md companions, link rebasing/stripping), a local theme with “View as Markdown”, CodingAgentWizard, and a new “Set up your coding agent” nav entry. Config excludes the template from published pages and disables edit links on generated API/function pages.

User guides add currency-handling.md and stringifyCurrency / TEXT cross-links in Excel/Sheets compatibility and i18n; clarify setRowOrder / setColumnOrder permutations; refresh building output formats; forum URL in issue template.

CI & tooling: lint.yml checks out hyperformula-tests via deploy token and runs fetch-tests.sh; .eslintignore adds snippets; minor .eslintrc.js syntax fix.

CHANGELOG.md records many unreleased product changes (function metadata API, new functions, lookup fixes, calculateFormula row/col, Indonesian locale, etc.)—treat as release notes bundled with this branch even when not all src/ hunks appear in the diff fragment.

Reviewed by Cursor Bugbot for commit b3a38af. Bugbot is set up for automated code reviews on this repo. Configure here.

sequba and others added 28 commits May 11, 2026 16:31
## Summary

The lint GitHub Action did not check out the `hyperformula-tests`
repository before running ESLint. As a result, lint errors inside files
under `test/hyperformula-tests/` were silently skipped in CI and only
surfaced when running `npm run lint` locally.

This PR aligns `.github/workflows/lint.yml` with
`.github/workflows/test.yml` so the linter sees the same source tree as
the test jobs.

## Changes

- `.github/workflows/lint.yml`
- Add a `Checkout hyperformula-tests repository` step (using
`DEPLOY_TOKEN`, target path `test/hyperformula-tests`).
- Add a `Fetch hyperformula-tests and sync branches` step running
`test/fetch-tests.sh`.
- Name the existing main-repo checkout step for consistency with
`test.yml`.
## Summary

Adds a single canonical `### OFFSET function` sub-section under `##
Nuances of the implemented functions` in
`docs/guide/known-limitations.md`. Documents all six behavioral limits
of the OFFSET function in HyperFormula, each backed either by an
existing test in `unit/parser/offset-translation.spec.ts` or by a
runtime check captured before this PR was opened.

Removes the now-superseded one-row OFFSET entry from
`docs/guide/list-of-differences.md` (per Kuba's decision in the
2026-04-21 meeting: *"można wtedy to stąd też usunąć. Żeby wszystko było
tam jednak"*). The HF-vs-Excel/Sheets behavioral differences for OFFSET
are not lost — they remain documented in full in `known-limitations.md`
under the new sub-section, which is the canonical place for parse-time
restrictions per the 04-21 decision to consolidate.

> **Note on PR routing**: this PR replaces handsontable#1662 which was opened from a
fork branch. Same content, now from upstream branch — CI will have full
access (no fork-PR DEPLOY_TOKEN issue). Closing handsontable#1662 in favor of this
one.

## Linked

- Closes
[handsontable#1572](handsontable#1572) —
*Docs: describe limitations of the OFFSET function*
- Tracks the dynamic-args follow-up:
[handsontable#910](handsontable#910)
- Out of scope (separate task):
[handsontable#943](handsontable#943) —
restructuring `known-limitations` / `list-of-differences` /
`specifications-and-limits` pages
- Unblocked by:
[handsontable/hyperformula-tests#12](handsontable/hyperformula-tests#12)
(merged 2026-05-14, cleared lint regression introduced by handsontable#1672)
- Internal spec / tech rationale / implementation plan: tracked in the
team workspace (not committed); summary in this PR description
- Supersedes: handsontable#1662

## Limits documented

1. First argument must be a single-cell reference (passing a range =
parser error stored as cell value)
2. Row/column/height/width arguments must be static integer literals
(parser error otherwise)
3. Height and width must be **bare** positive integer literals —
`NUMBER` AST nodes only (unary `+`, parens, non-integers, values <1 all
rejected at parse time)
4. Out-of-sheet target → `#REF!` error stored at parse time (not
evaluation time), with the message *Resulting reference is out of the
sheet*
5. `getCellFormula` returns the resolved reference, not the original
`=OFFSET(...)`
6. Architectural rationale: OFFSET is rewritten at parse time into a
plain cell reference, so introspection via `getCellFormula` shows the
resolved reference rather than the call

## Runtime verification

All six limits were verified against this branch's HEAD before
publishing:

```
A. OFFSET in registered names: false  (correct — OFFSET is parse-time, not registered)
B. getCellFormula recovers: "=B1"     (rewritten reference, NOT "=OFFSET(A1, 0, 1)")
C. Out-of-sheet value: { value: "#REF!", message: "Resulting reference is out of the sheet." }
```

Tests covering all six limits live in
`test/hyperformula-tests/unit/parser/offset-translation.spec.ts` (24
tests, lines 13–206 in the private repo). Run via `npm run test:jest --
--testPathPattern="offset-translation"`.

## Test plan

- [ ] CI green on `handsontable/hyperformula`
- [ ] Netlify deploy preview:
[`/guide/known-limitations`](https://deploy-preview-1666--hyperformula-dev-docs.netlify.app/docs/guide/known-limitations.html)
— verify the new `### OFFSET function` sub-section renders, including
the four embedded `js` code blocks
- [ ] Netlify deploy preview:
[`/guide/list-of-differences`](https://deploy-preview-1666--hyperformula-dev-docs.netlify.app/docs/guide/list-of-differences.html)
— verify the table is intact and the OFFSET row is gone

## Notes

- This is docs-only — no CHANGELOG entry per project convention.
- The internal `ErrorMessage.OutOfSheet` string is intentionally NOT
quoted verbatim in the docs; the bullet describes the behavior instead,
so future internal-string refactors don't break the docs.
- Post-Codex review (2026-05-14): wording clarified to distinguish
parser-error-as-cell-value vs API exception, and to specify that
height/width accept only bare `NUMBER` literals (unary `+` etc.
rejected).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk docs-only change; the main risk is confusing users if the
newly documented OFFSET constraints are inaccurate or drift from
implementation.
> 
> **Overview**
> Adds a canonical **`### OFFSET function`** section to
`docs/guide/known-limitations.md` describing HyperFormula’s parse-time
rewriting behavior and the resulting constraints (single-cell first arg,
static integer shifts/sizes, strict positive literal height/width,
out-of-sheet `#REF!` at parse time, and `getCellFormula` returning the
resolved reference), with small JS snippets.
> 
> Removes the now-redundant `OFFSET` row from
`docs/guide/list-of-differences.md` to consolidate documentation in one
place.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
67ad2cd. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
### Context
<!--- Why are your changes required? What problem do they solve? -->

Original PR: handsontable#1674 by
https://github.com/Reckbeg

### Types of changes
<!--- What types of changes does your code introduce? Put an `x` in each
box that applies. -->
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [x] Additional language file, or a change to an existing language file
(translations)
- [ ] Change to the documentation

### Checklist:
<!--- Go through the points below, and put an `x` in each box that
applies. -->
<!--- If you're unsure about any of these, contact us. We're always glad
to help! -->
- [ ] 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 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.
- [ ] My change is compatible with Google Sheets.
- [x] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [ ] My changes require a documentation update.
- [ ] My changes require a migration guide.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Translation-only addition with no engine logic changes; risk is
limited to incorrect Indonesian function/error strings affecting formula
parsing for `idID` users.
> 
> **Overview**
> Adds **Bahasa Indonesia** (`idID`) as a built-in HyperFormula language
pack so formulas and cell errors can use Indonesian names (e.g.
`JUMLAH`, `JIKA`, `#BAGI0!`).
> 
> The new `src/i18n/languages/idID.ts` dictionary mirrors other locales
(errors, function aliases, `NEW_SHEET_PREFIX: 'Lembar'`) and is
re-exported from `src/i18n/languages/index.ts` for
`hyperformula/i18n/languages/idID` imports. Docs now list **18**
supported languages and include Indonesian in the supported-languages
table; **CHANGELOG** records the addition under Unreleased.
> 
> Also adds `.cursor/settings.json` enabling the superpowers plugin
(unrelated to i18n).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
918edd1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: GreenFlux <support@greenflux.us>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Rofi Ibnu Haafizh <Reckbeg@users.noreply.github.com>
### Context
<!--- Why are your changes required? What problem do they solve? -->

Original PR handsontable#1675 by
https://github.com/Reckbeg

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation and CSS-only VuePress page tweaks with no runtime or
library behavior changes.
> 
> **Overview**
> Improves **docs usability on small viewports** and clarifies **i18n
naming sources** for maintainers.
> 
> On **`built-in-functions`** and **`list-of-differences`**, adds inline
CSS so tables on `.widePage` use **horizontal scrolling** (`overflow-x:
auto`, block display) instead of breaking the layout on narrow screens.
Also fixes a trivial heading whitespace on the differences page.
> 
> **`DEV_DOCS.md`** documents how to pick localized built-in function
names when Microsoft Excel does not support a locale: use **Google
Sheets** function tables via the `hl` query parameter, and **fall back
to English** when Sheets has no entry.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9d0e126. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: GreenFlux <support@greenflux.us>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Rofi Ibnu Haafizh <Reckbeg@users.noreply.github.com>
Co-authored-by: Hermes Bounty Bot <hermes-bot@example.com>
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
### Context

The in-repo developer documentation was scattered across several files
with significant duplication:

- `CLAUDE.md` duplicated build/test commands (also in
`docs/guide/building.md`), contributing rules (also in `CONTRIBUTING.md`
/ `docs/guide/contributing.md`), and translation sources (also in
`DEV_DOCS.md`).
- `CONTRIBUTING.md` was an almost-verbatim copy of
`docs/guide/contributing.md`.
- `DEV_DOCS.md` contained only a small subset of what a developer needs.
- Cursor-specific guidance did not exist.

This PR removes the duplication and gives both humans and AI agents a
single, predictable entry point.

### New structure

| File | Role |
| --- | --- |
| `DEV_DOCS.md` | **Canonical** dev docs. Project overview,
architecture, code style, definition of done, how to add a function,
translation sources, plus quick links to the build / contributing / test
docs. |
| `docs/guide/building.md` | Full description of build, verify, docs,
test, and lint commands, including output formats. Public, for users and
devs. |
| `docs/guide/contributing.md` | Full contributor guide. Public, for
users and devs. |
| `AGENTS.md` | Pointer to `DEV_DOCS.md` plus rules specific to AI
agents (attribution, response style). |
| `CLAUDE.md` | Pointer to `AGENTS.md`. |
| `.cursor/rules/main.mdc` | Cursor rule that points to `AGENTS.md`
(always applied). |
| `CONTRIBUTING.md` | Pointer to `docs/guide/contributing.md`. |

### How did you test your changes?

Manual review of the resulting file set:

- `DEV_DOCS.md` contains everything a developer needs and links to
`docs/guide/building.md`, `docs/guide/contributing.md`,
`docs/guide/code-of-conduct.md`, `test/README.md`, `docs/README.md`,
`CHANGELOG.md`, and `.github/pull_request_template.md`.
- `AGENTS.md` links to `DEV_DOCS.md` and only adds agent-specific rules.
- `CLAUDE.md`, `.cursor/rules/main.mdc`, and `CONTRIBUTING.md` are thin
pointers with no duplicated content.
- `docs/guide/building.md` is the only place that lists build/test/lint
commands and bundle output formats.
- `docs/guide/contributing.md` is unchanged (it already covered the
contributor flow in full).

### Types of changes

- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues

1. HF-65

### Checklist

- [x] 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 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.
- [ ] My change is compatible with Google Sheets.
- [ ] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [ ] My changes require a documentation update.
- [ ] My changes require a migration guide.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-0a7bf4d1-d3c8-4edd-aacd-3a9df3a69526"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-0a7bf4d1-d3c8-4edd-aacd-3a9df3a69526"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
Co-authored-by: marcin-kordas-hoc <marcin.kordas@handsontable.com>
…e#1688)

## Summary

- Reframes the SSR section to clarify that the benefit of `dynamic(...,
{ ssr: false })` is keeping HyperFormula out of the **initial JS bundle
sent to the browser**, not skipping the server-side render itself (which
is already a non-issue since `useEffect` never runs on the server).

## Test Plan

- [ ] Review the updated wording in
`docs/guide/integration-with-react.md`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only wording change with no runtime or API impact.
> 
> **Overview**
> Updates one sentence in the React integration guide’s SSR section so
it no longer suggests `dynamic(..., { ssr: false })` is mainly about
avoiding server render. The text now states the goal is keeping
**HyperFormula out of the initial JS bundle sent to the browser** (still
noting the library is a few hundred kB), which matches the earlier point
that `useEffect` already makes the pattern SSR-safe.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
46d1d61. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…ble#1689)

## Summary

- Replaces the `SpreadsheetProvider` class example with the idiomatic
`markRaw` pattern as the primary recommendation
- Reframes the SSR section: HyperFormula is SSR-safe by default,
`<ClientOnly>` is an optional optimization
- Adds a TypeScript tip with a plain JS note
- Tightens prose throughout (net -20 lines)

## Test plan

- [x] Review the rendered docs page for clarity and correctness
- [x] Verify all internal links resolve (`basic-operations.md`,
`configuration-options.md`, etc.)
- [x] Confirm the Vue 3 StackBlitz demo link still works


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only edits to integration guides; no application code,
auth, or data paths change.
> 
> **Overview**
> Aligns the **Vue** and **React** integration guides around the same
TypeScript-first docs pattern and makes **`markRaw`** the primary Vue
integration story instead of a separate provider class.
> 
> **Vue (`integration-with-vue.md`):** Drops the `SpreadsheetProvider`
wrapper and `spreadsheet-provider.ts` example in favor of a single
`<script setup>` sample that builds `HyperFormula` inside **`markRaw`**,
keeps sheet output in a `ref`, and calls **`destroy()`** on unmount.
Template buttons gain **disabled** states tied to whether results are
shown. Copy explains that only `values` is reactive and points mutators
at **Basic operations**. The **Nuxt/SSR** section is shortened:
HyperFormula is fine on the server; **`<ClientOnly>`** is framed as an
optional way to skip server work, not a requirement. Troubleshooting
keeps the same `licenseKeyValidityState` error but uses consistent
`markRaw` snippets (including `hf` naming).
> 
> **React (`integration-with-react.md`):** Adds the same **::: tip
TypeScript** block (remove annotations for plain JS) and removes the
redundant standalone “drop type annotations” paragraph after the
example.
> 
> No runtime or API changes—docs and examples only.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
650eb19. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…le#1691)

### Context

- Added a modern Angular (v20+) section — an example in the same style
as the demo: a service exposing a signal, a component using inject() +
OnPush, a template with @if/@for, and bootstrap with
provideZonelessChangeDetection.
- Marked the old example as the older-versions variant (BehaviorSubject
+ async pipe), while fixing its compatibility: the component is now
standalone with imports: [CommonModule], so it works without an
NgModule.
- Added a path for very old Angular (≤13) — an NgModule-based apps
subsection at the end of the "older" section, completing the three
tiers: modern → older/standalone → NgModule.
- Moved the Demo section right under the modern Angular code (StackBlitz
link, without describing the demo's internal standards).
- Clarified the "Provider scope" and "Cleanup" notes to state they apply
to both variants (they depend on DI scope, not on signals/RxJS).

In short: the guide was rewritten to show the modern pattern (matching
the demo) while preserving backward compatibility, with a clear split by
Angular version.


### How did you test your changes?
<!--- Describe in detail how you tested your changes. -->

### Types of changes
<!--- What types of changes does your code introduce? Put an `x` in each
box that applies. -->
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues:
1. Fixes #...
2.
3.

### Checklist:
<!--- Go through the points below, and put an `x` in each box that
applies. -->
<!--- If you're unsure about any of these, contact us. We're always glad
to help! -->
- [ ] 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 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.
- [ ] My change is compatible with Google Sheets.
- [ ] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [ ] My changes require a documentation update.
- [ ] My changes require a migration guide.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only changes to the Angular integration guide; no
runtime or library code affected.
> 
> **Overview**
> Rewrites the **Integration with Angular** guide into a
**version-tiered** layout: a new **modern Angular (v20+)** path
(signals, `inject()`, OnPush, `@if`/`@for`,
`provideZonelessChangeDetection`) and a preserved **older** path
(`BehaviorSubject` + `async` pipe).
> 
> The legacy example is updated for **standalone** components
(`standalone: true`, `imports: [CommonModule]`) and adds an
**`NgModule`-based** subsection for Angular 13 and below. Sample sheet
data in snippets changes from `[1, 2, '=A1+B1']` to `[1, 4, '=A1+B1']`.
**Provider scope** and **Cleanup** notes now state they apply to both
signal and RxJS variants.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
210c1ac. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: GreenFlux <support@greenflux.us>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
Co-authored-by: krzysztof.zielinski <kzielinski@speednet.pl>
…mber cells (HF-219) (handsontable#1693)

Fixes the `getAllSheetsSerialized` / `getRangeSerialized` JSDoc examples
that wrongly implied numeric strings are serialized as numbers —
serialization round-trips, preserving the exact input type. Mirrors
handsontable#1654. Documentation-only (a single source file changed), so no
CHANGELOG entry per the docs-only convention.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Comment and example changes only; no runtime or serialization logic
modified.
> 
> **Overview**
> **Documentation-only** updates to JSDoc for `getAllSheetsSerialized`
and `getRangeSerialized` in `HyperFormula.ts`.
> 
> The docs now state that **non-formula cells keep the exact
`RawCellContent` type they were set with** (string `'1'` vs number `1`),
and the embedded examples were corrected so numeric literals are numbers
in sample input/output instead of implying string digits are coerced to
numbers on serialize.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
691b8b1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…22) (handsontable#1695)

Documents how a range-valued named expression behaves in a formula and
fixes the inaccurate limitations bullet (HF-222).

- `named-expressions.md`: new **Using named ranges in formulas** section
— behavior as function argument (full range), operator operand (implicit
intersection → single cell; array mode → `#VALUE!`), and bare reference
(`#VALUE!`).
- `known-limitations.md`: replaces the inaccurate top-level named-ranges
bullet with a neutral pointer to that section.

Paired characterization tests: hyperformula-tests branch
`hf-222-named-ranges` (matching branch for fetch-tests). Docs-only; no
CHANGELOG.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only changes with no runtime or API impact.
> 
> **Overview**
> Documents **how range-valued named expressions behave in formulas**
and corrects a misleading limitations note.
> 
> **`named-expressions.md`** adds **Using named ranges in formulas**:
full range when passed to functions; implicit intersection to one cell
(or `#VALUE!`) when used with operators; `#VALUE!` for a bare range
reference. It also contrasts default evaluation with
**`useArrayArithmetic: true`** (e.g. `=SUM(myRange + 1)` vs element-wise
sum).
> 
> **`known-limitations.md`** drops the inaccurate claim that named
ranges can’t be used in comparisons like `=IF(firstRange>secondRange,
…)` and points readers to the new section instead.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2844367. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
)

## Summary

Adds a `stringifyCurrency` config option mirroring the existing
`stringifyDateTime` / `stringifyDuration` callbacks. When set, the
`TEXT` function consults the callback before falling through to the
built-in number formatter, so users can plug in locale-aware currency
formatting (for example via `Intl.NumberFormat` or a third-party
library) without bringing currency data into the HyperFormula core.

The default implementation returns `undefined` so existing TEXT behavior
is preserved bit-for-bit **for non-LCID-tagged formats**. LCID-tagged
currency strings (`[$SYMBOL-LCID]`) now correctly skip the date/time
parser (see **Added** in CHANGELOG); that is a separate behavioral fix,
not a regression.

> **Note on PR routing**: this PR replaces handsontable#1661 which was opened from a
fork branch (`marcin-kordas-hoc`). Fork-side PRs cannot access the
`DEPLOY_TOKEN` secret needed to clone the private `hyperformula-tests`
repo, so 3 of the matrix checks (`Test performance`, `unit-tests`,
`browser-tests`) failed structurally there. Same content, same SHA
(`0246ce0bcbbed09ac9bf5e24af38b0d8aa1ac099`), now from upstream branch —
CI will have full access. Closing handsontable#1661 in favor of this one.

## Linked

- Spec:
`agents/hyperformula/docs/specs/2026-04-21-hf-24-currency-in-text.md`
- Tech rationale:
`agents/hyperformula/docs/specs/2026-04-24-hf-24-tech-rationale.md`
- Implementation plan:
`agents/hyperformula/docs/specs/2026-04-27-hf-24-stringify-currency-plan.md`
- Supersedes: handsontable#1661

## Tests

Tests added in the matching `feature/hf-24-stringify-currency` branch of
`handsontable/hyperformula-tests`. Coverage:

- default callback returns `undefined`
- custom callback intercepts currency formats
- callback opts out (returns `undefined`) → fall-through to
`numberFormat`
- date / duration formats are not intercepted by `stringifyCurrency`
- five Excel format strings actively handled by the docs
`Intl.NumberFormat` adapter (USD shorthand, EUR via LCID, JPY via LCID,
PLN via LCID, accounting two-section), plus a fall-through case
demonstrating opt-out

## Notes

- **PLN format string change**: the spec example originally used
`#,##0.00 "zł"` (trailing quoted symbol). HF's formula parser does not
accept embedded quotes inside TEXT format strings, so the docs example
and the corresponding test were swapped to use `[$zł-415] #,##0.00`
(LCID-tagged symbol). The adapter still recognizes the trailing-quote
pattern for users invoking the callback outside HyperFormula.
- **EUR / JPY assertion shapes reflect ICU output, not the original
plan**: tests assert `'1.234,50 €'` (symbol-trailing) for `[$€-2]` and
`'¥1,235'` (full-width yen sign, no space) for `[$¥-411]` because that
is what `Intl.NumberFormat('de-DE'/'ja-JP', ...)` actually produces on
modern Node ICU. NBSP normalization in tests covers both `\u00A0` and
`\u202F` variants for ICU build robustness.
- **No-LCID `[$SYMBOL]` boundary**: the example regex requires the
`-LCID` segment. A bare `[$USD]` pattern is not handled by the adapter
and falls through to the built-in `numberFormat`, whose handling of
`[$...]` in HyperFormula is implementation-defined. Test `docs adapter
does not handle [$SYMBOL] without LCID segment` documents the boundary.
- **Additional minor changes** (not in summary): (a) two broken anchor
links fixed in `docs/guide/built-in-functions.md`; (b)
`docs/guide/i18n-features.md` replaces a stale `currencySymbol` code
example with a cross-reference; (c) `tsconfig.test.json` adds
`test-utils` to `include`; (d) `docs/guide/known-limitations.md` — new
bullet for the TEXT embedded-quote limitation (can't use `""` escape in
format strings; use LCID-tagged form or `stringifyCurrency` callback);
(e) `docs/guide/date-and-time-handling.md` — adds cross-reference
paragraph to `currency-handling.md`; (f)
`docs/guide/list-of-differences.md` — updates the "No currency
formatting in TEXT" row with callback instructions; (g) `.eslintignore`
— adds `test-utils/snippets` exclusion; (h)
`test-utils/snippets/*.generated.ts` — generated from the docs and **not
committed**; regenerated before every test run by inlining `npm run
snippets:extract &&` into the `test:jest`/`test:ci`/`test:browser`
scripts (`npm-run-all` does not fire `pre*` hooks) and git-ignored, so
the docs stay the single source of truth. (i)
`script/extract-doc-snippets.js` (257 LOC) — new codegen script that
walks the docs for `<!-- snippet:NAME -->` markers and syncs them into
`test-utils/snippets/*.generated.ts`. Heavy for one snippet but designed
for future docs expansion; see inline docstring for design rationale.
- **Performance benchmark shows +1.9% to +8.7% across metrics** vs the
post-merge base (`456adddff` = develop with HF-85 DatabasePlugin).
HF-24's runtime impact is one extra dispatcher call in `format()` per
`TEXT` invocation, which the Sheet A/B/T benchmarks don't exercise. The
variance is most likely benchmark noise or HF-85 import overhead carried
in via the develop merge, not HF-24-specific.
- **`Maybe<T>` in callback types**: `stringifyCurrency`,
`stringifyDateTime`, and `stringifyDuration` all declare return type as
`Maybe<string>` (= `string | undefined`). This is consistent public API
surface shared by all three siblings; the alias is re-exported from
HyperFormula's type definitions. Changing only `stringifyCurrency` to
bare `string | undefined` would create asymmetry. A follow-up can align
all three if desired.

## Test plan

- [x] CI green on `handsontable/hyperformula` PR
- [x] CI green on `handsontable/hyperformula-tests` PR (matching branch)
- [x] Manual: built docs locally — `vuepress build docs` returns EXIT 0
with 215 sitemap entries; new `currency-handling.md` guide renders with
the `Currency input` + `Currency output` sections (sidebar wired under
Internationalization) per Kuba's review feedback

---
**Private tests PR:** handsontable/hyperformula-tests#10

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Every `TEXT` invocation goes through an extra formatter dispatch step,
and LCID-tagged currency format strings change behavior (intentional
fix); misconfigured callbacks can alter formula output broadly.
> 
> **Overview**
> Introduces **`stringifyCurrency`**, a config callback parallel to
`stringifyDateTime` / `stringifyDuration`, so `TEXT` can delegate
currency output to custom formatters (e.g. `Intl.NumberFormat`) while
the default no-op preserves existing behavior for ordinary formats.
> 
> **Runtime:** `format()` now calls `stringifyCurrency` first.
`defaultStringifyDateTime` and `defaultStringifyDuration` skip Excel
**`[$SYMBOL-LCID]`** currency tags so those strings are no longer
mangled by date/time parsing and can reach the number formatter or a
user callback. Config wiring lives in `Config.ts`, `ConfigParams.ts`,
and `defaultStringifyCurrency` in `format.ts`.
> 
> **Docs & DX:** New **Currency handling** guide, sidebar entry,
compatibility/differences updates, changelog entries, and expanded
`DEV_DOCS` testing guidance. **`script/extract-doc-snippets.js`**
generates `test-utils/snippets/*.generated.ts` from `<!-- snippet:NAME
-->` blocks in docs; `test:jest` / `test:ci` / `test:browser` run
`snippets:extract` first; generated files are gitignored.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9cc8d6c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## Summary
- Update share capital value in LICENSE.txt from PLN 62,800.00 to PLN
67,200.00

[IT-535](https://app.clickup.com/t/9015210959/IT-535)


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only legal metadata with no effect on application
behavior or security.
> 
> **Overview**
> Updates the corporate disclosure in **`LICENSE.txt`** so HANDSONCODE’s
listed **share capital** reads **PLN 67,200.00** instead of **PLN
62,800.00**; no other license terms or code change.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c774208. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: GreenFlux <support@greenflux.us>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## Summary

Implements the Excel `XIRR` function (HF-76).

- Adds `XIRR(Values, Dates[, Guess])` to `FinancialPlugin` — returns the
internal rate of return for a schedule of cash flows that is not
necessarily periodic
- Optional `Guess` defaults to `0.1`; solver uses Newton–Raphson with
day-based discounting on a 365-day year
- Validates aligned value/date ranges (≥2 flows, mixed signs, date
ordering), coerces empty value cells to `0`, and returns `#NA!` for
single-cell or scalar arguments
- Registers `XIRR` in all language packs, updates
`built-in-functions.md` and `CHANGELOG.md`
- Unit tests added in the private test suite (`function-xirr.spec.ts`)

## Test plan

- [x] `npm test` — lint + unit + browser
- [x] Microsoft XIRR example, XNPV consistency, range layouts, date edge
cases, error paths, guess handling, non-convergence

### Types of changes

- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [x] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [x] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Additive built-in function in FinancialPlugin with localized names and
documentation; no changes to auth, persistence, or core engine APIs.
> 
> **Overview**
> Adds Excel-compatible **`XIRR(Values, Dates[, Guess])`** for irregular
cash-flow schedules, alongside existing **`IRR`** and **`XNPV`**.
> 
> Implementation lives in **`FinancialPlugin`**: aligned value/date
ranges are sanitized (empty value cells → `0`, errors propagated,
non-numeric → `#VALUE!`), with checks for length match, at least two
flows, mixed signs, valid dates, and `Guess > -1`. The rate is solved
with **Newton–Raphson** on day-based discounting using a **365-day
year**, including overshoot clamping when the iterate would fall at or
below **-1**.
> 
> **`XIRR`** is registered in all language packs, documented in
**`built-in-functions.md`**, noted in **`CHANGELOG.md`**, and
**`AGENTS.md`** gains a reminder to keep PR descriptions up to date.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
36c1465. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
### Context

Document HyperFormula's independent security assessment and certificate
awarded by TestArmy Group S.A. in July 2026. This adds credibility to
the security standards section and provides transparency about
third-party security validation.

### Changes

- Updated "Quality" page title to "Quality & Security" to better reflect
content scope
- Added new "Independent security certificate" subsection documenting:
- TestArmy Group S.A. security certificate (signed by CEO Wojciech
Humiński)
  - Assessment details for HyperFormula v3.3.0
- Assessment scope: code review, white-box penetration testing,
static/dynamic/manual analysis against OWASP ASVS and Top 10, dependency
analysis
- Updated sidebar navigation to reflect new page title

### Types of changes

- [x] Change to the documentation

### How did you test your changes?

Documentation changes are self-evident. Verified:
- Markdown syntax is valid
- Navigation config matches updated page title
- Links to npm package version are correct

### Checklist

- [x] My changes require a documentation update.

https://claude.ai/code/session_017L6WnY9qHHBmfSE4Qe2ZH7

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation and navigation label changes only; no runtime or library
behavior is modified.
> 
> **Overview**
> Renames the **Quality** guide page and sidebar entry to **Quality &
Security** so the nav matches the security content already on the page.
> 
> Adds an **Independent security certificate** subsection under Security
describing the July 2026 TestArmy Group S.A. assessment of HyperFormula
v3.3.0 (white-box testing, OWASP ASVS/Top 10–oriented analysis,
dependency review) and a link to download
`hyperformula_security_certificate.pdf`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
81f40b2. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
### Context
<!--- Why are your changes required? What problem do they solve? -->

### How did you test your changes?
<!--- Describe in detail how you tested your changes. -->

### Types of changes
<!--- What types of changes does your code introduce? Put an `x` in each
box that applies. -->
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [ ] Change to the documentation

### Related issues:
1. Fixes #...
2.
3.

### Checklist:
<!--- Go through the points below, and put an `x` in each box that
applies. -->
<!--- If you're unsure about any of these, contact us. We're always glad
to help! -->
- [ ] 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 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.
- [ ] My change is compatible with Google Sheets.
- [ ] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [ ] My changes require a documentation update.
- [ ] My changes require a migration guide.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Config-only URL change with no runtime or security impact.
> 
> **Overview**
> Updates the **Feature requests and questions** contact link in the
GitHub issue template config so it points to the [Handsontable forum
HyperFormula
category](https://forum.handsontable.com/t/about-the-hyperformula-category/9073)
instead of GitHub Discussions.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9638a72. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## What

Implements two new array-spilling functions, `VSTACK` and `HSTACK`, in
`ArrayPlugin`.

- **VSTACK(array1, [array2], ...)** — stacks arrays vertically. Result
height = sum of input heights, width = max of input widths. Narrower
inputs are padded on the right with `#N/A`.
- **HSTACK(array1, [array2], ...)** — stacks arrays horizontally. Result
width = sum of input widths, height = max of input heights. Shorter
inputs are padded at the bottom with `#N/A`.

Behaviour matches Excel 365 / Google Sheets, with the documented
HyperFormula nuances below.

## Scope

- `src/interpreter/plugin/ArrayPlugin.ts` — `VSTACK`/`HSTACK` metadata,
methods, and `*ArraySize` parse-time size calculations (shared
`stackSubChecks` helper; `VSTACK` aligns each row via `padRowToWidth`,
`HSTACK` pads inline).
- `src/i18n/languages/*.ts` — function-name entries for all 18 language
packs (`enUS` inherits from `enGB`). The names are identical across
locales, matching Excel's convention for these functions.
- `docs/guide/built-in-functions.md` — entries for both functions.
- `CHANGELOG.md` — `Added` entry under `[Unreleased]`.

## HyperFormula nuances vs. Excel

- **Empty cells** pass through as empty (`null`) rather than coercing to
`0`. HyperFormula preserves the empty value; Excel (which has no
empty-result cell) displays `0`. The stacked structure is identical.
- **A bare scalar argument that is itself an error** short-circuits the
whole call to that error, the same way every `runFunction`-based
function behaves (e.g. `ABS`, `FILTER`). Errors located *inside an input
range* pass through per cell, preserving their type (matches Excel).

## Test coverage

Unit tests live in the private tests repository:
handsontable/hyperformula-tests#18 (57 cases: 28 VSTACK + 29 HSTACK),
mirroring a validated Excel 365 oracle. They cover same-width/height
stacks, dimension mismatch with `#N/A` padding, scalars, single-arg
passthrough, mixed types, error passthrough, empty cells, jagged input
from a custom function, empty-array error propagation, nested
`VSTACK`/`HSTACK`, and integration with `SEQUENCE`/`TRANSPOSE`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Additive array functions in an isolated plugin with no changes to core
engine, auth, or persistence; behavior is covered by external oracle
tests.
> 
> **Overview**
> Adds **VSTACK** and **HSTACK** as new array-spilling functions in
`ArrayPlugin`, aligned with Excel 365 / Google Sheets.
> 
> **VSTACK** concatenates ranges vertically (height = sum of input
heights, width = max width); narrower rows get `#N/A` padding on the
right via `padRowToWidth`. **HSTACK** concatenates horizontally (width =
sum of widths, height = max height); shorter inputs get `#N/A` at the
bottom. Both use variadic `RANGE` arguments (`repeatLastArgs: 1`), array
arithmetic on arguments, and paired `*ArraySize` methods for spill
sizing; shared `stackSubChecks` resolves per-argument dimensions at
parse time.
> 
> Documentation and localization are updated: `built-in-functions.md`,
`CHANGELOG.md` under Unreleased, and `VSTACK`/`HSTACK` entries in all 18
language packs (names kept as `VSTACK`/`HSTACK` like Excel).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3db462b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…ontable#1687)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
### Context

Fixes [handsontable#1668](handsontable#1668).
The reporter ran `setRowOrder` expecting the array argument to mean
"previous position for each new row", got the opposite behavior, and was
unable to disambiguate from the docs.

Root cause: every example in the JSDoc and guide pages used a
*self-inverse* permutation (`[2, 1, 0]` on 3 rows, `[0, 3, 2, 1]` on 4
rows). Self-inverse permutations produce the same output under both of
the two natural interpretations of the argument:

- A. `newOrder[i]` = new position for the row/column currently at index
`i` (this is the actual behavior, verified in `mappingFromOrder` in
`src/CrudOperations.ts` and `Operations.setRowOrder` in
`src/Operations.ts`)
- B. `newOrder[i]` = previous position of the row/column that should end
up at index `i`

So the examples didn't actually demonstrate which interpretation was
correct.

### Changes

Doc-only. No runtime behavior changed.

- Replaced the self-inverse examples with non-self-inverse cyclic shifts
(`[1, 2, 0]` on a 3-element sheet → result `[['C'], ['A'], ['B']]`).
Under interpretation B the result would have been `[['B'], ['C'],
['A']]`, so the example now visibly reinforces the correct semantics.
- Added explicit prose to the JSDoc and guide pages stating the
convention: "the value at index `i` is the new position for the
row/column currently at index `i`".
- Added a `::: warning` callout in the sorting guide explicitly
contrasting the correct interpretation with the inverse-permutation
interpretation that the reporter assumed.
- Updated the analogous `setColumnOrder`/`isItPossibleToSetColumnOrder`
documentation for consistency.
- Added a note to `DEV_DOCS.md` clarifying that documentation-only PRs
do not require a `CHANGELOG.md` entry.

Files touched:

- `src/HyperFormula.ts` - JSDoc for `setRowOrder`,
`isItPossibleToSetRowOrder`, `setColumnOrder`,
`isItPossibleToSetColumnOrder`.
- `docs/guide/sorting-data.md` - intro paragraph, "Sorting rows" and
"Sorting columns" step-by-step sections.
- `docs/guide/basic-operations.md` - "Reordering rows" and "Reordering
columns" subsections.
- `DEV_DOCS.md` - new "Documentation-only changes" note under
"Definition of Done".

Out of scope: `swapRowIndexes` / `swapColumnIndexes` use `[[source,
target], ...]` pairs and are already described as "array mapping
original positions to final positions"; the ambiguity does not apply, so
those are untouched.

### How did you test your changes?

- `npm run lint` - 0 errors (pre-existing warnings in tests are
unrelated).
- `npm run compile` - clean TypeScript compile.
- Read the rendered JSDoc and Markdown by inspection to confirm the new
examples and prose are accurate against the verified implementation.

### Types of changes

- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues:

1. Fixes handsontable#1668

### Checklist:

- [x] 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 signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [x] 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.
- [x] My change is compatible with Microsoft Excel.
- [x] My change is compatible with Google Sheets.
- [ ] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file. (Not required: documentation-only change.)
- [x] My changes require a documentation update.
- [ ] My changes require a migration guide.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-bb4cd729-a5ee-4b7c-9de1-0437efa82903"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-bb4cd729-a5ee-4b7c-9de1-0437efa82903"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: GreenFlux <support@greenflux.us>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…OOKUP (HF-223) (handsontable#1697)

## What & why
Approximate `MATCH`, `VLOOKUP`, `HLOOKUP` and `XLOOKUP` returned `#N/A`
(or, in descending mode, the wrong position) when the sorted search
range contained genuinely empty cells. Excel 2021 and Google Sheets skip
empty cells when computing the lower/upper bound; HyperFormula instead
landed on an empty cell during binary search (its `EmptyValue` Symbol
never matched the key). [HF-223]

## Root cause
`findLastOccurrenceInOrderedRange` (`src/interpreter/binarySearch.ts`):
`compare()` ranks `EmptyValue` below every value, breaking the sort
invariant the binary search relies on, and the `typeof foundValue !==
typeof searchKey` guard then turned a landing-on-empty into `#N/A`.
Shared by approximate `MATCH(±1)`, sorted `VLOOKUP`/`HLOOKUP`, and
`XLOOKUP(searchMode ±2)`.

## Fix
- The binary search runs directly over the original range while tracking
whether the descent ever probes an empty cell. Empty cells are the only
source of non-monotonicity in the search predicate, so a descent that
never touches one is provably equivalent to a search over the range with
empty cells removed and its result is trusted as-is — the common case
stays `O(log n)`, including ranges that contain empty cells the descent
happens not to touch.
- Only when the descent probes an empty cell does the search fall back
to an `O(n)` compaction: the non-empty cell indices are collected, the
binary search re-runs over the compacted list, and the result maps back
to the original index space, so empty cells keep their slots and the
matched non-empty cell's original 1-based position is reported
unchanged. (In exact-match mode, a hit found by the direct descent is
accepted after an equality re-check, skipping the fallback.)
- The "no match at all" bound cases return the position of the first
**non-empty** cell (never the position of a leading empty cell), and the
approximate-bound "next" position steps to the next *non-empty* index,
so skipped empty slots never shift the reported position.
- `AdvancedFind.findNormalizedValue` skips `EmptyValue` on its in-memory
ordered path for the same reason, keeping the linear and binary search
modes consistent.
- A matched result cell that is empty is returned as `0`
(`zeroIfEmptyResult`), matching Excel, for `VLOOKUP`/`HLOOKUP`/`XLOOKUP`
including multi-cell `XLOOKUP` return arrays.
- Empty strings are unaffected (text ranks above numbers, so they still
terminate a numeric run).

## Performance & edge cases
- **Complexity:** `O(log n)` whenever the binary-search descent probes
no empty cell (always the case for gap-free ranges — the typical
sorted-lookup workload); `O(n)` only when an empty cell actually
interferes with the descent. A new `Sorted lookup` benchmark in the
performance suite guards this fast path.
- **All-empty range:** returns `NOT_FOUND` directly (the `if_not_found`
result / `#N/A`), never row 1.
- **Leading empty cells with the key outside the range:** the bound
modes return the first non-empty position, consistent with the linear
search modes on the same data.
- **Duplicates caveat:** in exact-match binary modes, when the range
contains both duplicates of the key and interspersed empty cells, which
duplicate is reported is unspecified (Excel's binary modes likewise
leave this unspecified); documented in the function's JSDoc.

## Excel / Google Sheets parity
Behaviour verified against the latest Excel and Google Sheets, including
exact-vs-blank, empty-string-not-skipped, and the descending early-stop
case. For the leading-empty bound cases, HyperFormula's binary modes are
consistent with its own linear modes (Excel documents binary search over
blank-containing ranges as unreliable).

## Tests
Public test suite: handsontable/hyperformula-tests#17 (matching branch
`task/hf-223-match-empty-cells`), including regression locks for the
stepping and leading-empty edge cases and a `Sorted lookup` performance
benchmark.

## Definition of Done
- [x] Production code + JSDoc
- [x] Tests (hyperformula-tests#17, matching branch)
- [x] Changelog (empty-cell search fix + empty-result → 0 coercion)
- [x] i18n — N/A (shared-logic fix, no function add/rename)
- [x] Docs — list-of-differences entry for the remaining Excel
divergence

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes shared lookup/binary-search logic used by MATCH and all major
lookup functions, so incorrect edge-case handling could affect many
formulas; scope is limited to lookup parity and is behavior-fix oriented
rather than new surface area.
> 
> **Overview**
> Fixes **HF-223**: approximate lookups no longer break when the search
range has **genuinely empty cells** interspersed among sorted values.
> 
> **Search behavior:** `findLastOccurrenceInOrderedRange` now treats
`EmptyValue` as non-participating in ordering. It keeps an **O(log n)**
binary search when the descent never hits an empty cell; if it does, it
**compacts non-empty indices** and re-searches, with exact-match hits
still accepted after an equality re-check. Lower/upper bound paths step
to the **first/next non-empty** index instead of landing on blanks or
returning row 1 on all-empty ranges. The in-memory path in
`AdvancedFind.findNormalizedValue` skips empty cells the same way so
linear and binary modes stay aligned.
> 
> **Return values:** `VLOOKUP`, `HLOOKUP`, and `XLOOKUP` coerce an
**empty matched result cell to `0`**, matching Excel.
> 
> Changelog and **list-of-differences** document the fix and remaining
Excel divergence on binary `XLOOKUP` over blank-heavy ranges.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d3a6f21. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
### Context
Updates the Support guide page (`docs/guide/support.md`) to match the
new support content defined in
[HF-128](https://app.clickup.com/t/9015210959/HF-128).

Changes:
- **Community support** now points developers to the [community
forum](https://forum.handsontable.com/) for questions/feature requests
and to the [bug report issue
template](https://github.com/handsontable/hyperformula/issues/new?template=bug_report.yaml)
for bugs.
- **Premium support** describes the three support tiers (Standard,
Priority, Enterprise) in a comparison table, with a link to the full
plan details.
- **Consulting services** section retained.

### How did you test your changes?
Documentation-only change. Verified the referenced `bug_report.yaml`
issue template exists in `.github/ISSUE_TEMPLATE/` and kept the internal
"Contact sales" link in the existing relative `contact.md` form.

### Types of changes
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues:
1. HF-128

### Checklist:
- [x] I have reviewed the guidelines about Contributing to HyperFormula
and I confirm that my code follows the code style of this project.
- [x] My changes require a documentation update.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only navigation and link changes with no runtime or API
impact.
> 
> **Overview**
> **Removes the in-docs Support guide** (`docs/guide/support.md`) and
drops it from the VuePress **About** sidebar in
`docs/.vuepress/config.js`.
> 
> **Support CTAs now go off-site:** the “Looking for technical support?”
section in `contact.md` and the support link at the end of `quality.md`
no longer point to `support.md`; they link to
`https://hyperformula.handsontable.com/#pricing` instead.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
8dc15dd. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## What & why

Implements **SORT** (`HF-69`, child of HF-28 "Modern dynamic array
functions", sibling of the shipped SEQUENCE and of VSTACK/HSTACK). Adds
`SORT(array, [sort_index], [sort_order], [by_col])` as a dynamic array
function.

Tests: handsontable/hyperformula-tests#24 (paired).
Sibling PR: handsontable#1708 (UNIQUE / HF-68).
ADR: `docs/adr/2026-07-13-sort-unique-array-functions.md`.

## Behavior

- Returns an array the **same shape** as the input.
- `sort_index` (default 1): 1-based index into the sort dimension.
- `sort_order`: `1` ascending (default) or `-1` descending.
- `by_col`: `FALSE` (default) reorders rows; `TRUE` reorders columns.
- Ordering reuses `ArithmeticHelper` (mixed types: numbers < text <
logical; empties; locale collation via
`caseSensitive`/`accentSensitive`) and is **stable** — ties keep input
order.

## Design

Mirrors the SEQUENCE/FILTER machinery: `sizeOfResultArrayMethod` +
`vectorizationForbidden: true`, runtime via `runFunction` returning
`SimpleRangeValue`/`CellError`, parse-time size method returning a
**fresh** `ArraySize` (the input's `isRef` flag is dropped — a
ref-flagged size is treated as scalar and would collapse the spill).

## Notes — divergences from Excel (surfaced here + inline + in tests)

- **`sort_order` is strictly `{1, -1}`**; any other value → `#VALUE!`.
Excel documents only `{1,-1}`; the reported "`sort_order=0` does not
error" quirk is undocumented and **could not be re-verified against live
Excel in this environment**, so the strict documented contract was
chosen (see ADR `dec_2`, `con_1`). Flagged for live-Excel/Kuba
confirmation.
- **Multi-key array-constant `sort_index`** (e.g. `{1,2}`) is **not
supported in v1** (documented in `known-limitations.md`; ADR `dec_6`).
- In-range errors propagate (first error found; ADR `dec_7`).

## Error-type map

`sort_order ∉ {1,-1}` → `#VALUE!` (BadMode) · `sort_index < 1` →
`#VALUE!` (LessThanOne) · `sort_index >` dimension → `#VALUE!`
(ValueLarge) · in-range error → propagate · wrong arity → `#N/A`.

## Definition of Done

- [x] Production code (`SortPlugin.ts`, registered via
`plugin/index.ts`)
- [x] i18n — all 17 language packs (authoritative MS Functions
Translator names; enUS inherits enGB)
- [x] Tests (paired tests PR) — across the standard array-function
groups, dual-env safe
- [x] Docs — `built-in-functions.md`, `known-limitations.md`
- [x] JSDoc on all methods
- [x] CHANGELOG entry
- [x] ADR with audit-verified citations

Source: https://app.clickup.com/t/86c89q1tt

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Self-contained new array function following existing SEQUENCE/FILTER
patterns; no changes to auth, persistence, or core recalculation beyond
registering one plugin.
> 
> **Overview**
> Adds the **SORT** dynamic array function: `SORT(Array, [SortIndex],
[SortOrder], [ByCol])` returns the input range reordered by row
(default) or column, same dimensions as the source.
> 
> Implementation lives in new `SortPlugin.ts`, wired like other array
functions (`sizeOfResultArrayMethod`, `vectorizationForbidden`, spill
size copied from input without propagating `isRef`). Sort keys use
`ArithmeticHelper` (with empty cells forced last); invalid `sort_order`
(not `1` or `-1`), bad `sort_index`, in-range errors, and empty ranges
get the documented `#VALUE!` / `#N/A` / error propagation behavior.
> 
> Docs and changelog are updated; **known-limitations** documents
single-key only, strict sort order, and HF comparison rules. **SORT** is
added to all 17 language packs.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f081be5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

## What & why

Implements **UNIQUE** (`HF-68`, child of HF-28 "Modern dynamic array
functions", sibling of the shipped SEQUENCE and of VSTACK/HSTACK). Adds
`UNIQUE(array, [by_col], [exactly_once])` as a dynamic array function.

Tests: handsontable/hyperformula-tests#25 (paired).
Sibling PR: handsontable#1707 (SORT / HF-69).
ADR: `docs/adr/2026-07-13-sort-unique-array-functions.md`.

## Behavior

- Returns the **distinct rows** (or columns when `by_col` is `TRUE`) of
the input, preserving first-occurrence order.
- `by_col`: `FALSE` (default) compares rows; `TRUE` compares columns.
- `exactly_once`: `TRUE` returns only rows/columns occurring exactly
once; `FALSE` (default) returns all distinct.
- Equality reuses `ArithmeticHelper.eq` → **case-insensitive by
default** (honors `caseSensitive`), matching Excel's UNIQUE.
- Result size is data-dependent; mirrors FILTER (predict input size as
upper bound, return the smaller actual result).

## Design

Mirrors the FILTER machinery for dynamic-size results:
`sizeOfResultArrayMethod` + `vectorizationForbidden: true`, runtime via
`runFunction`, parse-time size method returning a **fresh** `ArraySize`
(drops the input's `isRef` flag). Deduplication is O(n²) in the number
of vectors because locale-aware equality is not trivially hashable —
noted in code; acceptable for v1.

## Notes — divergences from Excel (surfaced here + inline + in tests)

- **Empty result** (only via `exactly_once` when nothing occurs exactly
once) → `#N/A`. Excel returns `#CALC!`, which HyperFormula has no type
for; mirrors FILTER's empty-result mapping (ADR `dec_8`).
- Comparison honors HF's collation config rather than a byte-for-byte
Excel oracle (no live Excel in this environment; ADR `con_1`).
- In-range errors propagate (first error found; ADR `dec_7`).

## Definition of Done

- [x] Production code (`UniquePlugin.ts`, registered via
`plugin/index.ts`)
- [x] i18n — all 17 language packs (authoritative MS Functions
Translator names; enUS inherits enGB)
- [x] Tests (paired tests PR) — across the standard array-function
groups, dual-env safe
- [x] Docs — `built-in-functions.md`, `known-limitations.md`
- [x] JSDoc on all methods
- [x] CHANGELOG entry
- [x] ADR with audit-verified citations

Source: https://app.clickup.com/t/86c89q1tq

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Additive array function behind existing dynamic-array machinery; no
changes to auth, persistence, or core evaluation paths beyond new plugin
registration.
> 
> **Overview**
> Adds the Excel-style **`UNIQUE(array, [ByCol], [ExactlyOnce])`**
dynamic array function so formulas can return distinct rows or columns
with first-occurrence order preserved.
> 
> **`UniquePlugin`** implements deduplication via `ArithmeticHelper.eq`
(honors `caseSensitive` / `accentSensitive`), optional column-wise mode
and “exactly once” filtering, propagates the first in-range error, and
returns **`#N/A`** when `ExactlyOnce` would yield an empty result
(aligned with FILTER). Spill sizing follows FILTER: parse-time upper
bound from input dimensions, `vectorizationForbidden: true`, and a fresh
`ArraySize` so `isRef` is not carried through.
> 
> Also registers the plugin, adds **`UNIQUE`** to all language packs,
documents the function and known limitations, and records the change in
the changelog.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a4097a4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…guide (HF-154) (handsontable#1703)

### Context

HF-154 — make the HyperFormula docs friendlier to coding agents and
LLMs. On top of the existing VuePress portal this adds:

- **Per-page `.md` companions** — every doc page is also served as
clean, VuePress-stripped Markdown (a build-time `md-companions` plugin),
plus an aggregate `llms-full.txt`. Both are also mirrored to the site
root so `/llms.txt` and `/llms-full.txt` resolve on GitHub Pages (prod)
and the Netlify preview.
- **`llms.txt`** — a top-level index pointing agents at the Markdown
sources.
- **View-as-Markdown link** (`ViewMarkdownLink.vue`) — links to the
page's `.md` source so an agent can open and read it directly.
- **Coding-agent setup guide** (`docs/guide/setup-coding-agent.md`) plus
a `CodingAgentWizard.vue` helper.
- **`context7.json`** so agent doc-access tooling (Context7 / GitMCP)
can discover the sources.

The Markdown stripper (`md-companions/strip.js`) is the
fidelity-critical piece: it turns VuePress-flavoured Markdown (`:::`
containers, `<script>`/Vue components, `[[toc]]`, `{{ }}` bindings,
Vue-bound `<a :href>`/`<img :src>`, live `:::example` demos, nested code
fences) into clean Markdown.

### How did you test your changes?

- Manually verified the stripper on representative inputs (tip/warning
containers, nested code fences, Vue-bound `<a :href>`/`<img :src>`,
`[[toc]]`, `<script>`/component removal, `:::example` demos) —
confirming code fences, **including Vue-shaped samples inside container
bodies**, survive verbatim while prose is cleaned.
- Sanity-checked the full corpus against the Netlify deploy-preview
(`llms-full.txt` populated, per-page `.md` clean, no leaked components).
- `npm run lint` clean.

### Note on test coverage

The `md-companions` stripper currently has **no automated regression
tests**. The earlier `test/docs/*.spec.js` suites (strip / corpus /
generated) were removed in `363a5e2` — the public repo carries smoke
tests only (`test/README.md`), and this is build-time docs tooling, not
shipped `src/` code, so a stripper regression degrades the generated
`.md`/`llms.txt`, not engine/product behaviour. The working safety net
is Cursor Bugbot + the Netlify deploy-preview + review.

That said, this is the **second fidelity bug** in the stripper (after
the Vue-bound link/image fix), and the removed suites did **not** cover
the case that regressed here — Vue markup inside a code fence sitting
**inside** a container. If we want coverage, the right home is the
private `hyperformula-tests` repo (restore the removed cases + this
container-fence intersection).

### Types of changes
- [x] New feature or improvement (a non-breaking change that adds
functionality)
- [x] Change to the documentation

### Related issues:
1. HF-154

### Checklist:
- [x] I have reviewed the guidelines about Contributing to HyperFormula
and I confirm that my code follows the code style of this project.
- [x] I described my changes in the CHANGELOG.md file.

### Notes

- Supersedes handsontable#1696 (moved to an upstream branch so CI can access the
private `hyperformula-tests` repo). Current `develop` is merged in.
- The docs-portal Astro migration (handsontable#1686) is a separate track. If it
lands first it supersedes this VuePress plumbing (the `md-companions`
plugin and `.vue` components are VuePress-specific); the agent-friendly
*outputs* (`.md` companions, `llms.txt`, setup guide) would need
re-homing in Astro.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Docs-site and build-time tooling only; no changes to `src/` engine
behavior. Main operational risk is stripper fidelity regressions in
generated `.md`/`llms-full.txt` (currently manual/preview validation, no
automated stripper tests in this PR).
> 
> **Overview**
> Adds **LLM/agent-friendly documentation outputs** on top of the
existing VuePress docs build, without changing the spreadsheet engine.
> 
> A new **`md-companions` VuePress plugin** runs at build time: it
strips VuePress-only syntax (`:::example` demos, Vue components,
`[[toc]]`, bound `<a :href>` / `<img :src>`, etc.) via **`strip.js`**,
resolves injected `{{ $page.* }}` values, rebases root-relative links
for the docs `base`, and writes a **clean `.md` companion** beside each
HTML page plus an aggregated **`llms-full.txt`** (with absolute links in
the corpus). **`context7.json`** points Context7-style tooling at the
`docs` folder with project-specific rules.
> 
> The **local theme** injects **`ViewMarkdownLink`** (“View as
Markdown”) on every page; **`setup-coding-agent.md`** and
**`CodingAgentWizard`** document Claude Code skills, Cursor/Copilot
rules, MCP (GitMCP / Context7), and copyable snippets (via
**`clipboard.js`**). Sidebar and **`DEV_DOCS.md`** /
**`docs/README.md`** are updated accordingly. **Netlify** build uses
**Node 22**; **`.eslintrc.js`** gets a minor override-array syntax fix.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f2e95fc. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
Co-authored-by: Kuba Sekowski <sequba@gmail.com>
…sand separator (DEV-2120) (handsontable#1713)

## Summary

Entering a long run of digits ending in a non-digit character (e.g.
`012345678901234567890123456789012345678901234567890123456789a`) into a
cell froze the page when formulas were enabled. Reported as
[handsontable#1520](handsontable#1520), where
the non-digit character is a space. DEV-2120 / HOT-9767.

## Root cause

`NumberLiteralHelper` builds its number-detection pattern by
interpolating the configured separators. With the **default**
`thousandSeparator: ''`, the group `(${thousandSeparator}\d{3,})*`
degenerates to `(\d{3,})*` placed immediately after `\d+`:

```
^([+-]?((\.\d+)|(\d+(\d{3,})*(\.\d*)?)))([eE][+-]?\d+)?$
             └──── nested quantifiers on the same class ────┘
```

For a long digit run that ultimately fails to match (trailing
non-digit), the engine explores exponentially many ways to partition the
digits between `\d+` and the repeated `\d{3,}` group — classic
catastrophic backtracking (ReDoS). Parse time roughly doubles every ~2
characters, so a 60-character input never returns.

The same pattern is reached from raw cell input (`CellContentParser`)
**and** from string→number coercion during formula evaluation
(`ArithmeticHelper`), so `=VALUE("…")` and arithmetic over such text
hung too. Fixing the pattern builder covers all entry points.

## Fix

Omit the thousand-separator group entirely when the separator is empty.
The emitted pattern for a non-empty separator (`,`, ` `, `.`) is
byte-for-byte unchanged — a literal separator is a mandatory anchor
between repetitions, so no ambiguous partition exists and those configs
were never vulnerable.

## Testing

Paired tests in handsontable/hyperformula-tests (branch
`fix/dev-2120-redos-number-parsing`):
- white-box guard that the default-config pattern contains no nested
digit quantifier (deterministic regression tripwire — a synchronous
ReDoS cannot be caught by a per-test timeout);
- behavioral coverage: trailing letter, trailing non-letter symbol,
separator matrix, long-integer value fidelity;
- end-to-end via `setCellContents` (raw, percent, currency) and formula
coercion (`=VALUE(...)`);
- the verbatim reproduction from
[handsontable#1520](handsontable#1520) (90
digits, a space, then `123`) built through `buildFromArray` with the
sheet layout from the issue, including the dependent `=SUM(A1,B1)`
formula.

## Reviewer notes

- **Why the white-box test** (asserting `numberPattern.source` has no
`(\d{3,})*`): a *synchronous* ReDoS cannot be caught by a Jest/Jasmine
per-test timeout — the timer can't fire while the regex is stuck on the
main thread — so asserting the emitted pattern shape is the one
deterministic regression tripwire. The behavioral/e2e tests still cover
actual behavior.
- **Verified against the reported input, not just a variant**: with the
`NumberLiteralHelper` change reverted, the new `handsontable#1520` test hangs until
killed (`timeout 90` → exit 124); with the fix it finishes in ~20 ms.
The default-config pattern goes from
`^([+-]?((\.\d+)|(\d+(\d{3,})*(\.\d*)?)))([eE][+-]?\d+)?$` to
`^([+-]?((\.\d+)|(\d+(\.\d*)?)))([eE][+-]?\d+)?$`.
- **Why no input-length cap**: the fix sits in the pattern builder, so
it covers every entry point at once, and non-empty separators are
provably linear (the literal separator anchors each repetition). A
length cap would be complementary defense-in-depth — deliberately left
out to keep this fix focused on the root cause.

## Notes

Branch brought up to date with `develop` by merge (not rebase) to
preserve review history.

Long-standing issue (reproduced on docs v17.1 and v18.0), not a v18
regression.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, targeted regex construction change with unchanged behavior for
non-empty thousand separators; low risk aside from edge cases in numeric
string detection.
> 
> **Overview**
> Fixes **UI freezes** when users enter a long digit string that fails
number parsing (e.g. trailing letter or space before more digits),
including the
[handsontable#1520](handsontable#1520)
reproduction.
> 
> `NumberLiteralHelper` no longer emits the `(\d{3,})*`
thousand-separator group when `thousandSeparator` is the default empty
string. That degenerate pattern sat next to `\d+` and caused
**catastrophic backtracking** on near-miss inputs; the same helper is
used for raw cell parsing and formula coercion (`VALUE`, arithmetic), so
one regex change covers those paths. Configs with a non-empty thousand
separator keep the previous pattern shape.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6ad1637. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…-249) (handsontable#1692)

## What & why

A function picker — the Formula Builder's, or any integrator's — needs
to answer two questions: *what functions exist?* and *what do this
function's arguments mean?* HyperFormula could only answer the first.
`getRegisteredFunctionNames()` returned 423 translated names and nothing
else; `getAllFunctionPlugins()` exposed `implementedFunctions`, which
has coercion rules and arity but no category, description,
human-readable parameter names or examples. Everything else a picker
needs existed only as prose, in the 551-line hand-maintained
`docs/guide/built-in-functions.md`.

And prose with no second copy drifts silently. That page published
`FVSCHEDULE` as `FV(Pv, Schedule)`, `RANDBETWEEN` as `RAND(...)`,
`COLUMN` as `COLUMNS(...)`, `F.TEST` with `Z.TEST`'s signature, `T.TEST`
with two of its four arguments, `DAYS360`'s arguments reversed; it
listed `NORMDIST` twice (the second row was really `NORMSDIST`) and
omitted `VERSION`, callable and counted in the page's own printed total
since 2020. Every one is a copy-paste from a neighbouring row — the
signature of data nothing can check against the thing it describes.

**So this PR moves that data into `src/` and reads it back through the
engine's own public API, making the published reference just its first
consumer.** A new authored catalogue under
`src/interpreter/functionMetadata/` (370 entries, one file per category,
730 parameter descriptions and 679 examples) supplies category,
description, `snake_case` parameter names and descriptions, examples and
a docs link; `implementedFunctions` still supplies arity and
optionality. Two methods expose the join, static and per-instance:

- **`getAvailableFunctions(code)`** → one cheap entry per function
(`localizedName`, `canonicalName`, `category`, `shortDescription`, plus
`aliasOf` on the 53 aliases), sorted by localized name under a collator
built from that language — enough to paint all 423 picker rows without a
second call.
- **`getFunctionDetails(canonicalName, code)`** → adds the ordered
`parameters` (name, description, `optional`), `repeatLastArgs`,
`documentationUrl` and `examples`. Deliberately **no pre-rendered syntax
string**; the caller composes `SUMIF(range, criteria, [sum_range])`
itself, and `script/formatFunctionSyntax.ts` is a reference
implementation of that renderer, kept out of `src/` on purpose.

`docs/guide/built-in-functions.md` becomes a build product of *the
public API* rather than of the catalogue directly — so generating the
page exercises the same alias resolution, listability gate and
optionality derivation a customer's picker will. It leaves git, is
gitignored, and is regenerated from `built-in-functions.tmpl.md` as the
first step of `docs:dev`/`docs:build`. "The docs are wrong" and "the API
is wrong" are now the same bug.

The regenerated page adds `VERSION`, gives `NORMSDIST` its own row, adds
a table of contents and a per-function anchor, and corrects optionality
on **27 functions** the old page showed as required (`IF`, `LOG`,
`ROUND`, `SUMIF`, `VLOOKUP`, the `*2*` conversions, …). Nine more change
only *notation*: a repeating argument group is now rendered as `...`
against `repeatLastArgs` instead of the old hand-written `[Range2,
Criterion2 [, ...RangeN, CriterionN]]`. `SWITCH` was also semantically
wrong — its parameters were `expression, value1, expression2`, but the
third argument is the *result* returned on a match.

## Design decisions worth a second opinion

- **The catalogue is authored data, not derived**, so it must be kept in
step with `implementedFunctions` by hand. Parameter *count* is
cross-checked, and on a mismatch **the implementation wins**:
`getFunctionDetails` reports one parameter per implemented argument
under positional names (`Arg1`, `Arg2`, …), discards the authored names
and descriptions, and warns on the console naming the function.
Category, description, examples and URL still come from the entry, and
the function stays listed in both tiers — drift costs the parameter
prose, never the availability. `DEV_DOCS.md` documents this and the
remaining silent-failure mode (an entry left behind after a rename
describes nothing and merely ships in the bundle).
- **Optionality is deliberately not authored or cross-checked** —
`optional` comes only from `optionalArg`/`defaultValue`. Hence the
single production edit outside the new module: `optionalArg: true` on
`SHEET`/`SHEETS`, which have always accepted `=SHEET()` while declaring
the argument required. Metadata-only and behaviour-neutral
(`runFunctionWithReferenceArgument` returns before argument-count
validation), and a sweep of all 423 ids found no other function with
this mismatch.
- **One rule decides how a function is described: does the catalogue
hold an entry for its id?** The catalogue is keyed by id, not by
implementation, so a user plugin registered over `SUMIF` is described
with `SUMIF`'s authored category and description, over its own
signature. An earlier revision gated this on a snapshot of built-in
plugin ownership, so a shadow reported as `'Custom'`; that is gone. It
bought little — a plugin re-implementing `SUMIF` is usually still a
`SUMIF` — and cost a module-init hook in `index.ts` (the plugin
identities can only come from the plugin barrel, and importing it from
the registry creates a load-order cycle that breaks the bundled build)
plus a second way for the whole built-in set to silently degrade to
`'Custom'` if that hook ever failed to run. **This is the bullet I'd
most like a second opinion on.**
- **Both tiers describe every registered function, custom ones
included.** `registerFunctionPlugin` is global, so a custom function is
callable everywhere and the static methods list it; the instance methods
list that instance's own registry, which differs when it was built with
the `functionPlugins` option. An id with no translation entry for the
active language is omitted from both, because the interpreter refuses to
evaluate it.
- **A custom function omits the fields it cannot author**, rather than
reporting an empty one: `shortDescription`, `documentationUrl` and
`examples` are absent (and optional in the public types, as `aliasOf`
already was), so a consumer can tell "no authored description" from "an
empty one" and the object survives `JSON.stringify` unchanged. Built-ins
are unaffected — the catalogue authors all three for every entry.
- **An instance describes its functions under the translation package it
was built with**, not under whatever is registered globally for that
code today. Otherwise re-registering a language could make the API
advertise a localized name that instance refuses to evaluate.
- **Exported:** `FunctionListEntry`, `FunctionDetails`,
`FunctionParameterDescription`, `FunctionCategory`.
`FUNCTION_CATEGORIES`, `FunctionDoc` and `CUSTOM_FUNCTION_CATEGORY` stay
internal — so a TS consumer cannot enumerate the categories to build a
filter and must compare against `'Custom'` as a string. Worth confirming
that is the right line.
- **`canonicalName` is matched exactly**: case-sensitive (`'sumif'` →
`undefined`, though `=sumif(...)` evaluates) and canonical English only.
Likeliest integration pitfall for a picker holding translated names.

## Already reviewed

Roughly three-quarters of the develop-diff is already reviewed and
merged, as sub-PRs into this branch: **handsontable#1699** (page generated from the
API), **handsontable#1705** (HF-300: examples, docs URLs, parameter descriptions),
**handsontable#1709** (`snake_case` parameter names), **handsontable#1710** (invalid-locale
collator guard). New here: the metadata API itself, the catalogue-keyed
resolution rule, custom functions in the static tier, `SHEET`/`SHEETS`,
the generated table of contents, and ~55 descriptions rewritten because
they documented Excel rather than HyperFormula — with the deviations
added to `list-of-differences.md` (`INT` truncates toward zero, `MOD`
takes the dividend's sign, `ISEVEN`/`ISODD` don't truncate,
`CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1).

## How I tested

Paired suite:
[handsontable/hyperformula-tests#14](handsontable/hyperformula-tests#14)
(branch `feature/hf-249-function-metadata-api`), 134 tests for this API
alone, green with the full repository suite (502 files, 6,214 tests). It
covers the static/instance split, i18n across all 18 packs, aliases,
custom functions, plugins shadowing a built-in id or a built-in alias
id, locale-aware ordering, and prototype-key ids (`toString`,
`__proto__`) — plus the two invariants most worth protecting: **every
canonical id declared by a registered built-in plugin resolves to
details**, so a missing catalogue entry fails CI instead of silently
dropping a function, and **the list and the details always agree on
which ids exist**. Each guard was mutation-tested: broken deliberately,
confirmed red, reverted. Assertions avoid jest-only matchers and never
rely on jest ignoring a key valued `undefined`, so they fail under the
jasmine/karma browser job too. Separately, all 679 authored examples
parse and name their own function, and a sampled slice is pinned to
Excel-cross-checked values.

## Known trade-offs

- **`examples` are English-spelled and `OFFSET` is lexed from its
translated name**, so `getFunctionDetails('OFFSET','deDE').examples`
yields `#NAME?` in all 16 non-English packs — and `ISREF`'s example
embeds `OFFSET`, returning `true` in enGB but `false` in plPL with no
error. The one item I'd want accepted with eyes open.
- **`SWITCH` publishes `repeatLastArgs: 1`**, understating its (value,
result) pair group. It cannot simply become `2`: the field also drives
runtime arity validation, and the optional trailing default needs a step
of 1.
- **`documentationUrl` is the same page for all 423 ids.** Per-function
anchors now exist on the generated page, so `#${canonicalName}` is a
follow-up, not a redesign.
- **The catalogue ships in the bundle** (~25 KB gzipped) and is not
tree-shakeable — `HyperFormula` and `FunctionRegistry` both import it
eagerly.

## Open for the reviewer

- **`CHANGELOG.md`** names only the two methods; it should also name the
four exported types, and needs a `### Changed` line for `SHEET`/`SHEETS`
now reporting their argument as optional.
- **`DEV_DOCS.md` carries general engineering policy** unrelated to
HF-249 (a `## Performance` section, six code-style bullets, and
additions to Definition of Done, Automatic tests and Documentation) —
which the atomic-PR rule added in this same PR says belongs elsewhere.
Split them out, or accept them explicitly.

Source: https://app.clickup.com/t/9015210959/HF-249

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Introduces a large, permanent public API and ships the full catalogue
in the bundle (~25 KB gzipped), while changing how published function
reference docs are produced; runtime formula evaluation is largely
unchanged aside from metadata alignment (e.g. optional reporting for
zero-arg reference functions).
> 
> **Overview**
> Adds **`getAvailableFunctions`** and **`getFunctionDetails`** (static
and instance) so integrators can build function pickers from engine data
instead of scraping docs. Metadata is authored in a new per-category
catalogue under `src/interpreter/functionMetadata/` (joined with
`implementedFunctions` for arity, optionality, and `repeatLastArgs`);
custom functions appear with category `'Custom'` and positional `ArgN`
names unless a plugin shadows a built-in id, in which case the catalogue
entry for that id still applies.
> 
> The hand-maintained **`docs/guide/built-in-functions.md`** is removed
from version control and **regenerated** at build time from
`built-in-functions.tmpl.md` plus the same API (`npm run
docs:generate-function-docs`, wired into `docs:dev` / `docs:build`).
VuePress excludes the template from routes and disables “edit this page”
on the generated guide.
> 
> Also exports **`FunctionListEntry`**, **`FunctionDetails`**,
**`FunctionParameterDescription`**, and **`FunctionCategory`**;
documents the catalogue workflow in **`DEV_DOCS.md`**; expands
**`repeatLastArgs`** guidance in the custom-functions guide; and records
additional Excel vs HyperFormula differences in
**`list-of-differences.md`**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f22560e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
Co-authored-by: Cursor Opus 5 <noreply@cursor.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@netlify

netlify Bot commented Jul 31, 2026

Copy link
Copy Markdown

👷 Deploy request for hyperformula-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit b3a38af

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants