├── .nvmrc ├── test ├── fixtures │ ├── examples │ │ ├── empty.md │ │ ├── serialize.md │ │ ├── etcd.md │ │ ├── offline.md │ │ ├── sqlite.md │ │ ├── mongo.md │ │ ├── caching-koa.md │ │ ├── caching-fastify.md │ │ ├── caching-express.md │ │ ├── caching-javascript.md │ │ ├── mysql.md │ │ ├── postgres.md │ │ ├── tiered.md │ │ ├── test-suite.md │ │ ├── caching-node.md │ │ ├── redis.md │ │ ├── caching-nestjs.md │ │ ├── memcache.md │ │ ├── single-site.md │ │ ├── docula-readme.md │ │ ├── no-front-matter.md │ │ ├── readme-example.md │ │ ├── front-matter.md │ │ ├── index.md │ │ └── keyv.md │ └── load-from-file.md ├── writr-render.test.ts ├── writr-hooks.test.ts ├── content-fixtures.ts ├── writr-cache.test.ts └── examples.test.ts ├── site ├── favicon.ico ├── docula.config.mjs ├── variables.css └── logo.svg ├── pnpm-workspace.yaml ├── SECURITY.md ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── tests.yml │ ├── code-coverage.yml │ ├── release.yml │ ├── deploy-site.yml │ └── code-ql.yml ├── vitest.config.ts ├── biome.json ├── LICENSE ├── tsconfig.json ├── .gitignore ├── CONTRIBUTING.md ├── src ├── writr-cache.ts └── writr.ts ├── package.json ├── CODE_OF_CONDUCT.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /test/fixtures/examples/empty.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredwray/writr/HEAD/site/favicon.ico -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - esbuild 3 | - unrs-resolver 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We attempt to keep this project up to date with the latest modules on a regular basis. Please make sure to upgrade to the latest to avoid major issues. To report a vulnerability please create an issue and assign the owner 'Jared Wray' to it as it will notify him and he is actively maintaining this project. 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Please check if the PR fulfills these requirements** 2 | - [ ] Followed the [Contributing](../blob/main/CONTRIBUTING.md) and [Code of Conduct](../blob/main/CODE_OF_CONDUCT.md) guidelines. 3 | - [ ] Tests for the changes have been added (for bug fixes/features) with 100% code coverage. 4 | 5 | **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 6 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ['text', 'json', 'lcov'], 7 | exclude: [ 8 | 'site/docula.config.cjs', 9 | 'site-output/**', 10 | '.pnp.*', 11 | '.yarn/**', 12 | 'vitest.config.ts', 13 | 'dist/**', 14 | 'site/**', 15 | 'test/**', 16 | ], 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **How To Reproduce (best to provide workable code or tests!)** 14 | Please provide code that can be ran in a stand alone fashion that will reproduce the error. If you can provide a test that will be even better. If you can't provide code, please provide a detailed description of how to reproduce the error. -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "includes": ["src/*.ts", "test/*.ts"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "linter": { 17 | "enabled": true, 18 | "rules": { 19 | "recommended": true 20 | } 21 | }, 22 | "javascript": { 23 | "formatter": { 24 | "quoteStyle": "double" 25 | } 26 | }, 27 | "assist": { 28 | "enabled": true, 29 | "actions": { 30 | "source": { 31 | "organizeImports": "on" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /site/docula.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import process from 'node:process'; 4 | 5 | export const options = { 6 | githubPath: 'jaredwray/writr', 7 | siteTitle: 'Writr', 8 | siteDescription: 'Beautiful Website for Your Projects', 9 | siteUrl: 'https://writr.org', 10 | }; 11 | 12 | export const onPrepare = async config => { 13 | const readmePath = path.join(process.cwd(), './README.md'); 14 | const readmeSitePath = path.join(config.sitePath, 'README.md'); 15 | const readme = await fs.promises.readFile(readmePath, 'utf8'); 16 | const updatedReadme = readme.replace('![Writr](site/logo.svg)', ''); 17 | console.log('writing updated readme to', readmeSitePath); 18 | await fs.promises.writeFile(readmeSitePath, updatedReadme); 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | tests: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: ['20', '22'] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install Dependencies 30 | run: npm install -g pnpm && pnpm install 31 | 32 | - name: Build 33 | run: pnpm build 34 | 35 | - name: Testing 36 | run: pnpm test:ci 37 | 38 | -------------------------------------------------------------------------------- /test/fixtures/examples/serialize.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/serialize' 3 | sidebarTitle: '@keyv/serialize' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/serialize 8 | 9 | > Serialization functionality for [Keyv](https://github.com/jaredwray/keyv) 10 | 11 | 12 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 13 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 14 | [![GitHub license](https://img.shields.io/github/license/jaredwray/keyv)](https://github.com/jaredwray/keyv/blob/master/LICENSE) 15 | [![npm](https://img.shields.io/npm/dm/@keyv/memcache)](https://npmjs.com/package/@keyv/memcache) 16 | 17 | ## License 18 | 19 | MIT © Jared Wray 20 | -------------------------------------------------------------------------------- /.github/workflows/code-coverage.yml: -------------------------------------------------------------------------------- 1 | name: code-coverage 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | code-coverage: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js 22 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 22 24 | 25 | - name: Install Dependencies 26 | run: npm install -g pnpm && pnpm install 27 | 28 | - name: Build 29 | run: pnpm build 30 | 31 | - name: Testing 32 | run: pnpm test:ci 33 | 34 | - name: Code Coverage 35 | uses: codecov/codecov-action@v5 36 | with: 37 | token: ${{ secrets.CODECOV_KEY }} 38 | files: coverage/lcov.info 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [released] 7 | 8 | permissions: 9 | contents: read 10 | id-token: write 11 | 12 | jobs: 13 | release: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js 22 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 22 23 | 24 | - name: Install Dependencies 25 | run: npm install -g pnpm && pnpm install 26 | 27 | - name: Build 28 | run: pnpm build 29 | 30 | - name: Testing 31 | run: pnpm test:ci 32 | 33 | - name: Setup npm auth 34 | run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 35 | 36 | - name: Publish 37 | run: pnpm publish --provenance --access public 38 | 39 | 40 | -------------------------------------------------------------------------------- /site/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-family: 'Open Sans', sans-serif; 3 | 4 | --color-primary: #373232; 5 | --color-secondary: #FF6808; 6 | --color-secondary-dark: #FF6808; 7 | --color-text: #373232; 8 | 9 | --background: #ffffff; 10 | --home-background: #ffffff; 11 | --header-background: #ffffff; 12 | 13 | --sidebar-background: #ffffff; 14 | --sidebar-text: #322d3c; 15 | --sidebar-text-active: #7d7887; 16 | 17 | --border: rgba(238,238,245,1); 18 | 19 | --background-search-highlight: var(--color-secondary-dark); 20 | --color-search-highlight: #ffffff; 21 | --search-input-background: var(--header-background); 22 | 23 | --code: rgba(238,238,245,1); 24 | 25 | --pagefind-ui-text: var(--color-text) !important; 26 | --pagefind-ui-font: var(--font-family) !important; 27 | --pagefind-ui-background: var(--background) !important; 28 | --pagefind-ui-border: var(--border) !important; 29 | --pagefind-ui-scale: .9 !important; 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/deploy-site.yml: -------------------------------------------------------------------------------- 1 | name: deploy-site 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [released] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | deploy-site: 13 | name: Deploy Website 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | # Test 21 | - name: Use Node.js 22 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22 25 | 26 | - name: Install Dependencies 27 | run: npm install -g pnpm && pnpm install 28 | 29 | - name: Build 30 | run: pnpm build 31 | 32 | - name: Build Website 33 | run: pnpm website:build 34 | 35 | - name: Publish to Cloudflare Pages 36 | uses: cloudflare/wrangler-action@v3 37 | with: 38 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 39 | command: pages deploy site/dist --project-name=writr-org --branch=main -------------------------------------------------------------------------------- /test/fixtures/examples/etcd.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/etcd' 3 | sidebarTitle: '@keyv/etcd' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/etcd 8 | 9 | > Etcd storage adapter for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/etcd.svg)](https://www.npmjs.com/package/@keyv/etcd) 14 | 15 | Etcd storage adapter for [Keyv](https://github.com/jaredwray/keyv). 16 | 17 | ## Install 18 | 19 | ```shell 20 | npm install --save keyv @keyv/etcd 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```js 26 | const Keyv = require('keyv'); 27 | 28 | const keyv = new Keyv('etcd://localhost:2379'); 29 | keyv.on('error', handleConnectionError); 30 | ``` 31 | 32 | ## License 33 | 34 | Copyright (c) 2022 Jared Wray 35 | -------------------------------------------------------------------------------- /test/fixtures/load-from-file.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello World from Load" 3 | --- 4 | 5 | # Super Comfortable Chair 6 | 7 | ## Description 8 | 9 | The **Super Comfortable Chair** is designed with ergonomics in mind, providing maximum comfort for long hours of sitting. Whether you're working from home or gaming, this chair has you covered. 10 | 11 | ## Features 12 | 13 | - Ergonomic design to reduce strain on your back. 14 | - Adjustable height and recline for personalized comfort. 15 | - Durable materials that stand the test of time. 16 | 17 | ## Price 18 | 19 | At just **$149.99**, this chair is a steal! 20 | 21 | ## Reviews 22 | 23 | > "This chair has completely changed my home office setup. I can work for hours without feeling fatigued." — *Jane Doe* 24 | 25 | > "Worth every penny! The comfort is unmatched." — *John Smith* 26 | 27 | ## Purchase 28 | 29 | Don't miss out on the opportunity to own the **Super Comfortable Chair**. Click [here](https://example.com/product/CHAIR12345) to purchase now! -------------------------------------------------------------------------------- /test/fixtures/examples/offline.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/offline' 3 | sidebarTitle: '@keyv/offline' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/offline 8 | 9 | > Offline storage adapter for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/offline.svg)](https://www.npmjs.com/package/@keyv/offline) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/offline)](https://npmjs.com/package/@keyv/offline) 15 | 16 | Offline storage adapter for [Keyv](https://github.com/jaredwray/keyv). 17 | 18 | ## Install 19 | 20 | ```shell 21 | npm install --save keyv @keyv/offline 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | const Keyv = require('keyv'); 28 | 29 | const keyv = new Keyv('offline://path/to/database.offline'); 30 | keyv.on('error', handleConnectionError); 31 | ``` 32 | 33 | ## License 34 | 35 | MIT © Jared Wray 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jared Wray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/fixtures/examples/sqlite.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/sqlite' 3 | sidebarTitle: '@keyv/sqlite' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/sqlite 8 | 9 | > SQLite storage adapter for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/sqlite.svg)](https://www.npmjs.com/package/@keyv/sqlite) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/sqlite)](https://npmjs.com/package/@keyv/sqlite) 15 | 16 | SQLite storage adapter for [Keyv](https://github.com/lukechilds/keyv). 17 | 18 | ## Install 19 | 20 | ```shell 21 | npm install --save keyv @keyv/sqlite 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | const Keyv = require('keyv'); 28 | 29 | const keyv = new Keyv('sqlite://path/to/database.sqlite'); 30 | keyv.on('error', handleConnectionError); 31 | ``` 32 | 33 | You can specify the `table` and [`busyTimeout`](https://sqlite.org/c3ref/busy_timeout.html) option. 34 | 35 | e.g: 36 | 37 | ```js 38 | const keyv = new Keyv('sqlite://path/to/database.sqlite', { 39 | table: 'cache', 40 | busyTimeout: 10000 41 | }); 42 | ``` 43 | 44 | ## License 45 | 46 | MIT © Jared Wray 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 7 | 8 | /* Emit */ 9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 12 | 13 | /* Interop Constraints */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | 17 | /* Type Checking */ 18 | "strict": true, /* Enable all strict type-checking options. */ 19 | 20 | /* Completeness */ 21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 22 | "lib": [ 23 | "ESNext", "DOM" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # apple 58 | .DS_Store 59 | 60 | # dotenv environment variables file 61 | .env 62 | yarn.lock 63 | package-lock.json 64 | 65 | #test-output 66 | blog_output 67 | blog_public 68 | dist 69 | test_output 70 | site-output 71 | 72 | # IDEs 73 | .idea/ 74 | pnpm-lock.yaml 75 | .pnp.* 76 | .yarn 77 | bun.lockb 78 | 79 | # Docula 80 | site/dist 81 | site/README.md 82 | 83 | # AI 84 | .claude 85 | 86 | 87 | -------------------------------------------------------------------------------- /test/fixtures/examples/mongo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/mongo' 3 | sidebarTitle: '@keyv/mongo' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/mongo 8 | 9 | > MongoDB storage adapter for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/mongo.svg)](https://www.npmjs.com/package/@keyv/mongo) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/mongo)](https://npmjs.com/package/@keyv/mongo) 15 | 16 | MongoDB storage adapter for [Keyv](https://github.com/jaredwray/keyv). 17 | 18 | Uses TTL indexes to automatically remove expired documents. However [MongoDB doesn't guarantee data will be deleted immediately upon expiration](https://docs.mongodb.com/manual/core/index-ttl/#timing-of-the-delete-operation), so expiry dates are revalidated in Keyv. 19 | 20 | ## Install 21 | 22 | ```shell 23 | npm install --save keyv @keyv/mongo 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | const Keyv = require('keyv'); 30 | 31 | const keyv = new Keyv('mongodb://user:pass@localhost:27017/dbname'); 32 | keyv.on('error', handleConnectionError); 33 | ``` 34 | 35 | You can specify the collection name, by default `'keyv'` is used. 36 | 37 | e.g: 38 | 39 | ```js 40 | const keyv = new Keyv('mongodb://user:pass@localhost:27017/dbname', { collection: 'cache' }); 41 | ``` 42 | 43 | ## License 44 | 45 | MIT © Jared Wray 46 | -------------------------------------------------------------------------------- /site/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/examples/caching-koa.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'How to Implement Caching with Koa' 3 | sidebarTitle: 'Caching with Koa' 4 | parent: 'Caching' 5 | --- 6 | 7 | # How to Implement Caching with Koa 8 | 9 | ## What is Koa? 10 | Koa is a web framework from the team behind Express that offers a smaller, more expressive, more robust foundation for APIs and web applications. Koa's use of async functions removes the need for callbacks and increases error handling. A Koa Context combines a node request and response object into a single object providing numerous helpful methods for writing APIs and web apps. 11 | 12 | ## What is a Cache? 13 | A cache is a short-term, high-speed data storage layer that stores a subset of data, enabling it to be retrieved faster than accessing it from its primary storage location. Caching allows you to reuse previously retrieved data efficiently. 14 | 15 | ## Caching Support in Keyv 16 | Caching will work in memory by default. However, users can also install a Keyv storage adapter that is initialized with a connection string or any other storage that implements the Map API. 17 | 18 | ### Example - Add Cache Support Using Koa 19 | 20 | ```js 21 | import keyv from 'keyv'; 22 | 23 | // ... 24 | const cache = new Keyv(); 25 | const cacheTTL = 1000 * 60 * 60 * 24; // 24 hours 26 | 27 | app.use(async ctx => { 28 | // this response is already cashed if `true` is returned, 29 | // so this middleware will automatically serve this response from cache 30 | if (await keyv.get(ctx.url)) { 31 | return keyv.get(ctx.url); 32 | } 33 | 34 | // set the response body here 35 | ctx.body = 'hello world!'; 36 | // cache the response 37 | await keyv.set(ctx.url, ctx.body. cacheTTL); 38 | }); 39 | ``` -------------------------------------------------------------------------------- /test/fixtures/examples/caching-fastify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'How to Implement Caching with Fastify' 3 | sidebarTitle: 'Caching with Fastify' 4 | parent: 'Caching' 5 | --- 6 | 7 | # How to Implement Caching with Fastify 8 | 9 | ## What is Fastify? 10 | Fastify is a web framework that provides a powerful plugin-based developer experience (inspired by Hapi and Express) with minimal overhead and is one of the fastest frameworks available, serving up to 30k requests per second. It is fully extensible via hooks, plugins, and decorators. Being schema-based, Fastify compiles schemas very efficiently. A TypeScript type declaration file is also maintained to support the growing TypeScript community. 11 | 12 | ## What is a Cache? 13 | A cache is a short-term, high-speed data storage layer that stores a subset of data, enabling it to be retrieved faster than accessing it from its primary storage location. Caching allows you to reuse previously retrieved data efficiently. 14 | 15 | ## Caching Support in Keyv 16 | Caching will work in memory by default. However, users can also install a Keyv storage adapter that is initialized with a connection string or any other storage that implements the Map API. 17 | 18 | ### Example - Add Cache Support Using Fastify 19 | 20 | ```js 21 | const Keyv = require('keyv'); 22 | 23 | //make sure to install @keyv/redis 24 | const keyv = new Keyv('redis://user:pass@localhost:6379'); 25 | 26 | const fastify = require('fastify')() 27 | fastify 28 | .register(require('@fastify/caching'), {cache: keyv}) 29 | 30 | fastify.get('/', (req, reply) => { 31 | fastify.cache.set('hello', {hello: 'world'}, 10000, (err) => { 32 | if (err) return reply.send(err) 33 | reply.send({hello: 'world'}) 34 | }) 35 | }) 36 | 37 | fastify.listen({ port: 3000 }, (err) => { 38 | if (err) throw err 39 | }) 40 | 41 | ``` -------------------------------------------------------------------------------- /test/fixtures/examples/caching-express.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'How to Implement Caching with Express' 3 | sidebarTitle: 'Caching with Express' 4 | parent: 'Caching' 5 | --- 6 | 7 | # How to Implement Caching with Express 8 | 9 | ## What is Express? 10 | Express is a minimal Node.js web application framework. Its APIs provide web and mobile application functionality. Its simplicity enables users to quickly create a robust API in a familiar environment with enhanced features, including Robust routing, high performance, HTTP helpers, support for 14+ view template engines, content negotiation, and an executable for generating applications quickly. 11 | 12 | ## What is a Cache? 13 | A cache is a short-term, high-speed data storage layer that stores a subset of data, enabling it to be retrieved faster than accessing it from its primary storage location. Caching allows you to reuse previously retrieved data efficiently. 14 | 15 | ## Caching Support in Keyv 16 | Caching will work in memory by default. However, users can also install a Keyv storage adapter that is initialized with a connection string or any other storage that implements the Map API. 17 | 18 | ### Example - Add Cache Support Using Express 19 | 20 | ```js 21 | const express = require('express'); 22 | const Keyv = require('keyv'); 23 | 24 | const keyv = new Keyv(); 25 | 26 | const app = express(); 27 | 28 | const ttl = 1000 * 60 * 60 * 24; // 24 hours 29 | 30 | app.get('/test-cache/:id', async (req, res) => { 31 | if(!req.params.id) return res.status(400).send('Missing id param'); 32 | const id = req.params.id; 33 | const cached = await keyv.get(id); 34 | if(cached) { 35 | return res.send(cached); 36 | } else { 37 | const data = await getData(id); 38 | await keyv.set(id, data, ttl); 39 | return res.send(data); 40 | } 41 | }); 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /test/fixtures/examples/caching-javascript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'How to Implement Caching in Javascript' 3 | sidebarTitle: 'Caching in Javascript' 4 | parent: 'Caching' 5 | --- 6 | 7 | # How to Implement Caching in Javascript 8 | 9 | ## What is a Cache? 10 | A cache is a short-term, high-speed data storage layer that stores a subset of data, enabling it to be retrieved faster than accessing it from its primary storage location. Caching allows you to reuse previously retrieved data efficiently. 11 | 12 | ## Caching Support in Keyv 13 | Caching will work in memory by default. However, users can also install a Keyv storage adapter that is initialized with a connection string or any other storage that implements the Map API. 14 | 15 | ## Extend your own Module with Keyv to Add Cache Support 16 | - Keyv can be easily embedded into other modules to add cache support. 17 | - You should also set a namespace for your module to safely call `.clear()` without clearing unrelated app data. 18 | 19 | >**Note**: 20 | > The recommended pattern is to expose a cache option in your module's options which is passed through to Keyv. 21 | 22 | ### Example - Add Cache Support to a Module 23 | 24 | 1. Install whichever storage adapter you will be using, `@keyv/redis` in this example 25 | ```sh 26 | npm install --save @keyv/redis 27 | ``` 28 | 2. Declare the Module with the cache controlled by a Keyv instance 29 | ```js 30 | class AwesomeModule { 31 | constructor(opts) { 32 | this.cache = new Keyv({ 33 | uri: typeof opts.cache === 'string' && opts.cache, 34 | store: typeof opts.cache !== 'string' && opts.cache, 35 | namespace: 'awesome-module' 36 | }); 37 | } 38 | } 39 | ``` 40 | 41 | 3. Create an Instance of the Module with caching support 42 | ```js 43 | const AwesomeModule = require('awesome-module'); 44 | const awesomeModule = new AwesomeModule({ cache: 'redis://localhost' }); 45 | ``` 46 | -------------------------------------------------------------------------------- /test/fixtures/examples/mysql.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/mysql' 3 | sidebarTitle: '@keyv/mysql' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/mysql 8 | 9 | > MySQL/MariaDB storage adapter for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/mysql.svg)](https://www.npmjs.com/package/@keyv/mysql) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/mysql)](https://npmjs.com/package/@keyv/mysql) 15 | 16 | MySQL/MariaDB storage adapter for [Keyv](https://github.com/jaredwray/keyv). 17 | 18 | ## Install 19 | 20 | ```shell 21 | npm install --save keyv @keyv/mysql 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | const Keyv = require('keyv'); 28 | 29 | const keyv = new Keyv('mysql://user:pass@localhost:3306/dbname'); 30 | keyv.on('error', handleConnectionError); 31 | ``` 32 | 33 | You can specify a custom table with the `table` option and the primary key size with `keySize`. 34 | 35 | e.g: 36 | 37 | ```js 38 | const keyv = new Keyv('mysql://user:pass@localhost:3306/dbname', { 39 | table: 'cache', 40 | keySize: 255 41 | }); 42 | ``` 43 | 44 | ## SSL 45 | 46 | ```js 47 | const fs = require('fs'); 48 | const path = require('path'); 49 | const KeyvMysql = require('@keyv/mysql'); 50 | 51 | const options = { 52 | ssl: { 53 | rejectUnauthorized: false, 54 | ca: fs.readFileSync(path.join(__dirname, '/certs/ca.pem')).toString(), 55 | key: fs.readFileSync(path.join(__dirname, '/certs/client-key.pem')).toString(), 56 | cert: fs.readFileSync(path.join(__dirname, '/certs/client-cert.pem')).toString(), 57 | }, 58 | }; 59 | 60 | const keyv = new KeyvMysql({uri, ...options}); 61 | 62 | ``` 63 | 64 | **Note:** Some MySQL/MariaDB installations won't allow a key size longer than 767 bytes. If you get an error on table creation try reducing `keySize` to 191 or lower. [#5](https://github.com/jaredwray/keyv-sql/issues/5) 65 | 66 | ## License 67 | 68 | MIT © Jared Wray 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 3 | 4 | Please note we have a [Code of Conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 5 | 6 | We release new versions of this project (maintenance/features) on a monthly cadence so please be aware that some items will not get released right away. 7 | 8 | # Pull Request Process 9 | You can contribute changes to this repo by opening a pull request: 10 | 11 | 1) After forking this repository to your Git account, make the proposed changes on your forked branch. 12 | 2) Run tests and linting locally. 13 | - Run `pnpm install && pnpm test`. 14 | 3) Commit your changes and push them to your forked repository. 15 | 4) Navigate to the main `writr` repository and select the *Pull Requests* tab. 16 | 5) Click the *New pull request* button, then select the option "Compare across forks" 17 | 6) Leave the base branch set to main. Set the compare branch to your forked branch, and open the pull request. 18 | 7) Once your pull request is created, ensure that all checks have passed and that your branch has no conflicts with the base branch. If there are any issues, resolve these changes in your local repository, and then commit and push them to git. 19 | 8) Similarly, respond to any reviewer comments or requests for changes by making edits to your local repository and pushing them to Git. 20 | 9) Once the pull request has been reviewed, those with write access to the branch will be able to merge your changes into the `writr` repository. 21 | 22 | If you need more information on the steps to create a pull request, you can find a detailed walkthrough in the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) 23 | 24 | # Code of Conduct 25 | Please refer to our [Code of Conduct](https://github.com/jaredwray/writr/blob/main/CODE_OF_CONDUCT.md) readme for how to contribute to this open source project and work within the community. 26 | -------------------------------------------------------------------------------- /test/fixtures/examples/postgres.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/postgres' 3 | sidebarTitle: '@keyv/postgres' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/postgres 8 | 9 | > PostgreSQL storage adapter for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/btestsuild.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/postgres.svg)](https://www.npmjs.com/package/@keyv/postgres) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/postgres)](https://npmjs.com/package/@keyv/postgres) 15 | 16 | PostgreSQL storage adapter for [Keyv](https://github.com/jaredwray/keyv). 17 | 18 | Requires Postgres 9.5 or newer for `ON CONFLICT` support to allow performant upserts. [Why?](https://stackoverflow.com/questions/17267417/how-to-upsert-merge-insert-on-duplicate-update-in-postgresql/17267423#17267423) 19 | 20 | ## Install 21 | 22 | ```shell 23 | npm install --save keyv @keyv/postgres 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | const Keyv = require('keyv'); 30 | 31 | const keyv = new Keyv('postgresql://user:pass@localhost:5432/dbname'); 32 | keyv.on('error', handleConnectionError); 33 | ``` 34 | 35 | You can specify the `table` option. 36 | 37 | e.g: 38 | 39 | ```js 40 | const keyv = new Keyv('postgresql://user:pass@localhost:5432/dbname', { table: 'cache' }); 41 | ``` 42 | 43 | You can specify the `schema` option (default is `public`). 44 | 45 | e.g: 46 | 47 | ```js 48 | const keyv = new Keyv('postgresql://user:pass@localhost:5432/dbname', { schema: 'keyv' }); 49 | ``` 50 | 51 | ## Testing 52 | 53 | When testing you can use our `docker-compose` postgresql instance by having docker installed and running. This will start a postgres server, run the tests, and stop the server: 54 | 55 | At the root of the Keyv mono repo: 56 | ```shell 57 | yarn test:services:start 58 | ``` 59 | 60 | To just test the postgres adapter go to the postgres directory (packages/postgres) and run: 61 | ```shell 62 | yarn test 63 | ``` 64 | 65 | ## License 66 | 67 | MIT © Jared Wray 68 | -------------------------------------------------------------------------------- /test/fixtures/examples/tiered.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/tiered' 3 | sidebarTitle: '@keyv/tiered' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/tiered 8 | 9 | > Tiered storage adapter for Keyv to manage local and remote store as one for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/tiered.svg)](https://www.npmjs.com/package/@keyv/tiered) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/tiered)](https://npmjs.com/package/@keyv/tiered) 15 | 16 | Tiered storage adapter for [Keyv](https://github.com/jaredwray/keyv). 17 | 18 | ## Install 19 | 20 | ```shell 21 | npm install --save keyv @keyv/tiered 22 | ``` 23 | 24 | ## Usage 25 | 26 | First, you need to provide your `local` and `remote` stores to be used, being possible to use any [Keyv storage adapter](https://github.com/jaredwray/keyv#storage-adapters): 27 | 28 | ```js 29 | const Keyv = require('keyv'); 30 | const KeyvSqlite = require('@keyv/sqlite'); 31 | const KeyvTiered = require('@keyv/tiered'); 32 | const remoteStore = () => new Keyv({ 33 | store: new KeyvSqlite({ 34 | uri: 'sqlite://test/testdb.sqlite', 35 | busyTimeout: 30_000, 36 | }), 37 | }); 38 | const localStore = () => new Keyv(); 39 | const remote = remoteStore(); 40 | const local = localStore(); 41 | const store = new KeyvTiered({remote, local}); 42 | const keyv = new Keyv({store}); 43 | keyv.on('error', handleConnectionError); 44 | ``` 45 | 46 | ## API 47 | 48 | ### KeyvTiered(\[options]) 49 | 50 | #### options 51 | 52 | ##### local 53 | 54 | Type: `Object`
55 | Default: `new Keyv()` 56 | 57 | A keyv instance to be used as local strategy. 58 | 59 | ##### remote 60 | 61 | Type: `Object`
62 | Default: `new Keyv()` 63 | 64 | A keyv instance to be used as remote strategy. 65 | 66 | ##### validator 67 | 68 | Type: `Function`
69 | Default: `() => true` 70 | 71 | The validator function is used as a precondition to determining is remote storage should be checked. 72 | 73 | ## License 74 | 75 | MIT © Jared Wray -------------------------------------------------------------------------------- /test/fixtures/examples/test-suite.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Test Suite' 3 | permalink: /docs/test-suite/ 4 | order: 6 5 | --- 6 | 7 | # @keyv/test-suite 8 | 9 | > Test suite for Keyv API compliance 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/test-suite.svg)](https://www.npmjs.com/package/@keyv/test-suite) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/test-suite)](https://npmjs.com/package/@keyv/test-suite) 15 | 16 | Complete [AVA](https://github.com/avajs/ava) test suite to test a [Keyv](https://github.com/jaredwray/keyv) storage adapter for API compliance. 17 | 18 | ## Usage 19 | 20 | ### Install 21 | 22 | Install AVA, Keyv and `@keyv/test-suite` as development dependencies. 23 | 24 | ```shell 25 | npm install --save-dev ava keyv @keyv/test-suite 26 | ``` 27 | 28 | Then update `keyv` and `@keyv/test-suite` versions to `*` in `package.json` to ensure you're always testing against the latest version. 29 | 30 | ### Create Test File 31 | 32 | `test.js` 33 | 34 | ```js 35 | import test from 'ava'; 36 | import keyvTestSuite from '@keyv/test-suite'; 37 | import Keyv from 'keyv'; 38 | import KeyvStore from './'; 39 | 40 | const store = () => new KeyvStore(); 41 | keyvTestSuite(test, Keyv, store); 42 | ``` 43 | 44 | Where `KeyvStore` is your storage adapter. 45 | 46 | Set your test script in `package.json` to `ava`. 47 | ```json 48 | "scripts": { 49 | "test": "ava" 50 | } 51 | ``` 52 | 53 | ## Example for Storage Adapters 54 | 55 | Take a look at [keyv-redis](https://github.com/jaredwray/keyv-redis) for an example of an existing storage adapter using `@keyv/test-suite`. 56 | 57 | ## Testing Compression Adapters 58 | 59 | If you're testing a compression adapter, you can use the `keyvCompresstionTests` method instead of `keyvTestSuite`. 60 | 61 | ```js 62 | const {keyvCompresstionTests} = require('@keyv/test-suite'); 63 | const KeyvGzip = require('@keyv/compress-gzip'); 64 | 65 | keyvCompresstionTests(test, new KeyvGzip()); 66 | ``` 67 | 68 | ## License 69 | 70 | MIT © Jared Wray 71 | -------------------------------------------------------------------------------- /.github/workflows/code-ql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "code-ql" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | language: [ 'javascript' ] 30 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 31 | # Learn more: 32 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v2 37 | 38 | # Initializes the CodeQL tools for scanning. 39 | - name: Initialize CodeQL 40 | uses: github/codeql-action/init@v1 41 | with: 42 | languages: ${{ matrix.language }} 43 | # If you wish to specify custom queries, you can do so here or in a config file. 44 | # By default, queries listed here will override any specified in a config file. 45 | # Prefix the list here with "+" to use these queries and those in the config file. 46 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 47 | 48 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 49 | # If this step fails, then you should remove it and run the build manually (see below) 50 | - name: Autobuild 51 | uses: github/codeql-action/autobuild@v1 52 | 53 | # ℹ️ Command-line programs to run using the OS shell. 54 | # 📚 https://git.io/JvXDl 55 | 56 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 57 | # and modify them (or add more) to build your code if your project 58 | # uses a compiled language 59 | 60 | #- run: | 61 | # make bootstrap 62 | # make release 63 | 64 | - name: Perform CodeQL Analysis 65 | uses: github/codeql-action/analyze@v1 66 | -------------------------------------------------------------------------------- /test/writr-render.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { describe, expect, test } from "vitest"; 3 | import { Writr } from "../src/writr.js"; 4 | 5 | const testContentOne = `--- 6 | title: "Super Comfortable Chair" 7 | product_id: "CHAIR12345" 8 | price: 149.99 9 | --- 10 | 11 | # Super Comfortable Chair 12 | This is a super chair and amazing chair. It is very comfortable and you will love it. 13 | `; 14 | 15 | const testContentOneResult = `

