Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b75de4c
feat(metadata): add formatFunctionSyntax helper (HF-249)
marcin-kordas-hoc Jun 29, 2026
1786603
feat(metadata): add pure built-in functions table renderer and marker…
marcin-kordas-hoc Jun 29, 2026
d4daccd
docs(guide): add autogeneration markers around the functions table (H…
marcin-kordas-hoc Jun 29, 2026
785bdba
docs(guide): keep functions-count line outside the autogeneration mar…
marcin-kordas-hoc Jun 29, 2026
4cb3744
docs(guide): place do-not-edit note above the START marker so regener…
marcin-kordas-hoc Jun 29, 2026
b25b121
feat(docs): generate built-in functions table from HF API (HF-249)
marcin-kordas-hoc Jun 29, 2026
bf81613
feat(metadata): surface OFFSET and VERSION via the function-metadata …
marcin-kordas-hoc Jul 1, 2026
6ca49cf
docs(guide): regenerate built-in functions table to include OFFSET an…
marcin-kordas-hoc Jul 1, 2026
1a04bc9
feat(docs): fail the drift check when the function set changes (HF-249)
marcin-kordas-hoc Jul 1, 2026
37ab896
style(docs): join id arrays in drift-check message to satisfy lint (H…
marcin-kordas-hoc Jul 1, 2026
9b84536
build(docs): add docs:generate-functions and drift-check scripts (HF-…
marcin-kordas-hoc Jul 1, 2026
d0918d3
docs(dev): avoid asserting CI wiring not yet added (HF-249)
marcin-kordas-hoc Jul 1, 2026
de80dbd
ci(docs): fail build-docs if the functions table is stale (HF-249)
marcin-kordas-hoc Jul 1, 2026
cb2ad0d
fix(metadata): break renderer ties by canonicalName, soften stale pro…
marcin-kordas-hoc Jul 1, 2026
4b5ef17
docs(metadata): drop internal spec ref, note English-only collator (H…
marcin-kordas-hoc Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Check built-in functions table is up to date
run: npm run docs:functions:check

- name: Build docs
run: npm run docs:build
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Added the `getAvailableFunctions()` and `getFunctionDetails()` methods (both static and instance) for retrieving function metadata. [#1692](https://github.com/handsontable/hyperformula/pull/1692)
- The built-in functions guide table is now generated from the function metadata API (single source of truth), and now also documents OFFSET and VERSION. [#1692](https://github.com/handsontable/hyperformula/pull/1692)
- Added an Indonesian (Bahasa Indonesia) language pack. [#1674](https://github.com/handsontable/hyperformula/pull/1674)

## [3.3.0] - 2026-05-20
Expand Down
5 changes: 5 additions & 0 deletions DEV_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ Adding a built-in function is similar to adding a [custom function](docs/guide/c
4. Add translations to all language files in `src/i18n/languages/`.
5. Add tests in `test/unit/interpreter/`.

The built-in functions table in `docs/guide/built-in-functions.md` is generated from HyperFormula's API
(`getAvailableFunctions`/`getFunctionDetails`). Do not hand-edit the region between the
`AUTOGENERATED:FUNCTIONS` markers — change the metadata in `src/interpreter/functionMetadata/` and run
`npm run docs:generate-functions`. Run `npm run docs:functions:check` in CI to block a stale table.

## Code style

- Prefer a functional approach where possible (`filter`, `map`, `reduce`).
Expand Down
896 changes: 451 additions & 445 deletions docs/guide/built-in-functions.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"docs:code-examples:generate-js": "bash docs/code-examples-generator.sh",
"docs:code-examples:generate-all-js": "bash docs/code-examples-generator.sh --generateAll",
"docs:code-examples:format-all-ts": "bash docs/code-examples-generator.sh --formatAllTsExamples",
"docs:generate-functions": "npm run tsnode scripts/generate-builtin-functions-doc.ts",
"docs:functions:check": "npm run tsnode scripts/generate-builtin-functions-doc.ts -- --check",
"bundle-all": "cross-env HF_COMPILE=1 npm-run-all clean compile bundle:** verify-bundles",
"bundle:es": "(node script/if-ne-env.js HF_COMPILE=1 || npm run compile) && cross-env-shell BABEL_ENV=es env-cmd -f ht.config.js babel lib --out-file-extension .mjs --out-dir es",
"bundle:cjs": "(node script/if-ne-env.js HF_COMPILE=1 || npm run compile) && cross-env-shell BABEL_ENV=commonjs env-cmd -f ht.config.js babel lib --out-dir commonjs",
Expand Down
60 changes: 60 additions & 0 deletions scripts/generate-builtin-functions-doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

/**
* HF-249 bullet 3 — generates the built-in functions table in `docs/guide/built-in-functions.md` from
* HyperFormula's public API (single source of truth). Dev-only; never shipped (`tsconfig.json` `include` is
* `["src"]`). Run: `npm run tsnode scripts/generate-builtin-functions-doc.ts` (writes the file) or
* `... --check` (CI: regenerate in memory, fail on drift / membership mismatch, write nothing).
*/

import * as fs from 'fs'
import * as path from 'path'
import {HyperFormula} from '../src'
import {renderBuiltinFunctionsTable, spliceFunctionsTable} from '../src/interpreter/functionMetadata/renderBuiltinFunctionsTable'

const REPO_ROOT = path.resolve(__dirname, '..')
const DOC_PATH = path.join(REPO_ROOT, 'docs/guide/built-in-functions.md')
const LANGUAGE = 'enGB'

/** Reads the doc file and returns a new version with the generated table region spliced in. */
function buildUpdatedFile(): string {
const entries = HyperFormula.getAvailableFunctions(LANGUAGE)
const detailsFor = (canonicalName: string) => HyperFormula.getFunctionDetails(canonicalName, LANGUAGE)
const generated = renderBuiltinFunctionsTable(entries, detailsFor)
const current = fs.readFileSync(DOC_PATH, 'utf8')
return spliceFunctionsTable(current, generated)
}

/** Writes the regenerated file (default) or exits non-zero if the file is out of date (`--check`). */
function main(): void {
const check = process.argv.includes('--check')
const updated = buildUpdatedFile()
const current = fs.readFileSync(DOC_PATH, 'utf8')
if (check) {
// Function IDs live in the per-function anchors (`<a id="SUM"></a>`).
const idsIn = (text: string) => new Set(
[...text.matchAll(/<a id="([^"]+)"><\/a>/g)].map(match => match[1])
)
const generatedIds = idsIn(updated)
const currentIds = idsIn(current)
const dropped = [...currentIds].filter(id => !generatedIds.has(id))
const added = [...generatedIds].filter(id => !currentIds.has(id))
if (dropped.length > 0 || added.length > 0) {
process.stderr.write(`Function set changed. dropped=[${dropped.join(', ')}] added=[${added.join(', ')}]\n`)
process.exit(1)
}
if (updated !== current) {
process.stderr.write('built-in-functions.md is out of date. Run `npm run docs:generate-functions`.\n')
process.exit(1)
}
process.stdout.write('built-in-functions.md is up to date.\n')
return
}
fs.writeFileSync(DOC_PATH, updated, 'utf8')
process.stdout.write('built-in-functions.md regenerated.\n')
}

main()
9 changes: 9 additions & 0 deletions src/HyperFormula.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {FunctionPluginDefinition} from './interpreter'
import {FUNCTION_DOCS} from './interpreter/functionMetadata'
import {buildCustomFunctionDetails, buildCustomFunctionListEntry, buildFunctionDetails, buildFunctionListEntry, StructuralMetadata} from './interpreter/functionMetadata/buildFunctionDescriptions'
import {FunctionDetails, FunctionDoc, FunctionListEntry} from './interpreter/functionMetadata/FunctionDescription'
import {PROTECTED_FUNCTION_METADATA} from './interpreter/functionMetadata/protectedFunctionMetadata'
import {FunctionRegistry, FunctionTranslationsPackage} from './interpreter/FunctionRegistry'
import {FormatInfo} from './interpreter/InterpreterValue'
import {LazilyTransformingAstService} from './LazilyTransformingAstService'
Expand Down Expand Up @@ -727,6 +728,14 @@ export class HyperFormula implements TypedEmitter {
* @param {FunctionPluginDefinition | undefined} plugin - the plugin registered for `functionId`, or `undefined`
*/
private static resolveFunctionMetadata(functionId: string, plugin: FunctionPluginDefinition | undefined): { doc: FunctionDoc | undefined, metadata: StructuralMetadata } | undefined {
// Protected ids (VERSION, OFFSET) are excluded from the plugin registry by design (`getFunctionPlugin` always
// returns `undefined` for them), so they would otherwise fall straight into the `plugin === undefined` case
// below and disappear from the metadata API. Kuba decided (HF-249) that they must still be described, because
// a user can call them from a formula: resolve them here from the authored catalogue doc and structural
// metadata instead of from a plugin. Functions without a catalogue entry stay unlisted.
if (FunctionRegistry.functionIsProtected(functionId) && FUNCTION_DOCS[functionId] !== undefined) {
return {doc: FUNCTION_DOCS[functionId], metadata: PROTECTED_FUNCTION_METADATA[functionId]}
}
if (plugin === undefined) {
return undefined
}
Expand Down
23 changes: 17 additions & 6 deletions src/interpreter/FunctionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,15 @@ export class FunctionRegistry {
}

/**
* Returns the ids of all registered, non-protected functions, including aliases. Used by the function-metadata
* API to list every function reachable in a formula (built-in canonical ids plus their aliases).
* Returns the ids of all functions the function-metadata API (`getAvailableFunctions`/`getFunctionDetails`)
* should describe: every registered non-protected function (built-in canonical ids plus their aliases), plus
* the protected ids (e.g. `VERSION`, `OFFSET`). Protected ids are excluded from registration (`this.plugins`)
* so they can never be unregistered or shadowed, but a user can still call them from a formula, so the metadata
* API surfaces them too (HF-249). Safe to include here: this method is consumed only by the metadata API, never
* by anything that would let a caller register/unregister against a protected id.
*/
public static getListableFunctionIds(): string[] {
return Array.from(this.plugins.keys())
return [...this.plugins.keys(), ...this._protectedPlugins.keys()]
}

public static getFunctionPlugin(functionId: string): Maybe<FunctionPluginDefinition> {
Expand Down Expand Up @@ -283,11 +287,18 @@ export class FunctionRegistry {
}

/**
* Returns the ids of all functions registered in this instance, including aliases and any custom (user-registered)
* functions, excluding protected ids. Used by the instance-level function-metadata API.
* Returns the ids of all functions the instance-level function-metadata API should describe: every function
* registered in this instance (aliases and any custom/user-registered functions included), plus the protected
* ids (e.g. `VERSION`, `OFFSET`). `instancePlugins` already contains `VERSION` (the constructor loads any
* protected id that has a plugin, unprotected, so it can be executed), but not `OFFSET` (it has no plugin at
* all — it is transformed at parse time). Appending `FunctionRegistry._protectedPlugins`'s keys explicitly, the
* same way the static {@link FunctionRegistry.getListableFunctionIds} does, covers both uniformly instead of
* relying on the constructor's incidental loading; de-duplicated via `Set` because `VERSION` would otherwise be
* listed twice (once from `instancePlugins`, once from `_protectedPlugins`). A user can see these ids via
* `getAvailableFunctions`/`getFunctionDetails` even though they can never be unregistered (HF-249).
*/
public getListableFunctionIds(): string[] {
return Array.from(this.instancePlugins.keys()).filter(id => !FunctionRegistry.functionIsProtected(id))
return Array.from(new Set([...this.instancePlugins.keys(), ...FunctionRegistry._protectedPlugins.keys()]))
}

public getFunction(functionId: string): Maybe<PluginFunctionType> {
Expand Down
13 changes: 11 additions & 2 deletions src/interpreter/functionMetadata/categories/information.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import {FunctionDoc} from '../FunctionDescription'

/**
* Catalogue entries for the "Information" category. Generated from `docs/guide/built-in-functions.md` by
* `scripts/hf249-migrate-function-docs.ts`; parameter descriptions are authored in a later phase.
* Catalogue entries for the "Information" category. Most entries were migrated from `docs/guide/built-in-functions.md`
* by `scripts/hf249-migrate-function-docs.ts`; some (e.g. the protected `VERSION` function) are hand-authored.
* Parameter descriptions are authored in a later phase.
*/
export const INFORMATION_DOCS: Record<string, FunctionDoc> = {
ISBINARY: {
Expand Down Expand Up @@ -90,4 +91,12 @@ export const INFORMATION_DOCS: Record<string, FunctionDoc> = {
shortDescription: 'Returns number of sheet of a given reference or number of all sheets in workbook when no argument is provided.',
parameters: [{name: 'Value', description: ''}],
},
// VERSION is a protected function (its plugin, VersionPlugin, is kept out of the general registry so it can
// never be unregistered; see `FunctionRegistry._protectedPlugins`). Its structural metadata (parameters,
// repeatLastArgs) is authored separately in `PROTECTED_FUNCTION_METADATA`.
VERSION: {
category: 'Information',
shortDescription: 'Returns the version number of HyperFormula.',
parameters: [],
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import {FunctionDoc} from '../FunctionDescription'

/**
* Catalogue entries for the "Lookup and reference" category. Generated from `docs/guide/built-in-functions.md` by
* `scripts/hf249-migrate-function-docs.ts`; parameter descriptions are authored in a later phase.
* Catalogue entries for the "Lookup and reference" category. Most entries were migrated from
* `docs/guide/built-in-functions.md` by `scripts/hf249-migrate-function-docs.ts`; some (e.g. the protected `OFFSET`
* function) are hand-authored. Parameter descriptions are authored in a later phase.
*/
export const LOOKUP_AND_REFERENCE_DOCS: Record<string, FunctionDoc> = {
ADDRESS: {
Expand Down Expand Up @@ -55,6 +56,14 @@ export const LOOKUP_AND_REFERENCE_DOCS: Record<string, FunctionDoc> = {
shortDescription: 'Returns the relative position of an item in an array that matches a specified value.',
parameters: [{name: 'Searchcriterion', description: ''}, {name: 'LookupArray', description: ''}, {name: 'MatchType', description: ''}],
},
// OFFSET is a protected function (parse-time transformed into a cell/range reference, so it has no plugin or
// implementation metadata; see `FunctionRegistry._protectedPlugins`). Its structural metadata (parameter
// optionality, repeatLastArgs) is authored separately in `PROTECTED_FUNCTION_METADATA`.
OFFSET: {
category: 'Lookup and reference',
shortDescription: 'Returns the value of a cell offset by a certain number of rows and columns from a given reference point.',
parameters: [{name: 'Reference', description: ''}, {name: 'Rows', description: ''}, {name: 'Columns', description: ''}, {name: 'Height', description: ''}, {name: 'Width', description: ''}],
},
ROW: {
category: 'Lookup and reference',
shortDescription: 'Returns row number of a given reference or formula reference if argument not provided.',
Expand Down
25 changes: 25 additions & 0 deletions src/interpreter/functionMetadata/formatFunctionSyntax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

/**
* Builds the human-readable syntax string for a function from its parameter list, e.g. `SUM(Number1, ...)`.
* Reuses the convention previously shipped as `generateSyntax`: each optional parameter is wrapped in its own
* `[brackets]`, and a single `, ...` suffix denotes that the trailing arguments repeat. Internal to the
* functionMetadata module (docs generator + future consumers); not exported from the package index.
*
* @param {string} localizedName - the function name as shown to the user, e.g. `'SUMIF'`
* @param {{ name: string, optional: boolean }[]} parameters - ordered parameters with optionality
* @param {number} repeatLastArgs - number of trailing parameters that repeat; `> 0` adds the `, ...` suffix
* @returns {string} the syntax string, e.g. `'SUMIF(Range, Criteria, [Sumrange])'`
*/
export function formatFunctionSyntax(
localizedName: string,
parameters: { name: string, optional: boolean }[],
repeatLastArgs: number,
): string {
const rendered = parameters.map(parameter => parameter.optional ? `[${parameter.name}]` : parameter.name)
const repeatSuffix = repeatLastArgs > 0 ? ', ...' : ''
return `${localizedName}(${rendered.join(', ')}${repeatSuffix})`
}
40 changes: 40 additions & 0 deletions src/interpreter/functionMetadata/protectedFunctionMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

import {FunctionArgumentType} from '../plugin/FunctionPlugin'
import {StructuralMetadata} from './buildFunctionDescriptions'

/**
* Authored structural metadata (parameter optionality, `repeatLastArgs`) for the protected built-in functions
* (`FunctionRegistry._protectedPlugins`), keyed by canonical id.
*
* Protected ids are not registered as ordinary plugins, so they carry no `implementedFunctions` entry for
* {@link HyperFormula.resolveFunctionMetadata} to read arity/optionality from:
* - `OFFSET` is transformed at parse time into a cell/range reference and has no plugin at all.
* - `VERSION` has a plugin (`VersionPlugin`), but it is deliberately excluded from the general registry so it can
* never be unregistered; reusing its `implementedFunctions` entry would require importing the protected plugin
* into the metadata builders, which this module avoids.
*
* Each entry's `parameters.length` MUST equal the corresponding `FunctionDoc.parameters.length` in `FUNCTION_DOCS`.
* Unlike plugin-backed functions, `HyperFormula.resolveFunctionMetadata`'s arity cross-check does not run for
* protected ids (they return early, before that check), so this invariant is not enforced at runtime — it must be
* kept true by hand when editing either map, otherwise `buildFunctionDetails` throws for the mismatched id.
*/
export const PROTECTED_FUNCTION_METADATA: Record<string, StructuralMetadata> = {
OFFSET: {
parameters: [
{argumentType: FunctionArgumentType.ANY},
{argumentType: FunctionArgumentType.NUMBER},
{argumentType: FunctionArgumentType.NUMBER},
{argumentType: FunctionArgumentType.NUMBER, optionalArg: true},
{argumentType: FunctionArgumentType.NUMBER, optionalArg: true},
],
repeatLastArgs: 0,
},
VERSION: {
parameters: [],
repeatLastArgs: 0,
},
}
Loading
Loading