Skip to content

Commit b8c8c79

Browse files
302 various completion issues (#307)
* Fix issue 1 * Fix issue 3 * Fix issue 4 * Fix issue 2 * Create hungry-jars-search.md * Update package.json * Fix lint / format issues * Update hungry-jars-search.md
1 parent 217b128 commit b8c8c79

File tree

11 files changed

+182
-88
lines changed

11 files changed

+182
-88
lines changed

.changeset/hungry-jars-search.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"remend": patch
3+
---
4+
5+
fix: should not add closing markers to overlapping bold and italic
6+
fix: should handle code block with incomplete inline code after
7+
fix: should not add closing markers to overlapping bold and italic
8+
fix: should close nested underscore italic before bold

apps/website/geistdocs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ export const translations = {
4747
},
4848
};
4949

50-
export const basePath: string | undefined = undefined;
50+
export const basePath: string | undefined = undefined;

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"scripts": {
55
"build": "turbo run build",
66
"dev": "turbo run dev",
7-
"check": "npx ultracite@latest check",
8-
"fix": "npx ultracite@latest fix",
7+
"check": "ultracite check",
8+
"fix": "ultracite fix",
99
"test": "turbo run test",
1010
"test:coverage": "turbo run test:coverage",
1111
"test:ui": "turbo run test:ui",

packages/remend/__tests__/bold-italic.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,21 @@ describe("bold-italic formatting (***)", () => {
8181
// Test lines 137-138: text that ends with >= 3 asterisks (but not 4+ consecutive)
8282
expect(remend("***word text***")).toBe("***word text***");
8383
});
84+
85+
it("should not add closing markers to overlapping bold and italic (#302)", () => {
86+
// When we have **bold and *italic***, the *** is closing both ** and *
87+
// It's not a bold-italic marker, so we shouldn't add closing ***
88+
expect(remend("Combined **bold and *italic*** text")).toBe(
89+
"Combined **bold and *italic*** text"
90+
);
91+
expect(remend("**bold and *italic*** more text")).toBe(
92+
"**bold and *italic*** more text"
93+
);
94+
expect(remend("test **bold and *italic*** end")).toBe(
95+
"test **bold and *italic*** end"
96+
);
97+
expect(remend("- Combined **bold and *italic*** text")).toBe(
98+
"- Combined **bold and *italic*** text"
99+
);
100+
});
84101
});

packages/remend/__tests__/code-blocks.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, expect, it } from "vitest";
22
import remend from "../src";
33

4+
// Top-level regex for performance (used in multiple tests)
5+
const trailingDoubleUnderscorePattern = /__$/;
6+
47
describe("code block handling", () => {
58
it("should handle incomplete multiline code blocks", () => {
69
expect(remend("```javascript\nconst x = 5;")).toBe(
@@ -177,7 +180,7 @@ Notes and tips:
177180

178181
const result = remend(input);
179182
expect(result).toBe(input);
180-
expect(result).not.toMatch(/__$/); // Should not end with __
183+
expect(result).not.toMatch(trailingDoubleUnderscorePattern); // Should not end with __
181184
});
182185

183186
it("should handle complete code blocks with underscores followed by asterisk list (#300)", () => {
@@ -190,7 +193,7 @@ def __init__(self):
190193

191194
const result = remend(input);
192195
expect(result).toBe(input);
193-
expect(result).not.toMatch(/__$/);
196+
expect(result).not.toMatch(trailingDoubleUnderscorePattern);
194197
});
195198

196199
it("should handle code blocks with underscores and following text with asterisks (#300)", () => {
@@ -206,6 +209,21 @@ Some notes:
206209

207210
const result = remend(input);
208211
expect(result).toBe(input);
209-
expect(result).not.toMatch(/__$/);
212+
expect(result).not.toMatch(trailingDoubleUnderscorePattern);
213+
});
214+
215+
it("should handle incomplete markdown after code block (#302)", () => {
216+
const text = `\`\`\`css
217+
code here
218+
\`\`\`
219+
220+
**incomplete bold`;
221+
expect(remend(text)).toBe(
222+
`\`\`\`css
223+
code here
224+
\`\`\`
225+
226+
**incomplete bold**`
227+
);
210228
});
211229
});

packages/remend/__tests__/inline-code.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,10 @@ describe("inline code formatting (`)", () => {
5656
const text5 = "text``````";
5757
expect(remend(text5)).toBe(text5);
5858
});
59+
60+
it("should handle code block with incomplete inline code after (#302)", () => {
61+
expect(remend("```\nblock\n```\n`inline")).toBe(
62+
"```\nblock\n```\n`inline`"
63+
);
64+
});
5965
});

packages/remend/__tests__/mixed-formatting.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,14 @@ describe("mixed formatting", () => {
6464
"**bold and *bold-italic***"
6565
);
6666
});
67+
68+
it("should close nested underscore italic before bold (#302)", () => {
69+
// When _ opens after **, the _ should close before ** (proper nesting)
70+
expect(remend("combined **_bold and italic")).toBe(
71+
"combined **_bold and italic_**"
72+
);
73+
expect(remend("**_text")).toBe("**_text_**");
74+
// When _ opens before **, the ** should close first (it's nested inside)
75+
expect(remend("_italic and **bold")).toBe("_italic and **bold**_");
76+
});
6777
});

