Skip to content

Commit

Permalink
Add SmartyPants flag (#5769)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bholmesdev committed Jan 6, 2023
1 parent 04bf679 commit 93e6339
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 16 deletions.
34 changes: 34 additions & 0 deletions .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,
}
});
```
17 changes: 17 additions & 0 deletions packages/astro/src/@types/astro.ts
Expand Up @@ -785,6 +785,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
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/config/schema.ts
Expand Up @@ -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
Expand Down
60 changes: 44 additions & 16 deletions packages/astro/test/astro-markdown-plugins.test.js
Expand Up @@ -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]) {
Expand All @@ -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!');
}
@@ -0,0 +1,3 @@
# Smartypants test

"Smartypants" is -- awesome
1 change: 1 addition & 0 deletions packages/integrations/mdx/package.json
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/mdx/src/index.ts
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/integrations/mdx/src/plugins.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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)];

Expand Down
Expand Up @@ -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
35 changes: 35 additions & 0 deletions packages/integrations/mdx/test/mdx-plugins.test.js
Expand Up @@ -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: [
Expand Down Expand Up @@ -88,6 +101,7 @@ describe('MDX plugins', () => {
markdown: {
remarkPlugins: [remarkToc],
gfm: false,
smartypants: false,
},
integrations: [
mdx({
Expand Down Expand Up @@ -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'
);
}
});
});
}

Expand Down Expand Up @@ -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]');
}
Expand Down
1 change: 1 addition & 0 deletions packages/markdown/remark/package.json
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions packages/markdown/remark/src/index.ts
Expand Up @@ -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';
Expand All @@ -43,6 +44,7 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft
rehypePlugins: [],
remarkRehype: {},
gfm: true,
smartypants: true,
};

/** Shared utility for rendering markdown */
Expand All @@ -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,
Expand All @@ -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));

Expand Down
1 change: 1 addition & 0 deletions packages/markdown/remark/src/types.ts
Expand Up @@ -48,6 +48,7 @@ export interface AstroMarkdownOptions {
rehypePlugins?: RehypePlugins;
remarkRehype?: RemarkRehype;
gfm?: boolean;
smartypants?: boolean;
}

export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
Expand Down
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 93e6339

Please sign in to comment.