0
Fork 0
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:
Colin Bate 2025-01-29 07:51:55 -04:00 committed by GitHub
parent 0879cc2ce7
commit db252e0692
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 141 additions and 12 deletions

View 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!
```

View file

@ -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",

View file

@ -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;
}

View file

@ -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
View file

@ -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