Super Comfortable Chair

16 |

This is a super chair and amazing chair. It is very comfortable and you will love it.

`; 17 | 18 | const options = { 19 | renderOptions: { 20 | caching: true, 21 | }, 22 | }; 23 | 24 | describe("Writr Async render with Caching", async () => { 25 | test("should render a template with caching", async () => { 26 | const writr = new Writr(testContentOne, options); 27 | const result = await writr.render(); 28 | expect(result).toBe(testContentOneResult); 29 | expect(writr.cache).toBeDefined(); 30 | expect(writr.cache.store.size).toBe(1); 31 | const result2 = await writr.render(); 32 | expect(result2).toBe(testContentOneResult); 33 | }); 34 | 35 | test("should sync render a template with caching", async () => { 36 | const writr = new Writr(testContentOne, options); 37 | const result = writr.renderSync(); 38 | expect(result).toBe(testContentOneResult); 39 | expect(writr.cache).toBeDefined(); 40 | expect(writr.cache.store.size).toBe(1); 41 | const result2 = writr.renderSync(); 42 | expect(result2).toBe(testContentOneResult); 43 | }); 44 | 45 | test("should render with async and then cache. Then render with sync via cache", async () => { 46 | const writr = new Writr(testContentOne, options); 47 | const result = await writr.render(); 48 | expect(result).toBe(testContentOneResult); 49 | expect(writr.cache).toBeDefined(); 50 | expect(writr.cache.store.size).toBe(1); 51 | const result2 = writr.renderSync(); 52 | expect(result2).toBe(testContentOneResult); 53 | }); 54 | }); 55 | 56 | describe("Render and Save to File", async () => { 57 | test("should render a template and save to file", async () => { 58 | const writr = new Writr(testContentOne, options); 59 | const fileName = "./test/fixtures/temp-render/test-output.html"; 60 | if (fs.existsSync(fileName)) { 61 | fs.unlinkSync(fileName); 62 | } 63 | 64 | await writr.renderToFile(fileName); 65 | 66 | expect(fs.existsSync(fileName)).toBe(true); 67 | 68 | fs.unlinkSync(fileName); 69 | }); 70 | 71 | test("should render a template and save to file sync", async () => { 72 | const writr = new Writr(testContentOne, options); 73 | const fileName = "./test/fixtures/temp-render/test-output-sync.html"; 74 | if (fs.existsSync(fileName)) { 75 | fs.unlinkSync(fileName); 76 | } 77 | 78 | writr.renderToFileSync(fileName); 79 | 80 | expect(fs.existsSync(fileName)).toBe(true); 81 | 82 | fs.unlinkSync(fileName); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/writr-cache.ts: -------------------------------------------------------------------------------- 1 | import { CacheableMemory } from "cacheable"; 2 | import { Hashery } from "hashery"; 3 | import type { RenderOptions } from "./writr.js"; 4 | 5 | export class WritrCache { 6 | private readonly _store: CacheableMemory = new CacheableMemory(); 7 | private readonly _hashStore: CacheableMemory = new CacheableMemory(); 8 | private readonly _hash: Hashery = new Hashery(); 9 | 10 | public get store(): CacheableMemory { 11 | return this._store; 12 | } 13 | 14 | public get hashStore(): CacheableMemory { 15 | return this._hashStore; 16 | } 17 | 18 | public get(markdown: string, options?: RenderOptions): string | undefined { 19 | const key = this.hash(markdown, options); 20 | return this._store.get(key); 21 | } 22 | 23 | public set(markdown: string, value: string, options?: RenderOptions) { 24 | const key = this.hash(markdown, options); 25 | this._store.set(key, value); 26 | } 27 | 28 | public clear() { 29 | this._store.clear(); 30 | this._hashStore.clear(); 31 | } 32 | 33 | public hash(markdown: string, options?: RenderOptions): string { 34 | const sanitizedOptions = this.sanitizeOptions(options); 35 | const content = { markdown, options: sanitizedOptions }; 36 | const key = JSON.stringify(content); 37 | const result = this._hashStore.get(key); 38 | if (result) { 39 | return result; 40 | } 41 | 42 | const hash = this._hash.toHashSync(content); 43 | this._hashStore.set(key, hash); 44 | 45 | return hash; 46 | } 47 | 48 | /** 49 | * Sanitizes render options to only include serializable properties for caching. 50 | * This prevents issues with structuredClone when options contain Promises, functions, or circular references. 51 | * @param {RenderOptions} [options] The render options to sanitize 52 | * @returns {RenderOptions | undefined} A new object with only the known RenderOptions properties 53 | */ 54 | private sanitizeOptions(options?: RenderOptions): RenderOptions | undefined { 55 | if (!options) { 56 | return undefined; 57 | } 58 | 59 | // Only extract the known, serializable properties from RenderOptions 60 | const sanitized: RenderOptions = {}; 61 | 62 | if (options.emoji !== undefined) { 63 | sanitized.emoji = options.emoji; 64 | } 65 | 66 | /* v8 ignore next -- @preserve */ 67 | if (options.toc !== undefined) { 68 | sanitized.toc = options.toc; 69 | } 70 | 71 | if (options.slug !== undefined) { 72 | sanitized.slug = options.slug; 73 | } 74 | 75 | if (options.highlight !== undefined) { 76 | sanitized.highlight = options.highlight; 77 | } 78 | 79 | if (options.gfm !== undefined) { 80 | sanitized.gfm = options.gfm; 81 | } 82 | 83 | if (options.math !== undefined) { 84 | sanitized.math = options.math; 85 | } 86 | 87 | if (options.mdx !== undefined) { 88 | sanitized.mdx = options.mdx; 89 | } 90 | 91 | if (options.caching !== undefined) { 92 | sanitized.caching = options.caching; 93 | } 94 | 95 | return sanitized; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/fixtures/examples/caching-node.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Utilizing Keyv for Caching in Node.js: A Step-by-Step Guide' 3 | sidebarTitle: 'Caching in Node.js' 4 | parent: 'Caching' 5 | --- 6 | 7 | # Utilizing Keyv for Caching in Node.js: A Step-by-Step Guide 8 | 9 | ## 1. Setting up the Project 10 | To start a new Node.js project, you first need to create a new directory for your project and then initialize a new Node.js project in that directory. 11 | 12 | ```bash 13 | mkdir keyv-cache-demo 14 | cd keyv-cache-demo 15 | npm init -y 16 | ``` 17 | The npm init -y command will create a new package.json file in your project directory with default settings. 18 | 19 | ## 2. Installing Keyv and its Dependencies 20 | In this step, you'll install Keyv and a Keyv storage adapter for your project. For this example, we'll use SQLite as the storage adapter. 21 | 22 | ```bash 23 | npm install keyv @keyv/sqlite 24 | ``` 25 | Keyv supports a variety of storage adapters like Redis, MongoDB, PostgreSQL, etc. Feel free to choose the one that best fits your project requirements. 26 | 27 | ## 3. Creating a Caching Service Example 28 | In this step, we'll create a simple caching service using Keyv. 29 | 30 | Create a new file named cacheService.js in your project directory and add the following code to that file. 31 | 32 | ```javascript 33 | const Keyv = require('keyv'); 34 | const keyv = new Keyv('sqlite://path/to/database.sqlite'); 35 | 36 | class CacheService { 37 | async get(key) { 38 | const value = await keyv.get(key); 39 | if (value) { 40 | console.log('Cache hit'); 41 | } else { 42 | console.log('Cache miss'); 43 | } 44 | return value; 45 | } 46 | 47 | async set(key, value, ttlInMilliseconds) { 48 | await keyv.set(key, value, ttlInMilliseconds); 49 | } 50 | 51 | async delete(key) { 52 | await keyv.delete(key); 53 | } 54 | } 55 | 56 | module.exports = CacheService; 57 | ``` 58 | 59 | In this code: 60 | 61 | We're importing the Keyv library and initializing it with an SQLite database. 62 | 63 | We're creating a CacheService class with get, set, and delete methods that wrap the corresponding methods of the Keyv instance. The get method includes console logs to indicate whether the requested value was found in the cache. 64 | 65 | The set method includes an optional ttlInMilliseconds parameter, which you can use to set a time-to-live (TTL) for the cached value. 66 | 67 | Now you have a reusable CacheService that you can use to add caching to your Node.js project. 68 | 69 | Here is how you could use the CacheService: 70 | 71 | ```javascript 72 | const CacheService = require('./cacheService'); 73 | const cache = new CacheService(); 74 | 75 | // Usage 76 | async function fetchData() { 77 | const key = 'myData'; 78 | let data = await cache.get(key); 79 | if (!data) { 80 | data = await getMyData(); // Function that fetches your data 81 | await cache.set(key, data, 10000); // Cache for 10 seconds 82 | } 83 | return data; 84 | } 85 | ``` 86 | 87 | This is a basic example, and Keyv provides a lot of flexibility, so you can modify this service to better suit your project's needs. 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "writr", 3 | "version": "5.0.1", 4 | "description": "Markdown Rendering Simplified", 5 | "type": "module", 6 | "main": "./dist/writr.js", 7 | "types": "./dist/writr.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/writr.d.ts", 11 | "import": "./dist/writr.js" 12 | } 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/jaredwray/writr.git" 17 | }, 18 | "author": "Jared Wray ", 19 | "engines": { 20 | "node": ">=20" 21 | }, 22 | "license": "MIT", 23 | "keywords": [ 24 | "markdown", 25 | "html", 26 | "renderer", 27 | "markdown-to-html", 28 | "toc", 29 | "table-of-contents", 30 | "emoji", 31 | "syntax-highlighting", 32 | "markdown-processor", 33 | "github-flavored-markdown", 34 | "gfm", 35 | "remark-plugin", 36 | "rehype-plugin", 37 | "markdown-editor", 38 | "content-management", 39 | "documentation-tool", 40 | "blogging", 41 | "markdown-extension", 42 | "seo-friendly", 43 | "markdown-anchors", 44 | "remark", 45 | "rehype", 46 | "react", 47 | "react-component", 48 | "react-markdown", 49 | "markdown-to-react" 50 | ], 51 | "scripts": { 52 | "clean": "rimraf ./dist ./coverage ./node_modules ./pnpm-lock.yaml ./site/README.md ./site/dist", 53 | "build": "rimraf ./dist && tsup src/writr.ts --format esm --dts --clean", 54 | "prepare": "pnpm build", 55 | "lint": "biome check --write --error-on-warnings", 56 | "test": "pnpm lint && vitest run --coverage", 57 | "test:ci": "biome check --error-on-warnings && vitest run --coverage", 58 | "website:build": "rimraf ./site/README.md ./site/dist && npx docula build -s ./site -o ./site/dist", 59 | "website:serve": "rimraf ./site/README.md ./site/dist && npx docula serve -s ./site -o ./site/dist" 60 | }, 61 | "dependencies": { 62 | "cacheable": "^2.2.0", 63 | "hashery": "^1.2.0", 64 | "hookified": "^1.13.0", 65 | "html-react-parser": "^5.2.10", 66 | "js-yaml": "^4.1.1", 67 | "react": "^19.2.0", 68 | "rehype-highlight": "^7.0.2", 69 | "rehype-katex": "^7.0.1", 70 | "rehype-slug": "^6.0.0", 71 | "rehype-stringify": "^10.0.1", 72 | "remark-emoji": "^5.0.2", 73 | "remark-gfm": "^4.0.1", 74 | "remark-github-blockquote-alert": "^2.0.0", 75 | "remark-math": "^6.0.0", 76 | "remark-mdx": "^3.1.1", 77 | "remark-parse": "^11.0.0", 78 | "remark-rehype": "^11.1.2", 79 | "remark-toc": "^9.0.0", 80 | "unified": "^11.0.5" 81 | }, 82 | "devDependencies": { 83 | "@biomejs/biome": "^2.3.7", 84 | "@types/js-yaml": "^4.0.9", 85 | "@types/node": "^24.10.1", 86 | "@types/react": "^19.2.7", 87 | "@vitest/coverage-v8": "^4.0.14", 88 | "docula": "^0.31.1", 89 | "rimraf": "^6.1.2", 90 | "tsup": "^8.5.1", 91 | "typescript": "^5.9.3", 92 | "vitest": "^4.0.14" 93 | }, 94 | "files": [ 95 | "dist", 96 | "README.md", 97 | "LICENSE" 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /test/writr-hooks.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { describe, expect, test } from "vitest"; 3 | import { Writr, WritrHooks } from "../src/writr.js"; 4 | 5 | describe("Writr Render Hooks", async () => { 6 | test("it should change the content before rendering", async () => { 7 | const writr = new Writr("Hello, World!"); 8 | writr.onHook(WritrHooks.beforeRender, (data) => { 9 | data.body = "Hello, Universe!"; 10 | }); 11 | const result = await writr.render(); 12 | expect(result).toBe("

Hello, Universe!

"); 13 | }); 14 | 15 | test("it should change the content before rendering sync", () => { 16 | const writr = new Writr("Hello, World!"); 17 | writr.onHook(WritrHooks.beforeRender, (data) => { 18 | data.body = "Hello, Sync!"; 19 | }); 20 | const result = writr.renderSync(); 21 | expect(result).toBe("

Hello, Sync!

