writr 4.5.1 → 5.0.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
@@ -56,7 +56,18 @@ plugins and working with the processor directly.
56
56
  - [`.loadFromFileSync(filePath: string)`](#loadfromfilesyncfilepath-string)
57
57
  - [`.saveToFile(filePath: string)`](#savetofilefilepath-string)
58
58
  - [`.saveToFileSync(filePath: string)`](#savetofilesyncfilepath-string)
59
+ - [Caching On Render](#caching-on-render)
60
+ - [GitHub Flavored Markdown (GFM)](#github-flavored-markdown-gfm)
61
+ - [GFM Features](#gfm-features)
62
+ - [Using GFM](#using-gfm)
63
+ - [Disabling GFM](#disabling-gfm)
59
64
  - [Hooks](#hooks)
65
+ - [Emitters](#emitters)
66
+ - [Error Events](#error-events)
67
+ - [Listening to Error Events](#listening-to-error-events)
68
+ - [Methods that Emit Errors](#methods-that-emit-errors)
69
+ - [Error Event Examples](#error-event-examples)
70
+ - [Event Emitter Methods](#event-emitter-methods)
60
71
  - [ESM and Node Version Support](#esm-and-node-version-support)
61
72
  - [Code of Conduct and Contributing](#code-of-conduct-and-contributing)
62
73
  - [License](#license)
@@ -413,6 +424,117 @@ writr.cache.store.lruSize = 100;
413
424
  writr.cache.store.ttl = '5m'; // setting it to 5 minutes
414
425
  ```
415
426
 
427
+ # GitHub Flavored Markdown (GFM)
428
+
429
+ Writr includes full support for [GitHub Flavored Markdown](https://github.github.com/gfm/) (GFM) through the `remark-gfm` and `remark-github-blockquote-alert` plugins. GFM is enabled by default and adds several powerful features to standard Markdown.
430
+
431
+ ## GFM Features
432
+
433
+ When GFM is enabled (which it is by default), you get access to the following features:
434
+
435
+ ### Tables
436
+
437
+ Create tables using pipes and hyphens:
438
+
439
+ ```markdown
440
+ | Feature | Supported |
441
+ |---------|-----------|
442
+ | Tables | Yes |
443
+ | Alerts | Yes |
444
+ ```
445
+
446
+ ### Strikethrough
447
+
448
+ Use `~~` to create strikethrough text:
449
+
450
+ ```markdown
451
+ ~~This text is crossed out~~
452
+ ```
453
+
454
+ ### Task Lists
455
+
456
+ Create interactive checkboxes:
457
+
458
+ ```markdown
459
+ - [x] Completed task
460
+ - [ ] Incomplete task
461
+ - [ ] Another task
462
+ ```
463
+
464
+ ### Autolinks
465
+
466
+ URLs are automatically converted to clickable links:
467
+
468
+ ```markdown
469
+ https://github.com
470
+ ```
471
+
472
+ ### GitHub Blockquote Alerts
473
+
474
+ GitHub-style alerts are supported to emphasize critical information. These are blockquote-based admonitions that render with special styling:
475
+
476
+ ```markdown
477
+ > [!NOTE]
478
+ > Useful information that users should know, even when skimming content.
479
+
480
+ > [!TIP]
481
+ > Helpful advice for doing things better or more easily.
482
+
483
+ > [!IMPORTANT]
484
+ > Key information users need to know to achieve their goal.
485
+
486
+ > [!WARNING]
487
+ > Urgent info that needs immediate user attention to avoid problems.
488
+
489
+ > [!CAUTION]
490
+ > Advises about risks or negative outcomes of certain actions.
491
+ ```
492
+
493
+ ## Using GFM
494
+
495
+ GFM is enabled by default. Here's an example:
496
+
497
+ ```javascript
498
+ import { Writr } from 'writr';
499
+
500
+ const markdown = `
501
+ # Task List Example
502
+
503
+ - [x] Learn Writr basics
504
+ - [ ] Master GFM features
505
+
506
+ > [!NOTE]
507
+ > GitHub Flavored Markdown is enabled by default!
508
+
509
+ | Feature | Status |
510
+ |---------|--------|
511
+ | GFM | ✓ |
512
+ `;
513
+
514
+ const writr = new Writr(markdown);
515
+ const html = await writr.render(); // Renders with full GFM support
516
+ ```
517
+
518
+ ## Disabling GFM
519
+
520
+ If you need to disable GFM features, you can set `gfm: false` in the render options:
521
+
522
+ ```javascript
523
+ import { Writr } from 'writr';
524
+
525
+ const writr = new Writr('~~strikethrough~~ text');
526
+
527
+ // Disable GFM
528
+ const html = await writr.render({ gfm: false });
529
+ // Output: <p>~~strikethrough~~ text</p>
530
+
531
+ // With GFM enabled (default)
532
+ const htmlWithGfm = await writr.render({ gfm: true });
533
+ // Output: <p><del>strikethrough</del> text</p>
534
+ ```
535
+
536
+ Note: When GFM is disabled, GitHub blockquote alerts will not be processed and will render as regular blockquotes.
537
+
416
538
  # Hooks
417
539
 
418
540
  Hooks are a way to add additional parsing to the render pipeline. You can add hooks to the the Writr instance. Here is an example of adding a hook to the instance of Writr:
@@ -476,6 +598,135 @@ export type loadFromFileData = {
476
598
 
477
599
  This is called when you call `loadFromFile`, `loadFromFileSync`.
478
600
 
601
+ # Emitters
602
+
603
+ Writr extends the [Hookified](https://github.com/jaredwray/hookified) class, which provides event emitter capabilities. This means you can listen to events emitted by Writr during its lifecycle, particularly error events.
604
+
605
+ ## Error Events
606
+
607
+ Writr emits an `error` event whenever an error occurs in any of its methods. This provides a centralized way to handle errors without wrapping every method call in a try/catch block.
608
+
609
+ ### Listening to Error Events
610
+
611
+ You can listen to error events using the `.on()` method:
612
+
613
+ ```javascript
614
+ import { Writr } from 'writr';
615
+
616
+ const writr = new Writr('# Hello World');
617
+
618
+ // Listen for any errors
619
+ writr.on('error', (error) => {
620
+ console.error('An error occurred:', error.message);
621
+ // Handle the error appropriately
622
+ // Log to error tracking service, display to user, etc.
623
+ });
624
+
625
+ // Now when any error occurs, your listener will be notified
626
+ try {
627
+ await writr.render();
628
+ } catch (error) {
629
+ // Error is also thrown, so you can handle it here too
630
+ }
631
+ ```
632
+
633
+ ### Methods that Emit Errors
634
+
635
+ The following methods emit error events when they fail:
636
+
637
+ **Rendering Methods:**
638
+ - `render()` - Emits error before throwing when markdown rendering fails
639
+ - `renderSync()` - Emits error before throwing when markdown rendering fails
640
+ - `renderReact()` - Emits error before throwing when React rendering fails
641
+ - `renderReactSync()` - Emits error before throwing when React rendering fails
642
+
643
+ **Validation Methods:**
644
+ - `validate()` - Emits error when validation fails (returns error in result object)
645
+ - `validateSync()` - Emits error when validation fails (returns error in result object)
646
+
647
+ **File Operations:**
648
+ - `renderToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
649
+ - `renderToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
650
+ - `loadFromFile()` - Emits error when file reading fails (does not throw if `throwErrors: false`)
651
+ - `loadFromFileSync()` - Emits error when file reading fails (does not throw if `throwErrors: false`)
652
+ - `saveToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
653
+ - `saveToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
654
+
655
+ **Front Matter Operations:**
656
+ - `frontMatter` getter - Emits error when YAML parsing fails
657
+ - `frontMatter` setter - Emits error when YAML serialization fails
658
+
659
+ ### Error Event Examples
660
+
661
+ **Example 1: Global Error Handler**
662
+
663
+ ```javascript
664
+ import { Writr } from 'writr';
665
+
666
+ const writr = new Writr();
667
+
668
+ // Set up a global error handler
669
+ writr.on('error', (error) => {
670
+ // Log to your monitoring service
671
+ console.error('Writr error:', error);
672
+
673
+ // Send to error tracking (e.g., Sentry, Rollbar)
674
+ // errorTracker.captureException(error);
675
+ });
676
+
677
+ // All errors will be emitted to the listener above
678
+ await writr.loadFromFile('./content.md');
679
+ const html = await writr.render();
680
+ ```
681
+
682
+ **Example 2: Validation with Error Listening**
683
+
684
+ ```javascript
685
+ import { Writr } from 'writr';
686
+
687
+ const writr = new Writr('# My Content');
688
+ let lastError = null;
689
+
690
+ writr.on('error', (error) => {
691
+ lastError = error;
692
+ });
693
+
694
+ const result = await writr.validate();
695
+
696
+ if (!result.valid) {
697
+ console.log('Validation failed');
698
+ console.log('Error details:', lastError);
699
+ // result.error is also available
700
+ }
701
+ ```
702
+
703
+ **Example 3: File Operations Without Try/Catch**
704
+
705
+ ```javascript
706
+ import { Writr } from 'writr';
707
+
708
+ const writr = new Writr('# Content', { throwErrors: false });
709
+
710
+ writr.on('error', (error) => {
711
+ console.error('File operation failed:', error.message);
712
+ // Handle gracefully - maybe use default content
713
+ });
714
+
715
+ // Won't throw, but will emit error event if file doesn't exist
716
+ await writr.loadFromFile('./maybe-missing.md');
717
+ ```
718
+
719
+ ### Event Emitter Methods
720
+
721
+ Since Writr extends Hookified, you have access to standard event emitter methods:
722
+
723
+ - `writr.on(event, handler)` - Add an event listener
724
+ - `writr.once(event, handler)` - Add a one-time event listener
725
+ - `writr.off(event, handler)` - Remove an event listener
726
+ - `writr.emit(event, data)` - Emit an event (used internally)
727
+
728
+ For more information about event handling capabilities, see the [Hookified documentation](https://github.com/jaredwray/hookified).
729
+
479
730
  # ESM and Node Version Support
480
731
 
481
732
  This package is ESM only and tested on the current lts version and its previous. Please don't open issues for questions regarding CommonJS / ESM or previous Nodejs versions.
package/dist/writr.d.ts CHANGED
@@ -37,8 +37,8 @@ type WritrOptions = {
37
37
  * @property {boolean} [highlight] - Code highlighting (default: true)
38
38
  * @property {boolean} [gfm] - Github flavor markdown (default: true)
39
39
  * @property {boolean} [math] - Math support (default: true)
40
- * @property {boolean} [mdx] - MDX support (default: true)
41
- * @property {boolean} [caching] - Caching (default: false)
40
+ * @property {boolean} [mdx] - MDX support (default: false)
41
+ * @property {boolean} [caching] - Caching (default: true)
42
42
  */
43
43
  type RenderOptions = {
44
44
  emoji?: boolean;
package/dist/writr.js CHANGED
@@ -10,6 +10,7 @@ import rehypeSlug from "rehype-slug";
10
10
  import rehypeStringify from "rehype-stringify";
11
11
  import remarkEmoji from "remark-emoji";
12
12
  import remarkGfm from "remark-gfm";
13
+ import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert";
13
14
  import remarkMath from "remark-math";
14
15
  import remarkMDX from "remark-mdx";
15
16
  import remarkParse from "remark-parse";
@@ -64,7 +65,7 @@ var WritrHooks = /* @__PURE__ */ ((WritrHooks2) => {
64
65
  return WritrHooks2;
65
66
  })(WritrHooks || {});
66
67
  var Writr = class extends Hookified {
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);
68
+ engine = unified().use(remarkParse).use(remarkGfm).use(remarkToc).use(remarkEmoji).use(remarkRehype).use(rehypeSlug).use(remarkMath).use(rehypeKatex).use(rehypeHighlight).use(rehypeStringify);
68
69
  // Stringify HTML
69
70
  _options = {
70
71
  throwErrors: false,
@@ -75,8 +76,8 @@ var Writr = class extends Hookified {
75
76
  highlight: true,
76
77
  gfm: true,
77
78
  math: true,
78
- mdx: true,
79
- caching: false
79
+ mdx: false,
80
+ caching: true
80
81
  }
81
82
  };
82
83
  _content = "";
@@ -191,12 +192,16 @@ var Writr = class extends Hookified {
191
192
  */
192
193
  // biome-ignore lint/suspicious/noExplicitAny: expected
193
194
  set frontMatter(data) {
194
- const frontMatter = this.frontMatterRaw;
195
- const yamlString = yaml.dump(data);
196
- const newFrontMatter = `---
195
+ try {
196
+ const frontMatter = this.frontMatterRaw;
197
+ const yamlString = yaml.dump(data);
198
+ const newFrontMatter = `---
197
199
  ${yamlString}---
198
200
  `;
199
- this._content = this._content.replace(frontMatter, newFrontMatter);
201
+ this._content = this._content.replace(frontMatter, newFrontMatter);
202
+ } catch (error) {
203
+ this.emit("error", error);
204
+ }
200
205
  }
201
206
  /**
202
207
  * Get the front matter value for a key.
@@ -245,6 +250,7 @@ ${yamlString}---
245
250
  await this.hook("afterRender" /* afterRender */, resultData);
246
251
  return resultData.result;
247
252
  } catch (error) {
253
+ this.emit("error", error);
248
254
  throw new Error(`Failed to render markdown: ${error.message}`);
249
255
  }
250
256
  }
@@ -287,6 +293,7 @@ ${yamlString}---
287
293
  this.hook("afterRender" /* afterRender */, resultData);
288
294
  return resultData.result;
289
295
  } catch (error) {
296
+ this.emit("error", error);
290
297
  throw new Error(`Failed to render markdown: ${error.message}`);
291
298
  }
292
299
  }
@@ -317,6 +324,7 @@ ${yamlString}---
317
324
  }
318
325
  return { valid: true };
319
326
  } catch (error) {
327
+ this.emit("error", error);
320
328
  if (content !== void 0) {
321
329
  this._content = originalContent;
322
330
  }
@@ -350,6 +358,7 @@ ${yamlString}---
350
358
  }
351
359
  return { valid: true };
352
360
  } catch (error) {
361
+ this.emit("error", error);
353
362
  if (content !== void 0) {
354
363
  this._content = originalContent;
355
364
  }
@@ -410,8 +419,13 @@ ${yamlString}---
410
419
  * @returns {Promise<string | React.JSX.Element | React.JSX.Element[]>} The rendered React content.
411
420
  */
412
421
  async renderReact(options, reactParseOptions) {
413
- const html = await this.render(options);
414
- return parse(html, reactParseOptions);
422
+ try {
423
+ const html = await this.render(options);
424
+ return parse(html, reactParseOptions);
425
+ } catch (error) {
426
+ this.emit("error", error);
427
+ throw new Error(`Failed to render React: ${error.message}`);
428
+ }
415
429
  }
416
430
  /**
417
431
  * Render the markdown content to React synchronously.
@@ -420,8 +434,13 @@ ${yamlString}---
420
434
  * @returns {string | React.JSX.Element | React.JSX.Element[]} The rendered React content.
421
435
  */
422
436
  renderReactSync(options, reactParseOptions) {
423
- const html = this.renderSync(options);
424
- return parse(html, reactParseOptions);
437
+ try {
438
+ const html = this.renderSync(options);
439
+ return parse(html, reactParseOptions);
440
+ } catch (error) {
441
+ this.emit("error", error);
442
+ throw new Error(`Failed to render React: ${error.message}`);
443
+ }
425
444
  }
426
445
  /**
427
446
  * Load markdown content from a file.
@@ -520,6 +539,7 @@ ${yamlString}---
520
539
  const processor = unified().use(remarkParse);
521
540
  if (options.gfm) {
522
541
  processor.use(remarkGfm);
542
+ processor.use(remarkGithubBlockquoteAlert);
523
543
  }
524
544
  if (options.toc) {
525
545
  processor.use(remarkToc, { heading: "toc|table of contents" });
@@ -585,3 +605,4 @@ export {
585
605
  Writr,
586
606
  WritrHooks
587
607
  };
608
+ /* v8 ignore next -- @preserve */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writr",
3
- "version": "4.5.1",
3
+ "version": "5.0.0",
4
4
  "description": "Markdown Rendering Simplified",
5
5
  "type": "module",
6
6
  "main": "./dist/writr.js",
@@ -55,17 +55,18 @@
55
55
  "website:serve": "rimraf ./site/README.md ./site/dist && npx docula serve -s ./site -o ./site/dist"
56
56
  },
57
57
  "dependencies": {
58
- "cacheable": "^2.0.2",
59
- "hookified": "^1.12.1",
60
- "html-react-parser": "^5.2.6",
58
+ "cacheable": "^2.1.1",
59
+ "hookified": "^1.12.2",
60
+ "html-react-parser": "^5.2.7",
61
61
  "js-yaml": "^4.1.0",
62
- "react": "^19.1.1",
62
+ "react": "^19.2.0",
63
63
  "rehype-highlight": "^7.0.2",
64
64
  "rehype-katex": "^7.0.1",
65
65
  "rehype-slug": "^6.0.0",
66
66
  "rehype-stringify": "^10.0.1",
67
67
  "remark-emoji": "^5.0.2",
68
68
  "remark-gfm": "^4.0.1",
69
+ "remark-github-blockquote-alert": "^2.0.0",
69
70
  "remark-math": "^6.0.0",
70
71
  "remark-mdx": "^3.1.1",
71
72
  "remark-parse": "^11.0.0",
@@ -74,23 +75,17 @@
74
75
  "unified": "^11.0.5"
75
76
  },
76
77
  "devDependencies": {
77
- "@biomejs/biome": "^2.2.4",
78
+ "@biomejs/biome": "^2.3.1",
78
79
  "@types/js-yaml": "^4.0.9",
79
- "@types/node": "^24.5.2",
80
- "@types/react": "^19.1.13",
81
- "@vitest/coverage-v8": "^3.2.4",
82
- "docula": "^0.30.0",
80
+ "@types/node": "^24.9.1",
81
+ "@types/react": "^19.2.2",
82
+ "@vitest/coverage-v8": "^4.0.4",
83
+ "docula": "^0.31.0",
83
84
  "rimraf": "^6.0.1",
84
85
  "ts-node": "^10.9.2",
85
86
  "tsup": "^8.5.0",
86
- "typescript": "^5.9.2",
87
- "vitest": "^3.2.4",
88
- "webpack": "^5.101.3"
89
- },
90
- "xo": {
91
- "ignores": [
92
- "docula.config.*"
93
- ]
87
+ "typescript": "^5.9.3",
88
+ "vitest": "^4.0.4"
94
89
  },
95
90
  "files": [
96
91
  "dist",