From e227a24797b732ce918e6f9c905bf2d9b7669b40 Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Thu, 28 May 2026 11:27:32 -0400 Subject: [PATCH 01/12] Migrate documentation from VuePress to Astro Starlight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the VuePress 1.x docs site with Astro 6 + Starlight 0.38. The previous `docs/.vuepress/` configuration stays in place for now since it's no longer wired into the build pipeline -- a separate cleanup will remove it. Migration highlights: - `docs/astro.config.mjs` configures Starlight with the Rapide theme, Expressive Code (line numbers + collapsible sections), a sitemap, and `starlight-page-actions` (Copy Markdown + Open in ChatGPT / Claude buttons on every page). - Guide pages, the homepage, and the 404 move from the legacy `docs/guide/*.md` + `docs/index.md` tree into `src/content/docs/` and are tracked in git. They were run through the VuePress preprocessor once at migration time, so they now contain Starlight-native asides (`:::tip[Title]`), HTML `
` accordions, fully resolved `@[code]` includes, and rewritten links with the `/docs` base prefix. - The TypeDoc-generated API reference is the only path that still passes through `scripts/generate-content.mjs` -- it normalizes TypeDoc's link forms and badge tokens. - The Netlify `_redirects` file (legacy `.html` URLs → clean URLs) is also generated by `generate-content.mjs`, now derived from the tracked content under `src/content/docs/guide/`. - Custom Starlight component overrides for `Head`, `Header`, `Footer`, and `ThemeSelect` reproduce the Handsontable docs look-and-feel (two-row header, sun/moon theme toggle, spreadsheet-style footer). - Root build orchestration (`docs:build` script chain, Netlify build command, GitHub Actions `build-docs.yml`) updated to invoke the Astro build via `npm ci && npm run build` inside `docs/`. Legacy `docs/guide/`, `docs/index.md`, and `docs/api-ref-readme.md` are deleted -- their content lives under `src/content/docs/` now. --- .eslintignore | 4 +- .github/workflows/build-docs.yml | 2 +- docs/.gitignore | 12 + docs/.nvmrc | 1 + docs/README.md | 97 +- docs/api-ref-readme.md | 53 - docs/astro.config.mjs | 113 + docs/guide/advanced-usage.md | 134 - docs/guide/basic-operations.md | 418 - docs/guide/basic-usage.md | 77 - docs/guide/batch-operations.md | 136 - docs/guide/clipboard-operations.md | 125 - docs/guide/date-and-time-handling.md | 111 - docs/guide/demo.md | 23 - docs/guide/i18n-features.md | 118 - docs/guide/localizing-functions.md | 145 - docs/guide/named-expressions.md | 198 - docs/guide/sorting-data.md | 208 - docs/guide/undo-redo.md | 38 - docs/package-lock.json | 7132 +++++++++++++++++ docs/package.json | 35 + docs/public/ast.png | Bin 0 -> 13678 bytes docs/public/crud-operations.png | Bin 0 -> 6853 bytes docs/public/eu-logos.png | Bin 0 -> 43009 bytes .../public/favicon/android-chrome-192x192.png | Bin 0 -> 6619 bytes .../public/favicon/android-chrome-512x512.png | Bin 0 -> 21832 bytes docs/public/favicon/apple-touch-icon.png | Bin 0 -> 6171 bytes docs/public/favicon/browserconfig.xml | 9 + docs/public/favicon/favicon-16x16.png | Bin 0 -> 817 bytes docs/public/favicon/favicon-32x32.png | Bin 0 -> 1340 bytes docs/public/favicon/favicon.ico | Bin 0 -> 15086 bytes docs/public/favicon/mstile-150x150.png | Bin 0 -> 4982 bytes docs/public/favicon/safari-pinned-tab.svg | 18 + docs/public/favicon/site.webmanifest | 19 + docs/public/hf-high-lvl-diagram.svg | 3 + docs/public/hf-logo-blue.svg | 1 + docs/public/hf_logo.png | Bin 0 -> 12296 bytes docs/public/hyperformula_brand_book.pdf | Bin 0 -> 529007 bytes docs/public/hyperformula_logo.zip | Bin 0 -> 685109 bytes docs/public/logo.png | Bin 0 -> 53610 bytes docs/public/ranges.png | Bin 0 -> 38360 bytes docs/public/robots.txt | 4 + docs/public/sample-sheet.png | Bin 0 -> 21682 bytes docs/public/topsort.png | Bin 0 -> 10431 bytes docs/scripts/generate-content.mjs | 127 + docs/scripts/test-build.mjs | 337 + docs/src/components/Footer.astro | 74 + docs/src/components/Head.astro | 19 + docs/src/components/Header.astro | 287 + docs/src/components/ThemeSelect.astro | 70 + docs/src/content.config.ts | 44 + docs/src/content/docs/404.md | 11 + docs/src/content/docs/guide/advanced-usage.md | 458 ++ docs/{ => src/content/docs}/guide/ai-sdk.md | 13 +- docs/{ => src/content/docs}/guide/arrays.md | 13 +- .../content/docs/guide/basic-operations.md | 1115 +++ docs/src/content/docs/guide/basic-usage.md | 327 + .../content/docs/guide/batch-operations.md | 483 ++ docs/{ => src/content/docs}/guide/branding.md | 11 +- docs/{ => src/content/docs}/guide/building.md | 7 +- .../content/docs}/guide/built-in-functions.md | 33 +- .../content/docs}/guide/cell-references.md | 23 +- .../docs}/guide/client-side-installation.md | 9 +- .../docs/guide/clipboard-operations.md | 447 ++ .../content/docs}/guide/code-of-conduct.md | 5 +- .../guide/compatibility-with-google-sheets.md | 49 +- .../compatibility-with-microsoft-excel.md | 77 +- .../docs}/guide/configuration-options.md | 9 +- docs/{ => src/content/docs}/guide/contact.md | 7 +- .../content/docs}/guide/contributing.md | 9 +- .../content/docs}/guide/custom-functions.md | 55 +- .../docs/guide/date-and-time-handling.md | 431 + docs/src/content/docs/guide/demo.md | 322 + .../content/docs}/guide/dependencies.md | 5 +- .../content/docs}/guide/dependency-graph.md | 17 +- .../content/docs}/guide/file-import.md | 7 +- docs/src/content/docs/guide/i18n-features.md | 502 ++ .../docs}/guide/integration-with-angular.md | 15 +- .../docs}/guide/integration-with-langchain.md | 13 +- .../docs}/guide/integration-with-react.md | 15 +- .../docs}/guide/integration-with-svelte.md | 17 +- .../docs}/guide/integration-with-vue.md | 17 +- .../content/docs}/guide/key-concepts.md | 17 +- .../content/docs}/guide/known-limitations.md | 7 +- .../content/docs}/guide/license-key.md | 19 +- .../{ => src/content/docs}/guide/licensing.md | 9 +- .../docs}/guide/list-of-differences.md | 13 +- .../docs/guide/localizing-functions.md | 462 ++ .../content/docs}/guide/mcp-server.md | 13 +- .../docs}/guide/migration-from-0.6-to-1.0.md | 13 +- .../docs}/guide/migration-from-1.x-to-2.0.md | 7 +- .../docs}/guide/migration-from-2.x-to-3.0.md | 5 +- .../content/docs/guide/named-expressions.md | 482 ++ .../docs}/guide/order-of-precendece.md | 7 +- .../content/docs}/guide/performance.md | 9 +- docs/{ => src/content/docs}/guide/quality.md | 9 +- .../content/docs}/guide/release-notes.md | 31 +- .../docs}/guide/server-side-installation.md | 9 +- docs/src/content/docs/guide/sorting-data.md | 518 ++ .../docs}/guide/specifications-and-limits.md | 7 +- docs/{ => src/content/docs}/guide/support.md | 9 +- .../content/docs}/guide/supported-browsers.md | 5 +- .../content/docs}/guide/types-of-errors.md | 5 +- .../content/docs}/guide/types-of-operators.md | 11 +- .../content/docs}/guide/types-of-values.md | 9 +- docs/src/content/docs/guide/undo-redo.md | 344 + .../content/docs}/guide/volatile-functions.md | 9 +- docs/{ => src/content/docs}/index.md | 46 +- docs/src/plugins/docs-data.mjs | 52 + docs/src/plugins/vuepress-preprocessor.mjs | 418 + docs/src/scripts/example-runner.ts | 47 + docs/src/scripts/theme-toggle.ts | 36 + docs/src/sidebar.mjs | 126 + docs/src/styles/base/variables.css | 107 + docs/src/styles/components/content.css | 186 + docs/src/styles/components/footer.css | 105 + docs/src/styles/components/header.css | 283 + .../styles/components/interactive-example.css | 73 + docs/src/styles/custom.css | 9 + docs/tsconfig.json | 19 + netlify.toml | 4 +- package.json | 4 +- 122 files changed, 16147 insertions(+), 2111 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/.nvmrc delete mode 100644 docs/api-ref-readme.md create mode 100644 docs/astro.config.mjs delete mode 100644 docs/guide/advanced-usage.md delete mode 100644 docs/guide/basic-operations.md delete mode 100644 docs/guide/basic-usage.md delete mode 100644 docs/guide/batch-operations.md delete mode 100644 docs/guide/clipboard-operations.md delete mode 100644 docs/guide/date-and-time-handling.md delete mode 100644 docs/guide/demo.md delete mode 100644 docs/guide/i18n-features.md delete mode 100644 docs/guide/localizing-functions.md delete mode 100644 docs/guide/named-expressions.md delete mode 100644 docs/guide/sorting-data.md delete mode 100644 docs/guide/undo-redo.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/public/ast.png create mode 100644 docs/public/crud-operations.png create mode 100644 docs/public/eu-logos.png create mode 100644 docs/public/favicon/android-chrome-192x192.png create mode 100644 docs/public/favicon/android-chrome-512x512.png create mode 100644 docs/public/favicon/apple-touch-icon.png create mode 100644 docs/public/favicon/browserconfig.xml create mode 100644 docs/public/favicon/favicon-16x16.png create mode 100644 docs/public/favicon/favicon-32x32.png create mode 100644 docs/public/favicon/favicon.ico create mode 100644 docs/public/favicon/mstile-150x150.png create mode 100644 docs/public/favicon/safari-pinned-tab.svg create mode 100644 docs/public/favicon/site.webmanifest create mode 100644 docs/public/hf-high-lvl-diagram.svg create mode 100644 docs/public/hf-logo-blue.svg create mode 100644 docs/public/hf_logo.png create mode 100644 docs/public/hyperformula_brand_book.pdf create mode 100644 docs/public/hyperformula_logo.zip create mode 100644 docs/public/logo.png create mode 100644 docs/public/ranges.png create mode 100644 docs/public/robots.txt create mode 100644 docs/public/sample-sheet.png create mode 100644 docs/public/topsort.png create mode 100644 docs/scripts/generate-content.mjs create mode 100644 docs/scripts/test-build.mjs create mode 100644 docs/src/components/Footer.astro create mode 100644 docs/src/components/Head.astro create mode 100644 docs/src/components/Header.astro create mode 100644 docs/src/components/ThemeSelect.astro create mode 100644 docs/src/content.config.ts create mode 100644 docs/src/content/docs/404.md create mode 100644 docs/src/content/docs/guide/advanced-usage.md rename docs/{ => src/content/docs}/guide/ai-sdk.md (93%) rename docs/{ => src/content/docs}/guide/arrays.md (95%) create mode 100644 docs/src/content/docs/guide/basic-operations.md create mode 100644 docs/src/content/docs/guide/basic-usage.md create mode 100644 docs/src/content/docs/guide/batch-operations.md rename docs/{ => src/content/docs}/guide/branding.md (79%) rename docs/{ => src/content/docs}/guide/building.md (99%) rename docs/{ => src/content/docs}/guide/built-in-functions.md (98%) rename docs/{ => src/content/docs}/guide/cell-references.md (91%) rename docs/{ => src/content/docs}/guide/client-side-installation.md (90%) create mode 100644 docs/src/content/docs/guide/clipboard-operations.md rename docs/{ => src/content/docs}/guide/code-of-conduct.md (99%) rename docs/{ => src/content/docs}/guide/compatibility-with-google-sheets.md (68%) rename docs/{ => src/content/docs}/guide/compatibility-with-microsoft-excel.md (67%) rename docs/{ => src/content/docs}/guide/configuration-options.md (73%) rename docs/{ => src/content/docs}/guide/contact.md (89%) rename docs/{ => src/content/docs}/guide/contributing.md (92%) rename docs/{ => src/content/docs}/guide/custom-functions.md (92%) create mode 100644 docs/src/content/docs/guide/date-and-time-handling.md create mode 100644 docs/src/content/docs/guide/demo.md rename docs/{ => src/content/docs}/guide/dependencies.md (97%) rename docs/{ => src/content/docs}/guide/dependency-graph.md (93%) rename docs/{ => src/content/docs}/guide/file-import.md (96%) create mode 100644 docs/src/content/docs/guide/i18n-features.md rename docs/{ => src/content/docs}/guide/integration-with-angular.md (88%) rename docs/{ => src/content/docs}/guide/integration-with-langchain.md (93%) rename docs/{ => src/content/docs}/guide/integration-with-react.md (87%) rename docs/{ => src/content/docs}/guide/integration-with-svelte.md (87%) rename docs/{ => src/content/docs}/guide/integration-with-vue.md (88%) rename docs/{ => src/content/docs}/guide/key-concepts.md (92%) rename docs/{ => src/content/docs}/guide/known-limitations.md (95%) rename docs/{ => src/content/docs}/guide/license-key.md (58%) rename docs/{ => src/content/docs}/guide/licensing.md (85%) rename docs/{ => src/content/docs}/guide/list-of-differences.md (95%) create mode 100644 docs/src/content/docs/guide/localizing-functions.md rename docs/{ => src/content/docs}/guide/mcp-server.md (93%) rename docs/{ => src/content/docs}/guide/migration-from-0.6-to-1.0.md (93%) rename docs/{ => src/content/docs}/guide/migration-from-1.x-to-2.0.md (92%) rename docs/{ => src/content/docs}/guide/migration-from-2.x-to-3.0.md (98%) create mode 100644 docs/src/content/docs/guide/named-expressions.md rename docs/{ => src/content/docs}/guide/order-of-precendece.md (96%) rename docs/{ => src/content/docs}/guide/performance.md (95%) rename docs/{ => src/content/docs}/guide/quality.md (97%) rename docs/{ => src/content/docs}/guide/release-notes.md (97%) rename docs/{ => src/content/docs}/guide/server-side-installation.md (87%) create mode 100644 docs/src/content/docs/guide/sorting-data.md rename docs/{ => src/content/docs}/guide/specifications-and-limits.md (98%) rename docs/{ => src/content/docs}/guide/support.md (89%) rename docs/{ => src/content/docs}/guide/supported-browsers.md (96%) rename docs/{ => src/content/docs}/guide/types-of-errors.md (98%) rename docs/{ => src/content/docs}/guide/types-of-operators.md (97%) rename docs/{ => src/content/docs}/guide/types-of-values.md (97%) create mode 100644 docs/src/content/docs/guide/undo-redo.md rename docs/{ => src/content/docs}/guide/volatile-functions.md (95%) rename docs/{ => src/content/docs}/index.md (74%) create mode 100644 docs/src/plugins/docs-data.mjs create mode 100644 docs/src/plugins/vuepress-preprocessor.mjs create mode 100644 docs/src/scripts/example-runner.ts create mode 100644 docs/src/scripts/theme-toggle.ts create mode 100644 docs/src/sidebar.mjs create mode 100644 docs/src/styles/base/variables.css create mode 100644 docs/src/styles/components/content.css create mode 100644 docs/src/styles/components/footer.css create mode 100644 docs/src/styles/components/header.css create mode 100644 docs/src/styles/components/interactive-example.css create mode 100644 docs/src/styles/custom.css create mode 100644 docs/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 03546876e2..1dba1602c8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,8 +1,8 @@ # Common files node_modules -# example files -docs/examples/ +# Docs site — separate Astro package with its own toolchain +docs/ # 3rd party src/interpreter/plugin/3rdparty diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 60c43dfc32..268c99bcaa 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -18,7 +18,7 @@ jobs: publish-docs: strategy: matrix: - node-version: [ '22' ] + node-version: [ '20' ] os: [ 'ubuntu-latest' ] name: build-docs runs-on: ${{ matrix.os }} diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..8f3fa9ac33 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +.astro/ +.DS_Store + +# Generated API reference — TypeDoc output processed by scripts/generate-content.mjs. +# Guide pages, the homepage, and 404 are now authored directly in src/content/docs/ +# and tracked in git; only the API tree is regenerated on every build. +src/content/docs/api/ + +# Generated Netlify redirects (legacy .html → clean URLs). +public/_redirects diff --git a/docs/.nvmrc b/docs/.nvmrc new file mode 100644 index 0000000000..209e3ef4b6 --- /dev/null +++ b/docs/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/docs/README.md b/docs/README.md index 26c9f2fb5f..ac3e6c83b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,58 +1,77 @@ # HyperFormula documentation -HyperFormula comes with a dedicated, regularly-updated documentation portal. +We treat documentation as an integral part of the HyperFormula developer experience. -View the documentation's latest production version at https://handsontable.com/docs/hyperformula. +View the documentation's latest production version at [hyperformula.handsontable.com](https://hyperformula.handsontable.com/). -## About HyperFormula documentation +**See also:** -The HyperFormula documentation is built with [VuePress](https://vuepress.vuejs.org/), a Vue-powered Static Site Generator. +- [Documentation standards](./CLAUDE.md) -- authoring rules for humans and AI agents +- [Documentation editing guidelines](./README-EDITING.md) -- practical reference for frontmatter, markdown containers, links, and interactive examples +- [Documentation deployment guidelines](./README-DEPLOYMENT.md) -- Netlify, CI, and the `docs:build` pipeline -When editing the docs, you can use features described [here](https://vuepress.vuejs.org/guide/markdown.html). +## Getting started -## Getting started with HyperFormula documentation +The docs site is built with [Astro](https://astro.build) and [Starlight](https://starlight.astro.build). **Requires Node 20** (separate from the HyperFormula core's Node version). -To start a local HyperFormula docs server: - -1. Make sure you're running [Node.js](https://nodejs.org/en/) 14+. -2. From the main `hyperformula` directory, install the docs dependencies: - ```bash - npm install - ``` -3. From the main `hyperformula` directory, build HyperFormula: +1. From the `docs` directory, install dependencies: ```bash - npm run bundle-all + npm install ``` -4. From the main `hyperformula` directory, create a dev build of the docs and start your local docs server: +2. Start the local docs server: ```bash - npm run docs:dev + npm run dev ``` -5. In your browser, go to: http://localhost:8080/hyperformula/. +3. In your browser, go to [http://localhost:4321/docs/](http://localhost:4321/docs/). + +> **Note:** Content collection files (`.md` under `src/content/docs/`) are cached by Astro's data store. After editing `.md` content files, restart the dev server with `npm run dev -- --force` to invalidate the cache. CSS and component changes are picked up by HMR automatically. -## HyperFormula documentation npm scripts +## npm scripts -From the `hyperformula` directory, you can run the following npm scripts: +From the `docs` directory: -* `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 dev` -- Generates content, then starts the local docs server at `localhost:4321/docs/`. +- `npm run start` -- Alias for `npm run dev`. +- `npm run build` -- Generates content, then builds the production output into `dist/`. +- `npm run preview` -- Previews the built output locally. +- `npm run generate:content` -- Runs `scripts/generate-content.mjs` to populate `src/content/docs/{guide,api}` from the legacy `docs/{guide,api}` trees plus the TypeDoc-generated API reference. +- `npm run test:build` -- Smoke-test the production build via `scripts/test-build.mjs`. +- `npm run docs:lint` -- Runs ESLint on `.js,.mjs,.ts,.astro` files in `src/`. -## HyperFormula docs directory structure +## Directory structure ```bash -docs # All documentation files -├── .vuepress # All VuePress files -│ ├── components # Vue components -│   ├── dist # The docs output. Both the docs and the API reference are built into this folder. -│   ├── public # Public assets -│   ├── styles # Style-related files -│   ├── subtheme # Subtheme files -│   ├── templates # HTML templates -│   ├── config.js # VuePress configuration -│   ├── enhanceApp.js # VuePress app-level enhancements -│   └── highlight.js # Code highlight configuration -├── api # The API reference files, generated automatically from JsDoc. Do not edit! -├── guide # The docs source files: Markdown content -├── api-ref-readme.md # The API reference welcome page -├── index.md # The main docs portal welcome page -└── README.md # The file you're looking at right now! +docs/ # All documentation files +├── astro.config.mjs # Astro + Starlight configuration +├── tsconfig.json # TypeScript configuration +├── package.json # Docs-only dependencies and scripts +├── CLAUDE.md # Documentation authoring standards +├── AGENTS.md # → symlink to CLAUDE.md +├── README.md # The file you're looking at right now +│ +├── src/ # Astro source +│ ├── components/ # Astro component overrides (Header, Footer, Head, ThemeSelect) +│ ├── content/ # Generated content collection (build artifact) +│ │ └── docs/ # Starlight content root +│ │ ├── guide/ # Guide pages -- authored here going forward +│ │ ├── api/ # API reference (auto-generated from TypeDoc) +│ │ └── index.md # Home page +│ ├── content.config.ts # Content collection schema (extends Starlight's docsSchema) +│ ├── plugins/ # Build-time plugins (vuepress-preprocessor, docs-data) +│ ├── scripts/ # Client-side runtime (example-runner, theme-toggle) +│ ├── sidebar.mjs # Sidebar navigation tree +│ └── styles/ # CSS partials +│ ├── base/ # Tokens (variables.css) +│ └── components/ # Per-component styles (header, footer, content, interactive-example) +│ +├── scripts/ # Docs build helpers +│ ├── generate-content.mjs # Populates src/content/docs/ from legacy sources +│ └── test-build.mjs # Production-build smoke test +│ +├── public/ # Static assets served as-is (logos, images, favicons) +└── examples/ # Live example source files referenced from `::: example` blocks ``` + +## Content sources + +New guide pages are authored directly in `src/content/docs/guide/`. A legacy `docs/guide/` tree exists from the VuePress era and is scheduled for migration -- it should not receive new content. The API reference under `src/content/docs/api/` is auto-generated from TypeDoc; do not edit those files by hand. See [CLAUDE.md §11](./CLAUDE.md#11-content-sources) for the full set of rules. diff --git a/docs/api-ref-readme.md b/docs/api-ref-readme.md deleted file mode 100644 index 69f252437f..0000000000 --- a/docs/api-ref-readme.md +++ /dev/null @@ -1,53 +0,0 @@ -Welcome to the HyperFormula `v{{ $page.version }}` API! - -The API reference documentation provides detailed information for methods, error types, event types, and all the configuration options available in HyperFormula. - -Current build: {{ $page.buildDate }} - -### API reference index - -The following sections explain shortly what can be found in the left sidebar navigation menu. - -#### HyperFormula -This section contains information about the class for creating HyperFormula instance. It enlists all available public methods alongside their descriptions, parameter types, and examples. - -The snippet shows an example how to use `buildFromArray` which is one of [three static methods](/api/classes/hyperformula.html#factories) for creating an instance of HyperFormula: -```javascript -const sheetData = [ - ['0', '=SUM(1, 2, 3)', '52'], - ['=SUM(A1:C1)', '', '=A1'], - ['2', '=SUM(A1:C1)', '91'], -]; - -const hfInstance = HyperFormula.buildFromArray(sheetData, options); -``` - -#### ConfigParams -This section contains information about options that allow you to configure the instance of HyperFormula. - -An example set of options: -```javascript -const options = { - licenseKey: 'gpl-v3', - nullDate: { year: 1900, month: 1, day: 1 }, - functionArgSeparator: '.' -}; -``` - -#### Listeners -In this section, you can find information about all events you can subscribe to. - -For example, subscribing to `sheetAdded` event: - -```javascript -const hfInstance = HyperFormula.buildFromSheets({ - MySheet1: [ ['1'] ], - MySheet2: [ ['10'] ], -}); - -const handler = ( ) => { console.log('baz') } - -hfInstance.on('sheetAdded', handler); - -const nameProvided = hfInstance.addSheet('MySheet3'); -``` diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs new file mode 100644 index 0000000000..c61c4f1d1d --- /dev/null +++ b/docs/astro.config.mjs @@ -0,0 +1,113 @@ +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; +import sitemap from '@astrojs/sitemap'; +import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'; +import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'; +import starlightThemeRapide from 'starlight-theme-rapide'; +import starlightPageActions from 'starlight-page-actions'; +import { sidebar } from './src/sidebar.mjs'; + +// BUILD_MODE is set by the deployment pipeline. Production-only third-party +// scripts (analytics) are injected only when it equals 'production'. +const isProduction = process.env.BUILD_MODE === 'production'; + +// Host and base path are env-overridable to preserve the existing VuePress +// DOCS_HOSTNAME / DOCS_BASE knobs. The live site is served under `/docs`. +const SITE = process.env.DOCS_HOSTNAME || 'https://hyperformula.handsontable.com'; +const BASE = process.env.DOCS_BASE || '/docs'; + +export default defineConfig({ + site: SITE, + base: BASE, + + // The dev toolbar adds noise to a docs project; keep it off. + devToolbar: { enabled: false }, + + integrations: [ + starlight({ + title: 'HyperFormula', + description: + 'HyperFormula is an open-source, high-performance calculation engine for spreadsheets and web applications.', + + favicon: '/favicon/favicon-32x32.png', + + social: [ + { icon: 'github', label: 'GitHub', href: 'https://github.com/handsontable/hyperformula' }, + ], + + editLink: { + baseUrl: 'https://github.com/handsontable/hyperformula/edit/develop/docs/content/', + }, + + expressiveCode: { + plugins: [pluginLineNumbers(), pluginCollapsibleSections()], + themes: ['github-dark', 'github-light'], + }, + + customCss: ['./src/styles/custom.css'], + + head: [ + // Google Search Console verification. + { + tag: 'meta', + attrs: { + name: 'google-site-verification', + content: 'MZpSOa8SNvFLRRGwUQpYVZ78kIHQoPVdVbafHhJ_d4Q', + }, + }, + // Sentry error monitoring (all environments). + { + tag: 'script', + attrs: { + id: 'Sentry.io', + src: 'https://js.sentry-cdn.com/50617701901516ce348cb7b252564a60.min.js', + crossorigin: 'anonymous', + defer: true, + }, + }, + // Google Tag Manager (production only). + ...(isProduction + ? [ + { + tag: 'script', + content: + "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-N59TZXR');", + }, + ] + : []), + ], + + sidebar, + + // Apply Handsontable's Rapide theme on top of Starlight; custom CSS in + // src/styles/* still wins because customCss loads after plugins. The + // page-actions plugin adds the "Copy Markdown", "Open in ChatGPT", + // "Open in Claude", and "Edit on GitHub" buttons under the page title. + plugins: [starlightThemeRapide(), starlightPageActions()], + + components: { + // Extends the default Head to load Inter + the example runner. + Head: './src/components/Head.astro', + // Custom 2-row header: logo + version, search, stars, nav, support. + Header: './src/components/Header.astro', + // Custom HT-style footer (links grid + social row). + Footer: './src/components/Footer.astro', + // Sun/moon toggle replacing the Auto/Light/Dark . Persists to +// the same `starlight-theme` localStorage key Starlight already manages. +--- + + + + diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts new file mode 100644 index 0000000000..bfcc66c3d7 --- /dev/null +++ b/docs/src/content.config.ts @@ -0,0 +1,44 @@ +/** + * Astro content collection schema for the HyperFormula docs. + * + * Uses Starlight's standard `docsLoader()` over `src/content/docs/` so content + * is rendered through Starlight's full markdown pipeline (asides, heading + * anchors, etc.). Unlike Handsontable, HyperFormula has no per-framework content + * variants, so no custom framework loader is needed. + * + * The `guide/` and `api/` trees are generated before each build (see + * `scripts/generate-content.mjs` and the TypeDoc step); only `index.md` is + * hand-authored. Legacy VuePress frontmatter fields are accepted via the schema + * extension so generated/migrated `.md` files validate. + */ +import { defineCollection } from 'astro:content'; +import { z } from 'astro/zod'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ + loader: docsLoader(), + schema: docsSchema({ + extend: z.object({ + /** VuePress unique page ID — coerced to string (some pages use numeric IDs). */ + id: z.coerce.string().optional(), + + /** Browser override. */ + metaTitle: z.string().optional(), + + /** VuePress permalink — Starlight derives URLs from file paths. */ + permalink: z.string().optional(), + + /** Canonical URL hint. */ + canonicalUrl: z.string().optional(), + + /** Sidebar category label. */ + category: z.string().optional(), + + /** Sidebar badge label (e.g. "New", "Updated"). */ + menuTag: z.string().optional(), + }), + }), + }), +}; diff --git a/docs/src/content/docs/404.md b/docs/src/content/docs/404.md new file mode 100644 index 0000000000..3a4860919e --- /dev/null +++ b/docs/src/content/docs/404.md @@ -0,0 +1,11 @@ +--- +title: 'Page not found' +template: splash +editUrl: false +prev: false +next: false +--- + +The page you're looking for doesn't exist — it may have moved. + +[Back to the documentation home](/docs/) · [API reference](/docs/api) diff --git a/docs/src/content/docs/guide/advanced-usage.md b/docs/src/content/docs/guide/advanced-usage.md new file mode 100644 index 0000000000..992609dbdb --- /dev/null +++ b/docs/src/content/docs/guide/advanced-usage.md @@ -0,0 +1,458 @@ +--- +title: "Advanced usage" +--- + + +:::tip +By default, cells are identified using a `SimpleCellAddress` which +consists of a sheet ID, column ID, and row ID, like +this: `{ sheet: 0, col: 0, row: 0 }` + +Alternatively, you can work with the **A1 notation** known from +spreadsheets like Excel or Google Sheets. The API provides the helper +function `simpleCellAddressFromString` which you can use to +retrieve the `SimpleCellAddress` . +::: + +The following example shows how to use formulas to find out which of +the two Teams (A or B) is the winning one. You will do that by +comparing the average scores of players in each team. + +The initial steps are the same as in the +[basic example](/docs/guide/basic-usage). First, import HyperFormula and choose +the configuration options: + +```javascript +import { HyperFormula } from 'hyperformula'; + +const options = { + licenseKey: 'gpl-v3' +}; +``` + +This time you will use the `buildFromEmpty` static method to +initialize the engine: + +```javascript +// initiate the engine with no data +const hfInstance = HyperFormula.buildEmpty(options); +``` + +Now, let's prepare some data. The first column will be players' +IDs and the second column will be their scores. Then, you will +define the formulas responsible for calculating the average scores. + +```javascript +// first column represents players' IDs +// second column represents players' scores +const playersA = [ + ['1', '2'], + ['2', '3'], + ['3', '5'], + ['4', '7'], + ['5', '13'], + ['6', '17'] +]; + +const playersB = [ + ['7', '19'], + ['8', '31'], + ['9', '61'], + ['10', '89'], + ['11', '107'], + ['12', '127'] +]; + +// in cell A1 a formula checks which team is the winning one +// in cells A2 and A3 formulas calculate the average score of players +const formulas = [ + ['=IF(Formulas!A2>Formulas!A3,"TeamA","TeamB")'], + ['=AVERAGE(TeamA!B1:B6)'], + ['=AVERAGE(TeamB!B1:B6)'] +]; +``` + +Now prepare sheets and insert the data into them: + +```javascript +// add 'TeamA' sheet +const sheetNameA = hfInstance.addSheet('TeamA'); +// get the new sheet ID for further API calls +const sheetIdA = hfInstance.getSheetId(sheetNameA); +// insert playersA content into targeted 'TeamA' sheet +hfInstance.setSheetContent(sheetIdA, playersA); + +// add 'TeamB' sheet +const sheetNameB = hfInstance.addSheet('TeamB'); +// get the new sheet ID for further API calls +const sheetIdB = hfInstance.getSheetId(sheetNameB); +// insert playersB content into targeted 'TeamB' sheet +hfInstance.setSheetContent(sheetIdB, playersB); + +// check the content in the console output +console.log(hfInstance.getAllSheetsValues()); +``` + +After setting everything up, you can add formulas: + +```javascript +// add a sheet named 'Formulas' +const sheetNameC = hfInstance.addSheet('Formulas'); +// get the new sheet ID for further API calls +const sheetIdC = hfInstance.getSheetId(sheetNameC); +// add formulas to that sheet +hfInstance.setSheetContent(sheetIdC, formulas); +``` + +Almost done! Now, you can use the `getSheetValues` method to get all +values including the calculated ones. Alternatively, you can use +`getCellValue`to get the value from a specific cell. + +```javascript +// get all sheet values +const sheetValues = hfInstance.getSheetValues(sheetIdC); + +// get the simple cell address of 'A1' from that sheet +const simpleCellAddress = hfInstance.simpleCellAddressFromString('A1', sheetIdC); + +// check the winning team 🎉 +const winningTeam = hfInstance.getCellValue(simpleCellAddress); + +// print the result to the console +console.log(winningTeam) +``` + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/advanced-usage/example1.js"> +<div class="example"> + <button id="run" class="run"> + Who won? + </button> + <div class="message"> + <span style="margin: 0 10px">🏆</span> + <span id="output"></span> + </div> + <div id="data-preview" class="container" style="display: flex; flex-wrap: wrap; gap: 5px; justify-content: space-between;"> + <div id="TeamA-container" style="width: 300px"> + <h3>Team A</h3> + <table> + <colgroup> + <col style="width:50%" /> + <col style="width:50%" /> + </colgroup> + <thead> + <tr> + <th>ID</th> + <th>Score</th> + </tr> + </thead> + <tbody></tbody> + </table> + </div> + <div id="TeamB-container" style="width: 300px"> + <h3>Team B</h3> + <table> + <colgroup> + <col style="width:50%" /> + <col style="width:50%" /> + </colgroup> + <thead> + <tr> + <th>ID</th> + <th>Score</th> + </tr> + </thead> + <tbody></tbody> + </table> + </div> + <div id="Formulas-container" style="max-width: 300px"> + <h3>Formulas</h3> + <table> + <tbody></tbody> + </table> + </div> + </div> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +// first column represents players' IDs +// second column represents players' scores +const playersAData = [ + ['1', '2'], + ['2', '3'], + ['3', '5'], + ['4', '7'], + ['5', '13'], + ['6', '17'], +]; + +const playersBData = [ + ['7', '19'], + ['8', '31'], + ['9', '61'], + ['10', '89'], + ['11', '107'], + ['12', '127'], +]; + +// in a cell A1 a formula checks which team is a winning one +// in cells A2 and A3 formulas calculate the average score of players +const formulasData = [ + ['=IF(Formulas!A2>Formulas!A3,"TeamA","TeamB")'], + ['=AVERAGE(TeamA!B1:B6)'], + ['=AVERAGE(TeamB!B1:B6)'], +]; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +const sheetInfo = { + teamA: { sheetName: 'TeamA' }, + teamB: { sheetName: 'TeamB' }, + formulas: { sheetName: 'Formulas' }, +}; + +// add 'TeamA' sheet +hf.addSheet(sheetInfo.teamA.sheetName); +// insert playersA content into targeted 'TeamA' sheet +hf.setSheetContent(hf.getSheetId(sheetInfo.teamA.sheetName), playersAData); +// add 'TeamB' sheet +hf.addSheet(sheetInfo.teamB.sheetName); +// insert playersB content into targeted 'TeamB' sheet +hf.setSheetContent(hf.getSheetId(sheetInfo.teamB.sheetName), playersBData); +// add a sheet named 'Formulas' +hf.addSheet(sheetInfo.formulas.sheetName); +// add formulas to that sheet +hf.setSheetContent(hf.getSheetId(sheetInfo.formulas.sheetName), formulasData); + +/** + * Fill the HTML table with data. + * + * @param {string} sheetName Sheet name. + */ +function renderTable(sheetName) { + const sheetId = hf.getSheetId(sheetName); + const tbodyDOM = document.querySelector( + `.example #${sheetName}-container tbody`, + ); + + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress) && !cellHasFormula) { + cellValue = hf.getCellValue(cellAddress); + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td><span>${cellValue}</span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Render the result block + */ +function renderResult() { + const resultOutputDOM = document.querySelector('.example #output'); + const cellAddress = hf.simpleCellAddressFromString( + `${sheetInfo.formulas.sheetName}!A1`, + hf.getSheetId(sheetInfo.formulas.sheetName), + ); + + resultOutputDOM.innerHTML = `<span> + <strong>${hf.getCellValue(cellAddress)}</strong> won! + </span>`; +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const runButton = document.querySelector('.example #run'); + + runButton.addEventListener('click', () => { + renderResult(); + }); +} + +// Bind the button events. +bindEvents(); + +// Render the preview tables. +for (const [_, tableInfo] of Object.entries(sheetInfo)) { + renderTable(tableInfo.sheetName); +} +``` + +</details> diff --git a/docs/guide/ai-sdk.md b/docs/src/content/docs/guide/ai-sdk.md similarity index 93% rename from docs/guide/ai-sdk.md rename to docs/src/content/docs/guide/ai-sdk.md index e38f89c524..4485fcd432 100644 --- a/docs/guide/ai-sdk.md +++ b/docs/src/content/docs/guide/ai-sdk.md @@ -1,8 +1,11 @@ -# HyperFormula AI SDK for Vercel +--- +title: "HyperFormula AI SDK for Vercel" +--- + A [Vercel AI SDK](https://sdk.vercel.ai/docs) tool that gives your agents deterministic spreadsheet and formula computation — backed by HyperFormula's Excel-compatible engine. -::: warning Not available yet — coming soon +:::caution[Not available yet — coming soon] This integration is on our roadmap and **cannot be installed or used today**. The API shown below is a preview and may still change before the first release. If you'd like to try it, [join the early access list](https://2fmjvg.share-eu1.hsforms.com/2e6drCkuLTn-1RuiYB91eJA) — we'll ping you the moment the first beta is ready, and your sign-up directly tells us how strongly to prioritize this integration. @@ -52,7 +55,7 @@ A single import, one extra line in `tools`, and the model can evaluate formulas, ## Get early access -::: tip Be the first to try it +:::tip[Be the first to try it] We're actively building this integration. Drop your email and we'll notify you the moment the first beta lands — so you can try it before the public release. [Join the early access list →](https://2fmjvg.share-eu1.hsforms.com/2e6drCkuLTn-1RuiYB91eJA) @@ -63,5 +66,5 @@ We're actively building this integration. Drop your email and we'll notify you t - [Vercel AI SDK documentation](https://sdk.vercel.ai/docs) - [HyperFormula on GitHub](https://github.com/handsontable/hyperformula) - [HyperFormula on npm](https://www.npmjs.com/package/hyperformula) -- [Built-in functions](built-in-functions.md) -- [Custom functions](custom-functions.md) +- [Built-in functions](/docs/guide/built-in-functions) +- [Custom functions](/docs/guide/custom-functions) diff --git a/docs/guide/arrays.md b/docs/src/content/docs/guide/arrays.md similarity index 95% rename from docs/guide/arrays.md rename to docs/src/content/docs/guide/arrays.md index dc02b87b60..aaf643d368 100644 --- a/docs/guide/arrays.md +++ b/docs/src/content/docs/guide/arrays.md @@ -1,4 +1,7 @@ -# Array formulas +--- +title: "Array formulas" +--- + Use array formulas to perform an operation (or call a function) on multiple cells at a time. @@ -17,12 +20,12 @@ An array is inherently a two-dimensional object. ### Inline arrays An inline array is defined by curly braces: `{ }`. It can contain one or more rows, separated by: -- The [`arrayColumnSeparator`](../api/classes/config.md#arraycolumnseparator) (default: `,`) -- The [`arrayRowSeparator`](../api/classes/config.md#arrayrowseparator) (default: `;`) +- The [`arrayColumnSeparator`](/docs/api/classes/config#arraycolumnseparator) (default: `,`) +- The [`arrayRowSeparator`](/docs/api/classes/config#arrayrowseparator) (default: `;`) Every row must be of equal length. -::: tip +:::tip **Inline arrays are not recomputed after initialization.** If an inline array contains a cell reference, and the cell's value changes, the array is not updated. @@ -58,7 +61,7 @@ To enable the array arithmetic mode once, within a particular function or formul To enable the array arithmetic mode by default, everywhere in your HyperFormula instance: -* In your HyperFormula [configuration](../api/interfaces/configparams.md#usearrayarithmetic), set the `useArrayArithmetic` option to `true`. +* In your HyperFormula [configuration](/docs/api/interfaces/configparams#usearrayarithmetic), set the `useArrayArithmetic` option to `true`. With the array arithmetic mode enabled globally, you can operate on arrays without using the `ARRAYFORMULA` function: diff --git a/docs/src/content/docs/guide/basic-operations.md b/docs/src/content/docs/guide/basic-operations.md new file mode 100644 index 0000000000..5319614348 --- /dev/null +++ b/docs/src/content/docs/guide/basic-operations.md @@ -0,0 +1,1115 @@ +--- +title: "Basic operations" +--- + + +HyperFormula can perform efficient **CRUD** operations on the workbook. +You can apply these operations to various workbook elements, such as: + +* Cells +* Rows / Columns +* Sheets + +**Check the [API](/docs/api)** for a full reference of methods available for CRUD +operations. + +HyperFormula automatically updates all references, both relative and +absolute, in all sheets affected by the change. + +Operations affecting only the dependency graph should not decrease +performance. However, multiple operations that have an impact on +calculation results may affect performance; these are [`clearSheet`](/docs/api/classes/hyperformula#clearsheet), +[`setSheetContent`](/docs/api/classes/hyperformula#setsheetcontent), [`setCellContents`](/docs/api/classes/hyperformula#setcellcontents), [`addNamedExpression`](/docs/api/classes/hyperformula#addnamedexpression), +[`changeNamedExpression`](/docs/api/classes/hyperformula#changenamedexpression), and [`removeNamedExpression`](/docs/api/classes/hyperformula#removenamedexpression). It is advised +to [batch](/docs/guide/batch-operations) them. + +## Sheets + +### Adding a sheet + +A sheet can be added by using the [`addSheet`](/docs/api/classes/hyperformula#addsheet) method. You can pass a +name for it or leave it without a parameter. In the latter case the +method will create an autogenerated name for it. That name can then +be returned for further use. + +```javascript +// the autogenerated sheet name can be assigned to a variable +const myNewSheet = hfInstance.addSheet(); + +// create a sheet with a specific name +hfInstance.addSheet('SheetName'); +``` + +You can also count sheets by using the [`countSheets`](/docs/api/classes/hyperformula#countsheets) method. This +method does not require any parameters. + +```javascript +// count the number of sheets you added +const sheetsCount = hfInstance.countSheets(); +``` + +### Removing a sheet + +A sheet can be removed by using the [`removeSheet`](/docs/api/classes/hyperformula#removesheet) method. To do that +you need to pass a mandatory parameter: the ID of a sheet to be +removed. +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by removing the sheet 0 +const changes = hfInstance.removeSheet(0); +``` + +### Renaming a sheet + +A sheet can be renamed by using the [`renameSheet`](/docs/api/classes/hyperformula#renamesheet) method. You need to +pass the ID of a sheet you want to rename (you can get it with the +[`getSheetId`](/docs/api/classes/hyperformula#getsheetid) method only if you know its name) along with a new name +as the first and second parameters, respectively. + +```javascript +// rename the first sheet +hfInstance.renameSheet(0, 'NewSheetName'); + +// you can retrieve the sheet ID if you know its name +const sheetID = hfInstance.getSheetId('SheetName'); + +// use the retrieved sheet ID in the method +hfInstance.renameSheet(sheetID, 'AnotherNewName'); +``` + +### Clearing a sheet + +A sheet's content can be cleared with the [`clearSheet`](/docs/api/classes/hyperformula#clearsheet) method. You need +to provide the ID of a sheet whose content you want to clear. +This method returns [an array of changed cells](#changes-array). + +```javascript +// clear the content of sheet 0 +const changes = hfInstance.clearSheet(0); +``` + +### Replacing sheet content + +Instead of removing and adding the content of a sheet you can replace +it right away. To do so use [`setSheetContent`](/docs/api/classes/hyperformula#setsheetcontent), in which you can pass +the sheet ID and its new values. +This method returns [an array of changed cells](#changes-array). + +```javascript +// set new values for sheet 0 +const changes = hfInstance.setSheetContent(0, [['50'], ['60']]); +``` + +## Rows + +### Adding rows + +You can add one or more rows by using the [`addRows`](/docs/api/classes/hyperformula#addrows) method. The first +parameter you need to pass is a sheet ID, and the second parameter +represents the position and the size of a block of rows to be added. +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by adding +// two rows at position 0 inside the first sheet +const changes = hfInstance.addRows(0, [0, 2]); +``` + +### Removing rows + +You can remove one or more rows by using the [`removeRows`](/docs/api/classes/hyperformula#removerows) method. The +first parameter you need to pass is a sheet ID, and the second +parameter represents the position and the size of a block of rows to +be removed. +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by removing +// two rows at position 0 inside the first sheet +const changes = hfInstance.removeRows(0, [0, 2]); +``` + +### Moving rows + +You can move one or more rows by using the [`moveRows`](/docs/api/classes/hyperformula#moverows) method. You need +to pass the following parameters: + +* Sheet ID +* Starting row +* Number of rows to be moved +* [Target row](/docs/api/classes/hyperformula#moverows) + +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by moving +// the first row in the first sheet into row 2 +const changes = hfInstance.moveRows(0, 0, 1, 2); +``` + +### Reordering rows + +You can change the order of rows by using the [`setRowOrder`](/docs/api/classes/hyperformula#setroworder) method. You need to pass the following parameters: +* Sheet ID +* [New row order](/docs/api/classes/hyperformula#setroworder) + +This method returns [an array of changed cells](#changes-array). + +```javascript +// row 0 and row 2 swap places +const changes = hfInstance.setRowOrder(0, [2, 1, 0]); +``` + +## Columns + +### Adding columns + +You can add one or more columns by using the [`addColumns`](/docs/api/classes/hyperformula#addcolumns) method. +The first parameter you need to pass is a sheet ID, and the second +parameter represents the position and the size of a block of columns +to be added. +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by adding +// two columns at position 0 inside the first sheet +const changes = hfInstance.addColumns(0, [0, 2]); +``` + +### Removing columns + +You can remove one or more columns by using the [`removeColumns`](/docs/api/classes/hyperformula#removecolumns) method. +The first parameter you need to pass is a sheet ID, and the second +parameter represents the position and the size of a block of columns +to be removed. +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by removing +// two columns at position 0 inside the first sheet +const changes = hfInstance.removeColumns(0, [0, 2]); +``` + +### Moving columns + +You can move one or more columns by using the [`moveColumns`](/docs/api/classes/hyperformula#movecolumns) method. +You need to pass the following parameters: + +* Sheet ID +* Starting column +* Number of columns to be moved +* [Target column](/docs/api/classes/hyperformula#movecolumns) + +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by moving +// the first column in the first sheet into column 2 +const changes = hfInstance.moveColumns(0, 0, 1, 2); +``` + +### Reordering columns + +You can change the order of columns by using the [`setColumnOrder`](/docs/api/classes/hyperformula#setcolumnorder) method. You need to pass the following parameters: +* Sheet ID +* [New column order](/docs/api/classes/hyperformula#setcolumnorder) + +This method returns [an array of changed cells](#changes-array). + +```javascript +// column 0 and column 2 swap places +const changes = hfInstance.setColumnOrder(0, [2, 1, 0]); +``` + +## Cells + +:::tip +By default, cells are identified using a [`SimpleCellAddress`](/docs/api/interfaces/simplecelladdress) which +consists of a sheet ID, column ID, and row ID, like this: +`{ sheet: 0, col: 0, row: 0 }` + +Alternatively, you can work with the **A1 notation** known from +spreadsheets like Excel or Google Sheets. The API provides the helper +function [`simpleCellAddressFromString`](/docs/api/classes/hyperformula#simplecelladdressfromstring) which you can use to retrieve +the [`SimpleCellAddress`](/docs/api/interfaces/simplecelladdress) . +::: + +### Moving cells + +You can move one or more cells using the [`moveCells`](/docs/api/classes/hyperformula#movecells) method. You need +to pass the following parameters: + +* Source range ([SimpleCellRange](/docs/api/interfaces/simplecellrange)) +* Top left corner of the destination range ([SimpleCellAddress](/docs/api/interfaces/simplecelladdress)) + +This method returns [an array of changed cells](#changes-array). + +```javascript +// choose the source cells +const source = { sheet: 0, col: 1, row: 0 }; +// choose the target cells +const destination = { sheet: 0, col: 3, row: 0 }; + +// track the changes triggered by moving +// one cell from source to target location +const changes = hfInstance.moveCells({ start: source, end: source }, destination); +``` + +### Updating cells + +You can set the content of a block of cells by using the +[`setCellContents`](/docs/api/classes/hyperformula#setcellcontents) method. You need to pass the top left corner address +of a block as a [`SimpleCellAddress`](/docs/api/interfaces/simplecelladdress), along with the content to be set. +It can be content for either a single cell or a set of cells in an array. +This method returns [an array of changed cells](#changes-array). + +```javascript +// track the changes triggered by setting +// a block of cells with content '=B1' +const changes = hfInstance.setCellContents({ col: 3, row: 0, sheet: 0 }, [['=B1']]); +``` + +### Getting cell value + +You can get the value of a cell by using [`getCellValue`](/docs/api/classes/hyperformula#getcellvalue) . Remember to +pass the coordinates as a [`SimpleCellAddress`](/docs/api/interfaces/simplecelladdress) . + +```javascript +// get the value of the B1 cell +const B1Value = hfInstance.getCellValue({ sheet: 0, col: 1, row: 0 }); +``` + +### Getting cell formula + +You can retrieve the formula from a cell by using [`getCellFormula`](/docs/api/classes/hyperformula#getcellformula). +Remember to pass the coordinates as a [`SimpleCellAddress`](/docs/api/interfaces/simplecelladdress) . + +```javascript +// get the formula from the A1 cell +const A1Formula = hfInstance.getCellFormula({ sheet: 0, col: 0, row: 0 }); +``` + +## Handling an error + +Each time you call a method, HyperFormula will perform the corresponding +operation. If there is an issue, it will throw an error. Methods +available in the HyperFormula's API might throw different errors, +but all of them follow the same pattern. Thus, the errors can be +handled in a similar manner. + +For example, imagine you let users rename their sheets in an +application but by mistake they choose a sheet ID that does not exist. +It would be nice to display the error to the user, so they are aware +of this fact. + +```javascript +// variable used to carry the message for the user +let messageUsedInUI; + +// attempt to rename a sheet +try { + hfInstance.renameSheet(5, "Payroll"); + + // whoops! there is no sheet with an ID of 5 +} catch (e) { + // notify the user that a sheet with an ID of 5 does not exist + if (e instanceof NoSheetWithIdError) { + messageUsedInUI = "Sheet with provided ID does not exist"; + } + // a generic error message, just in case + else { + messageUsedInUI = "Something went wrong"; + } +} +``` + +## isItPossibleTo* methods + +There are also methods that you may find useful to call in pair with +the above-mentioned operations. These methods are prefixed with +`isItPossibleTo*` whose sole purpose is to check if the desired +operation is possible. They all return a simple `boolean` value. +You will find it handy when you want to give the user a more generic +message and you don't want to react to specific errors. + +This can be particularly useful for interaction with the UI of the +application you work on. For example, you can allow the user to add +new sheets by typing a new sheet name inside an input field. You can +easily check if that action is allowed, and if it is not, throw an error. + +```javascript +// an instance with some example data +const hfInstance = HyperFormula.buildFromArray([ + ['1', '2'], +]); + +// a variable used to carry the message for the user +let messageUsedInUI; + +// use this method to check the possibility to remove columns +const isRemovable = hfInstance.isItPossibleToRemoveColumns(0, [1, 1]); + +// check if there is a possibility to remove columns +if (!isRemovable) { + messageUsedInUI = 'Sorry, you cannot perform a remove action' +} +``` + +## Changes array + +All data modification methods return an array of [`ExportedChange`](/docs/api/globals#exportedchange). +This is a collection of cells whose **values** were affected by an operation, +together with their absolute addresses and new values. + +```javascript +[{ + address: { sheet: 0, col: 0, row: 0 }, + newValue: { error: [CellError], value: '#REF!' }, +}] +``` + +This gives you information about where the change happened, what the +new value of a cell is, and even what type it is - in this case, an +error. + +The array of changes includes only cells that have different **values** after performing the operation. See the example: + +```js +const hf = HyperFormula.buildFromArray([ + [0], + [1], + ['=SUM(A1:A2)'], + ['=COUNTBLANK(A1:A3)'], +]); + +// insert an empty row between the row 0 and the row 1 +const changes = hf.addRows(0, [1, 1]); + +console.log(hf.getSheetSerialized(0)); +// sheet after adding the row: +// [ +// [0], +// [], +// [1], +// ['=SUM(A1:A3)'], +// ['=COUNTBLANK(A1:A4)'], +// ] + +console.log(changes); +// changes include only the COUNTBLANK cell: +// [{ +// address: { sheet: 0, row: 4, col: 0 }, +// newValue: 1, +// }] +``` + +## Demo + +This demo presents several basic operations integrated with a sample UI. + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +/* basic-operations form */ +.example #inputs { + display: none; +} +.example #inputs input, +.example #toolbar select, +.example #inputs button { + height: 38px; +} +.example #inputs input.inline, +.example #inputs select.inline { + border-bottom-right-radius: 0; + border-right: 0; + border-top-right-radius: 0; + margin: 0; + width: 10em; + float: left; +} +.example #inputs button.inline { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + margin: 0; +} +.example #inputs input.inline.middle { + border-radius: 0; + margin: 0; + width: 10em; + float: left; +} +.example #inputs input::placeholder { + opacity: 0.55; +} +.example #inputs input:disabled { + background-color: #f7f7f7; +} +.example #inputs.error input { + border: 1px solid red; +} +</style> +<div class="hf-example__preview" data-example-js="/examples/basic-operations/example1.js"> +<div class="example"> + <div id="toolbar" class="container"> + <div> + <div> + <select id="sheet-select"> + <option value="" disabled selected>SheetName</option> + </select> + <select id="action-select"> + <option value="" disabled selected>Select action</option> + <option value="add-sheet">Add sheet</option> + <option value="remove-sheet">Remove sheet</option> + <option value="add-rows">Add row(s)</option> + <option value="add-columns">Add column(s)</option> + <option value="remove-rows">Remove row(s)</option> + <option value="remove-columns">Remove column(s)</option> + <option value="get-value">Get value</option> + <option value="set-value">Set value</option> + </select><br /> + </div> + </div> + <div id="inputs"> + <div> + <input id="input-1" class="inline" /> + <input id="input-2" class="inline middle" /> + <button class="button run inline"> + Run + </button> + <div class="message-box"> + <p id="error-message" class="color: red;"></p> + <p id="disclaimer" class="opacity: 0.6;"></p> + </div> + </div> + </div> + </div> + <div id="preview"> + <table class="spreadsheet"> + <colgroup> + <col style="width:20%"/> + <col style="width:20%"/> + <col style="width:20%"/> + <col style="width:20%"/> + <col style="width:20%"/> + </colgroup> + <thead> + <tr> + <th><span></span></th> + <th><span></span></th> + <th><span></span></th> + <th><span></span></th> + <th><span></span></th> + </tr> + </thead> + <tbody></tbody> + </table> + </div> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +const ANIMATION_ENABLED = true; + +/** + * Return sample data for the provided number of rows and columns. + * + * @param {number} rows Amount of rows to create. + * @param {number} columns Amount of columns to create. + * @returns {string[][]} + */ +function getSampleData(rows, columns) { + const data = []; + + for (let r = 0; r < rows; r++) { + data.push([]); + + for (let c = 0; c < columns; c++) { + data[r].push(`${Math.floor(Math.random() * 999) + 1}`); + } + } + + return data; +} + +/** + * A simple state object for the demo. + * + * @type {object} + */ +const state = { + currentSheet: null, +}; + +/** + * Input configuration and definition. + * + * @type {object} + */ +const inputConfig = { + 'add-sheet': { + inputs: [ + { + type: 'text', + placeholder: 'Sheet name', + }, + ], + buttonText: 'Add Sheet', + disclaimer: + 'For the sake of this demo, the new sheets will be filled with random data.', + }, + 'remove-sheet': { + inputs: [ + { + type: 'text', + placeholder: 'Sheet name', + }, + ], + buttonText: 'Remove Sheet', + }, + 'add-rows': { + inputs: [ + { + type: 'number', + placeholder: 'Index', + }, + { + type: 'number', + placeholder: 'Amount', + }, + ], + buttonText: 'Add Rows', + }, + 'add-columns': { + inputs: [ + { + type: 'number', + placeholder: 'Index', + }, + { + type: 'number', + placeholder: 'Amount', + }, + ], + buttonText: 'Add Columns', + }, + 'remove-rows': { + inputs: [ + { + type: 'number', + placeholder: 'Index', + }, + { + type: 'number', + placeholder: 'Amount', + }, + ], + buttonText: 'Remove Rows', + }, + 'remove-columns': { + inputs: [ + { + type: 'number', + placeholder: 'Index', + }, + { + type: 'number', + placeholder: 'Amount', + }, + ], + buttonText: 'Remove Columns', + }, + 'get-value': { + inputs: [ + { + type: 'text', + placeholder: 'Cell Address', + }, + { + type: 'text', + disabled: 'disabled', + placeholder: '', + }, + ], + disclaimer: 'Cell addresses format examples: A1, B4, C6.', + buttonText: 'Get Value', + }, + 'set-value': { + inputs: [ + { + type: 'text', + placeholder: 'Cell Address', + }, + { + type: 'text', + placeholder: 'Value', + }, + ], + disclaimer: 'Cell addresses format examples: A1, B4, C6.', + buttonText: 'Set Value', + }, +}; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +state.currentSheet = 'InitialSheet'; + +const sheetName = hf.addSheet(state.currentSheet); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setSheetContent(sheetId, getSampleData(5, 5)); + +/** + * Fill the HTML table with data. + */ +function renderTable() { + const sheetId = hf.getSheetId(state.currentSheet); + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const isEmpty = hf.isCellEmpty(cellAddress); + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = cellHasFormula; + let cellValue = ''; + + if (isEmpty) { + cellValue = ''; + } else if (!showFormula) { + cellValue = hf.getCellValue(cellAddress); + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Updates the sheet dropdown. + */ +function updateSheetDropdown() { + const sheetNames = hf.getSheetNames(); + const sheetDropdownDOM = document.querySelector('.example #sheet-select'); + let dropdownContent = ''; + + sheetDropdownDOM.innerHTML = ''; + sheetNames.forEach((sheetName) => { + const isCurrent = sheetName === state.currentSheet; + + dropdownContent += `<option value="${sheetName}" ${isCurrent ? 'selected' : ''}>${sheetName}</option>`; + }); + sheetDropdownDOM.innerHTML = dropdownContent; +} + +/** + * Update the form to the provided action. + * + * @param {string} action Action chosen from the dropdown. + */ +function updateForm(action) { + const inputsDOM = document.querySelector('.example #inputs'); + const submitButtonDOM = document.querySelector('.example #inputs button'); + const allInputsDOM = document.querySelectorAll('.example #inputs input'); + const disclaimerDOM = document.querySelector('.example #disclaimer'); + + // Hide all inputs + allInputsDOM.forEach((input) => { + input.style.display = 'none'; + input.value = ''; + input.disabled = false; + }); + inputConfig[action].inputs.forEach((inputCfg, index) => { + const inputDOM = document.querySelector(`.example #input-${index + 1}`); + + // Show only those needed + inputDOM.style.display = 'block'; + + for (const [attribute, value] of Object.entries(inputCfg)) { + inputDOM.setAttribute(attribute, value); + } + }); + submitButtonDOM.innerText = inputConfig[action].buttonText; + + if (inputConfig[action].disclaimer) { + disclaimerDOM.innerHTML = inputConfig[action].disclaimer; + disclaimerDOM.parentElement.style.display = 'block'; + } else { + disclaimerDOM.innerHTML = ' '; + } + + inputsDOM.style.display = 'block'; +} + +/** + * Add the error overlay. + * + * @param {string} message Error message. + */ +function renderError(message) { + const inputsDOM = document.querySelector('.example #inputs'); + const errorDOM = document.querySelector('.example #error-message'); + + if (inputsDOM.className.indexOf('error') === -1) { + inputsDOM.className += ' error'; + } + + errorDOM.innerText = message; + errorDOM.parentElement.style.display = 'block'; +} + +/** + * Clear the error overlay. + */ +function clearError() { + const inputsDOM = document.querySelector('.example #inputs'); + const errorDOM = document.querySelector('.example #error-message'); + + inputsDOM.className = inputsDOM.className.replace(' error', ''); + errorDOM.innerText = ''; + errorDOM.parentElement.style.display = 'none'; +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const sheetDropdown = document.querySelector('.example #sheet-select'); + const actionDropdown = document.querySelector('.example #action-select'); + const submitButton = document.querySelector('.example #inputs button'); + + sheetDropdown.addEventListener('change', (event) => { + state.currentSheet = event.target.value; + clearError(); + renderTable(); + }); + actionDropdown.addEventListener('change', (event) => { + clearError(); + updateForm(event.target.value); + }); + submitButton.addEventListener('click', (event) => { + const action = document.querySelector('.example #action-select').value; + + doAction(action); + }); +} + +/** + * Perform the wanted action. + * + * @param {string} action Action to perform. + */ +function doAction(action) { + let cellAddress = null; + const inputValues = [ + document.querySelector('.example #input-1').value || void 0, + document.querySelector('.example #input-2').value || void 0, + ]; + + clearError(); + + switch (action) { + case 'add-sheet': + state.currentSheet = hf.addSheet(inputValues[0]); + handleError(() => { + hf.setSheetContent( + hf.getSheetId(state.currentSheet), + getSampleData(5, 5), + ); + }); + updateSheetDropdown(); + renderTable(); + + break; + case 'remove-sheet': + handleError(() => { + hf.removeSheet(hf.getSheetId(inputValues[0])); + }); + + if (state.currentSheet === inputValues[0]) { + state.currentSheet = hf.getSheetNames()[0]; + renderTable(); + } + + updateSheetDropdown(); + + break; + case 'add-rows': + handleError(() => { + hf.addRows(hf.getSheetId(state.currentSheet), [ + parseInt(inputValues[0], 10), + parseInt(inputValues[1], 10), + ]); + }); + renderTable(); + + break; + case 'add-columns': + handleError(() => { + hf.addColumns(hf.getSheetId(state.currentSheet), [ + parseInt(inputValues[0], 10), + parseInt(inputValues[1], 10), + ]); + }); + renderTable(); + + break; + case 'remove-rows': + handleError(() => { + hf.removeRows(hf.getSheetId(state.currentSheet), [ + parseInt(inputValues[0], 10), + parseInt(inputValues[1], 10), + ]); + }); + renderTable(); + + break; + case 'remove-columns': + handleError(() => { + hf.removeColumns(hf.getSheetId(state.currentSheet), [ + parseInt(inputValues[0], 10), + parseInt(inputValues[1], 10), + ]); + }); + renderTable(); + + break; + case 'get-value': + const resultDOM = document.querySelector('.example #input-2'); + + cellAddress = handleError(() => { + return hf.simpleCellAddressFromString( + inputValues[0], + hf.getSheetId(state.currentSheet), + ); + }, 'Invalid cell address format.'); + + if (cellAddress !== null) { + resultDOM.value = handleError(() => { + return hf.getCellValue(cellAddress); + }); + } + + break; + case 'set-value': + cellAddress = handleError(() => { + return hf.simpleCellAddressFromString( + inputValues[0], + hf.getSheetId(state.currentSheet), + ); + }, 'Invalid cell address format.'); + + if (cellAddress !== null) { + handleError(() => { + hf.setCellContents(cellAddress, inputValues[1]); + }); + } + + renderTable(); + + break; + default: + } +} + +/** + * Handle the HF errors. + * + * @param {Function} tryFunc Function to handle. + * @param {string} [message] Optional forced error message. + */ +function handleError(tryFunc, message = null) { + let result = null; + + try { + result = tryFunc(); + } catch (e) { + if (e instanceof Error) { + renderError(message || e.message); + } else { + renderError('Something went wrong'); + } + } + + return result; +} + +// // Bind the UI events. +bindEvents(); +// Render the table. +renderTable(); +// Refresh the sheet dropdown list +updateSheetDropdown(); +document.querySelector('.example .message-box').style.display = 'block'; +``` + +</details> diff --git a/docs/src/content/docs/guide/basic-usage.md b/docs/src/content/docs/guide/basic-usage.md new file mode 100644 index 0000000000..ec4820cffe --- /dev/null +++ b/docs/src/content/docs/guide/basic-usage.md @@ -0,0 +1,327 @@ +--- +title: "Basic usage" +--- + + +:::tip +The instance can be created with three static methods: +[`buildFromArray`](/docs/api/classes/hyperformula#buildfromarray), +`buildFromSheets` or `buildEmpty`. You can check all of their +descriptions in our [API reference](/docs/api). +::: + +If you've already installed the library, it's time to start writing the +first simple application. + +First, if you used NPM or Yarn to install the package, make sure you +have properly imported HyperFormula as shown below: + +```javascript +import { HyperFormula } from 'hyperformula'; +``` + +If you embed HyperFormula in the `<script>` tag using CDN, then it will +be accessible as global variable `HyperFormula` and ready to use. + +Now you can use the [available options](/docs/guide/configuration-options) to +configure the instance of HyperFormula according to your needs, like +this: + +```javascript +const options = { + licenseKey: 'gpl-v3' +}; +``` + +Then, prepare some data to be used by your app. In this case, the data +set will contain numbers and just one formula `=SUM(A1,B1)`. Use the +`buildFromArray` method to create the instance: + +```javascript +// define the data +const data = [['10', '20', '3.14159265359', '=SUM(A1:C1)']]; + +// build an instance with defined options and data +const hfInstance = HyperFormula.buildFromArray(data, options); +``` + +Alright, now it's time to do some calculations. Let's use the +`getCellValue` method to retrieve the results of a formula included +in the `data` . + +```javascript +// call getCellValue to get the calculation results +const mySum = hfInstance.getCellValue({ col: 3, row: 0, sheet: 0 }); +``` + +You can check the output in the console: + +```javascript +// this outputs the result in the browser's console +console.log(mySum); +``` + +That's it! You've grasped a basic idea of how the HyperFormula engine +works. It's time to move on to a more +[advanced example.](/docs/guide/advanced-usage) + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/basic-usage/example1.js"> +<div class="example"> + <button id="calculate" class="button run"> + Calculate + </button> + <div class="message-box"> + <span><span id="address-output"></span> result: <strong id="result-output"></strong></span> + </div> + <table class="spreadsheet"> + <colgroup> + <col style="width:30%"/> + <col style="width:30%"/> + <col style="width:30%"/> + </colgroup> + <thead></thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +const tableData = [['10', '20', '=SUM(A1,B1)']]; +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + precisionRounding: 9, + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); + +/** + * Fill the HTML table with data. + */ +function renderTable() { + const theadDOM = document.querySelector('.example thead'); + const tbodyDOM = document.querySelector('.example tbody'); + const { height, width } = hf.getSheetDimensions(sheetId); + let newTheadHTML = ''; + let newTbodyHTML = ''; + + for (let row = -1; row < height; row++) { + for (let col = 0; col < width; col++) { + if (row === -1) { + newTheadHTML += `<th><span></span></th>`; + + continue; + } + + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress) && !cellHasFormula) { + cellValue = hf.getCellValue(cellAddress); + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td><span> + ${cellValue} + </span></td>`; + } + } + + tbodyDOM.innerHTML = `<tr>${newTbodyHTML}</tr>`; + theadDOM.innerHTML = newTheadHTML; +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const calculateButton = document.querySelector('.example #calculate'); + const formulaPreview = document.querySelector('.example #address-output'); + const calculationResult = document.querySelector('.example #result-output'); + const cellAddress = { sheet: sheetId, row: 0, col: 2 }; + + formulaPreview.innerText = hf.simpleCellAddressToString(cellAddress, sheetId); + calculateButton.addEventListener('click', () => { + calculationResult.innerText = hf.getCellValue(cellAddress); + }); +} + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/src/content/docs/guide/batch-operations.md b/docs/src/content/docs/guide/batch-operations.md new file mode 100644 index 0000000000..3deec36c2e --- /dev/null +++ b/docs/src/content/docs/guide/batch-operations.md @@ -0,0 +1,483 @@ +--- +title: "Batch operations" +--- + + +HyperFormula offers a built-in feature for doing batch operations. +It allows you to combine multiple data modification actions into a single operation. + +In some cases, batch operations can result in better performance, +especially when your app requires doing a large number of operations. + +## How to batch + +### Using the [`batch`](/docs/api/classes/hyperformula#batch) method + +You can use the [`batch`](/docs/api/classes/hyperformula#batch) method to batch operations. This method accepts +just one parameter: a callback function that stacks the selected +operations into one. It performs the cumulative operation at the end. + +This method returns a list of cells whose values were affected by this +operation together with their absolute addresses and new values. + +```javascript +const hfInstance = HyperFormula.buildFromSheets({ + MySheet1: [ ['1'] ], + MySheet2: [ ['10'] ], +}); + +// multiple operations in a single callback will trigger evaluation only once +// and only one set of changes will be returned as a combined result of all +// the operations that were triggered within the callback +const changes = hfInstance.batch(() => { + hfInstance.setCellContents({ col: 3, row: 0, sheet: 0 }, [['=B1']]); + hfInstance.setCellContents({ col: 4, row: 0, sheet: 0 }, [['=A1']]); + + // and numerous others +}); +``` + +### Using the [`suspendEvaluation`](/docs/api/classes/hyperformula#suspendevaluation) and [`resumeEvaluation`](/docs/api/classes/hyperformula#resumeevaluation) methods + +The same result can be achieved by suspending and resuming the +evaluation. + +To do that you need to explicitly suspend the evaluation, then do the +operations one by one, and then resume the evaluation. + +This method returns a list of cells which values were affected by the +operation together with their absolute addresses and new values. + +```javascript +const hfInstance = HyperFormula.buildFromSheets({ + MySheet1: [ ['1'] ], + MySheet2: [ ['10'] ], +}); + +// suspend the evaluation +hfInstance.suspendEvaluation(); + +// perform operations +hfInstance.setCellContents({ col: 3, row: 0, sheet: 0 }, [['=B1']]); +hfInstance.setSheetContent(1, [['50'], ['60']]); + +// resume the evaluation +const changes = hfInstance.resumeEvaluation(); +``` + +You can resume the evaluation by calling the [`resumeEvaluation`](/docs/api/classes/hyperformula#resumeevaluation) method +which triggers the recalculation. Just like in the case of the [`batch`](/docs/api/classes/hyperformula#batch) +method, it returns a list of cells which values changed after the +operation, together with their absolute addresses, and new values. + +### Checking the evaluation suspension state + +When you need to check if the evaluation is suspended you can +call the [`isEvaluationSuspended`](/docs/api/classes/hyperformula#isevaluationsuspended) method. + +```javascript +const hfInstance = HyperFormula.buildEmpty(); + +// suspend the evaluation +hfInstance.suspendEvaluation(); + +// check if the evaluation is suspended +// this method returns a simple boolean value +const isEvaluationSuspended = hfInstance.isEvaluationSuspended(); + +// resume evaluation if needed +hfInstance.resumeEvaluation(); +``` + +## When to batch + +You can batch operations anytime you want to stack several actions into +one. However, if you want to see the most amazing benefits of this +feature, use batch operations when there are a lot of heavy methods. +This will result in better performance. The best candidates to +batch in this situation are the following methods: + +* `clearSheet` +* `setSheetContent` +* `setCellContents` +* `addNamedExpression` +* `changeNamedExpression` +* `removeNamedExpression` + +These operations have an impact on calculation results and may affect +the performance. + +Batching can be useful when there is a need for multiple memory-consuming +operations. In this case, you should consider using it to achieve +better performance in the application you develop; it will result +in faster calculation across the whole HyperFormula instance. + +Batching can also be useful when you decide to use HyperFormula +on the [server-side](server-side-installation). Several operations +can be sent as a single one. + +## What you can't batch + +You can't batch read operations. + +Methods such as [`getCellValue`](/docs/api/classes/hyperformula#getcellvalue), [`getSheetSerialized`](/docs/api/classes/hyperformula#getsheetserialized), or [`getFillRangeData`](/docs/api/classes/hyperformula#getfillrangedata) will result in an error when called inside a [batch callback](#using-the-batch-method) or when the evaluation is [suspended](#using-the-suspendevaluation-and-resumeevaluation-methods). + +The [paste](/docs/api/classes/hyperformula#paste) method also can't be called when batching as it reads the contents of the copied cells. + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/batch-operations/example1.js"> +<div class="example container"> + <div> + <div> + <button id="run" class="button run"> + Run batch operation + </button> + <button id="reset" class="button button-outline reset"> + Reset + </button> + </div> + </div> + <div> + <div style="display: flex; align-items: center;"> + <input id="isCalculated" type="checkbox" style="margin: .1em"/> + <label for="isCalculated">Calculated</label> + </div> + </div> + <div> + <table> + <colgroup> + <col style="width:25%" /> + <col style="width:15%" /> + <col style="width:20%" /> + <col style="width:20%" /> + <col style="width:20%" /> + </colgroup> + <thead> + <tr> + <th>Name</th> + <th>Year_1</th> + <th>Year_2</th> + <th>Average</th> + <th>Sum</th> + </tr> + </thead> + <tbody></tbody> + </table> + </div> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/** + * Initial table data. + */ +const tableData = [ + ['Greg Black', 4.66, '=B1*1.3', '=AVERAGE(B1:C1)', '=SUM(B1:C1)'], + ['Anne Carpenter', 5.25, '=$B$2*30%', '=AVERAGE(B2:C2)', '=SUM(B2:C2)'], + ['Natalie Dem', 3.59, '=B3*2.7+2+1', '=AVERAGE(B3:C3)', '=SUM(B3:C3)'], + ['John Sieg', 12.51, '=B4*(1.22+1)', '=AVERAGE(B4:C4)', '=SUM(B4:C4)'], + [ + 'Chris Aklips', + 7.63, + '=B5*1.1*SUM(10,20)+1', + '=AVERAGE(B5:C5)', + '=SUM(B5:C5)', + ], +]; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); +// Add named expressions for the "TOTAL" row. +hf.addNamedExpression('Year_1', '=SUM(main!$B$1:main!$B$5)'); +hf.addNamedExpression('Year_2', '=SUM(main!$C$1:main!$C$5)'); + +const ANIMATION_ENABLED = true; + +/** + * Fill the HTML table with data. + * + * @param {boolean} calculated `true` if it should render calculated values, `false` otherwise. + */ +function renderTable(calculated = false) { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const totals = ['=SUM(Year_1)', '=SUM(Year_2)']; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + let totalRowsHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = calculated || !cellHasFormula; + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress) && showFormula) { + cellValue = hf.getCellValue(cellAddress); + + if (!isNaN(cellValue)) { + cellValue = cellValue.toFixed(2); + } + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + totalRowsHTML = `<tr class="summary"> + <td>TOTAL</td> + <td class="${updatedCellClass}"> + <span>${ + calculated + ? hf.calculateFormula(totals[0], sheetId).toFixed(2) + : totals[0] + }</span> + </td> + <td class="${updatedCellClass}"> + <span>${ + calculated + ? hf.calculateFormula(totals[1], sheetId).toFixed(2) + : totals[1] + }</span> + </td> + <td colspan="2"></td> + </tr>`; + newTbodyHTML += totalRowsHTML; + tbodyDOM.innerHTML = newTbodyHTML; +} + +let IS_CALCULATED = false; + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const runButton = document.querySelector('.example #run'); + const resetButton = document.querySelector('.example #reset'); + const calculatedCheckbox = document.querySelector('.example #isCalculated'); + + runButton.addEventListener('click', () => { + runBatchOperations(); + }); + resetButton.addEventListener('click', () => { + resetTableData(); + }); + calculatedCheckbox.addEventListener('change', (e) => { + if (e.target.checked) { + renderTable(true); + } else { + renderTable(); + } + + IS_CALCULATED = e.target.checked; + }); +} + +/** + * Reset the data for the table. + */ +function resetTableData() { + hf.setSheetContent(sheetId, tableData); + renderTable(IS_CALCULATED); +} + +/** + * Run batch operations. + */ +function runBatchOperations() { + hf.batch(() => { + hf.setCellContents({ col: 1, row: 0, sheet: sheetId }, [['=B4']]); + hf.setCellContents({ col: 1, row: 1, sheet: sheetId }, [['=B4']]); + hf.setCellContents({ col: 1, row: 2, sheet: sheetId }, [['=B4']]); + hf.setCellContents({ col: 1, row: 4, sheet: sheetId }, [['=B4']]); + }); + renderTable(IS_CALCULATED); +} + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/guide/branding.md b/docs/src/content/docs/guide/branding.md similarity index 79% rename from docs/guide/branding.md rename to docs/src/content/docs/guide/branding.md index 159219f7f8..a67f01f792 100644 --- a/docs/guide/branding.md +++ b/docs/src/content/docs/guide/branding.md @@ -1,4 +1,7 @@ -# Branding +--- +title: "Branding" +--- + ## Our logo @@ -7,13 +10,13 @@ The logo is comprised of the word "HyperFormula" combined with the integral ## Logotype -<img :src="$withBase('/hf_logo.png')"> +<img src="/docs/hf_logo.png"> ## Resources -<a :href="$withBase('/hyperformula_logo.zip')">Download resources</a> +<a :href="/docs/hyperformula_logo.zip">Download resources</a> -<a :href="$withBase('/hyperformula_brand_book.pdf')">Download brand book</a> +<a :href="/docs/hyperformula_brand_book.pdf">Download brand book</a> ## Terms of use diff --git a/docs/guide/building.md b/docs/src/content/docs/guide/building.md similarity index 99% rename from docs/guide/building.md rename to docs/src/content/docs/guide/building.md index 1e75be016d..451053b87e 100644 --- a/docs/guide/building.md +++ b/docs/src/content/docs/guide/building.md @@ -1,4 +1,7 @@ -# Building +--- +title: "Building" +--- + The build process uses Webpack and Babel, as well as npm tasks listed in package.json. During this process, the source located in @@ -62,7 +65,7 @@ Most likely, you will want to document the code. You can use the following comma ## Run the tests -::: tip +:::tip HyperFormula main test suite is maintained outside of this repository. You can find more information [here](https://github.com/handsontable/hyperformula/blob/master/test/README.md). ::: diff --git a/docs/guide/built-in-functions.md b/docs/src/content/docs/guide/built-in-functions.md similarity index 98% rename from docs/guide/built-in-functions.md rename to docs/src/content/docs/guide/built-in-functions.md index 5798f0404d..60dae129ca 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/src/content/docs/guide/built-in-functions.md @@ -1,4 +1,7 @@ -# Built-in functions +--- +title: "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. @@ -28,10 +31,10 @@ 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. +Each of HyperFormula's built-in function names is available in [17 languages](/docs/guide/localizing-functions#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: +**418** functions grouped into categories: - [Array manipulation](#array-manipulation) - [Database](#database) @@ -49,13 +52,13 @@ The latest version of HyperFormula has an extensive collection of _Some categories such as compatibility and cube are yet to be supported._ -::: tip +:::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 }}** +Total number of functions: **418** ### Array manipulation @@ -70,14 +73,14 @@ Total number of functions: **{{ $page.functionsCount }}** | Function ID | Description | Syntax | |:-----------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------| -| DATE | Returns the specified date as the number of full days since [`nullDate`](../api/interfaces/configparams.md#nulldate). | DATE(Year, Month, Day) | +| DATE | Returns the specified date as the number of full days since [`nullDate`](/docs/api/interfaces/configparams#nulldate). | DATE(Year, Month, Day) | | DATEDIF | Calculates distance between two dates.<br>Supported units: "D" (days), "M" (months), "Y" (years), "MD" (days ignoring months and years), "YM" (months ignoring years), or "YD" (days ignoring years). | DATEDIF(Date1, Date2, Unit) | -| DATEVALUE | Parses a date string and returns it as the number of full days since [`nullDate`](../api/interfaces/configparams.md#nulldate).<br>Accepts formats set by the [`dateFormats`](../api/interfaces/configparams.md#dateformats) option. | DATEVALUE(Datestring) | +| DATEVALUE | Parses a date string and returns it as the number of full days since [`nullDate`](/docs/api/interfaces/configparams#nulldate).<br>Accepts formats set by the [`dateFormats`](/docs/api/interfaces/configparams#dateformats) option. | DATEVALUE(Datestring) | | DAY | Returns the day of the given date value. | DAY(Number) | | DAYS | Calculates the difference between two date values. | DAYS(Date2, Date1) | | DAYS360 | Calculates the difference between two date values in days, in 360-day basis. | DAYS360(Date2, Date1[, Format]) | -| EDATE | Shifts the given startdate by given number of months and returns it as the number of full days since [`nullDate`](../api/interfaces/configparams.md#nulldate).[^non-odff] | EDATE(Startdate, Months) | -| EOMONTH | Returns the date of the last day of a month which falls months away from the start date. Returns the value in the form of number of full days since [`nullDate`](../api/interfaces/configparams.md#nulldate).[^non-odff] | EOMONTH(Startdate, Months) | +| EDATE | Shifts the given startdate by given number of months and returns it as the number of full days since [`nullDate`](/docs/api/interfaces/configparams#nulldate).[^non-odff] | EDATE(Startdate, Months) | +| EOMONTH | Returns the date of the last day of a month which falls months away from the start date. Returns the value in the form of number of full days since [`nullDate`](/docs/api/interfaces/configparams#nulldate).[^non-odff] | EOMONTH(Startdate, Months) | | HOUR | Returns hour component of given time. | HOUR(Time) | | INTERVAL | Returns interval string from given number of seconds. | INTERVAL(Seconds) | | ISOWEEKNUM | Returns an ISO week number that corresponds to the week of year. | ISOWEEKNUM(Date) | @@ -85,11 +88,11 @@ Total number of functions: **{{ $page.functionsCount }}** | MONTH | Returns the month for the given date value. | MONTH(Number) | | NETWORKDAYS | Returns the number of working days between two given dates. | NETWORKDAYS(Date1, Date2[, Holidays]) | | NETWORKDAYS.INTL | Returns the number of working days between two given dates. | NETWORKDAYS.INTL(Date1, Date2[, Mode [, Holidays]]) | -| NOW | Returns current date + time as a number of days since [`nullDate`](../api/interfaces/configparams.md#nulldate). | NOW() | +| NOW | Returns current date + time as a number of days since [`nullDate`](/docs/api/interfaces/configparams#nulldate). | NOW() | | SECOND | Returns second component of given time. | SECOND(Time) | | TIME | Returns the number that represents a given time as a fraction of full day. | TIME(Hour, Minute, Second) | -| TIMEVALUE | Parses a time string and returns a number that represents it as a fraction of a full day.<br>Accepts formats set by the [`timeFormats`](../api/interfaces/configparams.md#timeformats) option. | TIMEVALUE(Timestring) | -| TODAY | Returns an integer representing the current date as the number of full days since [`nullDate`](../api/interfaces/configparams.md#nulldate). | TODAY() | +| TIMEVALUE | Parses a time string and returns a number that represents it as a fraction of a full day.<br>Accepts formats set by the [`timeFormats`](/docs/api/interfaces/configparams#timeformats) option. | TIMEVALUE(Timestring) | +| TODAY | Returns an integer representing the current date as the number of full days since [`nullDate`](/docs/api/interfaces/configparams#nulldate). | TODAY() | | WEEKDAY | Computes a number between 1-7 representing the day of week. | WEEKDAY(Date, Type) | | WEEKNUM | Returns a week number that corresponds to the week of year. | WEEKNUM(Date, Type) | | WORKDAY | Returns the working day number of days from start day. | WORKDAY(Date, Shift[, Holidays]) | @@ -245,7 +248,7 @@ Total number of functions: **{{ $page.functionsCount }}** | COLUMNS | Returns the number of columns in the given reference. | COLUMNS(Array) | | FORMULATEXT | Returns a formula in a given cell as a string. | FORMULATEXT(Reference) | | HLOOKUP | Searches horizontally with reference to adjacent cells to the bottom. | HLOOKUP(Search_Criterion, Array, Index, Sort_Order) | -| HYPERLINK | Stores the url in the cell's metadata. It can be read using method [`getCellHyperlink`](../api/classes/hyperformula.md#getcellhyperlink) | HYPERLINK(Url[, LinkLabel]) | +| HYPERLINK | Stores the url in the cell's metadata. It can be read using method [`getCellHyperlink`](/docs/api/classes/hyperformula#getcellhyperlink) | HYPERLINK(Url[, LinkLabel]) | | INDEX | Returns the contents of a cell specified by row and column number. The column number is optional and defaults to 1. | INDEX(Range, Row [, Column]) | | MATCH | Returns the relative position of an item in an array that matches a specified value. | MATCH(Searchcriterion, LookupArray [, MatchType]) | | OFFSET | Returns the value of a cell offset by a certain number of rows and columns from a given reference point. | OFFSET(Reference, Rows, Columns, Height, Width) | @@ -531,7 +534,7 @@ Total number of functions: **{{ $page.functionsCount }}** | SPLIT | Divides the provided text using the space character as a separator and returns the substring at the zero-based position specified by the second argument.<br>`SPLIT("Lorem ipsum", 0) -> "Lorem"`<br>`SPLIT("Lorem ipsum", 1) -> "ipsum"` | SPLIT(Text, Index) | | SUBSTITUTE | Returns string where occurrences of Old_text are replaced by New_text. Replaces only specific occurrence if last parameter is provided. | SUBSTITUTE(Text, Old_text, New_text, [Occurrence]) | | T | Returns text if given value is text, empty string otherwise. | T(Value) | -| TEXT | Converts a number into text according to a given format.<br>By default, accepts the same formats that can be passed to the [`dateFormats`](../api/interfaces/configparams.md#dateformats) option, but can be further customized with the [`stringifyDateTime`](../api/interfaces/configparams.md#stringifydatetime) option. | TEXT(Number, Format) | +| TEXT | Converts a number into text according to a given format.<br>By default, accepts the same formats that can be passed to the [`dateFormats`](/docs/api/interfaces/configparams#dateformats) option, but can be further customized with the [`stringifyDateTime`](/docs/api/interfaces/configparams#stringifydatetime) option. | TEXT(Number, Format) | | TEXTJOIN | Joins text from multiple strings and/or ranges with a delimiter. Supports array/range delimiters that cycle through gaps. When ignore_empty is TRUE, empty strings are skipped. Returns #VALUE! if result exceeds 32,767 characters. | TEXTJOIN(Delimiter, Ignore_empty, Text1, [Text2, ...]) | | TRIM | Strips extra spaces from text. | TRIM("Text") | | UNICHAR | Returns the character created by using provided code point. | UNICHAR(Number) | @@ -543,4 +546,4 @@ Total number of functions: **{{ $page.functionsCount }}** 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). + [compatibility notes](/docs/guide/list-of-differences). diff --git a/docs/guide/cell-references.md b/docs/src/content/docs/guide/cell-references.md similarity index 91% rename from docs/guide/cell-references.md rename to docs/src/content/docs/guide/cell-references.md index b3b286bda9..5ff256c602 100644 --- a/docs/guide/cell-references.md +++ b/docs/src/content/docs/guide/cell-references.md @@ -1,4 +1,7 @@ -# Cell references +--- +title: "Cell references" +--- + A formula can reference one or more cells and automatically update its contents whenever any of the referenced cells change. The values from @@ -57,7 +60,7 @@ different cells in the workbook. ### Referencing named expressions -You can reference [named expressions](./named-expressions.md) by their assigned names. For example, if you name the expression `=SUM(100+10)` as `MySum`, you can then reference that expression by `MySum`. +You can reference [named expressions](/docs/guide/named-expressions) by their assigned names. For example, if you name the expression `=SUM(100+10)` as `MySum`, you can then reference that expression by `MySum`. A named expression works within a scope. You define the scope when creating a named expression: @@ -77,12 +80,12 @@ HyperFormula is more limited than typical spreadsheet software when it comes to referencing named ranges. For more information about how HyperFormula handles named ranges, -see [this section](named-expressions.md). +see [this section](/docs/guide/named-expressions). ## Relative references Relative and absolute references play a huge role in -[copy and paste](clipboard-operations.md), autofill, and CRUD +[copy and paste](/docs/guide/clipboard-operations), autofill, and CRUD operations like moving cells or columns. By default, all references are relative which means that when you @@ -193,17 +196,17 @@ You can reference ranges: The following restraints apply: - You can't mix two different types of range references together (=A1:B). -- Range expressions can't contain [named expressions](/guide/named-expressions.md). +- Range expressions can't contain [named expressions](/docs/guide/named-expressions). - At the moment, HyperFormula doesn't support multi-cell range references (=A1:B2:C3). -::: tip -In contrast to Google Sheets or Microsoft Excel, HyperFormula doesn't treat single cells as ranges. Instead, it immediately instantiates references to single cells as their values. Applying a scalar value to a function that takes ranges throws the [`CellRangeExpected`](/api/classes/errormessage.md#cellrangeexpected) error. +:::tip +In contrast to Google Sheets or Microsoft Excel, HyperFormula doesn't treat single cells as ranges. Instead, it immediately instantiates references to single cells as their values. Applying a scalar value to a function that takes ranges throws the [`CellRangeExpected`](/docs/api/classes/errormessage#cellrangeexpected) error. ::: ### More about ranges -- [Ranges in the dependency graph](/guide/dependency-graph.md#ranges-in-the-dependency-graph) -- [Types of operators: Reference operators](/guide/types-of-operators.md#reference-operators) -- [API reference: Ranges](/api/classes/hyperformula.md#ranges) +- [Ranges in the dependency graph](/docs/guide/dependency-graph#ranges-in-the-dependency-graph) +- [Types of operators: Reference operators](/docs/guide/types-of-operators#reference-operators) +- [API reference: Ranges](/docs/api/classes/hyperformula#ranges) ## Sheet names in references diff --git a/docs/guide/client-side-installation.md b/docs/src/content/docs/guide/client-side-installation.md similarity index 90% rename from docs/guide/client-side-installation.md rename to docs/src/content/docs/guide/client-side-installation.md index e53636b185..a2b430c91c 100644 --- a/docs/guide/client-side-installation.md +++ b/docs/src/content/docs/guide/client-side-installation.md @@ -1,4 +1,7 @@ -# Client-side installation +--- +title: "Client-side installation" +--- + ### Install with npm or Yarn @@ -50,13 +53,13 @@ Or you may load just a minimal build and add the dependencies on your own: <script src="https://cdn.jsdelivr.net/npm/hyperformula/dist/hyperformula.min.js"></script> ``` -A useful option when you already use some of them and there is no need to duplicate the dependencies. You can read more about the dependencies of HyperFormula on a dedicated [Dependencies](/guide/dependencies.md) page. +A useful option when you already use some of them and there is no need to duplicate the dependencies. You can read more about the dependencies of HyperFormula on a dedicated [Dependencies](/docs/guide/dependencies) page. ## Clone from GitHub If you choose to clone the project or download it from GitHub you will need to build it prior to usage. Check the -[building section](building.md) for a full list of commands and their +[building section](/docs/guide/building) for a full list of commands and their descriptions. ### Clone with HTTPS diff --git a/docs/src/content/docs/guide/clipboard-operations.md b/docs/src/content/docs/guide/clipboard-operations.md new file mode 100644 index 0000000000..ce7ca318dd --- /dev/null +++ b/docs/src/content/docs/guide/clipboard-operations.md @@ -0,0 +1,447 @@ +--- +title: "Clipboard operations" +--- + + +Through a set of dedicated methods, HyperFormula supports clipboard operations, such as copying, cutting, +and pasting. This lets you integrate the functionality +of interacting with the clipboard. + +The copied or cut data is stored as a memory reference, not directly in the system clipboard. + +## Copy + +To copy the contents of a cell or range, use the [`copy()`](/docs/api/classes/hyperformula#copy) method. Pass arguments of type [`SimpleCellRange`](/docs/api/interfaces/simplecellrange). + +```javascript +const hfInstance = HyperFormula.buildFromArray([ + ['1', '2'], +]); + +// copy [ [ 2 ] ] +const clipboardContent = hfInstance.copy({ + start: { sheet: 0, col: 1, row: 0 }, + end: { sheet: 0, col: 1, row: 0 }, +}); +``` + +## Cut + +To cut the contents of a cell or range, use the [`cut()`](/docs/api/classes/hyperformula#cut) method. Pass arguments of type [`SimpleCellRange`](/docs/api/interfaces/simplecellrange). + +:::tip +Any CRUD operation called after the [`cut()`](/docs/api/classes/hyperformula#cut) method aborts the cut operation. +::: + +```javascript +const hfInstance = HyperFormula.buildFromArray([ + ['1', '2'], +]); + +// returns the values that were cut: [ [ 1 ] ] +const clipboardContent = hfInstance.cut({ + start: { sheet: 0, col: 0, row: 0 }, + end: { sheet: 0, col: 0, row: 0 }, +}); +``` + +## Paste + +To paste the contents of a cell or range, use the [`paste()`](/docs/api/classes/hyperformula#paste) method. + +[`paste()`](/docs/api/classes/hyperformula#paste) requires only one parameter: the top left corner of the target range. + +```javascript +const hfInstance = HyperFormula.buildFromArray([ + ['1', '2'], +]); + +// [ [ 2 ] ] was copied +const clipboardContent = hfInstance.copy({ + start: { sheet: 0, col: 1, row: 0 }, + end: { sheet: 0, col: 1, row: 0 }, +}); + +// returns a list of modified cells: their absolute addresses and new values +const changes = hfInstance.paste({ sheet: 0, col: 1, row: 0 }); +``` + +If the clipboard is empty, the [`paste()`](/docs/api/classes/hyperformula#paste) method doesn't do anything. + +### Copy and paste + +When called after [`copy()`](/docs/api/classes/hyperformula#copy), the [`paste()`](/docs/api/classes/hyperformula#paste) method: +- Pastes the copied data into the target range. +- Triggers a recalculation of all affected formulas. + +:::tip +If a formula `=A1` is copied from cell B1 into B2, the B2 formula becomes `=A2`. +::: + +### Cut and paste + +When called after [`cut()`](/docs/api/classes/hyperformula#cut), the [`paste()`](/docs/api/classes/hyperformula#paste) method: +- Moves the cut data into the target range, by calling the [`moveCells()`](/docs/api/classes/hyperformula#movecells) method. +- Removes the cut data from the source range. +- Triggers a recalculation of all affected formulas. + +:::tip +If a formula `=A1` is cut from cell B1 into B2, the B2 formula becomes `=A1`. +::: + +#### Pasting named expressions + +If a copied or cut formula contains a [named expression](/docs/guide/named-expressions) defined for a local scope, and the formula is pasted to a sheet that is out of scope for that expression, the expression's scope changes to global. + +If the copied or cut named expression's scope is the same as the target's, the expression's local scope remains the same. + +## Clear the clipboard + +To clear the clipboard, use the [`clearClipboard()`](/docs/api/classes/hyperformula#clearclipboard) +method. + +To check if the clipboard holds any data, use the [`isClipboardEmpty()`](/docs/api/classes/hyperformula#isclipboardempty) method. + +## Data storage + +The copied or cut data is stored as a memory reference, not directly in the system clipboard. + +Depending on what was cut, the data is stored as: +* An array of arrays +* A number +* A string +* A boolean +* An empty value + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/clipboard-operations/example1.js"> +<div class="example container"> + <div> + <div> + <button id="copy" class="button run"> + Copy + </button> + <button id="paste" class="button button-outline reset"> + Paste + </button> + <button id="reset" class="button button-outline reset"> + Reset + </button> + </div> + </div> + <div> + <div> + <span id="copyInfo"></span> + </div> + </div> + <table> + <colgroup> + <col style="width:30%" /> + <col style="width:30%" /> + <col style="width:30%" /> + </colgroup> + <thead> + <tr> + <th>Name</th> + <th>Surname</th> + <th>Both</th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/* start:skip-in-sandbox */ +const NothingToPasteError = HyperFormula.NothingToPasteError; +/* end:skip-in-sandbox */ +/** + * Initial table data. + */ +const tableData = [ + ['Greg', 'Black', '=CONCATENATE(A1, " ",B1)'], + ['Anne', 'Carpenter', '=CONCATENATE(A2, " ", B2)'], + ['Chris', 'Aklips', '=CONCATENATE(A3, " ",B3)'], +]; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +/** + * Reinitialize the HF data. + */ +function reinitializeData() { + hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, + ); +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const copyButton = document.querySelector('.example #copy'); + const pasteButton = document.querySelector('.example #paste'); + const resetButton = document.querySelector('.example #reset'); + + copyButton.addEventListener('click', () => { + copy(); + updateCopyInfo('Second row copied'); + }); + pasteButton.addEventListener('click', () => { + paste(); + }); + resetButton.addEventListener('click', () => { + reinitializeData(); + updateCopyInfo(''); + renderTable(); + }); +} + +/** + * Copy the second row. + */ +function copy() { + return hf.copy({ + start: { sheet: 0, col: 0, row: 1 }, + end: { sheet: 0, col: 2, row: 1 }, + }); +} + +/** + * Paste the HF clipboard into the first row. + */ +function paste() { + try { + hf.paste({ sheet: 0, col: 0, row: 0 }); + updateCopyInfo('Pasted into the first row'); + renderTable(); + } catch (error) { + if (error instanceof NothingToPasteError) { + updateCopyInfo('There is nothing to paste'); + } else { + throw error; + } + } +} + +const ANIMATION_ENABLED = true; + +/** + * Fill the HTML table with data. + */ +function renderTable() { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress)) { + cellValue = hf.getCellValue(cellAddress); + } + + newTbodyHTML += `<td class="${updatedCellClass}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Update the information about the copy/paste action. + * + * @param {string} message Message to display. + */ +function updateCopyInfo(message) { + const copyInfoDOM = document.querySelector('.example #copyInfo'); + + copyInfoDOM.innerText = message; +} + +// Fill the HyperFormula sheet with data. +reinitializeData(); +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> \ No newline at end of file diff --git a/docs/guide/code-of-conduct.md b/docs/src/content/docs/guide/code-of-conduct.md similarity index 99% rename from docs/guide/code-of-conduct.md rename to docs/src/content/docs/guide/code-of-conduct.md index 83bd5ca380..8bbc1c501a 100644 --- a/docs/guide/code-of-conduct.md +++ b/docs/src/content/docs/guide/code-of-conduct.md @@ -1,4 +1,7 @@ -# Code of conduct +--- +title: "Code of conduct" +--- + ## Our Pledge diff --git a/docs/guide/compatibility-with-google-sheets.md b/docs/src/content/docs/guide/compatibility-with-google-sheets.md similarity index 68% rename from docs/guide/compatibility-with-google-sheets.md rename to docs/src/content/docs/guide/compatibility-with-google-sheets.md index 5e98ea3b4a..a36843f8ed 100644 --- a/docs/guide/compatibility-with-google-sheets.md +++ b/docs/src/content/docs/guide/compatibility-with-google-sheets.md @@ -1,15 +1,18 @@ -# Compatibility with Google Sheets +--- +title: "Compatibility with Google Sheets" +--- + Achieve nearly full compatibility wih Google Sheets, using the right HyperFormula configuration. **Contents:** -[[toc]] + ## Overview While HyperFormula conforms to the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard, it also follows industry practices set by other spreadsheets such as Microsoft Excel or Google Sheets. -That said, there are cases when HyperFormula can't be compatible with all three at the same time, because of inconsistencies (between the OpenDocument standard, Microsoft Excel and Google Sheets), limitations of HyperFormula at its current development stage (version `{{ $page.version }}`), or limitations of Microsoft Excel or Google Sheets themselves. For the full list of such differences, see [this](list-of-differences.md) page. +That said, there are cases when HyperFormula can't be compatible with all three at the same time, because of inconsistencies (between the OpenDocument standard, Microsoft Excel and Google Sheets), limitations of HyperFormula at its current development stage (version `3.3.0`), or limitations of Microsoft Excel or Google Sheets themselves. For the full list of such differences, see [this](/docs/guide/list-of-differences) page. Still, with the right configuration, you can achieve nearly full compatibility. @@ -19,7 +22,7 @@ Still, with the right configuration, you can achieve nearly full compatibility. Google Sheets has built-in constants (keywords) for the boolean values (`TRUE` and `FALSE`). -To set up HyperFormula in the same way, define `TRUE` and `FALSE` as [named expressions](named-expressions.md), by using HyperFormula's [`TRUE()`](built-in-functions.md#logical) and [`FALSE()`](built-in-functions.md#logical) functions. +To set up HyperFormula in the same way, define `TRUE` and `FALSE` as [named expressions](/docs/guide/named-expressions), by using HyperFormula's [`TRUE()`](/docs/guide/built-in-functions#logical) and [`FALSE()`](/docs/guide/built-in-functions#logical) functions. ```js hfInstance.addNamedExpression('TRUE', '=TRUE()'); @@ -28,9 +31,9 @@ hfInstance.addNamedExpression('FALSE', '=FALSE()'); ### Array arithmetic mode -In Google Sheets, the [array arithmetic mode](arrays.md#array-arithmetic-mode) is disabled by default. +In Google Sheets, the [array arithmetic mode](/docs/guide/arrays#array-arithmetic-mode) is disabled by default. -To set up HyperFormula in the same way, set the [`useArrayArithmetic`](../api/interfaces/configparams.md#usearrayarithmetic) option to `false`. +To set up HyperFormula in the same way, set the [`useArrayArithmetic`](/docs/api/interfaces/configparams#usearrayarithmetic) option to `false`. ```js useArrayArithmetic: false, // set by default @@ -49,14 +52,14 @@ leapYear1900: false, // set by default ### Numerical precision Both HyperFormula and Google Sheets automatically round floating-point numbers. To configure this feature, use these options: -- [`smartRounding`](../api/interfaces/configparams.md#smartrounding) -- [`precisionEpsilon`](../api/interfaces/configparams.md#precisionepsilon) +- [`smartRounding`](/docs/api/interfaces/configparams#smartrounding) +- [`precisionEpsilon`](/docs/api/interfaces/configparams#precisionepsilon) ### Separators -In Google Sheets, separators depend on your configured locale, whereas in HyperFormula, you set up separators through options (e.g., [`decimalSeparator`](../api/interfaces/configparams.md#decimalseparator)). +In Google Sheets, separators depend on your configured locale, whereas in HyperFormula, you set up separators through options (e.g., [`decimalSeparator`](/docs/api/interfaces/configparams#decimalseparator)). -In Google Sheets' `en-US` locale, the thousands separator and the function argument separator use the same character: `,` (a comma). But in HyperFormula, [`functionArgSeparator`](../api/interfaces/configparams.md#functionargseparator) can't be the same as [`thousandSeparator`](../api/interfaces/configparams.md#thousandseparator). For this reason, you can't achieve full compatibility with Google Sheets' `en-US` locale. +In Google Sheets' `en-US` locale, the thousands separator and the function argument separator use the same character: `,` (a comma). But in HyperFormula, [`functionArgSeparator`](/docs/api/interfaces/configparams#functionargseparator) can't be the same as [`thousandSeparator`](/docs/api/interfaces/configparams#thousandseparator). For this reason, you can't achieve full compatibility with Google Sheets' `en-US` locale. To match Google Sheets' `en-US` locale as closely as possible, use the default configuration: @@ -69,27 +72,27 @@ arrayRowSeparator: ';', // set by default ``` Related options: -- [`functionArgSeparator`](../api/interfaces/configparams.md#functionargseparator) -- [`decimalSeparator`](../api/interfaces/configparams.md#decimalseparator) -- [`thousandSeparator`](../api/interfaces/configparams.md#thousandseparator) -- [`arrayRowSeparator`](../api/interfaces/configparams.md#arrayrowseparator) -- [`arrayColumnSeparator`](../api/interfaces/configparams.md#arraycolumnseparator) +- [`functionArgSeparator`](/docs/api/interfaces/configparams#functionargseparator) +- [`decimalSeparator`](/docs/api/interfaces/configparams#decimalseparator) +- [`thousandSeparator`](/docs/api/interfaces/configparams#thousandseparator) +- [`arrayRowSeparator`](/docs/api/interfaces/configparams#arrayrowseparator) +- [`arrayColumnSeparator`](/docs/api/interfaces/configparams#arraycolumnseparator) ### Date and time formats -In Google Sheets, date and time formats depend on the spreadsheet's locale and are [shared across all users](https://support.google.com/docs/answer/58515), whereas in HyperFormula you can [set them up freely](date-and-time-handling.md). +In Google Sheets, date and time formats depend on the spreadsheet's locale and are [shared across all users](https://support.google.com/docs/answer/58515), whereas in HyperFormula you can [set them up freely](/docs/guide/date-and-time-handling). Options related to date and time formats: -- [`dateFormats`](../api/interfaces/configparams.md#dateformats) -- [`timeFormats`](../api/interfaces/configparams.md#timeformats) -- [`nullYear`](../api/interfaces/configparams.md#nullyear) -- [`parseDateTime()`](../api/interfaces/configparams.md#parsedatetime) -- [`stringifyDateTime()`](../api/interfaces/configparams.md#stringifydatetime) -- [`stringifyDuration()`](../api/interfaces/configparams.md#stringifyduration) +- [`dateFormats`](/docs/api/interfaces/configparams#dateformats) +- [`timeFormats`](/docs/api/interfaces/configparams#timeformats) +- [`nullYear`](/docs/api/interfaces/configparams#nullyear) +- [`parseDateTime()`](/docs/api/interfaces/configparams#parsedatetime) +- [`stringifyDateTime()`](/docs/api/interfaces/configparams#stringifydatetime) +- [`stringifyDuration()`](/docs/api/interfaces/configparams#stringifyduration) ## Full configuration -This configuration aligns HyperFormula with the default behavior of Google Sheets (set to locale `en-US`), as closely as possible at this development stage (version `{{ $page.version }}`). +This configuration aligns HyperFormula with the default behavior of Google Sheets (set to locale `en-US`), as closely as possible at this development stage (version `3.3.0`). ```js // define options diff --git a/docs/guide/compatibility-with-microsoft-excel.md b/docs/src/content/docs/guide/compatibility-with-microsoft-excel.md similarity index 67% rename from docs/guide/compatibility-with-microsoft-excel.md rename to docs/src/content/docs/guide/compatibility-with-microsoft-excel.md index fade7ed966..bb031c89b0 100644 --- a/docs/guide/compatibility-with-microsoft-excel.md +++ b/docs/src/content/docs/guide/compatibility-with-microsoft-excel.md @@ -1,15 +1,18 @@ -# Compatibility with Microsoft Excel +--- +title: "Compatibility with Microsoft Excel" +--- + Achieve nearly full compatibility with Microsoft Excel, using the right HyperFormula configuration. **Contents:** -[[toc]] + ## Overview While HyperFormula conforms to the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard, it also follows industry practices set by other spreadsheets such as Microsoft Excel or Google Sheets. -That said, there are cases when HyperFormula can't be compatible with all three at the same time, because of inconsistencies (between the OpenDocument standard, Microsoft Excel and Google Sheets), limitations of HyperFormula at its current development stage (version `{{ $page.version }}`), or limitations of Microsoft Excel or Google Sheets themselves. For the full list of such differences, see [this](list-of-differences.md) page. +That said, there are cases when HyperFormula can't be compatible with all three at the same time, because of inconsistencies (between the OpenDocument standard, Microsoft Excel and Google Sheets), limitations of HyperFormula at its current development stage (version `3.3.0`), or limitations of Microsoft Excel or Google Sheets themselves. For the full list of such differences, see [this](/docs/guide/list-of-differences) page. Still, with the right configuration, you can achieve nearly full compatibility. @@ -17,18 +20,18 @@ Still, with the right configuration, you can achieve nearly full compatibility. HyperFormula implements **350 out of 515 Excel functions** (68% coverage), as of version 3.1.0 and Excel 2024. This means that **165 Excel functions** (32%) are not yet available in HyperFormula. -Additionally, HyperFormula includes some functions that are not part of Excel's standard function set, bringing the total number of available functions to **{{ $page.functionsCount }}**. +Additionally, HyperFormula includes some functions that are not part of Excel's standard function set, bringing the total number of available functions to **418**. -For a complete list of supported functions, see the [built-in functions](built-in-functions.md) page. +For a complete list of supported functions, see the [built-in functions](/docs/guide/built-in-functions) page. -If you need any of the missing Excel functions, you can [contact us](contact.md) or implement them as [custom functions](custom-functions.md), extending HyperFormula's capabilities to meet your specific requirements. +If you need any of the missing Excel functions, you can [contact us](/docs/guide/contact) or implement them as [custom functions](/docs/guide/custom-functions), extending HyperFormula's capabilities to meet your specific requirements. ## Configure compatibility with Microsoft Excel ### String comparison rules -In the US version of Microsoft Excel, by default, [string comparison](types-of-operators.md#comparing-strings) is accent-sensitive and case-insensitive. +In the US version of Microsoft Excel, by default, [string comparison](/docs/guide/types-of-operators#comparing-strings) is accent-sensitive and case-insensitive. To set up HyperFormula in the same way, use this configuration: @@ -40,11 +43,11 @@ localeLang: 'en-US', ``` Related options: -- [`caseSensitive`](../api/interfaces/configparams.md#casesensitive) -- [`accentSensitive`](../api/interfaces/configparams.md#accentsensitive) -- [`caseFirst`](../api/interfaces/configparams.md#casefirst) -- [`ignorePunctuation`](../api/interfaces/configparams.md#ignorepunctuation) -- [`localeLang`](../api/interfaces/configparams.md#localelang) +- [`caseSensitive`](/docs/api/interfaces/configparams#casesensitive) +- [`accentSensitive`](/docs/api/interfaces/configparams#accentsensitive) +- [`caseFirst`](/docs/api/interfaces/configparams#casefirst) +- [`ignorePunctuation`](/docs/api/interfaces/configparams#ignorepunctuation) +- [`localeLang`](/docs/api/interfaces/configparams#localelang) ### Function criteria @@ -59,15 +62,15 @@ matchWholeCell: true, // set by default ``` Related options: -- [`matchWholeCell`](../api/interfaces/configparams.md#matchwholecell) -- [`useRegularExpressions`](../api/interfaces/configparams.md#useregularexpressions) -- [`useWildcards`](../api/interfaces/configparams.md#usewildcards) +- [`matchWholeCell`](/docs/api/interfaces/configparams#matchwholecell) +- [`useRegularExpressions`](/docs/api/interfaces/configparams#useregularexpressions) +- [`useWildcards`](/docs/api/interfaces/configparams#usewildcards) ### `TRUE` and `FALSE` constants Microsoft Excel has built-in constants (keywords) for the boolean values (`TRUE` and `FALSE`). -To set up HyperFormula in the same way, define `TRUE` and `FALSE` as [named expressions](named-expressions.md), by using HyperFormula's [`TRUE()`](built-in-functions.md#logical) and [`FALSE()`](built-in-functions.md#logical) functions. +To set up HyperFormula in the same way, define `TRUE` and `FALSE` as [named expressions](/docs/guide/named-expressions), by using HyperFormula's [`TRUE()`](/docs/guide/built-in-functions#logical) and [`FALSE()`](/docs/guide/built-in-functions#logical) functions. ```js hfInstance.addNamedExpression('TRUE', '=TRUE()'); @@ -76,9 +79,9 @@ hfInstance.addNamedExpression('FALSE', '=FALSE()'); ### Array arithmetic mode -In Microsoft Excel, the [array arithmetic mode](arrays.md#array-arithmetic-mode) is enabled by default. +In Microsoft Excel, the [array arithmetic mode](/docs/guide/arrays#array-arithmetic-mode) is enabled by default. -To set up HyperFormula in the same way, set the [`useArrayArithmetic`](../api/interfaces/configparams.md#usearrayarithmetic) option to `true`. +To set up HyperFormula in the same way, set the [`useArrayArithmetic`](/docs/api/interfaces/configparams#usearrayarithmetic) option to `true`. ```js useArrayArithmetic: true, @@ -88,7 +91,7 @@ useArrayArithmetic: true, In Microsoft Excel, all whitespace characters inside formulas are ignored. -To set up HyperFormula in the same way, set the [`ignoreWhiteSpace`](../api/interfaces/configparams.md#ignorewhitespace) option to `'any'`. +To set up HyperFormula in the same way, set the [`ignoreWhiteSpace`](/docs/api/interfaces/configparams#ignorewhitespace) option to `'any'`. ```js ignoreWhiteSpace: 'any', @@ -98,7 +101,7 @@ ignoreWhiteSpace: 'any', In Microsoft Excel, formulas that evaluate to empty values are forced to evaluate to zero instead. -To set up HyperFormula in the same way, set the [`evaluateNullToZero`](../api/interfaces/configparams.md#evaluatenulltozero) option to `true`. +To set up HyperFormula in the same way, set the [`evaluateNullToZero`](/docs/api/interfaces/configparams#evaluatenulltozero) option to `true`. ```js evaluateNullToZero: true, @@ -118,14 +121,14 @@ nullDate: { year: 1899, month: 12, day: 31 }, ### Numerical precision Both HyperFormula and Microsoft Excel automatically round floating-point numbers. To configure this feature, use these options: -- [`smartRounding`](../api/interfaces/configparams.md#smartrounding) -- [`precisionEpsilon`](../api/interfaces/configparams.md#precisionepsilon) +- [`smartRounding`](/docs/api/interfaces/configparams#smartrounding) +- [`precisionEpsilon`](/docs/api/interfaces/configparams#precisionepsilon) ### Separators -In Microsoft Excel, separators depend on your configured locale, whereas in HyperFormula, you set up separators through options (e.g., [`decimalSeparator`](../api/interfaces/configparams.md#decimalseparator)). +In Microsoft Excel, separators depend on your configured locale, whereas in HyperFormula, you set up separators through options (e.g., [`decimalSeparator`](/docs/api/interfaces/configparams#decimalseparator)). -In Excel's `en-US` locale, the thousands separator and the function argument separator use the same character: `,` (a comma). But in HyperFormula, [`functionArgSeparator`](../api/interfaces/configparams.md#functionargseparator) can't be the same as [`thousandSeparator`](../api/interfaces/configparams.md#thousandseparator). For this reason, you can't achieve full compatibility with Excel's `en-US` locale. +In Excel's `en-US` locale, the thousands separator and the function argument separator use the same character: `,` (a comma). But in HyperFormula, [`functionArgSeparator`](/docs/api/interfaces/configparams#functionargseparator) can't be the same as [`thousandSeparator`](/docs/api/interfaces/configparams#thousandseparator). For this reason, you can't achieve full compatibility with Excel's `en-US` locale. To match Excel's `en-US` locale as closely as possible, use the default configuration: @@ -138,27 +141,27 @@ arrayRowSeparator: ';', // set by default ``` Related options: -- [`functionArgSeparator`](../api/interfaces/configparams.md#functionargseparator) -- [`decimalSeparator`](../api/interfaces/configparams.md#decimalseparator) -- [`thousandSeparator`](../api/interfaces/configparams.md#thousandseparator) -- [`arrayRowSeparator`](../api/interfaces/configparams.md#arrayrowseparator) -- [`arrayColumnSeparator`](../api/interfaces/configparams.md#arraycolumnseparator) +- [`functionArgSeparator`](/docs/api/interfaces/configparams#functionargseparator) +- [`decimalSeparator`](/docs/api/interfaces/configparams#decimalseparator) +- [`thousandSeparator`](/docs/api/interfaces/configparams#thousandseparator) +- [`arrayRowSeparator`](/docs/api/interfaces/configparams#arrayrowseparator) +- [`arrayColumnSeparator`](/docs/api/interfaces/configparams#arraycolumnseparator) ### Date and time formats -In Microsoft Excel, date and time formats depend on your configured locale, whereas in HyperFormula you can [set them up freely](date-and-time-handling.md). +In Microsoft Excel, date and time formats depend on your configured locale, whereas in HyperFormula you can [set them up freely](/docs/guide/date-and-time-handling). Options related to date and time formats: -- [`dateFormats`](../api/interfaces/configparams.md#dateformats) -- [`timeFormats`](../api/interfaces/configparams.md#timeformats) -- [`nullYear`](../api/interfaces/configparams.md#nullyear) -- [`parseDateTime()`](../api/interfaces/configparams.md#parsedatetime) -- [`stringifyDateTime()`](../api/interfaces/configparams.md#stringifydatetime) -- [`stringifyDuration()`](../api/interfaces/configparams.md#stringifyduration) +- [`dateFormats`](/docs/api/interfaces/configparams#dateformats) +- [`timeFormats`](/docs/api/interfaces/configparams#timeformats) +- [`nullYear`](/docs/api/interfaces/configparams#nullyear) +- [`parseDateTime()`](/docs/api/interfaces/configparams#parsedatetime) +- [`stringifyDateTime()`](/docs/api/interfaces/configparams#stringifydatetime) +- [`stringifyDuration()`](/docs/api/interfaces/configparams#stringifyduration) ## Full configuration -This configuration aligns HyperFormula with the default behavior of Microsoft Excel (set to locale `en-US`), as closely as possible at this development stage (version `{{ $page.version }}`). +This configuration aligns HyperFormula with the default behavior of Microsoft Excel (set to locale `en-US`), as closely as possible at this development stage (version `3.3.0`). ```js // define options diff --git a/docs/guide/configuration-options.md b/docs/src/content/docs/guide/configuration-options.md similarity index 73% rename from docs/guide/configuration-options.md rename to docs/src/content/docs/guide/configuration-options.md index 4f3076065d..7db3b5dfc2 100644 --- a/docs/guide/configuration-options.md +++ b/docs/src/content/docs/guide/configuration-options.md @@ -1,15 +1,18 @@ -# Configuration options +--- +title: "Configuration options" +--- + HyperFormula can be customized through easy-to-setup `options`. The only mandatory key is `licenseKey`. It has a -[dedicated section](license-key.md) in which you can find all allowed +[dedicated section](/docs/guide/license-key) in which you can find all allowed types of key values. Below you can see the example of a configuration object and the static method called to initiate a new instance of HyperFormula. -[See the full list of available options →](../api/interfaces/configparams.html) +[See the full list of available options →](/docs/api/interfaces/configparams) ## Example diff --git a/docs/guide/contact.md b/docs/src/content/docs/guide/contact.md similarity index 89% rename from docs/guide/contact.md rename to docs/src/content/docs/guide/contact.md index 496b3a6c55..59d1de83ae 100644 --- a/docs/guide/contact.md +++ b/docs/src/content/docs/guide/contact.md @@ -1,4 +1,7 @@ -# Contact +--- +title: "Contact" +--- + HyperFormula is a product of Handsoncode, the company that stands behind [Handsontable](https://handsontable.com/). We are here to answer all @@ -21,4 +24,4 @@ or submit your inquiry through the ## Looking for technical support? -Visit the page about [Support](support.md) \ No newline at end of file +Visit the page about [Support](/docs/guide/support) \ No newline at end of file diff --git a/docs/guide/contributing.md b/docs/src/content/docs/guide/contributing.md similarity index 92% rename from docs/guide/contributing.md rename to docs/src/content/docs/guide/contributing.md index ae1a4c7c64..d7a55d1a14 100644 --- a/docs/guide/contributing.md +++ b/docs/src/content/docs/guide/contributing.md @@ -1,4 +1,7 @@ -# Contributing +--- +title: "Contributing" +--- + You are welcome to contribute to HyperFormula's development. Your help is much appreciated in any of the following topics: @@ -21,7 +24,7 @@ library of translations is also a good task to start with. [Here](https://docs.google.com/spreadsheets/d/1UUskn4ZDDjLGSpO6kg73DOvabNoeqLbkJYyVfLyYlYw) you can find a list of function translations. -Visit the [building](building.md) section to +Visit the [building](/docs/guide/building) section to get more info about the development process and check the list of commands you can run in this project. Check the `/i18n` folder in the project - all translations are kept there. @@ -44,4 +47,4 @@ not `master`. ## Code of conduct By participating in this project, you are expected to uphold our -[Code of Conduct](code-of-conduct.md). \ No newline at end of file +[Code of Conduct](/docs/guide/code-of-conduct). \ No newline at end of file diff --git a/docs/guide/custom-functions.md b/docs/src/content/docs/guide/custom-functions.md similarity index 92% rename from docs/guide/custom-functions.md rename to docs/src/content/docs/guide/custom-functions.md index 49794b1d38..333faf6d69 100644 --- a/docs/guide/custom-functions.md +++ b/docs/src/content/docs/guide/custom-functions.md @@ -1,8 +1,11 @@ -# Custom functions +--- +title: "Custom functions" +--- + Expand the function library of your application by adding custom functions. -**Contents:** [[toc]] + ## Add a simple custom function @@ -28,7 +31,7 @@ an object that declares the functions provided by this plugin. The name of that object becomes the ID by which [translations](#function-name-translations), [aliases](#function-aliases), and other elements reference your function. Make the ID unique among all HyperFormula -functions ([built-in](built-in-functions.md#list-of-available-functions) and +functions ([built-in](/docs/guide/built-in-functions#list-of-available-functions) and custom). In your function's object, you can specify: @@ -51,7 +54,7 @@ MyCustomPlugin.implementedFunctions = { }; ``` -::: tip +:::tip To define multiple functions in a single function plugin, add them all to the `implementedFunctions` object. @@ -72,7 +75,7 @@ MyCustomPlugin.implementedFunctions = { In a separate object, define your function's names in every [language](#function-name-translations) that you want to support. -::: tip +:::tip Even if you support just a single language, you still need to define a translation for it. ::: @@ -109,10 +112,10 @@ Wrap your implementation in the built-in `runFunction()` method, which: [argument validation options](#argument-validation-options). - Duplicates the arguments according to the [`repeatLastArgs` option](#function-options). -- Handles the [array arithmetic mode](arrays.md#array-arithmetic-mode). +- Handles the [array arithmetic mode](/docs/guide/arrays#array-arithmetic-mode). - Performs - [function vectorization](arrays.md#passing-arrays-to-scalar-functions-vectorization). -- Performs [argument broadcasting](arrays.md#broadcasting). + [function vectorization](/docs/guide/arrays#passing-arrays-to-scalar-functions-vectorization). +- Performs [argument broadcasting](/docs/guide/arrays#broadcasting). ```js export class MyCustomPlugin extends FunctionPlugin { @@ -135,7 +138,7 @@ Register your function plugin and its translations so that HyperFormula can recognize it. You need to do this **before** you create your HyperFormula instance. Use the -[`registerFunctionPlugin()`](../api/classes/hyperformula.md#registerfunctionplugin) +[`registerFunctionPlugin()`](/docs/api/classes/hyperformula#registerfunctionplugin) method: ```js @@ -217,7 +220,7 @@ MyCustomPlugin.implementedFunctions = { ``` The range arguments are passed to the implementation method as instances of the -[`SimpleRangeValue` class](../api/classes/simplerangevalue.md): +[`SimpleRangeValue` class](/docs/api/classes/simplerangevalue): ```js export class MyCustomPlugin extends FunctionPlugin { @@ -237,8 +240,8 @@ export class MyCustomPlugin extends FunctionPlugin { ### Return an array of data -A function can return multiple values in the form of an [array](arrays.md). To -do that, use [`SimpleRangeValue` class](../api/classes/simplerangevalue.md): +A function can return multiple values in the form of an [array](/docs/guide/arrays). To +do that, use [`SimpleRangeValue` class](/docs/api/classes/simplerangevalue): ```js export class MyCustomPlugin extends FunctionPlugin { @@ -260,9 +263,9 @@ A function that returns an array will cause the `VALUE!` error unless you also declare a companion method for the array size. To do that, provide the `sizeOfResultArrayMethod` that calculates the size of the result array based on the function arguments and returns an instance of the -[`ArraySize` class](../api/classes/arraysize.md). +[`ArraySize` class](/docs/api/classes/arraysize). -::: tip +:::tip When you use your custom function in a formula, `sizeOfResultArrayMethod` is triggered every time the formula changes, but not when the dependencies of the formula change. This can cause unexpected behavior if the size of the result array depends on the values in the referenced cells. ::: @@ -295,9 +298,9 @@ MyCustomPlugin.implementedFunctions = { ### Validate the arguments and return an error To handle invalid inputs, the custom function should return an instance of the -[`CellError` class](../api/classes/cellerror.md) with the relevant -[error type](types-of-errors.md). Errors are localized according to your -[language settings](localizing-functions.md). +[`CellError` class](/docs/api/classes/cellerror) with the relevant +[error type](/docs/guide/types-of-errors). Errors are localized according to your +[language settings](/docs/guide/localizing-functions). ```js if (rangeData.some((row) => row.some((val) => typeof rawValue !== 'number'))) { @@ -308,8 +311,8 @@ if (rangeData.some((row) => row.some((val) => typeof rawValue !== 'number'))) { } ``` -::: tip -All HyperFormula [error types](types-of-errors.md) support optional +:::tip +All HyperFormula [error types](/docs/guide/types-of-errors) support optional custom error messages. Put them to good use: let your users know what caused the error and how to avoid it in the future. ::: @@ -376,11 +379,11 @@ You can set the following options for your function: | `returnNumberType` | String | If the function returns a numeric value, this option indicates how to interpret the returned number.<br/>Possible values: `NUMBER_RAW, NUMBER_DATE, NUMBER_TIME, NUMBER_DATETIME, NUMBER_CURRENCY, NUMBER_PERCENT`.<br/>Default: `NUMBER_RAW` | | `repeatLastArgs` | Number | For functions with a variable number of arguments: sets how many last arguments can be repeated indefinitely.<br/>Default: `0` | | `expandRanges` | Boolean | `true`: ranges in the function's arguments are inlined to (possibly multiple) scalar arguments.<br/>Default: `false` | -| `isVolatile` | Boolean | `true`: the function is [volatile](volatile-functions.md).<br/>Default: `false` | +| `isVolatile` | Boolean | `true`: the function is [volatile](/docs/guide/volatile-functions).<br/>Default: `false` | | `isDependentOnSheetStructureChange` | Boolean | `true`: the function gets recalculated with each sheet shape change (e.g., when adding/removing rows or columns).<br/>Default: `false` | | `doesNotNeedArgumentsToBeComputed` | Boolean | `true`: the function treats reference or range arguments as arguments that don't create dependency (other arguments are properly evaluated).<br/>Default: `false` | -| `enableArrayArithmeticForArguments` | Boolean | `true`: the function enables the [array arithmetic mode](arrays.md) in its arguments and nested expressions.<br/>Default: `false` | -| `vectorizationForbidden` | Boolean | `true`: the function will never get [vectorized](arrays.md#passing-arrays-to-scalar-functions-vectorization).<br/>Default: `false` | +| `enableArrayArithmeticForArguments` | Boolean | `true`: the function enables the [array arithmetic mode](/docs/guide/arrays) in its arguments and nested expressions.<br/>Default: `false` | +| `vectorizationForbidden` | Boolean | `true`: the function will never get [vectorized](/docs/guide/arrays#passing-arrays-to-scalar-functions-vectorization).<br/>Default: `false` | | `arraySizeMethod` | String | Deprecated; Use `sizeOfResultArrayMethod` instead. | | `arrayFunction` | Boolean | Deprecated; Use `enableArrayArithmeticForArguments` instead. | @@ -485,7 +488,7 @@ In a separate object, define the translations of your custom functions' names in every language you want to support. Function names are case-insensitive, as they are all normalized to uppercase. -::: tip +:::tip Even if you support just a single language, you still need to define a translation for it. ::: @@ -506,9 +509,9 @@ export const MyCustomPluginTranslations = { HyperFormula.registerFunctionPlugin(MyCustomPlugin, MyCustomPluginTranslations); ``` -::: tip +:::tip Before using a translated function name, remember to -[register and set the language](localizing-functions.md). +[register and set the language](/docs/guide/localizing-functions). ::: ## Function aliases @@ -525,7 +528,7 @@ MyCustomPlugin.aliases = { }; ``` -::: tip +:::tip For each alias of your function, define a translation, even if you want to support only one language. diff --git a/docs/src/content/docs/guide/date-and-time-handling.md b/docs/src/content/docs/guide/date-and-time-handling.md new file mode 100644 index 0000000000..a15a477444 --- /dev/null +++ b/docs/src/content/docs/guide/date-and-time-handling.md @@ -0,0 +1,431 @@ +--- +title: "Date and time handling" +--- + + +The formats for the default date and time parsing functions can be set using configuration options: +- [`dateFormats`](/docs/api/interfaces/configparams#dateformats), +- [`timeFormats`](/docs/api/interfaces/configparams#timeformats), +- [`nullYear`](/docs/api/interfaces/configparams#nullyear). + +The API reference of [`dateFormats`](/docs/api/interfaces/configparams#dateformats) and [`timeFormats`](/docs/api/interfaces/configparams#timeformats) describes the supported date and time formats in detail. + +## Example + +By default, HyperFormula uses the European date and time formats. + +```javascript +dateFormats: ['DD/MM/YYYY', 'DD/MM/YY'], // set by default +timeFormats: ['hh:mm', 'hh:mm:ss.sss'], // set by default +``` + +To use the US date and time formats, set: + +```javascript +dateFormats: ['MM/DD/YYYY', 'MM/DD/YY', 'YYYY/MM/DD'], // US date formats +timeFormats: ['hh:mm', 'hh:mm:ss.sss'], // set by default +``` + +## Custom date and time handling + +If date and time formats supported by the [`dateFormats`](/docs/api/interfaces/configparams#dateformats) and [`timeFormats`](/docs/api/interfaces/configparams#timeformats) parameters are not enough, you can extend them by providing the following options: + +- [`parseDateTime`](/docs/api/interfaces/configparams#parsedatetime), which allows to provide a function that accepts +a string representing date/time and parses it into an actual date/time format +- [`stringifyDateTime`](/docs/api/interfaces/configparams#stringifydatetime), which allows to provide a function that +takes the date/time and prints it as a string +- [`stringifyDuration`](/docs/api/interfaces/configparams#stringifyduration), which allows to provide a function that +takes time duration and prints it as a string + +To extend the number of possible date formats, you will need to +configure [`parseDateTime`](/docs/api/interfaces/configparams#parsedatetime) . This functionality is based on callbacks, +and you can customize the formats by integrating a third-party +library like [Moment.js](https://momentjs.com/), or by writing your +own custom function that returns a [`DateTime`](/docs/api/globals#datetime) object. + +The configuration of date formats and stringify options may impact some built-in functions. +For instance, the `VALUE` function transforms strings +into numbers, which means it uses [`parseDateTime`](/docs/api/interfaces/configparams#parsedatetime). The `TEXT` function +works the other way round - it accepts a number and returns a string, +so it uses `stringifyDateTime`. Any change here might give you +different results. Criteria-based functions (`SUMIF`, `AVERAGEIF`, etc.) perform comparisons, so they also need to +work on strings, dates, etc. + +## Moment.js integration + +In this example, you will add the possibility to parse dates in the +`"Do MMM YY"` custom format. + +To do so, you first need to write a function using +[Moment.js API](https://momentjs.com/docs/): + +```javascript +import moment from "moment"; + +// write a custom function for parsing dates +export const customParseDate = (dateString, dateFormat) => { + const momentDate = moment(dateString, dateFormat, true); + // check validity of a date with moment.js method + if (momentDate.isValid()) { + return { + year: momentDate.year(), + month: momentDate.month() + 1, + day: momentDate.date() + }; + } + // if the string was not recognized as + // a valid date return nothing + return undefined; +}; +``` + +Then, use it inside the +[configuration options](/docs/guide/configuration-options) like so: + +```javascript +const options = { + parseDateTime: customParseDate, + // you can add more formats + dateFormats: ["Do MMM YY"] +}; +``` + +After that, you should be able to add a dataset with dates in +your custom format: + +```javascript +const data = [["31st Jan 00", "2nd Jun 01", "=B1-A1"]]; +``` + +And now, HyperFormula recognizes these values as valid dates and can operate on them. + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/date-time/example1.js"> +<div class="example"> + <button id="run" class="button run"> + Run calculations + </button> + <button id="reset" class="button button-outline reset"> + Reset + </button> + <table> + <colgroup> + <col style="width:20%" /> + <col style="width:20%" /> + <col style="width:30%" /> + </colgroup> + <thead> + <tr> + <th>Release 1.0.0</th> + <th>Release 4.3.1</th> + <th>Number of days between</th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/** + * Function defining the way HF should handle the provided date string. + * + * @param {string} dateString The date string. + * @param {string} dateFormat The date format. + * @returns {{month: *, year: *, day: *}} Object with date-related information. + */ +const customParseDate = (dateString, dateFormat) => { + const momentDate = moment(dateString, dateFormat, true); + + if (momentDate.isValid()) { + return { + year: momentDate.year(), + month: momentDate.month() + 1, + day: momentDate.date(), + }; + } +}; + +/** + * Date formatting function. + * + * @param {{month: *, year: *, day: *}} dateObject Object with date-related information. + * @returns {string} Formatted date string. + */ +const getFormattedDate = (dateObject) => { + dateObject.month -= 1; + + return moment(dateObject).format('MMM D YY'); +}; + +/** + * Initial table data. + */ +const tableData = [['Jan 31 00', 'Jun 2 01', '=B1-A1']]; +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + parseDateTime: customParseDate, + dateFormats: ['MMM D YY'], + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); + +/** + * Fill the HTML table with data. + * + * @param {boolean} calculated `true` if it should render calculated values, `false` otherwise. + */ +function renderTable(calculated = false) { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = calculated || !cellHasFormula; + const cellValue = displayValue(cellAddress, showFormula); + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span> + ${cellValue} + </span></td>`; + } + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Force the table to display either the formula, the value or a raw source data value. + * + * @param {SimpleCellAddress} cellAddress Cell address. + * @param {boolean} showFormula `true` if the formula should be visible. + */ +function displayValue(cellAddress, showFormula) { + // Declare which columns should display the raw source data, instead of the data from HyperFormula. + const sourceColumns = [0, 1]; + let cellValue = ''; + + if (sourceColumns.includes(cellAddress.col)) { + cellValue = getFormattedDate(hf.numberToDate(hf.getCellValue(cellAddress))); + } else { + if (!hf.isCellEmpty(cellAddress) && showFormula) { + cellValue = hf.getCellValue(cellAddress); + } else { + cellValue = hf.getCellFormula(cellAddress); + } + } + + return cellValue; +} + +/** + * Replace formulas with their results. + */ +function runCalculations() { + renderTable(true); +} + +/** + * Replace the values in the table with initial data. + */ +function resetTable() { + renderTable(); +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const runButton = document.querySelector('.example #run'); + const resetButton = document.querySelector('.example #reset'); + + runButton.addEventListener('click', () => { + runCalculations(); + }); + resetButton.addEventListener('click', () => { + resetTable(); + }); +} + +const ANIMATION_ENABLED = true; + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/src/content/docs/guide/demo.md b/docs/src/content/docs/guide/demo.md new file mode 100644 index 0000000000..2efedbf0b2 --- /dev/null +++ b/docs/src/content/docs/guide/demo.md @@ -0,0 +1,322 @@ +--- +title: "Demo" +--- + + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/demo/example1.js"> +<div class="example"> + <button id="run" class="run"> + Run calculations + </button> + <button id="reset" class="outline reset"> + Reset + </button> + <table> + <colgroup> + <col style="width:22%" /> + <col style="width:15%" /> + <col style="width:23%" /> + <col style="width:20%" /> + <col style="width:20%" /> + </colgroup> + <thead> + <tr> + <th>Name</th> + <th>Year_1</th> + <th>Year_2</th> + <th>Average</th> + <th>Sum</th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +const tableData = [ + ['Greg Black', 4.66, '=B1*1.3', '=AVERAGE(B1:C1)', '=SUM(B1:C1)'], + ['Anne Carpenter', 5.25, '=$B$2*30%', '=AVERAGE(B2:C2)', '=SUM(B2:C2)'], + ['Natalie Dem', 3.59, '=B3*2.7+2+1', '=AVERAGE(B3:C3)', '=SUM(B3:C3)'], + ['John Sieg', 12.51, '=B4*(1.22+1)', '=AVERAGE(B4:C4)', '=SUM(B4:C4)'], + [ + 'Chris Aklips', + 7.63, + '=B5*1.1*SUM(10,20)+1', + '=AVERAGE(B5:C5)', + '=SUM(B5:C5)', + ], +]; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); +// Add named expressions for the "TOTAL" row. +hf.addNamedExpression('Year_1', '=SUM(main!$B$1:main!$B$5)'); +hf.addNamedExpression('Year_2', '=SUM(main!$C$1:main!$C$5)'); + +// Bind the events to the buttons. +function bindEvents() { + const runButton = document.querySelector('.example #run'); + const resetButton = document.querySelector('.example #reset'); + + runButton.addEventListener('click', () => { + runCalculations(); + }); + resetButton.addEventListener('click', () => { + resetTable(); + }); +} + +const ANIMATION_ENABLED = true; + +/** + * Fill the HTML table with data. + * + * @param {boolean} calculated `true` if it should render calculated values, `false` otherwise. + */ +function renderTable(calculated = false) { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const totals = ['=SUM(Year_1)', '=SUM(Year_2)']; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + let totalRowsHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = calculated || !cellHasFormula; + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress) && showFormula) { + cellValue = hf.getCellValue(cellAddress); + + if (!isNaN(cellValue)) { + cellValue = cellValue.toFixed(2); + } + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + totalRowsHTML = `<tr class="summary"> +<td>TOTAL</td> +<td class="${updatedCellClass}"> + <span>${calculated ? hf.calculateFormula(totals[0], sheetId).toFixed(2) : totals[0]}</span> +</td> +<td class="${updatedCellClass}"> + <span>${calculated ? hf.calculateFormula(totals[1], sheetId).toFixed(2) : totals[1]}</span> +</td> +<td colspan="2"></td> +</tr>`; + newTbodyHTML += totalRowsHTML; + tbodyDOM.innerHTML = newTbodyHTML; +} + +// Replace formulas with their results. +function runCalculations() { + renderTable(true); +} + +// Replace the values in the table with initial data. +function resetTable() { + renderTable(); +} + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> + +In this demo, you can see how HyperFormula handles basic operations by using API methods, such as: + +* `buildEmpty` static method to initialize the instance +* `addSheet` method to add a new sheet +* `setCellContents` method to add content +* `getSheetId` method to retrieve the sheet's ID +* `getCellValue` method to get the value of a cell +* `calculateFormula` method to calculate a formula +* `getCellFormula` method to retrieve a formula from a cell diff --git a/docs/guide/dependencies.md b/docs/src/content/docs/guide/dependencies.md similarity index 97% rename from docs/guide/dependencies.md rename to docs/src/content/docs/guide/dependencies.md index cdc9b3eab2..481eecae27 100644 --- a/docs/guide/dependencies.md +++ b/docs/src/content/docs/guide/dependencies.md @@ -1,4 +1,7 @@ -# Dependencies +--- +title: "Dependencies" +--- + HyperFormula depends on a few external libraries, as listed below. These dependencies are used to support some of the library's core diff --git a/docs/guide/dependency-graph.md b/docs/src/content/docs/guide/dependency-graph.md similarity index 93% rename from docs/guide/dependency-graph.md rename to docs/src/content/docs/guide/dependency-graph.md index 40e64530b7..f411a377a6 100644 --- a/docs/guide/dependency-graph.md +++ b/docs/src/content/docs/guide/dependency-graph.md @@ -1,4 +1,7 @@ -# Dependency graph +--- +title: "Dependency graph" +--- + For accuracy and performance, HyperFormula needs to process cells in a correct and optimal order. For example: in formula `C1=A1+B1`, cells `A1` and `B1` need to be evaluated before `C1`. @@ -17,7 +20,7 @@ The dependency graph may also contain ranges that are not used by any formula, f Range nodes can be connected to cell nodes and to other range nodes. -<img :src="$withBase('/ranges.png')"> +<img src="/docs/ranges.png"> ### Optimizations for large ranges @@ -63,7 +66,7 @@ range which is one row shorter. In this example, it would be `B5:D19`. If so, then it represents `B5:D20` as the composition of a range `B5:D19` and three cells in the last row: `B20`,`C20` and `D20`. -<img :src="$withBase('/ranges.png')"> +<img src="/docs/ranges.png"> More generally, the result of any associative operation is obtained as the result of operations for these small rows. There are many @@ -73,7 +76,7 @@ node and avoid duplicating the work during computation. ## Getting the immediate precedents of a cell or a range -To get the immediate precedents of a cell or a range (the in-neighbors of the cell node or the range node), use the [`getCellPrecedents()`](../api/classes/hyperformula.html#getcellprecedents) method: +To get the immediate precedents of a cell or a range (the in-neighbors of the cell node or the range node), use the [`getCellPrecedents()`](/docs/api/classes/hyperformula#getcellprecedents) method: ```js const hfInstance = HyperFormula.buildFromArray([[ '1', '2', '=A1', '=B1+C1' ]]); @@ -84,7 +87,7 @@ hfInstance.getCellPrecedents({ sheet: 0, col: 3, row: 0 }); ## Getting the immediate dependents of a cell or a range -To get the immediate dependents of a cell or a range (the out-neighbors of the cell node or the range node), use the [`getCellDependents()`](../api/classes/hyperformula.html#getcelldependents) method: +To get the immediate dependents of a cell or a range (the out-neighbors of the cell node or the range node), use the [`getCellDependents()`](/docs/api/classes/hyperformula#getcelldependents) method: ```js const hfInstance = HyperFormula.buildFromArray([[ '1', '=A1', '=A1+B1', '=B1+C1' ]]) @@ -95,7 +98,7 @@ hfInstance.getCellDependents({ sheet: 0, col: 0, row: 0 }) ## Getting all precedents of a cell or a range -To get all precedents of a cell or a range (all precedent nodes reachable from the cell node or the range node), use the [`getCellPrecedents()`](../api/classes/hyperformula.html#getcellprecedents) method to implement a [Breadth-first search (BFS)](https://en.wikipedia.org/wiki/Breadth-first_search) algorithm: +To get all precedents of a cell or a range (all precedent nodes reachable from the cell node or the range node), use the [`getCellPrecedents()`](/docs/api/classes/hyperformula#getcellprecedents) method to implement a [Breadth-first search (BFS)](https://en.wikipedia.org/wiki/Breadth-first_search) algorithm: <div class="language- extra-class"> <pre class="language-text" style="margin: 0; padding: 0;"> @@ -116,7 +119,7 @@ To get all precedents of a cell or a range (all precedent nodes reachable from t ## Getting all dependents of a cell or a range -To get all dependents of a cell or a range (all dependent nodes reachable from the cell node or the range node), use the [`getCellDependents()`](../api/classes/hyperformula.html#getcelldependents) method to implement a [Breadth-first search (BFS)](https://en.wikipedia.org/wiki/Breadth-first_search) algorithm: +To get all dependents of a cell or a range (all dependent nodes reachable from the cell node or the range node), use the [`getCellDependents()`](/docs/api/classes/hyperformula#getcelldependents) method to implement a [Breadth-first search (BFS)](https://en.wikipedia.org/wiki/Breadth-first_search) algorithm: <div class="language- extra-class"> <pre class="language-text" style="margin: 0; padding: 0;"> diff --git a/docs/guide/file-import.md b/docs/src/content/docs/guide/file-import.md similarity index 96% rename from docs/guide/file-import.md rename to docs/src/content/docs/guide/file-import.md index 1c9d04d278..7b37fe83be 100644 --- a/docs/guide/file-import.md +++ b/docs/src/content/docs/guide/file-import.md @@ -1,10 +1,13 @@ -# File import +--- +title: "File import" +--- + Import XLSX and CSV files into HyperFormula. ## Overview -HyperFormula has no built-in file import functionality. But its [factory methods](../api/classes/hyperformula.md#factories) use standard JavaScript data types, for easy integration with any way of importing data. +HyperFormula has no built-in file import functionality. But its [factory methods](/docs/api/classes/hyperformula#factories) use standard JavaScript data types, for easy integration with any way of importing data. ## Import CSV files diff --git a/docs/src/content/docs/guide/i18n-features.md b/docs/src/content/docs/guide/i18n-features.md new file mode 100644 index 0000000000..10bf774353 --- /dev/null +++ b/docs/src/content/docs/guide/i18n-features.md @@ -0,0 +1,502 @@ +--- +title: "Internationalization features" +--- + + +Configure HyperFormula to match the languages and regions of your users. + +**Contents:** + + +## Function names and errors + +Each of HyperFormula's [built-in functions](/docs/guide/built-in-functions) and [errors](/docs/guide/types-of-errors) is available in [18 languages](/docs/guide/localizing-functions#list-of-supported-languages). + +You can easily [switch between languages](/docs/guide/localizing-functions) ([`language`](/docs/api/interfaces/configparams#language)). + +When adding a [custom function](/docs/guide/custom-functions), you can define the function's [name](/docs/guide/custom-functions#3-add-your-function-s-names) in every language that you support. + +To support more languages, add a [custom language pack](/docs/guide/localizing-functions). + +## Date and time formats + +To match a region's calendar conventions, you can set multiple date formats ([`dateFormats`](/docs/api/interfaces/configparams#dateformats)) and time formats ([`timeFormats`](/docs/api/interfaces/configparams#timeformats)). + +By default, HyperFormula uses the European date and time formats. [You can easily change them](/docs/guide/date-and-time-handling#example). + +You can also add custom ways of [handling dates and times](/docs/guide/date-and-time-handling#custom-date-and-time-handling). + +## Number format + +To match a region's number format, configure HyperFormula's decimal separator ([`decimalSeparator`](/docs/api/interfaces/configparams#decimalseparator)) and thousands separator ([`thousandSeparator`](/docs/api/interfaces/configparams#thousandseparator)). + +By default, HyperFormula uses the European number format (`1000000.00`): + +```js +decimalSeparator: '.', // set by default +thousandSeparator: '', // set by default +``` + +To use the US number format (`1,000,000.00`), set: + +```js +decimalSeparator: '.', // set by default +thousandSeparator: ',', +``` + +:::tip + In HyperFormula, both [`decimalSeparator`](/docs/api/interfaces/configparams#decimalseparator) and [`thousandSeparator`](/docs/api/interfaces/configparams#thousandseparator) must be different from [`functionArgSeparator`](/docs/api/interfaces/configparams#functionargseparator). + In some cases it might cause compatibility issues with other spreadsheets, e.g., [Microsoft Excel](/docs/guide/compatibility-with-microsoft-excel#separators) or [Google Sheets](/docs/guide/compatibility-with-google-sheets#separators). +::: + +## Currency symbol + +To match your users' currency, you can configure multiple currency symbols ([`currencySymbol`](/docs/api/interfaces/configparams#currencysymbol)). + +The default currency symbol is `$`. To add `USD` as an alternative, set: + +```js +currencySymbol: ['$', 'USD'], +``` + +## String comparison rules + +To make sure that language-sensitive strings are compared in line with your users' language (e.g., `Préservation` vs. `Preservation`), set HyperFormula's [string comparison rules](/docs/guide/types-of-operators#comparing-strings) ([`localeLang`](/docs/api/interfaces/configparams#localelang)). + +The value of [`localeLang`](/docs/api/interfaces/configparams#localelang) is processed by [`Intl.Collator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator), a JavaScript standard object. + +The default setting is: + +```js +localeLang: 'en', // set by default +``` + +To set the `en-US` string comparison rules, set: + +```js +localeLang: 'en-US', +``` + +To further customize string comparison rules, use these options: +- [`caseSensitive`](/docs/api/interfaces/configparams#casesensitive) +- [`accentSensitive`](/docs/api/interfaces/configparams#accentsensitive) +- [`caseFirst`](/docs/api/interfaces/configparams#casefirst) +- [`ignorePunctuation`](/docs/api/interfaces/configparams#ignorepunctuation) + +## Compatibility with other spreadsheet software + +For information on compatibility with locale-dependent syntax in other spreadsheet software, see: +- [Compatibility with Microsoft Excel](/docs/guide/compatibility-with-microsoft-excel) +- [Compatibility with Google Sheets](/docs/guide/compatibility-with-google-sheets) + +## `en-US` configuration + +This configuration aligns HyperFormula with the `en-US` locale. Due to the configuration of [separators](#number-format), it might not be fully compatible with formulas coming from other spreadsheet software. + +```js +language: 'enUS', +dateFormats: ['MM/DD/YYYY', 'MM/DD/YY', 'YYYY/MM/DD'], +timeFormats: ['hh:mm', 'hh:mm:ss.sss'], // set by default +decimalSeparator: '.', // set by default +thousandSeparator: ',', +functionArgSeparator: ';', // might cause incompatibility with other spreadsheets +currencySymbol: ['$', 'USD'], +localeLang: 'en-US', +``` + +## `en-US` demo + +This demo shows HyperFormula configured for the `en-US` locale. + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/i18n/example1.js"> +<div class="example"> + <button id="run" class="button run"> + Run calculations + </button> + <button id="reset" class="button button-outline reset"> + Reset + </button> + <table> + <colgroup> + <col style="width:22%" /> + <col style="width:15%" /> + <col style="width:23%" /> + <col style="width:20%" /> + <col style="width:20%" /> + </colgroup> + <thead> + <tr> + <th>Name</th> + <th>Lunch time</th> + <th>Date of Birth</th> + <th>Age</th> + <th>Salary</th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/* start:skip-in-sandbox */ +const enUS = HyperFormula.languages.enUS; +/* end:skip-in-sandbox */ +/** + * Initial table data. + */ +const tableData = [ + [ + 'Greg Black', + '11:45 AM', + '05/23/1989', + '=YEAR(NOW())-YEAR(C1)', + '$80,000.00', + ], + [ + 'Anne Carpenter', + '12:30 PM', + '01/01/1980', + '=YEAR(NOW())-YEAR(C2)', + '$95,000.00', + ], + [ + 'Natalie Dem', + '1:30 PM', + '12/13/1973', + '=YEAR(NOW())-YEAR(C3)', + '$78,500.00', + ], + [ + 'John Sieg', + '2:00 PM', + '10/31/1995', + '=YEAR(NOW())-YEAR(C4)', + '$114,000.00', + ], + [ + 'Chris Aklips', + '11:30 AM', + '08/18/1987', + '=YEAR(NOW())-YEAR(C5)', + '$71,900.00', + ], + ['AVERAGE', null, null, '=AVERAGE(D1:D5)', '=AVERAGE(E1:E5)'], +]; + +const config = { + language: 'enUS', + dateFormats: ['MM/DD/YYYY', 'MM/DD/YY', 'YYYY/MM/DD'], + timeFormats: ['hh:mm', 'hh:mm:ss.sss'], + decimalSeparator: '.', + thousandSeparator: ',', + functionArgSeparator: ';', + currencySymbol: ['$', 'USD'], + localeLang: 'en-US', + licenseKey: 'gpl-v3', +}; + +if (!HyperFormula.getRegisteredLanguagesCodes().includes('enUS')) { + HyperFormula.registerLanguage('enUS', enUS); +} + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty(config); +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); + +const columnTypes = ['string', 'time', 'date', 'number', 'currency']; + +/** + * Display value in human-readable format + * + * @param {SimpleCellAddress} cellAddress Cell address. + */ +function formatCellValue(cellAddress) { + if (hf.isCellEmpty(cellAddress)) { + return ''; + } + + if (columnTypes[cellAddress.col] === 'time') { + return formatTime(hf.numberToTime(hf.getCellValue(cellAddress))); + } + + if (columnTypes[cellAddress.col] === 'date') { + return formatDate(hf.numberToDate(hf.getCellValue(cellAddress))); + } + + if (columnTypes[cellAddress.col] === 'currency') { + return formatCurrency(hf.getCellValue(cellAddress)); + } + + return hf.getCellValue(cellAddress); +} + +/** + * Date formatting function. + * + * @param {{month: *, year: *, day: *}} dateObject Object with date-related information. + */ +function formatDate(dateObject) { + dateObject.month -= 1; + + return moment(dateObject).format('MM/DD/YYYY'); +} + +/** + * Time formatting function. + * + * @param dateTimeObject Object with date and time information. + */ +function formatTime(dateTimeObject) { + return moment(dateTimeObject).format('h:mm A'); +} + +/** + * Currency formatting function. + * + * @param value Number representing the currency value + */ +function formatCurrency(value) { + return value.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }); +} + +/** + * Fill the HTML table with data. + * + * @param {boolean} calculated `true` if it should render calculated values, `false` otherwise. + */ +function renderTable(calculated = false) { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + newTbodyHTML += `<tr class="${row === height - 1 ? 'summary' : ''}">`; + + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = cellHasFormula && !calculated; + const displayValue = showFormula + ? hf.getCellFormula(cellAddress) + : formatCellValue(cellAddress); + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span>${displayValue}</span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Replace formulas with their results. + */ +function runCalculations() { + renderTable(true); +} + +/** + * Replace the values in the table with initial data. + */ +function resetTable() { + renderTable(); +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const runButton = document.querySelector('.example #run'); + const resetButton = document.querySelector('.example #reset'); + + runButton.addEventListener('click', () => { + runCalculations(); + }); + resetButton.addEventListener('click', () => { + resetTable(); + }); +} + +const ANIMATION_ENABLED = true; + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/guide/integration-with-angular.md b/docs/src/content/docs/guide/integration-with-angular.md similarity index 88% rename from docs/guide/integration-with-angular.md rename to docs/src/content/docs/guide/integration-with-angular.md index 491908f84e..45c0041e7e 100644 --- a/docs/guide/integration-with-angular.md +++ b/docs/src/content/docs/guide/integration-with-angular.md @@ -1,8 +1,11 @@ -# Integration with Angular +--- +title: "Integration with Angular" +--- + The HyperFormula API is identical in an Angular app and in plain JavaScript. This guide demonstrates how HyperFormula is integrated with an Angular app (typically as an injectable service), how it is cleaned up, and how you bridge its values into the change-detection cycle. -Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section. +Install with `npm install hyperformula`. For other options, see the [client-side installation](/docs/guide/client-side-installation) section. ## Basic usage @@ -117,10 +120,10 @@ The service above is already SSR-safe — HyperFormula has no browser-only API d ## Next steps -- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options -- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets -- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions -- [Custom functions](custom-functions.md) — register your own formulas +- [Configuration options](/docs/guide/configuration-options) — full list of `buildFromArray` / `buildEmpty` options +- [Basic operations](/docs/guide/basic-operations) — CRUD on cells, rows, columns, sheets +- [Advanced usage](/docs/guide/advanced-usage) — multi-sheet workbooks, named expressions +- [Custom functions](/docs/guide/custom-functions) — register your own formulas ## Demo diff --git a/docs/guide/integration-with-langchain.md b/docs/src/content/docs/guide/integration-with-langchain.md similarity index 93% rename from docs/guide/integration-with-langchain.md rename to docs/src/content/docs/guide/integration-with-langchain.md index 4bda40cba5..aeb20748c5 100644 --- a/docs/guide/integration-with-langchain.md +++ b/docs/src/content/docs/guide/integration-with-langchain.md @@ -1,8 +1,11 @@ -# Integration with LangChain/LangGraph +--- +title: "Integration with LangChain/LangGraph" +--- + A [LangChain.js](https://js.langchain.com/) / [LangGraph](https://langchain-ai.github.io/langgraphjs/) tool that gives your agents deterministic spreadsheet and formula computation — backed by HyperFormula's Excel-compatible engine. -::: warning Not available yet — coming soon +:::caution[Not available yet — coming soon] This integration is on our roadmap and **cannot be installed or used today**. The API shown below is a preview and may still change before the first release. If you'd like to try it, [join the early access list](https://2fmjvg.share-eu1.hsforms.com/2e6drCkuLTn-1RuiYB91eJA) — we'll ping you the moment the first beta is ready, and your sign-up directly tells us how strongly to prioritize this integration. @@ -55,7 +58,7 @@ A single import, one entry in `tools`, and the agent can evaluate formulas, read ## Get early access -::: tip Be the first to try it +:::tip[Be the first to try it] We're actively building this integration. Drop your email and we'll notify you the moment the first beta lands — so you can try it before the public release. [Join the early access list →](https://2fmjvg.share-eu1.hsforms.com/2e6drCkuLTn-1RuiYB91eJA) @@ -67,5 +70,5 @@ We're actively building this integration. Drop your email and we'll notify you t - [LangGraph documentation](https://langchain-ai.github.io/langgraphjs/) - [HyperFormula on GitHub](https://github.com/handsontable/hyperformula) - [HyperFormula on npm](https://www.npmjs.com/package/hyperformula) -- [Built-in functions](built-in-functions.md) -- [Custom functions](custom-functions.md) +- [Built-in functions](/docs/guide/built-in-functions) +- [Custom functions](/docs/guide/custom-functions) diff --git a/docs/guide/integration-with-react.md b/docs/src/content/docs/guide/integration-with-react.md similarity index 87% rename from docs/guide/integration-with-react.md rename to docs/src/content/docs/guide/integration-with-react.md index bdc7cd5677..2412feab23 100644 --- a/docs/guide/integration-with-react.md +++ b/docs/src/content/docs/guide/integration-with-react.md @@ -1,8 +1,11 @@ -# Integration with React +--- +title: "Integration with React" +--- + The HyperFormula API is identical in a React app and in plain JavaScript. This guide demonstrates how HyperFormula is integrated with the React component tree and how its lifecycle maps to React hooks. -Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section. +Install with `npm install hyperformula`. For other options, see the [client-side installation](/docs/guide/client-side-installation) section. ## Basic usage @@ -107,10 +110,10 @@ In the Pages Router, the same `dynamic(..., { ssr: false })` call works directly ## Next steps -- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options -- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets -- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions -- [Custom functions](custom-functions.md) — register your own formulas +- [Configuration options](/docs/guide/configuration-options) — full list of `buildFromArray` / `buildEmpty` options +- [Basic operations](/docs/guide/basic-operations) — CRUD on cells, rows, columns, sheets +- [Advanced usage](/docs/guide/advanced-usage) — multi-sheet workbooks, named expressions +- [Custom functions](/docs/guide/custom-functions) — register your own formulas ## Demo diff --git a/docs/guide/integration-with-svelte.md b/docs/src/content/docs/guide/integration-with-svelte.md similarity index 87% rename from docs/guide/integration-with-svelte.md rename to docs/src/content/docs/guide/integration-with-svelte.md index 007ddff3d9..8bdef27f03 100644 --- a/docs/guide/integration-with-svelte.md +++ b/docs/src/content/docs/guide/integration-with-svelte.md @@ -1,10 +1,13 @@ -# Integration with Svelte +--- +title: "Integration with Svelte" +--- + The HyperFormula API is identical in a Svelte app and in plain JavaScript. This guide demonstrates how HyperFormula integrates with the Svelte component's lifecycle and how you bridge its values into Svelte's reactivity. -Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section. +Install with `npm install hyperformula`. For other options, see the [client-side installation](/docs/guide/client-side-installation) section. -::: warning SvelteKit SSR +:::caution[SvelteKit SSR] The primary snippet below assumes a browser environment. If you use SvelteKit with default SSR, skip to [Server-side rendering](#server-side-rendering-sveltekit) — `HyperFormula.buildFromArray` at `<script>` top level will run on every server render, which is unnecessary work. ::: @@ -116,10 +119,10 @@ In SvelteKit, top-level statements in `<script>` run on the server too. HyperFor ## Next steps -- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options -- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets -- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions -- [Custom functions](custom-functions.md) — register your own formulas +- [Configuration options](/docs/guide/configuration-options) — full list of `buildFromArray` / `buildEmpty` options +- [Basic operations](/docs/guide/basic-operations) — CRUD on cells, rows, columns, sheets +- [Advanced usage](/docs/guide/advanced-usage) — multi-sheet workbooks, named expressions +- [Custom functions](/docs/guide/custom-functions) — register your own formulas ## Demo diff --git a/docs/guide/integration-with-vue.md b/docs/src/content/docs/guide/integration-with-vue.md similarity index 88% rename from docs/guide/integration-with-vue.md rename to docs/src/content/docs/guide/integration-with-vue.md index 087a84f61e..ef3cf5c572 100644 --- a/docs/guide/integration-with-vue.md +++ b/docs/src/content/docs/guide/integration-with-vue.md @@ -1,8 +1,11 @@ -# Integration with Vue +--- +title: "Integration with Vue" +--- + The HyperFormula API is identical in a Vue 3 app and in plain JavaScript. This guide demonstrates how HyperFormula integrates with the Vue reactivity system and how to surface its values in the template. -Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section. +Install with `npm install hyperformula`. For other options, see the [client-side installation](/docs/guide/client-side-installation) section. ## Basic usage @@ -106,15 +109,15 @@ const hfInstance = markRaw( ## Next steps -- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options -- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets -- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions -- [Custom functions](custom-functions.md) — register your own formulas +- [Configuration options](/docs/guide/configuration-options) — full list of `buildFromArray` / `buildEmpty` options +- [Basic operations](/docs/guide/basic-operations) — CRUD on cells, rows, columns, sheets +- [Advanced usage](/docs/guide/advanced-usage) — multi-sheet workbooks, named expressions +- [Custom functions](/docs/guide/custom-functions) — register your own formulas ## Demo For a more advanced example, check out the <a :href="'https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.3.x/vue-3-demo?v=' + $page.buildDateURIEncoded">Vue 3 demo on Stackblitz</a>. -::: tip +:::tip This demo uses the [Vue 3](https://v3.vuejs.org/) framework. If you are looking for an example using Vue 2, check out the [code on GitHub](https://github.com/handsontable/hyperformula-demos/tree/2.5.x/vue-demo). ::: diff --git a/docs/guide/key-concepts.md b/docs/src/content/docs/guide/key-concepts.md similarity index 92% rename from docs/guide/key-concepts.md rename to docs/src/content/docs/guide/key-concepts.md index b0183565ea..de2865e7ef 100644 --- a/docs/guide/key-concepts.md +++ b/docs/src/content/docs/guide/key-concepts.md @@ -1,8 +1,11 @@ -# Key concepts +--- +title: "Key concepts" +--- + ## High-level design diagram -<img :src="$withBase('/hf-high-lvl-diagram.svg')"> +<img src="/docs/hf-high-lvl-diagram.svg"> Data processing consists of three phases. @@ -14,7 +17,7 @@ so-called (AST). For example, the AST for `7*3-SIN(A5)` will look similar to this graph: -<img :src="$withBase('/ast.png')"> +<img src="/docs/ast.png"> ## Phase 2. Construction of the dependency graph @@ -27,9 +30,9 @@ exists if and only if there is no cycle in the dependency graph. There can be many such orders, like so: -<img :src="$withBase('/topsort.png')"> +<img src="/docs/topsort.png"> -Read more about the [dependency graph](/guide/dependency-graph.md). +Read more about the [dependency graph](/docs/guide/dependency-graph). ## Phase 3. Evaluation @@ -37,7 +40,7 @@ It is crucial to evaluate cells efficiently. For simple expressions, there is not much room for maneuver, but spreadsheet-like data sets definitely need more attention. -<img :src="$withBase('/sample-sheet.png')"> +<img src="/docs/sample-sheet.png"> ## Grammar @@ -97,7 +100,7 @@ cells, references inside formulas may need to be changed. For example, after adding a row, we need to shift all references in the formulas below like so: -<img :src="$withBase('/crud-operations.png')"> +<img src="/docs/crud-operations.png"> In more complex sheets this can lead to similar transformations in many formulas at once. On the other hand, such operations do not diff --git a/docs/guide/known-limitations.md b/docs/src/content/docs/guide/known-limitations.md similarity index 95% rename from docs/guide/known-limitations.md rename to docs/src/content/docs/guide/known-limitations.md index 3da1d2eece..0b38c22e90 100644 --- a/docs/guide/known-limitations.md +++ b/docs/src/content/docs/guide/known-limitations.md @@ -1,4 +1,7 @@ -# Known limitations +--- +title: "Known limitations" +--- + This page lists the known limitations of HyperFormula at its current development stage: @@ -17,7 +20,7 @@ a circular reference. * There is no data validation against named ranges. For example, you can't compare the arguments in a formula like this: =IF(firstRange>secondRange, TRUE, FALSE). -* [Custom functions](custom-functions.md) don't automatically recalculate the size of their [result arrays](custom-functions.md#return-an-array-of-data) when the formula dependencies change. +* [Custom functions](/docs/guide/custom-functions) don't automatically recalculate the size of their [result arrays](/docs/guide/custom-functions#return-an-array-of-data) when the formula dependencies change. * There is no relative referencing in named ranges. * The library doesn't offer (at least not yet) the following features: * 3D references diff --git a/docs/guide/license-key.md b/docs/src/content/docs/guide/license-key.md similarity index 58% rename from docs/guide/license-key.md rename to docs/src/content/docs/guide/license-key.md index 35ed847120..5f4cf90078 100644 --- a/docs/guide/license-key.md +++ b/docs/src/content/docs/guide/license-key.md @@ -1,10 +1,13 @@ -# License key +--- +title: "License key" +--- -To use HyperFormula, you need to specify which [license type](licensing.md#available-licenses) you use, by entering a license key in your [configuration options](configuration-options.md). + +To use HyperFormula, you need to specify which [license type](/docs/guide/licensing#available-licenses) you use, by entering a license key in your [configuration options](/docs/guide/configuration-options). ## GPLv3 license -If you use HyperFormula under [GNU General Public License v3.0](https://github.com/handsontable/hyperformula/blob/master/LICENSE.txt) (GPLv3), in your [configuration options](configuration-options.md), assign the mandatory `licenseKey` property to a string, `gpl-v3`: +If you use HyperFormula under [GNU General Public License v3.0](https://github.com/handsontable/hyperformula/blob/master/LICENSE.txt) (GPLv3), in your [configuration options](/docs/guide/configuration-options), assign the mandatory `licenseKey` property to a string, `gpl-v3`: ```js const options = { @@ -15,11 +18,11 @@ const options = { ## Proprietary license -To use HyperFormula under a [proprietary license](licensing.md#proprietary-license), follow these steps: +To use HyperFormula under a [proprietary license](/docs/guide/licensing#proprietary-license), follow these steps: -1. Contact our [Sales Team](licensing.md#proprietary-license) to purchase a proprietary license. +1. Contact our [Sales Team](/docs/guide/licensing#proprietary-license) to purchase a proprietary license. 2. Our Sales Team sends you your proprietary license key. -3. In your [configuration options](configuration-options.md), assign the mandatory `licenseKey` property to your proprietary license key: +3. In your [configuration options](/docs/guide/configuration-options), assign the mandatory `licenseKey` property to your proprietary license key: ```js const options = { @@ -31,7 +34,7 @@ const options = { ### Proprietary license key validation -::: tip +:::tip HyperFormula doesn't use an internet connection to validate your proprietary license key. ::: @@ -50,4 +53,4 @@ corresponding notification in the console. ## License key support -If you have any issues with your license key, [contact our team](contact.md). \ No newline at end of file +If you have any issues with your license key, [contact our team](/docs/guide/contact). \ No newline at end of file diff --git a/docs/guide/licensing.md b/docs/src/content/docs/guide/licensing.md similarity index 85% rename from docs/guide/licensing.md rename to docs/src/content/docs/guide/licensing.md index 00bf8ffcdd..95c9c9e8f7 100644 --- a/docs/guide/licensing.md +++ b/docs/src/content/docs/guide/licensing.md @@ -1,4 +1,7 @@ -# Licensing +--- +title: "Licensing" +--- + To make HyperFormula a better fit for different types of projects, the source code is available under different licenses. @@ -16,7 +19,7 @@ HyperFormula is available under the following licenses: In a non-commercial or open-source project, you can use [GNU General Public License v3.0](https://github.com/handsontable/hyperformula/blob/master/LICENSE.txt) (GPLv3). -To learn how to use the GPLv3 license, see the [License key](license-key.md#gplv3-license) page. +To learn how to use the GPLv3 license, see the [License key](/docs/guide/license-key#gplv3-license) page. ## Proprietary license @@ -31,4 +34,4 @@ Contact our Sales Team: To use HyperFormula, you need to specify which license type you use, by entering a license key. -To find out how to enter a license key, see the [License key](license-key.md) page. \ No newline at end of file +To find out how to enter a license key, see the [License key](/docs/guide/license-key) page. \ No newline at end of file diff --git a/docs/guide/list-of-differences.md b/docs/src/content/docs/guide/list-of-differences.md similarity index 95% rename from docs/guide/list-of-differences.md rename to docs/src/content/docs/guide/list-of-differences.md index 2ba4e9479a..fafceec457 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/src/content/docs/guide/list-of-differences.md @@ -1,4 +1,7 @@ -# List of differences with other spreadsheets +--- +title: "List of differences with other spreadsheets" +--- + <!-- The below dummy div uses a CSS class to alter the .page layout for the current page without any additional customization of VuePress. @@ -22,7 +25,7 @@ It makes the page wider to accommodate large tables See a full list of differences between HyperFormula, Microsoft Excel, and Google Sheets. **Contents:** -[[toc]] + ## General functionalities @@ -30,13 +33,13 @@ See a full list of differences between HyperFormula, Microsoft Excel, and Google |----------------------------------------------------|---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| | Dependency collection | A1:=IF(FALSE(), A1, 0)<br><br>ISREF(A1) | Dependencies are collected during the parsing phase, which finds cycles that wouldn't appear in the evaluation.<br><br>`CYCLE` error for both examples. | Dependencies are collected during evaluation.<br><br>`0` for both examples. | Same as Google Sheets. | | Named expressions and named ranges | SALARY:=$A$10 COST:=10*$B$5+100<br>PROFIT:=SALARY-COST<br>A1:=SALARY-COST | Only absolute addresses are allowed<br>(e.g., SALARY:= $A$10).<br><br>Named expressions can be global or scoped to one sheet only.<br><br>They can contain other named expressions. | Named expressions are not available.<br><br>Named ranges can be used to create aliases for addresses and ranges. | Named ranges and scoped named expressions are available. | -| Named expression names | ProductPrice1:=42 | A name must be distinctive from a cell reference (case-insensitive), so `ProductPrice1` is invalid. See [complete naming rules](named-expressions.md#name-rules). | A name that is a valid cell reference is allowed if the column address is at least 4-letter long, so `ProductPrice1` is valid. | A name that is a valid cell reference is allowed if the column address is at least 4-letter long, so `ProductPrice1` is valid. | +| Named expression names | ProductPrice1:=42 | A name must be distinctive from a cell reference (case-insensitive), so `ProductPrice1` is invalid. See [complete naming rules](/docs/guide/named-expressions#name-rules). | A name that is a valid cell reference is allowed if the column address is at least 4-letter long, so `ProductPrice1` is valid. | A name that is a valid cell reference is allowed if the column address is at least 4-letter long, so `ProductPrice1` is valid. | | Applying a scalar value to a function taking range | COLUMNS(A1) | `CellRangeExpected` error. | Treats the element as length-1 range. Returns 1 for the example. | Same as Google Sheets. | | Coercion of explicit arguments | VARP(2, 3, 4, TRUE(), FALSE(), "1",) | 1.9592, based on the behavior of Microsoft Excel. | GoogleSheets implementation is not consistent with the standard (see also `VAR.S`, `STDEV.P`, and `STDEV.S` function.) | 1.9592 | | Ranges created with `:` | A1:A2<br><br>A$1:$A$2<br><br>A:C<br><br>1:2<br><br>Sheet1!A1:A2 | Allowed ranges consist of two addresses (A1:B5), columns (A:C) or rows (3:5).<br>They cannot be mixed or contain named expressions. | Everything allowed. | Same as Google Sheets. | | Formatting inside the TEXT function | TEXT(A1,"dd-mm-yy")<br><br>TEXT(A1,"###.###”) | Not all formatting options are supported,<br>e.g., only some date formatting options: (`hh`, `mm`, `ss`, `am`, `pm`, `a`, `p`, `dd`, `yy`, and `yyyy`).<br><br>No currency formatting inside the TEXT function. | A wide variety of options for string formatting is supported. | Same as Google Sheets. | | Cell references inside inline arrays | ={A1, A2} | The array's value is calculated but not updated when the cells' values change. | The array's value is calculated and updated when the cells' values change. | ERROR: invalid array | -| SPLIT function | =SPLIT("Lorem ipsum dolor", 0) | This function works differently from Google Sheets version but should be sufficient to achieve the same functionality in most scenarios. Read SPLIT function description on [the Built-in Functions page](built-in-functions.md#text). | Different syntax and return value. | No such function. | +| SPLIT function | =SPLIT("Lorem ipsum dolor", 0) | This function works differently from Google Sheets version but should be sufficient to achieve the same functionality in most scenarios. Read SPLIT function description on [the Built-in Functions page](/docs/guide/built-in-functions#text). | Different syntax and return value. | No such function. | | DATEVALUE function | =DATEVALUE("25/02/1991") | Type of the returned value: `CellValueDetailedType.NUMBER_DATE` (compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard) | Cell auto-formatted as **regular number** | Cell auto-formatted as **regular number** | | TIMEVALUE function | =TIMEVALUE("14:31") | Type of the returned value: `CellValueDetailedType.NUMBER_TIME` (compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard) | Cell auto-formatted as **regular number** | Cell auto-formatted as **regular number** | | EDATE function | =EDATE(DATE(2019, 7, 31), 1) | Type of the returned value: `CellValueDetailedType.NUMBER_DATE`. This is non-compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard, which defines the return type as a Number, while describing it as a Date serial number through the function summary. | Cell auto-formatted as **date** | Cell auto-formatted as **regular number** | @@ -46,7 +49,7 @@ See a full list of differences between HyperFormula, Microsoft Excel, and Google Some built-in functions are implemented differently than in Google Sheets or Microsoft Excel. -To remove the differences, create [custom implementations](custom-functions.md) of those functions. +To remove the differences, create [custom implementations](/docs/guide/custom-functions) of those functions. | Function | Example | HyperFormula | Google Sheets | Microsoft Excel | |---------------|----------------------------------------------------------------|-------------:|--------------:|----------------:| diff --git a/docs/src/content/docs/guide/localizing-functions.md b/docs/src/content/docs/guide/localizing-functions.md new file mode 100644 index 0000000000..5ac936f4f3 --- /dev/null +++ b/docs/src/content/docs/guide/localizing-functions.md @@ -0,0 +1,462 @@ +--- +title: "Localizing functions" +--- + + +You can localize a function's ID and error +messages. Currently, HyperFormula supports 18 languages, with British English +as the default. + +To change the language all you need to do is import and +register the language like so: + +```javascript +// import the French language pack +import frFR from 'hyperformula/i18n/languages/frFR'; + +// register the language +HyperFormula.registerLanguage('frFR', frFR); +``` + +:::tip +To import the language packs, use the module-system-specific dedicated bundles at: +* **ES**: `hyperformula/i18n/languages/` +* **CommonJS**: `hyperformula/i18n/languages/` +* **UMD**: `hyperformula/dist/languages/` + +For the UMD build, the languages are accessible through `HyperFormula.languages`, e.g., `HyperFormula.languages.frFR`. +::: + +Then set it inside it the [configuration options](/docs/guide/configuration-options): + +```javascript +// configure the instance +const options = { + language: 'frFR' +}; +``` + +Language pack names should be passed as strings. They follow a +naming convention that incorporates two standards: ISO-639 and +ISO-3166-1. The pattern is `languageCOUNTRY`, for +example `enUS`, `enGB`, `frFR`, etc. + +You can freely use the localized names: `SUM` can be written as +`SOMME` and the functionality of the function will remain the same. + +Here are some example functions and their translations in French: + +```javascript +// localized functions +functions: { + MATCH: 'EQUIV', + CORREL: 'COEFFICIENT.CORRELATION', + AVERAGE: 'MOYENNE' +}, +``` + +Same goes for the [errors](/docs/guide/types-of-errors) displayed inside +cells when something goes wrong: + +```javascript +// localized errors +errors: { + CYCLE: '#CYCLE!', + DIV_BY_ZERO: '#DIV/0!', + ERROR: '#ERROR!', + NA: '#N/A', + NAME: '#NOM?', + NUM: '#NOMBRE!', + REF: '#REF!', + VALUE: '#VALEUR!', +} +``` + +## Creating a custom language pack + +If your desired language is not in the list of supported languages, you can create a custom language pack: + +```javascript +// Create a language pack object +const spanish = { + errors: { + NAME: '#¿NOMBRE?', + // ... + }, + functions: { + SUM: 'SUMA', + IF: 'SI', + // ... + }, + langCode: 'es', // Your custom language code + ui: { + NEW_SHEET_PREFIX: 'Sheet', + }, +}; + +// Register your language +HyperFormula.registerLanguage('es', spanish); + +// Use it in your configuration +const hf = HyperFormula.buildEmpty({ + language: 'es' +}); +``` + +:::tip +You can use an existing language pack as a template. Check the [language files in the repository](https://github.com/handsontable/hyperformula/tree/master/src/i18n/languages) to see complete examples with all available functions. +::: + +## Localizing custom functions + +You can localize your custom functions as well. For details, see the [Custom functions](/docs/guide/custom-functions#function-name-translations) guide. + +### List of supported languages +| Language name | Language code | +|:-----------------|:--------------| +| British English | enGB | +| American English | enUS | +| Czech | csCZ | +| Danish | daDK | +| Dutch | nlNL | +| Finnish | fiFI | +| French | frFR | +| German | deDE | +| Hungarian | huHU | +| Italian | itIT | +| Norwegian | nbNO | +| Polish | plPL | +| Portuguese | ptPT | +| Russian | ruRU | +| Spanish | esES | +| Swedish | svSE | +| Turkish | trTR | +| Indonesian | idID | + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/localizing-functions/example1.js"> +<div class="example"> + <button id="run" class="button run"> + Run calculations + </button> + <button id="reset" class="button button-outline reset"> + Reset + </button> + <table> + <colgroup> + <col style="width:25%" /> + <col style="width:15%" /> + <col style="width:20%" /> + <col style="width:20%" /> + <col style="width:20%" /> + </colgroup> + <thead> + <tr> + <th>Name</th> + <th>Year_1</th> + <th>Year_2</th> + <th>Average</th> + <th>Sum</th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/* start:skip-in-sandbox */ +const frFR = HyperFormula.languages.frFR; +/* end:skip-in-sandbox */ +/** + * Initial table data. + */ +const tableData = [ + ['Greg Black', 4.66, '=B1*1.3', '=MOYENNE(B1:C1)', '=SOMME(B1:C1)'], + ['Anne Carpenter', 5.25, '=$B$2*30%', '=MOYENNE(B2:C2)', '=SOMME(B2:C2)'], + ['Natalie Dem', 3.59, '=B3*2.7+2+1', '=MOYENNE(B3:C3)', '=SOMME(B3:C3)'], + ['John Sieg', 12.51, '=B4*(1.22+1)', '=MOYENNE(B4:C4)', '=SOMME(B4:C4)'], + [ + 'Chris Aklips', + 7.63, + '=B5*1.1*SUM(10,20)+1', + '=MOYENNE(B5:C5)', + '=SOMME(B5:C5)', + ], +]; + +// register language +if (!HyperFormula.getRegisteredLanguagesCodes().includes('frFR')) { + HyperFormula.registerLanguage('frFR', frFR); +} + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + language: 'frFR', + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); +// Add named expressions for the "TOTAL" row. +hf.addNamedExpression('Year_1', '=SOMME(main!$B$1:main!$B$5)'); +hf.addNamedExpression('Year_2', '=SOMME(main!$C$1:main!$C$5)'); + +/** + * Fill the HTML table with data. + * + * @param {boolean} calculated `true` if it should render calculated values, `false` otherwise. + */ +function renderTable(calculated = false) { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const totals = ['=SOMME(Year_1)', '=SOMME(Year_2)']; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + let totalRowsHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = calculated || !cellHasFormula; + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress) && showFormula) { + cellValue = hf.getCellValue(cellAddress); + + if (!isNaN(cellValue)) { + cellValue = cellValue.toFixed(2); + } + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + totalRowsHTML = `<tr class="summary"> +<td>TOTAL</td> +<td class="${updatedCellClass}"> + <span>${calculated ? hf.calculateFormula(totals[0], sheetId).toFixed(2) : totals[0]}</span> +</td> +<td class="${updatedCellClass}"> + <span>${calculated ? hf.calculateFormula(totals[1], sheetId).toFixed(2) : totals[1]}</span> +</td> +<td colspan="2"></td> +</tr>`; + newTbodyHTML += totalRowsHTML; + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Replace formulas with their results. + */ +function runCalculations() { + renderTable(true); +} + +/** + * Replace the values in the table with initial data. + */ +function resetTable() { + renderTable(); +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const runButton = document.querySelector('.example #run'); + const resetButton = document.querySelector('.example #reset'); + + runButton.addEventListener('click', () => { + runCalculations(); + }); + resetButton.addEventListener('click', () => { + resetTable(); + }); +} + +const ANIMATION_ENABLED = true; + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/guide/mcp-server.md b/docs/src/content/docs/guide/mcp-server.md similarity index 93% rename from docs/guide/mcp-server.md rename to docs/src/content/docs/guide/mcp-server.md index 9eee7a8396..50dacc0e85 100644 --- a/docs/guide/mcp-server.md +++ b/docs/src/content/docs/guide/mcp-server.md @@ -1,8 +1,11 @@ -# HyperFormula MCP Server +--- +title: "HyperFormula MCP Server" +--- + An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that exposes HyperFormula as a tool for any MCP-compatible AI client (Claude Desktop, Cursor, VS Code, and others) — giving LLMs deterministic spreadsheet and formula computation. -::: warning Not available yet — coming soon +:::caution[Not available yet — coming soon] This integration is on our roadmap and **cannot be installed or used today**. The API shown below is a preview and may still change before the first release. If you'd like to try it, [join the early access list](https://2fmjvg.share-eu1.hsforms.com/2e6drCkuLTn-1RuiYB91eJA) — we'll ping you the moment the first beta is ready, and your sign-up directly tells us how strongly to prioritize this integration. @@ -48,7 +51,7 @@ The client now sees tools like `evaluate`, `getCellValue`, and `setCellContents` ## Get early access -::: tip Be the first to try it +:::tip[Be the first to try it] We're actively building this integration. Drop your email and we'll notify you the moment the first beta lands — so you can try it before the public release. [Join the early access list →](https://2fmjvg.share-eu1.hsforms.com/2e6drCkuLTn-1RuiYB91eJA) @@ -59,5 +62,5 @@ We're actively building this integration. Drop your email and we'll notify you t - [Model Context Protocol specification](https://modelcontextprotocol.io/) - [HyperFormula on GitHub](https://github.com/handsontable/hyperformula) - [HyperFormula on npm](https://www.npmjs.com/package/hyperformula) -- [Built-in functions](built-in-functions.md) -- [Custom functions](custom-functions.md) +- [Built-in functions](/docs/guide/built-in-functions) +- [Custom functions](/docs/guide/custom-functions) diff --git a/docs/guide/migration-from-0.6-to-1.0.md b/docs/src/content/docs/guide/migration-from-0.6-to-1.0.md similarity index 93% rename from docs/guide/migration-from-0.6-to-1.0.md rename to docs/src/content/docs/guide/migration-from-0.6-to-1.0.md index 4e05d6994c..3f44546782 100644 --- a/docs/guide/migration-from-0.6-to-1.0.md +++ b/docs/src/content/docs/guide/migration-from-0.6-to-1.0.md @@ -1,4 +1,7 @@ -# Migrating from 0.6 to 1.0 +--- +title: "Migrating from 0.6 to 1.0" +--- + To upgrade your HyperFormula version from 0.6.x to 1.0.x, follow this guide. @@ -31,7 +34,7 @@ const options = { If you use the free non-commercial license, switch to the GPLv3 license or purchase a commercial license. -For more details on HyperFormula license keys, go [here](license-key.md). +For more details on HyperFormula license keys, go [here](/docs/guide/license-key). ## Step 2: Change `sheetName` to `sheetId` @@ -100,19 +103,19 @@ Adapt to the following changes in configuration option names, API method names a Switch from the matrix formula notation to the array formula notation. -For more information on the array formula notation, go [here](arrays.md). +For more information on the array formula notation, go [here](/docs/guide/arrays). Before: ```js ={ISEVEN(A2:A5*10)} ``` -Now, if the `useArrayArithmetic` configuration option is set to `false`, use the `ARRAYFORMULA` function to [enable the array arithmetic mode locally](arrays.md#enabling-the-array-arithmetic-mode-locally): +Now, if the `useArrayArithmetic` configuration option is set to `false`, use the `ARRAYFORMULA` function to [enable the array arithmetic mode locally](/docs/guide/arrays#enabling-the-array-arithmetic-mode-locally): ```js =ARRAYFORMULA(ISEVEN(A2:A5*10)) ``` -But when the `useArrayArithmetic` configuration option is set to `true`, you don't need to use the `ARRAYFORMULA` function, as the array arithmetic mode is [enabled globally](arrays.md#enabling-the-array-arithmetic-mode-globally): +But when the `useArrayArithmetic` configuration option is set to `true`, you don't need to use the `ARRAYFORMULA` function, as the array arithmetic mode is [enabled globally](/docs/guide/arrays#enabling-the-array-arithmetic-mode-globally): ```js =ISEVEN(A2:A5*10) ``` diff --git a/docs/guide/migration-from-1.x-to-2.0.md b/docs/src/content/docs/guide/migration-from-1.x-to-2.0.md similarity index 92% rename from docs/guide/migration-from-1.x-to-2.0.md rename to docs/src/content/docs/guide/migration-from-1.x-to-2.0.md index 58b0b5fafb..e0e9e78aad 100644 --- a/docs/guide/migration-from-1.x-to-2.0.md +++ b/docs/src/content/docs/guide/migration-from-1.x-to-2.0.md @@ -1,4 +1,7 @@ -# Migrating from 1.x to 2.0 +--- +title: "Migrating from 1.x to 2.0" +--- + To upgrade your HyperFormula version from 1.x.x to 2.0.0, follow this guide. @@ -22,6 +25,6 @@ const engine = HyperFormula.buildFromArray([[]], { }); ``` -::: tip +:::tip Functions that used GPU acceleration before (MMULT, MAXPOOL, MEDIANPOOL, and TRANSPOSE), still work. Their performance remains largely the same, except for very large data sets. ::: \ No newline at end of file diff --git a/docs/guide/migration-from-2.x-to-3.0.md b/docs/src/content/docs/guide/migration-from-2.x-to-3.0.md similarity index 98% rename from docs/guide/migration-from-2.x-to-3.0.md rename to docs/src/content/docs/guide/migration-from-2.x-to-3.0.md index ed511ede91..bc8d5aee3b 100644 --- a/docs/guide/migration-from-2.x-to-3.0.md +++ b/docs/src/content/docs/guide/migration-from-2.x-to-3.0.md @@ -1,4 +1,7 @@ -# Migrating from 2.x to 3.0 +--- +title: "Migrating from 2.x to 3.0" +--- + To upgrade your HyperFormula version from 2.x.x to 3.0.0, follow this guide. diff --git a/docs/src/content/docs/guide/named-expressions.md b/docs/src/content/docs/guide/named-expressions.md new file mode 100644 index 0000000000..71637dbcb3 --- /dev/null +++ b/docs/src/content/docs/guide/named-expressions.md @@ -0,0 +1,482 @@ +--- +title: "Named expressions" +--- + + +An expression can be assigned a human-friendly name. Thanks to this you can +refer to that name anywhere across the workbook. Names are especially useful +when you use some references repeatedly. In this case, names simplify the +formulas and reduce the risk of making a mistake. Such a worksheet is also +easier to maintain. + +You can name a formula, string, number, or any other type of data. + +By default, references in named expressions are absolute. Most people use +absolute references in spreadsheet software like Excel without even knowing +about it. Very few know that references can be relative too. Unfortunately, +HyperFormula doesn't support relative references inside named expressions at the +moment. + +Dynamic ranges are supported through functions such as INDEX and OFFSET. + +Named ranges can overlap each other, e.g., it is possible to define the names as +follows: + +- rangeOne: Sheet1!$A$1:$D$10 +- rangeTwo: Sheet1!$A$1:$E$1 + +## Examples + +| Type | Custom name | Example expression | +|:------------------------|:------------|:--------------------------| +| Named cell | myCell | =Sheet1!$A$1 | +| Named range of cells | myRange | =Sheet1!$A$1:$D$10 | +| Named constant (number) | myNumber | =10 | +| Named constant (string) | myText | ="One Small Step for Man" | +| Named formula | myFormula | =SUM(Sheet1!$A$1:$D$10) | + +## Naming rules + +Expression names are case-insensitive, and they: + +- Must start with a Unicode letter or with an underscore (`_`). +- Can contain only Unicode letters, numbers, underscores, and periods (`.`). +- Can't be the same as any possible reference in the A1 notation (for example, + `Q4` or `YEAR2023`). +- Can't be the same as any possible reference in the R1C1 notation (for example, + `R4C5`, `RC` or `R0C`). +- Must be unique within a given scope. + +:::tip +Expression names must be unique within a given scope, but you can override a +global named-expression with a local one. For example: + +```javascript +// `MyRevenue` has to be unique within the global scope +hfInstance.addNamedExpression('MyRevenue', '=SUM(100+10)'); + +// but you can still use `MyRevenue` within the local scope of Sheet2 (sheetId = 1) +hfInstance.addNamedExpression('MyRevenue', '=Sheet2!$A$1+100', 1); +``` +::: + +For examples of valid and invalid expression names, see the following table: + +| Name | Validity | +|:------------|:---------| +| my Revenue | Invalid | +| myRevenue | Valid | +| quarter1 | Invalid | +| quarter_1 | Valid | +| 1stQuarter | Invalid | +| _1stQuarter | Valid | +| .NET | Invalid | +| ASP.NET | Valid | +| A1 | Invalid | +| $A$1 | Invalid | +| RC | Invalid | + +## Using named expressions in formulas + +Named expressions can be used in any formula by referencing their names. Use them anywhere you would normally use a cell reference, range, or constant value. + +```javascript +// Define named expressions +hfInstance.addNamedExpression('TaxRate', '=0.08'); +hfInstance.addNamedExpression('SalesData', '=Sheet1!$A$1:$A$10'); + +// Use them in formulas +hfInstance.setCellContents({sheet: 0, col: 2, row: 0}, [['=SUM(SalesData)']]); +hfInstance.setCellContents({sheet: 0, col: 2, row: 1}, [['=SUM(SalesData) * TaxRate']]); +``` + +## Available methods + +These are the basic methods that can be used to add and manipulate named +expressions, including the creation and handling of named ranges. The full list +of methods is available in the [API reference](/docs/api). + +### Adding a named expression + +You can add a named expression in two ways: + +**During engine initialization**: You can provide named expressions as a parameter when creating a HyperFormula instance using the factory methods `buildEmpty`, `buildFromArray`, or `buildFromSheets`. This is the most efficient way to add multiple named expressions at once. + +```javascript +// Define named expressions during initialization +const namedExpressions = [ + { + name: 'prettyName', + expression: '=Sheet1!$A$1+100', + scope: 0 // optional: local scope for 'Sheet1' + }, + { + name: 'globalConstant', + expression: '=42' + // no scope specified = global scope + } +]; + +// Create engine with named expressions +const hfInstance = HyperFormula.buildEmpty({}, namedExpressions); +// or +const hfInstance = HyperFormula.buildFromArray(sheetData, {}, namedExpressions); +// or +const hfInstance = HyperFormula.buildFromSheets(sheetsData, {}, namedExpressions); +``` + +**After engine creation**: You can add a named expression by using the `addNamedExpression` method. It accepts name for the expression, the expression as a raw cell content, and optionally the scope. If you do not define the scope it will be set to global, meaning the expression name will be valid for the whole workbook. If you want to add many of them, it is advised to do so in a [batch](/docs/guide/batch-operations). This method returns [an array of changed cells](/docs/guide/basic-operations#changes-array). + +```javascript +// add 'prettyName' expression to the local scope of 'Sheet1' (sheetId = 0) +const changes = hfInstance.addNamedExpression( + 'prettyName', + '=Sheet1!$A$1+100', + 0 +); +``` + +### Changing a named expression + +You can change a named expression by using the `changeNamedExpression` method. +Select the name of an expression to change and pass it as the first parameter, +then define the new expression as raw cell content and optionally add the scope. +If you do not define the scope it will be set to global, meaning the expression +will be valid for the whole workbook. If you want to change many of them, it is +advised to do so in a [batch](/docs/guide/batch-operations). +This method returns [an array of changed cells](/docs/guide/basic-operations#changes-array). + +```javascript +// change the named expression +const changes = hfInstance.changeNamedExpression( + 'prettyName', + '=Sheet1!$A$1+200' +); +``` + +### Removing a named expression + +You can remove a named expression by using the `removeNamedExpression` method. +Select the name of an expression to remove and pass it as the first parameter +and optionally define the scope. If you do not define the scope it will be +understood as global, meaning, the whole workbook. +This method returns [an array of changed cells](/docs/guide/basic-operations#changes-array). + +```javascript +// remove 'prettyName' expression from 'Sheet1' (sheetId=0) +const changes = hfInstance.removeNamedExpression('prettyName', 0); +``` + +### Listing all named expressions + +You can retrieve a whole list of named expressions by using the +`listNamedExpressions` method. It requires no parameters and returns all named +expressions as an array of strings. + +```javascript +// get all named-expression names +const listOfExpressions = hfInstance.listNamedExpressions(); +``` + +## Handling errors + +Operations on named expressions throw errors when something goes wrong. These +errors can be [handled](/docs/guide/basic-operations#handling-an-error) to provide a good +user experience in the application. It is also possible to check the +availability of operations using `isItPossibleTo*` methods, which are also +described in [that section](/docs/guide/basic-operations#isitpossibleto-methods). + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/named-expressions/example1.js"> +<div class="example"> + <button id="run" class="button run"> + Run calculations + </button> + <button id="reset" class="button button-outline reset"> + Reset + </button> + <table> + <thead> + <tr> + <th>A</th> + <th>B</th> + <th>C</th> + <th>D</th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/** + * Initial table data. + */ +const tableData = [ + [10, 20, 20, 30], + [50, 60, 70, 80], + [90, 100, 110, 120], + ['=myOneCell', '=myTwoCells', '=myOneColumn', '=myTwoColumns'], + ['=myFormula+myNumber+34', '=myText', '=myOneRow', '=myTwoRows'], +]; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); +// Add named expressions +hf.addNamedExpression('myOneCell', '=main!$A$1'); +hf.addNamedExpression('myTwoCells', '=SUM(main!$A$1, main!$A$2)'); +hf.addNamedExpression('myOneColumn', '=SUM(main!$A$1:main!$A$3)'); +hf.addNamedExpression('myTwoColumns', '=SUM(main!$A$1:main!$B$3)'); +hf.addNamedExpression('myOneRow', '=SUM(main!$A$1:main!$D$1)'); +hf.addNamedExpression('myTwoRows', '=SUM(main!$A$1:main!$D$2)'); +hf.addNamedExpression('myFormula', '=SUM(0, 1, 1, 2, 3, 5, 8, 13)'); +hf.addNamedExpression('myNumber', '=21'); +hf.addNamedExpression('myText', 'Apollo 11'); + +/** + * Fill the HTML table with data. + * + * @param {boolean} calculated `true` if it should render calculated values, `false` otherwise. + */ +function renderTable(calculated = false) { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = calculated || !cellHasFormula; + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress) && showFormula) { + cellValue = hf.getCellValue(cellAddress); + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Replace formulas with their results. + */ +function runCalculations() { + renderTable(true); +} + +/** + * Replace the values in the table with initial data. + */ +function resetTable() { + renderTable(); +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const runButton = document.querySelector('.example #run'); + const resetButton = document.querySelector('.example #reset'); + + runButton.addEventListener('click', () => { + runCalculations(); + }); + resetButton.addEventListener('click', () => { + resetTable(); + }); +} + +const ANIMATION_ENABLED = true; + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/guide/order-of-precendece.md b/docs/src/content/docs/guide/order-of-precendece.md similarity index 96% rename from docs/guide/order-of-precendece.md rename to docs/src/content/docs/guide/order-of-precendece.md index 70b55d2810..77511ed2ca 100644 --- a/docs/guide/order-of-precendece.md +++ b/docs/src/content/docs/guide/order-of-precendece.md @@ -1,6 +1,9 @@ -# Order of precedence +--- +title: "Order of precedence" +--- -HyperFormula supports multiple [operators](types-of-operators.md) that + +HyperFormula supports multiple [operators](/docs/guide/types-of-operators) that can be used to perform mathematical operations in a formula. These operators are calculated in a specific order. If the formula contains operators of equal precedence, like addition and subtraction, then diff --git a/docs/guide/performance.md b/docs/src/content/docs/guide/performance.md similarity index 95% rename from docs/guide/performance.md rename to docs/src/content/docs/guide/performance.md index d8f9002694..f1912f5c2b 100644 --- a/docs/guide/performance.md +++ b/docs/src/content/docs/guide/performance.md @@ -1,4 +1,7 @@ -# Performance +--- +title: "Performance" +--- + We implemented various techniques to boost the performance of HyperFormula. In some cases, turning them on or off might increase @@ -70,14 +73,14 @@ by the update. Sometimes, a simple change can cause recalculation of a large part of the sheet, e.g., when the modified cell is at the very beginning of the dependency chain or when there are many -[volatile functions](volatile-functions.md) in the worksheet. +[volatile functions](/docs/guide/volatile-functions) in the worksheet. In such a case you may want to postpone the recalculation. The first option is to call `suspendEvaluation` before making changes and `resumeEvaluation` at a convenient moment. The second option is to pass the callback function with multiple -operations to a [batch function](batch-operations.md). Recalculation +operations to a [batch function](/docs/guide/batch-operations). Recalculation will be suspended before performing operations and resumed after them. In cases where you perform operations which may not cause a recalculation but only change the shape of the worksheet, like diff --git a/docs/guide/quality.md b/docs/src/content/docs/guide/quality.md similarity index 97% rename from docs/guide/quality.md rename to docs/src/content/docs/guide/quality.md index c1fd296252..433c722eb8 100644 --- a/docs/guide/quality.md +++ b/docs/src/content/docs/guide/quality.md @@ -1,4 +1,7 @@ -# Quality +--- +title: "Quality" +--- + HyperFormula is built with the highest standards of software quality, backed by rigorous research, comprehensive testing, and transparent development practices. @@ -25,7 +28,7 @@ Quality assurance is at the heart of our development process: - Continuous integration across multiple environments - Regular performance comparisons between versions -::: tip +:::tip Our high test coverage means you can be confident that HyperFormula will behave predictably in your application, even in complex scenarios. ::: @@ -75,4 +78,4 @@ HyperFormula offers comprehensive support options to ensure your success: Our expert team has been supporting enterprises since 2012 and understands how to respond to individual business needs. -[Learn more about support options →](support.md) +[Learn more about support options →](/docs/guide/support) diff --git a/docs/guide/release-notes.md b/docs/src/content/docs/guide/release-notes.md similarity index 97% rename from docs/guide/release-notes.md rename to docs/src/content/docs/guide/release-notes.md index 3e6c4529ae..83cb6643ea 100644 --- a/docs/guide/release-notes.md +++ b/docs/src/content/docs/guide/release-notes.md @@ -1,4 +1,7 @@ -# Release notes +--- +title: "Release notes" +--- + This page lists HyperFormula release notes. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -121,7 +124,7 @@ HyperFormula adheres to ### Fixed - Fixed an issue where operating on ranges of incompatible sizes resulted in a runtime exception. [#1267](https://github.com/handsontable/hyperformula/issues/1267) -- Fixed an issue where the [`simpleCellAddressFromString()`](../api/classes/hyperformula.md#simplecelladdressfromstring) method was crashing when called with a non-ASCII character in an unquoted sheet name. [#1312](https://github.com/handsontable/hyperformula/issues/1312) +- Fixed an issue where the [`simpleCellAddressFromString()`](/docs/api/classes/hyperformula#simplecelladdressfromstring) method was crashing when called with a non-ASCII character in an unquoted sheet name. [#1312](https://github.com/handsontable/hyperformula/issues/1312) - Fixed an issue where adding a row to a very large spreadsheet resulted in the `Maximum call stack size exceeded` error. [#1332](https://github.com/handsontable/hyperformula/issues/1332) - Fixed an issue where using a column-range reference to an empty sheet as a function argument resulted in the `Incorrect array size` error. [#1147](https://github.com/handsontable/hyperformula/issues/1147) - Fixed an issue where the SUBSTITUTE function wasn't working correctly with regex special characters. [#1289](https://github.com/handsontable/hyperformula/issues/1289) @@ -161,7 +164,7 @@ HyperFormula adheres to ### Changed -- Optimized the [`updateConfig()`](../api/classes/hyperformula.md#updateconfig) method to rebuild HyperFormula only when +- Optimized the [`updateConfig()`](/docs/api/classes/hyperformula#updateconfig) method to rebuild HyperFormula only when the new configuration is different from the old one. [#1251](https://github.com/handsontable/hyperformula/issues/1251) ### Fixed @@ -175,9 +178,9 @@ HyperFormula adheres to ### Added -- Exported the [`CellError`](../api/classes/cellerror.md) class as a public +- Exported the [`CellError`](/docs/api/classes/cellerror) class as a public API. [#1232](https://github.com/handsontable/hyperformula/issues/1232) -- Exported the [`SimpleRangeValue`](../api/classes/simplerangevalue.md) class as a public +- Exported the [`SimpleRangeValue`](/docs/api/classes/simplerangevalue) class as a public API. [#1178](https://github.com/handsontable/hyperformula/issues/1178) ### Fixed @@ -185,7 +188,7 @@ HyperFormula adheres to - Fixed an `EmptyCellVertex` data integrity issue between the `AddressMapping` and `DependencyGraph` objects. [#1188](https://github.com/handsontable/hyperformula/issues/1188) - Fixed a build issue with M1- and M2-chip MacBooks. [#1166](https://github.com/handsontable/hyperformula/issues/1166) -- Fixed an issue where the order of items returned by [`removeColumns()`](../api/classes/hyperformula.md#removecolumns) +- Fixed an issue where the order of items returned by [`removeColumns()`](/docs/api/classes/hyperformula#removecolumns) depended on the address mapping policy. [#1205](https://github.com/handsontable/hyperformula/issues/1205) ## 2.3.1 @@ -211,10 +214,10 @@ HyperFormula adheres to ### Added -- Exported the [`ArraySize`](../api/classes/arraysize.md) class as a public +- Exported the [`ArraySize`](/docs/api/classes/arraysize) class as a public API. [#843](https://github.com/handsontable/hyperformula/issues/843) - Renamed an internal interface from `ArgumentTypes` - to [`FunctionArgumentType`](../api/classes/hyperformulans.md#functionargumenttype), and exported it as a public + to [`FunctionArgumentType`](/docs/api/classes/hyperformulans#functionargumenttype), and exported it as a public API. [#1108](https://github.com/handsontable/hyperformula/pull/1108) - Exported `ImplementedFunctions` and `FunctionMetadata` as public APIs. [#1108](https://github.com/handsontable/hyperformula/pull/1108) @@ -249,7 +252,7 @@ HyperFormula adheres to ### Changed - Changed the rounding strategy of the default time-parsing function to be independent of - the [`timeFormats`](../api/interfaces/configparams.md#timeformats) configuration option. Now, time values are always + the [`timeFormats`](/docs/api/interfaces/configparams#timeformats) configuration option. Now, time values are always rounded to the nearest millisecond (0.001 s). [#953](https://github.com/handsontable/hyperformula/issues/953) ### Fixed @@ -286,7 +289,7 @@ HyperFormula adheres to ### Added - Added support for reversed ranges. [#834](https://github.com/handsontable/hyperformula/issues/834) -- Added a new configuration option, [`ignoreWhiteSpace`](/api/interfaces/configparams.md#ignorewhitespace), which allows +- Added a new configuration option, [`ignoreWhiteSpace`](/docs/api/interfaces/configparams#ignorewhitespace), which allows for parsing formulas that contain whitespace characters of any kind. [#898](https://github.com/handsontable/hyperformula/issues/898) @@ -317,19 +320,19 @@ HyperFormula adheres to ### Added -- Added a new static property: [`HyperFormula.defaultConfig`](../api/classes/hyperformula.md#defaultconfig). +- Added a new static property: [`HyperFormula.defaultConfig`](/docs/api/classes/hyperformula#defaultconfig). [#822](https://github.com/handsontable/hyperformula/issues/822) -- The [`getFillRangeData()`](../api/classes/hyperformula.md#getfillrangedata) +- The [`getFillRangeData()`](/docs/api/classes/hyperformula#getfillrangedata) method can now use one sheet for its source and another sheet for its target. [#836](https://github.com/handsontable/hyperformula/issues/836) ### Fixed -- Fixed the handling of Unicode characters and non-letter characters in the [PROPER](built-in-functions.md#text) +- Fixed the handling of Unicode characters and non-letter characters in the [PROPER](/docs/guide/built-in-functions#text) function. [#811](https://github.com/handsontable/hyperformula/issues/811) - Fixed unnecessary warnings caused by deprecated configuration options. [#830](https://github.com/handsontable/hyperformula/issues/830) -- Fixed the [SUMPRODUCT](built-in-functions.md#math-and-trigonometry) +- Fixed the [SUMPRODUCT](/docs/guide/built-in-functions#math-and-trigonometry) function. [#810](https://github.com/handsontable/hyperformula/issues/810) ## 1.2.0 diff --git a/docs/guide/server-side-installation.md b/docs/src/content/docs/guide/server-side-installation.md similarity index 87% rename from docs/guide/server-side-installation.md rename to docs/src/content/docs/guide/server-side-installation.md index c9e8da54b6..2768655410 100644 --- a/docs/guide/server-side-installation.md +++ b/docs/src/content/docs/guide/server-side-installation.md @@ -1,6 +1,9 @@ -# Server-side installation +--- +title: "Server-side installation" +--- -::: tip + +:::tip For full compatibility, the minimum required version of **Node is 13**. It is related to the support for ICU. There is a possibility to use lower versions of Node but you need to install an additional package @@ -8,7 +11,7 @@ as the dependency: [`full-icu`](https://github.com/unicode-org/full-icu-npm) ::: The basic steps are very similar to the ones in the -[client-side installation](client-side-installation.md) process. +[client-side installation](/docs/guide/client-side-installation) process. ## Install with npm or Yarn diff --git a/docs/src/content/docs/guide/sorting-data.md b/docs/src/content/docs/guide/sorting-data.md new file mode 100644 index 0000000000..25794a1aeb --- /dev/null +++ b/docs/src/content/docs/guide/sorting-data.md @@ -0,0 +1,518 @@ +--- +title: "Sorting data" +--- + + +In HyperFormula, you can sort data by reordering rows and columns. + +## Sorting data in HyperFormula + +To sort data in HyperFormula, you reorder rows (or columns), by providing your preferred permutation of row (or column) indexes. + +You can implement any sorting algorithm that returns an array of row or column indexes. + +## Sorting rows + +To sort rows, use the [`isItPossibleToSetRowOrder`](/docs/api/classes/hyperformula#isitpossibletosetroworder) and [`setRowOrder`](/docs/api/classes/hyperformula#setroworder) methods. + +### Step 1: Choose a new row order +Choose your required permutation of row indexes. + +For example, if you want to swap the first row with the third row, set the order to `[2, 1, 0]` instead of `[0, 1, 2]`: + +```js +// a HyperFormula instance with example data +const hfInstance = HyperFormula.buildFromArray([ + [1], + [2], + [4, 5], +]); + +// we'll set the row order to [2, 1, 0] in the next steps +``` + +:::tip +The [`setRowOrder`](/docs/api/classes/hyperformula#setroworder) method accepts an array of numbers, so you can implement any function that returns an array with your required row order. +::: + +### Step 2: Check if the new row order can be applied + +Before you change the row order, check if your specified row number permutation can actually be applied. + +Thanks to the [`isItPossibleTo*` methods](/docs/guide/basic-operations#isitpossibleto-methods), you can check if an operation is allowed, and display an error message if it's not. + +Use the [`isItPossibleToSetRowOrder`](/docs/api/classes/hyperformula#isitpossibletosetroworder) method: + +```js +const hfInstance = HyperFormula.buildFromArray([ + [1], + [2], + [4, 5], +]); + +// a variable to carry the user message +let messageUsedInUI; + +// check if your permutation can be applied +const isRowOrderOk = hfInstance.isItPossibleToSetRowOrder(0, [2, 1, 0]); + +// display an error message +if (!isRowOrderOk) { + messageUsedInUI = 'Sorry, you cannot sort rows in this way.' +} +``` + +### Step 3: Set the new row order + +If your specified row number permutation is valid, change the row order: + +```js +const hfInstance = HyperFormula.buildFromArray([ + [1], + [2], + [4, 5], +]); + +let messageUsedInUI; + +const isRowOrderOk = hfInstance.isItPossibleToSetRowOrder(0, [2, 1, 0]); + +if (!isRowOrderOk) { + messageUsedInUI = 'Sorry, you cannot sort rows in this way.' +} else { + // set the new row order + setRowOrder(0, [2, 1, 0]); +} +// rows 0 and 2 swap places + +// returns: +// [{ +// address: { sheet: 0, col: 0, row: 2 }, +// newValue: 1, +// }, +// { +// address: { sheet: 0, col: 1, row: 2 }, +// newValue: null, +// }, +// { +// address: { sheet: 0, col: 0, row: 0 }, +// newValue: 4, +// }, +// { +// address: { sheet: 0, col: 1, row: 0 }, +// newValue: 5, +// }] +``` + +## Sorting columns + +To sort columns, use the [`isItPossibleToSetColumnOrder`](/docs/api/classes/hyperformula#isitpossibletosetcolumnorder) and [`setColumnOrder`](/docs/api/classes/hyperformula#setcolumnorder) methods. + +### Step 1: Choose a new column order +Choose your required permutation of column indexes. + +For example, if you want to swap the first column with the third column, set the order to `[2, 1, 0]` instead of `[0, 1, 2]`: + +```js +// a HyperFormula instance with example data +const hfInstance = HyperFormula.buildFromArray([ + [1, 2, 4], + [5] +]); + +// we'll set the column order to [2, 1, 0] in the next steps +``` + +:::tip +The [`setColumnOrder`](/docs/api/classes/hyperformula#setcolumnorder) method accepts an array of numbers, so you can implement any function that returns an array with your required column order. +::: + +### Step 2: Check if the new column order can be applied + +Before you change the column order, check if your specified column number permutation can actually be applied. + +Thanks to the [`isItPossibleTo*` methods](/docs/guide/basic-operations#isitpossibleto-methods), you can check if an operation is allowed, and display an error message if it's not. + +Use the [`isItPossibleToSetColumnOrder`](/docs/api/classes/hyperformula#isitpossibletosetcolumnorder) method: + +```js +const hfInstance = HyperFormula.buildFromArray([ + [1, 2, 4], + [5] +]); + +// a variable to carry the user message +let messageUsedInUI; + +// check if your permutation can be applied +const isColumnOrderOk = hfInstance.isItPossibleToSetColumnOrder(0, [2, 1, 0]); + +// display an error message +if (!isColumnOrderOk) { + messageUsedInUI = 'Sorry, you cannot sort columns in this way.' +} +``` + +### Step 3: Set the new column order + +If your specified column number permutation is valid, change the column order: + +```js +const hfInstance = HyperFormula.buildFromArray([ + [1, 2, 4], + [5] +]); + +let messageUsedInUI; + +const isColumnOrderOk = hfInstance.isItPossibleToSetColumnOrder(0, [2, 1, 0]); + +if (!isColumnOrderOk) { + messageUsedInUI = 'Sorry, you cannot sort columns in this way.' +} else { + // set the new column order + setColumnOrder(0, [2, 1, 0]); +} +// columns 0 and 2 swap places + +//returns: +// [{ +// address: { sheet: 0, col: 2, row: 0 }, +// newValue: 1, +// }, +// { +// address: { sheet: 0, col: 2, row: 1 }, +// newValue: 5, +// }, +// { +// address: { sheet: 0, col: 0, row: 0 }, +// newValue: 4, +// }, +// { +// address: { sheet: 0, col: 0, row: 1 }, +// newValue: null, +// }] +``` + +## Data sorting demo + +The demo below shows how to sort rows in ascending and descending order, based on the results (calculated values) of the cells in the second column. + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/sorting-data/example1.js"> +<div class="example"> + <table> + <colgroup> + <col style="width:50%" /> + <col style="width:50%" /> + </colgroup> + <thead> + <tr> + <th>Name</th> + <th> + Score <button id="asc" class="button arrow" style="width: 20px; height: 20px; line-height: 0; padding: 10px 6px;">↑</button> + <button id="desc" class="button arrow" style="width: 20px; height: 20px; line-height: 0; padding: 10px 6px;">↓</button> + </th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/** + * Initial table data. + */ +const tableData = [ + ['Greg Black', '100'], + ['Anne Carpenter', '=SUM(100,100)'], + ['Natalie Dem', '500'], + ['John Sieg', '50'], + ['Chris Aklips', '20'], + ['Bart Hoopoe', '700'], + ['Chris Site', '80'], + ['Agnes Whitey', '90'], +]; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); + +/** + * Sort the HF's dataset. + * + * @param {boolean} ascending `true` if sorting in ascending order, `false` otherwise. + * @param {Function} callback The callback function. + */ +function sort(ascending, callback) { + const rowCount = hf.getSheetDimensions(sheetId).height; + const colValues = []; + let newOrder = null; + const newOrderMapping = []; + + for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { + colValues.push({ + rowIndex, + value: hf.getCellValue({ + sheet: sheetId, + col: 1, + row: rowIndex, + }), + }); + } + + colValues.sort((objA, objB) => { + const delta = objA.value - objB.value; + + return ascending ? delta : -delta; + }); + newOrder = colValues.map((el) => el.rowIndex); + newOrder.forEach((orderIndex, arrIndex) => { + newOrderMapping[orderIndex] = arrIndex; + }); + hf.setRowOrder(sheetId, newOrderMapping); + callback(); +} + +/** + * Fill the HTML table with data. + * + * @param {boolean} calculated `true` if it should render calculated values, `false` otherwise. + */ +function renderTable(calculated = false) { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellHasFormula = hf.doesCellHaveFormula(cellAddress); + const showFormula = calculated || !cellHasFormula; + let cellValue = ''; + + if (!hf.isCellEmpty(cellAddress) && showFormula) { + cellValue = hf.getCellValue(cellAddress); + } else { + cellValue = hf.getCellFormula(cellAddress); + } + + newTbodyHTML += `<td class="${cellHasFormula ? updatedCellClass : ''}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +const doSortASC = () => { + sort(true, () => { + renderTable(true); + }); +}; + +const doSortDESC = () => { + sort(false, () => { + renderTable(true); + }); +}; + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const ascSort = document.querySelector('.example #asc'); + const descSort = document.querySelector('.example #desc'); + + ascSort.addEventListener('click', () => { + doSortASC(); + }); + descSort.addEventListener('click', () => { + doSortDESC(); + }); +} + +const ANIMATION_ENABLED = true; + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/guide/specifications-and-limits.md b/docs/src/content/docs/guide/specifications-and-limits.md similarity index 98% rename from docs/guide/specifications-and-limits.md rename to docs/src/content/docs/guide/specifications-and-limits.md index 20a31fe137..7851b7b01f 100644 --- a/docs/guide/specifications-and-limits.md +++ b/docs/src/content/docs/guide/specifications-and-limits.md @@ -1,4 +1,7 @@ -# Specifications and limits +--- +title: "Specifications and limits" +--- + The following table presents the limits of features. Many of them are bounded only by system resources. This means the actual @@ -83,7 +86,7 @@ is running on. | Feature | Maximum limit | |:-------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Number precision | ~15 significant digits (with all the [limitations of the JavaScript floating-point arithmetics](https://patrickkarsh.medium.com/why-math-is-hard-in-javascript-floating-point-precision-in-javascript-41706aa7a89d)). HyperFormula rounds the operation results according to the [precisionRounding](../api/interfaces/configparams.html#precisionrounding) configuration option. | +| Number precision | ~15 significant digits (with all the [limitations of the JavaScript floating-point arithmetics](https://patrickkarsh.medium.com/why-math-is-hard-in-javascript-floating-point-precision-in-javascript-41706aa7a89d)). HyperFormula rounds the operation results according to the [precisionRounding](/docs/api/interfaces/configparams#precisionrounding) configuration option. | | Smallest magnitude allowed negative number | -5E-324 (inherited from JavaScript) | | Smallest magnitude allowed positive number | 5E-324 (inherited from JavaScript) | | Largest magnitude allowed positive number | 1.79E+308 (inherited from JavaScript) | diff --git a/docs/guide/support.md b/docs/src/content/docs/guide/support.md similarity index 89% rename from docs/guide/support.md rename to docs/src/content/docs/guide/support.md index 1f3c627b5b..b44c11b9b3 100644 --- a/docs/guide/support.md +++ b/docs/src/content/docs/guide/support.md @@ -1,4 +1,7 @@ -# Support +--- +title: "Support" +--- + ## Community support @@ -14,7 +17,7 @@ mission-critical applications. We can help you at every stage of your development. Our experts have been supporting enterprises since 2012 and know exactly how to respond to individual needs. -[Contact sales](contact.md) +[Contact sales](/docs/guide/contact) ## Consulting services @@ -29,4 +32,4 @@ we can help you in many ways. For instance: * Provide a tailored training program * Provide technical leadership -[Contact sales](contact.md) +[Contact sales](/docs/guide/contact) diff --git a/docs/guide/supported-browsers.md b/docs/src/content/docs/guide/supported-browsers.md similarity index 96% rename from docs/guide/supported-browsers.md rename to docs/src/content/docs/guide/supported-browsers.md index e0738d987f..d6bc578c41 100644 --- a/docs/guide/supported-browsers.md +++ b/docs/src/content/docs/guide/supported-browsers.md @@ -1,4 +1,7 @@ -# Supported browsers +--- +title: "Supported browsers" +--- + Each release of HyperFormula is tested on the **two latest versions** of every modern browser, on both mobile and desktop. In addition to diff --git a/docs/guide/types-of-errors.md b/docs/src/content/docs/guide/types-of-errors.md similarity index 98% rename from docs/guide/types-of-errors.md rename to docs/src/content/docs/guide/types-of-errors.md index 7f8546a90f..d5c2f6cb71 100644 --- a/docs/guide/types-of-errors.md +++ b/docs/src/content/docs/guide/types-of-errors.md @@ -1,4 +1,7 @@ -# Types of errors +--- +title: "Types of errors" +--- + HyperFormula returns an error when a formula cannot be processed properly. To make it easier for a user, each kind of error has its diff --git a/docs/guide/types-of-operators.md b/docs/src/content/docs/guide/types-of-operators.md similarity index 97% rename from docs/guide/types-of-operators.md rename to docs/src/content/docs/guide/types-of-operators.md index 0dbb716804..f2b8189cae 100644 --- a/docs/guide/types-of-operators.md +++ b/docs/src/content/docs/guide/types-of-operators.md @@ -1,9 +1,12 @@ -# Types of operators +--- +title: "Types of operators" +--- + The operators specify what type of actions are performed on arguments (operands) in the formula. HyperFormula supports the operators that are common in spreadsheet software. They are calculated in a -[specific order](order-of-precendece.md) which can be altered by +[specific order](/docs/guide/order-of-precendece) which can be altered by the use of parentheses. HyperFormula supports the following operators: @@ -230,13 +233,13 @@ comparison. For example, if you compare `AsTrOnAuT` with `aStroNaut` they will be understood as identical, the same goes for `Préservation` and `Preservation`. It applies to comparison operators only. It can be configured with `accentSensitive` and `caseSensitive` options in the -[configuration](configuration-options.md). +[configuration](/docs/guide/configuration-options). Apart from accents and case sensitivity, you can also configure `caseFirst.` This option defines whether upper case or lower case should come first. Additionally the `ignorePunctuation` option specifies whether punctuation should be ignored in string comparison. By default `caseFirst` is set to `'lower'` and `ignorePunctuation` is set to `false`. For more details -see the official [API reference](../api) of HyperFormula. +see the official [API reference](/docs/api) of HyperFormula. Here is an example configuration that overwrites default settings: diff --git a/docs/guide/types-of-values.md b/docs/src/content/docs/guide/types-of-values.md similarity index 97% rename from docs/guide/types-of-values.md rename to docs/src/content/docs/guide/types-of-values.md index 67bca940fa..3e10281221 100644 --- a/docs/guide/types-of-values.md +++ b/docs/src/content/docs/guide/types-of-values.md @@ -1,4 +1,7 @@ -# Types of values +--- +title: "Types of values" +--- + In HyperFormula, values can be of type Number, Text, Logical, Date, Time, DateTime, Error, Currency, or Percentage depending on the data. Functions may work differently based on the types of arguments. @@ -94,10 +97,10 @@ date and time values as numbers. This makes it easier to perform mathematical operations such as calculating the number of days between two dates. - A Date value is represented as the number of full days since - [`nullDate`](../api/interfaces/configparams.md#nulldate). + [`nullDate`](/docs/api/interfaces/configparams#nulldate). - A Time value is represented as a fraction of a full day. - A DateTime value is represented as the number of (possibly fractional) days - since [`nullDate`](../api/interfaces/configparams.md#nulldate). + since [`nullDate`](/docs/api/interfaces/configparams#nulldate). ## Text values diff --git a/docs/src/content/docs/guide/undo-redo.md b/docs/src/content/docs/guide/undo-redo.md new file mode 100644 index 0000000000..1340e7d709 --- /dev/null +++ b/docs/src/content/docs/guide/undo-redo.md @@ -0,0 +1,344 @@ +--- +title: "Undo-redo" +--- + + +HyperFormula supports undo-redo for CRUD and move operations. +By default, you can **undo 20 actions.** The `undoLimit` can be changed +inside the [configuration options](/docs/guide/configuration-options) so you +can adapt that number to your needs. Be careful when setting +`undoLimit` to large numbers. It may result in performance issues. + +Undo and redo work together as a synced pair, so each time you +**undo** some action it is put onto a **redo** stack. + +**Named expressions** behave just like any other +[CRUD operation](basic-operations). + +## isThereSomething* methods + +There are two methods which can be used to check the actual state +of the undo-redo stack:`isThereSomethingToUndo` and +`isThereSomethingToRedo`. + +## Batch operations + +When you [batch several operations](/docs/guide/batch-operations) remember +that undo-redo will recognize them as a single cumulative operation. + +## Demo + +<div class="hf-example not-content"> +<style> +/* general */ +.example { + color: #606c76; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: .01em; + line-height: 1.6; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.example *, +.example *::before, +.example *::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +/* buttons */ +.example button { + border: 0.1em solid #1c49e4; + border-radius: .3em; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: .85em; + font-family: inherit; + font-weight: 700; + height: 3em; + letter-spacing: .1em; + line-height: 3em; + padding: 0 3em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + margin-bottom: 20px; + background-color: #1c49e4; +} +.example button:hover { + background-color: #2350ea; +} +.example button.outline { + background-color: transparent; + color: #1c49e4; +} +/* labels */ +.example label { + display: inline-block; + margin-left: 5px; +} +/* inputs */ +.example input:not([type='checkbox']), .example select, .example textarea, .example fieldset { + margin-bottom: 1.5em; + border: 0.1em solid #d1d1d1; + border-radius: .4em; + height: 3.8em; + width: 12em; + padding: 0 .5em; +} +.example input:focus, +.example select:focus { + outline: none; + border-color: #1c49e4; +} +/* message */ +.example .message-box { + border: 1px solid #1c49e433; + background-color: #1c49e405; + border-radius: 0.2em; + padding: 10px; +} +.example .message-box span { + animation-name: cell-appear; + animation-duration: 0.2s; + margin: 0; +} +/* table */ +.example table { + table-layout: fixed; + border-spacing: 0; + overflow-x: auto; + text-align: left; + width: 100%; + counter-reset: row-counter col-counter; +} +.example table tr:nth-child(2n) { + background-color: #f6f8fa; +} +.example table tr td, +.example table tr th { + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 0.1em solid #e1e1e1; + padding: 0 1em; + height: 3.5em; +} +/* table: header row */ +.example table thead tr th span::before { + display: inline-block; + width: 20px; +} +.example table.spreadsheet thead tr th span::before { + content: counter(col-counter, upper-alpha); +} +.example table.spreadsheet thead tr th { + counter-increment: col-counter; +} +/* table: first column */ +.example table tbody tr td:first-child { + text-align: center; + padding: 0; +} +.example table thead tr th:first-child { + padding-left: 40px; +} +.example table tbody tr td:first-child span { + width: 100%; + display: inline-block; + text-align: left; + padding-left: 15px; + margin-left: 0; +} +.example table tbody tr td:first-child span::before { + content: counter(row-counter); + display: inline-block; + width: 20px; + position: relative; + left: -10px; +} +.example table tbody tr { + counter-increment: row-counter; +} +/* table: summary row */ +.example table tbody tr.summary { + font-weight: 600; +} +/* updated-cell animation */ +.example table tr td.updated-cell span { + animation-name: cell-appear; + animation-duration: 0.6s; +} +@keyframes cell-appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +</style> +<div class="hf-example__preview" data-example-js="/examples/undo-redo/example1.js"> +<div class="example"> + <button id="remove-row" class="button run"> + Remove the second row + </button> + <button id="undo" class="button button-outline undo"> + Undo + </button> + <p id="info-box" style="margin: 0 0 -16px 0"> </p> + <table> + <colgroup> + <col style="width:40%" /> + <col style="width:60%" /> + </colgroup> + <thead> + <tr> + <th>Name</th> + <th>Value</th> + </tr> + </thead> + <tbody></tbody> + </table> +</div> +</div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```js +/** + * Initial table data. + */ +const tableData = [ + ['Greg', '2'], + ['Chris', '4'], +]; + +// Create an empty HyperFormula instance. +const hf = HyperFormula.buildEmpty({ + licenseKey: 'gpl-v3', +}); + +// Add a new sheet and get its id. +const sheetName = hf.addSheet('main'); +const sheetId = hf.getSheetId(sheetName); + +// Fill the HyperFormula sheet with data. +hf.setCellContents( + { + row: 0, + col: 0, + sheet: sheetId, + }, + tableData, +); +// Clear the undo stack to prevent undoing the initialization steps. +hf.clearUndoStack(); + +/** + * Fill the HTML table with data. + */ +function renderTable() { + const tbodyDOM = document.querySelector('.example tbody'); + const updatedCellClass = ANIMATION_ENABLED ? 'updated-cell' : ''; + const { height, width } = hf.getSheetDimensions(sheetId); + let newTbodyHTML = ''; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const cellAddress = { sheet: sheetId, col, row }; + const cellValue = hf.getCellValue(cellAddress); + + newTbodyHTML += `<td class="${updatedCellClass}"><span> + ${cellValue} + </span></td>`; + } + + newTbodyHTML += '</tr>'; + } + + tbodyDOM.innerHTML = newTbodyHTML; +} + +/** + * Clear the existing information. + */ +function clearInfo() { + const infoBoxDOM = document.querySelector('.example #info-box'); + + infoBoxDOM.innerHTML = ' '; +} + +/** + * Display the provided message in the info box. + * + * @param {string} message Message to display. + */ +function displayInfo(message) { + const infoBoxDOM = document.querySelector('.example #info-box'); + + infoBoxDOM.innerText = message; +} + +/** + * Bind the events to the buttons. + */ +function bindEvents() { + const removeRowButton = document.querySelector('.example #remove-row'); + const undoButton = document.querySelector('.example #undo'); + + removeRowButton.addEventListener('click', () => { + removeSecondRow(); + }); + undoButton.addEventListener('click', () => { + undo(); + }); +} + +/** + * Remove the second row from the table. + */ +function removeSecondRow() { + const filledRowCount = hf.getSheetDimensions(sheetId).height; + + clearInfo(); + + if (filledRowCount < 2) { + displayInfo("There's not enough filled rows to perform this action."); + + return; + } + + hf.removeRows(sheetId, [1, 1]); + renderTable(); +} + +/** + * Run the HF undo action. + */ +function undo() { + clearInfo(); + + if (!hf.isThereSomethingToUndo()) { + displayInfo("There's nothing to undo."); + + return; + } + + hf.undo(); + renderTable(); +} + +const ANIMATION_ENABLED = true; + +// Bind the button events. +bindEvents(); +// Render the table. +renderTable(); +``` + +</details> diff --git a/docs/guide/volatile-functions.md b/docs/src/content/docs/guide/volatile-functions.md similarity index 95% rename from docs/guide/volatile-functions.md rename to docs/src/content/docs/guide/volatile-functions.md index 951a8dbea5..a8a0891f40 100644 --- a/docs/guide/volatile-functions.md +++ b/docs/src/content/docs/guide/volatile-functions.md @@ -1,4 +1,7 @@ -# Volatile functions +--- +title: "Volatile functions" +--- + If you work with spreadsheet software regularly, then you've probably heard about Volatile Functions. They are distinctive because they @@ -38,7 +41,7 @@ Functions that depend on the structure of the sheet act as if they were volatile - ROWS - FORMULATEXT -See the complete [list of functions](built-in-functions.md) available. +See the complete [list of functions](/docs/guide/built-in-functions) available. ## Volatile actions @@ -74,7 +77,7 @@ These actions trigger the recalculation process of volatile functions: The extensive use of volatile functions may cause a performance drop. To reduce the negative effect, you can try -[batching these operations](batch-operations.md). +[batching these operations](/docs/guide/batch-operations). ## Volatile custom functions diff --git a/docs/index.md b/docs/src/content/docs/index.md similarity index 74% rename from docs/index.md rename to docs/src/content/docs/index.md index b98ed71eb8..6b492612f2 100644 --- a/docs/index.md +++ b/docs/src/content/docs/index.md @@ -1,5 +1,9 @@ --- +title: HyperFormula description: HyperFormula® - An open-source headless spreadsheet for business web apps +head: + - tag: style + content: 'h1#_top { display: none; }' --- <br> @@ -43,36 +47,36 @@ HyperFormula doesn't assume any existing user interface, making it a general-pur ## Features -- [Function syntax compatible with Microsoft Excel](guide/compatibility-with-microsoft-excel.md) and [Google Sheets](guide/compatibility-with-google-sheets.md) +- [Function syntax compatible with Microsoft Excel](/docs/guide/compatibility-with-microsoft-excel) and [Google Sheets](/docs/guide/compatibility-with-google-sheets) - High-speed parsing and evaluation of spreadsheet formulas -- [A library of ~400 built-in functions](guide/built-in-functions.md) -- [Support for custom functions](guide/custom-functions.md) -- [Support for Node.js](guide/server-side-installation.md#install-with-npm-or-yarn) -- [Support for undo/redo](guide/undo-redo.md) -- [Support for CRUD operations](guide/basic-operations.md) -- [Support for clipboard](guide/clipboard-operations.md) -- [Support for named expressions](guide/named-expressions.md) -- [Support for data sorting](guide/sorting-data.md) -- [Support for formula localization with 17 built-in languages](guide/i18n-features.md) +- [A library of ~400 built-in functions](/docs/guide/built-in-functions) +- [Support for custom functions](/docs/guide/custom-functions) +- [Support for Node.js](/docs/guide/server-side-installation#install-with-npm-or-yarn) +- [Support for undo/redo](/docs/guide/undo-redo) +- [Support for CRUD operations](/docs/guide/basic-operations) +- [Support for clipboard](/docs/guide/clipboard-operations) +- [Support for named expressions](/docs/guide/named-expressions) +- [Support for data sorting](/docs/guide/sorting-data) +- [Support for formula localization with 17 built-in languages](/docs/guide/i18n-features) - Easy integration with any front-end or back-end application - GPLv3 or a [commercial license](https://handsontable.com/get-a-quote) - Maintained by the team that stands behind the [Handsontable](https://handsontable.com/) data grid ## Documentation -- [Client-side installation](guide/client-side-installation.md) -- [Server-side installation](guide/server-side-installation.md) -- [Basic usage](guide/basic-usage.md) -- [Configuration options](guide/configuration-options.md) -- [List of built-in functions](guide/built-in-functions.md) -- [API Reference](api/) +- [Client-side installation](/docs/guide/client-side-installation) +- [Server-side installation](/docs/guide/server-side-installation) +- [Basic usage](/docs/guide/basic-usage) +- [Configuration options](/docs/guide/configuration-options) +- [List of built-in functions](/docs/guide/built-in-functions) +- [API Reference](/docs/api) ## Integrations -- [Integration with React](guide/integration-with-react.md#demo) -- [Integration with Angular](guide/integration-with-angular.md#demo) -- [Integration with Vue](guide/integration-with-vue.md#demo) -- [Integration with Svelte](guide/integration-with-svelte.md#demo) +- [Integration with React](/docs/guide/integration-with-react#demo) +- [Integration with Angular](/docs/guide/integration-with-angular#demo) +- [Integration with Vue](/docs/guide/integration-with-vue#demo) +- [Integration with Svelte](/docs/guide/integration-with-svelte#demo) ## Installation and usage @@ -110,7 +114,7 @@ console.log(`${hf.getCellValue({ sheet: sheetId, row: 0, col: 0 })}: ${hf.getCel ## Contributing -Contributions are welcome, but before you make them, please read the [Contributing Guide](guide/contributing.md) and accept the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). +Contributions are welcome, but before you make them, please read the [Contributing Guide](/docs/guide/contributing) and accept the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). ## License diff --git a/docs/src/plugins/docs-data.mjs b/docs/src/plugins/docs-data.mjs new file mode 100644 index 0000000000..e9dcfd3f5f --- /dev/null +++ b/docs/src/plugins/docs-data.mjs @@ -0,0 +1,52 @@ +/** + * Build-time HyperFormula metadata injected into docs content in place of the + * VuePress `{{ $page.version }}`, `{{ $page.buildDate }}`, etc. template vars. + * + * Values are read from the built UMD bundle (`dist/hyperformula.full.js`) when + * available — it exposes `version`, `buildDate`, `releaseDate` and the + * registered-function list. The docs build always runs after `bundle-all`, so + * the bundle is present in CI/production. For local dev without a build, we + * fall back to the repo `package.json` version and the current date. + * + * @module docs-data + */ +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; +import { readFileSync } from 'fs'; + +const require = createRequire(import.meta.url); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); + +/** @returns {{ version: string, buildDate: string, releaseDate: string, functionsCount: number }} */ +function resolveDocsData() { + try { + // The UMD root export is the HyperFormula class with static metadata. + const HyperFormula = require(resolve(repoRoot, 'dist/hyperformula.full.js')); + + return { + version: HyperFormula.version, + buildDate: HyperFormula.buildDate, + releaseDate: HyperFormula.releaseDate, + functionsCount: HyperFormula.getRegisteredFunctionNames('enGB').length, + }; + } catch { + // Fallback for local dev when the library bundle has not been built yet. + let version = 'latest'; + + try { + version = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8')).version; + } catch { + /* ignore */ + } + + return { + version, + buildDate: new Date().toUTCString(), + releaseDate: new Date().toUTCString(), + functionsCount: 400, + }; + } +} + +export const DOCS_DATA = resolveDocsData(); diff --git a/docs/src/plugins/vuepress-preprocessor.mjs b/docs/src/plugins/vuepress-preprocessor.mjs new file mode 100644 index 0000000000..0211419861 --- /dev/null +++ b/docs/src/plugins/vuepress-preprocessor.mjs @@ -0,0 +1,418 @@ +/** + * Pure transform that converts a VuePress-flavoured markdown source string into + * Starlight-native markdown. Run by `scripts/generate-content.mjs` ahead of the + * Astro build (analogous to the TypeDoc API generation step), so Starlight then + * renders asides, tables of contents and links through its native pipeline. + * + * HyperFormula specifics (differs from Handsontable): + * - Guides have NO frontmatter; the page title lives in a body `# H1`. We lift + * it into a `title:` frontmatter field and strip the body H1 (otherwise + * Starlight renders a duplicate H1). + * - Examples are file includes, not inline code: `::: example` wraps + * `@[code](@/docs/examples/.../exampleN.{html,css,js,ts})`. We strip the + * example markers and resolve each include into a fenced code block. + * - Build-time vars `{{ $page.version|buildDate|releaseDate|functionsCount }}` + * are replaced with values from the built library (see docs-data.mjs). + * - Internal `.md` / `.html` / relative links are rewritten to clean, + * base-aware URLs (outside fenced code). + * + * Aside bodies are intentionally left as markdown: native Starlight asides + * render their content through the markdown pipeline, so no inline-markdown + * pre-rendering is needed. + * + * @module vuepress-preprocessor + */ +import { fileURLToPath } from 'url'; +import { dirname, resolve, posix } from 'path'; +import { readFileSync } from 'fs'; +import { DOCS_DATA } from './docs-data.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(__dirname, '../../..'); +const BASE = (process.env.DOCS_BASE || '/docs').replace(/\/+$/, '') || ''; + +const EXT_LANG = { + js: 'js', mjs: 'js', cjs: 'js', ts: 'ts', tsx: 'tsx', jsx: 'jsx', + html: 'html', css: 'css', scss: 'scss', json: 'json', sh: 'bash', bash: 'bash', +}; + +/** + * @param {string} content — raw VuePress markdown + * @param {string} slug — content-relative slug, e.g. "guide/basic-usage" + * @returns {string} Starlight-native markdown + */ +export function preprocessMarkdown(content, slug) { + let result = content; + + result = ensureFrontmatterTitle(result, slug); + // Remove VuePress [[toc]] (Starlight renders a ToC automatically). Drop the + // whole line so a "**Contents:**" style prefix doesn't dangle. + result = result.replace(/^.*\[\[\s*toc\s*\]\].*$/gm, ''); + + // VuePress containers → Starlight asides (warning → caution). + result = result.replace(/^::: tip(?:[ \t]+(.+))?$/gm, (_, l) => (l ? `:::tip[${l}]` : ':::tip')); + result = result.replace(/^::: warning(?:[ \t]+(.+))?$/gm, (_, l) => (l ? `:::caution[${l}]` : ':::caution')); + result = result.replace(/^::: danger(?:[ \t]+(.+))?$/gm, (_, l) => (l ? `:::danger[${l}]` : ':::danger')); + result = result.replace(/^::: note(?:[ \t]+(.+))?$/gm, (_, l) => (l ? `:::note[${l}]` : ':::note')); + + result = convertDetailsContainers(result); + result = transformExamples(result); + result = stripExampleContainers(result); + result = resolveCodeIncludes(result); + + // Env-var placeholders inside (now-inlined) example code. + result = result + .replace(/process\.env\.HT_BUILD_DATE as string/g, `'${DOCS_DATA.buildDate}'`) + .replace(/process\.env\.HT_VERSION as string/g, `'${DOCS_DATA.version}'`) + .replace(/process\.env\.HT_RELEASE_DATE as string/g, `'${DOCS_DATA.releaseDate}'`); + + // VuePress page template vars. + result = result + .replace(/\{\{\s*\$page\.buildDateURIEncoded\s*\}\}/g, encodeURIComponent(DOCS_DATA.buildDate)) + .replace(/\{\{\s*\$page\.version\s*\}\}/g, DOCS_DATA.version) + .replace(/\{\{\s*\$page\.buildDate\s*\}\}/g, DOCS_DATA.buildDate) + .replace(/\{\{\s*\$page\.releaseDate\s*\}\}/g, DOCS_DATA.releaseDate) + .replace(/\{\{\s*\$page\.functionsCount\s*\}\}/g, String(DOCS_DATA.functionsCount)); + + // VuePress <Badge> globals (heavy in generated API). In headings, drop them + // entirely so the heading slug stays the bare member name (matching the old + // VuePress anchors, e.g. #buildfromarray); elsewhere render as a pill. + result = result + .split('\n') + .map((line) => + /^#{1,6}\s/.test(line) + ? line.replace(/\s*<Badge\s+text="[^"]*"[^>]*\/>/g, '') + : line.replace(/<Badge\s+text="([^"]*)"[^>]*\/>/g, '<span class="hf-badge">$1</span>') + ) + .join('\n'); + + // VuePress `$withBase('/x')` asset helper → base-aware path, and Vue `:src` + // bindings on raw <img> tags → plain `src` attributes. + result = result.replace( + /\$withBase\(\s*['"]([^'"]+)['"]\s*\)/g, + (_m, p) => `${BASE}${p.startsWith('/') ? p : `/${p}`}` + ); + result = result.replace(/<img\s+:src=/g, '<img src='); + + result = rewriteLinks(result, slug); + + return result; +} + +/** Make a path slug-safe and predictable (dots → hyphens), matching the generator. */ +export function slugifyPath(p) { + return p + .split('/') + .map((seg) => seg.replace(/\./g, '-')) + .join('/'); +} + +const BADGE_TOKENS = + '(static|readonly|optional|const|abstract|namespace|let|class|interface|enumeration)'; +const BADGE_PREFIX_RE = new RegExp(`^#${BADGE_TOKENS}-`, 'i'); +const BADGE_SUFFIX_RE = new RegExp(`-${BADGE_TOKENS}$`, 'i'); + +/** + * Normalize an anchor to match what rehype-slug (Starlight) generates. + * - Strip TypeDoc badge tokens (Static/Readonly/Optional/…) at either end. + * - Drop VuePress's `_<digit>` numeric-heading prefix (`#_3-x` → `#3-x`), + * since rehype-slug doesn't add the leading underscore. + */ +function cleanBadgeAnchor(anchor) { + if (!anchor) return anchor; + return anchor + .replace(BADGE_PREFIX_RE, '#') + .replace(BADGE_SUFFIX_RE, '') + .replace(/^#_(\d)/, '#$1'); +} + +/** Lift the first body `# H1` into a `title:` frontmatter field and strip it. */ +function ensureFrontmatterTitle(content, slug) { + let frontmatter = null; + let body = content; + const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + + if (fm) { + frontmatter = fm[1]; + body = content.slice(fm[0].length); + } + + const hasTitle = frontmatter !== null && /^title\s*:/m.test(frontmatter); + + const h1 = body.match(/^[ \t]*#[ \t]+(.+?)[ \t]*$/m); + let extracted = null; + + if (h1) { + extracted = cleanTitleText(h1[1]); + body = body.slice(0, h1.index) + body.slice(h1.index + h1[0].length); + body = body.replace(/^\r?\n/, ''); + } + + if (hasTitle) { + return `---\n${frontmatter}\n---\n${body}`; + } + + const title = extracted || fallbackTitle(slug); + const titleLine = `title: ${yamlQuote(title)}`; + + return frontmatter !== null + ? `---\n${titleLine}\n${frontmatter}\n---\n${body}` + : `---\n${titleLine}\n---\n\n${body}`; +} + +function cleanTitleText(s) { + return s + .replace(/<[^>]+>/g, '') // strip HTML (e.g. TypeDoc <Badge text="Class"/>) + .replace(/`/g, '') + .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') + .replace(/\*\*/g, '') + .replace(/[*_]/g, '') + .trim(); +} + +function yamlQuote(s) { + return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; +} + +function fallbackTitle(slug) { + const last = slug.split('/').pop() || slug; + + return last.replace(/[-_]/g, ' ').replace(/^\w/, (c) => c.toUpperCase()); +} + +/** + * Convert a VuePress `::: example #id` block (which wraps `@[code]` includes for + * the demo's html/css/js/ts) into: a live-preview container the client runner + * hydrates (`data-example-js` points at the runnable module), the demo CSS, and + * a collapsible "Source code" block. The runner lives in `scripts/example-runner.ts`. + */ +function transformExamples(content) { + return content.replace( + /^::: example\s+#(\S+)[^\n]*\n([\s\S]*?)\n:::[ \t]*$/gm, + (_full, id, inner) => { + const files = {}; + + for (const m of inner.matchAll(/@\/docs\/examples\/[^\s)]+\.(html|css|js|ts)/g)) { + files[m[1]] = m[0]; + } + + const readAlias = (alias) => { + if (!alias) return ''; + + try { + return readFileSync(resolve(REPO_ROOT, alias.replace(/^@\//, '')), 'utf8').replace(/\s+$/, ''); + } catch { + // eslint-disable-next-line no-console + console.warn(`[generate-content] example file missing: ${alias}`); + return ''; + } + }; + + const html = readAlias(files.html); + const css = readAlias(files.css); + // The runner imports the .js by web path (Vite project root = docs/). + const jsWebPath = files.js ? files.js.replace(/^@\/docs/, '') : ''; + // Displayed source: drop the skip-in-compilation boilerplate (the import/log). + const jsSrc = readAlias(files.js).replace( + /\/\*\s*start:skip-in-compilation\s*\*\/[\s\S]*?\/\*\s*end:skip-in-compilation\s*\*\/\s*/g, + '' + ).trim(); + + // A blank line ends a raw-HTML block in markdown, so the preview wrapper + + // style + demo HTML must be one contiguous block — collapse blank lines. + const collapse = (s) => s.replace(/\n([ \t]*\n)+/g, '\n'); + const styleBlock = css ? `<style>\n${collapse(css)}\n</style>\n` : ''; + const previewOpen = `<div class="hf-example__preview"${jsWebPath ? ` data-example-js="${jsWebPath}"` : ''}>`; + + let out = + '<div class="hf-example not-content">\n' + + styleBlock + + `${previewOpen}\n${collapse(html)}\n</div>\n</div>`; + + if (jsSrc) { + // Inside <details>, blank lines switch between raw HTML and the markdown + // code fence (so Expressive Code renders it). + out += `\n\n<details class="hf-example__source">\n<summary>Source code</summary>\n\n\`\`\`js\n${jsSrc}\n\`\`\`\n\n</details>`; + } + + return out; + } + ); +} + +/** Strip `::: example` / `::: example-without-tabs` markers, keep inner content. */ +function stripExampleContainers(content) { + const lines = content.split('\n'); + const result = []; + let depth = 0; + + for (const line of lines) { + if (/^:::\s*(example|example-without-tabs)(\s|$)/.test(line)) { + depth++; + continue; + } + + if (depth > 0 && /^:::\s*$/.test(line)) { + depth--; + continue; + } + + result.push(line); + } + + return result.join('\n'); +} + +/** Resolve `@[code](path)` / `@[code](highlight=…)(path)` includes to fenced code. */ +function resolveCodeIncludes(content) { + return content.replace(/@\[code\]\(([^)]*)\)(?:\(([^)]*)\))?/g, (_full, g1, g2) => { + const hasOpts = g2 !== undefined; + const opts = hasOpts ? g1 : ''; + const rawPath = (hasOpts ? g2 : g1).trim(); + + let filePath; + + if (rawPath.startsWith('@/')) filePath = resolve(REPO_ROOT, rawPath.slice(2)); + else if (rawPath.startsWith('/')) filePath = resolve(REPO_ROOT, rawPath.slice(1)); + else filePath = resolve(REPO_ROOT, rawPath); + + const ext = (rawPath.split('.').pop() || '').toLowerCase(); + const lang = EXT_LANG[ext] || ext || ''; + const hl = opts.match(/highlight=([0-9,\-]+)/); + const meta = hl ? ` {${hl[1]}}` : ''; + + let code; + + try { + code = readFileSync(filePath, 'utf8').replace(/\s+$/, ''); + } catch { + // eslint-disable-next-line no-console + console.warn(`[generate-content] missing @[code] include: ${rawPath}`); + return ''; + } + + return `\`\`\`${lang}${meta}\n${code}\n\`\`\``; + }); +} + +/** `::: details Title` → `<details><summary>Title</summary>…</details>`. */ +function convertDetailsContainers(content) { + const lines = content.split('\n'); + const result = []; + let depth = 0; + + for (const line of lines) { + const open = line.match(/^:{3,}\s+details\s+(.+)$/); + + if (open) { + depth++; + result.push('<details>', `<summary>${open[1].trim()}</summary>`, ''); + continue; + } + + if (depth > 0 && /^:{3,}\s*$/.test(line)) { + depth--; + result.push('', '</details>'); + continue; + } + + result.push(line); + } + + return result.join('\n'); +} + +/** Rewrite internal `.md`/`.html`/relative links to clean, base-aware URLs. */ +function rewriteLinks(content, slug) { + const lines = content.split('\n'); + let inFence = false; + let inFrontmatter = lines[0] === '---'; + + return lines + .map((line, i) => { + if (inFrontmatter) { + if (i > 0 && /^---\s*$/.test(line)) inFrontmatter = false; + return line; + } + + if (/^\s*(```|~~~)/.test(line)) { + inFence = !inFence; + return line; + } + + if (inFence) return line; + + return rewriteInlineLinks(line, slug); + }) + .join('\n'); +} + +function rewriteInlineLinks(line, slug) { + const codeSpans = []; + let masked = line.replace(/`[^`]*`/g, (m) => { + codeSpans.push(m); + return ` �${codeSpans.length - 1}� `; + }); + + masked = masked.replace( + /(!?)\[([^\]]*)\]\(([^)\s]+)(\s+"[^"]*")?\)/g, + (full, bang, text, href, title) => { + const rewritten = rewriteHref(href, slug, bang === '!'); + + return rewritten === null ? full : `${bang}[${text}](${rewritten}${title || ''})`; + } + ); + + return masked.replace(/ �(\d+)� /g, (_m, i) => codeSpans[Number(i)]); +} + +function rewriteHref(href, slug, isImage) { + if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//') || href.startsWith('#')) { + return null; + } + + // Images: only fix root-absolute asset paths to be base-aware. + if (isImage) { + return href.startsWith('/') ? `${BASE}${href}` : null; + } + + const hashIdx = href.indexOf('#'); + let pathPart = hashIdx >= 0 ? href.slice(0, hashIdx) : href; + const anchor = hashIdx >= 0 ? href.slice(hashIdx) : ''; + + if (pathPart === '') return null; + + const isDocLink = + /\.(md|html)$/i.test(pathPart) || + pathPart.startsWith('./') || + pathPart.startsWith('../') || + pathPart.startsWith('/') || + /^(guide|api)(\/|$)/.test(pathPart); // bare relative doc links, e.g. api/ or guide/x + + if (!isDocLink) return null; + + pathPart = pathPart.replace(/\.(md|html)$/i, ''); + + let absSlug; + + if (pathPart.startsWith('/')) { + absSlug = pathPart.replace(/^\/+/, ''); + } else { + const dir = slug.includes('/') ? slug.slice(0, slug.lastIndexOf('/')) : ''; + absSlug = posix.normalize(posix.join(dir, pathPart)); + } + + if (absSlug.startsWith('..')) return null; + + absSlug = absSlug.replace(/\/?(index|README)$/i, ''); + absSlug = slugifyPath(absSlug); + + const out = `${BASE}/${absSlug}`.replace(/\/+$/, '') || `${BASE}/`; + + // Strip TypeDoc badge tokens from anchors. Our heading transform removes the + // `<Badge text="Static"/>` etc. from API headings, so the rendered heading id + // is just the member name. TypeDoc still generates cross-link anchors with + // the badge text baked in (either as a `static-X` prefix or `X-static` + // suffix). Normalize both forms to match our clean heading ids. + return `${out}${cleanBadgeAnchor(anchor)}`; +} diff --git a/docs/src/scripts/example-runner.ts b/docs/src/scripts/example-runner.ts new file mode 100644 index 0000000000..0e57d0116f --- /dev/null +++ b/docs/src/scripts/example-runner.ts @@ -0,0 +1,47 @@ +/** + * Mounts the interactive HyperFormula demos. + * + * The content preprocessor injects each demo's HTML into a + * `.hf-example__preview[data-example-js]` container. The runnable example module + * lives under `docs/examples/**` and is resolved at build time via Vite's + * `import.meta.glob`. Importing the module executes its top-level code, which + * wires up the injected DOM. + * + * Each demo runs in isolation (try/catch) so one broken example never blocks the + * rest of the page. + */ +const modules = import.meta.glob('/examples/**/*.js'); + +function runExamples(): void { + const containers = document.querySelectorAll<HTMLElement>('.hf-example__preview[data-example-js]'); + + containers.forEach((el) => { + const path = el.dataset.exampleJs; + + if (!path) return; + + const loader = modules[path]; + + if (!loader) { + // eslint-disable-next-line no-console + console.warn(`[example-runner] no module found for ${path}`); + return; + } + + el.classList.add('is-loading'); + + Promise.resolve(loader()) + .then(() => el.classList.remove('is-loading')) + .catch((err) => { + el.classList.remove('is-loading'); + // eslint-disable-next-line no-console + console.error(`[example-runner] failed to run ${path}`, err); + }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', runExamples); +} else { + runExamples(); +} diff --git a/docs/src/scripts/theme-toggle.ts b/docs/src/scripts/theme-toggle.ts new file mode 100644 index 0000000000..f12011a038 --- /dev/null +++ b/docs/src/scripts/theme-toggle.ts @@ -0,0 +1,36 @@ +/** + * Sun/moon theme toggle wiring. Bound by ThemeSelect.astro, which renders the + * `[data-hf-theme-toggle]` button. Writes to Starlight's `starlight-theme` + * localStorage key so the choice persists across page loads. + */ +const STORAGE_KEY = 'starlight-theme'; +const root = document.documentElement; + +function currentTheme(): 'light' | 'dark' { + return root.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; +} + +function setTheme(next: 'light' | 'dark'): void { + root.setAttribute('data-theme', next); + try { + localStorage.setItem(STORAGE_KEY, next); + } catch { + /* private mode etc. */ + } +} + +function wire(): void { + document.querySelectorAll<HTMLButtonElement>('[data-hf-theme-toggle]').forEach((btn) => { + if (btn.dataset.hfBound === 'true') return; + btn.dataset.hfBound = 'true'; + btn.addEventListener('click', () => { + setTheme(currentTheme() === 'dark' ? 'light' : 'dark'); + }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', wire); +} else { + wire(); +} diff --git a/docs/src/sidebar.mjs b/docs/src/sidebar.mjs new file mode 100644 index 0000000000..c5ea9c88f6 --- /dev/null +++ b/docs/src/sidebar.mjs @@ -0,0 +1,126 @@ +/** + * Starlight sidebar navigation for the HyperFormula docs. + * + * Ported from the VuePress `themeConfig.sidebar` tree. Links are site-root + * relative; Starlight prepends the configured `base` (`/docs`) automatically. + * The API Reference group is auto-generated from the `api/` content directory + * (populated by TypeDoc). + */ +export const sidebar = [ + { + label: 'Introduction', + items: [ + { label: 'Welcome', link: '/' }, + { label: 'Demo', link: '/guide/demo' }, + ], + }, + { + label: 'Getting started', + items: [ + { label: 'Client-side installation', link: '/guide/client-side-installation' }, + { label: 'Server-side installation', link: '/guide/server-side-installation' }, + { label: 'Basic usage', link: '/guide/basic-usage' }, + { label: 'Advanced usage', link: '/guide/advanced-usage' }, + { label: 'Configuration options', link: '/guide/configuration-options' }, + { label: 'License key', link: '/guide/license-key' }, + ], + }, + { + label: 'Integrations', + items: [ + { label: 'Integration with React', link: '/guide/integration-with-react' }, + { label: 'Integration with Vue', link: '/guide/integration-with-vue' }, + { label: 'Integration with Angular', link: '/guide/integration-with-angular' }, + { label: 'Integration with Svelte', link: '/guide/integration-with-svelte' }, + { label: 'Integration with Vercel AI SDK', link: '/guide/ai-sdk' }, + { label: 'Integration with LangChain', link: '/guide/integration-with-langchain' }, + { label: 'HyperFormula MCP Server', link: '/guide/mcp-server' }, + ], + }, + { + label: 'Data operations', + items: [ + { label: 'Basic operations', link: '/guide/basic-operations' }, + { label: 'Batch operations', link: '/guide/batch-operations' }, + { label: 'Clipboard operations', link: '/guide/clipboard-operations' }, + { label: 'Undo-redo', link: '/guide/undo-redo' }, + { label: 'Sorting data', link: '/guide/sorting-data' }, + ], + }, + { + label: 'Formulas', + items: [ + { label: 'Specifications and limits', link: '/guide/specifications-and-limits' }, + { label: 'Cell references', link: '/guide/cell-references' }, + { label: 'Types of values', link: '/guide/types-of-values' }, + { label: 'Types of errors', link: '/guide/types-of-errors' }, + { label: 'Types of operators', link: '/guide/types-of-operators' }, + { label: 'Order of precedence', link: '/guide/order-of-precendece' }, + { label: 'Built-in functions', link: '/guide/built-in-functions' }, + { label: 'Volatile functions', link: '/guide/volatile-functions' }, + { label: 'Named expressions', link: '/guide/named-expressions' }, + { label: 'Array formulas', link: '/guide/arrays' }, + ], + }, + { + label: 'Internationalization', + items: [ + { label: 'Internationalization features', link: '/guide/i18n-features' }, + { label: 'Localizing functions', link: '/guide/localizing-functions' }, + { label: 'Date and time handling', link: '/guide/date-and-time-handling' }, + ], + }, + { + label: 'Compatibility', + items: [ + { label: 'Compatibility with Microsoft Excel', link: '/guide/compatibility-with-microsoft-excel' }, + { label: 'Compatibility with Google Sheets', link: '/guide/compatibility-with-google-sheets' }, + { label: 'Runtime differences with Microsoft Excel and Google Sheets', link: '/guide/list-of-differences' }, + ], + }, + { + label: 'Advanced topics', + items: [ + { label: 'Key concepts', link: '/guide/key-concepts' }, + { label: 'Dependency graph', link: '/guide/dependency-graph' }, + { label: 'Building & testing', link: '/guide/building' }, + { label: 'Custom functions', link: '/guide/custom-functions' }, + { label: 'Performance', link: '/guide/performance' }, + { label: 'Known limitations', link: '/guide/known-limitations' }, + { label: 'File import', link: '/guide/file-import' }, + ], + }, + { + label: 'Upgrade and migration', + items: [ + { label: 'Release notes', link: '/guide/release-notes' }, + { label: 'Migrating from 0.6 to 1.0', link: '/guide/migration-from-0-6-to-1-0' }, + { label: 'Migrating from 1.x to 2.0', link: '/guide/migration-from-1-x-to-2-0' }, + { label: 'Migrating from 2.x to 3.0', link: '/guide/migration-from-2-x-to-3-0' }, + ], + }, + { + label: 'About', + items: [ + { label: 'Quality', link: '/guide/quality' }, + { label: 'Supported browsers', link: '/guide/supported-browsers' }, + { label: 'Dependencies', link: '/guide/dependencies' }, + { label: 'Licensing', link: '/guide/licensing' }, + { label: 'Support', link: '/guide/support' }, + ], + }, + { + label: 'Miscellaneous', + items: [ + { label: 'Contributing', link: '/guide/contributing' }, + { label: 'Code of conduct', link: '/guide/code-of-conduct' }, + { label: 'Branding', link: '/guide/branding' }, + { label: 'Contact', link: '/guide/contact' }, + ], + }, + { + label: 'API Reference', + collapsed: true, + autogenerate: { directory: 'api' }, + }, +]; diff --git a/docs/src/styles/base/variables.css b/docs/src/styles/base/variables.css new file mode 100644 index 0000000000..ecd51f2cb0 --- /dev/null +++ b/docs/src/styles/base/variables.css @@ -0,0 +1,107 @@ +/* + * HyperFormula docs theme tokens — matches the Handsontable docs palette + * exactly: oklch neutral grey scale, soft (non-white) body text, and the + * Starlight text-invert override that disables Starlight's "active item = + * inverted text on accent bg" behavior. Hex fallback covered for browsers + * without oklch via @supports. + */ + +:root { + /* Brand blue — used sparingly */ + --sl-color-accent-low: #1a3a6b; + --sl-color-accent: #2856ea; + --sl-color-accent-high: #a8c8ff; + + /* Typography */ + --sl-font: 'Inter', system-ui, -apple-system, sans-serif; + --sl-text-base: 1rem; + --sl-line-height: 1.7; + --sl-nav-height: 6.625rem; + --sl-content-gap-y: 1.25rem; + + /* Neutral grey scale — oklch, no blue cast */ + --sl-color-white: oklch(90% 0 0); + --sl-color-gray-1: oklch(80% 0 0); + --sl-color-gray-2: oklch(72% 0 0); + --sl-color-gray-3: oklch(65% 0 0); + --sl-color-gray-4: oklch(48% 0 0); + --sl-color-gray-5: oklch(29% 0 0); + --sl-color-gray-6: oklch(23% 0 0); + --sl-color-gray-7: oklch(20.5% 0 0); + --sl-color-gray-8: oklch(17% 0 0); + --sl-color-gray-9: oklch(14% 0 0); + --sl-color-black: oklch(11% 0 0); + + /* Text / surfaces. Page, header and sidebar share the same near-black canvas + * (Handsontable pattern). Code/aside surfaces sit on the slightly darker + * `block-bg` for inset depth. */ + --sl-color-text: var(--sl-color-gray-2); + --sl-color-text-accent: var(--sl-color-accent-low); + --sl-color-text-invert: var(--sl-color-text); + --sl-color-bg: var(--sl-color-black); + --sl-color-bg-nav: var(--sl-color-black); + --sl-color-bg-sidebar: var(--sl-color-black); + --sl-color-bg-inline-code: var(--sl-color-gray-7); + --sl-color-backdrop-overlay: oklch(24% 0 0 / 0.7); + --sl-color-hover-bg: oklch(23% 0 0); + --sl-color-block-bg: #121212; + + --hf-radius: 6px; +} + +:root[data-theme='light'] { + --sl-color-accent-low: #e6f3ff; + --sl-color-accent: #1a42e8; + --sl-color-accent-high: #0d47a1; + + --sl-color-white: oklch(30% 0 0); + --sl-color-gray-1: oklch(36% 0 0); + --sl-color-gray-2: oklch(36% 0 0); + --sl-color-gray-3: oklch(44% 0 0); + --sl-color-gray-4: oklch(82% 0 0); + --sl-color-gray-5: oklch(92% 0 0); + --sl-color-gray-6: oklch(94% 0 0); + --sl-color-gray-7: oklch(96% 0 0); + --sl-color-black: oklch(98% 0 0); + + --sl-color-backdrop-overlay: oklch(84% 0 0 / 0.7); + --sl-color-hover-bg: oklch(96% 0 0); + --sl-color-text-accent: var(--sl-color-accent-low); + --sl-color-text-invert: var(--sl-color-text); + --sl-color-bg: var(--sl-color-black); + --sl-color-bg-nav: var(--sl-color-black); + --sl-color-bg-sidebar: var(--sl-color-black); + --sl-color-block-bg: #f5f5f5; +} + +/* Hex fallback for browsers without oklch (Safari <15.4, Chrome <111, FF <113). */ +@supports not (color: oklch(0% 0 0)) { + :root { + --sl-color-white: #e6e6e6; + --sl-color-gray-1: #cccccc; + --sl-color-gray-2: #b8b8b8; + --sl-color-gray-3: #a0a0a0; + --sl-color-gray-4: #707070; + --sl-color-gray-5: #3b3b3b; + --sl-color-gray-6: #2e2e2e; + --sl-color-gray-7: #292929; + --sl-color-gray-8: #222222; + --sl-color-gray-9: #1c1c1c; + --sl-color-black: #161616; + --sl-color-hover-bg: #3b3b3b; + --sl-color-backdrop-overlay: rgba(28, 28, 28, 0.7); + } + + :root[data-theme='light'] { + --sl-color-white: #404040; + --sl-color-gray-1: #595959; + --sl-color-gray-2: #595959; + --sl-color-gray-3: #6e6e6e; + --sl-color-gray-4: #cfcfcf; + --sl-color-gray-5: #e6e6e6; + --sl-color-gray-6: #ededed; + --sl-color-gray-7: #f4f4f4; + --sl-color-black: #fafafa; + --sl-color-hover-bg: #f4f4f4; + } +} diff --git a/docs/src/styles/components/content.css b/docs/src/styles/components/content.css new file mode 100644 index 0000000000..21e0402754 --- /dev/null +++ b/docs/src/styles/components/content.css @@ -0,0 +1,186 @@ +/* Content-area component styles. */ + +/* + * Match Handsontable's wider canvas on big screens. Starlight's `.page` wraps + * the entire layout (header + sidebar + content + footer); capping it to + * 1500px and centering keeps line length pleasant without feeling cramped. + */ +.page { + max-width: 1500px; + margin-inline: auto; +} + +/* + * Starlight's sidebar uses `position: fixed; left: 0` so it stays put while + * scrolling. That detaches it from the centered `.page`, leaving a big gap + * between the sidebar (glued to viewport edge) and the content (centered) on + * wide screens. Slide the fixed sidebar in by the same horizontal margin + * `.page` has so the sidebar sits flush against the content again. + */ +@media (min-width: 1500px) { + .sidebar-pane { + left: calc((100vw - 1500px) / 2) !important; + } +} + +/* + * Diagrams (the high-level design diagram on "Key concepts", dependency-graph + * ranges, etc.) are SVG/PNG drawn for a light canvas — their connector lines + * are near-black and disappear on the dark theme background. Give *local* + * content images a light backing in dark mode so they stay legible. External + * images (shields/Snyk/FOSSA/codecov badges, the logo) are excluded so they + * render on their own background instead of in a white chip. + */ +:root[data-theme='dark'] .sl-markdown-content img:not([src^='http']) { + background: #fff; + border-radius: var(--hf-radius, 8px); + padding: 0.75rem; +} + +/* + * Badges, logos and other external images keep their own (transparent) + * background — never the diagram white canvas — so the homepage badge row + * stays clean in dark mode. + */ +.sl-markdown-content img[src^='http'] { + background: transparent; + padding: 0; +} + +/* + * Centered HTML paragraphs (homepage logo, tagline, badge row) lay out as a + * wrapping flex row so the shields/Snyk/FOSSA/codecov badges sit in neat, + * spaced rows instead of a ragged stack. + */ +.sl-markdown-content p[align='center'] { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 0.4rem 0.5rem; +} + +.sl-markdown-content p[align='center'] br { + display: none; +} + +/* TypeDoc/VuePress <Badge text="…"/> flags (Static, Readonly, Optional, …). */ +.hf-badge { + display: inline-block; + margin-inline-start: 0.4em; + padding: 0.05em 0.55em; + border-radius: 999px; + font-size: 0.7em; + font-weight: 600; + line-height: 1.6; + vertical-align: middle; + background: var(--sl-color-accent-low); + color: var(--sl-color-accent-high); +} + +/* <details> blocks converted from VuePress `::: details`. */ +.sl-markdown-content details { + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--hf-radius, 8px); + padding: 0.5rem 1rem; + margin-block: 1rem; +} + +.sl-markdown-content details > summary { + cursor: pointer; + font-weight: 600; +} + +/* + * Sidebar styling — mirrors Handsontable's docs sidebar: + * - Group chevron sits to the LEFT of the label (not the right). + * - Active item: brand-blue text + a full-height, non-rounded brand-blue + * left rail, no filled background. + * - Group labels remain bold neutral white. + */ + +/* Move the disclosure chevron to the left of the group label and tighten the + * gap. starlight-theme-rapide sets `flex-direction: row-reverse` on summary + * to put its chevron on the right — we force `row` here and use `order: -1` + * on the chevron so it consistently lands BEFORE the label (Handsontable + * pattern). */ +.sidebar details > summary, +nav.sidebar-content details > summary, +.sidebar-content details > summary { + flex-direction: row !important; + justify-content: flex-start; + gap: 0.5rem; + font-weight: 700; + color: var(--sl-color-white); +} + +.sidebar details > summary > svg.caret, +nav.sidebar-content details > summary > svg.caret, +.sidebar-content details > summary > svg.caret { + order: -1; + margin-inline: 0 0.25rem; + color: var(--sl-color-gray-3); +} + +/* Active link: brand-blue text on a transparent surface. The rail is drawn by + * Starlight on the parent <li> (rule below) so it sits flush at the sidebar + * gutter — matching Handsontable. */ +:where(starlight-menu-button) ~ nav a[aria-current='page'], +.sidebar a[aria-current='page'], +nav.sidebar-content a[aria-current='page'] { + background: transparent !important; + color: var(--sl-color-accent) !important; + font-weight: 600; + border-radius: 0 !important; + box-shadow: none !important; +} + +/* + * Starlight already paints a 1px accent-high border-inline-start on the LI + * wrapping the active link. Re-color it to a saturated 3px brand-blue rail so + * the gutter shows a single, clean indicator (no second hairline beside it). + */ +.sidebar li:has(> a[aria-current='page']), +nav.sidebar-content li:has(> a[aria-current='page']) { + border-inline-start: 3px solid var(--sl-color-accent) !important; +} + +/* + * Right-hand "On this page" ToC active item — Starlight defaults it to + * `--sl-color-text-accent` (= accent-low = #1a3a6b in our palette), which is + * unreadable navy on dark. Use the saturated brand blue + a subtle left rail + * to match the main sidebar's treatment. + */ +starlight-toc a[aria-current='true'], +starlight-toc nav a[aria-current='true'] { + color: var(--sl-color-accent) !important; + border-inline-start-color: var(--sl-color-accent) !important; + font-weight: 600; +} + +/* + * Slim themed scrollbar — matches Handsontable's 4px-wide, 2px-radius look. + * Firefox uses `scrollbar-*`; Chromium/WebKit use `::-webkit-scrollbar-*`. + */ +* { + scrollbar-width: thin; + scrollbar-color: var(--sl-color-gray-5) transparent; +} + +*::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: var(--sl-color-gray-5); + border-radius: 2px; +} + +*::-webkit-scrollbar-thumb:hover { + background-color: var(--sl-color-gray-4); +} diff --git a/docs/src/styles/components/footer.css b/docs/src/styles/components/footer.css new file mode 100644 index 0000000000..7c78168d44 --- /dev/null +++ b/docs/src/styles/components/footer.css @@ -0,0 +1,105 @@ +/* HT-style spreadsheet-grid footer + social row. */ + +.hf-footer { + margin-top: 4rem; + font-family: var(--sl-font); + font-size: 0.875rem; + color: var(--sl-color-gray-3); +} + +.hf-footer__grid { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(6, 1fr); + border-top: 1px solid var(--sl-color-hairline-light); + border-bottom: 1px solid var(--sl-color-hairline-light); +} + +.hf-footer__cell { + list-style: none; + border-inline-end: 1px solid var(--sl-color-hairline-light); +} + +.hf-footer__cell:last-child { + border-inline-end: none; +} + +.hf-footer__cell > a { + display: block; + padding: 1rem; + color: var(--sl-color-gray-2); + text-decoration: none; + transition: color 120ms, background 120ms; +} + +.hf-footer__cell > a:hover { + color: var(--sl-color-white); + background: var(--sl-color-gray-6); +} + +.hf-footer__bottom { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem; +} + +.hf-footer__social { + list-style: none; + margin: 0; + padding: 0; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.hf-footer__social li { + list-style: none; +} + +.hf-footer__social a { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 999px; + color: var(--sl-color-gray-3); + border: 1px solid var(--sl-color-hairline-light); + transition: color 120ms, border-color 120ms, background 120ms; +} + +.hf-footer__social a:hover { + color: var(--sl-color-white); + border-color: var(--sl-color-gray-4); + background: var(--sl-color-gray-6); +} + +.hf-footer__social svg { + width: 0.95rem; + height: 0.95rem; +} + +.hf-footer__copy { + margin: 0; + font-size: 0.8rem; + color: var(--sl-color-gray-3); +} + +@media (max-width: 50rem) { + .hf-footer__grid { + grid-template-columns: repeat(3, 1fr); + } + + .hf-footer__cell:nth-child(3n) { + border-inline-end: none; + } + + .hf-footer__bottom { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/docs/src/styles/components/header.css b/docs/src/styles/components/header.css new file mode 100644 index 0000000000..e78e57cb55 --- /dev/null +++ b/docs/src/styles/components/header.css @@ -0,0 +1,283 @@ +/* + * Two-row header layout (mirrors Handsontable's docs header): + * Row 1: logo + version | search | stars + theme toggle + * Row 2: primary nav | support dropdown + * + * Rows are `auto`-sized and each row carries its own vertical padding so + * controls (search, theme toggle, nav links) sit on a centered baseline with + * breathing room above and below — no element hugs the row-divider. + */ + +.hf-header { + display: grid; + grid-template-rows: auto auto; + height: 100%; + width: 100%; + /* On wide screens, keep the header content (brand · search · actions) within + * the same 1500px canvas as `.page`, so the search bar stays visually + * centered and the stars/theme controls don't drift to the far right edge. + * The outer `header.header` keeps a full-width dark surface so the bar still + * reads as a continuous strip behind the centered content. */ + max-width: 1500px; + margin-inline: auto; + font-family: var(--sl-font); +} + +/* + * starlight-theme-rapide sets the Starlight header to a translucent + * `--sl-rapide-ui-header-bg-color` with a backdrop blur. That lets the + * lighter content area below tint the top of the bar, making the brand row + * look slightly lighter than the sidebar/body. Force the header to a solid + * black surface and disable the backdrop filter so the whole chrome reads + * as one uniform dark canvas. + */ +:root { + --sl-rapide-ui-header-bg-color: var(--sl-color-black); +} + +header.header { + background: var(--sl-color-black) !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; +} + +.hf-header__row { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 1rem; + min-width: 0; + min-height: 3rem; +} + +/* + * No border-bottom here on purpose — the framed-tab strip below already + * supplies the visual divider via its 1px gray-5 border-top, so an additional + * row-1 hairline would double-stack into a 2px line. + */ + +/* — Brand (logo + version pill) — */ +.hf-brand { + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: inherit; + flex-shrink: 0; +} + +.hf-brand__logo { + height: 1.4rem; + width: auto; + display: block; +} + +.hf-brand__version { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.02em; + padding: 0.15em 0.55em; + border-radius: 999px; + color: var(--sl-color-gray-2); + background: var(--sl-color-gray-6); + border: 1px solid var(--sl-color-hairline-light); +} + +/* — Search slot — */ +.hf-header__search { + flex: 1 1 auto; + display: flex; + justify-content: center; + min-width: 0; +} + +.hf-header__search :global(button[data-open-modal]), +.hf-header__search > * { + width: 100%; + max-width: 28rem; +} + +/* — Right group on row 1 — */ +.hf-header__actions { + display: inline-flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} + +.hf-stars { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.6rem; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + text-decoration: none; + color: var(--sl-color-gray-2); + border: 1px solid var(--sl-color-hairline-light); + background: transparent; + transition: color 120ms, border-color 120ms, background 120ms; +} + +.hf-stars:hover { + color: var(--sl-color-white); + border-color: var(--sl-color-gray-4); + background: var(--sl-color-gray-6); +} + +.hf-stars__icon { + width: 0.95rem; + height: 0.95rem; +} + +.hf-stars__count { + font-variant-numeric: tabular-nums; +} + +/* — Primary nav (row 2) — */ +.hf-header__row--nav { + background: var(--sl-color-bg-nav); +} + +.hf-nav { + display: inline-flex; + align-items: stretch; + gap: 0; + flex: 1 1 auto; + min-width: 0; + /* Must stay `visible` (NOT auto/hidden) — the Support dropdown lives inside + * this strip and its absolutely-positioned menu drops below the nav row, + * so any overflow clip on the nav crops the menu to a sliver. The mobile + * breakpoint hides the whole row, so nothing to scroll horizontally. */ + overflow: visible; +} + +/* + * Connected framed-tab strip (mirrors Handsontable's .nav-link rule). Each tab + * has a 1px top + right hairline; the leftmost tab also gets a left hairline, + * so the row reads as a single segmented bar. The active tab's frame turns + * brand blue and lifts via z-index so its border overlays its neighbours. + */ +.hf-nav__link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border: 1px solid transparent; + border-top-color: var(--sl-color-gray-5); + border-right-color: var(--sl-color-gray-5); + border-radius: 0; + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + color: var(--sl-color-gray-2); + background: transparent; + white-space: nowrap; + position: relative; + transition: color 0.15s, border-color 0.15s, background 0.15s; +} + +.hf-nav__link:first-child { + border-left-color: var(--sl-color-gray-5); +} + +.hf-nav__link:hover { + color: var(--sl-color-white); + background: var(--sl-color-gray-7); +} + +.hf-nav__link[aria-current='page'], +.hf-nav__link[aria-current='page']:first-child { + color: var(--sl-color-white); + font-weight: 600; + border-color: var(--sl-color-accent); + background: var(--sl-color-gray-7); + z-index: 1; +} + +.hf-nav__icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +/* — Support dropdown — sits inside `.hf-nav` so its button inherits the + * framed-tab look from `.hf-nav__link`. We only add button-specific bits and + * the open-state border treatment. */ +.hf-support { + position: relative; + display: inline-flex; + align-items: stretch; +} + +.hf-support__btn { + font-family: inherit; + cursor: pointer; +} + +.hf-support__btn[aria-expanded='true'] { + color: var(--sl-color-white); + background: var(--sl-color-gray-7); + border-color: var(--sl-color-accent); + z-index: 1; +} + +.hf-support__chev { + width: 0.7rem; + height: 0.7rem; + margin-inline-start: 0.1rem; + transition: transform 0.15s; +} + +.hf-support__btn[aria-expanded='true'] .hf-support__chev { + transform: rotate(180deg); +} + +.hf-support__menu { + position: absolute; + right: 0; + top: calc(100% + 0.25rem); + min-width: 14rem; + margin: 0; + padding: 0.35rem; + list-style: none; + background: var(--sl-color-bg); + border: 1px solid var(--sl-color-hairline-light); + border-radius: var(--hf-radius, 6px); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4); + z-index: 100; +} + +.hf-support__menu li { + list-style: none; +} + +.hf-support__menu a { + display: block; + padding: 0.5rem 0.7rem; + border-radius: 4px; + font-size: 0.875rem; + color: var(--sl-color-gray-2); + text-decoration: none; +} + +.hf-support__menu a:hover { + background: var(--sl-color-gray-6); + color: var(--sl-color-white); +} + +/* — Responsive: collapse nav row on small screens (Row 1 keeps essentials) — */ +@media (max-width: 50rem) { + .hf-header { + grid-template-rows: auto; + } + + .hf-header__row--nav { + display: none; + } + + .hf-brand__version { + display: none; + } +} diff --git a/docs/src/styles/components/interactive-example.css b/docs/src/styles/components/interactive-example.css new file mode 100644 index 0000000000..1891cc63a9 --- /dev/null +++ b/docs/src/styles/components/interactive-example.css @@ -0,0 +1,73 @@ +/* Interactive example demos (.hf-example), injected by the content preprocessor. */ + +.hf-example { + margin-block: 1.5rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--hf-radius, 8px); + overflow: hidden; +} + +.hf-example__preview { + padding: 1rem; + background: var(--sl-color-bg); +} + +.hf-example__preview.is-loading { + opacity: 0.6; +} + +/* The demo source code sits directly under its preview. */ +.hf-example__source { + border-top: 1px solid var(--sl-color-gray-5); +} + +.hf-example__source > summary { + cursor: pointer; + padding: 0.5rem 1rem; + font-size: var(--sl-text-sm); + color: var(--sl-color-gray-2); + user-select: none; +} + +.hf-example__source > summary:hover { + color: var(--sl-color-white); +} + +.hf-example__source .expressive-code { + margin: 0; +} + +/* Example demo table (used by several demos) renders on its own surface. */ +.hf-example table.spreadsheet { + border-collapse: collapse; +} + +.hf-example table.spreadsheet th, +.hf-example table.spreadsheet td { + border: 1px solid var(--sl-color-gray-5); + padding: 0.25rem 0.5rem; +} + +/* + * The demos' own CSS hardcodes `color: #606c76` on `.example` and + * `background-color: #f6f8fa` on every other table row — both designed for a + * white page background. In dark mode that paints alternating rows blinding + * white with invisible dark text. Re-tint to the docs surface tokens. + */ +:root[data-theme='dark'] .hf-example .example { + color: var(--sl-color-gray-2); +} + +:root[data-theme='dark'] .hf-example .example table tr { + color: var(--sl-color-gray-2); +} + +:root[data-theme='dark'] .hf-example .example table tr:nth-child(2n) { + background-color: var(--sl-color-gray-8); +} + +:root[data-theme='dark'] .hf-example .example table thead tr, +:root[data-theme='dark'] .hf-example .example table.spreadsheet thead tr th { + background-color: var(--sl-color-gray-7); + color: var(--sl-color-white); +} diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css new file mode 100644 index 0000000000..8fe192ce74 --- /dev/null +++ b/docs/src/styles/custom.css @@ -0,0 +1,9 @@ +/* + * HyperFormula docs theme entry point (referenced from astro.config customCss). + * Imports the modular partials; keep theme rules in those files, not here. + */ +@import './base/variables.css'; +@import './components/content.css'; +@import './components/header.css'; +@import './components/footer.css'; +@import './components/interactive-example.css'; diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000000..532df7ba50 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "baseUrl": ".", + "allowJs": true, + "skipLibCheck": true, + "strictNullChecks": true + }, + "include": [ + "src/**/*", + ".astro/**/*" + ], + "exclude": [ + "node_modules", + ".vuepress", + "content", + "examples" + ] +} diff --git a/netlify.toml b/netlify.toml index f473180db4..87cf2f3ac8 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,6 @@ [build] command = "npm run docs:build" - publish = "docs/.vuepress/dist" + publish = "docs/dist" [build.environment] - NODE_VERSION = "18" + NODE_VERSION = "20" diff --git a/package.json b/package.json index e2c8d29d17..2023746e0b 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,8 @@ "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": "cd docs && npm run dev", + "docs:build": "npm run bundle-all && npm run typedoc:build-api && cd docs && npm ci && npm run build", "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", From ec5c7d1260171d8cec0f1d10a6e0937530c87e7d Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Thu, 28 May 2026 11:28:14 -0400 Subject: [PATCH 02/12] Add documentation authoring standards and editing/deployment guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces four governance files under `docs/` so contributors -- and AI coding agents -- have a clear, self-contained reference for how to work on the docs site: - `docs/CLAUDE.md`: authoring standards. Diátaxis page-type taxonomy with folder-to-type mapping, voice and style rules with a banned-word list, four page-structure templates (tutorial / how-to / reference / explanation), example-data domain conventions, code-example rules (language tags, `licenseKey: 'gpl-v3'`, builder methods), the frontmatter schema with permanent 8-char IDs and the `| HyperFormula` metaTitle suffix, link conventions, the Excel / Google Sheets trademark notice text, the sidebar registration step, the Starlight-native asides / live-runner HTML pattern, and a PR checklist. - `docs/AGENTS.md`: symlink to `CLAUDE.md` so both naming conventions resolve to the same file. - `docs/README-EDITING.md`: practical reference -- frontmatter long-form, Starlight aside examples, the live-runner HTML pattern with file layout, line highlighting in code blocks, and sidebar registration. - `docs/README-DEPLOYMENT.md`: deployment reference -- Netlify build command, the `docs:build` pipeline (`bundle-all` → `typedoc:build-api` → `npm ci && npm run build`), GitHub Actions CI verification, and the auto-generated `_redirects` map. --- docs/AGENTS.md | 1 + docs/CLAUDE.md | 539 ++++++++++++++++++++++++++++++++++++++ docs/README-DEPLOYMENT.md | 112 ++++++++ docs/README-EDITING.md | 222 ++++++++++++++++ 4 files changed, 874 insertions(+) create mode 120000 docs/AGENTS.md create mode 100644 docs/CLAUDE.md create mode 100644 docs/README-DEPLOYMENT.md create mode 100644 docs/README-EDITING.md diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 120000 index 0000000000..681311eb9c --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 100644 index 0000000000..2738a14cf3 --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1,539 @@ +# Documentation Standards + +This document is written for both human authors and AI agents. All rules are stated explicitly so both roles can apply them without ambiguity. + +Astro Starlight-based documentation site. **Requires Node 20** (separate from core's runtime requirements). + +--- + +## 1. Documentation Architecture (Diátaxis) + +Every page belongs to exactly one of four content types from the [Diátaxis framework](https://diataxis.fr/). Mixing types on a single page creates confusion. When content doesn't fit one type, split it into two pages. + +### The four content types + +| Type | Serves | User's question | HyperFormula example | +|---|---|---|---| +| **Tutorial** | Learning | "Teach me to do X" | "Build a discount calculator with HyperFormula" | +| **How-to guide** | Goals | "How do I accomplish X?" | "How to localize function names" | +| **Reference** | Information | "What are the options for X?" | "Configuration options" | +| **Explanation** | Understanding | "Why does X work this way?" | "Understanding the dependency graph" | + +### Decision tree + +Use this to pick the right type for a new page: + +1. Is the reader a beginner who needs guided instruction? → **Tutorial** +2. Does the reader already know the basics and needs to accomplish a specific task? → **How-to guide** +3. Is the reader looking up a specific fact, option, or API signature? → **Reference** +4. Is the page answering "why?" or explaining a concept, design, or trade-off? → **Explanation** +5. Does the content fit two or more types? → Split into separate pages. + +### Folder-to-type mapping + +| Folder / page pattern | Expected type | +|---|---| +| `guide/client-side-installation`, `guide/server-side-installation`, `guide/basic-usage`, `guide/advanced-usage` | How-to | +| `guide/integration-with-*`, `guide/mcp-server` | How-to | +| `guide/key-concepts`, `guide/dependency-graph`, `guide/volatile-functions`, `guide/order-of-precendece` | Explanation | +| `guide/configuration-options`, `guide/built-in-functions`, `guide/types-of-values`, `guide/types-of-errors`, `guide/types-of-operators`, `guide/specifications-and-limits` | Reference | +| `guide/migration-from-*` | How-to | +| `guide/release-notes`, `guide/known-limitations`, `guide/list-of-differences`, `guide/supported-browsers` | Reference | +| `guide/compatibility-with-microsoft-excel`, `guide/compatibility-with-google-sheets` | Reference | +| `guide/branding`, `guide/code-of-conduct`, `guide/contributing`, `guide/licensing`, `guide/license-key`, `guide/quality`, `guide/support`, `guide/contact`, `guide/dependencies` | Reference | +| `api/` | Reference (auto-generated from TypeDoc) | + +### Required frontmatter field + +Every page **must** declare its Diátaxis type in frontmatter: + +```yaml +type: tutorial | how-to | reference | explanation +``` + +This field is in addition to the other required frontmatter fields (see [§6 Frontmatter Schema](#6-frontmatter-schema)). + +--- + +## 2. Voice and Style + +### Person, tense, and voice + +- **Second person**: "you", not "we" or "the user". +- **Present tense**: "the engine evaluates", not "the engine will evaluate". +- **Active voice**: "Call `setCellContents()`", not "`setCellContents()` should be called". +- **Direct imperative for instructions**: "Call `setCellContents()`", not "You should call `setCellContents()`". + +### Words to avoid + +| Avoid | Use instead | +|---|---| +| simply, just, easy, straightforward | (omit -- state the fact directly) | +| note that, please | (omit -- restructure as a callout or sentence) | +| allows you to | "lets you" or rephrase actively | +| in order to | "to" | +| utilize | "use" | + +### Sentence length + +- Instructions: max ~25 words per sentence. +- One idea per sentence. +- Separate compound sentences at conjunctions. + +### Technical terms + +- Define on first use in the page: "A *named expression* -- a label that resolves to a formula or value -- can be used in place of a cell reference." +- On subsequent uses, link to the reference page once per page section. +- Use code formatting for all API names, option keys, file paths, and code values. + +### Formatting conventions + +- Hyphens (`-`) or double hyphens (`--`) to separate clauses. No en dashes or em dashes. +- Straight quotes (`"` and `'`) only. No curly/smart quotes. +- Bold for UI elements: **Save**, **Add sheet**. +- Inline code for API names: `buildFromArray()`, `setCellContents()`, `licenseKey`. +- Oxford comma in lists of three or more items. +- American English spelling. + +--- + +## 3. Page Structure Templates + +Use the appropriate template for each Diátaxis type. Do not omit required sections. + +### Tutorial template + +```markdown +--- +type: tutorial +id: <8-char alphanum> +title: <Verb phrase -- Build/Create/Set up X> +metaTitle: <title> | HyperFormula +description: <1-2 sentences summarizing outcome and who benefits> +permalink: /<slug> +tags: [keyword1, keyword2] +category: <nav category> +--- + +In this tutorial, you will [concrete outcome]. You will learn [skill or concept]. + +## Before you begin + +- [prerequisite 1] +- [prerequisite 2] + +## Step 1 -- [Action phrase] + +[Instruction text. Keep to ~3-5 sentences. Show one code block.] + +## Step 2 -- [Action phrase] + +... + +## What you learned + +- [learning point 1] +- [learning point 2] + +## Next steps + +- [link to related how-to or reference] +- [link to deeper topic] +``` + +### How-to guide template + +```markdown +--- +type: how-to +id: <8-char alphanum> +title: How to [specific goal] +metaTitle: How to [specific goal] | HyperFormula +description: <1-2 sentences: what this achieves and when to use it> +permalink: /<slug> +tags: [keyword1, keyword2] +category: <nav category> +--- + +[One sentence: what this accomplishes and when to use it.] + +## Prerequisites + +- [prerequisite 1] + +## Steps + +1. [First action] + + [Explanation and code block] + +2. [Second action] + + [Explanation and code block] + +## Result + +[Describe what the reader now has. One or two sentences.] + +## Related + +- [link to related reference] +- [link to related how-to] +``` + +### Reference template + +```markdown +--- +type: reference +id: <8-char alphanum> +title: [Feature / option / API name] +metaTitle: [Feature / option / API name] | HyperFormula +description: <1-2 sentences describing what this is> +permalink: /<slug> +tags: [keyword1, keyword2] +category: <nav category> +--- + +[One sentence describing what this is and what it does.] + +## Syntax / Signature + +```typescript +// function/option signature +``` + +## Parameters / Options + +| Name | Type | Default | Description | +|---|---|---|---| +| `name` | `string` | `'value'` | What this controls. | + +## Returns + +[Return type and description, if applicable.] + +## Examples + +[Minimal, runnable code example with language tag.] + +## Related + +- [link to how-to for this feature] +- [link to related reference] +``` + +### Explanation template + +```markdown +--- +type: explanation +id: <8-char alphanum> +title: Understanding [concept] +metaTitle: Understanding [concept] | HyperFormula +description: <1-2 sentences: why this concept matters and who should read this> +permalink: /<slug> +tags: [keyword1, keyword2] +category: <nav category> +--- + +[Why this concept matters and when it is relevant. 2-3 sentences.] + +## Background + +[Historical or architectural context.] + +## How it works + +[Mechanism, flow, or design explanation.] + +## Trade-offs + +[What you gain and what you give up. When to choose differently.] + +## Related + +- [link to how-to that applies this concept] +- [link to reference for this feature] +``` + +--- + +## 4. Example Data Standards + +### Never use + +The following placeholder values are banned from all published documentation: + +`A1`, `A2`, `A3`, `foo`, `bar`, `baz`, `test`, `Column1`, `Column2`, `Item1`, `value1`, `xxx`, `sample`, `dummy`, `placeholder`, `name1`, `name2`, `data1`, `data2` + +Cell coordinates (`A1`, `B2`) are still acceptable when discussing cell-reference syntax itself -- but never as stand-in data values. + +### Always use domain-realistic data + +Each example must use data from a coherent, plausible real-world domain. Pick one domain per example and stay consistent throughout. + +**Approved example domains:** + +| Domain | Example data | +|---|---| +| **Financial modeling** | NPV / IRR / DCF inputs, fiscal quarters (Q1 2026), currencies (USD, EUR), cash flows ($4.2M, -$1.1M) | +| **Pricing & quotes** | Line-item totals, discount tiers (10%, 15%, 20%), tax brackets, unit prices ($24.99, $199.00) | +| **HR / payroll** | Employee names (diverse: Ana García, James Okafor, Li Wei), hours worked, salaries, commissions, hire dates (2024-03-14) | +| **Inventory** | Product SKUs (SKU-4821), supplier names (Harbor Goods, Alpine Supply Co.), stock quantities (142, 0, 67), reorder points, COGS | +| **Analytics / KPIs** | Campaign names (Spring Sale 2026), conversion rates (3.4%, 8.1%), weighted scores, channels (Email, Paid Search, Organic) | +| **Project budgets** | Task hours, milestone dates (2026-06-30), assignees, status counts (In progress, Blocked), cost rollups | + +### Data coherence rules + +- All rows in an example use the same domain. +- Values must be plausible: no negative ages, no revenue of $1, no dates in 1900. +- The dataset should make the demonstrated feature meaningful. A `SUMIF` example must use data where conditional summing is useful. An array-formula example must use data where the array dimension is visible. +- Use at least five rows in table examples so the feature behavior is visible. + +--- + +## 5. Code Example Standards + +### Language tags + +All code blocks **must** include a language tag: + +````markdown +```javascript +```typescript +```html +```css +```shell +```json +```yaml +```` + +Untagged code blocks (```` ``` ````) are not allowed. + +### Example quality rules + +- Examples must be self-contained and runnable (or clearly labeled as a snippet). +- Use `const` and `let`. Never use `var`. +- Use the supported builders -- `HyperFormula.buildFromArray()`, `HyperFormula.buildFromSheets()`, or `HyperFormula.buildEmpty()` -- not the raw constructor. +- Every code example that constructs a HyperFormula instance must include a `licenseKey:` option. Use `'gpl-v3'` for all guide examples -- this is the open-source license key documented at [`guide/license-key`](/guide/license-key). The placeholder proprietary form (`'xxxx-xxxx-xxxx-xxxx-xxxx'`) appears only on the license-key page itself. +- No inline `// TODO` or `// ...` comments in published examples. +- Keep examples between 25 and 60 lines. If longer, link to a sandbox (CodeSandbox, StackBlitz) instead. +- TypeScript is the primary language for new examples. The JavaScript variant should match it line-for-line. +- Each HyperFormula example should follow the input → formula → output pattern: build the engine with realistic data, evaluate a formula, then show the returned value (via `getCellValue()` or equivalent). + +### Example embedding + +The site uses VuePress-style example containers preprocessed by `src/plugins/vuepress-preprocessor.mjs`. A live example references files in `docs/examples/`: + +```markdown +::: example #example1 +@[code](@/docs/examples/category/feature/example1.html) +@[code](@/docs/examples/category/feature/example1.css) +@[code](@/docs/examples/category/feature/example1.js) +::: +``` + +The runner renders an interactive preview followed by a collapsible **Source code** block. + +--- + +## 6. Frontmatter Schema + +Required fields for all pages: + +```yaml +--- +type: tutorial | how-to | reference | explanation # Diátaxis type (required) +id: abc12345 # 8 random alphanumeric chars -- NEVER change existing IDs +title: Feature name # Matches H1; do NOT add H1 in body (Starlight renders title once) +metaTitle: Feature name | HyperFormula +description: Short SEO description (1-2 sentences) +permalink: /feature-name +tags: [keyword1, keyword2] # Optional; lowercase kebab-case +category: Feature group # Sidebar grouping label +menuTag: new # Optional; sidebar badge ("new", "updated", "deprecated") +canonicalUrl: /feature-name # Optional; override Starlight's auto-generated canonical +--- +``` + +**Rules:** + +- Never change an existing `id:` value. IDs are permanent and used for cross-version redirects if HyperFormula adopts versioned docs. +- For new pages, generate 8 random lowercase alphanumeric characters (e.g. `q63yhvq5`). +- `title:` is the only H1. Do not add `# Title` in the Markdown body. +- `description:` is used in SEO meta and link previews -- make it specific and accurate (avoid generic phrases like "HyperFormula documentation"). +- `tags:` must be lowercase kebab-case. +- `metaTitle:` uses the suffix ` | HyperFormula` (note the space-pipe-space). + +--- + +## 7. Links and Paths + +Use clean, site-root-relative URLs (including the `/docs` base prefix) for internal links: + +```markdown +[link text](/docs/guide/page-slug#anchor) +[link text](/docs/api/classes/hyperformula#buildfromarray) +``` + +Rules: + +- Always include the `/docs` base prefix in internal links -- Starlight does not add it when authoring inline. +- Do not use relative paths (`../`) for internal links. +- Do not use absolute URLs (`https://hyperformula.handsontable.com/docs/...`) for internal links -- they break in dev and PR preview builds. +- External links use full `https://` URLs. + +--- + +## 8. Asides and Inline Components + +Use Starlight's native aside syntax for callouts (no leading space after the colons): + +| Syntax | Renders as | Use for | +|---|---|---| +| `:::tip[Title]` | Blue tip aside | Helpful info, recommendations | +| `:::caution[Title]` | Yellow caution aside | Things that can go wrong | +| `:::danger[Title]` | Red danger aside | Data loss, security, irreversible actions | +| `:::note[Title]` | Neutral note aside | Side notes, parenthetical context | + +Close every aside with a bare `:::` on its own line. The `[Title]` portion is optional; without it, Starlight uses a default heading derived from the type. + +For collapsible content, use the HTML `<details>` element: + +```markdown +<details> +<summary>Title</summary> + +Long supplementary content here. + +</details> +``` + +For live interactive HyperFormula examples, use the established HTML pattern (see [§5](#5-code-example-standards) for example file conventions). The runner in [`src/scripts/example-runner.ts`](./src/scripts/example-runner.ts) hydrates any element with `data-example-js` and loads the referenced JS module: + +```html +<div class="hf-example not-content"> + <style> + /* example-specific CSS (optional) */ + </style> + <div class="hf-example__preview" data-example-js="/examples/<category>/<feature>/example1.js"> + <!-- the DOM the example mounts into --> + </div> +</div> +``` + +Copy an existing live-example block as the starting template for new ones -- the `hf-example` / `hf-example__preview` class names are required for theme styling and runner discovery. + +--- + +## 9. Trademark Notices + +HyperFormula documents Excel-compatible and Google Sheets-compatible behavior on several pages. Each such page must include a trademark callout at the bottom: + +**Excel-only pages** (`guide/compatibility-with-microsoft-excel`, `guide/migration-from-*` where Excel is referenced): + +```markdown +::: tip Trademark notice +Microsoft® and Excel® are registered trademarks of Microsoft Corporation. +::: +``` + +**Google-Sheets-only pages** (`guide/compatibility-with-google-sheets`): + +```markdown +::: tip Trademark notice +Google Sheets™ is a trademark of Google LLC. +::: +``` + +**Pages mentioning both** (`guide/list-of-differences`): + +```markdown +::: tip Trademark notice +Microsoft® and Excel® are registered trademarks of Microsoft Corporation. Google Sheets™ is a trademark of Google LLC. +::: +``` + +--- + +## 10. Sidebar Registration + +Register new pages in `src/sidebar.mjs`. A page not registered there will not appear in navigation. + +Each top-level group has shape: + +```javascript +{ + label: 'Group name', + items: [ + { label: 'Page title', link: '/guide/page-slug' }, + ... + ], +} +``` + +`link:` values are site-root relative -- Starlight prepends the configured `base` (`/docs`) automatically. + +The API Reference group is auto-generated from the `api/` content directory; do not edit it manually. + +--- + +## 11. Content Sources + +### Guides, homepage, 404 + +Guide pages, the homepage (`index.md`), and the 404 (`404.md`) are authored directly in `docs/src/content/docs/` and tracked in git. They use Starlight-native markdown -- no preprocessing runs on them at request or build time. + +### API reference + +The API reference under `docs/src/content/docs/api/` is auto-generated from TypeDoc against the HyperFormula source. It is the only content path that still passes through the VuePress preprocessor (run by `scripts/generate-content.mjs`) because TypeDoc emits links and badge tokens that need normalizing. To update the API reference: + +1. Edit the relevant TSDoc / JSDoc comment in the HyperFormula library source (the `src/` at the repo root, not under `docs/`). +2. From the repo root, regenerate the API content: + ```bash + npm run typedoc:build-api + ``` +3. Re-run `npm run dev` (or `npm run build`) inside `docs/`. + +Do not edit the generated `.md` files under `docs/src/content/docs/api/` by hand -- they are overwritten on every build. + +### Example files + +Live examples referenced from page HTML live in `docs/examples/<category>/<feature>/`. Each folder contains `example1.{html,css,js,ts}` files. The runner in [`src/scripts/example-runner.ts`](./src/scripts/example-runner.ts) loads the `.js` module at request time; the `.html` / `.css` files exist as the single source of truth for the inline DOM and styles you paste into the guide page. + +--- + +## 12. Checklist Before Submitting a Docs PR + +Copy and complete this checklist in your PR description: + +```markdown +## Docs PR checklist + +- [ ] `type:` field added to frontmatter (tutorial | how-to | reference | explanation) +- [ ] Page uses the correct Diátaxis template for its type +- [ ] Title matches Diátaxis naming convention for its type + - Tutorial: verb phrase ("Build X", "Create X") + - How-to: starts with "How to ..." + - Reference: feature, option, or API name + - Explanation: starts with "Understanding ..." +- [ ] Intro paragraph states: what, for whom, and what outcome the reader gains +- [ ] No banned placeholder data (foo, bar, A1 as a value, Column1, etc.) +- [ ] All example data is domain-realistic and internally consistent +- [ ] All code blocks have language tags (```javascript, ```typescript, etc.) +- [ ] No `var` in code examples; uses `const` / `let` +- [ ] All HyperFormula instances are created via `buildFromArray` / `buildFromSheets` / `buildEmpty` with a `licenseKey:` option +- [ ] Heading hierarchy is correct (no skipped levels, e.g., H2 → H4) +- [ ] Active voice and second person ("you") used throughout +- [ ] No banned words: simply, just, easy, straightforward, note that, please +- [ ] Tutorials and how-tos have a Prerequisites section +- [ ] Tutorials have "What you learned" and "Next steps" sections +- [ ] How-tos have a "Result" section +- [ ] New page registered in `src/sidebar.mjs` +- [ ] `id:` field uses 8 random alphanumeric chars (existing IDs are unchanged) +- [ ] Trademark notice added on Excel or Google Sheets pages +- [ ] `metaTitle:` uses the ` | HyperFormula` suffix +- [ ] Asides use Starlight syntax (`:::tip[Title]`), not legacy VuePress `::: tip Title` +- [ ] Internal links include the `/docs` base prefix +``` diff --git a/docs/README-DEPLOYMENT.md b/docs/README-DEPLOYMENT.md new file mode 100644 index 0000000000..95d5562294 --- /dev/null +++ b/docs/README-DEPLOYMENT.md @@ -0,0 +1,112 @@ +# Documentation deployment guidelines + +The HyperFormula documentation is built with Astro + Starlight and deployed via Netlify. Unlike Handsontable, HyperFormula serves a **single documentation version** -- there is no `prod-docs/<MAJOR.MINOR>` branch model. + +## Where it's deployed + +- **Production:** [https://hyperformula.handsontable.com/docs/](https://hyperformula.handsontable.com/docs/) +- **PR deploy previews:** Netlify builds a per-PR preview and posts the URL as a check on the PR. The preview URL pattern is set by the Netlify project's deploy-context configuration. + +## How a deploy works + +The Netlify build is driven by [`netlify.toml`](../netlify.toml) at the **repository root** (not under `docs/`): + +```toml +[build] + command = "npm run docs:build" + publish = "docs/dist" + +[build.environment] + NODE_VERSION = "20" +``` + +On every push to a tracked branch, Netlify: + +1. Checks out the branch. +2. Runs `npm install` at the repo root. +3. Runs `npm run docs:build` from the repo root. +4. Publishes the contents of `docs/dist/`. + +## The `docs:build` pipeline + +The root `package.json` script chains the full pipeline: + +```bash +npm run docs:build +# ↓ expands to: +npm run bundle-all && npm run typedoc:build-api && cd docs && npm ci && npm run build +``` + +Step by step: + +| Step | What it does | +|---|---| +| `npm run bundle-all` | Compiles and bundles the HyperFormula library itself into `dist/` (UMD, ES, CommonJS). The docs reference the local build, so this must run first. | +| `npm run typedoc:build-api` | Runs TypeDoc against the HyperFormula source to generate the API reference Markdown into `docs/api/`. | +| `cd docs && npm ci` | Installs docs-only dependencies (Astro, Starlight, plugins). | +| `npm run build` (inside `docs/`) | Runs `generate:content && astro build` -- the preprocessor copies / transforms content into `src/content/docs/`, then Astro builds the static site into `docs/dist/`. | + +## GitHub Actions (CI verification) + +[`.github/workflows/build-docs.yml`](../.github/workflows/build-docs.yml) runs **build verification** -- it does **not** deploy. Netlify handles deployment independently. + +The workflow triggers on: + +- Pull requests (opened / reopened / synchronize / base-branch change). +- Pushes to `master`, `develop`, or any `release/**` branch. + +It runs: + +```bash +npm ci +npm run docs:build +``` + +A failing build blocks the PR. Use this to catch broken links, missing examples, or bad markdown before Netlify gets the chance to publish. + +## Redirects + +`docs/dist/_redirects` is auto-generated by [`scripts/generate-content.mjs`](./scripts/generate-content.mjs) on every build. It contains 301 redirects from the legacy VuePress `.html` URLs to the new clean Starlight URLs, so external bookmarks from the VuePress era continue to resolve: + +```text +/docs/guide/basic-usage.html /docs/guide/basic-usage 301 +/docs/guide/advanced-usage.html /docs/guide/advanced-usage 301 +… +``` + +To add a custom redirect, edit `scripts/generate-content.mjs` (the redirect-generation block) rather than editing `_redirects` directly -- the file is overwritten on every build. + +## Triggering a deploy manually + +There is no manual deploy command in this repo. To trigger a deploy: + +- **For an in-progress branch:** push commits; Netlify rebuilds the deploy preview. +- **For production:** merge to `master`; Netlify rebuilds the production site. +- **For a forced rebuild without code changes:** use Netlify's UI to clear cache and redeploy the latest commit, or push an empty commit (`git commit --allow-empty -m "chore: trigger docs rebuild"`). + +## Local production build (smoke-test before pushing) + +To reproduce what Netlify will run, from the repo root: + +```bash +npm run docs:build +npx serve docs/dist +``` + +Or, for an Astro-native preview (skips library + TypeDoc rebuild, useful after a recent `docs:build`): + +```bash +cd docs +npm run preview +``` + +## What this setup does **not** include + +To set realistic expectations for future migration work: + +- **No `starlight-page-actions` plugin** -- so no "View in Markdown", "Copy Markdown", or "Ask AI" buttons. See the migration gap report for details. +- **No `markdownRoutesIntegration`** -- `dist/_md/` is not generated. +- **No version-switcher dropdown** -- the header shows a static `v<x.y.z>` badge from the library's package.json. +- **No Algolia DocSearch** -- search uses Starlight's default (Pagefind), built into the static output. +- **No staging environment** distinct from PR deploy previews. PRs get a Netlify preview URL; there is no persistent `dev.hyperformula.handsontable.com` mirror. +- **No Playwright visual regression** -- changes are reviewed manually via the deploy preview. diff --git a/docs/README-EDITING.md b/docs/README-EDITING.md new file mode 100644 index 0000000000..93140a9c26 --- /dev/null +++ b/docs/README-EDITING.md @@ -0,0 +1,222 @@ +# Documentation editing guidelines + +This page covers practical, hands-on guidelines for editing the [HyperFormula documentation](https://hyperformula.handsontable.com/). It complements the higher-level rules in [CLAUDE.md](./CLAUDE.md) -- read that first. + +## Maintenance rules + +When adding new documentation files, check the documentation [directory structure](./README.md#directory-structure), and follow the guidelines below. + +### Filenames + +- Use only lower-case characters. +- Separate words with hyphens (`-`). +- Use the `.md` file extension. +- Match the slug used in [`src/sidebar.mjs`](./src/sidebar.mjs). + +### Frontmatter + +Every page declares its metadata in YAML frontmatter at the top of the file. + +| Tag | Meaning | Default value | +|---|---|---| +| `type` | The page's Diátaxis type: `tutorial`, `how-to`, `reference`, or `explanation`. | Required (see [CLAUDE.md §1](./CLAUDE.md#1-documentation-architecture-diataxis)). | +| `id` | The page's unique 8-character alphanumeric ID. Used for cross-version redirects if HyperFormula adopts versioned docs. Don't change existing IDs. For new pages, generate 8 random lowercase alphanumeric characters (e.g. via [random.org](https://www.random.org/strings/?num=20&len=8&digits=on&loweralpha=on&unique=on&format=html&rnd=new)). | Required for new pages. | +| `title` | The page's H1. Starlight renders this as the page title -- do not also add `# Title` in the body. | Required. | +| `metaTitle` | The page's `<title>` element. Use the suffix ` \| HyperFormula`. | Optional (Starlight auto-generates if absent). | +| `description` | The page's SEO meta description and social-card preview text (1-2 sentences). | Strongly recommended. | +| `permalink` | The page's unique URL. | Optional; Starlight derives the URL from the file path if absent. | +| `canonicalUrl` | Canonical URL override (rarely needed). | None. | +| `category` | Sidebar group label for organizing pages. | None. | +| `menuTag` | Optional sidebar badge: `new`, `updated`, `deprecated`. | None. | +| `tags` | Optional search tags. Use lowercase kebab-case. | None. | + +#### Frontmatter example + +```yaml +--- +type: how-to +id: q63yhvq5 +title: How to localize function names +metaTitle: How to localize function names | HyperFormula +description: Translate HyperFormula's built-in function names into any of 17 supported languages, or define your own translations. +permalink: /localizing-functions +category: Internationalization +tags: [i18n, localization, languages] +--- +``` + +## Editing the documentation + +### Editing guide pages + +Guide pages, the homepage, and the 404 are authored directly in `src/content/docs/*.md` using Starlight-native markdown. There is no preprocessing step for these files -- what you write is what Astro renders. Add new pages to [`src/sidebar.mjs`](./src/sidebar.mjs) so they appear in the navigation tree. + +To preview changes locally, run `npm run dev` from the `docs/` directory and browse to [http://localhost:4321/docs/](http://localhost:4321/docs/). Astro's content collection cache is sticky -- after editing `.md` files, restart with `npm run dev -- --force` to invalidate it. + +### Editing the API reference + +The API reference under `src/content/docs/api/` is auto-generated from TSDoc / JSDoc comments in the HyperFormula source code. To update it: + +1. Edit the relevant TSDoc / JSDoc comment in `src/` (the HyperFormula library code, not under `docs/`). +2. From the repository root, regenerate the API content: + ```bash + npm run typedoc:build-api + ``` +3. Re-run `npm run docs:dev` (or `npm run dev` inside `docs/`) to see the updated reference. + +Do **not** edit the generated `.md` files under `src/content/docs/api/` by hand -- changes are overwritten on every build. + +## Reviewing the documentation + +When reviewing someone else's changes: + +- **Locally:** check out the branch, run `npm run dev` from `docs/`, and browse to [http://localhost:4321/docs/](http://localhost:4321/docs/). +- **Netlify deploy preview:** Netlify builds a per-PR preview and posts the URL on the PR. See [README-DEPLOYMENT.md](./README-DEPLOYMENT.md) for deploy-context details. + +## Markdown links + +Use clean, site-root-relative URLs that include the `/docs` base prefix: + +```markdown +[Basic usage](/docs/guide/basic-usage) +[buildFromArray](/docs/api/classes/hyperformula#buildfromarray) +``` + +Starlight does **not** add the `/docs` prefix automatically when authoring inline -- you must include it in the link. + +### Rules + +- Always include the `/docs` base prefix. +- Don't use relative paths (`../guide/...`). +- Don't use absolute URLs (`https://hyperformula.handsontable.com/docs/...`) for internal links -- they break in dev and PR preview builds. +- External links use full `https://` URLs. + +## Asides and inline components + +Use Starlight's native aside syntax for callouts -- there is no preprocessing layer for guide pages, so VuePress-style `::: tip` (with a space) does not work. + +| Syntax | Renders as | Use for | +|---|---|---| +| `:::tip[Title]` | Starlight tip (blue) | Helpful info, recommendations | +| `:::caution[Title]` | Starlight caution (yellow) | Things that can go wrong | +| `:::danger[Title]` | Starlight danger (red) | Data loss, security, irreversible actions | +| `:::note[Title]` | Starlight note (neutral) | Side notes, parenthetical context | + +Close every aside with a bare `:::` on its own line. The `[Title]` portion is optional; without it, Starlight uses a default heading derived from the type. + +### Aside examples + +```markdown +:::tip[Quick win] +The `buildFromArray()` method is the fastest way to get started with sample data. +::: + +:::caution[Performance] +Volatile functions re-evaluate on every recalculation. Use them sparingly. +::: + +:::danger[Data loss] +Calling `destroy()` releases all internal state. Save results before destroying the instance. +::: +``` + +### Collapsible content + +Starlight does not have a dedicated container for accordions -- use the HTML `<details>` element directly: + +```markdown +<details> +<summary>See the full options list</summary> + +Long supplementary content here. + +</details> +``` + +## Adding interactive code examples + +The live runner in [`src/scripts/example-runner.ts`](./src/scripts/example-runner.ts) hydrates any element with `data-example-js` and loads the referenced JavaScript module from the site root. Author live examples as inline HTML inside guide pages: + +```html +<div class="hf-example not-content"> + <style> + /* example-specific CSS (optional, inline) */ + </style> + <div class="hf-example__preview" data-example-js="/examples/basic-usage/example1.js"> + <!-- the DOM the example mounts into --> + <div class="example"> + <button id="calculate">Calculate</button> + <div id="output"></div> + </div> + </div> +</div> + +<details class="hf-example__source"> +<summary>Source code</summary> + +```javascript +import { HyperFormula } from 'hyperformula'; + +const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' }); +// ... +``` + +</details> +``` + +The `hf-example` / `hf-example__preview` class names are required -- the runner discovers mount points via the `data-example-js` attribute, and the theme styling targets those classes. + +### File layout + +Each example lives in its own folder under `docs/examples/<category>/<feature>/`: + +```bash +docs/examples/basic-usage/ +├── example1.html # Reference HTML — copy this into the page inline +├── example1.css # Reference CSS — copy this into the page <style> block (optional) +├── example1.js # The runnable JavaScript module loaded by the runner +└── example1.ts # Optional TypeScript source for the JS file +``` + +The runner only fetches the `.js` file (via the `data-example-js` URL). The `.html` / `.css` files are not loaded at runtime -- they exist as a single source of truth for the inline DOM and styles you paste into the guide page HTML. + +### Tip: copy an existing example + +The fastest way to add a new live example is to copy the `<div class="hf-example not-content">...</div>` block from an adjacent guide page and adapt it. + +### Line highlighting in fenced code blocks + +For static code blocks (not interactive examples), use Expressive Code's `{n}` metadata to highlight lines: + +````markdown +```javascript {3,5-7} +import { HyperFormula } from 'hyperformula'; + +const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' }); +hf.setCellContents({ sheet: 0, row: 0, col: 0 }, [['=SUM(1, 2)']]); + +const value = hf.getCellValue({ sheet: 0, row: 0, col: 0 }); +console.log(value); +hf.destroy(); +``` +```` + +This renders lines 3, 5, 6, and 7 with a highlight background. + +## Sidebar registration + +A new page only appears in the navigation if it's listed in [`src/sidebar.mjs`](./src/sidebar.mjs). Find the appropriate group and add an entry: + +```javascript +{ + label: 'Internationalization', + items: [ + { label: 'Internationalization features', link: '/guide/i18n-features' }, + { label: 'Localizing functions', link: '/guide/localizing-functions' }, + { label: 'Date and time handling', link: '/guide/date-and-time-handling' }, + { label: 'Your new page', link: '/guide/your-new-page' }, // ← add here + ], +}, +``` + +`link:` values are site-root relative; Starlight prepends the configured `base` (`/docs`) automatically. From 510fb6d5a5578b6d39ebea7d4a24a6a3a35997bc Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Thu, 28 May 2026 11:44:32 -0400 Subject: [PATCH 03/12] Move page action buttons into the right sidebar The `starlight-page-actions` plugin defaults to rendering its action row directly under the page heading. Match the Handsontable docs UI by moving the actions (Copy Markdown, Open in ChatGPT, Open in Claude) into the right-hand sidebar, below the "On this page" table of contents. Adds two Starlight component overrides: - `PageTitle.astro`: renders only the default page title and drops the plugin's button row. The plugin's override is suppressed because user- configured overrides win over plugin-supplied ones in Starlight. - `PageSidebar.astro`: renders the default TOC, then appends a flat action list styled to match the right-sidebar typography. The Copy Markdown handler reuses the plugin's `.md` companion routes and shows a transient checkmark on success. --- docs/astro.config.mjs | 8 ++ docs/src/components/PageSidebar.astro | 174 ++++++++++++++++++++++++++ docs/src/components/PageTitle.astro | 13 ++ 3 files changed, 195 insertions(+) create mode 100644 docs/src/components/PageSidebar.astro create mode 100644 docs/src/components/PageTitle.astro diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index c61c4f1d1d..07fa38e525 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -94,6 +94,14 @@ export default defineConfig({ Footer: './src/components/Footer.astro', // Sun/moon toggle replacing the Auto/Light/Dark <select>. ThemeSelect: './src/components/ThemeSelect.astro', + // Strips the page-action button row that starlight-page-actions + // would otherwise inject under the page heading. The actions are + // moved into the right-hand sidebar (see PageSidebar override). + PageTitle: './src/components/PageTitle.astro', + // Adds a flat HT-style action list (Copy Markdown, Open in + // ChatGPT, Open in Claude) under the "On this page" table of + // contents. + PageSidebar: './src/components/PageSidebar.astro', }, }), diff --git a/docs/src/components/PageSidebar.astro b/docs/src/components/PageSidebar.astro new file mode 100644 index 0000000000..c2a483f91f --- /dev/null +++ b/docs/src/components/PageSidebar.astro @@ -0,0 +1,174 @@ +--- +/* + * Override of Starlight's PageSidebar that appends a Handsontable-style + * action list under the "On this page" TOC: a flat column of links for + * Copy Markdown, Open in ChatGPT, and Open in Claude. + * + * The companion `PageTitle.astro` strips the row of buttons that + * `starlight-page-actions` would normally render under the page heading, + * so the actions only appear here. + */ +import DefaultPageSidebar from '@astrojs/starlight/components/PageSidebar.astro'; + +const currentPath = Astro.url.pathname.replace(/\/$/, ''); +const currentUrl = Astro.url.href; +const promptUrl = encodeURIComponent( + `Read ${currentUrl}. I want to ask questions about it.` +); + +const actions = [ + { + id: 'copy-markdown', + label: 'Copy Markdown', + icon: 'copy', + }, + { + id: 'open-chatgpt', + label: 'Open in ChatGPT', + icon: 'external', + href: `https://chatgpt.com/?q=${promptUrl}`, + }, + { + id: 'open-claude', + label: 'Open in Claude', + icon: 'external', + href: `https://claude.ai/new?q=${promptUrl}`, + }, +]; +--- + +<DefaultPageSidebar /> + +{ + Astro.locals.starlightRoute.toc && ( + <div class="sl-hidden lg:sl-block hf-page-actions" data-md-path={currentPath}> + <div class="sl-container"> + <ul class="hf-page-actions__list"> + {actions.map(({ id, label, icon, href }) => ( + <li> + {href ? ( + <a + class="hf-page-actions__item" + id={id} + href={href} + target="_blank" + rel="noopener noreferrer" + > + <span class="hf-page-actions__icon" aria-hidden="true"> + {icon === 'external' && ( + <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> + <path d="M14 4h6v6" /> + <path d="M10 14L20 4" /> + <path d="M19 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h6" /> + </svg> + )} + </span> + <span class="hf-page-actions__label">{label}</span> + </a> + ) : ( + <button class="hf-page-actions__item" type="button" id={id}> + <span class="hf-page-actions__icon" aria-hidden="true"> + <svg class="hf-icon-default" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> + <rect x="9" y="9" width="11" height="11" rx="2" /> + <path d="M5 15V5a2 2 0 0 1 2-2h10" /> + </svg> + <svg class="hf-icon-success" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> + <path d="M5 12l5 5L20 7" /> + </svg> + </span> + <span class="hf-page-actions__label">{label}</span> + </button> + )} + </li> + ))} + </ul> + </div> + </div> + ) +} + +<style> + .hf-page-actions { + padding: 0 var(--sl-sidebar-pad-x); + margin-top: 0.5rem; + } + .hf-page-actions .sl-container { + width: calc(var(--sl-sidebar-width) - 2 * var(--sl-sidebar-pad-x)); + border-top: 1px solid var(--sl-color-hairline-light); + padding-top: 1rem; + } + .hf-page-actions__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.125rem; + } + .hf-page-actions__item { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.375rem 0; + background: transparent; + border: 0; + color: var(--sl-color-gray-2); + font-size: var(--sl-text-xs); + font-family: inherit; + text-decoration: none; + text-align: left; + cursor: pointer; + transition: color 120ms; + } + .hf-page-actions__item:hover { + color: var(--sl-color-white); + } + .hf-page-actions__icon { + display: inline-flex; + width: 16px; + height: 16px; + flex: 0 0 16px; + } + .hf-page-actions__icon svg { + width: 100%; + height: 100%; + } + + #copy-markdown .hf-icon-success { + display: none; + } + #copy-markdown.copied .hf-icon-default { + display: none; + } + #copy-markdown.copied .hf-icon-success { + display: block; + color: #4ade80; + } + #copy-markdown.copied .hf-page-actions__label { + color: #4ade80; + } +</style> + +<script> + const btn = document.querySelector<HTMLButtonElement>('#copy-markdown'); + const root = document.querySelector<HTMLElement>('.hf-page-actions'); + const path = root?.dataset.mdPath; + + if (btn && path) { + btn.addEventListener('click', async () => { + btn.disabled = true; + try { + const res = await fetch(`${path}.md`); + const text = await res.text(); + await navigator.clipboard.writeText(text); + btn.classList.add('copied'); + setTimeout(() => btn.classList.remove('copied'), 2000); + } catch (err) { + console.error('[hf-page-actions] copy failed', err); + } finally { + btn.disabled = false; + } + }); + } +</script> diff --git a/docs/src/components/PageTitle.astro b/docs/src/components/PageTitle.astro new file mode 100644 index 0000000000..065ef81074 --- /dev/null +++ b/docs/src/components/PageTitle.astro @@ -0,0 +1,13 @@ +--- +/* + * Override of Starlight's PageTitle to suppress the action-row that + * `starlight-page-actions` would otherwise inject under the heading. + * + * The actions (Copy Markdown, Open in ChatGPT, Open in Claude, etc.) are + * rendered in the right-hand sidebar by `PageSidebar.astro` below the + * "On this page" table of contents — matching the Handsontable docs UI. + */ +import DefaultPageTitle from '@astrojs/starlight/components/PageTitle.astro'; +--- + +<DefaultPageTitle /> From 0296af9a2e18ed1f1cdc3051f0765bc4f93815e3 Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Thu, 28 May 2026 12:01:56 -0400 Subject: [PATCH 04/12] Bump docs Node version to 22 and fix CodeQL sanitization warning CI flagged two issues on PR #1686: - The `build-docs` GitHub Actions job and the Netlify deploy preview both failed because Astro 6 requires Node >=22.12.0, but every Node pin in the docs setup specified 20. Bump `.nvmrc`, the docs `engines.node` constraint, the `build-docs.yml` job matrix, the Netlify `NODE_VERSION`, and the "Node 20" mentions in the README / CLAUDE.md / README-DEPLOYMENT to 22 (22.12 for the engines pin). - CodeQL flagged `cleanTitleText()` in `vuepress-preprocessor.mjs` for incomplete multi-character sanitization (`js/incomplete-multi-character-sanitization`). A single pass of `/<[^>]+>/g` could leave a `<script>` substring behind for inputs like `<sc<script>ript>`. Iterate the strip to a fixed point so nested tags collapse fully. The downstream consumer (Starlight rendering a YAML `title:`) already escapes HTML, so this is defence-in-depth, but the warning is correct in principle. --- .github/workflows/build-docs.yml | 2 +- docs/.nvmrc | 2 +- docs/CLAUDE.md | 2 +- docs/README-DEPLOYMENT.md | 2 +- docs/README.md | 2 +- docs/package.json | 2 +- docs/src/plugins/vuepress-preprocessor.mjs | 13 +++++++++++-- netlify.toml | 2 +- 8 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 268c99bcaa..60c43dfc32 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -18,7 +18,7 @@ jobs: publish-docs: strategy: matrix: - node-version: [ '20' ] + node-version: [ '22' ] os: [ 'ubuntu-latest' ] name: build-docs runs-on: ${{ matrix.os }} diff --git a/docs/.nvmrc b/docs/.nvmrc index 209e3ef4b6..2bd5a0a98a 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 2738a14cf3..301dbd69f0 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -2,7 +2,7 @@ This document is written for both human authors and AI agents. All rules are stated explicitly so both roles can apply them without ambiguity. -Astro Starlight-based documentation site. **Requires Node 20** (separate from core's runtime requirements). +Astro Starlight-based documentation site. **Requires Node 22.12+** (Astro 6 minimum; separate from the HyperFormula library's runtime requirements). --- diff --git a/docs/README-DEPLOYMENT.md b/docs/README-DEPLOYMENT.md index 95d5562294..660d07f7a7 100644 --- a/docs/README-DEPLOYMENT.md +++ b/docs/README-DEPLOYMENT.md @@ -17,7 +17,7 @@ The Netlify build is driven by [`netlify.toml`](../netlify.toml) at the **reposi publish = "docs/dist" [build.environment] - NODE_VERSION = "20" + NODE_VERSION = "22" ``` On every push to a tracked branch, Netlify: diff --git a/docs/README.md b/docs/README.md index ac3e6c83b5..d550ad6647 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ View the documentation's latest production version at [hyperformula.handsontable ## Getting started -The docs site is built with [Astro](https://astro.build) and [Starlight](https://starlight.astro.build). **Requires Node 20** (separate from the HyperFormula core's Node version). +The docs site is built with [Astro](https://astro.build) and [Starlight](https://starlight.astro.build). **Requires Node 22.12+** (Astro 6 minimum; separate from the HyperFormula library's Node version). 1. From the `docs` directory, install dependencies: ```bash diff --git a/docs/package.json b/docs/package.json index 4d38bc455e..22748a4679 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "description": "HyperFormula documentation", "engines": { - "node": ">=20" + "node": ">=22.12" }, "scripts": { "generate:content": "node scripts/generate-content.mjs", diff --git a/docs/src/plugins/vuepress-preprocessor.mjs b/docs/src/plugins/vuepress-preprocessor.mjs index 0211419861..e2c13c9714 100644 --- a/docs/src/plugins/vuepress-preprocessor.mjs +++ b/docs/src/plugins/vuepress-preprocessor.mjs @@ -161,8 +161,17 @@ function ensureFrontmatterTitle(content, slug) { } function cleanTitleText(s) { - return s - .replace(/<[^>]+>/g, '') // strip HTML (e.g. TypeDoc <Badge text="Class"/>) + // Strip HTML tags. Iterate to a fixed point so nested constructions like + // "<sc<script>ript>" collapse fully instead of leaving a <script> behind. + let prev; + let out = s; + + do { + prev = out; + out = out.replace(/<[^>]+>/g, ''); + } while (out !== prev); + + return out .replace(/`/g, '') .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') .replace(/\*\*/g, '') diff --git a/netlify.toml b/netlify.toml index 87cf2f3ac8..959093b614 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,4 +3,4 @@ publish = "docs/dist" [build.environment] - NODE_VERSION = "20" + NODE_VERSION = "22" From 0afad67a0443ea54d46e1928d06c13682749f7dc Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Thu, 28 May 2026 12:19:59 -0400 Subject: [PATCH 05/12] Bump root .nvmrc to 22 so Netlify picks Node 22 for the docs build The Netlify deploy preview for #1686 kept failing with "Node.js v18.20.8 is not supported by Astro" even after setting NODE_VERSION = "22" in netlify.toml's [build.environment]. The deploy log showed Netlify resolving Node from the root .nvmrc ("v18") rather than honoring the netlify.toml env var. Bump .nvmrc to 22 so both sources agree. The HyperFormula library is already exercised on Node 20/22/24 by .github/workflows/build.yml, so bumping the local-dev default to 22 is consistent with what CI already tests. --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 3f430af82b..2bd5a0a98a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18 +22 From af9710e7cb955592d412ad61a8313a87e8ed9c7e Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Thu, 28 May 2026 12:30:42 -0400 Subject: [PATCH 06/12] =?UTF-8?q?Add=20/docs/*=20=E2=86=92=20/:splat=20rew?= =?UTF-8?q?rite=20for=20Netlify=20deploy=20previews?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Astro builds the site with `base: '/docs'`, so generated HTML references `/docs/_astro/*`, `/docs/guide/*`, etc. In production the canonical URL `hyperformula.handsontable.com/docs/` is served by a reverse proxy that strips the `/docs` prefix before forwarding to the Netlify project, so the prefix in the HTML resolves to the right files. Netlify deploy previews (and the netlify.app subdomain in general) don't sit behind that proxy, so `/docs/`-prefixed paths 404 — only the home page rendered, and even then without styles or images because every `/docs/_astro/*` URL came back as a 404. Add a wildcard rewrite to netlify.toml so the preview mimics the production proxy: any `/docs/<path>` request is served from `<path>` in the publish directory. The Astro-generated `_redirects` file is processed first (so the legacy `.html` → clean URL redirects still match), then this catch-all rewrites everything else. --- netlify.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/netlify.toml b/netlify.toml index 959093b614..be21b6e9b1 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,3 +4,16 @@ [build.environment] NODE_VERSION = "22" + +# The docs site is built with `base: '/docs'` in astro.config.mjs, so the +# generated HTML references `/docs/_astro/*`, `/docs/guide/*`, etc. In +# production, `hyperformula.handsontable.com/docs/` is served by a reverse +# proxy that strips the `/docs/` prefix before forwarding to this Netlify +# project's subdomain root. Netlify deploy previews don't go through that +# proxy, so the `/docs/`-prefixed paths 404 by default. This rewrite makes +# the preview mimic the production proxy: any `/docs/...` request is served +# from the matching root path of the publish directory. +[[redirects]] + from = "/docs/*" + to = "/:splat" + status = 200 From 720ff8a006483b2b70314b5c394e0c7c45b55f1f Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Thu, 28 May 2026 12:47:04 -0400 Subject: [PATCH 07/12] Locate library root by package.json name in docs-data.mjs The version pill in the docs header rendered as "v0.0.0" on the Netlify deploy preview even though the same code returned the correct "3.3.0" locally. The cause was the brittle `../../..` arithmetic used to find the library root: in some build environments (apparently Astro's build container on Netlify), `import.meta.url` resolves through a location that's one directory deeper than expected, so three parent hops landed inside `docs/` instead of at the repo root. That made the fallback read `docs/package.json` (version `0.0.0`) instead of the library's `package.json`. Replace the path arithmetic with an upward walk that stops at the first `package.json` whose `name` is `hyperformula`. This is robust to any build-cache layout. While here, also fall back gracefully when the UMD bundle exists but is missing individual fields. --- docs/src/plugins/docs-data.mjs | 96 ++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/docs/src/plugins/docs-data.mjs b/docs/src/plugins/docs-data.mjs index e9dcfd3f5f..050db8a029 100644 --- a/docs/src/plugins/docs-data.mjs +++ b/docs/src/plugins/docs-data.mjs @@ -2,50 +2,92 @@ * Build-time HyperFormula metadata injected into docs content in place of the * VuePress `{{ $page.version }}`, `{{ $page.buildDate }}`, etc. template vars. * - * Values are read from the built UMD bundle (`dist/hyperformula.full.js`) when - * available — it exposes `version`, `buildDate`, `releaseDate` and the - * registered-function list. The docs build always runs after `bundle-all`, so - * the bundle is present in CI/production. For local dev without a build, we - * fall back to the repo `package.json` version and the current date. + * Values come from two sources in priority order: + * 1. The HyperFormula library's `package.json` (always available, source of + * truth for the version string). + * 2. The built UMD bundle (`dist/hyperformula.full.js`) if present -- it + * contributes `buildDate`, `releaseDate`, and the registered-function + * count. The docs build runs after `bundle-all` so the bundle is present + * in CI/production; the bundle is optional for local dev. + * + * The library root is located by walking upward from this file and looking + * for the `package.json` whose `name` field is `hyperformula`. This avoids + * brittle `../../..` arithmetic that can drift in different build + * environments (Astro's build cache layout differs slightly between local + * dev and Netlify's build container). * * @module docs-data */ import { createRequire } from 'module'; import { fileURLToPath } from 'url'; -import { dirname, resolve } from 'path'; -import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { readFileSync, existsSync } from 'fs'; const require = createRequire(import.meta.url); -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); + +/** + * Walk up the directory tree from `startDir`, returning the first directory + * whose `package.json` has `"name": <pkgName>`. Returns null if not found. + * + * @param {string} startDir + * @param {string} pkgName + * @returns {string | null} + */ +function findPackageRoot(startDir, pkgName) { + let dir = startDir; + + while (true) { + const pkgPath = join(dir, 'package.json'); + + if (existsSync(pkgPath)) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + + if (pkg.name === pkgName) return dir; + } catch { + // ignore unreadable / malformed package.json and continue upward + } + } + + const parent = dirname(dir); + + if (parent === dir) return null; + dir = parent; + } +} + +const libRoot = findPackageRoot(dirname(fileURLToPath(import.meta.url)), 'hyperformula'); /** @returns {{ version: string, buildDate: string, releaseDate: string, functionsCount: number }} */ function resolveDocsData() { - try { - // The UMD root export is the HyperFormula class with static metadata. - const HyperFormula = require(resolve(repoRoot, 'dist/hyperformula.full.js')); + const fallbackDate = new Date().toUTCString(); - return { - version: HyperFormula.version, - buildDate: HyperFormula.buildDate, - releaseDate: HyperFormula.releaseDate, - functionsCount: HyperFormula.getRegisteredFunctionNames('enGB').length, - }; + if (!libRoot) { + return { version: 'latest', buildDate: fallbackDate, releaseDate: fallbackDate, functionsCount: 400 }; + } + + let version = 'latest'; + + try { + version = JSON.parse(readFileSync(join(libRoot, 'package.json'), 'utf8')).version || 'latest'; } catch { - // Fallback for local dev when the library bundle has not been built yet. - let version = 'latest'; + // ignore -- defaults to 'latest' + } - try { - version = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8')).version; - } catch { - /* ignore */ - } + try { + // The UMD root export is the HyperFormula class with static metadata. + const HyperFormula = require(join(libRoot, 'dist/hyperformula.full.js')); return { version, - buildDate: new Date().toUTCString(), - releaseDate: new Date().toUTCString(), - functionsCount: 400, + buildDate: HyperFormula.buildDate || fallbackDate, + releaseDate: HyperFormula.releaseDate || fallbackDate, + functionsCount: HyperFormula.getRegisteredFunctionNames?.('enGB')?.length ?? 400, }; + } catch { + // Bundle not built yet (local dev). buildDate/releaseDate get the current + // date as a placeholder; version is still accurate from package.json. + return { version, buildDate: fallbackDate, releaseDate: fallbackDate, functionsCount: 400 }; } } From 44028e7d44e5321b8b81458b731fcf721b23104a Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:40:47 -0400 Subject: [PATCH 08/12] Make header divider a single continuous line like Handsontable docs The toolbar's horizontal divider was constructed from `border-top` on each `.hf-nav__link` element, so it only rendered where the framed nav tabs sat. To the left of the first tab and to the right of the Support dropdown, the line broke off and left visible gaps. Mirror Handsontable's `.header-row-2::before` pattern: add a single full-viewport `::before` pseudo-element to `.hf-header__row--nav` so a continuous 1px line stretches edge-to-edge. The per-tab top borders sit at the same Y-coordinate and overlap the pseudo-element where tabs exist; the pseudo-element bridges the gaps elsewhere. The active-tab brand-blue frame and z-index stacking are unchanged. --- docs/src/styles/components/header.css | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/src/styles/components/header.css b/docs/src/styles/components/header.css index e78e57cb55..60074b5d26 100644 --- a/docs/src/styles/components/header.css +++ b/docs/src/styles/components/header.css @@ -51,9 +51,11 @@ header.header { } /* - * No border-bottom here on purpose — the framed-tab strip below already - * supplies the visual divider via its 1px gray-5 border-top, so an additional - * row-1 hairline would double-stack into a 2px line. + * The single horizontal divider that runs across the toolbar is rendered as + * a `::before` pseudo-element on `.hf-header__row--nav` below. The framed + * `.hf-nav__link` top borders overlap that line where tabs exist; the + * pseudo-element fills the gaps to the viewport edges. Mirrors HT's + * `.header-row-2::before`. */ /* — Brand (logo + version pill) — */ @@ -138,6 +140,25 @@ header.header { /* — Primary nav (row 2) — */ .hf-header__row--nav { background: var(--sl-color-bg-nav); + position: relative; +} + +/* + * Continuous full-viewport divider line. Mirrors HT's `.header-row-2::before`. + * Each `.hf-nav__link` keeps its own 1px top border so the framed tabs read + * as a segmented strip; this pseudo-element sits at the same Y-coordinate + * and bridges the empty space to the left of the first tab and to the right + * of the Support dropdown so the divider reads as one unbroken rule. + */ +.hf-header__row--nav::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 100vw; + height: 1px; + background: var(--sl-color-gray-5); } .hf-nav { From c536a55b6fbae56ccb3f5a2e00e68bdf92186459 Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:22:17 -0400 Subject: [PATCH 09/12] Align framed nav tabs flush with the header's top divider and bottom border MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The continuous `::before` divider line was already in place, but two layout issues remained: 1. The per-tab `border-top` sat ~0.5rem below `::before` because `.hf-header__row` (the shared row class) applies `padding: 0.5rem 1rem` and `align-items: center` to every row. That offset rendered as a doubled line where the tabs were. 2. Inside `header.header`, Starlight's `padding-block` reserved ~0.75rem of dead space below `.hf-header`, so the per-tab vertical borders and hover backgrounds stopped short of the visible `border-bottom: 1px solid hairline-shade` boundary. Three targeted overrides — modeled on Handsontable's `.header-row-2` layout: - `.hf-header__row--nav` drops vertical padding, sets `align-items: stretch`, `gap: 0`, and `min-height: 0`. Nav links now sit flush against the row's top so their `border-top` overlaps `::before` perfectly (single line). - `.hf-header` changes `grid-template-rows` from `auto auto` to `auto 1fr` so row 2 absorbs the remaining vertical track height rather than leaving it empty. - `header.header` overrides Starlight's `padding-bottom` to `0` so `.hf-header__row--nav` reaches the header's actual bottom border. The per-tab vertical lines and hover backgrounds now extend the full height of the toolbar, matching Handsontable's design. --- docs/src/styles/components/header.css | 37 +++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/src/styles/components/header.css b/docs/src/styles/components/header.css index 60074b5d26..814d287dfc 100644 --- a/docs/src/styles/components/header.css +++ b/docs/src/styles/components/header.css @@ -10,7 +10,11 @@ .hf-header { display: grid; - grid-template-rows: auto auto; + /* Row 1 hugs its content (brand · search · actions). Row 2 absorbs the + * remaining height of `header.header` (driven by `--sl-nav-height`) so the + * framed nav tabs stretch from the divider line all the way down to the + * bottom of the Starlight header chrome — no dead space below the tabs. */ + grid-template-rows: auto 1fr; height: 100%; width: 100%; /* On wide screens, keep the header content (brand · search · actions) within @@ -39,6 +43,13 @@ header.header { background: var(--sl-color-black) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + /* Drop Starlight's bottom padding so `.hf-header__row--nav` extends all the + * way to `header.header`'s border-bottom. Without this, Starlight's + * 0.75rem padding-bottom would leave dead space below the framed nav tabs + * and the per-tab vertical borders would stop short of the visible header + * boundary. Mirrors HT's `header.header { padding-block: 1rem 0.5rem }` + * override (with the same nav-tab-flush intent). */ + padding-bottom: 0 !important; } .hf-header__row { @@ -138,17 +149,33 @@ header.header { } /* — Primary nav (row 2) — */ +/* + * Overrides the inherited `.hf-header__row` properties (padding 0.5rem 1rem, + * align-items: center) so the nav links sit flush against the row's top edge. + * That alignment is required because the `::before` divider below is anchored + * at `top: 0` of this row — without `align-items: stretch` and zero vertical + * padding, the `.hf-nav__link` top borders would land ~0.5rem below the + * pseudo-element and render as a doubled line. + * + * Layout mirrors HT's `.header-row-2`: + * docs-handsontable/handsontable/docs/src/components/Header.astro:557-583. + */ .hf-header__row--nav { background: var(--sl-color-bg-nav); position: relative; + align-items: stretch; + gap: 0; + padding-top: 0; + padding-bottom: 0; + min-height: 0; } /* * Continuous full-viewport divider line. Mirrors HT's `.header-row-2::before`. - * Each `.hf-nav__link` keeps its own 1px top border so the framed tabs read - * as a segmented strip; this pseudo-element sits at the same Y-coordinate - * and bridges the empty space to the left of the first tab and to the right - * of the Support dropdown so the divider reads as one unbroken rule. + * Sits at row-top (top: 0); the `.hf-nav__link` top borders overlap it at the + * same Y where tabs exist; the pseudo-element bridges the empty space to the + * left of the first tab and to the right of the Support dropdown so the + * divider reads as one unbroken rule across the full viewport. */ .hf-header__row--nav::before { content: ''; From 51f3cc0e91f826e99e689033dbecc0fa330a8688 Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:31:25 -0400 Subject: [PATCH 10/12] Fix editLink baseUrl path (caught by Cursor Bugbot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starlight constructs the "Edit this page" URL by appending `entry.filePath` (the source file path relative to the Astro project root, which is `docs/`) to `editLink.baseUrl`. With the previous value ending in `docs/content/`, the resulting URL would have been `.../edit/develop/docs/content/src/content/docs/guide/<slug>.md` — a path that does not exist in the repository. The `docs/content/` suffix was carried over from Handsontable's `astro.config.mjs`, where guides actually live at `docs/content/` via a symlink. In HyperFormula the sources live directly at `docs/src/content/docs/`, so the base needs to point at `docs/`. No "Edit on GitHub" link is currently rendered (my Footer / PageTitle / PageSidebar overrides don't include one, and `starlight-page-actions` doesn't expose an edit action), so the misconfigured URL was latent. This fix removes the trap before anyone adds the button back. --- docs/astro.config.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 07fa38e525..fc9f737b45 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -36,7 +36,12 @@ export default defineConfig({ ], editLink: { - baseUrl: 'https://github.com/handsontable/hyperformula/edit/develop/docs/content/', + // Starlight appends `entry.filePath` (relative to the Astro project + // root, which is `docs/`) to this baseUrl. Source files live under + // `docs/src/content/docs/`, so the base must point at `docs/` — not + // `docs/content/` (that was a copy-paste from Handsontable's setup, + // where content lives at `docs/content/` via a symlink). + baseUrl: 'https://github.com/handsontable/hyperformula/edit/develop/docs/', }, expressiveCode: { From caa067361b6a2a80aeaa32124e15a5723cacfd35 Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Wed, 3 Jun 2026 14:58:50 +0000 Subject: [PATCH 11/12] Handle failed markdown copy fetches Co-authored-by: GreenFlux <GreenFlux@users.noreply.github.com> --- docs/src/components/PageSidebar.astro | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/src/components/PageSidebar.astro b/docs/src/components/PageSidebar.astro index c2a483f91f..3984ce3a04 100644 --- a/docs/src/components/PageSidebar.astro +++ b/docs/src/components/PageSidebar.astro @@ -159,7 +159,13 @@ const actions = [ btn.addEventListener('click', async () => { btn.disabled = true; try { - const res = await fetch(`${path}.md`); + const markdownUrl = `${path}.md`; + const res = await fetch(markdownUrl); + + if (!res.ok) { + throw new Error(`Failed to fetch ${markdownUrl}: ${res.status} ${res.statusText}`); + } + const text = await res.text(); await navigator.clipboard.writeText(text); btn.classList.add('copied'); From 0509e26b9d955c9f90c2de55659014c3d1bec209 Mon Sep 17 00:00:00 2001 From: GreenFlux <24459976+GreenFlux@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:25:22 -0400 Subject: [PATCH 12/12] Set BUILD_MODE=production for the Netlify production context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught by Cursor Bugbot on PR #1686. `astro.config.mjs` gates the Google Tag Manager snippet on `process.env.BUILD_MODE === 'production'`, but nothing in the Netlify config or the GitHub Actions docs-build workflow was setting the variable — so the production deploy of the docs site would silently ship without GTM, dropping analytics that the previous VuePress site had always loaded. Setting `BUILD_MODE = "production"` under `[context.production.environment]` is the right Netlify-native pattern: it only applies to deploys from the production branch, leaving PR deploy previews and branch deploys with `BUILD_MODE` unset so analytics doesn't fire on non-canonical URLs. --- netlify.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/netlify.toml b/netlify.toml index be21b6e9b1..8472701c65 100644 --- a/netlify.toml +++ b/netlify.toml @@ -5,6 +5,13 @@ [build.environment] NODE_VERSION = "22" +# Production-context only: opts the build into the analytics + production +# scripts that `astro.config.mjs` gates on `BUILD_MODE === 'production'` +# (Google Tag Manager). PR deploy previews and other branch deploys keep +# `BUILD_MODE` unset so analytics doesn't fire on non-canonical URLs. +[context.production.environment] + BUILD_MODE = "production" + # The docs site is built with `base: '/docs'` in astro.config.mjs, so the # generated HTML references `/docs/_astro/*`, `/docs/guide/*`, etc. In # production, `hyperformula.handsontable.com/docs/` is served by a reverse