"); 22 | }); 23 | 24 | test("it should change the content before saving to file", async () => { 25 | const filePath = "./test-save-to-file.txt"; 26 | const writr = new Writr("Hello, World!"); 27 | writr.onHook(WritrHooks.saveToFile, (data) => { 28 | data.content = "Hello, File!"; 29 | }); 30 | await writr.saveToFile(filePath); 31 | await writr.loadFromFile(filePath); 32 | 33 | expect(writr.content).toBe("Hello, File!"); 34 | 35 | // Cleanup 36 | await fs.promises.rm(filePath); 37 | }); 38 | 39 | test("it should change the content before saving to file sync", async () => { 40 | const filePath = "./test-save-to-file-sync.txt"; 41 | const writr = new Writr("Hello, World!"); 42 | writr.onHook(WritrHooks.saveToFile, (data) => { 43 | data.content = "Hello, File Sync!"; 44 | }); 45 | writr.saveToFileSync(filePath); 46 | writr.loadFromFileSync(filePath); 47 | 48 | expect(writr.content).toBe("Hello, File Sync!"); 49 | 50 | // Cleanup 51 | await fs.promises.rm(filePath); 52 | }); 53 | 54 | test("it should change the content before render to file", async () => { 55 | const filePath = "./test-render-to-file.txt"; 56 | const writr = new Writr("Hello, World!"); 57 | writr.onHook(WritrHooks.renderToFile, (data) => { 58 | data.content = "Hello, File!"; 59 | }); 60 | await writr.renderToFile(filePath); 61 | const fileContent = await fs.promises.readFile(filePath); 62 | 63 | expect(fileContent.toString()).toContain("Hello, File!"); 64 | 65 | // Cleanup 66 | await fs.promises.rm(filePath); 67 | }); 68 | 69 | test("it should change the content before render to file sync", async () => { 70 | const filePath = "./test-render-to-file-sync.txt"; 71 | const writr = new Writr("Hello, World!"); 72 | writr.onHook(WritrHooks.renderToFile, (data) => { 73 | data.content = "Hello, File Sync!"; 74 | }); 75 | writr.renderToFileSync(filePath); 76 | const fileContent = await fs.promises.readFile(filePath); 77 | 78 | expect(fileContent.toString()).toContain("Hello, File Sync!"); 79 | 80 | // Cleanup 81 | await fs.promises.rm(filePath); 82 | }); 83 | 84 | test("it should change the content after loading from file", async () => { 85 | const filePath = "./test/fixtures/load-from-file.md"; 86 | const content = "Hello, Loaded!"; 87 | const writr = new Writr(); 88 | writr.onHook(WritrHooks.loadFromFile, (data) => { 89 | data.content = content; 90 | }); 91 | await writr.loadFromFile(filePath); 92 | expect(writr.content).toBe(content); 93 | }); 94 | 95 | test("it should change the content after loading from file sync", () => { 96 | const filePath = "./test/fixtures/load-from-file.md"; 97 | const content = "Hello, Loaded!"; 98 | const writr = new Writr(); 99 | writr.onHook(WritrHooks.loadFromFile, (data) => { 100 | data.content = content; 101 | }); 102 | writr.loadFromFileSync(filePath); 103 | expect(writr.content).toBe(content); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/fixtures/examples/redis.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/redis' 3 | sidebarTitle: '@keyv/redis' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/redis 8 | 9 | > Redis storage adapter for Keyv 10 | 11 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 12 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 13 | [![npm](https://img.shields.io/npm/v/@keyv/redis.svg)](https://www.npmjs.com/package/@keyv/redis) 14 | [![npm](https://img.shields.io/npm/dm/@keyv/redis)](https://npmjs.com/package/@keyv/redis) 15 | 16 | Redis storage adapter for [Keyv](https://github.com/jaredwray/keyv). 17 | 18 | TTL functionality is handled directly by Redis so no timestamps are stored and expired keys are cleaned up internally. 19 | 20 | ## Install 21 | 22 | ```shell 23 | npm install --save keyv @keyv/redis 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | const Keyv = require('keyv'); 30 | 31 | const keyv = new Keyv('redis://user:pass@localhost:6379'); 32 | keyv.on('error', handleConnectionError); 33 | ``` 34 | 35 | Any valid [`Redis`](https://github.com/luin/ioredis#connect-to-redis) options will be passed directly through. 36 | 37 | e.g: 38 | 39 | ```js 40 | const keyv = new Keyv('redis://user:pass@localhost:6379', { disable_resubscribing: true }); 41 | ``` 42 | 43 | Or you can manually create a storage adapter instance and pass it to Keyv: 44 | 45 | ```js 46 | const KeyvRedis = require('@keyv/redis'); 47 | const Keyv = require('keyv'); 48 | 49 | const keyvRedis = new KeyvRedis('redis://user:pass@localhost:6379'); 50 | const keyv = new Keyv({ store: keyvRedis }); 51 | ``` 52 | 53 | Or reuse a previous Redis instance: 54 | 55 | ```js 56 | const KeyvRedis = require('@keyv/redis'); 57 | const Redis = require('ioredis'); 58 | const Keyv = require('keyv'); 59 | 60 | const redis = new Redis('redis://user:pass@localhost:6379'); 61 | const keyvRedis = new KeyvRedis(redis); 62 | const keyv = new Keyv({ store: keyvRedis }); 63 | ``` 64 | 65 | Or reuse a previous Redis cluster: 66 | 67 | ```js 68 | const KeyvRedis = require('@keyv/redis'); 69 | const Redis = require('ioredis'); 70 | const Keyv = require('keyv'); 71 | 72 | const redis = new Redis.Cluster('redis://user:pass@localhost:6379'); 73 | const keyvRedis = new KeyvRedis(redis); 74 | const keyv = new Keyv({ store: keyvRedis }); 75 | ``` 76 | ## Options 77 | 78 | ### useRedisSets 79 | 80 | The `useRedisSets` option lets you decide whether to use Redis sets for key management. By default, this option is set to `true`. 81 | 82 | When `useRedisSets` is enabled (`true`): 83 | 84 | - A namespace for the Redis sets is created, and all created keys are added to this. This allows for group management of keys. 85 | - When a key is deleted, it's removed not only from the main storage but also from the Redis set. 86 | - When clearing all keys (using the `clear` function), all keys in the Redis set are looked up for deletion. The set itself is also deleted. 87 | 88 | **Note**: In high-performance scenarios, enabling `useRedisSets` might lead to memory leaks. If you're running a high-performance application or service, it is recommended to set `useRedisSets` to `false`. 89 | 90 | If you decide to set `useRedisSets` as `false`, keys will be handled individually and Redis sets won't be utilized. 91 | 92 | However, please note that setting `useRedisSets` to `false` could lead to performance issues in production when using the `clear` function, as it will need to iterate over all keys to delete them. 93 | 94 | #### Example 95 | 96 | Here's how you can use the `useRedisSets` option: 97 | 98 | ```js 99 | const Keyv = require('keyv'); 100 | 101 | const keyv = new Keyv('redis://user:pass@localhost:6379', { useRedisSets: false }); 102 | ``` 103 | 104 | ## License 105 | 106 | MIT © Jared Wray 107 | -------------------------------------------------------------------------------- /test/fixtures/examples/caching-nestjs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Utilizing Keyv for Caching in NestJS: A Step-by-Step Guide' 3 | sidebarTitle: 'Caching in Nest.js' 4 | parent: 'Caching' 5 | --- 6 | 7 | # Utilizing Keyv for Caching in NestJS: A Step-by-Step Guide 8 | 9 | Caching is an essential technique to enhance the performance of your application by storing frequently used data temporarily so that it can be quickly retrieved later. In this blog post, we'll explore how to use Keyv, a simple yet powerful key-value store for Node.js, to implement caching in a NestJS application. We will cover the basics of setting up Keyv with NestJS and demonstrate some examples of how to cache data effectively. 10 | 11 | ## 1. Setting Up the Project 12 | First, let's create a new NestJS project using the Nest CLI: 13 | 14 | ```bash 15 | $ npm i -g @nestjs/cli 16 | $ nest new nestjs-keyv-cache 17 | $ cd nestjs-keyv-cache 18 | ``` 19 | ## 2. Installing Keyv and its Dependencies 20 | 21 | To begin, install Keyv and a storage adapter of your choice. In this example, we'll use SQLite: 22 | ```bash 23 | $ npm install keyv keyv-sqlite 24 | ``` 25 | ## 3. Integrating Keyv with NestJS 26 | 27 | Create a new module named 'CacheModule' to manage the Keyv integration: 28 | ```bash 29 | $ nest generate module cache 30 | ``` 31 | 32 | Then, update the cache.module.ts file to import and configure Keyv: 33 | 34 | ```javascript 35 | import { Module } from '@nestjs/common'; 36 | import { Keyv } from 'keyv'; 37 | import KeyvSqlite from 'keyv-sqlite'; 38 | 39 | @Module({ 40 | providers: [ 41 | { 42 | provide: 'KEYV_INSTANCE', 43 | useFactory: () => new Keyv({ store: new KeyvSqlite('sqlite://cache.sqlite') }), 44 | }, 45 | ], 46 | exports: ['KEYV_INSTANCE'], 47 | }) 48 | export class CacheModule {} 49 | ``` 50 | 51 | Don't forget to import the CacheModule in app.module.ts: 52 | ```javascript 53 | import { Module } from '@nestjs/common'; 54 | import { CacheModule } from './cache/cache.module'; 55 | 56 | @Module({ 57 | imports: [CacheModule], 58 | }) 59 | export class AppModule {} 60 | ``` 61 | 62 | ## 4. Creating a Caching Service with Keyv 63 | Now, create a service to manage caching using Keyv: 64 | 65 | ```bash 66 | $ nest generate service cache 67 | Update the cache.service.ts file with caching methods: 68 | ``` 69 | 70 | ```javascript 71 | import { Inject, Injectable } from '@nestjs/common'; 72 | import { Keyv } from 'keyv'; 73 | 74 | @Injectable() 75 | export class CacheService { 76 | constructor(@Inject('KEYV_INSTANCE') private readonly keyv: Keyv) {} 77 | 78 | async get(key: string): Promise { 79 | return await this.keyv.get(key); 80 | } 81 | 82 | async set(key: string, value: T, ttl?: number): Promise { 83 | await this.keyv.set(key, value, ttl); 84 | } 85 | 86 | async delete(key: string): Promise { 87 | await this.keyv.delete(key); 88 | } 89 | } 90 | ``` 91 | 92 | ## 5. Implementing Caching in a Sample Controller 93 | Create a sample controller to demonstrate caching usage: 94 | 95 | ```bash 96 | $ nest generate controller sample 97 | ``` 98 | 99 | Update the sample.controller.ts file to use the caching service: 100 | ```javascript 101 | import { Controller, Get } from '@nestjs/common'; 102 | import { CacheService } from '../cache/cache.service'; 103 | 104 | @Controller('sample') 105 | export class SampleController { 106 | constructor(private readonly cacheService: CacheService) {} 107 | 108 | @Get() 109 | async getData() { 110 | const cacheKey = 'sample-data'; 111 | let data = await this.cacheService.get(cacheKey); 112 | 113 | if (!data) { 114 | // Simulate fetching data from an external API 115 | data = 'Sample data from external API'; 116 | await this.cacheService.set(cacheKey, data, 60 * 1000); // Cache for 1 minute 117 | } 118 | 119 | return { 120 | data, 121 | source: data === 'Sample data from external API' ? 'API' : 'Cache', 122 | }; 123 | } 124 | } 125 | ``` 126 | 127 | This SampleController demonstrates how to use the CacheService to cache and retrieve data. When a request is made to the /sample endpoint, the getData() method first checks if the data is available in the cache. If the data is not cached, it simulates fetching data from an external API, caches the data for 1 minute, and then returns the data along with its source (either "API" or "Cache"). -------------------------------------------------------------------------------- /test/content-fixtures.ts: -------------------------------------------------------------------------------- 1 | export const productPageWithMarkdown = ` 2 | --- 3 | title: "Super Comfortable Chair" 4 | product_id: "CHAIR12345" 5 | price: 149.99 6 | availability: "In Stock" 7 | featured: true 8 | categories: 9 | - "Furniture" 10 | - "Chairs" 11 | tags: 12 | - "comfort" 13 | - "ergonomic" 14 | - "home office" 15 | --- 16 | 17 | # Super Comfortable Chair 18 | 19 | ## Description 20 | 21 | The **Super Comfortable Chair** is designed with ergonomics in mind, providing maximum comfort for long hours of sitting. Whether you're working from home or gaming, this chair has you covered. 22 | 23 | ## Features 24 | 25 | - Ergonomic design to reduce strain on your back. 26 | - Adjustable height and recline for personalized comfort. 27 | - Durable materials that stand the test of time. 28 | 29 | ## Price 30 | 31 | At just **$149.99**, this chair is a steal! 32 | 33 | ## Reviews 34 | 35 | > "This chair has completely changed my home office setup. I can work for hours without feeling fatigued." — *Jane Doe* 36 | 37 | > "Worth every penny! The comfort is unmatched." — *John Smith* 38 | 39 | ## Purchase 40 | 41 | Don't miss out on the opportunity to own the **Super Comfortable Chair**. Click [here](https://example.com/product/CHAIR12345) to purchase now! 42 | `; 43 | 44 | export const projectDocumentationWithMarkdown = ` 45 | --- 46 | title: "Project Documentation" 47 | version: "1.0.0" 48 | contributors: 49 | - name: "John Smith" 50 | email: "john.smith@example.com" 51 | - name: "Alice Johnson" 52 | email: "alice.johnson@example.com" 53 | license: "MIT" 54 | --- 55 | 56 | # Overview 57 | 58 | This project aims to create a scalable and maintainable web application using modern technologies like React, Node.js, and MongoDB. 59 | 60 | ## Installation 61 | 62 | To install the project, clone the repository and run the following command: 63 | 64 | \`\`\`bash 65 | npm install 66 | \`\`\` 67 | 68 | ## Usage 69 | 70 | Start the development server by running: 71 | 72 | \`\`\`bash 73 | npm start 74 | \`\`\` 75 | 76 | ## Contributing 77 | 78 | We welcome contributions! Please follow the guidelines outlined in the \`CONTRIBUTING.md\` file. 79 | 80 | ## License 81 | 82 | This project is licensed under the MIT License. See the \`LICENSE\` file for more details. 83 | `; 84 | 85 | export const blogPostWithMarkdown = `--- 86 | title: "Understanding Async/Await in JavaScript" 87 | date: "2024-08-30" 88 | author: "Jane Doe" 89 | categories: 90 | - "JavaScript" 91 | - "Programming" 92 | tags: 93 | - "async" 94 | - "await" 95 | - "ES6" 96 | draft: false 97 | --- 98 | 99 | # Introduction 100 | 101 | Async/Await is a powerful feature introduced in ES6 that allows you to write asynchronous code in a synchronous manner. 102 | 103 | ## Why Use Async/Await? 104 | 105 | Using Async/Await makes your code cleaner and easier to understand by eliminating the need for complex callback chains or .then() methods. 106 | 107 | ## Example 108 | 109 | Here’s a simple example: 110 | 111 | \`\`\`javascript 112 | async function fetchData() { 113 | try { 114 | const response = await fetch('https://api.example.com/data'); 115 | const data = await response.json(); 116 | console.log(data); 117 | } catch (error) { 118 | console.error('Error fetching data:', error); 119 | } 120 | } 121 | 122 | fetchData(); 123 | \`\`\` 124 | `; 125 | 126 | export const markdownWithFrontMatter = ` 127 | --- 128 | title: "Sample Title" 129 | date: "2024-08-30" 130 | --- 131 | 132 | # Markdown Content Here 133 | `; 134 | 135 | export const markdownWithFrontMatterAndAdditional = ` 136 | --- 137 | title: "Sample Title" 138 | date: "2024-08-30" 139 | --- 140 | 141 | # Markdown Content Here 142 | 143 | --- 144 | 145 | This is additional content. 146 | `; 147 | 148 | export const markdownWithFrontMatterInOtherPlaces = ` 149 | # Markdown Content Here 150 | 151 | --- 152 | 153 | This is additional content. 154 | 155 | --- 156 | title: "Sample Is Wrong" 157 | date: "2024-08-30" 158 | --- 159 | 160 | Did this work? 161 | `; 162 | 163 | export const markdownWithBadFrontMatter = ` 164 | # Markdown Content Here 165 | --- 166 | title: My Awesome Blog Post 167 | date: 2024/10/30 168 | tags: 169 | - blog 170 | - markdown, yaml 171 | description This is an awesome blog post. 172 | published: yes 173 | author: 174 | - name: Jane Doe 175 | email: jane@example.com 176 | summary: "A brief summary 177 | of the post. 178 | --- 179 | 180 | This is additional content. 181 | 182 | --- 183 | title: "Sample Is Wrong" 184 | date: "2024-08-30" 185 | --- 186 | 187 | Did this work? 188 | `; 189 | -------------------------------------------------------------------------------- /test/fixtures/examples/memcache.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '@keyv/memcache' 3 | sidebarTitle: '@keyv/memcache' 4 | parent: 'Storage Adapters' 5 | --- 6 | 7 | # @keyv/memcache 8 | 9 | > Memcache storage adapter for [Keyv](https://github.com/jaredwray/keyv) 10 | 11 | 12 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 13 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 14 | [![GitHub license](https://img.shields.io/github/license/jaredwray/keyv)](https://github.com/jaredwray/keyv/blob/master/LICENSE) 15 | [![npm](https://img.shields.io/npm/dm/@keyv/memcache)](https://npmjs.com/package/@keyv/memcache) 16 | 17 | ## Install 18 | 19 | ```shell 20 | npm install --save @keyv/memcache 21 | ``` 22 | or 23 | ``` 24 | yarn add @keyv/memcache 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```js 30 | const Keyv = require('keyv'); 31 | const KeyvMemcache = require('@keyv/memcache'); 32 | 33 | const memcache = new KeyvMemcache('user:pass@localhost:11211'); 34 | const keyv = new Keyv({ store: memcache }); 35 | 36 | //set 37 | await keyv.set("foo","bar", 6000) //Expiring time is optional 38 | 39 | //get 40 | const obj = await keyv.get("foo"); 41 | 42 | //delete 43 | await keyv.delete("foo"); 44 | 45 | //clear 46 | await keyv.clear(); 47 | 48 | ``` 49 | 50 | ## Usage with Namespaces 51 | 52 | ```js 53 | const Keyv = require('keyv'); 54 | const KeyvMemcache = require('@keyv/memcache'); 55 | 56 | const memcache = new KeyvMemcache('user:pass@localhost:11211'); 57 | const keyv1 = new Keyv({ store: memcache, namespace: "namespace1" }); 58 | const keyv2 = new Keyv({ store: memcache, namespace: "namespace2" }); 59 | 60 | //set 61 | await keyv1.set("foo","bar1", 6000) //Expiring time is optional 62 | await keyv2.set("foo","bar2", 6000) //Expiring time is optional 63 | 64 | //get 65 | const obj1 = await keyv1.get("foo"); //will return bar1 66 | const obj2 = await keyv2.get("foo"); //will return bar2 67 | 68 | ``` 69 | 70 | # Works with Memcached, Memcachier, Redislabs, and Google Cloud 71 | 72 | ## Using Memcached 73 | 74 | 1. Install Memcached and start an instance 75 | ```js 76 | 77 | //set the server to the correct address and port 78 | const server = "localhost:11211" 79 | 80 | const Keyv = require("keyv"); 81 | const KeyvMemcache = require("@keyv/memcache"); 82 | 83 | const memcache = new KeyvMemcache(server); 84 | const keyv = new Keyv({ store: memcache}); 85 | ``` 86 | 87 | ## Using Memcachier 88 | 89 | 1. Go to https://www.memcachier.com and signup 90 | 2. Create a cache and setup where. 91 | 3. In the screen take the username, password, and url and place it into your code: 92 | ```js 93 | 94 | //best practice is to not hard code your config in code. 95 | const user = ""; 96 | const pass = ""; 97 | const server = "XXX.XXX.XXX.memcachier.com:11211" 98 | 99 | const Keyv = require("keyv"); 100 | const KeyvMemcache = require("@keyv/memcache"); 101 | 102 | const memcache = new KeyvMemcache(user +":"+ pass +"@"+ server); 103 | const keyv = new Keyv({ store: memcache}); 104 | 105 | ``` 106 | 107 | ## Using Redislabs Memcache Protocol 108 | 109 | 1. Go to https://www.redislabs.com and signup 110 | 2. Create a database and make sure to set the `Protocol` to memcached 111 | 3. In the screen take the username, password, and `endpoint` (the server) and place it into your code: 112 | ```js 113 | 114 | //best practice is to not hard code your config in code. 115 | const user = ""; 116 | const pass = ""; 117 | const server = "XXX.XXX.XXX.XXX.cloud.redislabs.com:XXX" 118 | 119 | const Keyv = require("keyv"); 120 | const KeyvMemcache = require("@keyv/memcache"); 121 | 122 | const memcache = new KeyvMemcache(user +":"+ pass +"@"+ server); 123 | const keyv = new Keyv({ store: memcache}); 124 | 125 | ``` 126 | 127 | ## Using Google Cloud 128 | 129 | 1. Go to https://cloud.google.com/ and sign up. 130 | 2. Go to the memcached configuration page in the google cloud console by navigating to Memorystore > Memcached. 131 | 3. On the memcached page (Eg. https://console.cloud.google.com/memorystore/memcached/instances?project=example), Click Create Instance 132 | 4. Fill in all mandatory fields as needed. You will need to set up a private service connection. 133 | 5. To set up a private service connection, click the Set Up Connection button. 134 | 6. Once required fields are complete, click the Create button to create the instance. 135 | 7. Google provides further documentation for connecting to and managing your Memecached instance [here](https://cloud.google.com/memorystore/docs/memcached). 136 | 137 | ```js 138 | 139 | const Keyv = require("keyv"); 140 | const KeyvMemcache = require("@keyv/memcache"); 141 | 142 | const memcache = new KeyvMemcache("insert the internal google memcached discovery endpoint"); 143 | const keyv = new Keyv({ store: memcache}); 144 | 145 | ``` 146 | 147 | 148 | ## License 149 | 150 | MIT © Jared Wray 151 | -------------------------------------------------------------------------------- /test/fixtures/examples/single-site.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Beautiful Website for Your Projects 4 | 5 | [![tests](https://github.com/jaredwray/docula/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/docula/actions/workflows/tests.yaml) 6 | [![GitHub license](https://img.shields.io/github/license/jaredwray/docula)](https://github.com/jaredwray/docula/blob/master/LICENSE) 7 | [![codecov](https://codecov.io/gh/jaredwray/docula/graph/badge.svg?token=RS0GPY4V4M)](https://codecov.io/gh/jaredwray/docula) 8 | [![npm](https://img.shields.io/npm/dm/docula)](https://npmjs.com/package/docula) 9 | [![npm](https://img.shields.io/npm/v/docula)](https://npmjs.com/package/docula) 10 | 11 | # Table of Contents 12 | - [Features](#features) 13 | - [Getting Started](#getting-started) 14 | - [Using Your own Template](#using-your-own-template) 15 | - [Building Multiple Pages](#building-multiple-pages) 16 | - [Helper Functions for Markdown](#helper-functions-for-markdown) 17 | - [Code of Conduct and Contributing](#code-of-conduct-and-contributing) 18 | - [License - MIT](#license) 19 | 20 | # Features 21 | * No configuration requrired. Just setup the folder structure with a logo, favicon, and css file. 22 | * Builds a static website that can be hosted anywhere. 23 | * For more complex projects easily add a `docula.config.mjs` file to customize the build process. With PRE and POST methods. 24 | * Support for single page with readme or multiple markdown pages in a docs folder. 25 | * Will generate a sitemap.xml and robots.txt for your site. 26 | * Uses Github release notes to generate a changelog / releases page. 27 | * Uses Github to show contributors and link to their profiles. 28 | * Simple search is provided by default out of the box. 29 | 30 | # Getting Started 31 | 32 | ## Install docula via init 33 | > npx docula init 34 | 35 | This will create a folder called site with the following structure: 36 | 37 | ``` 38 | site 39 | ├───site.css 40 | ├───logo.png 41 | ├───favicon.ico 42 | ├───README.md 43 | ├───docula.config.mjs 44 | ``` 45 | Note: for typescript do 'docula init --typescript' 46 | 47 | ## Add your content 48 | 49 | Simply replace the logo, favicon, and css file with your own. The readme is your root project readme and you just need to at build time move it over to the site folder. If you have it at the root of the project and this is a folder inside just delete the README.md file in the site folder and docula will copy it over for you automatically. 50 | 51 | ## Build your site 52 | 53 | > npx docula 54 | 55 | This will build your site and place it in the `dist` folder. You can then host it anywhere you like. 56 | 57 | # Using Your own Template 58 | 59 | If you want to use your own template you can do so by adding a `docula.config.ts` file to the root of your project. This file will be used to configure the build process. 60 | 61 | or at the command line: 62 | 63 | > npx docula --template path/to/template 64 | 65 | ##Building Multiple Pages 66 | 67 | If you want to build multiple pages you can easily do that by adding in a `docs` folder to the root of the site folder. Inside of that folder you can add as many pages as you like. Each page will be a markdown file and it will generate a table of contents for you. Here is an example of what it looks like: 68 | 69 | ``` 70 | site 71 | ├───site.css 72 | ├───logo.png 73 | ├───favicon.ico 74 | ├───docula.config.mjs 75 | ├───docs 76 | │ ├───getting-started.md 77 | │ ├───contributing.md 78 | │ ├───license.md 79 | │ ├───code-of-conduct.md 80 | ``` 81 | 82 | The `readme.md` file will be the root page and the rest will be added to the table of contents. If you want to control the title or order of the pages you can do so by setting the `title` and `order` properties in the front matter of the markdown file. Here is an example: 83 | 84 | ```md 85 | title: Getting Started 86 | order: 2 87 | ``` 88 | 89 | # Helper Functions for Markdown 90 | 91 | docula comes with some helper functions that you can use in your markdown files. 92 | * `doculaHelpers.getFrontMatter(fileName)` - Gets the front matter of a markdown file. 93 | * `doculaHelpers.setFrontMatter(fileName, frontMatter)` - Sets the front matter of a markdown file. 94 | * `doculaHelpers.createDoc(source, destination, frontMatter?, contentFn[]?)` - Creates a markdown file with the specified front matter and content. The contentFn is a function that is executed on the original content of the file. This is useful if you want to remove content from the original file. 95 | 96 | # Remove html content 97 | 98 | In some cases your markdown file will have html content in it such as the logo of your project or a badge. You can use the `doculaHelpers.removeHtmlContent()` helper function to remove that content from the page. Here is an example: 99 | 100 | # Get and Set the Front Matter of a Markdown File 101 | 102 | You can use the `doculaHelpers.getFrontMatter()` and `doculaHelpers.setFrontMatter()` helper functions to get and set the front matter of a markdown file. Here is an example: 103 | 104 | ```js 105 | const frontMatter = doculaHelpers.getFrontMatter('../readme.md'); 106 | frontMatter.title = 'My Title'; 107 | doculaHelpers.setFrontMatter('../readme.md', frontMatter); 108 | ``` 109 | 110 | # Code of Conduct and Contributing 111 | [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing](CONTRIBUTING.md) guidelines. 112 | 113 | # License 114 | 115 | MIT © [Jared Wray](https://jaredwray.com) 116 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me@jaredwray.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /test/fixtures/examples/docula-readme.md: -------------------------------------------------------------------------------- 1 | ![Docula](site/logo.svg) 2 | 3 | # Beautiful Website for Your Projects 4 | 5 | [![tests](https://github.com/jaredwray/docula/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/docula/actions/workflows/tests.yaml) 6 | [![GitHub license](https://img.shields.io/github/license/jaredwray/docula)](https://github.com/jaredwray/docula/blob/master/LICENSE) 7 | [![codecov](https://codecov.io/gh/jaredwray/docula/graph/badge.svg?token=RS0GPY4V4M)](https://codecov.io/gh/jaredwray/docula) 8 | [![npm](https://img.shields.io/npm/dm/docula)](https://npmjs.com/package/docula) 9 | [![npm](https://img.shields.io/npm/v/docula)](https://npmjs.com/package/docula) 10 | 11 | # Table of Contents 12 | - [Features](#features) 13 | - [Getting Started](#getting-started) 14 | - [Using Your own Template](#using-your-own-template) 15 | - [Building Multiple Pages](#building-multiple-pages) 16 | - [Using a Github Token](#using-a-github-token) 17 | - [Helper Functions for Markdown](#helper-functions-for-markdown) 18 | - [Code of Conduct and Contributing](#code-of-conduct-and-contributing) 19 | - [License - MIT](#license) 20 | 21 | # Features 22 | * No configuration requrired. Just setup the folder structure with a logo, favicon, and css file. 23 | * Builds a static website that can be hosted anywhere. 24 | * For more complex projects easily add a `docula.config.mjs` file to customize the build process. With PRE and POST methods. 25 | * Support for single page with readme or multiple markdown pages in a docs folder. 26 | * Will generate a sitemap.xml and robots.txt for your site. 27 | * Uses Github release notes to generate a changelog / releases page. 28 | * Uses Github to show contributors and link to their profiles. 29 | * Simple search is provided by default out of the box. 30 | 31 | # Getting Started 32 | 33 | ## Install docula via init 34 | > npx docula init 35 | 36 | This will create a folder called site with the following structure: 37 | 38 | ``` 39 | site 40 | ├───site.css 41 | ├───logo.png 42 | ├───favicon.ico 43 | ├───README.md 44 | ├───docula.config.mjs 45 | ``` 46 | Note: for typescript do 'docula init --typescript' 47 | 48 | ## Add your content 49 | 50 | Simply replace the logo, favicon, and css file with your own. The readme is your root project readme and you just need to at build time move it over to the site folder. If you have it at the root of the project and this is a folder inside just delete the README.md file in the site folder and docula will copy it over for you automatically. 51 | 52 | ## Build your site 53 | 54 | > npx docula 55 | 56 | This will build your site and place it in the `dist` folder. You can then host it anywhere you like. 57 | 58 | # Using Your own Template 59 | 60 | If you want to use your own template you can do so by adding a `docula.config.ts` file to the root of your project. This file will be used to configure the build process. 61 | 62 | or at the command line: 63 | 64 | > npx docula --template path/to/template 65 | 66 | # Building Multiple Pages 67 | 68 | If you want to build multiple pages you can easily do that by adding in a `docs` folder to the root of the site folder. Inside of that folder you can add as many pages as you like. Each page will be a markdown file and it will generate a table of contents for you. Here is an example of what it looks like: 69 | 70 | ``` 71 | site 72 | ├───site.css 73 | ├───logo.png 74 | ├───favicon.ico 75 | ├───docula.config.mjs 76 | ├───docs 77 | │ ├───getting-started.md 78 | │ ├───contributing.md 79 | │ ├───license.md 80 | │ ├───code-of-conduct.md 81 | ``` 82 | 83 | The `readme.md` file will be the root page and the rest will be added to the table of contents. If you want to control the title or order of the pages you can do so by setting the `title` and `order` properties in the front matter of the markdown file. Here is an example: 84 | 85 | ```md 86 | title: Getting Started 87 | order: 2 88 | ``` 89 | 90 | # Using a Github Token 91 | 92 | If you want to use the Github token to access the Github API you can do so by setting the `GITHUB_TOKEN` environment variable. This is useful if you want to access private repositories or if you want to access the Github API without hitting the rate limit. This is optional and you can still use docula without it but could hit rate limits and will not be able to access private repositories. 93 | 94 | # Helper Functions for Markdown 95 | 96 | docula comes with some helper functions that you can use in your markdown files. 97 | * `doculaHelpers.getFrontMatter(fileName)` - Gets the front matter of a markdown file. 98 | * `doculaHelpers.setFrontMatter(fileName, frontMatter)` - Sets the front matter of a markdown file. 99 | * `doculaHelpers.createDoc(source, destination, frontMatter?, contentFn[]?)` - Creates a markdown file with the specified front matter and content. The contentFn is a function that is executed on the original content of the file. This is useful if you want to remove content from the original file. 100 | 101 | # Remove html content 102 | 103 | In some cases your markdown file will have html content in it such as the logo of your project or a badge. You can use the `doculaHelpers.removeHtmlContent()` helper function to remove that content from the page. Here is an example: 104 | 105 | # Get and Set the Front Matter of a Markdown File 106 | 107 | You can use the `doculaHelpers.getFrontMatter()` and `doculaHelpers.setFrontMatter()` helper functions to get and set the front matter of a markdown file. Here is an example: 108 | 109 | ```js 110 | const frontMatter = doculaHelpers.getFrontMatter('../readme.md'); 111 | frontMatter.title = 'My Title'; 112 | doculaHelpers.setFrontMatter('../readme.md', frontMatter); 113 | ``` 114 | 115 | # Code of Conduct and Contributing 116 | [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing](CONTRIBUTING.md) guidelines. 117 | 118 | # License 119 | 120 | MIT © [Jared Wray](https://jaredwray.com) 121 | -------------------------------------------------------------------------------- /test/fixtures/examples/no-front-matter.md: -------------------------------------------------------------------------------- 1 | 2 | ## Beautiful Website for Your Projects 3 | [![Build](https://github.com/jaredwray/docula/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/docula/actions/workflows/tests.yml) 4 | [![GitHub license](https://img.shields.io/github/license/jaredwray/docula)](https://github.com/jaredwray/docula/blob/master/LICENSE) 5 | [![codecov](https://codecov.io/gh/jaredwray/docula/branch/master/graph/badge.svg?token=1YdMesM07X)](https://codecov.io/gh/jaredwray/docula) 6 | [![npm](https://img.shields.io/npm/dm/docula)](https://npmjs.com/package/docula) 7 | 8 | --- 9 | ## Table of Contents 10 | - [Features](#features) 11 | - [Getting Started](#getting-started) 12 | - [Using Your own Template](#using-your-own-template) 13 | - [Building Multiple Pages](#building-multiple-pages) 14 | - [Helper Functions for Markdown](#helper-functions-for-markdown) 15 | - [Code of Conduct and Contributing](#code-of-conduct-and-contributing) 16 | - [What Happened to it Generating a Blog](#what-happened-to-it-generating-a-blog) 17 | - [License - MIT](#license) 18 | 19 | ## Features 20 | * No configuration requrired. Just setup the folder structure with a logo, favicon, and css file. 21 | * Builds a static website that can be hosted anywhere. 22 | * For more complex projects easily add a `docula.config.mjs` file to customize the build process. 23 | * Support for single page with readme or multiple pages with a table of contents. 24 | * Will generate a sitemap.xml, robots.txt, and file for SEO. 25 | * Uses Github release notes to generate a changelog page. 26 | * Uses Github to show contributors and link to their profiles. 27 | * Simple search is provided by default out of the box. 28 | 29 | ## Getting Started 30 | 31 | ## 1. Install docula 32 | 33 | > npx docula init 34 | 35 | This will create a folder called site with the following structure: 36 | 37 | ``` 38 | site 39 | ├───site.css 40 | ├───logo.png 41 | ├───favicon.ico 42 | ├───README.md 43 | ├───docula.json 44 | ``` 45 | 46 | ## 2. Add your content 47 | 48 | Simply replace the logo, favicon, and css file with your own. The readme is your root project readme and you just need to at build time move it over to the site folder. If you have it at the root of the project and this is a folder inside just delete the README.md file in the site folder and docula will copy it over for you automatically. 49 | 50 | ## 3. Build your site 51 | 52 | > npx docula 53 | 54 | This will build your site and place it in the `dist` folder. You can then host it anywhere you like. 55 | 56 | ## Using Your own Template 57 | 58 | If you want to use your own template you can do so by adding a `docula.json` file to the root of your project. This file will be used to configure the build process. Here is an example of what it looks like: 59 | 60 | ```js 61 | { 62 | template: 'path/to/template', 63 | } 64 | ``` 65 | 66 | or at the command line: 67 | 68 | > npx docula --template path/to/template 69 | 70 | ## Building Multiple Pages 71 | 72 | If you want to build multiple pages you can easily do that by adding in a `docs` folder to the root of the site folder. Inside of that folder you can add as many pages as you like. Each page will be a markdown file and it will generate a table of contents for you. Here is an example of what it looks like: 73 | 74 | ``` 75 | site 76 | ├───site.css 77 | ├───logo.png 78 | ├───favicon.ico 79 | ├───docula.json 80 | ├───docs 81 | │ ├───readme.md 82 | │ ├───getting-started.md 83 | │ ├───contributing.md 84 | │ ├───license.md 85 | │ ├───code-of-conduct.md 86 | ``` 87 | 88 | The `readme.md` file will be the root page and the rest will be added to the table of contents. If you want to control the title or order of the pages you can do so by setting the `title` and `order` properties in the front matter of the markdown file. Here is an example: 89 | 90 | ```md 91 | --- 92 | title: Getting Started 93 | order: 2 94 | --- 95 | ``` 96 | 97 | ## Helper Functions for Markdown 98 | 99 | docula comes with some helper functions that you can use in your markdown files. 100 | * `docula.helpers.getFrontMatter(fileName)` - Gets the front matter of a markdown file. 101 | * `docula.helpers.setFrontMatter(fileName, frontMatter)` - Sets the front matter of a markdown file. 102 | * `docula.helpers.createDoc(source, destination, frontMatter?, contentFn[]?)` - Creates a markdown file with the specified front matter and content. The contentFn is a function that is executed on the original content of the file. This is useful if you want to remove content from the original file. 103 | 104 | ### Remove html content 105 | 106 | In some cases your markdown file will have html content in it such as the logo of your project or a badge. You can use the `wrtir.helpers.removeHtmlContent()` helper function to remove that content from the page. Here is an example: 107 | 108 | ```js 109 | docula.helpers.removeHtmlContent('../readme.md', ' npx docula init 36 | 37 | This will create a folder called site with the following structure: 38 | 39 | ``` 40 | site 41 | ├───site.css 42 | ├───logo.png 43 | ├───favicon.ico 44 | ├───README.md 45 | ├───docula.json 46 | ``` 47 | 48 | ## 2. Add your content 49 | 50 | Simply replace the logo, favicon, and css file with your own. The readme is your root project readme and you just need to at build time move it over to the site folder. If you have it at the root of the project and this is a folder inside just delete the README.md file in the site folder and docula will copy it over for you automatically. 51 | 52 | ## 3. Build your site 53 | 54 | > npx docula 55 | 56 | This will build your site and place it in the `dist` folder. You can then host it anywhere you like. 57 | 58 | ## Using Your own Template 59 | 60 | If you want to use your own template you can do so by adding a `docula.json` file to the root of your project. This file will be used to configure the build process. Here is an example of what it looks like: 61 | 62 | ```js 63 | { 64 | template: 'path/to/template', 65 | } 66 | ``` 67 | 68 | or at the command line: 69 | 70 | > npx docula --template path/to/template 71 | 72 | ## Building Multiple Pages 73 | 74 | If you want to build multiple pages you can easily do that by adding in a `docs` folder to the root of the site folder. Inside of that folder you can add as many pages as you like. Each page will be a markdown file and it will generate a table of contents for you. Here is an example of what it looks like: 75 | 76 | ``` 77 | site 78 | ├───site.css 79 | ├───logo.png 80 | ├───favicon.ico 81 | ├───docula.json 82 | ├───docs 83 | │ ├───readme.md 84 | │ ├───getting-started.md 85 | │ ├───contributing.md 86 | │ ├───license.md 87 | │ ├───code-of-conduct.md 88 | ``` 89 | 90 | The `readme.md` file will be the root page and the rest will be added to the table of contents. If you want to control the title or order of the pages you can do so by setting the `title` and `order` properties in the front matter of the markdown file. Here is an example: 91 | 92 | ```md 93 | --- 94 | title: Getting Started 95 | order: 2 96 | --- 97 | ``` 98 | 99 | ## Helper Functions for Markdown 100 | 101 | docula comes with some helper functions that you can use in your markdown files. 102 | * `docula.helpers.getFrontMatter(fileName)` - Gets the front matter of a markdown file. 103 | * `docula.helpers.setFrontMatter(fileName, frontMatter)` - Sets the front matter of a markdown file. 104 | * `docula.helpers.createDoc(source, destination, frontMatter?, contentFn[]?)` - Creates a markdown file with the specified front matter and content. The contentFn is a function that is executed on the original content of the file. This is useful if you want to remove content from the original file. 105 | 106 | ### Remove html content 107 | 108 | In some cases your markdown file will have html content in it such as the logo of your project or a badge. You can use the `wrtir.helpers.removeHtmlContent()` helper function to remove that content from the page. Here is an example: 109 | 110 | ```js 111 | docula.helpers.removeHtmlContent('../readme.md', ' npx docula init 38 | 39 | This will create a folder called site with the following structure: 40 | 41 | ``` 42 | site 43 | ├───site.css 44 | ├───logo.png 45 | ├───favicon.ico 46 | ├───README.md 47 | ├───docula.json 48 | ``` 49 | 50 | ## 2. Add your content 51 | 52 | Simply replace the logo, favicon, and css file with your own. The readme is your root project readme and you just need to at build time move it over to the site folder. If you have it at the root of the project and this is a folder inside just delete the README.md file in the site folder and docula will copy it over for you automatically. 53 | 54 | ## 3. Build your site 55 | 56 | > npx docula 57 | 58 | This will build your site and place it in the `dist` folder. You can then host it anywhere you like. 59 | 60 | ## Using Your own Template 61 | 62 | If you want to use your own template you can do so by adding a `docula.json` file to the root of your project. This file will be used to configure the build process. Here is an example of what it looks like: 63 | 64 | ```js 65 | { 66 | template: 'path/to/template', 67 | } 68 | ``` 69 | 70 | or at the command line: 71 | 72 | > npx docula --template path/to/template 73 | 74 | ## Building Multiple Pages 75 | 76 | If you want to build multiple pages you can easily do that by adding in a `docs` folder to the root of the site folder. Inside of that folder you can add as many pages as you like. Each page will be a markdown file and it will generate a table of contents for you. Here is an example of what it looks like: 77 | 78 | ``` 79 | site 80 | ├───site.css 81 | ├───logo.png 82 | ├───favicon.ico 83 | ├───docula.json 84 | ├───docs 85 | │ ├───readme.md 86 | │ ├───getting-started.md 87 | │ ├───contributing.md 88 | │ ├───license.md 89 | │ ├───code-of-conduct.md 90 | ``` 91 | 92 | The `readme.md` file will be the root page and the rest will be added to the table of contents. If you want to control the title or order of the pages you can do so by setting the `title` and `order` properties in the front matter of the markdown file. Here is an example: 93 | 94 | ```md 95 | --- 96 | title: Getting Started 97 | order: 2 98 | --- 99 | ``` 100 | 101 | ## Helper Functions for Markdown 102 | 103 | docula comes with some helper functions that you can use in your markdown files. 104 | * `docula.helpers.getFrontMatter(fileName)` - Gets the front matter of a markdown file. 105 | * `docula.helpers.setFrontMatter(fileName, frontMatter)` - Sets the front matter of a markdown file. 106 | * `docula.helpers.createDoc(source, destination, frontMatter?, contentFn[]?)` - Creates a markdown file with the specified front matter and content. The contentFn is a function that is executed on the original content of the file. This is useful if you want to remove content from the original file. 107 | 108 | ### Remove html content 109 | 110 | In some cases your markdown file will have html content in it such as the logo of your project or a badge. You can use the `wrtir.helpers.removeHtmlContent()` helper function to remove that content from the page. Here is an example: 111 | 112 | ```js 113 | docula.helpers.removeHtmlContent('../readme.md', ' { 5 | it("should be able to initialize", () => { 6 | const cache = new WritrCache(); 7 | expect(cache).toBeDefined(); 8 | }); 9 | 10 | it("should be able to set markdown", async () => { 11 | const cache = new WritrCache(); 12 | const markdown = "# Hello World"; 13 | const value = '

