├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── code-coverage.yml │ ├── code-ql.yml │ ├── deploy-site.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── package.json ├── pnpm-workspace.yaml ├── site ├── docula.config.mjs ├── favicon.ico ├── logo.svg └── variables.css ├── src ├── writr-cache.ts └── writr.ts ├── test ├── content-fixtures.ts ├── fixtures │ └── load-from-file.md ├── writr-cache.test.ts ├── writr-hooks.test.ts ├── writr-render.test.ts └── writr.test.ts ├── tsconfig.json └── vitest.config.mjs /.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. -------------------------------------------------------------------------------- /.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/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Please check if the PR fulfills these requirements** 2 | - [ ] Followed the [Contributing](https://github.com/jaredwray/writr/blob/main/CONTRIBUTING.md) guidelines. 3 | - [ ] Tests for the changes have been added (for bug fixes/features) with 100% code coverage. 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | 6 | **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 7 | -------------------------------------------------------------------------------- /.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 33 | 34 | - name: Code Coverage 35 | uses: codecov/codecov-action@v4 36 | with: 37 | token: ${{ secrets.CODECOV_KEY }} 38 | files: coverage/coverage-final.json 39 | 40 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | 11 | jobs: 12 | release: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js 22 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | 23 | - name: Install Dependencies 24 | run: npm install -g pnpm && pnpm install 25 | 26 | - name: Build 27 | run: pnpm build 28 | 29 | - name: Testing 30 | run: pnpm test 31 | 32 | - name: Code Coverage 33 | uses: codecov/codecov-action@v3 34 | with: 35 | token: ${{ secrets.CODECOV_KEY }} 36 | files: coverage/coverage-final.json 37 | 38 | - name: Publish 39 | run: | 40 | npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} 41 | npm publish --ignore-scripts 42 | env: 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | 45 | -------------------------------------------------------------------------------- /.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 37 | 38 | -------------------------------------------------------------------------------- /.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/README.md 71 | site-output 72 | 73 | # IDEs 74 | .idea/ 75 | pnpm-lock.yaml 76 | .pnp.* 77 | .yarn 78 | bun.lockb 79 | site/dist 80 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Writr](site/logo.svg) 2 | 3 | # Markdown Rendering Simplified 4 | [![tests](https://github.com/jaredwray/writr/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/writr/actions/workflows/tests.yml) 5 | [![GitHub license](https://img.shields.io/github/license/jaredwray/writr)](https://github.com/jaredwray/writr/blob/master/LICENSE) 6 | [![codecov](https://codecov.io/gh/jaredwray/writr/branch/master/graph/badge.svg?token=1YdMesM07X)](https://codecov.io/gh/jaredwray/writr) 7 | [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/writr/badge)](https://www.jsdelivr.com/package/npm/writr) 8 | [![npm](https://img.shields.io/npm/dm/writr)](https://npmjs.com/package/writr) 9 | [![npm](https://img.shields.io/npm/v/writr)](https://npmjs.com/package/writr) 10 | 11 | # Features 12 | * Removes the remark / unified complexity and easy to use. 13 | * Built in caching 💥 making it render very fast when there isnt 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 | # Table of Contents 27 | - [Getting Started](#getting-started) 28 | - [Running via Browser as an ESM Module](#running-via-browser-as-an-esm-module) 29 | - [API](#api) 30 | - [`new Writr(arg?: string | WritrOptions, options?: WritrOptions)`](#new-writrarg-string--writroptions-options-writroptions) 31 | - [`.content`](#content) 32 | - [`.body`](#body) 33 | - [`.options`](#options) 34 | - [`.frontmatter`](#frontmatter) 35 | - [`.frontMatterRaw`](#frontmatterraw) 36 | - [`.cache`](#cache) 37 | - [`.engine`](#engine) 38 | - [`.render(options?: RenderOptions): Promise`](#renderoptions-renderoptions-promisestring) 39 | - [`.renderSync(options?: RenderOptions): string`](#rendersyncoptions-renderoptions-string) 40 | - [`.renderToFile(filePath: string, options?: RenderOptions)`](#rendertofilefilepath-string-options-renderoptions) 41 | - [`.renderToFileSync(filePath: string, options?: RenderOptions): void`](#rendertofilesyncfilepath-string-options-renderoptions-void) 42 | - [`.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions): Promise`](#renderreactoptions-renderoptions-reactoptions-htmlreactparseroptions-promise-reactjsxelement-) 43 | - [`.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions): React.JSX.Element`](#renderreactsync-options-renderoptions-reactoptions-htmlreactparseroptions-reactjsxelement) 44 | - [`.loadFromFile(filePath: string): Promise`](#loadfromfilefilepath-string-promisevoid) 45 | - [`.loadFromFileSync(filePath: string): void`](#loadfromfilesyncfilepath-string-void) 46 | - [`.saveToFile(filePath: string): Promise`](#savetofilefilepath-string-promisevoid) 47 | - [`.saveToFileSync(filePath: string): void`](#savetofilesyncfilepath-string-void) 48 | - [Hooks](#hooks) 49 | - [ESM and Node Version Support](#esm-and-node-version-support) 50 | - [Code of Conduct and Contributing](#code-of-conduct-and-contributing) 51 | - [License](#license) 52 | 53 | # Getting Started 54 | 55 | ```bash 56 | > npm install writr 57 | ``` 58 | 59 | Then you can use it like this: 60 | 61 | ```javascript 62 | import { Writr } from 'writr'; 63 | 64 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 65 | 66 | const html = await writr.render(); //

Hello World 🙂

This is a test.

67 | ``` 68 | Its just that simple. Want to add some options? No problem. 69 | 70 | ```javascript 71 | import { Writr } from 'writr'; 72 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 73 | const options = { 74 | emoji: false 75 | } 76 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

77 | ``` 78 | 79 | An example passing in the options also via the constructor: 80 | 81 | ```javascript 82 | import { Writr, WritrOptions } from 'writr'; 83 | const writrOptions = { 84 | throwErrors: true, 85 | renderOptions: { 86 | emoji: true, 87 | toc: true, 88 | slug: true, 89 | highlight: true, 90 | gfm: true, 91 | math: true, 92 | mdx: true, 93 | caching: true, 94 | } 95 | }; 96 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, writrOptions); 97 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

98 | ``` 99 | 100 | # Running via Browser as an ESM Module 101 | 102 | You can also run Writr via the browser. Here is an example of how to do that. 103 | 104 | ```html 105 | 106 | 107 | 108 | 109 | 110 | Writr Example 111 | 119 | 120 | 121 | 122 | 123 | ``` 124 | 125 | This will render the markdown to HTML and display it in the body of the page. 126 | 127 | # API 128 | 129 | ## `new Writr(arg?: string | WritrOptions, options?: WritrOptions)` 130 | 131 | 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. 132 | 133 | ```javascript 134 | import { Writr, WritrOptions } from 'writr'; 135 | const writrOptions = { 136 | throwErrors: true, 137 | renderOptions: { 138 | emoji: true, 139 | toc: true, 140 | slug: true, 141 | highlight: true, 142 | gfm: true, 143 | math: true, 144 | mdx: true, 145 | caching: true, 146 | } 147 | }; 148 | const writr = new Writr(writrOptions); 149 | ``` 150 | 151 | ## `.content` 152 | 153 | 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`. 154 | 155 | ```javascript 156 | 157 | import { Writr } from 'writr'; 158 | const writr = new Writr(); 159 | writr.content = `--- 160 | title: Hello World 161 | --- 162 | # Hello World ::-):\n\n This is a test.`; 163 | ``` 164 | 165 | ## `.body` 166 | 167 | gets the body of the markdown content. This is the content without the frontmatter. 168 | 169 | ```javascript 170 | import { Writr } from 'writr'; 171 | const writr = new Writr(); 172 | writr.content = `--- 173 | title: Hello World 174 | --- 175 | # Hello World ::-):\n\n This is a test.`; 176 | console.log(writr.body); // '# Hello World ::-):\n\n This is a test.' 177 | ``` 178 | 179 | ## `.options` 180 | 181 | Accessing the default options for this instance of Writr. Here is the default settings for `WritrOptions`. 182 | 183 | ```javascript 184 | { 185 | throwErrors: false, 186 | renderOptions: { 187 | emoji: true, 188 | toc: false, 189 | slug: false, 190 | highlight: false, 191 | gfm: true, 192 | math: false, 193 | mdx: false, 194 | caching: false, 195 | } 196 | } 197 | ``` 198 | 199 | ## `.frontmatter` 200 | 201 | Accessing the frontmatter for this instance of Writr. This is a `Record` and can be set via the `.content` property. 202 | 203 | ```javascript 204 | import { Writr } from 'writr'; 205 | const writr = new Writr(); 206 | writr.content = `--- 207 | title: Hello World 208 | --- 209 | # Hello World ::-):\n\n This is a test.`; 210 | console.log(writr.frontmatter); // { title: 'Hello World' } 211 | ``` 212 | 213 | you can also set the front matter directly like this: 214 | 215 | ```javascript 216 | import { Writr } from 'writr'; 217 | const writr = new Writr(); 218 | writr.frontmatter = { title: 'Hello World' }; 219 | ``` 220 | 221 | ## `.frontMatterRaw` 222 | 223 | Accessing the raw frontmatter for this instance of Writr. This is a `string` and can be set via the `.content` property. 224 | 225 | ```javascript 226 | import { Writr } from 'writr'; 227 | const writr = new Writr(); 228 | writr.content = `--- 229 | title: Hello World 230 | --- 231 | # Hello World ::-):\n\n This is a test.`; 232 | console.log(writr.frontMatterRaw); // '---\ntitle: Hello World\n---' 233 | ``` 234 | 235 | ## `.cache` 236 | 237 | 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: 238 | 239 | ```javascript 240 | import { Writr } from 'writr'; 241 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 242 | const options = { 243 | caching: true 244 | } 245 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