packages/remend/__tests__/streaming.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ describe("chunked streaming scenarios", () => {
66
expect(remend("This is **bold with *ital")).toBe(
77
"This is **bold with *ital*"
88
);
9-
// When bold is unclosed, it gets closed first, then underscore
10-
expect(remend("**bold _und")).toBe("**bold _und**_");
9+
// When underscore opens after bold, underscore should close before bold (proper nesting)
10+
expect(remend("**bold _und")).toBe("**bold _und_**");
1111
});
1212

1313
it("should handle headings with incomplete formatting", () => {

packages/remend/src/emphasis-handlers.ts

Lines changed: 102 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
whitespaceOrMarkersPattern,
1010
} from "./patterns";
1111
import {
12-
hasCompleteCodeBlock,
1312
isHorizontalRule,
13+
isWithinCodeBlock,
1414
isWithinLinkOrImageUrl,
1515
isWithinMathBlock,
1616
isWordChar,
@@ -234,10 +234,6 @@ const shouldSkipBoldCompletion = (
234234

235235
// Completes incomplete bold formatting (**)
236236
export const handleIncompleteBold = (text: string): string => {
237-
if (hasCompleteCodeBlock(text)) {
238-
return text;
239-
}
240-
241237
const boldMatch = text.match(boldPattern);
242238
if (!boldMatch) {
243239
return text;
@@ -246,6 +242,11 @@ export const handleIncompleteBold = (text: string): string => {
246242
const contentAfterMarker = boldMatch[2];
247243
const markerIndex = text.lastIndexOf(boldMatch[1]);
248244

245+
// Check if the bold marker is within a code block
246+
if (isWithinCodeBlock(text, markerIndex)) {
247+
return text;
248+
}
249+
249250
if (shouldSkipBoldCompletion(text, contentAfterMarker, markerIndex)) {
250251
return text;
251252
}
@@ -292,10 +293,6 @@ const shouldSkipItalicCompletion = (
292293
export const handleIncompleteDoubleUnderscoreItalic = (
293294
text: string
294295
): string => {
295-
if (hasCompleteCodeBlock(text)) {
296-
return text;
297-
}
298-
299296
const italicMatch = text.match(italicPattern);
300297
if (!italicMatch) {
301298
return text;
@@ -304,6 +301,11 @@ export const handleIncompleteDoubleUnderscoreItalic = (
304301
const contentAfterMarker = italicMatch[2];
305302
const markerIndex = text.lastIndexOf(italicMatch[1]);
306303

304+
// Check if the italic marker is within a code block
305+
if (isWithinCodeBlock(text, markerIndex)) {
306+
return text;
307+
}
308+
307309
if (shouldSkipItalicCompletion(text, contentAfterMarker, markerIndex)) {
308310
return text;
309311
}
@@ -346,11 +348,6 @@ const findFirstSingleAsteriskIndex = (text: string): number => {
346348

347349
// Completes incomplete italic formatting with single asterisks (*)
348350
export const handleIncompleteSingleAsteriskItalic = (text: string): string => {
349-
// Don't process if inside a complete code block
350-
if (hasCompleteCodeBlock(text)) {
351-
return text;
352-
}
353-
354351
const singleAsteriskMatch = text.match(singleAsteriskPattern);
355352

356353
if (!singleAsteriskMatch) {
@@ -363,6 +360,11 @@ export const handleIncompleteSingleAsteriskItalic = (text: string): string => {
363360
return text;
364361
}
365362

363+
// Check if the asterisk is within a code block
364+
if (isWithinCodeBlock(text, firstSingleAsteriskIndex)) {
365+
return text;
366+
}
367+
366368
// Get content after the first single asterisk
367369
const contentAfterFirstAsterisk = text.substring(
368370
firstSingleAsteriskIndex + 1
@@ -430,15 +432,43 @@ const insertClosingUnderscore = (text: string): string => {
430432
return `${text}_`;
431433
};
432434

435+
// Helper to handle trailing ** for proper nesting of _ and ** markers
436+
const handleTrailingAsterisksForUnderscore = (text: string): string | null => {
437+
if (!text.endsWith("**")) {
438+
return null;
439+
}
440+
441+
const textWithoutTrailingAsterisks = text.slice(0, -2);
442+
const asteriskPairsAfterRemoval = (
443+
textWithoutTrailingAsterisks.match(/\*\*/g) || []
444+
).length;
445+
446+
// If removing trailing ** makes the count odd, it was added to close an unclosed **
447+
if (asteriskPairsAfterRemoval % 2 !== 1) {
448+
return null;
449+
}
450+
451+
const firstDoubleAsteriskIndex = textWithoutTrailingAsterisks.indexOf("**");
452+
const underscoreIndex = findFirstSingleUnderscoreIndex(
453+
textWithoutTrailingAsterisks
454+
);
455+
456+
// If ** opened before _, then _ should close before **
457+
if (
458+
firstDoubleAsteriskIndex !== -1 &&
459+
underscoreIndex !== -1 &&
460+
firstDoubleAsteriskIndex < underscoreIndex
461+
) {
462+
return `${textWithoutTrailingAsterisks}_**`;
463+
}
464+
465+
return null;
466+
};
467+
433468
// Completes incomplete italic formatting with single underscores (_)
434469
export const handleIncompleteSingleUnderscoreItalic = (
435470
text: string
436471
): string => {
437-
// Don't process if inside a complete code block
438-
if (hasCompleteCodeBlock(text)) {
439-
return text;
440-
}
441-
442472
const singleUnderscoreMatch = text.match(singleUnderscorePattern);
443473

444474
if (!singleUnderscoreMatch) {
@@ -451,6 +481,11 @@ export const handleIncompleteSingleUnderscoreItalic = (
451481
return text;
452482
}
453483

484+
// Check if the underscore is within a code block
485+
if (isWithinCodeBlock(text, firstSingleUnderscoreIndex)) {
486+
return text;
487+
}
488+
454489
// Get content after the first single underscore
455490
const contentAfterFirstUnderscore = text.substring(
456491
firstSingleUnderscoreIndex + 1
@@ -467,49 +502,72 @@ export const handleIncompleteSingleUnderscoreItalic = (
467502

468503
const singleUnderscores = countSingleUnderscores(text);
469504
if (singleUnderscores % 2 === 1) {
505+
// Check if we need to insert _ before trailing ** for proper nesting
506+
const trailingResult = handleTrailingAsterisksForUnderscore(text);
507+
if (trailingResult !== null) {
508+
return trailingResult;
509+
}
470510
return insertClosingUnderscore(text);
471511
}
472512

473513
return text;
474514
};
475515

476-
// Completes incomplete bold-italic formatting (***)
477-
export const handleIncompleteBoldItalic = (text: string): string => {
478-
// Don't process if inside a complete code block
479-
if (hasCompleteCodeBlock(text)) {
480-
return text;
516+
// Helper to check if bold-italic markers are already balanced
517+
const areBoldItalicMarkersBalanced = (text: string): boolean => {
518+
const asteriskPairs = (text.match(/\*\*/g) || []).length;
519+
const singleAsterisks = countSingleAsterisks(text);
520+
return asteriskPairs % 2 === 0 && singleAsterisks % 2 === 0;
521+
};
522+
523+
// Helper to check if bold-italic should be skipped
524+
const shouldSkipBoldItalicCompletion = (
525+
text: string,
526+
contentAfterMarker: string,
527+
markerIndex: number
528+
): boolean => {
529+
if (
530+
!contentAfterMarker ||
531+
whitespaceOrMarkersPattern.test(contentAfterMarker)
532+
) {
533+
return true;
481534
}
482535

536+
if (isWithinCodeBlock(text, markerIndex)) {
537+
return true;
538+
}
539+
540+
return isHorizontalRule(text, markerIndex, "*");
541+
};
542+
543+
// Completes incomplete bold-italic formatting (***)
544+
export const handleIncompleteBoldItalic = (text: string): string => {
483545
// Don't process if text is only asterisks and has 4 or more consecutive asterisks
484-
// This prevents cases like **** from being treated as incomplete ***
485546
if (fourOrMoreAsterisksPattern.test(text)) {
486547
return text;
487548
}
488549

489550
const boldItalicMatch = text.match(boldItalicPattern);
490551

491-
if (boldItalicMatch) {
492-
// Don't close if there's no meaningful content after the opening markers
493-
// boldItalicMatch[2] contains the content after ***
494-
// Check if content is only whitespace or other emphasis markers
495-
const contentAfterMarker = boldItalicMatch[2];
496-
if (
497-
!contentAfterMarker ||
498-
whitespaceOrMarkersPattern.test(contentAfterMarker)
499-
) {
500-
return text;
501-
}
552+
if (!boldItalicMatch) {
553+
return text;
554+
}
502555

503-
// Check if the *** is a horizontal rule
504-
const markerIndex = text.lastIndexOf(boldItalicMatch[1]);
505-
if (isHorizontalRule(text, markerIndex, "*")) {
506-
return text;
507-
}
556+
const contentAfterMarker = boldItalicMatch[2];
557+
const markerIndex = text.lastIndexOf(boldItalicMatch[1]);
508558

509-
const tripleAsteriskCount = countTripleAsterisks(text);
510-
if (tripleAsteriskCount % 2 === 1) {
511-
return `${text}***`;
559+
if (shouldSkipBoldItalicCompletion(text, contentAfterMarker, markerIndex)) {
560+
return text;
561+
}
562+
563+
const tripleAsteriskCount = countTripleAsterisks(text);
564+
if (tripleAsteriskCount % 2 === 1) {
565+
// If both ** and * are balanced, don't add closing ***
566+
// The *** is likely overlapping markers (e.g., **bold and *italic***)
567+
if (areBoldItalicMarkersBalanced(text)) {
568+
return text;
512569
}
570+
return `${text}***`;
513571
}
514572

515573
return text;

0 commit comments

Comments
 (0)