Hello World

'; 14 | cache.set(markdown, value); 15 | expect(cache.get(markdown)).toEqual(value); 16 | }); 17 | 18 | it("should be able to set markdown sync", () => { 19 | const cache = new WritrCache(); 20 | const markdown = "# Hello World"; 21 | const value = '

Hello World

'; 22 | cache.set(markdown, value); 23 | expect(cache.get(markdown)).toEqual(value); 24 | }); 25 | 26 | it("should be able to set markdown with options", async () => { 27 | const cache = new WritrCache(); 28 | const markdown = "# Hello World"; 29 | const value = '

Hello World

'; 30 | const options = { toc: true }; 31 | cache.set(markdown, value, options); 32 | expect(cache.get(markdown, options)).toEqual(value); 33 | }); 34 | 35 | it("should be able to set markdown sync with options", () => { 36 | const cache = new WritrCache(); 37 | const markdown = "# Hello World"; 38 | const value = '

Hello World

'; 39 | const options = { toc: true }; 40 | cache.set(markdown, value, options); 41 | expect(cache.get(markdown, options)).toEqual(value); 42 | }); 43 | 44 | it("should be able to set markdown with options", async () => { 45 | const cache = new WritrCache(); 46 | const markdown = "# Hello World"; 47 | const value = '

Hello World

'; 48 | const options = { toc: true, emoji: true }; 49 | cache.set(markdown, value, options); 50 | cache.get(markdown, options); 51 | }); 52 | 53 | it("should be able to do hash caching", () => { 54 | const cache = new WritrCache(); 55 | const markdown = "# Hello World"; 56 | let options = { toc: true, emoji: true }; 57 | const key = cache.hash(markdown, options); 58 | const key2 = cache.hash(markdown, options); 59 | expect(key).toEqual(key2); 60 | // The key should be based on the sanitized options (only known properties) 61 | const expectedKey = 62 | '{"markdown":"# Hello World","options":{"emoji":true,"toc":true}}'; 63 | expect(cache.hashStore.has(expectedKey)).toEqual(true); 64 | expect(cache.hashStore.size).toEqual(1); 65 | options = { toc: true, emoji: false }; 66 | cache.hash(markdown, options); 67 | expect(cache.hashStore.size).toEqual(2); 68 | }); 69 | 70 | it("Get and Set the Cache", async () => { 71 | const cache = new WritrCache(); 72 | expect(cache.store).toBeDefined(); 73 | }); 74 | 75 | it("should be able to clear the cache", async () => { 76 | const cache = new WritrCache(); 77 | const markdown = "# Hello World"; 78 | const value = '

Hello World

'; 79 | const options = { toc: true, emoji: true }; 80 | cache.set(markdown, value, options); 81 | cache.get(markdown, options); 82 | cache.clear(); 83 | expect(cache.get(markdown, options)).toBeUndefined(); 84 | }); 85 | 86 | it("should handle options with Promises by sanitizing them", () => { 87 | const cache = new WritrCache(); 88 | const markdown = "# Hello World"; 89 | 90 | // Simulate options that contain a Promise (like React or unified plugins might) 91 | // biome-ignore lint/suspicious/noExplicitAny: Testing edge case with non-serializable values 92 | const optionsWithPromise: any = { 93 | toc: true, 94 | emoji: true, 95 | // This would normally cause structuredClone to fail 96 | customPlugin: Promise.resolve("test"), 97 | }; 98 | 99 | // This should NOT throw because we sanitize options before hashing 100 | expect(() => { 101 | cache.hash(markdown, optionsWithPromise); 102 | }).not.toThrow(); 103 | 104 | // Verify it works with get/set too 105 | const value = '

Hello World

