Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
64746c1
feat(metadata): add formatFunctionSyntax helper (HF-249)
marcin-kordas-hoc Jun 29, 2026
014d735
feat(metadata): add pure built-in functions table renderer and marker…
marcin-kordas-hoc Jun 29, 2026
366e6f5
docs(guide): add autogeneration markers around the functions table (H…
marcin-kordas-hoc Jun 29, 2026
6111bb9
docs(guide): keep functions-count line outside the autogeneration mar…
marcin-kordas-hoc Jun 29, 2026
3738a50
docs(guide): place do-not-edit note above the START marker so regener…
marcin-kordas-hoc Jun 29, 2026
080838c
feat(docs): generate built-in functions table from HF API (HF-249)
marcin-kordas-hoc Jun 29, 2026
2ec0142
feat(metadata): surface OFFSET and VERSION via the function-metadata …
marcin-kordas-hoc Jul 1, 2026
f52cbaa
docs(guide): regenerate built-in functions table to include OFFSET an…
marcin-kordas-hoc Jul 1, 2026
b8b701f
feat(docs): fail the drift check when the function set changes (HF-249)
marcin-kordas-hoc Jul 1, 2026
f25e4c8
style(docs): join id arrays in drift-check message to satisfy lint (H…
marcin-kordas-hoc Jul 1, 2026
4d228dc
build(docs): add docs:generate-functions and drift-check scripts (HF-…
marcin-kordas-hoc Jul 1, 2026
0fce9e2
docs(dev): avoid asserting CI wiring not yet added (HF-249)
marcin-kordas-hoc Jul 1, 2026
52f1096
ci(docs): fail build-docs if the functions table is stale (HF-249)
marcin-kordas-hoc Jul 1, 2026
cfa6cd8
fix(metadata): break renderer ties by canonicalName, soften stale pro…
marcin-kordas-hoc Jul 1, 2026
18615a3
docs(metadata): drop internal spec ref, note English-only collator (H…
marcin-kordas-hoc Jul 1, 2026
bb9ef76
fix(metadata): keep static getFunctionDetails protected-aware after #…
marcin-kordas-hoc Jul 15, 2026
0fd31f7
build(docs): regenerate the functions table at docs:build; drop the C…
marcin-kordas-hoc Jul 15, 2026
66a5da5
feat(metadata): strip doc markup from getFunctionDetails shortDescrip…
marcin-kordas-hoc Jul 15, 2026
3d7bbe6
chore(metadata): remove the one-time migration script and its danglin…
marcin-kordas-hoc Jul 15, 2026
f6dde48
fix(metadata): snake_case OFFSET parameter names to match the catalog…
marcin-kordas-hoc Jul 15, 2026
490c6eb
docs(guide): regenerate built-in functions table from the catalogue (…
marcin-kordas-hoc Jul 15, 2026
7ae63e8
fix(metadata): keep protected metadata resolution fail-safe when stru…
marcin-kordas-hoc Jul 15, 2026
b1098fd
chore(docs): drop the vestigial --check mode from the functions-table…
marcin-kordas-hoc Jul 15, 2026
2181b70
Revert "feat(metadata): strip doc markup from getFunctionDetails shor…
marcin-kordas-hoc Jul 16, 2026
a8aa6f5
build(docs): generate built-in-functions.md as a gitignored build pro…
marcin-kordas-hoc Jul 16, 2026
fc9684d
refactor(docs): address review — move generator helpers to scripts/, …
marcin-kordas-hoc Jul 16, 2026
eba2a21
Merge feature/hf-249-function-metadata-api (brings HF-223) into docs …
marcin-kordas-hoc Jul 22, 2026
e617892
fix(hf-249): documentationUrl is always a string in FunctionDetails
marcin-kordas-hoc Jul 22, 2026
3e19410
fix(hf-249): restore DEFAULT_DOCUMENTATION_URL; explicit '' for prote…
marcin-kordas-hoc Jul 22, 2026
b848802
Merge remote-tracking branch 'upstream/feature/hf-249-function-metada…
marcin-kordas-hoc Jul 23, 2026
0ce7d5f
fix(hf-249): give protected OFFSET/VERSION the shared documentation URL
marcin-kordas-hoc Jul 23, 2026
27b213e
Merge remote-tracking branch 'upstream/feature/hf-249-function-metada…
marcin-kordas-hoc Jul 23, 2026
06f9ee0
docs(hf-249): add SORT and UNIQUE to the function-metadata catalogue
marcin-kordas-hoc Jul 23, 2026
6740603
Add instructions to DEV_DOECS about no automatic tests for code other…
sequba Jul 28, 2026
35b314a
Merge script and scripts directories
sequba Jul 28, 2026
9094337
Merge branch 'feature/hf-249-function-metadata-api' into feature/hf-2…
sequba Jul 28, 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ dev*.html
# Doc snippets are regenerated from the docs before every test run
# (the `test:*` scripts run `snippets:extract` first), so they are never committed.
/test-utils/snippets/*.generated.ts

# Generated at docs:build from built-in-functions.tmpl.md (HF-249 single-source); do not commit.
docs/guide/built-in-functions.md
3 changes: 2 additions & 1 deletion DEV_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ A single pull request should contain an atomic self-contained functional change

## Automatic tests

- All changes to the production code must be covered by automatic tests kept in the `test/` directory.
- All changes to the production code (the `src/` directory) must be covered by automatic tests kept in the `test/` directory.
- Each test case must be very simple and focused on a single assertion. Don't use loops, conditionals, or other control flow statements in test cases.
- Don't add tests for code in the `docs/`, `examples/`, and `script/` directories.

## Documentation

Expand Down
15 changes: 15 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ From the `hyperformula` directory, you can run the following npm scripts:

* `npm run docs:dev` - Starts a local docs server at http://localhost:8080/hyperformula/.
* `npm run docs:build` - Builds the docs output into `/docs/.vuepress/dist`.
* `npm run docs:generate-function-docs` - Regenerates the built-in functions guide page from the function metadata API. Runs automatically as the first step of `docs:dev` and `docs:build`.

## Built-in functions guide page

The built-in functions guide page `docs/guide/built-in-functions.md` is a **build product** and is
**gitignored** — do not commit or hand-edit it. It is generated from two committed sources:

- **prose** (intro, category list, footnotes) lives in `docs/guide/built-in-functions.tmpl.md`, which carries the
empty `AUTOGENERATED:FUNCTIONS` markers;
- the **function table** is rendered from HyperFormula's API (`getAvailableFunctions`/`getFunctionDetails`), i.e.
from the catalogue in `src/interpreter/functionMetadata/`.

`npm run docs:generate-function-docs` splices the table into the template and writes the gitignored page; it runs
automatically as the first step of `npm run docs:build` and `npm run docs:dev`. To change the wording, edit the
template; to change a function's row, edit its catalogue metadata.

## HyperFormula docs directory structure

Expand Down
580 changes: 0 additions & 580 deletions docs/guide/built-in-functions.md

This file was deleted.

69 changes: 69 additions & 0 deletions docs/guide/built-in-functions.tmpl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Built-in functions

<!--
The below dummy div uses a CSS class to alter the .page layout for the current page without any additional customization of VuePress.
It makes the page wider to accommodate large tables
-->
<div class="widePage"></div>
<style>
.page:has(.widePage) .theme-default-content:not(.custom), /* markdown content */
.page:has(.widePage) .page-edit, /* footer containing the "Help us improve the page" link */
.page:has(.widePage) .page-nav /* footer links to the next and prev page */ {
max-width: 1200px !important; /* override default max-width of 740px for this page */
}
/* Make wide tables horizontally scrollable on narrow screens */
.page:has(.widePage) table {
display: block !important;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
</style>

## Overview

HyperFormula comes with an extensive library of pre-built functions. You can use
them to create complex formulas for any business application. Formula syntax and
logic of function are similar to what is considered the standard in modern
spreadsheet software. That is because a spreadsheet is probably the most
universal software ever created. We wanted the same flexibility for HyperFormula
but without the constraints of the spreadsheet UI.

Each of HyperFormula's built-in function names is available in [17 languages](localizing-functions.md#list-of-supported-languages) and [custom language packs](localizing-functions) can be added.

The latest version of HyperFormula has an extensive collection of
**{{ $page.functionsCount }}** functions grouped into categories:

- [Array manipulation](#array-manipulation)
- [Database](#database)
- [Date and time](#date-and-time)
- [Engineering](#engineering)
- [Information](#information)
- [Financial](#financial)
- [Logical](#logical)
- [Lookup and reference](#lookup-and-reference)
- [Math and trigonometry](#math-and-trigonometry)
- [Matrix functions](#matrix-functions)
- [Operator](#operator)
- [Statistical](#statistical)
- [Text](#text)

_Some categories such as compatibility and cube are yet to be supported._

::: tip
You can modify the built-in functions or create your own, by adding a [custom function](custom-functions).
:::

## List of available functions

Total number of functions: **{{ $page.functionsCount }}**

<!-- Generated by `npm run docs:generate-function-docs` from HyperFormula's getAvailableFunctions/getFunctionDetails.
Do not hand-edit between these markers; edit the function metadata in src/ and regenerate. -->
<!-- AUTOGENERATED:FUNCTIONS:START -->
<!-- AUTOGENERATED:FUNCTIONS:END -->

[^non-odff]:
The return value of this function is compliant with the
[OpenDocument standard](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html),
but the return type is not. See
[compatibility notes](list-of-differences.md).
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@
"unpkg": "dist/hyperformula.min.js",
"typings": "./typings/index.d.ts",
"scripts": {
"docs:dev": "npm run typedoc:build-api && cross-env NODE_OPTIONS=--openssl-legacy-provider vuepress dev docs --silent --no-clear-screen --no-cache",
"docs:build": "npm run bundle-all && npm run typedoc:build-api && cross-env NODE_OPTIONS=--openssl-legacy-provider vuepress build docs",
"docs:dev": "npm run docs:generate-function-docs && npm run typedoc:build-api && cross-env NODE_OPTIONS=--openssl-legacy-provider vuepress dev docs --silent --no-clear-screen --no-cache",
"docs:build": "npm run docs:generate-function-docs && npm run bundle-all && npm run typedoc:build-api && cross-env NODE_OPTIONS=--openssl-legacy-provider vuepress build docs",
"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",
"snippets:extract": "node script/extract-doc-snippets.js",
"docs:generate-function-docs": "npm run tsnode script/generate-builtin-functions-doc.ts",
"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
25 changes: 25 additions & 0 deletions script/formatFunctionSyntax.ts
Comment thread
sequba marked this conversation as resolved.
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})`
}
41 changes: 41 additions & 0 deletions script/generate-builtin-functions-doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

/**
* HF-249 bullet 3 — generates the built-in functions guide page `docs/guide/built-in-functions.md` from
* HyperFormula's public API (single source of truth). Reads the committed template
* `built-in-functions.tmpl.md` (hand-written prose + empty `AUTOGENERATED:FUNCTIONS` markers), splices the
* generated table into the marker region, and writes `built-in-functions.md` — which is **gitignored** (a pure
* build product, per Kuba's HF-249 decision). Dev-only; never shipped (`tsconfig.json` `include` is `["src"]`).
* Runs as the first step of `docs:build`/`docs:dev` (also `npm run docs:generate-function-docs`), so the page is
* regenerated on every build and cannot drift from the catalogue.
*/

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

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

/** Reads the committed template and returns the page 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 template = fs.readFileSync(TEMPLATE_PATH, 'utf8')
return spliceFunctionsTable(template, generated)
}

/** Generates the guide page from the template + current catalogue. */
function main(): void {
fs.writeFileSync(DOC_PATH, buildUpdatedFile(), 'utf8')
process.stdout.write('built-in-functions.md generated from template.\n')
}

main()
98 changes: 98 additions & 0 deletions script/renderBuiltinFunctionsTable.ts
Comment thread
sequba marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

import {FUNCTION_CATEGORIES, FunctionCategory, FunctionDetails, FunctionListEntry} from '../src/interpreter/functionMetadata/FunctionDescription'
import {formatFunctionSyntax} from './formatFunctionSyntax'

/** Opening marker; the generated table region begins on the next line. */
export const FUNCTIONS_TABLE_START = '<!-- AUTOGENERATED:FUNCTIONS:START -->'
/** Closing marker; everything after it (e.g. footnote definitions) is hand-maintained. */
export const FUNCTIONS_TABLE_END = '<!-- AUTOGENERATED:FUNCTIONS:END -->'

/**
* Deterministic, environment-independent ordering for function names (never ambient locale). Intentionally
* pinned to English: the generated table is English-only, so ordering must not depend on the host locale. (The
* public API's list ordering is language-derived; this renderer's is deliberately English-only.)
*/
const COLLATOR = new Intl.Collator('en', {sensitivity: 'variant', caseFirst: 'upper'})

/** Escapes only the table-breaking pipe; all other markdown (links, code, `<br>`, footnotes) is verbatim. */
function escapeCell(text: string): string {
return text.replace(/\r?\n/g, '<br>').replace(/\|/g, '\\|')
}

/**
* Renders the built-in functions table as markdown: one `### Category` section per category (in the canonical
* `FUNCTION_CATEGORIES` order), each a 3-column table (Function ID | Description | Syntax). Rows are sorted by a
* pinned collator for deterministic, byte-idempotent output, with the canonical name as a stable tiebreaker for
* entries that share a localized name (mirrors `HyperFormula.buildAvailableFunctions`). Reads only the passed
* entries + details provider, so it is independent of how the catalogue is stored (forward-compatible with a
* per-function file split).
*
* @param {FunctionListEntry[]} entries - the function set to document (e.g. `HyperFormula.getAvailableFunctions`)
* @param {(canonicalName: string) => FunctionDetails | undefined} detailsFor - resolves a function's details
* @returns {string} the markdown for the table region (no surrounding markers, LF, trailing newline)
* @throws {Error} when a listed entry has no resolvable details
*/
export function renderBuiltinFunctionsTable(
entries: FunctionListEntry[],
detailsFor: (canonicalName: string) => FunctionDetails | undefined,
): string {
const byCategory = new Map<FunctionCategory, FunctionListEntry[]>()
for (const entry of entries) {
if (entry.category === undefined) {
continue
}
const bucket = byCategory.get(entry.category) ?? []
bucket.push(entry)
byCategory.set(entry.category, bucket)
}

const sections: string[] = []
for (const category of FUNCTION_CATEGORIES) {
const bucket = byCategory.get(category)
if (bucket === undefined || bucket.length === 0) {
continue
}
bucket.sort((a, b) => COLLATOR.compare(a.localizedName, b.localizedName) || COLLATOR.compare(a.canonicalName, b.canonicalName))
const rows = bucket.map(entry => {
const details = detailsFor(entry.canonicalName)
if (details === undefined) {
throw new Error(`No details for listed function "${entry.canonicalName}"`)
}
const syntax = formatFunctionSyntax(details.localizedName, details.parameters, details.repeatLastArgs)
const anchor = `<a id="${entry.canonicalName}"></a>`
return `| ${anchor}${escapeCell(entry.localizedName)} | ${escapeCell(entry.shortDescription)} | ${escapeCell(syntax)} |`
})
sections.push(`### ${category}\n\n| Function ID | Description | Syntax |\n|:---|:---|:---|\n${rows.join('\n')}`)
}

return sections.join('\n\n') + '\n'
}

/**
* Replaces the content between the autogenerated markers in a markdown file, preserving everything outside them
* (intro prose, footnote definitions). Fails loud on a malformed marker state rather than clobbering the file.
*
* @param {string} fileContent - the full current file content
* @param {string} generatedSection - the markdown to place between the markers (from renderBuiltinFunctionsTable)
* @returns {string} the updated file content
* @throws {Error} when either marker is missing, duplicated, or out of order
*/
export function spliceFunctionsTable(fileContent: string, generatedSection: string): string {
const startCount = fileContent.split(FUNCTIONS_TABLE_START).length - 1
const endCount = fileContent.split(FUNCTIONS_TABLE_END).length - 1
if (startCount !== 1 || endCount !== 1) {
throw new Error(`Expected exactly one START and one END marker, found ${startCount}/${endCount}`)
}
const startIdx = fileContent.indexOf(FUNCTIONS_TABLE_START)
const endIdx = fileContent.indexOf(FUNCTIONS_TABLE_END)
if (endIdx < startIdx) {
throw new Error('END marker appears before START marker')
}
const before = fileContent.slice(0, startIdx + FUNCTIONS_TABLE_START.length)
const after = fileContent.slice(endIdx)
return `${before}\n${generatedSection}\n${after}`
}
Loading
Loading