mirror of
https://github.com/withastro/astro.git
synced 2025-02-17 22:44:24 -05:00
feat(markdown): add support for TOML frontmatter in Markdown files. (#12850)
This commit is contained in:
parent
0879cc2ce7
commit
db252e0692
5 changed files with 141 additions and 12 deletions
24
.changeset/lazy-pandas-love.md
Normal file
24
.changeset/lazy-pandas-love.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
'@astrojs/markdown-remark': minor
|
||||
---
|
||||
|
||||
Adds support for TOML frontmatter in `.md` and `.mdx` files
|
||||
|
||||
Astro 5.2 automatically identifies the format of your Markdown and MDX frontmatter based on the delimiter used. With `+++` as a delimiter (instead of the `---` YAML code fence), your frontmatter will automatically be recognized and parsed as [TOML](https://toml.io).
|
||||
|
||||
This is useful for adding existing content files with TOML frontmatter to your project from another framework such as Hugo.
|
||||
|
||||
TOML frontmatter can also be used with [content collections](https://docs.astro.build/guides/content-collections/), and files with different frontmatter languages can live together in the same project.
|
||||
|
||||
No configuration is required to use TOML frontmatter in your content files. Your delimiter will indicate your chosen frontmatter language:
|
||||
|
||||
```md
|
||||
+++
|
||||
date = 2025-01-30
|
||||
title = 'Use TOML frontmatter in Astro!'
|
||||
[author]
|
||||
name = 'Colin Bate'
|
||||
+++
|
||||
|
||||
# Support for TOML frontmatter is here!
|
||||
```
|
|
@ -46,6 +46,7 @@
|
|||
"remark-rehype": "^11.1.1",
|
||||
"remark-smartypants": "^3.0.2",
|
||||
"shiki": "^1.29.1",
|
||||
"smol-toml": "^1.3.1",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-remove-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import yaml from 'js-yaml';
|
||||
import * as toml from 'smol-toml';
|
||||
|
||||
export function isFrontmatterValid(frontmatter: Record<string, any>) {
|
||||
try {
|
||||
|
@ -10,15 +11,19 @@ export function isFrontmatterValid(frontmatter: Record<string, any>) {
|
|||
return typeof frontmatter === 'object' && frontmatter !== null;
|
||||
}
|
||||
|
||||
// Capture frontmatter wrapped with `---`, including any characters and new lines within it.
|
||||
// Only capture if `---` exists near the top of the file, including:
|
||||
// Capture frontmatter wrapped with `---` or `+++`, including any characters and new lines within it.
|
||||
// Only capture if `---` or `+++` exists near the top of the file, including:
|
||||
// 1. Start of file (including if has BOM encoding)
|
||||
// 2. Start of file with any whitespace (but `---` must still start on a new line)
|
||||
const frontmatterRE = /(?:^\uFEFF?|^\s*\n)---([\s\S]*?\n)---/;
|
||||
// 2. Start of file with any whitespace (but `---` or `+++` must still start on a new line)
|
||||
const frontmatterRE = /(?:^\uFEFF?|^\s*\n)(?:---|\+\+\+)([\s\S]*?\n)(?:---|\+\+\+)/;
|
||||
const frontmatterTypeRE = /(?:^\uFEFF?|^\s*\n)(---|\+\+\+)/;
|
||||
export function extractFrontmatter(code: string): string | undefined {
|
||||
return frontmatterRE.exec(code)?.[1];
|
||||
}
|
||||
|
||||
function getFrontmatterParser(code: string): [string, (str: string) => unknown] {
|
||||
return frontmatterTypeRE.exec(code)?.[1] === '+++' ? ['+++', toml.parse] : ['---', yaml.load];
|
||||
}
|
||||
export interface ParseFrontmatterOptions {
|
||||
/**
|
||||
* How the frontmatter should be handled in the returned `content` string.
|
||||
|
@ -47,8 +52,8 @@ export function parseFrontmatter(
|
|||
if (rawFrontmatter == null) {
|
||||
return { frontmatter: {}, rawFrontmatter: '', content: code };
|
||||
}
|
||||
|
||||
const parsed = yaml.load(rawFrontmatter);
|
||||
const [delims, parser] = getFrontmatterParser(code);
|
||||
const parsed = parser(rawFrontmatter);
|
||||
const frontmatter = (parsed && typeof parsed === 'object' ? parsed : {}) as Record<string, any>;
|
||||
|
||||
let content: string;
|
||||
|
@ -57,16 +62,16 @@ export function parseFrontmatter(
|
|||
content = code;
|
||||
break;
|
||||
case 'remove':
|
||||
content = code.replace(`---${rawFrontmatter}---`, '');
|
||||
content = code.replace(`${delims}${rawFrontmatter}${delims}`, '');
|
||||
break;
|
||||
case 'empty-with-spaces':
|
||||
content = code.replace(
|
||||
`---${rawFrontmatter}---`,
|
||||
`${delims}${rawFrontmatter}${delims}`,
|
||||
` ${rawFrontmatter.replace(/[^\r\n]/g, ' ')} `,
|
||||
);
|
||||
break;
|
||||
case 'empty-with-lines':
|
||||
content = code.replace(`---${rawFrontmatter}---`, rawFrontmatter.replace(/[^\r\n]/g, ''));
|
||||
content = code.replace(`${delims}${rawFrontmatter}${delims}`, rawFrontmatter.replace(/[^\r\n]/g, ''));
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { extractFrontmatter, parseFrontmatter } from '../dist/index.js';
|
|||
const bom = '\uFEFF';
|
||||
|
||||
describe('extractFrontmatter', () => {
|
||||
it('works', () => {
|
||||
it('handles YAML', () => {
|
||||
const yaml = `\nfoo: bar\n`;
|
||||
assert.equal(extractFrontmatter(`---${yaml}---`), yaml);
|
||||
assert.equal(extractFrontmatter(`${bom}---${yaml}---`), yaml);
|
||||
|
@ -19,10 +19,25 @@ describe('extractFrontmatter', () => {
|
|||
assert.equal(extractFrontmatter(`---${yaml} ---`), undefined);
|
||||
assert.equal(extractFrontmatter(`text\n---${yaml}---\n\ncontent`), undefined);
|
||||
});
|
||||
|
||||
it('handles TOML', () => {
|
||||
const toml = `\nfoo = "bar"\n`;
|
||||
assert.equal(extractFrontmatter(`+++${toml}+++`), toml);
|
||||
assert.equal(extractFrontmatter(`${bom}+++${toml}+++`), toml);
|
||||
assert.equal(extractFrontmatter(`\n+++${toml}+++`), toml);
|
||||
assert.equal(extractFrontmatter(`\n \n+++${toml}+++`), toml);
|
||||
assert.equal(extractFrontmatter(`+++${toml}+++\ncontent`), toml);
|
||||
assert.equal(extractFrontmatter(`${bom}+++${toml}+++\ncontent`), toml);
|
||||
assert.equal(extractFrontmatter(`\n\n+++${toml}+++\n\ncontent`), toml);
|
||||
assert.equal(extractFrontmatter(`\n \n+++${toml}+++\n\ncontent`), toml);
|
||||
assert.equal(extractFrontmatter(` +++${toml}+++`), undefined);
|
||||
assert.equal(extractFrontmatter(`+++${toml} +++`), undefined);
|
||||
assert.equal(extractFrontmatter(`text\n+++${toml}+++\n\ncontent`), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFrontmatter', () => {
|
||||
it('works', () => {
|
||||
it('works for YAML', () => {
|
||||
const yaml = `\nfoo: bar\n`;
|
||||
assert.deepEqual(parseFrontmatter(`---${yaml}---`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
|
@ -81,7 +96,66 @@ describe('parseFrontmatter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('frontmatter style', () => {
|
||||
it('works for TOML', () => {
|
||||
const toml = `\nfoo = "bar"\n`;
|
||||
assert.deepEqual(parseFrontmatter(`+++${toml}+++`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: '',
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`${bom}+++${toml}+++`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: bom,
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`\n+++${toml}+++`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: '\n',
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`\n \n+++${toml}+++`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: '\n \n',
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`+++${toml}+++\ncontent`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: '\ncontent',
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`${bom}+++${toml}+++\ncontent`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: `${bom}\ncontent`,
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`\n\n+++${toml}+++\n\ncontent`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: '\n\n\n\ncontent',
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`\n \n+++${toml}+++\n\ncontent`), {
|
||||
frontmatter: { foo: 'bar' },
|
||||
rawFrontmatter: toml,
|
||||
content: '\n \n\n\ncontent',
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(` +++${toml}+++`), {
|
||||
frontmatter: {},
|
||||
rawFrontmatter: '',
|
||||
content: ` +++${toml}+++`,
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`+++${toml} +++`), {
|
||||
frontmatter: {},
|
||||
rawFrontmatter: '',
|
||||
content: `+++${toml} +++`,
|
||||
});
|
||||
assert.deepEqual(parseFrontmatter(`text\n+++${toml}+++\n\ncontent`), {
|
||||
frontmatter: {},
|
||||
rawFrontmatter: '',
|
||||
content: `text\n+++${toml}+++\n\ncontent`,
|
||||
});
|
||||
});
|
||||
|
||||
it('frontmatter style for YAML', () => {
|
||||
const yaml = `\nfoo: bar\n`;
|
||||
const parse1 = (style) => parseFrontmatter(`---${yaml}---`, { frontmatter: style }).content;
|
||||
assert.deepEqual(parse1('preserve'), `---${yaml}---`);
|
||||
|
@ -96,4 +170,20 @@ describe('parseFrontmatter', () => {
|
|||
assert.deepEqual(parse2('empty-with-spaces'), `\n \n \n \n \n\ncontent`);
|
||||
assert.deepEqual(parse2('empty-with-lines'), `\n \n\n\n\n\ncontent`);
|
||||
});
|
||||
|
||||
it('frontmatter style for TOML', () => {
|
||||
const toml = `\nfoo = "bar"\n`;
|
||||
const parse1 = (style) => parseFrontmatter(`+++${toml}+++`, { frontmatter: style }).content;
|
||||
assert.deepEqual(parse1('preserve'), `+++${toml}+++`);
|
||||
assert.deepEqual(parse1('remove'), '');
|
||||
assert.deepEqual(parse1('empty-with-spaces'), ` \n \n `);
|
||||
assert.deepEqual(parse1('empty-with-lines'), `\n\n`);
|
||||
|
||||
const parse2 = (style) =>
|
||||
parseFrontmatter(`\n \n+++${toml}+++\n\ncontent`, { frontmatter: style }).content;
|
||||
assert.deepEqual(parse2('preserve'), `\n \n+++${toml}+++\n\ncontent`);
|
||||
assert.deepEqual(parse2('remove'), '\n \n\n\ncontent');
|
||||
assert.deepEqual(parse2('empty-with-spaces'), `\n \n \n \n \n\ncontent`);
|
||||
assert.deepEqual(parse2('empty-with-lines'), `\n \n\n\n\n\ncontent`);
|
||||
});
|
||||
});
|
||||
|
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -5435,6 +5435,9 @@ importers:
|
|||
shiki:
|
||||
specifier: ^1.29.1
|
||||
version: 1.29.1
|
||||
smol-toml:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
unified:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
|
@ -10217,6 +10220,10 @@ packages:
|
|||
resolution: {integrity: sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==}
|
||||
hasBin: true
|
||||
|
||||
smol-toml@1.3.1:
|
||||
resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
solid-js@1.9.4:
|
||||
resolution: {integrity: sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==}
|
||||
|
||||
|
@ -16468,6 +16475,8 @@ snapshots:
|
|||
|
||||
smartypants@0.2.2: {}
|
||||
|
||||
smol-toml@1.3.1: {}
|
||||
|
||||
solid-js@1.9.4:
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
|
|
Loading…
Add table
Reference in a new issue