'; 106 | expect(() => { 107 | cache.set(markdown, value, optionsWithPromise); 108 | }).not.toThrow(); 109 | 110 | expect(() => { 111 | cache.get(markdown, optionsWithPromise); 112 | }).not.toThrow(); 113 | }); 114 | 115 | it("should handle options with functions by sanitizing them", () => { 116 | const cache = new WritrCache(); 117 | const markdown = "# Hello World"; 118 | 119 | // Simulate options that contain functions 120 | // biome-ignore lint/suspicious/noExplicitAny: Testing edge case with non-serializable values 121 | const optionsWithFunction: any = { 122 | toc: true, 123 | highlight: true, 124 | // This would normally cause structuredClone to fail 125 | customMethod: () => "test", 126 | }; 127 | 128 | // This should NOT throw because we sanitize options before hashing 129 | expect(() => { 130 | cache.hash(markdown, optionsWithFunction); 131 | }).not.toThrow(); 132 | }); 133 | 134 | it("should handle options with circular references by sanitizing them", () => { 135 | const cache = new WritrCache(); 136 | const markdown = "# Hello World"; 137 | 138 | // Create circular reference (common in React components) 139 | // biome-ignore lint/suspicious/noExplicitAny: Testing edge case with circular references 140 | const circularObj: any = { 141 | toc: true, 142 | emoji: true, 143 | }; 144 | circularObj.self = circularObj; 145 | 146 | // This should NOT throw because we sanitize options before hashing 147 | expect(() => { 148 | cache.hash(markdown, circularObj); 149 | }).not.toThrow(); 150 | }); 151 | 152 | it("should only use known RenderOptions properties for cache keys", () => { 153 | const cache = new WritrCache(); 154 | const markdown = "# Hello World"; 155 | 156 | // Two options with different extra properties but same known properties 157 | // biome-ignore lint/suspicious/noExplicitAny: Testing that extra properties are ignored 158 | const options1: any = { 159 | toc: true, 160 | emoji: true, 161 | extraProp1: "value1", 162 | }; 163 | 164 | // biome-ignore lint/suspicious/noExplicitAny: Testing that extra properties are ignored 165 | const options2: any = { 166 | toc: true, 167 | emoji: true, 168 | extraProp2: "value2", 169 | }; 170 | 171 | // Should produce the same hash because extra properties are ignored 172 | const hash1 = cache.hash(markdown, options1); 173 | const hash2 = cache.hash(markdown, options2); 174 | 175 | expect(hash1).toEqual(hash2); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/fixtures/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Getting Started Guide' 3 | order: 1 4 | --- 5 | 6 | # Getting Started Guide 7 | 8 | Keyv provides a consistent interface for key-value storage across multiple backends via storage adapters. It supports TTL based expiry, making it suitable as a cache or a persistent key-value store. Follow the steps below to get you up and running. 9 | 10 | ## 1. Make a Project Directory 11 | Make a directory with your project in it. 12 | 13 | ```sh 14 | mkdir keyv 15 | cd keyv 16 | ``` 17 | You’re now inside your project’s directory. 18 | 19 | ## 2. Install keyv 20 | 21 | ```sh 22 | npm install --save keyv 23 | ``` 24 | By default, everything is stored in memory; you can optionally also install a storage adapter; choose one from the following: 25 | 26 | ```sh 27 | npm install --save @keyv/redis 28 | npm install --save @keyv/memcache 29 | npm install --save @keyv/mongo 30 | npm install --save @keyv/sqlite 31 | npm install --save @keyv/postgres 32 | npm install --save @keyv/mysql 33 | npm install --save @keyv/etcd 34 | ``` 35 | 36 | > **Note**: You can also use third-party storage adapters 37 | 38 | The following are third-party storage adapters compatible with Keyv: 39 | - [quick-lru](https://github.com/sindresorhus/quick-lru) - Simple "Least Recently Used" (LRU) cache 40 | - [keyv-file](https://github.com/zaaack/keyv-file) - File system storage adapter for Keyv 41 | - [keyv-dynamodb](https://www.npmjs.com/package/keyv-dynamodb) - DynamoDB storage adapter for Keyv 42 | - [keyv-lru](https://www.npmjs.com/package/keyv-lru) - LRU storage adapter for Keyv 43 | - [keyv-null](https://www.npmjs.com/package/keyv-null) - Null storage adapter for Keyv 44 | - [keyv-firestore ](https://github.com/goto-bus-stop/keyv-firestore) – Firebase Cloud Firestore adapter for Keyv 45 | - [keyv-mssql](https://github.com/pmorgan3/keyv-mssql) - Microsoft Sql Server adapter for Keyv 46 | - [keyv-azuretable](https://github.com/howlowck/keyv-azuretable) - Azure Table Storage/API adapter for Keyv 47 | - [keyv-arango](https://github.com/TimMikeladze/keyv-arango) - ArangoDB storage adapter for Keyv 48 | - [keyv-momento](https://github.com/momentohq/node-keyv-adaptor/) - Momento storage adapter for Keyv 49 | 50 | 51 | ## 3. Create a New Keyv Instance 52 | Pass your connection string if applicable. Keyv will automatically load the correct storage adapter. //// 53 | ```js 54 | // example Keyv instance that uses sqlite storage adapter 55 | const keyv = new Keyv('sqlite://path/to/database.sqlite'); 56 | ``` 57 | 58 | 59 | `Keyv` Parameters 60 | 61 | Parameter | Type | Required | Description 62 | ------------ | ------------- | ------------- | ------------- 63 | uri | String | N | The connection string URI. Merged into the options object as options.uri. Default value: undefined 64 | options | Object | N | The options object is also passed through to the storage adapter. See the table below for a list of available options. 65 | 66 | `options` Parameters 67 | 68 | Parameter | Type | Required | Description 69 | ------------ | ------------- | ------------- | ------------- 70 | namespace | String | N | Namespace for the current instance. Default: 'keyv' 71 | ttl | Number | N | This is the default TTL, it can be overridden by specifying a TTL on .set(). Default: undefined 72 | compression | @keyv/compress\-\ | N | Compression package to use. See Compression for more details. Default: undefined. 73 | serialize | Function | N | A custom serialization function. Default: JSONB.stringify 74 | deserialize | Function | N | A custom deserialization function. Default: JSONB.parse 75 | store | Storage adapter instance | N | The storage adapter instance to be used by Keyv. Default: new Map() 76 | adapter | String | N | Specify an adapter to use. e.g 'redis' or 'mongodb'. Default: undefined 77 | 78 | ### Example - Create an Instance of Keyv with a connection URI 79 | The following example shows how you would create and Instance of Keyv with a `mongodb` connection URI. 80 | 81 | ```js 82 | const Keyv = require('keyv'); 83 | 84 | const keyv = new Keyv('mongodb://user:pass@localhost:27017/dbname'); 85 | 86 | // Handle DB connection errors 87 | keyv.on('error', err => console.log('Connection Error', err)); 88 | ``` 89 | ### Example - Create an Instance of Keyv using a third-party storage adapter 90 | 91 | [`quick-lru`](https://github.com/sindresorhus/quick-lru) is a third-party module that implements the Map API. 92 | 93 | ```js 94 | const Keyv = require('keyv'); 95 | const QuickLRU = require('quick-lru'); 96 | 97 | const lru = new QuickLRU({ maxSize: 1000 }); 98 | const keyv = new Keyv({ store: lru }); 99 | 100 | // Handle DB connection errors 101 | keyv.on('error', err => console.log('Connection Error', err)); 102 | ``` 103 | 104 | ## 4. Create Some Key Value Pairs 105 | 106 | Method: `set(key, value, [ttl])` - Set a value for a specified key. 107 | 108 | Parameter | Type | Required | Description 109 | ------------ | ------------- | ------------- | ------------- 110 | key | String | Y | Unique identifier which is used to look up the value. Keys are persistent by default. 111 | value | Any | Y | Data value associated with the key 112 | ttl | Number | N | Expiry time in milliseconds 113 | 114 | The following example code shows you how to create a key-value pair using the `set` method. 115 | 116 | ```js 117 | 118 | const keyv = new Keyv('redis://user:pass@localhost:6379'); 119 | 120 | // set a key value pair that expires in 1000 milliseconds 121 | await keyv.set('foo', 'expires in 1 second', 1000); // true 122 | 123 | // set a key value pair that never expires 124 | await keyv.set('bar', 'never expires'); // true 125 | ``` 126 | 127 | 128 | 129 | Method: `delete(key)` - Deletes an entry. 130 | 131 | Parameter | Type | Required | Description 132 | ------------ | ------------- | ------------- | ------------- 133 | key | String | Y | Unique identifier which is used to look up the value. Returns `true `if the key existed, `false` if not. 134 | 135 | To delete a key value pair use the `delete(key)` method as shown below: 136 | 137 | ```js 138 | // Delete the key value pair for the 'foo' key 139 | await keyv.delete('foo'); // true 140 | ``` 141 | 142 | 143 | ## 5. Advanced - Use Namespaces to Avoid Key Collisions 144 | You can namespace your Keyv instance to avoid key collisions and allow you to clear only a certain namespace while using the same database. 145 | 146 | The example code below creates two namespaces, 'users' and 'cache' and creates a key value pair using the key 'foo' in both namespaces, it also shows how to delete all values in a specified namespace. 147 | 148 | ```js 149 | const users = new Keyv('redis://user:pass@localhost:6379', { namespace: 'users' }); 150 | const cache = new Keyv('redis://user:pass@localhost:6379', { namespace: 'cache' }); 151 | 152 | // Set a key-value pair using the key 'foo' in both namespaces 153 | await users.set('foo', 'users'); // returns true 154 | await cache.set('foo', 'cache'); // returns true 155 | 156 | // Retrieve a Value 157 | await users.get('foo'); // returns 'users' 158 | await cache.get('foo'); // returns 'cache' 159 | 160 | // Delete all values for the specified namespace 161 | await users.clear(); 162 | ``` 163 | 164 | ## 6. Advanced - Enable Compression 165 | 166 | Keyv supports both `gzip` and `brotli` methods of compression. Before you can enable compression, you will need to install the compression package: 167 | 168 | ```sh 169 | npm install --save keyv @keyv/compress-gzip 170 | ``` 171 | 172 | ### Example - Enable Gzip compression 173 | To enable compression, pass the `compression` option to the constructor. 174 | 175 | ```js 176 | const KeyvGzip = require('@keyv/compress-gzip'); 177 | const Keyv = require('keyv'); 178 | 179 | const keyvGzip = new KeyvGzip(); 180 | const keyv = new Keyv({ compression: KeyvGzip }); 181 | ``` 182 | 183 | You can also pass a custom compression function to the compression option. Custom compression functions must follow the pattern of the official compression adapter (see below for further information). 184 | 185 | ### Want to build your own? 186 | 187 | Great! Keyv is designed to be easily extended. You can build your own compression adapter by following the pattern of the official compression adapters based on this interface: 188 | 189 | ```js 190 | interface CompressionAdapter { 191 | async compress(value: any, options?: any); 192 | async decompress(value: any, options?: any); 193 | async serialize(value: any); 194 | async deserialize(value: any); 195 | } 196 | ``` 197 | 198 | #### Test your custom compression adapter 199 | In addition to the interface, you can test it with our compression test suite using `@keyv/test-suite`: 200 | 201 | ```js 202 | const {keyvCompresstionTests} = require('@keyv/test-suite'); 203 | const KeyvGzip = require('@keyv/compress-gzip'); 204 | 205 | keyvCompresstionTests(test, new KeyvGzip()); 206 | ``` 207 | 208 | ## 7. Advanced - Extend your own Module with Keyv 209 | Keyv can be easily embedded into other modules to add cache support. 210 | - Caching will work in memory by default, and users can also install a Keyv storage adapter and pass in a connection string or any other storage that implements the Map API. 211 | - You should also set a namespace for your module to safely call `.clear()` without clearing unrelated app data. 212 | 213 | >**Note**: 214 | > The recommended pattern is to expose a cache option in your module's options which is passed through to Keyv. 215 | 216 | ### Example - Add Cache Support to a Module 217 | 218 | 1. Install whichever storage adapter you will be using, `keyv-redis` in this example 219 | ```sh 220 | npm install --save keyv-redis 221 | ``` 222 | 2. Declare the Module with the cache controlled by a Keyv instance 223 | ```js 224 | class AwesomeModule { 225 | constructor(opts) { 226 | this.cache = new Keyv({ 227 | uri: typeof opts.cache === 'string' && opts.cache, 228 | store: typeof opts.cache !== 'string' && opts.cache, 229 | namespace: 'awesome-module' 230 | }); 231 | } 232 | } 233 | ``` 234 | 235 | 3. Create an Instance of the Module with caching support 236 | ```js 237 | const AwesomeModule = require('awesome-module'); 238 | const awesomeModule = new AwesomeModule({ cache: 'redis://localhost' }); 239 | ``` 240 | -------------------------------------------------------------------------------- /test/examples.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { describe, expect, test } from "vitest"; 4 | import { Writr } from "../src/writr.js"; 5 | 6 | const examplesDir = "./test/fixtures/examples"; 7 | 8 | const options = { 9 | renderOptions: { 10 | caching: true, 11 | }, 12 | }; 13 | 14 | describe("Examples Rendering Tests", () => { 15 | test("should render empty.md with async render, sync render, and cache", async () => { 16 | const filePath = path.join(examplesDir, "empty.md"); 17 | const content = fs.readFileSync(filePath, "utf-8"); 18 | const writr = new Writr(content, options); 19 | 20 | // Test async render 21 | const result = await writr.render(); 22 | expect(result).toBeDefined(); 23 | expect(typeof result).toBe("string"); 24 | expect(writr.cache).toBeDefined(); 25 | expect(writr.cache.store.size).toBe(1); 26 | 27 | // Test async render from cache 28 | const result2 = await writr.render(); 29 | expect(result2).toBe(result); 30 | expect(writr.cache.store.size).toBe(1); 31 | 32 | // Test sync render from cache 33 | const result3 = writr.renderSync(); 34 | expect(result3).toBe(result); 35 | expect(writr.cache.store.size).toBe(1); 36 | }); 37 | 38 | test("should render docula-readme.md with async render, sync render, and cache", async () => { 39 | const filePath = path.join(examplesDir, "docula-readme.md"); 40 | const content = fs.readFileSync(filePath, "utf-8"); 41 | const writr = new Writr(content, options); 42 | 43 | // Test async render 44 | const result = await writr.render(); 45 | expect(result).toBeDefined(); 46 | expect(typeof result).toBe("string"); 47 | expect(result.length).toBeGreaterThan(0); 48 | expect(writr.cache).toBeDefined(); 49 | expect(writr.cache.store.size).toBe(1); 50 | 51 | // Test async render from cache 52 | const result2 = await writr.render(); 53 | expect(result2).toBe(result); 54 | expect(writr.cache.store.size).toBe(1); 55 | 56 | // Test sync render from cache 57 | const result3 = writr.renderSync(); 58 | expect(result3).toBe(result); 59 | expect(writr.cache.store.size).toBe(1); 60 | }); 61 | 62 | test("should render front-matter.md with async render, sync render, and cache", async () => { 63 | const filePath = path.join(examplesDir, "front-matter.md"); 64 | const content = fs.readFileSync(filePath, "utf-8"); 65 | const writr = new Writr(content, options); 66 | 67 | const result = await writr.render(); 68 | expect(result).toBeDefined(); 69 | expect(typeof result).toBe("string"); 70 | expect(result.length).toBeGreaterThan(0); 71 | expect(writr.cache.store.size).toBe(1); 72 | 73 | const result2 = await writr.render(); 74 | expect(result2).toBe(result); 75 | 76 | const result3 = writr.renderSync(); 77 | expect(result3).toBe(result); 78 | }); 79 | 80 | test("should render no-front-matter.md with async render, sync render, and cache", async () => { 81 | const filePath = path.join(examplesDir, "no-front-matter.md"); 82 | const content = fs.readFileSync(filePath, "utf-8"); 83 | const writr = new Writr(content, options); 84 | 85 | const result = await writr.render(); 86 | expect(result).toBeDefined(); 87 | expect(typeof result).toBe("string"); 88 | expect(result.length).toBeGreaterThan(0); 89 | expect(writr.cache.store.size).toBe(1); 90 | 91 | const result2 = await writr.render(); 92 | expect(result2).toBe(result); 93 | 94 | const result3 = writr.renderSync(); 95 | expect(result3).toBe(result); 96 | }); 97 | 98 | test("should render readme-example.md with async render, sync render, and cache", async () => { 99 | const filePath = path.join(examplesDir, "readme-example.md"); 100 | const content = fs.readFileSync(filePath, "utf-8"); 101 | const writr = new Writr(content, options); 102 | 103 | const result = await writr.render(); 104 | expect(result).toBeDefined(); 105 | expect(typeof result).toBe("string"); 106 | expect(result.length).toBeGreaterThan(0); 107 | expect(writr.cache.store.size).toBe(1); 108 | 109 | const result2 = await writr.render(); 110 | expect(result2).toBe(result); 111 | 112 | const result3 = writr.renderSync(); 113 | expect(result3).toBe(result); 114 | }); 115 | 116 | test("should render single-site.md with async render, sync render, and cache", async () => { 117 | const filePath = path.join(examplesDir, "single-site.md"); 118 | const content = fs.readFileSync(filePath, "utf-8"); 119 | const writr = new Writr(content, options); 120 | 121 | const result = await writr.render(); 122 | expect(result).toBeDefined(); 123 | expect(typeof result).toBe("string"); 124 | expect(result.length).toBeGreaterThan(0); 125 | expect(writr.cache.store.size).toBe(1); 126 | 127 | const result2 = await writr.render(); 128 | expect(result2).toBe(result); 129 | 130 | const result3 = writr.renderSync(); 131 | expect(result3).toBe(result); 132 | }); 133 | 134 | test("should render index.md with async render, sync render, and cache", async () => { 135 | const filePath = path.join(examplesDir, "index.md"); 136 | const content = fs.readFileSync(filePath, "utf-8"); 137 | const writr = new Writr(content, options); 138 | 139 | const result = await writr.render(); 140 | expect(result).toBeDefined(); 141 | expect(typeof result).toBe("string"); 142 | expect(result.length).toBeGreaterThan(0); 143 | expect(writr.cache.store.size).toBe(1); 144 | 145 | const result2 = await writr.render(); 146 | expect(result2).toBe(result); 147 | 148 | const result3 = writr.renderSync(); 149 | expect(result3).toBe(result); 150 | }); 151 | 152 | test("should render keyv.md with async render, sync render, and cache", async () => { 153 | const filePath = path.join(examplesDir, "keyv.md"); 154 | const content = fs.readFileSync(filePath, "utf-8"); 155 | const writr = new Writr(content, options); 156 | 157 | const result = await writr.render(); 158 | expect(result).toBeDefined(); 159 | expect(typeof result).toBe("string"); 160 | expect(result.length).toBeGreaterThan(0); 161 | expect(writr.cache.store.size).toBe(1); 162 | 163 | const result2 = await writr.render(); 164 | expect(result2).toBe(result); 165 | 166 | const result3 = writr.renderSync(); 167 | expect(result3).toBe(result); 168 | }); 169 | 170 | test("should render test-suite.md with async render, sync render, and cache", async () => { 171 | const filePath = path.join(examplesDir, "test-suite.md"); 172 | const content = fs.readFileSync(filePath, "utf-8"); 173 | const writr = new Writr(content, options); 174 | 175 | const result = await writr.render(); 176 | expect(result).toBeDefined(); 177 | expect(typeof result).toBe("string"); 178 | expect(result.length).toBeGreaterThan(0); 179 | expect(writr.cache.store.size).toBe(1); 180 | 181 | const result2 = await writr.render(); 182 | expect(result2).toBe(result); 183 | 184 | const result3 = writr.renderSync(); 185 | expect(result3).toBe(result); 186 | }); 187 | 188 | test("should render caching-express.md with async render, sync render, and cache", async () => { 189 | const filePath = path.join(examplesDir, "caching-express.md"); 190 | const content = fs.readFileSync(filePath, "utf-8"); 191 | const writr = new Writr(content, options); 192 | 193 | const result = await writr.render(); 194 | expect(result).toBeDefined(); 195 | expect(typeof result).toBe("string"); 196 | expect(result.length).toBeGreaterThan(0); 197 | expect(writr.cache.store.size).toBe(1); 198 | 199 | const result2 = await writr.render(); 200 | expect(result2).toBe(result); 201 | 202 | const result3 = writr.renderSync(); 203 | expect(result3).toBe(result); 204 | }); 205 | 206 | test("should render caching-fastify.md with async render, sync render, and cache", async () => { 207 | const filePath = path.join(examplesDir, "caching-fastify.md"); 208 | const content = fs.readFileSync(filePath, "utf-8"); 209 | const writr = new Writr(content, options); 210 | 211 | const result = await writr.render(); 212 | expect(result).toBeDefined(); 213 | expect(typeof result).toBe("string"); 214 | expect(result.length).toBeGreaterThan(0); 215 | expect(writr.cache.store.size).toBe(1); 216 | 217 | const result2 = await writr.render(); 218 | expect(result2).toBe(result); 219 | 220 | const result3 = writr.renderSync(); 221 | expect(result3).toBe(result); 222 | }); 223 | 224 | test("should render caching-javascript.md with async render, sync render, and cache", async () => { 225 | const filePath = path.join(examplesDir, "caching-javascript.md"); 226 | const content = fs.readFileSync(filePath, "utf-8"); 227 | const writr = new Writr(content, options); 228 | 229 | const result = await writr.render(); 230 | expect(result).toBeDefined(); 231 | expect(typeof result).toBe("string"); 232 | expect(result.length).toBeGreaterThan(0); 233 | expect(writr.cache.store.size).toBe(1); 234 | 235 | const result2 = await writr.render(); 236 | expect(result2).toBe(result); 237 | 238 | const result3 = writr.renderSync(); 239 | expect(result3).toBe(result); 240 | }); 241 | 242 | test("should render caching-koa.md with async render, sync render, and cache", async () => { 243 | const filePath = path.join(examplesDir, "caching-koa.md"); 244 | const content = fs.readFileSync(filePath, "utf-8"); 245 | const writr = new Writr(content, options); 246 | 247 | const result = await writr.render(); 248 | expect(result).toBeDefined(); 249 | expect(typeof result).toBe("string"); 250 | expect(result.length).toBeGreaterThan(0); 251 | expect(writr.cache.store.size).toBe(1); 252 | 253 | const result2 = await writr.render(); 254 | expect(result2).toBe(result); 255 | 256 | const result3 = writr.renderSync(); 257 | expect(result3).toBe(result); 258 | }); 259 | 260 | test("should render caching-nestjs.md with async render, sync render, and cache", async () => { 261 | const filePath = path.join(examplesDir, "caching-nestjs.md"); 262 | const content = fs.readFileSync(filePath, "utf-8"); 263 | const writr = new Writr(content, options); 264 | 265 | const result = await writr.render(); 266 | expect(result).toBeDefined(); 267 | expect(typeof result).toBe("string"); 268 | expect(result.length).toBeGreaterThan(0); 269 | expect(writr.cache.store.size).toBe(1); 270 | 271 | const result2 = await writr.render(); 272 | expect(result2).toBe(result); 273 | 274 | const result3 = writr.renderSync(); 275 | expect(result3).toBe(result); 276 | }); 277 | 278 | test("should render caching-node.md with async render, sync render, and cache", async () => { 279 | const filePath = path.join(examplesDir, "caching-node.md"); 280 | const content = fs.readFileSync(filePath, "utf-8"); 281 | const writr = new Writr(content, options); 282 | 283 | const result = await writr.render(); 284 | expect(result).toBeDefined(); 285 | expect(typeof result).toBe("string"); 286 | expect(result.length).toBeGreaterThan(0); 287 | expect(writr.cache.store.size).toBe(1); 288 | 289 | const result2 = await writr.render(); 290 | expect(result2).toBe(result); 291 | 292 | const result3 = writr.renderSync(); 293 | expect(result3).toBe(result); 294 | }); 295 | 296 | test("should render etcd.md with async render, sync render, and cache", async () => { 297 | const filePath = path.join(examplesDir, "etcd.md"); 298 | const content = fs.readFileSync(filePath, "utf-8"); 299 | const writr = new Writr(content, options); 300 | 301 | const result = await writr.render(); 302 | expect(result).toBeDefined(); 303 | expect(typeof result).toBe("string"); 304 | expect(result.length).toBeGreaterThan(0); 305 | expect(writr.cache.store.size).toBe(1); 306 | 307 | const result2 = await writr.render(); 308 | expect(result2).toBe(result); 309 | 310 | const result3 = writr.renderSync(); 311 | expect(result3).toBe(result); 312 | }); 313 | 314 | test("should render memcache.md with async render, sync render, and cache", async () => { 315 | const filePath = path.join(examplesDir, "memcache.md"); 316 | const content = fs.readFileSync(filePath, "utf-8"); 317 | const writr = new Writr(content, options); 318 | 319 | const result = await writr.render(); 320 | expect(result).toBeDefined(); 321 | expect(typeof result).toBe("string"); 322 | expect(result.length).toBeGreaterThan(0); 323 | expect(writr.cache.store.size).toBe(1); 324 | 325 | const result2 = await writr.render(); 326 | expect(result2).toBe(result); 327 | 328 | const result3 = writr.renderSync(); 329 | expect(result3).toBe(result); 330 | }); 331 | 332 | test("should render mongo.md with async render, sync render, and cache", async () => { 333 | const filePath = path.join(examplesDir, "mongo.md"); 334 | const content = fs.readFileSync(filePath, "utf-8"); 335 | const writr = new Writr(content, options); 336 | 337 | const result = await writr.render(); 338 | expect(result).toBeDefined(); 339 | expect(typeof result).toBe("string"); 340 | expect(result.length).toBeGreaterThan(0); 341 | expect(writr.cache.store.size).toBe(1); 342 | 343 | const result2 = await writr.render(); 344 | expect(result2).toBe(result); 345 | 346 | const result3 = writr.renderSync(); 347 | expect(result3).toBe(result); 348 | }); 349 | 350 | test("should render mysql.md with async render, sync render, and cache", async () => { 351 | const filePath = path.join(examplesDir, "mysql.md"); 352 | const content = fs.readFileSync(filePath, "utf-8"); 353 | const writr = new Writr(content, options); 354 | 355 | const result = await writr.render(); 356 | expect(result).toBeDefined(); 357 | expect(typeof result).toBe("string"); 358 | expect(result.length).toBeGreaterThan(0); 359 | expect(writr.cache.store.size).toBe(1); 360 | 361 | const result2 = await writr.render(); 362 | expect(result2).toBe(result); 363 | 364 | const result3 = writr.renderSync(); 365 | expect(result3).toBe(result); 366 | }); 367 | 368 | test("should render offline.md with async render, sync render, and cache", async () => { 369 | const filePath = path.join(examplesDir, "offline.md"); 370 | const content = fs.readFileSync(filePath, "utf-8"); 371 | const writr = new Writr(content, options); 372 | 373 | const result = await writr.render(); 374 | expect(result).toBeDefined(); 375 | expect(typeof result).toBe("string"); 376 | expect(result.length).toBeGreaterThan(0); 377 | expect(writr.cache.store.size).toBe(1); 378 | 379 | const result2 = await writr.render(); 380 | expect(result2).toBe(result); 381 | 382 | const result3 = writr.renderSync(); 383 | expect(result3).toBe(result); 384 | }); 385 | 386 | test("should render postgres.md with async render, sync render, and cache", async () => { 387 | const filePath = path.join(examplesDir, "postgres.md"); 388 | const content = fs.readFileSync(filePath, "utf-8"); 389 | const writr = new Writr(content, options); 390 | 391 | const result = await writr.render(); 392 | expect(result).toBeDefined(); 393 | expect(typeof result).toBe("string"); 394 | expect(result.length).toBeGreaterThan(0); 395 | expect(writr.cache.store.size).toBe(1); 396 | 397 | const result2 = await writr.render(); 398 | expect(result2).toBe(result); 399 | 400 | const result3 = writr.renderSync(); 401 | expect(result3).toBe(result); 402 | }); 403 | 404 | test("should render redis.md with async render, sync render, and cache", async () => { 405 | const filePath = path.join(examplesDir, "redis.md"); 406 | const content = fs.readFileSync(filePath, "utf-8"); 407 | const writr = new Writr(content, options); 408 | 409 | const result = await writr.render(); 410 | expect(result).toBeDefined(); 411 | expect(typeof result).toBe("string"); 412 | expect(result.length).toBeGreaterThan(0); 413 | expect(writr.cache.store.size).toBe(1); 414 | 415 | const result2 = await writr.render(); 416 | expect(result2).toBe(result); 417 | 418 | const result3 = writr.renderSync(); 419 | expect(result3).toBe(result); 420 | }); 421 | 422 | test("should render serialize.md with async render, sync render, and cache", async () => { 423 | const filePath = path.join(examplesDir, "serialize.md"); 424 | const content = fs.readFileSync(filePath, "utf-8"); 425 | const writr = new Writr(content, options); 426 | 427 | const result = await writr.render(); 428 | expect(result).toBeDefined(); 429 | expect(typeof result).toBe("string"); 430 | expect(result.length).toBeGreaterThan(0); 431 | expect(writr.cache.store.size).toBe(1); 432 | 433 | const result2 = await writr.render(); 434 | expect(result2).toBe(result); 435 | 436 | const result3 = writr.renderSync(); 437 | expect(result3).toBe(result); 438 | }); 439 | 440 | test("should render sqlite.md with async render, sync render, and cache", async () => { 441 | const filePath = path.join(examplesDir, "sqlite.md"); 442 | const content = fs.readFileSync(filePath, "utf-8"); 443 | const writr = new Writr(content, options); 444 | 445 | const result = await writr.render(); 446 | expect(result).toBeDefined(); 447 | expect(typeof result).toBe("string"); 448 | expect(result.length).toBeGreaterThan(0); 449 | expect(writr.cache.store.size).toBe(1); 450 | 451 | const result2 = await writr.render(); 452 | expect(result2).toBe(result); 453 | 454 | const result3 = writr.renderSync(); 455 | expect(result3).toBe(result); 456 | }); 457 | 458 | test("should render tiered.md with async render, sync render, and cache", async () => { 459 | const filePath = path.join(examplesDir, "tiered.md"); 460 | const content = fs.readFileSync(filePath, "utf-8"); 461 | const writr = new Writr(content, options); 462 | 463 | const result = await writr.render(); 464 | expect(result).toBeDefined(); 465 | expect(typeof result).toBe("string"); 466 | expect(result.length).toBeGreaterThan(0); 467 | expect(writr.cache.store.size).toBe(1); 468 | 469 | const result2 = await writr.render(); 470 | expect(result2).toBe(result); 471 | 472 | const result3 = writr.renderSync(); 473 | expect(result3).toBe(result); 474 | }); 475 | }); 476 | -------------------------------------------------------------------------------- /test/fixtures/examples/keyv.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Keyv API' 3 | order: 2 4 | --- 5 | 6 | 7 | 8 | > Simple key-value storage with support for multiple backends 9 | 10 | [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) 11 | [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv) 12 | [![npm](https://img.shields.io/npm/dm/keyv.svg)](https://www.npmjs.com/package/keyv) 13 | [![npm](https://img.shields.io/npm/v/keyv.svg)](https://www.npmjs.com/package/keyv) 14 | 15 | Keyv provides a consistent interface for key-value storage across multiple backends via storage adapters. It supports TTL based expiry, making it suitable as a cache or a persistent key-value store. 16 | 17 | ## Features 18 | 19 | There are a few existing modules similar to Keyv, however Keyv is different because it: 20 | 21 | - Isn't bloated 22 | - Has a simple Promise based API 23 | - Suitable as a TTL based cache or persistent key-value store 24 | - [Easily embeddable](#add-cache-support-to-your-module) inside another module 25 | - Works with any storage that implements the [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) API 26 | - Handles all JSON types plus `Buffer` 27 | - Supports namespaces 28 | - Wide range of [**efficient, well tested**](#official-storage-adapters) storage adapters 29 | - Connection errors are passed through (db failures won't kill your app) 30 | - Supports the current active LTS version of Node.js or higher 31 | 32 | ## Usage 33 | 34 | Install Keyv. 35 | 36 | ``` 37 | npm install --save keyv 38 | ``` 39 | 40 | By default everything is stored in memory, you can optionally also install a storage adapter. 41 | 42 | ``` 43 | npm install --save @keyv/redis 44 | npm install --save @keyv/mongo 45 | npm install --save @keyv/sqlite 46 | npm install --save @keyv/postgres 47 | npm install --save @keyv/mysql 48 | npm install --save @keyv/etcd 49 | ``` 50 | 51 | Create a new Keyv instance, passing your connection string if applicable. Keyv will automatically load the correct storage adapter. 52 | 53 | ```js 54 | const Keyv = require('keyv'); 55 | 56 | // One of the following 57 | const keyv = new Keyv(); 58 | const keyv = new Keyv('redis://user:pass@localhost:6379'); 59 | const keyv = new Keyv('mongodb://user:pass@localhost:27017/dbname'); 60 | const keyv = new Keyv('sqlite://path/to/database.sqlite'); 61 | const keyv = new Keyv('postgresql://user:pass@localhost:5432/dbname'); 62 | const keyv = new Keyv('mysql://user:pass@localhost:3306/dbname'); 63 | const keyv = new Keyv('etcd://localhost:2379'); 64 | 65 | // Handle DB connection errors 66 | keyv.on('error', err => console.log('Connection Error', err)); 67 | 68 | await keyv.set('foo', 'expires in 1 second', 1000); // true 69 | await keyv.set('foo', 'never expires'); // true 70 | await keyv.get('foo'); // 'never expires' 71 | await keyv.delete('foo'); // true 72 | await keyv.clear(); // undefined 73 | ``` 74 | 75 | ### Namespaces 76 | 77 | You can namespace your Keyv instance to avoid key collisions and allow you to clear only a certain namespace while using the same database. 78 | 79 | ```js 80 | const users = new Keyv('redis://user:pass@localhost:6379', { namespace: 'users' }); 81 | const cache = new Keyv('redis://user:pass@localhost:6379', { namespace: 'cache' }); 82 | 83 | await users.set('foo', 'users'); // true 84 | await cache.set('foo', 'cache'); // true 85 | await users.get('foo'); // 'users' 86 | await cache.get('foo'); // 'cache' 87 | await users.clear(); // undefined 88 | await users.get('foo'); // undefined 89 | await cache.get('foo'); // 'cache' 90 | ``` 91 | 92 | ### Events 93 | 94 | Keyv is a custom `EventEmitter` and will emit an `'error'` event if there is an error. In addition it will emit a `clear` and `disconnect` event when the corresponding methods are called. 95 | 96 | ```js 97 | const keyv = new Keyv(); 98 | const handleConnectionError = err => console.log('Connection Error', err); 99 | const handleClear = () => console.log('Cache Cleared'); 100 | const handleDisconnect = () => console.log('Disconnected'); 101 | 102 | keyv.on('error', handleConnectionError); 103 | keyv.on('clear', handleClear); 104 | keyv.on('disconnect', handleDisconnect); 105 | ``` 106 | 107 | ### Hooks 108 | 109 | Keyv supports hooks for `get`, `set`, and `delete` methods. Hooks are useful for logging, debugging, and other custom functionality. Here is a list of all the hooks: 110 | 111 | ``` 112 | PRE_GET 113 | POST_GET 114 | PRE_GET_MANY 115 | POST_GET_MANY 116 | PRE_SET 117 | POST_SET 118 | PRE_DELETE 119 | POST_DELETE 120 | ``` 121 | 122 | You can access this by importing `KeyvHooks` from the main Keyv package. 123 | 124 | ```js 125 | import Keyv, { KeyvHooks } from 'keyv'; 126 | ``` 127 | 128 | ```js 129 | //PRE_SET hook 130 | const keyv = new Keyv(); 131 | keyv.hooks.addListener(KeyvHooks.PRE_SET, (key, value) => console.log(`Setting key ${key} to ${value}`)); 132 | 133 | //POST_SET hook 134 | const keyv = new Keyv(); 135 | keyv.hooks.addListener(KeyvHooks.POST_SET, (key, value) => console.log(`Set key ${key} to ${value}`)); 136 | ``` 137 | 138 | In these examples you can also manipulate the value before it is set. For example, you could add a prefix to all keys. 139 | 140 | ```js 141 | const keyv = new Keyv(); 142 | keyv.hooks.addListener(KeyvHooks.PRE_SET, (key, value) => { 143 | console.log(`Setting key ${key} to ${value}`); 144 | key = `prefix-${key}`; 145 | }); 146 | ``` 147 | 148 | Now this key will have prefix- added to it before it is set. 149 | 150 | In `PRE_DELETE` and `POST_DELETE` hooks, the value could be a single item or an `Array`. This is based on the fact that `delete` can accept a single key or an `Array` of keys. 151 | 152 | 153 | ### Custom Serializers 154 | 155 | Keyv uses [`buffer`](https://github.com/feross/buffer) for data serialization to ensure consistency across different backends. 156 | 157 | You can optionally provide your own serialization functions to support extra data types or to serialize to something other than JSON. 158 | 159 | ```js 160 | const keyv = new Keyv({ serialize: JSON.stringify, deserialize: JSON.parse }); 161 | ``` 162 | 163 | **Warning:** Using custom serializers means you lose any guarantee of data consistency. You should do extensive testing with your serialisation functions and chosen storage engine. 164 | 165 | ## Official Storage Adapters 166 | 167 | The official storage adapters are covered by [over 150 integration tests](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml) to guarantee consistent behaviour. They are lightweight, efficient wrappers over the DB clients making use of indexes and native TTLs where available. 168 | 169 | Database | Adapter | Native TTL 170 | ---|---|--- 171 | Redis | [@keyv/redis](https://github.com/jaredwray/keyv/tree/master/packages/redis) | Yes 172 | MongoDB | [@keyv/mongo](https://github.com/jaredwray/keyv/tree/master/packages/mongo) | Yes 173 | SQLite | [@keyv/sqlite](https://github.com/jaredwray/keyv/tree/master/packages/sqlite) | No 174 | PostgreSQL | [@keyv/postgres](https://github.com/jaredwray/keyv/tree/master/packages/postgres) | No 175 | MySQL | [@keyv/mysql](https://github.com/jaredwray/keyv/tree/master/packages/mysql) | No 176 | Etcd | [@keyv/etcd](https://github.com/jaredwray/keyv/tree/master/packages/etcd) | Yes 177 | Memcache | [@keyv/memcache](https://github.com/jaredwray/keyv/tree/master/packages/memcache) | Yes 178 | 179 | ## Third-party Storage Adapters 180 | 181 | You can also use third-party storage adapters or build your own. Keyv will wrap these storage adapters in TTL functionality and handle complex types internally. 182 | 183 | ```js 184 | const Keyv = require('keyv'); 185 | const myAdapter = require('./my-storage-adapter'); 186 | 187 | const keyv = new Keyv({ store: myAdapter }); 188 | ``` 189 | 190 | Any store that follows the [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) api will work. 191 | 192 | ```js 193 | new Keyv({ store: new Map() }); 194 | ``` 195 | 196 | For example, [`quick-lru`](https://github.com/sindresorhus/quick-lru) is a completely unrelated module that implements the Map API. 197 | 198 | ```js 199 | const Keyv = require('keyv'); 200 | const QuickLRU = require('quick-lru'); 201 | 202 | const lru = new QuickLRU({ maxSize: 1000 }); 203 | const keyv = new Keyv({ store: lru }); 204 | ``` 205 | 206 | The following are third-party storage adapters compatible with Keyv: 207 | 208 | - [quick-lru](https://github.com/sindresorhus/quick-lru) - Simple "Least Recently Used" (LRU) cache 209 | - [keyv-file](https://github.com/zaaack/keyv-file) - File system storage adapter for Keyv 210 | - [keyv-dynamodb](https://www.npmjs.com/package/keyv-dynamodb) - DynamoDB storage adapter for Keyv 211 | - [keyv-lru](https://www.npmjs.com/package/keyv-lru) - LRU storage adapter for Keyv 212 | - [keyv-null](https://www.npmjs.com/package/keyv-null) - Null storage adapter for Keyv 213 | - [keyv-firestore ](https://github.com/goto-bus-stop/keyv-firestore) – Firebase Cloud Firestore adapter for Keyv 214 | - [keyv-mssql](https://github.com/pmorgan3/keyv-mssql) - Microsoft Sql Server adapter for Keyv 215 | - [keyv-azuretable](https://github.com/howlowck/keyv-azuretable) - Azure Table Storage/API adapter for Keyv 216 | - [keyv-arango](https://github.com/TimMikeladze/keyv-arango) - ArangoDB storage adapter for Keyv 217 | - [keyv-momento](https://github.com/momentohq/node-keyv-adaptor/) - Momento storage adapter for Keyv 218 | 219 | ## Add Cache Support to your Module 220 | 221 | Keyv is designed to be easily embedded into other modules to add cache support. The recommended pattern is to expose a `cache` option in your modules options which is passed through to Keyv. Caching will work in memory by default and users have the option to also install a Keyv storage adapter and pass in a connection string, or any other storage that implements the `Map` API. 222 | 223 | You should also set a namespace for your module so you can safely call `.clear()` without clearing unrelated app data. 224 | 225 | Inside your module: 226 | 227 | ```js 228 | class AwesomeModule { 229 | constructor(opts) { 230 | this.cache = new Keyv({ 231 | uri: typeof opts.cache === 'string' && opts.cache, 232 | store: typeof opts.cache !== 'string' && opts.cache, 233 | namespace: 'awesome-module' 234 | }); 235 | } 236 | } 237 | ``` 238 | 239 | Now it can be consumed like this: 240 | 241 | ```js 242 | const AwesomeModule = require('awesome-module'); 243 | 244 | // Caches stuff in memory by default 245 | const awesomeModule = new AwesomeModule(); 246 | 247 | // After npm install --save keyv-redis 248 | const awesomeModule = new AwesomeModule({ cache: 'redis://localhost' }); 249 | 250 | // Some third-party module that implements the Map API 251 | const awesomeModule = new AwesomeModule({ cache: some3rdPartyStore }); 252 | ``` 253 | 254 | ## Compression 255 | 256 | Keyv supports `gzip` and `brotli` compression. To enable compression, pass the `compress` option to the constructor. 257 | 258 | ```js 259 | const KeyvGzip = require('@keyv/compress-gzip'); 260 | const Keyv = require('keyv'); 261 | 262 | const keyvGzip = new KeyvGzip(); 263 | const keyv = new Keyv({ compression: KeyvGzip }); 264 | ``` 265 | 266 | You can also pass a custom compression function to the `compression` option. Following the pattern of the official compression adapters. 267 | 268 | ### Want to build your own? 269 | 270 | Great! Keyv is designed to be easily extended. You can build your own compression adapter by following the pattern of the official compression adapters based on this interface: 271 | 272 | ```typescript 273 | interface CompressionAdapter { 274 | async compress(value: any, options?: any); 275 | async decompress(value: any, options?: any); 276 | async serialize(value: any); 277 | async deserialize(value: any); 278 | } 279 | ``` 280 | 281 | In addition to the interface, you can test it with our compression test suite using @keyv/test-suite: 282 | 283 | ```js 284 | const {keyvCompresstionTests} = require('@keyv/test-suite'); 285 | const KeyvGzip = require('@keyv/compress-gzip'); 286 | 287 | keyvCompresstionTests(test, new KeyvGzip()); 288 | ``` 289 | 290 | ## API 291 | 292 | ### new Keyv([uri], [options]) 293 | 294 | Returns a new Keyv instance. 295 | 296 | The Keyv instance is also an `EventEmitter` that will emit an `'error'` event if the storage adapter connection fails. 297 | 298 | ### uri 299 | 300 | Type: `String`
301 | Default: `undefined` 302 | 303 | The connection string URI. 304 | 305 | Merged into the options object as options.uri. 306 | 307 | ### options 308 | 309 | Type: `Object` 310 | 311 | The options object is also passed through to the storage adapter. Check your storage adapter docs for any extra options. 312 | 313 | #### options.namespace 314 | 315 | Type: `String`
316 | Default: `'keyv'` 317 | 318 | Namespace for the current instance. 319 | 320 | #### options.ttl 321 | 322 | Type: `Number`
323 | Default: `undefined` 324 | 325 | Default TTL. Can be overridden by specififying a TTL on `.set()`. 326 | 327 | #### options.compression 328 | 329 | Type: `@keyv/compress-`
330 | Default: `undefined` 331 | 332 | Compression package to use. See [Compression](#compression) for more details. 333 | 334 | #### options.serialize 335 | 336 | Type: `Function`
337 | Default: `JSONB.stringify` 338 | 339 | A custom serialization function. 340 | 341 | #### options.deserialize 342 | 343 | Type: `Function`
344 | Default: `JSONB.parse` 345 | 346 | A custom deserialization function. 347 | 348 | #### options.store 349 | 350 | Type: `Storage adapter instance`
351 | Default: `new Map()` 352 | 353 | The storage adapter instance to be used by Keyv. 354 | 355 | #### options.adapter 356 | 357 | Type: `String`
358 | Default: `undefined` 359 | 360 | Specify an adapter to use. e.g `'redis'` or `'mongodb'`. 361 | 362 | ### Instance 363 | 364 | Keys must always be strings. Values can be of any type. 365 | 366 | #### .set(key, value, [ttl]) 367 | 368 | Set a value. 369 | 370 | By default keys are persistent. You can set an expiry TTL in milliseconds. 371 | 372 | Returns a promise which resolves to `true`. 373 | 374 | #### .get(key, [options]) 375 | 376 | Returns a promise which resolves to the retrieved value. 377 | 378 | ##### options.raw 379 | 380 | Type: `Boolean`
381 | Default: `false` 382 | 383 | If set to true the raw DB object Keyv stores internally will be returned instead of just the value. 384 | 385 | This contains the TTL timestamp. 386 | 387 | #### .delete(key) 388 | 389 | Deletes an entry. 390 | 391 | Returns a promise which resolves to `true` if the key existed, `false` if not. 392 | 393 | #### .clear() 394 | 395 | Delete all entries in the current namespace. 396 | 397 | Returns a promise which is resolved when the entries have been cleared. 398 | 399 | #### .iterator() 400 | 401 | Iterate over all entries of the current namespace. 402 | 403 | Returns a iterable that can be iterated by for-of loops. For example: 404 | 405 | ```js 406 | // please note that the "await" keyword should be used here 407 | for await (const [key, value] of this.keyv.iterator()) { 408 | console.log(key, value); 409 | }; 410 | ``` 411 | 412 | # How to Contribute 413 | 414 | In this section of the documentation we will cover: 415 | 416 | 1) How to set up this repository locally 417 | 2) How to get started with running commands 418 | 3) How to contribute changes using Pull Requests 419 | 420 | ## Dependencies 421 | 422 | This package requires the following dependencies to run: 423 | 424 | 1) [Yarn V1](https://yarnpkg.com/getting-started/install) 425 | 3) [Docker](https://docs.docker.com/get-docker/) 426 | 427 | ## Setting up your workspace 428 | 429 | To contribute to this repository, start by setting up this project locally: 430 | 431 | 1) Fork this repository into your Git account 432 | 2) Clone the forked repository to your local directory using `git clone` 433 | 3) Install any of the above missing dependencies 434 | 435 | ## Launching the project 436 | 437 | Once the project is installed locally, you are ready to start up its services: 438 | 439 | 1) Ensure that your Docker service is running. 440 | 2) From the root directory of your project, run the `yarn` command in the command prompt to install yarn. 441 | 3) Run the `yarn bootstrap` command to install any necessary dependencies. 442 | 4) Run `yarn test:services:start` to start up this project's Docker container. The container will launch all services within your workspace. 443 | 444 | ## Available Commands 445 | 446 | Once the project is running, you can execute a variety of commands. The root workspace and each subpackage contain a `package.json` file with a `scripts` field listing all the commands that can be executed from that directory. This project also supports native `yarn`, and `docker` commands. 447 | 448 | Here, we'll cover the primary commands that can be executed from the root directory. Unless otherwise noted, these commands can also be executed from a subpackage. If executed from a subpackage, they will only affect that subpackage, rather than the entire workspace. 449 | 450 | ### `yarn` 451 | 452 | The `yarn` command installs yarn in the workspace. 453 | 454 | ### `yarn bootstrap` 455 | 456 | The `yarn bootstrap` command installs all dependencies in the workspace. 457 | 458 | ### `yarn test:services:start` 459 | 460 | The `yarn test:services:start` command starts up the project's Docker container, launching all services in the workspace. This command must be executed from the root directory. 461 | 462 | ### `yarn test:services:stop` 463 | 464 | The `yarn test:services:stop` command brings down the project's Docker container, halting all services. This command must be executed from the root directory. 465 | 466 | ### `yarn test` 467 | 468 | The `yarn test` command runs all tests in the workspace. 469 | 470 | ### `yarn clean` 471 | 472 | The `yarn clean` command removes yarn and all dependencies installed by yarn. After executing this command, you must repeat the steps in *Setting up your workspace* to rebuild your workspace. 473 | 474 | ## Contributing Changes 475 | 476 | Now that you've set up your workspace, you're ready to contribute changes to the `keyv` repository. 477 | 478 | 1) Make any changes that you would like to contribute in your local workspace. 479 | 2) After making these changes, ensure that the project's tests still pass by executing the `yarn test` command in the root directory. 480 | 3) Commit your changes and push them to your forked repository. 481 | 4) Navigate to the original `keyv` repository and go the *Pull Requests* tab. 482 | 5) Click the *New pull request* button, and open a pull request for the branch in your repository that contains your changes. 483 | 6) Once your pull request is created, ensure that all checks have passed and that your branch has no conflicts with the base branch. If there are any issues, resolve these changes in your local repository, and then commit and push them to git. 484 | 7) Similarly, respond to any reviewer comments or requests for changes by making edits to your local repository and pushing them to Git. 485 | 8) Once the pull request has been reviewed, those with write access to the branch will be able to merge your changes into the `keyv` repository. 486 | 487 | If you need more information on the steps to create a pull request, you can find a detailed walkthrough in the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) 488 | 489 | ## License 490 | 491 | MIT © Jared Wray 492 | -------------------------------------------------------------------------------- /src/writr.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { dirname } from "node:path"; 3 | import { Hookified } from "hookified"; 4 | import parse, { type HTMLReactParserOptions } from "html-react-parser"; 5 | import * as yaml from "js-yaml"; 6 | import type React from "react"; 7 | import rehypeHighlight from "rehype-highlight"; 8 | import rehypeKatex from "rehype-katex"; 9 | import rehypeSlug from "rehype-slug"; 10 | import rehypeStringify from "rehype-stringify"; 11 | import remarkEmoji from "remark-emoji"; 12 | import remarkGfm from "remark-gfm"; 13 | import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert"; 14 | import remarkMath from "remark-math"; 15 | import remarkMDX from "remark-mdx"; 16 | import remarkParse from "remark-parse"; 17 | import remarkRehype from "remark-rehype"; 18 | import remarkToc from "remark-toc"; 19 | import { unified } from "unified"; 20 | import { WritrCache } from "./writr-cache.js"; 21 | 22 | /** 23 | * Writr options. 24 | * @typedef {Object} WritrOptions 25 | * @property {RenderOptions} [renderOptions] - Default render options (default: undefined) 26 | * @property {boolean} [throwErrors] - Throw error (default: false) 27 | */ 28 | export type WritrOptions = { 29 | renderOptions?: RenderOptions; // Default render options (default: undefined) 30 | throwErrors?: boolean; // Throw error (default: false) 31 | }; 32 | 33 | /** 34 | * Render options. 35 | * @typedef {Object} RenderOptions 36 | * @property {boolean} [emoji] - Emoji support (default: true) 37 | * @property {boolean} [toc] - Table of contents generation (default: true) 38 | * @property {boolean} [slug] - Slug generation (default: true) 39 | * @property {boolean} [highlight] - Code highlighting (default: true) 40 | * @property {boolean} [gfm] - Github flavor markdown (default: true) 41 | * @property {boolean} [math] - Math support (default: true) 42 | * @property {boolean} [mdx] - MDX support (default: false) 43 | * @property {boolean} [caching] - Caching (default: true) 44 | */ 45 | export type RenderOptions = { 46 | emoji?: boolean; // Emoji support (default: true) 47 | toc?: boolean; // Table of contents generation (default: true) 48 | slug?: boolean; // Slug generation (default: true) 49 | highlight?: boolean; // Code highlighting (default: true) 50 | gfm?: boolean; // Github flavor markdown (default: true) 51 | math?: boolean; // Math support (default: true) 52 | mdx?: boolean; // MDX support (default: false) 53 | caching?: boolean; // Caching (default: true) 54 | }; 55 | 56 | /** 57 | * Validation result. 58 | * @typedef {Object} WritrValidateResult 59 | * @property {boolean} valid - Whether the markdown is valid 60 | * @property {Error} [error] - Error if validation failed 61 | */ 62 | export type WritrValidateResult = { 63 | valid: boolean; 64 | error?: Error; 65 | }; 66 | 67 | export enum WritrHooks { 68 | beforeRender = "beforeRender", 69 | afterRender = "afterRender", 70 | saveToFile = "saveToFile", 71 | renderToFile = "renderToFile", 72 | loadFromFile = "loadFromFile", 73 | } 74 | 75 | export class Writr extends Hookified { 76 | public engine = unified() 77 | .use(remarkParse) 78 | .use(remarkGfm) // Use GitHub Flavored Markdown 79 | .use(remarkToc) // Add table of contents 80 | .use(remarkEmoji) // Add emoji support 81 | .use(remarkRehype) // Convert markdown to HTML 82 | .use(rehypeSlug) // Add slugs to headings in HTML 83 | .use(remarkMath) // Add math support 84 | .use(rehypeKatex) // Add math support 85 | .use(rehypeHighlight) // Apply syntax highlighting 86 | .use(rehypeStringify); // Stringify HTML 87 | 88 | private readonly _options: WritrOptions = { 89 | throwErrors: false, 90 | renderOptions: { 91 | emoji: true, 92 | toc: true, 93 | slug: true, 94 | highlight: true, 95 | gfm: true, 96 | math: true, 97 | mdx: false, 98 | caching: true, 99 | }, 100 | }; 101 | 102 | private _content = ""; 103 | 104 | private readonly _cache = new WritrCache(); 105 | 106 | /** 107 | * Initialize Writr. Accepts a string or options object. 108 | * @param {string | WritrOptions} [arguments1] If you send in a string, it will be used as the markdown content. If you send in an object, it will be used as the options. 109 | * @param {WritrOptions} [arguments2] This is if you send in the content in the first argument and also want to send in options. 110 | * 111 | * @example 112 | * const writr = new Writr('Hello, world!', {caching: false}); 113 | */ 114 | constructor(arguments1?: string | WritrOptions, arguments2?: WritrOptions) { 115 | super(); 116 | if (typeof arguments1 === "string") { 117 | this._content = arguments1; 118 | } else if (arguments1) { 119 | this._options = this.mergeOptions(this._options, arguments1); 120 | /* v8 ignore next -- @preserve */ 121 | if (this._options.renderOptions) { 122 | this.engine = this.createProcessor(this._options.renderOptions); 123 | } 124 | } 125 | 126 | if (arguments2) { 127 | this._options = this.mergeOptions(this._options, arguments2); 128 | /* v8 ignore next -- @preserve */ 129 | if (this._options.renderOptions) { 130 | this.engine = this.createProcessor(this._options.renderOptions); 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Get the options. 137 | * @type {WritrOptions} 138 | */ 139 | public get options(): WritrOptions { 140 | return this._options; 141 | } 142 | 143 | /** 144 | * Get the Content. This is the markdown content and front matter if it exists. 145 | * @type {WritrOptions} 146 | */ 147 | public get content(): string { 148 | return this._content; 149 | } 150 | 151 | /** 152 | * Set the Content. This is the markdown content and front matter if it exists. 153 | * @type {WritrOptions} 154 | */ 155 | public set content(value: string) { 156 | this._content = value; 157 | } 158 | 159 | /** 160 | * Get the cache. 161 | * @type {WritrCache} 162 | */ 163 | public get cache(): WritrCache { 164 | return this._cache; 165 | } 166 | 167 | /** 168 | * Get the front matter raw content. 169 | * @type {string} The front matter content including the delimiters. 170 | */ 171 | public get frontMatterRaw(): string { 172 | // Is there front matter content? 173 | if (!this._content.trimStart().startsWith("---")) { 174 | return ""; 175 | } 176 | 177 | const match = /^\s*(---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$))/.exec( 178 | this._content, 179 | ); 180 | if (match) { 181 | return match[1]; 182 | } 183 | 184 | return ""; 185 | } 186 | 187 | /** 188 | * Get the body content without the front matter. 189 | * @type {string} The markdown content without the front matter. 190 | */ 191 | public get body(): string { 192 | const frontMatter = this.frontMatterRaw; 193 | if (frontMatter === "") { 194 | return this._content; 195 | } 196 | 197 | return this._content 198 | .slice(this._content.indexOf(frontMatter) + frontMatter.length) 199 | .trim(); 200 | } 201 | 202 | /** 203 | * Get the markdown content. This is an alias for the body property. 204 | * @type {string} The markdown content. 205 | */ 206 | public get markdown(): string { 207 | return this.body; 208 | } 209 | 210 | /** 211 | * Get the front matter content as an object. 212 | * @type {Record} The front matter content as an object. 213 | */ 214 | // biome-ignore lint/suspicious/noExplicitAny: expected 215 | public get frontMatter(): Record { 216 | const frontMatter = this.frontMatterRaw; 217 | const match = /^---\s*([\s\S]*?)\s*---\s*/.exec(frontMatter); 218 | if (match) { 219 | try { 220 | // biome-ignore lint/suspicious/noExplicitAny: expected 221 | return yaml.load(match[1].trim()) as Record; 222 | } catch (error) { 223 | /* v8 ignore next -- @preserve */ 224 | this.emit("error", error); 225 | } 226 | } 227 | 228 | return {}; 229 | } 230 | 231 | /** 232 | * Set the front matter content as an object. 233 | * @type {Record} The front matter content as an object. 234 | */ 235 | // biome-ignore lint/suspicious/noExplicitAny: expected 236 | public set frontMatter(data: Record) { 237 | try { 238 | const frontMatter = this.frontMatterRaw; 239 | const yamlString = yaml.dump(data); 240 | const newFrontMatter = `---\n${yamlString}---\n`; 241 | this._content = this._content.replace(frontMatter, newFrontMatter); 242 | } catch (error) { 243 | /* v8 ignore next -- @preserve */ 244 | this.emit("error", error); 245 | } 246 | } 247 | 248 | /** 249 | * Get the front matter value for a key. 250 | * @param {string} key The key to get the value for. 251 | * @returns {T} The value for the key. 252 | */ 253 | public getFrontMatterValue(key: string): T { 254 | return this.frontMatter[key] as T; 255 | } 256 | 257 | /** 258 | * Render the markdown content to HTML. 259 | * @param {RenderOptions} [options] The render options. 260 | * @returns {Promise} The rendered HTML content. 261 | */ 262 | public async render(options?: RenderOptions): Promise { 263 | try { 264 | let { engine } = this; 265 | if (options) { 266 | options = { ...this._options.renderOptions, ...options }; 267 | engine = this.createProcessor(options); 268 | } 269 | 270 | const renderData = { 271 | content: this._content, 272 | body: this.body, 273 | options, 274 | }; 275 | 276 | await this.hook(WritrHooks.beforeRender, renderData); 277 | 278 | const resultData = { 279 | result: "", 280 | }; 281 | if (this.isCacheEnabled(renderData.options)) { 282 | const cached = this._cache.get(renderData.content, renderData.options); 283 | if (cached) { 284 | return cached; 285 | } 286 | } 287 | 288 | const file = await engine.process(renderData.body); 289 | resultData.result = String(file); 290 | if (this.isCacheEnabled(renderData.options)) { 291 | this._cache.set( 292 | renderData.content, 293 | resultData.result, 294 | renderData.options, 295 | ); 296 | } 297 | 298 | await this.hook(WritrHooks.afterRender, resultData); 299 | 300 | return resultData.result; 301 | } catch (error) { 302 | this.emit("error", error); 303 | throw new Error(`Failed to render markdown: ${(error as Error).message}`); 304 | } 305 | } 306 | 307 | /** 308 | * Render the markdown content to HTML synchronously. 309 | * @param {RenderOptions} [options] The render options. 310 | * @returns {string} The rendered HTML content. 311 | */ 312 | public renderSync(options?: RenderOptions): string { 313 | try { 314 | let { engine } = this; 315 | if (options) { 316 | options = { ...this._options.renderOptions, ...options }; 317 | engine = this.createProcessor(options); 318 | } 319 | 320 | const renderData = { 321 | content: this._content, 322 | body: this.body, 323 | options, 324 | }; 325 | 326 | this.hook(WritrHooks.beforeRender, renderData); 327 | 328 | const resultData = { 329 | result: "", 330 | }; 331 | /* v8 ignore next -- @preserve */ 332 | if (this.isCacheEnabled(renderData.options)) { 333 | const cached = this._cache.get(renderData.content, renderData.options); 334 | if (cached) { 335 | return cached; 336 | } 337 | } 338 | 339 | const file = engine.processSync(renderData.body); 340 | resultData.result = String(file); 341 | if (this.isCacheEnabled(renderData.options)) { 342 | this._cache.set( 343 | renderData.content, 344 | resultData.result, 345 | renderData.options, 346 | ); 347 | } 348 | 349 | this.hook(WritrHooks.afterRender, resultData); 350 | 351 | return resultData.result; 352 | } catch (error) { 353 | this.emit("error", error); 354 | throw new Error(`Failed to render markdown: ${(error as Error).message}`); 355 | } 356 | } 357 | 358 | /** 359 | * Validate the markdown content by attempting to render it. 360 | * @param {string} [content] The markdown content to validate. If not provided, uses the current content. 361 | * @param {RenderOptions} [options] The render options. 362 | * @returns {Promise} An object with a valid boolean and optional error. 363 | */ 364 | public async validate( 365 | content?: string, 366 | options?: RenderOptions, 367 | ): Promise { 368 | const originalContent = this._content; 369 | try { 370 | if (content !== undefined) { 371 | this._content = content; 372 | } 373 | 374 | let { engine } = this; 375 | if (options) { 376 | options = { 377 | ...this._options.renderOptions, 378 | ...options, 379 | caching: false, 380 | }; 381 | engine = this.createProcessor(options); 382 | } 383 | 384 | await engine.run(engine.parse(this.body)); 385 | 386 | if (content !== undefined) { 387 | this._content = originalContent; 388 | } 389 | 390 | return { valid: true }; 391 | } catch (error) { 392 | this.emit("error", error); 393 | if (content !== undefined) { 394 | this._content = originalContent; 395 | } 396 | return { valid: false, error: error as Error }; 397 | } 398 | } 399 | 400 | /** 401 | * Validate the markdown content by attempting to render it synchronously. 402 | * @param {string} [content] The markdown content to validate. If not provided, uses the current content. 403 | * @param {RenderOptions} [options] The render options. 404 | * @returns {WritrValidateResult} An object with a valid boolean and optional error. 405 | */ 406 | public validateSync( 407 | content?: string, 408 | options?: RenderOptions, 409 | ): WritrValidateResult { 410 | const originalContent = this._content; 411 | try { 412 | if (content !== undefined) { 413 | this._content = content; 414 | } 415 | 416 | let { engine } = this; 417 | if (options) { 418 | options = { 419 | ...this._options.renderOptions, 420 | ...options, 421 | caching: false, 422 | }; 423 | engine = this.createProcessor(options); 424 | } 425 | 426 | engine.runSync(engine.parse(this.body)); 427 | 428 | if (content !== undefined) { 429 | this._content = originalContent; 430 | } 431 | 432 | return { valid: true }; 433 | } catch (error) { 434 | this.emit("error", error); 435 | if (content !== undefined) { 436 | this._content = originalContent; 437 | } 438 | return { valid: false, error: error as Error }; 439 | } 440 | } 441 | 442 | /** 443 | * Render the markdown content and save it to a file. If the directory doesn't exist it will be created. 444 | * @param {string} filePath The file path to save the rendered markdown content to. 445 | * @param {RenderOptions} [options] the render options. 446 | */ 447 | public async renderToFile( 448 | filePath: string, 449 | options?: RenderOptions, 450 | ): Promise { 451 | try { 452 | const { writeFile, mkdir } = fs.promises; 453 | const directoryPath = dirname(filePath); 454 | const content = await this.render(options); 455 | await mkdir(directoryPath, { recursive: true }); 456 | const data = { 457 | filePath, 458 | content, 459 | }; 460 | await this.hook(WritrHooks.renderToFile, data); 461 | await writeFile(data.filePath, data.content); 462 | } catch (error) { 463 | this.emit("error", error); 464 | /* v8 ignore next -- @preserve */ 465 | if (this._options.throwErrors) { 466 | throw error; 467 | } 468 | } 469 | } 470 | 471 | /** 472 | * Render the markdown content and save it to a file synchronously. If the directory doesn't exist it will be created. 473 | * @param {string} filePath The file path to save the rendered markdown content to. 474 | * @param {RenderOptions} [options] the render options. 475 | */ 476 | public renderToFileSync(filePath: string, options?: RenderOptions): void { 477 | try { 478 | const directoryPath = dirname(filePath); 479 | const content = this.renderSync(options); 480 | fs.mkdirSync(directoryPath, { recursive: true }); 481 | const data = { 482 | filePath, 483 | content, 484 | }; 485 | 486 | this.hook(WritrHooks.renderToFile, data); 487 | 488 | fs.writeFileSync(data.filePath, data.content); 489 | } catch (error) { 490 | this.emit("error", error); 491 | /* v8 ignore next -- @preserve */ 492 | if (this._options.throwErrors) { 493 | /* v8 ignore next -- @preserve */ 494 | throw error; 495 | } 496 | } 497 | } 498 | 499 | /** 500 | * Render the markdown content to React. 501 | * @param {RenderOptions} [options] The render options. 502 | * @param {HTMLReactParserOptions} [reactParseOptions] The HTML React parser options. 503 | * @returns {Promise} The rendered React content. 504 | */ 505 | public async renderReact( 506 | options?: RenderOptions, 507 | reactParseOptions?: HTMLReactParserOptions, 508 | ): Promise { 509 | try { 510 | const html = await this.render(options); 511 | 512 | return parse(html, reactParseOptions); 513 | } catch (error) { 514 | this.emit("error", error); 515 | throw new Error(`Failed to render React: ${(error as Error).message}`); 516 | } 517 | } 518 | 519 | /** 520 | * Render the markdown content to React synchronously. 521 | * @param {RenderOptions} [options] The render options. 522 | * @param {HTMLReactParserOptions} [reactParseOptions] The HTML React parser options. 523 | * @returns {string | React.JSX.Element | React.JSX.Element[]} The rendered React content. 524 | */ 525 | public renderReactSync( 526 | options?: RenderOptions, 527 | reactParseOptions?: HTMLReactParserOptions, 528 | ): string | React.JSX.Element | React.JSX.Element[] { 529 | try { 530 | const html = this.renderSync(options); 531 | return parse(html, reactParseOptions); 532 | } catch (error) { 533 | this.emit("error", error); 534 | throw new Error(`Failed to render React: ${(error as Error).message}`); 535 | } 536 | } 537 | 538 | /** 539 | * Load markdown content from a file. 540 | * @param {string} filePath The file path to load the markdown content from. 541 | * @returns {Promise} 542 | */ 543 | public async loadFromFile(filePath: string): Promise { 544 | try { 545 | const { readFile } = fs.promises; 546 | const data = { 547 | content: "", 548 | }; 549 | data.content = await readFile(filePath, "utf8"); 550 | 551 | await this.hook(WritrHooks.loadFromFile, data); 552 | this._content = data.content; 553 | } catch (error) { 554 | this.emit("error", error); 555 | /* v8 ignore next -- @preserve */ 556 | if (this._options.throwErrors) { 557 | /* v8 ignore next -- @preserve */ 558 | throw error; 559 | } 560 | } 561 | } 562 | 563 | /** 564 | * Load markdown content from a file synchronously. 565 | * @param {string} filePath The file path to load the markdown content from. 566 | * @returns {void} 567 | */ 568 | public loadFromFileSync(filePath: string): void { 569 | try { 570 | const data = { 571 | content: "", 572 | }; 573 | data.content = fs.readFileSync(filePath, "utf8"); 574 | 575 | this.hook(WritrHooks.loadFromFile, data); 576 | this._content = data.content; 577 | } catch (error) { 578 | this.emit("error", error); 579 | /* v8 ignore next -- @preserve */ 580 | if (this._options.throwErrors) { 581 | /* v8 ignore next -- @preserve */ 582 | throw error; 583 | } 584 | } 585 | } 586 | 587 | /** 588 | * Save the markdown content to a file. If the directory doesn't exist it will be created. 589 | * @param {string} filePath The file path to save the markdown content to. 590 | * @returns {Promise} 591 | */ 592 | public async saveToFile(filePath: string): Promise { 593 | try { 594 | const { writeFile, mkdir } = fs.promises; 595 | const directoryPath = dirname(filePath); 596 | await mkdir(directoryPath, { recursive: true }); 597 | const data = { 598 | filePath, 599 | content: this._content, 600 | }; 601 | await this.hook(WritrHooks.saveToFile, data); 602 | 603 | await writeFile(data.filePath, data.content); 604 | } catch (error) { 605 | /* v8 ignore next -- @preserve */ 606 | this.emit("error", error); 607 | /* v8 ignore next -- @preserve */ 608 | if (this._options.throwErrors) { 609 | /* v8 ignore next -- @preserve */ 610 | throw error; 611 | } 612 | } 613 | } 614 | 615 | /** 616 | * Save the markdown content to a file synchronously. If the directory doesn't exist it will be created. 617 | * @param {string} filePath The file path to save the markdown content to. 618 | * @returns {void} 619 | */ 620 | public saveToFileSync(filePath: string): void { 621 | try { 622 | const directoryPath = dirname(filePath); 623 | fs.mkdirSync(directoryPath, { recursive: true }); 624 | const data = { 625 | filePath, 626 | content: this._content, 627 | }; 628 | 629 | this.hook(WritrHooks.saveToFile, data); 630 | 631 | fs.writeFileSync(data.filePath, data.content); 632 | } catch (error) { 633 | /* v8 ignore next -- @preserve */ 634 | this.emit("error", error); 635 | /* v8 ignore next -- @preserve */ 636 | if (this._options.throwErrors) { 637 | /* v8 ignore next -- @preserve */ 638 | throw error; 639 | } 640 | } 641 | } 642 | 643 | public mergeOptions( 644 | current: WritrOptions, 645 | options: WritrOptions, 646 | ): WritrOptions { 647 | if (options.throwErrors !== undefined) { 648 | current.throwErrors = options.throwErrors; 649 | } 650 | 651 | /* v8 ignore next -- @preserve */ 652 | if (options.renderOptions) { 653 | current.renderOptions ??= {}; 654 | 655 | this.mergeRenderOptions(current.renderOptions, options.renderOptions); 656 | } 657 | 658 | return current; 659 | } 660 | 661 | private isCacheEnabled(options?: RenderOptions): boolean { 662 | if (options?.caching !== undefined) { 663 | return options.caching; 664 | } 665 | 666 | /* v8 ignore next -- @preserve */ 667 | return this._options?.renderOptions?.caching ?? false; 668 | } 669 | 670 | // biome-ignore lint/suspicious/noExplicitAny: expected unified processor 671 | private createProcessor(options: RenderOptions): any { 672 | const processor = unified().use(remarkParse); 673 | 674 | if (options.gfm) { 675 | processor.use(remarkGfm); 676 | processor.use(remarkGithubBlockquoteAlert); 677 | } 678 | 679 | if (options.toc) { 680 | processor.use(remarkToc, { heading: "toc|table of contents" }); 681 | } 682 | 683 | if (options.emoji) { 684 | processor.use(remarkEmoji); 685 | } 686 | 687 | processor.use(remarkRehype); 688 | 689 | if (options.slug) { 690 | processor.use(rehypeSlug); 691 | } 692 | 693 | if (options.highlight) { 694 | processor.use(rehypeHighlight); 695 | } 696 | 697 | if (options.math) { 698 | processor.use(remarkMath).use(rehypeKatex); 699 | } 700 | 701 | if (options.mdx) { 702 | processor.use(remarkMDX); 703 | } 704 | 705 | processor.use(rehypeStringify); 706 | 707 | return processor; 708 | } 709 | 710 | private mergeRenderOptions( 711 | current: RenderOptions, 712 | options: RenderOptions, 713 | ): RenderOptions { 714 | if (options.emoji !== undefined) { 715 | current.emoji = options.emoji; 716 | } 717 | 718 | if (options.toc !== undefined) { 719 | current.toc = options.toc; 720 | } 721 | 722 | if (options.slug !== undefined) { 723 | current.slug = options.slug; 724 | } 725 | 726 | if (options.highlight !== undefined) { 727 | current.highlight = options.highlight; 728 | } 729 | 730 | if (options.gfm !== undefined) { 731 | current.gfm = options.gfm; 732 | } 733 | 734 | if (options.math !== undefined) { 735 | current.math = options.math; 736 | } 737 | 738 | if (options.mdx !== undefined) { 739 | current.mdx = options.mdx; 740 | } 741 | 742 | if (options.caching !== undefined) { 743 | current.caching = options.caching; 744 | } 745 | 746 | return current; 747 | } 748 | } 749 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Writr](site/logo.svg) 2 | 3 | # Markdown Rendering Simplified 4 | [![tests](https://github.com/jaredwray/writr/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/writr/actions/workflows/tests.yml) 5 | [![GitHub license](https://img.shields.io/github/license/jaredwray/writr)](https://github.com/jaredwray/writr/blob/master/LICENSE) 6 | [![codecov](https://codecov.io/gh/jaredwray/writr/branch/master/graph/badge.svg?token=1YdMesM07X)](https://codecov.io/gh/jaredwray/writr) 7 | [![npm](https://img.shields.io/npm/dm/writr)](https://npmjs.com/package/writr) 8 | [![npm](https://img.shields.io/npm/v/writr)](https://npmjs.com/package/writr) 9 | 10 | # Features 11 | * Removes the remark / unified complexity and easy to use. 12 | * Powered by the [unified processor](https://github.com/unifiedjs/unified) for an extensible plugin pipeline. 13 | * Built in caching 💥 making it render very fast when there isn't a change 14 | * Frontmatter support built in by default. :tada: 15 | * Easily Render to `React` or `HTML`. 16 | * Generates a Table of Contents for your markdown files (remark-toc). 17 | * Slug generation for your markdown files (rehype-slug). 18 | * Code Highlighting (rehype-highlight). 19 | * Math Support (rehype-katex). 20 | * Markdown to HTML (rehype-stringify). 21 | * Github Flavor Markdown (remark-gfm). 22 | * Emoji Support (remark-emoji). 23 | * MDX Support (remark-mdx). 24 | * Built in Hooks for adding code to render pipeline. 25 | 26 | # Unified Processor Engine 27 | 28 | Writr builds on top of the open source [unified](https://github.com/unifiedjs/unified) processor – the core project that powers 29 | [remark](https://github.com/remarkjs/remark), [rehype](https://github.com/rehypejs/rehype), and many other content tools. Unified 30 | provides a pluggable pipeline where each plugin transforms a syntax tree. Writr configures a default set of plugins to turn 31 | Markdown into HTML, but you can access the processor through the `.engine` property to add your own behavior with 32 | `writr.engine.use(myPlugin)`. The [unified documentation](https://unifiedjs.com/) has more details and guides for building 33 | plugins and working with the processor directly. 34 | 35 | # Table of Contents 36 | - [Unified Processor Engine](#unified-processor-engine) 37 | - [Getting Started](#getting-started) 38 | - [API](#api) 39 | - [`new Writr(arg?: string | WritrOptions, options?: WritrOptions)`](#new-writrarg-string--writroptions-options-writroptions) 40 | - [`.content`](#content) 41 | - [`.body`](#body) 42 | - [`.options`](#options) 43 | - [`.frontmatter`](#frontmatter) 44 | - [`.frontMatterRaw`](#frontmatterraw) 45 | - [`.cache`](#cache) 46 | - [`.engine`](#engine) 47 | - [`.render(options?: RenderOptions)`](#renderoptions-renderoptions) 48 | - [`.renderSync(options?: RenderOptions)`](#rendersyncoptions-renderoptions) 49 | - [`.renderToFile(filePath: string, options?)`](#rendertofilefilepath-string-options-renderoptions) 50 | - [`.renderToFileSync(filePath: string, options?)`](#rendertofilesyncfilepath-string-options-renderoptions) 51 | - [`.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`](#renderreactoptions-renderoptions-reactoptions-htmlreactparseroptions) 52 | - [`.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`](#renderreactsync-options-renderoptions-reactoptions-htmlreactparseroptions) 53 | - [`.validate(content?: string, options?: RenderOptions)`](#validatecontent-string-options-renderoptions) 54 | - [`.validateSync(content?: string, options?: RenderOptions)`](#validatesynccontent-string-options-renderoptions) 55 | - [`.loadFromFile(filePath: string)`](#loadfromfilefilepath-string) 56 | - [`.loadFromFileSync(filePath: string)`](#loadfromfilesyncfilepath-string) 57 | - [`.saveToFile(filePath: string)`](#savetofilefilepath-string) 58 | - [`.saveToFileSync(filePath: string)`](#savetofilesyncfilepath-string) 59 | - [Caching On Render](#caching-on-render) 60 | - [GitHub Flavored Markdown (GFM)](#github-flavored-markdown-gfm) 61 | - [GFM Features](#gfm-features) 62 | - [Using GFM](#using-gfm) 63 | - [Disabling GFM](#disabling-gfm) 64 | - [Hooks](#hooks) 65 | - [Emitters](#emitters) 66 | - [Error Events](#error-events) 67 | - [Listening to Error Events](#listening-to-error-events) 68 | - [Methods that Emit Errors](#methods-that-emit-errors) 69 | - [Error Event Examples](#error-event-examples) 70 | - [Event Emitter Methods](#event-emitter-methods) 71 | - [ESM and Node Version Support](#esm-and-node-version-support) 72 | - [Code of Conduct and Contributing](#code-of-conduct-and-contributing) 73 | - [License](#license) 74 | 75 | # Getting Started 76 | 77 | ```bash 78 | > npm install writr 79 | ``` 80 | 81 | Then you can use it like this: 82 | 83 | ```javascript 84 | import { Writr } from 'writr'; 85 | 86 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 87 | 88 | const html = await writr.render(); //

Hello World 🙂

This is a test.

89 | ``` 90 | Its just that simple. Want to add some options? No problem. 91 | 92 | ```javascript 93 | import { Writr } from 'writr'; 94 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 95 | const options = { 96 | emoji: false 97 | } 98 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

99 | ``` 100 | 101 | An example passing in the options also via the constructor: 102 | 103 | ```javascript 104 | import { Writr, WritrOptions } from 'writr'; 105 | const writrOptions = { 106 | throwErrors: true, 107 | renderOptions: { 108 | emoji: true, 109 | toc: true, 110 | slug: true, 111 | highlight: true, 112 | gfm: true, 113 | math: true, 114 | mdx: true, 115 | caching: true, 116 | } 117 | }; 118 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, writrOptions); 119 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

