summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/angry-pots-boil.md34
-rw-r--r--packages/astro/src/@types/astro.ts17
-rw-r--r--packages/astro/src/core/config/schema.ts1
-rw-r--r--packages/astro/test/astro-markdown-plugins.test.js60
-rw-r--r--packages/astro/test/fixtures/astro-markdown-plugins/src/pages/with-smartypants.md3
-rw-r--r--packages/integrations/mdx/package.json1
-rw-r--r--packages/integrations/mdx/src/index.ts1
-rw-r--r--packages/integrations/mdx/src/plugins.ts4
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx2
-rw-r--r--packages/integrations/mdx/test/mdx-plugins.test.js35
-rw-r--r--packages/markdown/remark/package.json1
-rw-r--r--packages/markdown/remark/src/index.ts7
-rw-r--r--packages/markdown/remark/src/types.ts1
-rw-r--r--pnpm-lock.yaml4
14 files changed, 155 insertions, 16 deletions
diff --git a/.changeset/angry-pots-boil.md b/.changeset/angry-pots-boil.md
new file mode 100644
index 000000000..7d367da3c
--- /dev/null
+++ b/.changeset/angry-pots-boil.md
@@ -0,0 +1,34 @@
+---
+'astro': minor
+'@astrojs/mdx': minor
+'@astrojs/markdown-remark': minor
+---
+
+Introduce a `smartypants` flag to opt-out of Astro's default SmartyPants plugin.
+
+```js
+{
+ markdown: {
+ smartypants: false,
+ }
+}
+```
+
+ #### Migration
+
+ 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:
+ - `markdown.gfm` to disable GitHub-Flavored Markdown
+ - `markdown.smartypants` to disable SmartyPants
+
+ ```diff
+ // astro.config.mjs
+ import { defineConfig } from 'astro/config';
+
+ export default defineConfig({
+ markdown: {
+ - extendDefaultPlugins: false,
+ + smartypants: false,
+ + gfm: false,
+ }
+ });
+ ```
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 7b8968f93..872abeaf8 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -787,6 +787,23 @@ export interface AstroUserConfig {
gfm?: boolean;
/**
* @docs
+ * @name markdown.smartypants
+ * @type {boolean}
+ * @default `true`
+ * @description
+ * Astro uses the [SmartyPants formatter](https://daringfireball.net/projects/smartypants/) by default. To disable this, set the `smartypants` flag to `false`:
+ *
+ * ```js
+ * {
+ * markdown: {
+ * smartypants: false,
+ * }
+ * }
+ * ```
+ */
+ smartypants?: boolean;
+ /**
+ * @docs
* @name markdown.remarkRehype
* @type {RemarkRehype}
* @description
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index a4dccb994..1cd49b7fb 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -163,6 +163,7 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.markdown.remarkRehype),
gfm: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.gfm),
+ smartypants: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.smartypants),
})
.default({}),
vite: z
diff --git a/packages/astro/test/astro-markdown-plugins.test.js b/packages/astro/test/astro-markdown-plugins.test.js
index 1fc6218b3..63f6eda67 100644
--- a/packages/astro/test/astro-markdown-plugins.test.js
+++ b/packages/astro/test/astro-markdown-plugins.test.js
@@ -47,25 +47,23 @@ describe('Astro Markdown plugins', () => {
});
// Asserts Astro 1.0 behavior is removed. Test can be removed in Astro 3.0.
- it('Still applies GFM when user plugins are provided', async () => {
+ it('Still applies default plugins when user plugins are provided', async () => {
const fixture = await buildFixture({
markdown: {
remarkPlugins: [remarkExamplePlugin],
rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
},
});
- const html = await fixture.readFile('/with-gfm/index.html');
- const $ = cheerio.load(html);
-
- // test 1: GFM autolink applied correctly
- expect($('a[href="https://example.com"]')).to.have.lengthOf(1);
+ const gfmHtml = await fixture.readFile('/with-gfm/index.html');
+ const $1 = cheerio.load(gfmHtml);
+ expect($1('a[href="https://example.com"]')).to.have.lengthOf(1);
- // test 2: remark plugins still applied
- expect(html).to.include('Remark plugin applied!');
+ const smartypantsHtml = await fixture.readFile('/with-smartypants/index.html');
+ const $2 = cheerio.load(smartypantsHtml);
+ expect($2('p').html()).to.equal('“Smartypants” is — awesome');
- // test 3: rehype plugins still applied
- expect($('#github-flavored-markdown-test')).to.have.lengthOf(1);
- expect($('#github-flavored-markdown-test').hasClass('title')).to.equal(true);
+ testRemark(gfmHtml);
+ testRehype(gfmHtml, '#github-flavored-markdown-test');
});
for (const gfm of [true, false]) {
@@ -87,12 +85,42 @@ describe('Astro Markdown plugins', () => {
expect($('a[href="https://example.com"]')).to.have.lengthOf(0);
}
- // test 2: remark plugins still applied
- expect(html).to.include('Remark plugin applied!');
+ testRemark(html);
+ testRehype(html, '#github-flavored-markdown-test');
+ });
+ }
+
+ for (const smartypants of [true, false]) {
+ it(`Handles SmartyPants when smartypants = ${smartypants}`, async () => {
+ const fixture = await buildFixture({
+ markdown: {
+ remarkPlugins: [remarkExamplePlugin],
+ rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
+ smartypants,
+ },
+ });
+ const html = await fixture.readFile('/with-smartypants/index.html');
+ const $ = cheerio.load(html);
+
+ // test 1: GFM autolink applied correctly
+ if (smartypants === true) {
+ expect($('p').html()).to.equal('“Smartypants” is — awesome');
+ } else {
+ expect($('p').html()).to.equal('"Smartypants" is -- awesome');
+ }
- // test 3: rehype plugins still applied
- expect($('#github-flavored-markdown-test')).to.have.lengthOf(1);
- expect($('#github-flavored-markdown-test').hasClass('title')).to.equal(true);
+ testRemark(html);
+ testRehype(html, '#smartypants-test');
});
}
});
+
+function testRehype(html, headingId) {
+ const $ = cheerio.load(html);
+ expect($(headingId)).to.have.lengthOf(1);
+ expect($(headingId).hasClass('title')).to.equal(true);
+}
+
+function testRemark(html) {
+ expect(html).to.include('Remark plugin applied!');
+}
diff --git a/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/with-smartypants.md b/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/with-smartypants.md
new file mode 100644
index 000000000..5d7a85ab1
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/with-smartypants.md
@@ -0,0 +1,3 @@
+# Smartypants test
+
+"Smartypants" is -- awesome
diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json
index 1d3caad5e..053cb3efb 100644
--- a/packages/integrations/mdx/package.json
+++ b/packages/integrations/mdx/package.json
@@ -43,6 +43,7 @@
"rehype-raw": "^6.1.1",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
+ "remark-smartypants": "^2.0.0",
"shiki": "^0.11.1",
"unist-util-visit": "^4.1.0",
"vfile": "^5.3.2"
diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts
index 7a49498e6..756e6d24f 100644
--- a/packages/integrations/mdx/src/index.ts
+++ b/packages/integrations/mdx/src/index.ts
@@ -186,6 +186,7 @@ function applyDefaultOptions({
recmaPlugins: options.recmaPlugins ?? defaults.recmaPlugins,
remarkRehype: options.remarkRehype ?? defaults.remarkRehype,
gfm: options.gfm ?? defaults.gfm,
+ smartypants: options.smartypants ?? defaults.smartypants,
remarkPlugins: options.remarkPlugins ?? defaults.remarkPlugins,
rehypePlugins: options.rehypePlugins ?? defaults.rehypePlugins,
shikiConfig: options.shikiConfig ?? defaults.shikiConfig,
diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts
index f5557b8a3..6637b57d7 100644
--- a/packages/integrations/mdx/src/plugins.ts
+++ b/packages/integrations/mdx/src/plugins.ts
@@ -14,6 +14,7 @@ import type { Image } from 'mdast';
import { pathToFileURL } from 'node:url';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
+import remarkSmartypants from 'remark-smartypants';
import { visit } from 'unist-util-visit';
import type { VFile } from 'vfile';
import { MdxOptions } from './index.js';
@@ -153,6 +154,9 @@ export async function getRemarkPlugins(
if (mdxOptions.gfm) {
remarkPlugins.push(remarkGfm);
}
+ if (mdxOptions.smartypants) {
+ remarkPlugins.push(remarkSmartypants);
+ }
remarkPlugins = [...remarkPlugins, ...ignoreStringPlugins(mdxOptions.remarkPlugins)];
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx b/packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx
index fcd8ae181..8699f4a22 100644
--- a/packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx
+++ b/packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx
@@ -21,3 +21,5 @@ Oh cool, more text!
And section 2, with a hyperlink to check GFM is preserved: https://handle-me-gfm.com
<div data-recma-plugin-works={recmaPluginWorking}></div>
+
+> "Smartypants" is -- awesome
diff --git a/packages/integrations/mdx/test/mdx-plugins.test.js b/packages/integrations/mdx/test/mdx-plugins.test.js
index f74ded3ea..828bcb3d5 100644
--- a/packages/integrations/mdx/test/mdx-plugins.test.js
+++ b/packages/integrations/mdx/test/mdx-plugins.test.js
@@ -36,6 +36,19 @@ describe('MDX plugins', () => {
expect(selectGfmLink(document)).to.not.be.null;
});
+ it('Applies SmartyPants by default', async () => {
+ const fixture = await buildFixture({
+ integrations: [mdx()],
+ });
+
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ const quote = selectSmartypantsQuote(document);
+ expect(quote).to.not.be.null;
+ expect(quote.textContent).to.contain('“Smartypants” is — awesome');
+ });
+
it('supports custom rehype plugins', async () => {
const fixture = await buildFixture({
integrations: [
@@ -88,6 +101,7 @@ describe('MDX plugins', () => {
markdown: {
remarkPlugins: [remarkToc],
gfm: false,
+ smartypants: false,
},
integrations: [
mdx({
@@ -129,6 +143,23 @@ describe('MDX plugins', () => {
expect(selectGfmLink(document), 'Respects `markdown.gfm` unexpectedly.').to.not.be.null;
}
});
+
+ it('Handles smartypants', async () => {
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ const quote = selectSmartypantsQuote(document);
+
+ if (extendMarkdownConfig === true) {
+ expect(quote.textContent, 'Does not respect `markdown.smartypants` option.').to.contain(
+ '"Smartypants" is -- awesome'
+ );
+ } else {
+ expect(quote.textContent, 'Respects `markdown.smartypants` unexpectedly.').to.contain(
+ '“Smartypants” is — awesome'
+ );
+ }
+ });
});
}
@@ -202,6 +233,10 @@ function selectGfmLink(document) {
return document.querySelector('a[href="https://handle-me-gfm.com"]');
}
+function selectSmartypantsQuote(document) {
+ return document.querySelector('blockquote');
+}
+
function selectRemarkExample(document) {
return document.querySelector('div[data-remark-plugin-works]');
}
diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json
index ae3853430..4368d6bdd 100644
--- a/packages/markdown/remark/package.json
+++ b/packages/markdown/remark/package.json
@@ -43,6 +43,7 @@
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
+ "remark-smartypants": "^2.0.0",
"shiki": "^0.11.1",
"unified": "^10.1.2",
"unist-util-map": "^3.1.1",
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts
index fc134ee46..ec25870de 100644
--- a/packages/markdown/remark/src/index.ts
+++ b/packages/markdown/remark/src/index.ts
@@ -24,6 +24,7 @@ import remarkUnwrap from './remark-unwrap.js';
import rehypeRaw from 'rehype-raw';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
+import remarkSmartypants from 'remark-smartypants';
import markdown from 'remark-parse';
import markdownToHtml from 'remark-rehype';
import { unified } from 'unified';
@@ -43,6 +44,7 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft
rehypePlugins: [],
remarkRehype: {},
gfm: true,
+ smartypants: true,
};
/** Shared utility for rendering markdown */
@@ -58,6 +60,7 @@ export async function renderMarkdown(
rehypePlugins = markdownConfigDefaults.rehypePlugins,
remarkRehype = markdownConfigDefaults.remarkRehype,
gfm = markdownConfigDefaults.gfm,
+ smartypants = markdownConfigDefaults.smartypants,
isAstroFlavoredMd = false,
isExperimentalContentCollections = false,
contentDir,
@@ -75,6 +78,10 @@ export async function renderMarkdown(
parser.use(remarkGfm);
}
+ if (smartypants) {
+ parser.use(remarkSmartypants);
+ }
+
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts
index ccab542e9..d5133aaf0 100644
--- a/packages/markdown/remark/src/types.ts
+++ b/packages/markdown/remark/src/types.ts
@@ -48,6 +48,7 @@ export interface AstroMarkdownOptions {
rehypePlugins?: RehypePlugins;
remarkRehype?: RemarkRehype;
gfm?: boolean;
+ smartypants?: boolean;
}
export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 75610958d..163ce7fa2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2917,6 +2917,7 @@ importers:
remark-gfm: ^3.0.1
remark-rehype: ^10.1.0
remark-shiki-twoslash: ^3.1.0
+ remark-smartypants: ^2.0.0
remark-toc: ^8.0.1
shiki: ^0.11.1
unist-util-visit: ^4.1.0
@@ -2936,6 +2937,7 @@ importers:
rehype-raw: 6.1.1
remark-frontmatter: 4.0.1
remark-gfm: 3.0.1
+ remark-smartypants: 2.0.0
shiki: 0.11.1
unist-util-visit: 4.1.1
vfile: 5.3.6
@@ -3520,6 +3522,7 @@ importers:
remark-gfm: ^3.0.1
remark-parse: ^10.0.1
remark-rehype: ^10.1.0
+ remark-smartypants: ^2.0.0
shiki: ^0.11.1
unified: ^10.1.2
unist-util-map: ^3.1.1
@@ -3544,6 +3547,7 @@ importers:
remark-gfm: 3.0.1
remark-parse: 10.0.1
remark-rehype: 10.1.0
+ remark-smartypants: 2.0.0
shiki: 0.11.1
unified: 10.1.2
unist-util-map: 3.1.2