@hanna84/mcp-writing 3.5.1 → 3.5.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/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,16 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v3.5.2](https://github.com/hannasdev/mcp-writing/compare/v3.5.1...v3.5.2)
|
|
8
|
+
|
|
9
|
+
- Improve beta prose flow and centered scene dateline [`#184`](https://github.com/hannasdev/mcp-writing/pull/184)
|
|
10
|
+
|
|
7
11
|
#### [v3.5.1](https://github.com/hannasdev/mcp-writing/compare/v3.5.0...v3.5.1)
|
|
8
12
|
|
|
13
|
+
> 8 May 2026
|
|
14
|
+
|
|
9
15
|
- Fix beta accountability follow-ups and test coverage [`#183`](https://github.com/hannasdev/mcp-writing/pull/183)
|
|
16
|
+
- Release 3.5.1 [`28d77f7`](https://github.com/hannasdev/mcp-writing/commit/28d77f755708dc8e987caa69dfcb6f3c5408b916)
|
|
10
17
|
|
|
11
18
|
#### [v3.5.0](https://github.com/hannasdev/mcp-writing/compare/v3.4.4...v3.5.0)
|
|
12
19
|
|
package/package.json
CHANGED
|
@@ -233,6 +233,122 @@ function readProse(filePath, { syncDir } = {}) {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
function normalizeHardWrappedProse(rawProse) {
|
|
237
|
+
const prose = String(rawProse ?? "").replace(/\r\n?/g, "\n").trim();
|
|
238
|
+
if (!prose) return "";
|
|
239
|
+
const paragraphs = prose
|
|
240
|
+
.split(/\n\s*\n/g)
|
|
241
|
+
.map(paragraph => paragraph.replace(/\s*\n\s*/g, " ").trim())
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
return paragraphs.join("\n\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function extractSceneDateline(prose) {
|
|
247
|
+
const normalized = String(prose ?? "").replace(/\r\n?/g, "\n").trim();
|
|
248
|
+
if (!normalized) {
|
|
249
|
+
return { dateline: null, body: "" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lines = normalized
|
|
253
|
+
.split("\n")
|
|
254
|
+
.map(line => line.trim())
|
|
255
|
+
.filter(Boolean);
|
|
256
|
+
if (lines.length === 0) {
|
|
257
|
+
return { dateline: null, body: "" };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const firstParagraph = lines[0];
|
|
261
|
+
const dashMatch = firstParagraph.match(/^(.+?)\s*[–-]\s*(.+)$/);
|
|
262
|
+
const left = dashMatch?.[1]?.trim() ?? "";
|
|
263
|
+
const right = dashMatch?.[2]?.trim() ?? "";
|
|
264
|
+
const totalWords = firstParagraph.split(/\s+/).filter(Boolean).length;
|
|
265
|
+
const looksLikeDateline = (
|
|
266
|
+
firstParagraph.length >= 6
|
|
267
|
+
&& firstParagraph.length <= 90
|
|
268
|
+
&& Boolean(dashMatch)
|
|
269
|
+
&& left.length >= 2
|
|
270
|
+
&& right.length >= 2
|
|
271
|
+
&& totalWords <= 14
|
|
272
|
+
&& !/[!?]/.test(firstParagraph)
|
|
273
|
+
&& !/[“”"']/.test(firstParagraph)
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (!looksLikeDateline) {
|
|
277
|
+
return { dateline: null, body: normalized };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
dateline: firstParagraph,
|
|
282
|
+
body: lines.slice(1).join("\n"),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeBetaProseFlow(prose) {
|
|
287
|
+
const normalized = String(prose ?? "").replace(/\r\n?/g, "\n").trim();
|
|
288
|
+
if (!normalized) return "";
|
|
289
|
+
const paragraphs = normalized
|
|
290
|
+
.split(/\n\s*\n/g)
|
|
291
|
+
.map(paragraph => paragraph
|
|
292
|
+
.split("\n")
|
|
293
|
+
.map(line => line.trim())
|
|
294
|
+
.filter(Boolean)
|
|
295
|
+
.join("\n"))
|
|
296
|
+
.filter(Boolean);
|
|
297
|
+
// For beta exports, convert paragraph blocks into regular line breaks so the
|
|
298
|
+
// reading flow stays continuous without large section gaps.
|
|
299
|
+
return paragraphs.join("\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizeBetaTypography(prose) {
|
|
303
|
+
return String(prose ?? "")
|
|
304
|
+
.replace(/(^|\s)--(\s|$)/g, "$1—$2");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderProseWithInlineEmphasis(doc, prose, {
|
|
308
|
+
bodyFont,
|
|
309
|
+
italicFont,
|
|
310
|
+
fontSize,
|
|
311
|
+
width,
|
|
312
|
+
align = "left",
|
|
313
|
+
lineGap = 0,
|
|
314
|
+
paragraphGap = 0,
|
|
315
|
+
blankLineMoveDown = 0.15,
|
|
316
|
+
}) {
|
|
317
|
+
const lines = String(prose ?? "").split("\n");
|
|
318
|
+
for (const line of lines) {
|
|
319
|
+
if (line.length === 0) {
|
|
320
|
+
doc.moveDown(blankLineMoveDown);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (line.trim() === "***") {
|
|
325
|
+
doc.moveDown(0.5);
|
|
326
|
+
doc.fontSize(fontSize).font(bodyFont);
|
|
327
|
+
doc.text("***", { align: "center", width, lineGap, paragraphGap: 0 });
|
|
328
|
+
doc.moveDown(0.5);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const segments = line.split(/(\*[^*\n]+\*)/g).filter(Boolean);
|
|
333
|
+
|
|
334
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
335
|
+
const segment = segments[index];
|
|
336
|
+
const isItalic = /^\*[^*\n]+\*$/.test(segment);
|
|
337
|
+
const text = isItalic ? segment.slice(1, -1) : segment;
|
|
338
|
+
if (!text) continue;
|
|
339
|
+
doc.fontSize(fontSize).font(isItalic ? italicFont : bodyFont);
|
|
340
|
+
doc.text(text, {
|
|
341
|
+
align,
|
|
342
|
+
width,
|
|
343
|
+
lineGap,
|
|
344
|
+
paragraphGap,
|
|
345
|
+
continued: index < segments.length - 1,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
doc.font(bodyFont).fontSize(fontSize);
|
|
350
|
+
}
|
|
351
|
+
|
|
236
352
|
function renderSceneBlock(scene, options) {
|
|
237
353
|
const {
|
|
238
354
|
profile,
|
|
@@ -278,11 +394,11 @@ function renderSceneBlock(scene, options) {
|
|
|
278
394
|
|
|
279
395
|
const prose = scene.prose ?? "";
|
|
280
396
|
if (!includeParagraphAnchors || prose.length === 0) {
|
|
281
|
-
parts.push(prose);
|
|
397
|
+
parts.push(normalizeHardWrappedProse(prose));
|
|
282
398
|
return parts.join("\n\n");
|
|
283
399
|
}
|
|
284
400
|
|
|
285
|
-
const paragraphs = prose
|
|
401
|
+
const paragraphs = normalizeHardWrappedProse(prose)
|
|
286
402
|
.split(/\n\s*\n/g)
|
|
287
403
|
.map(p => p.trim())
|
|
288
404
|
.filter(Boolean);
|
|
@@ -433,12 +549,13 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
433
549
|
const syncDir = syncDirOpt ?? process.env.WRITING_SYNC_DIR ?? null;
|
|
434
550
|
const isBetaProfile = profile === "beta_reader_personalized";
|
|
435
551
|
const proseFontSize = isBetaProfile ? 8 : 10;
|
|
436
|
-
const proseLineGap = isBetaProfile ?
|
|
552
|
+
const proseLineGap = isBetaProfile ? 1.6 : 3;
|
|
437
553
|
const bodyFont = profile === "beta_reader_personalized" ? "Times-Roman" : "Helvetica";
|
|
438
554
|
const coverHeadingFont = profile === "beta_reader_personalized" ? "Times-Bold" : "Helvetica-Bold";
|
|
439
555
|
// Beta scene headings intentionally use body font (non-bold) per product direction.
|
|
440
556
|
const sceneHeadingFont = isBetaProfile ? bodyFont : coverHeadingFont;
|
|
441
|
-
const
|
|
557
|
+
const italicFont = profile === "beta_reader_personalized" ? "Times-Italic" : "Helvetica-Oblique";
|
|
558
|
+
const metaFont = italicFont;
|
|
442
559
|
|
|
443
560
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
444
561
|
const rows = loadBundleSceneRows(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
@@ -494,7 +611,7 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
494
611
|
doc.restore();
|
|
495
612
|
// Restore prose style so auto-flowed text keeps consistent typography
|
|
496
613
|
// on pages added during long text rendering.
|
|
497
|
-
doc.font(bodyFont).fontSize(proseFontSize);
|
|
614
|
+
doc.font(bodyFont).fontSize(proseFontSize).fillColor("#000000");
|
|
498
615
|
doc.x = previousX;
|
|
499
616
|
doc.y = previousY;
|
|
500
617
|
};
|
|
@@ -530,8 +647,9 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
530
647
|
|
|
531
648
|
try {
|
|
532
649
|
doc.addPage();
|
|
533
|
-
|
|
534
|
-
doc.
|
|
650
|
+
const coverLabel = `Review Bundle: ${plan.resolved_scope.project_id}`;
|
|
651
|
+
doc.fontSize(isBetaProfile ? 11 : 24).font(coverHeadingFont).text(coverLabel, { align: "left" });
|
|
652
|
+
doc.moveDown(isBetaProfile ? 0.2 : 0.5);
|
|
535
653
|
doc.fontSize(11).font(bodyFont);
|
|
536
654
|
if (profile !== "beta_reader_personalized") {
|
|
537
655
|
doc.text(`Profile: ${profile}`, { align: "left" });
|
|
@@ -604,14 +722,35 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
604
722
|
}
|
|
605
723
|
);
|
|
606
724
|
}
|
|
607
|
-
|
|
725
|
+
let sceneDateline = null;
|
|
726
|
+
if (isBetaProfile) {
|
|
727
|
+
prose = normalizeBetaProseFlow(resolved);
|
|
728
|
+
const extracted = extractSceneDateline(prose);
|
|
729
|
+
sceneDateline = extracted.dateline ? normalizeBetaTypography(extracted.dateline) : null;
|
|
730
|
+
prose = normalizeBetaTypography(extracted.body);
|
|
731
|
+
} else {
|
|
732
|
+
prose = normalizeHardWrappedProse(resolved);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (sceneDateline) {
|
|
736
|
+
doc.fontSize(10).font(metaFont);
|
|
737
|
+
doc.text(sceneDateline, {
|
|
738
|
+
align: "center",
|
|
739
|
+
width: doc.page.width - doc.page.margins.left - doc.page.margins.right,
|
|
740
|
+
});
|
|
741
|
+
doc.moveDown(1.0);
|
|
742
|
+
}
|
|
608
743
|
|
|
609
744
|
const textWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
610
|
-
doc
|
|
611
|
-
|
|
745
|
+
renderProseWithInlineEmphasis(doc, prose, {
|
|
746
|
+
bodyFont,
|
|
747
|
+
italicFont,
|
|
748
|
+
fontSize: proseFontSize,
|
|
612
749
|
align: "left",
|
|
613
750
|
width: textWidth,
|
|
614
751
|
lineGap: proseLineGap,
|
|
752
|
+
paragraphGap: 0,
|
|
753
|
+
blankLineMoveDown: isBetaProfile ? 0.15 : 0.65,
|
|
615
754
|
});
|
|
616
755
|
}
|
|
617
756
|
|
|
@@ -642,4 +781,5 @@ export {
|
|
|
642
781
|
buildPageFingerprintToken,
|
|
643
782
|
buildFingerprintSeed,
|
|
644
783
|
buildFingerprintSeedHash,
|
|
784
|
+
extractSceneDateline,
|
|
645
785
|
};
|