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
This commit is contained in:
parent
04bf679a5d
commit
93e633922c
14 changed files with 155 additions and 16 deletions
34
.changeset/angry-pots-boil.md
Normal file
34
.changeset/angry-pots-boil.md
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
|
@ -785,6 +785,23 @@ export interface AstroUserConfig {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
gfm?: boolean;
|
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
|
* @docs
|
||||||
* @name markdown.remarkRehype
|
* @name markdown.remarkRehype
|
||||||
|
|
|
@ -163,6 +163,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.markdown.remarkRehype),
|
.default(ASTRO_CONFIG_DEFAULTS.markdown.remarkRehype),
|
||||||
gfm: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.gfm),
|
gfm: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.gfm),
|
||||||
|
smartypants: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.smartypants),
|
||||||
})
|
})
|
||||||
.default({}),
|
.default({}),
|
||||||
vite: z
|
vite: z
|
||||||
|
|
|
@ -47,25 +47,23 @@ describe('Astro Markdown plugins', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Asserts Astro 1.0 behavior is removed. Test can be removed in Astro 3.0.
|
// 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({
|
const fixture = await buildFixture({
|
||||||
markdown: {
|
markdown: {
|
||||||
remarkPlugins: [remarkExamplePlugin],
|
remarkPlugins: [remarkExamplePlugin],
|
||||||
rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
|
rehypePlugins: [[addClasses, { 'h1,h2,h3': 'title' }]],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const html = await fixture.readFile('/with-gfm/index.html');
|
const gfmHtml = await fixture.readFile('/with-gfm/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $1 = cheerio.load(gfmHtml);
|
||||||
|
expect($1('a[href="https://example.com"]')).to.have.lengthOf(1);
|
||||||
|
|
||||||
// test 1: GFM autolink applied correctly
|
const smartypantsHtml = await fixture.readFile('/with-smartypants/index.html');
|
||||||
expect($('a[href="https://example.com"]')).to.have.lengthOf(1);
|
const $2 = cheerio.load(smartypantsHtml);
|
||||||
|
expect($2('p').html()).to.equal('“Smartypants” is — awesome');
|
||||||
|
|
||||||
// test 2: remark plugins still applied
|
testRemark(gfmHtml);
|
||||||
expect(html).to.include('Remark plugin applied!');
|
testRehype(gfmHtml, '#github-flavored-markdown-test');
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const gfm of [true, false]) {
|
for (const gfm of [true, false]) {
|
||||||
|
@ -87,12 +85,42 @@ describe('Astro Markdown plugins', () => {
|
||||||
expect($('a[href="https://example.com"]')).to.have.lengthOf(0);
|
expect($('a[href="https://example.com"]')).to.have.lengthOf(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// test 2: remark plugins still applied
|
testRemark(html);
|
||||||
expect(html).to.include('Remark plugin applied!');
|
testRehype(html, '#github-flavored-markdown-test');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// test 3: rehype plugins still applied
|
for (const smartypants of [true, false]) {
|
||||||
expect($('#github-flavored-markdown-test')).to.have.lengthOf(1);
|
it(`Handles SmartyPants when smartypants = ${smartypants}`, async () => {
|
||||||
expect($('#github-flavored-markdown-test').hasClass('title')).to.equal(true);
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
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!');
|
||||||
|
}
|
||||||
|
|
3
packages/astro/test/fixtures/astro-markdown-plugins/src/pages/with-smartypants.md
vendored
Normal file
3
packages/astro/test/fixtures/astro-markdown-plugins/src/pages/with-smartypants.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Smartypants test
|
||||||
|
|
||||||
|
"Smartypants" is -- awesome
|
|
@ -43,6 +43,7 @@
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^4.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-smartypants": "^2.0.0",
|
||||||
"shiki": "^0.11.1",
|
"shiki": "^0.11.1",
|
||||||
"unist-util-visit": "^4.1.0",
|
"unist-util-visit": "^4.1.0",
|
||||||
"vfile": "^5.3.2"
|
"vfile": "^5.3.2"
|
||||||
|
|
|
@ -186,6 +186,7 @@ function applyDefaultOptions({
|
||||||
recmaPlugins: options.recmaPlugins ?? defaults.recmaPlugins,
|
recmaPlugins: options.recmaPlugins ?? defaults.recmaPlugins,
|
||||||
remarkRehype: options.remarkRehype ?? defaults.remarkRehype,
|
remarkRehype: options.remarkRehype ?? defaults.remarkRehype,
|
||||||
gfm: options.gfm ?? defaults.gfm,
|
gfm: options.gfm ?? defaults.gfm,
|
||||||
|
smartypants: options.smartypants ?? defaults.smartypants,
|
||||||
remarkPlugins: options.remarkPlugins ?? defaults.remarkPlugins,
|
remarkPlugins: options.remarkPlugins ?? defaults.remarkPlugins,
|
||||||
rehypePlugins: options.rehypePlugins ?? defaults.rehypePlugins,
|
rehypePlugins: options.rehypePlugins ?? defaults.rehypePlugins,
|
||||||
shikiConfig: options.shikiConfig ?? defaults.shikiConfig,
|
shikiConfig: options.shikiConfig ?? defaults.shikiConfig,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type { Image } from 'mdast';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkSmartypants from 'remark-smartypants';
|
||||||
import { visit } from 'unist-util-visit';
|
import { visit } from 'unist-util-visit';
|
||||||
import type { VFile } from 'vfile';
|
import type { VFile } from 'vfile';
|
||||||
import { MdxOptions } from './index.js';
|
import { MdxOptions } from './index.js';
|
||||||
|
@ -153,6 +154,9 @@ export async function getRemarkPlugins(
|
||||||
if (mdxOptions.gfm) {
|
if (mdxOptions.gfm) {
|
||||||
remarkPlugins.push(remarkGfm);
|
remarkPlugins.push(remarkGfm);
|
||||||
}
|
}
|
||||||
|
if (mdxOptions.smartypants) {
|
||||||
|
remarkPlugins.push(remarkSmartypants);
|
||||||
|
}
|
||||||
|
|
||||||
remarkPlugins = [...remarkPlugins, ...ignoreStringPlugins(mdxOptions.remarkPlugins)];
|
remarkPlugins = [...remarkPlugins, ...ignoreStringPlugins(mdxOptions.remarkPlugins)];
|
||||||
|
|
||||||
|
|
|
@ -21,3 +21,5 @@ Oh cool, more text!
|
||||||
And section 2, with a hyperlink to check GFM is preserved: https://handle-me-gfm.com
|
And section 2, with a hyperlink to check GFM is preserved: https://handle-me-gfm.com
|
||||||
|
|
||||||
<div data-recma-plugin-works={recmaPluginWorking}></div>
|
<div data-recma-plugin-works={recmaPluginWorking}></div>
|
||||||
|
|
||||||
|
> "Smartypants" is -- awesome
|
||||||
|
|
|
@ -36,6 +36,19 @@ describe('MDX plugins', () => {
|
||||||
expect(selectGfmLink(document)).to.not.be.null;
|
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 () => {
|
it('supports custom rehype plugins', async () => {
|
||||||
const fixture = await buildFixture({
|
const fixture = await buildFixture({
|
||||||
integrations: [
|
integrations: [
|
||||||
|
@ -88,6 +101,7 @@ describe('MDX plugins', () => {
|
||||||
markdown: {
|
markdown: {
|
||||||
remarkPlugins: [remarkToc],
|
remarkPlugins: [remarkToc],
|
||||||
gfm: false,
|
gfm: false,
|
||||||
|
smartypants: false,
|
||||||
},
|
},
|
||||||
integrations: [
|
integrations: [
|
||||||
mdx({
|
mdx({
|
||||||
|
@ -129,6 +143,23 @@ describe('MDX plugins', () => {
|
||||||
expect(selectGfmLink(document), 'Respects `markdown.gfm` unexpectedly.').to.not.be.null;
|
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"]');
|
return document.querySelector('a[href="https://handle-me-gfm.com"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectSmartypantsQuote(document) {
|
||||||
|
return document.querySelector('blockquote');
|
||||||
|
}
|
||||||
|
|
||||||
function selectRemarkExample(document) {
|
function selectRemarkExample(document) {
|
||||||
return document.querySelector('div[data-remark-plugin-works]');
|
return document.querySelector('div[data-remark-plugin-works]');
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-parse": "^10.0.1",
|
"remark-parse": "^10.0.1",
|
||||||
"remark-rehype": "^10.1.0",
|
"remark-rehype": "^10.1.0",
|
||||||
|
"remark-smartypants": "^2.0.0",
|
||||||
"shiki": "^0.11.1",
|
"shiki": "^0.11.1",
|
||||||
"unified": "^10.1.2",
|
"unified": "^10.1.2",
|
||||||
"unist-util-map": "^3.1.1",
|
"unist-util-map": "^3.1.1",
|
||||||
|
|
|
@ -24,6 +24,7 @@ import remarkUnwrap from './remark-unwrap.js';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import rehypeStringify from 'rehype-stringify';
|
import rehypeStringify from 'rehype-stringify';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkSmartypants from 'remark-smartypants';
|
||||||
import markdown from 'remark-parse';
|
import markdown from 'remark-parse';
|
||||||
import markdownToHtml from 'remark-rehype';
|
import markdownToHtml from 'remark-rehype';
|
||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
|
@ -43,6 +44,7 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft
|
||||||
rehypePlugins: [],
|
rehypePlugins: [],
|
||||||
remarkRehype: {},
|
remarkRehype: {},
|
||||||
gfm: true,
|
gfm: true,
|
||||||
|
smartypants: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Shared utility for rendering markdown */
|
/** Shared utility for rendering markdown */
|
||||||
|
@ -58,6 +60,7 @@ export async function renderMarkdown(
|
||||||
rehypePlugins = markdownConfigDefaults.rehypePlugins,
|
rehypePlugins = markdownConfigDefaults.rehypePlugins,
|
||||||
remarkRehype = markdownConfigDefaults.remarkRehype,
|
remarkRehype = markdownConfigDefaults.remarkRehype,
|
||||||
gfm = markdownConfigDefaults.gfm,
|
gfm = markdownConfigDefaults.gfm,
|
||||||
|
smartypants = markdownConfigDefaults.smartypants,
|
||||||
isAstroFlavoredMd = false,
|
isAstroFlavoredMd = false,
|
||||||
isExperimentalContentCollections = false,
|
isExperimentalContentCollections = false,
|
||||||
contentDir,
|
contentDir,
|
||||||
|
@ -75,6 +78,10 @@ export async function renderMarkdown(
|
||||||
parser.use(remarkGfm);
|
parser.use(remarkGfm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (smartypants) {
|
||||||
|
parser.use(remarkSmartypants);
|
||||||
|
}
|
||||||
|
|
||||||
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
||||||
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ export interface AstroMarkdownOptions {
|
||||||
rehypePlugins?: RehypePlugins;
|
rehypePlugins?: RehypePlugins;
|
||||||
remarkRehype?: RemarkRehype;
|
remarkRehype?: RemarkRehype;
|
||||||
gfm?: boolean;
|
gfm?: boolean;
|
||||||
|
smartypants?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
||||||
|
|
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
|
@ -2917,6 +2917,7 @@ importers:
|
||||||
remark-gfm: ^3.0.1
|
remark-gfm: ^3.0.1
|
||||||
remark-rehype: ^10.1.0
|
remark-rehype: ^10.1.0
|
||||||
remark-shiki-twoslash: ^3.1.0
|
remark-shiki-twoslash: ^3.1.0
|
||||||
|
remark-smartypants: ^2.0.0
|
||||||
remark-toc: ^8.0.1
|
remark-toc: ^8.0.1
|
||||||
shiki: ^0.11.1
|
shiki: ^0.11.1
|
||||||
unist-util-visit: ^4.1.0
|
unist-util-visit: ^4.1.0
|
||||||
|
@ -2936,6 +2937,7 @@ importers:
|
||||||
rehype-raw: 6.1.1
|
rehype-raw: 6.1.1
|
||||||
remark-frontmatter: 4.0.1
|
remark-frontmatter: 4.0.1
|
||||||
remark-gfm: 3.0.1
|
remark-gfm: 3.0.1
|
||||||
|
remark-smartypants: 2.0.0
|
||||||
shiki: 0.11.1
|
shiki: 0.11.1
|
||||||
unist-util-visit: 4.1.1
|
unist-util-visit: 4.1.1
|
||||||
vfile: 5.3.6
|
vfile: 5.3.6
|
||||||
|
@ -3520,6 +3522,7 @@ importers:
|
||||||
remark-gfm: ^3.0.1
|
remark-gfm: ^3.0.1
|
||||||
remark-parse: ^10.0.1
|
remark-parse: ^10.0.1
|
||||||
remark-rehype: ^10.1.0
|
remark-rehype: ^10.1.0
|
||||||
|
remark-smartypants: ^2.0.0
|
||||||
shiki: ^0.11.1
|
shiki: ^0.11.1
|
||||||
unified: ^10.1.2
|
unified: ^10.1.2
|
||||||
unist-util-map: ^3.1.1
|
unist-util-map: ^3.1.1
|
||||||
|
@ -3544,6 +3547,7 @@ importers:
|
||||||
remark-gfm: 3.0.1
|
remark-gfm: 3.0.1
|
||||||
remark-parse: 10.0.1
|
remark-parse: 10.0.1
|
||||||
remark-rehype: 10.1.0
|
remark-rehype: 10.1.0
|
||||||
|
remark-smartypants: 2.0.0
|
||||||
shiki: 0.11.1
|
shiki: 0.11.1
|
||||||
unified: 10.1.2
|
unified: 10.1.2
|
||||||
unist-util-map: 3.1.2
|
unist-util-map: 3.1.2
|
||||||
|
|
Loading…
Add table
Reference in a new issue