writr 6.1.0 → 6.1.2

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/dist/writr.js DELETED
@@ -1,1031 +0,0 @@
1
- // src/writr.ts
2
- import fs from "fs";
3
- import { dirname } from "path";
4
- import { Hookified } from "hookified";
5
- import parse from "html-react-parser";
6
- import * as yaml from "js-yaml";
7
- import rehypeHighlight from "rehype-highlight";
8
- import rehypeKatex from "rehype-katex";
9
- import rehypeRaw from "rehype-raw";
10
- import rehypeSlug from "rehype-slug";
11
- import rehypeStringify from "rehype-stringify";
12
- import remarkEmoji from "remark-emoji";
13
- import remarkGfm from "remark-gfm";
14
- import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert";
15
- import remarkMath from "remark-math";
16
- import remarkMDX from "remark-mdx";
17
- import remarkParse from "remark-parse";
18
- import remarkRehype from "remark-rehype";
19
- import remarkToc from "remark-toc";
20
- import { unified } from "unified";
21
-
22
- // src/writr-cache.ts
23
- import { CacheableMemory } from "cacheable";
24
- import { Hashery } from "hashery";
25
- var WritrCache = class {
26
- _store = new CacheableMemory();
27
- _hashStore = new CacheableMemory();
28
- _hash = new Hashery();
29
- get store() {
30
- return this._store;
31
- }
32
- get hashStore() {
33
- return this._hashStore;
34
- }
35
- get(markdown, options) {
36
- const key = this.hash(markdown, options);
37
- return this._store.get(key);
38
- }
39
- set(markdown, value, options) {
40
- const key = this.hash(markdown, options);
41
- this._store.set(key, value);
42
- }
43
- clear() {
44
- this._store.clear();
45
- this._hashStore.clear();
46
- }
47
- hash(markdown, options) {
48
- const sanitizedOptions = this.sanitizeOptions(options);
49
- const content = { markdown, options: sanitizedOptions };
50
- const key = JSON.stringify(content);
51
- const result = this._hashStore.get(key);
52
- if (result) {
53
- return result;
54
- }
55
- const hash = this._hash.toHashSync(content);
56
- this._hashStore.set(key, hash);
57
- return hash;
58
- }
59
- /**
60
- * Sanitizes render options to only include serializable properties for caching.
61
- * This prevents issues with structuredClone when options contain Promises, functions, or circular references.
62
- * @param {RenderOptions} [options] The render options to sanitize
63
- * @returns {RenderOptions | undefined} A new object with only the known RenderOptions properties
64
- */
65
- sanitizeOptions(options) {
66
- if (!options) {
67
- return void 0;
68
- }
69
- const sanitized = {};
70
- if (options.emoji !== void 0) {
71
- sanitized.emoji = options.emoji;
72
- }
73
- if (options.toc !== void 0) {
74
- sanitized.toc = options.toc;
75
- }
76
- if (options.slug !== void 0) {
77
- sanitized.slug = options.slug;
78
- }
79
- if (options.highlight !== void 0) {
80
- sanitized.highlight = options.highlight;
81
- }
82
- if (options.gfm !== void 0) {
83
- sanitized.gfm = options.gfm;
84
- }
85
- if (options.math !== void 0) {
86
- sanitized.math = options.math;
87
- }
88
- if (options.mdx !== void 0) {
89
- sanitized.mdx = options.mdx;
90
- }
91
- if (options.caching !== void 0) {
92
- sanitized.caching = options.caching;
93
- }
94
- return sanitized;
95
- }
96
- };
97
-
98
- // src/types.ts
99
- var WritrHooks = /* @__PURE__ */ ((WritrHooks2) => {
100
- WritrHooks2["beforeRender"] = "beforeRender";
101
- WritrHooks2["afterRender"] = "afterRender";
102
- WritrHooks2["saveToFile"] = "saveToFile";
103
- WritrHooks2["renderToFile"] = "renderToFile";
104
- WritrHooks2["loadFromFile"] = "loadFromFile";
105
- return WritrHooks2;
106
- })(WritrHooks || {});
107
-
108
- // src/writr-ai.ts
109
- import { generateText, Output } from "ai";
110
- import { z } from "zod";
111
-
112
- // src/writr-ai-cache.ts
113
- import { CacheableMemory as CacheableMemory2 } from "cacheable";
114
- import { Hashery as Hashery2 } from "hashery";
115
- var WritrAICache = class {
116
- _store = new CacheableMemory2();
117
- _hashStore = new CacheableMemory2();
118
- _hash = new Hashery2();
119
- get store() {
120
- return this._store;
121
- }
122
- get hashStore() {
123
- return this._hashStore;
124
- }
125
- get(key, context) {
126
- const hash = this.hash(key, context);
127
- return this._store.get(hash);
128
- }
129
- set(key, context, value) {
130
- const hash = this.hash(key, context);
131
- this._store.set(hash, value);
132
- }
133
- hash(key, context) {
134
- const content = { key, context };
135
- const cacheKey = JSON.stringify(content);
136
- const result = this._hashStore.get(cacheKey);
137
- if (result) {
138
- return result;
139
- }
140
- const hash = this._hash.toHashSync(content);
141
- this._hashStore.set(cacheKey, hash);
142
- return hash;
143
- }
144
- clear() {
145
- this._store.clear();
146
- this._hashStore.clear();
147
- }
148
- };
149
-
150
- // src/writr-ai.ts
151
- var AVERAGE_WORDS_PER_MINUTE = 200;
152
- var WritrAI = class {
153
- /**
154
- * Creates a new WritrAI instance bound to a specific Writr document.
155
- *
156
- * @param writr - The base Writr instance this AI helper operates on.
157
- * @param options - The AI model and optional cache/prompt settings.
158
- */
159
- constructor(writr, options) {
160
- this.writr = writr;
161
- this.model = options.model;
162
- this.prompts = options.prompts ?? {};
163
- this.cache = options.cache ? new WritrAICache() : void 0;
164
- }
165
- /**
166
- * The AI SDK model used for all generation requests.
167
- */
168
- model;
169
- /**
170
- * The prompt templates used by this WritrAI instance.
171
- */
172
- prompts;
173
- /**
174
- * Optional in-memory cache for generated AI results.
175
- */
176
- cache;
177
- /**
178
- * Generates metadata for the current markdown document.
179
- *
180
- * @param options - Controls which metadata fields should be generated.
181
- * @returns A metadata object for the current document.
182
- */
183
- async getMetadata(options) {
184
- const cacheKey = `metadata:${JSON.stringify(options ?? {})}`;
185
- const cached = this.cache?.get(cacheKey, this.writr.content);
186
- if (cached) {
187
- return cached;
188
- }
189
- const fields = this.resolveMetadataFields(options);
190
- const result = {};
191
- if (fields.includes("wordCount")) {
192
- result.wordCount = this.computeWordCount();
193
- }
194
- if (fields.includes("readingTime")) {
195
- result.readingTime = this.computeReadingTime();
196
- }
197
- const aiFields = fields.filter(
198
- (f) => f !== "wordCount" && f !== "readingTime"
199
- );
200
- if (aiFields.length > 0) {
201
- const schema = this.buildMetadataSchema(aiFields);
202
- const prompt = this.prompts.metadata ?? "Analyze the following markdown document and generate metadata for it. Be concise and accurate.";
203
- const { output } = await generateText({
204
- model: this.model,
205
- output: Output.object({ schema }),
206
- prompt: `${prompt}
207
-
208
- ---
209
-
210
- ${this.writr.content}`
211
- });
212
- Object.assign(result, output);
213
- }
214
- this.cache?.set(cacheKey, this.writr.content, result);
215
- return result;
216
- }
217
- /**
218
- * Generates SEO metadata for the current markdown document.
219
- *
220
- * @param options - Controls which SEO fields should be generated.
221
- * @returns An SEO metadata object for the current document.
222
- */
223
- async getSEO(options) {
224
- const cacheKey = `seo:${JSON.stringify(options ?? {})}`;
225
- const cached = this.cache?.get(cacheKey, this.writr.content);
226
- if (cached) {
227
- return cached;
228
- }
229
- const fields = this.resolveSEOFields(options);
230
- const schema = this.buildSEOSchema(fields);
231
- const prompt = this.prompts.seo ?? "Analyze the following markdown document and generate SEO metadata for it. Be concise and accurate.";
232
- const { output } = await generateText({
233
- model: this.model,
234
- output: Output.object({ schema }),
235
- prompt: `${prompt}
236
-
237
- ---
238
-
239
- ${this.writr.content}`
240
- });
241
- const result = output;
242
- this.cache?.set(cacheKey, this.writr.content, result);
243
- return result;
244
- }
245
- /**
246
- * Generates a translated version of the current document.
247
- *
248
- * @param options - Translation settings including target locale.
249
- * @returns A new translated Writr instance.
250
- */
251
- async getTranslation(options) {
252
- const cacheKey = `translation:${JSON.stringify(options)}`;
253
- const cached = this.cache?.get(cacheKey, this.writr.content);
254
- if (cached) {
255
- return new Writr(cached);
256
- }
257
- const fromClause = options.from ? ` from ${options.from}` : "";
258
- const frontMatterClause = options.translateFrontMatter ? " Also translate any string values in the YAML frontmatter." : " Preserve the YAML frontmatter exactly as-is without translating it.";
259
- const prompt = this.prompts.translation ?? `Translate the following markdown document${fromClause} to ${options.to}.${frontMatterClause} Preserve all markdown formatting, links, code blocks, and structure. Return only the translated markdown document with no additional commentary.`;
260
- let { text } = await generateText({
261
- model: this.model,
262
- prompt: `${prompt}
263
-
264
- ---
265
-
266
- ${this.writr.content}`
267
- });
268
- text = this.stripCodeFence(text);
269
- this.cache?.set(cacheKey, this.writr.content, text);
270
- return new Writr(text);
271
- }
272
- /**
273
- * Generates metadata and applies it to the document frontmatter.
274
- *
275
- * @param options - Controls generation, overwrite behavior, and field mapping.
276
- * @returns A result object describing what metadata was generated and applied.
277
- */
278
- async applyMetadata(options) {
279
- const generated = await this.getMetadata(options?.generate);
280
- const frontMatter = { ...this.writr.frontMatter };
281
- const fieldMap = options?.fieldMap ?? {};
282
- const overwrite = options?.overwrite;
283
- const applied = [];
284
- const overwritten = [];
285
- const skipped = [];
286
- const overwriteSet = Array.isArray(overwrite) ? new Set(overwrite) : void 0;
287
- for (const key of Object.keys(generated)) {
288
- const value = generated[key];
289
- if (value === void 0) {
290
- continue;
291
- }
292
- const frontMatterKey = fieldMap[key] ?? key;
293
- const exists = frontMatterKey in frontMatter;
294
- if (!exists) {
295
- frontMatter[frontMatterKey] = value;
296
- applied.push(key);
297
- } else if (overwrite === true || overwriteSet?.has(key)) {
298
- frontMatter[frontMatterKey] = value;
299
- overwritten.push(key);
300
- } else {
301
- skipped.push(key);
302
- }
303
- }
304
- this.writr.frontMatter = frontMatter;
305
- return {
306
- writr: this.writr,
307
- generated,
308
- applied,
309
- overwritten,
310
- skipped
311
- };
312
- }
313
- resolveMetadataFields(options) {
314
- const allFields = [
315
- "title",
316
- "tags",
317
- "keywords",
318
- "description",
319
- "preview",
320
- "summary",
321
- "category",
322
- "topic",
323
- "audience",
324
- "difficulty",
325
- "readingTime",
326
- "wordCount"
327
- ];
328
- if (!options) {
329
- return allFields;
330
- }
331
- return allFields.filter(
332
- (field) => options[field] === true
333
- );
334
- }
335
- resolveSEOFields(options) {
336
- const allFields = ["slug", "openGraph"];
337
- if (!options) {
338
- return allFields;
339
- }
340
- return allFields.filter(
341
- (field) => options[field] === true
342
- );
343
- }
344
- // biome-ignore lint/suspicious/noExplicitAny: dynamic schema construction
345
- buildMetadataSchema(fields) {
346
- const fieldSet = new Set(fields);
347
- const entries = [];
348
- if (fieldSet.has("title")) {
349
- entries.push([
350
- "title",
351
- z.string().describe("The best-fit title for the document")
352
- ]);
353
- }
354
- if (fieldSet.has("tags")) {
355
- entries.push([
356
- "tags",
357
- z.array(z.string()).describe("Human-friendly labels for organizing the document")
358
- ]);
359
- }
360
- if (fieldSet.has("keywords")) {
361
- entries.push([
362
- "keywords",
363
- z.array(z.string()).describe("Search-oriented terms related to the document")
364
- ]);
365
- }
366
- if (fieldSet.has("description")) {
367
- entries.push([
368
- "description",
369
- z.string().describe("A concise meta-style description of the document")
370
- ]);
371
- }
372
- if (fieldSet.has("preview")) {
373
- entries.push([
374
- "preview",
375
- z.string().describe("A short teaser or preview of the content")
376
- ]);
377
- }
378
- if (fieldSet.has("summary")) {
379
- entries.push([
380
- "summary",
381
- z.string().describe("A slightly longer overview of the document")
382
- ]);
383
- }
384
- if (fieldSet.has("category")) {
385
- entries.push([
386
- "category",
387
- z.string().describe('A broad grouping such as "docs", "guide", or "blog"')
388
- ]);
389
- }
390
- if (fieldSet.has("topic")) {
391
- entries.push([
392
- "topic",
393
- z.string().describe("The primary subject the document is about")
394
- ]);
395
- }
396
- if (fieldSet.has("audience")) {
397
- entries.push([
398
- "audience",
399
- z.string().describe(
400
- 'The intended audience such as "developers" or "beginners"'
401
- )
402
- ]);
403
- }
404
- if (fieldSet.has("difficulty")) {
405
- entries.push([
406
- "difficulty",
407
- z.enum(["beginner", "intermediate", "advanced"]).describe(
408
- "The estimated skill level required to understand the document"
409
- )
410
- ]);
411
- }
412
- return z.object(Object.fromEntries(entries));
413
- }
414
- // biome-ignore lint/suspicious/noExplicitAny: dynamic schema construction
415
- buildSEOSchema(fields) {
416
- const fieldSet = new Set(fields);
417
- const entries = [];
418
- if (fieldSet.has("slug")) {
419
- entries.push([
420
- "slug",
421
- z.string().describe("A URL-safe identifier for the document")
422
- ]);
423
- }
424
- if (fieldSet.has("openGraph")) {
425
- entries.push([
426
- "openGraph",
427
- z.object({
428
- title: z.string().describe("The social sharing title"),
429
- description: z.string().describe("The social sharing description"),
430
- image: z.string().describe("The image URL for social sharing").nullable()
431
- }).describe("Open Graph metadata")
432
- ]);
433
- }
434
- return z.object(Object.fromEntries(entries));
435
- }
436
- computeWordCount() {
437
- const text = this.writr.body;
438
- const words = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]*`/g, "").replace(/[#*_[\]()>~|-]/g, " ").split(/\s+/).filter((word) => word.length > 0);
439
- return words.length;
440
- }
441
- computeReadingTime() {
442
- const wordCount = this.computeWordCount();
443
- return Math.max(1, Math.ceil(wordCount / AVERAGE_WORDS_PER_MINUTE));
444
- }
445
- stripCodeFence(text) {
446
- const trimmed = text.trim();
447
- const match = /^```\w*\n([\s\S]*?)```$/.exec(trimmed);
448
- return match ? match[1].trim() : text;
449
- }
450
- };
451
-
452
- // src/writr.ts
453
- var Writr = class extends Hookified {
454
- engine = unified().use(remarkParse).use(remarkGfm).use(remarkToc).use(remarkEmoji).use(remarkRehype).use(rehypeSlug).use(remarkMath).use(rehypeKatex).use(rehypeHighlight).use(rehypeStringify);
455
- // Stringify HTML
456
- _options = {
457
- renderOptions: {
458
- emoji: true,
459
- toc: true,
460
- slug: true,
461
- highlight: true,
462
- gfm: true,
463
- math: true,
464
- mdx: false,
465
- rawHtml: false,
466
- caching: true
467
- }
468
- };
469
- _content = "";
470
- _cache = new WritrCache();
471
- _ai;
472
- /**
473
- * Initialize Writr. Accepts a string or options object.
474
- * @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.
475
- * @param {WritrOptions} [arguments2] This is if you send in the content in the first argument and also want to send in options.
476
- *
477
- * @example
478
- * const writr = new Writr('Hello, world!', {caching: false});
479
- */
480
- constructor(arguments1, arguments2) {
481
- const options = typeof arguments1 === "object" ? arguments1 : arguments2;
482
- super({
483
- throwOnEmitError: options?.throwOnEmitError,
484
- throwOnHookError: options?.throwOnHookError,
485
- throwOnEmptyListeners: options?.throwOnEmptyListeners,
486
- eventLogger: options?.eventLogger
487
- });
488
- if (typeof arguments1 === "string") {
489
- this._content = arguments1;
490
- } else if (arguments1) {
491
- this._options = this.mergeOptions(this._options, arguments1);
492
- if (this._options.renderOptions) {
493
- this.engine = this.createProcessor(this._options.renderOptions);
494
- }
495
- }
496
- if (arguments2) {
497
- this._options = this.mergeOptions(this._options, arguments2);
498
- if (this._options.renderOptions) {
499
- this.engine = this.createProcessor(this._options.renderOptions);
500
- }
501
- }
502
- if (this._options.ai) {
503
- this._ai = new WritrAI(this, this._options.ai);
504
- }
505
- }
506
- /**
507
- * Get the options.
508
- * @type {WritrOptions}
509
- */
510
- get options() {
511
- return this._options;
512
- }
513
- /**
514
- * Get the WritrAI instance if AI options were provided.
515
- * @type {WritrAI | undefined}
516
- */
517
- get ai() {
518
- return this._ai;
519
- }
520
- /**
521
- * Get the Content. This is the markdown content and front matter if it exists.
522
- * @type {WritrOptions}
523
- */
524
- get content() {
525
- return this._content;
526
- }
527
- /**
528
- * Set the Content. This is the markdown content and front matter if it exists.
529
- * @type {WritrOptions}
530
- */
531
- set content(value) {
532
- this._content = value;
533
- }
534
- /**
535
- * Get the cache.
536
- * @type {WritrCache}
537
- */
538
- get cache() {
539
- return this._cache;
540
- }
541
- /**
542
- * Get the front matter raw content.
543
- * @type {string} The front matter content including the delimiters.
544
- */
545
- get frontMatterRaw() {
546
- if (!this._content.trimStart().startsWith("---")) {
547
- return "";
548
- }
549
- const match = /^\s*(---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$))/.exec(
550
- this._content
551
- );
552
- if (match) {
553
- return match[1];
554
- }
555
- return "";
556
- }
557
- /**
558
- * Get the body content without the front matter.
559
- * @type {string} The markdown content without the front matter.
560
- */
561
- get body() {
562
- const frontMatter = this.frontMatterRaw;
563
- if (frontMatter === "") {
564
- return this._content;
565
- }
566
- return this._content.slice(this._content.indexOf(frontMatter) + frontMatter.length).trim();
567
- }
568
- /**
569
- * Get the markdown content. This is an alias for the body property.
570
- * @type {string} The markdown content.
571
- */
572
- get markdown() {
573
- return this.body;
574
- }
575
- /**
576
- * Get the front matter content as an object.
577
- * @type {Record<string, any>} The front matter content as an object.
578
- */
579
- // biome-ignore lint/suspicious/noExplicitAny: expected
580
- get frontMatter() {
581
- const frontMatter = this.frontMatterRaw;
582
- const match = /^---\s*([\s\S]*?)\s*---\s*/.exec(frontMatter);
583
- if (match) {
584
- try {
585
- return yaml.load(match[1].trim());
586
- } catch (error) {
587
- this.emit("error", error);
588
- }
589
- }
590
- return {};
591
- }
592
- /**
593
- * Set the front matter content as an object.
594
- * @type {Record<string, any>} The front matter content as an object.
595
- */
596
- // biome-ignore lint/suspicious/noExplicitAny: expected
597
- set frontMatter(data) {
598
- try {
599
- const frontMatter = this.frontMatterRaw;
600
- const yamlString = yaml.dump(data);
601
- const newFrontMatter = `---
602
- ${yamlString}---
603
- `;
604
- this._content = this._content.replace(frontMatter, newFrontMatter);
605
- } catch (error) {
606
- this.emit("error", error);
607
- }
608
- }
609
- /**
610
- * Get the front matter value for a key.
611
- * @param {string} key The key to get the value for.
612
- * @returns {T} The value for the key.
613
- */
614
- getFrontMatterValue(key) {
615
- return this.frontMatter[key];
616
- }
617
- /**
618
- * Render the markdown content to HTML.
619
- * @param {RenderOptions} [options] The render options.
620
- * @returns {Promise<string>} The rendered HTML content.
621
- */
622
- async render(options) {
623
- try {
624
- let { engine } = this;
625
- if (options) {
626
- options = { ...this._options.renderOptions, ...options };
627
- engine = this.createProcessor(options);
628
- }
629
- const renderData = {
630
- content: this._content,
631
- body: this.body,
632
- options
633
- };
634
- await this.hook("beforeRender" /* beforeRender */, renderData);
635
- const resultData = {
636
- result: ""
637
- };
638
- if (this.isCacheEnabled(renderData.options)) {
639
- const cached = this._cache.get(renderData.content, renderData.options);
640
- if (cached) {
641
- return cached;
642
- }
643
- }
644
- const file = await engine.process(renderData.body);
645
- resultData.result = String(file);
646
- if (this.isCacheEnabled(renderData.options)) {
647
- this._cache.set(
648
- renderData.content,
649
- resultData.result,
650
- renderData.options
651
- );
652
- }
653
- await this.hook("afterRender" /* afterRender */, resultData);
654
- return resultData.result;
655
- } catch (error) {
656
- this.emit("error", error);
657
- }
658
- return "";
659
- }
660
- /**
661
- * Render the markdown content to HTML synchronously.
662
- * @param {RenderOptions} [options] The render options.
663
- * @returns {string} The rendered HTML content.
664
- */
665
- renderSync(options) {
666
- try {
667
- let { engine } = this;
668
- if (options) {
669
- options = { ...this._options.renderOptions, ...options };
670
- engine = this.createProcessor(options);
671
- }
672
- const renderData = {
673
- content: this._content,
674
- body: this.body,
675
- options
676
- };
677
- this.hook("beforeRender" /* beforeRender */, renderData);
678
- const resultData = {
679
- result: ""
680
- };
681
- if (this.isCacheEnabled(renderData.options)) {
682
- const cached = this._cache.get(renderData.content, renderData.options);
683
- if (cached) {
684
- return cached;
685
- }
686
- }
687
- const file = engine.processSync(renderData.body);
688
- resultData.result = String(file);
689
- if (this.isCacheEnabled(renderData.options)) {
690
- this._cache.set(
691
- renderData.content,
692
- resultData.result,
693
- renderData.options
694
- );
695
- }
696
- this.hook("afterRender" /* afterRender */, resultData);
697
- return resultData.result;
698
- } catch (error) {
699
- this.emit("error", error);
700
- }
701
- return "";
702
- }
703
- /**
704
- * Validate the markdown content by attempting to render it.
705
- * @param {string} [content] The markdown content to validate. If not provided, uses the current content.
706
- * @param {RenderOptions} [options] The render options.
707
- * @returns {Promise<WritrValidateResult>} An object with a valid boolean and optional error.
708
- */
709
- async validate(content, options) {
710
- const originalContent = this._content;
711
- try {
712
- if (content !== void 0) {
713
- this._content = content;
714
- }
715
- let { engine } = this;
716
- if (options) {
717
- options = {
718
- ...this._options.renderOptions,
719
- ...options,
720
- caching: false
721
- };
722
- engine = this.createProcessor(options);
723
- }
724
- await engine.run(engine.parse(this.body));
725
- if (content !== void 0) {
726
- this._content = originalContent;
727
- }
728
- return { valid: true };
729
- } catch (error) {
730
- if (content !== void 0) {
731
- this._content = originalContent;
732
- }
733
- return { valid: false, error };
734
- }
735
- }
736
- /**
737
- * Validate the markdown content by attempting to render it synchronously.
738
- * @param {string} [content] The markdown content to validate. If not provided, uses the current content.
739
- * @param {RenderOptions} [options] The render options.
740
- * @returns {WritrValidateResult} An object with a valid boolean and optional error.
741
- */
742
- validateSync(content, options) {
743
- const originalContent = this._content;
744
- try {
745
- if (content !== void 0) {
746
- this._content = content;
747
- }
748
- let { engine } = this;
749
- if (options) {
750
- options = {
751
- ...this._options.renderOptions,
752
- ...options,
753
- caching: false
754
- };
755
- engine = this.createProcessor(options);
756
- }
757
- engine.runSync(engine.parse(this.body));
758
- if (content !== void 0) {
759
- this._content = originalContent;
760
- }
761
- return { valid: true };
762
- } catch (error) {
763
- this.emit("error", error);
764
- if (content !== void 0) {
765
- this._content = originalContent;
766
- }
767
- return { valid: false, error };
768
- }
769
- }
770
- /**
771
- * Render the markdown content and save it to a file. If the directory doesn't exist it will be created.
772
- * @param {string} filePath The file path to save the rendered markdown content to.
773
- * @param {RenderOptions} [options] the render options.
774
- */
775
- async renderToFile(filePath, options) {
776
- try {
777
- const { writeFile, mkdir } = fs.promises;
778
- const directoryPath = dirname(filePath);
779
- const content = await this.render(options);
780
- await mkdir(directoryPath, { recursive: true });
781
- const data = {
782
- filePath,
783
- content
784
- };
785
- await this.hook("renderToFile" /* renderToFile */, data);
786
- await writeFile(data.filePath, data.content);
787
- } catch (error) {
788
- this.emit("error", error);
789
- }
790
- }
791
- /**
792
- * Render the markdown content and save it to a file synchronously. If the directory doesn't exist it will be created.
793
- * @param {string} filePath The file path to save the rendered markdown content to.
794
- * @param {RenderOptions} [options] the render options.
795
- */
796
- renderToFileSync(filePath, options) {
797
- try {
798
- const directoryPath = dirname(filePath);
799
- const content = this.renderSync(options);
800
- fs.mkdirSync(directoryPath, { recursive: true });
801
- const data = {
802
- filePath,
803
- content
804
- };
805
- this.hook("renderToFile" /* renderToFile */, data);
806
- fs.writeFileSync(data.filePath, data.content);
807
- } catch (error) {
808
- this.emit("error", error);
809
- }
810
- }
811
- /**
812
- * Render the markdown content to React.
813
- * @param {RenderOptions} [options] The render options.
814
- * @param {HTMLReactParserOptions} [reactParseOptions] The HTML React parser options.
815
- * @returns {Promise<string | React.JSX.Element | React.JSX.Element[]>} The rendered React content.
816
- */
817
- async renderReact(options, reactParseOptions) {
818
- try {
819
- const html = await this.render(options);
820
- return parse(html, reactParseOptions);
821
- } catch (error) {
822
- this.emit("error", error);
823
- }
824
- return "";
825
- }
826
- /**
827
- * Render the markdown content to React synchronously.
828
- * @param {RenderOptions} [options] The render options.
829
- * @param {HTMLReactParserOptions} [reactParseOptions] The HTML React parser options.
830
- * @returns {string | React.JSX.Element | React.JSX.Element[]} The rendered React content.
831
- */
832
- renderReactSync(options, reactParseOptions) {
833
- try {
834
- const html = this.renderSync(options);
835
- return parse(html, reactParseOptions);
836
- } catch (error) {
837
- this.emit("error", error);
838
- }
839
- return "";
840
- }
841
- /**
842
- * Load markdown content from a file.
843
- * @param {string} filePath The file path to load the markdown content from.
844
- * @returns {Promise<void>}
845
- */
846
- async loadFromFile(filePath) {
847
- try {
848
- const { readFile } = fs.promises;
849
- const data = {
850
- content: ""
851
- };
852
- data.content = await readFile(filePath, "utf8");
853
- await this.hook("loadFromFile" /* loadFromFile */, data);
854
- this._content = data.content;
855
- } catch (error) {
856
- this.emit("error", error);
857
- }
858
- }
859
- /**
860
- * Load markdown content from a file synchronously.
861
- * @param {string} filePath The file path to load the markdown content from.
862
- * @returns {void}
863
- */
864
- loadFromFileSync(filePath) {
865
- try {
866
- const data = {
867
- content: ""
868
- };
869
- data.content = fs.readFileSync(filePath, "utf8");
870
- this.hook("loadFromFile" /* loadFromFile */, data);
871
- this._content = data.content;
872
- } catch (error) {
873
- this.emit("error", error);
874
- }
875
- }
876
- /**
877
- * Save the markdown content to a file. If the directory doesn't exist it will be created.
878
- * @param {string} filePath The file path to save the markdown content to.
879
- * @returns {Promise<void>}
880
- */
881
- async saveToFile(filePath) {
882
- try {
883
- const { writeFile, mkdir } = fs.promises;
884
- const directoryPath = dirname(filePath);
885
- await mkdir(directoryPath, { recursive: true });
886
- const data = {
887
- filePath,
888
- content: this._content
889
- };
890
- await this.hook("saveToFile" /* saveToFile */, data);
891
- await writeFile(data.filePath, data.content);
892
- } catch (error) {
893
- this.emit("error", error);
894
- }
895
- }
896
- /**
897
- * Save the markdown content to a file synchronously. If the directory doesn't exist it will be created.
898
- * @param {string} filePath The file path to save the markdown content to.
899
- * @returns {void}
900
- */
901
- saveToFileSync(filePath) {
902
- try {
903
- const directoryPath = dirname(filePath);
904
- fs.mkdirSync(directoryPath, { recursive: true });
905
- const data = {
906
- filePath,
907
- content: this._content
908
- };
909
- this.hook("saveToFile" /* saveToFile */, data);
910
- fs.writeFileSync(data.filePath, data.content);
911
- } catch (error) {
912
- this.emit("error", error);
913
- }
914
- }
915
- mergeOptions(current, options) {
916
- if (options.throwOnEmitError !== void 0) {
917
- this.throwOnEmitError = options.throwOnEmitError;
918
- }
919
- if (options.renderOptions) {
920
- current.renderOptions ??= {};
921
- this.mergeRenderOptions(current.renderOptions, options.renderOptions);
922
- }
923
- if (options.ai) {
924
- current.ai = options.ai;
925
- }
926
- return current;
927
- }
928
- isCacheEnabled(options) {
929
- if (options?.caching !== void 0) {
930
- return options.caching;
931
- }
932
- return this._options?.renderOptions?.caching ?? false;
933
- }
934
- // biome-ignore lint/suspicious/noExplicitAny: expected unified processor
935
- createProcessor(options) {
936
- const processor = unified().use(remarkParse);
937
- if (options.gfm) {
938
- processor.use(remarkGfm);
939
- processor.use(remarkGithubBlockquoteAlert);
940
- }
941
- if (options.toc) {
942
- processor.use(remarkToc, { heading: "toc|table of contents" });
943
- }
944
- if (options.emoji) {
945
- processor.use(remarkEmoji);
946
- }
947
- if (options.mdx) {
948
- processor.use(remarkMDX);
949
- }
950
- const rehypeOptions = {};
951
- if (options.rawHtml) {
952
- rehypeOptions.allowDangerousHtml = true;
953
- }
954
- if (options.mdx) {
955
- rehypeOptions.handlers = {
956
- mdxJsxFlowElement: mdxJsxHandler,
957
- mdxJsxTextElement: mdxJsxHandler
958
- };
959
- }
960
- processor.use(remarkRehype, rehypeOptions);
961
- if (options.rawHtml) {
962
- processor.use(rehypeRaw);
963
- }
964
- if (options.slug) {
965
- processor.use(rehypeSlug);
966
- }
967
- if (options.highlight) {
968
- processor.use(rehypeHighlight);
969
- }
970
- if (options.math) {
971
- processor.use(remarkMath).use(rehypeKatex);
972
- }
973
- processor.use(rehypeStringify);
974
- return processor;
975
- }
976
- mergeRenderOptions(current, options) {
977
- if (options.emoji !== void 0) {
978
- current.emoji = options.emoji;
979
- }
980
- if (options.toc !== void 0) {
981
- current.toc = options.toc;
982
- }
983
- if (options.slug !== void 0) {
984
- current.slug = options.slug;
985
- }
986
- if (options.highlight !== void 0) {
987
- current.highlight = options.highlight;
988
- }
989
- if (options.gfm !== void 0) {
990
- current.gfm = options.gfm;
991
- }
992
- if (options.math !== void 0) {
993
- current.math = options.math;
994
- }
995
- if (options.mdx !== void 0) {
996
- current.mdx = options.mdx;
997
- }
998
- if (options.rawHtml !== void 0) {
999
- current.rawHtml = options.rawHtml;
1000
- }
1001
- if (options.caching !== void 0) {
1002
- current.caching = options.caching;
1003
- }
1004
- return current;
1005
- }
1006
- };
1007
- function mdxJsxHandler(state, node) {
1008
- const properties = {};
1009
- for (const attr of node.attributes) {
1010
- if (attr.type === "mdxJsxAttribute") {
1011
- if (attr.value === null) {
1012
- properties[attr.name] = true;
1013
- } else if (typeof attr.value === "string") {
1014
- properties[attr.name] = attr.value;
1015
- }
1016
- }
1017
- }
1018
- return {
1019
- type: "element",
1020
- tagName: node.name ?? "div",
1021
- properties,
1022
- children: state.all(node)
1023
- };
1024
- }
1025
- export {
1026
- Writr,
1027
- WritrAI,
1028
- WritrAICache,
1029
- WritrHooks
1030
- };
1031
- /* v8 ignore next -- @preserve */