writr
Version:
Markdown Rendering Simplified
740 lines (548 loc) • 24.4 kB
Markdown

# Markdown Rendering Simplified
[](https://github.com/jaredwray/writr/actions/workflows/tests.yml)
[](https://github.com/jaredwray/writr/blob/master/LICENSE)
[](https://codecov.io/gh/jaredwray/writr)
[](https://npmjs.com/package/writr)
[](https://npmjs.com/package/writr)
# Features
* Removes the remark / unified complexity and easy to use.
* Powered by the [unified processor](https://github.com/unifiedjs/unified) for an extensible plugin pipeline.
* Built in caching 💥 making it render very fast when there isn't a change
* Frontmatter support built in by default. :tada:
* Easily Render to `React` or `HTML`.
* Generates a Table of Contents for your markdown files (remark-toc).
* Slug generation for your markdown files (rehype-slug).
* Code Highlighting (rehype-highlight).
* Math Support (rehype-katex).
* Markdown to HTML (rehype-stringify).
* Github Flavor Markdown (remark-gfm).
* Emoji Support (remark-emoji).
* MDX Support (remark-mdx).
* Built in Hooks for adding code to render pipeline.
# Unified Processor Engine
Writr builds on top of the open source [unified](https://github.com/unifiedjs/unified) processor – the core project that powers
[remark](https://github.com/remarkjs/remark), [rehype](https://github.com/rehypejs/rehype), and many other content tools. Unified
provides a pluggable pipeline where each plugin transforms a syntax tree. Writr configures a default set of plugins to turn
Markdown into HTML, but you can access the processor through the `.engine` property to add your own behavior with
`writr.engine.use(myPlugin)`. The [unified documentation](https://unifiedjs.com/) has more details and guides for building
plugins and working with the processor directly.
# Table of Contents
- [Unified Processor Engine](#unified-processor-engine)
- [Getting Started](#getting-started)
- [API](#api)
- [`new Writr(arg?: string | WritrOptions, options?: WritrOptions)`](#new-writrarg-string--writroptions-options-writroptions)
- [`.content`](#content)
- [`.body`](#body)
- [`.options`](#options)
- [`.frontmatter`](#frontmatter)
- [`.frontMatterRaw`](#frontmatterraw)
- [`.cache`](#cache)
- [`.engine`](#engine)
- [`.render(options?: RenderOptions)`](#renderoptions-renderoptions)
- [`.renderSync(options?: RenderOptions)`](#rendersyncoptions-renderoptions)
- [`.renderToFile(filePath: string, options?)`](#rendertofilefilepath-string-options-renderoptions)
- [`.renderToFileSync(filePath: string, options?)`](#rendertofilesyncfilepath-string-options-renderoptions)
- [`.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`](#renderreactoptions-renderoptions-reactoptions-htmlreactparseroptions)
- [`.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`](#renderreactsync-options-renderoptions-reactoptions-htmlreactparseroptions)
- [`.validate(content?: string, options?: RenderOptions)`](#validatecontent-string-options-renderoptions)
- [`.validateSync(content?: string, options?: RenderOptions)`](#validatesynccontent-string-options-renderoptions)
- [`.loadFromFile(filePath: string)`](#loadfromfilefilepath-string)
- [`.loadFromFileSync(filePath: string)`](#loadfromfilesyncfilepath-string)
- [`.saveToFile(filePath: string)`](#savetofilefilepath-string)
- [`.saveToFileSync(filePath: string)`](#savetofilesyncfilepath-string)
- [Caching On Render](#caching-on-render)
- [GitHub Flavored Markdown (GFM)](#github-flavored-markdown-gfm)
- [GFM Features](#gfm-features)
- [Using GFM](#using-gfm)
- [Disabling GFM](#disabling-gfm)
- [Hooks](#hooks)
- [Emitters](#emitters)
- [Error Events](#error-events)
- [Listening to Error Events](#listening-to-error-events)
- [Methods that Emit Errors](#methods-that-emit-errors)
- [Error Event Examples](#error-event-examples)
- [Event Emitter Methods](#event-emitter-methods)
- [ESM and Node Version Support](#esm-and-node-version-support)
- [Code of Conduct and Contributing](#code-of-conduct-and-contributing)
- [License](#license)
# Getting Started
```bash
> npm install writr
```
Then you can use it like this:
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
const html = await writr.render(); // <h1>Hello World 🙂</h1><p>This is a test.</p>
```
Its just that simple. Want to add some options? No problem.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
const options = {
emoji: false
}
const html = await writr.render(options); // <h1>Hello World ::-):</h1><p>This is a test.</p>
```
An example passing in the options also via the constructor:
```javascript
import { Writr, WritrOptions } from 'writr';
const writrOptions = {
throwErrors: true,
renderOptions: {
emoji: true,
toc: true,
slug: true,
highlight: true,
gfm: true,
math: true,
mdx: true,
caching: true,
}
};
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, writrOptions);
const html = await writr.render(options); // <h1>Hello World ::-):</h1><p>This is a test.</p>
```
# API
## `new Writr(arg?: string | WritrOptions, options?: WritrOptions)`
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.
```javascript
import { Writr, WritrOptions } from 'writr';
const writrOptions = {
throwErrors: true,
renderOptions: {
emoji: true,
toc: true,
slug: true,
highlight: true,
gfm: true,
math: true,
mdx: true,
caching: true,
}
};
const writr = new Writr(writrOptions);
```
## `.content`
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`.
```javascript
import { Writr } from 'writr';
const writr = new Writr();
writr.content = `---
title: Hello World
---
# Hello World ::-):\n\n This is a test.`;
```
## `.body`
gets the body of the markdown content. This is the content without the frontmatter.
```javascript
import { Writr } from 'writr';
const writr = new Writr();
writr.content = `---
title: Hello World
---
# Hello World ::-):\n\n This is a test.`;
console.log(writr.body); // '# Hello World ::-):\n\n This is a test.'
```
## `.options`
Accessing the default options for this instance of Writr. Here is the default settings for `WritrOptions`. These are the default settings for the `WritrOptions`:
```javascript
{
throwErrors: false,
renderOptions: {
emoji: true,
toc: true,
slug: true,
highlight: true,
gfm: true,
math: true,
mdx: true,
caching: false,
}
}
```
## `.frontmatter`
Accessing the frontmatter for this instance of Writr. This is a `Record<string, any>` and can be set via the `.content` property.
```javascript
import { Writr } from 'writr';
const writr = new Writr();
writr.content = `---
title: Hello World
---
# Hello World ::-):\n\n This is a test.`;
console.log(writr.frontmatter); // { title: 'Hello World' }
```
you can also set the front matter directly like this:
```javascript
import { Writr } from 'writr';
const writr = new Writr();
writr.frontmatter = { title: 'Hello World' };
```
## `.frontMatterRaw`
Accessing the raw frontmatter for this instance of Writr. This is a `string` and can be set via the `.content` property.
```javascript
import { Writr } from 'writr';
const writr = new Writr();
writr.content = `---
title: Hello World
---
# Hello World ::-):\n\n This is a test.`;
console.log(writr.frontMatterRaw); // '---\ntitle: Hello World\n---'
```
## `.cache`
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:
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
const options = {
caching: true
}
const html = await writr.render(options); // <h1>Hello World ::-):</h1><p>This is a test.</p>
```
## `.engine`
Accessing the underlying engine for this instance of Writr. This is a `Processor<Root, Root, Root, undefined, undefined>` from the core [`unified`](https://github.com/unifiedjs/unified) project and uses the familiar `.use()` plugin pattern. You can chain additional unified plugins on this processor to customize the render pipeline. Learn more about the unified engine at [unifiedjs.com](https://unifiedjs.com/) and check out the [getting started guide](https://unifiedjs.com/learn/guide/using-unified/) for examples.
## `.render(options?: RenderOptions)`
Rendering markdown to HTML. the options are based on RenderOptions. Which you can access from the Writr instance.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
const html = await writr.render(); // <h1>Hello World 🙂</h1><p>This is a test.</p>
//passing in with render options
const options = {
emoji: false
}
const html = await writr.render(options); // <h1>Hello World ::-):</h1><p>This is a test.</p>
```
## `.renderSync(options?: RenderOptions)`
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.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
const html = writr.renderSync(); // <h1>Hello World 🙂</h1><p>This is a test.</p>
```
## `.renderToFile(filePath: string, options?: RenderOptions)`
Rendering markdown to a file. The options are based on RenderOptions.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
await writr.renderToFile('path/to/file.html');
```
## `.renderToFileSync(filePath: string, options?: RenderOptions)`
Rendering markdown to a file synchronously. The options are based on RenderOptions.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
writr.renderToFileSync('path/to/file.html');
```
## `.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`
Rendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
const reactElement = await writr.renderReact(); // Will return a React.JSX.Element
```
## `.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`
Rendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
const reactElement = writr.renderReactSync(); // Will return a React.JSX.Element
```
## `.validate(content?: string, options?: RenderOptions)`
Validate markdown content by attempting to render it. Returns a `WritrValidateResult` object with a `valid` boolean and optional `error` property. Note that this will disable caching on render to ensure accurate validation.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World\n\nThis is a test.`);
// Validate current content
const result = await writr.validate();
console.log(result.valid); // true
// Validate external content without changing the instance
const externalResult = await writr.validate('## Different Content');
console.log(externalResult.valid); // true
console.log(writr.content); // Still "# Hello World\n\nThis is a test."
// Handle validation errors
const invalidWritr = new Writr('Put invalid markdown here');
const errorResult = await invalidWritr.validate();
console.log(errorResult.valid); // false
console.log(errorResult.error?.message); // "Failed to render markdown: Invalid plugin"
```
## `.validateSync(content?: string, options?: RenderOptions)`
Synchronously validate markdown content by attempting to render it. Returns a `WritrValidateResult` object with a `valid` boolean and optional `error` property.
This is the synchronous version of `.validate()` with the same parameters and behavior.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World\n\nThis is a test.`);
// Validate current content synchronously
const result = writr.validateSync();
console.log(result.valid); // true
// Validate external content without changing the instance
const externalResult = writr.validateSync('## Different Content');
console.log(externalResult.valid); // true
console.log(writr.content); // Still "# Hello World\n\nThis is a test."
```
## `.loadFromFile(filePath: string)`
Load your markdown content from a file path.
```javascript
import { Writr } from 'writr';
const writr = new Writr();
await writr.loadFromFile('path/to/file.md');
```
## `.loadFromFileSync(filePath: string)`
Load your markdown content from a file path synchronously.
```javascript
import { Writr } from 'writr';
const writr = new Writr();
writr.loadFromFileSync('path/to/file.md');
```
## `.saveToFile(filePath: string)`
Save your markdown and frontmatter (if included) content to a file path.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
await writr.saveToFile('path/to/file.md');
```
## `.saveToFileSync(filePath: string)`
Save your markdown and frontmatter (if included) content to a file path synchronously.
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
writr.saveToFileSync('path/to/file.md');
```
# Caching On Render
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:
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, { renderOptions: { caching: true } });
```
or via `RenderOptions` such as:
```javascript
import { Writr } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
await writr.render({ caching: true});
```
If you want to set the caching options for the instance of Writr you can do so like this:
```javascript
// we will set the lruSize of the cache and the default ttl
import {Writr} from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`, { renderOptions: { caching: true } });
writr.cache.store.lruSize = 100;
writr.cache.store.ttl = '5m'; // setting it to 5 minutes
```
# GitHub Flavored Markdown (GFM)
Writr includes full support for [GitHub Flavored Markdown](https://github.github.com/gfm/) (GFM) through the `remark-gfm` and `remark-github-blockquote-alert` plugins. GFM is enabled by default and adds several powerful features to standard Markdown.
## GFM Features
When GFM is enabled (which it is by default), you get access to the following features:
### Tables
Create tables using pipes and hyphens:
```markdown
| Feature | Supported |
|---------|-----------|
| Tables | Yes |
| Alerts | Yes |
```
### Strikethrough
Use `~~` to create strikethrough text:
```markdown
~~This text is crossed out~~
```
### Task Lists
Create interactive checkboxes:
```markdown
- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
```
### Autolinks
URLs are automatically converted to clickable links:
```markdown
https://github.com
```
### GitHub Blockquote Alerts
GitHub-style alerts are supported to emphasize critical information. These are blockquote-based admonitions that render with special styling:
```markdown
> [!NOTE]
> Useful information that users should know, even when skimming content.
> [!TIP]
> Helpful advice for doing things better or more easily.
> [!IMPORTANT]
> Key information users need to know to achieve their goal.
> [!WARNING]
> Urgent info that needs immediate user attention to avoid problems.
> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.
```
## Using GFM
GFM is enabled by default. Here's an example:
```javascript
import { Writr } from 'writr';
const markdown = `
# Task List Example
- [x] Learn Writr basics
- [ ] Master GFM features
> [!NOTE]
> GitHub Flavored Markdown is enabled by default!
| Feature | Status |
|---------|--------|
| GFM | ✓ |
`;
const writr = new Writr(markdown);
const html = await writr.render(); // Renders with full GFM support
```
## Disabling GFM
If you need to disable GFM features, you can set `gfm: false` in the render options:
```javascript
import { Writr } from 'writr';
const writr = new Writr('~~strikethrough~~ text');
// Disable GFM
const html = await writr.render({ gfm: false });
// Output: <p>~~strikethrough~~ text</p>
// With GFM enabled (default)
const htmlWithGfm = await writr.render({ gfm: true });
// Output: <p><del>strikethrough</del> text</p>
```
Note: When GFM is disabled, GitHub blockquote alerts will not be processed and will render as regular blockquotes.
# Hooks
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:
```javascript
import { Writr, WritrHooks } from 'writr';
const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
writr.onHook(WritrHooks.beforeRender, data => {
data.body = 'Hello, Universe!';
});
const result = await writr.render();
console.log(result); // Hello, Universe!
```
For `beforeRender` the data object is a `renderData` object. Here is the interface for `renderData`:
```typescript
export type renderData = {
body: string
options: RenderOptions;
}
```
For `afterRender` the data object is a `resultData` object. Here is the interface for `resultData`:
```typescript
export type resultData = {
result: string;
}
```
For `saveToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `saveToFileData`:
```typescript
export type saveToFileData = {
filePath: string;
content: string;
}
```
This is called when you call `saveToFile`, `saveToFileSync`.
For `renderToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `renderToFileData`:
```typescript
export type renderToFileData = {
filePath: string;
content: string;
}
```
This is called when you call `renderToFile`, `renderToFileSync`.
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`:
```typescript
export type loadFromFileData = {
content: string;
}
```
This is called when you call `loadFromFile`, `loadFromFileSync`.
# Emitters
Writr extends the [Hookified](https://github.com/jaredwray/hookified) class, which provides event emitter capabilities. This means you can listen to events emitted by Writr during its lifecycle, particularly error events.
## Error Events
Writr emits an `error` event whenever an error occurs in any of its methods. This provides a centralized way to handle errors without wrapping every method call in a try/catch block.
### Listening to Error Events
You can listen to error events using the `.on()` method:
```javascript
import { Writr } from 'writr';
const writr = new Writr('# Hello World');
// Listen for any errors
writr.on('error', (error) => {
console.error('An error occurred:', error.message);
// Handle the error appropriately
// Log to error tracking service, display to user, etc.
});
// Now when any error occurs, your listener will be notified
try {
await writr.render();
} catch (error) {
// Error is also thrown, so you can handle it here too
}
```
### Methods that Emit Errors
The following methods emit error events when they fail:
**Rendering Methods:**
- `render()` - Emits error before throwing when markdown rendering fails
- `renderSync()` - Emits error before throwing when markdown rendering fails
- `renderReact()` - Emits error before throwing when React rendering fails
- `renderReactSync()` - Emits error before throwing when React rendering fails
**Validation Methods:**
- `validate()` - Emits error when validation fails (returns error in result object)
- `validateSync()` - Emits error when validation fails (returns error in result object)
**File Operations:**
- `renderToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
- `renderToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
- `loadFromFile()` - Emits error when file reading fails (does not throw if `throwErrors: false`)
- `loadFromFileSync()` - Emits error when file reading fails (does not throw if `throwErrors: false`)
- `saveToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
- `saveToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
**Front Matter Operations:**
- `frontMatter` getter - Emits error when YAML parsing fails
- `frontMatter` setter - Emits error when YAML serialization fails
### Error Event Examples
**Example 1: Global Error Handler**
```javascript
import { Writr } from 'writr';
const writr = new Writr();
// Set up a global error handler
writr.on('error', (error) => {
// Log to your monitoring service
console.error('Writr error:', error);
// Send to error tracking (e.g., Sentry, Rollbar)
// errorTracker.captureException(error);
});
// All errors will be emitted to the listener above
await writr.loadFromFile('./content.md');
const html = await writr.render();
```
**Example 2: Validation with Error Listening**
```javascript
import { Writr } from 'writr';
const writr = new Writr('# My Content');
let lastError = null;
writr.on('error', (error) => {
lastError = error;
});
const result = await writr.validate();
if (!result.valid) {
console.log('Validation failed');
console.log('Error details:', lastError);
// result.error is also available
}
```
**Example 3: File Operations Without Try/Catch**
```javascript
import { Writr } from 'writr';
const writr = new Writr('# Content', { throwErrors: false });
writr.on('error', (error) => {
console.error('File operation failed:', error.message);
// Handle gracefully - maybe use default content
});
// Won't throw, but will emit error event if file doesn't exist
await writr.loadFromFile('./maybe-missing.md');
```
### Event Emitter Methods
Since Writr extends Hookified, you have access to standard event emitter methods:
- `writr.on(event, handler)` - Add an event listener
- `writr.once(event, handler)` - Add a one-time event listener
- `writr.off(event, handler)` - Remove an event listener
- `writr.emit(event, data)` - Emit an event (used internally)
For more information about event handling capabilities, see the [Hookified documentation](https://github.com/jaredwray/hookified).
# ESM and Node Version Support
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.
# Code of Conduct and Contributing
Please use our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing](CONTRIBUTING.md) guidelines for development and testing. We appreciate your contributions!
# License
[MIT](LICENSE) & © [Jared Wray](https://jaredwray.com)