Skip to content

Commit 93e6339

Browse files
authored
Add SmartyPants flag (#5769)
* feat: add smartypants flag * test: smartypants in markdown and mdx * docs: Smartypants -> SmartyPants * chore: changeset * chore: update changeset with 1.0 -> 2.0 in mind * chore: bump to minor change
1 parent 04bf679 commit 93e6339

14 files changed

Lines changed: 155 additions & 16 deletions

File tree

.changeset/angry-pots-boil.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'astro': minor
3+
'@astrojs/mdx': minor
4+
'@astrojs/markdown-remark': minor
5+
---
6+
7+
Introduce a `smartypants` flag to opt-out of Astro's default SmartyPants plugin.
8+
9+
```js
10+
{
11+
markdown: {
12+
smartypants: false,
13+
}
14+
}
15+
```
16+
17+
#### Migration
18+
19+
You may have disabled Astro's built-in plugins (GitHub-Flavored Markdown and Smartypants) with the `extendDefaultPlugins` option. This has now been split into 2 flags to disable each plugin individually:
20+
- `markdown.gfm` to disable GitHub-Flavored Markdown
21+
- `markdown.smartypants` to disable SmartyPants
22+
23+
```diff
24+
// astro.config.mjs
25+
import { defineConfig } from 'astro/config';
26+
27+
export default defineConfig({
28+
markdown: {
29+
- extendDefaultPlugins: false,
30+
+ smartypants: false,
31+
+ gfm: false,
32+
}
33+
});
34+
```

packages/astro/src/@types/astro.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,23 @@ export interface AstroUserConfig {
785785
* ```
786786
*/
787787
gfm?: boolean;
788+
/**
789+
* @docs
790+
* @name markdown.smartypants
791+
* @type {boolean}
792+
* @default `true`
793+
* @description
794+
* Astro uses the [SmartyPants formatter](https://daringfireball.net/projects/smartypants/) by default. To disable this, set the `smartypants` flag to `false`:
795+
*
796+
* ```js
797+
* {
798+
* markdown: {
799+
* smartypants: false,
800+
* }
801+
* }
802+
* ```
803+
*/
804+
smartypants?: boolean;
788805
/**
789806
* @docs
790807
* @name markdown.remarkRehype

packages/astro/src/core/config/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export const AstroConfigSchema = z.object({
163163
.optional()
164164
.default(ASTRO_CONFIG_DEFAULTS.markdown.remarkRehype),
165165
gfm: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.gfm),
166+
smartypants: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.smartypants),
166167
})
167168
.default({}),
168169
vite: z

packages/astro/test/astro-markdown-plugins.test.js

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,23 @@ describe('Astro Markdown plugins', () => {
4747
});
4848

4949
// Asserts Astro 1.0 behavior is removed. Test can be removed in Astro 3.0.
50-
it('Still applies GFM when user plugins are provided', async () => {
50+
it('Still applies default plugins when user plugins are provided', async () => {
5151
const fixture = await buildFixture({
5252
markdown: {
5353
remarkPlugins: [remarkExamplePlugin],
5454
rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
5555
},
5656
});
57-
const html = await fixture.readFile('/with-gfm/index.html');
58-
const $ = cheerio.load(html);
59-
60-
// test 1: GFM autolink applied correctly
61-
expect($('a[href="https://example.com"]')).to.have.lengthOf(1);
57+
const gfmHtml = await fixture.readFile('/with-gfm/index.html');
58+
const $1 = cheerio.load(gfmHtml);
59+
expect($1('a[href="https://example.com"]')).to.have.lengthOf(1);
6260

63-
// test 2: remark plugins still applied
64-
expect(html).to.include('Remark plugin applied!');
61+
const smartypantsHtml = await fixture.readFile('/with-smartypants/index.html');
62+
const $2 = cheerio.load(smartypantsHtml);
63+
expect($2('p').html()).to.equal('“Smartypants” is — awesome');
6564

66-
// test 3: rehype plugins still applied
67-
expect($('#github-flavored-markdown-test')).to.have.lengthOf(1);
68-
expect($('#github-flavored-markdown-test').hasClass('title')).to.equal(true);
65+
testRemark(gfmHtml);
66+
testRehype(gfmHtml, '#github-flavored-markdown-test');
6967
});
7068

7169
for (const gfm of [true, false]) {
@@ -87,12 +85,42 @@ describe('Astro Markdown plugins', () => {
8785
expect($('a[href="https://example.com"]')).to.have.lengthOf(0);
8886
}
8987

90-
// test 2: remark plugins still applied
91-
expect(html).to.include('Remark plugin applied!');
88+
testRemark(html);
89+
testRehype(html, '#github-flavored-markdown-test');
90+
});
91+
}
92+
93+
for (const smartypants of [true, false]) {
94+
it(`Handles SmartyPants when smartypants = ${smartypants}`, async () => {
95+
const fixture = await buildFixture({
96+
markdown: {
97+
remarkPlugins: [remarkExamplePlugin],
98+
rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
99+
smartypants,
100+
},
101+
});
102+
const html = await fixture.readFile('/with-smartypants/index.html');
103+
const $ = cheerio.load(html);
104+
105+
// test 1: GFM autolink applied correctly
106+
if (smartypants === true) {
107+
expect($('p').html()).to.equal('“Smartypants” is — awesome');
108+
} else {
109+
expect($('p').html()).to.equal('"Smartypants" is -- awesome');
110+
}
92111

93-
// test 3: rehype plugins still applied
94-
expect($('#github-flavored-markdown-test')).to.have.lengthOf(1);
95-
expect($('#github-flavored-markdown-test').hasClass('title')).to.equal(true);
112+
testRemark(html);
113+
testRehype(html, '#smartypants-test');
96114
});
97115
}
98116
});
117+
118+
function testRehype(html, headingId) {
119+
const $ = cheerio.load(html);
120+
expect($(headingId)).to.have.lengthOf(1);
121+
expect($(headingId).hasClass('title')).to.equal(true);
122+
}
123+
124+
function testRemark(html) {
125+
expect(html).to.include('Remark plugin applied!');
126+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Smartypants test
2+
3+
"Smartypants" is -- awesome

packages/integrations/mdx/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"rehype-raw": "^6.1.1",
4444
"remark-frontmatter": "^4.0.1",
4545
"remark-gfm": "^3.0.1",
46+
"remark-smartypants": "^2.0.0",
4647
"shiki": "^0.11.1",
4748
"unist-util-visit": "^4.1.0",
4849
"vfile": "^5.3.2"

packages/integrations/mdx/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ function applyDefaultOptions({
186186
recmaPlugins: options.recmaPlugins ?? defaults.recmaPlugins,
187187
remarkRehype: options.remarkRehype ?? defaults.remarkRehype,
188188
gfm: options.gfm ?? defaults.gfm,
189+
smartypants: options.smartypants ?? defaults.smartypants,
189190
remarkPlugins: options.remarkPlugins ?? defaults.remarkPlugins,
190191
rehypePlugins: options.rehypePlugins ?? defaults.rehypePlugins,
191192
shikiConfig: options.shikiConfig ?? defaults.shikiConfig,

packages/integrations/mdx/src/plugins.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { Image } from 'mdast';
1414
import { pathToFileURL } from 'node:url';
1515
import rehypeRaw from 'rehype-raw';
1616
import remarkGfm from 'remark-gfm';
17+
import remarkSmartypants from 'remark-smartypants';
1718
import { visit } from 'unist-util-visit';
1819
import type { VFile } from 'vfile';
1920
import { MdxOptions } from './index.js';
@@ -153,6 +154,9 @@ export async function getRemarkPlugins(
153154
if (mdxOptions.gfm) {
154155
remarkPlugins.push(remarkGfm);
155156
}
157+
if (mdxOptions.smartypants) {
158+
remarkPlugins.push(remarkSmartypants);
159+
}
156160

157161
remarkPlugins = [...remarkPlugins, ...ignoreStringPlugins(mdxOptions.remarkPlugins)];
158162

packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ Oh cool, more text!
2121
And section 2, with a hyperlink to check GFM is preserved: https://handle-me-gfm.com
2222

2323
<div data-recma-plugin-works={recmaPluginWorking}></div>
24+
25+
> "Smartypants" is -- awesome

packages/integrations/mdx/test/mdx-plugins.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ describe('MDX plugins', () => {
3636
expect(selectGfmLink(document)).to.not.be.null;
3737
});
3838

39+
it('Applies SmartyPants by default', async () => {
40+
const fixture = await buildFixture({
41+
integrations: [mdx()],
42+
});
43+
44+
const html = await fixture.readFile(FILE);
45+
const { document } = parseHTML(html);
46+
47+
const quote = selectSmartypantsQuote(document);
48+
expect(quote).to.not.be.null;
49+
expect(quote.textContent).to.contain('“Smartypants” is — awesome');
50+
});
51+
3952
it('supports custom rehype plugins', async () => {
4053
const fixture = await buildFixture({
4154
integrations: [
@@ -88,6 +101,7 @@ describe('MDX plugins', () => {
88101
markdown: {
89102
remarkPlugins: [remarkToc],
90103
gfm: false,
104+
smartypants: false,
91105
},
92106
integrations: [
93107
mdx({
@@ -129,6 +143,23 @@ describe('MDX plugins', () => {
129143
expect(selectGfmLink(document), 'Respects `markdown.gfm` unexpectedly.').to.not.be.null;
130144
}
131145
});
146+
147+
it('Handles smartypants', async () => {
148+
const html = await fixture.readFile(FILE);
149+
const { document } = parseHTML(html);
150+
151+
const quote = selectSmartypantsQuote(document);
152+
153+
if (extendMarkdownConfig === true) {
154+
expect(quote.textContent, 'Does not respect `markdown.smartypants` option.').to.contain(
155+
'"Smartypants" is -- awesome'
156+
);
157+
} else {
158+
expect(quote.textContent, 'Respects `markdown.smartypants` unexpectedly.').to.contain(
159+
'“Smartypants” is — awesome'
160+
);
161+
}
162+
});
132163
});
133164
}
134165

@@ -202,6 +233,10 @@ function selectGfmLink(document) {
202233
return document.querySelector('a[href="https://handle-me-gfm.com"]');
203234
}
204235

236+
function selectSmartypantsQuote(document) {
237+
return document.querySelector('blockquote');
238+
}
239+
205240
function selectRemarkExample(document) {
206241
return document.querySelector('div[data-remark-plugin-works]');
207242
}

0 commit comments

Comments
 (0)