246 | ``` 247 | 248 | 249 | ## `.engine` 250 | 251 | Accessing the underlying engine for this instance of Writr. This is a `Processor` fro the unified `.use()` function. You can use this to add additional plugins to the engine. 252 | 253 | ## `.render(options?: RenderOptions): Promise` 254 | 255 | Rendering markdown to HTML. the options are based on RenderOptions. Which you can access from the Writr instance. 256 | 257 | ```javascript 258 | import { Writr } from 'writr'; 259 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 260 | const html = await writr.render(); //

Hello World 🙂

This is a test.

261 | 262 | //passing in with render options 263 | const options = { 264 | emoji: false 265 | } 266 | 267 | const html = await writr.render(options); //

Hello World ::-):

This is a test.

268 | ``` 269 | 270 | ## `.renderSync(options?: RenderOptions): string` 271 | 272 | 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. 273 | 274 | ```javascript 275 | import { Writr } from 'writr'; 276 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 277 | const html = writr.renderSync(); //

Hello World 🙂

This is a test.

278 | ``` 279 | 280 | ## '.renderToFile(filePath: string, options?: RenderOptions)' 281 | 282 | Rendering markdown to a file. The options are based on RenderOptions. 283 | 284 | ```javascript 285 | import { Writr } from 'writr'; 286 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 287 | await writr.renderToFile('path/to/file.html'); 288 | ``` 289 | 290 | ## '.renderToFileSync(filePath: string, options?: RenderOptions): void' 291 | 292 | Rendering markdown to a file synchronously. The options are based on RenderOptions. 293 | 294 | ```javascript 295 | import { Writr } from 'writr'; 296 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 297 | writr.renderToFileSync('path/to/file.html'); 298 | ``` 299 | 300 | ## '.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions): Promise' 301 | 302 | Rendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`. 303 | 304 | ```javascript 305 | import { Writr } from 'writr'; 306 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 307 | const reactElement = await writr.renderReact(); // Will return a React.JSX.Element 308 | ``` 309 | 310 | ## '.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions): React.JSX.Element' 311 | 312 | Rendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`. 313 | 314 | ```javascript 315 | import { Writr } from 'writr'; 316 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 317 | const reactElement = writr.renderReactSync(); // Will return a React.JSX.Element 318 | ``` 319 | 320 | ## `.loadFromFile(filePath: string): Promise` 321 | 322 | Load your markdown content from a file path. 323 | 324 | ```javascript 325 | import { Writr } from 'writr'; 326 | const writr = new Writr(); 327 | await writr.loadFromFile('path/to/file.md'); 328 | ``` 329 | 330 | ## `.loadFromFileSync(filePath: string): void` 331 | 332 | Load your markdown content from a file path synchronously. 333 | 334 | ## `.saveToFile(filePath: string): Promise` 335 | 336 | Save your markdown and frontmatter (if included) content to a file path. 337 | 338 | ```javascript 339 | import { Writr } from 'writr'; 340 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 341 | await writr.saveToFile('path/to/file.md'); 342 | ``` 343 | 344 | ## `.saveToFileSync(filePath: string): void` 345 | 346 | Save your markdown and frontmatter (if included) content to a file path synchronously. 347 | 348 | # Caching On Render 349 | 350 | 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: 351 | 352 | ```javascript 353 | import { Writr } from 'writr'; 354 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, { renderOptions: { caching: true } }); 355 | ``` 356 | 357 | or via `RenderOptions` such as: 358 | 359 | ```javascript 360 | import { Writr } from 'writr'; 361 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 362 | await writr.render({ caching: true}); 363 | ``` 364 | 365 | If you want to set the caching options for the instance of Writr you can do so like this: 366 | 367 | ```javascript 368 | // we will set the lruSize of the cache and the default ttl 369 | import {Writr} from 'writr'; 370 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, { renderOptions: { caching: true } }); 371 | writr.cache.store.lruSize = 100; 372 | writr.cache.store.ttl = '5m'; // setting it to 5 minutes 373 | ``` 374 | 375 | # Hooks 376 | 377 | 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: 378 | 379 | ```javascript 380 | import { Writr, WritrHooks } from 'writr'; 381 | const writr = new Writr(`# Hello World ::-):\n\n This is a test.`); 382 | writr.onHook(WritrHooks.beforeRender, data => { 383 | data.body = 'Hello, Universe!'; 384 | }); 385 | const result = await writr.render(); 386 | console.log(result); // Hello, Universe! 387 | ``` 388 | 389 | For `beforeRender` the data object is a `renderData` object. Here is the interface for `renderData`: 390 | 391 | ```typescript 392 | export type renderData = { 393 | body: string 394 | options: RenderOptions; 395 | } 396 | ``` 397 | 398 | For `afterRender` the data object is a `resultData` object. Here is the interface for `resultData`: 399 | 400 | ```typescript 401 | export type resultData = { 402 | result: string; 403 | } 404 | ``` 405 | 406 | For `saveToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `saveToFileData`: 407 | 408 | ```typescript 409 | export type saveToFileData = { 410 | filePath: string; 411 | content: string; 412 | } 413 | ``` 414 | 415 | This is called when you call `saveToFile`, `saveToFileSync`. 416 | 417 | For `renderToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `renderToFileData`: 418 | 419 | ```typescript 420 | export type renderToFileData = { 421 | filePath: string; 422 | content: string; 423 | } 424 | ``` 425 | 426 | This is called when you call `renderToFile`, `renderToFileSync`. 427 | 428 | 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`: 429 | 430 | ```typescript 431 | export type loadFromFileData = { 432 | content: string; 433 | } 434 | ``` 435 | 436 | This is called when you call `loadFromFile`, `loadFromFileSync`. 437 | 438 | # ESM and Node Version Support 439 | 440 | 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. To learn more about using ESM please read this from `sindresorhus`: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 441 | 442 | # Code of Conduct and Contributing 443 | [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing](CONTRIBUTING.md) guidelines. 444 | 445 | # License 446 | 447 | [MIT](LICENSE) & © [Jared Wray](https://jaredwray.com) 448 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "writr", 3 | "version": "4.4.4", 4 | "description": "Markdown Rendering Simplified", 5 | "type": "module", 6 | "main": "./dist/writr.js", 7 | "types": "./dist/writr.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/writr.js" 11 | } 12 | }, 13 | "repository": "https://github.com/jaredwray/writr.git", 14 | "author": "Jared Wray ", 15 | "engines": { 16 | "node": ">=20" 17 | }, 18 | "license": "MIT", 19 | "keywords": [ 20 | "markdown", 21 | "html", 22 | "renderer", 23 | "markdown-to-html", 24 | "toc", 25 | "table-of-contents", 26 | "emoji", 27 | "syntax-highlighting", 28 | "markdown-processor", 29 | "github-flavored-markdown", 30 | "gfm", 31 | "remark-plugin", 32 | "rehype-plugin", 33 | "markdown-editor", 34 | "content-management", 35 | "documentation-tool", 36 | "blogging", 37 | "markdown-extension", 38 | "seo-friendly", 39 | "markdown-anchors", 40 | "remark", 41 | "rehype", 42 | "react", 43 | "react-component", 44 | "react-markdown", 45 | "markdown-to-react" 46 | ], 47 | "scripts": { 48 | "clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./site/README.md ./site/dist", 49 | "build": "rimraf ./dist && tsup src/writr.ts --format esm --dts --clean", 50 | "prepare": "npm run build", 51 | "test": "xo --fix && vitest run --coverage", 52 | "website:build": "rimraf ./site/README.md ./site/dist && npx docula build -s ./site -o ./site/dist", 53 | "website:serve": "rimraf ./site/README.md ./site/dist && npx docula serve -s ./site -o ./site/dist" 54 | }, 55 | "dependencies": { 56 | "cacheable": "^1.9.0", 57 | "hookified": "^1.9.0", 58 | "html-react-parser": "^5.2.5", 59 | "js-yaml": "^4.1.0", 60 | "react": "^19.1.0", 61 | "rehype-highlight": "^7.0.2", 62 | "rehype-katex": "^7.0.1", 63 | "rehype-slug": "^6.0.0", 64 | "rehype-stringify": "^10.0.1", 65 | "remark-emoji": "^5.0.1", 66 | "remark-gfm": "^4.0.1", 67 | "remark-math": "^6.0.0", 68 | "remark-mdx": "^3.1.0", 69 | "remark-parse": "^11.0.0", 70 | "remark-rehype": "^11.1.2", 71 | "remark-toc": "^9.0.0", 72 | "unified": "^11.0.5" 73 | }, 74 | "devDependencies": { 75 | "@types/js-yaml": "^4.0.9", 76 | "@types/node": "^22.15.23", 77 | "@types/react": "^19.1.6", 78 | "@vitest/coverage-v8": "^3.1.4", 79 | "docula": "^0.12.1", 80 | "rimraf": "^6.0.1", 81 | "ts-node": "^10.9.2", 82 | "tsup": "^8.5.0", 83 | "typescript": "^5.8.3", 84 | "vitest": "^3.1.4", 85 | "webpack": "^5.99.9", 86 | "xo": "^1.0.0" 87 | }, 88 | "xo": { 89 | "ignores": [ 90 | "docula.config.*" 91 | ] 92 | }, 93 | "files": [ 94 | "dist", 95 | "README.md", 96 | "LICENSE" 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - esbuild 3 | - unrs-resolver 4 | -------------------------------------------------------------------------------- /site/docula.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import process from 'node:process'; 4 | 5 | export const options = { 6 | githubPath: 'jaredwray/writr', 7 | siteTitle: 'Writr', 8 | siteDescription: 'Beautiful Website for Your Projects', 9 | siteUrl: 'https://writr.org', 10 | }; 11 | 12 | export const onPrepare = async config => { 13 | const readmePath = path.join(process.cwd(), './README.md'); 14 | const readmeSitePath = path.join(config.sitePath, 'README.md'); 15 | const readme = await fs.promises.readFile(readmePath, 'utf8'); 16 | const updatedReadme = readme.replace('![Writr](site/logo.svg)', ''); 17 | console.log('writing updated readme to', readmeSitePath); 18 | await fs.promises.writeFile(readmeSitePath, updatedReadme); 19 | }; 20 | -------------------------------------------------------------------------------- /site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredwray/writr/7666a181bf2c299041318ff5afec9c2cc268c3b1/site/favicon.ico -------------------------------------------------------------------------------- /site/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/writr-cache.ts: -------------------------------------------------------------------------------- 1 | import {CacheableMemory} from 'cacheable'; 2 | import {type RenderOptions} from './writr.js'; 3 | 4 | export class WritrCache { 5 | private readonly _store: CacheableMemory = new CacheableMemory(); 6 | private readonly _hashStore: CacheableMemory = new CacheableMemory(); 7 | 8 | public get store(): CacheableMemory { 9 | return this._store; 10 | } 11 | 12 | public get hashStore(): CacheableMemory { 13 | return this._hashStore; 14 | } 15 | 16 | public get(markdown: string, options?: RenderOptions): string | undefined { 17 | const key = this.hash(markdown, options); 18 | return this._store.get(key); 19 | } 20 | 21 | public set(markdown: string, value: string, options?: RenderOptions) { 22 | const key = this.hash(markdown, options); 23 | this._store.set(key, value); 24 | } 25 | 26 | public clear() { 27 | this._store.clear(); 28 | this._hashStore.clear(); 29 | } 30 | 31 | public hash(markdown: string, options?: RenderOptions): string { 32 | const content = {markdown, options}; 33 | const key = JSON.stringify(content); 34 | let result = this._hashStore.get(key); 35 | if (result) { 36 | return result; 37 | } 38 | 39 | result = this._hashStore.hash(content); 40 | this._hashStore.set(key, result); 41 | 42 | return result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/writr.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import {dirname} from 'node:path'; 3 | import {unified} from 'unified'; 4 | import remarkParse from 'remark-parse'; 5 | import remarkRehype from 'remark-rehype'; 6 | import rehypeSlug from 'rehype-slug'; 7 | import rehypeHighlight from 'rehype-highlight'; 8 | import rehypeStringify from 'rehype-stringify'; 9 | import remarkToc from 'remark-toc'; 10 | import remarkMath from 'remark-math'; 11 | import rehypeKatex from 'rehype-katex'; 12 | import remarkGfm from 'remark-gfm'; 13 | import remarkEmoji from 'remark-emoji'; 14 | import remarkMDX from 'remark-mdx'; 15 | import type React from 'react'; 16 | import parse, {type HTMLReactParserOptions} from 'html-react-parser'; 17 | import * as yaml from 'js-yaml'; 18 | import {Hookified} from 'hookified'; 19 | import {WritrCache} from './writr-cache.js'; 20 | 21 | /** 22 | * Writr options. 23 | * @typedef {Object} WritrOptions 24 | * @property {RenderOptions} [renderOptions] - Default render options (default: undefined) 25 | * @property {boolean} [throwErrors] - Throw error (default: false) 26 | */ 27 | export type WritrOptions = { 28 | renderOptions?: RenderOptions; // Default render options (default: undefined) 29 | throwErrors?: boolean; // Throw error (default: false) 30 | }; 31 | 32 | /** 33 | * Render options. 34 | * @typedef {Object} RenderOptions 35 | * @property {boolean} [emoji] - Emoji support (default: true) 36 | * @property {boolean} [toc] - Table of contents generation (default: true) 37 | * @property {boolean} [slug] - Slug generation (default: true) 38 | * @property {boolean} [highlight] - Code highlighting (default: true) 39 | * @property {boolean} [gfm] - Github flavor markdown (default: true) 40 | * @property {boolean} [math] - Math support (default: true) 41 | * @property {boolean} [mdx] - MDX support (default: true) 42 | * @property {boolean} [caching] - Caching (default: false) 43 | */ 44 | export type RenderOptions = { 45 | emoji?: boolean; // Emoji support (default: true) 46 | toc?: boolean; // Table of contents generation (default: true) 47 | slug?: boolean; // Slug generation (default: true) 48 | highlight?: boolean; // Code highlighting (default: true) 49 | gfm?: boolean; // Github flavor markdown (default: true) 50 | math?: boolean; // Math support (default: true) 51 | mdx?: boolean; // MDX support (default: true) 52 | caching?: boolean; // Caching (default: false) 53 | }; 54 | 55 | export enum WritrHooks { 56 | beforeRender = 'beforeRender', 57 | afterRender = 'afterRender', 58 | saveToFile = 'saveToFile', 59 | renderToFile = 'renderToFile', 60 | loadFromFile = 'loadFromFile', 61 | } 62 | 63 | export class Writr extends Hookified { 64 | public engine = unified() 65 | .use(remarkParse) 66 | .use(remarkGfm) // Use GitHub Flavored Markdown 67 | .use(remarkToc) // Add table of contents 68 | .use(remarkEmoji) // Add emoji support 69 | .use(remarkRehype) // Convert markdown to HTML 70 | .use(rehypeSlug) // Add slugs to headings in HTML 71 | .use(remarkMath) // Add math support 72 | .use(rehypeKatex) // Add math support 73 | .use(rehypeHighlight) // Apply syntax highlighting 74 | .use(remarkMDX) // Add MDX support 75 | .use(rehypeStringify); // Stringify HTML 76 | 77 | private readonly _options: WritrOptions = { 78 | throwErrors: false, 79 | renderOptions: { 80 | emoji: true, 81 | toc: true, 82 | slug: true, 83 | highlight: true, 84 | gfm: true, 85 | math: true, 86 | mdx: true, 87 | caching: false, 88 | }, 89 | }; 90 | 91 | private _content = ''; 92 | 93 | private readonly _cache = new WritrCache(); 94 | 95 | /** 96 | * Initialize Writr. Accepts a string or options object. 97 | * @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. 98 | * @param {WritrOptions} [arguments2] This is if you send in the content in the first argument and also want to send in options. 99 | * 100 | * @example 101 | * const writr = new Writr('Hello, world!', {caching: false}); 102 | */ 103 | constructor(arguments1?: string | WritrOptions, arguments2?: WritrOptions) { 104 | super(); 105 | if (typeof arguments1 === 'string') { 106 | this._content = arguments1; 107 | } else if (arguments1) { 108 | this._options = this.mergeOptions(this._options, arguments1); 109 | if (this._options.renderOptions) { 110 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 111 | this.engine = this.createProcessor(this._options.renderOptions); 112 | } 113 | } 114 | 115 | if (arguments2) { 116 | this._options = this.mergeOptions(this._options, arguments2); 117 | if (this._options.renderOptions) { 118 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 119 | this.engine = this.createProcessor(this._options.renderOptions); 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * Get the options. 126 | * @type {WritrOptions} 127 | */ 128 | public get options(): WritrOptions { 129 | return this._options; 130 | } 131 | 132 | /** 133 | * Get the Content. This is the markdown content and front matter if it exists. 134 | * @type {WritrOptions} 135 | */ 136 | public get content(): string { 137 | return this._content; 138 | } 139 | 140 | /** 141 | * Set the Content. This is the markdown content and front matter if it exists. 142 | * @type {WritrOptions} 143 | */ 144 | public set content(value: string) { 145 | this._content = value; 146 | } 147 | 148 | /** 149 | * Get the cache. 150 | * @type {WritrCache} 151 | */ 152 | public get cache(): WritrCache { 153 | return this._cache; 154 | } 155 | 156 | /** 157 | * Get the front matter raw content. 158 | * @type {string} The front matter content including the delimiters. 159 | */ 160 | public get frontMatterRaw(): string { 161 | // Is there front matter content? 162 | if (!this._content.trimStart().startsWith('---')) { 163 | return ''; 164 | } 165 | 166 | const start = this._content.indexOf('---\n'); 167 | 168 | const end = this._content.indexOf('\n---\n', start + 4); 169 | if (end === -1) { 170 | return ''; 171 | } // Return empty string if no ending delimiter is found 172 | 173 | return this._content.slice(start, end + 5); // Extract front matter including delimiters 174 | } 175 | 176 | /** 177 | * Get the body content without the front matter. 178 | * @type {string} The markdown content without the front matter. 179 | */ 180 | public get body(): string { 181 | // Is there front matter content? 182 | if (this.frontMatterRaw === '') { 183 | return this._content; 184 | } 185 | 186 | const end = this._content.indexOf('\n---\n'); 187 | 188 | // Return the content after the closing --- marker 189 | return this._content.slice(Math.max(0, end + 5)).trim(); 190 | } 191 | 192 | /** 193 | * Get the markdown content. This is an alias for the body property. 194 | * @type {string} The markdown content. 195 | */ 196 | public get markdown(): string { 197 | return this.body; 198 | } 199 | 200 | /** 201 | * Get the front matter content as an object. 202 | * @type {Record} The front matter content as an object. 203 | */ 204 | public get frontMatter(): Record { 205 | const frontMatter = this.frontMatterRaw; 206 | const match = /^---\s*([\s\S]*?)\s*---\s*/.exec(frontMatter); 207 | if (match) { 208 | try { 209 | return yaml.load(match[1].trim()) as Record; 210 | /* c8 ignore next 4 */ 211 | } catch (error) { 212 | this.emit('error', error); 213 | } 214 | } 215 | 216 | return {}; 217 | } 218 | 219 | /** 220 | * Set the front matter content as an object. 221 | * @type {Record} The front matter content as an object. 222 | */ 223 | public set frontMatter(data: Record) { 224 | const frontMatter = this.frontMatterRaw; 225 | const yamlString = yaml.dump(data); 226 | const newFrontMatter = `---\n${yamlString}---\n`; 227 | this._content = this._content.replace(frontMatter, newFrontMatter); 228 | } 229 | 230 | /** 231 | * Get the front matter value for a key. 232 | * @param {string} key The key to get the value for. 233 | * @returns {T} The value for the key. 234 | */ 235 | public getFrontMatterValue(key: string): T { 236 | return this.frontMatter[key] as T; 237 | } 238 | 239 | /** 240 | * Render the markdown content to HTML. 241 | * @param {RenderOptions} [options] The render options. 242 | * @returns {Promise} The rendered HTML content. 243 | */ 244 | public async render(options?: RenderOptions): Promise { 245 | try { 246 | let {engine} = this; 247 | if (options) { 248 | options = {...this._options.renderOptions, ...options}; 249 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 250 | engine = this.createProcessor(options); 251 | } 252 | 253 | const renderData = { 254 | content: this._content, 255 | body: this.body, 256 | options, 257 | }; 258 | 259 | await this.hook(WritrHooks.beforeRender, renderData); 260 | 261 | const resultData = { 262 | result: '', 263 | }; 264 | if (this.isCacheEnabled(renderData.options)) { 265 | const cached = this._cache.get(renderData.content, renderData.options); 266 | if (cached) { 267 | return cached; 268 | } 269 | } 270 | 271 | const file = await engine.process(renderData.body); 272 | resultData.result = String(file); 273 | if (this.isCacheEnabled(renderData.options)) { 274 | this._cache.set(renderData.content, resultData.result, renderData.options); 275 | } 276 | 277 | await this.hook(WritrHooks.afterRender, resultData); 278 | 279 | return resultData.result; 280 | } catch (error) { 281 | throw new Error(`Failed to render markdown: ${(error as Error).message}`); 282 | } 283 | } 284 | 285 | /** 286 | * Render the markdown content to HTML synchronously. 287 | * @param {RenderOptions} [options] The render options. 288 | * @returns {string} The rendered HTML content. 289 | */ 290 | public renderSync(options?: RenderOptions): string { 291 | try { 292 | let {engine} = this; 293 | if (options) { 294 | options = {...this._options.renderOptions, ...options}; 295 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 296 | engine = this.createProcessor(options); 297 | } 298 | 299 | const renderData = { 300 | content: this._content, 301 | body: this.body, 302 | options, 303 | }; 304 | 305 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 306 | this.hook(WritrHooks.beforeRender, renderData); 307 | 308 | const resultData = { 309 | result: '', 310 | }; 311 | if (this.isCacheEnabled(renderData.options)) { 312 | const cached = this._cache.get(renderData.content, renderData.options); 313 | if (cached) { 314 | return cached; 315 | } 316 | } 317 | 318 | const file = engine.processSync(renderData.body); 319 | resultData.result = String(file); 320 | if (this.isCacheEnabled(renderData.options)) { 321 | this._cache.set(renderData.content, resultData.result, renderData.options); 322 | } 323 | 324 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 325 | this.hook(WritrHooks.afterRender, resultData); 326 | 327 | return resultData.result; 328 | } catch (error) { 329 | throw new Error(`Failed to render markdown: ${(error as Error).message}`); 330 | } 331 | } 332 | 333 | /** 334 | * Render the markdown content and save it to a file. If the directory doesn't exist it will be created. 335 | * @param {string} filePath The file path to save the rendered markdown content to. 336 | * @param {RenderOptions} [options] the render options. 337 | */ 338 | public async renderToFile(filePath: string, options?: RenderOptions): Promise { 339 | try { 340 | const {writeFile, mkdir} = fs.promises; 341 | const directoryPath = dirname(filePath); 342 | const content = await this.render(options); 343 | await mkdir(directoryPath, {recursive: true}); 344 | const data = { 345 | filePath, 346 | content, 347 | }; 348 | await this.hook(WritrHooks.renderToFile, data); 349 | await writeFile(data.filePath, data.content); 350 | /* c8 ignore next 6 */ 351 | } catch (error) { 352 | this.emit('error', error); 353 | if (this._options.throwErrors) { 354 | throw error; 355 | } 356 | } 357 | } 358 | 359 | /** 360 | * Render the markdown content and save it to a file synchronously. If the directory doesn't exist it will be created. 361 | * @param {string} filePath The file path to save the rendered markdown content to. 362 | * @param {RenderOptions} [options] the render options. 363 | */ 364 | public renderToFileSync(filePath: string, options?: RenderOptions): void { 365 | try { 366 | const directoryPath = dirname(filePath); 367 | const content = this.renderSync(options); 368 | fs.mkdirSync(directoryPath, {recursive: true}); 369 | const data = { 370 | filePath, 371 | content, 372 | }; 373 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 374 | this.hook(WritrHooks.renderToFile, data); 375 | 376 | fs.writeFileSync(data.filePath, data.content); 377 | /* c8 ignore next 6 */ 378 | } catch (error) { 379 | this.emit('error', error); 380 | if (this._options.throwErrors) { 381 | throw error; 382 | } 383 | } 384 | } 385 | 386 | /** 387 | * Render the markdown content to React. 388 | * @param {RenderOptions} [options] The render options. 389 | * @param {HTMLReactParserOptions} [reactParseOptions] The HTML React parser options. 390 | * @returns {Promise} The rendered React content. 391 | */ 392 | public async renderReact(options?: RenderOptions, reactParseOptions?: HTMLReactParserOptions): Promise { 393 | const html = await this.render(options); 394 | 395 | return parse(html, reactParseOptions); 396 | } 397 | 398 | /** 399 | * Render the markdown content to React synchronously. 400 | * @param {RenderOptions} [options] The render options. 401 | * @param {HTMLReactParserOptions} [reactParseOptions] The HTML React parser options. 402 | * @returns {string | React.JSX.Element | React.JSX.Element[]} The rendered React content. 403 | */ 404 | public renderReactSync(options?: RenderOptions, reactParseOptions?: HTMLReactParserOptions): string | React.JSX.Element | React.JSX.Element[] { 405 | const html = this.renderSync(options); 406 | return parse(html, reactParseOptions); 407 | } 408 | 409 | /** 410 | * Load markdown content from a file. 411 | * @param {string} filePath The file path to load the markdown content from. 412 | * @returns {Promise} 413 | */ 414 | public async loadFromFile(filePath: string): Promise { 415 | try { 416 | const {readFile} = fs.promises; 417 | const data = { 418 | content: '', 419 | }; 420 | data.content = await readFile(filePath, 'utf8'); 421 | 422 | await this.hook(WritrHooks.loadFromFile, data); 423 | this._content = data.content; 424 | /* c8 ignore next 6 */ 425 | } catch (error) { 426 | this.emit('error', error); 427 | if (this._options.throwErrors) { 428 | throw error; 429 | } 430 | } 431 | } 432 | 433 | /** 434 | * Load markdown content from a file synchronously. 435 | * @param {string} filePath The file path to load the markdown content from. 436 | * @returns {void} 437 | */ 438 | public loadFromFileSync(filePath: string): void { 439 | try { 440 | const data = { 441 | content: '', 442 | }; 443 | data.content = fs.readFileSync(filePath, 'utf8'); 444 | 445 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 446 | this.hook(WritrHooks.loadFromFile, data); 447 | this._content = data.content; 448 | /* c8 ignore next 6 */ 449 | } catch (error) { 450 | this.emit('error', error); 451 | if (this._options.throwErrors) { 452 | throw error; 453 | } 454 | } 455 | } 456 | 457 | /** 458 | * Save the markdown content to a file. If the directory doesn't exist it will be created. 459 | * @param {string} filePath The file path to save the markdown content to. 460 | * @returns {Promise} 461 | */ 462 | public async saveToFile(filePath: string): Promise { 463 | try { 464 | const {writeFile, mkdir} = fs.promises; 465 | const directoryPath = dirname(filePath); 466 | await mkdir(directoryPath, {recursive: true}); 467 | const data = { 468 | filePath, 469 | content: this._content, 470 | }; 471 | await this.hook(WritrHooks.saveToFile, data); 472 | 473 | await writeFile(data.filePath, data.content); 474 | /* c8 ignore next 6 */ 475 | } catch (error) { 476 | this.emit('error', error); 477 | if (this._options.throwErrors) { 478 | throw error; 479 | } 480 | } 481 | } 482 | 483 | /** 484 | * Save the markdown content to a file synchronously. If the directory doesn't exist it will be created. 485 | * @param {string} filePath The file path to save the markdown content to. 486 | * @returns {void} 487 | */ 488 | public saveToFileSync(filePath: string): void { 489 | try { 490 | const directoryPath = dirname(filePath); 491 | fs.mkdirSync(directoryPath, {recursive: true}); 492 | const data = { 493 | filePath, 494 | content: this._content, 495 | }; 496 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 497 | this.hook(WritrHooks.saveToFile, data); 498 | 499 | fs.writeFileSync(data.filePath, data.content); 500 | /* c8 ignore next 6 */ 501 | } catch (error) { 502 | this.emit('error', error); 503 | if (this._options.throwErrors) { 504 | throw error; 505 | } 506 | } 507 | } 508 | 509 | private isCacheEnabled(options?: RenderOptions): boolean { 510 | if (options?.caching !== undefined) { 511 | return options.caching; 512 | } 513 | 514 | return this._options?.renderOptions?.caching ?? false; 515 | } 516 | 517 | private createProcessor(options: RenderOptions): any { 518 | const processor = unified().use(remarkParse); 519 | 520 | if (options.gfm) { 521 | processor.use(remarkGfm); 522 | } 523 | 524 | if (options.toc) { 525 | processor.use(remarkToc, {heading: 'toc|table of contents'}); 526 | } 527 | 528 | if (options.emoji) { 529 | processor.use(remarkEmoji); 530 | } 531 | 532 | processor.use(remarkRehype); 533 | 534 | if (options.slug) { 535 | processor.use(rehypeSlug); 536 | } 537 | 538 | if (options.highlight) { 539 | processor.use(rehypeHighlight); 540 | } 541 | 542 | if (options.math) { 543 | processor.use(remarkMath).use(rehypeKatex); 544 | } 545 | 546 | if (options.mdx) { 547 | processor.use(remarkMDX); 548 | } 549 | 550 | processor.use(rehypeStringify); 551 | 552 | return processor; 553 | } 554 | 555 | private mergeOptions(current: WritrOptions, options: WritrOptions): WritrOptions { 556 | if (options.throwErrors !== undefined) { 557 | current.throwErrors = options.throwErrors; 558 | } 559 | 560 | if (options.renderOptions) { 561 | current.renderOptions ??= {}; 562 | 563 | this.mergeRenderOptions(current.renderOptions, options.renderOptions); 564 | } 565 | 566 | return current; 567 | } 568 | 569 | private mergeRenderOptions(current: RenderOptions, options: RenderOptions): RenderOptions { 570 | if (options.emoji !== undefined) { 571 | current.emoji = options.emoji; 572 | } 573 | 574 | if (options.toc !== undefined) { 575 | current.toc = options.toc; 576 | } 577 | 578 | if (options.slug !== undefined) { 579 | current.slug = options.slug; 580 | } 581 | 582 | if (options.highlight !== undefined) { 583 | current.highlight = options.highlight; 584 | } 585 | 586 | if (options.gfm !== undefined) { 587 | current.gfm = options.gfm; 588 | } 589 | 590 | if (options.math !== undefined) { 591 | current.math = options.math; 592 | } 593 | 594 | if (options.mdx !== undefined) { 595 | current.mdx = options.mdx; 596 | } 597 | 598 | if (options.caching !== undefined) { 599 | current.caching = options.caching; 600 | } 601 | 602 | return current; 603 | } 604 | } 605 | 606 | -------------------------------------------------------------------------------- /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 | 190 | -------------------------------------------------------------------------------- /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/writr-cache.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, it, expect} from 'vitest'; 2 | import {WritrCache} from '../src/writr-cache.js'; 3 | 4 | describe('writr-cache', () => { 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 | expect(cache.hashStore.has('{"markdown":"# Hello World","options":{"toc":true,"emoji":true}}')).toEqual(true); 61 | expect(cache.hashStore.size).toEqual(1); 62 | options = {toc: true, emoji: false}; 63 | const key3 = cache.hash(markdown, options); 64 | expect(cache.hashStore.size).toEqual(2); 65 | }); 66 | 67 | it('Get and Set the Cache', async () => { 68 | const cache = new WritrCache(); 69 | expect(cache.store).toBeDefined(); 70 | }); 71 | 72 | it('should be able to clear the cache', async () => { 73 | const cache = new WritrCache(); 74 | const markdown = '# Hello World'; 75 | const value = '

