Skip to content

Commit

Permalink
Merge pull request #87 from rwblickhan/rwblickhan-fix-blockquotes
Browse files Browse the repository at this point in the history
Fix smart quotes at the start of paragraphs
  • Loading branch information
silvenon committed Jul 8, 2024
2 parents 3fca928 + b7360aa commit 0ed7519
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 11 deletions.
37 changes: 37 additions & 0 deletions plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,43 @@ describe("handles quotes around inline code", async () => {
});
});

describe("handles quotes at the edges of a paragraph", () => {
it("at start after another paragraph", async () => {
const file = await process('paragraph\n\n"after paragraph"');
expect(file.toString()).toMatchInlineSnapshot(`
"paragraph\n\n“after paragraph”
"`);
});

it("at start after a blockquote", async () => {
const file = await process('> blockquote\n\n"after blockquote"');
expect(file.toString()).toMatchInlineSnapshot(`
"> blockquote\n\n“after blockquote”
"`);
});

it("at start within a blockquote", async () => {
const file = await process('> blockquote\n>\n> "within blockquote"');
expect(file.toString()).toMatchInlineSnapshot(`
"> blockquote\n>\n> “within blockquote”
"`);
});

it("at end before another paragraph", async () => {
const file = await process('"before paragraph"\n\nparagraph');
expect(file.toString()).toMatchInlineSnapshot(`
"“before paragraph”\n\nparagraph
"`);
});

it("at end before a blockquote", async () => {
const file = await process('"before blockquote"\n\n> blockquote');
expect(file.toString()).toMatchInlineSnapshot(`
"“before blockquote”\n\n> blockquote
"`);
});
});

describe("should ignore parent nodes", () => {
const mdxCompiler = remark().use(remarkMdx).use(remarkSmartypants);
const process = mdxCompiler.process.bind(mdxCompiler);
Expand Down
39 changes: 28 additions & 11 deletions plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Plugin } from "unified";
import type { Test } from "unist-util-is";
import type { Node } from "unist";

const VISITED_NODES = new Set(["text", "inlineCode"]);
const VISITED_NODES = new Set(["text", "inlineCode", "paragraph"]);

const IGNORED_HTML_ELEMENTS = new Set(["style", "script"]);

Expand All @@ -17,7 +17,7 @@ const check: Test = (node, index, parent) => {
typeof parent.name === "string" &&
!IGNORED_HTML_ELEMENTS.has(parent.name))) &&
VISITED_NODES.has(node.type) &&
isLiteral(node)
(isLiteral(node) || isParagraph(node))
);
};

Expand Down Expand Up @@ -45,12 +45,18 @@ const remarkSmartypants: Plugin<[Options?]> = (options) => {
return (tree) => {
let allText = "";
let startIndex = 0;
const nodes: Literal[] = [];
const nodes: (Literal | Paragraph)[] = [];

visit(tree, check, (node) => {
if (!isLiteral(node)) return;
allText +=
node.type === "text" ? node.value : "A".repeat(node.value.length);
if (isLiteral(node)) {
allText +=
node.type === "text" ? node.value : "A".repeat(node.value.length);
} else if (isParagraph(node)) {
// Inject a "fake" space because otherwise, when concatenated below,
// smartypants will fail to recognize opening quotes at the start of
// paragraphs
allText += " ";
}
nodes.push(node);
});

Expand All @@ -59,12 +65,17 @@ const remarkSmartypants: Plugin<[Options?]> = (options) => {
allText = processor.processSync(allText).toString();

for (const node of nodes) {
const endIndex = startIndex + node.value.length;
if (node.type === "text") {
const processedText = allText.slice(startIndex, endIndex);
node.value = processor2.processSync(processedText).toString();
if (isLiteral(node)) {
const endIndex = startIndex + node.value.length;
if (node.type === "text") {
const processedText = allText.slice(startIndex, endIndex);
node.value = processor2.processSync(processedText).toString();
}
startIndex = endIndex;
} else if (isParagraph(node)) {
// Skip over the space we added above
startIndex += 1;
}
startIndex = endIndex;
}
};
};
Expand All @@ -78,4 +89,10 @@ function isLiteral(node: Node): node is Literal {
return "value" in node && typeof node.value === "string";
}

interface Paragraph extends Node {}

function isParagraph(node: Node): node is Paragraph {
return node.type === "paragraph";
}

export default remarkSmartypants;

0 comments on commit 0ed7519

Please sign in to comment.