writr 4.1.4 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,12 +22,15 @@
22
22
  - [`.engine`](#engine)
23
23
  - [`.render(options?: RenderOptions): Promise<string>`](#renderoptions-renderoptions-promisestring)
24
24
  - [`.renderSync(options?: RenderOptions): string`](#rendersyncoptions-renderoptions-string)
25
+ - [`.renderToFile(filePath: string, options?: RenderOptions)`](#rendertofilefilepath-string-options-renderoptions)
26
+ - [`.renderToFileSync(filePath: string, options?: RenderOptions): void`](#rendertofilesyncfilepath-string-options-renderoptions-void)
25
27
  - [`.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions): Promise<React.JSX.Element />`](#renderreactoptions-renderoptions-reactoptions-htmlreactparseroptions-promise-reactjsxelement-)
26
28
  - [`.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions): React.JSX.Element`](#renderreactsync-options-renderoptions-reactoptions-htmlreactparseroptions-reactjsxelement)
27
29
  - [`.loadFromFile(filePath: string): Promise<void>`](#loadfromfilefilepath-string-promisevoid)
28
30
  - [`.loadFromFileSync(filePath: string): void`](#loadfromfilesyncfilepath-string-void)
29
31
  - [`.saveToFile(filePath: string): Promise<void>`](#savetofilefilepath-string-promisevoid)
30
32
  - [`.saveToFileSync(filePath: string): void`](#savetofilesyncfilepath-string-void)
33
+ - [Hooks](#hooks)
31
34
  - [Code of Conduct and Contributing](#code-of-conduct-and-contributing)
32
35
  - [License](#license)
33
36
 
@@ -45,6 +48,7 @@
45
48
  * Github Flavor Markdown (remark-gfm).
46
49
  * Emoji Support (remark-emoji).
47
50
  * MDX Support (remark-mdx).
51
+ * Built in Hooks for adding code to render pipeline.
48
52
 
49
53
  # ESM and Node Version Support
50
54
 
@@ -81,6 +85,7 @@ An example passing in the options also via the constructor:
81
85
  ```javascript
82
86
  import { Writr, WritrOptions } from 'writr';
83
87
  const writrOptions = {
88
+ throwErrors: true,
84
89
  renderOptions: {
85
90
  emoji: true,
86
91
  toc: true,
@@ -105,6 +110,7 @@ By default the constructor takes in a markdown `string` or `WritrOptions` in the
105
110
  ```javascript
106
111
  import { Writr, WritrOptions } from 'writr';
107
112
  const writrOptions = {
113
+ throwErrors: true,
108
114
  renderOptions: {
109
115
  emoji: true,
110
116
  toc: true,
@@ -149,7 +155,23 @@ console.log(writr.body); // '# Hello World ::-):\n\n This is a test.'
149
155
 
150
156
  ## `.options`
151
157
 
152
- Accessing the default options for this instance of Writr.
158
+ Accessing the default options for this instance of Writr. Here is the default settings for `WritrOptions`.
159
+
160
+ ```javascript
161
+ {
162
+ throwErrors: false,
163
+ renderOptions: {
164
+ emoji: true,
165
+ toc: false,
166
+ slug: false,
167
+ highlight: false,
168
+ gfm: true,
169
+ math: false,
170
+ mdx: false,
171
+ caching: false,
172
+ }
173
+ }
174
+ ```
153
175
 
154
176
  ## `.frontmatter`
155
177
 
@@ -232,6 +254,26 @@ const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
232
254
  const html = writr.renderSync(); // <h1>Hello World 🙂</h1><p>This is a test.</p>
233
255
  ```
234
256
 
257
+ ## '.renderToFile(filePath: string, options?: RenderOptions)'
258
+
259
+ Rendering markdown to a file. The options are based on RenderOptions.
260
+
261
+ ```javascript
262
+ import { Writr } from 'writr';
263
+ const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
264
+ await writr.renderToFile('path/to/file.html');
265
+ ```
266
+
267
+ ## '.renderToFileSync(filePath: string, options?: RenderOptions): void'
268
+
269
+ Rendering markdown to a file synchronously. The options are based on RenderOptions.
270
+
271
+ ```javascript
272
+ import { Writr } from 'writr';
273
+ const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
274
+ writr.renderToFileSync('path/to/file.html');
275
+ ```
276
+
235
277
  ## '.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions): Promise<React.JSX.Element />'
236
278
 
237
279
  Rendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`.
@@ -307,9 +349,41 @@ writr.cache.store.lruSize = 100;
307
349
  writr.cache.store.ttl = '5m'; // setting it to 5 minutes
308
350
  ```
309
351
 
352
+ # Hooks
353
+
354
+ 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:
355
+
356
+ ```javascript
357
+ import { Writr, WritrHooks } from 'writr';
358
+ const writr = new Writr(`# Hello World ::-):\n\n This is a test.`);
359
+ writr.onHook(WritrHooks.beforeRender, data => {
360
+ data.body = 'Hello, Universe!';
361
+ });
362
+ const result = await writr.render();
363
+ console.log(result); // Hello, Universe!
364
+ ```
365
+
366
+ For `beforeRender` the data object is a `renderData` object. Here is the interface for `renderData`:
367
+
368
+ ```typescript
369
+ export type renderData {
370
+ body: string;
371
+ content: string;
372
+ options: RenderOptions;
373
+ }
374
+ ```
375
+
376
+ For `afterRender` the data object is a `resultData` object. Here is the interface for `resultData`:
377
+
378
+ ```typescript
379
+ export type resultData {
380
+ result: string;
381
+ }
382
+ ```
383
+
310
384
  # Code of Conduct and Contributing
311
385
  [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing](CONTRIBUTING.md) guidelines.
312
386
 
313
387
  # License
314
388
 
315
- MIT © [Jared Wray](https://jaredwray.com)
389
+ [MIT](LICENSE) & © [Jared Wray](https://jaredwray.com)
package/dist/writr.d.ts CHANGED
@@ -20,12 +20,12 @@ declare class WritrCache {
20
20
  /**
21
21
  * Writr options.
22
22
  * @typedef {Object} WritrOptions
23
- * @property {string} [openai] - Openai api key (default: undefined)
24
23
  * @property {RenderOptions} [renderOptions] - Default render options (default: undefined)
24
+ * @property {boolean} [throwErrors] - Throw error (default: false)
25
25
  */
26
26
  type WritrOptions = {
27
- openai?: string;
28
27
  renderOptions?: RenderOptions;
28
+ throwErrors?: boolean;
29
29
  };
30
30
  /**
31
31
  * Render options.
@@ -37,7 +37,7 @@ type WritrOptions = {
37
37
  * @property {boolean} [gfm] - Github flavor markdown (default: true)
38
38
  * @property {boolean} [math] - Math support (default: true)
39
39
  * @property {boolean} [mdx] - MDX support (default: true)
40
- * @property {boolean} [caching] - Caching (default: true)
40
+ * @property {boolean} [caching] - Caching (default: false)
41
41
  */
42
42
  type RenderOptions = {
43
43
  emoji?: boolean;
@@ -49,6 +49,14 @@ type RenderOptions = {
49
49
  mdx?: boolean;
50
50
  caching?: boolean;
51
51
  };
52
+ declare enum WritrHooks {
53
+ beforeRender = "beforeRender",
54
+ afterRender = "afterRender",
55
+ beforeSaveToFile = "beforeSaveToFile",
56
+ afterSaveToFile = "afterSaveToFile",
57
+ beforeLoadFromFile = "beforeLoadFromFile",
58
+ afterLoadFromFile = "afterLoadFromFile"
59
+ }
52
60
  declare class Writr extends Hookified {
53
61
  engine: unified.Processor<mdast.Root, mdast.Root, hast.Root, hast.Root, string>;
54
62
  private readonly _options;
@@ -126,6 +134,18 @@ declare class Writr extends Hookified {
126
134
  * @returns {string} The rendered HTML content.
127
135
  */
128
136
  renderSync(options?: RenderOptions): string;
137
+ /**
138
+ * Render the markdown content and save it to a file. If the directory doesn't exist it will be created.
139
+ * @param {string} filePath The file path to save the rendered markdown content to.
140
+ * @param {RenderOptions} [options] the render options.
141
+ */
142
+ renderToFile(filePath: string, options?: RenderOptions): Promise<void>;
143
+ /**
144
+ * Render the markdown content and save it to a file synchronously. If the directory doesn't exist it will be created.
145
+ * @param {string} filePath The file path to save the rendered markdown content to.
146
+ * @param {RenderOptions} [options] the render options.
147
+ */
148
+ renderToFileSync(filePath: string, options?: RenderOptions): void;
129
149
  /**
130
150
  * Render the markdown content to React.
131
151
  * @param {RenderOptions} [options] The render options.
@@ -170,4 +190,4 @@ declare class Writr extends Hookified {
170
190
  private mergeRenderOptions;
171
191
  }
172
192
 
173
- export { type RenderOptions, Writr, type WritrOptions };
193
+ export { type RenderOptions, Writr, WritrHooks, type WritrOptions };
package/dist/writr.js CHANGED
@@ -54,11 +54,20 @@ var WritrCache = class {
54
54
  };
55
55
 
56
56
  // src/writr.ts
57
+ var WritrHooks = /* @__PURE__ */ ((WritrHooks2) => {
58
+ WritrHooks2["beforeRender"] = "beforeRender";
59
+ WritrHooks2["afterRender"] = "afterRender";
60
+ WritrHooks2["beforeSaveToFile"] = "beforeSaveToFile";
61
+ WritrHooks2["afterSaveToFile"] = "afterSaveToFile";
62
+ WritrHooks2["beforeLoadFromFile"] = "beforeLoadFromFile";
63
+ WritrHooks2["afterLoadFromFile"] = "afterLoadFromFile";
64
+ return WritrHooks2;
65
+ })(WritrHooks || {});
57
66
  var Writr = class extends Hookified {
58
67
  engine = unified().use(remarkParse).use(remarkGfm).use(remarkToc).use(remarkEmoji).use(remarkRehype).use(rehypeSlug).use(remarkMath).use(rehypeKatex).use(rehypeHighlight).use(remarkMDX).use(rehypeStringify);
59
68
  // Stringify HTML
60
69
  _options = {
61
- openai: void 0,
70
+ throwErrors: false,
62
71
  renderOptions: {
63
72
  emoji: true,
64
73
  toc: true,
@@ -201,24 +210,33 @@ ${yamlString}---
201
210
  */
202
211
  async render(options) {
203
212
  try {
204
- let result = "";
205
- if (this.isCacheEnabled(options)) {
206
- const cached = this._cache.get(this._content, options);
207
- if (cached) {
208
- return cached;
209
- }
210
- }
211
213
  let { engine } = this;
212
214
  if (options) {
213
215
  options = { ...this._options.renderOptions, ...options };
214
216
  engine = this.createProcessor(options);
215
217
  }
216
- const file = await engine.process(this.body);
217
- result = String(file);
218
- if (this.isCacheEnabled(options)) {
219
- this._cache.set(this._content, result, options);
218
+ const renderData = {
219
+ content: this._content,
220
+ body: this.body,
221
+ options
222
+ };
223
+ await this.hook("beforeRender" /* beforeRender */, renderData);
224
+ const resultData = {
225
+ result: ""
226
+ };
227
+ if (this.isCacheEnabled(renderData.options)) {
228
+ const cached = this._cache.get(renderData.content, renderData.options);
229
+ if (cached) {
230
+ return cached;
231
+ }
232
+ }
233
+ const file = await engine.process(renderData.body);
234
+ resultData.result = String(file);
235
+ if (this.isCacheEnabled(renderData.options)) {
236
+ this._cache.set(renderData.content, resultData.result, renderData.options);
220
237
  }
221
- return result;
238
+ await this.hook("afterRender" /* afterRender */, resultData);
239
+ return resultData.result;
222
240
  } catch (error) {
223
241
  throw new Error(`Failed to render markdown: ${error.message}`);
224
242
  }
@@ -230,28 +248,74 @@ ${yamlString}---
230
248
  */
231
249
  renderSync(options) {
232
250
  try {
233
- let result = "";
234
- if (this.isCacheEnabled(options)) {
235
- const cached = this._cache.get(this._content, options);
236
- if (cached) {
237
- return cached;
238
- }
239
- }
240
251
  let { engine } = this;
241
252
  if (options) {
242
253
  options = { ...this._options.renderOptions, ...options };
243
254
  engine = this.createProcessor(options);
244
255
  }
245
- const file = engine.processSync(this.body);
246
- result = String(file);
247
- if (this.isCacheEnabled(options)) {
248
- this._cache.set(this._content, result, options);
256
+ const renderData = {
257
+ content: this._content,
258
+ body: this.body,
259
+ options
260
+ };
261
+ this.hook("beforeRender" /* beforeRender */, renderData);
262
+ const resultData = {
263
+ result: ""
264
+ };
265
+ if (this.isCacheEnabled(renderData.options)) {
266
+ const cached = this._cache.get(renderData.content, renderData.options);
267
+ if (cached) {
268
+ return cached;
269
+ }
270
+ }
271
+ const file = engine.processSync(renderData.body);
272
+ resultData.result = String(file);
273
+ if (this.isCacheEnabled(renderData.options)) {
274
+ this._cache.set(renderData.content, resultData.result, renderData.options);
249
275
  }
250
- return result;
276
+ this.hook("afterRender" /* afterRender */, resultData);
277
+ return resultData.result;
251
278
  } catch (error) {
252
279
  throw new Error(`Failed to render markdown: ${error.message}`);
253
280
  }
254
281
  }
282
+ /**
283
+ * Render the markdown content and save it to a file. If the directory doesn't exist it will be created.
284
+ * @param {string} filePath The file path to save the rendered markdown content to.
285
+ * @param {RenderOptions} [options] the render options.
286
+ */
287
+ async renderToFile(filePath, options) {
288
+ try {
289
+ const { writeFile, mkdir } = fs.promises;
290
+ const directoryPath = dirname(filePath);
291
+ const content = await this.render(options);
292
+ await mkdir(directoryPath, { recursive: true });
293
+ await writeFile(filePath, content, "utf8");
294
+ } catch (error) {
295
+ this.emit("error", error);
296
+ if (this._options.throwErrors) {
297
+ throw error;
298
+ }
299
+ }
300
+ }
301
+ /**
302
+ * Render the markdown content and save it to a file synchronously. If the directory doesn't exist it will be created.
303
+ * @param {string} filePath The file path to save the rendered markdown content to.
304
+ * @param {RenderOptions} [options] the render options.
305
+ */
306
+ renderToFileSync(filePath, options) {
307
+ try {
308
+ const directoryPath = dirname(filePath);
309
+ const content = this.renderSync(options);
310
+ fs.mkdirSync(directoryPath, { recursive: true });
311
+ fs.writeFileSync(filePath, content, "utf8");
312
+ } catch (error) {
313
+ this.emit("error", error);
314
+ if (this._options.throwErrors) {
315
+ throw error;
316
+ }
317
+ }
318
+ }
255
319
  /**
256
320
  * Render the markdown content to React.
257
321
  * @param {RenderOptions} [options] The render options.
@@ -295,10 +359,17 @@ ${yamlString}---
295
359
  * @returns {Promise<void>}
296
360
  */
297
361
  async saveToFile(filePath) {
298
- const { writeFile, mkdir } = fs.promises;
299
- const directoryPath = dirname(filePath);
300
- await mkdir(directoryPath, { recursive: true });
301
- await writeFile(filePath, this._content, "utf8");
362
+ try {
363
+ const { writeFile, mkdir } = fs.promises;
364
+ const directoryPath = dirname(filePath);
365
+ await mkdir(directoryPath, { recursive: true });
366
+ await writeFile(filePath, this._content, "utf8");
367
+ } catch (error) {
368
+ this.emit("error", error);
369
+ if (this._options.throwErrors) {
370
+ throw error;
371
+ }
372
+ }
302
373
  }
303
374
  /**
304
375
  * Save the markdown content to a file synchronously. If the directory doesn't exist it will be created.
@@ -306,9 +377,16 @@ ${yamlString}---
306
377
  * @returns {void}
307
378
  */
308
379
  saveToFileSync(filePath) {
309
- const directoryPath = dirname(filePath);
310
- fs.mkdirSync(directoryPath, { recursive: true });
311
- fs.writeFileSync(filePath, this._content, "utf8");
380
+ try {
381
+ const directoryPath = dirname(filePath);
382
+ fs.mkdirSync(directoryPath, { recursive: true });
383
+ fs.writeFileSync(filePath, this._content, "utf8");
384
+ } catch (error) {
385
+ this.emit("error", error);
386
+ if (this._options.throwErrors) {
387
+ throw error;
388
+ }
389
+ }
312
390
  }
313
391
  isCacheEnabled(options) {
314
392
  if (options?.caching !== void 0) {
@@ -344,8 +422,8 @@ ${yamlString}---
344
422
  return processor;
345
423
  }
346
424
  mergeOptions(current, options) {
347
- if (options.openai) {
348
- current.openai = options.openai;
425
+ if (options.throwErrors !== void 0) {
426
+ current.throwErrors = options.throwErrors;
349
427
  }
350
428
  if (options.renderOptions) {
351
429
  current.renderOptions ??= {};
@@ -382,5 +460,6 @@ ${yamlString}---
382
460
  }
383
461
  };
384
462
  export {
385
- Writr
463
+ Writr,
464
+ WritrHooks
386
465
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writr",
3
- "version": "4.1.4",
3
+ "version": "4.3.0",
4
4
  "description": "Markdown Rendering Simplified",
5
5
  "type": "module",
6
6
  "main": "./dist/writr.js",
@@ -53,11 +53,11 @@
53
53
  "website:serve": "rimraf ./site/README.md ./site/dist && npx docula serve -s ./site -o ./site/dist"
54
54
  },
55
55
  "dependencies": {
56
- "cacheable": "^1.8.2",
57
- "hookified": "^1.5.0",
58
- "html-react-parser": "^5.1.18",
56
+ "cacheable": "^1.8.7",
57
+ "hookified": "^1.6.0",
58
+ "html-react-parser": "^5.2.2",
59
59
  "js-yaml": "^4.1.0",
60
- "react": "^18.3.1",
60
+ "react": "^19.0.0",
61
61
  "rehype-highlight": "^7.0.1",
62
62
  "rehype-katex": "^7.0.1",
63
63
  "rehype-slug": "^6.0.0",
@@ -73,17 +73,17 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@types/js-yaml": "^4.0.9",
76
- "@types/node": "^22.8.4",
77
- "@types/react": "^18.3.12",
78
- "@vitest/coverage-v8": "^2.1.4",
79
- "docula": "^0.9.4",
76
+ "@types/node": "^22.10.2",
77
+ "@types/react": "^19.0.2",
78
+ "@vitest/coverage-v8": "^2.1.8",
79
+ "docula": "^0.9.6",
80
80
  "rimraf": "^6.0.1",
81
81
  "ts-node": "^10.9.2",
82
82
  "tsup": "^8.3.5",
83
- "typescript": "^5.6.3",
84
- "vitest": "^2.1.4",
85
- "webpack": "^5.95.0",
86
- "xo": "^0.59.3"
83
+ "typescript": "^5.7.2",
84
+ "vitest": "^2.1.8",
85
+ "webpack": "^5.97.1",
86
+ "xo": "^0.60.0"
87
87
  },
88
88
  "xo": {
89
89
  "ignores": [