Hello World

'; 76 | const options = {toc: true, emoji: true}; 77 | cache.set(markdown, value, options); 78 | cache.get(markdown, options); 79 | cache.clear(); 80 | expect(cache.get(markdown, options)).toBeUndefined(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/writr-hooks.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import fs from 'node:fs'; 3 | import { 4 | test, describe, expect, 5 | } from 'vitest'; 6 | import {Writr, WritrHooks} from '../src/writr.js'; 7 | 8 | describe('Writr Render Hooks', async () => { 9 | test('it should change the content before rendering', async () => { 10 | const writr = new Writr('Hello, World!'); 11 | writr.onHook(WritrHooks.beforeRender, data => { 12 | data.body = 'Hello, Universe!'; 13 | }); 14 | const result = await writr.render(); 15 | expect(result).toBe('

Hello, Universe!

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

Hello, Sync!

'); 25 | }); 26 | 27 | test('it should change the content before saving to file', async () => { 28 | const filePath = './test-save-to-file.txt'; 29 | const writr = new Writr('Hello, World!'); 30 | writr.onHook(WritrHooks.saveToFile, data => { 31 | data.content = 'Hello, File!'; 32 | }); 33 | await writr.saveToFile(filePath); 34 | await writr.loadFromFile(filePath); 35 | 36 | expect(writr.content).toBe('Hello, File!'); 37 | 38 | // Cleanup 39 | await fs.promises.rm(filePath); 40 | }); 41 | 42 | test('it should change the content before saving to file sync', async () => { 43 | const filePath = './test-save-to-file-sync.txt'; 44 | const writr = new Writr('Hello, World!'); 45 | writr.onHook(WritrHooks.saveToFile, data => { 46 | data.content = 'Hello, File Sync!'; 47 | }); 48 | writr.saveToFileSync(filePath); 49 | writr.loadFromFileSync(filePath); 50 | 51 | expect(writr.content).toBe('Hello, File Sync!'); 52 | 53 | // Cleanup 54 | await fs.promises.rm(filePath); 55 | }); 56 | 57 | test('it should change the content before render to file', async () => { 58 | const filePath = './test-render-to-file.txt'; 59 | const writr = new Writr('Hello, World!'); 60 | writr.onHook(WritrHooks.renderToFile, data => { 61 | data.content = 'Hello, File!'; 62 | }); 63 | await writr.renderToFile(filePath); 64 | const fileContent = await fs.promises.readFile(filePath); 65 | 66 | expect(fileContent.toString()).toContain('Hello, File!'); 67 | 68 | // Cleanup 69 | await fs.promises.rm(filePath); 70 | }); 71 | 72 | test('it should change the content before render to file sync', async () => { 73 | const filePath = './test-render-to-file-sync.txt'; 74 | const writr = new Writr('Hello, World!'); 75 | writr.onHook(WritrHooks.renderToFile, data => { 76 | data.content = 'Hello, File Sync!'; 77 | }); 78 | writr.renderToFileSync(filePath); 79 | const fileContent = await fs.promises.readFile(filePath); 80 | 81 | expect(fileContent.toString()).toContain('Hello, File Sync!'); 82 | 83 | // Cleanup 84 | await fs.promises.rm(filePath); 85 | }); 86 | 87 | test('it should change the content after loading from file', async () => { 88 | const filePath = './test/fixtures/load-from-file.md'; 89 | const content = 'Hello, Loaded!'; 90 | const writr = new Writr(); 91 | writr.onHook(WritrHooks.loadFromFile, data => { 92 | data.content = content; 93 | }); 94 | await writr.loadFromFile(filePath); 95 | expect(writr.content).toBe(content); 96 | }); 97 | 98 | test('it should change the content after loading from file sync', () => { 99 | const filePath = './test/fixtures/load-from-file.md'; 100 | const content = 'Hello, Loaded!'; 101 | const writr = new Writr(); 102 | writr.onHook(WritrHooks.loadFromFile, data => { 103 | data.content = content; 104 | }); 105 | writr.loadFromFileSync(filePath); 106 | expect(writr.content).toBe(content); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/writr-render.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { 3 | test, describe, expect, 4 | } from 'vitest'; 5 | import {Writr} from '../src/writr.js'; 6 | 7 | const testContentOne = `--- 8 | title: "Super Comfortable Chair" 9 | product_id: "CHAIR12345" 10 | price: 149.99 11 | --- 12 | 13 | # Super Comfortable Chair 14 | This is a super chair and amazing chair. It is very comfortable and you will love it. 15 | `; 16 | 17 | const testContentOneResult = `