120 | ``` 121 | 122 | # API 123 | 124 | ## `new Writr(arg?: string | WritrOptions, options?: WritrOptions)` 125 | 126 | By default the constructor takes in a markdown `string` or `WritrOptions` in the first parameter. You can also send in nothing and set the markdown via `.content` property. If you want to pass in your markdown and options you can easily do this with `new Writr('## Your Markdown Here', { ...options here})`. You can access the `WritrOptions` from the instance of Writr. Here is an example of WritrOptions. 127 | 128 | ```javascript 129 | import { Writr, WritrOptions } from 'writr'; 130 | const writrOptions = { 131 | throwErrors: true, 132 | renderOptions: { 133 | emoji: true, 134 | toc: true, 135 | slug: true, 136 | highlight: true, 137 | gfm: true, 138 | math: true, 139 | mdx: true, 140 | caching: true, 141 | } 142 | }; 143 | const writr = new Writr(writrOptions); 144 | ``` 145 | 146 | ## `.content` 147 | 148 | Setting the markdown content for the instance of Writr. This can be set via the constructor or directly on the instance and can even handle `frontmatter`. 149 | 150 | ```javascript 151 | 152 | import { Writr } from 'writr'; 153 | const writr = new Writr(); 154 | writr.content = `--- 155 | title: Hello World 156 | --- 157 | # Hello World ::-):\n\n This is a test.`; 158 | ``` 159 | 160 | ## `.body` 161 | 162 | gets the body of the markdown content. This is the content without the frontmatter. 163 | 164 | ```javascript 165 | import { Writr } from 'writr'; 166 | const writr = new Writr(); 167 | writr.content = `--- 168 | title: Hello World 169 | --- 170 | # Hello World ::-):\n\n This is a test.`; 171 | console.log(writr.body); // '# Hello World ::-):\n\n This is a test.' 172 | ``` 173 | 174 | ## `.options` 175 | 176 | Accessing the default options for this instance of Writr. Here is the default settings for `WritrOptions`. These are the default settings for the `WritrOptions`: 177 | 178 | ```javascript 179 | { 180 | throwErrors: false, 181 | renderOptions: { 182 | emoji: true, 183 | toc: true, 184 | slug: true, 185 | highlight: true, 186 | gfm: true, 187 | math: true, 188 | mdx: true, 189 | caching: false, 190 | } 191 | } 192 | ``` 193 | 194 | ## `.frontmatter` 195 | 196 | Accessing the frontmatter for this instance of Writr. This is a `Record` and can be set via the `.content` property. 197 | 198 | ```javascript 199 | import { Writr } from 'writr'; 200 | const writr = new Writr(); 201 | writr.content = `--- 202 | title: Hello World 203 | --- 204 | # Hello World ::-):\n\n This is a test.`; 205 | console.log(writr.frontmatter); // { title: 'Hello World' } 206 | ``` 207 | 208 | you can also set the front matter directly like this: 209 | 210 | ```javascript 211 | import { Writr } from 'writr'; 212 | const writr = new Writr(); 213 | writr.frontmatter = { title: 'Hello World' }; 214 | ``` 215 | 216 | ## `.frontMatterRaw` 217 | 218 | Accessing the raw frontmatter for this instance of Writr. This is a `string` and can be set via the `.content` property. 219 | 220 | ```javascript 221 | import { Writr } from 'writr'; 222 | const writr = new Writr(); 223 | writr.content = `--- 224 | title: Hello World 225 | --- 226 | # Hello World ::-):\n\n This is a test.`; 227 | console.log(writr.frontMatterRaw); // '---\ntitle: Hello World\n---' 228 | ``` 229 | 230 | ## `.cache` 231 | 232 | Accessing the cache for this instance of Writr. By default this is an in memory cache and is disabled (set to false) by default. You can enable this by setting `caching: true` in the `RenderOptions` of the `WritrOptions` or when calling render passing the `RenderOptions` like here: 233 | 234 | ```javascript 235 | import { Writr } from 'writr'; 236 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 237 | const options = { 238 | caching: true 239 | } 240 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

