├── .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('', '');
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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
13 | [](https://codecov.io/gh/jaredwray/keyv)
14 | [](https://github.com/jaredwray/keyv/blob/master/LICENSE)
15 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/offline)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/sqlite)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/mongo)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/mysql)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/btestsuild.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/postgres)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/tiered)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/test-suite)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
12 | [](https://codecov.io/gh/jaredwray/keyv)
13 | [](https://www.npmjs.com/package/@keyv/redis)
14 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
13 | [](https://codecov.io/gh/jaredwray/keyv)
14 | [](https://github.com/jaredwray/keyv/blob/master/LICENSE)
15 | [](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 | [](https://github.com/jaredwray/docula/actions/workflows/tests.yaml)
6 | [](https://github.com/jaredwray/docula/blob/master/LICENSE)
7 | [](https://codecov.io/gh/jaredwray/docula)
8 | [](https://npmjs.com/package/docula)
9 | [](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 | 
2 |
3 | # Beautiful Website for Your Projects
4 |
5 | [](https://github.com/jaredwray/docula/actions/workflows/tests.yaml)
6 | [](https://github.com/jaredwray/docula/blob/master/LICENSE)
7 | [](https://codecov.io/gh/jaredwray/docula)
8 | [](https://npmjs.com/package/docula)
9 | [](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 | [](https://github.com/jaredwray/docula/actions/workflows/tests.yml)
4 | [](https://github.com/jaredwray/docula/blob/master/LICENSE)
5 | [](https://codecov.io/gh/jaredwray/docula)
6 | [](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 | [](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
11 | [](https://codecov.io/gh/jaredwray/keyv)
12 | [](https://www.npmjs.com/package/keyv)
13 | [](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 | 
2 |
3 | # Markdown Rendering Simplified
4 | [](https://github.com/jaredwray/writr/actions/workflows/tests.yml)
5 | [](https://github.com/jaredwray/writr/blob/master/LICENSE)
6 | [](https://codecov.io/gh/jaredwray/writr)
7 | [](https://npmjs.com/package/writr)
8 | [](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 |
--------------------------------------------------------------------------------