-
Notifications
You must be signed in to change notification settings - Fork 166
HF-249 bullet 3: generate built-in functions docs from the HF API (single source of truth) #1699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sequba
merged 36 commits into
feature/hf-249-function-metadata-api
from
feature/hf-249-docs-single-source
Jul 28, 2026
+377
−902
Merged
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 014d735
feat(metadata): add pure built-in functions table renderer and marker…
marcin-kordas-hoc 366e6f5
docs(guide): add autogeneration markers around the functions table (H…
marcin-kordas-hoc 6111bb9
docs(guide): keep functions-count line outside the autogeneration mar…
marcin-kordas-hoc 3738a50
docs(guide): place do-not-edit note above the START marker so regener…
marcin-kordas-hoc 080838c
feat(docs): generate built-in functions table from HF API (HF-249)
marcin-kordas-hoc 2ec0142
feat(metadata): surface OFFSET and VERSION via the function-metadata …
marcin-kordas-hoc f52cbaa
docs(guide): regenerate built-in functions table to include OFFSET an…
marcin-kordas-hoc b8b701f
feat(docs): fail the drift check when the function set changes (HF-249)
marcin-kordas-hoc f25e4c8
style(docs): join id arrays in drift-check message to satisfy lint (H…
marcin-kordas-hoc 4d228dc
build(docs): add docs:generate-functions and drift-check scripts (HF-…
marcin-kordas-hoc 0fce9e2
docs(dev): avoid asserting CI wiring not yet added (HF-249)
marcin-kordas-hoc 52f1096
ci(docs): fail build-docs if the functions table is stale (HF-249)
marcin-kordas-hoc cfa6cd8
fix(metadata): break renderer ties by canonicalName, soften stale pro…
marcin-kordas-hoc 18615a3
docs(metadata): drop internal spec ref, note English-only collator (H…
marcin-kordas-hoc bb9ef76
fix(metadata): keep static getFunctionDetails protected-aware after #…
marcin-kordas-hoc 0fd31f7
build(docs): regenerate the functions table at docs:build; drop the C…
marcin-kordas-hoc 66a5da5
feat(metadata): strip doc markup from getFunctionDetails shortDescrip…
marcin-kordas-hoc 3d7bbe6
chore(metadata): remove the one-time migration script and its danglin…
marcin-kordas-hoc f6dde48
fix(metadata): snake_case OFFSET parameter names to match the catalog…
marcin-kordas-hoc 490c6eb
docs(guide): regenerate built-in functions table from the catalogue (…
marcin-kordas-hoc 7ae63e8
fix(metadata): keep protected metadata resolution fail-safe when stru…
marcin-kordas-hoc b1098fd
chore(docs): drop the vestigial --check mode from the functions-table…
marcin-kordas-hoc 2181b70
Revert "feat(metadata): strip doc markup from getFunctionDetails shor…
marcin-kordas-hoc a8aa6f5
build(docs): generate built-in-functions.md as a gitignored build pro…
marcin-kordas-hoc fc9684d
refactor(docs): address review — move generator helpers to scripts/, …
marcin-kordas-hoc eba2a21
Merge feature/hf-249-function-metadata-api (brings HF-223) into docs …
marcin-kordas-hoc e617892
fix(hf-249): documentationUrl is always a string in FunctionDetails
marcin-kordas-hoc 3e19410
fix(hf-249): restore DEFAULT_DOCUMENTATION_URL; explicit '' for prote…
marcin-kordas-hoc b848802
Merge remote-tracking branch 'upstream/feature/hf-249-function-metada…
marcin-kordas-hoc 0ce7d5f
fix(hf-249): give protected OFFSET/VERSION the shared documentation URL
marcin-kordas-hoc 27b213e
Merge remote-tracking branch 'upstream/feature/hf-249-function-metada…
marcin-kordas-hoc 06f9ee0
docs(hf-249): add SORT and UNIQUE to the function-metadata catalogue
marcin-kordas-hoc 6740603
Add instructions to DEV_DOECS about no automatic tests for code other…
sequba 35b314a
Merge script and scripts directories
sequba 9094337
Merge branch 'feature/hf-249-function-metadata-api' into feature/hf-2…
sequba File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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})` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
|
sequba marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}` | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.