writr 5.0.4 → 6.0.1
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 +357 -43
- package/dist/writr.d.ts +357 -24
- package/dist/writr.js +376 -28
- package/package.json +17 -10
package/dist/writr.js
CHANGED
|
@@ -94,7 +94,7 @@ var WritrCache = class {
|
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
// src/
|
|
97
|
+
// src/types.ts
|
|
98
98
|
var WritrHooks = /* @__PURE__ */ ((WritrHooks2) => {
|
|
99
99
|
WritrHooks2["beforeRender"] = "beforeRender";
|
|
100
100
|
WritrHooks2["afterRender"] = "afterRender";
|
|
@@ -103,11 +103,356 @@ var WritrHooks = /* @__PURE__ */ ((WritrHooks2) => {
|
|
|
103
103
|
WritrHooks2["loadFromFile"] = "loadFromFile";
|
|
104
104
|
return WritrHooks2;
|
|
105
105
|
})(WritrHooks || {});
|
|
106
|
+
|
|
107
|
+
// src/writr-ai.ts
|
|
108
|
+
import { generateText, Output } from "ai";
|
|
109
|
+
import { z } from "zod";
|
|
110
|
+
|
|
111
|
+
// src/writr-ai-cache.ts
|
|
112
|
+
import { CacheableMemory as CacheableMemory2 } from "cacheable";
|
|
113
|
+
import { Hashery as Hashery2 } from "hashery";
|
|
114
|
+
var WritrAICache = class {
|
|
115
|
+
_store = new CacheableMemory2();
|
|
116
|
+
_hashStore = new CacheableMemory2();
|
|
117
|
+
_hash = new Hashery2();
|
|
118
|
+
get store() {
|
|
119
|
+
return this._store;
|
|
120
|
+
}
|
|
121
|
+
get hashStore() {
|
|
122
|
+
return this._hashStore;
|
|
123
|
+
}
|
|
124
|
+
get(key, context) {
|
|
125
|
+
const hash = this.hash(key, context);
|
|
126
|
+
return this._store.get(hash);
|
|
127
|
+
}
|
|
128
|
+
set(key, context, value) {
|
|
129
|
+
const hash = this.hash(key, context);
|
|
130
|
+
this._store.set(hash, value);
|
|
131
|
+
}
|
|
132
|
+
hash(key, context) {
|
|
133
|
+
const content = { key, context };
|
|
134
|
+
const cacheKey = JSON.stringify(content);
|
|
135
|
+
const result = this._hashStore.get(cacheKey);
|
|
136
|
+
if (result) {
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
const hash = this._hash.toHashSync(content);
|
|
140
|
+
this._hashStore.set(cacheKey, hash);
|
|
141
|
+
return hash;
|
|
142
|
+
}
|
|
143
|
+
clear() {
|
|
144
|
+
this._store.clear();
|
|
145
|
+
this._hashStore.clear();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/writr-ai.ts
|
|
150
|
+
var AVERAGE_WORDS_PER_MINUTE = 200;
|
|
151
|
+
var WritrAI = class {
|
|
152
|
+
/**
|
|
153
|
+
* Creates a new WritrAI instance bound to a specific Writr document.
|
|
154
|
+
*
|
|
155
|
+
* @param writr - The base Writr instance this AI helper operates on.
|
|
156
|
+
* @param options - The AI model and optional cache/prompt settings.
|
|
157
|
+
*/
|
|
158
|
+
constructor(writr, options) {
|
|
159
|
+
this.writr = writr;
|
|
160
|
+
this.model = options.model;
|
|
161
|
+
this.prompts = options.prompts ?? {};
|
|
162
|
+
this.cache = options.cache ? new WritrAICache() : void 0;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The AI SDK model used for all generation requests.
|
|
166
|
+
*/
|
|
167
|
+
model;
|
|
168
|
+
/**
|
|
169
|
+
* The prompt templates used by this WritrAI instance.
|
|
170
|
+
*/
|
|
171
|
+
prompts;
|
|
172
|
+
/**
|
|
173
|
+
* Optional in-memory cache for generated AI results.
|
|
174
|
+
*/
|
|
175
|
+
cache;
|
|
176
|
+
/**
|
|
177
|
+
* Generates metadata for the current markdown document.
|
|
178
|
+
*
|
|
179
|
+
* @param options - Controls which metadata fields should be generated.
|
|
180
|
+
* @returns A metadata object for the current document.
|
|
181
|
+
*/
|
|
182
|
+
async getMetadata(options) {
|
|
183
|
+
const cacheKey = `metadata:${JSON.stringify(options ?? {})}`;
|
|
184
|
+
const cached = this.cache?.get(cacheKey, this.writr.content);
|
|
185
|
+
if (cached) {
|
|
186
|
+
return cached;
|
|
187
|
+
}
|
|
188
|
+
const fields = this.resolveMetadataFields(options);
|
|
189
|
+
const result = {};
|
|
190
|
+
if (fields.includes("wordCount")) {
|
|
191
|
+
result.wordCount = this.computeWordCount();
|
|
192
|
+
}
|
|
193
|
+
if (fields.includes("readingTime")) {
|
|
194
|
+
result.readingTime = this.computeReadingTime();
|
|
195
|
+
}
|
|
196
|
+
const aiFields = fields.filter(
|
|
197
|
+
(f) => f !== "wordCount" && f !== "readingTime"
|
|
198
|
+
);
|
|
199
|
+
if (aiFields.length > 0) {
|
|
200
|
+
const schema = this.buildMetadataSchema(aiFields);
|
|
201
|
+
const prompt = this.prompts.metadata ?? "Analyze the following markdown document and generate metadata for it. Be concise and accurate.";
|
|
202
|
+
const { output } = await generateText({
|
|
203
|
+
model: this.model,
|
|
204
|
+
output: Output.object({ schema }),
|
|
205
|
+
prompt: `${prompt}
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
${this.writr.content}`
|
|
210
|
+
});
|
|
211
|
+
Object.assign(result, output);
|
|
212
|
+
}
|
|
213
|
+
this.cache?.set(cacheKey, this.writr.content, result);
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Generates SEO metadata for the current markdown document.
|
|
218
|
+
*
|
|
219
|
+
* @param options - Controls which SEO fields should be generated.
|
|
220
|
+
* @returns An SEO metadata object for the current document.
|
|
221
|
+
*/
|
|
222
|
+
async getSEO(options) {
|
|
223
|
+
const cacheKey = `seo:${JSON.stringify(options ?? {})}`;
|
|
224
|
+
const cached = this.cache?.get(cacheKey, this.writr.content);
|
|
225
|
+
if (cached) {
|
|
226
|
+
return cached;
|
|
227
|
+
}
|
|
228
|
+
const fields = this.resolveSEOFields(options);
|
|
229
|
+
const schema = this.buildSEOSchema(fields);
|
|
230
|
+
const prompt = this.prompts.seo ?? "Analyze the following markdown document and generate SEO metadata for it. Be concise and accurate.";
|
|
231
|
+
const { output } = await generateText({
|
|
232
|
+
model: this.model,
|
|
233
|
+
output: Output.object({ schema }),
|
|
234
|
+
prompt: `${prompt}
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
${this.writr.content}`
|
|
239
|
+
});
|
|
240
|
+
const result = output;
|
|
241
|
+
this.cache?.set(cacheKey, this.writr.content, result);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Generates a translated version of the current document.
|
|
246
|
+
*
|
|
247
|
+
* @param options - Translation settings including target locale.
|
|
248
|
+
* @returns A new translated Writr instance.
|
|
249
|
+
*/
|
|
250
|
+
async getTranslation(options) {
|
|
251
|
+
const cacheKey = `translation:${JSON.stringify(options)}`;
|
|
252
|
+
const cached = this.cache?.get(cacheKey, this.writr.content);
|
|
253
|
+
if (cached) {
|
|
254
|
+
return new Writr(cached);
|
|
255
|
+
}
|
|
256
|
+
const fromClause = options.from ? ` from ${options.from}` : "";
|
|
257
|
+
const frontMatterClause = options.translateFrontMatter ? " Also translate any string values in the YAML frontmatter." : " Preserve the YAML frontmatter exactly as-is without translating it.";
|
|
258
|
+
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.`;
|
|
259
|
+
let { text } = await generateText({
|
|
260
|
+
model: this.model,
|
|
261
|
+
prompt: `${prompt}
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
${this.writr.content}`
|
|
266
|
+
});
|
|
267
|
+
text = this.stripCodeFence(text);
|
|
268
|
+
this.cache?.set(cacheKey, this.writr.content, text);
|
|
269
|
+
return new Writr(text);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Generates metadata and applies it to the document frontmatter.
|
|
273
|
+
*
|
|
274
|
+
* @param options - Controls generation, overwrite behavior, and field mapping.
|
|
275
|
+
* @returns A result object describing what metadata was generated and applied.
|
|
276
|
+
*/
|
|
277
|
+
async applyMetadata(options) {
|
|
278
|
+
const generated = await this.getMetadata(options?.generate);
|
|
279
|
+
const frontMatter = { ...this.writr.frontMatter };
|
|
280
|
+
const fieldMap = options?.fieldMap ?? {};
|
|
281
|
+
const overwrite = options?.overwrite;
|
|
282
|
+
const applied = [];
|
|
283
|
+
const overwritten = [];
|
|
284
|
+
const skipped = [];
|
|
285
|
+
const overwriteSet = Array.isArray(overwrite) ? new Set(overwrite) : void 0;
|
|
286
|
+
for (const key of Object.keys(generated)) {
|
|
287
|
+
const value = generated[key];
|
|
288
|
+
if (value === void 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const frontMatterKey = fieldMap[key] ?? key;
|
|
292
|
+
const exists = frontMatterKey in frontMatter;
|
|
293
|
+
if (!exists) {
|
|
294
|
+
frontMatter[frontMatterKey] = value;
|
|
295
|
+
applied.push(key);
|
|
296
|
+
} else if (overwrite === true || overwriteSet?.has(key)) {
|
|
297
|
+
frontMatter[frontMatterKey] = value;
|
|
298
|
+
overwritten.push(key);
|
|
299
|
+
} else {
|
|
300
|
+
skipped.push(key);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.writr.frontMatter = frontMatter;
|
|
304
|
+
return {
|
|
305
|
+
writr: this.writr,
|
|
306
|
+
generated,
|
|
307
|
+
applied,
|
|
308
|
+
overwritten,
|
|
309
|
+
skipped
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
resolveMetadataFields(options) {
|
|
313
|
+
const allFields = [
|
|
314
|
+
"title",
|
|
315
|
+
"tags",
|
|
316
|
+
"keywords",
|
|
317
|
+
"description",
|
|
318
|
+
"preview",
|
|
319
|
+
"summary",
|
|
320
|
+
"category",
|
|
321
|
+
"topic",
|
|
322
|
+
"audience",
|
|
323
|
+
"difficulty",
|
|
324
|
+
"readingTime",
|
|
325
|
+
"wordCount"
|
|
326
|
+
];
|
|
327
|
+
if (!options) {
|
|
328
|
+
return allFields;
|
|
329
|
+
}
|
|
330
|
+
return allFields.filter(
|
|
331
|
+
(field) => options[field] === true
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
resolveSEOFields(options) {
|
|
335
|
+
const allFields = ["slug", "openGraph"];
|
|
336
|
+
if (!options) {
|
|
337
|
+
return allFields;
|
|
338
|
+
}
|
|
339
|
+
return allFields.filter(
|
|
340
|
+
(field) => options[field] === true
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic schema construction
|
|
344
|
+
buildMetadataSchema(fields) {
|
|
345
|
+
const fieldSet = new Set(fields);
|
|
346
|
+
const entries = [];
|
|
347
|
+
if (fieldSet.has("title")) {
|
|
348
|
+
entries.push([
|
|
349
|
+
"title",
|
|
350
|
+
z.string().describe("The best-fit title for the document")
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
if (fieldSet.has("tags")) {
|
|
354
|
+
entries.push([
|
|
355
|
+
"tags",
|
|
356
|
+
z.array(z.string()).describe("Human-friendly labels for organizing the document")
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
if (fieldSet.has("keywords")) {
|
|
360
|
+
entries.push([
|
|
361
|
+
"keywords",
|
|
362
|
+
z.array(z.string()).describe("Search-oriented terms related to the document")
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
if (fieldSet.has("description")) {
|
|
366
|
+
entries.push([
|
|
367
|
+
"description",
|
|
368
|
+
z.string().describe("A concise meta-style description of the document")
|
|
369
|
+
]);
|
|
370
|
+
}
|
|
371
|
+
if (fieldSet.has("preview")) {
|
|
372
|
+
entries.push([
|
|
373
|
+
"preview",
|
|
374
|
+
z.string().describe("A short teaser or preview of the content")
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
if (fieldSet.has("summary")) {
|
|
378
|
+
entries.push([
|
|
379
|
+
"summary",
|
|
380
|
+
z.string().describe("A slightly longer overview of the document")
|
|
381
|
+
]);
|
|
382
|
+
}
|
|
383
|
+
if (fieldSet.has("category")) {
|
|
384
|
+
entries.push([
|
|
385
|
+
"category",
|
|
386
|
+
z.string().describe('A broad grouping such as "docs", "guide", or "blog"')
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
if (fieldSet.has("topic")) {
|
|
390
|
+
entries.push([
|
|
391
|
+
"topic",
|
|
392
|
+
z.string().describe("The primary subject the document is about")
|
|
393
|
+
]);
|
|
394
|
+
}
|
|
395
|
+
if (fieldSet.has("audience")) {
|
|
396
|
+
entries.push([
|
|
397
|
+
"audience",
|
|
398
|
+
z.string().describe(
|
|
399
|
+
'The intended audience such as "developers" or "beginners"'
|
|
400
|
+
)
|
|
401
|
+
]);
|
|
402
|
+
}
|
|
403
|
+
if (fieldSet.has("difficulty")) {
|
|
404
|
+
entries.push([
|
|
405
|
+
"difficulty",
|
|
406
|
+
z.enum(["beginner", "intermediate", "advanced"]).describe(
|
|
407
|
+
"The estimated skill level required to understand the document"
|
|
408
|
+
)
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
411
|
+
return z.object(Object.fromEntries(entries));
|
|
412
|
+
}
|
|
413
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic schema construction
|
|
414
|
+
buildSEOSchema(fields) {
|
|
415
|
+
const fieldSet = new Set(fields);
|
|
416
|
+
const entries = [];
|
|
417
|
+
if (fieldSet.has("slug")) {
|
|
418
|
+
entries.push([
|
|
419
|
+
"slug",
|
|
420
|
+
z.string().describe("A URL-safe identifier for the document")
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
if (fieldSet.has("openGraph")) {
|
|
424
|
+
entries.push([
|
|
425
|
+
"openGraph",
|
|
426
|
+
z.object({
|
|
427
|
+
title: z.string().describe("The social sharing title"),
|
|
428
|
+
description: z.string().describe("The social sharing description"),
|
|
429
|
+
image: z.string().describe("The image URL for social sharing").nullable()
|
|
430
|
+
}).describe("Open Graph metadata")
|
|
431
|
+
]);
|
|
432
|
+
}
|
|
433
|
+
return z.object(Object.fromEntries(entries));
|
|
434
|
+
}
|
|
435
|
+
computeWordCount() {
|
|
436
|
+
const text = this.writr.body;
|
|
437
|
+
const words = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]*`/g, "").replace(/[#*_[\]()>~|-]/g, " ").split(/\s+/).filter((word) => word.length > 0);
|
|
438
|
+
return words.length;
|
|
439
|
+
}
|
|
440
|
+
computeReadingTime() {
|
|
441
|
+
const wordCount = this.computeWordCount();
|
|
442
|
+
return Math.max(1, Math.ceil(wordCount / AVERAGE_WORDS_PER_MINUTE));
|
|
443
|
+
}
|
|
444
|
+
stripCodeFence(text) {
|
|
445
|
+
const trimmed = text.trim();
|
|
446
|
+
const match = /^```\w*\n([\s\S]*?)```$/.exec(trimmed);
|
|
447
|
+
return match ? match[1].trim() : text;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/writr.ts
|
|
106
452
|
var Writr = class extends Hookified {
|
|
107
453
|
engine = unified().use(remarkParse).use(remarkGfm).use(remarkToc).use(remarkEmoji).use(remarkRehype).use(rehypeSlug).use(remarkMath).use(rehypeKatex).use(rehypeHighlight).use(rehypeStringify);
|
|
108
454
|
// Stringify HTML
|
|
109
455
|
_options = {
|
|
110
|
-
throwErrors: false,
|
|
111
456
|
renderOptions: {
|
|
112
457
|
emoji: true,
|
|
113
458
|
toc: true,
|
|
@@ -121,6 +466,7 @@ var Writr = class extends Hookified {
|
|
|
121
466
|
};
|
|
122
467
|
_content = "";
|
|
123
468
|
_cache = new WritrCache();
|
|
469
|
+
_ai;
|
|
124
470
|
/**
|
|
125
471
|
* Initialize Writr. Accepts a string or options object.
|
|
126
472
|
* @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.
|
|
@@ -130,7 +476,13 @@ var Writr = class extends Hookified {
|
|
|
130
476
|
* const writr = new Writr('Hello, world!', {caching: false});
|
|
131
477
|
*/
|
|
132
478
|
constructor(arguments1, arguments2) {
|
|
133
|
-
|
|
479
|
+
const options = typeof arguments1 === "object" ? arguments1 : arguments2;
|
|
480
|
+
super({
|
|
481
|
+
throwOnEmitError: options?.throwOnEmitError,
|
|
482
|
+
throwOnHookError: options?.throwOnHookError,
|
|
483
|
+
throwOnEmptyListeners: options?.throwOnEmptyListeners,
|
|
484
|
+
eventLogger: options?.eventLogger
|
|
485
|
+
});
|
|
134
486
|
if (typeof arguments1 === "string") {
|
|
135
487
|
this._content = arguments1;
|
|
136
488
|
} else if (arguments1) {
|
|
@@ -145,6 +497,9 @@ var Writr = class extends Hookified {
|
|
|
145
497
|
this.engine = this.createProcessor(this._options.renderOptions);
|
|
146
498
|
}
|
|
147
499
|
}
|
|
500
|
+
if (this._options.ai) {
|
|
501
|
+
this._ai = new WritrAI(this, this._options.ai);
|
|
502
|
+
}
|
|
148
503
|
}
|
|
149
504
|
/**
|
|
150
505
|
* Get the options.
|
|
@@ -153,6 +508,13 @@ var Writr = class extends Hookified {
|
|
|
153
508
|
get options() {
|
|
154
509
|
return this._options;
|
|
155
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Get the WritrAI instance if AI options were provided.
|
|
513
|
+
* @type {WritrAI | undefined}
|
|
514
|
+
*/
|
|
515
|
+
get ai() {
|
|
516
|
+
return this._ai;
|
|
517
|
+
}
|
|
156
518
|
/**
|
|
157
519
|
* Get the Content. This is the markdown content and front matter if it exists.
|
|
158
520
|
* @type {WritrOptions}
|
|
@@ -290,8 +652,8 @@ ${yamlString}---
|
|
|
290
652
|
return resultData.result;
|
|
291
653
|
} catch (error) {
|
|
292
654
|
this.emit("error", error);
|
|
293
|
-
throw new Error(`Failed to render markdown: ${error.message}`);
|
|
294
655
|
}
|
|
656
|
+
return "";
|
|
295
657
|
}
|
|
296
658
|
/**
|
|
297
659
|
* Render the markdown content to HTML synchronously.
|
|
@@ -333,8 +695,8 @@ ${yamlString}---
|
|
|
333
695
|
return resultData.result;
|
|
334
696
|
} catch (error) {
|
|
335
697
|
this.emit("error", error);
|
|
336
|
-
throw new Error(`Failed to render markdown: ${error.message}`);
|
|
337
698
|
}
|
|
699
|
+
return "";
|
|
338
700
|
}
|
|
339
701
|
/**
|
|
340
702
|
* Validate the markdown content by attempting to render it.
|
|
@@ -363,7 +725,6 @@ ${yamlString}---
|
|
|
363
725
|
}
|
|
364
726
|
return { valid: true };
|
|
365
727
|
} catch (error) {
|
|
366
|
-
this.emit("error", error);
|
|
367
728
|
if (content !== void 0) {
|
|
368
729
|
this._content = originalContent;
|
|
369
730
|
}
|
|
@@ -423,9 +784,6 @@ ${yamlString}---
|
|
|
423
784
|
await writeFile(data.filePath, data.content);
|
|
424
785
|
} catch (error) {
|
|
425
786
|
this.emit("error", error);
|
|
426
|
-
if (this._options.throwErrors) {
|
|
427
|
-
throw error;
|
|
428
|
-
}
|
|
429
787
|
}
|
|
430
788
|
}
|
|
431
789
|
/**
|
|
@@ -446,9 +804,6 @@ ${yamlString}---
|
|
|
446
804
|
fs.writeFileSync(data.filePath, data.content);
|
|
447
805
|
} catch (error) {
|
|
448
806
|
this.emit("error", error);
|
|
449
|
-
if (this._options.throwErrors) {
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
452
807
|
}
|
|
453
808
|
}
|
|
454
809
|
/**
|
|
@@ -463,8 +818,8 @@ ${yamlString}---
|
|
|
463
818
|
return parse(html, reactParseOptions);
|
|
464
819
|
} catch (error) {
|
|
465
820
|
this.emit("error", error);
|
|
466
|
-
throw new Error(`Failed to render React: ${error.message}`);
|
|
467
821
|
}
|
|
822
|
+
return "";
|
|
468
823
|
}
|
|
469
824
|
/**
|
|
470
825
|
* Render the markdown content to React synchronously.
|
|
@@ -478,8 +833,8 @@ ${yamlString}---
|
|
|
478
833
|
return parse(html, reactParseOptions);
|
|
479
834
|
} catch (error) {
|
|
480
835
|
this.emit("error", error);
|
|
481
|
-
throw new Error(`Failed to render React: ${error.message}`);
|
|
482
836
|
}
|
|
837
|
+
return "";
|
|
483
838
|
}
|
|
484
839
|
/**
|
|
485
840
|
* Load markdown content from a file.
|
|
@@ -497,9 +852,6 @@ ${yamlString}---
|
|
|
497
852
|
this._content = data.content;
|
|
498
853
|
} catch (error) {
|
|
499
854
|
this.emit("error", error);
|
|
500
|
-
if (this._options.throwErrors) {
|
|
501
|
-
throw error;
|
|
502
|
-
}
|
|
503
855
|
}
|
|
504
856
|
}
|
|
505
857
|
/**
|
|
@@ -517,9 +869,6 @@ ${yamlString}---
|
|
|
517
869
|
this._content = data.content;
|
|
518
870
|
} catch (error) {
|
|
519
871
|
this.emit("error", error);
|
|
520
|
-
if (this._options.throwErrors) {
|
|
521
|
-
throw error;
|
|
522
|
-
}
|
|
523
872
|
}
|
|
524
873
|
}
|
|
525
874
|
/**
|
|
@@ -540,9 +889,6 @@ ${yamlString}---
|
|
|
540
889
|
await writeFile(data.filePath, data.content);
|
|
541
890
|
} catch (error) {
|
|
542
891
|
this.emit("error", error);
|
|
543
|
-
if (this._options.throwErrors) {
|
|
544
|
-
throw error;
|
|
545
|
-
}
|
|
546
892
|
}
|
|
547
893
|
}
|
|
548
894
|
/**
|
|
@@ -562,19 +908,19 @@ ${yamlString}---
|
|
|
562
908
|
fs.writeFileSync(data.filePath, data.content);
|
|
563
909
|
} catch (error) {
|
|
564
910
|
this.emit("error", error);
|
|
565
|
-
if (this._options.throwErrors) {
|
|
566
|
-
throw error;
|
|
567
|
-
}
|
|
568
911
|
}
|
|
569
912
|
}
|
|
570
913
|
mergeOptions(current, options) {
|
|
571
|
-
if (options.
|
|
572
|
-
|
|
914
|
+
if (options.throwOnEmitError !== void 0) {
|
|
915
|
+
this.throwOnEmitError = options.throwOnEmitError;
|
|
573
916
|
}
|
|
574
917
|
if (options.renderOptions) {
|
|
575
918
|
current.renderOptions ??= {};
|
|
576
919
|
this.mergeRenderOptions(current.renderOptions, options.renderOptions);
|
|
577
920
|
}
|
|
921
|
+
if (options.ai) {
|
|
922
|
+
current.ai = options.ai;
|
|
923
|
+
}
|
|
578
924
|
return current;
|
|
579
925
|
}
|
|
580
926
|
isCacheEnabled(options) {
|
|
@@ -642,6 +988,8 @@ ${yamlString}---
|
|
|
642
988
|
};
|
|
643
989
|
export {
|
|
644
990
|
Writr,
|
|
991
|
+
WritrAI,
|
|
992
|
+
WritrAICache,
|
|
645
993
|
WritrHooks
|
|
646
994
|
};
|
|
647
995
|
/* v8 ignore next -- @preserve */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "writr",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Markdown Rendering Simplified",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/writr.js",
|
|
@@ -49,9 +49,10 @@
|
|
|
49
49
|
"markdown-to-react"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"
|
|
52
|
+
"ai": "^6.0.116",
|
|
53
|
+
"cacheable": "^2.3.3",
|
|
53
54
|
"hashery": "^1.5.0",
|
|
54
|
-
"hookified": "^1.
|
|
55
|
+
"hookified": "^2.1.0",
|
|
55
56
|
"html-react-parser": "^5.2.17",
|
|
56
57
|
"js-yaml": "^4.1.1",
|
|
57
58
|
"react": "^19.2.4",
|
|
@@ -61,31 +62,36 @@
|
|
|
61
62
|
"rehype-stringify": "^10.0.1",
|
|
62
63
|
"remark-emoji": "^5.0.2",
|
|
63
64
|
"remark-gfm": "^4.0.1",
|
|
64
|
-
"remark-github-blockquote-alert": "^2.0
|
|
65
|
+
"remark-github-blockquote-alert": "^2.1.0",
|
|
65
66
|
"remark-math": "^6.0.0",
|
|
66
67
|
"remark-mdx": "^3.1.1",
|
|
67
68
|
"remark-parse": "^11.0.0",
|
|
68
69
|
"remark-rehype": "^11.1.2",
|
|
69
70
|
"remark-toc": "^9.0.0",
|
|
70
|
-
"unified": "^11.0.5"
|
|
71
|
+
"unified": "^11.0.5",
|
|
72
|
+
"zod": "^4.3.6"
|
|
71
73
|
},
|
|
72
74
|
"devDependencies": {
|
|
73
|
-
"@
|
|
75
|
+
"@ai-sdk/anthropic": "^3.0.58",
|
|
76
|
+
"@ai-sdk/google": "^3.0.43",
|
|
77
|
+
"@ai-sdk/openai": "^3.0.41",
|
|
78
|
+
"@biomejs/biome": "^2.4.7",
|
|
74
79
|
"@monstermann/tinybench-pretty-printer": "^0.3.0",
|
|
75
80
|
"@types/js-yaml": "^4.0.9",
|
|
76
81
|
"@types/markdown-it": "^14.1.2",
|
|
77
|
-
"@types/node": "^25.
|
|
82
|
+
"@types/node": "^25.5.0",
|
|
78
83
|
"@types/react": "^19.2.14",
|
|
79
|
-
"@vitest/coverage-v8": "^4.0
|
|
84
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
80
85
|
"docula": "^0.40.0",
|
|
86
|
+
"dotenv": "^17.3.1",
|
|
81
87
|
"markdown-it": "^14.1.1",
|
|
82
|
-
"marked": "^17.0.
|
|
88
|
+
"marked": "^17.0.4",
|
|
83
89
|
"rimraf": "^6.1.3",
|
|
84
90
|
"tinybench": "^6.0.0",
|
|
85
91
|
"tsup": "^8.5.1",
|
|
86
92
|
"tsx": "^4.21.0",
|
|
87
93
|
"typescript": "^5.9.3",
|
|
88
|
-
"vitest": "^4.0
|
|
94
|
+
"vitest": "^4.1.0"
|
|
89
95
|
},
|
|
90
96
|
"files": [
|
|
91
97
|
"dist",
|
|
@@ -99,6 +105,7 @@
|
|
|
99
105
|
"benchmark": "tsx benchmark/benchmark-minimal.ts && tsx benchmark/benchmark-standard.ts",
|
|
100
106
|
"test": "pnpm lint && vitest run --coverage",
|
|
101
107
|
"test:ci": "biome check --error-on-warnings && vitest run --coverage",
|
|
108
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
102
109
|
"website:build": "rimraf ./site/README.md ./site/dist && npx docula build -s ./site -o ./site/dist",
|
|
103
110
|
"website:serve": "rimraf ./site/README.md ./site/dist && npx docula serve -s ./site -o ./site/dist"
|
|
104
111
|
}
|