Super Comfortable Chair

18 |

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

`; 19 | 20 | const options = { 21 | renderOptions: { 22 | caching: true, 23 | }, 24 | }; 25 | 26 | describe('Writr Async render with Caching', async () => { 27 | test('should render a template with caching', async () => { 28 | const writr = new Writr(testContentOne, options); 29 | const result = await writr.render(); 30 | expect(result).toBe(testContentOneResult); 31 | expect(writr.cache).toBeDefined(); 32 | expect(writr.cache.store.size).toBe(1); 33 | const result2 = await writr.render(); 34 | expect(result2).toBe(testContentOneResult); 35 | }); 36 | 37 | test('should sync render a template with caching', async () => { 38 | const writr = new Writr(testContentOne, options); 39 | const result = writr.renderSync(); 40 | expect(result).toBe(testContentOneResult); 41 | expect(writr.cache).toBeDefined(); 42 | expect(writr.cache.store.size).toBe(1); 43 | const result2 = writr.renderSync(); 44 | expect(result2).toBe(testContentOneResult); 45 | }); 46 | 47 | test('should render with async and then cache. Then render with sync via cache', async () => { 48 | const writr = new Writr(testContentOne, options); 49 | const result = await writr.render(); 50 | expect(result).toBe(testContentOneResult); 51 | expect(writr.cache).toBeDefined(); 52 | expect(writr.cache.store.size).toBe(1); 53 | const result2 = writr.renderSync(); 54 | expect(result2).toBe(testContentOneResult); 55 | }); 56 | }); 57 | 58 | describe('Render and Save to File', async () => { 59 | test('should render a template and save to file', async () => { 60 | const writr = new Writr(testContentOne, options); 61 | const fileName = './test/fixtures/temp-render/test-output.html'; 62 | if (fs.existsSync(fileName)) { 63 | fs.unlinkSync(fileName); 64 | } 65 | 66 | await writr.renderToFile(fileName); 67 | 68 | expect(fs.existsSync(fileName)).toBe(true); 69 | 70 | fs.unlinkSync(fileName); 71 | }); 72 | 73 | test('should render a template and save to file sync', async () => { 74 | const writr = new Writr(testContentOne, options); 75 | const fileName = './test/fixtures/temp-render/test-output-sync.html'; 76 | if (fs.existsSync(fileName)) { 77 | fs.unlinkSync(fileName); 78 | } 79 | 80 | writr.renderToFileSync(fileName); 81 | 82 | expect(fs.existsSync(fileName)).toBe(true); 83 | 84 | fs.unlinkSync(fileName); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/writr.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import fs from 'node:fs'; 3 | import { 4 | it, test, describe, expect, 5 | } from 'vitest'; 6 | import {Writr} from '../src/writr.js'; 7 | import { 8 | productPageWithMarkdown, 9 | blogPostWithMarkdown, 10 | projectDocumentationWithMarkdown, 11 | markdownWithFrontMatter, 12 | markdownWithFrontMatterAndAdditional, 13 | markdownWithFrontMatterInOtherPlaces, 14 | markdownWithBadFrontMatter, 15 | } from './content-fixtures.js'; 16 | 17 | describe('writr', () => { 18 | it('should be able to initialize', () => { 19 | const writr = new Writr(); 20 | expect(writr).toBeDefined(); 21 | }); 22 | 23 | it('should be able to set options', () => { 24 | const options = { 25 | throwErrors: true, 26 | renderOptions: { 27 | toc: false, 28 | slug: false, 29 | highlight: false, 30 | mdx: false, 31 | gfm: false, 32 | math: false, 33 | emoji: false, 34 | caching: false, 35 | }, 36 | }; 37 | const writr = new Writr(options); 38 | expect(writr.options).toBeDefined(); 39 | expect(writr.options.throwErrors).toEqual(true); 40 | expect(writr.options.renderOptions).toBeInstanceOf(Object); 41 | expect(writr.options.renderOptions?.emoji).toEqual(false); 42 | expect(writr.options.renderOptions?.gfm).toEqual(false); 43 | expect(writr.options.renderOptions?.mdx).toEqual(false); 44 | expect(writr.options.renderOptions?.highlight).toEqual(false); 45 | expect(writr.options.renderOptions?.math).toEqual(false); 46 | expect(writr.options.renderOptions?.slug).toEqual(false); 47 | expect(writr.options.renderOptions?.toc).toEqual(false); 48 | expect(writr.options.renderOptions?.caching).toEqual(false); 49 | }); 50 | 51 | it('should be able to set markdown', () => { 52 | const writr = new Writr('# Hello World'); 53 | expect(writr.markdown).toEqual('# Hello World'); 54 | expect(writr.renderSync()).toEqual('

Hello World

'); 55 | writr.content = '# Hello World\n\nThis is a test.'; 56 | expect(writr.markdown).toEqual('# Hello World\n\nThis is a test.'); 57 | expect(writr.renderSync()).toEqual('

Hello World

\n

This is a test.

'); 58 | }); 59 | it('should be able to set options on emoji', () => { 60 | const options = { 61 | renderOptions: { 62 | emoji: true, 63 | }, 64 | }; 65 | const writr = new Writr(options); 66 | expect(writr.options.renderOptions?.emoji).toEqual(true); 67 | }); 68 | it('should be able to set options on toc', () => { 69 | const options = { 70 | renderOptions: { 71 | toc: true, 72 | }, 73 | }; 74 | const writr = new Writr(options); 75 | expect(writr.options.renderOptions?.toc).toEqual(true); 76 | }); 77 | it('should render a simple markdown example', async () => { 78 | const writr = new Writr('# Hello World'); 79 | const result = await writr.render(); 80 | expect(result).toEqual('

Hello World

'); 81 | }); 82 | it('should render a simple markdown example via constructor with render options', async () => { 83 | const writr = new Writr('# Hello World'); 84 | const result = await writr.render({ 85 | emoji: false, 86 | }); 87 | expect(result).toEqual('

Hello World

'); 88 | }); 89 | 90 | it('should renderSync a simple markdown example', async () => { 91 | const writr = new Writr('# Hello World'); 92 | const result = writr.renderSync(); 93 | expect(result).toEqual('

Hello World

'); 94 | }); 95 | it('should renderSync a simple markdown example via constructor', async () => { 96 | const writr = new Writr(); 97 | writr.content = '# Hello World'; 98 | const result = writr.renderSync({ 99 | emoji: false, 100 | }); 101 | expect(result).toEqual('

Hello World

'); 102 | }); 103 | it('should render a simple markdown example with options - slug', async () => { 104 | const writr = new Writr('# Hello World', { 105 | renderOptions: { 106 | slug: false, 107 | }, 108 | }); 109 | const result = await writr.render(); 110 | expect(result).toEqual('

Hello World

'); 111 | }); 112 | it('should renderSync a simple markdown example with options - emoji', async () => { 113 | const writr = new Writr('# Hello World :dog:'); 114 | const options = { 115 | emoji: false, 116 | }; 117 | const result = writr.renderSync(options); 118 | expect(result).toEqual('

Hello World :dog:

'); 119 | }); 120 | it('should render a simple markdown example with options - emoji', async () => { 121 | const writr = new Writr('# Hello World :dog:'); 122 | const options = { 123 | emoji: false, 124 | }; 125 | const result = await writr.render(options); 126 | expect(result).toEqual('

Hello World :dog:

'); 127 | }); 128 | it('should render a simple markdown example with options - gfm', async () => { 129 | const writr = new Writr('# Hello World :dog:'); 130 | const options = { 131 | gfm: false, 132 | }; 133 | const result = await writr.render(options); 134 | expect(result).toEqual('

Hello World 🐶

'); 135 | }); 136 | 137 | it('should render from cache a simple markdown example with options - gfm', async () => { 138 | const writr = new Writr('# Hello World :dog:'); 139 | const options = { 140 | gfm: false, 141 | }; 142 | const result = await writr.render(options); 143 | expect(result).toEqual('

Hello World 🐶

'); 144 | const result2 = await writr.render(options); 145 | expect(result2).toEqual('

Hello World 🐶

'); 146 | }); 147 | 148 | it('should render a simple markdown example with options - toc', async () => { 149 | const writr = new Writr(); 150 | const options = { 151 | toc: false, 152 | }; 153 | const markdownString = `# Pluto\n\nPluto is a dwarf planet in the Kuiper belt.\n\n## Contents\n\n## History 154 | \n\n### Discovery\n\nIn the 1840s, Urbain Le Verrier used Newtonian mechanics to predict the\nposition of…`; 155 | writr.content = markdownString; 156 | const resultToc = await writr.render(); 157 | expect(resultToc).contains('
  • Discovery
  • '); 158 | const result = await writr.render(options); 159 | expect(result).not.contain('
  • Discovery
  • '); 160 | }); 161 | it('should render a simple markdown example with options - code highlight', async () => { 162 | const writr = new Writr(); 163 | const options = { 164 | highlight: false, 165 | }; 166 | // eslint-disable-next-line @stylistic/max-len 167 | const markdownString = '# Code Example\n\nThis is an inline code example: `const x = 10;`\n\nAnd here is a multi-line code block:\n\n```javascript\nconst greet = () => {\n console.log("Hello, world!");\n};\ngreet();\n```'; 168 | writr.content = markdownString; 169 | const resultFull = await writr.render(); 170 | expect(resultFull).contains('
    const');
    171 | 		const result = await writr.render(options);
    172 | 		expect(result).contain('
    const greet = () => {');
    173 | 	});
    174 | 	it('should throw an error on bad plugin or parsing', async () => {
    175 | 		const writr = new Writr('# Hello World');
    176 | 		const customPlugin = () => {
    177 | 			throw new Error('Custom Plugin Error: Required configuration missing.');
    178 | 		};
    179 | 
    180 | 		writr.engine.use(customPlugin);
    181 | 		try {
    182 | 			await writr.render();
    183 | 		} catch (error) {
    184 | 			expect((error as Error).message).toEqual('Failed to render markdown: Custom Plugin Error: Required configuration missing.');
    185 | 		}
    186 | 	});
    187 | 	it('should throw an error on bad plugin or parsing on renderSync', () => {
    188 | 		const writr = new Writr('# Hello World');
    189 | 		const customPlugin = () => {
    190 | 			throw new Error('Custom Plugin Error: Required configuration missing.');
    191 | 		};
    192 | 
    193 | 		writr.engine.use(customPlugin);
    194 | 		try {
    195 | 			writr.renderSync();
    196 | 		} catch (error) {
    197 | 			expect((error as Error).message).toEqual('Failed to render markdown: Custom Plugin Error: Required configuration missing.');
    198 | 		}
    199 | 	});
    200 | 	it('should be able to do math', async () => {
    201 | 		const writr = new Writr();
    202 | 		writr.content = '$$\n\\frac{1}{2}\n$$';
    203 | 		const result = await writr.render();
    204 | 		expect(result).toContain(' {
    207 | 		const writr = new Writr();
    208 | 		const markdownString = '## Hello World\n\n';
    209 | 		writr.content = markdownString;
    210 | 		const result = await writr.renderReact() as React.JSX.Element;
    211 | 		expect(result.type).toEqual('h2');
    212 | 	});
    213 | 	it('should be able to render react components sync', async () => {
    214 | 		const writr = new Writr();
    215 | 		const markdownString = '## Hello World\n\n';
    216 | 		writr.content = markdownString;
    217 | 		const result = writr.renderReactSync() as React.JSX.Element;
    218 | 		expect(result.type).toEqual('h2');
    219 | 	});
    220 | 
    221 | 	it('should be able to get/set cache', async () => {
    222 | 		const writr = new Writr();
    223 | 		writr.cache.set('# Hello World', '

    Hello World

    '); 224 | expect(writr.cache.get('# Hello World')).toEqual('

    Hello World

    '); 225 | }); 226 | 227 | it('should return a valid cached result', async () => { 228 | const content = '# Hello World'; 229 | const writr = new Writr(content, {renderOptions: {caching: true}}); // By defualt cache is enabled 230 | const result = await writr.render(); 231 | expect(result).toEqual('

    Hello World

    '); 232 | const hashKey = writr.cache.hash(content); 233 | expect(writr.cache.store.get(hashKey)).toEqual(result); 234 | }); 235 | 236 | it('should return non cached result via options', async () => { 237 | const writr = new Writr('# Hello World'); // By defualt cache is enabled 238 | const result = await writr.render(); 239 | expect(result).toEqual('

    Hello World

    '); 240 | const result2 = await writr.render({caching: false}); 241 | expect(result2).toEqual('

    Hello World

    '); 242 | }); 243 | 244 | it('should strip out the front matter on render', async () => { 245 | const writr = new Writr(blogPostWithMarkdown); 246 | const result = await writr.render(); 247 | expect(result).to.not.contain('title: "Understanding Async/Await in JavaScript"'); 248 | expect(result).to.contain('

    Introduction

    '); 249 | }); 250 | }); 251 | 252 | describe('WritrFrontMatter', () => { 253 | test('should initialize with content and work from same object', () => { 254 | const writr = new Writr(productPageWithMarkdown); 255 | expect(writr.content).toBe(productPageWithMarkdown); 256 | const meta = writr.frontMatter; 257 | meta.title = 'New Title 123'; 258 | writr.frontMatter = meta; 259 | expect(writr.content).to.contain('New Title 123'); 260 | }); 261 | 262 | test('should return the raw front matter', () => { 263 | const writr = new Writr(productPageWithMarkdown); 264 | expect(writr.frontMatterRaw).to.not.contain('## Description'); 265 | expect(writr.frontMatterRaw).to.contain('title: "Super Comfortable Chair"'); 266 | }); 267 | 268 | test('should return blank object with no frontmatter', () => { 269 | const markdown = '## Description\nThis is a description'; 270 | const writr = new Writr(markdown); 271 | expect(writr.frontMatterRaw).toBe(''); 272 | expect(writr.frontMatter).toStrictEqual({}); 273 | }); 274 | 275 | test('should return the body without front matter', () => { 276 | const writr = new Writr(blogPostWithMarkdown); 277 | expect(writr.body).to.contain('# Introduction'); 278 | expect(writr.body).to.contain('Using Async/Await makes your code cleaner and easier to understand by eliminating the need for complex callback chains or .then() methods.'); 279 | expect(writr.body).to.not.contain('title: "Super Comfortable Chair"'); 280 | expect(writr.body.split('\n')).to.not.contain('---'); 281 | }); 282 | 283 | test('should return the front matter as an object', () => { 284 | const writr = new Writr(projectDocumentationWithMarkdown); 285 | const {frontMatter} = writr; 286 | expect(frontMatter).to.haveOwnProperty('title', 'Project Documentation'); 287 | }); 288 | 289 | test('should return the front matter as an object with additional properties', () => { 290 | const writr = new Writr(markdownWithFrontMatterAndAdditional); 291 | expect(writr.frontMatter).to.haveOwnProperty('title', 'Sample Title'); 292 | expect(writr.frontMatter).to.haveOwnProperty('date', '2024-08-30'); 293 | expect(writr.content).to.contain('---'); 294 | expect(writr.content).to.contain('This is additional content.'); 295 | }); 296 | 297 | test('should set the front matter', () => { 298 | const writr = new Writr(projectDocumentationWithMarkdown); 299 | const meta = writr.frontMatter; 300 | meta.title = 'New Title'; 301 | if (!Array.isArray(meta.contributors)) { 302 | meta.contributors = []; 303 | } 304 | 305 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 306 | meta.contributors.push({name: 'Jane Doe', email: 'jane@doe.org'}); 307 | writr.frontMatter = meta; 308 | expect(writr.frontMatter.title).toBe('New Title'); 309 | expect(writr.content).to.contain('New Title'); 310 | expect(writr.content).to.contain('jane@doe.org'); 311 | }); 312 | 313 | test('should return a value from the front matter', () => { 314 | const writr = new Writr(blogPostWithMarkdown); 315 | expect(writr.getFrontMatterValue('title')).toBe('Understanding Async/Await in JavaScript'); 316 | expect(writr.getFrontMatterValue('author')).toBe('Jane Doe'); 317 | expect(writr.getFrontMatterValue('draft')).toBe(false); 318 | expect(writr.getFrontMatterValue('tags')).toStrictEqual(['async', 'await', 'ES6']); 319 | }); 320 | 321 | test('body should only contain the body', () => { 322 | const writr = new Writr(blogPostWithMarkdown); 323 | expect(writr.body.split('\n')[0]).to.contain('# Introduction'); 324 | }); 325 | 326 | test('should return the entire content if closing delimiter is not found', () => { 327 | const markdownWithIncompleteFrontMatter = ` 328 | --- 329 | title: "Sample Title" 330 | date: "2024-08-30" 331 | # Missing the closing delimiter 332 | 333 | # Markdown Content Here 334 | `; 335 | 336 | const frontMatter = new Writr(markdownWithIncompleteFrontMatter); 337 | const {body} = frontMatter; 338 | 339 | // The body should be the entire content since the closing delimiter is missing 340 | expect(body.trim()).toBe(markdownWithIncompleteFrontMatter.trim()); 341 | expect(frontMatter.frontMatterRaw).toBe(''); 342 | }); 343 | 344 | test('should be able to parse front matter and get body', () => { 345 | const writr = new Writr(markdownWithFrontMatter as string); 346 | expect(writr.body).to.contain('# Markdown Content Here'); 347 | }); 348 | test('should not parse wrong front matter', () => { 349 | const writr = new Writr(markdownWithFrontMatterInOtherPlaces as string); 350 | expect(writr.body).to.contain('---'); 351 | }); 352 | test('if frontMatter is not correct yaml it should emit an error and return {}', () => { 353 | const writr = new Writr(markdownWithBadFrontMatter as string); 354 | expect(writr.frontMatter).toStrictEqual({}); 355 | }); 356 | }); 357 | 358 | describe('Writr Files', async () => { 359 | test('should be able to save and load a file', async () => { 360 | const writr = new Writr(productPageWithMarkdown); 361 | const path = './test/fixtures/sample.md'; 362 | await writr.saveToFile(path); 363 | const writr2 = new Writr(); 364 | await writr2.loadFromFile(path); 365 | expect(writr2.content).to.contain('Super Comfortable Chair'); 366 | await fs.promises.unlink(path); 367 | }); 368 | test('should be able to save and load a file with front matter sync', () => { 369 | const writr = new Writr(blogPostWithMarkdown); 370 | const path = './test/fixtures/sample2.md'; 371 | writr.saveToFileSync(path); 372 | const writr2 = new Writr(); 373 | writr2.loadFromFileSync(path); 374 | expect(writr2.content).to.contain('Understanding Async/Await in JavaScript'); 375 | fs.unlinkSync(path); 376 | }); 377 | }); 378 | 379 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | exclude: [ 7 | 'site/docula.config.cjs', 8 | 'site-output/**', 9 | '.pnp.*', 10 | '.yarn/**', 11 | 'vitest.config.mjs', 12 | 'dist/**', 13 | 'site/**', 14 | 'test/**', 15 | ], 16 | }, 17 | }, 18 | }); 19 | --------------------------------------------------------------------------------