241 | ``` 242 | 243 | 244 | ## `.engine` 245 | 246 | Accessing the underlying engine for this instance of Writr. This is a `Processor` from the core [`unified`](https://github.com/unifiedjs/unified) project and uses the familiar `.use()` plugin pattern. You can chain additional unified plugins on this processor to customize the render pipeline. Learn more about the unified engine at [unifiedjs.com](https://unifiedjs.com/) and check out the [getting started guide](https://unifiedjs.com/learn/guide/using-unified/) for examples. 247 | 248 | 249 | ## `.render(options?: RenderOptions)` 250 | 251 | Rendering markdown to HTML. the options are based on RenderOptions. Which you can access from the Writr instance. 252 | 253 | ```javascript 254 | import { Writr } from 'writr'; 255 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 256 | const html = await writr.render(); //

Hello World 🙂

This is a test.

257 | 258 | //passing in with render options 259 | const options = { 260 | emoji: false 261 | } 262 | 263 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

264 | ``` 265 | 266 | ## `.renderSync(options?: RenderOptions)` 267 | 268 | Rendering markdown to HTML synchronously. the options are based on RenderOptions. Which you can access from the Writr instance. The parameters are the same as the `.render()` function. 269 | 270 | ```javascript 271 | import { Writr } from 'writr'; 272 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 273 | const html = writr.renderSync(); //

Hello World 🙂

This is a test.

274 | ``` 275 | 276 | ## `.renderToFile(filePath: string, options?: RenderOptions)` 277 | 278 | Rendering markdown to a file. The options are based on RenderOptions. 279 | 280 | ```javascript 281 | import { Writr } from 'writr'; 282 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 283 | await writr.renderToFile('path/to/file.html'); 284 | ``` 285 | 286 | ## `.renderToFileSync(filePath: string, options?: RenderOptions)` 287 | 288 | Rendering markdown to a file synchronously. The options are based on RenderOptions. 289 | 290 | ```javascript 291 | import { Writr } from 'writr'; 292 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 293 | writr.renderToFileSync('path/to/file.html'); 294 | ``` 295 | 296 | ## `.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions)` 297 | 298 | Rendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`. 299 | 300 | ```javascript 301 | import { Writr } from 'writr'; 302 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 303 | const reactElement = await writr.renderReact(); // Will return a React.JSX.Element 304 | ``` 305 | 306 | ## `.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions)` 307 | 308 | Rendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`. 309 | 310 | ```javascript 311 | import { Writr } from 'writr'; 312 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 313 | const reactElement = writr.renderReactSync(); // Will return a React.JSX.Element 314 | ``` 315 | 316 | ## `.validate(content?: string, options?: RenderOptions)` 317 | 318 | Validate markdown content by attempting to render it. Returns a `WritrValidateResult` object with a `valid` boolean and optional `error` property. Note that this will disable caching on render to ensure accurate validation. 319 | 320 | ```javascript 321 | import { Writr } from 'writr'; 322 | const writr = new Writr(`# Hello World\n\nThis is a test.`); 323 | 324 | // Validate current content 325 | const result = await writr.validate(); 326 | console.log(result.valid); // true 327 | 328 | // Validate external content without changing the instance 329 | const externalResult = await writr.validate('## Different Content'); 330 | console.log(externalResult.valid); // true 331 | console.log(writr.content); // Still "# Hello World\n\nThis is a test." 332 | 333 | // Handle validation errors 334 | const invalidWritr = new Writr('Put invalid markdown here'); 335 | const errorResult = await invalidWritr.validate(); 336 | console.log(errorResult.valid); // false 337 | console.log(errorResult.error?.message); // "Failed to render markdown: Invalid plugin" 338 | ``` 339 | 340 | ## `.validateSync(content?: string, options?: RenderOptions)` 341 | 342 | Synchronously validate markdown content by attempting to render it. Returns a `WritrValidateResult` object with a `valid` boolean and optional `error` property. 343 | 344 | This is the synchronous version of `.validate()` with the same parameters and behavior. 345 | 346 | ```javascript 347 | import { Writr } from 'writr'; 348 | const writr = new Writr(`# Hello World\n\nThis is a test.`); 349 | 350 | // Validate current content synchronously 351 | const result = writr.validateSync(); 352 | console.log(result.valid); // true 353 | 354 | // Validate external content without changing the instance 355 | const externalResult = writr.validateSync('## Different Content'); 356 | console.log(externalResult.valid); // true 357 | console.log(writr.content); // Still "# Hello World\n\nThis is a test." 358 | ``` 359 | 360 | ## `.loadFromFile(filePath: string)` 361 | 362 | Load your markdown content from a file path. 363 | 364 | ```javascript 365 | import { Writr } from 'writr'; 366 | const writr = new Writr(); 367 | await writr.loadFromFile('path/to/file.md'); 368 | ``` 369 | 370 | ## `.loadFromFileSync(filePath: string)` 371 | 372 | Load your markdown content from a file path synchronously. 373 | 374 | ```javascript 375 | import { Writr } from 'writr'; 376 | const writr = new Writr(); 377 | writr.loadFromFileSync('path/to/file.md'); 378 | ``` 379 | 380 | ## `.saveToFile(filePath: string)` 381 | 382 | Save your markdown and frontmatter (if included) content to a file path. 383 | 384 | ```javascript 385 | import { Writr } from 'writr'; 386 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 387 | await writr.saveToFile('path/to/file.md'); 388 | ``` 389 | 390 | ## `.saveToFileSync(filePath: string)` 391 | 392 | Save your markdown and frontmatter (if included) content to a file path synchronously. 393 | 394 | ```javascript 395 | import { Writr } from 'writr'; 396 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 397 | writr.saveToFileSync('path/to/file.md'); 398 | ``` 399 | 400 | # Caching On Render 401 | 402 | Caching is built into Writr and is an in-memory cache using `CacheableMemory` from [Cacheable](https://cacheable.org). It is turned off by default and can be enabled by setting `caching: true` in the `RenderOptions` of the `WritrOptions` or when calling render passing the `RenderOptions` like here: 403 | 404 | ```javascript 405 | import { Writr } from 'writr'; 406 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, { renderOptions: { caching: true } }); 407 | ``` 408 | 409 | or via `RenderOptions` such as: 410 | 411 | ```javascript 412 | import { Writr } from 'writr'; 413 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 414 | await writr.render({ caching: true}); 415 | ``` 416 | 417 | If you want to set the caching options for the instance of Writr you can do so like this: 418 | 419 | ```javascript 420 | // we will set the lruSize of the cache and the default ttl 421 | import {Writr} from 'writr'; 422 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, { renderOptions: { caching: true } }); 423 | writr.cache.store.lruSize = 100; 424 | writr.cache.store.ttl = '5m'; // setting it to 5 minutes 425 | ``` 426 | 427 | # GitHub Flavored Markdown (GFM) 428 | 429 | Writr includes full support for [GitHub Flavored Markdown](https://github.github.com/gfm/) (GFM) through the `remark-gfm` and `remark-github-blockquote-alert` plugins. GFM is enabled by default and adds several powerful features to standard Markdown. 430 | 431 | ## GFM Features 432 | 433 | When GFM is enabled (which it is by default), you get access to the following features: 434 | 435 | ### Tables 436 | 437 | Create tables using pipes and hyphens: 438 | 439 | ```markdown 440 | | Feature | Supported | 441 | |---------|-----------| 442 | | Tables | Yes | 443 | | Alerts | Yes | 444 | ``` 445 | 446 | ### Strikethrough 447 | 448 | Use `~~` to create strikethrough text: 449 | 450 | ```markdown 451 | ~~This text is crossed out~~ 452 | ``` 453 | 454 | ### Task Lists 455 | 456 | Create interactive checkboxes: 457 | 458 | ```markdown 459 | - [x] Completed task 460 | - [ ] Incomplete task 461 | - [ ] Another task 462 | ``` 463 | 464 | ### Autolinks 465 | 466 | URLs are automatically converted to clickable links: 467 | 468 | ```markdown 469 | https://github.com 470 | ``` 471 | 472 | ### GitHub Blockquote Alerts 473 | 474 | GitHub-style alerts are supported to emphasize critical information. These are blockquote-based admonitions that render with special styling: 475 | 476 | ```markdown 477 | > [!NOTE] 478 | > Useful information that users should know, even when skimming content. 479 | 480 | > [!TIP] 481 | > Helpful advice for doing things better or more easily. 482 | 483 | > [!IMPORTANT] 484 | > Key information users need to know to achieve their goal. 485 | 486 | > [!WARNING] 487 | > Urgent info that needs immediate user attention to avoid problems. 488 | 489 | > [!CAUTION] 490 | > Advises about risks or negative outcomes of certain actions. 491 | ``` 492 | 493 | ## Using GFM 494 | 495 | GFM is enabled by default. Here's an example: 496 | 497 | ```javascript 498 | import { Writr } from 'writr'; 499 | 500 | const markdown = ` 501 | # Task List Example 502 | 503 | - [x] Learn Writr basics 504 | - [ ] Master GFM features 505 | 506 | > [!NOTE] 507 | > GitHub Flavored Markdown is enabled by default! 508 | 509 | | Feature | Status | 510 | |---------|--------| 511 | | GFM | ✓ | 512 | `; 513 | 514 | const writr = new Writr(markdown); 515 | const html = await writr.render(); // Renders with full GFM support 516 | ``` 517 | 518 | ## Disabling GFM 519 | 520 | If you need to disable GFM features, you can set `gfm: false` in the render options: 521 | 522 | ```javascript 523 | import { Writr } from 'writr'; 524 | 525 | const writr = new Writr('~~strikethrough~~ text'); 526 | 527 | // Disable GFM 528 | const html = await writr.render({ gfm: false }); 529 | // Output:

~~strikethrough~~ text

530 | 531 | // With GFM enabled (default) 532 | const htmlWithGfm = await writr.render({ gfm: true }); 533 | // Output:

strikethrough text

534 | ``` 535 | 536 | Note: When GFM is disabled, GitHub blockquote alerts will not be processed and will render as regular blockquotes. 537 | 538 | # Hooks 539 | 540 | Hooks are a way to add additional parsing to the render pipeline. You can add hooks to the the Writr instance. Here is an example of adding a hook to the instance of Writr: 541 | 542 | ```javascript 543 | import { Writr, WritrHooks } from 'writr'; 544 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 545 | writr.onHook(WritrHooks.beforeRender, data => { 546 | data.body = 'Hello, Universe!'; 547 | }); 548 | const result = await writr.render(); 549 | console.log(result); // Hello, Universe! 550 | ``` 551 | 552 | For `beforeRender` the data object is a `renderData` object. Here is the interface for `renderData`: 553 | 554 | ```typescript 555 | export type renderData = { 556 | body: string 557 | options: RenderOptions; 558 | } 559 | ``` 560 | 561 | For `afterRender` the data object is a `resultData` object. Here is the interface for `resultData`: 562 | 563 | ```typescript 564 | export type resultData = { 565 | result: string; 566 | } 567 | ``` 568 | 569 | For `saveToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `saveToFileData`: 570 | 571 | ```typescript 572 | export type saveToFileData = { 573 | filePath: string; 574 | content: string; 575 | } 576 | ``` 577 | 578 | This is called when you call `saveToFile`, `saveToFileSync`. 579 | 580 | For `renderToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `renderToFileData`: 581 | 582 | ```typescript 583 | export type renderToFileData = { 584 | filePath: string; 585 | content: string; 586 | } 587 | ``` 588 | 589 | This is called when you call `renderToFile`, `renderToFileSync`. 590 | 591 | For `loadFromFile` the data object is an object with `content` so you can change before it is set on `writr.content`. Here is the interface for `loadFromFileData`: 592 | 593 | ```typescript 594 | export type loadFromFileData = { 595 | content: string; 596 | } 597 | ``` 598 | 599 | This is called when you call `loadFromFile`, `loadFromFileSync`. 600 | 601 | # Emitters 602 | 603 | Writr extends the [Hookified](https://github.com/jaredwray/hookified) class, which provides event emitter capabilities. This means you can listen to events emitted by Writr during its lifecycle, particularly error events. 604 | 605 | ## Error Events 606 | 607 | Writr emits an `error` event whenever an error occurs in any of its methods. This provides a centralized way to handle errors without wrapping every method call in a try/catch block. 608 | 609 | ### Listening to Error Events 610 | 611 | You can listen to error events using the `.on()` method: 612 | 613 | ```javascript 614 | import { Writr } from 'writr'; 615 | 616 | const writr = new Writr('# Hello World'); 617 | 618 | // Listen for any errors 619 | writr.on('error', (error) => { 620 | console.error('An error occurred:', error.message); 621 | // Handle the error appropriately 622 | // Log to error tracking service, display to user, etc. 623 | }); 624 | 625 | // Now when any error occurs, your listener will be notified 626 | try { 627 | await writr.render(); 628 | } catch (error) { 629 | // Error is also thrown, so you can handle it here too 630 | } 631 | ``` 632 | 633 | ### Methods that Emit Errors 634 | 635 | The following methods emit error events when they fail: 636 | 637 | **Rendering Methods:** 638 | - `render()` - Emits error before throwing when markdown rendering fails 639 | - `renderSync()` - Emits error before throwing when markdown rendering fails 640 | - `renderReact()` - Emits error before throwing when React rendering fails 641 | - `renderReactSync()` - Emits error before throwing when React rendering fails 642 | 643 | **Validation Methods:** 644 | - `validate()` - Emits error when validation fails (returns error in result object) 645 | - `validateSync()` - Emits error when validation fails (returns error in result object) 646 | 647 | **File Operations:** 648 | - `renderToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`) 649 | - `renderToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`) 650 | - `loadFromFile()` - Emits error when file reading fails (does not throw if `throwErrors: false`) 651 | - `loadFromFileSync()` - Emits error when file reading fails (does not throw if `throwErrors: false`) 652 | - `saveToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`) 653 | - `saveToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`) 654 | 655 | **Front Matter Operations:** 656 | - `frontMatter` getter - Emits error when YAML parsing fails 657 | - `frontMatter` setter - Emits error when YAML serialization fails 658 | 659 | ### Error Event Examples 660 | 661 | **Example 1: Global Error Handler** 662 | 663 | ```javascript 664 | import { Writr } from 'writr'; 665 | 666 | const writr = new Writr(); 667 | 668 | // Set up a global error handler 669 | writr.on('error', (error) => { 670 | // Log to your monitoring service 671 | console.error('Writr error:', error); 672 | 673 | // Send to error tracking (e.g., Sentry, Rollbar) 674 | // errorTracker.captureException(error); 675 | }); 676 | 677 | // All errors will be emitted to the listener above 678 | await writr.loadFromFile('./content.md'); 679 | const html = await writr.render(); 680 | ``` 681 | 682 | **Example 2: Validation with Error Listening** 683 | 684 | ```javascript 685 | import { Writr } from 'writr'; 686 | 687 | const writr = new Writr('# My Content'); 688 | let lastError = null; 689 | 690 | writr.on('error', (error) => { 691 | lastError = error; 692 | }); 693 | 694 | const result = await writr.validate(); 695 | 696 | if (!result.valid) { 697 | console.log('Validation failed'); 698 | console.log('Error details:', lastError); 699 | // result.error is also available 700 | } 701 | ``` 702 | 703 | **Example 3: File Operations Without Try/Catch** 704 | 705 | ```javascript 706 | import { Writr } from 'writr'; 707 | 708 | const writr = new Writr('# Content', { throwErrors: false }); 709 | 710 | writr.on('error', (error) => { 711 | console.error('File operation failed:', error.message); 712 | // Handle gracefully - maybe use default content 713 | }); 714 | 715 | // Won't throw, but will emit error event if file doesn't exist 716 | await writr.loadFromFile('./maybe-missing.md'); 717 | ``` 718 | 719 | ### Event Emitter Methods 720 | 721 | Since Writr extends Hookified, you have access to standard event emitter methods: 722 | 723 | - `writr.on(event, handler)` - Add an event listener 724 | - `writr.once(event, handler)` - Add a one-time event listener 725 | - `writr.off(event, handler)` - Remove an event listener 726 | - `writr.emit(event, data)` - Emit an event (used internally) 727 | 728 | For more information about event handling capabilities, see the [Hookified documentation](https://github.com/jaredwray/hookified). 729 | 730 | # ESM and Node Version Support 731 | 732 | This package is ESM only and tested on the current lts version and its previous. Please don't open issues for questions regarding CommonJS / ESM or previous Nodejs versions. 733 | 734 | # Code of Conduct and Contributing 735 | Please use our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing](CONTRIBUTING.md) guidelines for development and testing. We appreciate your contributions! 736 | 737 | # License 738 | 739 | [MIT](LICENSE) & © [Jared Wray](https://jaredwray.com) 740 | --------------------------------------------------------------------------------